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. Copy it, review every limit against the deployment's
44    /// concurrency and memory policy, and tighten values before release.
45    #[doc(alias = "DEPLOYMENT_TEMPLATE")]
46    pub const DEPLOYMENT_STARTING_POINT: Self = Self {
47        max_input_bytes: 2 << 20,
48        max_list_items: 16_384,
49        max_nesting_depth: 64,
50        max_total_allocation: 4 << 20,
51        max_proof_nodes: 4096,
52        max_total_items: 65_536,
53    };
54
55    /// Validates the input length before parsing starts.
56    pub fn check_input_len(self, len: usize) -> Result<(), DecodeError> {
57        if len > self.max_input_bytes {
58            return Err(DecodeError::InputTooLarge);
59        }
60        Ok(())
61    }
62
63    /// Validates a decoded list item count.
64    pub fn check_list_count(self, count: usize) -> Result<(), DecodeError> {
65        if count > self.max_list_items {
66            return Err(DecodeError::ListTooLong);
67        }
68        Ok(())
69    }
70
71    /// Validates the current nesting depth.
72    ///
73    /// RLP list decoding also enforces [`crate::MAX_RLP_LIST_TRAVERSAL_DEPTH`]
74    /// as a hard traversal-stack cap.
75    pub fn check_nesting_depth(self, depth: usize) -> Result<(), DecodeError> {
76        if depth > self.max_nesting_depth {
77            return Err(DecodeError::NestingTooDeep);
78        }
79        Ok(())
80    }
81
82    /// Validates one requested allocation against the allocation budget.
83    ///
84    /// This helper is for single-allocation checks only. Decoders that can make
85    /// more than one allocation must use [`DecodeAccumulator`] to enforce the
86    /// cumulative budget.
87    pub fn check_single_allocation_limit(self, size: usize) -> Result<(), DecodeError> {
88        if size > self.max_total_allocation {
89            return Err(DecodeError::AllocationExceeded);
90        }
91        Ok(())
92    }
93
94    /// Validates one proof-node count against the proof-node budget.
95    ///
96    /// Decoders that traverse more than one proof segment must use
97    /// [`DecodeAccumulator::account_proof_nodes`] for cumulative accounting.
98    pub fn check_proof_node_count(self, count: usize) -> Result<(), DecodeError> {
99        if count > self.max_proof_nodes {
100            return Err(DecodeError::ProofTooLarge);
101        }
102        Ok(())
103    }
104
105    /// Validates one decoded item count against the cumulative item budget.
106    ///
107    /// This helper is for single-count checks only. Decoders that visit items
108    /// incrementally must use [`DecodeAccumulator::account_items`].
109    pub fn check_item_count(self, count: usize) -> Result<(), DecodeError> {
110        if count > self.max_total_items {
111            return Err(DecodeError::ItemCountExceeded);
112        }
113        Ok(())
114    }
115
116    /// Starts stateful budget accounting for a decoder invocation.
117    #[must_use]
118    pub const fn accumulator(self) -> DecodeAccumulator {
119        DecodeAccumulator {
120            limits: self,
121            total_allocated: 0,
122            total_items: 0,
123            proof_nodes: 0,
124        }
125    }
126
127    /// Rejects use of the unchanged deployment starting point as a final
128    /// production policy.
129    pub fn validate_deployment_policy(self) -> Result<(), DecodeError> {
130        if self == Self::DEPLOYMENT_STARTING_POINT {
131            return Err(DecodeError::UnreviewedDeploymentPolicy);
132        }
133        Ok(())
134    }
135}
136
137/// Stateful budget accounting for one decoder invocation.
138#[derive(Debug, Eq, PartialEq)]
139pub struct DecodeAccumulator {
140    limits: DecodeLimits,
141    total_allocated: usize,
142    total_items: usize,
143    proof_nodes: usize,
144}
145
146impl DecodeAccumulator {
147    /// Returns the active decode limits.
148    #[must_use]
149    pub const fn limits(&self) -> DecodeLimits {
150        self.limits
151    }
152
153    /// Returns the cumulative allocation accounted so far.
154    #[must_use]
155    pub const fn total_allocated(&self) -> usize {
156        self.total_allocated
157    }
158
159    /// Returns the cumulative decoded items accounted so far.
160    #[must_use]
161    pub const fn total_items(&self) -> usize {
162        self.total_items
163    }
164
165    /// Returns the cumulative proof nodes accounted so far.
166    #[must_use]
167    pub const fn proof_nodes(&self) -> usize {
168        self.proof_nodes
169    }
170
171    /// Validates the input length before parsing starts.
172    pub fn check_input_len(&self, len: usize) -> Result<(), DecodeError> {
173        self.limits.check_input_len(len)
174    }
175
176    /// Validates a decoded list item count.
177    pub fn check_list_count(&self, count: usize) -> Result<(), DecodeError> {
178        self.limits.check_list_count(count)
179    }
180
181    /// Validates the current nesting depth.
182    pub fn check_nesting_depth(&self, depth: usize) -> Result<(), DecodeError> {
183        self.limits.check_nesting_depth(depth)
184    }
185
186    /// Accounts for one allocation against the cumulative allocation budget.
187    pub fn check_allocation(&mut self, size: usize) -> Result<(), DecodeError> {
188        let new_total = self
189            .total_allocated
190            .checked_add(size)
191            .ok_or(DecodeError::AllocationExceeded)?;
192        if new_total > self.limits.max_total_allocation {
193            return Err(DecodeError::AllocationExceeded);
194        }
195        self.total_allocated = new_total;
196        Ok(())
197    }
198
199    /// Accounts for decoded items against the cumulative item budget.
200    pub fn account_items(&mut self, count: usize) -> Result<(), DecodeError> {
201        let new_total = self
202            .total_items
203            .checked_add(count)
204            .ok_or(DecodeError::ItemCountExceeded)?;
205        if new_total > self.limits.max_total_items {
206            return Err(DecodeError::ItemCountExceeded);
207        }
208        self.total_items = new_total;
209        Ok(())
210    }
211
212    /// Accounts for proof nodes against the cumulative proof-node budget.
213    pub fn account_proof_nodes(&mut self, count: usize) -> Result<(), DecodeError> {
214        let new_total = self
215            .proof_nodes
216            .checked_add(count)
217            .ok_or(DecodeError::ProofTooLarge)?;
218        if new_total > self.limits.max_proof_nodes {
219            return Err(DecodeError::ProofTooLarge);
220        }
221        self.proof_nodes = new_total;
222        Ok(())
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn rejects_oversized_input() {
232        let limits = DecodeLimits {
233            max_input_bytes: 2,
234            ..DecodeLimits::TEST_FIXTURE
235        };
236        assert_eq!(limits.check_input_len(3), Err(DecodeError::InputTooLarge));
237    }
238
239    #[test]
240    fn rejects_oversized_list() {
241        let limits = DecodeLimits {
242            max_list_items: 2,
243            ..DecodeLimits::TEST_FIXTURE
244        };
245        assert_eq!(limits.check_list_count(3), Err(DecodeError::ListTooLong));
246    }
247
248    #[test]
249    fn rejects_excessive_nesting_depth() {
250        let limits = DecodeLimits {
251            max_nesting_depth: 2,
252            ..DecodeLimits::TEST_FIXTURE
253        };
254        assert_eq!(
255            limits.check_nesting_depth(3),
256            Err(DecodeError::NestingTooDeep)
257        );
258    }
259
260    #[test]
261    fn rejects_excessive_allocation() {
262        let limits = DecodeLimits {
263            max_total_allocation: 2,
264            ..DecodeLimits::TEST_FIXTURE
265        };
266        assert_eq!(
267            limits.check_single_allocation_limit(3),
268            Err(DecodeError::AllocationExceeded)
269        );
270    }
271
272    #[test]
273    fn rejects_excessive_proof_nodes() {
274        let limits = DecodeLimits {
275            max_proof_nodes: 2,
276            ..DecodeLimits::TEST_FIXTURE
277        };
278        assert_eq!(
279            limits.check_proof_node_count(3),
280            Err(DecodeError::ProofTooLarge)
281        );
282    }
283
284    #[test]
285    fn rejects_excessive_total_items() {
286        let limits = DecodeLimits {
287            max_total_items: 2,
288            ..DecodeLimits::TEST_FIXTURE
289        };
290        assert_eq!(
291            limits.check_item_count(3),
292            Err(DecodeError::ItemCountExceeded)
293        );
294    }
295
296    #[test]
297    fn fixture_and_deployment_starting_point_limits_are_distinct() {
298        let production = DecodeLimits::DEPLOYMENT_STARTING_POINT;
299        let fixture = DecodeLimits::TEST_FIXTURE;
300
301        assert!(production.max_input_bytes > fixture.max_input_bytes);
302        assert!(production.max_total_allocation > fixture.max_total_allocation);
303        assert!(production.max_proof_nodes > fixture.max_proof_nodes);
304        assert!(production.max_total_items > fixture.max_total_items);
305    }
306
307    #[test]
308    fn accumulator_rejects_cumulative_allocation_over_budget() {
309        let limits = DecodeLimits {
310            max_total_allocation: 4,
311            ..DecodeLimits::TEST_FIXTURE
312        };
313        let mut accumulator = limits.accumulator();
314
315        assert_eq!(accumulator.check_allocation(3), Ok(()));
316        assert_eq!(accumulator.total_allocated(), 3);
317        assert_eq!(
318            accumulator.check_allocation(2),
319            Err(DecodeError::AllocationExceeded)
320        );
321        assert_eq!(accumulator.total_allocated(), 3);
322    }
323
324    #[test]
325    fn accumulator_rejects_allocation_counter_overflow() {
326        let limits = DecodeLimits {
327            max_total_allocation: usize::MAX,
328            ..DecodeLimits::TEST_FIXTURE
329        };
330        let mut accumulator = limits.accumulator();
331
332        assert_eq!(accumulator.check_allocation(usize::MAX), Ok(()));
333        assert_eq!(
334            accumulator.check_allocation(1),
335            Err(DecodeError::AllocationExceeded)
336        );
337    }
338
339    #[test]
340    fn accumulator_rejects_cumulative_items_over_budget() {
341        let limits = DecodeLimits {
342            max_total_items: 4,
343            ..DecodeLimits::TEST_FIXTURE
344        };
345        let mut accumulator = limits.accumulator();
346
347        assert_eq!(accumulator.account_items(3), Ok(()));
348        assert_eq!(accumulator.total_items(), 3);
349        assert_eq!(
350            accumulator.account_items(2),
351            Err(DecodeError::ItemCountExceeded)
352        );
353        assert_eq!(accumulator.total_items(), 3);
354    }
355
356    #[test]
357    fn accumulator_rejects_cumulative_proof_nodes_over_budget() {
358        let limits = DecodeLimits {
359            max_proof_nodes: 4,
360            ..DecodeLimits::TEST_FIXTURE
361        };
362        let mut accumulator = limits.accumulator();
363
364        assert_eq!(accumulator.account_proof_nodes(3), Ok(()));
365        assert_eq!(accumulator.proof_nodes(), 3);
366        assert_eq!(
367            accumulator.account_proof_nodes(2),
368            Err(DecodeError::ProofTooLarge)
369        );
370        assert_eq!(accumulator.proof_nodes(), 3);
371    }
372
373    #[test]
374    fn deployment_starting_point_must_be_reviewed_before_use() {
375        assert_eq!(
376            DecodeLimits::DEPLOYMENT_STARTING_POINT.validate_deployment_policy(),
377            Err(DecodeError::UnreviewedDeploymentPolicy)
378        );
379
380        let reviewed = DecodeLimits {
381            max_input_bytes: DecodeLimits::DEPLOYMENT_STARTING_POINT.max_input_bytes / 2,
382            ..DecodeLimits::DEPLOYMENT_STARTING_POINT
383        };
384        assert_eq!(reviewed.validate_deployment_policy(), Ok(()));
385    }
386}