diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp index b648dffa0..3272d13df 100644 --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -1,461 +1,542 @@ // 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 static UniValue getavalanchekey(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ "getavalanchekey", "Returns the key used to sign avalanche messages.\n", {}, RPCResult{RPCResult::Type::STR_HEX, "", ""}, RPCExamples{HelpExampleRpc("getavalanchekey", "")}, } .Check(request); 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 UniValue addavalanchenode(const Config &config, const JSONRPCRequest &request) { 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."}, }, RPCResult{RPCResult::Type::BOOL, "success", "Whether the addition succeeded or not."}, RPCExamples{ HelpExampleRpc("addavalanchenode", "5, \"\", \"\"")}, } .Check(request); 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(); const CPubKey key = ParsePubKey(request.params[1]); avalanche::Proof proof; bilingual_str error; if (!avalanche::Proof::FromHex(proof, request.params[2].get_str(), error)) { throw JSONRPCError(RPC_INVALID_PARAMETER, error.original); } if (key != proof.getMaster()) { // TODO: we want to provide a proper delegation. return false; } return g_avalanche->addNode(nodeid, proof, avalanche::DelegationBuilder(proof).build()); } static UniValue buildavalancheproof(const Config &config, const JSONRPCRequest &request) { 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_HEX, RPCArg::Optional::NO, "The master public key"}, { "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"}, }, }, }, }, }, RPCResult{RPCResult::Type::STR_HEX, "proof", "A string that is a serialized, hex-encoded proof data."}, RPCExamples{HelpExampleRpc("buildavalancheproof", "0 1234567800 \"\" []")}, } .Check(request); RPCTypeCheck(request.params, {UniValue::VNUM, UniValue::VNUM, UniValue::VSTR, UniValue::VARR}); - if (!g_avalanche) { - throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); - } - const uint64_t sequence = request.params[0].get_int64(); const int64_t expiration = request.params[1].get_int64(); avalanche::ProofBuilder pb(sequence, expiration, ParsePubKey(request.params[2])); 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 must be positive"); } 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 (!pb.addUTXO(utxo, amount, uint32_t(height), iscoinbase, std::move(key))) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid private key"); } } const avalanche::Proof proof = pb.build(); CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss << proof; return HexStr(ss); } +static UniValue decodeavalancheproof(const Config &config, + const JSONRPCRequest &request) { + RPCHelpMan{ + "decodeavalancheproof", + "Convert a serialized, hex-encoded proof, into JSON object. " + "The validity of the proof is not verified.\n", + { + {"hexstring", 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_HEX, "proofid", + "The proof's unique identifier"}, + {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", "\"\"")}, + } + .Check(request); + + 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())); + result.pushKV("proofid", proof.getId().ToString()); + + 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", ValueFromAmount(s.getStake().getAmount())); + stake.pushKV("height", uint64_t(s.getStake().getHeight())); + stake.pushKV("iscoinbase", s.getStake().isCoinbase()); + stake.pushKV("pubkey", HexStr(s.getStake().getPubkey())); + stake.pushKV("signature", EncodeBase64(s.getSignature())); + stakes.push_back(stake); + } + result.pushKV("stakes", stakes); + + return result; +} + static UniValue delegateavalancheproof(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ "delegateavalancheproof", "Delegate the avalanche proof to another public key.\n", { {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "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", "\"\" \"\" \"\"")}, } .Check(request); RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VSTR, UniValue::VSTR}); if (!g_avalanche) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); } avalanche::Proof proof; bilingual_str error; if (!avalanche::Proof::FromHex(proof, request.params[0].get_str(), error)) { throw JSONRPCError(RPC_INVALID_PARAMETER, error.original); } avalanche::ProofValidationState proofState; if (!proof.verify(proofState)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "The proof is invalid"); } 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]); avalanche::DelegationBuilder dgb(proof); CPubKey auth; if (request.params.size() >= 4 && !request.params[3].isNull()) { avalanche::Delegation dg; CDataStream ss(ParseHexV(request.params[3], "delegation"), SER_NETWORK, PROTOCOL_VERSION); ss >> dg; avalanche::DelegationState dgState; if (!dg.verify(dgState, proof, auth)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "The supplied delegation is not valid"); } if (!dgb.importDelegation(dg)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Failed to import the delegation"); } } else { auth = proof.getMaster(); } if (privkey.GetPubKey() != auth) { throw JSONRPCError( RPC_INVALID_PARAMETER, "The private key does not match the proof or the delegation"); } 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 UniValue getavalanchepeerinfo(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ "getavalanchepeerinfo", "Returns data about each connected avalanche peer as a json array of " "objects.\n", {}, 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, "sequence", "The proof's sequence"}, {RPCResult::Type::NUM_TIME, "expiration", "The proof's expiration timestamp"}, {RPCResult::Type::STR_HEX, "master", "The proof's master public key"}, { RPCResult::Type::ARR, "stakes", "", {{ RPCResult::Type::OBJ, "", "", {{ {RPCResult::Type::STR_HEX, "txid", ""}, {RPCResult::Type::NUM, "vout", ""}, {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 wether the UTXO is a coinbase"}, {RPCResult::Type::STR_HEX, "pubkey", ""}, }}, }}, }, {RPCResult::Type::ARR, "nodes", "", { {RPCResult::Type::NUM, "nodeid", "Node id, as returned by getpeerinfo"}, }}, }}, }}, }, RPCExamples{HelpExampleCli("getavalanchepeerinfo", "") + HelpExampleRpc("getavalanchepeerinfo", "")}, } .Check(request); if (!g_avalanche) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); } UniValue ret(UniValue::VARR); for (const auto &peer : g_avalanche->getPeers()) { 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)); obj.pushKV("sequence", peer.proof.getSequence()); obj.pushKV("expiration", peer.proof.getExpirationTime()); obj.pushKV("master", HexStr(peer.proof.getMaster())); UniValue stakes(UniValue::VARR); for (const auto &s : peer.proof.getStakes()) { UniValue stake(UniValue::VOBJ); stake.pushKV("txid", s.getStake().getUTXO().GetTxId().GetHex()); stake.pushKV("vout", uint64_t(s.getStake().getUTXO().GetN())); stake.pushKV("amount", ValueFromAmount(s.getStake().getAmount())); stake.pushKV("height", uint64_t(s.getStake().getHeight())); stake.pushKV("iscoinbase", s.getStake().isCoinbase()); stake.pushKV("pubkey", HexStr(s.getStake().getPubkey())); stakes.push_back(stake); } obj.pushKV("stakes", stakes); UniValue nodes(UniValue::VARR); for (const auto &id : g_avalanche->getNodeIdsForPeer(peer.peerid)) { nodes.push_back(id); } obj.pushKV("nodes", nodes); obj.pushKV("nodecount", uint64_t(peer.node_count)); ret.push_back(obj); } return ret; } static UniValue verifyavalancheproof(const Config &config, const JSONRPCRequest &request) { 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", "\"\"")}, } .Check(request); 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_INVALID_PARAMETER, error.original); } NodeContext &node = EnsureNodeContext(request.context); 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()); } } return true; } void RegisterAvalancheRPCCommands(CRPCTable &t) { // clang-format off static const CRPCCommand commands[] = { // category name actor (function) argNames // ------------------- ------------------------ ---------------------- ---------- { "avalanche", "getavalanchekey", getavalanchekey, {}}, { "avalanche", "addavalanchenode", addavalanchenode, {"nodeid"}}, { "avalanche", "buildavalancheproof", buildavalancheproof, {"sequence", "expiration", "master", "stakes"}}, + { "avalanche", "decodeavalancheproof", decodeavalancheproof, {"proof"}}, { "avalanche", "delegateavalancheproof", delegateavalancheproof, {"proof", "privatekey", "publickey", "delegation"}}, { "avalanche", "getavalanchepeerinfo", getavalanchepeerinfo, {}}, { "avalanche", "verifyavalancheproof", verifyavalancheproof, {"proof"}}, }; // clang-format on for (unsigned int vcidx = 0; vcidx < ARRAYLEN(commands); vcidx++) { t.appendCommand(commands[vcidx].name, &commands[vcidx]); } } diff --git a/test/functional/abc_rpc_avalancheproof.py b/test/functional/abc_rpc_avalancheproof.py index 3836bc1fe..65bbe449f 100644 --- a/test/functional/abc_rpc_avalancheproof.py +++ b/test/functional/abc_rpc_avalancheproof.py @@ -1,285 +1,315 @@ #!/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.avatools import ( create_coinbase_stakes, create_stakes, ) from test_framework.key import ECKey, bytes_to_wif -from test_framework.messages import AvalancheDelegation +from test_framework.messages import ( + AvalancheDelegation, + AvalancheProof, + FromHex, +) from test_framework.mininode import P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.test_node import ErrorMatch from test_framework.util import ( append_config, + assert_equal, wait_until, assert_raises_rpc_error, ) AVALANCHE_MAX_PROOF_STAKES = 1000 PROOF_DUST_THRESHOLD = 1.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 AvalancheProofTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-enableavalanche=1', '-avacooldown=0'], ] self.supports_cli = False self.rpc_timeout = 120 def run_test(self): node = self.nodes[0] addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(100, addrkey0.address) self.log.info( "Make build a valid proof and restart the node to use it") privkey = ECKey() privkey.set(bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) 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, proof_master, - create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key)) + proof_sequence, proof_expiration, proof_master, stakes) + + self.log.info("Test decodeavalancheproof RPC") + proofobj = FromHex(AvalancheProof(), proof) + decodedproof = node.decodeavalancheproof(proof) + assert_equal(decodedproof["sequence"], proof_sequence) + assert_equal(decodedproof["expiration"], proof_expiration) + assert_equal(decodedproof["master"], proof_master) + assert_equal(decodedproof["proofid"], f"{proofobj.proofid:0{64}x}") + 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]["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]) # Restart the node, making sure it is initially in IBD mode minchainwork = int(node.getblockchaininfo()["chainwork"], 16) + 1 self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", "-minimumchainwork=0x{:x}".format(minchainwork), ]) self.log.info( "The proof verification should be delayed until IBD is complete") assert node.getblockchaininfo()["initialblockdownload"] is True # Our proof cannot be verified during IBD, so we should have no peer assert not node.getavalanchepeerinfo() # Mining a few more blocks should cause us to leave IBD node.generate(2) # Our proof is now verified and our node is added as a peer assert node.getblockchaininfo()["initialblockdownload"] is False wait_until(lambda: len(node.getavalanchepeerinfo()) == 1, timeout=5) if self.is_wallet_compiled(): self.log.info( "A proof using the maximum number of stakes is accepted...") 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, proof_master, maximum_stakes) peerid1 = add_interface_node(node) assert node.addavalanchenode(peerid1, proof_master, good_proof) self.log.info( "A proof using too many stakes should be rejected...") too_many_utxos = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, too_many_stakes) peerid2 = add_interface_node(node) assert not node.addavalanchenode( peerid2, proof_master, too_many_utxos) self.log.info("Generate delegations for the proof") # Stack up a few delegation levels def gen_privkey(): pk = ECKey() pk.generate() return pk delegator_privkey = privkey delegation = None for _ in range(10): delegated_privkey = gen_privkey() delegation = node.delegateavalancheproof( proof, bytes_to_wif(delegator_privkey.get_bytes()), get_hex_pubkey(delegated_privkey), delegation, ) 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, proof_master, []) assert_raises_rpc_error(-8, "The proof is invalid", node.delegateavalancheproof, no_stake, bytes_to_wif(privkey.get_bytes()), random_pubkey, ) # Invalid privkey assert_raises_rpc_error(-5, "The private key is invalid", node.delegateavalancheproof, proof, bytes_to_wif(bytes(32)), random_pubkey, ) # Invalid delegation bad_dg = AvalancheDelegation() assert_raises_rpc_error(-8, "The supplied delegation is not valid", node.delegateavalancheproof, proof, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Wrong privkey, does not match the proof assert_raises_rpc_error(-8, "The private key does not match the proof or the delegation", node.delegateavalancheproof, proof, bytes_to_wif(random_privkey.get_bytes()), random_pubkey, ) # Wrong privkey, match the proof but does not match the delegation assert_raises_rpc_error(-8, "The private key does not match the proof or the delegation", node.delegateavalancheproof, proof, bytes_to_wif(privkey.get_bytes()), random_pubkey, delegation, ) # Test invalid proofs dust = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount="0")) dust_amount = Decimal(f"{PROOF_DUST_THRESHOLD * 0.9999:.4f}") dust2 = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount=str(dust_amount))) duplicate_stake = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]] * 2, addrkey0.key)) bad_sig = ("0b000000000000000c0000000000000021030b4c866585dd868a9d62348" "a9cd008d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085" "583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30000000000f20" "52a0100000003000000210227d85ba011276cf25b51df6a188b75e604b3" "8770a462b2d0e9fb2fc839ef5d3faf07f001dd38e9b4a43d07d5d449cc0" "f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" "1321b91a79b82d1c2cfd47793261e4ba003cf5") self.log.info("Check the verifyavalancheproof RPC") assert_raises_rpc_error(-8, "Proof must be an hexadecimal string", node.verifyavalancheproof, "f00") assert_raises_rpc_error(-8, "Proof has invalid format", node.verifyavalancheproof, "f00d") def check_verifyavalancheproof_failure(proof, message): assert_raises_rpc_error(-8, "The proof is invalid: " + message, node.verifyavalancheproof, proof) check_verifyavalancheproof_failure(no_stake, "no-stake") check_verifyavalancheproof_failure(dust, "amount-below-dust-threshold") check_verifyavalancheproof_failure(duplicate_stake, "duplicated-stake") check_verifyavalancheproof_failure(bad_sig, "invalid-signature") if self.is_wallet_compiled(): check_verifyavalancheproof_failure( too_many_utxos, "too-many-utxos") # Good proof assert node.verifyavalancheproof(proof) 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, ) if __name__ == '__main__': AvalancheProofTest().main()