diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp index 7236bab20..fb9927bf2 100644 --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -1,766 +1,777 @@ // Copyright (c) 2020 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include <avalanche/peermanager.h> #include <avalanche/avalanche.h> #include <avalanche/delegation.h> #include <avalanche/validation.h> #include <random.h> #include <validation.h> // For ChainstateActive() #include <algorithm> #include <cassert> namespace avalanche { bool PeerManager::addNode(NodeId nodeid, const ProofId &proofid) { auto &pview = peers.get<by_proofid>(); auto it = pview.find(proofid); if (it == pview.end()) { // If the node exists, it is actually updating its proof to an unknown // one. In this case we need to remove it so it is not both active and // pending at the same time. removeNode(nodeid); pendingNodes.emplace(proofid, nodeid); return false; } return addOrUpdateNode(peers.project<0>(it), nodeid); } bool PeerManager::addOrUpdateNode(const PeerSet::iterator &it, NodeId nodeid) { assert(it != peers.end()); const PeerId peerid = it->peerid; auto nit = nodes.find(nodeid); if (nit == nodes.end()) { if (!nodes.emplace(nodeid, peerid).second) { return false; } } else { const PeerId oldpeerid = nit->peerid; if (!nodes.modify(nit, [&](Node &n) { n.peerid = peerid; })) { return false; } // We actually have this node already, we need to update it. bool success = removeNodeFromPeer(peers.find(oldpeerid)); assert(success); } bool success = addNodeToPeer(it); assert(success); // If the added node was in the pending set, remove it pendingNodes.get<by_nodeid>().erase(nodeid); return true; } bool PeerManager::addNodeToPeer(const PeerSet::iterator &it) { assert(it != peers.end()); return peers.modify(it, [&](Peer &p) { if (p.node_count++ > 0) { // We are done. return; } // We need to allocate this peer. p.index = uint32_t(slots.size()); const uint32_t score = p.getScore(); const uint64_t start = slotCount; slots.emplace_back(start, score, it->peerid); slotCount = start + score; // Add to our allocated score when we allocate a new peer in the slots connectedPeersScore += score; }); } bool PeerManager::removeNode(NodeId nodeid) { if (pendingNodes.get<by_nodeid>().erase(nodeid) > 0) { // If this was a pending node, there is nothing else to do. return true; } auto it = nodes.find(nodeid); if (it == nodes.end()) { return false; } const PeerId peerid = it->peerid; nodes.erase(it); // Keep the track of the reference count. bool success = removeNodeFromPeer(peers.find(peerid)); assert(success); return true; } bool PeerManager::removeNodeFromPeer(const PeerSet::iterator &it, uint32_t count) { // It is possible for nodes to be dangling. If there was an inflight query // when the peer gets removed, the node was not erased. In this case there // is nothing to do. if (it == peers.end()) { return true; } assert(count <= it->node_count); if (count == 0) { // This is a NOOP. return false; } const uint32_t new_count = it->node_count - count; if (!peers.modify(it, [&](Peer &p) { p.node_count = new_count; })) { return false; } if (new_count > 0) { // We are done. return true; } // There are no more nodes left, we need to clean up. Subtract allocated // score and remove from slots. const size_t i = it->index; assert(i < slots.size()); assert(connectedPeersScore >= slots[i].getScore()); connectedPeersScore -= slots[i].getScore(); if (i + 1 == slots.size()) { slots.pop_back(); slotCount = slots.empty() ? 0 : slots.back().getStop(); } else { fragmentation += slots[i].getScore(); slots[i] = slots[i].withPeerId(NO_PEER); } return true; } bool PeerManager::updateNextRequestTime(NodeId nodeid, TimePoint timeout) { auto it = nodes.find(nodeid); if (it == nodes.end()) { return false; } return nodes.modify(it, [&](Node &n) { n.nextRequestTime = timeout; }); } static bool isOrphanState(const ProofValidationState &state) { return state.GetResult() == ProofValidationResult::MISSING_UTXO || state.GetResult() == ProofValidationResult::HEIGHT_MISMATCH; } bool PeerManager::updateNextPossibleConflictTime( PeerId peerid, const std::chrono::seconds &nextTime) { auto it = peers.find(peerid); if (it == peers.end()) { // No such peer return false; } // Make sure we don't move the time in the past. peers.modify(it, [&](Peer &p) { p.nextPossibleConflictTime = std::max(p.nextPossibleConflictTime, nextTime); }); return it->nextPossibleConflictTime == nextTime; } template <typename ProofContainer> void PeerManager::moveToConflictingPool(const ProofContainer &proofs) { auto &peersView = peers.get<by_proofid>(); for (const ProofRef &proof : proofs) { auto it = peersView.find(proof->getId()); if (it != peersView.end()) { removePeer(it->peerid); } conflictingProofPool.addProofIfPreferred(proof); } } bool PeerManager::registerProof(const ProofRef &proof, ProofRegistrationState ®istrationState, RegistrationMode mode) { assert(proof); const ProofId &proofid = proof->getId(); auto invalidate = [&](ProofRegistrationResult result, const std::string &message) { return registrationState.Invalid( result, message, strprintf("proofid: %s", proofid.ToString())); }; if ((mode != RegistrationMode::FORCE_ACCEPT || !isInConflictingPool(proofid)) && exists(proofid)) { // In default mode, we expect the proof to be unknown, i.e. in none of // the pools. // In forced accept mode, the proof can be in the conflicting pool. return invalidate(ProofRegistrationResult::ALREADY_REGISTERED, "proof-already-registered"); } // Check the proof's validity. ProofValidationState validationState; if (!WITH_LOCK(cs_main, return proof->verify(validationState, ::ChainstateActive().CoinsTip()))) { if (isOrphanState(validationState)) { - orphanProofPool.addProofIfPreferred(proof); + // Only accept orphan proofs if there's room in the orphan pool. + auto status = orphanProofPool.addProofIfNoConflict(proof); + if (status != ProofPool::AddProofStatus::SUCCEED) { + // Attempt proof replacement + orphanProofPool.addProofIfPreferred(proof); + } else if (orphanProofPool.countProofs() > + AVALANCHE_MAX_ORPHAN_PROOFS) { + // Adding this proof exceeds the orphan pool limit, so remove + // it. + orphanProofPool.removeProof(proof->getId()); + } + return invalidate(ProofRegistrationResult::ORPHAN, "orphan-proof"); } // Reject invalid proof. return invalidate(ProofRegistrationResult::INVALID, "invalid-proof"); } auto now = GetTime<std::chrono::seconds>(); auto nextCooldownTimePoint = now + std::chrono::seconds( gArgs.GetArg("-avalancheconflictingproofcooldown", AVALANCHE_DEFAULT_CONFLICTING_PROOF_COOLDOWN)); ProofPool::ConflictingProofSet conflictingProofs; switch (validProofPool.addProofIfNoConflict(proof, conflictingProofs)) { case ProofPool::AddProofStatus::REJECTED: { if (mode != RegistrationMode::FORCE_ACCEPT) { auto bestPossibleConflictTime = std::chrono::seconds(); auto &pview = peers.get<by_proofid>(); for (auto &conflictingProof : conflictingProofs) { auto it = pview.find(conflictingProof->getId()); assert(it != pview.end()); // Search the most recent time over the peers bestPossibleConflictTime = std::max( bestPossibleConflictTime, it->nextPossibleConflictTime); updateNextPossibleConflictTime(it->peerid, nextCooldownTimePoint); } if (bestPossibleConflictTime > now) { // Cooldown not elapsed, reject the proof. return invalidate( ProofRegistrationResult::COOLDOWN_NOT_ELAPSED, "cooldown-not-elapsed"); } // If proof replacement is enabled, give the proof a chance to // replace the conflicting ones. if (gArgs.GetBoolArg( "-enableavalancheproofreplacement", AVALANCHE_DEFAULT_PROOF_REPLACEMENT_ENABLED)) { if (validProofPool.addProofIfPreferred(proof)) { // If we have overridden other proofs due to conflict, // remove the peers and attempt to move them to the // conflicting pool. moveToConflictingPool(conflictingProofs); // Replacement is successful, continue to peer creation break; } } // Not the preferred proof, or replacement is not enabled return conflictingProofPool.addProofIfPreferred(proof) == ProofPool::AddProofStatus::REJECTED ? invalidate(ProofRegistrationResult::REJECTED, "rejected-proof") : invalidate(ProofRegistrationResult::CONFLICTING, "conflicting-utxos"); } conflictingProofPool.removeProof(proofid); // Move the conflicting proofs from the valid pool to the // conflicting pool moveToConflictingPool(conflictingProofs); auto status = validProofPool.addProofIfNoConflict(proof); assert(status == ProofPool::AddProofStatus::SUCCEED); break; } case ProofPool::AddProofStatus::DUPLICATED: // If the proof was already in the pool, don't duplicate the peer. return invalidate(ProofRegistrationResult::ALREADY_REGISTERED, "proof-already-registered"); case ProofPool::AddProofStatus::SUCCEED: break; // No default case, so the compiler can warn about missing cases } // At this stage we are going to create a peer so the proof should never // exist in the conflicting pool, but use belt and suspenders. conflictingProofPool.removeProof(proofid); // New peer means new peerid! const PeerId peerid = nextPeerId++; // We have no peer for this proof, time to create it. auto inserted = peers.emplace(peerid, proof, nextCooldownTimePoint); assert(inserted.second); auto insertedRadixTree = shareableProofs.insert(proof); assert(insertedRadixTree); // Add to our registered score when adding to the peer list totalPeersScore += proof->getScore(); // If there are nodes waiting for this proof, add them auto &pendingNodesView = pendingNodes.get<by_proofid>(); auto range = pendingNodesView.equal_range(proofid); // We want to update the nodes then remove them from the pending set. That // will invalidate the range iterators, so we need to save the node ids // first before we can loop over them. std::vector<NodeId> nodeids; nodeids.reserve(std::distance(range.first, range.second)); std::transform(range.first, range.second, std::back_inserter(nodeids), [](const PendingNode &n) { return n.nodeid; }); for (const NodeId &nodeid : nodeids) { addOrUpdateNode(inserted.first, nodeid); } return true; } bool PeerManager::rejectProof(const ProofId &proofid, RejectionMode mode) { if (!exists(proofid)) { return false; } if (orphanProofPool.removeProof(proofid)) { return true; } if (mode == RejectionMode::DEFAULT && conflictingProofPool.getProof(proofid)) { // In default mode we keep the proof in the conflicting pool return true; } if (mode == RejectionMode::INVALIDATE && conflictingProofPool.removeProof(proofid)) { // In invalidate mode we remove the proof completely return true; } auto &pview = peers.get<by_proofid>(); auto it = pview.find(proofid); assert(it != pview.end()); const ProofRef proof = it->proof; if (!removePeer(it->peerid)) { return false; } // If there was conflicting proofs, attempt to pull them back for (const SignedStake &ss : proof->getStakes()) { const ProofRef conflictingProof = conflictingProofPool.getProof(ss.getStake().getUTXO()); if (!conflictingProof) { continue; } conflictingProofPool.removeProof(conflictingProof->getId()); registerProof(conflictingProof); } if (mode == RejectionMode::DEFAULT) { conflictingProofPool.addProofIfPreferred(proof); } return true; } NodeId PeerManager::selectNode() { for (int retry = 0; retry < SELECT_NODE_MAX_RETRY; retry++) { const PeerId p = selectPeer(); // If we cannot find a peer, it may be due to the fact that it is // unlikely due to high fragmentation, so compact and retry. if (p == NO_PEER) { compact(); continue; } // See if that peer has an available node. auto &nview = nodes.get<next_request_time>(); auto it = nview.lower_bound(boost::make_tuple(p, TimePoint())); if (it != nview.end() && it->peerid == p && it->nextRequestTime <= std::chrono::steady_clock::now()) { return it->nodeid; } } // We failed to find a node to query, flag this so we can request more needMoreNodes = true; return NO_NODE; } void PeerManager::updatedBlockTip() { std::vector<ProofId> invalidProofIds; std::vector<ProofRef> newOrphans; { LOCK(cs_main); const CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); for (const auto &p : peers) { ProofValidationState state; if (!p.proof->verify(state, coins)) { if (isOrphanState(state)) { newOrphans.push_back(p.proof); } invalidProofIds.push_back(p.getProofId()); } } } // Remove the invalid proofs before the orphans rescan. This makes it // possible to pull back proofs with utxos that conflicted with these // invalid proofs. for (const ProofId &invalidProofId : invalidProofIds) { rejectProof(invalidProofId, RejectionMode::INVALIDATE); } orphanProofPool.rescan(*this); for (auto &p : newOrphans) { orphanProofPool.addProofIfPreferred(p); } } ProofRef PeerManager::getProof(const ProofId &proofid) const { ProofRef proof; forPeer(proofid, [&](const Peer &p) { proof = p.proof; return true; }); if (!proof) { proof = conflictingProofPool.getProof(proofid); } if (!proof) { proof = orphanProofPool.getProof(proofid); } return proof; } bool PeerManager::isBoundToPeer(const ProofId &proofid) const { auto &pview = peers.get<by_proofid>(); return pview.find(proofid) != pview.end(); } bool PeerManager::isOrphan(const ProofId &proofid) const { return orphanProofPool.getProof(proofid) != nullptr; } bool PeerManager::isInConflictingPool(const ProofId &proofid) const { return conflictingProofPool.getProof(proofid) != nullptr; } bool PeerManager::removePeer(const PeerId peerid) { auto it = peers.find(peerid); if (it == peers.end()) { return false; } // Remove all nodes from this peer. removeNodeFromPeer(it, it->node_count); auto &nview = nodes.get<next_request_time>(); // Add the nodes to the pending set auto range = nview.equal_range(peerid); for (auto &nit = range.first; nit != range.second; ++nit) { pendingNodes.emplace(it->getProofId(), nit->nodeid); }; // Remove nodes associated with this peer, unless their timeout is still // active. This ensure that we don't overquery them in case they are // subsequently added to another peer. nview.erase(nview.lower_bound(boost::make_tuple(peerid, TimePoint())), nview.upper_bound(boost::make_tuple( peerid, std::chrono::steady_clock::now()))); // Release UTXOs attached to this proof. validProofPool.removeProof(it->getProofId()); auto removed = shareableProofs.remove(Uint256RadixKey(it->getProofId())); assert(removed != nullptr); m_unbroadcast_proofids.erase(it->getProofId()); // Remove the peer from the PeerSet and remove its score from the registered // score total. assert(totalPeersScore >= it->getScore()); totalPeersScore -= it->getScore(); peers.erase(it); return true; } PeerId PeerManager::selectPeer() const { if (slots.empty() || slotCount == 0) { return NO_PEER; } const uint64_t max = slotCount; for (int retry = 0; retry < SELECT_PEER_MAX_RETRY; retry++) { size_t i = selectPeerImpl(slots, GetRand(max), max); if (i != NO_PEER) { return i; } } return NO_PEER; } uint64_t PeerManager::compact() { // There is nothing to compact. if (fragmentation == 0) { return 0; } std::vector<Slot> newslots; newslots.reserve(peers.size()); uint64_t prevStop = 0; uint32_t i = 0; for (auto it = peers.begin(); it != peers.end(); it++) { if (it->node_count == 0) { continue; } newslots.emplace_back(prevStop, it->getScore(), it->peerid); prevStop = slots[i].getStop(); if (!peers.modify(it, [&](Peer &p) { p.index = i++; })) { return 0; } } slots = std::move(newslots); const uint64_t saved = slotCount - prevStop; slotCount = prevStop; fragmentation = 0; return saved; } bool PeerManager::verify() const { uint64_t prevStop = 0; uint32_t scoreFromSlots = 0; for (size_t i = 0; i < slots.size(); i++) { const Slot &s = slots[i]; // Slots must be in correct order. if (s.getStart() < prevStop) { return false; } prevStop = s.getStop(); // If this is a dead slot, then nothing more needs to be checked. if (s.getPeerId() == NO_PEER) { continue; } // We have a live slot, verify index. auto it = peers.find(s.getPeerId()); if (it == peers.end() || it->index != i) { return false; } // Accumulate score across slots scoreFromSlots += slots[i].getScore(); } // Score across slots must be the same as our allocated score if (scoreFromSlots != connectedPeersScore) { return false; } uint32_t scoreFromAllPeers = 0; uint32_t scoreFromPeersWithNodes = 0; std::unordered_set<COutPoint, SaltedOutpointHasher> peersUtxos; for (const auto &p : peers) { // Accumulate the score across peers to compare with total known score scoreFromAllPeers += p.getScore(); // A peer should have a proof attached if (!p.proof) { return false; } // Check proof pool consistency for (const auto &ss : p.proof->getStakes()) { const COutPoint &outpoint = ss.getStake().getUTXO(); auto proof = validProofPool.getProof(outpoint); if (!proof) { // Missing utxo return false; } if (proof != p.proof) { // Wrong proof return false; } if (!peersUtxos.emplace(outpoint).second) { // Duplicated utxo return false; } } // Count node attached to this peer. const auto count_nodes = [&]() { size_t count = 0; auto &nview = nodes.get<next_request_time>(); auto begin = nview.lower_bound(boost::make_tuple(p.peerid, TimePoint())); auto end = nview.upper_bound(boost::make_tuple(p.peerid + 1, TimePoint())); for (auto it = begin; it != end; ++it) { count++; } return count; }; if (p.node_count != count_nodes()) { return false; } // If there are no nodes attached to this peer, then we are done. if (p.node_count == 0) { continue; } scoreFromPeersWithNodes += p.getScore(); // The index must point to a slot refering to this peer. if (p.index >= slots.size() || slots[p.index].getPeerId() != p.peerid) { return false; } // If the score do not match, same thing. if (slots[p.index].getScore() != p.getScore()) { return false; } // Check the proof is in the radix tree if (shareableProofs.get(p.getProofId()) == nullptr) { return false; } } // Check our accumulated scores against our registred and allocated scores if (scoreFromAllPeers != totalPeersScore) { return false; } if (scoreFromPeersWithNodes != connectedPeersScore) { return false; } // We checked the utxo consistency for all our peers utxos already, so if // the pool size differs from the expected one there are dangling utxos. if (validProofPool.size() != peersUtxos.size()) { return false; } // Check there is no dangling proof in the radix tree return shareableProofs.forEachLeaf([&](RCUPtr<const Proof> pLeaf) { return isBoundToPeer(pLeaf->getId()); }); } PeerId selectPeerImpl(const std::vector<Slot> &slots, const uint64_t slot, const uint64_t max) { assert(slot <= max); size_t begin = 0, end = slots.size(); uint64_t bottom = 0, top = max; // Try to find the slot using dichotomic search. while ((end - begin) > 8) { // The slot we picked in not allocated. if (slot < bottom || slot >= top) { return NO_PEER; } // Guesstimate the position of the slot. size_t i = begin + ((slot - bottom) * (end - begin) / (top - bottom)); assert(begin <= i && i < end); // We have a match. if (slots[i].contains(slot)) { return slots[i].getPeerId(); } // We undershooted. if (slots[i].precedes(slot)) { begin = i + 1; if (begin >= end) { return NO_PEER; } bottom = slots[begin].getStart(); continue; } // We overshooted. if (slots[i].follows(slot)) { end = i; top = slots[end].getStart(); continue; } // We have an unalocated slot. return NO_PEER; } // Enough of that nonsense, let fallback to linear search. for (size_t i = begin; i < end; i++) { // We have a match. if (slots[i].contains(slot)) { return slots[i].getPeerId(); } } // We failed to find a slot, retry. return NO_PEER; } void PeerManager::addUnbroadcastProof(const ProofId &proofid) { // The proof should be bound to a peer if (isBoundToPeer(proofid)) { m_unbroadcast_proofids.insert(proofid); } } void PeerManager::removeUnbroadcastProof(const ProofId &proofid) { m_unbroadcast_proofids.erase(proofid); } } // namespace avalanche diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h index 550c8c24b..2e6cd6f87 100644 --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -1,387 +1,396 @@ // Copyright (c) 2020 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_AVALANCHE_PEERMANAGER_H #define BITCOIN_AVALANCHE_PEERMANAGER_H #include <avalanche/node.h> #include <avalanche/proof.h> #include <avalanche/proofpool.h> #include <avalanche/proofradixtreeadapter.h> #include <coins.h> #include <consensus/validation.h> #include <pubkey.h> #include <radix.h> #include <salteduint256hasher.h> #include <util/time.h> #include <boost/multi_index/composite_key.hpp> #include <boost/multi_index/hashed_index.hpp> #include <boost/multi_index/mem_fun.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_set> #include <vector> namespace avalanche { +/** + * Maximum number of orphan proofs the peer manager will accept from the + * network. Under good conditions, this allows the node to collect relevant + * proofs during IBD. Note that reorgs can cause the orphan pool to + * temporarily exceed this limit. But a change in chaintip cause previously + * reorged proofs to be trimmed. + */ +static constexpr uint32_t AVALANCHE_MAX_ORPHAN_PROOFS = 4000; + class Delegation; namespace { struct TestPeerManager; } struct Slot { private: uint64_t start; uint32_t score; PeerId peerid; public: Slot(uint64_t startIn, uint32_t scoreIn, PeerId peeridIn) : start(startIn), score(scoreIn), peerid(peeridIn) {} Slot withStart(uint64_t startIn) const { return Slot(startIn, score, peerid); } Slot withScore(uint64_t scoreIn) const { return Slot(start, scoreIn, peerid); } Slot withPeerId(PeerId peeridIn) const { return Slot(start, score, peeridIn); } uint64_t getStart() const { return start; } uint64_t getStop() const { return start + score; } uint32_t getScore() const { return score; } PeerId getPeerId() const { return peerid; } bool contains(uint64_t slot) const { return getStart() <= slot && slot < getStop(); } bool precedes(uint64_t slot) const { return slot >= getStop(); } bool follows(uint64_t slot) const { return getStart() > slot; } }; struct Peer { PeerId peerid; uint32_t index = -1; uint32_t node_count = 0; ProofRef proof; // The network stack uses timestamp in seconds, so we oblige. std::chrono::seconds registration_time; std::chrono::seconds nextPossibleConflictTime; Peer(PeerId peerid_, ProofRef proof_, std::chrono::seconds nextPossibleConflictTime_) : peerid(peerid_), proof(std::move(proof_)), registration_time(GetTime<std::chrono::seconds>()), nextPossibleConflictTime(std::move(nextPossibleConflictTime_)) {} const ProofId &getProofId() const { return proof->getId(); } uint32_t getScore() const { return proof->getScore(); } }; struct proof_index { using result_type = ProofId; result_type operator()(const Peer &p) const { return p.proof->getId(); } }; struct score_index { using result_type = uint32_t; result_type operator()(const Peer &p) const { return p.getScore(); } }; struct next_request_time {}; struct PendingNode { ProofId proofid; NodeId nodeid; PendingNode(ProofId proofid_, NodeId nodeid_) : proofid(proofid_), nodeid(nodeid_){}; }; struct by_proofid; struct by_nodeid; struct by_score; enum class ProofRegistrationResult { NONE = 0, ALREADY_REGISTERED, ORPHAN, INVALID, CONFLICTING, REJECTED, COOLDOWN_NOT_ELAPSED, }; class ProofRegistrationState : public ValidationState<ProofRegistrationResult> { }; namespace bmi = boost::multi_index; class PeerManager { std::vector<Slot> slots; uint64_t slotCount = 0; uint64_t fragmentation = 0; /** * Several nodes can make an avalanche peer. In this case, all nodes are * considered interchangeable parts of the same peer. */ using PeerSet = boost::multi_index_container< Peer, bmi::indexed_by< // index by peerid bmi::hashed_unique<bmi::member<Peer, PeerId, &Peer::peerid>>, // index by proof bmi::hashed_unique<bmi::tag<by_proofid>, proof_index, SaltedProofIdHasher>, // ordered by score, decreasing order bmi::ordered_non_unique<bmi::tag<by_score>, score_index, std::greater<uint32_t>>>>; PeerId nextPeerId = 0; PeerSet peers; ProofPool validProofPool; ProofPool conflictingProofPool; ProofPool orphanProofPool; using ProofRadixTree = RadixTree<const Proof, ProofRadixTreeAdapter>; ProofRadixTree shareableProofs; using NodeSet = boost::multi_index_container< Node, bmi::indexed_by< // index by nodeid bmi::hashed_unique<bmi::member<Node, NodeId, &Node::nodeid>>, // sorted by peerid/nextRequestTime bmi::ordered_non_unique< bmi::tag<next_request_time>, bmi::composite_key< Node, bmi::member<Node, PeerId, &Node::peerid>, bmi::member<Node, TimePoint, &Node::nextRequestTime>>>>>; NodeSet nodes; /** * Flag indicating that we failed to select a node and need to expand our * node set. */ std::atomic<bool> needMoreNodes{false}; using PendingNodeSet = boost::multi_index_container< PendingNode, bmi::indexed_by< // index by proofid bmi::hashed_non_unique< bmi::tag<by_proofid>, bmi::member<PendingNode, ProofId, &PendingNode::proofid>, SaltedProofIdHasher>, // index by nodeid bmi::hashed_unique< bmi::tag<by_nodeid>, bmi::member<PendingNode, NodeId, &PendingNode::nodeid>>>>; PendingNodeSet pendingNodes; static constexpr int SELECT_PEER_MAX_RETRY = 3; static constexpr int SELECT_NODE_MAX_RETRY = 3; /** * Track proof ids to broadcast */ std::unordered_set<ProofId, SaltedProofIdHasher> m_unbroadcast_proofids; /** * Quorum management. */ uint32_t totalPeersScore = 0; uint32_t connectedPeersScore = 0; public: /** * Node API. */ bool addNode(NodeId nodeid, const ProofId &proofid); bool removeNode(NodeId nodeid); size_t getNodeCount() const { return nodes.size(); } size_t getPendingNodeCount() const { return pendingNodes.size(); } // Update when a node is to be polled next. bool updateNextRequestTime(NodeId nodeid, TimePoint timeout); // Randomly select a node to poll. NodeId selectNode(); /** * Returns true if we encountered a lack of node since the last call. */ bool shouldRequestMoreNodes() { return needMoreNodes.exchange(false); } template <typename Callable> bool forNode(NodeId nodeid, Callable &&func) const { auto it = nodes.find(nodeid); return it != nodes.end() && func(*it); } template <typename Callable> void forEachNode(const Peer &peer, Callable &&func) const { auto &nview = nodes.get<next_request_time>(); auto range = nview.equal_range(peer.peerid); for (auto it = range.first; it != range.second; ++it) { func(*it); } } /** * Proof and Peer related API. */ /** * Update the time before which a proof is not allowed to have conflicting * UTXO with this peer's proof. */ bool updateNextPossibleConflictTime(PeerId peerid, const std::chrono::seconds &nextTime); /** * Registration mode * - DEFAULT: Default policy, register only if the proof is unknown and has * no conflict. * - FORCE_ACCEPT: Turn a valid proof into a peer even if it has conflicts * and is not the best candidate. */ enum class RegistrationMode { DEFAULT, FORCE_ACCEPT, }; bool registerProof(const ProofRef &proof, ProofRegistrationState ®istrationState, RegistrationMode mode = RegistrationMode::DEFAULT); bool registerProof(const ProofRef &proof, RegistrationMode mode = RegistrationMode::DEFAULT) { ProofRegistrationState dummy; return registerProof(proof, dummy, mode); } /** * Rejection mode * - DEFAULT: Default policy, reject a proof and attempt to keep it in the * conflicting pool if possible. * - INVALIDATE: Reject a proof by removing it from any of the pool. * * In any case if a peer is rejected, it attempts to pull the conflicting * proofs back. */ enum class RejectionMode { DEFAULT, INVALIDATE, }; bool rejectProof(const ProofId &proofid, RejectionMode mode = RejectionMode::DEFAULT); bool exists(const ProofId &proofid) const { return getProof(proofid) != nullptr; } template <typename Callable> bool forPeer(const ProofId &proofid, Callable &&func) const { auto &pview = peers.get<by_proofid>(); auto it = pview.find(proofid); return it != pview.end() && func(*it); } template <typename Callable> void forEachPeer(Callable &&func) const { for (const auto &p : peers) { func(p); } } /** * Update the peer set when a new block is connected. */ void updatedBlockTip(); /** * Proof broadcast API. */ void addUnbroadcastProof(const ProofId &proofid); void removeUnbroadcastProof(const ProofId &proofid); auto getUnbroadcastProofs() const { return m_unbroadcast_proofids; } /* * Quorum management */ uint32_t getTotalPeersScore() const { return totalPeersScore; } uint32_t getConnectedPeersScore() const { return connectedPeersScore; } /**************************************************** * Functions which are public for testing purposes. * ****************************************************/ /** * Remove an existing peer. */ bool removePeer(const PeerId peerid); /** * Randomly select a peer to poll. */ PeerId selectPeer() const; /** * Trigger maintenance of internal data structures. * Returns how much slot space was saved after compaction. */ uint64_t compact(); /** * Perform consistency check on internal data structures. */ bool verify() const; // Accessors. uint64_t getSlotCount() const { return slotCount; } uint64_t getFragmentation() const { return fragmentation; } ProofRef getProof(const ProofId &proofid) const; bool isBoundToPeer(const ProofId &proofid) const; bool isOrphan(const ProofId &proofid) const; bool isInConflictingPool(const ProofId &proofid) const; const ProofRadixTree &getShareableProofsSnapshot() const { return shareableProofs; } private: template <typename ProofContainer> void moveToConflictingPool(const ProofContainer &proofs); bool addOrUpdateNode(const PeerSet::iterator &it, NodeId nodeid); bool addNodeToPeer(const PeerSet::iterator &it); bool removeNodeFromPeer(const PeerSet::iterator &it, uint32_t count = 1); friend struct ::avalanche::TestPeerManager; }; /** * Internal methods that are exposed for testing purposes. */ PeerId selectPeerImpl(const std::vector<Slot> &slots, const uint64_t slot, const uint64_t max); } // namespace avalanche #endif // BITCOIN_AVALANCHE_PEERMANAGER_H diff --git a/src/avalanche/test/peermanager_tests.cpp b/src/avalanche/test/peermanager_tests.cpp index b0e1389c2..6ebe07ecb 100644 --- a/src/avalanche/test/peermanager_tests.cpp +++ b/src/avalanche/test/peermanager_tests.cpp @@ -1,1744 +1,1818 @@ // Copyright (c) 2020 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include <avalanche/delegationbuilder.h> #include <avalanche/peermanager.h> #include <avalanche/proofbuilder.h> #include <avalanche/proofcomparator.h> #include <avalanche/test/util.h> #include <script/standard.h> #include <util/time.h> #include <util/translation.h> #include <validation.h> #include <test/util/setup_common.h> #include <boost/test/unit_test.hpp> using namespace avalanche; namespace avalanche { namespace { struct TestPeerManager { static bool nodeBelongToPeer(const PeerManager &pm, NodeId nodeid, PeerId peerid) { return pm.forNode(nodeid, [&](const Node &node) { return node.peerid == peerid; }); } static bool isNodePending(const PeerManager &pm, NodeId nodeid) { auto &pendingNodesView = pm.pendingNodes.get<by_nodeid>(); return pendingNodesView.find(nodeid) != pendingNodesView.end(); } static PeerId getPeerIdForProofId(PeerManager &pm, const ProofId &proofid) { auto &pview = pm.peers.get<by_proofid>(); auto it = pview.find(proofid); return it == pview.end() ? NO_PEER : it->peerid; } static PeerId registerAndGetPeerId(PeerManager &pm, const ProofRef &proof) { pm.registerProof(proof); return getPeerIdForProofId(pm, proof->getId()); } static std::vector<uint32_t> getOrderedScores(const PeerManager &pm) { std::vector<uint32_t> scores; auto &peerView = pm.peers.get<by_score>(); for (const Peer &peer : peerView) { scores.push_back(peer.getScore()); } return scores; } }; static void addCoin(const COutPoint &outpoint, const CKey &key, const Amount amount = 10 * COIN, uint32_t height = 100, bool is_coinbase = false) { CScript script = GetScriptForDestination(PKHash(key.GetPubKey())); LOCK(cs_main); CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); coins.AddCoin(outpoint, Coin(CTxOut(amount, script), height, is_coinbase), false); } static COutPoint createUtxo(const CKey &key, const Amount amount = 10 * COIN, uint32_t height = 100, bool is_coinbase = false) { COutPoint outpoint(TxId(GetRandHash()), 0); addCoin(outpoint, key, amount, height, is_coinbase); return outpoint; } static ProofRef buildProof(const CKey &key, const std::vector<std::tuple<COutPoint, Amount>> &outpoints, const CKey &master = CKey::MakeCompressedKey(), int64_t sequence = 1, uint32_t height = 100, bool is_coinbase = false, int64_t expirationTime = 0) { ProofBuilder pb(sequence, expirationTime, master); for (const auto &outpoint : outpoints) { BOOST_CHECK(pb.addUTXO(std::get<0>(outpoint), std::get<1>(outpoint), height, is_coinbase, key)); } return pb.build(); } template <typename... Args> static ProofRef buildProofWithOutpoints(const CKey &key, const std::vector<COutPoint> &outpoints, Amount amount, Args &&...args) { std::vector<std::tuple<COutPoint, Amount>> outpointsWithAmount; std::transform( outpoints.begin(), outpoints.end(), std::back_inserter(outpointsWithAmount), [amount](const auto &o) { return std::make_tuple(o, amount); }); return buildProof(key, outpointsWithAmount, std::forward<Args>(args)...); } static ProofRef buildProofWithSequence(const CKey &key, const std::vector<COutPoint> &outpoints, int64_t sequence) { return buildProofWithOutpoints(key, outpoints, 10 * COIN, key, sequence); } } // namespace } // namespace avalanche namespace { struct NoCoolDownFixture : public TestingSetup { NoCoolDownFixture() { gArgs.ForceSetArg("-avalancheconflictingproofcooldown", "0"); } ~NoCoolDownFixture() { gArgs.ClearForcedArg("-avalancheconflictingproofcooldown"); } }; } // namespace BOOST_FIXTURE_TEST_SUITE(peermanager_tests, TestingSetup) BOOST_AUTO_TEST_CASE(select_peer_linear) { // No peers. BOOST_CHECK_EQUAL(selectPeerImpl({}, 0, 0), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl({}, 1, 3), NO_PEER); // One peer const std::vector<Slot> oneslot = {{100, 100, 23}}; // Undershoot BOOST_CHECK_EQUAL(selectPeerImpl(oneslot, 0, 300), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(oneslot, 42, 300), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(oneslot, 99, 300), NO_PEER); // Nailed it BOOST_CHECK_EQUAL(selectPeerImpl(oneslot, 100, 300), 23); BOOST_CHECK_EQUAL(selectPeerImpl(oneslot, 142, 300), 23); BOOST_CHECK_EQUAL(selectPeerImpl(oneslot, 199, 300), 23); // Overshoot BOOST_CHECK_EQUAL(selectPeerImpl(oneslot, 200, 300), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(oneslot, 242, 300), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(oneslot, 299, 300), NO_PEER); // Two peers const std::vector<Slot> twoslots = {{100, 100, 69}, {300, 100, 42}}; // Undershoot BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 0, 500), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 42, 500), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 99, 500), NO_PEER); // First entry BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 100, 500), 69); BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 142, 500), 69); BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 199, 500), 69); // In between BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 200, 500), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 242, 500), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 299, 500), NO_PEER); // Second entry BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 300, 500), 42); BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 342, 500), 42); BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 399, 500), 42); // Overshoot BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 400, 500), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 442, 500), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(twoslots, 499, 500), NO_PEER); } BOOST_AUTO_TEST_CASE(select_peer_dichotomic) { std::vector<Slot> slots; // 100 peers of size 1 with 1 empty element apart. uint64_t max = 1; for (int i = 0; i < 100; i++) { slots.emplace_back(max, 1, i); max += 2; } BOOST_CHECK_EQUAL(selectPeerImpl(slots, 4, max), NO_PEER); // Check that we get what we expect. for (int i = 0; i < 100; i++) { BOOST_CHECK_EQUAL(selectPeerImpl(slots, 2 * i, max), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(slots, 2 * i + 1, max), i); } BOOST_CHECK_EQUAL(selectPeerImpl(slots, max, max), NO_PEER); // Update the slots to be heavily skewed toward the last element. slots[99] = slots[99].withScore(101); max = slots[99].getStop(); BOOST_CHECK_EQUAL(max, 300); for (int i = 0; i < 100; i++) { BOOST_CHECK_EQUAL(selectPeerImpl(slots, 2 * i, max), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(slots, 2 * i + 1, max), i); } BOOST_CHECK_EQUAL(selectPeerImpl(slots, 200, max), 99); BOOST_CHECK_EQUAL(selectPeerImpl(slots, 256, max), 99); BOOST_CHECK_EQUAL(selectPeerImpl(slots, 299, max), 99); BOOST_CHECK_EQUAL(selectPeerImpl(slots, 300, max), NO_PEER); // Update the slots to be heavily skewed toward the first element. for (int i = 0; i < 100; i++) { slots[i] = slots[i].withStart(slots[i].getStart() + 100); } slots[0] = Slot(1, slots[0].getStop() - 1, slots[0].getPeerId()); slots[99] = slots[99].withScore(1); max = slots[99].getStop(); BOOST_CHECK_EQUAL(max, 300); BOOST_CHECK_EQUAL(selectPeerImpl(slots, 0, max), NO_PEER); BOOST_CHECK_EQUAL(selectPeerImpl(slots, 1, max), 0); BOOST_CHECK_EQUAL(selectPeerImpl(slots, 42, max), 0); for (int i = 0; i < 100; i++) { BOOST_CHECK_EQUAL(selectPeerImpl(slots, 100 + 2 * i + 1, max), i); BOOST_CHECK_EQUAL(selectPeerImpl(slots, 100 + 2 * i + 2, max), NO_PEER); } } BOOST_AUTO_TEST_CASE(select_peer_random) { for (int c = 0; c < 1000; c++) { size_t size = InsecureRandBits(10) + 1; std::vector<Slot> slots; slots.reserve(size); uint64_t max = InsecureRandBits(3); auto next = [&]() { uint64_t r = max; max += InsecureRandBits(3); return r; }; for (size_t i = 0; i < size; i++) { const uint64_t start = next(); const uint32_t score = InsecureRandBits(3); max += score; slots.emplace_back(start, score, i); } for (int k = 0; k < 100; k++) { uint64_t s = max > 0 ? InsecureRandRange(max) : 0; auto i = selectPeerImpl(slots, s, max); // /!\ Because of the way we construct the vector, the peer id is // always the index. This might not be the case in practice. BOOST_CHECK(i == NO_PEER || slots[i].contains(s)); } } } static void addNodeWithScore(avalanche::PeerManager &pm, NodeId node, uint32_t score) { auto proof = buildRandomProof(score); BOOST_CHECK(pm.registerProof(proof)); BOOST_CHECK(pm.addNode(node, proof->getId())); }; BOOST_AUTO_TEST_CASE(peer_probabilities) { // No peers. avalanche::PeerManager pm; BOOST_CHECK_EQUAL(pm.selectNode(), NO_NODE); const NodeId node0 = 42, node1 = 69, node2 = 37; // One peer, we always return it. addNodeWithScore(pm, node0, MIN_VALID_PROOF_SCORE); BOOST_CHECK_EQUAL(pm.selectNode(), node0); // Two peers, verify ratio. addNodeWithScore(pm, node1, 2 * MIN_VALID_PROOF_SCORE); std::unordered_map<PeerId, int> results = {}; for (int i = 0; i < 10000; i++) { size_t n = pm.selectNode(); BOOST_CHECK(n == node0 || n == node1); results[n]++; } BOOST_CHECK(abs(2 * results[0] - results[1]) < 500); // Three peers, verify ratio. addNodeWithScore(pm, node2, MIN_VALID_PROOF_SCORE); results.clear(); for (int i = 0; i < 10000; i++) { size_t n = pm.selectNode(); BOOST_CHECK(n == node0 || n == node1 || n == node2); results[n]++; } BOOST_CHECK(abs(results[0] - results[1] + results[2]) < 500); } BOOST_AUTO_TEST_CASE(remove_peer) { // No peers. avalanche::PeerManager pm; BOOST_CHECK_EQUAL(pm.selectPeer(), NO_PEER); // Add 4 peers. std::array<PeerId, 8> peerids; for (int i = 0; i < 4; i++) { auto p = buildRandomProof(100); peerids[i] = TestPeerManager::registerAndGetPeerId(pm, p); BOOST_CHECK(pm.addNode(InsecureRand32(), p->getId())); } BOOST_CHECK_EQUAL(pm.getSlotCount(), 400); BOOST_CHECK_EQUAL(pm.getFragmentation(), 0); for (int i = 0; i < 100; i++) { PeerId p = pm.selectPeer(); BOOST_CHECK(p == peerids[0] || p == peerids[1] || p == peerids[2] || p == peerids[3]); } // Remove one peer, it nevers show up now. BOOST_CHECK(pm.removePeer(peerids[2])); BOOST_CHECK_EQUAL(pm.getSlotCount(), 400); BOOST_CHECK_EQUAL(pm.getFragmentation(), 100); // Make sure we compact to never get NO_PEER. BOOST_CHECK_EQUAL(pm.compact(), 100); BOOST_CHECK(pm.verify()); BOOST_CHECK_EQUAL(pm.getSlotCount(), 300); BOOST_CHECK_EQUAL(pm.getFragmentation(), 0); for (int i = 0; i < 100; i++) { PeerId p = pm.selectPeer(); BOOST_CHECK(p == peerids[0] || p == peerids[1] || p == peerids[3]); } // Add 4 more peers. for (int i = 0; i < 4; i++) { auto p = buildRandomProof(100); peerids[i + 4] = TestPeerManager::registerAndGetPeerId(pm, p); BOOST_CHECK(pm.addNode(InsecureRand32(), p->getId())); } BOOST_CHECK_EQUAL(pm.getSlotCount(), 700); BOOST_CHECK_EQUAL(pm.getFragmentation(), 0); BOOST_CHECK(pm.removePeer(peerids[0])); BOOST_CHECK_EQUAL(pm.getSlotCount(), 700); BOOST_CHECK_EQUAL(pm.getFragmentation(), 100); // Removing the last entry do not increase fragmentation. BOOST_CHECK(pm.removePeer(peerids[7])); BOOST_CHECK_EQUAL(pm.getSlotCount(), 600); BOOST_CHECK_EQUAL(pm.getFragmentation(), 100); // Make sure we compact to never get NO_PEER. BOOST_CHECK_EQUAL(pm.compact(), 100); BOOST_CHECK(pm.verify()); BOOST_CHECK_EQUAL(pm.getSlotCount(), 500); BOOST_CHECK_EQUAL(pm.getFragmentation(), 0); for (int i = 0; i < 100; i++) { PeerId p = pm.selectPeer(); BOOST_CHECK(p == peerids[1] || p == peerids[3] || p == peerids[4] || p == peerids[5] || p == peerids[6]); } // Removing non existent peers fails. BOOST_CHECK(!pm.removePeer(peerids[0])); BOOST_CHECK(!pm.removePeer(peerids[2])); BOOST_CHECK(!pm.removePeer(peerids[7])); BOOST_CHECK(!pm.removePeer(NO_PEER)); } BOOST_AUTO_TEST_CASE(compact_slots) { avalanche::PeerManager pm; // Add 4 peers. std::array<PeerId, 4> peerids; for (int i = 0; i < 4; i++) { auto p = buildRandomProof(100); peerids[i] = TestPeerManager::registerAndGetPeerId(pm, p); BOOST_CHECK(pm.addNode(InsecureRand32(), p->getId())); } // Remove all peers. for (auto p : peerids) { pm.removePeer(p); } BOOST_CHECK_EQUAL(pm.getSlotCount(), 300); BOOST_CHECK_EQUAL(pm.getFragmentation(), 300); for (int i = 0; i < 100; i++) { BOOST_CHECK_EQUAL(pm.selectPeer(), NO_PEER); } BOOST_CHECK_EQUAL(pm.compact(), 300); BOOST_CHECK(pm.verify()); BOOST_CHECK_EQUAL(pm.getSlotCount(), 0); BOOST_CHECK_EQUAL(pm.getFragmentation(), 0); } BOOST_AUTO_TEST_CASE(node_crud) { avalanche::PeerManager pm; // Create one peer. auto proof = buildRandomProof(10000000 * MIN_VALID_PROOF_SCORE); BOOST_CHECK(pm.registerProof(proof)); BOOST_CHECK_EQUAL(pm.selectNode(), NO_NODE); // Add 4 nodes. const ProofId &proofid = proof->getId(); for (int i = 0; i < 4; i++) { BOOST_CHECK(pm.addNode(i, proofid)); } for (int i = 0; i < 100; i++) { NodeId n = pm.selectNode(); BOOST_CHECK(n >= 0 && n < 4); BOOST_CHECK( pm.updateNextRequestTime(n, std::chrono::steady_clock::now())); } // Remove a node, check that it doesn't show up. BOOST_CHECK(pm.removeNode(2)); for (int i = 0; i < 100; i++) { NodeId n = pm.selectNode(); BOOST_CHECK(n == 0 || n == 1 || n == 3); BOOST_CHECK( pm.updateNextRequestTime(n, std::chrono::steady_clock::now())); } // Push a node's timeout in the future, so that it doesn't show up. BOOST_CHECK(pm.updateNextRequestTime(1, std::chrono::steady_clock::now() + std::chrono::hours(24))); for (int i = 0; i < 100; i++) { NodeId n = pm.selectNode(); BOOST_CHECK(n == 0 || n == 3); BOOST_CHECK( pm.updateNextRequestTime(n, std::chrono::steady_clock::now())); } // Move a node from a peer to another. This peer has a very low score such // as chances of being picked are 1 in 10 million. addNodeWithScore(pm, 3, MIN_VALID_PROOF_SCORE); int node3selected = 0; for (int i = 0; i < 100; i++) { NodeId n = pm.selectNode(); if (n == 3) { // Selecting this node should be exceedingly unlikely. BOOST_CHECK(node3selected++ < 1); } else { BOOST_CHECK_EQUAL(n, 0); } BOOST_CHECK( pm.updateNextRequestTime(n, std::chrono::steady_clock::now())); } } BOOST_AUTO_TEST_CASE(node_binding) { avalanche::PeerManager pm; auto proof = buildRandomProof(MIN_VALID_PROOF_SCORE); const ProofId &proofid = proof->getId(); BOOST_CHECK_EQUAL(pm.getNodeCount(), 0); BOOST_CHECK_EQUAL(pm.getPendingNodeCount(), 0); // Add a bunch of nodes with no associated peer for (int i = 0; i < 10; i++) { BOOST_CHECK(!pm.addNode(i, proofid)); BOOST_CHECK(TestPeerManager::isNodePending(pm, i)); BOOST_CHECK_EQUAL(pm.getNodeCount(), 0); BOOST_CHECK_EQUAL(pm.getPendingNodeCount(), i + 1); } // Now create the peer and check all the nodes are bound const PeerId peerid = TestPeerManager::registerAndGetPeerId(pm, proof); BOOST_CHECK_NE(peerid, NO_PEER); for (int i = 0; i < 10; i++) { BOOST_CHECK(!TestPeerManager::isNodePending(pm, i)); BOOST_CHECK(TestPeerManager::nodeBelongToPeer(pm, i, peerid)); BOOST_CHECK_EQUAL(pm.getNodeCount(), 10); BOOST_CHECK_EQUAL(pm.getPendingNodeCount(), 0); } BOOST_CHECK(pm.verify()); // Disconnect some nodes for (int i = 0; i < 5; i++) { BOOST_CHECK(pm.removeNode(i)); BOOST_CHECK(!TestPeerManager::isNodePending(pm, i)); BOOST_CHECK(!TestPeerManager::nodeBelongToPeer(pm, i, peerid)); BOOST_CHECK_EQUAL(pm.getNodeCount(), 10 - i - 1); BOOST_CHECK_EQUAL(pm.getPendingNodeCount(), 0); } // Add nodes when the peer already exists for (int i = 0; i < 5; i++) { BOOST_CHECK(pm.addNode(i, proofid)); BOOST_CHECK(!TestPeerManager::isNodePending(pm, i)); BOOST_CHECK(TestPeerManager::nodeBelongToPeer(pm, i, peerid)); BOOST_CHECK_EQUAL(pm.getNodeCount(), 5 + i + 1); BOOST_CHECK_EQUAL(pm.getPendingNodeCount(), 0); } auto alt_proof = buildRandomProof(MIN_VALID_PROOF_SCORE); const ProofId &alt_proofid = alt_proof->getId(); // Update some nodes from a known proof to an unknown proof for (int i = 0; i < 5; i++) { BOOST_CHECK(!pm.addNode(i, alt_proofid)); BOOST_CHECK(TestPeerManager::isNodePending(pm, i)); BOOST_CHECK(!TestPeerManager::nodeBelongToPeer(pm, i, peerid)); BOOST_CHECK_EQUAL(pm.getNodeCount(), 10 - i - 1); BOOST_CHECK_EQUAL(pm.getPendingNodeCount(), i + 1); } auto alt2_proof = buildRandomProof(MIN_VALID_PROOF_SCORE); const ProofId &alt2_proofid = alt2_proof->getId(); // Update some nodes from an unknown proof to another unknown proof for (int i = 0; i < 5; i++) { BOOST_CHECK(!pm.addNode(i, alt2_proofid)); BOOST_CHECK(TestPeerManager::isNodePending(pm, i)); BOOST_CHECK_EQUAL(pm.getNodeCount(), 5); BOOST_CHECK_EQUAL(pm.getPendingNodeCount(), 5); } // Update some nodes from an unknown proof to a known proof for (int i = 0; i < 5; i++) { BOOST_CHECK(pm.addNode(i, proofid)); BOOST_CHECK(!TestPeerManager::isNodePending(pm, i)); BOOST_CHECK(TestPeerManager::nodeBelongToPeer(pm, i, peerid)); BOOST_CHECK_EQUAL(pm.getNodeCount(), 5 + i + 1); BOOST_CHECK_EQUAL(pm.getPendingNodeCount(), 5 - i - 1); } // Remove the peer, the nodes should be pending again BOOST_CHECK(pm.removePeer(peerid)); BOOST_CHECK(!pm.exists(proof->getId())); for (int i = 0; i < 10; i++) { BOOST_CHECK(TestPeerManager::isNodePending(pm, i)); BOOST_CHECK(!TestPeerManager::nodeBelongToPeer(pm, i, peerid)); BOOST_CHECK_EQUAL(pm.getNodeCount(), 0); BOOST_CHECK_EQUAL(pm.getPendingNodeCount(), 10); } BOOST_CHECK(pm.verify()); // Remove the remaining pending nodes, check the count drops accordingly for (int i = 0; i < 10; i++) { BOOST_CHECK(pm.removeNode(i)); BOOST_CHECK(!TestPeerManager::isNodePending(pm, i)); BOOST_CHECK(!TestPeerManager::nodeBelongToPeer(pm, i, peerid)); BOOST_CHECK_EQUAL(pm.getNodeCount(), 0); BOOST_CHECK_EQUAL(pm.getPendingNodeCount(), 10 - i - 1); } } BOOST_AUTO_TEST_CASE(node_binding_reorg) { avalanche::PeerManager pm; auto key = CKey::MakeCompressedKey(); COutPoint utxo = createUtxo(key); auto proof = buildProofWithOutpoints(key, {utxo}, 10 * COIN); const ProofId &proofid = proof->getId(); PeerId peerid = TestPeerManager::registerAndGetPeerId(pm, proof); BOOST_CHECK_NE(peerid, NO_PEER); BOOST_CHECK(pm.verify()); // Add nodes to our peer for (int i = 0; i < 10; i++) { BOOST_CHECK(pm.addNode(i, proofid)); BOOST_CHECK(!TestPeerManager::isNodePending(pm, i)); BOOST_CHECK(TestPeerManager::nodeBelongToPeer(pm, i, peerid)); } // Orphan the proof { LOCK(cs_main); CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); coins.SpendCoin(utxo); } pm.updatedBlockTip(); BOOST_CHECK(pm.isOrphan(proofid)); BOOST_CHECK(!pm.isBoundToPeer(proofid)); for (int i = 0; i < 10; i++) { BOOST_CHECK(TestPeerManager::isNodePending(pm, i)); BOOST_CHECK(!TestPeerManager::nodeBelongToPeer(pm, i, peerid)); } BOOST_CHECK(pm.verify()); // Make the proof great again addCoin(utxo, key); pm.updatedBlockTip(); BOOST_CHECK(!pm.isOrphan(proofid)); BOOST_CHECK(pm.isBoundToPeer(proofid)); // The peerid has certainly been updated peerid = TestPeerManager::registerAndGetPeerId(pm, proof); BOOST_CHECK_NE(peerid, NO_PEER); for (int i = 0; i < 10; i++) { BOOST_CHECK(!TestPeerManager::isNodePending(pm, i)); BOOST_CHECK(TestPeerManager::nodeBelongToPeer(pm, i, peerid)); } BOOST_CHECK(pm.verify()); } BOOST_AUTO_TEST_CASE(proof_conflict) { auto key = CKey::MakeCompressedKey(); TxId txid1(GetRandHash()); TxId txid2(GetRandHash()); BOOST_CHECK(txid1 != txid2); const Amount v = 10 * COIN; const int height = 100; for (uint32_t i = 0; i < 10; i++) { addCoin({txid1, i}, key); addCoin({txid2, i}, key); } avalanche::PeerManager pm; CKey masterKey = CKey::MakeCompressedKey(); const auto getPeerId = [&](const std::vector<COutPoint> &outpoints) { return TestPeerManager::registerAndGetPeerId( pm, buildProofWithOutpoints(key, outpoints, v, masterKey, 0, height, false, 0)); }; // Add one peer. const PeerId peer1 = getPeerId({COutPoint(txid1, 0)}); BOOST_CHECK(peer1 != NO_PEER); // Same proof, same peer. BOOST_CHECK_EQUAL(getPeerId({COutPoint(txid1, 0)}), peer1); // Different txid, different proof. const PeerId peer2 = getPeerId({COutPoint(txid2, 0)}); BOOST_CHECK(peer2 != NO_PEER && peer2 != peer1); // Different index, different proof. const PeerId peer3 = getPeerId({COutPoint(txid1, 1)}); BOOST_CHECK(peer3 != NO_PEER && peer3 != peer1); // Empty proof, no peer. BOOST_CHECK_EQUAL(getPeerId({}), NO_PEER); // Multiple inputs. const PeerId peer4 = getPeerId({COutPoint(txid1, 2), COutPoint(txid2, 2)}); BOOST_CHECK(peer4 != NO_PEER && peer4 != peer1); // Duplicated input. { ProofBuilder pb(0, 0, CKey::MakeCompressedKey()); COutPoint o(txid1, 3); BOOST_CHECK(pb.addUTXO(o, v, height, false, key)); BOOST_CHECK( !pm.registerProof(TestProofBuilder::buildDuplicatedStakes(pb))); } // Multiple inputs, collision on first input. BOOST_CHECK_EQUAL(getPeerId({COutPoint(txid1, 0), COutPoint(txid2, 4)}), NO_PEER); // Mutliple inputs, collision on second input. BOOST_CHECK_EQUAL(getPeerId({COutPoint(txid1, 4), COutPoint(txid2, 0)}), NO_PEER); // Mutliple inputs, collision on both inputs. BOOST_CHECK_EQUAL(getPeerId({COutPoint(txid1, 0), COutPoint(txid2, 2)}), NO_PEER); } BOOST_AUTO_TEST_CASE(orphan_proofs) { avalanche::PeerManager pm; auto key = CKey::MakeCompressedKey(); COutPoint outpoint1 = COutPoint(TxId(GetRandHash()), 0); COutPoint outpoint2 = COutPoint(TxId(GetRandHash()), 0); COutPoint outpoint3 = COutPoint(TxId(GetRandHash()), 0); const Amount v = 5 * COIN; const int height = 1234; const int wrongHeight = 12345; const auto makeProof = [&](const COutPoint &outpoint, const int h) { return buildProofWithOutpoints(key, {outpoint}, v, key, 0, h); }; auto proof1 = makeProof(outpoint1, height); auto proof2 = makeProof(outpoint2, height); auto proof3 = makeProof(outpoint3, wrongHeight); // Add outpoints 1 and 3, not 2 addCoin(outpoint1, key, v, height); addCoin(outpoint3, key, v, height); // Add the proofs BOOST_CHECK(pm.registerProof(proof1)); auto registerOrphan = [&](const ProofRef &proof) { ProofRegistrationState state; BOOST_CHECK(!pm.registerProof(proof, state)); BOOST_CHECK(state.GetResult() == ProofRegistrationResult::ORPHAN); }; registerOrphan(proof2); registerOrphan(proof3); auto checkOrphan = [&](const ProofRef &proof, bool expectedOrphan) { const ProofId &proofid = proof->getId(); BOOST_CHECK(pm.exists(proofid)); BOOST_CHECK_EQUAL(pm.isOrphan(proofid), expectedOrphan); BOOST_CHECK_EQUAL(pm.isBoundToPeer(proofid), !expectedOrphan); bool ret = false; pm.forEachPeer([&](const Peer &peer) { if (proof->getId() == peer.proof->getId()) { ret = true; } }); BOOST_CHECK_EQUAL(ret, !expectedOrphan); }; // Good checkOrphan(proof1, false); // MISSING_UTXO checkOrphan(proof2, true); // HEIGHT_MISMATCH checkOrphan(proof3, true); // Add outpoint2, proof2 is no longer considered orphan addCoin(outpoint2, key, v, height); pm.updatedBlockTip(); checkOrphan(proof2, false); // The status of proof1 and proof3 are unchanged checkOrphan(proof1, false); checkOrphan(proof3, true); // Spend outpoint1, proof1 becomes orphan { LOCK(cs_main); CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); coins.SpendCoin(outpoint1); } pm.updatedBlockTip(); checkOrphan(proof1, true); // The status of proof2 and proof3 are unchanged checkOrphan(proof2, false); checkOrphan(proof3, true); // A reorg could make a previous HEIGHT_MISMATCH become valid { LOCK(cs_main); CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); coins.SpendCoin(outpoint3); } addCoin(outpoint3, key, v, wrongHeight); pm.updatedBlockTip(); checkOrphan(proof3, false); // The status of proof 1 and proof2 are unchanged checkOrphan(proof1, true); checkOrphan(proof2, false); + + // Track expected orphans so we can test them later + std::vector<ProofRef> orphans; + orphans.push_back(proof1); + orphans.push_back(proof2); + orphans.push_back(proof3); + + // Fill up orphan pool to test the size limit + for (uint32_t i = 1; i < AVALANCHE_MAX_ORPHAN_PROOFS; i++) { + COutPoint outpoint = COutPoint(TxId(GetRandHash()), 0); + auto proof = + buildProofWithOutpoints(key, {outpoint}, 10 * COIN, key, 0, height); + registerOrphan(proof); + orphans.push_back(proof); + } + + // New orphans are rejected when the pool is full, even if they have higher + // proof scores. + { + COutPoint outpoint = COutPoint(TxId(GetRandHash()), 0); + auto proof = + buildProofWithOutpoints(key, {outpoint}, 20 * COIN, key, 0, height); + registerOrphan(proof); + BOOST_CHECK(!pm.exists(proof->getId())); + } + + // Replacement when the pool is full still works + { + auto proof = buildProofWithOutpoints(key, {outpoint1}, 10 * COIN, key, + 1, height); + registerOrphan(proof); + checkOrphan(proof, true); + BOOST_CHECK(!pm.exists(proof1->getId())); + orphans.push_back(proof); + orphans.erase(orphans.begin()); + } + + // Reorg so that some more proofs become orphans + { + LOCK(cs_main); + CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); + coins.SpendCoin(outpoint2); + coins.SpendCoin(outpoint3); + } + + pm.updatedBlockTip(); + + // New orphans are rejected when the pool is full, even if they have higher + // proof scores. + { + COutPoint outpoint = COutPoint(TxId(GetRandHash()), 0); + auto proof = + buildProofWithOutpoints(key, {outpoint}, 20 * COIN, key, 0, height); + registerOrphan(proof); + BOOST_CHECK(!pm.exists(proof->getId())); + } + + // Even though we've exceeded the orphan pool limit, the reorged proofs are + // still being tracked. + for (auto &proof : orphans) { + checkOrphan(proof, true); + } + + // Another block causes orphans to be trimmed to the limit + pm.updatedBlockTip(); + + int numOrphans = 0; + for (auto &proof : orphans) { + if (pm.exists(proof->getId())) { + checkOrphan(proof, true); + numOrphans++; + } + } + BOOST_CHECK_EQUAL(numOrphans, AVALANCHE_MAX_ORPHAN_PROOFS); } BOOST_AUTO_TEST_CASE(dangling_node) { avalanche::PeerManager pm; auto proof = buildRandomProof(MIN_VALID_PROOF_SCORE); PeerId peerid = TestPeerManager::registerAndGetPeerId(pm, proof); BOOST_CHECK_NE(peerid, NO_PEER); const TimePoint theFuture(std::chrono::steady_clock::now() + std::chrono::hours(24)); // Add nodes to this peer and update their request time far in the future for (int i = 0; i < 10; i++) { BOOST_CHECK(pm.addNode(i, proof->getId())); BOOST_CHECK(pm.updateNextRequestTime(i, theFuture)); } // Remove the peer BOOST_CHECK(pm.removePeer(peerid)); // Check the nodes are still there for (int i = 0; i < 10; i++) { BOOST_CHECK(pm.forNode(i, [](const Node &n) { return true; })); } // Build a new one proof = buildRandomProof(MIN_VALID_PROOF_SCORE); peerid = TestPeerManager::registerAndGetPeerId(pm, proof); BOOST_CHECK_NE(peerid, NO_PEER); // Update the nodes with the new proof for (int i = 0; i < 10; i++) { BOOST_CHECK(pm.addNode(i, proof->getId())); BOOST_CHECK(pm.forNode( i, [&](const Node &n) { return n.nextRequestTime == theFuture; })); } // Remove the peer BOOST_CHECK(pm.removePeer(peerid)); // Disconnect the nodes for (int i = 0; i < 10; i++) { BOOST_CHECK(pm.removeNode(i)); } } BOOST_AUTO_TEST_CASE(proof_accessors) { avalanche::PeerManager pm; constexpr int numProofs = 10; std::vector<ProofRef> proofs; proofs.reserve(numProofs); for (int i = 0; i < numProofs; i++) { proofs.push_back(buildRandomProof(MIN_VALID_PROOF_SCORE)); } for (int i = 0; i < numProofs; i++) { BOOST_CHECK(pm.registerProof(proofs[i])); { ProofRegistrationState state; // Fail to add an existing proof BOOST_CHECK(!pm.registerProof(proofs[i], state)); BOOST_CHECK(state.GetResult() == ProofRegistrationResult::ALREADY_REGISTERED); } for (int added = 0; added <= i; added++) { auto proof = pm.getProof(proofs[added]->getId()); BOOST_CHECK(proof != nullptr); const ProofId &proofid = proof->getId(); BOOST_CHECK_EQUAL(proofid, proofs[added]->getId()); } } // No stake, copied from proof_tests.cpp const std::string badProofHex( "96527eae083f1f24625f049d9e54bb9a2102a93d98bf42ab90cfc0bf9e7c634ed76a7" "3e95b02cacfd357b64e4fb6c92e92dd00"); bilingual_str error; auto badProof = RCUPtr<Proof>::make(); BOOST_CHECK(Proof::FromHex(*badProof, badProofHex, error)); ProofRegistrationState state; BOOST_CHECK(!pm.registerProof(badProof, state)); BOOST_CHECK(state.GetResult() == ProofRegistrationResult::INVALID); } BOOST_FIXTURE_TEST_CASE(conflicting_proof_rescan, NoCoolDownFixture) { avalanche::PeerManager pm; const CKey key = CKey::MakeCompressedKey(); const COutPoint conflictingOutpoint = createUtxo(key); const COutPoint outpointToSend = createUtxo(key); const Amount amount = 10 * COIN; ProofRef proofToInvalidate = buildProofWithOutpoints( key, {conflictingOutpoint, outpointToSend}, amount); BOOST_CHECK(pm.registerProof(proofToInvalidate)); ProofRef conflictingProof = buildProofWithOutpoints( key, {conflictingOutpoint, createUtxo(key)}, amount); ProofRegistrationState state; BOOST_CHECK(!pm.registerProof(conflictingProof, state)); BOOST_CHECK(state.GetResult() == ProofRegistrationResult::CONFLICTING); BOOST_CHECK(pm.isInConflictingPool(conflictingProof->getId())); { LOCK(cs_main); CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); // Make proofToInvalidate invalid coins.SpendCoin(outpointToSend); } pm.updatedBlockTip(); BOOST_CHECK(pm.isOrphan(proofToInvalidate->getId())); BOOST_CHECK(!pm.isInConflictingPool(conflictingProof->getId())); BOOST_CHECK(pm.isBoundToPeer(conflictingProof->getId())); } BOOST_FIXTURE_TEST_CASE(conflicting_proof_selection, NoCoolDownFixture) { const CKey key = CKey::MakeCompressedKey(); const Amount amount(10 * COIN); const uint32_t height = 100; const bool is_coinbase = false; // This will be the conflicting UTXO for all the following proofs auto conflictingOutpoint = createUtxo(key, amount); auto proof_base = buildProofWithSequence(key, {conflictingOutpoint}, 10); gArgs.ForceSetArg("-enableavalancheproofreplacement", "1"); ConflictingProofComparator comparator; auto checkPreferred = [&](const ProofRef &candidate, const ProofRef &reference, bool expectAccepted) { BOOST_CHECK_EQUAL(comparator(candidate, reference), expectAccepted); BOOST_CHECK_EQUAL(comparator(reference, candidate), !expectAccepted); avalanche::PeerManager pm; BOOST_CHECK(pm.registerProof(reference)); BOOST_CHECK(pm.isBoundToPeer(reference->getId())); ProofRegistrationState state; BOOST_CHECK_EQUAL(pm.registerProof(candidate, state), expectAccepted); BOOST_CHECK_EQUAL(state.IsValid(), expectAccepted); BOOST_CHECK_EQUAL(state.GetResult() == ProofRegistrationResult::CONFLICTING, !expectAccepted); BOOST_CHECK_EQUAL(pm.isBoundToPeer(candidate->getId()), expectAccepted); BOOST_CHECK_EQUAL(pm.isInConflictingPool(candidate->getId()), !expectAccepted); BOOST_CHECK_EQUAL(pm.isBoundToPeer(reference->getId()), !expectAccepted); BOOST_CHECK_EQUAL(pm.isInConflictingPool(reference->getId()), expectAccepted); }; // Same master key, lower sequence number checkPreferred(buildProofWithSequence(key, {conflictingOutpoint}, 9), proof_base, false); // Same master key, higher sequence number checkPreferred(buildProofWithSequence(key, {conflictingOutpoint}, 11), proof_base, true); auto buildProofFromAmounts = [&](const CKey &master, std::vector<Amount> &&amounts) { std::vector<std::tuple<COutPoint, Amount>> outpointsWithAmount{ {conflictingOutpoint, amount}}; std::transform(amounts.begin(), amounts.end(), std::back_inserter(outpointsWithAmount), [&key](const Amount amount) { return std::make_tuple(createUtxo(key, amount), amount); }); return buildProof(key, outpointsWithAmount, master, 0, height, is_coinbase, 0); }; auto proof_multiUtxo = buildProofFromAmounts(key, {10 * COIN, 10 * COIN}); // Test for both the same master and a different one. The sequence number // is the same for all these tests. for (const CKey &k : {key, CKey::MakeCompressedKey()}) { // Low amount checkPreferred(buildProofFromAmounts(k, {10 * COIN, 5 * COIN}), proof_multiUtxo, false); // High amount checkPreferred(buildProofFromAmounts(k, {10 * COIN, 15 * COIN}), proof_multiUtxo, true); // Same amount, low stake count checkPreferred(buildProofFromAmounts(k, {20 * COIN}), proof_multiUtxo, true); // Same amount, high stake count checkPreferred( buildProofFromAmounts(k, {10 * COIN, 5 * COIN, 5 * COIN}), proof_multiUtxo, false); // Same amount, same stake count, selection is done on proof id auto proofSimilar = buildProofFromAmounts(k, {10 * COIN, 10 * COIN}); checkPreferred(proofSimilar, proof_multiUtxo, proofSimilar->getId() < proof_multiUtxo->getId()); } gArgs.ClearForcedArg("-enableavalancheproofreplacement"); } BOOST_AUTO_TEST_CASE(conflicting_orphans) { avalanche::PeerManager pm; const CKey key = CKey::MakeCompressedKey(); const COutPoint conflictingOutpoint(TxId(GetRandHash()), 0); const COutPoint randomOutpoint1(TxId(GetRandHash()), 0); auto orphan10 = buildProofWithSequence(key, {conflictingOutpoint}, 10); auto orphan20 = buildProofWithSequence(key, {conflictingOutpoint, randomOutpoint1}, 20); BOOST_CHECK(!pm.registerProof(orphan10)); BOOST_CHECK(pm.isOrphan(orphan10->getId())); BOOST_CHECK(!pm.registerProof(orphan20)); BOOST_CHECK(pm.isOrphan(orphan20->getId())); BOOST_CHECK(!pm.exists(orphan10->getId())); const COutPoint outpointToSend(TxId(GetRandHash()), 0); // Add both randomOutpoint1 and outpointToSend to the UTXO set. The orphan20 // proof is still an orphan because the conflictingOutpoint is unknown. addCoin(randomOutpoint1, key); addCoin(outpointToSend, key); // Build and register proof valid proof that will conflict with the orphan auto proof30 = buildProofWithSequence(key, {randomOutpoint1, outpointToSend}, 30); BOOST_CHECK(pm.registerProof(proof30)); BOOST_CHECK(pm.isBoundToPeer(proof30->getId())); // Spend the outpointToSend to orphan proof30 { LOCK(cs_main); CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); coins.SpendCoin(outpointToSend); } // Check that a rescan will also select the preferred orphan, in this case // proof30 will replace orphan20. pm.updatedBlockTip(); BOOST_CHECK(!pm.isBoundToPeer(proof30->getId())); BOOST_CHECK(pm.isOrphan(proof30->getId())); BOOST_CHECK(!pm.exists(orphan20->getId())); } BOOST_FIXTURE_TEST_CASE(preferred_conflicting_proof, NoCoolDownFixture) { avalanche::PeerManager pm; const CKey key = CKey::MakeCompressedKey(); const COutPoint conflictingOutpoint = createUtxo(key); auto proofSeq10 = buildProofWithSequence(key, {conflictingOutpoint}, 10); auto proofSeq20 = buildProofWithSequence(key, {conflictingOutpoint}, 20); auto proofSeq30 = buildProofWithSequence(key, {conflictingOutpoint}, 30); BOOST_CHECK(pm.registerProof(proofSeq30)); BOOST_CHECK(pm.isBoundToPeer(proofSeq30->getId())); BOOST_CHECK(!pm.isInConflictingPool(proofSeq30->getId())); // proofSeq10 is a worst candidate than proofSeq30, so it goes to the // conflicting pool. BOOST_CHECK(!pm.registerProof(proofSeq10)); BOOST_CHECK(pm.isBoundToPeer(proofSeq30->getId())); BOOST_CHECK(!pm.isBoundToPeer(proofSeq10->getId())); BOOST_CHECK(pm.isInConflictingPool(proofSeq10->getId())); // proofSeq20 is a worst candidate than proofSeq30 but a better one than // proogSeq10, so it replaces it in the conflicting pool and proofSeq10 is // evicted. BOOST_CHECK(!pm.registerProof(proofSeq20)); BOOST_CHECK(pm.isBoundToPeer(proofSeq30->getId())); BOOST_CHECK(!pm.isBoundToPeer(proofSeq20->getId())); BOOST_CHECK(pm.isInConflictingPool(proofSeq20->getId())); BOOST_CHECK(!pm.exists(proofSeq10->getId())); } BOOST_FIXTURE_TEST_CASE(update_next_conflict_time, NoCoolDownFixture) { avalanche::PeerManager pm; auto now = GetTime<std::chrono::seconds>(); SetMockTime(now.count()); // Updating the time of an unknown peer should fail for (size_t i = 0; i < 10; i++) { BOOST_CHECK( !pm.updateNextPossibleConflictTime(PeerId(GetRandInt(1000)), now)); } auto proof = buildRandomProof(MIN_VALID_PROOF_SCORE); PeerId peerid = TestPeerManager::registerAndGetPeerId(pm, proof); auto checkNextPossibleConflictTime = [&](std::chrono::seconds expected) { BOOST_CHECK(pm.forPeer(proof->getId(), [&](const Peer &p) { return p.nextPossibleConflictTime == expected; })); }; checkNextPossibleConflictTime(now); // Move the time in the past is not possible BOOST_CHECK(!pm.updateNextPossibleConflictTime( peerid, now - std::chrono::seconds{1})); checkNextPossibleConflictTime(now); BOOST_CHECK(pm.updateNextPossibleConflictTime( peerid, now + std::chrono::seconds{1})); checkNextPossibleConflictTime(now + std::chrono::seconds{1}); } BOOST_FIXTURE_TEST_CASE(register_force_accept, NoCoolDownFixture) { avalanche::PeerManager pm; const CKey key = CKey::MakeCompressedKey(); const COutPoint conflictingOutpoint = createUtxo(key); auto proofSeq10 = buildProofWithSequence(key, {conflictingOutpoint}, 10); auto proofSeq20 = buildProofWithSequence(key, {conflictingOutpoint}, 20); auto proofSeq30 = buildProofWithSequence(key, {conflictingOutpoint}, 30); BOOST_CHECK(pm.registerProof(proofSeq30)); BOOST_CHECK(pm.isBoundToPeer(proofSeq30->getId())); BOOST_CHECK(!pm.isInConflictingPool(proofSeq30->getId())); // proofSeq20 is a worst candidate than proofSeq30, so it goes to the // conflicting pool. BOOST_CHECK(!pm.registerProof(proofSeq20)); BOOST_CHECK(pm.isBoundToPeer(proofSeq30->getId())); BOOST_CHECK(pm.isInConflictingPool(proofSeq20->getId())); // We can force the acceptance of proofSeq20 using RegistrationMode = avalanche::PeerManager::RegistrationMode; BOOST_CHECK(pm.registerProof(proofSeq20, RegistrationMode::FORCE_ACCEPT)); BOOST_CHECK(pm.isBoundToPeer(proofSeq20->getId())); BOOST_CHECK(pm.isInConflictingPool(proofSeq30->getId())); // We can also force the acceptance of a proof which is not already in the // conflicting pool. BOOST_CHECK(!pm.registerProof(proofSeq10)); BOOST_CHECK(!pm.exists(proofSeq10->getId())); BOOST_CHECK(pm.registerProof(proofSeq10, RegistrationMode::FORCE_ACCEPT)); BOOST_CHECK(pm.isBoundToPeer(proofSeq10->getId())); BOOST_CHECK(!pm.exists(proofSeq20->getId())); BOOST_CHECK(pm.isInConflictingPool(proofSeq30->getId())); // Attempting to register again fails, and has no impact on the pools for (size_t i = 0; i < 10; i++) { BOOST_CHECK(!pm.registerProof(proofSeq10)); BOOST_CHECK( !pm.registerProof(proofSeq10, RegistrationMode::FORCE_ACCEPT)); BOOST_CHECK(pm.isBoundToPeer(proofSeq10->getId())); BOOST_CHECK(!pm.exists(proofSeq20->getId())); BOOST_CHECK(pm.isInConflictingPool(proofSeq30->getId())); } // Revert between proofSeq10 and proofSeq30 a few times for (size_t i = 0; i < 10; i++) { BOOST_CHECK( pm.registerProof(proofSeq30, RegistrationMode::FORCE_ACCEPT)); BOOST_CHECK(pm.isBoundToPeer(proofSeq30->getId())); BOOST_CHECK(pm.isInConflictingPool(proofSeq10->getId())); BOOST_CHECK( pm.registerProof(proofSeq10, RegistrationMode::FORCE_ACCEPT)); BOOST_CHECK(pm.isBoundToPeer(proofSeq10->getId())); BOOST_CHECK(pm.isInConflictingPool(proofSeq30->getId())); } } BOOST_FIXTURE_TEST_CASE(evicted_proof, NoCoolDownFixture) { avalanche::PeerManager pm; const CKey key = CKey::MakeCompressedKey(); const COutPoint conflictingOutpoint = createUtxo(key); auto proofSeq10 = buildProofWithSequence(key, {conflictingOutpoint}, 10); auto proofSeq20 = buildProofWithSequence(key, {conflictingOutpoint}, 20); auto proofSeq30 = buildProofWithSequence(key, {conflictingOutpoint}, 30); { ProofRegistrationState state; BOOST_CHECK(pm.registerProof(proofSeq30, state)); BOOST_CHECK(state.IsValid()); } { ProofRegistrationState state; BOOST_CHECK(!pm.registerProof(proofSeq20, state)); BOOST_CHECK(state.GetResult() == ProofRegistrationResult::CONFLICTING); } { ProofRegistrationState state; BOOST_CHECK(!pm.registerProof(proofSeq10, state)); BOOST_CHECK(state.GetResult() == ProofRegistrationResult::REJECTED); } } BOOST_AUTO_TEST_CASE(conflicting_proof_cooldown) { avalanche::PeerManager pm; const CKey key = CKey::MakeCompressedKey(); const COutPoint conflictingOutpoint = createUtxo(key); auto proofSeq20 = buildProofWithSequence(key, {conflictingOutpoint}, 20); auto proofSeq30 = buildProofWithSequence(key, {conflictingOutpoint}, 30); auto proofSeq40 = buildProofWithSequence(key, {conflictingOutpoint}, 40); int64_t conflictingProofCooldown = 100; gArgs.ForceSetArg("-avalancheconflictingproofcooldown", strprintf("%d", conflictingProofCooldown)); int64_t now = GetTime(); auto increaseMockTime = [&](int64_t s) { now += s; SetMockTime(now); }; increaseMockTime(0); BOOST_CHECK(pm.registerProof(proofSeq30)); BOOST_CHECK(pm.isBoundToPeer(proofSeq30->getId())); auto checkRegistrationFailure = [&](const ProofRef &proof, ProofRegistrationResult reason) { ProofRegistrationState state; BOOST_CHECK(!pm.registerProof(proof, state)); BOOST_CHECK(state.GetResult() == reason); }; // Registering a conflicting proof will fail due to the conflicting proof // cooldown checkRegistrationFailure(proofSeq20, ProofRegistrationResult::COOLDOWN_NOT_ELAPSED); BOOST_CHECK(!pm.exists(proofSeq20->getId())); // The cooldown applies as well if the proof is the favorite checkRegistrationFailure(proofSeq40, ProofRegistrationResult::COOLDOWN_NOT_ELAPSED); BOOST_CHECK(!pm.exists(proofSeq40->getId())); // Elapse the cooldown increaseMockTime(conflictingProofCooldown); // The proof will now be added to conflicting pool checkRegistrationFailure(proofSeq20, ProofRegistrationResult::CONFLICTING); BOOST_CHECK(pm.isInConflictingPool(proofSeq20->getId())); // But no other checkRegistrationFailure(proofSeq40, ProofRegistrationResult::COOLDOWN_NOT_ELAPSED); BOOST_CHECK(!pm.exists(proofSeq40->getId())); BOOST_CHECK(pm.isInConflictingPool(proofSeq20->getId())); // Elapse the cooldown increaseMockTime(conflictingProofCooldown); // The proof will now be added to conflicting pool checkRegistrationFailure(proofSeq40, ProofRegistrationResult::CONFLICTING); BOOST_CHECK(pm.isInConflictingPool(proofSeq40->getId())); BOOST_CHECK(!pm.exists(proofSeq20->getId())); gArgs.ClearForcedArg("-avalancheconflictingproofcooldown"); } BOOST_FIXTURE_TEST_CASE(reject_proof, NoCoolDownFixture) { avalanche::PeerManager pm; const CKey key = CKey::MakeCompressedKey(); const COutPoint conflictingOutpoint = createUtxo(key); // The good, the bad and the ugly auto proofSeq10 = buildProofWithSequence(key, {conflictingOutpoint}, 10); auto proofSeq20 = buildProofWithSequence(key, {conflictingOutpoint}, 20); auto orphan30 = buildProofWithSequence( key, {conflictingOutpoint, {TxId(GetRandHash()), 0}}, 30); BOOST_CHECK(pm.registerProof(proofSeq20)); BOOST_CHECK(!pm.registerProof(proofSeq10)); BOOST_CHECK(!pm.registerProof(orphan30)); BOOST_CHECK(pm.isBoundToPeer(proofSeq20->getId())); BOOST_CHECK(pm.isInConflictingPool(proofSeq10->getId())); BOOST_CHECK(pm.isOrphan(orphan30->getId())); // Rejecting a proof that doesn't exist should fail for (size_t i = 0; i < 10; i++) { BOOST_CHECK( !pm.rejectProof(avalanche::ProofId(GetRandHash()), avalanche::PeerManager::RejectionMode::DEFAULT)); BOOST_CHECK( !pm.rejectProof(avalanche::ProofId(GetRandHash()), avalanche::PeerManager::RejectionMode::INVALIDATE)); } auto checkRejectDefault = [&](const ProofId &proofid) { BOOST_CHECK(pm.exists(proofid)); const bool isOrphan = pm.isOrphan(proofid); BOOST_CHECK(pm.rejectProof( proofid, avalanche::PeerManager::RejectionMode::DEFAULT)); BOOST_CHECK(!pm.isBoundToPeer(proofid)); BOOST_CHECK_EQUAL(pm.exists(proofid), !isOrphan); }; auto checkRejectInvalidate = [&](const ProofId &proofid) { BOOST_CHECK(pm.exists(proofid)); BOOST_CHECK(pm.rejectProof( proofid, avalanche::PeerManager::RejectionMode::INVALIDATE)); }; // Reject from the orphan pool checkRejectDefault(orphan30->getId()); BOOST_CHECK(!pm.registerProof(orphan30)); BOOST_CHECK(pm.isOrphan(orphan30->getId())); checkRejectInvalidate(orphan30->getId()); // Reject from the conflicting pool checkRejectDefault(proofSeq10->getId()); checkRejectInvalidate(proofSeq10->getId()); // Add again a proof to the conflicting pool BOOST_CHECK(!pm.registerProof(proofSeq10)); BOOST_CHECK(pm.isInConflictingPool(proofSeq10->getId())); // Reject from the valid pool, default mode checkRejectDefault(proofSeq20->getId()); // The conflicting proof should be promoted to a peer BOOST_CHECK(!pm.isInConflictingPool(proofSeq10->getId())); BOOST_CHECK(pm.isBoundToPeer(proofSeq10->getId())); // Reject from the valid pool, invalidate mode checkRejectInvalidate(proofSeq10->getId()); // The conflicting proof should also be promoted to a peer BOOST_CHECK(!pm.isInConflictingPool(proofSeq20->getId())); BOOST_CHECK(pm.isBoundToPeer(proofSeq20->getId())); } BOOST_AUTO_TEST_CASE(should_request_more_nodes) { avalanche::PeerManager pm; auto proof = buildRandomProof(MIN_VALID_PROOF_SCORE); BOOST_CHECK(pm.registerProof(proof)); // We have no nodes, so select node will fail and flag that we need more // nodes BOOST_CHECK_EQUAL(pm.selectNode(), NO_NODE); BOOST_CHECK(pm.shouldRequestMoreNodes()); for (size_t i = 0; i < 10; i++) { // The flag will not trigger again until we fail to select nodes again BOOST_CHECK(!pm.shouldRequestMoreNodes()); } // Add a few nodes. const ProofId &proofid = proof->getId(); for (size_t i = 0; i < 10; i++) { BOOST_CHECK(pm.addNode(i, proofid)); } auto cooldownTimepoint = std::chrono::steady_clock::now() + 10s; // All the nodes can be selected once for (size_t i = 0; i < 10; i++) { NodeId selectedId = pm.selectNode(); BOOST_CHECK_NE(selectedId, NO_NODE); BOOST_CHECK(pm.updateNextRequestTime(selectedId, cooldownTimepoint)); BOOST_CHECK(!pm.shouldRequestMoreNodes()); } // All the nodes have been requested, next select will fail and the flag // should trigger BOOST_CHECK_EQUAL(pm.selectNode(), NO_NODE); BOOST_CHECK(pm.shouldRequestMoreNodes()); for (size_t i = 0; i < 10; i++) { // The flag will not trigger again until we fail to select nodes again BOOST_CHECK(!pm.shouldRequestMoreNodes()); } // Make it possible to request a node again BOOST_CHECK(pm.updateNextRequestTime(0, std::chrono::steady_clock::now())); BOOST_CHECK_NE(pm.selectNode(), NO_NODE); BOOST_CHECK(!pm.shouldRequestMoreNodes()); } BOOST_AUTO_TEST_CASE(score_ordering) { avalanche::PeerManager pm; std::vector<uint32_t> expectedScores(10); // Expect the peers to be ordered by descending score std::generate(expectedScores.rbegin(), expectedScores.rend(), [n = 1]() mutable { return n++ * MIN_VALID_PROOF_SCORE; }); std::vector<ProofRef> proofs; proofs.reserve(expectedScores.size()); for (uint32_t score : expectedScores) { proofs.push_back(buildRandomProof(score)); } // Shuffle the proofs so they are registered in a random score order Shuffle(proofs.begin(), proofs.end(), FastRandomContext()); for (auto &proof : proofs) { BOOST_CHECK(pm.registerProof(proof)); } auto peersScores = TestPeerManager::getOrderedScores(pm); BOOST_CHECK_EQUAL_COLLECTIONS(peersScores.begin(), peersScores.end(), expectedScores.begin(), expectedScores.end()); } BOOST_FIXTURE_TEST_CASE(known_score_tracking, NoCoolDownFixture) { avalanche::PeerManager pm; const CKey key = CKey::MakeCompressedKey(); const Amount amount10(10 * COIN); const Amount amount20(20 * COIN); const COutPoint peer1ConflictingOutput = createUtxo(key, amount10); const COutPoint peer1SecondaryOutpoint = createUtxo(key, amount20); auto peer1Proof1 = buildProof(key, {{peer1ConflictingOutput, amount10}, {peer1SecondaryOutpoint, amount20}}, key, 10); auto peer1Proof2 = buildProof(key, {{peer1ConflictingOutput, amount10}}, key, 20); auto peer1Proof3 = buildProof(key, {{peer1ConflictingOutput, amount10}, {COutPoint{TxId(GetRandHash()), 0}, amount10}}, key, 30); const uint32_t peer1Score1 = Proof::amountToScore(amount10 + amount20); const uint32_t peer1Score2 = Proof::amountToScore(amount10); // Add first peer and check that we have its score tracked BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 0); BOOST_CHECK(pm.registerProof(peer1Proof2)); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), peer1Score2); // Ensure failing to add conflicting proofs doesn't affect the score, the // first proof stays bound and counted BOOST_CHECK(!pm.registerProof(peer1Proof1)); BOOST_CHECK(!pm.registerProof(peer1Proof3)); BOOST_CHECK(pm.isBoundToPeer(peer1Proof2->getId())); BOOST_CHECK(pm.isInConflictingPool(peer1Proof1->getId())); BOOST_CHECK(pm.isOrphan(peer1Proof3->getId())); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), peer1Score2); auto checkRejectDefault = [&](const ProofId &proofid) { BOOST_CHECK(pm.exists(proofid)); const bool isOrphan = pm.isOrphan(proofid); BOOST_CHECK(pm.rejectProof( proofid, avalanche::PeerManager::RejectionMode::DEFAULT)); BOOST_CHECK(!pm.isBoundToPeer(proofid)); BOOST_CHECK_EQUAL(pm.exists(proofid), !isOrphan); }; auto checkRejectInvalidate = [&](const ProofId &proofid) { BOOST_CHECK(pm.exists(proofid)); BOOST_CHECK(pm.rejectProof( proofid, avalanche::PeerManager::RejectionMode::INVALIDATE)); }; // Reject from the orphan pool doesn't affect tracked score checkRejectDefault(peer1Proof3->getId()); BOOST_CHECK(!pm.registerProof(peer1Proof3)); BOOST_CHECK(pm.isOrphan(peer1Proof3->getId())); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), peer1Score2); checkRejectInvalidate(peer1Proof3->getId()); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), peer1Score2); // Reject from the conflicting pool checkRejectDefault(peer1Proof1->getId()); checkRejectInvalidate(peer1Proof1->getId()); // Add again a proof to the conflicting pool BOOST_CHECK(!pm.registerProof(peer1Proof1)); BOOST_CHECK(pm.isInConflictingPool(peer1Proof1->getId())); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), peer1Score2); // Reject from the valid pool, default mode // Now the score should change as the new peer is promoted checkRejectDefault(peer1Proof2->getId()); BOOST_CHECK(!pm.isInConflictingPool(peer1Proof1->getId())); BOOST_CHECK(pm.isBoundToPeer(peer1Proof1->getId())); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), peer1Score1); // Reject from the valid pool, invalidate mode // Now the score should change as the old peer is re-promoted checkRejectInvalidate(peer1Proof1->getId()); // The conflicting proof should also be promoted to a peer BOOST_CHECK(!pm.isInConflictingPool(peer1Proof2->getId())); BOOST_CHECK(pm.isBoundToPeer(peer1Proof2->getId())); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), peer1Score2); // Now add another peer and check that combined scores are correct uint32_t peer2Score = 1 * MIN_VALID_PROOF_SCORE; auto peer2Proof1 = buildRandomProof(peer2Score); PeerId peerid2 = TestPeerManager::registerAndGetPeerId(pm, peer2Proof1); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), peer1Score2 + peer2Score); // Trying to remove non-existent peer doesn't affect score BOOST_CHECK(!pm.removePeer(1234)); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), peer1Score2 + peer2Score); // Removing new peer removes its score BOOST_CHECK(pm.removePeer(peerid2)); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), peer1Score2); PeerId peerid1 = TestPeerManager::getPeerIdForProofId(pm, peer1Proof2->getId()); BOOST_CHECK(pm.removePeer(peerid1)); BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), 0); } BOOST_AUTO_TEST_CASE(connected_score_tracking) { avalanche::PeerManager pm; const auto checkScores = [&pm](uint32_t known, uint32_t connected) { BOOST_CHECK_EQUAL(pm.getTotalPeersScore(), known); BOOST_CHECK_EQUAL(pm.getConnectedPeersScore(), connected); }; // Start out with 0s checkScores(0, 0); // Create one peer without a node. Its score should be registered but not // connected uint32_t score1 = 10000000 * MIN_VALID_PROOF_SCORE; auto proof1 = buildRandomProof(score1); PeerId peerid1 = TestPeerManager::registerAndGetPeerId(pm, proof1); checkScores(score1, 0); // Add nodes. We now have a connected score, but it doesn't matter how many // nodes we add the score is the same const ProofId &proofid1 = proof1->getId(); const uint8_t nodesToAdd = 10; for (int i = 0; i < nodesToAdd; i++) { BOOST_CHECK(pm.addNode(i, proofid1)); checkScores(score1, score1); } // Remove all but 1 node and ensure the score doesn't change for (int i = 0; i < nodesToAdd - 1; i++) { BOOST_CHECK(pm.removeNode(i)); checkScores(score1, score1); } // Removing the last node should remove the score from the connected count BOOST_CHECK(pm.removeNode(nodesToAdd - 1)); checkScores(score1, 0); // Add 2 nodes to peer and create peer2. Without a node peer2 has no // connected score but after adding a node it does. BOOST_CHECK(pm.addNode(0, proofid1)); BOOST_CHECK(pm.addNode(1, proofid1)); checkScores(score1, score1); uint32_t score2 = 1 * MIN_VALID_PROOF_SCORE; auto proof2 = buildRandomProof(score2); PeerId peerid2 = TestPeerManager::registerAndGetPeerId(pm, proof2); checkScores(score1 + score2, score1); BOOST_CHECK(pm.addNode(2, proof2->getId())); checkScores(score1 + score2, score1 + score2); // The first peer has two nodes left. Remove one and nothing happens, remove // the other and its score is no longer in the connected counter.. BOOST_CHECK(pm.removeNode(0)); checkScores(score1 + score2, score1 + score2); BOOST_CHECK(pm.removeNode(1)); checkScores(score1 + score2, score2); // Removing a peer with no allocated score has no affect. BOOST_CHECK(pm.removePeer(peerid1)); checkScores(score2, score2); // Remove the second peer's node removes its allocated score. BOOST_CHECK(pm.removeNode(2)); checkScores(score2, 0); // Removing the second peer takes us back to 0. BOOST_CHECK(pm.removePeer(peerid2)); checkScores(0, 0); // Add 2 peers with nodes and remove them without removing the nodes first. // Both score counters should be reduced by each peer's score when it's // removed. peerid1 = TestPeerManager::registerAndGetPeerId(pm, proof1); checkScores(score1, 0); peerid2 = TestPeerManager::registerAndGetPeerId(pm, proof2); checkScores(score1 + score2, 0); BOOST_CHECK(pm.addNode(0, proof1->getId())); checkScores(score1 + score2, score1); BOOST_CHECK(pm.addNode(1, proof2->getId())); checkScores(score1 + score2, score1 + score2); BOOST_CHECK(pm.removePeer(peerid2)); checkScores(score1, score1); BOOST_CHECK(pm.removePeer(peerid1)); checkScores(0, 0); } BOOST_FIXTURE_TEST_CASE(proof_radix_tree, NoCoolDownFixture) { avalanche::PeerManager pm; gArgs.ForceSetArg("-enableavalancheproofreplacement", "1"); struct ProofComparatorById { bool operator()(const ProofRef &lhs, const ProofRef &rhs) const { return lhs->getId() < rhs->getId(); }; }; using ProofSetById = std::set<ProofRef, ProofComparatorById>; // Maintain a list of the expected proofs through this test ProofSetById expectedProofs; auto matchExpectedContent = [&](const auto &tree) { auto it = expectedProofs.begin(); return tree.forEachLeaf([&](auto pLeaf) { return it != expectedProofs.end() && pLeaf->getId() == (*it++)->getId(); }); }; CKey key = CKey::MakeCompressedKey(); const int64_t sequence = 10; // Add some initial proofs for (size_t i = 0; i < 10; i++) { auto outpoint = createUtxo(key); auto proof = buildProofWithSequence(key, {{outpoint}}, sequence); BOOST_CHECK(pm.registerProof(proof)); expectedProofs.insert(std::move(proof)); } const auto &treeRef = pm.getShareableProofsSnapshot(); BOOST_CHECK(matchExpectedContent(treeRef)); // Create a copy auto tree = pm.getShareableProofsSnapshot(); // Adding more proofs doesn't change the tree... ProofSetById addedProofs; std::vector<COutPoint> outpointsToSpend; for (size_t i = 0; i < 10; i++) { auto outpoint = createUtxo(key); auto proof = buildProofWithSequence(key, {{outpoint}}, sequence); BOOST_CHECK(pm.registerProof(proof)); addedProofs.insert(std::move(proof)); outpointsToSpend.push_back(std::move(outpoint)); } BOOST_CHECK(matchExpectedContent(tree)); // ...until we get a new copy tree = pm.getShareableProofsSnapshot(); expectedProofs.insert(addedProofs.begin(), addedProofs.end()); BOOST_CHECK(matchExpectedContent(tree)); // Spend some coins to make the associated proofs invalid { LOCK(cs_main); CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); for (const auto &outpoint : outpointsToSpend) { coins.SpendCoin(outpoint); } } pm.updatedBlockTip(); // This doesn't change the tree... BOOST_CHECK(matchExpectedContent(tree)); // ...until we get a new copy tree = pm.getShareableProofsSnapshot(); for (const auto &proof : addedProofs) { BOOST_CHECK_EQUAL(expectedProofs.erase(proof), 1); } BOOST_CHECK(matchExpectedContent(tree)); // Add some more proof for which we will create conflicts std::vector<ProofRef> conflictingProofs; std::vector<COutPoint> conflictingOutpoints; for (size_t i = 0; i < 10; i++) { auto outpoint = createUtxo(key); auto proof = buildProofWithSequence(key, {{outpoint}}, sequence); BOOST_CHECK(pm.registerProof(proof)); conflictingProofs.push_back(std::move(proof)); conflictingOutpoints.push_back(std::move(outpoint)); } tree = pm.getShareableProofsSnapshot(); expectedProofs.insert(conflictingProofs.begin(), conflictingProofs.end()); BOOST_CHECK(matchExpectedContent(tree)); // Build a bunch of conflicting proofs, half better, half worst for (size_t i = 0; i < 10; i += 2) { // The worst proof is not added to the expected set BOOST_CHECK(!pm.registerProof(buildProofWithSequence( key, {{conflictingOutpoints[i]}}, sequence - 1))); // But the better proof should replace its conflicting one auto replacementProof = buildProofWithSequence( key, {{conflictingOutpoints[i + 1]}}, sequence + 1); BOOST_CHECK(pm.registerProof(replacementProof)); BOOST_CHECK_EQUAL(expectedProofs.erase(conflictingProofs[i + 1]), 1); BOOST_CHECK(expectedProofs.insert(replacementProof).second); } tree = pm.getShareableProofsSnapshot(); BOOST_CHECK(matchExpectedContent(tree)); // Check for consistency pm.verify(); gArgs.ClearForcedArg("-enableavalancheproofreplacement"); } BOOST_AUTO_TEST_SUITE_END()