diff --git a/src/avalanche/delegationbuilder.h b/src/avalanche/delegationbuilder.h --- a/src/avalanche/delegationbuilder.h +++ b/src/avalanche/delegationbuilder.h @@ -24,7 +24,9 @@ std::vector levels; public: - DelegationBuilder(const Proof &p); + explicit DelegationBuilder(const Proof &p); + + bool importDelegation(const Delegation &d); bool addLevel(const CKey &key, const CPubKey &newMaster); diff --git a/src/avalanche/delegationbuilder.cpp b/src/avalanche/delegationbuilder.cpp --- a/src/avalanche/delegationbuilder.cpp +++ b/src/avalanche/delegationbuilder.cpp @@ -13,6 +13,29 @@ levels.push_back({p.getMaster(), {}}); } +bool DelegationBuilder::importDelegation(const Delegation &d) { + if (d.getProofId() != proofid) { + return false; + } + + if (levels.size() > 1) { + // We already imported a delegation + return false; + } + + if (!d.levels.size()) { + return true; + } + + dgid = d.getId(); + for (auto &l : d.levels) { + levels.back().sig = l.sig; + levels.push_back({l.pubkey, {}}); + } + + return true; +} + bool DelegationBuilder::addLevel(const CKey &key, const CPubKey &master) { // Ensures that the private key provided is the one we need. if (levels.back().pubkey != key.GetPubKey()) { diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -2,11 +2,13 @@ // 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 @@ -205,6 +207,95 @@ return HexStr(ss); } +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; + { + CDataStream ss(ParseHexV(request.params[0], "proof"), SER_NETWORK, + PROTOCOL_VERSION); + ss >> proof; + } + 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{ @@ -317,6 +408,7 @@ { "avalanche", "getavalanchekey", getavalanchekey, {}}, { "avalanche", "addavalanchenode", addavalanchenode, {"nodeid"}}, { "avalanche", "buildavalancheproof", buildavalancheproof, {"sequence", "expiration", "master", "stakes"}}, + { "avalanche", "delegateavalancheproof", delegateavalancheproof, {"proof", "privatekey", "publickey", "delegation"}}, { "avalanche", "getavalanchepeerinfo", getavalanchepeerinfo, {}}, }; // 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 @@ -5,13 +5,15 @@ """Test building avalanche proofs and using them to add avalanche peers.""" from test_framework.avatools import get_stakes -from test_framework.key import ECKey +from test_framework.key import ECKey, bytes_to_wif +from test_framework.messages import AvalancheDelegation 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, wait_until, + assert_raises_rpc_error, ) AVALANCHE_MAX_PROOF_STAKES = 1000 @@ -47,7 +49,10 @@ privkey.set(bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) - proof_master = privkey.get_pubkey().get_bytes().hex() + def get_hex_pubkey(privkey): + return privkey.get_pubkey().get_bytes().hex() + + proof_master = get_hex_pubkey(privkey) proof_sequence = 11 proof_expiration = 12 proof = node.buildavalancheproof( @@ -93,6 +98,72 @@ 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 + assert_raises_rpc_error(-8, "The proof is invalid", + node.delegateavalancheproof, + too_many_utxos, + 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 self.log.info("Bad proof should be rejected at startup") no_stake = node.buildavalancheproof(