diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp index a44e16912..8372a4d8f 100644 --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -1,1016 +1,1043 @@ // 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, node.chainman->ActiveChainstate().CoinsTip())) { 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 = std::make_shared(); NodeContext &node = EnsureNodeContext(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(); CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss << *proof; return HexStr(ss); }, }; } 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", 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::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", "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::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", "", { {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); CDataStream serproof(SER_NETWORK, PROTOCOL_VERSION); serproof << *peer.proof; obj.pushKV("peerid", uint64_t(peer.peerid)); obj.pushKV("proof", HexStr(serproof)); 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); 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 = std::make_shared(); NodeContext &node = EnsureNodeContext(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(EnsureNodeContext(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_rpc_avalancheproof.py b/test/functional/abc_rpc_avalancheproof.py index 6976bd1eb..6fd2cf593 100644 --- a/test/functional/abc_rpc_avalancheproof.py +++ b/test/functional/abc_rpc_avalancheproof.py @@ -1,571 +1,574 @@ #!/usr/bin/env python3 # Copyright (c) 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 building avalanche proofs and using them to add avalanche peers.""" import base64 from decimal import Decimal from test_framework.address import ADDRESS_ECREG_UNSPENDABLE, base58_to_byte from test_framework.avatools import ( create_coinbase_stakes, create_stakes, get_proof_ids, wait_for_proof, ) from test_framework.cashaddr import PUBKEY_TYPE, encode_full from test_framework.key import ECKey from test_framework.messages import ( AvalancheDelegation, AvalancheDelegationLevel, AvalancheProof, FromHex, LegacyAvalancheProof, ) from test_framework.p2p import P2PInterface, p2p_lock from test_framework.test_framework import BitcoinTestFramework from test_framework.test_node import ErrorMatch from test_framework.util import ( append_config, assert_equal, assert_raises_rpc_error, ) from test_framework.wallet_util import bytes_to_wif AVALANCHE_MAX_PROOF_STAKES = 1000 PROOF_DUST_THRESHOLD = 1000000.0 """Minimum amount per UTXO in a proof (in coins, not in satoshis)""" def add_interface_node(test_node) -> str: """Create a mininode, connect it to test_node, return the nodeid of the mininode as registered by test_node. """ n = P2PInterface() test_node.add_p2p_connection(n) n.wait_for_verack() return test_node.getpeerinfo()[-1]['id'] class LegacyAvalancheProofTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [ ['-enableavalanche=1', '-avacooldown=0', '-legacyavaproof=1']] * self.num_nodes self.supports_cli = False self.rpc_timeout = 120 def run_test(self): # Turn off node 1 while node 0 mines blocks to generate stakes, # so that we can later try starting node 1 with an orphan proof. self.stop_node(1) node = self.nodes[0] # FIXME Remove after the hardcoded addresses have been converted in the # LUT from test_node.py def legacy_to_ecash_p2pkh(legacy): payload, _ = base58_to_byte(legacy) return encode_full('ecregtest', PUBKEY_TYPE, payload) addrkey0 = node.get_deterministic_priv_key() node_ecash_addr = legacy_to_ecash_p2pkh(addrkey0.address) blockhashes = node.generatetoaddress(100, node_ecash_addr) self.log.info( "Make build a valid proof and restart the node to use it") privkey = ECKey() privkey.set(bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) wif_privkey = bytes_to_wif(privkey.get_bytes()) def get_hex_pubkey(privkey): return privkey.get_pubkey().get_bytes().hex() proof_master = get_hex_pubkey(privkey) proof_sequence = 11 proof_expiration = 12 stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) proof = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, stakes) self.log.info("Test decodeavalancheproof RPC") proofobj = FromHex(LegacyAvalancheProof(), proof) decodedproof = node.decodeavalancheproof(proof) limited_id_hex = f"{proofobj.limited_proofid:0{64}x}" proofid_hex = f"{proofobj.proofid:0{64}x}" assert_equal(decodedproof["sequence"], proof_sequence) assert_equal(decodedproof["expiration"], proof_expiration) assert_equal(decodedproof["master"], proof_master) assert_equal(decodedproof["payoutscript"]["hex"], "") assert "signature" not in decodedproof.keys() assert_equal(decodedproof["proofid"], proofid_hex) assert_equal(decodedproof["limitedid"], limited_id_hex) assert_equal(decodedproof["staked_amount"], Decimal('50000000.00')) assert_equal(decodedproof["score"], 5000) assert_equal(decodedproof["stakes"][0]["txid"], stakes[0]["txid"]) assert_equal(decodedproof["stakes"][0]["vout"], stakes[0]["vout"]) assert_equal(decodedproof["stakes"][0]["height"], stakes[0]["height"]) assert_equal( decodedproof["stakes"][0]["iscoinbase"], stakes[0]["iscoinbase"]) assert_equal( decodedproof["stakes"][0]["address"], node_ecash_addr) assert_equal( decodedproof["stakes"][0]["signature"], base64.b64encode(proofobj.stakes[0].sig).decode("ascii")) # Invalid hex (odd number of hex digits) assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", node.decodeavalancheproof, proof[:-1]) # Valid hex but invalid proof assert_raises_rpc_error(-22, "Proof has invalid format", node.decodeavalancheproof, proof[:-2]) self.log.info( "Testing decodeavalancheproof with legacyavaproof disabled") self.restart_node(0, self.extra_args[0] + ["-legacyavaproof=0"]) regular_proof = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, stakes, ADDRESS_ECREG_UNSPENDABLE) decoded_regular_proof = node.decodeavalancheproof(regular_proof) assert_equal( decoded_regular_proof["sequence"], decodedproof["sequence"]) assert_equal( decoded_regular_proof["expiration"], decodedproof["expiration"]) assert_equal(decoded_regular_proof["master"], decodedproof["master"]) assert_equal(decoded_regular_proof["payoutscript"], { "asm": "OP_DUP OP_HASH160 0000000000000000000000000000000000000000 OP_EQUALVERIFY OP_CHECKSIG", "hex": "76a914000000000000000000000000000000000000000088ac", "reqSigs": 1, "type": "pubkeyhash", "addresses": [ADDRESS_ECREG_UNSPENDABLE], }) regular_proof_obj = FromHex(AvalancheProof(), regular_proof) assert_equal( decoded_regular_proof["signature"], base64.b64encode(regular_proof_obj.signature).decode("ascii")) assert_equal( decoded_regular_proof["proofid"], f"{regular_proof_obj.proofid:0{64}x}") assert_equal( decoded_regular_proof["limitedid"], f"{regular_proof_obj.limited_proofid:0{64}x}") assert_equal( decoded_regular_proof["staked_amount"], decodedproof["staked_amount"]) assert_equal( decoded_regular_proof["score"], decodedproof["score"]) assert_equal( decoded_regular_proof["stakes"][0]["txid"], decodedproof["stakes"][0]["txid"]) assert_equal( decoded_regular_proof["stakes"][0]["vout"], decodedproof["stakes"][0]["vout"]) assert_equal( decoded_regular_proof["stakes"][0]["height"], decodedproof["stakes"][0]["height"]) assert_equal( decoded_regular_proof["stakes"][0]["iscoinbase"], decodedproof["stakes"][0]["iscoinbase"]) assert_equal( decoded_regular_proof["stakes"][0]["address"], decodedproof["stakes"][0]["address"]) assert_equal( decoded_regular_proof["stakes"][0]["signature"], base64.b64encode(regular_proof_obj.stakes[0].sig).decode("ascii")) # Restart the node with this proof self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) self.log.info("The proof is registered at first chaintip update") assert_equal(len(node.getavalanchepeerinfo()), 0) node.generate(1) self.wait_until(lambda: len(node.getavalanchepeerinfo()) == 1, timeout=5) # This case will occur for users building proofs with a third party # tool and then starting a new node that is not yet aware of the # transactions used for stakes. self.log.info("Start a node with an orphan proof") self.start_node(1, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) # Mine a block to trigger an attempt at registering the proof self.nodes[1].generate(1) wait_for_proof(self.nodes[1], proofid_hex, expect_orphan=True) self.log.info("Connect to an up-to-date node to unorphan the proof") self.connect_nodes(1, node.index) self.sync_all() wait_for_proof(self.nodes[1], proofid_hex, expect_orphan=False) - self.log.info("Generate delegations for the proof and decode them") + self.log.info( + "Generate delegations for the proof, verify and decode them") # Stack up a few delegation levels def gen_privkey(): pk = ECKey() pk.generate() return pk delegator_privkey = privkey delegation = None for i in range(10): delegated_privkey = gen_privkey() delegated_pubkey = get_hex_pubkey(delegated_privkey) delegation = node.delegateavalancheproof( limited_id_hex, bytes_to_wif(delegator_privkey.get_bytes()), delegated_pubkey, delegation, ) + assert node.verifyavalanchedelegation(delegation) + dg_info = node.decodeavalanchedelegation(delegation) assert_equal(dg_info['pubkey'], delegated_pubkey) assert_equal(dg_info['proofmaster'], proof_master) assert 'delegationid' in dg_info.keys() assert_equal(dg_info['limitedid'], limited_id_hex) assert_equal(dg_info['proofid'], proofid_hex) assert_equal(dg_info['depth'], i + 1) assert_equal(len(dg_info['levels']), dg_info['depth']) assert_equal(dg_info['levels'][-1]['pubkey'], delegated_pubkey) assert 'signature' in dg_info['levels'][-1] delegator_privkey = delegated_privkey random_privkey = gen_privkey() random_pubkey = get_hex_pubkey(random_privkey) # Invalid proof no_stake = node.buildavalancheproof(proof_sequence, proof_expiration, wif_privkey, []) # Invalid privkey assert_raises_rpc_error(-5, "The private key is invalid", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(bytes(32)), random_pubkey, ) # Invalid delegation bad_dg = AvalancheDelegation() assert_raises_rpc_error(-8, "The delegation does not match the proof", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Still invalid, but with a matching proofid bad_dg.limited_proofid = proofobj.limited_proofid bad_dg.proof_master = proofobj.master bad_dg.levels = [AvalancheDelegationLevel()] assert_raises_rpc_error(-8, "The delegation is invalid", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Wrong privkey, match the proof but does not match the delegation assert_raises_rpc_error(-5, "The private key does not match the delegation", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, delegation, ) # Delegation not hex assert_raises_rpc_error(-22, "Delegation must be an hexadecimal string.", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, "f00", ) # Delegation is hex but ill-formed assert_raises_rpc_error(-22, "Delegation has invalid format", node.delegateavalancheproof, limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, "dead", ) # Test invalid proofs dust = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount="0")) dust2 = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount=f"{PROOF_DUST_THRESHOLD * 0.9999:.2f}")) missing_stake = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, [{ 'txid': '0' * 64, 'vout': 0, 'amount': 10000000, 'height': 42, 'iscoinbase': False, 'privatekey': addrkey0.key, }] ) duplicate_stake = ("0b000000000000000c0000000000000021030b4c866585dd868" "a9d62348a9cd008d6a312937048fff31670e7e920cfc7a74402" "05c5f72f5d6da3085583e75ee79340eb4eff208c89988e7ed0e" "fb30b87298fa30000000000f2052a0100000003000000210227" "d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2" "fc839ef5d3f86076def2e8bc3c40671c1a0eb505da5857a950a" "0cf4625a80018cdd75ac62e61273ff8142f747de67e73f6368c" "8648942b0ef6c065d72a81ad7438a23c11cca05c5f72f5d6da3" "085583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30" "000000000f2052a0100000003000000210227d85ba011276cf2" "5b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d3f860" "76def2e8bc3c40671c1a0eb505da5857a950a0cf4625a80018c" "dd75ac62e61273ff8142f747de67e73f6368c8648942b0ef6c0" "65d72a81ad7438a23c11cca") bad_sig = ("0b000000000000000c0000000000000021030b4c866585dd868a9d62348" "a9cd008d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085" "583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30000000000f20" "52a0100000003000000210227d85ba011276cf25b51df6a188b75e604b3" "8770a462b2d0e9fb2fc839ef5d3faf07f001dd38e9b4a43d07d5d449cc0" "f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" "1321b91a79b82d1c2cfd47793261e4ba003cf5") wrong_order = ("c964aa6fde575e4ce8404581c7be874e21023beefdde700a6bc0203" "6335b4df141c8bc67bb05a971f5ac2745fd683797dde30305d427b7" "06705a5d4b6a368a231d6db62abacf8c29bc32b61e7f65a0a6976aa" "8b86b687bc0260e821e4f0200b9d3bf6d2102449fb5237efe8f647d" "32e8b64f06c22d1d40368eaca2a71ffc6a13ecc8bce68052365271b" "6c71189f5cd7e3b694b77b579080f0b35bae567b96590ab6aa3019b" "018ff9f061f52f1426bdb195d4b6d4dff5114cee90e33dabf0c588e" "badf7774418f54247f6390791706af36fac782302479898b5273f9e" "51a92cb1fb5af43deeb6c8c269403d30ffcb380300134398c42103e" "49f9df52de2dea81cf7838b82521b69f2ea360f1c4eed9e6c89b7d0" "f9e645efa08e97ea0c60e1f0a064fbf08989c084707082727e85dcb" "9f79bb503f76ee6c8dad42a07ef15c89b3750a5631d604b21fafff0" "f4de354ade95c2f28160ae549af0d4ce48c4ca9d0714b1fa5192027" "0f8575e0af610f07b4e602a018ecdbb649b64fff614c0026e9fc8e0" "030092533d422103aac52f4cfca700e7e9824298e0184755112e32f" "359c832f5f6ad2ef62a2c024af812d6d7f2ecc6223a774e19bce1fb" "20d94d6b01ea693638f55c74fdaa5358fa9239d03e4caf3d817e8f7" "48ccad55a27b9d365db06ad5a0b779ac385f3dc8710") self.log.info( "Check the verifyavalancheproof and sendavalancheproof RPCs") if self.is_wallet_compiled(): self.log.info( "Check a proof with the maximum number of UTXO is valid") new_blocks = node.generate(AVALANCHE_MAX_PROOF_STAKES // 10 + 1) # confirm the coinbase UTXOs node.generate(101) too_many_stakes = create_stakes( node, new_blocks, AVALANCHE_MAX_PROOF_STAKES + 1) maximum_stakes = too_many_stakes[:-1] good_proof = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, maximum_stakes) too_many_utxos = node.buildavalancheproof( proof_sequence, proof_expiration, wif_privkey, too_many_stakes) assert node.verifyavalancheproof(good_proof) for rpc in [node.verifyavalancheproof, node.sendavalancheproof]: assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", rpc, "f00") assert_raises_rpc_error(-22, "Proof has invalid format", rpc, "f00d") def check_rpc_failure(proof, message): assert_raises_rpc_error(-8, "The proof is invalid: " + message, rpc, proof) check_rpc_failure(no_stake, "no-stake") check_rpc_failure(dust, "amount-below-dust-threshold") check_rpc_failure(duplicate_stake, "duplicated-stake") check_rpc_failure(missing_stake, "utxo-missing-or-spent") check_rpc_failure(bad_sig, "invalid-stake-signature") check_rpc_failure(wrong_order, "wrong-stake-ordering") if self.is_wallet_compiled(): check_rpc_failure(too_many_utxos, "too-many-utxos") conflicting_utxo = node.buildavalancheproof( proof_sequence + 1, proof_expiration, wif_privkey, stakes) assert_raises_rpc_error(-8, "The proof has conflicting utxo with an existing proof", node.sendavalancheproof, conflicting_utxo) # Clear the proof pool self.restart_node(0) # Good proof assert node.verifyavalancheproof(proof) peer = node.add_p2p_connection(P2PInterface()) proofid = FromHex(LegacyAvalancheProof(), proof).proofid node.sendavalancheproof(proof) assert proofid in get_proof_ids(node) def inv_found(): with p2p_lock: return peer.last_message.get( "inv") and peer.last_message["inv"].inv[-1].hash == proofid self.wait_until(inv_found) self.log.info("Check the getrawproof RPC") raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], False) assert_equal(raw_proof['isBoundToPeer'], True) assert_raises_rpc_error(-8, "Proof not found", node.getrawavalancheproof, '0' * 64) # Orphan the proof by sending the stake raw_tx = node.createrawtransaction( [{"txid": stakes[-1]["txid"], "vout": 0}], {ADDRESS_ECREG_UNSPENDABLE: stakes[-1] ["amount"] - Decimal('10000')} ) signed_tx = node.signrawtransactionwithkey(raw_tx, [addrkey0.key]) node.sendrawtransaction(signed_tx["hex"]) node.generate(1) self.wait_until(lambda: proofid not in get_proof_ids(node)) raw_proof = node.getrawavalancheproof("{:064x}".format(proofid)) assert_equal(raw_proof['proof'], proof) assert_equal(raw_proof['orphan'], True) assert_equal(raw_proof['isBoundToPeer'], False) self.log.info("Bad proof should be rejected at startup") self.stop_node(0) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avasessionkey=0", ], expected_msg="Error: The avalanche session key is invalid.", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), ], expected_msg="Error: The avalanche master key is missing for the avalanche proof.", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=0", ], expected_msg="Error: The avalanche master key is invalid.", ) def check_proof_init_error(proof, message): node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: " + message, ) check_proof_init_error(no_stake, "The avalanche proof has no stake.") check_proof_init_error(dust, "The avalanche proof stake is too low.") check_proof_init_error(dust2, "The avalanche proof stake is too low.") check_proof_init_error(duplicate_stake, "The avalanche proof has duplicated stake.") check_proof_init_error(bad_sig, "The avalanche proof has invalid stake signatures.") if self.is_wallet_compiled(): # The too many utxos case creates a proof which is that large that it # cannot fit on the command line append_config(node.datadir, ["avaproof={}".format(too_many_utxos)]) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: The avalanche proof has too many utxos.", match=ErrorMatch.PARTIAL_REGEX, ) # Master private key mismatch random_privkey = ECKey() random_privkey.generate() node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(random_privkey.get_bytes())), ], expected_msg="Error: The master key does not match the proof public key.", ) self.log.info("Bad delegation should be rejected at startup") def check_delegation_init_error(delegation, message): node.assert_start_raises_init_error( self.extra_args[0] + [ "-avadelegation={}".format(delegation), "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(delegated_privkey.get_bytes())), ], expected_msg="Error: " + message, ) check_delegation_init_error( AvalancheDelegation().serialize().hex(), "The delegation does not match the proof.") bad_level_sig = FromHex(AvalancheDelegation(), delegation) # Tweak some key to cause the signature to mismatch bad_level_sig.levels[-2].pubkey = bytes.fromhex(proof_master) check_delegation_init_error(bad_level_sig.serialize().hex(), "The avalanche delegation has invalid signatures.") node.assert_start_raises_init_error( self.extra_args[0] + [ "-avadelegation={}".format(delegation), "-avaproof={}".format(proof), "-avamasterkey={}".format( bytes_to_wif(random_privkey.get_bytes())), ], expected_msg="Error: The master key does not match the delegation public key.", ) if __name__ == '__main__': LegacyAvalancheProofTest().main()