diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -461,6 +461,49 @@ } } +static UniValue sendavalancheproof(const Config &config, + const JSONRPCRequest &request) { + 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", "")}, + } + .Check(request); + + 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 (!g_avalanche->getProof(proofid) && !g_avalanche->addProof(proof)) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "The proof has conflicting utxo with an existing proof"); + } + + // TODO actually announce the proof via an inv message + // RelayProof(proofid, *node.connman); + + return true; +} + static UniValue verifyavalancheproof(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ @@ -496,6 +539,7 @@ { "avalanche", "decodeavalancheproof", decodeavalancheproof, {"proof"}}, { "avalanche", "delegateavalancheproof", delegateavalancheproof, {"proof", "privatekey", "publickey", "delegation"}}, { "avalanche", "getavalanchepeerinfo", getavalanchepeerinfo, {}}, + { "avalanche", "sendavalancheproof", sendavalancheproof, {"proof"}}, { "avalanche", "verifyavalancheproof", verifyavalancheproof, {"proof"}}, }; // clang-format on 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 @@ -9,6 +9,7 @@ from test_framework.avatools import ( create_coinbase_stakes, create_stakes, + get_proof_ids, ) from test_framework.key import ECKey, bytes_to_wif from test_framework.messages import ( @@ -232,28 +233,46 @@ "f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" "1321b91a79b82d1c2cfd47793261e4ba003cf5") - self.log.info("Check the verifyavalancheproof RPC") - - assert_raises_rpc_error(-22, "Proof must be an hexadecimal string", - node.verifyavalancheproof, "f00") - assert_raises_rpc_error(-22, "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") + self.log.info( + "Check the verifyavalancheproof and sendavalancheproof RPCs") + 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(bad_sig, "invalid-signature") + if self.is_wallet_compiled(): + check_rpc_failure(too_many_utxos, "too-many-utxos") + + conflicting_utxo = node.buildavalancheproof( + proof_sequence + 1, proof_expiration, proof_master, stakes) + assert_raises_rpc_error(-8, "The proof has conflicting utxo with an existing proof", + node.sendavalancheproof, conflicting_utxo) # Good proof assert node.verifyavalancheproof(proof) + proofid = FromHex(AvalancheProof(), proof).proofid + node.sendavalancheproof(proof) + assert proofid in get_proof_ids(node) + + # TODO Once implemented we expect the sendavalancheproof to trigger the + # sending of an inv message with our proof: + # + # def inv_found(): + # with p2p_lock: + # return peer.last_message.get( + # "inv") and peer.last_message["inv"].inv[-1].hash == proofid + # wait_until(inv_found) + self.log.info("Bad proof should be rejected at startup") self.stop_node(0) diff --git a/test/functional/test_framework/avatools.py b/test/functional/test_framework/avatools.py --- a/test/functional/test_framework/avatools.py +++ b/test/functional/test_framework/avatools.py @@ -7,6 +7,7 @@ from typing import Any, Optional, List, Dict from .messages import ( + AvalancheProof, CTransaction, FromHex, ToHex @@ -114,3 +115,8 @@ }) return stakes + + +def get_proof_ids(node): + return [FromHex(AvalancheProof(), peer['proof'] + ).proofid for peer in node.getavalanchepeerinfo()]