diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -13,3 +13,5 @@ - Some log messages have been moved to debug categories, to improve the signal-to-noise ratio. Users relying on these log messages can enable them with the `-debug=blockstore,validation` command-line argument. + - New `getavalancheproofs` RPC to retrieve all avalanche proof IDs tracked by + the node. diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -407,6 +407,10 @@ uint64_t getSlotCount() const { return slotCount; } uint64_t getFragmentation() const { return fragmentation; } + const ProofPool &getValidProofPool() { return validProofPool; } + const ProofPool &getConflictingProofPool() { return conflictingProofPool; } + const ProofPool &getImmatureProofPool() { return immatureProofPool; } + ProofRef getProof(const ProofId &proofid) const; bool isBoundToPeer(const ProofId &proofid) const; bool isImmature(const ProofId &proofid) const; diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -914,6 +914,77 @@ }; } +static RPCHelpMan getavalancheproofs() { + return RPCHelpMan{ + "getavalancheproofs", + "Returns an object containing all tracked proofids.\n", + {}, + RPCResult{ + RPCResult::Type::OBJ, + "", + "", + { + {RPCResult::Type::ARR, + "valid", + "", + { + {RPCResult::Type::STR_HEX, "proofid", + "Avalanche proof id"}, + }}, + {RPCResult::Type::ARR, + "conflicting", + "", + { + {RPCResult::Type::STR_HEX, "proofid", + "Avalanche proof id"}, + }}, + {RPCResult::Type::ARR, + "immature", + "", + { + {RPCResult::Type::STR_HEX, "proofid", + "Avalanche proof id"}, + }}, + }, + }, + RPCExamples{HelpExampleCli("getavalancheproofs", "") + + HelpExampleRpc("getavalancheproofs", "")}, + [&](const RPCHelpMan &self, const Config &config, + const JSONRPCRequest &request) -> UniValue { + if (!g_avalanche) { + throw JSONRPCError(RPC_INTERNAL_ERROR, + "Avalanche is not initialized"); + } + + UniValue ret(UniValue::VOBJ); + g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) { + UniValue validProofIds(UniValue::VARR); + UniValue conflictingProofIds(UniValue::VARR); + UniValue immatureProofIds(UniValue::VARR); + + for (const avalanche::ProofId &proofid : + pm.getValidProofPool().getProofIds()) { + validProofIds.push_back(proofid.ToString()); + } + for (const avalanche::ProofId &proofid : + pm.getConflictingProofPool().getProofIds()) { + conflictingProofIds.push_back(proofid.ToString()); + } + for (const avalanche::ProofId &proofid : + pm.getImmatureProofPool().getProofIds()) { + immatureProofIds.push_back(proofid.ToString()); + } + + ret.pushKV("valid", validProofIds); + ret.pushKV("conflicting", conflictingProofIds); + ret.pushKV("immature", immatureProofIds); + }); + + return ret; + }, + }; +} + static RPCHelpMan getrawavalancheproof() { return RPCHelpMan{ "getrawavalancheproof", @@ -1242,6 +1313,7 @@ { "avalanche", decodeavalanchedelegation, }, { "avalanche", getavalancheinfo, }, { "avalanche", getavalanchepeerinfo, }, + { "avalanche", getavalancheproofs, }, { "avalanche", getrawavalancheproof, }, { "avalanche", isfinalblock, }, { "avalanche", isfinaltransaction, }, diff --git a/test/functional/abc_rpc_getavalancheproofs.py b/test/functional/abc_rpc_getavalancheproofs.py new file mode 100755 --- /dev/null +++ b/test/functional/abc_rpc_getavalancheproofs.py @@ -0,0 +1,183 @@ +#!/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 getavalancheproofs RPC.""" +import time + +from test_framework.avatools import ( + AvaP2PInterface, + avalanche_proof_from_hex, + create_coinbase_stakes, + gen_proof, + wait_for_proof, +) +from test_framework.messages import AvalancheProofVoteResponse, AvalancheVote +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import try_rpc, uint256_hex +from test_framework.wallet_util import bytes_to_wif + + +class GetAvalancheProofsTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.conflicting_proof_cooldown = 100 + self.extra_args = [[ + '-avalanche=1', + f'-avalancheconflictingproofcooldown={self.conflicting_proof_cooldown}', + '-avaproofstakeutxoconfirmations=2', + '-avacooldown=0', + '-avaminquorumstake=250000000', + '-avaminquorumconnectedstakeratio=0.9', + '-avaproofstakeutxodustthreshold=1000000', + '-avaminavaproofsnodecount=0', + ]] + + def run_test(self): + node = self.nodes[0] + + privkey, proof = gen_proof(node) + + # Make the proof mature + node.generate(1) + + def avalancheproofs_equals(expected): + proofs = node.getavalancheproofs() + for key, proof_list in proofs.items(): + proof_list.sort() + for key, proof_list in expected.items(): + proof_list.sort() + return proofs == expected + + self.log.info("The test node has no proof") + + assert avalancheproofs_equals({ + "valid": [], + "conflicting": [], + "immature": [], + }) + + self.log.info("The test node has a proof") + + self.restart_node(0, self.extra_args[0] + [ + '-avalanche=1', + '-avaproof={}'.format(proof.serialize().hex()), + '-avamasterkey={}'.format(bytes_to_wif(privkey.get_bytes())) + ]) + + # Before local proof is validated + assert avalancheproofs_equals({ + "valid": [], + "conflicting": [], + "immature": [], + }) + + # Mine a block to trigger proof validation + node.generate(1) + assert avalancheproofs_equals({ + "valid": [uint256_hex(proof.proofid)], + "conflicting": [], + "immature": [], + }) + + self.log.info("Connect a bunch of peers and nodes") + + mock_time = int(time.time()) + node.setmocktime(mock_time) + + privkeys = [] + proofs = [proof] + conflicting_proofs = [] + quorum = [] + N = 13 + for _ in range(N): + _privkey, _proof = gen_proof(node) + proofs.append(_proof) + privkeys.append(_privkey) + + # For each proof, also make a conflicting one + stakes = create_coinbase_stakes( + node, [node.getbestblockhash()], node.get_deterministic_priv_key().key) + conflicting_proof_hex = node.buildavalancheproof( + 10, 0, bytes_to_wif(_privkey.get_bytes()), stakes) + conflicting_proof = avalanche_proof_from_hex(conflicting_proof_hex) + conflicting_proofs.append(conflicting_proof) + + # Make the proof and its conflicting proof mature + node.generate(1) + + n = AvaP2PInterface() + n.proof = _proof + n.master_privkey = _privkey + node.add_p2p_connection(n) + quorum.append(n) + + n.send_avaproof(_proof) + wait_for_proof(node, uint256_hex(_proof.proofid)) + + mock_time += self.conflicting_proof_cooldown + node.setmocktime(mock_time) + n.send_avaproof(conflicting_proof) + + # Generate an immature proof + _, immature_proof = gen_proof(node) + n.send_avaproof(immature_proof) + + self.wait_until( + lambda: avalancheproofs_equals({ + "valid": [uint256_hex(p.proofid) for p in proofs], + "conflicting": [uint256_hex(p.proofid) for p in conflicting_proofs], + "immature": [uint256_hex(immature_proof.proofid)], + }) + ) + + self.log.info("Finalize the proofs for some peers") + + def vote_for_all_proofs(): + for i, n in enumerate(quorum): + if not n.is_connected: + continue + + poll = n.get_avapoll_if_available() + + # That node has not received a poll + if poll is None: + continue + + # Respond yes to all polls except the conflicting proofs + votes = [] + for inv in poll.invs: + response = AvalancheProofVoteResponse.ACTIVE + if inv.hash in [p.proofid for p in conflicting_proofs]: + response = AvalancheProofVoteResponse.REJECTED + + votes.append(AvalancheVote(response, inv.hash)) + + n.send_avaresponse(poll.round, votes, privkeys[i]) + + # Check if all proofs are finalized or invalidated + return all( + [node.getrawavalancheproof(uint256_hex(p.proofid)).get("finalized", False) for p in proofs] + + [try_rpc(-8, "Proof not found", node.getrawavalancheproof, + uint256_hex(c.proofid)) for c in conflicting_proofs] + ) + + # Vote until all the proofs have finalized (including ours) + expected_logs = [] + for p in proofs: + expected_logs.append( + f"Avalanche finalized proof {uint256_hex(p.proofid)}") + with node.assert_debug_log(expected_logs): + self.wait_until(lambda: vote_for_all_proofs()) + + self.wait_until( + lambda: avalancheproofs_equals({ + "valid": [uint256_hex(p.proofid) for p in proofs], + "conflicting": [], + "immature": [uint256_hex(immature_proof.proofid)], + }) + ) + + +if __name__ == '__main__': + GetAvalancheProofsTest().main()