77
88#include < net.h> // for NodeId
99#include < sync.h>
10+ #include < uint256.h> // PendingMessage stores a uint256 by value
1011
1112#include < list>
1213#include < map>
1718
1819class CDataStream ;
1920class CBlockIndex ;
20- class uint256 ;
2121
2222namespace Consensus {
2323struct LLMQParams ;
@@ -48,28 +48,79 @@ enum class QuorumPhase {
4848 * handler thread.
4949 *
5050 * Each message type has it's own instance of this class.
51+ *
52+ * Retention is bounded both per NodeId (@ref maxMessagesPerNode) and globally
53+ * (@ref maxMessagesTotal = maxMessagesPerNode * MAX_PENDING_NODES_FACTOR). The
54+ * global bound is required because NodeId is ephemeral: a reconnecting peer
55+ * receives a fresh id and would otherwise obtain a fresh full per-node quota
56+ * forever. @ref RemoveNode drops a disconnected peer's counter and still-queued
57+ * payloads so live peers can use the capacity.
58+ *
59+ * The global bound must not become a way for one peer to stall the DKG for
60+ * everyone, so it is enforced by evicting the peer that currently occupies the
61+ * most slots rather than by rejecting whoever pushes next: a flooder is always
62+ * its own victim and honest members keep their place in the queue. Locally
63+ * generated messages (@c from < 0) bypass the bound entirely — they are produced
64+ * by our own phase handler, are a handful per round, and are the only path by
65+ * which our own contribution reaches the quorum.
5166 */
5267class CDKGPendingMessages
5368{
5469public:
5570 using BinaryMessage = std::pair<NodeId, std::shared_ptr<CDataStream>>;
5671
72+ // ! Cap total retained messages at this multiple of the per-node limit. A
73+ // ! legitimate round needs at most maxMessagesPerNode messages in total (one
74+ // ! per member, doubled to catch double-sends), so this leaves generous
75+ // ! headroom for honest traffic while still bounding what peers can pin.
76+ static constexpr size_t MAX_PENDING_NODES_FACTOR = 8 ;
77+
5778private:
79+ struct PendingMessage {
80+ NodeId from;
81+ std::shared_ptr<CDataStream> msg;
82+ // ! Content hash as inserted into @ref seenMessages. Stored so eviction
83+ // ! and disconnect cleanup never have to re-hash the payload (which would
84+ // ! put unbounded SHA256 work under ::cs_main via FinalizeNode).
85+ uint256 hash;
86+ };
87+
5888 const size_t maxMessagesPerNode;
89+ const size_t maxMessagesTotal;
5990 mutable Mutex cs_messages;
60- std::list<BinaryMessage> pendingMessages GUARDED_BY (cs_messages);
91+ std::list<PendingMessage> pendingMessages GUARDED_BY (cs_messages);
92+ // ! Per-connection lifetime quota counter. Deliberately not decremented when a
93+ // ! message is popped: the quota is per connection, not per queue slot, so a
94+ // ! peer cannot regain capacity by getting its messages processed. Released
95+ // ! only by @ref RemoveNode and @ref Clear.
6196 std::map<NodeId, size_t > messagesPerNode GUARDED_BY (cs_messages);
97+ // ! Live queue occupancy per NodeId, kept in sync with @ref pendingMessages.
98+ // ! Used to pick the greediest peer on overflow and to skip the list scan in
99+ // ! @ref RemoveNode when the disconnecting peer has nothing queued.
100+ std::map<NodeId, size_t > queuedPerNode GUARDED_BY (cs_messages);
62101 Uint256HashSet seenMessages GUARDED_BY (cs_messages);
63102
103+ // ! Erase one queue entry, keeping queuedPerNode and seenMessages in sync.
104+ // ! Returns the iterator following the erased entry.
105+ std::list<PendingMessage>::iterator EraseEntry (std::list<PendingMessage>::iterator it)
106+ EXCLUSIVE_LOCKS_REQUIRED(cs_messages);
107+ // ! Drop the oldest queued message of the peer occupying the most slots.
108+ // ! Never evicts our own (from < 0) messages. Returns false if nothing could
109+ // ! be evicted (i.e. the queue holds only own messages).
110+ bool EvictGreediestNode () EXCLUSIVE_LOCKS_REQUIRED(cs_messages);
111+
64112public:
65113 explicit CDKGPendingMessages (size_t _maxMessagesPerNode) :
66- maxMessagesPerNode(_maxMessagesPerNode) {};
114+ maxMessagesPerNode(_maxMessagesPerNode),
115+ maxMessagesTotal(_maxMessagesPerNode * MAX_PENDING_NODES_FACTOR ) {};
67116
68117 /* *
69118 * Enqueue a serialized DKG message under @p from with content hash @p hash.
70119 * Caller is responsible for hashing the payload and (for real peers)
71120 * routing the erase-request to PeerManager. Drops the message silently on
72- * per-node capacity overflow or duplicate hash.
121+ * per-node capacity overflow or duplicate hash. On global capacity overflow
122+ * the greediest *other* peer is evicted instead (see the class comment), so
123+ * a flooder cannot deny the queue to honest members.
73124 */
74125 void PushPendingMessage (NodeId from, std::shared_ptr<CDataStream> pm, const uint256& hash)
75126 EXCLUSIVE_LOCKS_REQUIRED(!cs_messages);
@@ -78,6 +129,22 @@ class CDKGPendingMessages
78129 bool HasSeen (const uint256& hash) const EXCLUSIVE_LOCKS_REQUIRED(!cs_messages);
79130 void Clear () EXCLUSIVE_LOCKS_REQUIRED(!cs_messages);
80131
132+ /* *
133+ * Drop @p nodeId's per-node counter, any still-queued payloads from that
134+ * peer, and the corresponding content hashes in @ref seenMessages. Called
135+ * on disconnect so a reconnecting attacker cannot pin memory (or grow
136+ * seenMessages) under abandoned NodeIds.
137+ *
138+ * Only *un-processed* messages are dropped. Anything already popped by the
139+ * phase handler has been accepted into the DKG session state and is
140+ * unaffected, so disconnecting a peer never invalidates a contribution the
141+ * session already took — the DKG completes as before.
142+ */
143+ void RemoveNode (NodeId nodeId) EXCLUSIVE_LOCKS_REQUIRED(!cs_messages);
144+
145+ // ! Number of still-queued binary messages (for tests / diagnostics).
146+ size_t Size () const EXCLUSIVE_LOCKS_REQUIRED(!cs_messages);
147+
81148 // Might return nullptr messages, which indicates that deserialization failed for some reason
82149 template <typename Message>
83150 std::vector<std::pair<NodeId, std::shared_ptr<Message>>> PopAndDeserializeMessages (size_t maxCount)
@@ -123,6 +190,7 @@ class CDKGSessionHandler
123190 virtual ~CDKGSessionHandler ();
124191
125192 void ClearPendingMessages ();
193+ void RemoveNode (NodeId nodeId);
126194
127195public:
128196 virtual bool GetContribution (const uint256& hash, CDKGContribution& ret) const { return false ; }
0 commit comments