forked from Qortal/qortal
Compare commits
28 Commits
blockminte
...
prioritize
Author | SHA1 | Date | |
---|---|---|---|
|
b37f2c7d7f | ||
|
0c0c5ff077 | ||
|
e12b99d17e | ||
|
d599146c3a | ||
|
476731a2c3 | ||
|
1e491dd8fb | ||
|
ba6397b963 | ||
|
3146da6aec | ||
|
5643e57ede | ||
|
f532dbe7b4 | ||
|
ec2af62b4d | ||
|
423142d730 | ||
|
bdddb526da | ||
|
dbf1ed40b3 | ||
|
02ace06526 | ||
|
2d2bfc0a4c | ||
|
3c22a12cbb | ||
|
3071ef2f36 | ||
|
3022cb22d6 | ||
|
e9b4a3f6b3 | ||
|
4312ebfcc3 | ||
|
2c0e099d1c | ||
|
b1eb02eb1d | ||
|
c919797553 | ||
|
08dacab05c | ||
|
2efc9218df | ||
|
41505dae11 | ||
|
22e3140ff0 |
@@ -547,7 +547,7 @@ public class AdminResource {
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
repository.exportNodeLocalData();
|
||||
repository.exportNodeLocalData(true);
|
||||
return "true";
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
|
@@ -135,16 +135,19 @@ public class BlockMinter extends Thread {
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
peers.removeIf(Controller.hasMisbehaved);
|
||||
|
||||
// Disregard peers that don't have a recent block
|
||||
peers.removeIf(Controller.hasNoRecentBlock);
|
||||
// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
|
||||
// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
|
||||
if (Controller.getInstance().getRecoveryMode() == false)
|
||||
peers.removeIf(Controller.hasNoRecentBlock);
|
||||
|
||||
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
continue;
|
||||
|
||||
// If our latest block isn't recent then we need to synchronize instead of minting.
|
||||
// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
|
||||
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||
continue;
|
||||
if (Controller.getInstance().getRecoveryMode() == false)
|
||||
continue;
|
||||
|
||||
// There are enough peers with a recent block and our latest block is recent
|
||||
// so go ahead and mint a block if possible.
|
||||
@@ -165,6 +168,14 @@ public class BlockMinter extends Thread {
|
||||
// Do we need to build any potential new blocks?
|
||||
List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
|
||||
|
||||
// We might need to sit the next block out, if one of our minting accounts signed the previous one
|
||||
final byte[] previousBlockMinter = previousBlockData.getMinterPublicKey();
|
||||
final boolean mintedLastBlock = mintingAccountsData.stream().anyMatch(mintingAccount -> Arrays.equals(mintingAccount.getPublicKey(), previousBlockMinter));
|
||||
if (mintedLastBlock) {
|
||||
LOGGER.trace(String.format("One of our keys signed the last block, so we won't sign the next one"));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
|
||||
// First block does the AT heavy-lifting
|
||||
if (newBlocks.isEmpty()) {
|
||||
@@ -282,15 +293,17 @@ public class BlockMinter extends Thread {
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
|
||||
|
||||
if (rewardShareData != null) {
|
||||
LOGGER.info(String.format("Minted block %d, sig %.8s by %s on behalf of %s",
|
||||
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s on behalf of %s",
|
||||
newBlock.getBlockData().getHeight(),
|
||||
Base58.encode(newBlock.getBlockData().getSignature()),
|
||||
Base58.encode(newBlock.getParent().getSignature()),
|
||||
rewardShareData.getMinter(),
|
||||
rewardShareData.getRecipient()));
|
||||
} else {
|
||||
LOGGER.info(String.format("Minted block %d, sig %.8s by %s",
|
||||
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s",
|
||||
newBlock.getBlockData().getHeight(),
|
||||
Base58.encode(newBlock.getBlockData().getSignature()),
|
||||
Base58.encode(newBlock.getParent().getSignature()),
|
||||
newBlock.getMinter().getAddress()));
|
||||
}
|
||||
|
||||
|
@@ -121,6 +121,7 @@ public class Controller extends Thread {
|
||||
private static final long NTP_PRE_SYNC_CHECK_PERIOD = 5 * 1000L; // ms
|
||||
private static final long NTP_POST_SYNC_CHECK_PERIOD = 5 * 60 * 1000L; // ms
|
||||
private static final long DELETE_EXPIRED_INTERVAL = 5 * 60 * 1000L; // ms
|
||||
private static final long RECOVERY_MODE_TIMEOUT = 10 * 60 * 1000L; // ms
|
||||
|
||||
// To do with online accounts list
|
||||
private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
|
||||
@@ -175,6 +176,11 @@ public class Controller extends Thread {
|
||||
/** Latest block signatures from other peers that we know are on inferior chains. */
|
||||
List<ByteArray> inferiorChainSignatures = new ArrayList<>();
|
||||
|
||||
/** Recovery mode, which is used to bring back a stalled network */
|
||||
private boolean recoveryMode = false;
|
||||
private boolean peersAvailable = true; // peersAvailable must default to true
|
||||
private long timePeersLastAvailable = 0;
|
||||
|
||||
/**
|
||||
* Map of recent requests for ARBITRARY transaction data payloads.
|
||||
* <p>
|
||||
@@ -358,6 +364,10 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getRecoveryMode() {
|
||||
return this.recoveryMode;
|
||||
}
|
||||
|
||||
// Entry point
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -629,6 +639,13 @@ public class Controller extends Thread {
|
||||
// Disregard peers that don't have a recent block
|
||||
peers.removeIf(hasNoRecentBlock);
|
||||
|
||||
checkRecoveryModeForPeers(peers);
|
||||
if (recoveryMode) {
|
||||
peers = Network.getInstance().getHandshakedPeers();
|
||||
peers.removeIf(hasOnlyGenesisBlock);
|
||||
peers.removeIf(hasMisbehaved);
|
||||
}
|
||||
|
||||
// Check we have enough peers to potentially synchronize
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
return;
|
||||
@@ -639,9 +656,31 @@ public class Controller extends Thread {
|
||||
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
||||
peers.removeIf(hasInferiorChainTip);
|
||||
|
||||
final int peersBeforeComparison = peers.size();
|
||||
|
||||
// Request recent block summaries from the remaining peers, and locate our common block with each
|
||||
Synchronizer.getInstance().findCommonBlocksWithPeers(peers);
|
||||
|
||||
// Compare the peers against each other, and against our chain, which will return an updated list excluding those without common blocks
|
||||
peers = Synchronizer.getInstance().comparePeers(peers);
|
||||
|
||||
// We may have added more inferior chain tips when comparing peers, so remove any peers that are currently on those chains
|
||||
peers.removeIf(hasInferiorChainTip);
|
||||
|
||||
final int peersRemoved = peersBeforeComparison - peers.size();
|
||||
if (peersRemoved > 0)
|
||||
LOGGER.info(String.format("Ignoring %d peers on inferior chains. Peers remaining: %d", peersRemoved, peers.size()));
|
||||
|
||||
if (peers.isEmpty())
|
||||
return;
|
||||
|
||||
if (peers.size() > 1) {
|
||||
StringBuilder finalPeersString = new StringBuilder();
|
||||
for (Peer peer : peers)
|
||||
finalPeersString = finalPeersString.length() > 0 ? finalPeersString.append(", ").append(peer) : finalPeersString.append(peer);
|
||||
LOGGER.info(String.format("Choosing random peer from: [%s]", finalPeersString.toString()));
|
||||
}
|
||||
|
||||
// Pick random peer to sync with
|
||||
int index = new SecureRandom().nextInt(peers.size());
|
||||
Peer peer = peers.get(index);
|
||||
@@ -744,6 +783,46 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkRecoveryModeForPeers(List<Peer> qualifiedPeers) {
|
||||
List<Peer> handshakedPeers = Network.getInstance().getHandshakedPeers();
|
||||
|
||||
if (handshakedPeers.size() > 0) {
|
||||
// There is at least one handshaked peer
|
||||
if (qualifiedPeers.isEmpty()) {
|
||||
// There are no 'qualified' peers - i.e. peers that have a recent block we can sync to
|
||||
boolean werePeersAvailable = peersAvailable;
|
||||
peersAvailable = false;
|
||||
|
||||
// If peers only just became unavailable, update our record of the time they were last available
|
||||
if (werePeersAvailable)
|
||||
timePeersLastAvailable = NTP.getTime();
|
||||
|
||||
// If enough time has passed, enter recovery mode, which lifts some restrictions on who we can sync with and when we can mint
|
||||
if (NTP.getTime() - timePeersLastAvailable > RECOVERY_MODE_TIMEOUT) {
|
||||
if (recoveryMode == false) {
|
||||
LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", RECOVERY_MODE_TIMEOUT/60/1000));
|
||||
recoveryMode = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We now have at least one peer with a recent block, so we can exit recovery mode and sync normally
|
||||
peersAvailable = true;
|
||||
if (recoveryMode) {
|
||||
LOGGER.info("Peers have become available again. Exiting recovery mode...");
|
||||
recoveryMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return recoveryMode;
|
||||
}
|
||||
|
||||
public void addInferiorChainSignature(byte[] inferiorSignature) {
|
||||
// Update our list of inferior chain tips
|
||||
ByteArray inferiorChainSignature = new ByteArray(inferiorSignature);
|
||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||
inferiorChainSignatures.add(inferiorChainSignature);
|
||||
}
|
||||
|
||||
public static class StatusChangeEvent implements Event {
|
||||
public StatusChangeEvent() {
|
||||
}
|
||||
@@ -775,7 +854,7 @@ public class Controller extends Thread {
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
|
||||
}
|
||||
|
||||
String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height);
|
||||
String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height) + "\n" + String.format("Build version: %s", this.buildVersion);
|
||||
SysTray.getInstance().setToolTipText(tooltip);
|
||||
|
||||
this.callbackExecutor.execute(() -> {
|
||||
|
@@ -8,6 +8,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -17,6 +18,7 @@ import org.qortal.block.Block;
|
||||
import org.qortal.block.Block.ValidationResult;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.block.CommonBlockData;
|
||||
import org.qortal.data.network.PeerChainTipData;
|
||||
import org.qortal.data.transaction.RewardShareTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -54,7 +56,7 @@ public class Synchronizer {
|
||||
private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings?
|
||||
|
||||
/** Number of retry attempts if a peer fails to respond with the requested data */
|
||||
private static final int MAXIMUM_RETRIES = 1; // XXX move to Settings?
|
||||
private static final int MAXIMUM_RETRIES = 2; // XXX move to Settings?
|
||||
|
||||
|
||||
private static Synchronizer instance;
|
||||
@@ -75,6 +77,368 @@ public class Synchronizer {
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Iterate through a list of supplied peers, and attempt to find our common block with each.
|
||||
* If a common block is found, its summary will be retained in the peer's commonBlockSummary property, for processing later.
|
||||
* <p>
|
||||
* Will return <tt>SynchronizationResult.OK</tt> on success.
|
||||
* <p>
|
||||
* @param peers
|
||||
* @return SynchronizationResult.OK if the process completed successfully, or a different SynchronizationResult if something went wrong.
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public SynchronizationResult findCommonBlocksWithPeers(List<Peer> peers) throws InterruptedException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
try {
|
||||
|
||||
if (peers.size() == 0)
|
||||
return SynchronizationResult.NOTHING_TO_DO;
|
||||
|
||||
// If our latest block is very old, it's best that we don't try and determine the best peers to sync to.
|
||||
// This is because it can involve very large chain comparisons, which is too intensive.
|
||||
// In reality, most forking problems occur near the chain tips, so we will reserve this functionality for those situations.
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
return SynchronizationResult.REPOSITORY_ISSUE;
|
||||
|
||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
||||
LOGGER.debug(String.format("Our latest block is very old, so we won't collect common block info from peers"));
|
||||
return SynchronizationResult.NOTHING_TO_DO;
|
||||
}
|
||||
|
||||
LOGGER.debug(String.format("Searching for common blocks with %d peers...", peers.size()));
|
||||
final long startTime = System.currentTimeMillis();
|
||||
int commonBlocksFound = 0;
|
||||
|
||||
for (Peer peer : peers) {
|
||||
// Are we shutting down?
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
// Check if we can use the cached common block data, by comparing the peer's current chain tip against the peer's chain tip when we last found our common block
|
||||
if (peer.canUseCachedCommonBlockData()) {
|
||||
LOGGER.debug(String.format("Skipping peer %s because we already have the latest common block data in our cache. Cached common block sig is %.08s", peer, Base58.encode(peer.getCommonBlockData().getCommonBlockSummary().getSignature())));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Cached data is stale, so clear it and repopulate
|
||||
peer.setCommonBlockData(null);
|
||||
|
||||
// Search for the common block
|
||||
Synchronizer.getInstance().findCommonBlockWithPeer(peer, repository);
|
||||
if (peer.getCommonBlockData() != null)
|
||||
commonBlocksFound++;
|
||||
}
|
||||
|
||||
final long totalTimeTaken = System.currentTimeMillis() - startTime;
|
||||
LOGGER.info(String.format("Finished searching for common blocks with %d peer%s. Found: %d. Total time taken: %d ms", peers.size(), (peers.size() != 1 ? "s" : ""), commonBlocksFound, totalTimeTaken));
|
||||
|
||||
return SynchronizationResult.OK;
|
||||
} finally {
|
||||
repository.discardChanges(); // Free repository locks, if any, also in case anything went wrong
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue during synchronization with peer", e);
|
||||
return SynchronizationResult.REPOSITORY_ISSUE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to find the find our common block with supplied peer.
|
||||
* If a common block is found, its summary will be retained in the peer's commonBlockSummary property, for processing later.
|
||||
* <p>
|
||||
* Will return <tt>SynchronizationResult.OK</tt> on success.
|
||||
* <p>
|
||||
* @param peer
|
||||
* @param repository
|
||||
* @return SynchronizationResult.OK if the process completed successfully, or a different SynchronizationResult if something went wrong.
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public SynchronizationResult findCommonBlockWithPeer(Peer peer, Repository repository) throws InterruptedException {
|
||||
try {
|
||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
final int ourInitialHeight = ourLatestBlockData.getHeight();
|
||||
|
||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
int peerHeight = peerChainTipData.getLastHeight();
|
||||
byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
|
||||
|
||||
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
|
||||
LOGGER.debug(String.format("Fetching summaries from peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
|
||||
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
|
||||
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
||||
|
||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||
SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, false, peerBlockSummaries);
|
||||
if (findCommonBlockResult != SynchronizationResult.OK) {
|
||||
// Logging performed by fetchSummariesFromCommonBlock() above
|
||||
peer.setCommonBlockData(null);
|
||||
return findCommonBlockResult;
|
||||
}
|
||||
|
||||
// First summary is common block
|
||||
final BlockData commonBlockData = repository.getBlockRepository().fromSignature(peerBlockSummaries.get(0).getSignature());
|
||||
final BlockSummaryData commonBlockSummary = new BlockSummaryData(commonBlockData);
|
||||
final int commonBlockHeight = commonBlockData.getHeight();
|
||||
final byte[] commonBlockSig = commonBlockData.getSignature();
|
||||
final String commonBlockSig58 = Base58.encode(commonBlockSig);
|
||||
LOGGER.debug(String.format("Common block with peer %s is at height %d, sig %.8s, ts %d", peer,
|
||||
commonBlockHeight, commonBlockSig58, commonBlockData.getTimestamp()));
|
||||
peerBlockSummaries.remove(0);
|
||||
|
||||
// Store the common block summary against the peer, and the current chain tip (for caching)
|
||||
peer.setCommonBlockData(new CommonBlockData(commonBlockSummary, peerChainTipData));
|
||||
|
||||
return SynchronizationResult.OK;
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue during synchronization with peer", e);
|
||||
return SynchronizationResult.REPOSITORY_ISSUE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compare a list of peers to determine the best peer(s) to sync to next.
|
||||
* <p>
|
||||
* Will return a filtered list of peers on success, or an identical list of peers on failure.
|
||||
* This allows us to fall back to legacy behaviour (random selection from the entire list of peers), if we are unable to make the comparison.
|
||||
* <p>
|
||||
* @param peers
|
||||
* @return a list of peers, possibly filtered.
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public List<Peer> comparePeers(List<Peer> peers) throws InterruptedException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
try {
|
||||
|
||||
// If our latest block is very old, it's best that we don't try and determine the best peers to sync to.
|
||||
// This is because it can involve very large chain comparisons, which is too intensive.
|
||||
// In reality, most forking problems occur near the chain tips, so we will reserve this functionality for those situations.
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
return peers;
|
||||
|
||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
||||
LOGGER.debug(String.format("Our latest block is very old, so we won't filter the peers list"));
|
||||
return peers;
|
||||
}
|
||||
|
||||
// Retrieve a list of unique common blocks from this list of peers
|
||||
List<BlockSummaryData> commonBlocks = this.uniqueCommonBlocks(peers);
|
||||
|
||||
// Order common blocks by height, in ascending order
|
||||
// This is essential for the logic below to make the correct decisions when discarding chains - do not remove
|
||||
commonBlocks.sort((b1, b2) -> Integer.valueOf(b1.getHeight()).compareTo(Integer.valueOf(b2.getHeight())));
|
||||
|
||||
// Get our latest height
|
||||
final int ourHeight = ourLatestBlockData.getHeight();
|
||||
|
||||
// Create a placeholder to track of common blocks that we can discard due to being inferior chains
|
||||
int dropPeersAfterCommonBlockHeight = 0;
|
||||
|
||||
// Remove peers with no common block data
|
||||
Iterator iterator = peers.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Peer peer = (Peer) iterator.next();
|
||||
if (peer.getCommonBlockData() == null) {
|
||||
LOGGER.debug(String.format("Removed peer %s because it has no common block data", peer));
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through each group of common blocks
|
||||
for (BlockSummaryData commonBlockSummary : commonBlocks) {
|
||||
List<Peer> peersSharingCommonBlock = peers.stream().filter(peer -> peer.getCommonBlockData().getCommonBlockSummary().equals(commonBlockSummary)).collect(Collectors.toList());
|
||||
|
||||
// Check if we need to discard this group of peers
|
||||
if (dropPeersAfterCommonBlockHeight > 0) {
|
||||
if (commonBlockSummary.getHeight() > dropPeersAfterCommonBlockHeight) {
|
||||
// We have already determined that the correct chain diverged from a lower height. We are safe to skip these peers.
|
||||
for (Peer peer : peersSharingCommonBlock) {
|
||||
LOGGER.debug(String.format("Peer %s has common block at height %d but the superior chain is at height %d. Removing it from this round.", peer, commonBlockSummary.getHeight(), dropPeersAfterCommonBlockHeight));
|
||||
Controller.getInstance().addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the length of the shortest peer chain sharing this common block, including our chain
|
||||
final int ourAdditionalBlocksAfterCommonBlock = ourHeight - commonBlockSummary.getHeight();
|
||||
int minChainLength = this.calculateMinChainLength(commonBlockSummary, ourAdditionalBlocksAfterCommonBlock, peersSharingCommonBlock);
|
||||
|
||||
// Fetch block summaries from each peer
|
||||
for (Peer peer : peersSharingCommonBlock) {
|
||||
|
||||
// If we're shutting down, just return the latest peer list
|
||||
if (Controller.isStopping())
|
||||
return peers;
|
||||
|
||||
// Count the number of blocks this peer has beyond our common block
|
||||
final int peerHeight = peer.getChainTipData().getLastHeight();
|
||||
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
||||
// Limit the number of blocks we are comparing. FUTURE: we could request more in batches, but there may not be a case when this is needed
|
||||
int summariesRequired = Math.min(peerAdditionalBlocksAfterCommonBlock, MAXIMUM_REQUEST_SIZE);
|
||||
|
||||
// Check if we can use the cached common block summaries, by comparing the peer's current chain tip against the peer's chain tip when we last found our common block
|
||||
boolean useCachedSummaries = false;
|
||||
if (peer.canUseCachedCommonBlockData()) {
|
||||
if (peer.getCommonBlockData().getBlockSummariesAfterCommonBlock() != null) {
|
||||
if (peer.getCommonBlockData().getBlockSummariesAfterCommonBlock().size() == summariesRequired) {
|
||||
LOGGER.debug(String.format("Using cached block summaries for peer %s", peer));
|
||||
useCachedSummaries = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (useCachedSummaries == false) {
|
||||
if (summariesRequired > 0) {
|
||||
LOGGER.trace(String.format("Requesting %d block summar%s from peer %s after common block %.8s. Peer height: %d", summariesRequired, (summariesRequired != 1 ? "ies" : "y"), peer, Base58.encode(commonBlockSummary.getSignature()), peerHeight));
|
||||
|
||||
List<BlockSummaryData> blockSummaries = this.getBlockSummaries(peer, commonBlockSummary.getSignature(), summariesRequired);
|
||||
peer.getCommonBlockData().setBlockSummariesAfterCommonBlock(blockSummaries);
|
||||
|
||||
if (blockSummaries != null) {
|
||||
LOGGER.trace(String.format("Peer %s returned %d block summar%s", peer, blockSummaries.size(), (blockSummaries.size() != 1 ? "ies" : "y")));
|
||||
|
||||
// We need to adjust minChainLength if peers fail to return all expected block summaries
|
||||
if (blockSummaries.size() < summariesRequired) {
|
||||
// This could mean that the peer has re-orged. But we still have the same common block, so it's safe to proceed with this set of signatures instead.
|
||||
LOGGER.debug(String.format("Peer %s returned %d block summar%s instead of expected %d", peer, blockSummaries.size(), (blockSummaries.size() != 1 ? "ies" : "y"), summariesRequired));
|
||||
|
||||
// Update minChainLength if we have at least 1 block for this peer. If we don't have any blocks, this peer will be excluded from chain weight comparisons later in the process, so we shouldn't update minChainLength
|
||||
if (blockSummaries.size() > 0)
|
||||
minChainLength = blockSummaries.size();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There are no block summaries after this common block
|
||||
peer.getCommonBlockData().setBlockSummariesAfterCommonBlock(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch our corresponding block summaries. Limit to MAXIMUM_REQUEST_SIZE, in order to make the comparison fairer, as peers have been limited too
|
||||
final int ourSummariesRequired = Math.min(ourAdditionalBlocksAfterCommonBlock, MAXIMUM_REQUEST_SIZE);
|
||||
LOGGER.trace(String.format("About to fetch our block summaries from %d to %d. Our height: %d", commonBlockSummary.getHeight() + 1, commonBlockSummary.getHeight() + ourSummariesRequired, ourHeight));
|
||||
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockSummary.getHeight() + 1, commonBlockSummary.getHeight() + ourSummariesRequired);
|
||||
if (ourBlockSummaries.isEmpty())
|
||||
LOGGER.debug(String.format("We don't have any block summaries so can't compare our chain against peers with this common block. We can still compare them against each other."));
|
||||
else
|
||||
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
|
||||
|
||||
// Create array to hold peers for comparison
|
||||
List<Peer> superiorPeersForComparison = new ArrayList<>();
|
||||
|
||||
// Calculate our chain weight
|
||||
BigInteger ourChainWeight = BigInteger.valueOf(0);
|
||||
if (ourBlockSummaries.size() > 0)
|
||||
ourChainWeight = Block.calcChainWeight(commonBlockSummary.getHeight(), commonBlockSummary.getSignature(), ourBlockSummaries, minChainLength);
|
||||
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
NumberFormat accurateFormatter = new DecimalFormat("0.################E0");
|
||||
LOGGER.debug(String.format("Our chain weight based on %d blocks is %s", ourBlockSummaries.size(), formatter.format(ourChainWeight)));
|
||||
|
||||
LOGGER.debug(String.format("Listing peers with common block %.8s...", Base58.encode(commonBlockSummary.getSignature())));
|
||||
for (Peer peer : peersSharingCommonBlock) {
|
||||
final int peerHeight = peer.getChainTipData().getLastHeight();
|
||||
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
||||
final CommonBlockData peerCommonBlockData = peer.getCommonBlockData();
|
||||
|
||||
if (peerCommonBlockData == null || peerCommonBlockData.getBlockSummariesAfterCommonBlock() == null || peerCommonBlockData.getBlockSummariesAfterCommonBlock().isEmpty()) {
|
||||
// No response - remove this peer for now
|
||||
LOGGER.debug(String.format("Peer %s doesn't have any block summaries - removing it from this round", peer));
|
||||
peers.remove(peer);
|
||||
continue;
|
||||
}
|
||||
|
||||
final List<BlockSummaryData> peerBlockSummariesAfterCommonBlock = peerCommonBlockData.getBlockSummariesAfterCommonBlock();
|
||||
populateBlockSummariesMinterLevels(repository, peerBlockSummariesAfterCommonBlock);
|
||||
|
||||
// Calculate cumulative chain weight of this blockchain subset, from common block to highest mutual block held by all peers in this group.
|
||||
LOGGER.debug(String.format("About to calculate chain weight based on %d blocks for peer %s with common block %.8s (peer has %d blocks after common block)", peerBlockSummariesAfterCommonBlock.size(), peer, Base58.encode(commonBlockSummary.getSignature()), peerAdditionalBlocksAfterCommonBlock));
|
||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockSummary.getHeight(), commonBlockSummary.getSignature(), peerBlockSummariesAfterCommonBlock, minChainLength);
|
||||
peer.getCommonBlockData().setChainWeight(peerChainWeight);
|
||||
LOGGER.debug(String.format("Chain weight of peer %s based on %d blocks (%d - %d) is %s", peer, peerBlockSummariesAfterCommonBlock.size(), peerBlockSummariesAfterCommonBlock.get(0).getHeight(), peerBlockSummariesAfterCommonBlock.get(peerBlockSummariesAfterCommonBlock.size()-1).getHeight(), formatter.format(peerChainWeight)));
|
||||
|
||||
// Compare against our chain - if our blockchain has greater weight then don't synchronize with peer (or any others in this group)
|
||||
if (ourChainWeight.compareTo(peerChainWeight) > 0) {
|
||||
// This peer is on an inferior chain - remove it
|
||||
LOGGER.debug(String.format("Peer %s is on an inferior chain to us - removing it from this round", peer));
|
||||
peers.remove(peer);
|
||||
}
|
||||
else {
|
||||
// Our chain is inferior
|
||||
LOGGER.debug(String.format("Peer %s is on a better chain to us. We will compare the other peers sharing this common block against each other, and drop all peers sharing higher common blocks.", peer));
|
||||
dropPeersAfterCommonBlockHeight = commonBlockSummary.getHeight();
|
||||
superiorPeersForComparison.add(peer);
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we have selected the best peers, compare them against each other and remove any with lower weights
|
||||
if (superiorPeersForComparison.size() > 0) {
|
||||
BigInteger bestChainWeight = null;
|
||||
for (Peer peer : superiorPeersForComparison) {
|
||||
// Increase bestChainWeight if needed
|
||||
if (bestChainWeight == null || peer.getCommonBlockData().getChainWeight().compareTo(bestChainWeight) >= 0)
|
||||
bestChainWeight = peer.getCommonBlockData().getChainWeight();
|
||||
}
|
||||
for (Peer peer : superiorPeersForComparison) {
|
||||
// Check if we should discard an inferior peer
|
||||
if (peer.getCommonBlockData().getChainWeight().compareTo(bestChainWeight) < 0) {
|
||||
BigInteger difference = bestChainWeight.subtract(peer.getCommonBlockData().getChainWeight());
|
||||
LOGGER.debug(String.format("Peer %s has a lower chain weight (difference: %s) than other peer(s) in this group - removing it from this round.", peer, accurateFormatter.format(difference)));
|
||||
peers.remove(peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return peers;
|
||||
} finally {
|
||||
repository.discardChanges(); // Free repository locks, if any, also in case anything went wrong
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue during peer comparison", e);
|
||||
return peers;
|
||||
}
|
||||
}
|
||||
|
||||
private List<BlockSummaryData> uniqueCommonBlocks(List<Peer> peers) {
|
||||
List<BlockSummaryData> commonBlocks = new ArrayList<>();
|
||||
|
||||
for (Peer peer : peers) {
|
||||
if (peer.getCommonBlockData() != null && peer.getCommonBlockData().getCommonBlockSummary() != null) {
|
||||
LOGGER.debug(String.format("Peer %s has common block %.8s", peer, Base58.encode(peer.getCommonBlockData().getCommonBlockSummary().getSignature())));
|
||||
|
||||
BlockSummaryData commonBlockSummary = peer.getCommonBlockData().getCommonBlockSummary();
|
||||
if (!commonBlocks.contains(commonBlockSummary))
|
||||
commonBlocks.add(commonBlockSummary);
|
||||
}
|
||||
else {
|
||||
LOGGER.debug(String.format("Peer %s has no common block data. Skipping...", peer));
|
||||
}
|
||||
}
|
||||
|
||||
return commonBlocks;
|
||||
}
|
||||
|
||||
private int calculateMinChainLength(BlockSummaryData commonBlockSummary, int ourAdditionalBlocksAfterCommonBlock, List<Peer> peersSharingCommonBlock) {
|
||||
// Calculate the length of the shortest peer chain sharing this common block, including our chain
|
||||
int minChainLength = ourAdditionalBlocksAfterCommonBlock;
|
||||
for (Peer peer : peersSharingCommonBlock) {
|
||||
final int peerHeight = peer.getChainTipData().getLastHeight();
|
||||
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
||||
|
||||
if (peerAdditionalBlocksAfterCommonBlock < minChainLength)
|
||||
minChainLength = peerAdditionalBlocksAfterCommonBlock;
|
||||
}
|
||||
return minChainLength;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to synchronize blockchain with peer.
|
||||
* <p>
|
||||
@@ -110,9 +474,12 @@ public class Synchronizer {
|
||||
|
||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||
SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, force, peerBlockSummaries);
|
||||
if (findCommonBlockResult != SynchronizationResult.OK)
|
||||
if (findCommonBlockResult != SynchronizationResult.OK) {
|
||||
// Logging performed by fetchSummariesFromCommonBlock() above
|
||||
// Clear our common block cache for this peer
|
||||
peer.setCommonBlockData(null);
|
||||
return findCommonBlockResult;
|
||||
}
|
||||
|
||||
// First summary is common block
|
||||
final BlockData commonBlockData = repository.getBlockRepository().fromSignature(peerBlockSummaries.get(0).getSignature());
|
||||
@@ -399,11 +766,22 @@ public class Synchronizer {
|
||||
LOGGER.info(String.format("Peer %s failed to respond with more block signatures after height %d, sig %.8s", peer,
|
||||
height, Base58.encode(latestPeerSignature)));
|
||||
|
||||
// If we have already received blocks from this peer, go ahead and apply them
|
||||
// Clear our cache of common block summaries for this peer, as they are likely to be invalid
|
||||
CommonBlockData cachedCommonBlockData = peer.getCommonBlockData();
|
||||
if (cachedCommonBlockData != null)
|
||||
cachedCommonBlockData.setBlockSummariesAfterCommonBlock(null);
|
||||
|
||||
// If we have already received RECENT blocks from this peer, go ahead and apply them
|
||||
if (peerBlocks.size() > 0) {
|
||||
break;
|
||||
final Block peerLatestBlock = peerBlocks.get(peerBlocks.size() - 1);
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (peerLatestBlock != null && minLatestBlockTimestamp != null
|
||||
&& peerLatestBlock.getBlockData().getTimestamp() > minLatestBlockTimestamp) {
|
||||
LOGGER.debug("Newly received blocks are recent, so we will apply them");
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Otherwise, give up and move on to the next peer
|
||||
// Otherwise, give up and move on to the next peer, to avoid putting our chain into an outdated state
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
}
|
||||
|
||||
@@ -428,11 +806,17 @@ public class Synchronizer {
|
||||
|
||||
if (retryCount >= MAXIMUM_RETRIES) {
|
||||
|
||||
// If we have already received blocks from this peer, go ahead and apply them
|
||||
// If we have already received RECENT blocks from this peer, go ahead and apply them
|
||||
if (peerBlocks.size() > 0) {
|
||||
break;
|
||||
final Block peerLatestBlock = peerBlocks.get(peerBlocks.size() - 1);
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (peerLatestBlock != null && minLatestBlockTimestamp != null
|
||||
&& peerLatestBlock.getBlockData().getTimestamp() > minLatestBlockTimestamp) {
|
||||
LOGGER.debug("Newly received blocks are recent, so we will apply them");
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Otherwise, give up and move on to the next peer
|
||||
// Otherwise, give up and move on to the next peer, to avoid putting our chain into an outdated state
|
||||
return SynchronizationResult.NO_REPLY;
|
||||
|
||||
} else {
|
||||
|
@@ -211,6 +211,9 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
|
||||
|
||||
// Attempt to backup the trade bot data
|
||||
TradeBot.backupTradeBotData(repository);
|
||||
|
||||
// Return to user for signing and broadcast as we don't have their Qortal private key
|
||||
try {
|
||||
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
||||
@@ -283,6 +286,9 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
|
||||
|
||||
// Attempt to backup the trade bot data
|
||||
TradeBot.backupTradeBotData(repository);
|
||||
|
||||
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
|
||||
long p2shFee;
|
||||
try {
|
||||
|
@@ -7,6 +7,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -267,6 +268,22 @@ public class TradeBot implements Listener {
|
||||
return secret;
|
||||
}
|
||||
|
||||
/*package*/ static void backupTradeBotData(Repository repository) {
|
||||
// Attempt to backup the trade bot data. This an optional step and doesn't impact trading, so don't throw an exception on failure
|
||||
try {
|
||||
LOGGER.info("About to backup trade bot data...");
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
blockchainLock.lockInterruptibly();
|
||||
try {
|
||||
repository.exportNodeLocalData(true);
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException | DataException e) {
|
||||
LOGGER.info(String.format("Failed to obtain blockchain lock when exporting trade bot data: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates trade-bot entry to new state, with current timestamp, logs message and notifies state-change listeners. */
|
||||
/*package*/ static void updateTradeBotState(Repository repository, TradeBotData tradeBotData,
|
||||
String newState, int newStateValue, Supplier<String> logMessageSupplier) throws DataException {
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.data.block;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.Arrays;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BlockSummaryData {
|
||||
@@ -84,4 +85,21 @@ public class BlockSummaryData {
|
||||
this.minterLevel = minterLevel;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
BlockSummaryData otherBlockSummary = (BlockSummaryData) o;
|
||||
if (this.getSignature() == null || otherBlockSummary.getSignature() == null)
|
||||
return false;
|
||||
|
||||
// Treat two block summaries as equal if they have matching signatures
|
||||
return Arrays.equals(this.getSignature(), otherBlockSummary.getSignature());
|
||||
}
|
||||
|
||||
}
|
||||
|
56
src/main/java/org/qortal/data/block/CommonBlockData.java
Normal file
56
src/main/java/org/qortal/data/block/CommonBlockData.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package org.qortal.data.block;
|
||||
|
||||
import org.qortal.data.network.PeerChainTipData;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class CommonBlockData {
|
||||
|
||||
// Properties
|
||||
private BlockSummaryData commonBlockSummary = null;
|
||||
private List<BlockSummaryData> blockSummariesAfterCommonBlock = null;
|
||||
private BigInteger chainWeight = null;
|
||||
private PeerChainTipData chainTipData = null;
|
||||
|
||||
// Constructors
|
||||
|
||||
protected CommonBlockData() {
|
||||
}
|
||||
|
||||
public CommonBlockData(BlockSummaryData commonBlockSummary, PeerChainTipData chainTipData) {
|
||||
this.commonBlockSummary = commonBlockSummary;
|
||||
this.chainTipData = chainTipData;
|
||||
}
|
||||
|
||||
|
||||
// Getters / setters
|
||||
|
||||
public BlockSummaryData getCommonBlockSummary() {
|
||||
return this.commonBlockSummary;
|
||||
}
|
||||
|
||||
public List<BlockSummaryData> getBlockSummariesAfterCommonBlock() {
|
||||
return this.blockSummariesAfterCommonBlock;
|
||||
}
|
||||
|
||||
public void setBlockSummariesAfterCommonBlock(List<BlockSummaryData> blockSummariesAfterCommonBlock) {
|
||||
this.blockSummariesAfterCommonBlock = blockSummariesAfterCommonBlock;
|
||||
}
|
||||
|
||||
public BigInteger getChainWeight() {
|
||||
return this.chainWeight;
|
||||
}
|
||||
|
||||
public void setChainWeight(BigInteger chainWeight) {
|
||||
this.chainWeight = chainWeight;
|
||||
}
|
||||
|
||||
public PeerChainTipData getChainTipData() {
|
||||
return this.chainTipData;
|
||||
}
|
||||
|
||||
}
|
@@ -15,6 +15,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
@@ -22,6 +23,7 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.data.block.CommonBlockData;
|
||||
import org.qortal.data.network.PeerChainTipData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.network.message.ChallengeMessage;
|
||||
@@ -106,6 +108,9 @@ public class Peer {
|
||||
/** Latest block info as reported by peer. */
|
||||
private PeerChainTipData peersChainTipData;
|
||||
|
||||
/** Our common block with this peer */
|
||||
private CommonBlockData commonBlockData;
|
||||
|
||||
// Constructors
|
||||
|
||||
/** Construct unconnected, outbound Peer using socket address in peer data */
|
||||
@@ -272,6 +277,18 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
public CommonBlockData getCommonBlockData() {
|
||||
synchronized (this.peerInfoLock) {
|
||||
return this.commonBlockData;
|
||||
}
|
||||
}
|
||||
|
||||
public void setCommonBlockData(CommonBlockData commonBlockData) {
|
||||
synchronized (this.peerInfoLock) {
|
||||
this.commonBlockData = commonBlockData;
|
||||
}
|
||||
}
|
||||
|
||||
/*package*/ void queueMessage(Message message) {
|
||||
if (!this.pendingMessages.offer(message))
|
||||
LOGGER.info(() -> String.format("No room to queue message from peer %s - discarding", this));
|
||||
@@ -616,6 +633,25 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Common block data
|
||||
|
||||
public boolean canUseCachedCommonBlockData() {
|
||||
PeerChainTipData peerChainTipData = this.getChainTipData();
|
||||
CommonBlockData commonBlockData = this.getCommonBlockData();
|
||||
|
||||
if (peerChainTipData != null && commonBlockData != null) {
|
||||
PeerChainTipData commonBlockChainTipData = commonBlockData.getChainTipData();
|
||||
if (peerChainTipData.getLastBlockSignature() != null && commonBlockChainTipData != null && commonBlockChainTipData.getLastBlockSignature() != null) {
|
||||
if (Arrays.equals(peerChainTipData.getLastBlockSignature(), commonBlockChainTipData.getLastBlockSignature())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Utility methods
|
||||
|
||||
/** Returns true if ports and addresses (or hostnames) match */
|
||||
|
@@ -49,7 +49,7 @@ public interface Repository extends AutoCloseable {
|
||||
|
||||
public void performPeriodicMaintenance() throws DataException;
|
||||
|
||||
public void exportNodeLocalData() throws DataException;
|
||||
public void exportNodeLocalData(boolean keepArchivedCopy) throws DataException;
|
||||
|
||||
public void importDataFromFile(String filename) throws DataException;
|
||||
|
||||
|
@@ -52,6 +52,7 @@ import org.qortal.repository.TransactionRepository;
|
||||
import org.qortal.repository.VotingRepository;
|
||||
import org.qortal.repository.hsqldb.transaction.HSQLDBTransactionRepository;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class HSQLDBRepository implements Repository {
|
||||
|
||||
@@ -459,10 +460,44 @@ public class HSQLDBRepository implements Repository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportNodeLocalData() throws DataException {
|
||||
public void exportNodeLocalData(boolean keepArchivedCopy) throws DataException {
|
||||
|
||||
// Create the qortal-backup folder if it doesn't exist
|
||||
Path backupPath = Paths.get("qortal-backup");
|
||||
try {
|
||||
Files.createDirectories(backupPath);
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Unable to create backup folder");
|
||||
throw new DataException("Unable to create backup folder");
|
||||
}
|
||||
|
||||
// We need to rename or delete an existing TradeBotStates backup before creating a new one
|
||||
File tradeBotStatesBackupFile = new File("qortal-backup/TradeBotStates.script");
|
||||
if (tradeBotStatesBackupFile.exists()) {
|
||||
if (keepArchivedCopy) {
|
||||
// Rename existing TradeBotStates backup, to make sure that we're not overwriting any keys
|
||||
File archivedBackupFile = new File(String.format("qortal-backup/TradeBotStates-archive-%d.script", NTP.getTime()));
|
||||
if (tradeBotStatesBackupFile.renameTo(archivedBackupFile))
|
||||
LOGGER.info(String.format("Moved existing TradeBotStates backup file to %s", archivedBackupFile.getPath()));
|
||||
else
|
||||
throw new DataException("Unable to rename existing TradeBotStates backup");
|
||||
} else {
|
||||
// Delete existing copy
|
||||
LOGGER.info("Deleting existing TradeBotStates backup because it is being replaced with a new one");
|
||||
tradeBotStatesBackupFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// There's currently no need to take an archived copy of the MintingAccounts data - just delete the old one if it exists
|
||||
File mintingAccountsBackupFile = new File("qortal-backup/MintingAccounts.script");
|
||||
if (mintingAccountsBackupFile.exists()) {
|
||||
LOGGER.info("Deleting existing MintingAccounts backup because it is being replaced with a new one");
|
||||
mintingAccountsBackupFile.delete();
|
||||
}
|
||||
|
||||
try (Statement stmt = this.connection.createStatement()) {
|
||||
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'MintingAccounts.script'");
|
||||
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.script'");
|
||||
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'qortal-backup/MintingAccounts.script'");
|
||||
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'qortal-backup/TradeBotStates.script'");
|
||||
LOGGER.info("Exported sensitive/node-local data: minting keys and trade bot states");
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to export sensitive/node-local data from repository");
|
||||
@@ -475,12 +510,12 @@ public class HSQLDBRepository implements Repository {
|
||||
LOGGER.info(() -> String.format("Importing data into repository from %s", filename));
|
||||
|
||||
String escapedFilename = stmt.enquoteLiteral(filename);
|
||||
stmt.execute("PERFORM IMPORT SCRIPT DATA FROM " + escapedFilename + " STOP ON ERROR");
|
||||
stmt.execute("PERFORM IMPORT SCRIPT DATA FROM " + escapedFilename + " CONTINUE ON ERROR");
|
||||
|
||||
LOGGER.info(() -> String.format("Imported data into repository from %s", filename));
|
||||
} catch (SQLException e) {
|
||||
LOGGER.info(() -> String.format("Failed to import data into repository from %s: %s", filename, e.getMessage()));
|
||||
throw new DataException("Unable to export sensitive/node-local data from repository: " + e.getMessage());
|
||||
throw new DataException("Unable to import sensitive/node-local data to repository: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,7 +716,7 @@ public class HSQLDBRepository implements Repository {
|
||||
/**
|
||||
* Execute PreparedStatement and return changed row count.
|
||||
*
|
||||
* @param preparedStatement
|
||||
* @param sql
|
||||
* @param objects
|
||||
* @return number of changed rows
|
||||
* @throws SQLException
|
||||
@@ -693,8 +728,8 @@ public class HSQLDBRepository implements Repository {
|
||||
/**
|
||||
* Execute batched PreparedStatement
|
||||
*
|
||||
* @param preparedStatement
|
||||
* @param objects
|
||||
* @param sql
|
||||
* @param batchedObjects
|
||||
* @return number of changed rows
|
||||
* @throws SQLException
|
||||
*/
|
||||
@@ -818,7 +853,7 @@ public class HSQLDBRepository implements Repository {
|
||||
*
|
||||
* @param tableName
|
||||
* @param whereClause
|
||||
* @param objects
|
||||
* @param batchedObjects
|
||||
* @throws SQLException
|
||||
*/
|
||||
public int deleteBatch(String tableName, String whereClause, List<Object[]> batchedObjects) throws SQLException {
|
||||
|
@@ -52,7 +52,7 @@ public class Settings {
|
||||
// UI servers
|
||||
private int uiPort = 12388;
|
||||
private String[] uiLocalServers = new String[] {
|
||||
"localhost", "127.0.0.1", "172.24.1.1", "qor.tal"
|
||||
"localhost", "127.0.0.1"
|
||||
};
|
||||
private String[] uiRemoteServers = new String[] {
|
||||
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
|
||||
|
Reference in New Issue
Block a user