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