diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -6,3 +6,5 @@ This release includes the following features and fixes: - Improve avalanche node stability under rare network conditions. + - The new `isfinalblock` and `isfinaltransaction` RPCs can be used to check if + a block or a transaction has been finalized by the avalanche voting. diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -5200,11 +5200,18 @@ return; } } break; - case avalanche::VoteStatus::Accepted: - case avalanche::VoteStatus::Finalized: { + case avalanche::VoteStatus::Accepted: { LOCK(cs_main); m_chainman.ActiveChainstate().UnparkBlock(pindex); } break; + case avalanche::VoteStatus::Finalized: { + { + LOCK(cs_main); + m_chainman.ActiveChainstate().UnparkBlock(pindex); + } + m_chainman.ActiveChainstate().AvalancheFinalizeBlock( + pindex); + } break; case avalanche::VoteStatus::Stale: // Fall back on Nakamoto consensus in the absence of // Avalanche votes for other competing or descendant diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -985,6 +986,106 @@ }; } +static RPCHelpMan isfinalblock() { + return RPCHelpMan{ + "isfinalblock", + "Check if a block has been finalized by avalanche votes.\n", + { + {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, + "The hash of the block."}, + }, + RPCResult{RPCResult::Type::BOOL, "success", + "Whether the block has been finalized by avalanche votes."}, + RPCExamples{HelpExampleRpc("isfinalblock", "") + + HelpExampleCli("isfinalblock", "")}, + [&](const RPCHelpMan &self, const Config &config, + const JSONRPCRequest &request) -> UniValue { + if (!g_avalanche) { + throw JSONRPCError(RPC_INTERNAL_ERROR, + "Avalanche is not initialized"); + } + + ChainstateManager &chainman = EnsureAnyChainman(request.context); + const BlockHash blockhash( + ParseHashV(request.params[0], "blockhash")); + const CBlockIndex *pindex; + + { + LOCK(cs_main); + pindex = chainman.m_blockman.LookupBlockIndex(blockhash); + + if (!pindex) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Block not found"); + } + } + + return chainman.ActiveChainstate().IsBlockAvalancheFinalized( + pindex); + }, + }; +} + +static RPCHelpMan isfinaltransaction() { + return RPCHelpMan{ + "isfinaltransaction", + "Check if a transaction has been finalized by avalanche votes.\n", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, + "The id of the transaction."}, + {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, + "The block in which to look for the transaction"}, + }, + RPCResult{ + RPCResult::Type::BOOL, "success", + "Whether the transaction has been finalized by avalanche votes."}, + RPCExamples{HelpExampleRpc("isfinaltransaction", " ") + + HelpExampleCli("isfinaltransaction", " ")}, + [&](const RPCHelpMan &self, const Config &config, + const JSONRPCRequest &request) -> UniValue { + if (!g_avalanche) { + throw JSONRPCError(RPC_INTERNAL_ERROR, + "Avalanche is not initialized"); + } + + const NodeContext &node = EnsureAnyNodeContext(request.context); + ChainstateManager &chainman = EnsureChainman(node); + const TxId txid = TxId(ParseHashV(request.params[0], "txid")); + CBlockIndex *pindex = nullptr; + + if (!request.params[1].isNull()) { + const BlockHash blockhash( + ParseHashV(request.params[1], "blockhash")); + + LOCK(cs_main); + pindex = chainman.m_blockman.LookupBlockIndex(blockhash); + if (!pindex) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Block not found"); + } + } + + if (g_txindex && !pindex) { + g_txindex->BlockUntilSyncedToCurrentChain(); + } + + BlockHash hash_block; + const CTransactionRef tx = GetTransaction( + pindex, node.mempool.get(), txid, + config.GetChainParams().GetConsensus(), hash_block); + + if (!pindex) { + LOCK(cs_main); + pindex = chainman.m_blockman.LookupBlockIndex(hash_block); + } + + return tx != nullptr && !node.mempool->exists(txid) && + chainman.ActiveChainstate().IsBlockAvalancheFinalized( + pindex); + }, + }; +} + static RPCHelpMan sendavalancheproof() { return RPCHelpMan{ "sendavalancheproof", @@ -1098,6 +1199,8 @@ { "avalanche", getavalancheinfo, }, { "avalanche", getavalanchepeerinfo, }, { "avalanche", getrawavalancheproof, }, + { "avalanche", isfinalblock, }, + { "avalanche", isfinaltransaction, }, { "avalanche", sendavalancheproof, }, { "avalanche", verifyavalancheproof, }, { "avalanche", verifyavalanchedelegation, }, diff --git a/src/validation.h b/src/validation.h --- a/src/validation.h +++ b/src/validation.h @@ -802,6 +802,15 @@ */ const CBlockIndex *m_finalizedBlockIndex GUARDED_BY(cs_main) = nullptr; + mutable Mutex cs_avalancheFinalizedBlockIndex; + + /** + * The best block via avalanche voting. + * This block cannot be reorged in any way except by explicit user action. + */ + const CBlockIndex *m_avalancheFinalizedBlockIndex + GUARDED_BY(cs_avalancheFinalizedBlockIndex) = nullptr; + public: //! Reference to a BlockManager instance which itself is shared across all //! CChainState instances. @@ -981,6 +990,16 @@ bool IsBlockFinalized(const CBlockIndex *pindex) const EXCLUSIVE_LOCKS_REQUIRED(cs_main); + /** + * Mark a block as finalized by avalanche. + */ + bool AvalancheFinalizeBlock(CBlockIndex *pindex); + + /** + * Checks if a block is finalized by avalanche voting. + */ + bool IsBlockAvalancheFinalized(const CBlockIndex *pindex) const; + /** Remove invalidity status from a block and its descendants. */ void ResetBlockFailureFlags(CBlockIndex *pindex) EXCLUSIVE_LOCKS_REQUIRED(cs_main); diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3489,6 +3489,35 @@ return m_finalizedBlockIndex; } +bool CChainState::AvalancheFinalizeBlock(CBlockIndex *pindex) { + if (!pindex) { + return false; + } + + if (!m_chain.Contains(pindex)) { + LogPrint(BCLog::AVALANCHE, + "The block to mark finalized by avalanche is not on the " + "active chain: %s\n", + pindex->GetBlockHash().ToString()); + return false; + } + + if (IsBlockAvalancheFinalized(pindex)) { + return true; + } + + LOCK(cs_avalancheFinalizedBlockIndex); + m_avalancheFinalizedBlockIndex = pindex; + return true; +} + +bool CChainState::IsBlockAvalancheFinalized(const CBlockIndex *pindex) const { + LOCK(cs_avalancheFinalizedBlockIndex); + return pindex && m_avalancheFinalizedBlockIndex && + m_avalancheFinalizedBlockIndex->GetAncestor(pindex->nHeight) == + pindex; +} + CBlockIndex *BlockManager::AddToBlockIndex(const CBlockHeader &block) { AssertLockHeld(cs_main); diff --git a/test/functional/abc_rpc_isfinal.py b/test/functional/abc_rpc_isfinal.py new file mode 100755 --- /dev/null +++ b/test/functional/abc_rpc_isfinal.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 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 isfinalxxx RPCS.""" +import random + +from test_framework.address import ADDRESS_ECREG_UNSPENDABLE +from test_framework.avatools import AvaP2PInterface +from test_framework.messages import AvalancheVote, AvalancheVoteError +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_raises_rpc_error, uint256_hex + +QUORUM_NODE_COUNT = 16 + + +class AvalancheIsFinalTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [ + [ + '-avalanche=1', + '-avaproofstakeutxodustthreshold=1000000', + '-avaproofstakeutxoconfirmations=1', + '-avacooldown=0', + '-avaminquorumstake=0', + '-avaminavaproofsnodecount=0', + ] + ] + + def run_test(self): + node = self.nodes[0] + + # Build a fake quorum of nodes. + def get_quorum(): + return [node.add_p2p_connection(AvaP2PInterface(node)) + for _ in range(0, QUORUM_NODE_COUNT)] + + # Pick on node from the quorum for polling. + quorum = get_quorum() + + def is_quorum_established(): + return node.getavalancheinfo()['ready_to_poll'] is True + self.wait_until(is_quorum_established) + + def can_find_block_in_poll( + blockhash, resp=AvalancheVoteError.ACCEPTED): + found_hash = False + for n in quorum: + poll = n.get_avapoll_if_available() + + # That node has not received a poll + if poll is None: + continue + + # We got a poll, check for the hash and repond + votes = [] + for inv in poll.invs: + # Vote yes to everything + r = AvalancheVoteError.ACCEPTED + + # Look for what we expect + if inv.hash == int(blockhash, 16): + r = resp + found_hash = True + + votes.append(AvalancheVote(r, inv.hash)) + + n.send_avaresponse(poll.round, votes, n.delegated_privkey) + + return found_hash + + blockhash = node.generate(1)[0] + cb_txid = node.getblock(blockhash)['tx'][0] + assert not node.isfinalblock(blockhash) + assert not node.isfinaltransaction(cb_txid, blockhash) + + def is_finalblock(blockhash): + can_find_block_in_poll(blockhash) + return node.isfinalblock(blockhash) + + with node.assert_debug_log([f"Avalanche finalized block {blockhash}"]): + self.wait_until(lambda: is_finalblock(blockhash)) + assert node.isfinaltransaction(cb_txid, blockhash) + + self.log.info("Check block ancestors are finalized as well") + tip_height = node.getblockheader(blockhash)['height'] + for height in range(0, tip_height): + hash = node.getblockhash(height) + assert node.isfinalblock(hash) + txid = node.getblock(hash)['tx'][0] + assert node.isfinaltransaction(txid, hash) + + if self.is_wallet_compiled(): + self.log.info("Check mempool transactions are not finalized") + # Mature some utxos + node.generate(100) + wallet_txid = node.sendtoaddress( + ADDRESS_ECREG_UNSPENDABLE, 1_000_000) + assert wallet_txid in node.getrawmempool() + assert not node.isfinaltransaction( + wallet_txid, node.getbestblockhash()) + + self.log.info( + "A transaction is only finalized if the containing block is finalized") + tip = node.generate(1)[0] + assert wallet_txid not in node.getrawmempool() + assert not node.isfinaltransaction(wallet_txid, tip) + self.wait_until(lambda: is_finalblock(tip)) + assert node.isfinaltransaction(wallet_txid, tip) + # Needs -txindex + assert not node.isfinaltransaction(wallet_txid) + + self.log.info( + "Repeat with -txindex so we don't need the blockhash") + self.restart_node(0, self.extra_args[0] + ['-txindex']) + + quorum = get_quorum() + self.wait_until(is_quorum_established) + self.wait_until(lambda: node.getindexinfo()[ + 'txindex']['synced'] is True) + + self.wait_until(lambda: is_finalblock(tip)) + assert node.isfinaltransaction(wallet_txid) + + wallet_txid = node.sendtoaddress( + ADDRESS_ECREG_UNSPENDABLE, 1_000_000) + assert wallet_txid in node.getrawmempool() + assert not node.isfinaltransaction(wallet_txid) + + assert not node.isfinaltransaction( + uint256_hex(random.randint(0, 2**256 - 1))) + + self.log.info("Check unknown item") + for _ in range(10): + assert_raises_rpc_error( + -8, + "Block not found", + node.isfinalblock, + uint256_hex(random.randint(0, 2**256 - 1)), + ) + assert_raises_rpc_error( + -8, + "Block not found", + node.isfinaltransaction, + uint256_hex(random.randint(0, 2**256 - 1)), + uint256_hex(random.randint(0, 2**256 - 1)), + ) + + +if __name__ == '__main__': + AvalancheIsFinalTest().main()