Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13711424
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
71 KB
Subscribers
None
View Options
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 ¶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 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
Details
Attached
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)
Attached To
rABC Bitcoin ABC
Event Timeline
Log In to Comment