Skip to main content

eth_valkyoth_codec/
budget.rs

1use crate::DecodeError;
2
3/// Resource limits required by every untrusted decoder.
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5pub struct DecodeLimits {
6    /// Maximum accepted input length in bytes.
7    pub max_input_bytes: usize,
8    /// Maximum items accepted in any single decoded list.
9    pub max_list_items: usize,
10    /// Maximum nested list depth.
11    pub max_nesting_depth: usize,
12    /// Maximum total allocation a decoder may request.
13    pub max_total_allocation: usize,
14    /// Maximum proof nodes accepted by a proof decoder.
15    pub max_proof_nodes: usize,
16    /// Maximum total decoded items visited by one decoder invocation.
17    pub max_total_items: usize,
18}
19
20impl DecodeLimits {
21    /// Limits for unit tests and conformance fixtures.
22    ///
23    /// This is not a production policy. Production decoders should choose
24    /// deployment-specific limits or start from [`Self::DEPLOYMENT_TEMPLATE`].
25    pub const TEST_FIXTURE: Self = Self {
26        max_input_bytes: 1 << 20,
27        max_list_items: 4096,
28        max_nesting_depth: 64,
29        max_total_allocation: 1 << 20,
30        max_proof_nodes: 1024,
31        max_total_items: 8192,
32    };
33
34    /// Deployment template for externally reachable wire decoders.
35    ///
36    /// Using this constant unchanged in production is a security
37    /// misconfiguration. Copy it, review every limit against the deployment's
38    /// concurrency and memory policy, and tighten values before release.
39    pub const DEPLOYMENT_TEMPLATE: Self = Self {
40        max_input_bytes: 2 << 20,
41        max_list_items: 16_384,
42        max_nesting_depth: 64,
43        max_total_allocation: 4 << 20,
44        max_proof_nodes: 4096,
45        max_total_items: 65_536,
46    };
47
48    /// Validates the input length before parsing starts.
49    pub fn check_input_len(self, len: usize) -> Result<(), DecodeError> {
50        if len > self.max_input_bytes {
51            return Err(DecodeError::InputTooLarge);
52        }
53        Ok(())
54    }
55
56    /// Validates a decoded list item count.
57    pub fn check_list_count(self, count: usize) -> Result<(), DecodeError> {
58        if count > self.max_list_items {
59            return Err(DecodeError::ListTooLong);
60        }
61        Ok(())
62    }
63
64    /// Validates the current nesting depth.
65    pub fn check_nesting_depth(self, depth: usize) -> Result<(), DecodeError> {
66        if depth > self.max_nesting_depth {
67            return Err(DecodeError::NestingTooDeep);
68        }
69        Ok(())
70    }
71
72    /// Validates one requested allocation against the allocation budget.
73    ///
74    /// This helper is for single-allocation checks only. Decoders that can make
75    /// more than one allocation must use [`DecodeAccumulator`] to enforce the
76    /// cumulative budget.
77    pub fn check_single_allocation_limit(self, size: usize) -> Result<(), DecodeError> {
78        if size > self.max_total_allocation {
79            return Err(DecodeError::AllocationExceeded);
80        }
81        Ok(())
82    }
83
84    /// Validates one proof-node count against the proof-node budget.
85    ///
86    /// Decoders that traverse more than one proof segment must use
87    /// [`DecodeAccumulator::account_proof_nodes`] for cumulative accounting.
88    pub fn check_proof_node_count(self, count: usize) -> Result<(), DecodeError> {
89        if count > self.max_proof_nodes {
90            return Err(DecodeError::ProofTooLarge);
91        }
92        Ok(())
93    }
94
95    /// Validates one decoded item count against the cumulative item budget.
96    ///
97    /// This helper is for single-count checks only. Decoders that visit items
98    /// incrementally must use [`DecodeAccumulator::account_items`].
99    pub fn check_item_count(self, count: usize) -> Result<(), DecodeError> {
100        if count > self.max_total_items {
101            return Err(DecodeError::ItemCountExceeded);
102        }
103        Ok(())
104    }
105
106    /// Starts stateful budget accounting for a decoder invocation.
107    #[must_use]
108    pub const fn accumulator(self) -> DecodeAccumulator {
109        DecodeAccumulator {
110            limits: self,
111            total_allocated: 0,
112            total_items: 0,
113            proof_nodes: 0,
114        }
115    }
116}
117
118/// Stateful budget accounting for one decoder invocation.
119#[derive(Debug, Eq, PartialEq)]
120pub struct DecodeAccumulator {
121    limits: DecodeLimits,
122    total_allocated: usize,
123    total_items: usize,
124    proof_nodes: usize,
125}
126
127impl DecodeAccumulator {
128    /// Returns the active decode limits.
129    #[must_use]
130    pub const fn limits(&self) -> DecodeLimits {
131        self.limits
132    }
133
134    /// Returns the cumulative allocation accounted so far.
135    #[must_use]
136    pub const fn total_allocated(&self) -> usize {
137        self.total_allocated
138    }
139
140    /// Returns the cumulative decoded items accounted so far.
141    #[must_use]
142    pub const fn total_items(&self) -> usize {
143        self.total_items
144    }
145
146    /// Returns the cumulative proof nodes accounted so far.
147    #[must_use]
148    pub const fn proof_nodes(&self) -> usize {
149        self.proof_nodes
150    }
151
152    /// Validates the input length before parsing starts.
153    pub fn check_input_len(&self, len: usize) -> Result<(), DecodeError> {
154        self.limits.check_input_len(len)
155    }
156
157    /// Validates a decoded list item count.
158    pub fn check_list_count(&self, count: usize) -> Result<(), DecodeError> {
159        self.limits.check_list_count(count)
160    }
161
162    /// Validates the current nesting depth.
163    pub fn check_nesting_depth(&self, depth: usize) -> Result<(), DecodeError> {
164        self.limits.check_nesting_depth(depth)
165    }
166
167    /// Accounts for one allocation against the cumulative allocation budget.
168    pub fn check_allocation(&mut self, size: usize) -> Result<(), DecodeError> {
169        let new_total = self
170            .total_allocated
171            .checked_add(size)
172            .ok_or(DecodeError::AllocationExceeded)?;
173        if new_total > self.limits.max_total_allocation {
174            return Err(DecodeError::AllocationExceeded);
175        }
176        self.total_allocated = new_total;
177        Ok(())
178    }
179
180    /// Accounts for decoded items against the cumulative item budget.
181    pub fn account_items(&mut self, count: usize) -> Result<(), DecodeError> {
182        let new_total = self
183            .total_items
184            .checked_add(count)
185            .ok_or(DecodeError::ItemCountExceeded)?;
186        if new_total > self.limits.max_total_items {
187            return Err(DecodeError::ItemCountExceeded);
188        }
189        self.total_items = new_total;
190        Ok(())
191    }
192
193    /// Accounts for proof nodes against the cumulative proof-node budget.
194    pub fn account_proof_nodes(&mut self, count: usize) -> Result<(), DecodeError> {
195        let new_total = self
196            .proof_nodes
197            .checked_add(count)
198            .ok_or(DecodeError::ProofTooLarge)?;
199        if new_total > self.limits.max_proof_nodes {
200            return Err(DecodeError::ProofTooLarge);
201        }
202        self.proof_nodes = new_total;
203        Ok(())
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn rejects_oversized_input() {
213        let limits = DecodeLimits {
214            max_input_bytes: 2,
215            ..DecodeLimits::TEST_FIXTURE
216        };
217        assert_eq!(limits.check_input_len(3), Err(DecodeError::InputTooLarge));
218    }
219
220    #[test]
221    fn rejects_oversized_list() {
222        let limits = DecodeLimits {
223            max_list_items: 2,
224            ..DecodeLimits::TEST_FIXTURE
225        };
226        assert_eq!(limits.check_list_count(3), Err(DecodeError::ListTooLong));
227    }
228
229    #[test]
230    fn rejects_excessive_nesting_depth() {
231        let limits = DecodeLimits {
232            max_nesting_depth: 2,
233            ..DecodeLimits::TEST_FIXTURE
234        };
235        assert_eq!(
236            limits.check_nesting_depth(3),
237            Err(DecodeError::NestingTooDeep)
238        );
239    }
240
241    #[test]
242    fn rejects_excessive_allocation() {
243        let limits = DecodeLimits {
244            max_total_allocation: 2,
245            ..DecodeLimits::TEST_FIXTURE
246        };
247        assert_eq!(
248            limits.check_single_allocation_limit(3),
249            Err(DecodeError::AllocationExceeded)
250        );
251    }
252
253    #[test]
254    fn rejects_excessive_proof_nodes() {
255        let limits = DecodeLimits {
256            max_proof_nodes: 2,
257            ..DecodeLimits::TEST_FIXTURE
258        };
259        assert_eq!(
260            limits.check_proof_node_count(3),
261            Err(DecodeError::ProofTooLarge)
262        );
263    }
264
265    #[test]
266    fn rejects_excessive_total_items() {
267        let limits = DecodeLimits {
268            max_total_items: 2,
269            ..DecodeLimits::TEST_FIXTURE
270        };
271        assert_eq!(
272            limits.check_item_count(3),
273            Err(DecodeError::ItemCountExceeded)
274        );
275    }
276
277    #[test]
278    fn fixture_and_deployment_template_limits_are_distinct() {
279        let production = DecodeLimits::DEPLOYMENT_TEMPLATE;
280        let fixture = DecodeLimits::TEST_FIXTURE;
281
282        assert!(production.max_input_bytes > fixture.max_input_bytes);
283        assert!(production.max_total_allocation > fixture.max_total_allocation);
284        assert!(production.max_proof_nodes > fixture.max_proof_nodes);
285        assert!(production.max_total_items > fixture.max_total_items);
286    }
287
288    #[test]
289    fn accumulator_rejects_cumulative_allocation_over_budget() {
290        let limits = DecodeLimits {
291            max_total_allocation: 4,
292            ..DecodeLimits::TEST_FIXTURE
293        };
294        let mut accumulator = limits.accumulator();
295
296        assert_eq!(accumulator.check_allocation(3), Ok(()));
297        assert_eq!(accumulator.total_allocated(), 3);
298        assert_eq!(
299            accumulator.check_allocation(2),
300            Err(DecodeError::AllocationExceeded)
301        );
302        assert_eq!(accumulator.total_allocated(), 3);
303    }
304
305    #[test]
306    fn accumulator_rejects_allocation_counter_overflow() {
307        let limits = DecodeLimits {
308            max_total_allocation: usize::MAX,
309            ..DecodeLimits::TEST_FIXTURE
310        };
311        let mut accumulator = limits.accumulator();
312
313        assert_eq!(accumulator.check_allocation(usize::MAX), Ok(()));
314        assert_eq!(
315            accumulator.check_allocation(1),
316            Err(DecodeError::AllocationExceeded)
317        );
318    }
319
320    #[test]
321    fn accumulator_rejects_cumulative_items_over_budget() {
322        let limits = DecodeLimits {
323            max_total_items: 4,
324            ..DecodeLimits::TEST_FIXTURE
325        };
326        let mut accumulator = limits.accumulator();
327
328        assert_eq!(accumulator.account_items(3), Ok(()));
329        assert_eq!(accumulator.total_items(), 3);
330        assert_eq!(
331            accumulator.account_items(2),
332            Err(DecodeError::ItemCountExceeded)
333        );
334        assert_eq!(accumulator.total_items(), 3);
335    }
336
337    #[test]
338    fn accumulator_rejects_cumulative_proof_nodes_over_budget() {
339        let limits = DecodeLimits {
340            max_proof_nodes: 4,
341            ..DecodeLimits::TEST_FIXTURE
342        };
343        let mut accumulator = limits.accumulator();
344
345        assert_eq!(accumulator.account_proof_nodes(3), Ok(()));
346        assert_eq!(accumulator.proof_nodes(), 3);
347        assert_eq!(
348            accumulator.account_proof_nodes(2),
349            Err(DecodeError::ProofTooLarge)
350        );
351        assert_eq!(accumulator.proof_nodes(), 3);
352    }
353}