Skip to content

Commit

Permalink
Add an RPC to finalize a block
Browse files Browse the repository at this point in the history
Summary:
A finalized block cannot be reorged. It effectively behaves very similarly to a checkpoint.

Based on work from @dagurval

Depends on D2057

Test Plan:
  ./test/functional/test_runner.py abc-finalize-block

Reviewers: #bitcoin_abc, schancel, jasonbcox

Reviewed By: #bitcoin_abc, schancel, jasonbcox

Subscribers: sickpig, dagurval, jasonbcox, schancel

Differential Revision: https://reviews.bitcoinabc.org/D2083
  • Loading branch information
deadalnix committed Nov 20, 2018
1 parent 45ca215 commit 59ee4de
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 4 deletions.
7 changes: 7 additions & 0 deletions src/chain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,10 @@ const CBlockIndex *LastCommonAncestor(const CBlockIndex *pa,
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;
}
5 changes: 5 additions & 0 deletions src/chain.h
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ int64_t GetBlockProofEquivalentTime(const CBlockIndex &to,
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:
Expand Down
41 changes: 41 additions & 0 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,46 @@ UniValue preciousblock(const Config &config, const JSONRPCRequest &request) {
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(
Expand Down Expand Up @@ -1787,6 +1827,7 @@ static const ContextFreeRPCCommand commands[] = {
{ "blockchain", "preciousblock", preciousblock, {"blockhash"} },

/* Not shown in help */
{ "hidden", "finalizeblock", finalizeblock, {"blockhash"} },
{ "hidden", "invalidateblock", invalidateblock, {"blockhash"} },
{ "hidden", "parkblock", parkblock, {"blockhash"} },
{ "hidden", "reconsiderblock", reconsiderblock, {"blockhash"} },
Expand Down
60 changes: 59 additions & 1 deletion src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ namespace {
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
Expand Down Expand Up @@ -2192,6 +2198,11 @@ static bool DisconnectTip(const Config &config, CValidationState &state,
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
Expand Down Expand Up @@ -2374,6 +2385,7 @@ static bool ConnectTip(const Config &config, CValidationState &state,
* invalid (it's however far from certain to be valid).
*/
static CBlockIndex *FindMostWorkChain() {
AssertLockHeld(cs_main);
do {
CBlockIndex *pindexNew = nullptr;

Expand All @@ -2387,6 +2399,16 @@ static CBlockIndex *FindMostWorkChain() {
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
Expand Down Expand Up @@ -2830,6 +2852,43 @@ static bool UnwindBlock(const Config &config, CValidationState &state,
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);
Expand Down Expand Up @@ -3537,7 +3596,6 @@ static bool AcceptBlockHeader(const Config &config, const CBlockHeader &block,
}

CheckBlockIndex(chainparams.GetConsensus());

return true;
}

Expand Down
15 changes: 12 additions & 3 deletions src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,13 @@ CBlockIndex *FindForkInGlobalIndex(const CChain &chain,
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);
Expand Down Expand Up @@ -677,9 +684,9 @@ int32_t ComputeBlockVersion(const CBlockIndex *pindexPrev,
const Consensus::Params &params);

/**
* 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 */
Expand All @@ -688,6 +695,8 @@ static const unsigned int REJECT_HIGHFEE = 0x100;
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);
Expand Down
80 changes: 80 additions & 0 deletions test/functional/abc-finalize-block.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 59ee4de

Please sign in to comment.