diff --git a/src/avalanche/delegation.h b/src/avalanche/delegation.h index dec33816d..9236674c4 100644 --- a/src/avalanche/delegation.h +++ b/src/avalanche/delegation.h @@ -1,60 +1,63 @@ // Copyright (c) 2020 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_AVALANCHE_DELEGATION_H #define BITCOIN_AVALANCHE_DELEGATION_H #include #include #include #include #include #include namespace avalanche { class DelegationState; class Proof; class Delegation { LimitedProofId limitedProofid; CPubKey proofMaster; DelegationId dgid; DelegationId computeDelegationId() const; struct Level { CPubKey pubkey; SchnorrSig sig; SERIALIZE_METHODS(Level, obj) { READWRITE(obj.pubkey, obj.sig); } }; std::vector levels; friend class DelegationBuilder; Delegation(const LimitedProofId &limitedProofid_, const CPubKey &proofMaster_, const DelegationId &dgid_, std::vector levels_) : limitedProofid(limitedProofid_), proofMaster(proofMaster_), dgid(dgid_), levels(std::move(levels_)) {} public: explicit Delegation() {} const DelegationId &getId() const { return dgid; } + const LimitedProofId &getLimitedProofId() const { return limitedProofid; } + const CPubKey &getProofMaster() const { return proofMaster; } + ProofId getProofId() const; SERIALIZE_METHODS(Delegation, obj) { READWRITE(obj.limitedProofid, obj.proofMaster, obj.levels); SER_READ(obj, obj.dgid = obj.computeDelegationId()); } bool verify(DelegationState &state, CPubKey &auth) const; }; } // namespace avalanche #endif // BITCOIN_AVALANCHE_DELEGATION_H diff --git a/src/avalanche/delegationbuilder.cpp b/src/avalanche/delegationbuilder.cpp index 4a717b53c..8763bb092 100644 --- a/src/avalanche/delegationbuilder.cpp +++ b/src/avalanche/delegationbuilder.cpp @@ -1,70 +1,71 @@ // Copyright (c) 2020 The Bitcoin developers // 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 namespace avalanche { -DelegationBuilder::DelegationBuilder(const Proof &p) - : limitedProofid(p.getLimitedId()), proofid(p.getId()), dgid(proofid) { - levels.push_back({p.getMaster(), {}}); +DelegationBuilder::DelegationBuilder(const LimitedProofId <dProofId, + const CPubKey &proofMaster, + const DelegationId &delegationId) + : limitedProofid(ltdProofId), dgid(delegationId) { + levels.push_back({proofMaster, {}}); } -bool DelegationBuilder::importDelegation(const Delegation &d) { - if (d.getProofId() != proofid) { - return false; - } +DelegationBuilder::DelegationBuilder(const LimitedProofId <dProofId, + const CPubKey &proofMaster) + : DelegationBuilder(ltdProofId, proofMaster, + DelegationId(ltdProofId.computeProofId(proofMaster))) {} - if (levels.size() > 1) { - // We already imported a delegation - return false; - } - - if (!d.levels.size()) { - return true; - } +DelegationBuilder::DelegationBuilder(const Proof &p) + : DelegationBuilder(p.getLimitedId(), p.getMaster(), + DelegationId(p.getId())) {} - dgid = d.getId(); - for (auto &l : d.levels) { +DelegationBuilder::DelegationBuilder(const Delegation &dg) + : DelegationBuilder(dg.getLimitedProofId(), dg.getProofMaster(), + dg.getId()) { + for (auto &l : dg.levels) { levels.back().sig = l.sig; levels.push_back({l.pubkey, {}}); } - - return true; } bool DelegationBuilder::addLevel(const CKey &delegatorKey, const CPubKey &delegatedPubKey) { // Ensures that the private key provided is the one we need. if (levels.back().pubkey != delegatorKey.GetPubKey()) { return false; } CHashWriter ss(SER_GETHASH, 0); ss << dgid; ss << delegatedPubKey; auto hash = ss.GetHash(); if (!delegatorKey.SignSchnorr(hash, levels.back().sig)) { return false; } dgid = DelegationId(hash); levels.push_back({delegatedPubKey, {}}); return true; } Delegation DelegationBuilder::build() const { std::vector dglvls; for (size_t i = 1; i < levels.size(); i++) { dglvls.push_back({levels[i].pubkey, levels[i - 1].sig}); } return Delegation(limitedProofid, levels[0].pubkey, dgid, std::move(dglvls)); } } // namespace avalanche diff --git a/src/avalanche/delegationbuilder.h b/src/avalanche/delegationbuilder.h index 8b6188709..948136af6 100644 --- a/src/avalanche/delegationbuilder.h +++ b/src/avalanche/delegationbuilder.h @@ -1,39 +1,43 @@ // Copyright (c) 2020 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_AVALANCHE_DELEGATIONBUILDER_H #define BITCOIN_AVALANCHE_DELEGATIONBUILDER_H #include -#include -#include #include class CKey; +class CPubKey; namespace avalanche { +struct LimitedProofId; class Proof; class DelegationBuilder { LimitedProofId limitedProofid; - ProofId proofid; DelegationId dgid; std::vector levels; + DelegationBuilder(const LimitedProofId <dProofId, + const CPubKey &proofMaster, + const DelegationId &delegationId); + public: + DelegationBuilder(const LimitedProofId <dProofId, + const CPubKey &proofMaster); explicit DelegationBuilder(const Proof &p); - - bool importDelegation(const Delegation &d); + explicit DelegationBuilder(const Delegation &dg); bool addLevel(const CKey &delegatorKey, const CPubKey &delegatedPubKey); Delegation build() const; }; } // namespace avalanche #endif // BITCOIN_AVALANCHE_DELEGATIONBUILDER_H diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp index 779a6e280..66a2d79a5 100644 --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -1,510 +1,501 @@ // Copyright (c) 2020 The Bitcoin developers // 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 #include #include #include #include #include #include static UniValue getavalanchekey(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ "getavalanchekey", "Returns the key used to sign avalanche messages.\n", {}, RPCResult{RPCResult::Type::STR_HEX, "", ""}, RPCExamples{HelpExampleRpc("getavalanchekey", "")}, } .Check(request); if (!g_avalanche) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); } return HexStr(g_avalanche->getSessionPubKey()); } static CPubKey ParsePubKey(const UniValue ¶m) { const std::string keyHex = param.get_str(); if ((keyHex.length() != 2 * CPubKey::COMPRESSED_SIZE && keyHex.length() != 2 * CPubKey::SIZE) || !IsHex(keyHex)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid public key: %s\n", keyHex)); } return HexToPubKey(keyHex); } static UniValue addavalanchenode(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ "addavalanchenode", "Add a node in the set of peers to poll for avalanche.\n", { {"nodeid", RPCArg::Type::NUM, RPCArg::Optional::NO, "Node to be added to avalanche."}, {"publickey", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The public key of the node."}, {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Proof that the node is not a sybil."}, }, RPCResult{RPCResult::Type::BOOL, "success", "Whether the addition succeeded or not."}, RPCExamples{ HelpExampleRpc("addavalanchenode", "5, \"\", \"\"")}, } .Check(request); 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]); auto proof = std::make_shared(); bilingual_str error; if (!avalanche::Proof::FromHex(*proof, request.params[2].get_str(), error)) { throw JSONRPCError(RPC_INVALID_PARAMETER, error.original); } if (key != proof->getMaster()) { // TODO: we want to provide a proper delegation. return false; } return g_avalanche->addNode(nodeid, proof, avalanche::DelegationBuilder(*proof).build()); } static UniValue buildavalancheproof(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ "buildavalancheproof", "Build a proof for avalanche's sybil resistance.\n", { {"sequence", RPCArg::Type::NUM, RPCArg::Optional::NO, "The proof's sequence"}, {"expiration", RPCArg::Type::NUM, RPCArg::Optional::NO, "A timestamp indicating when the proof expire"}, {"master", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The master public key"}, { "stakes", RPCArg::Type::ARR, RPCArg::Optional::NO, "The stakes to be signed and associated private keys", { { "stake", RPCArg::Type::OBJ, RPCArg::Optional::NO, "A stake to be attached to this proof", { {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, {"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The amount in this UTXO"}, {"height", RPCArg::Type::NUM, RPCArg::Optional::NO, "The height at which this UTXO was mined"}, {"iscoinbase", RPCArg::Type::BOOL, /* default */ "false", "Indicate wether the UTXO is a coinbase"}, {"privatekey", RPCArg::Type::STR, RPCArg::Optional::NO, "private key in base58-encoding"}, }, }, }, }, }, RPCResult{RPCResult::Type::STR_HEX, "proof", "A string that is a serialized, hex-encoded proof data."}, RPCExamples{HelpExampleRpc("buildavalancheproof", "0 1234567800 \"\" []")}, } .Check(request); RPCTypeCheck(request.params, {UniValue::VNUM, UniValue::VNUM, UniValue::VSTR, UniValue::VARR}); const uint64_t sequence = request.params[0].get_int64(); const int64_t expiration = request.params[1].get_int64(); avalanche::ProofBuilder pb(sequence, expiration, ParsePubKey(request.params[2])); const UniValue &stakes = request.params[3].get_array(); for (size_t i = 0; i < stakes.size(); i++) { const UniValue &stake = stakes[i]; RPCTypeCheckObj(stake, { {"txid", UniValue::VSTR}, {"vout", UniValue::VNUM}, // "amount" is also required but check is done below // due to UniValue::VNUM erroneously not accepting // quoted numerics (which are valid JSON) {"height", UniValue::VNUM}, {"privatekey", UniValue::VSTR}, }); int nOut = find_value(stake, "vout").get_int(); if (nOut < 0) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "vout must be positive"); } const int height = find_value(stake, "height").get_int(); if (height < 1) { throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "height must be positive"); } const TxId txid(ParseHashO(stake, "txid")); const COutPoint utxo(txid, nOut); if (!stake.exists("amount")) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Missing amount"); } const Amount amount = AmountFromValue(find_value(stake, "amount")); const UniValue &iscbparam = find_value(stake, "iscoinbase"); const bool iscoinbase = iscbparam.isNull() ? false : iscbparam.get_bool(); CKey key = DecodeSecret(find_value(stake, "privatekey").get_str()); if (!pb.addUTXO(utxo, amount, uint32_t(height), iscoinbase, std::move(key))) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid private key"); } } const avalanche::Proof proof = pb.build(); CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss << proof; 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, "limitedid", "A hash of the proof data excluding the master key."}, {RPCResult::Type::STR_HEX, "proofid", "A hash of the limitedid and master key."}, {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("limitedid", proof.getLimitedId().ToString()); 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", 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{ "delegateavalancheproof", "Delegate the avalanche proof to another public key.\n", { - {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, - "The proof to be delegated."}, + {"limitedproofid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, + "The limited id of 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", - "\"\" \"\" \"\"")}, + 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; - bilingual_str error; - if (!avalanche::Proof::FromHex(proof, request.params[0].get_str(), error)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, error.original); - } - - avalanche::ProofValidationState proofState; - if (!proof.verify(proofState)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "The proof is invalid"); - } + avalanche::LimitedProofId limitedProofId{ + ParseHashV(request.params[0], "limitedproofid")}; 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; + std::unique_ptr dgb; 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; - if (dg.getProofId() != proof.getId()) { + if (dg.getProofId() != + limitedProofId.computeProofId(dg.getProofMaster())) { throw JSONRPCError( RPC_INVALID_PARAMETER, "The supplied delegation does not match the proof"); } + CPubKey auth; avalanche::DelegationState dgState; if (!dg.verify(dgState, 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"); + if (privkey.GetPubKey() != auth) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "The supplied private key does not match the delegation"); } + dgb = std::make_unique(dg); } else { - auth = proof.getMaster(); - } - - if (privkey.GetPubKey() != auth) { - throw JSONRPCError( - RPC_INVALID_PARAMETER, - "The private key does not match the proof or the delegation"); + dgb = std::make_unique( + limitedProofId, privkey.GetPubKey()); } - if (!dgb.addLevel(privkey, pubkey)) { + if (!dgb->addLevel(privkey, pubkey)) { throw JSONRPCError(RPC_MISC_ERROR, "Unable to build the delegation"); } CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); - ss << dgb.build(); + ss << dgb->build(); return HexStr(ss); } static UniValue getavalanchepeerinfo(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ "getavalanchepeerinfo", "Returns data about each connected avalanche peer as a json array of " "objects.\n", {}, RPCResult{ RPCResult::Type::ARR, "", "", {{ RPCResult::Type::OBJ, "", "", {{ {RPCResult::Type::NUM, "peerid", "The peer id"}, {RPCResult::Type::STR_HEX, "proof", "The avalanche proof used by this peer"}, {RPCResult::Type::ARR, "nodes", "", { {RPCResult::Type::NUM, "nodeid", "Node id, as returned by getpeerinfo"}, }}, }}, }}, }, RPCExamples{HelpExampleCli("getavalanchepeerinfo", "") + HelpExampleRpc("getavalanchepeerinfo", "")}, } .Check(request); if (!g_avalanche) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Avalanche is not initialized"); } UniValue ret(UniValue::VARR); for (const auto &peer : g_avalanche->getPeers()) { UniValue obj(UniValue::VOBJ); CDataStream serproof(SER_NETWORK, PROTOCOL_VERSION); serproof << *peer.proof; obj.pushKV("peerid", uint64_t(peer.peerid)); obj.pushKV("proof", HexStr(serproof)); UniValue nodes(UniValue::VARR); for (const auto &id : g_avalanche->getNodeIdsForPeer(peer.peerid)) { nodes.push_back(id); } obj.pushKV("nodes", nodes); obj.pushKV("nodecount", uint64_t(peer.node_count)); ret.push_back(obj); } return ret; } static UniValue verifyavalancheproof(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ "verifyavalancheproof", "Verify an avalanche proof is valid and return the error otherwise.\n", { {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Proof to verify."}, }, RPCResult{RPCResult::Type::BOOL, "success", "Whether the proof is valid or not."}, RPCExamples{HelpExampleRpc("verifyavalancheproof", "\"\"")}, } .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_INVALID_PARAMETER, error.original); } NodeContext &node = EnsureNodeContext(request.context); avalanche::ProofValidationState state; { LOCK(cs_main); if (!proof.verify(state, node.chainman->ActiveChainstate().CoinsTip())) { throw JSONRPCError(RPC_INVALID_PARAMETER, "The proof is invalid: " + state.ToString()); } } return true; } void RegisterAvalancheRPCCommands(CRPCTable &t) { // clang-format off static const CRPCCommand commands[] = { // category name actor (function) argNames // ------------------- ------------------------ ---------------------- ---------- { "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"}}, }; // clang-format on for (unsigned int vcidx = 0; vcidx < ARRAYLEN(commands); vcidx++) { t.appendCommand(commands[vcidx].name, &commands[vcidx]); } } diff --git a/test/functional/abc_p2p_avalanche.py b/test/functional/abc_p2p_avalanche.py index d913ee18a..2f6d24b7e 100755 --- a/test/functional/abc_p2p_avalanche.py +++ b/test/functional/abc_p2p_avalanche.py @@ -1,414 +1,419 @@ #!/usr/bin/env python3 # Copyright (c) 2020-2021 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the resolution of forks via avalanche.""" import random import struct from test_framework.avatools import create_coinbase_stakes from test_framework.key import ( bytes_to_wif, ECKey, ECPubKey, ) from test_framework.p2p import P2PInterface, p2p_lock from test_framework.messages import ( AvalancheDelegation, + AvalancheProof, AvalancheResponse, AvalancheVote, CInv, FromHex, hash256, msg_avahello, msg_avapoll, msg_tcpavaresponse, NODE_AVALANCHE, NODE_NETWORK, TCPAvalancheResponse, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, wait_until, ) BLOCK_ACCEPTED = 0 BLOCK_INVALID = 1 BLOCK_PARKED = 2 BLOCK_FORK = 3 BLOCK_UNKNOWN = -1 BLOCK_MISSING = -2 BLOCK_PENDING = -3 QUORUM_NODE_COUNT = 16 class TestNode(P2PInterface): def __init__(self): self.round = 0 self.avahello = None self.avaresponses = [] self.avapolls = [] super().__init__() def peer_connect(self, *args, **kwargs): create_conn = super().peer_connect(*args, **kwargs) # Save the nonce and extra entropy so they can be reused later. self.local_nonce = self.on_connection_send_msg.nNonce self.local_extra_entropy = self.on_connection_send_msg.nExtraEntropy return create_conn def on_version(self, message): super().on_version(message) # Save the nonce and extra entropy so they can be reused later. self.remote_nonce = message.nNonce self.remote_extra_entropy = message.nExtraEntropy def on_avaresponse(self, message): self.avaresponses.append(message.response) def on_avapoll(self, message): self.avapolls.append(message.poll) def on_avahello(self, message): assert(self.avahello is None) self.avahello = message def send_avaresponse(self, round, votes, privkey): response = AvalancheResponse(round, 0, votes) sig = privkey.sign_schnorr(response.get_hash()) msg = msg_tcpavaresponse() msg.response = TCPAvalancheResponse(response, sig) self.send_message(msg) def wait_for_avaresponse(self, timeout=5): wait_until( lambda: len(self.avaresponses) > 0, timeout=timeout, lock=p2p_lock) with p2p_lock: return self.avaresponses.pop(0) def send_poll(self, hashes): msg = msg_avapoll() msg.poll.round = self.round self.round += 1 for h in hashes: msg.poll.invs.append(CInv(2, h)) self.send_message(msg) def get_avapoll_if_available(self): with p2p_lock: return self.avapolls.pop(0) if len(self.avapolls) > 0 else None def wait_for_avahello(self, timeout=5): wait_until( lambda: self.avahello is not None, timeout=timeout, lock=p2p_lock) with p2p_lock: return self.avahello def send_avahello(self, delegation_hex: str, delegated_privkey: ECKey): delegation = FromHex(AvalancheDelegation(), delegation_hex) local_sighash = hash256( delegation.getid() + struct.pack(" 2): unexpected-ava-response']): # unknown voting round poll_node.send_avaresponse( round=2**32 - 1, votes=[], privkey=privkey) self.log.info( "Check the node is signalling the avalanche service bit only if there is a proof.") assert_equal( int(node.getnetworkinfo()['localservices'], 16) & NODE_AVALANCHE, 0) # Restart the node self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ]) assert_equal( int(node.getnetworkinfo()['localservices'], 16) & NODE_AVALANCHE, NODE_AVALANCHE) self.log.info("Test the avahello signature (node -> P2PInterface)") good_interface = get_node() avahello = good_interface.wait_for_avahello().hello avakey.set(bytes.fromhex(node.getavalanchekey())) assert avakey.verify_schnorr( avahello.sig, avahello.get_sighash(good_interface)) self.log.info("Test the avahello signature (P2PInterface -> node)") stakes = create_coinbase_stakes(node, [blockhashes[1]], addrkey0.key) interface_proof_hex = node.buildavalancheproof( proof_sequence, proof_expiration, pubkey.get_bytes().hex(), stakes) + limited_id = FromHex( + AvalancheProof(), + interface_proof_hex).limited_proofid + # delegate delegated_key = ECKey() delegated_key.generate() interface_delegation_hex = node.delegateavalancheproof( - interface_proof_hex, + f"{limited_id:0{64}x}", bytes_to_wif(privkey.get_bytes()), delegated_key.get_pubkey().get_bytes().hex(), None) good_interface.send_avahello(interface_delegation_hex, delegated_key) # Quick check that the good interface is still connected # FIXME: when proof relaying is implemented, replace this with a check # that the interface is added as a peer. good_interface.sync_with_ping() self.log.info("Test that wrong avahello signature causes a ban") bad_interface = get_node() wrong_key = ECKey() wrong_key.generate() with self.nodes[0].assert_debug_log( ["Misbehaving", "peer=1 (0 -> 100) BAN THRESHOLD EXCEEDED: invalid-avahello-signature"]): bad_interface.send_avahello(interface_delegation_hex, wrong_key) bad_interface.wait_for_disconnect() if __name__ == '__main__': AvalancheTest().main() diff --git a/test/functional/abc_rpc_avalancheproof.py b/test/functional/abc_rpc_avalancheproof.py index 550b13d74..ad928284e 100644 --- a/test/functional/abc_rpc_avalancheproof.py +++ b/test/functional/abc_rpc_avalancheproof.py @@ -1,331 +1,316 @@ #!/usr/bin/env python3 # Copyright (c) 2021 The Bitcoin developers # 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 ( create_coinbase_stakes, create_stakes, ) from test_framework.key import ECKey, bytes_to_wif from test_framework.messages import ( AvalancheDelegation, AvalancheDelegationLevel, AvalancheProof, FromHex, ) from test_framework.p2p 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, ) AVALANCHE_MAX_PROOF_STAKES = 1000 PROOF_DUST_THRESHOLD = 1.0 """Minimum amount per UTXO in a proof (in coins, not in satoshis)""" def add_interface_node(test_node) -> str: """Create a mininode, connect it to test_node, return the nodeid of the mininode as registered by test_node. """ n = P2PInterface() test_node.add_p2p_connection(n) n.wait_for_verack() return test_node.getpeerinfo()[-1]['id'] class AvalancheProofTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [['-enableavalanche=1', '-avacooldown=0'], ] self.supports_cli = False self.rpc_timeout = 120 def run_test(self): node = self.nodes[0] addrkey0 = node.get_deterministic_priv_key() blockhashes = node.generatetoaddress(100, addrkey0.address) self.log.info( "Make build a valid proof and restart the node to use it") privkey = ECKey() privkey.set(bytes.fromhex( "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) def get_hex_pubkey(privkey): return privkey.get_pubkey().get_bytes().hex() 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, stakes) self.log.info("Test decodeavalancheproof RPC") proofobj = FromHex(AvalancheProof(), proof) decodedproof = node.decodeavalancheproof(proof) + limited_id_hex = f"{proofobj.limited_proofid:0{64}x}" 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["limitedid"], - f"{proofobj.limited_proofid:0{64}x}") + assert_equal(decodedproof["limitedid"], limited_id_hex) 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 self.restart_node(0, self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", "-minimumchainwork=0x{:x}".format(minchainwork), ]) self.log.info( "The proof verification should be delayed until IBD is complete") assert node.getblockchaininfo()["initialblockdownload"] is True # Our proof cannot be verified during IBD, so we should have no peer assert not node.getavalanchepeerinfo() # Mining a few more blocks should cause us to leave IBD node.generate(2) # Our proof is now verified and our node is added as a peer assert node.getblockchaininfo()["initialblockdownload"] is False wait_until(lambda: len(node.getavalanchepeerinfo()) == 1, timeout=5) if self.is_wallet_compiled(): self.log.info( "A proof using the maximum number of stakes is accepted...") new_blocks = node.generate(AVALANCHE_MAX_PROOF_STAKES // 10 + 1) # confirm the coinbase UTXOs node.generate(101) too_many_stakes = create_stakes( node, new_blocks, AVALANCHE_MAX_PROOF_STAKES + 1) maximum_stakes = too_many_stakes[:-1] good_proof = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, maximum_stakes) peerid1 = add_interface_node(node) assert node.addavalanchenode(peerid1, proof_master, good_proof) self.log.info( "A proof using too many stakes should be rejected...") too_many_utxos = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, too_many_stakes) 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, + limited_id_hex, 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 - no_stake = node.buildavalancheproof( - proof_sequence, proof_expiration, proof_master, []) - assert_raises_rpc_error(-8, "The proof is invalid", - node.delegateavalancheproof, - no_stake, - bytes_to_wif(privkey.get_bytes()), - random_pubkey, - ) + no_stake = node.buildavalancheproof(proof_sequence, proof_expiration, + proof_master, []) # Invalid privkey assert_raises_rpc_error(-5, "The private key is invalid", node.delegateavalancheproof, - proof, + limited_id_hex, bytes_to_wif(bytes(32)), random_pubkey, ) # Invalid delegation bad_dg = AvalancheDelegation() assert_raises_rpc_error(-8, "The supplied delegation does not match the proof", node.delegateavalancheproof, - proof, + limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, bad_dg.serialize().hex(), ) # Still invalid, but with a matching proofid bad_dg.limited_proofid = proofobj.limited_proofid bad_dg.proof_master = proofobj.master bad_dg.levels = [AvalancheDelegationLevel()] assert_raises_rpc_error(-8, "The supplied delegation is not valid", node.delegateavalancheproof, - proof, + limited_id_hex, 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", + assert_raises_rpc_error(-8, "The supplied private key does not match the delegation", node.delegateavalancheproof, - proof, + limited_id_hex, bytes_to_wif(privkey.get_bytes()), random_pubkey, delegation, ) # Test invalid proofs dust = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount="0")) dust_amount = Decimal(f"{PROOF_DUST_THRESHOLD * 0.9999:.4f}") dust2 = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount=str(dust_amount))) duplicate_stake = node.buildavalancheproof( proof_sequence, proof_expiration, proof_master, create_coinbase_stakes(node, [blockhashes[0]] * 2, addrkey0.key)) bad_sig = ("0b000000000000000c0000000000000021030b4c866585dd868a9d62348" "a9cd008d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085" "583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30000000000f20" "52a0100000003000000210227d85ba011276cf25b51df6a188b75e604b3" "8770a462b2d0e9fb2fc839ef5d3faf07f001dd38e9b4a43d07d5d449cc0" "f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" "1321b91a79b82d1c2cfd47793261e4ba003cf5") self.log.info("Check the verifyavalancheproof RPC") assert_raises_rpc_error(-8, "Proof must be an hexadecimal string", node.verifyavalancheproof, "f00") assert_raises_rpc_error(-8, "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") # Good proof assert node.verifyavalancheproof(proof) self.log.info("Bad proof should be rejected at startup") self.stop_node(0) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avasessionkey=0", ], expected_msg="Error: the avalanche session key is invalid", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), ], expected_msg="Error: the avalanche master key is missing for the avalanche proof", ) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=0", ], expected_msg="Error: the avalanche master key is invalid", ) def check_proof_init_error(proof, message): node.assert_start_raises_init_error( self.extra_args[0] + [ "-avaproof={}".format(proof), "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: " + message, ) check_proof_init_error(no_stake, "the avalanche proof has no stake") check_proof_init_error(dust, "the avalanche proof stake is too low") check_proof_init_error(dust2, "the avalanche proof stake is too low") check_proof_init_error(duplicate_stake, "the avalanche proof has duplicated stake") check_proof_init_error(bad_sig, "the avalanche proof has invalid stake signatures") if self.is_wallet_compiled(): # The too many utxos case creates a proof which is that large that it # cannot fit on the command line append_config(node.datadir, ["avaproof={}".format(too_many_utxos)]) node.assert_start_raises_init_error( self.extra_args[0] + [ "-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN", ], expected_msg="Error: the avalanche proof has too many utxos", match=ErrorMatch.PARTIAL_REGEX, ) if __name__ == '__main__': AvalancheProofTest().main()