diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index ee1a9fbaca..f03de5f8ed 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -151,6 +151,7 @@ testScripts = [ 'invalidtxrequest.py', # NOTE: needs dash_hash to pass 'abandonconflict.py', 'p2p-versionbits-warning.py', + 'preciousblock.py', 'importprunedfunds.py', 'signmessages.py', ] diff --git a/qa/rpc-tests/preciousblock.py b/qa/rpc-tests/preciousblock.py new file mode 100755 index 0000000000..854dcc7251 --- /dev/null +++ b/qa/rpc-tests/preciousblock.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# +# Test PreciousBlock code +# + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + +def unidirectional_node_sync_via_rpc(node_src, node_dest): + blocks_to_copy = [] + blockhash = node_src.getbestblockhash() + while True: + try: + assert(len(node_dest.getblock(blockhash, False)) > 0) + break + except: + blocks_to_copy.append(blockhash) + blockhash = node_src.getblockheader(blockhash, True)['previousblockhash'] + blocks_to_copy.reverse() + for blockhash in blocks_to_copy: + blockdata = node_src.getblock(blockhash, False) + assert(node_dest.submitblock(blockdata) in (None, 'inconclusive')) + +def node_sync_via_rpc(nodes): + for node_src in nodes: + for node_dest in nodes: + if node_src is node_dest: + continue + unidirectional_node_sync_via_rpc(node_src, node_dest) + +class PreciousTest(BitcoinTestFramework): + def setup_chain(self): + print("Initializing test directory "+self.options.tmpdir) + initialize_chain_clean(self.options.tmpdir, 3) + + def setup_network(self): + self.nodes = [] + self.is_network_split = False + self.nodes.append(start_node(0, self.options.tmpdir, ["-debug"])) + self.nodes.append(start_node(1, self.options.tmpdir, ["-debug"])) + self.nodes.append(start_node(2, self.options.tmpdir, ["-debug"])) + + def run_test(self): + print("Ensure submitblock can in principle reorg to a competing chain") + self.nodes[0].generate(1) + assert(self.nodes[0].getblockcount() == 1) + (hashY, hashZ) = self.nodes[1].generate(2) + assert(self.nodes[1].getblockcount() == 2) + node_sync_via_rpc(self.nodes[0:3]) + assert(self.nodes[0].getbestblockhash() == hashZ) + + print("Mine blocks A-B-C on Node 0") + (hashA, hashB, hashC) = self.nodes[0].generate(3) + assert(self.nodes[0].getblockcount() == 5) + print("Mine competing blocks E-F-G on Node 1") + (hashE, hashF, hashG) = self.nodes[1].generate(3) + assert(self.nodes[1].getblockcount() == 5) + assert(hashC != hashG) + print("Connect nodes and check no reorg occurs") + # Submit competing blocks via RPC so any reorg should occur before we proceed (no way to wait on inaction for p2p sync) + node_sync_via_rpc(self.nodes[0:2]) + connect_nodes_bi(self.nodes,0,1) + assert(self.nodes[0].getbestblockhash() == hashC) + assert(self.nodes[1].getbestblockhash() == hashG) + print("Make Node0 prefer block G") + self.nodes[0].preciousblock(hashG) + assert(self.nodes[0].getbestblockhash() == hashG) + print("Make Node0 prefer block C again") + self.nodes[0].preciousblock(hashC) + assert(self.nodes[0].getbestblockhash() == hashC) + print("Make Node1 prefer block C") + self.nodes[1].preciousblock(hashC) + sync_chain(self.nodes[0:2]) # wait because node 1 may not have downloaded hashC + assert(self.nodes[1].getbestblockhash() == hashC) + print("Make Node1 prefer block G again") + self.nodes[1].preciousblock(hashG) + assert(self.nodes[1].getbestblockhash() == hashG) + print("Make Node0 prefer block G again") + self.nodes[0].preciousblock(hashG) + assert(self.nodes[0].getbestblockhash() == hashG) + print("Make Node1 prefer block C again") + self.nodes[1].preciousblock(hashC) + assert(self.nodes[1].getbestblockhash() == hashC) + print("Mine another block (E-F-G-)H on Node 0 and reorg Node 1") + self.nodes[0].generate(1) + assert(self.nodes[0].getblockcount() == 6) + sync_blocks(self.nodes[0:2]) + hashH = self.nodes[0].getbestblockhash() + assert(self.nodes[1].getbestblockhash() == hashH) + print("Node1 should not be able to prefer block C anymore") + self.nodes[1].preciousblock(hashC) + assert(self.nodes[1].getbestblockhash() == hashH) + print("Mine competing blocks I-J-K-L on Node 2") + self.nodes[2].generate(4) + assert(self.nodes[2].getblockcount() == 6) + hashL = self.nodes[2].getbestblockhash() + print("Connect nodes and check no reorg occurs") + node_sync_via_rpc(self.nodes[0:3]) + connect_nodes_bi(self.nodes,1,2) + connect_nodes_bi(self.nodes,0,2) + assert(self.nodes[0].getbestblockhash() == hashH) + assert(self.nodes[1].getbestblockhash() == hashH) + assert(self.nodes[2].getbestblockhash() == hashL) + print("Make Node1 prefer block L") + self.nodes[1].preciousblock(hashL) + assert(self.nodes[1].getbestblockhash() == hashL) + print("Make Node2 prefer block H") + self.nodes[2].preciousblock(hashH) + assert(self.nodes[2].getbestblockhash() == hashH) + +if __name__ == '__main__': + PreciousTest().main() diff --git a/qa/rpc-tests/test_framework/util.py b/qa/rpc-tests/test_framework/util.py index f904df79fe..dff8b851a1 100644 --- a/qa/rpc-tests/test_framework/util.py +++ b/qa/rpc-tests/test_framework/util.py @@ -147,6 +147,16 @@ def sync_blocks(rpc_connections, wait=1, timeout=60): maxheight = max(heights) raise AssertionError("Block sync failed") +def sync_chain(rpc_connections, wait=1): + """ + Wait until everybody has the same best block + """ + while True: + counts = [ x.getbestblockhash() for x in rpc_connections ] + if counts == [ counts[0] ]*len(counts): + break + time.sleep(wait) + def sync_mempools(rpc_connections, wait=1, timeout=60): """ Wait until everybody has the same transactions in their memory diff --git a/src/chain.h b/src/chain.h index 79a81f535c..2d7c439fcb 100644 --- a/src/chain.h +++ b/src/chain.h @@ -198,7 +198,7 @@ public: unsigned int nNonce; //! (memory only) Sequential id assigned to distinguish order in which blocks are received. - uint32_t nSequenceId; + int32_t nSequenceId; void SetNull() { diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index f9cc0a46a9..0c94fdf8b8 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1410,6 +1410,44 @@ UniValue getmempoolinfo(const UniValue& params, bool fHelp) return mempoolInfoToJSON(); } +UniValue preciousblock(const UniValue& params, bool fHelp) +{ + if (fHelp || params.size() != 1) + throw runtime_error( + "preciousblock \"hash\"\n" + "\nTreats a block as if it were received before others with the same work.\n" + "\nA later preciousblock call can override the effect of an earlier one.\n" + "\nThe effects of preciousblock are not retained across restarts.\n" + "\nArguments:\n" + "1. hash (string, required) the hash of the block to mark as precious\n" + "\nResult:\n" + "\nExamples:\n" + + HelpExampleCli("preciousblock", "\"blockhash\"") + + HelpExampleRpc("preciousblock", "\"blockhash\"") + ); + + std::string strHash = params[0].get_str(); + uint256 hash(uint256S(strHash)); + CBlockIndex* pblockindex; + + { + LOCK(cs_main); + if (mapBlockIndex.count(hash) == 0) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); + + pblockindex = mapBlockIndex[hash]; + } + + CValidationState state; + PreciousBlock(state, Params(), pblockindex); + + if (!state.IsValid()) { + throw JSONRPCError(RPC_DATABASE_ERROR, state.GetRejectReason()); + } + + return NullUniValue; +} + UniValue invalidateblock(const UniValue& params, bool fHelp) { if (fHelp || params.size() != 1) @@ -1507,6 +1545,8 @@ static const CRPCCommand commands[] = { "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true }, { "blockchain", "verifychain", &verifychain, true }, + { "blockchain", "preciousblock", &preciousblock, true }, + /* Not shown in help */ { "hidden", "invalidateblock", &invalidateblock, true }, { "hidden", "reconsiderblock", &reconsiderblock, true }, diff --git a/src/validation.cpp b/src/validation.cpp index 91fe0618e7..e3cf8b8ae4 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -170,7 +170,11 @@ namespace { */ CCriticalSection cs_nBlockSequenceId; /** Blocks loaded from disk are assigned id 0, so start the counter at 1. */ - uint32_t nBlockSequenceId = 1; + int32_t nBlockSequenceId = 1; + /** Decreasing counter (used by subsequent preciousblock calls). */ + int32_t nBlockReverseSequenceId = -1; + /** chainwork for the last block that preciousblock has been applied to. */ + arith_uint256 nLastPreciousChainwork = 0; /** Dirty block index entries. */ set setDirtyBlockIndex; @@ -2924,6 +2928,36 @@ bool ActivateBestChain(CValidationState &state, const CChainParams& chainparams, return true; } + +bool PreciousBlock(CValidationState& state, const CChainParams& params, CBlockIndex *pindex) +{ + { + LOCK(cs_main); + if (pindex->nChainWork < chainActive.Tip()->nChainWork) { + // Nothing to do, this block is not at the tip. + return true; + } + if (chainActive.Tip()->nChainWork > nLastPreciousChainwork) { + // The chain has been extended since the last call, reset the counter. + nBlockReverseSequenceId = -1; + } + nLastPreciousChainwork = chainActive.Tip()->nChainWork; + setBlockIndexCandidates.erase(pindex); + pindex->nSequenceId = nBlockReverseSequenceId; + if (nBlockReverseSequenceId > std::numeric_limits::min()) { + // We can't keep reducing the counter if somebody really wants to + // call preciousblock 2**31-1 times on the same set of tips... + nBlockReverseSequenceId--; + } + if (pindex->IsValid(BLOCK_VALID_TRANSACTIONS) && pindex->nChainTx) { + setBlockIndexCandidates.insert(pindex); + PruneBlockIndexCandidates(); + } + } + + return ActivateBestChain(state, params); +} + bool InvalidateBlock(CValidationState& state, const CChainParams& chainparams, CBlockIndex *pindex) { AssertLockHeld(cs_main); @@ -4254,7 +4288,7 @@ void static CheckBlockIndex(const Consensus::Params& consensusParams) assert(pindex->GetBlockHash() == consensusParams.hashGenesisBlock); // Genesis block's hash must match. assert(pindex == chainActive.Genesis()); // The current active chain's genesis block must be this block. } - if (pindex->nChainTx == 0) assert(pindex->nSequenceId == 0); // nSequenceId can't be set for blocks that aren't linked + if (pindex->nChainTx == 0) assert(pindex->nSequenceId <= 0); // nSequenceId can't be set positive for blocks that aren't linked (negative is used for preciousblock) // VALID_TRANSACTIONS is equivalent to nTx > 0 for all nodes (whether or not pruning has occurred). // HAVE_DATA is only equivalent to nTx > 0 (or VALID_TRANSACTIONS) if no pruning has occurred. if (!fHavePruned) { diff --git a/src/validation.h b/src/validation.h index 222199a09d..d2ebc11c26 100644 --- a/src/validation.h +++ b/src/validation.h @@ -492,6 +492,9 @@ public: /** Find the last common block between the parameter chain and a locator. */ CBlockIndex* FindForkInGlobalIndex(const CChain& chain, const CBlockLocator& locator); +/** Mark a block as precious and reorganize. */ +bool PreciousBlock(CValidationState& state, const CChainParams& params, CBlockIndex *pindex); + /** Mark a block as invalid. */ bool InvalidateBlock(CValidationState& state, const CChainParams& chainparams, CBlockIndex *pindex);