diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp index 04eaf21df..fd5fc9de2 100644 --- a/src/avalanche/processor.cpp +++ b/src/avalanche/processor.cpp @@ -1,1368 +1,1396 @@ // 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) { { // 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); - return stakingRewards - .emplace(pindex->GetBlockHash(), std::move(_stakingRewards)) - .second; + rewardsInserted = + stakingRewards + .emplace(pindex->GetBlockHash(), std::move(_stakingRewards)) + .second; } - return false; + if (m_stakingPreConsensus) { + // If pindex has not been promoted in the contender cache yet, this will + // be a no-op. + setContenderStatusForLocalWinner(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); + { + 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. + setContenderStatusForLocalWinner(activeTip); // TODO reconcile remoteProofs contenders } +void Processor::setContenderStatusForLocalWinner(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; + } + + const StakeContenderId contenderId(prevblockhash, winners[0].first); + LOCK(cs_stakeContenderCache); + stakeContenderCache.finalize(contenderId); +} + 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/processor.h b/src/avalanche/processor.h index 8da35926f..490df4b3d 100644 --- a/src/avalanche/processor.h +++ b/src/avalanche/processor.h @@ -1,460 +1,467 @@ // 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. #ifndef BITCOIN_AVALANCHE_PROCESSOR_H #define BITCOIN_AVALANCHE_PROCESSOR_H #include <avalanche/config.h> #include <avalanche/node.h> #include <avalanche/proof.h> #include <avalanche/proofcomparator.h> #include <avalanche/protocol.h> #include <avalanche/stakecontendercache.h> #include <avalanche/voterecord.h> // For AVALANCHE_MAX_INFLIGHT_POLL #include <blockindex.h> #include <blockindexcomparators.h> #include <common/bloom.h> #include <eventloop.h> #include <interfaces/chain.h> #include <interfaces/handler.h> #include <key.h> #include <net.h> #include <primitives/transaction.h> #include <rwcollection.h> #include <util/variant.h> #include <validationinterface.h> #include <boost/multi_index/composite_key.hpp> #include <boost/multi_index/hashed_index.hpp> #include <boost/multi_index/member.hpp> #include <boost/multi_index/ordered_index.hpp> #include <boost/multi_index_container.hpp> #include <atomic> #include <chrono> #include <cstdint> #include <memory> #include <unordered_map> #include <variant> #include <vector> class ArgsManager; class CConnman; class CNode; class CScheduler; class Config; class PeerManager; struct bilingual_str; /** * Maximum item that can be polled at once. */ static constexpr size_t AVALANCHE_MAX_ELEMENT_POLL = 16; /** * How long before we consider that a query timed out. */ static constexpr std::chrono::milliseconds AVALANCHE_DEFAULT_QUERY_TIMEOUT{ 10000}; /** * The size of the finalized items filter. It should be large enough that an * influx of inventories cannot roll any particular item out of the filter on * demand. For example, transactions will roll blocks out of the filter. * Tracking many more items than can possibly be polled at once ensures that * recently polled items will come to a stable state on the network before * rolling out of the filter. */ static constexpr uint32_t AVALANCHE_FINALIZED_ITEMS_FILTER_NUM_ELEMENTS = AVALANCHE_MAX_INFLIGHT_POLL * 20; namespace avalanche { class Delegation; class PeerManager; class ProofRegistrationState; struct VoteRecord; enum struct VoteStatus : uint8_t { Invalid, Rejected, Accepted, Finalized, Stale, }; using AnyVoteItem = std::variant<const ProofRef, const CBlockIndex *, const CTransactionRef>; class VoteItemUpdate { AnyVoteItem item; VoteStatus status; public: VoteItemUpdate(AnyVoteItem itemIn, VoteStatus statusIn) : item(std::move(itemIn)), status(statusIn) {} const VoteStatus &getStatus() const { return status; } const AnyVoteItem &getVoteItem() const { return item; } }; struct VoteMapComparator { bool operator()(const AnyVoteItem &lhs, const AnyVoteItem &rhs) const { // If the variants are of different types, sort them by variant index if (lhs.index() != rhs.index()) { return lhs.index() < rhs.index(); } return std::visit( variant::overloaded{ [](const ProofRef &lhs, const ProofRef &rhs) { return ProofComparatorByScore()(lhs, rhs); }, [](const CBlockIndex *lhs, const CBlockIndex *rhs) { // Reverse ordering so we get the highest work first return CBlockIndexWorkComparator()(rhs, lhs); }, [](const CTransactionRef &lhs, const CTransactionRef &rhs) { return lhs->GetId() < rhs->GetId(); }, [](const auto &lhs, const auto &rhs) { // This serves 2 purposes: // - This makes sure that we don't forget to implement a // comparison case when adding a new variant type. // - This avoids having to write all the cross type cases // which are already handled by the index sort above. // Because the compiler has no way to determine that, we // cannot use static assertions here without having to // define the whole type matrix also. assert(false); // Return any bool, it's only there to make the compiler // happy. return false; }, }, lhs, rhs); } }; using VoteMap = std::map<AnyVoteItem, VoteRecord, VoteMapComparator>; struct query_timeout {}; namespace { struct AvalancheTest; } // FIXME Implement a proper notification handler for node disconnection instead // of implementing the whole NetEventsInterface for a single interesting event. class Processor final : public NetEventsInterface { Config avaconfig; CConnman *connman; ChainstateManager &chainman; CTxMemPool *mempool; /** * Items to run avalanche on. */ RWCollection<VoteMap> voteRecords; /** * Keep track of peers and queries sent. */ std::atomic<uint64_t> round; /** * Keep track of the peers and associated infos. */ mutable Mutex cs_peerManager; std::unique_ptr<PeerManager> peerManager GUARDED_BY(cs_peerManager); struct Query { NodeId nodeid; uint64_t round; SteadyMilliseconds timeout; /** * We declare this as mutable so it can be modified in the multi_index. * This is ok because we do not use this field to index in anyway. * * /!\ Do not use any mutable field as index. */ mutable std::vector<CInv> invs; }; using QuerySet = boost::multi_index_container< Query, boost::multi_index::indexed_by< // index by nodeid/round boost::multi_index::hashed_unique<boost::multi_index::composite_key< Query, boost::multi_index::member<Query, NodeId, &Query::nodeid>, boost::multi_index::member<Query, uint64_t, &Query::round>>>, // sorted by timeout boost::multi_index::ordered_non_unique< boost::multi_index::tag<query_timeout>, boost::multi_index::member<Query, SteadyMilliseconds, &Query::timeout>>>>; RWCollection<QuerySet> queries; /** Data required to participate. */ struct PeerData; std::unique_ptr<PeerData> peerData; CKey sessionKey; /** Event loop machinery. */ EventLoop eventLoop; /** * Quorum management. */ uint32_t minQuorumScore; double minQuorumConnectedScoreRatio; std::atomic<bool> quorumIsEstablished{false}; std::atomic<bool> m_canShareLocalProof{false}; int64_t minAvaproofsNodeCount; std::atomic<int64_t> avaproofsNodeCounter{0}; /** Voting parameters. */ const uint32_t staleVoteThreshold; const uint32_t staleVoteFactor; /** Registered interfaces::Chain::Notifications handler. */ class NotificationsHandler; std::unique_ptr<interfaces::Handler> chainNotificationsHandler; mutable Mutex cs_finalizationTip; const CBlockIndex *finalizationTip GUARDED_BY(cs_finalizationTip){nullptr}; mutable Mutex cs_delayedAvahelloNodeIds; /** * A list of the nodes that did not get our proof announced via avahello * yet because we had no inbound connection. */ std::unordered_set<NodeId> delayedAvahelloNodeIds GUARDED_BY(cs_delayedAvahelloNodeIds); struct StakingReward { int blockheight; // Ordered list of acceptable winners, only the first is used for mining std::vector<std::pair<ProofId, CScript>> winners; }; mutable Mutex cs_stakingRewards; std::unordered_map<BlockHash, StakingReward, SaltedUint256Hasher> stakingRewards GUARDED_BY(cs_stakingRewards); mutable Mutex cs_stakeContenderCache; StakeContenderCache stakeContenderCache GUARDED_BY(cs_stakeContenderCache); Processor(Config avaconfig, interfaces::Chain &chain, CConnman *connmanIn, ChainstateManager &chainman, 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 stakeUtxoDustThresholdIn, bool preConsensus, bool stakingPreConsensus); public: const bool m_preConsensus{false}; const bool m_stakingPreConsensus{false}; ~Processor(); static std::unique_ptr<Processor> MakeProcessor(const ArgsManager &argsman, interfaces::Chain &chain, CConnman *connman, ChainstateManager &chainman, CTxMemPool *mempoolIn, CScheduler &scheduler, bilingual_str &error); bool addToReconcile(const AnyVoteItem &item) EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizedItems); /** * Wrapper around the addToReconcile for proofs that adds back the * finalization flag to the peer if it is not polled due to being recently * finalized. */ bool reconcileOrFinalize(const ProofRef &proof) EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_finalizedItems); bool isAccepted(const AnyVoteItem &item) const; int getConfidence(const AnyVoteItem &item) const; bool isRecentlyFinalized(const uint256 &itemId) const EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizedItems); void clearFinalizedItems() EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizedItems); // TODO: Refactor the API to remove the dependency on avalanche/protocol.h void sendResponse(CNode *pfrom, Response response) const; bool registerVotes(NodeId nodeid, const Response &response, std::vector<VoteItemUpdate> &updates, int &banscore, std::string &error) EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_finalizedItems, !cs_invalidatedBlocks, !cs_finalizationTip); template <typename Callable> auto withPeerManager(Callable &&func) const EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager) { LOCK(cs_peerManager); return func(*peerManager); } CPubKey getSessionPubKey() const; /** * @brief Send a avahello message * * @param pfrom The node to send the message to * @return True if a non-null delegation has been announced */ bool sendHello(CNode *pfrom) EXCLUSIVE_LOCKS_REQUIRED(!cs_delayedAvahelloNodeIds); void sendDelayedAvahello() EXCLUSIVE_LOCKS_REQUIRED(!cs_delayedAvahelloNodeIds); ProofRef getLocalProof() const; ProofRegistrationState getLocalProofRegistrationState() const; /* * Return whether the avalanche service flag should be set. */ bool isAvalancheServiceAvailable() { return !!peerData; } /** Whether there is a finalized tip */ bool hasFinalizedTip() const EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizationTip) { LOCK(cs_finalizationTip); return finalizationTip != nullptr; } bool startEventLoop(CScheduler &scheduler); bool stopEventLoop(); void avaproofsSent(NodeId nodeid) LOCKS_EXCLUDED(cs_main) EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager); int64_t getAvaproofsNodeCounter() const { return avaproofsNodeCounter.load(); } bool isQuorumEstablished() LOCKS_EXCLUDED(cs_main) - EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_stakingRewards); + EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_stakingRewards, + !cs_stakeContenderCache); bool canShareLocalProof(); bool computeStakingReward(const CBlockIndex *pindex) - EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_stakingRewards); + EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_stakingRewards, + !cs_stakeContenderCache); bool eraseStakingRewardWinner(const BlockHash &prevBlockHash) EXCLUSIVE_LOCKS_REQUIRED(!cs_stakingRewards); void cleanupStakingRewards(const int minHeight) EXCLUSIVE_LOCKS_REQUIRED(!cs_stakingRewards, !cs_stakeContenderCache); bool getStakingRewardWinners( const BlockHash &prevBlockHash, std::vector<std::pair<ProofId, CScript>> &winners) const EXCLUSIVE_LOCKS_REQUIRED(!cs_stakingRewards); bool getStakingRewardWinners(const BlockHash &prevBlockHash, std::vector<CScript> &payouts) const EXCLUSIVE_LOCKS_REQUIRED(!cs_stakingRewards); bool setStakingRewardWinners(const CBlockIndex *pprev, const std::vector<CScript> &payouts) EXCLUSIVE_LOCKS_REQUIRED(!cs_stakingRewards, !cs_stakeContenderCache); // Implement NetEventInterface. Only FinalizeNode is of interest. void InitializeNode(const ::Config &config, CNode &pnode, ServiceFlags our_services) override {} bool ProcessMessages(const ::Config &config, CNode *pnode, std::atomic<bool> &interrupt) override { return false; } bool SendMessages(const ::Config &config, CNode *pnode) override { return false; } /** Handle removal of a node */ void FinalizeNode(const ::Config &config, const CNode &node) override LOCKS_EXCLUDED(cs_main) EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_delayedAvahelloNodeIds); /** Track votes on stake contenders */ void addStakeContender(const ProofRef &proof) EXCLUSIVE_LOCKS_REQUIRED(cs_main, !cs_stakeContenderCache); int getStakeContenderStatus(const StakeContenderId &contenderId) const EXCLUSIVE_LOCKS_REQUIRED(!cs_stakeContenderCache, !cs_stakingRewards); /** Promote stake contender cache entries to the latest chain tip */ void promoteStakeContendersToTip() - EXCLUSIVE_LOCKS_REQUIRED(!cs_stakeContenderCache, !cs_peerManager, - !cs_finalizationTip); + EXCLUSIVE_LOCKS_REQUIRED(!cs_stakeContenderCache, !cs_stakingRewards, + !cs_peerManager, !cs_finalizationTip); private: void updatedBlockTip() EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_finalizedItems, - !cs_finalizationTip, !cs_stakeContenderCache); + !cs_finalizationTip, !cs_stakeContenderCache, + !cs_stakingRewards); void transactionAddedToMempool(const CTransactionRef &tx) EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizedItems); void runEventLoop() EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_stakingRewards, - !cs_finalizedItems); + !cs_stakeContenderCache, !cs_finalizedItems); void clearTimedoutRequests() EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager); std::vector<CInv> getInvsForNextPoll(bool forPoll = true) EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager, !cs_finalizedItems); bool sendHelloInternal(CNode *pfrom) EXCLUSIVE_LOCKS_REQUIRED(cs_delayedAvahelloNodeIds); AnyVoteItem getVoteItemFromInv(const CInv &inv) const EXCLUSIVE_LOCKS_REQUIRED(!cs_peerManager); + /** Helper to set the local winner in the contender cache */ + void setContenderStatusForLocalWinner(const CBlockIndex *pindex) + EXCLUSIVE_LOCKS_REQUIRED(!cs_stakeContenderCache, !cs_stakingRewards); + /** * We don't need many blocks but a low false positive rate. * In the event of a false positive the node might skip polling this block. * Such a block will not get marked as finalized until it is reconsidered * for polling (if the filter changed its state) or another block is found. */ mutable Mutex cs_invalidatedBlocks; CRollingBloomFilter invalidatedBlocks GUARDED_BY(cs_invalidatedBlocks){ 100, 0.0000001}; /** * Rolling bloom filter to track recently finalized inventory items of any * type. Once placed in this filter, those items will not be polled again * unless they roll out. Note that this one filter tracks all types so * blocks may be rolled out by transaction activity for example. * * We want a low false positive rate to prevent accidentally not polling * for an item when it is first seen. */ mutable Mutex cs_finalizedItems; CRollingBloomFilter finalizedItems GUARDED_BY(cs_finalizedItems){ AVALANCHE_FINALIZED_ITEMS_FILTER_NUM_ELEMENTS, 0.0000001}; struct IsWorthPolling { const Processor &processor; IsWorthPolling(const Processor &_processor) : processor(_processor){}; bool operator()(const CBlockIndex *pindex) const LOCKS_EXCLUDED(cs_main); bool operator()(const ProofRef &proof) const LOCKS_EXCLUDED(cs_peerManager); bool operator()(const CTransactionRef &tx) const; }; bool isWorthPolling(const AnyVoteItem &item) const EXCLUSIVE_LOCKS_REQUIRED(!cs_finalizedItems); struct GetLocalAcceptance { const Processor &processor; GetLocalAcceptance(const Processor &_processor) : processor(_processor){}; bool operator()(const CBlockIndex *pindex) const LOCKS_EXCLUDED(cs_main); bool operator()(const ProofRef &proof) const LOCKS_EXCLUDED(cs_peerManager); bool operator()(const CTransactionRef &tx) const; }; bool getLocalAcceptance(const AnyVoteItem &item) const { return std::visit(GetLocalAcceptance(*this), item); } friend struct ::avalanche::AvalancheTest; }; } // namespace avalanche #endif // BITCOIN_AVALANCHE_PROCESSOR_H diff --git a/src/avalanche/test/processor_tests.cpp b/src/avalanche/test/processor_tests.cpp index 5819b987a..6151d6934 100644 --- a/src/avalanche/test/processor_tests.cpp +++ b/src/avalanche/test/processor_tests.cpp @@ -1,2563 +1,2560 @@ // 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(chainstate.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 - // TODO contender2 is REJECTED because the locally selected stake winner is - // not yet factored into stake contenders. This will be ACCEPTED once it - // does. const StakeContenderId contender2_block2 = StakeContenderId(chaintip->GetBlockHash(), proofid2); BOOST_CHECK_EQUAL(m_processor->getStakeContenderStatus(contender2_block2), - 1); + 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), - 1); + 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); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/functional/abc_p2p_avalanche_contender_voting.py b/test/functional/abc_p2p_avalanche_contender_voting.py index 01cc29c53..ea8c025e9 100644 --- a/test/functional/abc_p2p_avalanche_contender_voting.py +++ b/test/functional/abc_p2p_avalanche_contender_voting.py @@ -1,183 +1,185 @@ # Copyright (c) 2024 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test the resolution of stake contender preconsensus via avalanche.""" import time from test_framework.avatools import can_find_inv_in_poll, get_ava_p2p_interface from test_framework.key import ECPubKey from test_framework.messages import ( MSG_AVA_STAKE_CONTENDER, AvalancheContenderVoteError, AvalancheVote, hash256, ser_uint256, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, uint256_hex QUORUM_NODE_COUNT = 16 class AvalancheContenderVotingTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [ [ "-avalanchestakingpreconsensus=1", "-avalanchestakingrewards=1", "-avaproofstakeutxodustthreshold=1000000", "-avaproofstakeutxoconfirmations=1", "-avacooldown=0", "-avaminquorumstake=0", "-avaminavaproofsnodecount=0", "-whitelist=noban@127.0.0.1", "-persistavapeers=0", ], ] self.supports_cli = False def run_test(self): node = self.nodes[0] # Set mock time so we can control when proofs will be considered for staking rewards now = int(time.time()) node.setmocktime(now) # Build a fake quorum of nodes. def get_quorum(): def new_ava_interface(node): # This test depends on each proof being added to the contender cache before # the next block arrives, so we wait until that happens. peer = get_ava_p2p_interface(self, node) blockhash = node.getbestblockhash() self.wait_until( lambda: node.getstakecontendervote( blockhash, uint256_hex(peer.proof.proofid) ) == AvalancheContenderVoteError.PENDING ) return peer return [new_ava_interface(node) for _ in range(0, QUORUM_NODE_COUNT)] # Pick one node from the quorum for polling. quorum = get_quorum() poll_node = quorum[0] assert node.getavalancheinfo()["ready_to_poll"] is True def has_finalized_proof(proofid): can_find_inv_in_poll(quorum, proofid) return node.getrawavalancheproof(uint256_hex(proofid))["finalized"] for peer in quorum: self.wait_until(lambda: has_finalized_proof(peer.proof.proofid)) # Get the key so we can verify signatures. avakey = ECPubKey() avakey.set(bytes.fromhex(node.getavalanchekey())) def assert_response(expected): response = poll_node.wait_for_avaresponse() r = response.response # Verify signature assert avakey.verify_schnorr(response.sig, r.get_hash()) votes = r.votes assert_equal(len(votes), len(expected)) for i in range(0, len(votes)): assert_equal(repr(votes[i]), repr(expected[i])) def make_contender_id(prevblockhash, proofid): return int.from_bytes( hash256(ser_uint256(int(prevblockhash, 16)) + ser_uint256(proofid)), "little", ) # Before finalizing any blocks, no contender promotion occurs in the cache, # so the only way to test if the node knows about a particular contender is # to check it at the block height that the proof was first seen at. for i in range(0, QUORUM_NODE_COUNT): # We started with a clean chain and a new block is mined when creating # each quorum proof to make it valid, so the first block the proof was # seen at is the quorum proof's index + 1. blockhash = node.getblockhash(i + 1) poll_ids = [] expected = [] for p in range(0, QUORUM_NODE_COUNT): contender_proof = quorum[p].proof contender_id = make_contender_id(blockhash, contender_proof.proofid) poll_ids.append(contender_id) # If the node knows about the contender, it will respond as INVALID expected_vote = ( AvalancheContenderVoteError.PENDING if p == i else AvalancheContenderVoteError.UNKNOWN ) expected.append(AvalancheVote(expected_vote, contender_id)) poll_node.send_poll(poll_ids, inv_type=MSG_AVA_STAKE_CONTENDER) assert_response(expected) # Unknown contender unknown_contender_id = 0x123 poll_node.send_poll([unknown_contender_id], inv_type=MSG_AVA_STAKE_CONTENDER) assert_response( [AvalancheVote(AvalancheContenderVoteError.UNKNOWN, unknown_contender_id)] ) def has_finalized_tip(tip_expected): hash_tip_final = int(tip_expected, 16) can_find_inv_in_poll(quorum, hash_tip_final) return node.isfinalblock(tip_expected) # Finalize a block so we promote the contender cache with every block tip = node.getbestblockhash() self.wait_until(lambda: has_finalized_tip(tip)) assert_equal(node.getbestblockhash(), tip) # Now trigger building the whole cache for a block tip = self.generate(node, 1)[0] # Unknown contender is still unknown poll_node.send_poll([unknown_contender_id], inv_type=MSG_AVA_STAKE_CONTENDER) assert_response( [AvalancheVote(AvalancheContenderVoteError.UNKNOWN, unknown_contender_id)] ) # Pick a proof and poll for its status (not a winner since mock time has not # advanced past the staking rewards minimum registration delay) manual_winner = quorum[1].proof contender_id = make_contender_id(tip, manual_winner.proofid) poll_node.send_poll([contender_id], inv_type=MSG_AVA_STAKE_CONTENDER) assert_response( [AvalancheVote(AvalancheContenderVoteError.PENDING, contender_id)] ) # Advance time past the staking rewards minimum registration delay and # mine a block. now += 60 * 60 + 1 node.setmocktime(now) tip = self.generate(node, 1)[0] # Staking rewards has been computed - # TODO When implemented, the response could be ACCEPTED if this proof was - # selected as a winner. For now, it is always INVALID. contender_id = make_contender_id(tip, manual_winner.proofid) poll_node.send_poll([contender_id], inv_type=MSG_AVA_STAKE_CONTENDER) - assert_response( - [AvalancheVote(AvalancheContenderVoteError.INVALID, contender_id)] - ) + expectedVote = AvalancheContenderVoteError.INVALID + if node.getstakingreward(tip)[0]["proofid"] == uint256_hex( + manual_winner.proofid + ): + # If manual_winner happens to be selected as the winner, it will be accepted + expectedVote = AvalancheContenderVoteError.ACCEPTED + assert_response([AvalancheVote(expectedVote, contender_id)]) # Manually set this contender as a winner node.setstakingreward(tip, manual_winner.payout_script.hex()) poll_node.send_poll([contender_id], inv_type=MSG_AVA_STAKE_CONTENDER) assert_response( [AvalancheVote(AvalancheContenderVoteError.ACCEPTED, contender_id)] ) if __name__ == "__main__": AvalancheContenderVotingTest().main()