Skip to main content

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}