Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13711121
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
146 KB
Subscribers
None
View Options
diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp
index 8e7f07e3a..6a4ff4dbf 100644
--- a/src/avalanche/processor.cpp
+++ b/src/avalanche/processor.cpp
@@ -1,1411 +1,1412 @@
// Copyright (c) 2018-2019 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/processor.h>
#include <avalanche/avalanche.h>
#include <avalanche/delegationbuilder.h>
#include <avalanche/peermanager.h>
#include <avalanche/proofcomparator.h>
#include <avalanche/validation.h>
#include <avalanche/voterecord.h>
#include <chain.h>
#include <common/args.h>
#include <key_io.h> // For DecodeSecret
#include <net.h>
#include <netbase.h>
#include <netmessagemaker.h>
#include <policy/block/stakingrewards.h>
#include <scheduler.h>
#include <util/bitmanip.h>
#include <util/moneystr.h>
#include <util/time.h>
#include <util/translation.h>
#include <validation.h>
#include <chrono>
#include <limits>
#include <tuple>
/**
* Run the avalanche event loop every 10ms.
*/
static constexpr std::chrono::milliseconds AVALANCHE_TIME_STEP{10};
static const std::string AVAPEERS_FILE_NAME{"avapeers.dat"};
namespace avalanche {
static const uint256 GetVoteItemId(const AnyVoteItem &item) {
return std::visit(variant::overloaded{
[](const ProofRef &proof) {
uint256 id = proof->getId();
return id;
},
[](const CBlockIndex *pindex) {
uint256 hash = pindex->GetBlockHash();
return hash;
},
[](const CTransactionRef &tx) {
uint256 id = tx->GetId();
return id;
},
},
item);
}
static bool VerifyProof(const Amount &stakeUtxoDustThreshold,
const Proof &proof, bilingual_str &error) {
ProofValidationState proof_state;
if (!proof.verify(stakeUtxoDustThreshold, proof_state)) {
switch (proof_state.GetResult()) {
case ProofValidationResult::NO_STAKE:
error = _("The avalanche proof has no stake.");
return false;
case ProofValidationResult::DUST_THRESHOLD:
error = _("The avalanche proof stake is too low.");
return false;
case ProofValidationResult::DUPLICATE_STAKE:
error = _("The avalanche proof has duplicated stake.");
return false;
case ProofValidationResult::INVALID_STAKE_SIGNATURE:
error = _("The avalanche proof has invalid stake signatures.");
return false;
case ProofValidationResult::TOO_MANY_UTXOS:
error = strprintf(
_("The avalanche proof has too many utxos (max: %u)."),
AVALANCHE_MAX_PROOF_STAKES);
return false;
default:
error = _("The avalanche proof is invalid.");
return false;
}
}
return true;
}
static bool VerifyDelegation(const Delegation &dg,
const CPubKey &expectedPubKey,
bilingual_str &error) {
DelegationState dg_state;
CPubKey auth;
if (!dg.verify(dg_state, auth)) {
switch (dg_state.GetResult()) {
case avalanche::DelegationResult::INVALID_SIGNATURE:
error = _("The avalanche delegation has invalid signatures.");
return false;
case avalanche::DelegationResult::TOO_MANY_LEVELS:
error = _(
"The avalanche delegation has too many delegation levels.");
return false;
default:
error = _("The avalanche delegation is invalid.");
return false;
}
}
if (auth != expectedPubKey) {
error = _(
"The avalanche delegation does not match the expected public key.");
return false;
}
return true;
}
struct Processor::PeerData {
ProofRef proof;
Delegation delegation;
mutable Mutex cs_proofState;
ProofRegistrationState proofState GUARDED_BY(cs_proofState);
};
class Processor::NotificationsHandler
: public interfaces::Chain::Notifications {
Processor *m_processor;
public:
NotificationsHandler(Processor *p) : m_processor(p) {}
void updatedBlockTip() override { m_processor->updatedBlockTip(); }
void transactionAddedToMempool(const CTransactionRef &tx,
uint64_t mempool_sequence) override {
m_processor->transactionAddedToMempool(tx);
}
};
Processor::Processor(Config avaconfigIn, interfaces::Chain &chain,
CConnman *connmanIn, ChainstateManager &chainmanIn,
CTxMemPool *mempoolIn, CScheduler &scheduler,
std::unique_ptr<PeerData> peerDataIn, CKey sessionKeyIn,
uint32_t minQuorumTotalScoreIn,
double minQuorumConnectedScoreRatioIn,
int64_t minAvaproofsNodeCountIn,
uint32_t staleVoteThresholdIn, uint32_t staleVoteFactorIn,
Amount stakeUtxoDustThreshold, bool preConsensus,
bool stakingPreConsensus)
: avaconfig(std::move(avaconfigIn)), connman(connmanIn),
chainman(chainmanIn), mempool(mempoolIn), round(0),
peerManager(std::make_unique<PeerManager>(
stakeUtxoDustThreshold, chainman,
peerDataIn ? peerDataIn->proof : ProofRef())),
peerData(std::move(peerDataIn)), sessionKey(std::move(sessionKeyIn)),
minQuorumScore(minQuorumTotalScoreIn),
minQuorumConnectedScoreRatio(minQuorumConnectedScoreRatioIn),
minAvaproofsNodeCount(minAvaproofsNodeCountIn),
staleVoteThreshold(staleVoteThresholdIn),
staleVoteFactor(staleVoteFactorIn), m_preConsensus(preConsensus),
m_stakingPreConsensus(stakingPreConsensus) {
// Make sure we get notified of chain state changes.
chainNotificationsHandler =
chain.handleNotifications(std::make_shared<NotificationsHandler>(this));
scheduler.scheduleEvery(
[this]() -> bool {
std::unordered_set<ProofRef, SaltedProofHasher> registeredProofs;
WITH_LOCK(cs_peerManager,
peerManager->cleanupDanglingProofs(registeredProofs));
for (const auto &proof : registeredProofs) {
LogPrint(BCLog::AVALANCHE,
"Promoting previously dangling proof %s\n",
proof->getId().ToString());
reconcileOrFinalize(proof);
}
return true;
},
5min);
if (!gArgs.GetBoolArg("-persistavapeers", DEFAULT_PERSIST_AVAPEERS)) {
return;
}
std::unordered_set<ProofRef, SaltedProofHasher> registeredProofs;
// Attempt to load the peer file if it exists.
const fs::path dumpPath = gArgs.GetDataDirNet() / AVAPEERS_FILE_NAME;
WITH_LOCK(cs_peerManager, return peerManager->loadPeersFromFile(
dumpPath, registeredProofs));
// We just loaded the previous finalization status, but make sure to trigger
// another round of vote for these proofs to avoid issue if the network
// status changed since the peers file was dumped.
for (const auto &proof : registeredProofs) {
addToReconcile(proof);
}
LogPrint(BCLog::AVALANCHE, "Loaded %d peers from the %s file\n",
registeredProofs.size(), PathToString(dumpPath));
}
Processor::~Processor() {
chainNotificationsHandler.reset();
stopEventLoop();
if (!gArgs.GetBoolArg("-persistavapeers", DEFAULT_PERSIST_AVAPEERS)) {
return;
}
LOCK(cs_peerManager);
// Discard the status output: if it fails we want to continue normally.
peerManager->dumpPeersToFile(gArgs.GetDataDirNet() / AVAPEERS_FILE_NAME);
}
std::unique_ptr<Processor>
Processor::MakeProcessor(const ArgsManager &argsman, interfaces::Chain &chain,
CConnman *connman, ChainstateManager &chainman,
CTxMemPool *mempool, CScheduler &scheduler,
bilingual_str &error) {
std::unique_ptr<PeerData> peerData;
CKey masterKey;
CKey sessionKey;
Amount stakeUtxoDustThreshold = PROOF_DUST_THRESHOLD;
if (argsman.IsArgSet("-avaproofstakeutxodustthreshold") &&
!ParseMoney(argsman.GetArg("-avaproofstakeutxodustthreshold", ""),
stakeUtxoDustThreshold)) {
error = _("The avalanche stake utxo dust threshold amount is invalid.");
return nullptr;
}
if (argsman.IsArgSet("-avasessionkey")) {
sessionKey = DecodeSecret(argsman.GetArg("-avasessionkey", ""));
if (!sessionKey.IsValid()) {
error = _("The avalanche session key is invalid.");
return nullptr;
}
} else {
// Pick a random key for the session.
sessionKey.MakeNewKey(true);
}
if (argsman.IsArgSet("-avaproof")) {
if (!argsman.IsArgSet("-avamasterkey")) {
error = _(
"The avalanche master key is missing for the avalanche proof.");
return nullptr;
}
masterKey = DecodeSecret(argsman.GetArg("-avamasterkey", ""));
if (!masterKey.IsValid()) {
error = _("The avalanche master key is invalid.");
return nullptr;
}
auto proof = RCUPtr<Proof>::make();
if (!Proof::FromHex(*proof, argsman.GetArg("-avaproof", ""), error)) {
// error is set by FromHex
return nullptr;
}
peerData = std::make_unique<PeerData>();
peerData->proof = proof;
if (!VerifyProof(stakeUtxoDustThreshold, *peerData->proof, error)) {
// error is set by VerifyProof
return nullptr;
}
std::unique_ptr<DelegationBuilder> dgb;
const CPubKey &masterPubKey = masterKey.GetPubKey();
if (argsman.IsArgSet("-avadelegation")) {
Delegation dg;
if (!Delegation::FromHex(dg, argsman.GetArg("-avadelegation", ""),
error)) {
// error is set by FromHex()
return nullptr;
}
if (dg.getProofId() != peerData->proof->getId()) {
error = _("The delegation does not match the proof.");
return nullptr;
}
if (masterPubKey != dg.getDelegatedPubkey()) {
error = _(
"The master key does not match the delegation public key.");
return nullptr;
}
dgb = std::make_unique<DelegationBuilder>(dg);
} else {
if (masterPubKey != peerData->proof->getMaster()) {
error =
_("The master key does not match the proof public key.");
return nullptr;
}
dgb = std::make_unique<DelegationBuilder>(*peerData->proof);
}
// Generate the delegation to the session key.
const CPubKey sessionPubKey = sessionKey.GetPubKey();
if (sessionPubKey != masterPubKey) {
if (!dgb->addLevel(masterKey, sessionPubKey)) {
error = _("Failed to generate a delegation for this session.");
return nullptr;
}
}
peerData->delegation = dgb->build();
if (!VerifyDelegation(peerData->delegation, sessionPubKey, error)) {
// error is set by VerifyDelegation
return nullptr;
}
}
const auto queryTimeoutDuration =
std::chrono::milliseconds(argsman.GetIntArg(
"-avatimeout", AVALANCHE_DEFAULT_QUERY_TIMEOUT.count()));
// Determine quorum parameters
Amount minQuorumStake = AVALANCHE_DEFAULT_MIN_QUORUM_STAKE;
if (argsman.IsArgSet("-avaminquorumstake") &&
!ParseMoney(argsman.GetArg("-avaminquorumstake", ""), minQuorumStake)) {
error = _("The avalanche min quorum stake amount is invalid.");
return nullptr;
}
if (!MoneyRange(minQuorumStake)) {
error = _("The avalanche min quorum stake amount is out of range.");
return nullptr;
}
double minQuorumConnectedStakeRatio =
AVALANCHE_DEFAULT_MIN_QUORUM_CONNECTED_STAKE_RATIO;
if (argsman.IsArgSet("-avaminquorumconnectedstakeratio")) {
// Parse the parameter with a precision of 0.000001.
int64_t megaMinRatio;
if (!ParseFixedPoint(
argsman.GetArg("-avaminquorumconnectedstakeratio", ""), 6,
&megaMinRatio)) {
error =
_("The avalanche min quorum connected stake ratio is invalid.");
return nullptr;
}
minQuorumConnectedStakeRatio = double(megaMinRatio) / 1000000;
}
if (minQuorumConnectedStakeRatio < 0 || minQuorumConnectedStakeRatio > 1) {
error = _(
"The avalanche min quorum connected stake ratio is out of range.");
return nullptr;
}
int64_t minAvaproofsNodeCount =
argsman.GetIntArg("-avaminavaproofsnodecount",
AVALANCHE_DEFAULT_MIN_AVAPROOFS_NODE_COUNT);
if (minAvaproofsNodeCount < 0) {
error = _("The minimum number of node that sent avaproofs message "
"should be non-negative");
return nullptr;
}
// Determine voting parameters
int64_t staleVoteThreshold = argsman.GetIntArg(
"-avastalevotethreshold", AVALANCHE_VOTE_STALE_THRESHOLD);
if (staleVoteThreshold < AVALANCHE_VOTE_STALE_MIN_THRESHOLD) {
error = strprintf(_("The avalanche stale vote threshold must be "
"greater than or equal to %d"),
AVALANCHE_VOTE_STALE_MIN_THRESHOLD);
return nullptr;
}
if (staleVoteThreshold > std::numeric_limits<uint32_t>::max()) {
error = strprintf(_("The avalanche stale vote threshold must be less "
"than or equal to %d"),
std::numeric_limits<uint32_t>::max());
return nullptr;
}
int64_t staleVoteFactor =
argsman.GetIntArg("-avastalevotefactor", AVALANCHE_VOTE_STALE_FACTOR);
if (staleVoteFactor <= 0) {
error = _("The avalanche stale vote factor must be greater than 0");
return nullptr;
}
if (staleVoteFactor > std::numeric_limits<uint32_t>::max()) {
error = strprintf(_("The avalanche stale vote factor must be less than "
"or equal to %d"),
std::numeric_limits<uint32_t>::max());
return nullptr;
}
Config avaconfig(queryTimeoutDuration);
// We can't use std::make_unique with a private constructor
return std::unique_ptr<Processor>(new Processor(
std::move(avaconfig), chain, connman, chainman, mempool, scheduler,
std::move(peerData), std::move(sessionKey),
Proof::amountToScore(minQuorumStake), minQuorumConnectedStakeRatio,
minAvaproofsNodeCount, staleVoteThreshold, staleVoteFactor,
stakeUtxoDustThreshold,
argsman.GetBoolArg("-avalanchepreconsensus",
DEFAULT_AVALANCHE_PRECONSENSUS),
argsman.GetBoolArg("-avalanchestakingpreconsensus",
DEFAULT_AVALANCHE_STAKING_PRECONSENSUS)));
}
static bool isNull(const AnyVoteItem &item) {
return item.valueless_by_exception() ||
std::visit([](const auto &item) { return item == nullptr; }, item);
};
bool Processor::addToReconcile(const AnyVoteItem &item) {
if (isNull(item)) {
return false;
}
if (!isWorthPolling(item)) {
return false;
}
// getLocalAcceptance() takes the voteRecords read lock, so we can't inline
// the calls or we get a deadlock.
const bool accepted = getLocalAcceptance(item);
return voteRecords.getWriteView()
->insert(std::make_pair(item, VoteRecord(accepted)))
.second;
}
bool Processor::reconcileOrFinalize(const ProofRef &proof) {
if (!proof) {
return false;
}
if (isRecentlyFinalized(proof->getId())) {
PeerId peerid;
LOCK(cs_peerManager);
if (peerManager->forPeer(proof->getId(), [&](const Peer &peer) {
peerid = peer.peerid;
return true;
})) {
return peerManager->setFinalized(peerid);
}
}
return addToReconcile(proof);
}
bool Processor::isAccepted(const AnyVoteItem &item) const {
if (isNull(item)) {
return false;
}
auto r = voteRecords.getReadView();
auto it = r->find(item);
if (it == r.end()) {
return false;
}
return it->second.isAccepted();
}
int Processor::getConfidence(const AnyVoteItem &item) const {
if (isNull(item)) {
return -1;
}
auto r = voteRecords.getReadView();
auto it = r->find(item);
if (it == r.end()) {
return -1;
}
return it->second.getConfidence();
}
bool Processor::isRecentlyFinalized(const uint256 &itemId) const {
return WITH_LOCK(cs_finalizedItems, return finalizedItems.contains(itemId));
}
void Processor::clearFinalizedItems() {
LOCK(cs_finalizedItems);
finalizedItems.reset();
}
namespace {
/**
* When using TCP, we need to sign all messages as the transport layer is
* not secure.
*/
class TCPResponse {
Response response;
SchnorrSig sig;
public:
TCPResponse(Response responseIn, const CKey &key)
: response(std::move(responseIn)) {
HashWriter hasher{};
hasher << response;
const uint256 hash = hasher.GetHash();
// Now let's sign!
if (!key.SignSchnorr(hash, sig)) {
sig.fill(0);
}
}
// serialization support
SERIALIZE_METHODS(TCPResponse, obj) {
READWRITE(obj.response, obj.sig);
}
};
} // namespace
void Processor::sendResponse(CNode *pfrom, Response response) const {
connman->PushMessage(
pfrom, CNetMsgMaker(pfrom->GetCommonVersion())
.Make(NetMsgType::AVARESPONSE,
TCPResponse(std::move(response), sessionKey)));
}
bool Processor::registerVotes(NodeId nodeid, const Response &response,
std::vector<VoteItemUpdate> &updates,
int &banscore, std::string &error) {
+ updates.clear();
{
// Save the time at which we can query again.
LOCK(cs_peerManager);
// FIXME: This will override the time even when we received an old stale
// message. This should check that the message is indeed the most up to
// date one before updating the time.
peerManager->updateNextRequestTime(
nodeid, Now<SteadyMilliseconds>() +
std::chrono::milliseconds(response.getCooldown()));
}
std::vector<CInv> invs;
{
// Check that the query exists. There is a possibility that it has been
// deleted if the query timed out, so we don't increase the ban score to
// slowly banning nodes for poor networking over time. Banning has to be
// handled at callsite to avoid DoS.
auto w = queries.getWriteView();
auto it = w->find(std::make_tuple(nodeid, response.getRound()));
if (it == w.end()) {
banscore = 0;
error = "unexpected-ava-response";
return false;
}
invs = std::move(it->invs);
w->erase(it);
}
// Verify that the request and the vote are consistent.
const std::vector<Vote> &votes = response.GetVotes();
size_t size = invs.size();
if (votes.size() != size) {
banscore = 100;
error = "invalid-ava-response-size";
return false;
}
for (size_t i = 0; i < size; i++) {
if (invs[i].hash != votes[i].GetHash()) {
banscore = 100;
error = "invalid-ava-response-content";
return false;
}
}
std::map<AnyVoteItem, Vote, VoteMapComparator> responseItems;
// At this stage we are certain that invs[i] matches votes[i], so we can use
// the inv type to retrieve what is being voted on.
for (size_t i = 0; i < size; i++) {
auto item = getVoteItemFromInv(invs[i]);
if (isNull(item)) {
// This should not happen, but just in case...
continue;
}
if (!isWorthPolling(item)) {
// There is no point polling this item.
continue;
}
responseItems.insert(std::make_pair(std::move(item), votes[i]));
}
auto voteRecordsWriteView = voteRecords.getWriteView();
// Register votes.
for (const auto &p : responseItems) {
auto item = p.first;
const Vote &v = p.second;
auto it = voteRecordsWriteView->find(item);
if (it == voteRecordsWriteView.end()) {
// We are not voting on that item anymore.
continue;
}
auto &vr = it->second;
if (!vr.registerVote(nodeid, v.GetError())) {
if (vr.isStale(staleVoteThreshold, staleVoteFactor)) {
updates.emplace_back(std::move(item), VoteStatus::Stale);
// Just drop stale votes. If we see this item again, we'll
// do a new vote.
voteRecordsWriteView->erase(it);
}
// This vote did not provide any extra information, move on.
continue;
}
if (!vr.hasFinalized()) {
// This item has not been finalized, so we have nothing more to
// do.
updates.emplace_back(std::move(item), vr.isAccepted()
? VoteStatus::Accepted
: VoteStatus::Rejected);
continue;
}
// We just finalized a vote. If it is valid, then let the caller
// know. Either way, remove the item from the map.
updates.emplace_back(std::move(item), vr.isAccepted()
? VoteStatus::Finalized
: VoteStatus::Invalid);
voteRecordsWriteView->erase(it);
}
// FIXME This doesn't belong here as it has nothing to do with vote
// registration.
for (const auto &update : updates) {
if (update.getStatus() != VoteStatus::Finalized &&
update.getStatus() != VoteStatus::Invalid) {
continue;
}
const auto &item = update.getVoteItem();
if (update.getStatus() == VoteStatus::Finalized) {
// Always track finalized items regardless of type. Once finalized
// they should never become invalid.
WITH_LOCK(cs_finalizedItems,
return finalizedItems.insert(GetVoteItemId(item)));
}
if (!std::holds_alternative<const CBlockIndex *>(item)) {
continue;
}
if (update.getStatus() == VoteStatus::Invalid) {
// Track invalidated blocks. Other invalidated types are not
// tracked because they may be rejected for transient reasons
// (ex: immature proofs or orphaned txs) With blocks this is not
// the case. A rejected block will not be mined on. To prevent
// reorgs, invalidated blocks should never be polled again.
LOCK(cs_invalidatedBlocks);
invalidatedBlocks.insert(GetVoteItemId(item));
continue;
}
// At this point the block index can only be finalized
const CBlockIndex *pindex = std::get<const CBlockIndex *>(item);
LOCK(cs_finalizationTip);
if (finalizationTip &&
finalizationTip->GetAncestor(pindex->nHeight) == pindex) {
continue;
}
finalizationTip = pindex;
}
return true;
}
CPubKey Processor::getSessionPubKey() const {
return sessionKey.GetPubKey();
}
bool Processor::sendHelloInternal(CNode *pfrom) {
AssertLockHeld(cs_delayedAvahelloNodeIds);
Delegation delegation;
if (peerData) {
if (!canShareLocalProof()) {
if (!delayedAvahelloNodeIds.emplace(pfrom->GetId()).second) {
// Nothing to do
return false;
}
} else {
delegation = peerData->delegation;
}
}
HashWriter hasher{};
hasher << delegation.getId();
hasher << pfrom->GetLocalNonce();
hasher << pfrom->nRemoteHostNonce;
hasher << pfrom->GetLocalExtraEntropy();
hasher << pfrom->nRemoteExtraEntropy;
// Now let's sign!
SchnorrSig sig;
if (!sessionKey.SignSchnorr(hasher.GetHash(), sig)) {
return false;
}
connman->PushMessage(
pfrom, CNetMsgMaker(pfrom->GetCommonVersion())
.Make(NetMsgType::AVAHELLO, Hello(delegation, sig)));
return delegation.getLimitedProofId() != uint256::ZERO;
}
bool Processor::sendHello(CNode *pfrom) {
return WITH_LOCK(cs_delayedAvahelloNodeIds,
return sendHelloInternal(pfrom));
}
void Processor::sendDelayedAvahello() {
LOCK(cs_delayedAvahelloNodeIds);
auto it = delayedAvahelloNodeIds.begin();
while (it != delayedAvahelloNodeIds.end()) {
if (connman->ForNode(*it, [&](CNode *pnode) EXCLUSIVE_LOCKS_REQUIRED(
cs_delayedAvahelloNodeIds) {
return sendHelloInternal(pnode);
})) {
// Our proof has been announced to this node
it = delayedAvahelloNodeIds.erase(it);
} else {
++it;
}
}
}
ProofRef Processor::getLocalProof() const {
return peerData ? peerData->proof : ProofRef();
}
ProofRegistrationState Processor::getLocalProofRegistrationState() const {
return peerData
? WITH_LOCK(peerData->cs_proofState, return peerData->proofState)
: ProofRegistrationState();
}
bool Processor::startEventLoop(CScheduler &scheduler) {
return eventLoop.startEventLoop(
scheduler, [this]() { this->runEventLoop(); }, AVALANCHE_TIME_STEP);
}
bool Processor::stopEventLoop() {
return eventLoop.stopEventLoop();
}
void Processor::avaproofsSent(NodeId nodeid) {
AssertLockNotHeld(cs_main);
if (chainman.ActiveChainstate().IsInitialBlockDownload()) {
// Before IBD is complete there is no way to make sure a proof is valid
// or not, e.g. it can be spent in a block we don't know yet. In order
// to increase confidence that our proof set is similar to other nodes
// on the network, the messages received during IBD are not accounted.
return;
}
LOCK(cs_peerManager);
if (peerManager->latchAvaproofsSent(nodeid)) {
avaproofsNodeCounter++;
}
}
/*
* Returns a bool indicating whether we have a usable Avalanche quorum enabling
* us to take decisions based on polls.
*/
bool Processor::isQuorumEstablished() {
AssertLockNotHeld(cs_main);
{
LOCK(cs_peerManager);
if (peerManager->getNodeCount() < 8) {
// There is no point polling if we know the vote cannot converge
return false;
}
}
/*
* The following parameters can naturally go temporarly below the threshold
* under normal circumstances, like during a proof replacement with a lower
* stake amount, or the discovery of a new proofs for which we don't have a
* node yet.
* In order to prevent our node from starting and stopping the polls
* spuriously on such event, the quorum establishement is latched. The only
* parameters that should not latched is the minimum node count, as this
* would cause the poll to be inconclusive anyway and should not happen
* under normal circumstances.
*/
if (quorumIsEstablished) {
return true;
}
// Don't do Avalanche while node is IBD'ing
if (chainman.ActiveChainstate().IsInitialBlockDownload()) {
return false;
}
if (avaproofsNodeCounter < minAvaproofsNodeCount) {
return false;
}
auto localProof = getLocalProof();
// Get the registered proof score and registered score we have nodes for
uint32_t totalPeersScore;
uint32_t connectedPeersScore;
{
LOCK(cs_peerManager);
totalPeersScore = peerManager->getTotalPeersScore();
connectedPeersScore = peerManager->getConnectedPeersScore();
// Consider that we are always connected to our proof, even if we are
// the single node using that proof.
if (localProof &&
peerManager->forPeer(localProof->getId(), [](const Peer &peer) {
return peer.node_count == 0;
})) {
connectedPeersScore += localProof->getScore();
}
}
// Ensure enough is being staked overall
if (totalPeersScore < minQuorumScore) {
return false;
}
// Ensure we have connected score for enough of the overall score
uint32_t minConnectedScore =
std::round(double(totalPeersScore) * minQuorumConnectedScoreRatio);
if (connectedPeersScore < minConnectedScore) {
return false;
}
quorumIsEstablished = true;
// Attempt to compute the staking rewards winner now so we don't have to
// wait for a block if we already have all the prerequisites.
const CBlockIndex *pprev = WITH_LOCK(cs_main, return chainman.ActiveTip());
if (pprev && IsStakingRewardsActivated(chainman.GetConsensus(), pprev)) {
computeStakingReward(pprev);
}
return true;
}
bool Processor::canShareLocalProof() {
// The flag is latched
if (m_canShareLocalProof) {
return true;
}
// Don't share our proof if we don't have any inbound connection.
// This is a best effort measure to prevent advertising a proof if we have
// limited network connectivity.
m_canShareLocalProof = connman->GetNodeCount(ConnectionDirection::In) > 0;
return m_canShareLocalProof;
}
bool Processor::computeStakingReward(const CBlockIndex *pindex) {
if (!pindex) {
return false;
}
// If the quorum is not established there is no point picking a winner that
// will be rejected.
if (!isQuorumEstablished()) {
return false;
}
{
LOCK(cs_stakingRewards);
if (stakingRewards.count(pindex->GetBlockHash()) > 0) {
return true;
}
}
StakingReward _stakingRewards;
_stakingRewards.blockheight = pindex->nHeight;
bool rewardsInserted = false;
if (WITH_LOCK(cs_peerManager, return peerManager->selectStakingRewardWinner(
pindex, _stakingRewards.winners))) {
LOCK(cs_stakingRewards);
rewardsInserted =
stakingRewards
.emplace(pindex->GetBlockHash(), std::move(_stakingRewards))
.second;
}
if (m_stakingPreConsensus) {
// If pindex has not been promoted in the contender cache yet, this will
// be a no-op.
setContenderStatusForLocalWinners(pindex);
}
return rewardsInserted;
}
bool Processor::eraseStakingRewardWinner(const BlockHash &prevBlockHash) {
LOCK(cs_stakingRewards);
return stakingRewards.erase(prevBlockHash) > 0;
}
void Processor::cleanupStakingRewards(const int minHeight) {
{
LOCK(cs_stakingRewards);
// std::erase_if is only defined since C++20
for (auto it = stakingRewards.begin(); it != stakingRewards.end();) {
if (it->second.blockheight < minHeight) {
it = stakingRewards.erase(it);
} else {
++it;
}
}
}
if (m_stakingPreConsensus) {
WITH_LOCK(cs_stakeContenderCache,
return stakeContenderCache.cleanup(minHeight));
}
}
bool Processor::getStakingRewardWinners(
const BlockHash &prevBlockHash,
std::vector<std::pair<ProofId, CScript>> &winners) const {
LOCK(cs_stakingRewards);
auto it = stakingRewards.find(prevBlockHash);
if (it == stakingRewards.end()) {
return false;
}
winners = it->second.winners;
return true;
}
bool Processor::getStakingRewardWinners(const BlockHash &prevBlockHash,
std::vector<CScript> &payouts) const {
std::vector<std::pair<ProofId, CScript>> winners;
if (!getStakingRewardWinners(prevBlockHash, winners)) {
return false;
}
payouts.clear();
payouts.reserve(winners.size());
for (auto &winner : winners) {
payouts.push_back(std::move(winner.second));
}
return true;
}
bool Processor::setStakingRewardWinners(const CBlockIndex *pprev,
const std::vector<CScript> &payouts) {
assert(pprev);
StakingReward stakingReward;
stakingReward.blockheight = pprev->nHeight;
stakingReward.winners.reserve(payouts.size());
for (const CScript &payout : payouts) {
stakingReward.winners.push_back({ProofId(), payout});
}
if (m_stakingPreConsensus) {
LOCK(cs_stakeContenderCache);
stakeContenderCache.setWinners(pprev, payouts);
}
LOCK(cs_stakingRewards);
return stakingRewards.insert_or_assign(pprev->GetBlockHash(), stakingReward)
.second;
}
void Processor::FinalizeNode(const ::Config &config, const CNode &node) {
AssertLockNotHeld(cs_main);
const NodeId nodeid = node.GetId();
WITH_LOCK(cs_peerManager, peerManager->removeNode(nodeid));
WITH_LOCK(cs_delayedAvahelloNodeIds, delayedAvahelloNodeIds.erase(nodeid));
}
void Processor::addStakeContender(const ProofRef &proof) {
AssertLockHeld(cs_main);
const CBlockIndex *activeTip = chainman.ActiveTip();
WITH_LOCK(cs_stakeContenderCache,
return stakeContenderCache.add(activeTip, proof));
}
int Processor::getStakeContenderStatus(
const StakeContenderId &contenderId) const {
BlockHash prevblockhash;
int status = WITH_LOCK(
cs_stakeContenderCache,
return stakeContenderCache.getVoteStatus(contenderId, prevblockhash));
std::vector<std::pair<ProofId, CScript>> winners;
getStakingRewardWinners(prevblockhash, winners);
if (status != -1 && winners.size() == 0) {
// If we have not selected a local staking rewards winner yet, indicate
// this contender is pending to avoid convergence issues.
return -2;
}
return status;
}
void Processor::promoteStakeContendersToTip() {
const CBlockIndex *activeTip =
WITH_LOCK(cs_main, return chainman.ActiveTip());
assert(activeTip);
if (!hasFinalizedTip()) {
// Avoid growing the contender cache until we have finalized a block
return;
}
{
LOCK(cs_peerManager);
LOCK(cs_stakeContenderCache);
stakeContenderCache.promoteToBlock(activeTip, *peerManager);
}
// If staking rewards have not been computed yet, we will try again when
// they have been.
setContenderStatusForLocalWinners(activeTip);
// TODO reconcile remoteProofs contenders
}
void Processor::setContenderStatusForLocalWinners(const CBlockIndex *pindex) {
const BlockHash prevblockhash = pindex->GetBlockHash();
std::vector<std::pair<ProofId, CScript>> winners;
getStakingRewardWinners(prevblockhash, winners);
if (winners.size() == 0) {
// Staking rewards not computed yet
return;
}
// Set status for local winners
LOCK(cs_stakeContenderCache);
for (const auto &winner : winners) {
const StakeContenderId contenderId(prevblockhash, winner.first);
stakeContenderCache.finalize(contenderId);
}
// Treat the highest ranking contender similarly to local winners except
// that it is not automatically included in the winner set (unless it
// happens to be selected as a local winner).
std::vector<StakeContenderId> pollableContenders;
if (stakeContenderCache.getPollableContenders(
prevblockhash, AVALANCHE_CONTENDER_MAX_POLLABLE,
pollableContenders) > 0) {
// Accept the highest ranking contender. This is a no-op if the highest
// ranking contender is already the local winner.
stakeContenderCache.accept(pollableContenders[0]);
}
}
void Processor::updatedBlockTip() {
const bool registerLocalProof = canShareLocalProof();
auto registerProofs = [&]() {
LOCK(cs_peerManager);
auto registeredProofs = peerManager->updatedBlockTip();
ProofRegistrationState localProofState;
if (peerData && peerData->proof && registerLocalProof) {
if (peerManager->registerProof(peerData->proof, localProofState)) {
registeredProofs.insert(peerData->proof);
}
if (localProofState.GetResult() ==
ProofRegistrationResult::ALREADY_REGISTERED) {
// If our proof already exists, that's fine but we don't want to
// erase the state with a duplicated proof status, so let's
// retrieve the proper state. It also means we are able to
// update the status should the proof move from one pool to the
// other.
const ProofId &localProofId = peerData->proof->getId();
if (peerManager->isImmature(localProofId)) {
localProofState.Invalid(ProofRegistrationResult::IMMATURE,
"immature-proof");
}
if (peerManager->isInConflictingPool(localProofId)) {
localProofState.Invalid(
ProofRegistrationResult::CONFLICTING,
"conflicting-utxos");
}
if (peerManager->isBoundToPeer(localProofId)) {
localProofState = ProofRegistrationState();
}
}
WITH_LOCK(peerData->cs_proofState,
peerData->proofState = std::move(localProofState));
}
return registeredProofs;
};
auto registeredProofs = registerProofs();
for (const auto &proof : registeredProofs) {
reconcileOrFinalize(proof);
}
if (m_stakingPreConsensus) {
promoteStakeContendersToTip();
}
}
void Processor::transactionAddedToMempool(const CTransactionRef &tx) {
if (m_preConsensus) {
addToReconcile(tx);
}
}
void Processor::runEventLoop() {
// Don't poll if quorum hasn't been established yet
if (!isQuorumEstablished()) {
return;
}
// First things first, check if we have requests that timed out and clear
// them.
clearTimedoutRequests();
// Make sure there is at least one suitable node to query before gathering
// invs.
NodeId nodeid = WITH_LOCK(cs_peerManager, return peerManager->selectNode());
if (nodeid == NO_NODE) {
return;
}
std::vector<CInv> invs = getInvsForNextPoll();
if (invs.empty()) {
return;
}
LOCK(cs_peerManager);
do {
/**
* If we lost contact to that node, then we remove it from nodeids, but
* never add the request to queries, which ensures bad nodes get cleaned
* up over time.
*/
bool hasSent = connman->ForNode(
nodeid, [this, &invs](CNode *pnode) EXCLUSIVE_LOCKS_REQUIRED(
cs_peerManager) {
uint64_t current_round = round++;
{
// Compute the time at which this requests times out.
auto timeout = Now<SteadyMilliseconds>() +
avaconfig.queryTimeoutDuration;
// Register the query.
queries.getWriteView()->insert(
{pnode->GetId(), current_round, timeout, invs});
// Set the timeout.
peerManager->updateNextRequestTime(pnode->GetId(), timeout);
}
pnode->invsPolled(invs.size());
// Send the query to the node.
connman->PushMessage(
pnode, CNetMsgMaker(pnode->GetCommonVersion())
.Make(NetMsgType::AVAPOLL,
Poll(current_round, std::move(invs))));
return true;
});
// Success!
if (hasSent) {
return;
}
// This node is obsolete, delete it.
peerManager->removeNode(nodeid);
// Get next suitable node to try again
nodeid = peerManager->selectNode();
} while (nodeid != NO_NODE);
}
void Processor::clearTimedoutRequests() {
auto now = Now<SteadyMilliseconds>();
std::map<CInv, uint8_t> timedout_items{};
{
// Clear expired requests.
auto w = queries.getWriteView();
auto it = w->get<query_timeout>().begin();
while (it != w->get<query_timeout>().end() && it->timeout < now) {
for (const auto &i : it->invs) {
timedout_items[i]++;
}
w->get<query_timeout>().erase(it++);
}
}
if (timedout_items.empty()) {
return;
}
// In flight request accounting.
auto voteRecordsWriteView = voteRecords.getWriteView();
for (const auto &p : timedout_items) {
auto item = getVoteItemFromInv(p.first);
if (isNull(item)) {
continue;
}
auto it = voteRecordsWriteView->find(item);
if (it == voteRecordsWriteView.end()) {
continue;
}
it->second.clearInflightRequest(p.second);
}
}
std::vector<CInv> Processor::getInvsForNextPoll(bool forPoll) {
std::vector<CInv> invs;
{
// First remove all items that are not worth polling.
auto w = voteRecords.getWriteView();
for (auto it = w->begin(); it != w->end();) {
if (!isWorthPolling(it->first)) {
it = w->erase(it);
} else {
++it;
}
}
}
auto buildInvFromVoteItem = variant::overloaded{
[](const ProofRef &proof) {
return CInv(MSG_AVA_PROOF, proof->getId());
},
[](const CBlockIndex *pindex) {
return CInv(MSG_BLOCK, pindex->GetBlockHash());
},
[](const CTransactionRef &tx) { return CInv(MSG_TX, tx->GetHash()); },
};
auto r = voteRecords.getReadView();
for (const auto &[item, voteRecord] : r) {
if (invs.size() >= AVALANCHE_MAX_ELEMENT_POLL) {
// Make sure we do not produce more invs than specified by the
// protocol.
return invs;
}
const bool shouldPoll =
forPoll ? voteRecord.registerPoll() : voteRecord.shouldPoll();
if (!shouldPoll) {
continue;
}
invs.emplace_back(std::visit(buildInvFromVoteItem, item));
}
return invs;
}
AnyVoteItem Processor::getVoteItemFromInv(const CInv &inv) const {
if (inv.IsMsgBlk()) {
return WITH_LOCK(cs_main, return chainman.m_blockman.LookupBlockIndex(
BlockHash(inv.hash)));
}
if (inv.IsMsgProof()) {
return WITH_LOCK(cs_peerManager,
return peerManager->getProof(ProofId(inv.hash)));
}
if (mempool && inv.IsMsgTx()) {
LOCK(mempool->cs);
if (CTransactionRef tx = mempool->get(TxId(inv.hash))) {
return tx;
}
if (CTransactionRef tx = mempool->withConflicting(
[&inv](const TxConflicting &conflicting) {
return conflicting.GetTx(TxId(inv.hash));
})) {
return tx;
}
}
return {nullptr};
}
bool Processor::IsWorthPolling::operator()(const CBlockIndex *pindex) const {
AssertLockNotHeld(cs_main);
LOCK(cs_main);
if (pindex->nStatus.isInvalid()) {
// No point polling invalid blocks.
return false;
}
if (WITH_LOCK(processor.cs_finalizationTip,
return processor.finalizationTip &&
processor.finalizationTip->GetAncestor(
pindex->nHeight) == pindex)) {
// There is no point polling blocks that are ancestor of a block that
// has been accepted by the network.
return false;
}
if (WITH_LOCK(processor.cs_invalidatedBlocks,
return processor.invalidatedBlocks.contains(
pindex->GetBlockHash()))) {
// Blocks invalidated by Avalanche should not be polled twice.
return false;
}
return true;
}
bool Processor::IsWorthPolling::operator()(const ProofRef &proof) const {
// Avoid lock order issues cs_main -> cs_peerManager
AssertLockNotHeld(::cs_main);
AssertLockNotHeld(processor.cs_peerManager);
const ProofId &proofid = proof->getId();
LOCK(processor.cs_peerManager);
// No point polling immature or discarded proofs
return processor.peerManager->isBoundToPeer(proofid) ||
processor.peerManager->isInConflictingPool(proofid);
}
bool Processor::IsWorthPolling::operator()(const CTransactionRef &tx) const {
if (!processor.mempool) {
return false;
}
AssertLockNotHeld(processor.mempool->cs);
LOCK(processor.mempool->cs);
return processor.mempool->exists(tx->GetId()) ||
processor.mempool->withConflicting(
[&tx](const TxConflicting &conflicting) {
return conflicting.HaveTx(tx->GetId());
});
}
bool Processor::isWorthPolling(const AnyVoteItem &item) const {
return std::visit(IsWorthPolling(*this), item) &&
!isRecentlyFinalized(GetVoteItemId(item));
}
bool Processor::GetLocalAcceptance::operator()(
const CBlockIndex *pindex) const {
AssertLockNotHeld(cs_main);
return WITH_LOCK(cs_main,
return processor.chainman.ActiveChain().Contains(pindex));
}
bool Processor::GetLocalAcceptance::operator()(const ProofRef &proof) const {
AssertLockNotHeld(processor.cs_peerManager);
return WITH_LOCK(
processor.cs_peerManager,
return processor.peerManager->isBoundToPeer(proof->getId()));
}
bool Processor::GetLocalAcceptance::operator()(
const CTransactionRef &tx) const {
if (!processor.mempool) {
return false;
}
AssertLockNotHeld(processor.mempool->cs);
return WITH_LOCK(processor.mempool->cs,
return processor.mempool->exists(tx->GetId()));
}
} // namespace avalanche
diff --git a/src/avalanche/test/processor_tests.cpp b/src/avalanche/test/processor_tests.cpp
index 3bccaf813..e0bc54521 100644
--- a/src/avalanche/test/processor_tests.cpp
+++ b/src/avalanche/test/processor_tests.cpp
@@ -1,2656 +1,2649 @@
// Copyright (c) 2018-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/processor.h>
#include <arith_uint256.h>
#include <avalanche/avalanche.h>
#include <avalanche/delegationbuilder.h>
#include <avalanche/peermanager.h>
#include <avalanche/proofbuilder.h>
#include <avalanche/voterecord.h>
#include <chain.h>
#include <config.h>
#include <core_io.h>
#include <key_io.h>
#include <net_processing.h> // For ::PeerManager
#include <reverse_iterator.h>
#include <scheduler.h>
#include <util/time.h>
#include <util/translation.h> // For bilingual_str
#include <avalanche/test/util.h>
#include <test/util/setup_common.h>
#include <boost/mpl/list.hpp>
#include <boost/test/unit_test.hpp>
#include <functional>
#include <limits>
#include <type_traits>
#include <vector>
using namespace avalanche;
namespace avalanche {
namespace {
struct AvalancheTest {
static void runEventLoop(avalanche::Processor &p) { p.runEventLoop(); }
static std::vector<CInv> getInvsForNextPoll(Processor &p) {
return p.getInvsForNextPoll(false);
}
static NodeId getSuitableNodeToQuery(Processor &p) {
return WITH_LOCK(p.cs_peerManager,
return p.peerManager->selectNode());
}
static uint64_t getRound(const Processor &p) { return p.round; }
static uint32_t getMinQuorumScore(const Processor &p) {
return p.minQuorumScore;
}
static double getMinQuorumConnectedScoreRatio(const Processor &p) {
return p.minQuorumConnectedScoreRatio;
}
static void clearavaproofsNodeCounter(Processor &p) {
p.avaproofsNodeCounter = 0;
}
static void addVoteRecord(Processor &p, AnyVoteItem &item,
VoteRecord &voteRecord) {
p.voteRecords.getWriteView()->insert(
std::make_pair(item, voteRecord));
}
static void setFinalizationTip(Processor &p,
const CBlockIndex *pindex) {
LOCK(p.cs_finalizationTip);
p.finalizationTip = pindex;
}
static void setLocalProofShareable(Processor &p, bool shareable) {
p.m_canShareLocalProof = shareable;
}
static void updatedBlockTip(Processor &p) { p.updatedBlockTip(); }
static void addProofToRecentfinalized(Processor &p,
const ProofId &proofid) {
WITH_LOCK(p.cs_finalizedItems,
return p.finalizedItems.insert(proofid));
}
};
} // namespace
struct TestVoteRecord : public VoteRecord {
explicit TestVoteRecord(uint16_t conf) : VoteRecord(true) {
confidence |= conf << 1;
}
};
} // namespace avalanche
namespace {
struct CConnmanTest : public CConnman {
using CConnman::CConnman;
void AddNode(CNode &node) {
LOCK(m_nodes_mutex);
m_nodes.push_back(&node);
}
void ClearNodes() {
LOCK(m_nodes_mutex);
for (CNode *node : m_nodes) {
delete node;
}
m_nodes.clear();
}
};
CService ip(uint32_t i) {
struct in_addr s;
s.s_addr = i;
return CService(CNetAddr(s), Params().GetDefaultPort());
}
struct AvalancheTestingSetup : public TestChain100Setup {
const ::Config &config;
CConnmanTest *m_connman;
std::unique_ptr<Processor> m_processor;
// The master private key we delegate to.
CKey masterpriv;
std::unordered_set<std::string> m_overridden_args;
AvalancheTestingSetup()
: TestChain100Setup(), config(GetConfig()),
masterpriv(CKey::MakeCompressedKey()) {
// Deterministic randomness for tests.
auto connman = std::make_unique<CConnmanTest>(config, 0x1337, 0x1337,
*m_node.addrman);
m_connman = connman.get();
m_node.connman = std::move(connman);
// Get the processor ready.
setArg("-avaminquorumstake", "0");
setArg("-avaminquorumconnectedstakeratio", "0");
setArg("-avaminavaproofsnodecount", "0");
setArg("-avaproofstakeutxoconfirmations", "1");
bilingual_str error;
m_processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(),
*Assert(m_node.chainman), m_node.mempool.get(), *m_node.scheduler,
error);
BOOST_CHECK(m_processor);
m_node.peerman = ::PeerManager::make(
*m_connman, *m_node.addrman, m_node.banman.get(), *m_node.chainman,
*m_node.mempool, m_processor.get(), {});
m_node.chain = interfaces::MakeChain(m_node, config.GetChainParams());
}
~AvalancheTestingSetup() {
m_connman->ClearNodes();
SyncWithValidationInterfaceQueue();
ArgsManager &argsman = *Assert(m_node.args);
for (const std::string &key : m_overridden_args) {
argsman.ClearForcedArg(key);
}
m_overridden_args.clear();
}
CNode *ConnectNode(ServiceFlags nServices) {
static NodeId id = 0;
CAddress addr(ip(GetRand<uint32_t>()), NODE_NONE);
auto node =
new CNode(id++, /*sock=*/nullptr, addr,
/* nKeyedNetGroupIn */ 0,
/* nLocalHostNonceIn */ 0,
/* nLocalExtraEntropyIn */ 0, CAddress(),
/* pszDest */ "", ConnectionType::OUTBOUND_FULL_RELAY,
/* inbound_onion */ false);
node->SetCommonVersion(PROTOCOL_VERSION);
node->m_has_all_wanted_services =
HasAllDesirableServiceFlags(nServices);
m_node.peerman->InitializeNode(config, *node, NODE_NETWORK);
node->nVersion = 1;
node->fSuccessfullyConnected = true;
m_connman->AddNode(*node);
return node;
}
ProofRef GetProof(CScript payoutScript = UNSPENDABLE_ECREG_PAYOUT_SCRIPT) {
const CKey key = CKey::MakeCompressedKey();
const COutPoint outpoint{TxId(GetRandHash()), 0};
CScript script = GetScriptForDestination(PKHash(key.GetPubKey()));
const Amount amount = PROOF_DUST_THRESHOLD;
const uint32_t height = 100;
LOCK(cs_main);
CCoinsViewCache &coins =
Assert(m_node.chainman)->ActiveChainstate().CoinsTip();
coins.AddCoin(outpoint, Coin(CTxOut(amount, script), height, false),
false);
ProofBuilder pb(0, 0, masterpriv, payoutScript);
BOOST_CHECK(pb.addUTXO(outpoint, amount, height, false, key));
return pb.build();
}
bool addNode(NodeId nodeid, const ProofId &proofid) {
return m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
return pm.addNode(nodeid, proofid);
});
}
bool addNode(NodeId nodeid) {
auto proof = GetProof();
return m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
return pm.registerProof(proof) &&
pm.addNode(nodeid, proof->getId());
});
}
std::array<CNode *, 8> ConnectNodes() {
auto proof = GetProof();
BOOST_CHECK(
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
return pm.registerProof(proof);
}));
const ProofId &proofid = proof->getId();
std::array<CNode *, 8> nodes;
for (CNode *&n : nodes) {
n = ConnectNode(NODE_AVALANCHE);
BOOST_CHECK(addNode(n->GetId(), proofid));
}
return nodes;
}
void runEventLoop() { AvalancheTest::runEventLoop(*m_processor); }
NodeId getSuitableNodeToQuery() {
return AvalancheTest::getSuitableNodeToQuery(*m_processor);
}
std::vector<CInv> getInvsForNextPoll() {
return AvalancheTest::getInvsForNextPoll(*m_processor);
}
uint64_t getRound() const { return AvalancheTest::getRound(*m_processor); }
bool registerVotes(NodeId nodeid, const avalanche::Response &response,
std::vector<avalanche::VoteItemUpdate> &updates,
std::string &error) {
int banscore;
return m_processor->registerVotes(nodeid, response, updates, banscore,
error);
}
bool registerVotes(NodeId nodeid, const avalanche::Response &response,
std::vector<avalanche::VoteItemUpdate> &updates) {
int banscore;
std::string error;
return m_processor->registerVotes(nodeid, response, updates, banscore,
error);
}
void setArg(std::string key, const std::string &value) {
ArgsManager &argsman = *Assert(m_node.args);
argsman.ForceSetArg(key, value);
m_overridden_args.emplace(std::move(key));
}
bool addToReconcile(const AnyVoteItem &item) {
return m_processor->addToReconcile(item);
}
};
struct BlockProvider {
AvalancheTestingSetup *fixture;
uint32_t invType;
BlockProvider(AvalancheTestingSetup *_fixture)
: fixture(_fixture), invType(MSG_BLOCK) {}
CBlockIndex *buildVoteItem() const {
CBlock block = fixture->CreateAndProcessBlock({}, CScript());
const BlockHash blockHash = block.GetHash();
LOCK(cs_main);
return Assert(fixture->m_node.chainman)
->m_blockman.LookupBlockIndex(blockHash);
}
uint256 getVoteItemId(const CBlockIndex *pindex) const {
return pindex->GetBlockHash();
}
std::vector<Vote> buildVotesForItems(uint32_t error,
std::vector<CBlockIndex *> &&items) {
size_t numItems = items.size();
std::vector<Vote> votes;
votes.reserve(numItems);
// Votes are sorted by most work first
std::sort(items.begin(), items.end(), CBlockIndexWorkComparator());
for (auto &item : reverse_iterate(items)) {
votes.emplace_back(error, item->GetBlockHash());
}
return votes;
}
void invalidateItem(CBlockIndex *pindex) {
LOCK(::cs_main);
pindex->nStatus = pindex->nStatus.withFailed();
}
const CBlockIndex *fromAnyVoteItem(const AnyVoteItem &item) {
return std::get<const CBlockIndex *>(item);
}
};
struct ProofProvider {
AvalancheTestingSetup *fixture;
uint32_t invType;
ProofProvider(AvalancheTestingSetup *_fixture)
: fixture(_fixture), invType(MSG_AVA_PROOF) {}
ProofRef buildVoteItem() const {
const ProofRef proof = fixture->GetProof();
fixture->m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof));
});
return proof;
}
uint256 getVoteItemId(const ProofRef &proof) const {
return proof->getId();
}
std::vector<Vote> buildVotesForItems(uint32_t error,
std::vector<ProofRef> &&items) {
size_t numItems = items.size();
std::vector<Vote> votes;
votes.reserve(numItems);
// Votes are sorted by high score first
std::sort(items.begin(), items.end(), ProofComparatorByScore());
for (auto &item : items) {
votes.emplace_back(error, item->getId());
}
return votes;
}
void invalidateItem(const ProofRef &proof) {
fixture->m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.rejectProof(proof->getId(),
avalanche::PeerManager::RejectionMode::INVALIDATE);
});
}
const ProofRef fromAnyVoteItem(const AnyVoteItem &item) {
return std::get<const ProofRef>(item);
}
};
struct TxProvider {
AvalancheTestingSetup *fixture;
std::vector<avalanche::VoteItemUpdate> updates;
uint32_t invType;
TxProvider(AvalancheTestingSetup *_fixture)
: fixture(_fixture), invType(MSG_TX) {}
CTransactionRef buildVoteItem() const {
CMutableTransaction mtx;
mtx.nVersion = 2;
mtx.vin.emplace_back(COutPoint{TxId(FastRandomContext().rand256()), 0});
mtx.vout.emplace_back(1 * COIN, CScript() << OP_TRUE);
CTransactionRef tx = MakeTransactionRef(std::move(mtx));
TestMemPoolEntryHelper mempoolEntryHelper;
auto entry = mempoolEntryHelper.FromTx(tx);
CTxMemPool *mempool = Assert(fixture->m_node.mempool.get());
{
LOCK2(cs_main, mempool->cs);
mempool->addUnchecked(entry);
BOOST_CHECK(mempool->exists(tx->GetId()));
}
return tx;
}
uint256 getVoteItemId(const CTransactionRef &tx) const {
return tx->GetId();
}
std::vector<Vote> buildVotesForItems(uint32_t error,
std::vector<CTransactionRef> &&items) {
size_t numItems = items.size();
std::vector<Vote> votes;
votes.reserve(numItems);
// Transactions are sorted by TxId
std::sort(items.begin(), items.end(),
[](const CTransactionRef &lhs, const CTransactionRef &rhs) {
return lhs->GetId() < rhs->GetId();
});
for (auto &item : items) {
votes.emplace_back(error, item->GetId());
}
return votes;
}
void invalidateItem(const CTransactionRef &tx) {
BOOST_CHECK(tx != nullptr);
CTxMemPool *mempool = Assert(fixture->m_node.mempool.get());
LOCK(mempool->cs);
mempool->removeRecursive(*tx, MemPoolRemovalReason::CONFLICT);
BOOST_CHECK(!mempool->exists(tx->GetId()));
}
const CTransactionRef fromAnyVoteItem(const AnyVoteItem &item) {
return std::get<const CTransactionRef>(item);
}
};
} // namespace
BOOST_FIXTURE_TEST_SUITE(processor_tests, AvalancheTestingSetup)
// FIXME A std::tuple can be used instead of boost::mpl::list after boost 1.67
using VoteItemProviders =
boost::mpl::list<BlockProvider, ProofProvider, TxProvider>;
BOOST_AUTO_TEST_CASE_TEMPLATE(voteitemupdate, P, VoteItemProviders) {
P provider(this);
std::set<VoteStatus> status{
VoteStatus::Invalid, VoteStatus::Rejected, VoteStatus::Accepted,
VoteStatus::Finalized, VoteStatus::Stale,
};
auto item = provider.buildVoteItem();
for (auto s : status) {
VoteItemUpdate itemUpdate(item, s);
// The use of BOOST_CHECK instead of BOOST_CHECK_EQUAL prevents from
// having to define operator<<() for each argument type.
BOOST_CHECK(provider.fromAnyVoteItem(itemUpdate.getVoteItem()) == item);
BOOST_CHECK(itemUpdate.getStatus() == s);
}
}
namespace {
Response next(Response &r) {
auto copy = r;
r = {r.getRound() + 1, r.getCooldown(), r.GetVotes()};
return copy;
}
} // namespace
BOOST_AUTO_TEST_CASE_TEMPLATE(item_reconcile_twice, P, VoteItemProviders) {
P provider(this);
ChainstateManager &chainman = *Assert(m_node.chainman);
const CBlockIndex *chaintip =
WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip());
auto item = provider.buildVoteItem();
auto itemid = provider.getVoteItemId(item);
// Adding the item twice does nothing.
BOOST_CHECK(addToReconcile(item));
BOOST_CHECK(!addToReconcile(item));
BOOST_CHECK(m_processor->isAccepted(item));
// Create nodes that supports avalanche so we can finalize the item.
auto avanodes = ConnectNodes();
int nextNodeIndex = 0;
std::vector<avalanche::VoteItemUpdate> updates;
auto registerNewVote = [&](const Response &resp) {
runEventLoop();
auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId();
BOOST_CHECK(registerVotes(nodeid, resp, updates));
};
// Finalize the item.
auto finalize = [&](const auto finalizeItemId) {
Response resp = {getRound(), 0, {Vote(0, finalizeItemId)}};
for (int i = 0; i < AVALANCHE_FINALIZATION_SCORE + 6; i++) {
registerNewVote(next(resp));
if (updates.size() > 0) {
break;
}
}
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized);
- updates.clear();
};
finalize(itemid);
// The finalized item cannot be reconciled for a while.
BOOST_CHECK(!addToReconcile(item));
auto finalizeNewItem = [&]() {
auto anotherItem = provider.buildVoteItem();
AnyVoteItem anotherVoteItem = AnyVoteItem(anotherItem);
auto anotherItemId = provider.getVoteItemId(anotherItem);
TestVoteRecord voteRecord(AVALANCHE_FINALIZATION_SCORE - 1);
AvalancheTest::addVoteRecord(*m_processor, anotherVoteItem, voteRecord);
finalize(anotherItemId);
};
// The filter can have new items added up to its size and the item will
// still not reconcile.
for (uint32_t i = 0; i < AVALANCHE_FINALIZED_ITEMS_FILTER_NUM_ELEMENTS;
i++) {
finalizeNewItem();
BOOST_CHECK(!addToReconcile(item));
}
// But if we keep going it will eventually roll out of the filter and can
// be reconciled again.
for (uint32_t i = 0; i < AVALANCHE_FINALIZED_ITEMS_FILTER_NUM_ELEMENTS;
i++) {
finalizeNewItem();
}
// Roll back the finalization point so that reconciling the old block does
// not fail the finalization check. This is a no-op for other types.
AvalancheTest::setFinalizationTip(*m_processor, chaintip);
BOOST_CHECK(addToReconcile(item));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(item_null, P, VoteItemProviders) {
P provider(this);
// Check that null case is handled on the public interface
BOOST_CHECK(!m_processor->isAccepted(nullptr));
BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1);
auto item = decltype(provider.buildVoteItem())();
BOOST_CHECK(item == nullptr);
BOOST_CHECK(!addToReconcile(item));
// Check that adding item to vote on doesn't change the outcome. A
// comparator is used under the hood, and this is skipped if there are no
// vote records.
item = provider.buildVoteItem();
BOOST_CHECK(addToReconcile(item));
BOOST_CHECK(!m_processor->isAccepted(nullptr));
BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1);
}
BOOST_AUTO_TEST_CASE_TEMPLATE(vote_item_register, P, VoteItemProviders) {
P provider(this);
const uint32_t invType = provider.invType;
auto item = provider.buildVoteItem();
auto itemid = provider.getVoteItemId(item);
// Create nodes that supports avalanche.
auto avanodes = ConnectNodes();
// Querying for random item returns false.
BOOST_CHECK(!m_processor->isAccepted(item));
// Add a new item. Check it is added to the polls.
BOOST_CHECK(addToReconcile(item));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
BOOST_CHECK(m_processor->isAccepted(item));
int nextNodeIndex = 0;
std::vector<avalanche::VoteItemUpdate> updates;
auto registerNewVote = [&](const Response &resp) {
runEventLoop();
auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId();
BOOST_CHECK(registerVotes(nodeid, resp, updates));
};
// Let's vote for this item a few times.
Response resp{0, 0, {Vote(0, itemid)}};
for (int i = 0; i < 6; i++) {
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 0);
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// A single neutral vote do not change anything.
resp = {getRound(), 0, {Vote(-1, itemid)}};
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 0);
BOOST_CHECK_EQUAL(updates.size(), 0);
resp = {getRound(), 0, {Vote(0, itemid)}};
for (int i = 1; i < 7; i++) {
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), i);
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// Two neutral votes will stall progress.
resp = {getRound(), 0, {Vote(-1, itemid)}};
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 6);
BOOST_CHECK_EQUAL(updates.size(), 0);
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 6);
BOOST_CHECK_EQUAL(updates.size(), 0);
resp = {getRound(), 0, {Vote(0, itemid)}};
for (int i = 2; i < 8; i++) {
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), 6);
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// We vote for it numerous times to finalize it.
for (int i = 7; i < AVALANCHE_FINALIZATION_SCORE; i++) {
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item), i);
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// As long as it is not finalized, we poll.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
// Now finalize the decision.
registerNewVote(next(resp));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized);
- updates.clear();
// Once the decision is finalized, there is no poll for it.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 0);
// Get a new item to vote on
item = provider.buildVoteItem();
itemid = provider.getVoteItemId(item);
BOOST_CHECK(addToReconcile(item));
// Now let's finalize rejection.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
resp = {getRound(), 0, {Vote(1, itemid)}};
for (int i = 0; i < 6; i++) {
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// Now the state will flip.
registerNewVote(next(resp));
BOOST_CHECK(!m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Rejected);
- updates.clear();
// Now it is rejected, but we can vote for it numerous times.
for (int i = 1; i < AVALANCHE_FINALIZATION_SCORE; i++) {
registerNewVote(next(resp));
BOOST_CHECK(!m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// As long as it is not finalized, we poll.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
// Now finalize the decision.
registerNewVote(next(resp));
BOOST_CHECK(!m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Invalid);
- updates.clear();
// Once the decision is finalized, there is no poll for it.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 0);
}
BOOST_AUTO_TEST_CASE_TEMPLATE(multi_item_register, P, VoteItemProviders) {
P provider(this);
const uint32_t invType = provider.invType;
auto itemA = provider.buildVoteItem();
auto itemidA = provider.getVoteItemId(itemA);
auto itemB = provider.buildVoteItem();
auto itemidB = provider.getVoteItemId(itemB);
// Create several nodes that support avalanche.
auto avanodes = ConnectNodes();
// Querying for random item returns false.
BOOST_CHECK(!m_processor->isAccepted(itemA));
BOOST_CHECK(!m_processor->isAccepted(itemB));
// Start voting on item A.
BOOST_CHECK(addToReconcile(itemA));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemidA);
uint64_t round = getRound();
runEventLoop();
std::vector<avalanche::VoteItemUpdate> updates;
BOOST_CHECK(registerVotes(avanodes[0]->GetId(),
{round, 0, {Vote(0, itemidA)}}, updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
// Start voting on item B after one vote.
std::vector<Vote> votes = provider.buildVotesForItems(0, {itemA, itemB});
Response resp{round + 1, 0, votes};
BOOST_CHECK(addToReconcile(itemB));
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 2);
// Ensure the inv ordering is as expected
for (size_t i = 0; i < invs.size(); i++) {
BOOST_CHECK_EQUAL(invs[i].type, invType);
BOOST_CHECK(invs[i].hash == votes[i].GetHash());
}
// Let's vote for these items a few times.
for (int i = 0; i < 4; i++) {
NodeId nodeid = getSuitableNodeToQuery();
runEventLoop();
BOOST_CHECK(registerVotes(nodeid, next(resp), updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// Now it is accepted, but we can vote for it numerous times.
for (int i = 0; i < AVALANCHE_FINALIZATION_SCORE; i++) {
NodeId nodeid = getSuitableNodeToQuery();
runEventLoop();
BOOST_CHECK(registerVotes(nodeid, next(resp), updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// Running two iterration of the event loop so that vote gets triggered on A
// and B.
NodeId firstNodeid = getSuitableNodeToQuery();
runEventLoop();
NodeId secondNodeid = getSuitableNodeToQuery();
runEventLoop();
BOOST_CHECK(firstNodeid != secondNodeid);
// Next vote will finalize item A.
BOOST_CHECK(registerVotes(firstNodeid, next(resp), updates));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == itemA);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized);
- updates.clear();
// We do not vote on A anymore.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemidB);
// Next vote will finalize item B.
BOOST_CHECK(registerVotes(secondNodeid, resp, updates));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == itemB);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized);
- updates.clear();
// There is nothing left to vote on.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 0);
}
BOOST_AUTO_TEST_CASE_TEMPLATE(poll_and_response, P, VoteItemProviders) {
P provider(this);
const uint32_t invType = provider.invType;
auto item = provider.buildVoteItem();
auto itemid = provider.getVoteItemId(item);
// There is no node to query.
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), NO_NODE);
// Add enough nodes to have a valid quorum, and the same amount with no
// avalanche support
std::set<NodeId> avanodeIds;
auto avanodes = ConnectNodes();
for (auto avanode : avanodes) {
ConnectNode(NODE_NONE);
avanodeIds.insert(avanode->GetId());
}
auto getSelectedAvanodeId = [&]() {
NodeId avanodeid = getSuitableNodeToQuery();
BOOST_CHECK(avanodeIds.find(avanodeid) != avanodeIds.end());
return avanodeid;
};
// It returns one of the avalanche peer.
NodeId avanodeid = getSelectedAvanodeId();
// Register an item and check it is added to the list of elements to poll.
BOOST_CHECK(addToReconcile(item));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
std::set<NodeId> unselectedNodeids = avanodeIds;
unselectedNodeids.erase(avanodeid);
const size_t remainingNodeIds = unselectedNodeids.size();
uint64_t round = getRound();
for (size_t i = 0; i < remainingNodeIds; i++) {
// Trigger a poll on avanode.
runEventLoop();
// Another node is selected
NodeId nodeid = getSuitableNodeToQuery();
BOOST_CHECK(unselectedNodeids.find(nodeid) != avanodeIds.end());
unselectedNodeids.erase(nodeid);
}
// There is no more suitable peer available, so return nothing.
BOOST_CHECK(unselectedNodeids.empty());
runEventLoop();
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), NO_NODE);
// Respond to the request.
Response resp = {round, 0, {Vote(0, itemid)}};
std::vector<avalanche::VoteItemUpdate> updates;
BOOST_CHECK(registerVotes(avanodeid, resp, updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
// Now that avanode fullfilled his request, it is added back to the list of
// queriable nodes.
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
auto checkRegisterVotesError = [&](NodeId nodeid,
const avalanche::Response &response,
const std::string &expectedError) {
std::string error;
BOOST_CHECK(!registerVotes(nodeid, response, updates, error));
BOOST_CHECK_EQUAL(error, expectedError);
BOOST_CHECK_EQUAL(updates.size(), 0);
};
// Sending a response when not polled fails.
checkRegisterVotesError(avanodeid, next(resp), "unexpected-ava-response");
// Trigger a poll on avanode.
round = getRound();
runEventLoop();
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), NO_NODE);
// Sending responses that do not match the request also fails.
// 1. Too many results.
resp = {round, 0, {Vote(0, itemid), Vote(0, itemid)}};
runEventLoop();
checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-size");
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
// 2. Not enough results.
resp = {getRound(), 0, {}};
runEventLoop();
checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-size");
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
// 3. Do not match the poll.
resp = {getRound(), 0, {Vote()}};
runEventLoop();
checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-content");
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
// At this stage we have reached the max inflight requests for our inv, so
// it won't be requested anymore until the requests are fullfilled. Let's
// vote on another item with no inflight request so the remaining tests
// makes sense.
invs = getInvsForNextPoll();
BOOST_CHECK(invs.empty());
item = provider.buildVoteItem();
itemid = provider.getVoteItemId(item);
BOOST_CHECK(addToReconcile(item));
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
// 4. Invalid round count. Request is not discarded.
uint64_t queryRound = getRound();
runEventLoop();
resp = {queryRound + 1, 0, {Vote()}};
checkRegisterVotesError(avanodeid, resp, "unexpected-ava-response");
resp = {queryRound - 1, 0, {Vote()}};
checkRegisterVotesError(avanodeid, resp, "unexpected-ava-response");
// 5. Making request for invalid nodes do not work. Request is not
// discarded.
resp = {queryRound, 0, {Vote(0, itemid)}};
checkRegisterVotesError(avanodeid + 1234, resp, "unexpected-ava-response");
// Proper response gets processed and avanode is available again.
resp = {queryRound, 0, {Vote(0, itemid)}};
BOOST_CHECK(registerVotes(avanodeid, resp, updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
// Out of order response are rejected.
const auto item2 = provider.buildVoteItem();
BOOST_CHECK(addToReconcile(item2));
std::vector<Vote> votes = provider.buildVotesForItems(0, {item, item2});
resp = {getRound(), 0, {votes[1], votes[0]}};
runEventLoop();
checkRegisterVotesError(avanodeid, resp, "invalid-ava-response-content");
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
// But they are accepted in order.
resp = {getRound(), 0, votes};
runEventLoop();
BOOST_CHECK(registerVotes(avanodeid, resp, updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid);
}
BOOST_AUTO_TEST_CASE_TEMPLATE(dont_poll_invalid_item, P, VoteItemProviders) {
P provider(this);
const uint32_t invType = provider.invType;
auto itemA = provider.buildVoteItem();
auto itemB = provider.buildVoteItem();
auto avanodes = ConnectNodes();
// Build votes to get proper ordering
std::vector<Vote> votes = provider.buildVotesForItems(0, {itemA, itemB});
// Register the items and check they are added to the list of elements to
// poll.
BOOST_CHECK(addToReconcile(itemA));
BOOST_CHECK(addToReconcile(itemB));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 2);
for (size_t i = 0; i < invs.size(); i++) {
BOOST_CHECK_EQUAL(invs[i].type, invType);
BOOST_CHECK(invs[i].hash == votes[i].GetHash());
}
// When an item is marked invalid, stop polling.
provider.invalidateItem(itemB);
Response goodResp{getRound(), 0, {Vote(0, provider.getVoteItemId(itemA))}};
std::vector<avalanche::VoteItemUpdate> updates;
runEventLoop();
BOOST_CHECK(registerVotes(avanodes[0]->GetId(), goodResp, updates));
BOOST_CHECK_EQUAL(updates.size(), 0);
// Votes including itemB are rejected
Response badResp{getRound(), 0, votes};
runEventLoop();
std::string error;
BOOST_CHECK(!registerVotes(avanodes[1]->GetId(), badResp, updates, error));
BOOST_CHECK_EQUAL(error, "invalid-ava-response-size");
}
BOOST_TEST_DECORATOR(*boost::unit_test::timeout(60))
BOOST_AUTO_TEST_CASE_TEMPLATE(poll_inflight_timeout, P, VoteItemProviders) {
P provider(this);
ChainstateManager &chainman = *Assert(m_node.chainman);
auto queryTimeDuration = std::chrono::milliseconds(10);
setArg("-avatimeout", ToString(queryTimeDuration.count()));
bilingual_str error;
m_processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(), chainman,
m_node.mempool.get(), *m_node.scheduler, error);
const auto item = provider.buildVoteItem();
const auto itemid = provider.getVoteItemId(item);
// Add the item
BOOST_CHECK(addToReconcile(item));
// Create a quorum of nodes that support avalanche.
ConnectNodes();
NodeId avanodeid = NO_NODE;
// Expire requests after some time.
for (int i = 0; i < 10; i++) {
Response resp = {getRound(), 0, {Vote(0, itemid)}};
avanodeid = getSuitableNodeToQuery();
auto start = Now<SteadyMilliseconds>();
runEventLoop();
// We cannot guarantee that we'll wait for just 1ms, so we have to bail
// if we aren't within the proper time range.
std::this_thread::sleep_for(std::chrono::milliseconds(1));
runEventLoop();
std::vector<avalanche::VoteItemUpdate> updates;
bool ret = registerVotes(avanodeid, next(resp), updates);
if (Now<SteadyMilliseconds>() > start + queryTimeDuration) {
// We waited for too long, bail. Because we can't know for sure when
// previous steps ran, ret is not deterministic and we do not check
// it.
i--;
continue;
}
// We are within time bounds, so the vote should have worked.
BOOST_CHECK(ret);
avanodeid = getSuitableNodeToQuery();
// Now try again but wait for expiration.
runEventLoop();
std::this_thread::sleep_for(queryTimeDuration);
runEventLoop();
BOOST_CHECK(!registerVotes(avanodeid, next(resp), updates));
}
}
BOOST_AUTO_TEST_CASE_TEMPLATE(poll_inflight_count, P, VoteItemProviders) {
P provider(this);
const uint32_t invType = provider.invType;
// Create enough nodes so that we run into the inflight request limit.
auto proof = GetProof();
BOOST_CHECK(m_processor->withPeerManager(
[&](avalanche::PeerManager &pm) { return pm.registerProof(proof); }));
std::array<CNode *, AVALANCHE_MAX_INFLIGHT_POLL + 1> nodes;
for (auto &n : nodes) {
n = ConnectNode(NODE_AVALANCHE);
BOOST_CHECK(addNode(n->GetId(), proof->getId()));
}
// Add an item to poll
const auto item = provider.buildVoteItem();
const auto itemid = provider.getVoteItemId(item);
BOOST_CHECK(addToReconcile(item));
// Ensure there are enough requests in flight.
std::map<NodeId, uint64_t> node_round_map;
for (int i = 0; i < AVALANCHE_MAX_INFLIGHT_POLL; i++) {
NodeId nodeid = getSuitableNodeToQuery();
BOOST_CHECK(node_round_map.find(nodeid) == node_round_map.end());
node_round_map.insert(std::pair<NodeId, uint64_t>(nodeid, getRound()));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
runEventLoop();
}
// Now that we have enough in flight requests, we shouldn't poll.
auto suitablenodeid = getSuitableNodeToQuery();
BOOST_CHECK(suitablenodeid != NO_NODE);
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 0);
runEventLoop();
BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), suitablenodeid);
// Send one response, now we can poll again.
auto it = node_round_map.begin();
Response resp = {it->second, 0, {Vote(0, itemid)}};
std::vector<avalanche::VoteItemUpdate> updates;
BOOST_CHECK(registerVotes(it->first, resp, updates));
node_round_map.erase(it);
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
}
BOOST_AUTO_TEST_CASE(quorum_diversity) {
std::vector<VoteItemUpdate> updates;
CBlock block = CreateAndProcessBlock({}, CScript());
const BlockHash blockHash = block.GetHash();
const CBlockIndex *pindex;
{
LOCK(cs_main);
pindex =
Assert(m_node.chainman)->m_blockman.LookupBlockIndex(blockHash);
}
// Create nodes that supports avalanche.
auto avanodes = ConnectNodes();
// Querying for random block returns false.
BOOST_CHECK(!m_processor->isAccepted(pindex));
// Add a new block. Check it is added to the polls.
BOOST_CHECK(m_processor->addToReconcile(pindex));
// Do one valid round of voting.
uint64_t round = getRound();
Response resp{round, 0, {Vote(0, blockHash)}};
// Check that all nodes can vote.
for (size_t i = 0; i < avanodes.size(); i++) {
runEventLoop();
BOOST_CHECK(registerVotes(avanodes[i]->GetId(), next(resp), updates));
}
// Generate a query for every single node.
const NodeId firstNodeId = getSuitableNodeToQuery();
std::map<NodeId, uint64_t> node_round_map;
round = getRound();
for (size_t i = 0; i < avanodes.size(); i++) {
NodeId nodeid = getSuitableNodeToQuery();
BOOST_CHECK(node_round_map.find(nodeid) == node_round_map.end());
node_round_map[nodeid] = getRound();
runEventLoop();
}
// Now only the first node can vote. All others would be duplicate in the
// quorum.
auto confidence = m_processor->getConfidence(pindex);
BOOST_REQUIRE(confidence > 0);
for (auto &[nodeid, r] : node_round_map) {
if (nodeid == firstNodeId) {
// Node 0 is the only one which can vote at this stage.
round = r;
continue;
}
BOOST_CHECK(
registerVotes(nodeid, {r, 0, {Vote(0, blockHash)}}, updates));
BOOST_CHECK_EQUAL(m_processor->getConfidence(pindex), confidence);
}
BOOST_CHECK(
registerVotes(firstNodeId, {round, 0, {Vote(0, blockHash)}}, updates));
BOOST_CHECK_EQUAL(m_processor->getConfidence(pindex), confidence + 1);
}
BOOST_AUTO_TEST_CASE(event_loop) {
CScheduler s;
CBlock block = CreateAndProcessBlock({}, CScript());
const BlockHash blockHash = block.GetHash();
const CBlockIndex *pindex;
{
LOCK(cs_main);
pindex =
Assert(m_node.chainman)->m_blockman.LookupBlockIndex(blockHash);
}
// Starting the event loop.
BOOST_CHECK(m_processor->startEventLoop(s));
// There is one task planned in the next hour (our event loop).
std::chrono::steady_clock::time_point start, stop;
BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 1);
// Starting twice doesn't start it twice.
BOOST_CHECK(!m_processor->startEventLoop(s));
// Start the scheduler thread.
std::thread schedulerThread(std::bind(&CScheduler::serviceQueue, &s));
// Create a quorum of nodes that support avalanche.
auto avanodes = ConnectNodes();
// There is no query in flight at the moment.
NodeId nodeid = getSuitableNodeToQuery();
BOOST_CHECK_NE(nodeid, NO_NODE);
// Add a new block. Check it is added to the polls.
uint64_t queryRound = getRound();
BOOST_CHECK(m_processor->addToReconcile(pindex));
// Wait until all nodes got a poll
for (int i = 0; i < 60 * 1000; i++) {
// Technically, this is a race condition, but this should do just fine
// as we wait up to 1 minute for an event that should take 80ms.
UninterruptibleSleep(std::chrono::milliseconds(1));
if (getRound() == queryRound + avanodes.size()) {
break;
}
}
// Check that we effectively got a request and not timed out.
BOOST_CHECK(getRound() > queryRound);
// Respond and check the cooldown time is respected.
uint64_t responseRound = getRound();
auto queryTime = Now<SteadyMilliseconds>() + std::chrono::milliseconds(100);
std::vector<VoteItemUpdate> updates;
// Only the first node answers, so it's the only one that gets polled again
BOOST_CHECK(registerVotes(nodeid, {queryRound, 100, {Vote(0, blockHash)}},
updates));
for (int i = 0; i < 10000; i++) {
// We make sure that we do not get a request before queryTime.
UninterruptibleSleep(std::chrono::milliseconds(1));
if (getRound() != responseRound) {
BOOST_CHECK(Now<SteadyMilliseconds>() >= queryTime);
break;
}
}
// But we eventually get one.
BOOST_CHECK(getRound() > responseRound);
// Stop event loop.
BOOST_CHECK(m_processor->stopEventLoop());
// We don't have any task scheduled anymore.
BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 0);
// Can't stop the event loop twice.
BOOST_CHECK(!m_processor->stopEventLoop());
// Wait for the scheduler to stop.
s.StopWhenDrained();
schedulerThread.join();
}
BOOST_AUTO_TEST_CASE(destructor) {
CScheduler s;
std::chrono::steady_clock::time_point start, stop;
std::thread schedulerThread;
BOOST_CHECK(m_processor->startEventLoop(s));
BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 1);
// Start the service thread after the queue size check to prevent a race
// condition where the thread may be processing the event loop task during
// the check.
schedulerThread = std::thread(std::bind(&CScheduler::serviceQueue, &s));
// Destroy the processor.
m_processor.reset();
// Now that avalanche is destroyed, there is no more scheduled tasks.
BOOST_CHECK_EQUAL(s.getQueueInfo(start, stop), 0);
// Wait for the scheduler to stop.
s.StopWhenDrained();
schedulerThread.join();
}
BOOST_AUTO_TEST_CASE(add_proof_to_reconcile) {
uint32_t score = MIN_VALID_PROOF_SCORE;
Chainstate &active_chainstate = Assert(m_node.chainman)->ActiveChainstate();
auto addProofToReconcile = [&](uint32_t proofScore) {
auto proof = buildRandomProof(active_chainstate, proofScore);
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof));
});
BOOST_CHECK(m_processor->addToReconcile(proof));
return proof;
};
for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL; i++) {
auto proof = addProofToReconcile(++score);
auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
BOOST_CHECK_EQUAL(invs.size(), i + 1);
BOOST_CHECK(invs.front().IsMsgProof());
BOOST_CHECK_EQUAL(invs.front().hash, proof->getId());
}
// From here a new proof is only polled if its score is in the top
// AVALANCHE_MAX_ELEMENT_POLL
ProofId lastProofId;
for (size_t i = 0; i < 10; i++) {
auto proof = addProofToReconcile(++score);
auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
BOOST_CHECK_EQUAL(invs.size(), AVALANCHE_MAX_ELEMENT_POLL);
BOOST_CHECK(invs.front().IsMsgProof());
BOOST_CHECK_EQUAL(invs.front().hash, proof->getId());
lastProofId = proof->getId();
}
for (size_t i = 0; i < 10; i++) {
auto proof = addProofToReconcile(--score);
auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
BOOST_CHECK_EQUAL(invs.size(), AVALANCHE_MAX_ELEMENT_POLL);
BOOST_CHECK(invs.front().IsMsgProof());
BOOST_CHECK_EQUAL(invs.front().hash, lastProofId);
}
{
// The score is not high enough to get polled
auto proof = addProofToReconcile(--score);
auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
for (auto &inv : invs) {
BOOST_CHECK_NE(inv.hash, proof->getId());
}
}
}
BOOST_AUTO_TEST_CASE(proof_record) {
setArg("-avaproofstakeutxoconfirmations", "2");
setArg("-avalancheconflictingproofcooldown", "0");
BOOST_CHECK(!m_processor->isAccepted(nullptr));
BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1);
const CKey key = CKey::MakeCompressedKey();
const COutPoint conflictingOutpoint{TxId(GetRandHash()), 0};
const COutPoint immatureOutpoint{TxId(GetRandHash()), 0};
{
CScript script = GetScriptForDestination(PKHash(key.GetPubKey()));
LOCK(cs_main);
CCoinsViewCache &coins =
Assert(m_node.chainman)->ActiveChainstate().CoinsTip();
coins.AddCoin(conflictingOutpoint,
Coin(CTxOut(PROOF_DUST_THRESHOLD, script), 10, false),
false);
coins.AddCoin(immatureOutpoint,
Coin(CTxOut(PROOF_DUST_THRESHOLD, script), 100, false),
false);
}
auto buildProof = [&](const COutPoint &outpoint, uint64_t sequence,
uint32_t height = 10) {
ProofBuilder pb(sequence, 0, key, UNSPENDABLE_ECREG_PAYOUT_SCRIPT);
BOOST_CHECK(
pb.addUTXO(outpoint, PROOF_DUST_THRESHOLD, height, false, key));
return pb.build();
};
auto conflictingProof = buildProof(conflictingOutpoint, 1);
auto validProof = buildProof(conflictingOutpoint, 2);
auto immatureProof = buildProof(immatureOutpoint, 3, 100);
BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
BOOST_CHECK(!m_processor->isAccepted(validProof));
BOOST_CHECK(!m_processor->isAccepted(immatureProof));
BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), -1);
BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), -1);
BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
// Reconciling proofs that don't exist will fail
BOOST_CHECK(!m_processor->addToReconcile(conflictingProof));
BOOST_CHECK(!m_processor->addToReconcile(validProof));
BOOST_CHECK(!m_processor->addToReconcile(immatureProof));
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(conflictingProof));
BOOST_CHECK(pm.registerProof(validProof));
BOOST_CHECK(!pm.registerProof(immatureProof));
BOOST_CHECK(pm.isBoundToPeer(validProof->getId()));
BOOST_CHECK(pm.isInConflictingPool(conflictingProof->getId()));
BOOST_CHECK(pm.isImmature(immatureProof->getId()));
});
BOOST_CHECK(m_processor->addToReconcile(conflictingProof));
BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
BOOST_CHECK(!m_processor->isAccepted(validProof));
BOOST_CHECK(!m_processor->isAccepted(immatureProof));
BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0);
BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), -1);
BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
BOOST_CHECK(m_processor->addToReconcile(validProof));
BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
BOOST_CHECK(m_processor->isAccepted(validProof));
BOOST_CHECK(!m_processor->isAccepted(immatureProof));
BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0);
BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), 0);
BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
BOOST_CHECK(!m_processor->addToReconcile(immatureProof));
BOOST_CHECK(!m_processor->isAccepted(conflictingProof));
BOOST_CHECK(m_processor->isAccepted(validProof));
BOOST_CHECK(!m_processor->isAccepted(immatureProof));
BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0);
BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), 0);
BOOST_CHECK_EQUAL(m_processor->getConfidence(immatureProof), -1);
}
BOOST_AUTO_TEST_CASE(quorum_detection) {
// Set min quorum parameters for our test
int minStake = 400'000'000;
setArg("-avaminquorumstake", ToString(minStake));
setArg("-avaminquorumconnectedstakeratio", "0.5");
// Create a new processor with our given quorum parameters
const auto currency = Currency::get();
uint32_t minScore = Proof::amountToScore(minStake * currency.baseunit);
Chainstate &active_chainstate = Assert(m_node.chainman)->ActiveChainstate();
const CKey key = CKey::MakeCompressedKey();
auto localProof =
buildRandomProof(active_chainstate, minScore / 4, 100, key);
setArg("-avamasterkey", EncodeSecret(key));
setArg("-avaproof", localProof->ToHex());
bilingual_str error;
ChainstateManager &chainman = *Assert(m_node.chainman);
m_processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(), chainman,
m_node.mempool.get(), *m_node.scheduler, error);
BOOST_CHECK(m_processor != nullptr);
BOOST_CHECK(m_processor->getLocalProof() != nullptr);
BOOST_CHECK_EQUAL(m_processor->getLocalProof()->getId(),
localProof->getId());
BOOST_CHECK_EQUAL(AvalancheTest::getMinQuorumScore(*m_processor), minScore);
BOOST_CHECK_EQUAL(
AvalancheTest::getMinQuorumConnectedScoreRatio(*m_processor), 0.5);
// The local proof has not been validated yet
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 0);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0);
});
BOOST_CHECK(!m_processor->isQuorumEstablished());
// Register the local proof. This is normally done when the chain tip is
// updated. The local proof should be accounted for in the min quorum
// computation but the peer manager doesn't know about that.
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(m_processor->getLocalProof()));
BOOST_CHECK(pm.isBoundToPeer(m_processor->getLocalProof()->getId()));
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore / 4);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0);
});
BOOST_CHECK(!m_processor->isQuorumEstablished());
// Add enough nodes to get a conclusive vote
for (NodeId id = 0; id < 8; id++) {
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.addNode(id, m_processor->getLocalProof()->getId());
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore / 4);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
}
// Add part of the required stake and make sure we still report no quorum
auto proof1 = buildRandomProof(active_chainstate, minScore / 2);
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof1));
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 3 * minScore / 4);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
BOOST_CHECK(!m_processor->isQuorumEstablished());
// Add the rest of the stake, but we are still lacking connected stake
const int64_t tipTime =
WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip())
->GetBlockTime();
const COutPoint utxo{TxId(GetRandHash()), 0};
const Amount amount = (int64_t(minScore / 4) * COIN) / 100;
const int height = 100;
const bool isCoinbase = false;
{
LOCK(cs_main);
CCoinsViewCache &coins = active_chainstate.CoinsTip();
coins.AddCoin(utxo,
Coin(CTxOut(amount, GetScriptForDestination(
PKHash(key.GetPubKey()))),
height, isCoinbase),
false);
}
ProofBuilder pb(1, tipTime + 1, key, UNSPENDABLE_ECREG_PAYOUT_SCRIPT);
BOOST_CHECK(pb.addUTXO(utxo, amount, height, isCoinbase, key));
auto proof2 = pb.build();
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof2));
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
BOOST_CHECK(!m_processor->isQuorumEstablished());
// Adding a node should cause the quorum to be detected and locked-in
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.addNode(8, proof2->getId());
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore);
// The peer manager knows that proof2 has a node attached ...
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 2);
});
// ... but the processor also account for the local proof, so we reached 50%
BOOST_CHECK(m_processor->isQuorumEstablished());
// Go back to not having enough connected score, but we've already latched
// the quorum as established
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.removeNode(8);
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
BOOST_CHECK(m_processor->isQuorumEstablished());
// Removing one more node drops our count below the minimum and the quorum
// is no longer ready
m_processor->withPeerManager(
[&](avalanche::PeerManager &pm) { pm.removeNode(7); });
BOOST_CHECK(!m_processor->isQuorumEstablished());
// It resumes when we have enough nodes again
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.addNode(7, m_processor->getLocalProof()->getId());
});
BOOST_CHECK(m_processor->isQuorumEstablished());
// Remove peers one at a time until the quorum is no longer established
auto spendProofUtxo = [&](ProofRef proof) {
{
LOCK(cs_main);
CCoinsViewCache &coins = chainman.ActiveChainstate().CoinsTip();
coins.SpendCoin(proof->getStakes()[0].getStake().getUTXO());
}
m_processor->withPeerManager([&proof](avalanche::PeerManager &pm) {
pm.updatedBlockTip();
BOOST_CHECK(!pm.isBoundToPeer(proof->getId()));
});
};
// Expire proof2, the quorum is still latched
for (int64_t i = 0; i < 6; i++) {
SetMockTime(proof2->getExpirationTime() + i);
CreateAndProcessBlock({}, CScript());
}
BOOST_CHECK_EQUAL(
WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip())
->GetMedianTimePast(),
proof2->getExpirationTime());
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.updatedBlockTip();
BOOST_CHECK(!pm.exists(proof2->getId()));
});
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 3 * minScore / 4);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
BOOST_CHECK(m_processor->isQuorumEstablished());
spendProofUtxo(proof1);
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore / 4);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4);
});
BOOST_CHECK(m_processor->isQuorumEstablished());
spendProofUtxo(m_processor->getLocalProof());
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 0);
BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0);
});
// There is no node left
BOOST_CHECK(!m_processor->isQuorumEstablished());
}
BOOST_AUTO_TEST_CASE(quorum_detection_parameter_validation) {
// Create vector of tuples of:
// <min stake, min ratio, min avaproofs messages, success bool>
const std::vector<std::tuple<std::string, std::string, std::string, bool>>
testCases = {
// All parameters are invalid
{"", "", "", false},
{"-1", "-1", "-1", false},
// Min stake is out of range
{"-1", "0", "0", false},
{"-0.01", "0", "0", false},
{"21000000000000.01", "0", "0", false},
// Min connected ratio is out of range
{"0", "-1", "0", false},
{"0", "1.1", "0", false},
// Min avaproofs messages ratio is out of range
{"0", "0", "-1", false},
// All parameters are valid
{"0", "0", "0", true},
{"0.00", "0", "0", true},
{"0.01", "0", "0", true},
{"1", "0.1", "0", true},
{"10", "0.5", "0", true},
{"10", "1", "0", true},
{"21000000000000.00", "0", "0", true},
{"0", "0", "1", true},
{"0", "0", "100", true},
};
// For each case set the parameters and check that making the processor
// succeeds or fails as expected
for (const auto &[stake, stakeRatio, numProofsMessages, success] :
testCases) {
setArg("-avaminquorumstake", stake);
setArg("-avaminquorumconnectedstakeratio", stakeRatio);
setArg("-avaminavaproofsnodecount", numProofsMessages);
bilingual_str error;
std::unique_ptr<Processor> processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(),
*Assert(m_node.chainman), m_node.mempool.get(), *m_node.scheduler,
error);
if (success) {
BOOST_CHECK(processor != nullptr);
BOOST_CHECK(error.empty());
BOOST_CHECK_EQUAL(error.original, "");
} else {
BOOST_CHECK(processor == nullptr);
BOOST_CHECK(!error.empty());
BOOST_CHECK(error.original != "");
}
}
}
BOOST_AUTO_TEST_CASE(min_avaproofs_messages) {
ChainstateManager &chainman = *Assert(m_node.chainman);
auto checkMinAvaproofsMessages = [&](int64_t minAvaproofsMessages) {
setArg("-avaminavaproofsnodecount", ToString(minAvaproofsMessages));
bilingual_str error;
auto processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(), chainman,
m_node.mempool.get(), *m_node.scheduler, error);
auto addNode = [&](NodeId nodeid) {
auto proof = buildRandomProof(chainman.ActiveChainstate(),
MIN_VALID_PROOF_SCORE);
processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof));
BOOST_CHECK(pm.addNode(nodeid, proof->getId()));
});
};
// Add enough node to have a conclusive vote, but don't account any
// avaproofs.
// NOTE: we can't use the test facilites like ConnectNodes() because we
// are not testing on m_processor.
for (NodeId id = 100; id < 108; id++) {
addNode(id);
}
BOOST_CHECK_EQUAL(processor->isQuorumEstablished(),
minAvaproofsMessages <= 0);
for (int64_t i = 0; i < minAvaproofsMessages - 1; i++) {
addNode(i);
processor->avaproofsSent(i);
BOOST_CHECK_EQUAL(processor->getAvaproofsNodeCounter(), i + 1);
// Receiving again on the same node does not increase the counter
processor->avaproofsSent(i);
BOOST_CHECK_EQUAL(processor->getAvaproofsNodeCounter(), i + 1);
BOOST_CHECK(!processor->isQuorumEstablished());
}
addNode(minAvaproofsMessages);
processor->avaproofsSent(minAvaproofsMessages);
BOOST_CHECK(processor->isQuorumEstablished());
// Check the latch
AvalancheTest::clearavaproofsNodeCounter(*processor);
BOOST_CHECK(processor->isQuorumEstablished());
};
checkMinAvaproofsMessages(0);
checkMinAvaproofsMessages(1);
checkMinAvaproofsMessages(10);
checkMinAvaproofsMessages(100);
}
BOOST_AUTO_TEST_CASE_TEMPLATE(voting_parameters, P, VoteItemProviders) {
// Check that setting voting parameters has the expected effect
setArg("-avastalevotethreshold",
ToString(AVALANCHE_VOTE_STALE_MIN_THRESHOLD));
setArg("-avastalevotefactor", "2");
const std::vector<std::tuple<int, int>> testCases = {
// {number of yes votes, number of neutral votes}
{0, AVALANCHE_VOTE_STALE_MIN_THRESHOLD},
{AVALANCHE_FINALIZATION_SCORE + 4, AVALANCHE_FINALIZATION_SCORE - 6},
};
bilingual_str error;
m_processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(),
*Assert(m_node.chainman), m_node.mempool.get(), *m_node.scheduler,
error);
BOOST_CHECK(m_processor != nullptr);
BOOST_CHECK(error.empty());
P provider(this);
const uint32_t invType = provider.invType;
const auto item = provider.buildVoteItem();
const auto itemid = provider.getVoteItemId(item);
// Create nodes that supports avalanche.
auto avanodes = ConnectNodes();
int nextNodeIndex = 0;
std::vector<avalanche::VoteItemUpdate> updates;
for (const auto &[numYesVotes, numNeutralVotes] : testCases) {
// Add a new item. Check it is added to the polls.
BOOST_CHECK(addToReconcile(item));
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
BOOST_CHECK(m_processor->isAccepted(item));
auto registerNewVote = [&](const Response &resp) {
runEventLoop();
auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId();
BOOST_CHECK(registerVotes(nodeid, resp, updates));
};
// Add some confidence
for (int i = 0; i < numYesVotes; i++) {
Response resp = {getRound(), 0, {Vote(0, itemid)}};
registerNewVote(next(resp));
BOOST_CHECK(m_processor->isAccepted(item));
BOOST_CHECK_EQUAL(m_processor->getConfidence(item),
i >= 6 ? i - 5 : 0);
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// Vote until just before item goes stale
for (int i = 0; i < numNeutralVotes; i++) {
Response resp = {getRound(), 0, {Vote(-1, itemid)}};
registerNewVote(next(resp));
BOOST_CHECK_EQUAL(updates.size(), 0);
}
// As long as it is not stale, we poll.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, invType);
BOOST_CHECK(invs[0].hash == itemid);
// Now stale
Response resp = {getRound(), 0, {Vote(-1, itemid)}};
registerNewVote(next(resp));
BOOST_CHECK_EQUAL(updates.size(), 1);
BOOST_CHECK(provider.fromAnyVoteItem(updates[0].getVoteItem()) == item);
BOOST_CHECK(updates[0].getStatus() == VoteStatus::Stale);
- updates.clear();
// Once stale, there is no poll for it.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 0);
}
}
BOOST_AUTO_TEST_CASE(block_vote_finalization_tip) {
BlockProvider provider(this);
BOOST_CHECK(!m_processor->hasFinalizedTip());
std::vector<CBlockIndex *> blockIndexes;
for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL; i++) {
CBlockIndex *pindex = provider.buildVoteItem();
BOOST_CHECK(addToReconcile(pindex));
blockIndexes.push_back(pindex);
}
auto invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), AVALANCHE_MAX_ELEMENT_POLL);
for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL; i++) {
BOOST_CHECK_EQUAL(
invs[i].hash,
blockIndexes[AVALANCHE_MAX_ELEMENT_POLL - i - 1]->GetBlockHash());
}
// Build a vote vector with the 11th block only being accepted and others
// unknown.
const BlockHash eleventhBlockHash =
blockIndexes[AVALANCHE_MAX_ELEMENT_POLL - 10 - 1]->GetBlockHash();
std::vector<Vote> votes;
votes.reserve(AVALANCHE_MAX_ELEMENT_POLL);
for (size_t i = AVALANCHE_MAX_ELEMENT_POLL; i > 0; i--) {
BlockHash blockhash = blockIndexes[i - 1]->GetBlockHash();
votes.emplace_back(blockhash == eleventhBlockHash ? 0 : -1, blockhash);
}
auto avanodes = ConnectNodes();
int nextNodeIndex = 0;
std::vector<avalanche::VoteItemUpdate> updates;
auto registerNewVote = [&]() {
Response resp = {getRound(), 0, votes};
runEventLoop();
auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId();
BOOST_CHECK(registerVotes(nodeid, resp, updates));
};
BOOST_CHECK(!m_processor->hasFinalizedTip());
// Vote for the blocks until the one being accepted finalizes
bool eleventhBlockFinalized = false;
for (size_t i = 0; i < 10000 && !eleventhBlockFinalized; i++) {
registerNewVote();
for (auto &update : updates) {
if (update.getStatus() == VoteStatus::Finalized &&
provider.fromAnyVoteItem(update.getVoteItem())
->GetBlockHash() == eleventhBlockHash) {
eleventhBlockFinalized = true;
BOOST_CHECK(m_processor->hasFinalizedTip());
} else {
BOOST_CHECK(!m_processor->hasFinalizedTip());
}
}
}
BOOST_CHECK(eleventhBlockFinalized);
BOOST_CHECK(m_processor->hasFinalizedTip());
// From now only the 10 blocks with more work are polled for
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 10);
for (size_t i = 0; i < 10; i++) {
BOOST_CHECK_EQUAL(
invs[i].hash,
blockIndexes[AVALANCHE_MAX_ELEMENT_POLL - i - 1]->GetBlockHash());
}
// Adding ancestor blocks to reconcile will fail
for (size_t i = 0; i < AVALANCHE_MAX_ELEMENT_POLL - 10 - 1; i++) {
BOOST_CHECK(!addToReconcile(blockIndexes[i]));
}
// Create a couple concurrent chain tips
CBlockIndex *tip = provider.buildVoteItem();
auto &activeChainstate = m_node.chainman->ActiveChainstate();
BlockValidationState state;
activeChainstate.InvalidateBlock(state, tip);
// Use another script to make sure we don't generate the same block again
CBlock altblock = CreateAndProcessBlock({}, CScript() << OP_TRUE);
auto alttip = WITH_LOCK(
cs_main, return Assert(m_node.chainman)
->m_blockman.LookupBlockIndex(altblock.GetHash()));
BOOST_CHECK(alttip);
BOOST_CHECK(alttip->pprev == tip->pprev);
BOOST_CHECK(alttip->GetBlockHash() != tip->GetBlockHash());
// Reconsider the previous tip valid, so we have concurrent tip candidates
{
LOCK(cs_main);
activeChainstate.ResetBlockFailureFlags(tip);
}
activeChainstate.ActivateBestChain(state);
BOOST_CHECK(addToReconcile(tip));
BOOST_CHECK(addToReconcile(alttip));
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 12);
// Vote for the tip until it finalizes
BlockHash tiphash = tip->GetBlockHash();
votes.clear();
votes.reserve(12);
for (auto &inv : invs) {
votes.emplace_back(inv.hash == tiphash ? 0 : -1, inv.hash);
}
bool tipFinalized = false;
for (size_t i = 0; i < 10000 && !tipFinalized; i++) {
registerNewVote();
for (auto &update : updates) {
if (update.getStatus() == VoteStatus::Finalized &&
provider.fromAnyVoteItem(update.getVoteItem())
->GetBlockHash() == tiphash) {
tipFinalized = true;
}
}
}
BOOST_CHECK(tipFinalized);
// Now the tip and all its ancestors will be removed from polls. Only the
// alttip remains because it is on a forked chain so we want to keep polling
// for that one until it's invalidated or stalled.
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].hash, alttip->GetBlockHash());
// Cannot reconcile a finalized block
BOOST_CHECK(!addToReconcile(tip));
// Vote for alttip until it invalidates
BlockHash alttiphash = alttip->GetBlockHash();
votes = {{1, alttiphash}};
bool alttipInvalidated = false;
for (size_t i = 0; i < 10000 && !alttipInvalidated; i++) {
registerNewVote();
for (auto &update : updates) {
if (update.getStatus() == VoteStatus::Invalid &&
provider.fromAnyVoteItem(update.getVoteItem())
->GetBlockHash() == alttiphash) {
alttipInvalidated = true;
}
}
}
BOOST_CHECK(alttipInvalidated);
invs = getInvsForNextPoll();
BOOST_CHECK_EQUAL(invs.size(), 0);
// Cannot reconcile an invalidated block
BOOST_CHECK(!addToReconcile(alttip));
}
BOOST_AUTO_TEST_CASE(vote_map_comparator) {
ChainstateManager &chainman = *Assert(m_node.chainman);
Chainstate &activeChainState = chainman.ActiveChainstate();
const int numberElementsEachType = 100;
FastRandomContext rng;
std::vector<ProofRef> proofs;
for (size_t i = 1; i <= numberElementsEachType; i++) {
auto proof =
buildRandomProof(activeChainState, i * MIN_VALID_PROOF_SCORE);
BOOST_CHECK(proof != nullptr);
proofs.emplace_back(std::move(proof));
}
Shuffle(proofs.begin(), proofs.end(), rng);
std::vector<CBlockIndex> indexes;
for (size_t i = 1; i <= numberElementsEachType; i++) {
CBlockIndex index;
index.nChainWork = i;
indexes.emplace_back(std::move(index));
}
Shuffle(indexes.begin(), indexes.end(), rng);
auto allItems = std::make_tuple(std::move(proofs), std::move(indexes));
static const size_t numTypes = std::tuple_size<decltype(allItems)>::value;
RWCollection<VoteMap> voteMap;
{
auto writeView = voteMap.getWriteView();
for (size_t i = 0; i < numberElementsEachType; i++) {
// Randomize the insert order at each loop increment
const size_t firstType = rng.randrange(numTypes);
for (size_t j = 0; j < numTypes; j++) {
switch ((firstType + j) % numTypes) {
// ProofRef
case 0:
writeView->insert(std::make_pair(
std::get<0>(allItems)[i], VoteRecord(true)));
break;
// CBlockIndex *
case 1:
writeView->insert(std::make_pair(
&std::get<1>(allItems)[i], VoteRecord(true)));
break;
default:
break;
}
}
}
}
{
// Check ordering
auto readView = voteMap.getReadView();
auto it = readView.begin();
// The first batch of items is the proofs ordered by score (descending)
uint32_t lastScore = std::numeric_limits<uint32_t>::max();
for (size_t i = 0; i < numberElementsEachType; i++) {
BOOST_CHECK(std::holds_alternative<const ProofRef>(it->first));
uint32_t currentScore =
std::get<const ProofRef>(it->first)->getScore();
BOOST_CHECK_LT(currentScore, lastScore);
lastScore = currentScore;
it++;
}
// The next batch of items is the block indexes ordered by work
// (descending)
arith_uint256 lastWork = ~arith_uint256(0);
for (size_t i = 0; i < numberElementsEachType; i++) {
BOOST_CHECK(std::holds_alternative<const CBlockIndex *>(it->first));
arith_uint256 currentWork =
std::get<const CBlockIndex *>(it->first)->nChainWork;
BOOST_CHECK(currentWork < lastWork);
lastWork = currentWork;
it++;
}
BOOST_CHECK(it == readView.end());
}
}
BOOST_AUTO_TEST_CASE(block_reconcile_initial_vote) {
auto &chainman = Assert(m_node.chainman);
Chainstate &chainstate = chainman->ActiveChainstate();
const auto block = std::make_shared<const CBlock>(
this->CreateBlock({}, CScript(), chainstate));
const BlockHash blockhash = block->GetHash();
BlockValidationState state;
CBlockIndex *blockindex;
{
LOCK(cs_main);
BOOST_CHECK(chainman->AcceptBlock(block, state,
/*fRequested=*/true, /*dbp=*/nullptr,
/*fNewBlock=*/nullptr,
/*min_pow_checked=*/true));
blockindex = chainman->m_blockman.LookupBlockIndex(blockhash);
BOOST_CHECK(blockindex);
}
// The block is not connected yet, and not added to the poll list yet
BOOST_CHECK(AvalancheTest::getInvsForNextPoll(*m_processor).empty());
BOOST_CHECK(!m_processor->isAccepted(blockindex));
// Call ActivateBestChain to connect the new block
BOOST_CHECK(chainstate.ActivateBestChain(state, block, m_processor.get()));
// It is a valid block so the tip is updated
BOOST_CHECK_EQUAL(chainstate.m_chain.Tip(), blockindex);
// Check the block is added to the poll
auto invs = AvalancheTest::getInvsForNextPoll(*m_processor);
BOOST_CHECK_EQUAL(invs.size(), 1);
BOOST_CHECK_EQUAL(invs[0].type, MSG_BLOCK);
BOOST_CHECK_EQUAL(invs[0].hash, blockhash);
// This block is our new tip so we should vote "yes"
BOOST_CHECK(m_processor->isAccepted(blockindex));
// Prevent a data race between UpdatedBlockTip and the Processor destructor
SyncWithValidationInterfaceQueue();
}
BOOST_AUTO_TEST_CASE(compute_staking_rewards) {
auto now = GetTime<std::chrono::seconds>();
SetMockTime(now);
// Pick in the middle
BlockHash prevBlockHash{uint256::ZERO};
std::vector<CScript> winners;
BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
// Null index
BOOST_CHECK(!m_processor->computeStakingReward(nullptr));
BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
CBlockIndex prevBlock;
prevBlock.phashBlock = &prevBlockHash;
prevBlock.nHeight = 100;
prevBlock.nTime = now.count();
// No quorum
BOOST_CHECK(!m_processor->computeStakingReward(&prevBlock));
BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
// Setup a bunch of proofs
size_t numProofs = 10;
std::vector<ProofRef> proofs;
proofs.reserve(numProofs);
for (size_t i = 0; i < numProofs; i++) {
const CKey key = CKey::MakeCompressedKey();
CScript payoutScript = GetScriptForRawPubKey(key.GetPubKey());
auto proof = GetProof(payoutScript);
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof));
BOOST_CHECK(pm.addNode(i, proof->getId()));
// Finalize the proof
BOOST_CHECK(pm.forPeer(proof->getId(), [&](const Peer peer) {
return pm.setFinalized(peer.peerid);
}));
});
proofs.emplace_back(std::move(proof));
}
BOOST_CHECK(m_processor->isQuorumEstablished());
// Proofs are too recent so we still have no winner
BOOST_CHECK(!m_processor->computeStakingReward(&prevBlock));
BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
// Make sure we picked a payout script from one of our proofs
auto winnerExists = [&](const CScript &expectedWinner) {
const std::string winnerString = FormatScript(expectedWinner);
for (const ProofRef &proof : proofs) {
if (winnerString == FormatScript(proof->getPayoutScript())) {
return true;
}
}
return false;
};
// Elapse some time
now += 1h + 1s;
SetMockTime(now);
prevBlock.nTime = now.count();
// Now we successfully inserted a winner in our map
BOOST_CHECK(m_processor->computeStakingReward(&prevBlock));
BOOST_CHECK(m_processor->getStakingRewardWinners(prevBlockHash, winners));
BOOST_CHECK(winnerExists(winners[0]));
// Subsequent calls are a no-op
BOOST_CHECK(m_processor->computeStakingReward(&prevBlock));
BOOST_CHECK(m_processor->getStakingRewardWinners(prevBlockHash, winners));
BOOST_CHECK(winnerExists(winners[0]));
CBlockIndex prevBlockHigh = prevBlock;
BlockHash prevBlockHashHigh =
BlockHash(ArithToUint256({std::numeric_limits<uint64_t>::max()}));
prevBlockHigh.phashBlock = &prevBlockHashHigh;
prevBlockHigh.nHeight = 101;
BOOST_CHECK(m_processor->computeStakingReward(&prevBlockHigh));
BOOST_CHECK(
m_processor->getStakingRewardWinners(prevBlockHashHigh, winners));
BOOST_CHECK(winnerExists(winners[0]));
// No impact on previous winner so far
BOOST_CHECK(m_processor->getStakingRewardWinners(prevBlockHash, winners));
BOOST_CHECK(winnerExists(winners[0]));
// Cleanup to height 101
m_processor->cleanupStakingRewards(101);
// Now the previous winner has been cleared
BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
// But the last one remain
BOOST_CHECK(
m_processor->getStakingRewardWinners(prevBlockHashHigh, winners));
BOOST_CHECK(winnerExists(winners[0]));
// We can add it again
BOOST_CHECK(m_processor->computeStakingReward(&prevBlock));
BOOST_CHECK(m_processor->getStakingRewardWinners(prevBlockHash, winners));
BOOST_CHECK(winnerExists(winners[0]));
// Cleanup to higher height
m_processor->cleanupStakingRewards(200);
// No winner anymore
BOOST_CHECK(!m_processor->getStakingRewardWinners(prevBlockHash, winners));
BOOST_CHECK(
!m_processor->getStakingRewardWinners(prevBlockHashHigh, winners));
}
BOOST_AUTO_TEST_CASE(local_proof_status) {
const CKey key = CKey::MakeCompressedKey();
const COutPoint outpoint{TxId(GetRandHash()), 0};
{
CScript script = GetScriptForDestination(PKHash(key.GetPubKey()));
LOCK(cs_main);
CCoinsViewCache &coins =
Assert(m_node.chainman)->ActiveChainstate().CoinsTip();
coins.AddCoin(outpoint,
Coin(CTxOut(PROOF_DUST_THRESHOLD, script), 100, false),
false);
}
auto buildProof = [&](const COutPoint &outpoint, uint64_t sequence,
uint32_t height) {
ProofBuilder pb(sequence, 0, key, UNSPENDABLE_ECREG_PAYOUT_SCRIPT);
BOOST_CHECK(
pb.addUTXO(outpoint, PROOF_DUST_THRESHOLD, height, false, key));
return pb.build();
};
auto localProof = buildProof(outpoint, 1, 100);
setArg("-avamasterkey", EncodeSecret(key));
setArg("-avaproof", localProof->ToHex());
setArg("-avalancheconflictingproofcooldown", "0");
setArg("-avalanchepeerreplacementcooldown", "0");
setArg("-avaproofstakeutxoconfirmations", "3");
bilingual_str error;
ChainstateManager &chainman = *Assert(m_node.chainman);
m_processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(), chainman,
m_node.mempool.get(), *m_node.scheduler, error);
BOOST_CHECK_EQUAL(m_processor->getLocalProof()->getId(),
localProof->getId());
auto checkLocalProofState =
[&](const bool boundToPeer,
const ProofRegistrationResult expectedResult) {
BOOST_CHECK_EQUAL(
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
return pm.isBoundToPeer(localProof->getId());
}),
boundToPeer);
BOOST_CHECK_MESSAGE(
m_processor->getLocalProofRegistrationState().GetResult() ==
expectedResult,
m_processor->getLocalProofRegistrationState().ToString());
};
checkLocalProofState(false, ProofRegistrationResult::NONE);
// Not ready to share, the local proof isn't registered
BOOST_CHECK(!m_processor->canShareLocalProof());
AvalancheTest::updatedBlockTip(*m_processor);
checkLocalProofState(false, ProofRegistrationResult::NONE);
// Ready to share, but the proof is immature
AvalancheTest::setLocalProofShareable(*m_processor, true);
BOOST_CHECK(m_processor->canShareLocalProof());
AvalancheTest::updatedBlockTip(*m_processor);
checkLocalProofState(false, ProofRegistrationResult::IMMATURE);
// Mine a block to re-evaluate the proof, it remains immature
mineBlocks(1);
AvalancheTest::updatedBlockTip(*m_processor);
checkLocalProofState(false, ProofRegistrationResult::IMMATURE);
// One more block and the proof turns mature
mineBlocks(1);
AvalancheTest::updatedBlockTip(*m_processor);
checkLocalProofState(true, ProofRegistrationResult::NONE);
// Build a conflicting proof and check the status is updated accordingly
auto conflictingProof = buildProof(outpoint, 2, 100);
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(conflictingProof));
BOOST_CHECK(pm.isBoundToPeer(conflictingProof->getId()));
BOOST_CHECK(pm.isInConflictingPool(localProof->getId()));
});
AvalancheTest::updatedBlockTip(*m_processor);
checkLocalProofState(false, ProofRegistrationResult::CONFLICTING);
}
BOOST_AUTO_TEST_CASE(reconcileOrFinalize) {
setArg("-avalancheconflictingproofcooldown", "0");
setArg("-avalanchepeerreplacementcooldown", "0");
// Proof is null
BOOST_CHECK(!m_processor->reconcileOrFinalize(ProofRef()));
ChainstateManager &chainman = *Assert(m_node.chainman);
Chainstate &activeChainState = chainman.ActiveChainstate();
const CKey key = CKey::MakeCompressedKey();
const COutPoint outpoint{TxId(GetRandHash()), 0};
{
CScript script = GetScriptForDestination(PKHash(key.GetPubKey()));
LOCK(cs_main);
CCoinsViewCache &coins = activeChainState.CoinsTip();
coins.AddCoin(outpoint,
Coin(CTxOut(PROOF_DUST_THRESHOLD, script), 100, false),
false);
}
auto buildProof = [&](const COutPoint &outpoint, uint64_t sequence) {
ProofBuilder pb(sequence, 0, key, UNSPENDABLE_ECREG_PAYOUT_SCRIPT);
BOOST_CHECK(
pb.addUTXO(outpoint, PROOF_DUST_THRESHOLD, 100, false, key));
return pb.build();
};
auto proof = buildProof(outpoint, 1);
BOOST_CHECK(proof);
// Not a peer nor conflicting
BOOST_CHECK(!m_processor->reconcileOrFinalize(proof));
// Register the proof
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(proof));
BOOST_CHECK(pm.isBoundToPeer(proof->getId()));
BOOST_CHECK(!pm.isInConflictingPool(proof->getId()));
});
// Reconcile works
BOOST_CHECK(m_processor->reconcileOrFinalize(proof));
// Repeated calls fail and do nothing
BOOST_CHECK(!m_processor->reconcileOrFinalize(proof));
// Finalize
AvalancheTest::addProofToRecentfinalized(*m_processor, proof->getId());
BOOST_CHECK(m_processor->isRecentlyFinalized(proof->getId()));
BOOST_CHECK(m_processor->reconcileOrFinalize(proof));
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
// The peer is marked as final
BOOST_CHECK(pm.forPeer(proof->getId(), [&](const Peer &peer) {
return peer.hasFinalized;
}));
BOOST_CHECK(pm.isBoundToPeer(proof->getId()));
BOOST_CHECK(!pm.isInConflictingPool(proof->getId()));
});
// Same proof with a higher sequence number
auto betterProof = buildProof(outpoint, 2);
BOOST_CHECK(betterProof);
// Not registered nor conflicting yet
BOOST_CHECK(!m_processor->reconcileOrFinalize(betterProof));
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
BOOST_CHECK(pm.registerProof(betterProof));
BOOST_CHECK(pm.isBoundToPeer(betterProof->getId()));
BOOST_CHECK(!pm.isInConflictingPool(betterProof->getId()));
BOOST_CHECK(!pm.isBoundToPeer(proof->getId()));
BOOST_CHECK(pm.isInConflictingPool(proof->getId()));
});
// Recently finalized, not worth polling
BOOST_CHECK(!m_processor->reconcileOrFinalize(proof));
// But the better proof can be polled
BOOST_CHECK(m_processor->reconcileOrFinalize(betterProof));
}
BOOST_AUTO_TEST_CASE(stake_contenders) {
setArg("-avalanchestakingpreconsensus", "1");
bilingual_str error;
m_processor = Processor::MakeProcessor(
*m_node.args, *m_node.chain, m_node.connman.get(),
*Assert(m_node.chainman), m_node.mempool.get(), *m_node.scheduler,
error);
BOOST_CHECK(m_processor);
auto now = GetTime<std::chrono::seconds>();
SetMockTime(now);
ChainstateManager &chainman = *Assert(m_node.chainman);
Chainstate &active_chainstate = chainman.ActiveChainstate();
CBlockIndex *chaintip =
WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip());
auto proof1 = buildRandomProof(active_chainstate, MIN_VALID_PROOF_SCORE);
const ProofId proofid1 = proof1->getId();
const StakeContenderId contender1_block1(chaintip->GetBlockHash(),
proofid1);
auto proof2 = buildRandomProof(active_chainstate, MIN_VALID_PROOF_SCORE);
const ProofId proofid2 = proof2->getId();
const StakeContenderId contender2_block1(chaintip->GetBlockHash(),
proofid2);
// Add stake contenders. Without computing staking rewards, the status is
// pending.
{
LOCK(cs_main);
m_processor->addStakeContender(proof1);
m_processor->addStakeContender(proof2);
}
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block1),
-2);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block1),
-2);
// Sanity check unknown contender
const StakeContenderId unknownContender(chaintip->GetBlockHash(),
ProofId(GetRandHash()));
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(unknownContender),
-1);
// Register proof2 and save it as a remote proof so that it will be promoted
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
ConnectNode(NODE_AVALANCHE);
pm.registerProof(proof2);
for (NodeId n = 0; n < 8; n++) {
pm.addNode(n, proofid2);
}
pm.saveRemoteProof(proofid2, 0, true);
BOOST_CHECK(pm.forPeer(proofid2, [&](const Peer peer) {
return pm.setFinalized(peer.peerid);
}));
});
// Need to have finalization tip set for contenders to be promoted
AvalancheTest::setFinalizationTip(*m_processor, chaintip);
// Make proofs old enough to be considered for staking rewards
now += 1h + 1s;
SetMockTime(now);
// Advance chaintip
CBlock block = CreateAndProcessBlock({}, CScript());
chaintip =
WITH_LOCK(cs_main, return Assert(m_node.chainman)
->m_blockman.LookupBlockIndex(block.GetHash()));
AvalancheTest::updatedBlockTip(*m_processor);
// Compute local stake winner
BOOST_CHECK(m_processor->isQuorumEstablished());
BOOST_CHECK(m_processor->computeStakingReward(chaintip));
{
std::vector<CScript> winners;
BOOST_CHECK(m_processor->getStakingRewardWinners(
chaintip->GetBlockHash(), winners));
BOOST_CHECK_EQUAL(winners.size(), 1);
BOOST_CHECK(winners[0] == proof2->getPayoutScript());
}
// Sanity check unknown contender
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(unknownContender),
-1);
// Old contender cache entries unaffected
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block1),
-2);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block1),
-2);
// contender1 was not promoted
const StakeContenderId contender1_block2 =
StakeContenderId(chaintip->GetBlockHash(), proofid1);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block2),
-1);
// contender2 was promoted
const StakeContenderId contender2_block2 =
StakeContenderId(chaintip->GetBlockHash(), proofid2);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block2),
0);
// Advance the finalization tip
AvalancheTest::setFinalizationTip(*m_processor, chaintip);
// Now that the finalization point has passed the block where contender1 was
// added, cleaning up the cache will remove its entry. contender2 will have
// its old entry cleaned up, but the promoted one remains.
m_processor->cleanupStakingRewards(chaintip->nHeight);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(unknownContender),
-1);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block1),
-1);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block2),
-1);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block1),
-1);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block2),
0);
// Manually set contenders as winners
m_processor->setStakingRewardWinners(
chaintip, {proof1->getPayoutScript(), proof2->getPayoutScript()});
// contender1 has been forgotten, which is expected. When a proof becomes
// invalid and is cleaned up from the cache, we do not expect peers to poll
// for it any more.
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block2),
-1);
// contender2 is a winner despite avalanche not finalizing it
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block2),
0);
// Reject proof2, mine a new chain tip, finalize it, and cleanup the cache
m_processor->withPeerManager(
[&](avalanche::PeerManager &pm) { pm.rejectProof(proofid2); });
block = CreateAndProcessBlock({}, CScript());
chaintip =
WITH_LOCK(cs_main, return Assert(m_node.chainman)
->m_blockman.LookupBlockIndex(block.GetHash()));
AvalancheTest::updatedBlockTip(*m_processor);
AvalancheTest::setFinalizationTip(*m_processor, chaintip);
m_processor->cleanupStakingRewards(chaintip->nHeight);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(unknownContender),
-1);
// Old entries were cleaned up
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block2),
-1);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block2),
-1);
// Neither contender was promoted and contender2 was cleaned up even though
// it was once a manual winner.
const StakeContenderId contender1_block3 =
StakeContenderId(chaintip->GetBlockHash(), proofid1);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender1_block3),
-1);
const StakeContenderId contender2_block3 =
StakeContenderId(chaintip->GetBlockHash(), proofid2);
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block3),
-1);
// Generate a bunch of flaky proofs
size_t numProofs = 8;
std::vector<ProofRef> proofs;
proofs.reserve(numProofs);
for (size_t i = 0; i < numProofs; i++) {
auto proof = buildRandomProof(active_chainstate, MIN_VALID_PROOF_SCORE);
const ProofId proofid = proof->getId();
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
pm.registerProof(proof);
// Make it a remote proof so that it will be promoted
pm.saveRemoteProof(proofid, i, true);
BOOST_CHECK(pm.forPeer(proofid, [&](const Peer peer) {
return pm.setFinalized(peer.peerid);
}));
});
// Add it as a stake contender so it will be promoted
WITH_LOCK(cs_main, m_processor->addStakeContender(proof));
proofs.emplace_back(std::move(proof));
}
// Add nodes only for the first proof so we have a quorum
m_processor->withPeerManager([&](avalanche::PeerManager &pm) {
const ProofId proofid = proofs[0]->getId();
for (NodeId n = 0; n < 8; n++) {
pm.addNode(n, proofid);
}
});
// Make proofs old enough to be considered for staking rewards
now += 1h + 1s;
SetMockTime(now);
// Try a few times in case the non-flaky proof get selected as winner
std::vector<CScript> winners;
for (int attempt = 0; attempt < 10; attempt++) {
// Advance chaintip so the proofs are older than the last block time
block = CreateAndProcessBlock({}, CScript());
chaintip = WITH_LOCK(
cs_main, return Assert(m_node.chainman)
->m_blockman.LookupBlockIndex(block.GetHash()));
AvalancheTest::updatedBlockTip(*m_processor);
// Compute local stake winner
BOOST_CHECK(m_processor->isQuorumEstablished());
BOOST_CHECK(m_processor->computeStakingReward(chaintip));
BOOST_CHECK(m_processor->getStakingRewardWinners(
chaintip->GetBlockHash(), winners));
if (winners.size() == 8) {
break;
}
}
BOOST_CHECK(winners.size() == 8);
// Verify that all winners were accepted
size_t numAccepted = 0;
for (const auto &proof : proofs) {
const ProofId proofid = proof->getId();
const StakeContenderId contender =
StakeContenderId(chaintip->GetBlockHash(), proofid);
if (m_processor->getStakeContenderStatus(contender) == 0) {
numAccepted++;
BOOST_CHECK(std::find(winners.begin(), winners.end(),
proof->getPayoutScript()) != winners.end());
}
}
BOOST_CHECK_EQUAL(winners.size(), numAccepted);
// Check that a highest ranking contender that was not selected as local
// winner is still accepted.
block = CreateAndProcessBlock({}, CScript());
chaintip =
WITH_LOCK(cs_main, return Assert(m_node.chainman)
->m_blockman.LookupBlockIndex(block.GetHash()));
auto bestproof = buildRandomProof(active_chainstate,
std::numeric_limits<uint32_t>::max());
WITH_LOCK(cs_main, m_processor->addStakeContender(bestproof));
AvalancheTest::updatedBlockTip(*m_processor);
// Compute local stake winners
BOOST_CHECK(m_processor->isQuorumEstablished());
BOOST_CHECK(m_processor->computeStakingReward(chaintip));
BOOST_CHECK(m_processor->getStakingRewardWinners(chaintip->GetBlockHash(),
winners));
// Sanity check bestproof was not selected as a winner
BOOST_CHECK(std::find(winners.begin(), winners.end(),
bestproof->getPayoutScript()) == winners.end());
// Best contender is accepted
const StakeContenderId bestcontender =
StakeContenderId(chaintip->GetBlockHash(), bestproof->getId());
BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(bestcontender), 0);
}
BOOST_AUTO_TEST_SUITE_END()
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Apr 27, 10:34 (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573278
Default Alt Text
(146 KB)
Attached To
rABC Bitcoin ABC
Event Timeline
Log In to Comment