diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp index e5db6291b..d5531775e 100644 --- a/src/avalanche/processor.cpp +++ b/src/avalanche/processor.cpp @@ -1,914 +1,946 @@ // 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 #include #include #include #include #include #include #include #include // For DecodeSecret #include #include #include #include #include #include #include #include #include #include #include /** * Run the avalanche event loop every 10ms. */ static constexpr std::chrono::milliseconds AVALANCHE_TIME_STEP{10}; // Unfortunately, the bitcoind codebase is full of global and we are kinda // forced into it here. std::unique_ptr g_avalanche; namespace avalanche { static bool VerifyProof(const Proof &proof, bilingual_str &error) { ProofValidationState proof_state; if (!proof.verify(proof_state)) { switch (proof_state.GetResult()) { case ProofValidationResult::NO_STAKE: error = _("The avalanche proof has no stake."); return false; case ProofValidationResult::DUST_THRESOLD: 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; }; class Processor::NotificationsHandler : public interfaces::Chain::Notifications { Processor *m_processor; public: NotificationsHandler(Processor *p) : m_processor(p) {} void updatedBlockTip() override { LOCK(m_processor->cs_peerManager); if (m_processor->peerData && m_processor->peerData->proof) { m_processor->peerManager->registerProof( m_processor->peerData->proof); } m_processor->peerManager->updatedBlockTip(); } }; Processor::Processor(const ArgsManager &argsman, interfaces::Chain &chain, CConnman *connmanIn, CScheduler &scheduler, std::unique_ptr peerDataIn, CKey sessionKeyIn, uint32_t minQuorumTotalScoreIn, double minQuorumConnectedScoreRatioIn, int64_t minAvaproofsNodeCountIn, uint32_t staleVoteThresholdIn, uint32_t staleVoteFactorIn) : connman(connmanIn), queryTimeoutDuration(argsman.GetArg( "-avatimeout", AVALANCHE_DEFAULT_QUERY_TIMEOUT.count())), round(0), peerManager(std::make_unique(scheduler)), peerData(std::move(peerDataIn)), sessionKey(std::move(sessionKeyIn)), minQuorumScore(minQuorumTotalScoreIn), minQuorumConnectedScoreRatio(minQuorumConnectedScoreRatioIn), minAvaproofsNodeCount(minAvaproofsNodeCountIn), staleVoteThreshold(staleVoteThresholdIn), staleVoteFactor(staleVoteFactorIn) { // Make sure we get notified of chain state changes. chainNotificationsHandler = chain.handleNotifications(std::make_shared(this)); } Processor::~Processor() { chainNotificationsHandler.reset(); stopEventLoop(); } std::unique_ptr Processor::MakeProcessor(const ArgsManager &argsman, interfaces::Chain &chain, CConnman *connman, CScheduler &scheduler, bilingual_str &error) { std::unique_ptr peerData; CKey masterKey; CKey sessionKey; 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::make(); if (!Proof::FromHex(*proof, argsman.GetArg("-avaproof", ""), error)) { // error is set by FromHex return nullptr; } peerData = std::make_unique(); peerData->proof = std::move(proof); if (!VerifyProof(*peerData->proof, error)) { // error is set by VerifyProof return nullptr; } std::unique_ptr 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(dg); } else { if (masterPubKey != peerData->proof->getMaster()) { error = _("The master key does not match the proof public key."); return nullptr; } dgb = std::make_unique(*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; } } // 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") && !ParseDouble(argsman.GetArg("-avaminquorumconnectedstakeratio", ""), &minQuorumConnectedStakeRatio)) { error = _("The avalanche min quorum connected stake ratio is invalid."); return nullptr; } if (minQuorumConnectedStakeRatio < 0 || minQuorumConnectedStakeRatio > 1) { error = _( "The avalanche min quorum connected stake ratio is out of range."); return nullptr; } int64_t minAvaproofsNodeCount = argsman.GetArg("-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.GetArg("-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::max()) { error = strprintf(_("The avalanche stale vote threshold must be less " "than or equal to %d"), std::numeric_limits::max()); return nullptr; } int64_t staleVoteFactor = argsman.GetArg("-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::max()) { error = strprintf(_("The avalanche stale vote factor must be less than " "or equal to %d"), std::numeric_limits::max()); return nullptr; } // We can't use std::make_unique with a private constructor return std::unique_ptr(new Processor( argsman, chain, connman, scheduler, std::move(peerData), std::move(sessionKey), Proof::amountToScore(minQuorumStake), minQuorumConnectedStakeRatio, minAvaproofsNodeCount, staleVoteThreshold, staleVoteFactor)); } bool Processor::addBlockToReconcile(const CBlockIndex *pindex) { - bool isAccepted; - if (!pindex) { // isWorthPolling expects this to be non-null, so bail early. return false; } + bool isAccepted; { LOCK(cs_main); if (!isWorthPolling(pindex)) { // There is no point polling this block. return false; } isAccepted = ::ChainActive().Contains(pindex); } return blockVoteRecords.getWriteView() ->insert(std::make_pair(pindex, VoteRecord(isAccepted))) .second; } -void Processor::addProofToReconcile(const ProofRef &proof) { - // TODO We don't want to accept an infinite number of conflicting proofs. - // They should be some rules to make them expensive and/or limited by - // design. - const bool isAccepted = WITH_LOCK( - cs_peerManager, return peerManager->isBoundToPeer(proof->getId())); +bool Processor::addProofToReconcile(const ProofRef &proof) { + if (!proof) { + // isWorthPolling expects this to be non-null, so bail early. + return false; + } + + bool isAccepted; + { + LOCK(cs_peerManager); + if (!isWorthPolling(proof)) { + return false; + } + + isAccepted = peerManager->isBoundToPeer(proof->getId()); + } - proofVoteRecords.getWriteView()->insert( - std::make_pair(proof, VoteRecord(isAccepted))); + return proofVoteRecords.getWriteView() + ->insert(std::make_pair(proof, VoteRecord(isAccepted))) + .second; } bool Processor::isAccepted(const CBlockIndex *pindex) const { if (!pindex) { // CBlockIndexWorkComparator expects this to be non-null, so bail early. return false; } auto r = blockVoteRecords.getReadView(); auto it = r->find(pindex); if (it == r.end()) { return false; } return it->second.isAccepted(); } bool Processor::isAccepted(const ProofRef &proof) const { auto r = proofVoteRecords.getReadView(); auto it = r->find(proof); if (it == r.end()) { return false; } return it->second.isAccepted(); } int Processor::getConfidence(const CBlockIndex *pindex) const { if (!pindex) { // CBlockIndexWorkComparator expects this to be non-null, so bail early. return -1; } auto r = blockVoteRecords.getReadView(); auto it = r->find(pindex); if (it == r.end()) { return -1; } return it->second.getConfidence(); } int Processor::getConfidence(const ProofRef &proof) const { auto r = proofVoteRecords.getReadView(); auto it = r->find(proof); if (it == r.end()) { return -1; } return it->second.getConfidence(); } 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)) { CHashWriter hasher(SER_GETHASH, 0); 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 &blockUpdates, std::vector &proofUpdates, 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, std::chrono::steady_clock::now() + std::chrono::milliseconds(response.getCooldown())); } std::vector invs; { // Check that the query exists. auto w = queries.getWriteView(); auto it = w->find(std::make_tuple(nodeid, response.getRound())); if (it == w.end()) { banscore = 2; 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 &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 responseIndex; std::map responseProof; // 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++) { if (invs[i].IsMsgBlk()) { CBlockIndex *pindex; { LOCK(cs_main); pindex = g_chainman.m_blockman.LookupBlockIndex( BlockHash(votes[i].GetHash())); if (!pindex) { // This should not happen, but just in case... continue; } if (!isWorthPolling(pindex)) { // There is no point polling this block. continue; } } responseIndex.insert(std::make_pair(pindex, votes[i])); } if (invs[i].IsMsgProof()) { const ProofId proofid(votes[i].GetHash()); - const ProofRef proof = WITH_LOCK( - cs_peerManager, return peerManager->getProof(proofid)); - if (!proof) { - continue; + ProofRef proof; + { + LOCK(cs_peerManager); + proof = peerManager->getProof(proofid); + if (!proof) { + continue; + } + + if (!isWorthPolling(proof)) { + continue; + } } responseProof.insert(std::make_pair(proof, votes[i])); } } // Thanks to C++14 generic lambdas, we can apply the same logic to various // parameter types sharing the same interface. auto registerVoteItems = [&](auto voteRecordsWriteView, auto &updates, auto responseItems) { // 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(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 note been finalized, so we have nothing more to // do. updates.emplace_back(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(item, vr.isAccepted() ? VoteStatus::Finalized : VoteStatus::Invalid); voteRecordsWriteView->erase(it); } }; registerVoteItems(blockVoteRecords.getWriteView(), blockUpdates, responseIndex); registerVoteItems(proofVoteRecords.getWriteView(), proofUpdates, responseProof); return true; } CPubKey Processor::getSessionPubKey() const { return sessionKey.GetPubKey(); } bool Processor::sendHello(CNode *pfrom) const { if (!peerData) { // We do not have a delegation to advertise. return false; } // Now let's sign! SchnorrSig sig; { const uint256 hash = buildLocalSighash(pfrom); if (!sessionKey.SignSchnorr(hash, sig)) { return false; } } connman->PushMessage(pfrom, CNetMsgMaker(pfrom->GetCommonVersion()) .Make(NetMsgType::AVAHELLO, Hello(peerData->delegation, sig))); pfrom->AddKnownProof(peerData->delegation.getProofId()); return true; } ProofRef Processor::getLocalProof() const { return peerData ? peerData->proof : ProofRef(); } 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) { if (::ChainstateActive().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() { if (quorumIsEstablished) { return true; } // Don't do Avalanche while node is IBD'ing if (::ChainstateActive().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; return true; } void Processor::FinalizeNode(const Config &config, const CNode &node, bool &update_connection_time) { WITH_LOCK(cs_peerManager, peerManager->removeNode(node.GetId())); } 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 = getSuitableNodeToQuery(); if (nodeid == NO_NODE) { return; } std::vector invs = getInvsForNextPoll(); if (invs.empty()) { return; } 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) { uint64_t current_round = round++; { // Compute the time at which this requests times out. auto timeout = std::chrono::steady_clock::now() + queryTimeoutDuration; // Register the query. queries.getWriteView()->insert( {pnode->GetId(), current_round, timeout, invs}); // Set the timeout. LOCK(cs_peerManager); peerManager->updateNextRequestTime(pnode->GetId(), timeout); } pnode->m_avalanche_state->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. LOCK(cs_peerManager); peerManager->removeNode(nodeid); } // Get next suitable node to try again nodeid = getSuitableNodeToQuery(); } while (nodeid != NO_NODE); } void Processor::clearTimedoutRequests() { auto now = std::chrono::steady_clock::now(); std::map timedout_items{}; { // Clear expired requests. auto w = queries.getWriteView(); auto it = w->get().begin(); while (it != w->get().end() && it->timeout < now) { for (const auto &i : it->invs) { timedout_items[i]++; } w->get().erase(it++); } } if (timedout_items.empty()) { return; } auto clearInflightRequest = [&](auto &voteRecords, const auto &voteItem, uint8_t count) { if (!voteItem) { return false; } auto voteRecordsWriteView = voteRecords.getWriteView(); auto it = voteRecordsWriteView->find(voteItem); if (it == voteRecordsWriteView.end()) { return false; } it->second.clearInflightRequest(count); return true; }; // In flight request accounting. for (const auto &p : timedout_items) { const CInv &inv = p.first; if (inv.IsMsgBlk()) { const CBlockIndex *pindex = WITH_LOCK( cs_main, return g_chainman.m_blockman.LookupBlockIndex( BlockHash(inv.hash))); if (!clearInflightRequest(blockVoteRecords, pindex, p.second)) { continue; } } if (inv.IsMsgProof()) { const ProofRef proof = WITH_LOCK(cs_peerManager, return peerManager->getProof(ProofId(inv.hash))); if (!clearInflightRequest(proofVoteRecords, proof, p.second)) { continue; } } } } std::vector Processor::getInvsForNextPoll(bool forPoll) { std::vector invs; + // Use NO_THREAD_SAFETY_ANALYSIS to avoid false positive due to + // isWorthPolling requiring a different lock depending of the prototype. + auto removeItemsNotWorthPolling = + [&](auto &itemVoteRecords) NO_THREAD_SAFETY_ANALYSIS { + auto w = itemVoteRecords.getWriteView(); + for (auto it = w->begin(); it != w->end();) { + if (!isWorthPolling(it->first)) { + it = w->erase(it); + } else { + ++it; + } + } + }; + auto extractVoteRecordsToInvs = [&](const auto &itemVoteRecordRange, auto buildInvFromVoteItem) { for (const auto &[item, voteRecord] : itemVoteRecordRange) { if (invs.size() >= AVALANCHE_MAX_ELEMENT_POLL) { // Make sure we do not produce more invs than specified by the // protocol. return true; } const bool shouldPoll = forPoll ? voteRecord.registerPoll() : voteRecord.shouldPoll(); if (!shouldPoll) { continue; } invs.emplace_back(buildInvFromVoteItem(item)); } return invs.size() >= AVALANCHE_MAX_ELEMENT_POLL; }; + // First remove all proofs that are not worth polling. + WITH_LOCK(cs_peerManager, removeItemsNotWorthPolling(proofVoteRecords)); + if (extractVoteRecordsToInvs(proofVoteRecords.getReadView(), [](const ProofRef &proof) { return CInv(MSG_AVA_PROOF, proof->getId()); })) { // The inventory vector is full, we're done return invs; } // First remove all blocks that are not worth polling. - { - LOCK(cs_main); - auto w = blockVoteRecords.getWriteView(); - for (auto it = w->begin(); it != w->end();) { - const CBlockIndex *pindex = it->first; - if (!isWorthPolling(pindex)) { - w->erase(it++); - } else { - ++it; - } - } - } + WITH_LOCK(cs_main, removeItemsNotWorthPolling(blockVoteRecords)); auto r = blockVoteRecords.getReadView(); extractVoteRecordsToInvs(reverse_iterate(r), [](const CBlockIndex *pindex) { return CInv(MSG_BLOCK, pindex->GetBlockHash()); }); return invs; } NodeId Processor::getSuitableNodeToQuery() { LOCK(cs_peerManager); return peerManager->selectNode(); } uint256 Processor::buildLocalSighash(CNode *pfrom) const { CHashWriter hasher(SER_GETHASH, 0); hasher << peerData->delegation.getId(); hasher << pfrom->GetLocalNonce(); hasher << pfrom->nRemoteHostNonce; hasher << pfrom->GetLocalExtraEntropy(); hasher << pfrom->nRemoteExtraEntropy; return hasher.GetHash(); } bool Processor::isWorthPolling(const CBlockIndex *pindex) const { AssertLockHeld(cs_main); if (pindex->nStatus.isInvalid()) { // No point polling invalid blocks. return false; } if (::ChainstateActive().IsBlockFinalized(pindex)) { // There is no point polling finalized block. return false; } return true; } +bool Processor::isWorthPolling(const ProofRef &proof) const { + AssertLockHeld(cs_peerManager); + + const ProofId &proofid = proof->getId(); + + // No point polling orphans or discarded proofs + return peerManager->isBoundToPeer(proofid) || + peerManager->isInConflictingPool(proofid); +} + } // namespace avalanche diff --git a/src/avalanche/processor.h b/src/avalanche/processor.h index 2ce9a1269..aa9058341 100644 --- a/src/avalanche/processor.h +++ b/src/avalanche/processor.h @@ -1,269 +1,271 @@ // 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class ArgsManager; class CBlockIndex; 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}; namespace avalanche { class Delegation; class PeerManager; class Proof; struct VoteRecord; enum struct VoteStatus : uint8_t { Invalid, Rejected, Accepted, Finalized, Stale, }; template class VoteItemUpdate { VoteItem item; VoteStatus status; public: VoteItemUpdate(const VoteItem itemIn, VoteStatus statusIn) : item(std::move(itemIn)), status(statusIn) {} const VoteStatus &getStatus() const { return status; } VoteItem getVoteItem() { return item; } const VoteItem getVoteItem() const { return item; } }; using BlockUpdate = VoteItemUpdate; using ProofUpdate = VoteItemUpdate; using BlockVoteMap = std::map; using ProofVoteMap = std::map; 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 { CConnman *connman; std::chrono::milliseconds queryTimeoutDuration; /** * Blocks to run avalanche on. */ RWCollection blockVoteRecords; /** * Proofs to run avalanche on. */ RWCollection proofVoteRecords; /** * Keep track of peers and queries sent. */ std::atomic round; /** * Keep track of the peers and associated infos. */ mutable Mutex cs_peerManager; std::unique_ptr peerManager GUARDED_BY(cs_peerManager); struct Query { NodeId nodeid; uint64_t round; TimePoint 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 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::member>>, // sorted by timeout boost::multi_index::ordered_non_unique< boost::multi_index::tag, boost::multi_index::member>>>; RWCollection queries; /** Data required to participate. */ struct PeerData; std::unique_ptr peerData; CKey sessionKey; /** Event loop machinery. */ EventLoop eventLoop; /** * Quorum management. */ uint32_t minQuorumScore; double minQuorumConnectedScoreRatio; std::atomic quorumIsEstablished{false}; int64_t minAvaproofsNodeCount; std::atomic avaproofsNodeCounter{0}; /** Voting parameters. */ const uint32_t staleVoteThreshold; const uint32_t staleVoteFactor; /** Registered interfaces::Chain::Notifications handler. */ class NotificationsHandler; std::unique_ptr chainNotificationsHandler; Processor(const ArgsManager &argsman, interfaces::Chain &chain, CConnman *connmanIn, CScheduler &scheduler, std::unique_ptr peerDataIn, CKey sessionKeyIn, uint32_t minQuorumTotalScoreIn, double minQuorumConnectedScoreRatioIn, int64_t minAvaproofsNodeCountIn, uint32_t staleVoteThresholdIn, uint32_t staleVoteFactorIn); public: ~Processor(); static std::unique_ptr MakeProcessor(const ArgsManager &argsman, interfaces::Chain &chain, CConnman *connman, CScheduler &scheduler, bilingual_str &error); void setQueryTimeoutDuration(std::chrono::milliseconds d) { queryTimeoutDuration = d; } bool addBlockToReconcile(const CBlockIndex *pindex); - void addProofToReconcile(const ProofRef &proof); + bool addProofToReconcile(const ProofRef &proof); bool isAccepted(const CBlockIndex *pindex) const; bool isAccepted(const ProofRef &proof) const; int getConfidence(const CBlockIndex *pindex) const; int getConfidence(const ProofRef &proof) const; // 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 &blockUpdates, std::vector &proofUpdates, int &banscore, std::string &error); template auto withPeerManager(Callable &&func) const { LOCK(cs_peerManager); return func(*peerManager); } CPubKey getSessionPubKey() const; bool sendHello(CNode *pfrom) const; ProofRef getLocalProof() const; /* * Return whether the avalanche service flag should be set. */ bool isAvalancheServiceAvailable() { return !!peerData; } bool startEventLoop(CScheduler &scheduler); bool stopEventLoop(); void avaproofsSent(NodeId nodeid); int64_t getAvaproofsNodeCounter() const { return avaproofsNodeCounter.load(); } bool isQuorumEstablished(); // Implement NetEventInterface. Only FinalizeNode is of interest. void InitializeNode(const Config &config, CNode *pnode) override {} bool ProcessMessages(const Config &config, CNode *pnode, std::atomic &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, bool &update_connection_time) override; private: void runEventLoop(); void clearTimedoutRequests(); std::vector getInvsForNextPoll(bool forPoll = true); NodeId getSuitableNodeToQuery(); /** * Build and return the challenge whose signature is included in the * AVAHELLO message that we send to a peer. */ uint256 buildLocalSighash(CNode *pfrom) const; bool isWorthPolling(const CBlockIndex *pindex) const EXCLUSIVE_LOCKS_REQUIRED(cs_main); + bool isWorthPolling(const ProofRef &proof) const + EXCLUSIVE_LOCKS_REQUIRED(cs_peerManager); 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 92408c3aa..4fa29ca4f 100644 --- a/src/avalanche/test/processor_tests.cpp +++ b/src/avalanche/test/processor_tests.cpp @@ -1,1455 +1,1519 @@ // 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 #include #include #include #include #include #include #include #include // For ::PeerManager #include #include #include #include // For bilingual_str // D6970 moved LookupBlockIndex from chain.h to validation.h TODO: remove this // when LookupBlockIndex is refactored out of validation #include #include #include #include #include #include +#include #include using namespace avalanche; namespace avalanche { namespace { struct AvalancheTest { static void runEventLoop(avalanche::Processor &p) { p.runEventLoop(); } static std::vector getInvsForNextPoll(Processor &p) { return p.getInvsForNextPoll(false); } static NodeId getSuitableNodeToQuery(Processor &p) { return p.getSuitableNodeToQuery(); } 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; } }; } // namespace } // namespace avalanche namespace { struct CConnmanTest : public CConnman { using CConnman::CConnman; void AddNode(CNode &node) { LOCK(cs_vNodes); vNodes.push_back(&node); } void ClearNodes() { LOCK(cs_vNodes); for (CNode *node : vNodes) { delete node; } vNodes.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 m_processor; // The master private key we delegate to. CKey masterpriv; AvalancheTestingSetup() : TestChain100Setup(), config(GetConfig()), masterpriv(CKey::MakeCompressedKey()) { // Deterministic randomness for tests. auto connman = std::make_unique(config, 0x1337, 0x1337); m_connman = connman.get(); m_node.connman = std::move(connman); m_node.peerman = ::PeerManager::make( config.GetChainParams(), *m_connman, m_node.banman.get(), *m_node.scheduler, *m_node.chainman, *m_node.mempool, false); m_node.chain = interfaces::MakeChain(m_node, config.GetChainParams()); // Get the processor ready. bilingual_str error; m_processor = Processor::MakeProcessor(*m_node.args, *m_node.chain, m_node.connman.get(), *m_node.scheduler, error); BOOST_CHECK(m_processor); gArgs.ForceSetArg("-avaproofstakeutxoconfirmations", "1"); } ~AvalancheTestingSetup() { m_connman->ClearNodes(); SyncWithValidationInterfaceQueue(); gArgs.ClearForcedArg("-avaproofstakeutxoconfirmations"); } CNode *ConnectNode(ServiceFlags nServices) { static NodeId id = 0; CAddress addr(ip(GetRandInt(0xffffffff)), NODE_NONE); auto node = new CNode(id++, ServiceFlags(NODE_NETWORK), INVALID_SOCKET, addr, /* nKeyedNetGroupIn */ 0, /* nLocalHostNonceIn */ 0, /* nLocalExtraEntropyIn */ 0, CAddress(), /* pszDest */ "", ConnectionType::OUTBOUND_FULL_RELAY, /* inbound_onion */ false); node->SetCommonVersion(PROTOCOL_VERSION); node->nServices = nServices; m_node.peerman->InitializeNode(config, node); node->nVersion = 1; node->fSuccessfullyConnected = true; node->m_avalanche_state = std::make_unique(); m_connman->AddNode(*node); return node; } size_t next_coinbase = 0; ProofRef GetProof() { size_t current_coinbase = next_coinbase++; const CTransaction &coinbase = *m_coinbase_txns[current_coinbase]; ProofBuilder pb(0, 0, masterpriv); BOOST_CHECK(pb.addUTXO(COutPoint(coinbase.GetId(), 0), coinbase.vout[0].nValue, current_coinbase + 1, true, coinbaseKey)); 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 ConnectNodes() { auto proof = GetProof(); BOOST_CHECK( m_processor->withPeerManager([&](avalanche::PeerManager &pm) { return pm.registerProof(proof); })); const ProofId &proofid = proof->getId(); std::array 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 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 &blockUpdates) { int banscore; std::string error; std::vector proofUpdates; return m_processor->registerVotes(nodeid, response, blockUpdates, proofUpdates, banscore, error); } }; struct BlockProvider { AvalancheTestingSetup *fixture; std::vector updates; 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 g_chainman.m_blockman.LookupBlockIndex(blockHash); } uint256 getVoteItemId(const CBlockIndex *pindex) const { return pindex->GetBlockHash(); } bool registerVotes(NodeId nodeid, const avalanche::Response &response, std::string &error) { int banscore; std::vector proofUpdates; return fixture->m_processor->registerVotes( nodeid, response, updates, proofUpdates, banscore, error); } bool registerVotes(NodeId nodeid, const avalanche::Response &response) { std::string error; return registerVotes(nodeid, response, error); } bool addToReconcile(const CBlockIndex *pindex) { return fixture->m_processor->addBlockToReconcile(pindex); } std::vector buildVotesForItems(uint32_t error, std::vector &&items) { size_t numItems = items.size(); std::vector 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) { + pindex->nStatus = pindex->nStatus.withFailed(); + } }; struct ProofProvider { AvalancheTestingSetup *fixture; std::vector updates; 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(); } bool registerVotes(NodeId nodeid, const avalanche::Response &response, std::string &error) { int banscore; std::vector blockUpdates; return fixture->m_processor->registerVotes( nodeid, response, blockUpdates, updates, banscore, error); } bool registerVotes(NodeId nodeid, const avalanche::Response &response) { std::string error; return registerVotes(nodeid, response, error); } bool addToReconcile(const ProofRef &proof) { - fixture->m_processor->addProofToReconcile(proof); - return true; + return fixture->m_processor->addProofToReconcile(proof); } std::vector buildVotesForItems(uint32_t error, std::vector &&items) { size_t numItems = items.size(); std::vector 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); + }); + } }; } // 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; BOOST_AUTO_TEST_CASE(block_update) { CBlockIndex index; CBlockIndex *pindex = &index; std::set status{ VoteStatus::Invalid, VoteStatus::Rejected, VoteStatus::Accepted, VoteStatus::Finalized, VoteStatus::Stale, }; for (auto s : status) { BlockUpdate abu(pindex, s); // The use of BOOST_CHECK instead of BOOST_CHECK_EQUAL prevents from // having to define operator<<() for each argument type. BOOST_CHECK(abu.getVoteItem() == pindex); BOOST_CHECK(abu.getStatus() == s); } } -BOOST_AUTO_TEST_CASE(block_reconcile_twice) { - CBlock block = CreateAndProcessBlock({}, CScript()); - const BlockHash blockHash = block.GetHash(); - CBlockIndex *pindex; - { - LOCK(cs_main); - pindex = g_chainman.m_blockman.LookupBlockIndex(blockHash); - } +BOOST_AUTO_TEST_CASE_TEMPLATE(item_reconcile_twice, P, VoteItemProviders) { + P provider(this); - // Adding the block twice does nothing. - BOOST_CHECK(m_processor->addBlockToReconcile(pindex)); - BOOST_CHECK(!m_processor->addBlockToReconcile(pindex)); - BOOST_CHECK(m_processor->isAccepted(pindex)); + auto item = provider.buildVoteItem(); + + // Adding the item twice does nothing. + BOOST_CHECK(provider.addToReconcile(item)); + BOOST_CHECK(!provider.addToReconcile(item)); + BOOST_CHECK(m_processor->isAccepted(item)); } -BOOST_AUTO_TEST_CASE(block_null) { +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); - BOOST_CHECK(!m_processor->addBlockToReconcile(nullptr)); + auto item = decltype(provider.buildVoteItem())(); + BOOST_CHECK(item == nullptr); + BOOST_CHECK(!provider.addToReconcile(item)); - // Check that adding blocks to vote on doesn't change the outcome. A + // 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. - CBlock block = CreateAndProcessBlock({}, CScript()); - const BlockHash blockHash = block.GetHash(); - CBlockIndex *pindex; - { - LOCK(cs_main); - pindex = g_chainman.m_blockman.LookupBlockIndex(blockHash); - } - BOOST_CHECK(m_processor->addBlockToReconcile(pindex)); + item = provider.buildVoteItem(); + BOOST_CHECK(provider.addToReconcile(item)); BOOST_CHECK(!m_processor->isAccepted(nullptr)); BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1); } namespace { Response next(Response &r) { auto copy = r; r = {r.getRound() + 1, r.getCooldown(), r.GetVotes()}; return copy; } } // namespace BOOST_AUTO_TEST_CASE_TEMPLATE(vote_item_register, P, VoteItemProviders) { P provider(this); auto &updates = provider.updates; 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(); // 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(provider.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; auto registerNewVote = [&](const Response &resp) { runEventLoop(); auto nodeid = avanodes[nextNodeIndex++ % avanodes.size()]->GetId(); BOOST_CHECK(provider.registerVotes(nodeid, resp)); }; // 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(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); // Now let's undo this and finalize rejection. BOOST_CHECK(provider.addToReconcile(item)); 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(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(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); auto &updates = provider.updates; 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(provider.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(); BOOST_CHECK(provider.registerVotes(avanodes[0]->GetId(), {round, 0, {Vote(0, itemidA)}})); BOOST_CHECK_EQUAL(updates.size(), 0); // Start voting on item B after one vote. std::vector votes = provider.buildVotesForItems(0, {itemA, itemB}); Response resp{round + 1, 0, votes}; BOOST_CHECK(provider.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(provider.registerVotes(nodeid, next(resp))); 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(provider.registerVotes(nodeid, next(resp))); 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(provider.registerVotes(firstNodeid, next(resp))); BOOST_CHECK_EQUAL(updates.size(), 1); BOOST_CHECK(updates[0].getVoteItem() == itemA); BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized); updates = {}; // 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(provider.registerVotes(secondNodeid, resp)); BOOST_CHECK_EQUAL(updates.size(), 1); BOOST_CHECK(updates[0].getVoteItem() == itemB); BOOST_CHECK(updates[0].getStatus() == VoteStatus::Finalized); updates = {}; // 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); auto &updates = provider.updates; const uint32_t invType = provider.invType; const auto item = provider.buildVoteItem(); const auto itemid = provider.getVoteItemId(item); // There is no node to query. BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), NO_NODE); // Create a node that supports avalanche and one that doesn't. ConnectNode(NODE_NONE); auto avanode = ConnectNode(NODE_AVALANCHE); NodeId avanodeid = avanode->GetId(); BOOST_CHECK(addNode(avanodeid)); // It returns the avalanche peer. BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid); // Register an item and check it is added to the list of elements to poll. BOOST_CHECK(provider.addToReconcile(item)); auto invs = getInvsForNextPoll(); BOOST_CHECK_EQUAL(invs.size(), 1); BOOST_CHECK_EQUAL(invs[0].type, invType); BOOST_CHECK(invs[0].hash == itemid); // Trigger a poll on avanode. uint64_t round = getRound(); runEventLoop(); // There is no more suitable peer available, so return nothing. BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), NO_NODE); // Respond to the request. Response resp = {round, 0, {Vote(0, itemid)}}; BOOST_CHECK(provider.registerVotes(avanodeid, resp)); 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(!provider.registerVotes(nodeid, response, 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); // 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(provider.registerVotes(avanodeid, resp)); BOOST_CHECK_EQUAL(updates.size(), 0); BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid); // Out of order response are rejected. const auto item2 = provider.buildVoteItem(); BOOST_CHECK(provider.addToReconcile(item2)); std::vector 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(provider.registerVotes(avanodeid, resp)); BOOST_CHECK_EQUAL(updates.size(), 0); BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), avanodeid); } -BOOST_AUTO_TEST_CASE(dont_poll_invalid_block) { - std::vector updates; +BOOST_AUTO_TEST_CASE_TEMPLATE(dont_poll_invalid_item, P, VoteItemProviders) { + P provider(this); + auto &updates = provider.updates; + const uint32_t invType = provider.invType; - CBlock blockA = CreateAndProcessBlock({}, CScript()); - CBlock blockB = CreateAndProcessBlock({}, CScript()); - const BlockHash blockHashA = blockA.GetHash(); - const BlockHash blockHashB = blockB.GetHash(); - const CBlockIndex *pindexA; - CBlockIndex *pindexB; - { - LOCK(cs_main); - pindexA = g_chainman.m_blockman.LookupBlockIndex(blockHashA); - pindexB = g_chainman.m_blockman.LookupBlockIndex(blockHashB); - } + auto itemA = provider.buildVoteItem(); + auto itemB = provider.buildVoteItem(); auto avanodes = ConnectNodes(); - // Register the blocks and check they are added to the list of elements to + // Build votes to get proper ordering + std::vector votes = provider.buildVotesForItems(0, {itemA, itemB}); + + // Register the items and check they are added to the list of elements to // poll. - BOOST_CHECK(m_processor->addBlockToReconcile(pindexA)); - BOOST_CHECK(m_processor->addBlockToReconcile(pindexB)); + BOOST_CHECK(provider.addToReconcile(itemA)); + BOOST_CHECK(provider.addToReconcile(itemB)); auto invs = getInvsForNextPoll(); BOOST_CHECK_EQUAL(invs.size(), 2); - BOOST_CHECK_EQUAL(invs[0].type, MSG_BLOCK); - BOOST_CHECK(invs[0].hash == blockHashB); - BOOST_CHECK_EQUAL(invs[1].type, MSG_BLOCK); - BOOST_CHECK(invs[1].hash == blockHashA); - - // When a block is marked invalid, stop polling. - pindexB->nStatus = pindexB->nStatus.withFailed(); - Response resp{getRound(), 0, {Vote(0, blockHashA)}}; + 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))}}; runEventLoop(); - BOOST_CHECK(registerVotes(avanodes[0]->GetId(), resp, updates)); + BOOST_CHECK(provider.registerVotes(avanodes[0]->GetId(), goodResp)); BOOST_CHECK_EQUAL(updates.size(), 0); + + // Votes including itemB are rejected + Response badResp{getRound(), 0, votes}; + runEventLoop(); + std::string error; + BOOST_CHECK(!provider.registerVotes(avanodes[1]->GetId(), badResp, 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); const auto item = provider.buildVoteItem(); const auto itemid = provider.getVoteItemId(item); // Add the item BOOST_CHECK(provider.addToReconcile(item)); // Create a node that supports avalanche. auto avanode = ConnectNode(NODE_AVALANCHE); NodeId avanodeid = avanode->GetId(); BOOST_CHECK(addNode(avanodeid)); // Expire requests after some time. auto queryTimeDuration = std::chrono::milliseconds(10); m_processor->setQueryTimeoutDuration(queryTimeDuration); for (int i = 0; i < 10; i++) { Response resp = {getRound(), 0, {Vote(0, itemid)}}; auto start = std::chrono::steady_clock::now(); 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(); bool ret = provider.registerVotes(avanodeid, next(resp)); if (std::chrono::steady_clock::now() > 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); // Now try again but wait for expiration. runEventLoop(); std::this_thread::sleep_for(queryTimeDuration); runEventLoop(); BOOST_CHECK(!provider.registerVotes(avanodeid, next(resp))); } } 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 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(provider.addToReconcile(item)); // Ensure there are enough requests in flight. std::map 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, 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)}}; BOOST_CHECK(provider.registerVotes(it->first, resp)); 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 updates; CBlock block = CreateAndProcessBlock({}, CScript()); const BlockHash blockHash = block.GetHash(); const CBlockIndex *pindex; { LOCK(cs_main); pindex = g_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->addBlockToReconcile(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 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 = g_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::system_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 node that supports avalanche. auto avanode = ConnectNode(NODE_AVALANCHE); NodeId nodeid = avanode->GetId(); BOOST_CHECK(addNode(nodeid)); // There is no query in flight at the moment. BOOST_CHECK_EQUAL(getSuitableNodeToQuery(), nodeid); // Add a new block. Check it is added to the polls. uint64_t queryRound = getRound(); BOOST_CHECK(m_processor->addBlockToReconcile(pindex)); 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 10ms. UninterruptibleSleep(std::chrono::milliseconds(1)); if (getRound() != queryRound) { 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 = std::chrono::steady_clock::now() + std::chrono::milliseconds(100); std::vector updates; 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(std::chrono::steady_clock::now() > 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::system_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; auto addProofToReconcile = [&](uint32_t proofScore) { auto proof = buildRandomProof(proofScore); - m_processor->addProofToReconcile(proof); + m_processor->withPeerManager([&](avalanche::PeerManager &pm) { + BOOST_CHECK(pm.registerProof(proof)); + }); + BOOST_CHECK(m_processor->addProofToReconcile(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) { + gArgs.ForceSetArg("-enableavalancheproofreplacement", "1"); + gArgs.ForceSetArg("-avalancheconflictingproofcooldown", "0"); + BOOST_CHECK(!m_processor->isAccepted(nullptr)); BOOST_CHECK_EQUAL(m_processor->getConfidence(nullptr), -1); - auto proofA = GetProof(); - auto proofB = GetProof(); + const CKey key = CKey::MakeCompressedKey(); - BOOST_CHECK(!m_processor->isAccepted(proofA)); - BOOST_CHECK(!m_processor->isAccepted(proofB)); - BOOST_CHECK_EQUAL(m_processor->getConfidence(proofA), -1); - BOOST_CHECK_EQUAL(m_processor->getConfidence(proofB), -1); + const COutPoint conflictingOutpoint{TxId(GetRandHash()), 0}; + { + CScript script = GetScriptForDestination(PKHash(key.GetPubKey())); - m_processor->addProofToReconcile(proofA); - BOOST_CHECK(!m_processor->isAccepted(proofA)); - BOOST_CHECK(!m_processor->isAccepted(proofB)); - BOOST_CHECK_EQUAL(m_processor->getConfidence(proofA), 0); - BOOST_CHECK_EQUAL(m_processor->getConfidence(proofB), -1); + LOCK(cs_main); + CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); + coins.AddCoin(conflictingOutpoint, + Coin(CTxOut(10 * COIN, script), 10, false), false); + } + const COutPoint missingOutpoint{TxId(GetRandHash()), 0}; + + auto buildProof = [&](const COutPoint &outpoint, uint64_t sequence) { + ProofBuilder pb(sequence, 0, key); + BOOST_CHECK(pb.addUTXO(outpoint, 10 * COIN, 10, false, key)); + return pb.build(); + }; + + auto conflictingProof = buildProof(conflictingOutpoint, 1); + auto validProof = buildProof(conflictingOutpoint, 2); + auto orphanProof = buildProof(missingOutpoint, 3); + + BOOST_CHECK(!m_processor->isAccepted(conflictingProof)); + BOOST_CHECK(!m_processor->isAccepted(validProof)); + BOOST_CHECK(!m_processor->isAccepted(orphanProof)); + BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), -1); + BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), -1); + BOOST_CHECK_EQUAL(m_processor->getConfidence(orphanProof), -1); + + // Reconciling proofs that don't exist will fail + BOOST_CHECK(!m_processor->addProofToReconcile(conflictingProof)); + BOOST_CHECK(!m_processor->addProofToReconcile(validProof)); + BOOST_CHECK(!m_processor->addProofToReconcile(orphanProof)); m_processor->withPeerManager([&](avalanche::PeerManager &pm) { - BOOST_CHECK(pm.registerProof(proofB)); + BOOST_CHECK(pm.registerProof(conflictingProof)); + BOOST_CHECK(pm.registerProof(validProof)); + BOOST_CHECK(!pm.registerProof(orphanProof)); + + BOOST_CHECK(pm.isBoundToPeer(validProof->getId())); + BOOST_CHECK(pm.isInConflictingPool(conflictingProof->getId())); + BOOST_CHECK(pm.isOrphan(orphanProof->getId())); }); - m_processor->addProofToReconcile(proofB); - BOOST_CHECK(!m_processor->isAccepted(proofA)); - BOOST_CHECK(m_processor->isAccepted(proofB)); - BOOST_CHECK_EQUAL(m_processor->getConfidence(proofA), 0); - BOOST_CHECK_EQUAL(m_processor->getConfidence(proofB), 0); + + BOOST_CHECK(m_processor->addProofToReconcile(conflictingProof)); + BOOST_CHECK(!m_processor->isAccepted(conflictingProof)); + BOOST_CHECK(!m_processor->isAccepted(validProof)); + BOOST_CHECK(!m_processor->isAccepted(orphanProof)); + BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0); + BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), -1); + BOOST_CHECK_EQUAL(m_processor->getConfidence(orphanProof), -1); + + BOOST_CHECK(m_processor->addProofToReconcile(validProof)); + BOOST_CHECK(!m_processor->isAccepted(conflictingProof)); + BOOST_CHECK(m_processor->isAccepted(validProof)); + BOOST_CHECK(!m_processor->isAccepted(orphanProof)); + BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0); + BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), 0); + BOOST_CHECK_EQUAL(m_processor->getConfidence(orphanProof), -1); + + BOOST_CHECK(!m_processor->addProofToReconcile(orphanProof)); + BOOST_CHECK(!m_processor->isAccepted(conflictingProof)); + BOOST_CHECK(m_processor->isAccepted(validProof)); + BOOST_CHECK(!m_processor->isAccepted(orphanProof)); + BOOST_CHECK_EQUAL(m_processor->getConfidence(conflictingProof), 0); + BOOST_CHECK_EQUAL(m_processor->getConfidence(validProof), 0); + BOOST_CHECK_EQUAL(m_processor->getConfidence(orphanProof), -1); + + gArgs.ClearForcedArg("-avalancheconflictingproofcooldown"); + gArgs.ClearForcedArg("-enableavalancheproofreplacement"); } BOOST_AUTO_TEST_CASE(quorum_detection) { // Set min quorum parameters for our test int minStake = 4'000'000; gArgs.ForceSetArg("-avaminquorumstake", ToString(minStake)); gArgs.ForceSetArg("-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); const CKey key = CKey::MakeCompressedKey(); auto localProof = buildRandomProof(minScore / 4, 100, key); gArgs.ForceSetArg("-avamasterkey", EncodeSecret(key)); gArgs.ForceSetArg("-avaproof", localProof->ToHex()); bilingual_str error; std::unique_ptr processor = Processor::MakeProcessor( *m_node.args, *m_node.chain, m_node.connman.get(), *m_node.scheduler, error); BOOST_CHECK(processor != nullptr); BOOST_CHECK(processor->getLocalProof() != nullptr); BOOST_CHECK_EQUAL(processor->getLocalProof()->getId(), localProof->getId()); BOOST_CHECK_EQUAL(AvalancheTest::getMinQuorumScore(*processor), minScore); BOOST_CHECK_EQUAL( AvalancheTest::getMinQuorumConnectedScoreRatio(*processor), 0.5); // The local proof has not been validated yet processor->withPeerManager([&](avalanche::PeerManager &pm) { BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 0); BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0); }); BOOST_CHECK(!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. processor->withPeerManager([&](avalanche::PeerManager &pm) { BOOST_CHECK(pm.registerProof(processor->getLocalProof())); BOOST_CHECK(pm.isBoundToPeer(processor->getLocalProof()->getId())); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore / 4); BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0); }); BOOST_CHECK(!processor->isQuorumEstablished()); // Add part of the required stake and make sure we still report no quorum auto proof1 = buildRandomProof(minScore / 2); processor->withPeerManager([&](avalanche::PeerManager &pm) { BOOST_CHECK(pm.registerProof(proof1)); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 3 * minScore / 4); BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0); }); BOOST_CHECK(!processor->isQuorumEstablished()); // Add the rest of the stake, but we are still lacking connected stake auto proof2 = buildRandomProof(minScore / 4); processor->withPeerManager([&](avalanche::PeerManager &pm) { BOOST_CHECK(pm.registerProof(proof2)); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore); BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0); }); BOOST_CHECK(!processor->isQuorumEstablished()); // Adding a node should cause the quorum to be detected and locked-in processor->withPeerManager([&](avalanche::PeerManager &pm) { pm.addNode(0, proof2->getId()); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore); // The peer manager knows that proof2 has a node attached ... BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), minScore / 4); }); // ... but the processor also account for the local proof, so we reached 50% BOOST_CHECK(processor->isQuorumEstablished()); // Go back to not having enough connected nodes, but we've already latched // the quorum as established processor->withPeerManager([&](avalanche::PeerManager &pm) { pm.removeNode(0); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore); BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0); }); BOOST_CHECK(processor->isQuorumEstablished()); // Remove peers one at a time by orphaning their proofs, and ensure the // quorum stays established auto orphanProof = [&processor](ProofRef proof) { { LOCK(cs_main); CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); coins.SpendCoin(proof->getStakes()[0].getStake().getUTXO()); } processor->withPeerManager([&proof](avalanche::PeerManager &pm) { pm.updatedBlockTip(); BOOST_CHECK(pm.isOrphan(proof->getId())); BOOST_CHECK(!pm.isBoundToPeer(proof->getId())); }); }; orphanProof(proof2); processor->withPeerManager([&](avalanche::PeerManager &pm) { BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 3 * minScore / 4); BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0); }); BOOST_CHECK(processor->isQuorumEstablished()); orphanProof(proof1); processor->withPeerManager([&](avalanche::PeerManager &pm) { BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), minScore / 4); BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0); }); BOOST_CHECK(processor->isQuorumEstablished()); orphanProof(processor->getLocalProof()); processor->withPeerManager([&](avalanche::PeerManager &pm) { BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 0); BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), 0); }); BOOST_CHECK(processor->isQuorumEstablished()); gArgs.ClearForcedArg("-avamasterkey"); gArgs.ClearForcedArg("-avaproof"); gArgs.ClearForcedArg("-avaminquorumstake"); gArgs.ClearForcedArg("-avaminquorumconnectedstakeratio"); } BOOST_AUTO_TEST_CASE(quorum_detection_parameter_validation) { // Create vector of tuples of: // std::vector> tests = { // 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 (auto it = tests.begin(); it != tests.end(); ++it) { gArgs.ForceSetArg("-avaminquorumstake", std::get<0>(*it)); gArgs.ForceSetArg("-avaminquorumconnectedstakeratio", std::get<1>(*it)); gArgs.ForceSetArg("-avaminavaproofsnodecount", std::get<2>(*it)); bilingual_str error; std::unique_ptr processor = Processor::MakeProcessor( *m_node.args, *m_node.chain, m_node.connman.get(), *m_node.scheduler, error); if (std::get<3>(*it)) { 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 != ""); } } gArgs.ClearForcedArg("-avaminquorumstake"); gArgs.ClearForcedArg("-avaminquorumconnectedstakeratio"); gArgs.ClearForcedArg("-avaminavaproofsnodecount"); } BOOST_AUTO_TEST_CASE(min_avaproofs_messages) { ArgsManager argsman; argsman.ForceSetArg("-avaminquorumstake", "0"); argsman.ForceSetArg("-avaminquorumconnectedstakeratio", "0"); auto checkMinAvaproofsMessages = [&](int64_t minAvaproofsMessages) { argsman.ForceSetArg("-avaminavaproofsnodecount", ToString(minAvaproofsMessages)); bilingual_str error; auto processor = Processor::MakeProcessor(argsman, *m_node.chain, m_node.connman.get(), *m_node.scheduler, error); BOOST_CHECK_EQUAL(processor->isQuorumEstablished(), minAvaproofsMessages <= 0); auto addNode = [&](NodeId nodeid) { auto proof = buildRandomProof(MIN_VALID_PROOF_SCORE); processor->withPeerManager([&](avalanche::PeerManager &pm) { BOOST_CHECK(pm.registerProof(proof)); BOOST_CHECK(pm.addNode(nodeid, proof->getId())); }); }; 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 gArgs.ForceSetArg("-avastalevotethreshold", ToString(AVALANCHE_VOTE_STALE_MIN_THRESHOLD)); gArgs.ForceSetArg("-avastalevotefactor", "2"); std::vector> 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(), *m_node.scheduler, error); BOOST_CHECK(m_processor != nullptr); BOOST_CHECK(error.empty()); P provider(this); auto &updates = provider.updates; 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; for (auto &testCase : testCases) { // Add a new item. Check it is added to the polls. BOOST_CHECK(provider.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(provider.registerVotes(nodeid, resp)); }; // Add some confidence for (int i = 0; i < std::get<0>(testCase); 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 < std::get<1>(testCase); 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(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); } gArgs.ClearForcedArg("-avastalevotethreshold"); gArgs.ClearForcedArg("-avastalevotefactor"); } BOOST_AUTO_TEST_SUITE_END()