diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2397,12 +2397,58 @@ while (hasValidAncestor && pindexTest && pindexTest != pindexFork) { assert(pindexTest->nChainTx || pindexTest->nHeight == 0); + // If this is a parked chain, but it has enough PoW, clear the park + // state. + bool fParkedChain = pindexTest->nStatus.isOnParkedChain(); + if (fParkedChain && gArgs.GetBoolArg("-parkdeepreorg", true)) { + const CBlockIndex *pindexTip = chainActive.Tip(); + + // During initialization, pindexTip and/or pindexFork may be + // null. In this case, we just ignore the fact that the chain is + // parked. + if (!pindexTip || !pindexFork) { + UnparkBlock(pindexTest); + continue; + } + + // A parked chain can be unparked if it has twice as much PoW + // accumulated as the main chain has since the fork block. + CBlockIndex const *pindexExtraPow = pindexTip; + arith_uint256 requiredWork = pindexTip->nChainWork; + switch (pindexTip->nHeight - pindexFork->nHeight) { + // Limit the penality for depth 1, 2 and 3 to half a block + // worth of work to ensure we don't fork accidentaly. + case 3: + case 2: + pindexExtraPow = pindexExtraPow->pprev; + // FALLTHROUGH + case 1: { + const arith_uint256 deltaWork = + pindexExtraPow->nChainWork - pindexFork->nChainWork; + requiredWork += (deltaWork >> 1); + break; + } + default: + requiredWork += + pindexExtraPow->nChainWork - pindexFork->nChainWork; + break; + } + + if (pindexNew->nChainWork > requiredWork) { + // We have enough, clear the parked state. + LogPrintf("Unpark block %s as its chain has accumulated " + "enough PoW.\n", + pindexTest->GetBlockHash().ToString()); + fParkedChain = false; + UnparkBlock(pindexTest); + } + } + // Pruned nodes may have entries in setBlockIndexCandidates for // which block files have been deleted. Remove those as candidates // for the most work chain if we come across them; we can't switch // to a chain unless we have all the non-active-chain parent blocks. bool fInvalidChain = pindexTest->nStatus.isInvalid(); - bool fParkedChain = pindexTest->nStatus.isOnParkedChain(); bool fMissingData = !pindexTest->nStatus.hasData(); if (!(fInvalidChain || fParkedChain || fMissingData)) { // The current block is acceptable, move to the parent, up to @@ -2721,6 +2767,10 @@ nBlockReverseSequenceId--; } + // In case this was parked, unpark it. + UnparkBlock(pindex); + + // Make sure it is added to the candidate list if apropriate. if (pindex->IsValid(BlockValidity::TRANSACTIONS) && pindex->nChainTx) { setBlockIndexCandidates.insert(pindex); PruneBlockIndexCandidates(); @@ -3621,6 +3671,20 @@ block.GetHash().ToString()); } + // If this is a deep reorg (a regorg of more than one block), preemptively + // mark the chain as parked. If it has enough work, it'll unpark + // automatically. We mark the block as parked at the very last minute so we + // can make sure everything is ready to be reorged if needed. + if (gArgs.GetBoolArg("-parkdeepreorg", true)) { + const CBlockIndex *pindexFork = chainActive.FindFork(pindex); + if (pindexFork && pindexFork->nHeight + 1 < pindex->nHeight) { + LogPrintf("Park block %s as it would cause a deep reorg.\n", + pindex->GetBlockHash().ToString()); + pindex->nStatus = pindex->nStatus.withParked(); + setDirtyBlockIndex.insert(pindex); + } + } + // Header is valid/has work and the merkle tree is good. // Relay now, but if it does not build on our best tip, let the // SendMessages loop relay it. diff --git a/test/functional/abc-parkedchain.py b/test/functional/abc-parkedchain.py --- a/test/functional/abc-parkedchain.py +++ b/test/functional/abc-parkedchain.py @@ -6,12 +6,13 @@ import os from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal +from test_framework.util import assert_equal, connect_nodes_bi, sync_blocks, wait_until class ParkedChainTest(BitcoinTestFramework): def set_test_params(self): - self.num_nodes = 1 + self.num_nodes = 2 + self.extra_args = [["-noparkdeepreorg"], []] # There should only be one chaintip, which is expected_tip def only_valid_tip(self, expected_tip, other_tip_status=None): @@ -36,6 +37,8 @@ # Let's park the chain. assert(parked_tip != tip) + assert(block_to_park != tip) + assert(block_to_park != parked_tip) node.parkblock(block_to_park) assert_equal(node.getbestblockhash(), tip) @@ -122,6 +125,49 @@ node.reconsiderblock(bad_tip) self.only_valid_tip(good_tip) + # First, make sure both nodes are in sync. + parking_node = self.nodes[1] + connect_nodes_bi(self.nodes, 0, 1) + sync_blocks(self.nodes[0:2]) + + assert_equal(node.getbestblockhash(), parking_node.getbestblockhash()) + + # Wait for node 1 to park the chain. + def wait_for_parked_block(block): + def check_block(): + for tip in parking_node.getchaintips(): + if tip["hash"] == block: + assert(tip["status"] != "active") + return tip["status"] == "parked" + return False + wait_until(check_block) + + def check_reorg_protection(depth, extra_blocks): + self.log.info("Test deep reorg parking, %d block deep" % depth) + + # Invalidate the tip on node 0, so it doesn't follow node 1. + node.invalidateblock(node.getbestblockhash()) + # Mine block to create a fork of proper depth + parking_node.generate(depth - 1) + node.generate(depth) + # extra block should now find themselves parked + for i in range(extra_blocks): + node.generate(1) + wait_for_parked_block(node.getbestblockhash()) + + # If we mine one more block, the node reorgs. + node.generate(1) + wait_until(lambda: parking_node.getbestblockhash() + == node.getbestblockhash()) + + check_reorg_protection(1, 0) + check_reorg_protection(2, 0) + check_reorg_protection(3, 1) + check_reorg_protection(4, 4) + check_reorg_protection(5, 5) + check_reorg_protection(6, 6) + check_reorg_protection(100, 100) + if __name__ == '__main__': ParkedChainTest().main() diff --git a/test/functional/bip68-sequence.py b/test/functional/bip68-sequence.py --- a/test/functional/bip68-sequence.py +++ b/test/functional/bip68-sequence.py @@ -26,7 +26,7 @@ class BIP68Test(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 - self.extra_args = [["-blockprioritypercentage=0"], + self.extra_args = [["-blockprioritypercentage=0", "-noparkdeepreorg"], ["-blockprioritypercentage=0", "-acceptnonstdtxn=0"]] def run_test(self): diff --git a/test/functional/dbcrash.py b/test/functional/dbcrash.py --- a/test/functional/dbcrash.py +++ b/test/functional/dbcrash.py @@ -53,7 +53,8 @@ # Set -rpcservertimeout=900 to reduce socket disconnects in this # long-running test self.base_args = ["-limitdescendantsize=0", "-maxmempool=0", - "-rpcservertimeout=900", "-dbbatchsize=200000"] + "-rpcservertimeout=900", "-dbbatchsize=200000", + "-noparkdeepreorg"] # Set different crash ratios and cache sizes. Note that not all of # -dbcache goes to pcoinsTip. diff --git a/test/functional/getchaintips.py b/test/functional/getchaintips.py --- a/test/functional/getchaintips.py +++ b/test/functional/getchaintips.py @@ -14,6 +14,7 @@ class GetChainTipsTest (BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 + self.extra_args = [["-noparkdeepreorg"], ["-noparkdeepreorg"], [], []] def run_test(self): tips = self.nodes[0].getchaintips() diff --git a/test/functional/invalidateblock.py b/test/functional/invalidateblock.py --- a/test/functional/invalidateblock.py +++ b/test/functional/invalidateblock.py @@ -15,6 +15,7 @@ def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 + self.extra_args = [["-noparkdeepreorg"], [], []] def setup_network(self): self.setup_nodes() diff --git a/test/functional/listsinceblock.py b/test/functional/listsinceblock.py --- a/test/functional/listsinceblock.py +++ b/test/functional/listsinceblock.py @@ -11,6 +11,7 @@ def set_test_params(self): self.num_nodes = 4 self.setup_clean_chain = True + self.extra_args = [["-noparkdeepreorg"], ["-noparkdeepreorg"], [], []] def run_test(self): ''' diff --git a/test/functional/p2p-acceptblock.py b/test/functional/p2p-acceptblock.py --- a/test/functional/p2p-acceptblock.py +++ b/test/functional/p2p-acceptblock.py @@ -76,7 +76,8 @@ def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 - self.extra_args = [[], ["-whitelist=127.0.0.1"], + self.extra_args = [["-noparkdeepreorg"], + ["-noparkdeepreorg", "-whitelist=127.0.0.1"], ["-minimumchainwork=0x10"]] def setup_network(self): diff --git a/test/functional/p2p-fullblocktest.py b/test/functional/p2p-fullblocktest.py --- a/test/functional/p2p-fullblocktest.py +++ b/test/functional/p2p-fullblocktest.py @@ -61,7 +61,7 @@ # Change the "outcome" variable from each TestInstance object to only do the comparison. def set_test_params(self): self.num_nodes = 1 - self.extra_args = [['-whitelist=127.0.0.1']] + self.extra_args = [['-whitelist=127.0.0.1', '-noparkdeepreorg']] self.setup_clean_chain = True self.block_heights = {} self.coinbase_key = CECKey() diff --git a/test/functional/preciousblock.py b/test/functional/preciousblock.py --- a/test/functional/preciousblock.py +++ b/test/functional/preciousblock.py @@ -45,6 +45,8 @@ def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 + self.extra_args = [["-noparkdeepreorg"], + ["-noparkdeepreorg"], ["-noparkdeepreorg"]] def setup_network(self): self.setup_nodes() diff --git a/test/functional/pruning.py b/test/functional/pruning.py --- a/test/functional/pruning.py +++ b/test/functional/pruning.py @@ -37,16 +37,21 @@ # Create nodes 0 and 1 to mine. # Create node 2 to test pruning. - self.full_node_default_args = ["-maxreceivebuffer=20000", "-blockmaxsize=999000", "-checkblocks=5", - "-limitdescendantcount=100", "-limitdescendantsize=5000", "-limitancestorcount=100", "-limitancestorsize=5000"] + self.full_node_default_args = ["-maxreceivebuffer=20000", "-blockmaxsize=999000", + "-checkblocks=5", "-noparkdeepreorg", + "-limitdescendantcount=100", "-limitdescendantsize=5000", + "-limitancestorcount=100", "-limitancestorsize=5000"] # Create nodes 3 and 4 to test manual pruning (they will be re-started with manual pruning later) # Create nodes 5 to test wallet in prune mode, but do not connect self.extra_args = [self.full_node_default_args, self.full_node_default_args, - ["-maxreceivebuffer=20000", "-prune=550"], - ["-maxreceivebuffer=20000", "-blockmaxsize=999000"], - ["-maxreceivebuffer=20000", "-blockmaxsize=999000"], - ["-prune=550"]] + ["-maxreceivebuffer=20000", + "-prune=550", "-noparkdeepreorg"], + ["-maxreceivebuffer=20000", + "-blockmaxsize=999000", "-noparkdeepreorg"], + ["-maxreceivebuffer=20000", + "-blockmaxsize=999000", "-noparkdeepreorg"], + ["-prune=550", "-noparkdeepreorg"]] def setup_network(self): self.setup_nodes() @@ -146,7 +151,8 @@ # transactions (from disconnected blocks) self.stop_node(1) self.start_node(1, extra_args=[ - "-maxreceivebuffer=20000", "-blockmaxsize=5000", "-checkblocks=5", "-disablesafemode"]) + "-maxreceivebuffer=20000", "-blockmaxsize=5000", "-checkblocks=5", + "-disablesafemode", "-noparkdeepreorg"]) height = self.nodes[1].getblockcount() self.log.info("Current block height: %d" % height) @@ -172,7 +178,8 @@ # Reboot node1 to clear those giant tx's from mempool self.stop_node(1) self.start_node(1, extra_args=[ - "-maxreceivebuffer=20000", "-blockmaxsize=5000", "-checkblocks=5", "-disablesafemode"]) + "-maxreceivebuffer=20000", "-blockmaxsize=5000", "-checkblocks=5", + "-disablesafemode", "-noparkdeepreorg"]) self.log.info("Generating new longer chain of 300 more blocks") self.nodes[1].generate(300) @@ -364,7 +371,7 @@ # check that the pruning node's wallet is still in good shape self.log.info("Stop and start pruning node to trigger wallet rescan") self.stop_node(2) - self.start_node(2, extra_args=["-prune=550"]) + self.start_node(2, extra_args=["-prune=550", "-noparkdeepreorg"]) self.log.info("Success") # check that wallet loads loads successfully when restarting a pruned node after IBD. @@ -374,7 +381,7 @@ nds = [self.nodes[0], self.nodes[5]] sync_blocks(nds, wait=5, timeout=300) self.stop_node(5) # stop and start to trigger rescan - self.start_node(5, extra_args=["-prune=550"]) + self.start_node(5, extra_args=["-prune=550", "-noparkdeepreorg"]) self.log.info("Success") def run_test(self): diff --git a/test/functional/sendheaders.py b/test/functional/sendheaders.py --- a/test/functional/sendheaders.py +++ b/test/functional/sendheaders.py @@ -182,6 +182,7 @@ def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 + self.extra_args = [["-noparkdeepreorg"], ["-noparkdeepreorg"]] # mine count blocks and return the new tip def mine_blocks(self, count): diff --git a/test/functional/txn_clone.py b/test/functional/txn_clone.py --- a/test/functional/txn_clone.py +++ b/test/functional/txn_clone.py @@ -11,6 +11,7 @@ class TxnMallTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 + self.extra_args = [["-noparkdeepreorg"], ["-noparkdeepreorg"], [], []] def add_options(self, parser): parser.add_argument("--mineblock", dest="mine_block", default=False, action="store_true", diff --git a/test/functional/txn_doublespend.py b/test/functional/txn_doublespend.py --- a/test/functional/txn_doublespend.py +++ b/test/functional/txn_doublespend.py @@ -11,6 +11,7 @@ class TxnMallTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 4 + self.extra_args = [["-noparkdeepreorg"], ["-noparkdeepreorg"], [], []] def add_options(self, parser): parser.add_argument("--mineblock", dest="mine_block", default=False, action="store_true",