From 54e5a65cf046db5126921eab30e095a81e874d70 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sat, 18 Sep 2021 10:41:58 +0100 Subject: [PATCH] Allow an alternative block to be minted if the chain stalls due to an invalid block If it has been more than 10 minutes since receiving the last valid block, but we have had at least one invalid block since then, this is indicative of a stuck chain due to no valid block candidates. In this case, we want to allow the block minter to mint an alternative candidate so that the chain can continue. This would create a fork at the point of the invalid block, in which two chains (valid an invalid) would diverge. The valid chain could never rejoin the invalid one, however it's likely that the invalid chain would be discarded in favour of the valid one shortly after, on the assumption that the majority of nodes would have picked the valid one. --- .../org/qortal/controller/BlockMinter.java | 21 ++++++++++++++++++- .../org/qortal/controller/Synchronizer.java | 18 ++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java index 8b6563f2..67a202df 100644 --- a/src/main/java/org/qortal/controller/BlockMinter.java +++ b/src/main/java/org/qortal/controller/BlockMinter.java @@ -44,6 +44,9 @@ public class BlockMinter extends Thread { private static Long lastLogTimestamp; private static Long logTimeout; + // Recovery + public static final long INVALID_BLOCK_RECOVERY_TIMEOUT = 10 * 60 * 1000L; // ms + // Constructors public BlockMinter() { @@ -144,9 +147,25 @@ public class BlockMinter extends Thread { if (peers.size() < Settings.getInstance().getMinBlockchainPeers()) continue; + // If we are stuck on an invalid block, we should allow an alternative to be minted + boolean recoverInvalidBlock = false; + if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) { + // We've had at least one invalid block + long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived; + long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived; + if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) { + if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) { + // Last valid block was more than 10 mins ago, but we've had an invalid block since then + // Assume that the chain has stalled because there is no alternative valid candidate + // Enter recovery mode to allow alternative, valid candidates to be minted + recoverInvalidBlock = true; + } + } + } + // 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) - if (Controller.getInstance().getRecoveryMode() == false) + if (Controller.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false) continue; // There are enough peers with a recent block and our latest block is recent diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index 113af107..30f3c6ee 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -62,6 +62,11 @@ public class Synchronizer { // Keep track of the size of the last re-org, so it can be logged private int lastReorgSize; + // Keep track of invalid blocks so that we don't keep trying to sync them + private List invalidBlockSignatures = new ArrayList<>(); + public Long timeValidBlockLastReceived = null; + public Long timeInvalidBlockLastReceived = null; + private static Synchronizer instance; public enum SynchronizationResult { @@ -526,6 +531,11 @@ public class Synchronizer { // Reset last re-org size as we are starting a new sync round this.lastReorgSize = 0; + // Set the initial value of timeValidBlockLastReceived if it's null + if (this.timeValidBlockLastReceived == null) { + this.timeValidBlockLastReceived = NTP.getTime(); + } + List peerBlockSummaries = new ArrayList<>(); SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, force, peerBlockSummaries, true); if (findCommonBlockResult != SynchronizationResult.OK) { @@ -980,9 +990,13 @@ public class Synchronizer { if (blockResult != ValidationResult.OK) { LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer, newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getSignature()), blockResult.name())); + this.timeInvalidBlockLastReceived = NTP.getTime(); return SynchronizationResult.INVALID_DATA; } + // Block is valid + this.timeValidBlockLastReceived = NTP.getTime(); + // Save transactions attached to this block for (Transaction transaction : newBlock.getTransactions()) { TransactionData transactionData = transaction.getTransactionData(); @@ -1068,9 +1082,13 @@ public class Synchronizer { if (blockResult != ValidationResult.OK) { LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer, ourHeight, Base58.encode(latestPeerSignature), blockResult.name())); + this.timeInvalidBlockLastReceived = NTP.getTime(); return SynchronizationResult.INVALID_DATA; } + // Block is valid + this.timeValidBlockLastReceived = NTP.getTime(); + // Save transactions attached to this block for (Transaction transaction : newBlock.getTransactions()) { TransactionData transactionData = transaction.getTransactionData();