rings_core/chunk/limits.rs
1use rings_transport::core::transport::MAX_DATA_CHANNEL_MESSAGE_SIZE;
2
3use crate::consts::MIN_CHUNK_DATA;
4use crate::consts::TRANSPORT_MAX_SIZE;
5
6/// The limits a [`super::MessageReassembler`] enforces on incoming chunks, as an explicit value
7/// rather than module globals. This keeps the core admission rule independent of *where* the
8/// numbers come from: the shell supplies them (see [`ReassemblyLimits::production`]), the
9/// reassembler only enforces what it is given, and tests can use small limits instead of giant
10/// synthetic payloads.
11#[derive(Debug, Clone, Copy)]
12pub struct ReassemblyLimits {
13 /// Max number of distinct in-flight message ids (a cheap first-line cap; the byte budgets are
14 /// the real memory guard).
15 pub max_pending_messages: usize,
16 /// Max `data` bytes a single chunk may carry.
17 pub max_chunk_data_len: usize,
18 /// Max buffered data bytes for one in-flight message.
19 pub max_message_bytes: usize,
20 /// Max number of slots (chunks) one in-flight message may have - i.e. the largest `total` a
21 /// chunk may claim. Caps the slot/`BTreeMap` count of a single message so a hostile peer cannot
22 /// use one id with a huge `total` and tiny chunks to allocate millions of slots while staying
23 /// under [`max_message_bytes`](Self::max_message_bytes) (which only counts data bytes).
24 pub max_chunks_per_message: usize,
25 /// Max buffered cost (data bytes + per-slot overhead) summed across all in-flight messages.
26 pub max_total_buffered_cost: usize,
27 /// Bookkeeping charge per slot - a *conservative estimate* (not an exact measurement) of the
28 /// `BTreeMap` node plus `Bytes` header/refcount a slot costs, so a flood of *tiny* chunks is
29 /// bounded by slot count, not only by summed data bytes. Real per-slot heap use may differ;
30 /// this is deliberately generous so the budget over- rather than under-counts.
31 pub slot_overhead: usize,
32 /// Max number of recently-completed message ids remembered as tombstones, to suppress a
33 /// re-delivery if a message is fully retransmitted after it already completed (within its TTL
34 /// window). The same number separately caps invalid-terminal ids and ids rejected before local
35 /// capacity could retain state. Invalid ids fail open without peer attribution when their set
36 /// is full, with a bounded saturation horizon preserving that decision while tombstones drain;
37 /// capacity-history saturation temporarily rejects all new ids for that peer. Thus
38 /// terminal bookkeeping retains at most three times this many ids across the independent
39 /// completed, invalid, and capacity sets, plus two scalar saturation horizons. NOTE: past
40 /// this many *concurrent* live completion
41 /// tombstones the oldest is dropped even if its TTL has not elapsed, so the "no
42 /// post-completion redelivery" guarantee holds only for the most recent `max_completed_ids`
43 /// completions within a TTL window.
44 pub max_completed_ids: usize,
45}
46
47impl ReassemblyLimits {
48 /// The limits used in production, derived from the transport / message ceilings. This is the one
49 /// place that reaches for transport-specific constants; the reassembler itself does not.
50 pub fn production() -> Self {
51 Self {
52 max_pending_messages: 512,
53 // A chunk crosses the wire as one data-channel message, capped by SCTP.
54 max_chunk_data_len: MAX_DATA_CHANNEL_MESSAGE_SIZE,
55 // The sender refuses to send more than this, so a larger reassembled message is forged;
56 // this is what stops the "one id, huge `total`, stream unique positions" attack.
57 max_message_bytes: TRANSPORT_MAX_SIZE,
58 // The sender never produces chunks smaller than `MIN_CHUNK_DATA`, so a legitimate
59 // message needs at most this many; a larger `total` is forged.
60 max_chunks_per_message: TRANSPORT_MAX_SIZE / MIN_CHUNK_DATA + 1,
61 // Admits several concurrent maximum-size transfers while staying hard-bounded.
62 max_total_buffered_cost: TRANSPORT_MAX_SIZE * 4,
63 slot_overhead: 128,
64 max_completed_ids: 1024,
65 }
66 }
67
68 /// Lower-concurrency limits for constrained deployments.
69 ///
70 /// The per-message ceiling remains protocol-compatible with production.
71 /// Constrained nodes instead admit fewer simultaneous messages and only one
72 /// maximum-size reassembly, including its contiguous output copy.
73 pub fn constrained() -> Self {
74 const CONSTRAINED_MESSAGE_BYTES: usize = TRANSPORT_MAX_SIZE;
75 const CONSTRAINED_MAX_CHUNKS: usize = CONSTRAINED_MESSAGE_BYTES / MIN_CHUNK_DATA + 1;
76 const CONSTRAINED_SLOT_OVERHEAD: usize = 128;
77 const CONSTRAINED_TOTAL_COST: usize =
78 crate::fair_admission::retained_wire_bytes(CONSTRAINED_MESSAGE_BYTES)
79 + CONSTRAINED_MAX_CHUNKS * CONSTRAINED_SLOT_OVERHEAD;
80
81 Self {
82 max_pending_messages: 64,
83 max_chunk_data_len: MAX_DATA_CHANNEL_MESSAGE_SIZE,
84 max_message_bytes: CONSTRAINED_MESSAGE_BYTES,
85 max_chunks_per_message: CONSTRAINED_MAX_CHUNKS,
86 max_total_buffered_cost: CONSTRAINED_TOTAL_COST,
87 slot_overhead: CONSTRAINED_SLOT_OVERHEAD,
88 max_completed_ids: 256,
89 }
90 }
91
92 /// Clamp nonsensical values to safe minimums so a caller-supplied [`ReassemblyLimits`] cannot
93 /// disable an invariant: every cap is forced to at least `1` (a `0` cap would, depending on the
94 /// field, reject all traffic or - for `max_completed_ids` - silently void the tombstone
95 /// guarantee the docs advertise). Applied by [`super::MessageReassembler::with_limits`].
96 pub(super) fn normalized(self) -> Self {
97 Self {
98 max_pending_messages: self.max_pending_messages.max(1),
99 max_chunk_data_len: self.max_chunk_data_len.max(1),
100 max_message_bytes: self.max_message_bytes.max(1),
101 max_chunks_per_message: self.max_chunks_per_message.max(1),
102 max_total_buffered_cost: self.max_total_buffered_cost.max(1),
103 slot_overhead: self.slot_overhead,
104 max_completed_ids: self.max_completed_ids.max(1),
105 }
106 }
107
108 /// Pending cost one peer may retain. One maximum-size legitimate message
109 /// still fits, while the node-wide budget keeps capacity available to other
110 /// peers instead of allowing a single incomplete-chunk flood to consume it.
111 pub(super) fn max_peer_buffered_cost(self) -> usize {
112 self.max_message_bytes
113 .saturating_add(
114 self.max_chunks_per_message
115 .saturating_mul(self.slot_overhead),
116 )
117 .min(self.max_total_buffered_cost)
118 }
119}
120
121impl Default for ReassemblyLimits {
122 fn default() -> Self {
123 Self::production()
124 }
125}