diff --git a/doc/release-notes.md b/doc/release-notes.md
index c225ae3cc..3feeff071 100644
--- a/doc/release-notes.md
+++ b/doc/release-notes.md
@@ -1,7 +1,12 @@
# Bitcoin ABC 0.27.2 Release Notes
Bitcoin ABC version 0.27.2 is now available from:
-This is a maintenance release with no user-visible change.
+This release includes the following features and fixes:
+ - `getavalanchepeerinfo` returns a new field `availability_score` that
+ indicates how responsive a peer's nodes are (collectively) to polls from
+ this node. Higher scores indicate a peer has at least one node that that
+ responds to polls often. Lower scores indicate a peer has nodes that do not
+ respond to polls reliably.
diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h
index 3d8c40c3d..9547eb15f 100644
--- a/src/avalanche/peermanager.h
+++ b/src/avalanche/peermanager.h
@@ -1,470 +1,469 @@
// 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
#include
#include
#include
#include
#include
#include
#include
#include
class ChainstateManager;
class CScheduler;
namespace avalanche {
/**
* Maximum number of immature proofs the peer manager will accept from the
* network. Note that reorgs can cause the immature pool to temporarily exceed
* this limit, but a change in chaintip cause previously reorged proofs to be
* trimmed.
*/
static constexpr uint32_t AVALANCHE_MAX_IMMATURE_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;
bool hasFinalized = false;
// The network stack uses timestamp in seconds, so we oblige.
std::chrono::seconds registration_time;
std::chrono::seconds nextPossibleConflictTime;
double availabilityScore = 0.0;
/**
* Consider dropping the peer if no node is attached after this timeout
* expired.
*/
static constexpr auto DANGLING_TIMEOUT = 15min;
Peer(PeerId peerid_, ProofRef proof_,
std::chrono::seconds nextPossibleConflictTime_)
: peerid(peerid_), proof(std::move(proof_)),
registration_time(GetTime()),
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,
IMMATURE,
INVALID,
CONFLICTING,
REJECTED,
COOLDOWN_NOT_ELAPSED,
DANGLING,
MISSING_UTXO,
};
class ProofRegistrationState : public ValidationState {
};
namespace bmi = boost::multi_index;
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, bmi::indexed_by<
// index by peerid
bmi::hashed_unique>,
// index by proof
bmi::hashed_unique, proof_index,
SaltedProofIdHasher>,
// ordered by score, decreasing order
bmi::ordered_non_unique, score_index,
std::greater>>>;
PeerId nextPeerId = 0;
PeerSet peers;
ProofPool validProofPool;
ProofPool conflictingProofPool;
ProofPool immatureProofPool;
using ProofRadixTree = RadixTree;
ProofRadixTree shareableProofs;
using NodeSet = boost::multi_index_container<
Node,
bmi::indexed_by<
// index by nodeid
bmi::hashed_unique>,
// sorted by peerid/nextRequestTime
bmi::ordered_non_unique<
bmi::tag,
bmi::composite_key<
Node, bmi::member,
bmi::member>>>>;
NodeSet nodes;
/**
* Flag indicating that we failed to select a node and need to expand our
* node set.
*/
std::atomic needMoreNodes{false};
using PendingNodeSet = boost::multi_index_container<
PendingNode,
bmi::indexed_by<
// index by proofid
bmi::hashed_non_unique<
bmi::tag,
bmi::member,
SaltedProofIdHasher>,
// index by nodeid
bmi::hashed_unique<
bmi::tag,
bmi::member>>>;
PendingNodeSet pendingNodes;
static constexpr int SELECT_PEER_MAX_RETRY = 3;
static constexpr int SELECT_NODE_MAX_RETRY = 3;
/**
* Track proof ids to broadcast
*/
ProofIdSet m_unbroadcast_proofids;
/**
* Remember the last proofs that have been evicted because they had no node
* attached.
* A false positive would cause the proof to fail to register if there is
* no previously known node that is claiming it, which is acceptable
* intended the low expected false positive rate.
*/
CRollingBloomFilter danglingProofIds{10000, 0.00001};
/**
* Quorum management.
*/
uint32_t totalPeersScore = 0;
uint32_t connectedPeersScore = 0;
Amount stakeUtxoDustThreshold;
ChainstateManager &chainman;
public:
PeerManager(const Amount &stakeUtxoDustThresholdIn,
ChainstateManager &chainmanIn)
: stakeUtxoDustThreshold(stakeUtxoDustThresholdIn),
chainman(chainmanIn){};
/**
* 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);
/**
* Flag that a node did send its compact proofs.
* @return True if the flag changed state, i;e. if this is the first time
* the message is accounted for this node.
*/
bool latchAvaproofsSent(NodeId nodeid);
// 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
bool forNode(NodeId nodeid, Callable &&func) const {
auto it = nodes.find(nodeid);
return it != nodes.end() && func(*it);
}
template
void forEachNode(const Peer &peer, Callable &&func) const {
auto &nview = nodes.get();
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);
/**
* Latch on that this peer has a finalized proof.
*/
bool setFinalized(PeerId peerid);
/**
* 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;
}
void cleanupDanglingProofs(const ProofRef &localProof);
template
bool forPeer(const ProofId &proofid, Callable &&func) const {
auto &pview = peers.get();
auto it = pview.find(proofid);
return it != pview.end() && func(*it);
}
template void forEachPeer(Callable &&func) const {
for (const auto &p : peers) {
func(p);
}
}
/**
* Update the peer set when a new block is connected.
*/
std::unordered_set 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; }
template
void updateAvailabilityScores(const double decayFactor,
Callable &&getNodeAvailabilityScore) {
for (auto it = peers.begin(); it != peers.end(); it++) {
peers.modify(it, [&](Peer &peer) {
// Calculate average of current node scores
double peerScore{0.0};
forEachNode(peer, [&](const avalanche::Node &node) {
peerScore += getNodeAvailabilityScore(node.nodeid);
});
- peerScore /= peer.node_count;
// Calculate exponential moving average of averaged node scores
peer.availabilityScore =
decayFactor * peerScore +
(1. - decayFactor) * peer.availabilityScore;
});
}
}
/****************************************************
* 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; }
const ProofPool &getValidProofPool() const { return validProofPool; }
const ProofPool &getConflictingProofPool() const {
return conflictingProofPool;
}
const ProofPool &getImmatureProofPool() const { return immatureProofPool; }
ProofRef getProof(const ProofId &proofid) const;
bool isBoundToPeer(const ProofId &proofid) const;
bool isImmature(const ProofId &proofid) const;
bool isInConflictingPool(const ProofId &proofid) const;
const ProofRadixTree &getShareableProofsSnapshot() const {
return shareableProofs;
}
const Amount &getStakeUtxoDustThreshold() const {
return stakeUtxoDustThreshold;
}
private:
template
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 &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 1b55eabf0..0966c59e9 100644
--- a/src/avalanche/test/peermanager_tests.cpp
+++ b/src/avalanche/test/peermanager_tests.cpp
@@ -1,2232 +1,2234 @@
// 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
#include
#include
#include