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