Skip to main content

eth_valkyoth_codec/rlp/
integer.rs

1use core::convert::TryFrom;
2
3use crate::{DecodeAccumulator, DecodeError, DecodeLimits, require_exact_consumption};
4
5use super::{RlpScalar, RlpScalarForm, decode_rlp_scalar_partial};
6
7/// Ethereum U256 maximum byte width in bytes.
8pub const MAX_RLP_U256_BYTES: usize = 32;
9
10const MAX_U64_BYTES: usize = 8;
11const MAX_U128_BYTES: usize = 16;
12const INTEGER_RADIX_U64: u64 = 256;
13const INTEGER_RADIX_U128: u128 = 256;
14
15/// Borrowed canonical RLP integer.
16///
17/// Ethereum integers are encoded as shortest-form unsigned big-endian bytes.
18/// The empty payload represents zero. A non-empty payload whose first byte is
19/// zero is rejected.
20///
21/// Fields are private so downstream crates cannot construct unvalidated
22/// decoded values and feed them into trusted re-encoding paths.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub struct RlpInteger<'a> {
25    scalar: RlpScalar<'a>,
26}
27
28impl<'a> RlpInteger<'a> {
29    /// Validates a decoded RLP scalar as a canonical integer.
30    pub fn try_from_scalar(scalar: RlpScalar<'a>) -> Result<Self, DecodeError> {
31        validate_rlp_integer_payload(scalar.payload())?;
32        Ok(Self { scalar })
33    }
34
35    /// Returns the underlying scalar byte string.
36    #[must_use]
37    pub const fn scalar(self) -> RlpScalar<'a> {
38        self.scalar
39    }
40
41    /// Returns the canonical integer payload bytes.
42    #[must_use]
43    pub const fn payload(self) -> &'a [u8] {
44        self.scalar.payload()
45    }
46
47    /// Returns the total encoded item length in bytes.
48    #[must_use]
49    pub const fn encoded_len(self) -> usize {
50        self.scalar.encoded_len()
51    }
52
53    /// Returns the RLP header length in bytes.
54    #[must_use]
55    pub const fn header_len(self) -> usize {
56        self.scalar.header_len()
57    }
58
59    /// Returns the canonical scalar form used by the integer encoding.
60    #[must_use]
61    pub const fn form(self) -> RlpScalarForm {
62        self.scalar.form()
63    }
64
65    /// Returns true when the integer value is zero.
66    #[must_use]
67    pub const fn is_zero(self) -> bool {
68        self.scalar.payload().is_empty()
69    }
70
71    /// Converts this integer to `u64` after checking the byte width.
72    ///
73    /// The payload helper re-validates canonicality as part of its public API
74    /// contract. For a correctly constructed [`RlpInteger`], this validation
75    /// was already enforced by [`Self::try_from_scalar`].
76    pub fn to_u64(self) -> Result<u64, DecodeError> {
77        rlp_integer_payload_to_u64(self.payload())
78    }
79
80    /// Converts this integer to `u128` after checking the byte width.
81    ///
82    /// The payload helper re-validates canonicality as part of its public API
83    /// contract. For a correctly constructed [`RlpInteger`], this validation
84    /// was already enforced by [`Self::try_from_scalar`].
85    pub fn to_u128(self) -> Result<u128, DecodeError> {
86        rlp_integer_payload_to_u128(self.payload())
87    }
88
89    /// Converts this integer to right-aligned unsigned 256-bit bytes.
90    ///
91    /// The payload helper re-validates canonicality as part of its public API
92    /// contract. For a correctly constructed [`RlpInteger`], this validation
93    /// was already enforced by [`Self::try_from_scalar`].
94    pub fn to_be_bytes32(self) -> Result<[u8; 32], DecodeError> {
95        rlp_integer_payload_to_u256_bytes(self.payload())
96    }
97}
98
99impl<'a> TryFrom<RlpScalar<'a>> for RlpInteger<'a> {
100    type Error = DecodeError;
101
102    fn try_from(value: RlpScalar<'a>) -> Result<Self, Self::Error> {
103        Self::try_from_scalar(value)
104    }
105}
106
107/// Decodes exactly one canonical RLP integer.
108pub fn decode_rlp_integer<'a>(
109    input: &'a [u8],
110    limits: DecodeLimits,
111) -> Result<RlpInteger<'a>, DecodeError> {
112    let mut accumulator = limits.accumulator();
113    let integer = decode_rlp_integer_partial(input, &mut accumulator)?;
114    require_exact_consumption(integer.encoded_len(), input.len())?;
115    Ok(integer)
116}
117
118/// Decodes one canonical RLP integer from the start of `input`.
119///
120/// Warning: this intentionally accepts trailing bytes. Use
121/// [`decode_rlp_integer`] when the full input must be consumed.
122///
123/// The input-length budget check applies to the full `input` slice, not only
124/// the consumed integer bytes. Callers that decode from a larger outer buffer
125/// must pre-slice before calling this helper.
126pub fn decode_rlp_integer_partial<'a>(
127    input: &'a [u8],
128    accumulator: &mut DecodeAccumulator,
129) -> Result<RlpInteger<'a>, DecodeError> {
130    let scalar = decode_rlp_scalar_partial(input, accumulator)?;
131    RlpInteger::try_from_scalar(scalar)
132}
133
134/// Decodes exactly one canonical RLP integer and converts it to `u64`.
135pub fn decode_rlp_u64(input: &[u8], limits: DecodeLimits) -> Result<u64, DecodeError> {
136    decode_rlp_integer(input, limits)?.to_u64()
137}
138
139/// Decodes exactly one canonical RLP integer and converts it to `u128`.
140pub fn decode_rlp_u128(input: &[u8], limits: DecodeLimits) -> Result<u128, DecodeError> {
141    decode_rlp_integer(input, limits)?.to_u128()
142}
143
144/// Decodes exactly one canonical RLP integer as unsigned 256-bit bytes.
145pub fn decode_rlp_u256_bytes(input: &[u8], limits: DecodeLimits) -> Result<[u8; 32], DecodeError> {
146    decode_rlp_integer(input, limits)?.to_be_bytes32()
147}
148
149/// Validates canonical Ethereum RLP integer payload bytes.
150///
151/// This validates the decoded scalar payload only, not a complete RLP item.
152/// The empty payload represents zero. Non-empty payloads must be shortest-form
153/// unsigned big-endian bytes without a leading zero.
154///
155/// Callers operating on untrusted wire data should use [`decode_rlp_integer`],
156/// which enforces [`DecodeLimits`] before reaching this payload helper.
157pub fn validate_rlp_integer_payload(payload: &[u8]) -> Result<(), DecodeError> {
158    if payload.first().is_some_and(|byte| *byte == 0) {
159        return Err(DecodeError::Malformed);
160    }
161    Ok(())
162}
163
164/// Converts canonical Ethereum RLP integer payload bytes to `u64`.
165///
166/// This is the payload-level source of truth for primitive domains that store
167/// Ethereum integers as `u64`.
168///
169/// Callers operating on untrusted wire data should use [`decode_rlp_u64`],
170/// which enforces [`DecodeLimits`] before reaching this payload helper.
171pub fn rlp_integer_payload_to_u64(payload: &[u8]) -> Result<u64, DecodeError> {
172    validate_rlp_integer_payload(payload)?;
173    if payload.len() > MAX_U64_BYTES {
174        return Err(DecodeError::LengthOverflow);
175    }
176
177    let mut value = 0_u64;
178    for byte in payload {
179        value = value
180            .checked_mul(INTEGER_RADIX_U64)
181            .ok_or(DecodeError::LengthOverflow)?;
182        value = value
183            .checked_add(u64::from(*byte))
184            .ok_or(DecodeError::LengthOverflow)?;
185    }
186    Ok(value)
187}
188
189/// Converts canonical Ethereum RLP integer payload bytes to `u128`.
190///
191/// Callers operating on untrusted wire data should use [`decode_rlp_u128`],
192/// which enforces [`DecodeLimits`] before reaching this payload helper.
193pub fn rlp_integer_payload_to_u128(payload: &[u8]) -> Result<u128, DecodeError> {
194    validate_rlp_integer_payload(payload)?;
195    if payload.len() > MAX_U128_BYTES {
196        return Err(DecodeError::LengthOverflow);
197    }
198
199    let mut value = 0_u128;
200    for byte in payload {
201        value = value
202            .checked_mul(INTEGER_RADIX_U128)
203            .ok_or(DecodeError::LengthOverflow)?;
204        value = value
205            .checked_add(u128::from(*byte))
206            .ok_or(DecodeError::LengthOverflow)?;
207    }
208    Ok(value)
209}
210
211/// Converts canonical Ethereum RLP integer payload bytes to right-aligned U256.
212///
213/// Callers operating on untrusted wire data should use
214/// [`decode_rlp_u256_bytes`], which enforces [`DecodeLimits`] before reaching
215/// this payload helper.
216pub fn rlp_integer_payload_to_u256_bytes(payload: &[u8]) -> Result<[u8; 32], DecodeError> {
217    validate_rlp_integer_payload(payload)?;
218    if payload.len() > MAX_RLP_U256_BYTES {
219        return Err(DecodeError::LengthOverflow);
220    }
221
222    let mut output = [0_u8; 32];
223    let start = MAX_RLP_U256_BYTES
224        .checked_sub(payload.len())
225        .ok_or(DecodeError::LengthOverflow)?;
226    // start is 32 - payload.len(), with payload.len() <= 32, so the range is
227    // always inside output. Keep the Result form to satisfy indexing policy.
228    let target = output
229        .get_mut(start..)
230        .ok_or(DecodeError::OffsetOutOfBounds)?;
231    target.copy_from_slice(payload);
232    Ok(output)
233}