Page MenuHomePhabricator

No OneTemporary

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

Mime Type
text/x-diff
Expires
Sun, Apr 27, 10:34 (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573278
Default Alt Text
(146 KB)

Event Timeline