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    ///
12    /// RLP list decoding is also capped by
13    /// [`crate::MAX_RLP_LIST_TRAVERSAL_DEPTH`], even when this configured limit
14    /// is higher.
15    pub max_nesting_depth: usize,
16    /// Maximum total allocation a decoder may request.
17    pub max_total_allocation: usize,
18    /// Maximum proof nodes accepted by a proof decoder.
19    pub max_proof_nodes: usize,
20    /// Maximum total decoded items visited by one decoder invocation.
21    pub max_total_items: usize,
22}
23
24impl DecodeLimits {
25    /// Limits for unit tests, conformance fixtures, and fuzz targets.
26    ///
27    /// This is not a production policy. Production decoders should choose
28    /// deployment-specific limits or start from
29    /// [`Self::DEPLOYMENT_STARTING_POINT`].
30    #[cfg(any(test, feature = "testing"))]
31    pub const TEST_FIXTURE: Self = Self {
32        max_input_bytes: 1 << 20,
33        max_list_items: 4096,
34        max_nesting_depth: 64,
35        max_total_allocation: 1 << 20,
36        max_proof_nodes: 1024,
37        max_total_items: 8192,
38    };
39
40    /// Starting point for externally reachable wire decoders.
41    ///
42    /// Using this constant unchanged in production is a security
43    /// misconfiguration. Changing only one field is likely also a
44    /// misconfiguration. Copy it, review every limit against the deployment's
45    /// concurrency, memory, and protocol policy, and tighten values before
46    /// release.
47    #[doc(alias = "DEPLOYMENT_TEMPLATE")]
48    pub const DEPLOYMENT_STARTING_POINT: Self = Self {
49        max_input_bytes: 2 << 20,
50        max_list_items: 16_384,
51        max_nesting_depth: 64,
52        max_total_allocation: 4 << 20,
53        max_proof_nodes: 4096,
54        max_total_items: 65_536,
55    };
56
57    /// Creates a reviewed deployment policy from explicit values.
58    ///
59    /// This helper intentionally has no defaults. Production deployments should
60    /// pass every value from their own concurrency, memory, and protocol review
61    /// instead of copying [`Self::DEPLOYMENT_STARTING_POINT`] and changing one
62    /// field.
63    #[must_use]
64    pub const fn reviewed_policy(
65        max_input_bytes: usize,
66        max_list_items: usize,
67        max_nesting_depth: usize,
68        max_total_allocation: usize,
69        max_proof_nodes: usize,
70        max_total_items: usize,
71    ) -> Self {
72        Self {
73            max_input_bytes,
74            max_list_items,
75            max_nesting_depth,
76            max_total_allocation,
77            max_proof_nodes,
78            max_total_items,
79        }
80    }
81
82    /// Validates the input length before parsing starts.
83    pub fn check_input_len(self, len: usize) -> Result<(), DecodeError> {
84        if len > self.max_input_bytes {
85            return Err(DecodeError::InputTooLarge);
86        }
87        Ok(())
88    }
89
90    /// Validates a decoded list item count.
91    pub fn check_list_count(self, count: usize) -> Result<(), DecodeError> {
92        if count > self.max_list_items {
93            return Err(DecodeError::ListTooLong);
94        }
95        Ok(())
96    }
97
98    /// Validates the current nesting depth.
99    ///
100    /// RLP list decoding also enforces [`crate::MAX_RLP_LIST_TRAVERSAL_DEPTH`]
101    /// as a hard traversal-stack cap.
102    pub fn check_nesting_depth(self, depth: usize) -> Result<(), DecodeError> {
103        if depth > self.max_nesting_depth {
104            return Err(DecodeError::NestingTooDeep);
105        }
106        Ok(())
107    }
108
109    /// Validates one requested allocation against the allocation budget.
110    ///
111    /// This helper is for single-allocation checks only. Decoders that can make
112    /// more than one allocation must use [`DecodeAccumulator`] to enforce the
113    /// cumulative budget.
114    pub fn check_single_allocation_limit(self, size: usize) -> Result<(), DecodeError> {
115        if size > self.max_total_allocation {
116            return Err(DecodeError::AllocationExceeded);
117        }
118        Ok(())
119    }
120
121    /// Validates one proof-node count against the proof-node budget.
122    ///
123    /// Decoders that traverse more than one proof segment must use
124    /// [`DecodeAccumulator::account_proof_nodes`] for cumulative accounting.
125    pub fn check_proof_node_count(self, count: usize) -> Result<(), DecodeError> {
126        if count > self.max_proof_nodes {
127            return Err(DecodeError::ProofTooLarge);
128        }
129        Ok(())
130    }
131
132    /// Validates one decoded item count against the cumulative item budget.
133    ///
134    /// This helper is for single-count checks only. Decoders that visit items
135    /// incrementally must use [`DecodeAccumulator::account_items`].
136    pub fn check_item_count(self, count: usize) -> Result<(), DecodeError> {
137        if count > self.max_total_items {
138            return Err(DecodeError::ItemCountExceeded);
139        }
140        Ok(())
141    }
142
143    /// Starts stateful budget accounting for a decoder invocation.
144    #[must_use]
145    pub const fn accumulator(self) -> DecodeAccumulator {
146        DecodeAccumulator {
147            limits: self,
148            total_allocated: 0,
149            total_items: 0,
150            proof_nodes: 0,
151        }
152    }
153
154    /// Rejects use of the unchanged deployment starting point as a production
155    /// policy.
156    ///
157    /// This is an identity check against the starter template. Changing only
158    /// one field is likely also a misconfiguration. Review every limit against
159    /// the deployment's concurrency, memory, and protocol constraints before
160    /// treating a policy as production-ready.
161    pub fn validate_deployment_policy(self) -> Result<(), DecodeError> {
162        if self == Self::DEPLOYMENT_STARTING_POINT {
163            return Err(DecodeError::UnreviewedDeploymentPolicy);
164        }
165        Ok(())
166    }
167}
168
169/// Stateful budget accounting for one decoder invocation.
170#[derive(Debug, Eq, PartialEq)]
171pub struct DecodeAccumulator {
172    limits: DecodeLimits,
173    total_allocated: usize,
174    total_items: usize,
175    proof_nodes: usize,
176}
177
178impl DecodeAccumulator {
179    /// Returns the active decode limits.
180    #[must_use]
181    pub const fn limits(&self) -> DecodeLimits {
182        self.limits
183    }
184
185    /// Returns the cumulative allocation accounted so far.
186    #[must_use]
187    pub const fn total_allocated(&self) -> usize {
188        self.total_allocated
189    }
190
191    /// Returns the cumulative decoded items accounted so far.
192    #[must_use]
193    pub const fn total_items(&self) -> usize {
194        self.total_items
195    }
196
197    /// Returns the cumulative proof nodes accounted so far.
198    #[must_use]
199    pub const fn proof_nodes(&self) -> usize {
200        self.proof_nodes
201    }
202
203    /// Validates the input length before parsing starts.
204    pub fn check_input_len(&self, len: usize) -> Result<(), DecodeError> {
205        self.limits.check_input_len(len)
206    }
207
208    /// Validates a decoded list item count.
209    pub fn check_list_count(&self, count: usize) -> Result<(), DecodeError> {
210        self.limits.check_list_count(count)
211    }
212
213    /// Validates the current nesting depth.
214    pub fn check_nesting_depth(&self, depth: usize) -> Result<(), DecodeError> {
215        self.limits.check_nesting_depth(depth)
216    }
217
218    /// Accounts for one allocation against the cumulative allocation budget.
219    pub fn check_allocation(&mut self, size: usize) -> Result<(), DecodeError> {
220        let new_total = self
221            .total_allocated
222            .checked_add(size)
223            .ok_or(DecodeError::AllocationExceeded)?;
224        if new_total > self.limits.max_total_allocation {
225            return Err(DecodeError::AllocationExceeded);
226        }
227        self.total_allocated = new_total;
228        Ok(())
229    }
230
231    /// Accounts for decoded items against the cumulative item budget.
232    pub fn account_items(&mut self, count: usize) -> Result<(), DecodeError> {
233        let new_total = self
234            .total_items
235            .checked_add(count)
236            .ok_or(DecodeError::ItemCountExceeded)?;
237        if new_total > self.limits.max_total_items {
238            return Err(DecodeError::ItemCountExceeded);
239        }
240        self.total_items = new_total;
241        Ok(())
242    }
243
244    /// Accounts for proof nodes against the cumulative proof-node budget.
245    pub fn account_proof_nodes(&mut self, count: usize) -> Result<(), DecodeError> {
246        let new_total = self
247            .proof_nodes
248            .checked_add(count)
249            .ok_or(DecodeError::ProofTooLarge)?;
250        if new_total > self.limits.max_proof_nodes {
251            return Err(DecodeError::ProofTooLarge);
252        }
253        self.proof_nodes = new_total;
254        Ok(())
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn rejects_oversized_input() {
264        let limits = DecodeLimits {
265            max_input_bytes: 2,
266            ..DecodeLimits::TEST_FIXTURE
267        };
268        assert_eq!(limits.check_input_len(3), Err(DecodeError::InputTooLarge));
269    }
270
271    #[test]
272    fn rejects_oversized_list() {
273        let limits = DecodeLimits {
274            max_list_items: 2,
275            ..DecodeLimits::TEST_FIXTURE
276        };
277        assert_eq!(limits.check_list_count(3), Err(DecodeError::ListTooLong));
278    }
279
280    #[test]
281    fn rejects_excessive_nesting_depth() {
282        let limits = DecodeLimits {
283            max_nesting_depth: 2,
284            ..DecodeLimits::TEST_FIXTURE
285        };
286        assert_eq!(
287            limits.check_nesting_depth(3),
288            Err(DecodeError::NestingTooDeep)
289        );
290    }
291
292    #[test]
293    fn rejects_excessive_allocation() {
294        let limits = DecodeLimits {
295            max_total_allocation: 2,
296            ..DecodeLimits::TEST_FIXTURE
297        };
298        assert_eq!(
299            limits.check_single_allocation_limit(3),
300            Err(DecodeError::AllocationExceeded)
301        );
302    }
303
304    #[test]
305    fn rejects_excessive_proof_nodes() {
306        let limits = DecodeLimits {
307            max_proof_nodes: 2,
308            ..DecodeLimits::TEST_FIXTURE
309        };
310        assert_eq!(
311            limits.check_proof_node_count(3),
312            Err(DecodeError::ProofTooLarge)
313        );
314    }
315
316    #[test]
317    fn rejects_excessive_total_items() {
318        let limits = DecodeLimits {
319            max_total_items: 2,
320            ..DecodeLimits::TEST_FIXTURE
321        };
322        assert_eq!(
323            limits.check_item_count(3),
324            Err(DecodeError::ItemCountExceeded)
325        );
326    }
327
328    #[test]
329    fn fixture_and_deployment_starting_point_limits_are_distinct() {
330        let production = DecodeLimits::DEPLOYMENT_STARTING_POINT;
331        let fixture = DecodeLimits::TEST_FIXTURE;
332
333        assert!(production.max_input_bytes > fixture.max_input_bytes);
334        assert!(production.max_total_allocation > fixture.max_total_allocation);
335        assert!(production.max_proof_nodes > fixture.max_proof_nodes);
336        assert!(production.max_total_items > fixture.max_total_items);
337    }
338
339    #[test]
340    fn accumulator_rejects_cumulative_allocation_over_budget() {
341        let limits = DecodeLimits {
342            max_total_allocation: 4,
343            ..DecodeLimits::TEST_FIXTURE
344        };
345        let mut accumulator = limits.accumulator();
346
347        assert_eq!(accumulator.check_allocation(3), Ok(()));
348        assert_eq!(accumulator.total_allocated(), 3);
349        assert_eq!(
350            accumulator.check_allocation(2),
351            Err(DecodeError::AllocationExceeded)
352        );
353        assert_eq!(accumulator.total_allocated(), 3);
354    }
355
356    #[test]
357    fn accumulator_rejects_allocation_counter_overflow() {
358        let limits = DecodeLimits {
359            max_total_allocation: usize::MAX,
360            ..DecodeLimits::TEST_FIXTURE
361        };
362        let mut accumulator = limits.accumulator();
363
364        assert_eq!(accumulator.check_allocation(usize::MAX), Ok(()));
365        assert_eq!(
366            accumulator.check_allocation(1),
367            Err(DecodeError::AllocationExceeded)
368        );
369    }
370
371    #[test]
372    fn accumulator_rejects_cumulative_items_over_budget() {
373        let limits = DecodeLimits {
374            max_total_items: 4,
375            ..DecodeLimits::TEST_FIXTURE
376        };
377        let mut accumulator = limits.accumulator();
378
379        assert_eq!(accumulator.account_items(3), Ok(()));
380        assert_eq!(accumulator.total_items(), 3);
381        assert_eq!(
382            accumulator.account_items(2),
383            Err(DecodeError::ItemCountExceeded)
384        );
385        assert_eq!(accumulator.total_items(), 3);
386    }
387
388    #[test]
389    fn accumulator_rejects_cumulative_proof_nodes_over_budget() {
390        let limits = DecodeLimits {
391            max_proof_nodes: 4,
392            ..DecodeLimits::TEST_FIXTURE
393        };
394        let mut accumulator = limits.accumulator();
395
396        assert_eq!(accumulator.account_proof_nodes(3), Ok(()));
397        assert_eq!(accumulator.proof_nodes(), 3);
398        assert_eq!(
399            accumulator.account_proof_nodes(2),
400            Err(DecodeError::ProofTooLarge)
401        );
402        assert_eq!(accumulator.proof_nodes(), 3);
403    }
404
405    #[test]
406    fn deployment_starting_point_must_be_reviewed_before_use() {
407        assert_eq!(
408            DecodeLimits::DEPLOYMENT_STARTING_POINT.validate_deployment_policy(),
409            Err(DecodeError::UnreviewedDeploymentPolicy)
410        );
411
412        let reviewed = DecodeLimits {
413            max_input_bytes: DecodeLimits::DEPLOYMENT_STARTING_POINT.max_input_bytes / 2,
414            ..DecodeLimits::DEPLOYMENT_STARTING_POINT
415        };
416        assert_eq!(reviewed.validate_deployment_policy(), Ok(()));
417    }
418
419    #[test]
420    fn reviewed_policy_requires_every_limit_explicitly() {
421        let reviewed = DecodeLimits::reviewed_policy(1024, 32, 8, 4096, 16, 128);
422
423        assert_eq!(reviewed.max_input_bytes, 1024);
424        assert_eq!(reviewed.max_list_items, 32);
425        assert_eq!(reviewed.max_nesting_depth, 8);
426        assert_eq!(reviewed.max_total_allocation, 4096);
427        assert_eq!(reviewed.max_proof_nodes, 16);
428        assert_eq!(reviewed.max_total_items, 128);
429        assert_eq!(reviewed.validate_deployment_policy(), Ok(()));
430    }
431}