diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp index eed4503fa..e09fbfb52 100644 --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -1,344 +1,441 @@ // 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 #include // For ChainstateActive() #include namespace avalanche { bool PeerManager::addNode(NodeId nodeid, const Proof &proof, const Delegation &delegation) { - const PeerId peerid = getPeerId(proof); - if (peerid == NO_PEER) { + auto it = fetchOrCreatePeer(proof); + if (it == peers.end()) { return false; } + const PeerId peerid = it->peerid; + DelegationState state; CPubKey pubkey; if (!delegation.verify(state, proof, pubkey)) { return false; } auto nit = nodes.find(nodeid); if (nit == nodes.end()) { - return nodes.emplace(nodeid, peerid, std::move(pubkey)).second; + if (!nodes.emplace(nodeid, peerid, std::move(pubkey)).second) { + return false; + } + } else { + const PeerId oldpeerid = nit->peerid; + if (!nodes.modify(nit, [&](Node &n) { + n.peerid = peerid; + n.pubkey = std::move(pubkey); + })) { + return false; + } + + // We actually have this node already, we need to update it. + bool success = removeNodeFromPeer(peers.find(oldpeerid)); + assert(success); + + // Make sure it is not invalidated. + it = peers.find(peerid); } - // We actually have this node already, we need to update it. - return nodes.modify(nit, [&](Node &n) { - n.peerid = peerid; - n.pubkey = std::move(pubkey); + bool success = addNodeToPeer(it); + assert(success); + + 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 ned 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; }); } bool PeerManager::removeNode(NodeId nodeid) { - return nodes.erase(nodeid) > 0; + 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) { + assert(it != peers.end()); + 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 node left, we need to cleanup. + const 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); + } + + return true; } 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; } 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); } } PeerId PeerManager::getPeerId(const Proof &proof) { auto it = fetchOrCreatePeer(proof); return it == peers.end() ? NO_PEER : it->peerid; } PeerManager::PeerSet::iterator PeerManager::fetchOrCreatePeer(const Proof &proof) { { // Check if we already know of that peer. auto &pview = peers.get(); auto it = pview.find(proof.getId()); if (it != pview.end()) { return peers.project<0>(it); } } { // Reject invalid proof. LOCK(cs_main); const CCoinsViewCache &coins = ::ChainstateActive().CoinsTip(); ProofValidationState state; if (!proof.verify(state, coins)) { return peers.end(); } } // 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 peers.end(); } // We have no peer for this proof, time to create it. - auto inserted = peers.emplace(peerid, uint32_t(slots.size()), proof); + auto inserted = peers.emplace(peerid, 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 inserted.first; } 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 all nodes from this peer. + removeNodeFromPeer(it, it->node_count); // Remove nodes associated with this peer, unless their timeout is still - // active. This ensure that we don't overquery them in case their are + // active. This ensure that we don't overquery them in case they 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; } 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(); - } + std::vector newslots; + newslots.reserve(peers.size()); 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(); + if (it->node_count == 0) { + continue; + } - peers.modify(it, [&](Peer &p) { p.index = i++; }); + 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; 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) { + // Count node attached to this peer. + const auto count_nodes = [&]() { + size_t count = 0; + auto &nview = nodes.get(); + 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; + } + // 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; } } // namespace avalanche diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h index d3b0ca9db..212c7c468 100644 --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -1,199 +1,202 @@ // 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 { class Delegation; 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; + uint32_t index = -1; + uint32_t node_count = 0; Proof proof; - Peer(PeerId peerid_, uint32_t index_, Proof proof_) - : peerid(peerid_), index(index_), proof(std::move(proof_)) {} + Peer(PeerId peerid_, Proof proof_) + : peerid(peerid_), 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: /** * Node API. */ bool addNode(NodeId nodeid, const Proof &proof, const Delegation &delegation); bool removeNode(NodeId nodeid); bool forNode(NodeId nodeid, std::function func) const; bool updateNextRequestTime(NodeId nodeid, TimePoint timeout); /** * Randomly select a node to poll. */ NodeId selectNode(); /** * Update the peer set when a nw block is connected. */ void updatedBlockTip(); /**************************************************** * Functions which are public for testing purposes. * ****************************************************/ /** * Provide the PeerId associated with the given proof. If the peer does not * exists, then it is created. */ PeerId getPeerId(const Proof &proof); /** * Remove an existing peer. */ bool removePeer(const PeerId peerid); /** * Randomly select a peer to poll. */ 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. */ bool verify() const; // Accssors. uint64_t getSlotCount() const { return slotCount; } uint64_t getFragmentation() const { return fragmentation; } private: PeerSet::iterator fetchOrCreatePeer(const Proof &proof); + bool addNodeToPeer(const PeerSet::iterator &it); + bool removeNodeFromPeer(const PeerSet::iterator &it, uint32_t count = 1); }; /** * 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 8ac7adf56..9ebe1186c 100644 --- a/src/avalanche/test/peermanager_tests.cpp +++ b/src/avalanche/test/peermanager_tests.cpp @@ -1,432 +1,441 @@ // 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 #include