diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -73,10 +73,6 @@ 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]); @@ -209,6 +205,94 @@ 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_HEX, "signature", + "Signature of the proofid with this UTXO's private " + "key"}, + }}, + }}, + }}, + RPCExamples{HelpExampleCli("decodeavalancheproof", "\"\"") + + HelpExampleRpc("decodeavalancheproof", "\"\"")}, + } + .Check(request); + + RPCTypeCheck(request.params, {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_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", HexStr(proof.getId())); + + UniValue stakes(UniValue::VARR); + for (avalanche::SignedStake s : proof.getStakes()) { + COutPoint utxo = s.getStake().getUTXO(); + UniValue stake(UniValue::VOBJ); + stake.pushKV("txid", utxo.GetTxId().GetHex()); + stake.pushKV("vout", int64_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", HexStr(s.getSignature())); + stakes.push_back(stake); + } + result.pushKV("stakes", stakes); + + return result; +} + static UniValue delegateavalancheproof(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ @@ -448,6 +532,7 @@ { "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"}}, diff --git a/test/functional/abc_rpc_avalancheproof.py b/test/functional/abc_rpc_avalancheproof.py --- a/test/functional/abc_rpc_avalancheproof.py +++ b/test/functional/abc_rpc_avalancheproof.py @@ -10,7 +10,12 @@ 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, + ser_uint256, +) from test_framework.mininode import P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.test_node import ErrorMatch @@ -62,9 +67,30 @@ 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 decodedproof["sequence"] == proof_sequence + assert decodedproof["expiration"] == proof_expiration + assert decodedproof["master"] == proof_master + assert decodedproof["proofid"] == ser_uint256(proofobj.proofid).hex() + assert decodedproof["stakes"][0]["txid"] == stakes[0]["txid"] + assert decodedproof["stakes"][0]["vout"] == stakes[0]["vout"] + assert decodedproof["stakes"][0]["height"] == stakes[0]["height"] + assert decodedproof["stakes"][0]["iscoinbase"] == stakes[0]["iscoinbase"] + assert decodedproof["stakes"][0]["signature"] == \ + proofobj.stakes[0].sig.hex() + + # 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