diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -148,10 +148,6 @@ 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, @@ -210,6 +206,90 @@ 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{ @@ -449,6 +529,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 @@ -3,6 +3,7 @@ # 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 ( @@ -10,12 +11,17 @@ 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, ) @@ -62,9 +68,33 @@ 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