Page MenuHomePhabricator

No OneTemporary

diff --git a/src/avalanche/delegation.h b/src/avalanche/delegation.h
index 4987457ef..46b6d95fe 100644
--- a/src/avalanche/delegation.h
+++ b/src/avalanche/delegation.h
@@ -1,69 +1,70 @@
// 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 <avalanche/delegationid.h>
#include <avalanche/proofid.h>
#include <key.h>
#include <pubkey.h>
#include <serialize.h>
#include <vector>
struct bilingual_str;
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<Level> levels;
friend class DelegationBuilder;
Delegation(const LimitedProofId &limitedProofid_,
const CPubKey &proofMaster_, const DelegationId &dgid_,
std::vector<Level> levels_)
: limitedProofid(limitedProofid_), proofMaster(proofMaster_),
dgid(dgid_), levels(std::move(levels_)) {}
public:
explicit Delegation() {}
static bool FromHex(Delegation &dg, const std::string &dgHex,
bilingual_str &errorOut);
const DelegationId &getId() const { return dgid; }
const LimitedProofId &getLimitedProofId() const { return limitedProofid; }
const CPubKey &getProofMaster() const { return proofMaster; }
const CPubKey &getDelegatedPubkey() const;
+ const std::vector<Level> &getLevels() const { return levels; }
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/rpc/avalanche.cpp b/src/rpc/avalanche.cpp
index 3d05143cf..a44e16912 100644
--- a/src/rpc/avalanche.cpp
+++ b/src/rpc/avalanche.cpp
@@ -1,930 +1,1016 @@
// 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 <avalanche/avalanche.h>
#include <avalanche/delegation.h>
#include <avalanche/delegationbuilder.h>
#include <avalanche/peermanager.h>
#include <avalanche/processor.h>
#include <avalanche/proof.h>
#include <avalanche/proofbuilder.h>
#include <avalanche/validation.h>
#include <config.h>
#include <core_io.h>
#include <key_io.h>
#include <net_processing.h>
#include <node/context.h>
#include <rpc/blockchain.h>
#include <rpc/server.h>
#include <rpc/util.h>
#include <util/strencodings.h>
#include <util/translation.h>
#include <univalue.h>
static RPCHelpMan getavalanchekey() {
return RPCHelpMan{
"getavalanchekey",
"Returns the key used to sign avalanche messages.\n",
{},
RPCResult{RPCResult::Type::STR_HEX, "", ""},
RPCExamples{HelpExampleRpc("getavalanchekey", "")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
if (!g_avalanche) {
throw JSONRPCError(RPC_INTERNAL_ERROR,
"Avalanche is not initialized");
}
return HexStr(g_avalanche->getSessionPubKey());
},
};
}
static CPubKey ParsePubKey(const UniValue &param) {
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 bool registerProofIfNeeded(avalanche::ProofRef proof) {
return g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) {
return pm.getProof(proof->getId()) ||
pm.registerProof(std::move(proof));
});
}
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 void verifyProofOrThrow(const NodeContext &node, avalanche::Proof &proof,
const std::string &proofHex) {
bilingual_str error;
if (!avalanche::Proof::FromHex(proof, proofHex, error)) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, error.original);
}
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());
}
}
}
static RPCHelpMan addavalanchenode() {
return 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."},
{"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."},
RPCExamples{
HelpExampleRpc("addavalanchenode", "5, \"<pubkey>\", \"<proof>\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
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();
CPubKey key = ParsePubKey(request.params[1]);
auto proof = std::make_shared<avalanche::Proof>();
NodeContext &node = EnsureNodeContext(request.context);
verifyProofOrThrow(node, *proof, request.params[2].get_str());
const avalanche::ProofId &proofid = proof->getId();
if (key != proof->getMaster()) {
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");
}
}
if (!registerProofIfNeeded(proof)) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"The proof has conflicting utxos");
}
if (!node.connman->ForNode(nodeid, [&](CNode *pnode) {
// FIXME This is not thread safe, and might cause issues if
// the unlikely event the peer sends an avahello message at
// the same time.
if (!pnode->m_avalanche_state) {
pnode->m_avalanche_state =
std::make_unique<CNode::AvalancheState>();
}
pnode->m_avalanche_state->pubkey = std::move(key);
return true;
})) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("The node does not exist: %d", nodeid));
;
}
return g_avalanche->withPeerManager(
[&](avalanche::PeerManager &pm) {
if (!pm.addNode(nodeid, proofid)) {
return false;
}
pm.addUnbroadcastProof(proofid);
return true;
});
},
};
}
static RPCHelpMan buildavalancheproof() {
return 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, RPCArg::Optional::NO,
"The master private key in base58-encoding"},
{
"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"},
},
},
},
},
{"payoutAddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"A payout address (not required for legacy proofs)"},
},
RPCResult{RPCResult::Type::STR_HEX, "proof",
"A string that is a serialized, hex-encoded proof data."},
RPCExamples{HelpExampleRpc("buildavalancheproof",
"0 1234567800 \"<master>\" []")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
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();
CKey masterKey = DecodeSecret(request.params[2].get_str());
if (!masterKey.IsValid()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid master key");
}
CTxDestination payoutAddress = CNoDestination();
if (!avalanche::Proof::useLegacy(gArgs)) {
if (request.params[4].isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"A payout address is required if "
"`-legacyavaproof` is false");
}
payoutAddress = DecodeDestination(request.params[4].get_str(),
config.GetChainParams());
if (!IsValidDestination(payoutAddress)) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Invalid payout address");
}
}
avalanche::ProofBuilder pb(sequence, expiration, masterKey,
GetScriptForDestination(payoutAddress));
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 cannot be negative");
}
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 (!key.IsValid()) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Invalid private key");
}
if (!pb.addUTXO(utxo, amount, uint32_t(height), iscoinbase,
std::move(key))) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Duplicated stake");
}
}
const avalanche::ProofRef proof = pb.build();
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << *proof;
return HexStr(ss);
},
};
}
static RPCHelpMan decodeavalancheproof() {
return RPCHelpMan{
"decodeavalancheproof",
"Convert a serialized, hex-encoded proof, into JSON object. "
"The validity of the proof is not verified.\n",
{
{"proof", 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, "signature",
"The proof signature (base64 encoded). Not available when "
"-legacyavaproof is enabled."},
{RPCResult::Type::OBJ,
"payoutscript",
"The proof payout script. Always empty when -legacyavaproof "
"is enabled.",
{
{RPCResult::Type::STR, "asm", "Decoded payout script"},
{RPCResult::Type::STR_HEX, "hex",
"Raw payout script in hex format"},
{RPCResult::Type::STR, "type",
"The output type (e.g. " + GetAllOutputTypes() + ")"},
{RPCResult::Type::NUM, "reqSigs",
"The required signatures"},
{RPCResult::Type::ARR,
"addresses",
"",
{
{RPCResult::Type::STR, "address", "eCash address"},
}},
}},
{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::STR_AMOUNT, "staked_amount",
"The total staked amount of this proof in " +
Currency::get().ticker + "."},
{RPCResult::Type::NUM, "score", "The score of this proof."},
{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", "\"<hex proof>\"") +
HelpExampleRpc("decodeavalancheproof", "\"<hex proof>\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
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()));
const auto signature = proof.getSignature();
if (signature) {
result.pushKV("signature", EncodeBase64(*signature));
}
const auto payoutScript = proof.getPayoutScript();
UniValue payoutScriptObj(UniValue::VOBJ);
ScriptPubKeyToUniv(payoutScript, payoutScriptObj,
/* fIncludeHex */ true);
result.pushKV("payoutscript", payoutScriptObj);
result.pushKV("limitedid", proof.getLimitedId().ToString());
result.pushKV("proofid", proof.getId().ToString());
result.pushKV("staked_amount", proof.getStakedAmount());
result.pushKV("score", uint64_t(proof.getScore()));
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()));
// Only PKHash destination is supported, so this is safe
stake.pushKV("address",
EncodeDestination(PKHash(s.getStake().getPubkey()),
config));
stake.pushKV("signature", EncodeBase64(s.getSignature()));
stakes.push_back(stake);
}
result.pushKV("stakes", stakes);
return result;
},
};
}
static RPCHelpMan delegateavalancheproof() {
return RPCHelpMan{
"delegateavalancheproof",
"Delegate the avalanche proof to another public key.\n",
{
{"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",
"\"<limitedproofid>\" \"<privkey>\" \"<pubkey>\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
RPCTypeCheck(request.params,
{UniValue::VSTR, UniValue::VSTR, UniValue::VSTR});
if (!g_avalanche) {
throw JSONRPCError(RPC_INTERNAL_ERROR,
"Avalanche is not initialized");
}
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]);
std::unique_ptr<avalanche::DelegationBuilder> dgb;
if (request.params.size() >= 4 && !request.params[3].isNull()) {
avalanche::Delegation dg;
CPubKey auth;
verifyDelegationOrThrow(dg, request.params[3].get_str(), auth);
if (dg.getProofId() !=
limitedProofId.computeProofId(dg.getProofMaster())) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"The delegation does not match the proof");
}
if (privkey.GetPubKey() != auth) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"The private key does not match the delegation");
}
dgb = std::make_unique<avalanche::DelegationBuilder>(dg);
} else {
dgb = std::make_unique<avalanche::DelegationBuilder>(
limitedProofId, privkey.GetPubKey());
}
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 RPCHelpMan decodeavalanchedelegation() {
+ return RPCHelpMan{
+ "decodeavalanchedelegation",
+ "Convert a serialized, hex-encoded avalanche proof delegation, into "
+ "JSON object. \n"
+ "The validity of the delegation is not verified.\n",
+ {
+ {"delegation", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
+ "The delegation hex string"},
+ },
+ RPCResult{
+ RPCResult::Type::OBJ,
+ "",
+ "",
+ {
+ {RPCResult::Type::STR_HEX, "pubkey",
+ "The public key the proof is delegated to."},
+ {RPCResult::Type::STR_HEX, "proofmaster",
+ "The delegated proof master public key."},
+ {RPCResult::Type::STR_HEX, "delegationid",
+ "The identifier of this delegation."},
+ {RPCResult::Type::STR_HEX, "limitedid",
+ "A delegated proof data hash excluding the master key."},
+ {RPCResult::Type::STR_HEX, "proofid",
+ "A hash of the delegated proof limitedid and master key."},
+ {RPCResult::Type::NUM, "depth",
+ "The number of delegation levels."},
+ {RPCResult::Type::ARR,
+ "levels",
+ "",
+ {
+ {RPCResult::Type::OBJ,
+ "",
+ "",
+ {
+ {RPCResult::Type::NUM, "index",
+ "The index of this delegation level."},
+ {RPCResult::Type::STR_HEX, "pubkey",
+ "This delegated public key for this level"},
+ {RPCResult::Type::STR, "signature",
+ "Signature of this delegation level (base64 "
+ "encoded)"},
+ }},
+ }},
+ }},
+ RPCExamples{HelpExampleCli("decodeavalanchedelegation",
+ "\"<hex delegation>\"") +
+ HelpExampleRpc("decodeavalanchedelegation",
+ "\"<hex delegation>\"")},
+ [&](const RPCHelpMan &self, const Config &config,
+ const JSONRPCRequest &request) -> UniValue {
+ RPCTypeCheck(request.params, {UniValue::VSTR});
+
+ avalanche::Delegation delegation;
+ bilingual_str error;
+ if (!avalanche::Delegation::FromHex(
+ delegation, request.params[0].get_str(), error)) {
+ throw JSONRPCError(RPC_DESERIALIZATION_ERROR, error.original);
+ }
+
+ UniValue result(UniValue::VOBJ);
+ result.pushKV("pubkey", HexStr(delegation.getDelegatedPubkey()));
+ result.pushKV("proofmaster", HexStr(delegation.getProofMaster()));
+ result.pushKV("delegationid", delegation.getId().ToString());
+ result.pushKV("limitedid",
+ delegation.getLimitedProofId().ToString());
+ result.pushKV("proofid", delegation.getProofId().ToString());
+
+ auto levels = delegation.getLevels();
+ result.pushKV("depth", levels.size());
+
+ UniValue levelsArray(UniValue::VARR);
+ for (auto &level : levels) {
+ UniValue obj(UniValue::VOBJ);
+ obj.pushKV("pubkey", HexStr(level.pubkey));
+ obj.pushKV("signature", EncodeBase64(level.sig));
+ levelsArray.push_back(std::move(obj));
+ }
+ result.pushKV("levels", levelsArray);
+
+ return result;
+ },
+ };
+}
+
static RPCHelpMan getavalancheinfo() {
return RPCHelpMan{
"getavalancheinfo",
"Returns an object containing various state info regarding avalanche "
"networking.\n",
{},
RPCResult{
RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::OBJ,
"local",
"Only available if -avaproof has been supplied to the node",
{
{RPCResult::Type::STR_HEX, "proofid",
"The node local proof id."},
{RPCResult::Type::STR_HEX, "limited_proofid",
"The node local limited proof id."},
{RPCResult::Type::STR_HEX, "master",
"The node local proof master public key."},
{RPCResult::Type::STR, "payout_address",
"The node local proof payout address. This might be "
"omitted if the payout script is not one of P2PK, P2PKH "
"or P2SH, in which case decodeavalancheproof can be used "
"to get more details."},
{RPCResult::Type::STR_AMOUNT, "stake_amount",
"The node local proof staked amount."},
}},
{RPCResult::Type::OBJ,
"network",
"",
{
{RPCResult::Type::NUM, "proof_count",
"The number of valid avalanche proofs we know exist."},
{RPCResult::Type::NUM, "connected_proof_count",
"The number of avalanche proofs with at least one node "
"we are connected to."},
{RPCResult::Type::STR_AMOUNT, "total_stake_amount",
"The total staked amount over all the valid proofs in " +
Currency::get().ticker + "."},
{RPCResult::Type::STR_AMOUNT, "connected_stake_amount",
"The total staked amount over all the connected proofs "
"in " +
Currency::get().ticker + "."},
{RPCResult::Type::NUM, "node_count",
"The number of avalanche nodes we are connected to."},
{RPCResult::Type::NUM, "connected_node_count",
"The number of avalanche nodes associated with an "
"avalanche proof."},
{RPCResult::Type::NUM, "pending_node_count",
"The number of avalanche nodes pending for a proof."},
}},
},
},
RPCExamples{HelpExampleCli("getavalancheinfo", "") +
HelpExampleRpc("getavalancheinfo", "")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
if (!g_avalanche) {
throw JSONRPCError(RPC_INTERNAL_ERROR,
"Avalanche is not initialized");
}
UniValue ret(UniValue::VOBJ);
auto localProof = g_avalanche->getLocalProof();
if (localProof != nullptr) {
UniValue local(UniValue::VOBJ);
local.pushKV("live", g_avalanche->withPeerManager(
[&](const avalanche::PeerManager &pm) {
return pm.isBoundToPeer(
localProof->getId());
}));
local.pushKV("proofid", localProof->getId().ToString());
local.pushKV("limited_proofid",
localProof->getLimitedId().ToString());
local.pushKV("master", HexStr(localProof->getMaster()));
CTxDestination destination;
if (ExtractDestination(localProof->getPayoutScript(),
destination)) {
local.pushKV("payout_address",
EncodeDestination(destination, config));
}
local.pushKV("stake_amount", localProof->getStakedAmount());
ret.pushKV("local", local);
}
g_avalanche->withPeerManager([&](const avalanche::PeerManager &pm) {
UniValue network(UniValue::VOBJ);
uint64_t proofCount{0};
uint64_t connectedProofCount{0};
Amount totalStakes = Amount::zero();
Amount connectedStakes = Amount::zero();
pm.forEachPeer([&](const avalanche::Peer &peer) {
CHECK_NONFATAL(peer.proof != nullptr);
// Don't account for our local proof here
if (peer.proof == localProof) {
return;
}
const Amount proofStake = peer.proof->getStakedAmount();
++proofCount;
totalStakes += proofStake;
if (peer.node_count > 0) {
++connectedProofCount;
connectedStakes += proofStake;
}
});
network.pushKV("proof_count", proofCount);
network.pushKV("connected_proof_count", connectedProofCount);
network.pushKV("total_stake_amount", totalStakes);
network.pushKV("connected_stake_amount", connectedStakes);
const uint64_t connectedNodes = pm.getNodeCount();
const uint64_t pendingNodes = pm.getPendingNodeCount();
network.pushKV("node_count", connectedNodes + pendingNodes);
network.pushKV("connected_node_count", connectedNodes);
network.pushKV("pending_node_count", pendingNodes);
ret.pushKV("network", network);
});
return ret;
},
};
}
static RPCHelpMan getavalanchepeerinfo() {
return RPCHelpMan{
"getavalanchepeerinfo",
"Returns data about an avalanche peer as a json array of objects. If "
"no proofid is provided, returns data about all the peers.\n",
{
{"proofid", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED,
"The hex encoded avalanche proof identifier."},
},
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::NUM, "nodecount",
"The number of nodes for this peer"},
{RPCResult::Type::ARR,
"nodes",
"",
{
{RPCResult::Type::NUM, "nodeid",
"Node id, as returned by getpeerinfo"},
}},
}},
}},
},
RPCExamples{HelpExampleCli("getavalanchepeerinfo", "") +
HelpExampleCli("getavalanchepeerinfo", "\"proofid\"") +
HelpExampleRpc("getavalanchepeerinfo", "") +
HelpExampleRpc("getavalanchepeerinfo", "\"proofid\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
RPCTypeCheck(request.params, {UniValue::VSTR});
if (!g_avalanche) {
throw JSONRPCError(RPC_INTERNAL_ERROR,
"Avalanche is not initialized");
}
auto peerToUniv = [](const avalanche::PeerManager &pm,
const avalanche::Peer &peer) {
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);
pm.forEachNode(peer, [&](const avalanche::Node &n) {
nodes.push_back(n.nodeid);
});
obj.pushKV("nodecount", uint64_t(peer.node_count));
obj.pushKV("nodes", nodes);
return obj;
};
UniValue ret(UniValue::VARR);
g_avalanche->withPeerManager([&](const avalanche::PeerManager &pm) {
// If a proofid is provided, only return the associated peer
if (!request.params[0].isNull()) {
const avalanche::ProofId proofid =
avalanche::ProofId::fromHex(
request.params[0].get_str());
if (!pm.isBoundToPeer(proofid)) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Proofid not found");
}
pm.forPeer(proofid, [&](const avalanche::Peer &peer) {
return ret.push_back(peerToUniv(pm, peer));
});
return;
}
// If no proofid is provided, return all the peers
pm.forEachPeer([&](const avalanche::Peer &peer) {
ret.push_back(peerToUniv(pm, peer));
});
});
return ret;
},
};
}
static RPCHelpMan getrawavalancheproof() {
return RPCHelpMan{
"getrawavalancheproof",
"Lookup for a known avalanche proof by id.\n",
{
{"proofid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
"The hex encoded avalanche proof identifier."},
},
RPCResult{
RPCResult::Type::OBJ,
"",
"",
{{
{RPCResult::Type::STR_HEX, "proof",
"The hex encoded proof matching the identifier."},
{RPCResult::Type::BOOL, "orphan",
"Whether the proof is an orphan."},
{RPCResult::Type::BOOL, "isBoundToPeer",
"Whether the proof is bound to an avalanche peer."},
}},
},
RPCExamples{HelpExampleRpc("getrawavalancheproof", "<proofid>")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
if (!g_avalanche) {
throw JSONRPCError(RPC_INTERNAL_ERROR,
"Avalanche is not initialized");
}
const avalanche::ProofId proofid =
avalanche::ProofId::fromHex(request.params[0].get_str());
bool isOrphan = false;
bool isBoundToPeer = false;
auto proof =
g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) {
isOrphan = pm.isOrphan(proofid);
isBoundToPeer = pm.isBoundToPeer(proofid);
return pm.getProof(proofid);
});
if (!proof) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Proof not found");
}
UniValue ret(UniValue::VOBJ);
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << *proof;
ret.pushKV("proof", HexStr(ss));
ret.pushKV("orphan", isOrphan);
ret.pushKV("isBoundToPeer", isBoundToPeer);
return ret;
},
};
}
static RPCHelpMan sendavalancheproof() {
return 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", "<proof>")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
if (!g_avalanche) {
throw JSONRPCError(RPC_INTERNAL_ERROR,
"Avalanche is not initialized");
}
auto proof = std::make_shared<avalanche::Proof>();
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 (!registerProofIfNeeded(proof)) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"The proof has conflicting utxo with an existing proof");
}
g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) {
pm.addUnbroadcastProof(proofid);
});
RelayProof(proofid, *node.connman);
return true;
},
};
}
static RPCHelpMan verifyavalancheproof() {
return 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", "\"<proof>\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
RPCTypeCheck(request.params, {UniValue::VSTR});
avalanche::Proof proof;
verifyProofOrThrow(EnsureNodeContext(request.context), proof,
request.params[0].get_str());
return true;
},
};
}
void RegisterAvalancheRPCCommands(CRPCTable &t) {
// clang-format off
static const CRPCCommand commands[] = {
// category actor (function)
// ----------------- --------------------
- { "avalanche", getavalanchekey, },
- { "avalanche", addavalanchenode, },
- { "avalanche", buildavalancheproof, },
- { "avalanche", decodeavalancheproof, },
- { "avalanche", delegateavalancheproof, },
- { "avalanche", getavalancheinfo, },
- { "avalanche", getavalanchepeerinfo, },
- { "avalanche", getrawavalancheproof, },
- { "avalanche", sendavalancheproof, },
- { "avalanche", verifyavalancheproof, },
+ { "avalanche", getavalanchekey, },
+ { "avalanche", addavalanchenode, },
+ { "avalanche", buildavalancheproof, },
+ { "avalanche", decodeavalancheproof, },
+ { "avalanche", delegateavalancheproof, },
+ { "avalanche", decodeavalanchedelegation, },
+ { "avalanche", getavalancheinfo, },
+ { "avalanche", getavalanchepeerinfo, },
+ { "avalanche", getrawavalancheproof, },
+ { "avalanche", sendavalancheproof, },
+ { "avalanche", verifyavalancheproof, },
};
// clang-format on
for (const auto &c : commands) {
t.appendCommand(c.name, &c);
}
}
diff --git a/test/functional/abc_rpc_avalancheproof.py b/test/functional/abc_rpc_avalancheproof.py
index 0c39319d8..6976bd1eb 100644
--- a/test/functional/abc_rpc_avalancheproof.py
+++ b/test/functional/abc_rpc_avalancheproof.py
@@ -1,559 +1,571 @@
#!/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.address import ADDRESS_ECREG_UNSPENDABLE, base58_to_byte
from test_framework.avatools import (
create_coinbase_stakes,
create_stakes,
get_proof_ids,
wait_for_proof,
)
from test_framework.cashaddr import PUBKEY_TYPE, encode_full
from test_framework.key import ECKey
from test_framework.messages import (
AvalancheDelegation,
AvalancheDelegationLevel,
AvalancheProof,
FromHex,
LegacyAvalancheProof,
)
from test_framework.p2p import P2PInterface, p2p_lock
from test_framework.test_framework import BitcoinTestFramework
from test_framework.test_node import ErrorMatch
from test_framework.util import (
append_config,
assert_equal,
assert_raises_rpc_error,
)
from test_framework.wallet_util import bytes_to_wif
AVALANCHE_MAX_PROOF_STAKES = 1000
PROOF_DUST_THRESHOLD = 1000000.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 LegacyAvalancheProofTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
self.num_nodes = 2
self.extra_args = [
['-enableavalanche=1', '-avacooldown=0', '-legacyavaproof=1']] * self.num_nodes
self.supports_cli = False
self.rpc_timeout = 120
def run_test(self):
# Turn off node 1 while node 0 mines blocks to generate stakes,
# so that we can later try starting node 1 with an orphan proof.
self.stop_node(1)
node = self.nodes[0]
# FIXME Remove after the hardcoded addresses have been converted in the
# LUT from test_node.py
def legacy_to_ecash_p2pkh(legacy):
payload, _ = base58_to_byte(legacy)
return encode_full('ecregtest', PUBKEY_TYPE, payload)
addrkey0 = node.get_deterministic_priv_key()
node_ecash_addr = legacy_to_ecash_p2pkh(addrkey0.address)
blockhashes = node.generatetoaddress(100, node_ecash_addr)
self.log.info(
"Make build a valid proof and restart the node to use it")
privkey = ECKey()
privkey.set(bytes.fromhex(
"12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True)
wif_privkey = bytes_to_wif(privkey.get_bytes())
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, wif_privkey, stakes)
self.log.info("Test decodeavalancheproof RPC")
proofobj = FromHex(LegacyAvalancheProof(), proof)
decodedproof = node.decodeavalancheproof(proof)
limited_id_hex = f"{proofobj.limited_proofid:0{64}x}"
+ proofid_hex = f"{proofobj.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["payoutscript"]["hex"], "")
assert "signature" not in decodedproof.keys()
- assert_equal(decodedproof["proofid"], f"{proofobj.proofid:0{64}x}")
+ assert_equal(decodedproof["proofid"], proofid_hex)
assert_equal(decodedproof["limitedid"], limited_id_hex)
assert_equal(decodedproof["staked_amount"], Decimal('50000000.00'))
assert_equal(decodedproof["score"], 5000)
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]["address"],
node_ecash_addr)
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])
self.log.info(
"Testing decodeavalancheproof with legacyavaproof disabled")
self.restart_node(0, self.extra_args[0] + ["-legacyavaproof=0"])
regular_proof = node.buildavalancheproof(
proof_sequence, proof_expiration, wif_privkey, stakes, ADDRESS_ECREG_UNSPENDABLE)
decoded_regular_proof = node.decodeavalancheproof(regular_proof)
assert_equal(
decoded_regular_proof["sequence"],
decodedproof["sequence"])
assert_equal(
decoded_regular_proof["expiration"],
decodedproof["expiration"])
assert_equal(decoded_regular_proof["master"], decodedproof["master"])
assert_equal(decoded_regular_proof["payoutscript"], {
"asm": "OP_DUP OP_HASH160 0000000000000000000000000000000000000000 OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a914000000000000000000000000000000000000000088ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [ADDRESS_ECREG_UNSPENDABLE],
})
regular_proof_obj = FromHex(AvalancheProof(), regular_proof)
assert_equal(
decoded_regular_proof["signature"],
base64.b64encode(regular_proof_obj.signature).decode("ascii"))
assert_equal(
decoded_regular_proof["proofid"],
f"{regular_proof_obj.proofid:0{64}x}")
assert_equal(
decoded_regular_proof["limitedid"],
f"{regular_proof_obj.limited_proofid:0{64}x}")
assert_equal(
decoded_regular_proof["staked_amount"],
decodedproof["staked_amount"])
assert_equal(
decoded_regular_proof["score"],
decodedproof["score"])
assert_equal(
decoded_regular_proof["stakes"][0]["txid"],
decodedproof["stakes"][0]["txid"])
assert_equal(
decoded_regular_proof["stakes"][0]["vout"],
decodedproof["stakes"][0]["vout"])
assert_equal(
decoded_regular_proof["stakes"][0]["height"],
decodedproof["stakes"][0]["height"])
assert_equal(
decoded_regular_proof["stakes"][0]["iscoinbase"],
decodedproof["stakes"][0]["iscoinbase"])
assert_equal(
decoded_regular_proof["stakes"][0]["address"],
decodedproof["stakes"][0]["address"])
assert_equal(
decoded_regular_proof["stakes"][0]["signature"],
base64.b64encode(regular_proof_obj.stakes[0].sig).decode("ascii"))
# Restart the node with this proof
self.restart_node(0, self.extra_args[0] + [
"-avaproof={}".format(proof),
"-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN",
])
self.log.info("The proof is registered at first chaintip update")
assert_equal(len(node.getavalanchepeerinfo()), 0)
node.generate(1)
self.wait_until(lambda: len(node.getavalanchepeerinfo()) == 1,
timeout=5)
# This case will occur for users building proofs with a third party
# tool and then starting a new node that is not yet aware of the
# transactions used for stakes.
self.log.info("Start a node with an orphan proof")
self.start_node(1, self.extra_args[0] + [
"-avaproof={}".format(proof),
"-avamasterkey=cND2ZvtabDbJ1gucx9GWH6XT9kgTAqfb6cotPt5Q5CyxVDhid2EN",
])
# Mine a block to trigger an attempt at registering the proof
self.nodes[1].generate(1)
- wait_for_proof(self.nodes[1], f"{proofobj.proofid:0{64}x}",
- expect_orphan=True)
+ wait_for_proof(self.nodes[1], proofid_hex, expect_orphan=True)
self.log.info("Connect to an up-to-date node to unorphan the proof")
self.connect_nodes(1, node.index)
self.sync_all()
- wait_for_proof(self.nodes[1], f"{proofobj.proofid:0{64}x}",
- expect_orphan=False)
+ wait_for_proof(self.nodes[1], proofid_hex, expect_orphan=False)
- self.log.info("Generate delegations for the proof")
+ self.log.info("Generate delegations for the proof and decode them")
# Stack up a few delegation levels
def gen_privkey():
pk = ECKey()
pk.generate()
return pk
delegator_privkey = privkey
delegation = None
- for _ in range(10):
+ for i in range(10):
delegated_privkey = gen_privkey()
+ delegated_pubkey = get_hex_pubkey(delegated_privkey)
delegation = node.delegateavalancheproof(
limited_id_hex,
bytes_to_wif(delegator_privkey.get_bytes()),
- get_hex_pubkey(delegated_privkey),
+ delegated_pubkey,
delegation,
)
+
+ dg_info = node.decodeavalanchedelegation(delegation)
+ assert_equal(dg_info['pubkey'], delegated_pubkey)
+ assert_equal(dg_info['proofmaster'], proof_master)
+ assert 'delegationid' in dg_info.keys()
+ assert_equal(dg_info['limitedid'], limited_id_hex)
+ assert_equal(dg_info['proofid'], proofid_hex)
+ assert_equal(dg_info['depth'], i + 1)
+ assert_equal(len(dg_info['levels']), dg_info['depth'])
+ assert_equal(dg_info['levels'][-1]['pubkey'], delegated_pubkey)
+ assert 'signature' in dg_info['levels'][-1]
+
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,
wif_privkey, [])
# Invalid privkey
assert_raises_rpc_error(-5, "The private key is invalid",
node.delegateavalancheproof,
limited_id_hex,
bytes_to_wif(bytes(32)),
random_pubkey,
)
# Invalid delegation
bad_dg = AvalancheDelegation()
assert_raises_rpc_error(-8, "The delegation does not match the proof",
node.delegateavalancheproof,
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 delegation is invalid",
node.delegateavalancheproof,
limited_id_hex,
bytes_to_wif(privkey.get_bytes()),
random_pubkey,
bad_dg.serialize().hex(),
)
# Wrong privkey, match the proof but does not match the delegation
assert_raises_rpc_error(-5, "The private key does not match the delegation",
node.delegateavalancheproof,
limited_id_hex,
bytes_to_wif(privkey.get_bytes()),
random_pubkey,
delegation,
)
# Delegation not hex
assert_raises_rpc_error(-22, "Delegation must be an hexadecimal string.",
node.delegateavalancheproof,
limited_id_hex,
bytes_to_wif(privkey.get_bytes()),
random_pubkey,
"f00",
)
# Delegation is hex but ill-formed
assert_raises_rpc_error(-22, "Delegation has invalid format",
node.delegateavalancheproof,
limited_id_hex,
bytes_to_wif(privkey.get_bytes()),
random_pubkey,
"dead",
)
# Test invalid proofs
dust = node.buildavalancheproof(
proof_sequence, proof_expiration, wif_privkey,
create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key, amount="0"))
dust2 = node.buildavalancheproof(
proof_sequence, proof_expiration, wif_privkey,
create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key,
amount=f"{PROOF_DUST_THRESHOLD * 0.9999:.2f}"))
missing_stake = node.buildavalancheproof(
proof_sequence, proof_expiration, wif_privkey, [{
'txid': '0' * 64,
'vout': 0,
'amount': 10000000,
'height': 42,
'iscoinbase': False,
'privatekey': addrkey0.key,
}]
)
duplicate_stake = ("0b000000000000000c0000000000000021030b4c866585dd868"
"a9d62348a9cd008d6a312937048fff31670e7e920cfc7a74402"
"05c5f72f5d6da3085583e75ee79340eb4eff208c89988e7ed0e"
"fb30b87298fa30000000000f2052a0100000003000000210227"
"d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2"
"fc839ef5d3f86076def2e8bc3c40671c1a0eb505da5857a950a"
"0cf4625a80018cdd75ac62e61273ff8142f747de67e73f6368c"
"8648942b0ef6c065d72a81ad7438a23c11cca05c5f72f5d6da3"
"085583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30"
"000000000f2052a0100000003000000210227d85ba011276cf2"
"5b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d3f860"
"76def2e8bc3c40671c1a0eb505da5857a950a0cf4625a80018c"
"dd75ac62e61273ff8142f747de67e73f6368c8648942b0ef6c0"
"65d72a81ad7438a23c11cca")
bad_sig = ("0b000000000000000c0000000000000021030b4c866585dd868a9d62348"
"a9cd008d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085"
"583e75ee79340eb4eff208c89988e7ed0efb30b87298fa30000000000f20"
"52a0100000003000000210227d85ba011276cf25b51df6a188b75e604b3"
"8770a462b2d0e9fb2fc839ef5d3faf07f001dd38e9b4a43d07d5d449cc0"
"f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974"
"1321b91a79b82d1c2cfd47793261e4ba003cf5")
wrong_order = ("c964aa6fde575e4ce8404581c7be874e21023beefdde700a6bc0203"
"6335b4df141c8bc67bb05a971f5ac2745fd683797dde30305d427b7"
"06705a5d4b6a368a231d6db62abacf8c29bc32b61e7f65a0a6976aa"
"8b86b687bc0260e821e4f0200b9d3bf6d2102449fb5237efe8f647d"
"32e8b64f06c22d1d40368eaca2a71ffc6a13ecc8bce68052365271b"
"6c71189f5cd7e3b694b77b579080f0b35bae567b96590ab6aa3019b"
"018ff9f061f52f1426bdb195d4b6d4dff5114cee90e33dabf0c588e"
"badf7774418f54247f6390791706af36fac782302479898b5273f9e"
"51a92cb1fb5af43deeb6c8c269403d30ffcb380300134398c42103e"
"49f9df52de2dea81cf7838b82521b69f2ea360f1c4eed9e6c89b7d0"
"f9e645efa08e97ea0c60e1f0a064fbf08989c084707082727e85dcb"
"9f79bb503f76ee6c8dad42a07ef15c89b3750a5631d604b21fafff0"
"f4de354ade95c2f28160ae549af0d4ce48c4ca9d0714b1fa5192027"
"0f8575e0af610f07b4e602a018ecdbb649b64fff614c0026e9fc8e0"
"030092533d422103aac52f4cfca700e7e9824298e0184755112e32f"
"359c832f5f6ad2ef62a2c024af812d6d7f2ecc6223a774e19bce1fb"
"20d94d6b01ea693638f55c74fdaa5358fa9239d03e4caf3d817e8f7"
"48ccad55a27b9d365db06ad5a0b779ac385f3dc8710")
self.log.info(
"Check the verifyavalancheproof and sendavalancheproof RPCs")
if self.is_wallet_compiled():
self.log.info(
"Check a proof with the maximum number of UTXO is valid")
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,
wif_privkey, maximum_stakes)
too_many_utxos = node.buildavalancheproof(
proof_sequence, proof_expiration,
wif_privkey, too_many_stakes)
assert node.verifyavalancheproof(good_proof)
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(missing_stake, "utxo-missing-or-spent")
check_rpc_failure(bad_sig, "invalid-stake-signature")
check_rpc_failure(wrong_order, "wrong-stake-ordering")
if self.is_wallet_compiled():
check_rpc_failure(too_many_utxos, "too-many-utxos")
conflicting_utxo = node.buildavalancheproof(
proof_sequence + 1, proof_expiration, wif_privkey, stakes)
assert_raises_rpc_error(-8, "The proof has conflicting utxo with an existing proof",
node.sendavalancheproof, conflicting_utxo)
# Clear the proof pool
self.restart_node(0)
# Good proof
assert node.verifyavalancheproof(proof)
peer = node.add_p2p_connection(P2PInterface())
proofid = FromHex(LegacyAvalancheProof(), proof).proofid
node.sendavalancheproof(proof)
assert proofid in get_proof_ids(node)
def inv_found():
with p2p_lock:
return peer.last_message.get(
"inv") and peer.last_message["inv"].inv[-1].hash == proofid
self.wait_until(inv_found)
self.log.info("Check the getrawproof RPC")
raw_proof = node.getrawavalancheproof("{:064x}".format(proofid))
assert_equal(raw_proof['proof'], proof)
assert_equal(raw_proof['orphan'], False)
assert_equal(raw_proof['isBoundToPeer'], True)
assert_raises_rpc_error(-8, "Proof not found",
node.getrawavalancheproof, '0' * 64)
# Orphan the proof by sending the stake
raw_tx = node.createrawtransaction(
[{"txid": stakes[-1]["txid"], "vout": 0}],
{ADDRESS_ECREG_UNSPENDABLE: stakes[-1]
["amount"] - Decimal('10000')}
)
signed_tx = node.signrawtransactionwithkey(raw_tx, [addrkey0.key])
node.sendrawtransaction(signed_tx["hex"])
node.generate(1)
self.wait_until(lambda: proofid not in get_proof_ids(node))
raw_proof = node.getrawavalancheproof("{:064x}".format(proofid))
assert_equal(raw_proof['proof'], proof)
assert_equal(raw_proof['orphan'], True)
assert_equal(raw_proof['isBoundToPeer'], False)
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,
)
# Master private key mismatch
random_privkey = ECKey()
random_privkey.generate()
node.assert_start_raises_init_error(
self.extra_args[0] + [
"-avaproof={}".format(proof),
"-avamasterkey={}".format(
bytes_to_wif(random_privkey.get_bytes())),
],
expected_msg="Error: The master key does not match the proof public key.",
)
self.log.info("Bad delegation should be rejected at startup")
def check_delegation_init_error(delegation, message):
node.assert_start_raises_init_error(
self.extra_args[0] + [
"-avadelegation={}".format(delegation),
"-avaproof={}".format(proof),
"-avamasterkey={}".format(
bytes_to_wif(delegated_privkey.get_bytes())),
],
expected_msg="Error: " + message,
)
check_delegation_init_error(
AvalancheDelegation().serialize().hex(),
"The delegation does not match the proof.")
bad_level_sig = FromHex(AvalancheDelegation(), delegation)
# Tweak some key to cause the signature to mismatch
bad_level_sig.levels[-2].pubkey = bytes.fromhex(proof_master)
check_delegation_init_error(bad_level_sig.serialize().hex(),
"The avalanche delegation has invalid signatures.")
node.assert_start_raises_init_error(
self.extra_args[0] + [
"-avadelegation={}".format(delegation),
"-avaproof={}".format(proof),
"-avamasterkey={}".format(
bytes_to_wif(random_privkey.get_bytes())),
],
expected_msg="Error: The master key does not match the delegation public key.",
)
if __name__ == '__main__':
LegacyAvalancheProofTest().main()

File Metadata

Mime Type
text/x-diff
Expires
Sun, Apr 27, 11:56 (1 d, 2 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573485
Default Alt Text
(71 KB)

Event Timeline