diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -5,5 +5,7 @@ This release includes the following features and fixes: -- A new `total_fee` field showing the total fees for all transactions in the - mempool has been added to the `getmempoolinfo` RPC. + - A new `total_fee` field showing the total fees for all transactions in the + mempool has been added to the `getmempoolinfo` RPC. + - Added a 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,12 @@ uint64_t getSlotCount() const { return slotCount; } uint64_t getFragmentation() const { return fragmentation; } + const ProofPool &getValidProofPool() const { return validProofPool; } + const ProofPool &getConflictingProofPool() const { + return conflictingProofPool; + } + const ProofPool &getImmatureProofPool() const { 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,70 @@ }; } +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) { + auto appendProofIds = [&ret](const avalanche::ProofPool &pool, + const std::string &key) { + UniValue arrOut(UniValue::VARR); + for (const avalanche::ProofId &proofid : + pool.getProofIds()) { + arrOut.push_back(proofid.ToString()); + } + ret.pushKV(key, arrOut); + }; + + appendProofIds(pm.getValidProofPool(), "valid"); + appendProofIds(pm.getConflictingProofPool(), "conflicting"); + appendProofIds(pm.getImmatureProofPool(), "immature"); + }); + + return ret; + }, + }; +} + static RPCHelpMan getrawavalancheproof() { return RPCHelpMan{ "getrawavalancheproof", @@ -1242,6 +1306,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,193 @@ +#!/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 assert_equal, 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] + [ + '-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) + self.wait_until( + lambda: 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)], + }) + ) + + assert_equal(node.getavalancheinfo()['ready_to_poll'], True) + + 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) + 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)], + }) + ) + + # Make the immature proof mature + node.generate(1) + proofs.append(immature_proof) + + self.wait_until( + lambda: avalancheproofs_equals({ + "valid": [uint256_hex(p.proofid) for p in proofs], + "conflicting": [], + "immature": [], + }) + ) + + +if __name__ == '__main__': + GetAvalancheProofsTest().main()