Skip to main content

eth_valkyoth_codec/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3//! Bounded decoding policy for untrusted Ethereum wire inputs.
4
5#[cfg(feature = "std")]
6extern crate std;
7
8use core::{cmp::Ordering, fmt};
9
10/// Resource limits required by every untrusted decoder.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub struct DecodeLimits {
13    /// Maximum accepted input length in bytes.
14    pub max_input_bytes: usize,
15    /// Maximum items accepted in any decoded list.
16    pub max_list_items: usize,
17    /// Maximum nested list depth.
18    pub max_nesting_depth: usize,
19    /// Maximum total allocation a decoder may request.
20    pub max_total_allocation: usize,
21}
22
23impl DecodeLimits {
24    /// Limits for unit tests and conformance fixtures.
25    ///
26    /// This is not a production policy. Production decoders should choose
27    /// deployment-specific limits or start from [`Self::PRODUCTION_RECOMMENDED`].
28    pub const TEST_FIXTURE: Self = Self {
29        max_input_bytes: 1 << 20,
30        max_list_items: 4096,
31        max_nesting_depth: 64,
32        max_total_allocation: 1 << 20,
33    };
34
35    /// Recommended starting point for production wire decoders.
36    ///
37    /// Review and tighten these values per deployment context before relying
38    /// on them for externally reachable services.
39    pub const PRODUCTION_RECOMMENDED: 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    };
45
46    /// Validates the input length before parsing starts.
47    pub fn check_input_len(self, len: usize) -> Result<(), DecodeError> {
48        if len > self.max_input_bytes {
49            return Err(DecodeError::InputTooLarge);
50        }
51        Ok(())
52    }
53
54    /// Validates a decoded list item count.
55    pub fn check_list_count(self, count: usize) -> Result<(), DecodeError> {
56        if count > self.max_list_items {
57            return Err(DecodeError::ListTooLong);
58        }
59        Ok(())
60    }
61
62    /// Validates the current nesting depth.
63    pub fn check_nesting_depth(self, depth: usize) -> Result<(), DecodeError> {
64        if depth > self.max_nesting_depth {
65            return Err(DecodeError::NestingTooDeep);
66        }
67        Ok(())
68    }
69
70    /// Validates one requested allocation against the allocation budget.
71    ///
72    /// This helper is for single-allocation checks only. Decoders that can make
73    /// more than one allocation must use [`DecodeAccumulator`] to enforce the
74    /// cumulative budget.
75    pub fn check_single_allocation_limit(self, size: usize) -> Result<(), DecodeError> {
76        if size > self.max_total_allocation {
77            return Err(DecodeError::AllocationExceeded);
78        }
79        Ok(())
80    }
81
82    /// Starts stateful budget accounting for a decoder invocation.
83    #[must_use]
84    pub const fn accumulator(self) -> DecodeAccumulator {
85        DecodeAccumulator {
86            limits: self,
87            total_allocated: 0,
88        }
89    }
90}
91
92/// Stateful budget accounting for one decoder invocation.
93#[derive(Debug, Eq, PartialEq)]
94pub struct DecodeAccumulator {
95    limits: DecodeLimits,
96    total_allocated: usize,
97}
98
99impl DecodeAccumulator {
100    /// Returns the active decode limits.
101    #[must_use]
102    pub const fn limits(&self) -> DecodeLimits {
103        self.limits
104    }
105
106    /// Returns the cumulative allocation accounted so far.
107    #[must_use]
108    pub const fn total_allocated(&self) -> usize {
109        self.total_allocated
110    }
111
112    /// Validates the input length before parsing starts.
113    pub fn check_input_len(&self, len: usize) -> Result<(), DecodeError> {
114        self.limits.check_input_len(len)
115    }
116
117    /// Validates a decoded list item count.
118    pub fn check_list_count(&self, count: usize) -> Result<(), DecodeError> {
119        self.limits.check_list_count(count)
120    }
121
122    /// Validates the current nesting depth.
123    pub fn check_nesting_depth(&self, depth: usize) -> Result<(), DecodeError> {
124        self.limits.check_nesting_depth(depth)
125    }
126
127    /// Accounts for one allocation against the cumulative allocation budget.
128    pub fn check_allocation(&mut self, size: usize) -> Result<(), DecodeError> {
129        let new_total = self
130            .total_allocated
131            .checked_add(size)
132            .ok_or(DecodeError::AllocationExceeded)?;
133        if new_total > self.limits.max_total_allocation {
134            return Err(DecodeError::AllocationExceeded);
135        }
136        self.total_allocated = new_total;
137        Ok(())
138    }
139}
140
141/// Shared decode failure categories.
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
143pub enum DecodeError {
144    /// The byte input is larger than the active decode budget.
145    InputTooLarge,
146    /// The input contains trailing bytes after a decoded value.
147    TrailingBytes,
148    /// A decoder reported consuming more bytes than the input contains.
149    DecoderOverread,
150    /// The input is malformed for the selected wire format.
151    Malformed,
152    /// A decoded list contains more items than the active decode budget.
153    ListTooLong,
154    /// Decoding exceeded the active nesting-depth budget.
155    NestingTooDeep,
156    /// A decoder requested allocation beyond the active allocation budget.
157    AllocationExceeded,
158}
159
160impl DecodeError {
161    /// Stable machine-readable error code.
162    #[must_use]
163    pub const fn code(self) -> &'static str {
164        match self {
165            Self::InputTooLarge => "ETH_CODEC_INPUT_TOO_LARGE",
166            Self::TrailingBytes => "ETH_CODEC_TRAILING_BYTES",
167            Self::DecoderOverread => "ETH_CODEC_DECODER_OVERREAD",
168            Self::Malformed => "ETH_CODEC_MALFORMED",
169            Self::ListTooLong => "ETH_CODEC_LIST_TOO_LONG",
170            Self::NestingTooDeep => "ETH_CODEC_NESTING_TOO_DEEP",
171            Self::AllocationExceeded => "ETH_CODEC_ALLOCATION_EXCEEDED",
172        }
173    }
174
175    /// Stable human-readable error message.
176    #[must_use]
177    pub const fn message(self) -> &'static str {
178        match self {
179            Self::InputTooLarge => "input exceeds the active decode byte limit",
180            Self::TrailingBytes => "decoded value did not consume the full input",
181            Self::DecoderOverread => "decoder consumed more bytes than were available",
182            Self::Malformed => "input is malformed for the selected codec",
183            Self::ListTooLong => "decoded list exceeds the active item limit",
184            Self::NestingTooDeep => "decoded structure exceeds the active nesting limit",
185            Self::AllocationExceeded => "decoder exceeded the active allocation limit",
186        }
187    }
188
189    /// Stable high-level category for policy decisions.
190    #[must_use]
191    pub const fn category(self) -> DecodeErrorCategory {
192        match self {
193            Self::InputTooLarge
194            | Self::ListTooLong
195            | Self::NestingTooDeep
196            | Self::AllocationExceeded => DecodeErrorCategory::ResourceExhaustion,
197            Self::TrailingBytes | Self::DecoderOverread | Self::Malformed => {
198                DecodeErrorCategory::MalformedInput
199            }
200        }
201    }
202
203    /// Returns the resource budget that was exceeded, if this is a resource
204    /// exhaustion error.
205    #[must_use]
206    pub const fn resource(self) -> Option<ResourceError> {
207        match self {
208            Self::InputTooLarge => Some(ResourceError::InputBytes),
209            Self::ListTooLong => Some(ResourceError::ListItems),
210            Self::NestingTooDeep => Some(ResourceError::NestingDepth),
211            Self::AllocationExceeded => Some(ResourceError::AllocationBytes),
212            Self::TrailingBytes | Self::DecoderOverread | Self::Malformed => None,
213        }
214    }
215}
216
217impl fmt::Display for DecodeError {
218    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219        formatter.write_str(self.message())
220    }
221}
222
223#[cfg(feature = "std")]
224impl std::error::Error for DecodeError {}
225
226/// Stable high-level decode error categories.
227#[derive(Clone, Copy, Debug, Eq, PartialEq)]
228pub enum DecodeErrorCategory {
229    /// The input is malformed or internally inconsistent.
230    MalformedInput,
231    /// The input exceeded an explicit resource budget.
232    ResourceExhaustion,
233}
234
235/// Stable resource budget categories.
236#[derive(Clone, Copy, Debug, Eq, PartialEq)]
237pub enum ResourceError {
238    /// Input byte limit was exceeded.
239    InputBytes,
240    /// Decoded list item limit was exceeded.
241    ListItems,
242    /// Nesting depth limit was exceeded.
243    NestingDepth,
244    /// Cumulative allocation limit was exceeded.
245    AllocationBytes,
246}
247
248impl ResourceError {
249    /// Stable machine-readable error code.
250    #[must_use]
251    pub const fn code(self) -> &'static str {
252        match self {
253            Self::InputBytes => "ETH_RESOURCE_INPUT_BYTES",
254            Self::ListItems => "ETH_RESOURCE_LIST_ITEMS",
255            Self::NestingDepth => "ETH_RESOURCE_NESTING_DEPTH",
256            Self::AllocationBytes => "ETH_RESOURCE_ALLOCATION_BYTES",
257        }
258    }
259
260    /// Stable human-readable error message.
261    #[must_use]
262    pub const fn message(self) -> &'static str {
263        match self {
264            Self::InputBytes => "input byte budget exceeded",
265            Self::ListItems => "list item budget exceeded",
266            Self::NestingDepth => "nesting depth budget exceeded",
267            Self::AllocationBytes => "allocation byte budget exceeded",
268        }
269    }
270}
271
272impl fmt::Display for ResourceError {
273    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
274        formatter.write_str(self.message())
275    }
276}
277
278#[cfg(feature = "std")]
279impl std::error::Error for ResourceError {}
280
281/// Ensures a decoder consumed the whole input.
282pub fn require_exact_consumption(consumed: usize, input_len: usize) -> Result<(), DecodeError> {
283    match consumed.cmp(&input_len) {
284        Ordering::Equal => Ok(()),
285        Ordering::Less => Err(DecodeError::TrailingBytes),
286        Ordering::Greater => Err(DecodeError::DecoderOverread),
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    extern crate std;
294    use std::string::ToString;
295
296    #[test]
297    fn rejects_oversized_input() {
298        let limits = DecodeLimits {
299            max_input_bytes: 2,
300            ..DecodeLimits::TEST_FIXTURE
301        };
302        assert_eq!(limits.check_input_len(3), Err(DecodeError::InputTooLarge));
303    }
304
305    #[test]
306    fn rejects_oversized_list() {
307        let limits = DecodeLimits {
308            max_list_items: 2,
309            ..DecodeLimits::TEST_FIXTURE
310        };
311        assert_eq!(limits.check_list_count(3), Err(DecodeError::ListTooLong));
312    }
313
314    #[test]
315    fn rejects_excessive_nesting_depth() {
316        let limits = DecodeLimits {
317            max_nesting_depth: 2,
318            ..DecodeLimits::TEST_FIXTURE
319        };
320        assert_eq!(
321            limits.check_nesting_depth(3),
322            Err(DecodeError::NestingTooDeep)
323        );
324    }
325
326    #[test]
327    fn rejects_excessive_allocation() {
328        let limits = DecodeLimits {
329            max_total_allocation: 2,
330            ..DecodeLimits::TEST_FIXTURE
331        };
332        assert_eq!(
333            limits.check_single_allocation_limit(3),
334            Err(DecodeError::AllocationExceeded)
335        );
336    }
337
338    #[test]
339    fn fixture_and_production_limits_are_distinct() {
340        let production = DecodeLimits::PRODUCTION_RECOMMENDED;
341        let fixture = DecodeLimits::TEST_FIXTURE;
342
343        assert!(production.max_input_bytes > fixture.max_input_bytes);
344        assert!(production.max_total_allocation > fixture.max_total_allocation);
345    }
346
347    #[test]
348    fn decode_errors_have_stable_codes_and_messages() {
349        assert_eq!(DecodeError::Malformed.code(), "ETH_CODEC_MALFORMED");
350        assert_eq!(
351            DecodeError::Malformed.message(),
352            "input is malformed for the selected codec"
353        );
354        assert_eq!(
355            DecodeError::Malformed.category(),
356            DecodeErrorCategory::MalformedInput
357        );
358        assert_eq!(
359            DecodeError::Malformed.to_string(),
360            "input is malformed for the selected codec"
361        );
362    }
363
364    #[test]
365    fn resource_errors_are_classified_without_payloads() {
366        let error = DecodeError::AllocationExceeded;
367
368        assert_eq!(error.category(), DecodeErrorCategory::ResourceExhaustion);
369        assert_eq!(error.resource(), Some(ResourceError::AllocationBytes));
370        assert_eq!(
371            ResourceError::AllocationBytes.code(),
372            "ETH_RESOURCE_ALLOCATION_BYTES"
373        );
374        assert_eq!(
375            ResourceError::AllocationBytes.to_string(),
376            "allocation byte budget exceeded"
377        );
378    }
379
380    #[test]
381    fn accumulator_rejects_cumulative_allocation_over_budget() {
382        let limits = DecodeLimits {
383            max_total_allocation: 4,
384            ..DecodeLimits::TEST_FIXTURE
385        };
386        let mut accumulator = limits.accumulator();
387
388        assert_eq!(accumulator.check_allocation(3), Ok(()));
389        assert_eq!(accumulator.total_allocated(), 3);
390        assert_eq!(
391            accumulator.check_allocation(2),
392            Err(DecodeError::AllocationExceeded)
393        );
394        assert_eq!(accumulator.total_allocated(), 3);
395    }
396
397    #[test]
398    fn accumulator_rejects_allocation_counter_overflow() {
399        let limits = DecodeLimits {
400            max_total_allocation: usize::MAX,
401            ..DecodeLimits::TEST_FIXTURE
402        };
403        let mut accumulator = limits.accumulator();
404
405        assert_eq!(accumulator.check_allocation(usize::MAX), Ok(()));
406        assert_eq!(
407            accumulator.check_allocation(1),
408            Err(DecodeError::AllocationExceeded)
409        );
410    }
411
412    #[test]
413    fn detects_trailing_bytes() {
414        assert_eq!(
415            require_exact_consumption(1, 2),
416            Err(DecodeError::TrailingBytes)
417        );
418    }
419
420    #[test]
421    fn detects_decoder_overread() {
422        assert_eq!(
423            require_exact_consumption(3, 2),
424            Err(DecodeError::DecoderOverread)
425        );
426    }
427}