diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp index b65250b99..1516b9a69 100644 --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -1,296 +1,330 @@ // 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 #include #include #include // For ChainstateActive() #include namespace avalanche { PeerId PeerManager::getPeer(const Proof &proof) { - auto &pview = peers.get(); - auto it = pview.find(proof.getId()); - if (it != pview.end()) { - return it->peerid; + { + // Check if we already know of that peer. + auto &pview = peers.get(); + auto it = pview.find(proof.getId()); + if (it != pview.end()) { + return it->peerid; + } } { // Reject invalid proof. LOCK(cs_main); const CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); ProofValidationState state; if (!proof.verify(state, coins)) { return NO_PEER; } } - // We have no peer for this proof, time to create it. + // New peer means new peerid! const PeerId peerid = nextPeerId++; + + // Attach UTXOs to this proof. + std::unordered_set conflicting_peerids; + for (const auto &s : proof.getStakes()) { + auto p = utxos.emplace(s.getStake().getUTXO(), peerid); + if (!p.second) { + // We have a collision with an existing proof. + conflicting_peerids.insert(p.first->second); + } + } + + // For now, if there is a conflict, just ceanup the mess. + if (conflicting_peerids.size() > 0) { + for (const auto &s : proof.getStakes()) { + auto it = utxos.find(s.getStake().getUTXO()); + assert(it != utxos.end()); + + // We need to delete that one. + if (it->second == peerid) { + utxos.erase(it); + } + } + + return NO_PEER; + } + + // We have no peer for this proof, time to create it. auto inserted = peers.emplace(peerid, uint32_t(slots.size()), proof); assert(inserted.second); const uint32_t score = proof.getScore(); const uint64_t start = slotCount; slots.emplace_back(start, score, peerid); slotCount = start + score; return peerid; } bool PeerManager::removePeer(const PeerId peerid) { auto it = peers.find(peerid); if (it == peers.end()) { return false; } size_t i = it->index; assert(i < slots.size()); 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); } // Remove nodes associated with this peer, unless their timeout is still // active. This ensure that we don't overquery them in case their are // subsequently added to another peer. auto &nview = nodes.get(); 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. + for (const auto &s : it->proof.getStakes()) { + bool deleted = utxos.erase(s.getStake().getUTXO()) > 0; + assert(deleted); + } + peers.erase(it); return true; } bool PeerManager::addNode(NodeId nodeid, const Proof &proof, const CPubKey &pubkey) { const PeerId peerid = getPeer(proof); if (peerid == NO_PEER) { return false; } auto nit = nodes.find(nodeid); if (nit == nodes.end()) { return nodes.emplace(nodeid, peerid, pubkey).second; } // We actually have this node already, we need to update it. return nodes.modify(nit, [&](Node &n) { n.peerid = peerid; n.pubkey = pubkey; }); } bool PeerManager::removeNode(NodeId nodeid) { return nodes.erase(nodeid) > 0; } bool PeerManager::forNode(NodeId nodeid, std::function func) const { auto it = nodes.find(nodeid); return it != nodes.end() && func(*it); } 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; }); } 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(); 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; } } return NO_NODE; } 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; } // Shrink the vector to the expected size. while (slots.size() > peers.size()) { slots.pop_back(); } uint64_t prevStop = 0; uint32_t i = 0; for (auto it = peers.begin(); it != peers.end(); it++) { slots[i] = Slot(prevStop, it->getScore(), it->peerid); prevStop = slots[i].getStop(); peers.modify(it, [&](Peer &p) { p.index = i++; }); } const uint64_t saved = slotCount - prevStop; slotCount = prevStop; fragmentation = 0; return saved; } bool PeerManager::verify() const { uint64_t prevStop = 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; } } for (const auto &p : peers) { // 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; } } return true; } PeerId selectPeerImpl(const std::vector &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::updatedBlockTip() { std::vector invalidPeers; { LOCK(cs_main); const CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); for (const auto &p : peers) { ProofValidationState state; if (!p.proof.verify(state, coins)) { invalidPeers.push_back(p.peerid); } } } for (const auto &pid : invalidPeers) { removePeer(pid); } - - compact(); } } // namespace avalanche diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h index fdf7352bf..692713e25 100644 --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -1,186 +1,189 @@ // 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 #include +#include #include #include #include #include #include #include #include #include #include #include #include namespace avalanche { 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; Proof proof; Peer(PeerId peerid_, uint32_t index_, Proof proof_) : peerid(peerid_), index(index_), proof(std::move(proof_)) {} 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(); } }; class SaltedProofIdHasher : private SaltedUint256Hasher { public: SaltedProofIdHasher() : SaltedUint256Hasher() {} size_t operator()(const ProofId &proofid) const { return hash(proofid); } }; struct next_request_time {}; class PeerManager { std::vector 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, boost::multi_index::indexed_by< // index by peerid boost::multi_index::hashed_unique< boost::multi_index::member>, // index by proof boost::multi_index::hashed_unique< boost::multi_index::tag, proof_index, SaltedProofIdHasher>>>; PeerId nextPeerId = 0; PeerSet peers; + std::unordered_map utxos; + using NodeSet = boost::multi_index_container< Node, boost::multi_index::indexed_by< // index by nodeid boost::multi_index::hashed_unique< boost::multi_index::member>, // sorted by peerid/nextRequestTime boost::multi_index::ordered_non_unique< boost::multi_index::tag, boost::multi_index::composite_key< Node, boost::multi_index::member, boost::multi_index::member>>>>; NodeSet nodes; static constexpr int SELECT_PEER_MAX_RETRY = 3; static constexpr int SELECT_NODE_MAX_RETRY = 3; public: /** * Provide the peer associated with the given proof. If the peer does not * exists, then it is created. */ PeerId getPeer(const Proof &proof); /** * Remove an existing peer. * This is not meant for public consumption. */ bool removePeer(const PeerId peerid); /** * Node API. */ bool addNode(NodeId nodeid, const Proof &proof, const CPubKey &pubkey); bool removeNode(NodeId nodeid); NodeId selectNode(); bool forNode(NodeId nodeid, std::function func) const; bool updateNextRequestTime(NodeId nodeid, TimePoint timeout); /** * Exposed for tests. */ PeerId selectPeer() const; /** * Trigger maintenance of internal datastructures. * Returns how much slot space was saved after compaction. */ uint64_t compact(); /** * Perform consistency check on internal data structures. * Mostly useful for tests. */ bool verify() const; // Accssors. uint64_t getSlotCount() const { return slotCount; } uint64_t getFragmentation() const { return fragmentation; } /** * Update the peer set when a nw block is connected. */ void updatedBlockTip(); }; /** * This is an internal method that is exposed for testing purposes. */ PeerId selectPeerImpl(const std::vector &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 600803f6d..c46080a0f 100644 --- a/src/avalanche/test/peermanager_tests.cpp +++ b/src/avalanche/test/peermanager_tests.cpp @@ -1,349 +1,425 @@ // 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 +#include +#include