diff --git a/src/avalanche/processor.h b/src/avalanche/processor.h --- a/src/avalanche/processor.h +++ b/src/avalanche/processor.h @@ -155,8 +155,9 @@ class NotificationsHandler; std::unique_ptr chainNotificationsHandler; - Processor(interfaces::Chain &chain, CConnman *connmanIn, - std::unique_ptr peerDataIn, CKey sessionKeyIn); + Processor(const ArgsManager &argsman, interfaces::Chain &chain, + CConnman *connmanIn, std::unique_ptr peerDataIn, + CKey sessionKeyIn); public: ~Processor(); diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp --- a/src/avalanche/processor.cpp +++ b/src/avalanche/processor.cpp @@ -129,9 +129,12 @@ } }; -Processor::Processor(interfaces::Chain &chain, CConnman *connmanIn, - std::unique_ptr peerDataIn, CKey sessionKeyIn) - : connman(connmanIn), queryTimeoutDuration(AVALANCHE_DEFAULT_QUERY_TIMEOUT), +Processor::Processor(const ArgsManager &argsman, interfaces::Chain &chain, + CConnman *connmanIn, std::unique_ptr peerDataIn, + CKey sessionKeyIn) + : connman(connmanIn), + queryTimeoutDuration(argsman.GetArg( + "-avatimeout", AVALANCHE_DEFAULT_QUERY_TIMEOUT.count())), round(0), peerManager(std::make_unique()), peerData(std::move(peerDataIn)), sessionKey(std::move(sessionKeyIn)) { // Make sure we get notified of chain state changes. @@ -240,7 +243,7 @@ // We can't use std::make_unique with a private constructor return std::unique_ptr(new Processor( - chain, connman, std::move(peerData), std::move(sessionKey))); + argsman, chain, connman, std::move(peerData), std::move(sessionKey))); } bool Processor::addBlockToReconcile(const CBlockIndex *pindex) { diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -1330,6 +1330,11 @@ strprintf("Mandatory cooldown between two avapoll (default: %u)", AVALANCHE_DEFAULT_COOLDOWN), ArgsManager::ALLOW_ANY, OptionsCategory::AVALANCHE); + argsman.AddArg( + "-avatimeout", + strprintf("Avalanche query timeout in milliseconds (default: %u)", + AVALANCHE_DEFAULT_QUERY_TIMEOUT.count()), + ArgsManager::ALLOW_ANY, OptionsCategory::AVALANCHE); argsman.AddArg( "-avadelegation", "Avalanche proof delegation to the master key used by this node " diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -545,6 +545,140 @@ }; } +static RPCHelpMan getavalancheinfo() { + return RPCHelpMan{ + "getavalancheinfo", + "Returns an object containing various state info regarding avalanche " + "networking.\n", + {}, + RPCResult{ + RPCResult::Type::OBJ, + "", + "", + { + {RPCResult::Type::OBJ, + "local", + "Only available if -avaproof has been supplied to the node", + { + {RPCResult::Type::STR_HEX, "proofid", + "The node local proof id."}, + {RPCResult::Type::STR_HEX, "limited_proofid", + "The node local limited proof id."}, + {RPCResult::Type::STR_HEX, "master", + "The node local proof master public key."}, + {RPCResult::Type::STR, "payout_address", + "The node local proof payout address. This might be " + "omitted if the payout script is not one of P2PK, P2PKH " + "or P2SH, in which case decodeavalancheproof can be used " + "to get more details."}, + {RPCResult::Type::STR_AMOUNT, "stake_amount", + "The node local proof staked amount."}, + }}, + {RPCResult::Type::OBJ, + "network", + "", + { + {RPCResult::Type::NUM, "proof_count", + "The number of valid avalanche proofs we know exist."}, + {RPCResult::Type::NUM, "connected_proof_count", + "The number of avalanche proofs with at least one node " + "we are connected to."}, + {RPCResult::Type::STR_AMOUNT, "total_stake_amount", + "The total staked amount over all the valid proofs in " + + Currency::get().ticker + "."}, + {RPCResult::Type::STR_AMOUNT, "connected_stake_amount", + "The total staked amount over all the connected proofs " + "in " + + Currency::get().ticker + "."}, + {RPCResult::Type::NUM, "node_count", + "The number of avalanche nodes we are connected to."}, + {RPCResult::Type::NUM, "connected_node_count", + "The number of avalanche nodes associated with an " + "avalanche proof."}, + {RPCResult::Type::NUM, "pending_node_count", + "The number of avalanche nodes pending for a proof."}, + }}, + }, + }, + RPCExamples{HelpExampleCli("getavalancheinfo", "") + + HelpExampleRpc("getavalancheinfo", "")}, + [&](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); + + auto localProof = g_avalanche->getLocalProof(); + if (localProof != nullptr) { + UniValue local(UniValue::VOBJ); + local.pushKV("live", g_avalanche->withPeerManager( + [&](const avalanche::PeerManager &pm) { + return pm.isBoundToPeer( + localProof->getId()); + })); + local.pushKV("proofid", localProof->getId().ToString()); + local.pushKV("limited_proofid", + localProof->getLimitedId().ToString()); + local.pushKV("master", HexStr(localProof->getMaster())); + CTxDestination destination; + if (ExtractDestination(localProof->getPayoutScript(), + destination)) { + local.pushKV("payout_address", + EncodeDestination(destination, config)); + } + local.pushKV("stake_amount", localProof->getStakedAmount()); + ret.pushKV("local", local); + } + + g_avalanche->withPeerManager([&](const avalanche::PeerManager &pm) { + UniValue network(UniValue::VOBJ); + + uint64_t proofCount{0}; + uint64_t connectedProofCount{0}; + Amount totalStakes = Amount::zero(); + Amount connectedStakes = Amount::zero(); + + pm.forEachPeer([&](const avalanche::Peer &peer) { + CHECK_NONFATAL(peer.proof != nullptr); + + // Don't account for our local proof here + if (peer.proof == localProof) { + return; + } + + const Amount proofStake = peer.proof->getStakedAmount(); + + ++proofCount; + totalStakes += proofStake; + + if (peer.node_count > 0) { + ++connectedProofCount; + connectedStakes += proofStake; + } + }); + + network.pushKV("proof_count", proofCount); + network.pushKV("connected_proof_count", connectedProofCount); + network.pushKV("total_stake_amount", totalStakes); + network.pushKV("connected_stake_amount", connectedStakes); + + const uint64_t connectedNodes = pm.getNodeCount(); + const uint64_t pendingNodes = pm.getPendingNodeCount(); + network.pushKV("node_count", connectedNodes + pendingNodes); + network.pushKV("connected_node_count", connectedNodes); + network.pushKV("pending_node_count", pendingNodes); + + ret.pushKV("network", network); + }); + + return ret; + }, + }; +} + static RPCHelpMan getavalanchepeerinfo() { return RPCHelpMan{ "getavalanchepeerinfo", @@ -782,6 +916,7 @@ { "avalanche", buildavalancheproof, }, { "avalanche", decodeavalancheproof, }, { "avalanche", delegateavalancheproof, }, + { "avalanche", getavalancheinfo, }, { "avalanche", getavalanchepeerinfo, }, { "avalanche", getrawavalancheproof, }, { "avalanche", sendavalancheproof, }, diff --git a/test/functional/abc_rpc_getavalancheinfo.py b/test/functional/abc_rpc_getavalancheinfo.py new file mode 100755 --- /dev/null +++ b/test/functional/abc_rpc_getavalancheinfo.py @@ -0,0 +1,209 @@ +#!/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 getavalancheinfo RPC.""" +from decimal import Decimal + +from test_framework.address import ADDRESS_ECREG_UNSPENDABLE +from test_framework.avatools import gen_proof, get_ava_p2p_interface +from test_framework.key import ECKey +from test_framework.messages import LegacyAvalancheProof +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal +from test_framework.wallet_util import bytes_to_wif + + +class GetAvalancheInfoTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [['-enableavalanche=1', + '-avacooldown=', + '-avatimeout=100', + '-enableavalanchepeerdiscovery=1']] + + def run_test(self): + node = self.nodes[0] + + privkey, proof = gen_proof(node) + is_legacy = isinstance(proof, LegacyAvalancheProof) + + def handle_legacy_format(expected): + # Add the payout address to the expected output if the legacy format + # is diabled + if not is_legacy and "local" in expected.keys(): + expected["local"]["payout_address"] = ADDRESS_ECREG_UNSPENDABLE + + return expected + + def assert_avalancheinfo(expected): + assert_equal( + node.getavalancheinfo(), + handle_legacy_format(expected) + ) + + coinbase_amount = Decimal('25000000.00') + + self.log.info("The test node has no proof") + + assert_avalancheinfo({ + "network": { + "proof_count": 0, + "connected_proof_count": 0, + "total_stake_amount": Decimal('0.00'), + "connected_stake_amount": Decimal('0.00'), + "node_count": 0, + "connected_node_count": 0, + "pending_node_count": 0, + } + }) + + self.log.info("The test node has a proof") + + self.restart_node(0, self.extra_args[0] + [ + '-enableavalanche=1', + '-avaproof={}'.format(proof.serialize().hex()), + '-avamasterkey={}'.format(bytes_to_wif(privkey.get_bytes())) + ]) + assert_avalancheinfo({ + "local": { + "live": False, + "proofid": f"{proof.proofid:0{64}x}", + "limited_proofid": f"{proof.limited_proofid:0{64}x}", + "master": privkey.get_pubkey().get_bytes().hex(), + "stake_amount": coinbase_amount, + }, + "network": { + "proof_count": 0, + "connected_proof_count": 0, + "total_stake_amount": Decimal('0.00'), + "connected_stake_amount": Decimal('0.00'), + "node_count": 0, + "connected_node_count": 0, + "pending_node_count": 0, + } + }) + + # Mine a block to trigger proof validation + node.generate(1) + assert_avalancheinfo({ + "local": { + "live": True, + "proofid": f"{proof.proofid:0{64}x}", + "limited_proofid": f"{proof.limited_proofid:0{64}x}", + "master": privkey.get_pubkey().get_bytes().hex(), + "stake_amount": coinbase_amount, + }, + "network": { + "proof_count": 0, + "connected_proof_count": 0, + "total_stake_amount": Decimal('0.00'), + "connected_stake_amount": Decimal('0.00'), + "node_count": 0, + "connected_node_count": 0, + "pending_node_count": 0, + } + }) + + self.log.info("Connect a bunch of peers and nodes") + + N = 10 + for _ in range(N): + _privkey, _proof = gen_proof(node) + n = get_ava_p2p_interface(node) + success = node.addavalanchenode( + n.nodeid, _privkey.get_pubkey().get_bytes().hex(), _proof.serialize().hex()) + assert success is True + + assert_avalancheinfo({ + "local": { + "live": True, + "proofid": f"{proof.proofid:0{64}x}", + "limited_proofid": f"{proof.limited_proofid:0{64}x}", + "master": privkey.get_pubkey().get_bytes().hex(), + "stake_amount": coinbase_amount, + }, + "network": { + "proof_count": N, + "connected_proof_count": N, + "total_stake_amount": coinbase_amount * N, + "connected_stake_amount": coinbase_amount * N, + "node_count": N, + "connected_node_count": N, + "pending_node_count": 0, + } + }) + + self.log.info("Disconnect some nodes") + + D = 3 + for _ in range(D): + n = node.p2ps.pop() + n.peer_disconnect() + n.wait_for_disconnect() + + self.wait_until( + lambda: node.getavalancheinfo() == handle_legacy_format({ + "local": { + "live": True, + "proofid": f"{proof.proofid:0{64}x}", + "limited_proofid": f"{proof.limited_proofid:0{64}x}", + "master": privkey.get_pubkey().get_bytes().hex(), + "stake_amount": coinbase_amount, + }, + "network": { + "proof_count": N, + "connected_proof_count": N - D, + "total_stake_amount": coinbase_amount * N, + "connected_stake_amount": coinbase_amount * (N - D), + "node_count": N - D, + "connected_node_count": N - D, + "pending_node_count": 0, + } + }) + ) + + self.log.info("Add some pending nodes") + + P = 3 + for _ in range(P): + dg_priv = ECKey() + dg_priv.generate() + dg_pub = dg_priv.get_pubkey().get_bytes().hex() + + _privkey, _proof = gen_proof(node) + delegation = node.delegateavalancheproof( + f"{_proof.limited_proofid:0{64}x}", + bytes_to_wif(_privkey.get_bytes()), + dg_pub, + None + ) + + n = get_ava_p2p_interface(node) + n.send_avahello(delegation, dg_priv) + # Make sure we completed at least one time the ProcessMessage or we + # might miss the last pending node for the following assert + n.sync_with_ping() + + assert_avalancheinfo({ + "local": { + "live": True, + "proofid": f"{proof.proofid:0{64}x}", + "limited_proofid": f"{proof.limited_proofid:0{64}x}", + "master": privkey.get_pubkey().get_bytes().hex(), + "stake_amount": coinbase_amount, + }, + "network": { + "proof_count": N, + "connected_proof_count": N - D, + "total_stake_amount": coinbase_amount * N, + "connected_stake_amount": coinbase_amount * (N - D), + "node_count": N - D + P, + "connected_node_count": N - D, + "pending_node_count": P, + } + }) + + +if __name__ == '__main__': + GetAvalancheInfoTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -136,6 +136,7 @@ "abc_p2p_proof_inventory.py": [["--nolegacyavaproof"]], "abc_rpc_addavalanchenode.py": [["--nolegacyavaproof"]], "abc_rpc_buildavalancheproof.py": [["--nolegacyavaproof"]], + "abc_rpc_getavalancheinfo.py": [["--nolegacyavaproof"]], "abc_rpc_getavalanchepeerinfo.py": [["--nolegacyavaproof"]], "p2p_eviction.py": [["--nolegacyavaproof"]], "p2p_inv_download.py": [["--nolegacyavaproof"]],