diff --git a/src/chain.h b/src/chain.h --- a/src/chain.h +++ b/src/chain.h @@ -262,6 +262,11 @@ const CBlockIndex *LastCommonAncestor(const CBlockIndex *pa, const CBlockIndex *pb); +/** + * Check if two block index are on the same fork. + */ +bool AreOnTheSameFork(const CBlockIndex *pa, const CBlockIndex *pb); + /** Used to marshal pointers into hashes for db storage. */ class CDiskBlockIndex : public CBlockIndex { public: diff --git a/src/chain.cpp b/src/chain.cpp --- a/src/chain.cpp +++ b/src/chain.cpp @@ -184,3 +184,10 @@ assert(pa == pb); return pa; } + +bool AreOnTheSameFork(const CBlockIndex *pa, const CBlockIndex *pb) { + // The common ancestor needs to be either pa (pb is a child of pa) or pb (pa + // is a child of pb). + const CBlockIndex *pindexCommon = LastCommonAncestor(pa, pb); + return pindexCommon == pa || pindexCommon == pb; +} diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1506,6 +1506,46 @@ return NullUniValue; } +UniValue finalizeblock(const Config &config, const JSONRPCRequest &request) { + if (request.fHelp || request.params.size() != 1) { + throw std::runtime_error( + "finalizeblock \"blockhash\"\n" + + "\nTreats a block as final. It cannot be reorged. Any chain\n" + "that does not contain this block is invalid. Used on a less\n" + "work chain, it can effectively PUTS YOU OUT OF CONSENSUS.\n" + "USE WITH CAUTION!\n" + "\nResult:\n" + "\nExamples:\n" + + HelpExampleCli("finalizeblock", "\"blockhash\"") + + HelpExampleRpc("finalizeblock", "\"blockhash\"")); + } + + std::string strHash = request.params[0].get_str(); + uint256 hash(uint256S(strHash)); + CValidationState state; + + { + LOCK(cs_main); + if (mapBlockIndex.count(hash) == 0) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); + } + + CBlockIndex *pblockindex = mapBlockIndex[hash]; + FinalizeBlock(config, state, pblockindex); + } + + if (state.IsValid()) { + ActivateBestChain(config, state); + } + + if (!state.IsValid()) { + throw JSONRPCError(RPC_DATABASE_ERROR, state.GetRejectReason()); + } + + return NullUniValue; +} + UniValue invalidateblock(const Config &config, const JSONRPCRequest &request) { if (request.fHelp || request.params.size() != 1) { throw std::runtime_error( @@ -1787,6 +1827,7 @@ { "blockchain", "preciousblock", preciousblock, {"blockhash"} }, /* Not shown in help */ + { "hidden", "finalizeblock", finalizeblock, {"blockhash"} }, { "hidden", "invalidateblock", invalidateblock, {"blockhash"} }, { "hidden", "parkblock", parkblock, {"blockhash"} }, { "hidden", "reconsiderblock", reconsiderblock, {"blockhash"} }, diff --git a/src/validation.h b/src/validation.h --- a/src/validation.h +++ b/src/validation.h @@ -625,6 +625,13 @@ bool PreciousBlock(const Config &config, CValidationState &state, CBlockIndex *pindex); +/** + * Mark a block as finalized. + * A finalized block can not be reorged in any way. + */ +bool FinalizeBlock(const Config &config, CValidationState &state, + CBlockIndex *pindex); + /** Mark a block as invalid. */ bool InvalidateBlock(const Config &config, CValidationState &state, CBlockIndex *pindex); @@ -677,9 +684,9 @@ const Consensus::Params ¶ms); /** - * Reject codes greater or equal to this can be returned by AcceptToMemPool for - * transactions, to signal internal conditions. They cannot and should not be - * sent over the P2P network. + * Reject codes greater or equal to this can be returned by AcceptToMemPool or + * AcceptBlock for blocks/transactions, to signal internal conditions. They + * cannot and should not be sent over the P2P network. */ static const unsigned int REJECT_INTERNAL = 0x100; /** Too high fee. Can not be triggered by P2P transactions */ @@ -688,6 +695,8 @@ static const unsigned int REJECT_ALREADY_KNOWN = 0x101; /** Transaction conflicts with a transaction already known */ static const unsigned int REJECT_CONFLICT = 0x102; +/** Block conflicts with a transaction already known */ +static const unsigned int REJECT_AGAINST_FINALIZED = 0x103; /** Get block file info entry for one block file */ CBlockFileInfo *GetBlockFileInfo(size_t n); diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -101,6 +101,12 @@ CBlockIndex *pindexBestInvalid; CBlockIndex *pindexBestParked; +/** + * The best finalized block. + * This block cannot be reorged in any way, shape or form. + */ +CBlockIndex const *pindexFinalized; + /** * The set of all CBlockIndex entries with BLOCK_VALID_TRANSACTIONS (for itself * and all ancestors) and as good as our current tip or better. Entries may be @@ -2192,6 +2198,11 @@ disconnectpool->addForBlock(block.vtx); } + // If the tip is finalized, then undo it. + if (pindexFinalized == pindexDelete) { + pindexFinalized = pindexDelete->pprev; + } + // Update chainActive and related variables. UpdateTip(config, pindexDelete->pprev); // Let wallets know transactions went from 1-confirmed to @@ -2374,6 +2385,7 @@ * invalid (it's however far from certain to be valid). */ static CBlockIndex *FindMostWorkChain() { + AssertLockHeld(cs_main); do { CBlockIndex *pindexNew = nullptr; @@ -2387,6 +2399,16 @@ pindexNew = *it; } + // If this block will cause a finalized block to be reorged, then we + // mark it as invalid. + if (pindexFinalized && !AreOnTheSameFork(pindexNew, pindexFinalized)) { + LogPrintf("Mark block %s invalid because it forks prior to the " + "finalization point %d.\n", + pindexNew->GetBlockHash().ToString(), + pindexFinalized->nHeight); + pindexNew->nStatus = pindexNew->nStatus.withFailed(); + } + const CBlockIndex *pindexFork = chainActive.FindFork(pindexNew); // Check whether all blocks on the path between the currently active @@ -2830,6 +2852,43 @@ return true; } +bool FinalizeBlock(const Config &config, CValidationState &state, + CBlockIndex *pindex) { + AssertLockHeld(cs_main); + if (pindex->nStatus.isInvalid()) { + // We try to finalize an invalid block. + return state.DoS(100, + error("%s: Trying to finalize invalid block %s", + __func__, pindex->GetBlockHash().ToString()), + REJECT_INVALID, "finalize-invalid-block"); + } + + // Check that the request is consistent with current finalization. + if (pindexFinalized && !AreOnTheSameFork(pindex, pindexFinalized)) { + return state.DoS( + 20, error("%s: Trying to finalize block %s which conflicts " + "with already finalized block", + __func__, pindex->GetBlockHash().ToString()), + REJECT_AGAINST_FINALIZED, "bad-fork-prior-finalized"); + } + + // We have a valid candidate, make sure it is not parked. + pindexFinalized = pindex; + if (pindex->nStatus.isOnParkedChain()) { + UnparkBlock(pindex); + } + + // If the finalized block is not on the active chain, we need to rewind. + if (!AreOnTheSameFork(pindex, chainActive.Tip())) { + const CBlockIndex *pindexFork = chainActive.FindFork(pindex); + CBlockIndex *pindexToInvalidate = + chainActive.Tip()->GetAncestor(pindexFork->nHeight + 1); + return InvalidateBlock(config, state, pindexToInvalidate); + } + + return true; +} + bool InvalidateBlock(const Config &config, CValidationState &state, CBlockIndex *pindex) { return UnwindBlock(config, state, pindex, true); @@ -3537,7 +3596,6 @@ } CheckBlockIndex(chainparams.GetConsensus()); - return true; } diff --git a/test/functional/abc-finalize-block.py b/test/functional/abc-finalize-block.py new file mode 100755 --- /dev/null +++ b/test/functional/abc-finalize-block.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test the finalizeblock RPC calls.""" +import os + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error, connect_nodes_bi, sync_blocks, wait_until + +RPC_FINALIZE_INVALID_BLOCK_ERROR = 'finalize-invalid-block' + + +class FinalizeBlockTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + + # There should only be one chaintip, which is expected_tip + def only_valid_tip(self, expected_tip, other_tip_status=None): + node = self.nodes[0] + assert_equal(node.getbestblockhash(), expected_tip) + for tip in node.getchaintips(): + if tip["hash"] == expected_tip: + assert_equal(tip["status"], "active") + else: + assert_equal(tip["status"], other_tip_status) + + def run_test(self): + node = self.nodes[0] + + self.log.info("Test block finalization...") + node.generate(10) + tip = node.getbestblockhash() + node.finalizeblock(tip) + assert_equal(node.getbestblockhash(), tip) + + alt_node = self.nodes[1] + connect_nodes_bi(self.nodes, 0, 1) + sync_blocks(self.nodes[0:2]) + + alt_node.invalidateblock(tip) + alt_node.generate(10) + + # Wait for node 0 to invalidate the chain. + def wait_for_invalid_block(block): + def check_block(): + for tip in node.getchaintips(): + if tip["hash"] == block: + assert(tip["status"] != "active") + return tip["status"] == "invalid" + return False + wait_until(check_block) + + wait_for_invalid_block(alt_node.getbestblockhash()) + + self.log.info("Test that an invalid block cannot be finalized...") + assert_raises_rpc_error(-20, RPC_FINALIZE_INVALID_BLOCK_ERROR, + node.finalizeblock, alt_node.getbestblockhash()) + + self.log.info( + "Test that invalidating a finalized block moves the finalization backward...") + node.invalidateblock(tip) + node.reconsiderblock(tip) + assert_equal(node.getbestblockhash(), tip) + + # The node will now accept that chain as the finalized block moved back. + node.reconsiderblock(alt_node.getbestblockhash()) + assert_equal(node.getbestblockhash(), alt_node.getbestblockhash()) + + self.log.info("Trigger reorg via block finalization...") + node.finalizeblock(tip) + assert_equal(node.getbestblockhash(), tip) + + self.log.info("Try to finalized a block on a competiting fork...") + assert_raises_rpc_error(-20, RPC_FINALIZE_INVALID_BLOCK_ERROR, + node.finalizeblock, alt_node.getbestblockhash()) + + +if __name__ == '__main__': + FinalizeBlockTest().main()