diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -59,6 +59,20 @@ }); } +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 UniValue addavalanchenode(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ @@ -71,6 +85,8 @@ "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."}, @@ -96,12 +112,28 @@ throw JSONRPCError(RPC_DESERIALIZATION_ERROR, error.original); } + const avalanche::ProofId &proofid = proof->getId(); if (key != proof->getMaster()) { - // TODO: we want to provide a proper delegation. - return false; + 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"); + } } - const avalanche::ProofId &proofid = proof->getId(); if (!registerProofIfNeeded(proof)) { return false; } @@ -376,11 +408,8 @@ std::unique_ptr dgb; if (request.params.size() >= 4 && !request.params[3].isNull()) { avalanche::Delegation dg; - bilingual_str error; - if (!avalanche::Delegation::FromHex(dg, request.params[3].get_str(), - error)) { - throw JSONRPCError(RPC_DESERIALIZATION_ERROR, error.original); - } + CPubKey auth; + verifyDelegationOrThrow(dg, request.params[3].get_str(), auth); if (dg.getProofId() != limitedProofId.computeProofId(dg.getProofMaster())) { @@ -388,14 +417,6 @@ "The delegation does not match the proof"); } - CPubKey auth; - avalanche::DelegationState dgState; - if (!dg.verify(dgState, auth)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, - "The delegation is invalid: " + - dgState.ToString()); - } - if (privkey.GetPubKey() != auth) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "The private key does not match the delegation"); diff --git a/test/functional/abc_rpc_addavalanchenode.py b/test/functional/abc_rpc_addavalanchenode.py --- a/test/functional/abc_rpc_addavalanchenode.py +++ b/test/functional/abc_rpc_addavalanchenode.py @@ -6,6 +6,14 @@ from test_framework.avatools import create_coinbase_stakes from test_framework.key import ECKey +from test_framework.messages import ( + AvalancheDelegation, + AvalancheDelegationLevel, + AvalancheProof, + FromHex, + hash256, + ser_string, +) from test_framework.p2p import P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( @@ -47,7 +55,7 @@ nodeid = add_interface_node(node) def check_addavalanchenode_error( - error_code, error_message, proof=proof, pubkey=proof_master): + error_code, error_message, proof=proof, pubkey=proof_master, delegation=None): assert_raises_rpc_error( error_code, error_message, @@ -55,6 +63,7 @@ nodeid, pubkey, proof, + delegation, ) self.log.info("Invalid proof") @@ -64,20 +73,78 @@ check_addavalanchenode_error(-22, "Proof has invalid format", proof="f000") - - self.log.info("Key mismatch") - random_privkey = ECKey() - random_privkey.generate() - random_pubkey = random_privkey.get_pubkey().get_bytes().hex() - assert not node.addavalanchenode(nodeid, random_pubkey, proof) + no_stake = node.buildavalancheproof( + proof_sequence, proof_expiration, proof_master, []) + assert not node.addavalanchenode(nodeid, proof_master, no_stake) self.log.info("Node doesn't exist") assert not node.addavalanchenode(nodeid + 1, proof_master, proof) - self.log.info("Invalid proof") - no_stake = node.buildavalancheproof( - proof_sequence, proof_expiration, proof_master, []) - assert not node.addavalanchenode(nodeid, proof_master, no_stake) + self.log.info("Invalid delegation") + dg_privkey = ECKey() + dg_privkey.generate() + dg_pubkey = dg_privkey.get_pubkey().get_bytes() + check_addavalanchenode_error(-22, + "Delegation must be an hexadecimal string", + pubkey=dg_pubkey.hex(), + delegation="not a delegation") + check_addavalanchenode_error(-22, + "Delegation has invalid format", + pubkey=dg_pubkey.hex(), + delegation="f000") + + self.log.info("Delegation mismatch with the proof") + delegation_wrong_proofid = AvalancheDelegation() + check_addavalanchenode_error(-8, + "The delegation does not match the proof", + pubkey=dg_pubkey.hex(), + delegation=delegation_wrong_proofid.serialize().hex()) + + proofobj = FromHex(AvalancheProof(), proof) + delegation = AvalancheDelegation( + limited_proofid=proofobj.limited_proofid, + proof_master=proofobj.master, + ) + + self.log.info("Delegation with bad signature") + bad_level = AvalancheDelegationLevel( + pubkey=dg_pubkey, + ) + delegation.levels.append(bad_level) + check_addavalanchenode_error(-8, + "The delegation is invalid", + pubkey=dg_pubkey.hex(), + delegation=delegation.serialize().hex()) + + delegation.levels = [] + level = AvalancheDelegationLevel( + pubkey=dg_pubkey, + sig=privkey.sign_schnorr( + hash256( + delegation.getid() + + ser_string(dg_pubkey) + ) + ) + ) + delegation.levels.append(level) + + self.log.info("Key mismatch with the proof") + check_addavalanchenode_error( + -5, + "The public key does not match the proof", + pubkey=dg_pubkey.hex(), + ) + + self.log.info("Key mismatch with the delegation") + random_privkey = ECKey() + random_privkey.generate() + random_pubkey = random_privkey.get_pubkey() + check_addavalanchenode_error( + -5, + "The public key does not match the delegation", + pubkey=random_pubkey.get_bytes().hex(), + delegation=delegation.serialize().hex(), + ) self.log.info("Happy path") assert node.addavalanchenode(nodeid, proof_master, proof) @@ -100,6 +167,14 @@ ) assert node.addavalanchenode(nodeid, hardcoded_pubkey, hardcoded_proof) + self.log.info("Add a node with a valid delegation") + assert node.addavalanchenode( + nodeid, + dg_pubkey.hex(), + proof, + delegation.serialize().hex(), + ) + self.log.info("Several nodes can share a proof") nodeid2 = add_interface_node(node) assert node.addavalanchenode(nodeid2, proof_master, proof)