Skip to main content

eth_valkyoth_primitives/
rlp.rs

1use core::fmt;
2
3use eth_valkyoth_codec::{
4    DecodeError, DecodeLimits, decode_rlp_scalar, decode_rlp_u64, decode_rlp_u256_bytes,
5    encode_rlp_integer, encode_rlp_scalar, encoded_rlp_integer_len, encoded_rlp_scalar_len,
6};
7
8use crate::{Address, B256, BlockNumber, ChainId, Gas, Nonce, PrimitiveError, UnixTimestamp, Wei};
9
10/// Primitive RLP bridge failures.
11///
12/// This enum is `#[non_exhaustive]`. New variants may be added in minor
13/// releases. Downstream wildcard `match` arms can silently handle new failure
14/// categories, so security monitors should re-audit wildcard arms when this
15/// type changes.
16#[non_exhaustive]
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum PrimitiveRlpError {
19    /// The underlying bounded RLP codec rejected the input or output buffer.
20    Decode(DecodeError),
21    /// The decoded payload did not fit the target primitive domain.
22    Primitive(PrimitiveError),
23    /// A fixed-width primitive was encoded with the wrong scalar byte length.
24    FixedWidthScalar {
25        /// Required scalar payload width for the target primitive.
26        expected: usize,
27        /// Actual decoded scalar payload width.
28        found: usize,
29    },
30}
31
32impl From<DecodeError> for PrimitiveRlpError {
33    fn from(error: DecodeError) -> Self {
34        Self::Decode(error)
35    }
36}
37
38impl From<PrimitiveError> for PrimitiveRlpError {
39    fn from(error: PrimitiveError) -> Self {
40        Self::Primitive(error)
41    }
42}
43
44impl fmt::Display for PrimitiveRlpError {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Self::Decode(error) => write!(f, "RLP decode error: {error}"),
48            Self::Primitive(error) => write!(f, "primitive domain error: {error}"),
49            Self::FixedWidthScalar { expected, found } => write!(
50                f,
51                "RLP scalar payload has wrong byte width for this primitive: expected {expected}, found {found}"
52            ),
53        }
54    }
55}
56
57#[cfg(feature = "std")]
58impl std::error::Error for PrimitiveRlpError {
59    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60        match self {
61            Self::Decode(error) => Some(error),
62            Self::Primitive(error) => Some(error),
63            Self::FixedWidthScalar { .. } => None,
64        }
65    }
66}
67
68macro_rules! rlp_u64_bridge {
69    ($name:ident) => {
70        impl $name {
71            /// Returns the canonical RLP encoded length.
72            pub fn encoded_rlp_len(self) -> Result<usize, PrimitiveRlpError> {
73                encoded_u64_len(self.get())
74            }
75
76            /// Canonically encodes this value into `output`.
77            ///
78            /// Returns the number of bytes written. `output` is not modified
79            /// unless this function returns `Ok`.
80            pub fn encode_rlp(self, output: &mut [u8]) -> Result<usize, PrimitiveRlpError> {
81                encode_u64(self.get(), output)
82            }
83
84            /// Decodes exactly one canonical RLP integer into this domain.
85            pub fn try_from_rlp(
86                input: &[u8],
87                limits: DecodeLimits,
88            ) -> Result<Self, PrimitiveRlpError> {
89                decode_rlp_u64(input, limits)
90                    .map(Self::new)
91                    .map_err(Into::into)
92            }
93        }
94    };
95}
96
97impl ChainId {
98    /// Returns the canonical RLP encoded length.
99    pub fn encoded_rlp_len(self) -> Result<usize, PrimitiveRlpError> {
100        encoded_u64_len(self.get())
101    }
102
103    /// Canonically encodes this value into `output`.
104    ///
105    /// Returns the number of bytes written. `output` is not modified unless
106    /// this function returns `Ok`.
107    pub fn encode_rlp(self, output: &mut [u8]) -> Result<usize, PrimitiveRlpError> {
108        encode_u64(self.get(), output)
109    }
110
111    /// Decodes exactly one canonical RLP integer into this domain.
112    ///
113    /// **EIP-155 note**: This function accepts `ChainId(0)`, which is reserved
114    /// for unsigned legacy transactions. Callers that validate signed
115    /// transaction chain IDs must reject `ChainId(0)` independently; this
116    /// decoder does not enforce that constraint.
117    pub fn try_from_rlp(input: &[u8], limits: DecodeLimits) -> Result<Self, PrimitiveRlpError> {
118        decode_rlp_u64(input, limits)
119            .map(Self::new)
120            .map_err(Into::into)
121    }
122
123    /// Decodes a signed EIP-155 transaction chain ID.
124    ///
125    /// Rejects `0`, which is reserved for unsigned legacy transactions and
126    /// must not be accepted as a signed transaction replay domain.
127    pub fn try_from_rlp_signed(
128        input: &[u8],
129        limits: DecodeLimits,
130    ) -> Result<Self, PrimitiveRlpError> {
131        let chain_id = Self::try_from_rlp(input, limits)?;
132        if chain_id.get() == 0 {
133            return Err(PrimitiveRlpError::Primitive(
134                PrimitiveError::ReservedLegacyType,
135            ));
136        }
137        Ok(chain_id)
138    }
139}
140
141rlp_u64_bridge!(BlockNumber);
142rlp_u64_bridge!(Gas);
143rlp_u64_bridge!(Nonce);
144rlp_u64_bridge!(UnixTimestamp);
145
146impl Wei {
147    /// Returns the canonical RLP encoded length.
148    pub fn encoded_rlp_len(self) -> Result<usize, PrimitiveRlpError> {
149        let bytes = self.to_be_bytes();
150        encoded_rlp_integer_len(trim_u256_payload(&bytes)).map_err(Into::into)
151    }
152
153    /// Canonically encodes this value into `output`.
154    ///
155    /// Returns the number of bytes written. `output` is not modified unless
156    /// this function returns `Ok`.
157    pub fn encode_rlp(self, output: &mut [u8]) -> Result<usize, PrimitiveRlpError> {
158        let bytes = self.to_be_bytes();
159        encode_rlp_integer(trim_u256_payload(&bytes), output).map_err(Into::into)
160    }
161
162    /// Decodes exactly one canonical RLP U256 integer into `Wei`.
163    pub fn try_from_rlp(input: &[u8], limits: DecodeLimits) -> Result<Self, PrimitiveRlpError> {
164        decode_rlp_u256_bytes(input, limits)
165            .map(Self::from_be_bytes)
166            .map_err(Into::into)
167    }
168}
169
170impl Address {
171    /// Returns the canonical RLP encoded length.
172    ///
173    /// This is currently infallible for the fixed 20-byte address payload but
174    /// keeps the same `Result` shape as the other primitive bridge helpers.
175    pub fn encoded_rlp_len(self) -> Result<usize, PrimitiveRlpError> {
176        encoded_rlp_scalar_len(&self.to_bytes()).map_err(Into::into)
177    }
178
179    /// Canonically encodes this address as a fixed-width scalar.
180    ///
181    /// Returns the number of bytes written. `output` is not modified unless
182    /// this function returns `Ok`.
183    pub fn encode_rlp(self, output: &mut [u8]) -> Result<usize, PrimitiveRlpError> {
184        encode_rlp_scalar(&self.to_bytes(), output).map_err(Into::into)
185    }
186
187    /// Decodes exactly one fixed-width address scalar.
188    pub fn try_from_rlp(input: &[u8], limits: DecodeLimits) -> Result<Self, PrimitiveRlpError> {
189        let scalar = decode_rlp_scalar(input, limits)?;
190        let found = scalar.payload().len();
191        let bytes: [u8; 20] =
192            scalar
193                .payload()
194                .try_into()
195                .map_err(|_| PrimitiveRlpError::FixedWidthScalar {
196                    expected: 20,
197                    found,
198                })?;
199        Ok(Self::from_bytes(bytes))
200    }
201}
202
203impl B256 {
204    /// Returns the canonical RLP encoded length.
205    ///
206    /// This is currently infallible for the fixed 32-byte hash payload but
207    /// keeps the same `Result` shape as the other primitive bridge helpers.
208    pub fn encoded_rlp_len(self) -> Result<usize, PrimitiveRlpError> {
209        encoded_rlp_scalar_len(&self.to_bytes()).map_err(Into::into)
210    }
211
212    /// Canonically encodes this hash as a fixed-width scalar.
213    ///
214    /// Returns the number of bytes written. `output` is not modified unless
215    /// this function returns `Ok`.
216    pub fn encode_rlp(self, output: &mut [u8]) -> Result<usize, PrimitiveRlpError> {
217        encode_rlp_scalar(&self.to_bytes(), output).map_err(Into::into)
218    }
219
220    /// Decodes exactly one fixed-width hash scalar.
221    pub fn try_from_rlp(input: &[u8], limits: DecodeLimits) -> Result<Self, PrimitiveRlpError> {
222        let scalar = decode_rlp_scalar(input, limits)?;
223        let found = scalar.payload().len();
224        let bytes: [u8; 32] =
225            scalar
226                .payload()
227                .try_into()
228                .map_err(|_| PrimitiveRlpError::FixedWidthScalar {
229                    expected: 32,
230                    found,
231                })?;
232        Ok(Self::from_bytes(bytes))
233    }
234}
235
236fn encoded_u64_len(value: u64) -> Result<usize, PrimitiveRlpError> {
237    let bytes = value.to_be_bytes();
238    encoded_rlp_integer_len(trim_u64_payload(&bytes)).map_err(Into::into)
239}
240
241fn encode_u64(value: u64, output: &mut [u8]) -> Result<usize, PrimitiveRlpError> {
242    let bytes = value.to_be_bytes();
243    encode_rlp_integer(trim_u64_payload(&bytes), output).map_err(Into::into)
244}
245
246fn trim_u64_payload(bytes: &[u8; 8]) -> &[u8] {
247    // Variable-time scan: execution time leaks the bit width of the value.
248    // This path is for public Ethereum protocol fields such as chain IDs,
249    // nonces, gas values, timestamps, and block numbers. Do not reuse it for
250    // secret or pre-disclosure values.
251    let start = bytes.iter().position(|byte| *byte != 0).unwrap_or(8);
252    bytes.get(start..).unwrap_or(&[])
253}
254
255fn trim_u256_payload(bytes: &[u8; 32]) -> &[u8] {
256    // Variable-time scan: execution time leaks the bit width of the value.
257    // Wei values are public in normal Ethereum transactions. Do not reuse this
258    // helper for secret or pre-disclosure amounts without re-auditing timing.
259    let start = bytes.iter().position(|byte| *byte != 0).unwrap_or(32);
260    bytes.get(start..).unwrap_or(&[])
261}