diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp index ec27c4483..5c748ce95 100644 --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -1,1064 +1,1065 @@ // Copyright (c) 2020 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static RPCHelpMan getavalanchekey() { return RPCHelpMan{ "getavalanchekey", "Returns the key used to sign avalanche messages.\n", {}, RPCResult{RPCResult::Type::STR_HEX, "", ""}, RPCExamples{HelpExampleRpc("getavalanchekey", "")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { if (!g_avalanche) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); } return HexStr(g_avalanche->getSessionPubKey()); }, }; } static CPubKey ParsePubKey(const UniValue ¶m) { const std::string keyHex = param.get_str(); if ((keyHex.length() != 2 * CPubKey::COMPRESSED_SIZE && keyHex.length() != 2 * CPubKey::SIZE) || !IsHex(keyHex)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid public key: %s\n", keyHex)); } return HexToPubKey(keyHex); } static bool registerProofIfNeeded(avalanche::ProofRef proof) { return g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) { return pm.getProof(proof->getId()) || pm.registerProof(std::move(proof)); }); } static void verifyDelegationOrThrow(avalanche::Delegation &dg, const std::string &dgHex, CPubKey &auth) { bilingual_str error; if (!avalanche::Delegation::FromHex(dg, dgHex, error)) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, error.original); } avalanche::DelegationState state; if (!dg.verify(state, auth)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "The delegation is invalid: " + state.ToString()); } } static void verifyProofOrThrow(const NodeContext &node, avalanche::Proof &proof, const std::string &proofHex) { bilingual_str error; if (!avalanche::Proof::FromHex(proof, proofHex, error)) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, error.original); } avalanche::ProofValidationState state; { LOCK(cs_main); if (!proof.verify(state, *Assert(node.chainman))) { throw JSONRPCError(RPC_INVALID_PARAMETER, "The proof is invalid: " + state.ToString()); } } } static RPCHelpMan addavalanchenode() { return RPCHelpMan{ "addavalanchenode", "Add a node in the set of peers to poll for avalanche.\n", { {"nodeid", RPCArg::Type::NUM, RPCArg::Optional::NO, "Node to be added to avalanche."}, {"publickey", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The public key of the node."}, {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Proof that the node is not a sybil."}, {"delegation", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "The proof delegation the the node public key"}, }, RPCResult{RPCResult::Type::BOOL, "success", "Whether the addition succeeded or not."}, RPCExamples{ HelpExampleRpc("addavalanchenode", "5, \"\", \"\"")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { RPCTypeCheck(request.params, {UniValue::VNUM, UniValue::VSTR, UniValue::VSTR}); if (!g_avalanche) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); } const NodeId nodeid = request.params[0].get_int64(); CPubKey key = ParsePubKey(request.params[1]); auto proof = RCUPtr::make(); NodeContext &node = EnsureAnyNodeContext(request.context); verifyProofOrThrow(node, *proof, request.params[2].get_str()); const avalanche::ProofId &proofid = proof->getId(); if (key != proof->getMaster()) { if (request.params.size() < 4 || request.params[3].isNull()) { throw JSONRPCError( RPC_INVALID_ADDRESS_OR_KEY, "The public key does not match the proof"); } avalanche::Delegation dg; CPubKey auth; verifyDelegationOrThrow(dg, request.params[3].get_str(), auth); if (dg.getProofId() != proofid) { throw JSONRPCError( RPC_INVALID_PARAMETER, "The delegation does not match the proof"); } if (key != auth) { throw JSONRPCError( RPC_INVALID_ADDRESS_OR_KEY, "The public key does not match the delegation"); } } if (!registerProofIfNeeded(proof)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "The proof has conflicting utxos"); } if (!node.connman->ForNode(nodeid, [&](CNode *pnode) { // FIXME This is not thread safe, and might cause issues if // the unlikely event the peer sends an avahello message at // the same time. if (!pnode->m_avalanche_state) { pnode->m_avalanche_state = std::make_unique(); } pnode->m_avalanche_state->pubkey = std::move(key); return true; })) { throw JSONRPCError( RPC_INVALID_PARAMETER, strprintf("The node does not exist: %d", nodeid)); ; } return g_avalanche->withPeerManager( [&](avalanche::PeerManager &pm) { if (!pm.addNode(nodeid, proofid)) { return false; } pm.addUnbroadcastProof(proofid); return true; }); }, }; } static RPCHelpMan buildavalancheproof() { return RPCHelpMan{ "buildavalancheproof", "Build a proof for avalanche's sybil resistance.\n", { {"sequence", RPCArg::Type::NUM, RPCArg::Optional::NO, "The proof's sequence"}, {"expiration", RPCArg::Type::NUM, RPCArg::Optional::NO, "A timestamp indicating when the proof expire"}, {"master", RPCArg::Type::STR, RPCArg::Optional::NO, "The master private key in base58-encoding"}, { "stakes", RPCArg::Type::ARR, RPCArg::Optional::NO, "The stakes to be signed and associated private keys", { { "stake", RPCArg::Type::OBJ, RPCArg::Optional::NO, "A stake to be attached to this proof", { {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, {"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The amount in this UTXO"}, {"height", RPCArg::Type::NUM, RPCArg::Optional::NO, "The height at which this UTXO was mined"}, {"iscoinbase", RPCArg::Type::BOOL, /* default */ "false", "Indicate wether the UTXO is a coinbase"}, {"privatekey", RPCArg::Type::STR, RPCArg::Optional::NO, "private key in base58-encoding"}, }, }, }, }, {"payoutAddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A payout address (not required for legacy proofs)"}, }, RPCResult{RPCResult::Type::STR_HEX, "proof", "A string that is a serialized, hex-encoded proof data."}, RPCExamples{HelpExampleRpc("buildavalancheproof", "0 1234567800 \"\" []")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { RPCTypeCheck(request.params, {UniValue::VNUM, UniValue::VNUM, UniValue::VSTR, UniValue::VARR}); const uint64_t sequence = request.params[0].get_int64(); const int64_t expiration = request.params[1].get_int64(); CKey masterKey = DecodeSecret(request.params[2].get_str()); if (!masterKey.IsValid()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid master key"); } CTxDestination payoutAddress = CNoDestination(); if (!avalanche::Proof::useLegacy(gArgs)) { if (request.params[4].isNull()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "A payout address is required if " "`-legacyavaproof` is false"); } payoutAddress = DecodeDestination(request.params[4].get_str(), config.GetChainParams()); if (!IsValidDestination(payoutAddress)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid payout address"); } } avalanche::ProofBuilder pb(sequence, expiration, masterKey, GetScriptForDestination(payoutAddress)); const UniValue &stakes = request.params[3].get_array(); for (size_t i = 0; i < stakes.size(); i++) { const UniValue &stake = stakes[i]; RPCTypeCheckObj( stake, { {"txid", UniValue::VSTR}, {"vout", UniValue::VNUM}, // "amount" is also required but check is done below // due to UniValue::VNUM erroneously not accepting // quoted numerics (which are valid JSON) {"height", UniValue::VNUM}, {"privatekey", UniValue::VSTR}, }); int nOut = find_value(stake, "vout").get_int(); if (nOut < 0) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "vout cannot be negative"); } const int height = find_value(stake, "height").get_int(); if (height < 1) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "height must be positive"); } const TxId txid(ParseHashO(stake, "txid")); const COutPoint utxo(txid, nOut); if (!stake.exists("amount")) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Missing amount"); } const Amount amount = AmountFromValue(find_value(stake, "amount")); const UniValue &iscbparam = find_value(stake, "iscoinbase"); const bool iscoinbase = iscbparam.isNull() ? false : iscbparam.get_bool(); CKey key = DecodeSecret(find_value(stake, "privatekey").get_str()); if (!key.IsValid()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid private key"); } if (!pb.addUTXO(utxo, amount, uint32_t(height), iscoinbase, std::move(key))) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Duplicated stake"); } } const avalanche::ProofRef proof = pb.build(); return proof->ToHex(); }, }; } static RPCHelpMan decodeavalancheproof() { return RPCHelpMan{ "decodeavalancheproof", "Convert a serialized, hex-encoded proof, into JSON object. " "The validity of the proof is not verified.\n", { {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The proof hex string"}, }, RPCResult{ RPCResult::Type::OBJ, "", "", { {RPCResult::Type::NUM, "sequence", "The proof's sequential number"}, {RPCResult::Type::NUM, "expiration", "A timestamp indicating when the proof expires"}, {RPCResult::Type::STR_HEX, "master", "The master public key"}, {RPCResult::Type::STR, "signature", "The proof signature (base64 encoded). Not available when " "-legacyavaproof is enabled."}, {RPCResult::Type::OBJ, "payoutscript", "The proof payout script. Always empty when -legacyavaproof " "is enabled.", { {RPCResult::Type::STR, "asm", "Decoded payout script"}, {RPCResult::Type::STR_HEX, "hex", "Raw payout script in hex format"}, {RPCResult::Type::STR, "type", "The output type (e.g. " + GetAllOutputTypes() + ")"}, {RPCResult::Type::NUM, "reqSigs", "The required signatures"}, {RPCResult::Type::ARR, "addresses", "", { {RPCResult::Type::STR, "address", "eCash address"}, }}, }}, {RPCResult::Type::STR_HEX, "limitedid", "A hash of the proof data excluding the master key."}, {RPCResult::Type::STR_HEX, "proofid", "A hash of the limitedid and master key."}, {RPCResult::Type::STR_AMOUNT, "staked_amount", "The total staked amount of this proof in " + Currency::get().ticker + "."}, {RPCResult::Type::NUM, "score", "The score of this proof."}, {RPCResult::Type::ARR, "stakes", "", { {RPCResult::Type::OBJ, "", "", { {RPCResult::Type::STR_HEX, "txid", "The transaction id"}, {RPCResult::Type::NUM, "vout", "The output number"}, {RPCResult::Type::STR_AMOUNT, "amount", "The amount in this UTXO"}, {RPCResult::Type::NUM, "height", "The height at which this UTXO was mined"}, {RPCResult::Type::BOOL, "iscoinbase", "Indicate whether the UTXO is a coinbase"}, {RPCResult::Type::STR_HEX, "pubkey", "This UTXO's public key"}, {RPCResult::Type::STR, "signature", "Signature of the proofid with this UTXO's private " "key (base64 encoded)"}, }}, }}, }}, RPCExamples{HelpExampleCli("decodeavalancheproof", "\"\"") + HelpExampleRpc("decodeavalancheproof", "\"\"")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { RPCTypeCheck(request.params, {UniValue::VSTR}); avalanche::Proof proof; bilingual_str error; if (!avalanche::Proof::FromHex(proof, request.params[0].get_str(), error)) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, error.original); } UniValue result(UniValue::VOBJ); result.pushKV("sequence", proof.getSequence()); result.pushKV("expiration", proof.getExpirationTime()); result.pushKV("master", HexStr(proof.getMaster())); const auto signature = proof.getSignature(); if (signature) { result.pushKV("signature", EncodeBase64(*signature)); } const auto payoutScript = proof.getPayoutScript(); UniValue payoutScriptObj(UniValue::VOBJ); ScriptPubKeyToUniv(payoutScript, payoutScriptObj, /* fIncludeHex */ true); result.pushKV("payoutscript", payoutScriptObj); result.pushKV("limitedid", proof.getLimitedId().ToString()); result.pushKV("proofid", proof.getId().ToString()); result.pushKV("staked_amount", proof.getStakedAmount()); result.pushKV("score", uint64_t(proof.getScore())); UniValue stakes(UniValue::VARR); for (const avalanche::SignedStake &s : proof.getStakes()) { const COutPoint &utxo = s.getStake().getUTXO(); UniValue stake(UniValue::VOBJ); stake.pushKV("txid", utxo.GetTxId().ToString()); stake.pushKV("vout", uint64_t(utxo.GetN())); stake.pushKV("amount", s.getStake().getAmount()); stake.pushKV("height", uint64_t(s.getStake().getHeight())); stake.pushKV("iscoinbase", s.getStake().isCoinbase()); stake.pushKV("pubkey", HexStr(s.getStake().getPubkey())); // Only PKHash destination is supported, so this is safe stake.pushKV("address", EncodeDestination(PKHash(s.getStake().getPubkey()), config)); stake.pushKV("signature", EncodeBase64(s.getSignature())); stakes.push_back(stake); } result.pushKV("stakes", stakes); return result; }, }; } static RPCHelpMan delegateavalancheproof() { return RPCHelpMan{ "delegateavalancheproof", "Delegate the avalanche proof to another public key.\n", { {"limitedproofid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The limited id of the proof to be delegated."}, {"privatekey", RPCArg::Type::STR, RPCArg::Optional::NO, "The private key in base58-encoding. Must match the proof master " "public key or the upper level parent delegation public key if " " supplied."}, {"publickey", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The public key to delegate the proof to."}, {"delegation", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A string that is the serialized, hex-encoded delegation for the " "proof and which is a parent for the delegation to build."}, }, RPCResult{RPCResult::Type::STR_HEX, "delegation", "A string that is a serialized, hex-encoded delegation."}, RPCExamples{ HelpExampleRpc("delegateavalancheproof", "\"\" \"\" \"\"")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VSTR, UniValue::VSTR}); if (!g_avalanche) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); } avalanche::LimitedProofId limitedProofId{ ParseHashV(request.params[0], "limitedproofid")}; const CKey privkey = DecodeSecret(request.params[1].get_str()); if (!privkey.IsValid()) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "The private key is invalid"); } const CPubKey pubkey = ParsePubKey(request.params[2]); std::unique_ptr dgb; if (request.params.size() >= 4 && !request.params[3].isNull()) { avalanche::Delegation dg; CPubKey auth; verifyDelegationOrThrow(dg, request.params[3].get_str(), auth); if (dg.getProofId() != limitedProofId.computeProofId(dg.getProofMaster())) { throw JSONRPCError( RPC_INVALID_PARAMETER, "The delegation does not match the proof"); } if (privkey.GetPubKey() != auth) { throw JSONRPCError( RPC_INVALID_ADDRESS_OR_KEY, "The private key does not match the delegation"); } dgb = std::make_unique(dg); } else { dgb = std::make_unique( limitedProofId, privkey.GetPubKey()); } if (!dgb->addLevel(privkey, pubkey)) { throw JSONRPCError(RPC_MISC_ERROR, "Unable to build the delegation"); } CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss << dgb->build(); return HexStr(ss); }, }; } static RPCHelpMan decodeavalanchedelegation() { return RPCHelpMan{ "decodeavalanchedelegation", "Convert a serialized, hex-encoded avalanche proof delegation, into " "JSON object. \n" "The validity of the delegation is not verified.\n", { {"delegation", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The delegation hex string"}, }, RPCResult{ RPCResult::Type::OBJ, "", "", { {RPCResult::Type::STR_HEX, "pubkey", "The public key the proof is delegated to."}, {RPCResult::Type::STR_HEX, "proofmaster", "The delegated proof master public key."}, {RPCResult::Type::STR_HEX, "delegationid", "The identifier of this delegation."}, {RPCResult::Type::STR_HEX, "limitedid", "A delegated proof data hash excluding the master key."}, {RPCResult::Type::STR_HEX, "proofid", "A hash of the delegated proof limitedid and master key."}, {RPCResult::Type::NUM, "depth", "The number of delegation levels."}, {RPCResult::Type::ARR, "levels", "", { {RPCResult::Type::OBJ, "", "", { {RPCResult::Type::NUM, "index", "The index of this delegation level."}, {RPCResult::Type::STR_HEX, "pubkey", "This delegated public key for this level"}, {RPCResult::Type::STR, "signature", "Signature of this delegation level (base64 " "encoded)"}, }}, }}, }}, RPCExamples{HelpExampleCli("decodeavalanchedelegation", "\"\"") + HelpExampleRpc("decodeavalanchedelegation", "\"\"")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { RPCTypeCheck(request.params, {UniValue::VSTR}); avalanche::Delegation delegation; bilingual_str error; if (!avalanche::Delegation::FromHex( delegation, request.params[0].get_str(), error)) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, error.original); } UniValue result(UniValue::VOBJ); result.pushKV("pubkey", HexStr(delegation.getDelegatedPubkey())); result.pushKV("proofmaster", HexStr(delegation.getProofMaster())); result.pushKV("delegationid", delegation.getId().ToString()); result.pushKV("limitedid", delegation.getLimitedProofId().ToString()); result.pushKV("proofid", delegation.getProofId().ToString()); auto levels = delegation.getLevels(); result.pushKV("depth", uint64_t(levels.size())); UniValue levelsArray(UniValue::VARR); for (auto &level : levels) { UniValue obj(UniValue::VOBJ); obj.pushKV("pubkey", HexStr(level.pubkey)); obj.pushKV("signature", EncodeBase64(level.sig)); levelsArray.push_back(std::move(obj)); } result.pushKV("levels", levelsArray); return result; }, }; } static RPCHelpMan getavalancheinfo() { return RPCHelpMan{ "getavalancheinfo", "Returns an object containing various state info regarding avalanche " "networking.\n", {}, RPCResult{ RPCResult::Type::OBJ, "", "", { {RPCResult::Type::BOOL, "active", "Whether the node is ready to start polling and voting."}, {RPCResult::Type::OBJ, "local", "Only available if -avaproof has been supplied to the node", { {RPCResult::Type::BOOL, "live", "Whether the node local proof has been verified or not."}, {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::NUM, "finalized_proof_count", "The number of known avalanche proofs that have been " "finalized by avalanche."}, {RPCResult::Type::NUM, "conflicting_proof_count", "The number of known avalanche proofs that conflict with " "valid proofs."}, {RPCResult::Type::NUM, "orphan_proof_count", "The number of known avalanche proofs that are " "orphaned."}, {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); ret.pushKV("active", g_avalanche->isQuorumEstablished()); 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([&](avalanche::PeerManager &pm) { UniValue network(UniValue::VOBJ); uint64_t proofCount{0}; uint64_t connectedProofCount{0}; uint64_t finalizedProofCount{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.hasFinalized) { ++finalizedProofCount; } if (peer.node_count > 0) { ++connectedProofCount; connectedStakes += proofStake; } }); network.pushKV("proof_count", proofCount); network.pushKV("connected_proof_count", connectedProofCount); network.pushKV("finalized_proof_count", finalizedProofCount); network.pushKV("conflicting_proof_count", uint64_t(pm.getConflictingProofCount())); network.pushKV("orphan_proof_count", uint64_t(pm.getOrphanProofCount())); 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", "Returns data about an avalanche peer as a json array of objects. If " "no proofid is provided, returns data about all the peers.\n", { {"proofid", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "The hex encoded avalanche proof identifier."}, }, RPCResult{ RPCResult::Type::ARR, "", "", {{ RPCResult::Type::OBJ, "", "", {{ - {RPCResult::Type::NUM, "peerid", "The peer id"}, + {RPCResult::Type::NUM, "avalanche_peerid", + "The avalanche internal peer identifier"}, {RPCResult::Type::STR_HEX, "proofid", "The avalanche proof id used by this peer"}, {RPCResult::Type::STR_HEX, "proof", "The avalanche proof used by this peer"}, {RPCResult::Type::NUM, "nodecount", "The number of nodes for this peer"}, {RPCResult::Type::ARR, - "nodes", + "node_list", "", { {RPCResult::Type::NUM, "nodeid", "Node id, as returned by getpeerinfo"}, }}, }}, }}, }, RPCExamples{HelpExampleCli("getavalanchepeerinfo", "") + HelpExampleCli("getavalanchepeerinfo", "\"proofid\"") + HelpExampleRpc("getavalanchepeerinfo", "") + HelpExampleRpc("getavalanchepeerinfo", "\"proofid\"")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { RPCTypeCheck(request.params, {UniValue::VSTR}); if (!g_avalanche) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); } auto peerToUniv = [](const avalanche::PeerManager &pm, const avalanche::Peer &peer) { UniValue obj(UniValue::VOBJ); - obj.pushKV("peerid", uint64_t(peer.peerid)); + obj.pushKV("avalanche_peerid", uint64_t(peer.peerid)); obj.pushKV("proofid", peer.getProofId().ToString()); obj.pushKV("proof", peer.proof->ToHex()); UniValue nodes(UniValue::VARR); pm.forEachNode(peer, [&](const avalanche::Node &n) { nodes.push_back(n.nodeid); }); obj.pushKV("nodecount", uint64_t(peer.node_count)); - obj.pushKV("nodes", nodes); + obj.pushKV("node_list", nodes); return obj; }; UniValue ret(UniValue::VARR); g_avalanche->withPeerManager([&](const avalanche::PeerManager &pm) { // If a proofid is provided, only return the associated peer if (!request.params[0].isNull()) { const avalanche::ProofId proofid = avalanche::ProofId::fromHex( request.params[0].get_str()); if (!pm.isBoundToPeer(proofid)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Proofid not found"); } pm.forPeer(proofid, [&](const avalanche::Peer &peer) { return ret.push_back(peerToUniv(pm, peer)); }); return; } // If no proofid is provided, return all the peers pm.forEachPeer([&](const avalanche::Peer &peer) { ret.push_back(peerToUniv(pm, peer)); }); }); return ret; }, }; } static RPCHelpMan getrawavalancheproof() { return RPCHelpMan{ "getrawavalancheproof", "Lookup for a known avalanche proof by id.\n", { {"proofid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex encoded avalanche proof identifier."}, }, RPCResult{ RPCResult::Type::OBJ, "", "", {{ {RPCResult::Type::STR_HEX, "proof", "The hex encoded proof matching the identifier."}, {RPCResult::Type::BOOL, "orphan", "Whether the proof is an orphan."}, {RPCResult::Type::BOOL, "isBoundToPeer", "Whether the proof is bound to an avalanche peer."}, }}, }, RPCExamples{HelpExampleRpc("getrawavalancheproof", "")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { if (!g_avalanche) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); } const avalanche::ProofId proofid = avalanche::ProofId::fromHex(request.params[0].get_str()); bool isOrphan = false; bool isBoundToPeer = false; auto proof = g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) { isOrphan = pm.isOrphan(proofid); isBoundToPeer = pm.isBoundToPeer(proofid); return pm.getProof(proofid); }); if (!proof) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Proof not found"); } UniValue ret(UniValue::VOBJ); CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss << *proof; ret.pushKV("proof", HexStr(ss)); ret.pushKV("orphan", isOrphan); ret.pushKV("isBoundToPeer", isBoundToPeer); return ret; }, }; } static RPCHelpMan sendavalancheproof() { return RPCHelpMan{ "sendavalancheproof", "Broadcast an avalanche proof.\n", { {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The avalanche proof to broadcast."}, }, RPCResult{RPCResult::Type::BOOL, "success", "Whether the proof was sent successfully or not."}, RPCExamples{HelpExampleRpc("sendavalancheproof", "")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { if (!g_avalanche) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); } auto proof = RCUPtr::make(); NodeContext &node = EnsureAnyNodeContext(request.context); // Verify the proof. Note that this is redundant with the // verification done when adding the proof to the pool, but we get a // chance to give a better error message. verifyProofOrThrow(node, *proof, request.params[0].get_str()); // Add the proof to the pool if we don't have it already. Since the // proof verification has already been done, a failure likely // indicates that there already is a proof with conflicting utxos. const avalanche::ProofId &proofid = proof->getId(); if (!registerProofIfNeeded(proof)) { throw JSONRPCError( RPC_INVALID_PARAMETER, "The proof has conflicting utxo with an existing proof"); } g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) { pm.addUnbroadcastProof(proofid); }); RelayProof(proofid, *node.connman); return true; }, }; } static RPCHelpMan verifyavalancheproof() { return RPCHelpMan{ "verifyavalancheproof", "Verify an avalanche proof is valid and return the error otherwise.\n", { {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Proof to verify."}, }, RPCResult{RPCResult::Type::BOOL, "success", "Whether the proof is valid or not."}, RPCExamples{HelpExampleRpc("verifyavalancheproof", "\"\"")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { RPCTypeCheck(request.params, {UniValue::VSTR}); avalanche::Proof proof; verifyProofOrThrow(EnsureAnyNodeContext(request.context), proof, request.params[0].get_str()); return true; }, }; } static RPCHelpMan verifyavalanchedelegation() { return RPCHelpMan{ "verifyavalanchedelegation", "Verify an avalanche delegation is valid and return the error " "otherwise.\n", { {"delegation", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The avalanche proof delegation to verify."}, }, RPCResult{RPCResult::Type::BOOL, "success", "Whether the delegation is valid or not."}, RPCExamples{HelpExampleRpc("verifyavalanchedelegation", "\"\"")}, [&](const RPCHelpMan &self, const Config &config, const JSONRPCRequest &request) -> UniValue { RPCTypeCheck(request.params, {UniValue::VSTR}); avalanche::Delegation delegation; CPubKey dummy; verifyDelegationOrThrow(delegation, request.params[0].get_str(), dummy); return true; }, }; } void RegisterAvalancheRPCCommands(CRPCTable &t) { // clang-format off static const CRPCCommand commands[] = { // category actor (function) // ----------------- -------------------- { "avalanche", getavalanchekey, }, { "avalanche", addavalanchenode, }, { "avalanche", buildavalancheproof, }, { "avalanche", decodeavalancheproof, }, { "avalanche", delegateavalancheproof, }, { "avalanche", decodeavalanchedelegation, }, { "avalanche", getavalancheinfo, }, { "avalanche", getavalanchepeerinfo, }, { "avalanche", getrawavalancheproof, }, { "avalanche", sendavalancheproof, }, { "avalanche", verifyavalancheproof, }, { "avalanche", verifyavalanchedelegation, }, }; // clang-format on for (const auto &c : commands) { t.appendCommand(c.name, &c); } } diff --git a/test/functional/abc_p2p_getavaaddr.py b/test/functional/abc_p2p_getavaaddr.py index d854c4d49..a5d81ab4f 100755 --- a/test/functional/abc_p2p_getavaaddr.py +++ b/test/functional/abc_p2p_getavaaddr.py @@ -1,444 +1,444 @@ #!/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 getavaaddr p2p message""" import time from decimal import Decimal from test_framework.avatools import AvaP2PInterface, gen_proof from test_framework.messages import ( NODE_AVALANCHE, NODE_NETWORK, AvalancheVote, AvalancheVoteError, msg_getavaaddr, ) from test_framework.p2p import P2PInterface, p2p_lock from test_framework.test_framework import BitcoinTestFramework from test_framework.util import MAX_NODES, assert_equal, p2p_port # getavaaddr time interval in seconds, as defined in net_processing.cpp # A node will ignore repeated getavaaddr during this interval GETAVAADDR_INTERVAL = 2 * 60 # Address are sent every 30s on average, with a Poisson filter. Use a large # enough delay so it's very unlikely we don't get the message within this time. MAX_ADDR_SEND_DELAY = 5 * 60 # The interval between avalanche statistics computation AVALANCHE_STATISTICS_INTERVAL = 10 * 60 # The getavaaddr messages are sent every 2 to 5 minutes MAX_GETAVAADDR_DELAY = 5 * 60 class AddrReceiver(P2PInterface): def __init__(self): super().__init__() self.received_addrs = None def get_received_addrs(self): with p2p_lock: return self.received_addrs def on_addr(self, message): self.received_addrs = [] for addr in message.addrs: self.received_addrs.append(f"{addr.ip}:{addr.port}") def addr_received(self): return self.received_addrs is not None class MutedAvaP2PInterface(AvaP2PInterface): def __init__(self, node=None): super().__init__(node) self.is_responding = False self.privkey = None self.addr = None self.poll_received = 0 def set_addr(self, addr): self.addr = addr def on_avapoll(self, message): self.poll_received += 1 class AllYesAvaP2PInterface(MutedAvaP2PInterface): def __init__(self, node=None): super().__init__(node) self.is_responding = True def on_avapoll(self, message): self.send_avaresponse( message.poll.round, [ AvalancheVote( AvalancheVoteError.ACCEPTED, inv.hash) for inv in message.poll.invs], self.master_privkey if self.delegation is None else self.delegated_privkey) super().on_avapoll(message) class AvaAddrTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = False self.num_nodes = 1 self.extra_args = [['-enableavalanche=1', '-enableavalanchepeerdiscovery=1', '-avaproofstakeutxoconfirmations=1', '-avacooldown=0', '-whitelist=noban@127.0.0.1']] def check_all_peers_received_getavaaddr_once(self, avapeers): def received_all_getavaaddr(avapeers): with p2p_lock: return all([p.last_message.get("getavaaddr") for p in avapeers]) self.wait_until(lambda: received_all_getavaaddr(avapeers)) with p2p_lock: assert all([p.message_count.get( "getavaaddr", 0) == 1 for p in avapeers]) def getavaaddr_interval_test(self): node = self.nodes[0] # Init mock time mock_time = int(time.time()) node.setmocktime(mock_time) # Add some avalanche peers to the node for _ in range(10): node.add_p2p_connection(AllYesAvaP2PInterface(node)) # Build some statistics to ensure some addresses will be returned def all_peers_received_poll(): with p2p_lock: return all([avanode.poll_received > 0 for avanode in node.p2ps]) self.wait_until(all_peers_received_poll) node.mockscheduler(AVALANCHE_STATISTICS_INTERVAL) requester = node.add_p2p_connection(AddrReceiver()) requester.send_message(msg_getavaaddr()) # Remember the time we sent the getavaaddr message getavaddr_time = mock_time # Spamming more get getavaaddr has no effect for _ in range(10): with node.assert_debug_log(["Ignoring repeated getavaaddr from peer"]): requester.send_message(msg_getavaaddr()) # Move the time so we get an addr response mock_time += MAX_ADDR_SEND_DELAY node.setmocktime(mock_time) requester.wait_until(requester.addr_received) # Elapse the getavaaddr interval and check our message is now accepted # again mock_time = getavaddr_time + GETAVAADDR_INTERVAL node.setmocktime(mock_time) requester.send_message(msg_getavaaddr()) # We can get an addr message again mock_time += MAX_ADDR_SEND_DELAY node.setmocktime(mock_time) requester.wait_until(requester.addr_received) def address_test(self, maxaddrtosend, num_proof, num_avanode): self.restart_node( 0, extra_args=self.extra_args[0] + [f'-maxaddrtosend={maxaddrtosend}']) node = self.nodes[0] # Init mock time mock_time = int(time.time()) node.setmocktime(mock_time) # Create a bunch of proofs and associate each a bunch of nodes. avanodes = [] for _ in range(num_proof): master_privkey, proof = gen_proof(node) for n in range(num_avanode): avanode = AllYesAvaP2PInterface() if n % 2 else MutedAvaP2PInterface() avanode.master_privkey = master_privkey avanode.proof = proof node.add_p2p_connection(avanode) peerinfo = node.getpeerinfo()[-1] avanode.set_addr(peerinfo["addr"]) avanodes.append(avanode) responding_addresses = [ avanode.addr for avanode in avanodes if avanode.is_responding] assert_equal(len(responding_addresses), num_proof * num_avanode // 2) # Check we have what we expect def all_nodes_connected(): avapeers = node.getavalanchepeerinfo() if len(avapeers) != num_proof: return False for avapeer in avapeers: - if len(avapeer['nodes']) != num_avanode: + if avapeer['nodecount'] != num_avanode: return False return True self.wait_until(all_nodes_connected) # Force the availability score to diverge between the responding and the # muted nodes. def poll_all_for_block(): node.generate(1) with p2p_lock: return all([avanode.poll_received > ( 10 if avanode.is_responding else 0) for avanode in avanodes]) self.wait_until(poll_all_for_block) # Move the scheduler time 10 minutes forward so that so that our peers # get an availability score computed. node.mockscheduler(AVALANCHE_STATISTICS_INTERVAL) requester = node.add_p2p_connection(AddrReceiver()) requester.send_and_ping(msg_getavaaddr()) # Sanity check that the availability score is set up as expected peerinfo = node.getpeerinfo() muted_addresses = [ avanode.addr for avanode in avanodes if not avanode.is_responding] assert all([p['availability_score'] < 0 for p in peerinfo if p["addr"] in muted_addresses]) assert all([p['availability_score'] > 0 for p in peerinfo if p["addr"] in responding_addresses]) # Requester has no availability_score because it's not an avalanche # peer assert 'availability_score' not in peerinfo[-1].keys() mock_time += MAX_ADDR_SEND_DELAY node.setmocktime(mock_time) requester.wait_until(requester.addr_received) addresses = requester.get_received_addrs() assert_equal(len(addresses), min(maxaddrtosend, len(responding_addresses))) # Check all the addresses belong to responding peer assert all([address in responding_addresses for address in addresses]) def getavaaddr_outbound_test(self): self.log.info( "Check we send a getavaaddr message to our avalanche outbound peers") node = self.nodes[0] # Get rid of previously connected nodes node.disconnect_p2ps() avapeers = [] for i in range(16): avapeer = AvaP2PInterface() node.add_outbound_p2p_connection( avapeer, p2p_idx=i, ) avapeers.append(avapeer) self.check_all_peers_received_getavaaddr_once(avapeers) # Generate some block to poll for node.generate(1) # Because none of the avalanche peers is responding, our node should # fail out of option shortly and send a getavaaddr message to one of its # outbound avalanche peers. node.mockscheduler(MAX_GETAVAADDR_DELAY) def any_peer_received_getavaaddr(): with p2p_lock: return any([p.message_count.get( "getavaaddr", 0) > 1 for p in avapeers]) self.wait_until(any_peer_received_getavaaddr) def getavaaddr_manual_test(self): self.log.info( "Check we send a getavaaddr message to our manually connected peers that support avalanche") node = self.nodes[0] # Get rid of previously connected nodes node.disconnect_p2ps() def added_node_connected(ip_port): added_node_info = node.getaddednodeinfo(ip_port) return len( added_node_info) == 1 and added_node_info[0]['connected'] def connect_callback(address, port): self.log.debug("Connecting to {}:{}".format(address, port)) p = AvaP2PInterface() p2p_idx = 1 p.peer_accept_connection( connect_cb=connect_callback, connect_id=p2p_idx, net=node.chain, timeout_factor=node.timeout_factor, )() ip_port = f"127.0.01:{p2p_port(MAX_NODES - p2p_idx)}" node.addnode(node=ip_port, command="add") self.wait_until(lambda: added_node_connected(ip_port)) assert_equal(node.getpeerinfo()[-1]['addr'], ip_port) assert_equal(node.getpeerinfo()[-1]['connection_type'], 'manual') p.wait_until(lambda: p.last_message.get("getavaaddr")) # Generate some block to poll for node.generate(1) # Because our avalanche peer is not responding, our node should fail # out of option shortly and send another getavaaddr message. node.mockscheduler(MAX_GETAVAADDR_DELAY) p.wait_until(lambda: p.message_count.get("getavaaddr", 0) > 1) def getavaaddr_noquorum(self): self.log.info( "Check we send a getavaaddr message while our quorum is not established") node = self.nodes[0] self.restart_node(0, extra_args=self.extra_args[0] + [ '-avaminquorumstake=1000000000', '-avaminquorumconnectedstakeratio=0.8', ]) avapeers = [] for i in range(16): avapeer = AllYesAvaP2PInterface(node) node.add_outbound_p2p_connection( avapeer, p2p_idx=i, connection_type="avalanche", services=NODE_NETWORK | NODE_AVALANCHE, ) avapeers.append(avapeer) peerinfo = node.getpeerinfo()[-1] avapeer.set_addr(peerinfo["addr"]) self.check_all_peers_received_getavaaddr_once(avapeers) def total_getavaaddr_msg(): with p2p_lock: return sum([p.message_count.get("getavaaddr", 0) for p in avapeers]) # Because we have not enough stake to start polling, we keep requesting # more addresses total_getavaaddr = total_getavaaddr_msg() for i in range(5): node.mockscheduler(MAX_GETAVAADDR_DELAY) self.wait_until(lambda: total_getavaaddr_msg() > total_getavaaddr) total_getavaaddr = total_getavaaddr_msg() # Move the schedulter time forward to make seure we get statistics # computed. But since we did not start polling yet it should remain all # zero. node.mockscheduler(AVALANCHE_STATISTICS_INTERVAL) def wait_for_availability_score(): peerinfo = node.getpeerinfo() return all([p.get('availability_score', None) == Decimal(0) for p in peerinfo]) self.wait_until(wait_for_availability_score) requester = node.add_p2p_connection(AddrReceiver()) requester.send_and_ping(msg_getavaaddr()) node.setmocktime(int(time.time() + MAX_ADDR_SEND_DELAY)) # Check all the peers addresses are returned. requester.wait_until(requester.addr_received) addresses = requester.get_received_addrs() assert_equal(len(addresses), len(avapeers)) expected_addresses = [avapeer.addr for avapeer in avapeers] assert all([address in expected_addresses for address in addresses]) def test_send_inbound_getavaaddr_until_quorum_is_established(self): self.log.info( "Check we also request the inbounds until the quorum is established") node = self.nodes[0] self.restart_node( 0, extra_args=self.extra_args[0] + ['-avaminquorumstake=1000000']) assert_equal(node.getavalancheinfo()['active'], False) outbound = MutedAvaP2PInterface() node.add_outbound_p2p_connection(outbound, p2p_idx=0) inbound = MutedAvaP2PInterface() node.add_p2p_connection(inbound) inbound.nodeid = node.getpeerinfo()[-1]['id'] def count_getavaaddr(peers): with p2p_lock: return sum([peer.message_count.get("getavaaddr", 0) for peer in peers]) # Upon connection only the outbound gets a getavaaddr message assert_equal(count_getavaaddr([inbound]), 0) self.wait_until(lambda: count_getavaaddr([outbound]) == 1) # Periodic send will include the inbound as well current_total = count_getavaaddr([inbound, outbound]) while count_getavaaddr([inbound]) == 0: node.mockscheduler(MAX_GETAVAADDR_DELAY) self.wait_until(lambda: count_getavaaddr( [inbound, outbound]) > current_total) current_total = count_getavaaddr([inbound, outbound]) # Connect the minimum amount of stake privkey, proof = gen_proof(node) assert node.addavalanchenode( inbound.nodeid, privkey.get_pubkey().get_bytes().hex(), proof.serialize().hex()) assert_equal(node.getavalancheinfo()['active'], True) # From now only the outbound is requested count_inbound = count_getavaaddr([inbound]) for _ in range(10): # Trigger a poll node.generate(1) inbound.sync_send_with_ping() node.mockscheduler(MAX_GETAVAADDR_DELAY) self.wait_until(lambda: count_getavaaddr( [inbound, outbound]) > current_total) current_total = count_getavaaddr([inbound, outbound]) assert_equal(count_getavaaddr([inbound]), count_inbound) def run_test(self): self.getavaaddr_interval_test() # Limited by maxaddrtosend self.address_test(maxaddrtosend=3, num_proof=2, num_avanode=8) # Limited by the number of good nodes self.address_test(maxaddrtosend=100, num_proof=2, num_avanode=8) self.getavaaddr_outbound_test() self.getavaaddr_manual_test() self.getavaaddr_noquorum() self.test_send_inbound_getavaaddr_until_quorum_is_established() if __name__ == '__main__': AvaAddrTest().main() diff --git a/test/functional/abc_rpc_getavalanchepeerinfo.py b/test/functional/abc_rpc_getavalanchepeerinfo.py index f6e5f0e12..6e769eef5 100755 --- a/test/functional/abc_rpc_getavalanchepeerinfo.py +++ b/test/functional/abc_rpc_getavalanchepeerinfo.py @@ -1,91 +1,92 @@ #!/usr/bin/env python3 # Copyright (c) 2020-2021 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 getavalanchepeerinfo RPC.""" from random import choice from test_framework.avatools import ( avalanche_proof_from_hex, create_coinbase_stakes, get_ava_p2p_interface, ) from test_framework.key import ECKey from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error from test_framework.wallet_util import bytes_to_wif class GetAvalanchePeerInfoTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-enableavalanche=1', '-avaproofstakeutxoconfirmations=1', '-avacooldown=0']] def run_test(self): node = self.nodes[0] peercount = 5 nodecount = 10 self.log.info( f"Generating {peercount} peers with {nodecount} nodes each") addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(peercount, addrkey0.address) # Use the first coinbase to create a stake stakes = create_coinbase_stakes(node, blockhashes, addrkey0.key) def getProof(stake): privkey = ECKey() privkey.generate() pubkey = privkey.get_pubkey() proof_sequence = 11 proof_expiration = 12 proof = node.buildavalancheproof( proof_sequence, proof_expiration, bytes_to_wif( privkey.get_bytes()), [stake]) return (pubkey.get_bytes().hex(), proof) # Create peercount * nodecount node array nodes = [[get_ava_p2p_interface(node) for _ in range( nodecount)] for _ in range(peercount)] # Add peercount peers and bind all the nodes to each proofs = [] for i in range(peercount): pubkey_hex, proof = getProof(stakes[i]) proofs.append(proof) [node.addavalanchenode(n.nodeid, pubkey_hex, proof) for n in nodes[i]] self.log.info("Testing getavalanchepeerinfo...") avapeerinfo = node.getavalanchepeerinfo() assert_equal(len(avapeerinfo), peercount) for i, peer in enumerate(avapeerinfo): proofid_hex = f"{avalanche_proof_from_hex(proofs[i]).proofid:0{64}x}" - assert_equal(peer["peerid"], i) + assert_equal(peer["avalanche_peerid"], i) assert_equal(peer["proofid"], proofid_hex) assert_equal(peer["proof"], proofs[i]) assert_equal(peer["nodecount"], nodecount) - assert_equal(set(peer["nodes"]), set([n.nodeid for n in nodes[i]])) + assert_equal(set(peer["node_list"]), set( + [n.nodeid for n in nodes[i]])) self.log.info("Testing with a specified proofid") assert_raises_rpc_error(-8, "Proofid not found", node.getavalanchepeerinfo, proofid="0" * 64) target_proof = choice(proofs) target_proofid = avalanche_proof_from_hex(target_proof).proofid avapeerinfo = node.getavalanchepeerinfo( proofid=f"{target_proofid:0{64}x}") assert_equal(len(avapeerinfo), 1) assert_equal(avapeerinfo[0]["proof"], target_proof) if __name__ == '__main__': GetAvalanchePeerInfoTest().main() diff --git a/test/functional/test_framework/avatools.py b/test/functional/test_framework/avatools.py index 4169760bd..b5a4a582a 100644 --- a/test/functional/test_framework/avatools.py +++ b/test/functional/test_framework/avatools.py @@ -1,383 +1,382 @@ #!/usr/bin/env python3 # Copyright (c) 2021 The Bitcoin ABC developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Utilities for avalanche tests.""" import random import struct from typing import Any, Dict, List, Optional from .authproxy import JSONRPCException from .key import ECKey from .messages import ( MSG_AVA_PROOF, MSG_BLOCK, NODE_AVALANCHE, NODE_NETWORK, AvalancheDelegation, AvalancheProof, AvalancheResponse, CInv, CTransaction, FromHex, LegacyAvalancheProof, TCPAvalancheResponse, ToHex, calculate_shortid, hash256, msg_avahello, msg_avapoll, msg_avaproof, msg_avaproofs, msg_notfound, msg_tcpavaresponse, ) from .p2p import P2PInterface, p2p_lock from .test_node import TestNode from .util import assert_equal, satoshi_round, wait_until_helper from .wallet_util import bytes_to_wif def avalanche_proof_from_hex(proof_hex: str) -> AvalancheProof: try: return FromHex(AvalancheProof(), proof_hex) except struct.error: # If the proof deserialization failed, fallback to the legacy # format return FromHex(LegacyAvalancheProof(), proof_hex) def create_coinbase_stakes( node: TestNode, blockhashes: List[str], priv_key: str, amount: Optional[str] = None) -> List[Dict[str, Any]]: """Returns a list of dictionaries representing stakes, in a format compatible with the buildavalancheproof RPC, using only coinbase transactions. :param node: Test node used to get the block and coinbase data. :param blockhashes: List of block hashes, whose coinbase tx will be used as a stake. :param priv_key: Private key controlling the coinbase UTXO :param amount: If specified, this overwrites the amount information in the coinbase dicts. """ blocks = [node.getblock(h, 2) for h in blockhashes] coinbases = [ { 'height': b['height'], 'txid': b['tx'][0]['txid'], 'n': 0, 'value': b['tx'][0]['vout'][0]['value'], } for b in blocks ] return [{ 'txid': coinbase['txid'], 'vout': coinbase['n'], 'amount': amount or coinbase['value'], 'height': coinbase['height'], 'iscoinbase': True, 'privatekey': priv_key, } for coinbase in coinbases] def get_utxos_in_blocks(node: TestNode, blockhashes: List[str]) -> List[Dict]: """Return all UTXOs in the specified list of blocks. """ utxos = filter( lambda u: node.gettransaction(u["txid"])["blockhash"] in blockhashes, node.listunspent()) return list(utxos) def create_stakes( node: TestNode, blockhashes: List[str], count: int ) -> List[Dict[str, Any]]: """ Create a list of stakes by splitting existing UTXOs from a specified list of blocks into 10 new coins. This function can generate more valid stakes than `get_coinbase_stakes` does, because on the regtest chain halving happens every 150 blocks so the coinbase amount is below the dust threshold after only 900 blocks. :param node: Test node used to generate blocks and send transactions :param blockhashes: List of block hashes whose UTXOs will be split. :param count: Number of stakes to return. """ assert 10 * len(blockhashes) >= count utxos = get_utxos_in_blocks(node, blockhashes) addresses = [node.getnewaddress() for _ in range(10)] private_keys = {addr: node.dumpprivkey(addr) for addr in addresses} for u in utxos: inputs = [{"txid": u["txid"], "vout": u["vout"]}] outputs = { addr: satoshi_round(u['amount'] / 10) for addr in addresses} raw_tx = node.createrawtransaction(inputs, outputs) ctx = FromHex(CTransaction(), raw_tx) ctx.vout[0].nValue -= node.calculate_fee(ctx) signed_tx = node.signrawtransactionwithwallet(ToHex(ctx))["hex"] node.sendrawtransaction(signed_tx) # confirm the transactions new_blocks = [] while node.getmempoolinfo()['size'] > 0: new_blocks += node.generate(1) utxos = get_utxos_in_blocks(node, new_blocks) stakes = [] # cache block heights heights = {} for utxo in utxos[:count]: blockhash = node.gettransaction(utxo["txid"])["blockhash"] if blockhash not in heights: heights[blockhash] = node.getblock(blockhash, 1)["height"] stakes.append({ 'txid': utxo['txid'], 'vout': utxo['vout'], 'amount': utxo['amount'], 'iscoinbase': utxo['label'] == "coinbase", 'height': heights[blockhash], 'privatekey': private_keys[utxo["address"]], }) return stakes def get_proof_ids(node): - return [int(node.decodeavalancheproof(peer['proof'])['proofid'], 16) - for peer in node.getavalanchepeerinfo()] + return [int(peer['proofid'], 16) for peer in node.getavalanchepeerinfo()] def wait_for_proof(node, proofid_hex, timeout=60, expect_orphan=None): """ Wait for the proof to be known by the node. If expect_orphan is set, the proof should match the orphan state, otherwise it's a don't care parameter. """ def proof_found(): try: wait_for_proof.is_orphan = node.getrawavalancheproof(proofid_hex)[ "orphan"] return True except JSONRPCException: return False wait_until_helper(proof_found, timeout=timeout) if expect_orphan is not None: assert_equal(expect_orphan, wait_for_proof.is_orphan) class NoHandshakeAvaP2PInterface(P2PInterface): """P2PInterface with avalanche capabilities""" def __init__(self): self.round = 0 self.avahello = None self.avaresponses = [] self.avapolls = [] self.nodeid: Optional[int] = None super().__init__() def peer_connect(self, *args, **kwargs): create_conn = super().peer_connect(*args, **kwargs) # Save the nonce and extra entropy so they can be reused later. self.local_nonce = self.on_connection_send_msg.nNonce self.local_extra_entropy = self.on_connection_send_msg.nExtraEntropy return create_conn def peer_accept_connection(self, *args, **kwargs): create_conn = super().peer_accept_connection(*args, **kwargs) # Save the nonce and extra entropy so they can be reused later. self.local_nonce = self.on_connection_send_msg.nNonce self.local_extra_entropy = self.on_connection_send_msg.nExtraEntropy return create_conn def on_version(self, message): super().on_version(message) # Save the nonce and extra entropy so they can be reused later. self.remote_nonce = message.nNonce self.remote_extra_entropy = message.nExtraEntropy def on_avaresponse(self, message): self.avaresponses.append(message.response) def on_avapoll(self, message): self.avapolls.append(message.poll) def on_avahello(self, message): assert(self.avahello is None) self.avahello = message def send_avaresponse(self, round, votes, privkey): response = AvalancheResponse(round, 0, votes) sig = privkey.sign_schnorr(response.get_hash()) msg = msg_tcpavaresponse() msg.response = TCPAvalancheResponse(response, sig) self.send_message(msg) def wait_for_avaresponse(self, timeout=5): self.wait_until( lambda: len(self.avaresponses) > 0, timeout=timeout) with p2p_lock: return self.avaresponses.pop(0) def send_poll(self, hashes, type=MSG_BLOCK): msg = msg_avapoll() msg.poll.round = self.round self.round += 1 for h in hashes: msg.poll.invs.append(CInv(type, h)) self.send_message(msg) def send_proof(self, proof): msg = msg_avaproof() msg.proof = proof self.send_message(msg) def get_avapoll_if_available(self): with p2p_lock: return self.avapolls.pop(0) if len(self.avapolls) > 0 else None def wait_for_avahello(self, timeout=5): self.wait_until( lambda: self.avahello is not None, timeout=timeout) with p2p_lock: return self.avahello def build_avahello(self, delegation: AvalancheDelegation, delegated_privkey: ECKey) -> msg_avahello: local_sighash = hash256( delegation.getid() + struct.pack(" 0: self.send_message(msg_notfound(not_found)) def get_ava_p2p_interface( node: TestNode, services=NODE_NETWORK | NODE_AVALANCHE) -> NoHandshakeAvaP2PInterface: """Build and return a NoHandshakeAvaP2PInterface connected to the specified TestNode. """ n = NoHandshakeAvaP2PInterface() node.add_p2p_connection( n, services=services) n.wait_for_verack() n.nodeid = node.getpeerinfo()[-1]['id'] return n def gen_proof(node, coinbase_utxos=1): blockhashes = node.generate(coinbase_utxos) privkey = ECKey() privkey.generate() stakes = create_coinbase_stakes( node, blockhashes, node.get_deterministic_priv_key().key) proof_hex = node.buildavalancheproof( 42, 2000000000, bytes_to_wif(privkey.get_bytes()), stakes) return privkey, avalanche_proof_from_hex(proof_hex) def build_msg_avaproofs(proofs: List[AvalancheProof], prefilled_proofs: Optional[List[AvalancheProof]] = None, key_pair: Optional[List[int]] = None) -> msg_avaproofs: if key_pair is None: key_pair = [random.randint(0, 2**64 - 1)] * 2 msg = msg_avaproofs() msg.key0 = key_pair[0] msg.key1 = key_pair[1] msg.prefilled_proofs = prefilled_proofs or [] msg.shortids = [ calculate_shortid( msg.key0, msg.key1, proof.proofid) for proof in proofs] return msg