Skip to main content

eth_valkyoth_primitives/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3//! Core `no_std` Ethereum primitive types used across the `eth` workspace.
4
5#[cfg(feature = "std")]
6extern crate std;
7
8use core::fmt;
9use core::hash::{Hash, Hasher};
10use eth_valkyoth_codec::{
11    DecodeError, rlp_integer_payload_to_u64, rlp_integer_payload_to_u256_bytes,
12};
13pub use subtle::Choice;
14use subtle::ConstantTimeEq as _;
15
16mod rlp;
17mod rlp_traits;
18#[cfg(test)]
19mod tests;
20
21pub use rlp::PrimitiveRlpError;
22
23fn parse_canonical_u64(bytes: &[u8]) -> Result<u64, PrimitiveError> {
24    // Do not duplicate Ethereum RLP integer canonicality in this crate.
25    // `eth-valkyoth-codec` is the single source of truth for payload parsing.
26    rlp_integer_payload_to_u64(bytes).map_err(map_rlp_integer_error)
27}
28
29fn canonical_u256_bytes(bytes: &[u8]) -> Result<[u8; 32], PrimitiveError> {
30    // Do not duplicate Ethereum RLP integer canonicality in this crate.
31    // `eth-valkyoth-codec` is the single source of truth for payload parsing.
32    rlp_integer_payload_to_u256_bytes(bytes).map_err(map_rlp_integer_error)
33}
34
35fn map_rlp_integer_error(error: DecodeError) -> PrimitiveError {
36    match error {
37        DecodeError::Malformed => PrimitiveError::NonCanonicalInteger,
38        DecodeError::LengthOverflow => PrimitiveError::IntegerTooLarge,
39        DecodeError::OffsetOutOfBounds => PrimitiveError::IntegerTooLarge,
40        DecodeError::InputTooLarge
41        | DecodeError::TrailingBytes
42        | DecodeError::DecoderOverread
43        | DecodeError::UnexpectedList
44        | DecodeError::UnexpectedScalar
45        | DecodeError::ListTooLong
46        | DecodeError::NestingTooDeep
47        | DecodeError::AllocationExceeded
48        | DecodeError::ProofTooLarge
49        | DecodeError::ItemCountExceeded
50        | DecodeError::UnreviewedDeploymentPolicy => PrimitiveError::UnexpectedCodecError,
51        _ => PrimitiveError::UnexpectedCodecError,
52    }
53}
54
55macro_rules! id_type {
56    ($name:ident, $inner:ty, $doc:literal) => {
57        #[doc = $doc]
58        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
59        pub struct $name($inner);
60
61        impl $name {
62            /// Creates a new value.
63            #[must_use]
64            pub const fn new(value: $inner) -> Self {
65                Self(value)
66            }
67
68            /// Returns the raw integer value.
69            #[must_use]
70            pub const fn get(self) -> $inner {
71                self.0
72            }
73
74            /// Creates a value from a canonical RLP integer payload.
75            ///
76            /// The empty payload represents zero. Non-empty payloads must be
77            /// shortest-form unsigned big-endian bytes without a leading zero.
78            pub fn try_from_canonical_be_slice(bytes: &[u8]) -> Result<Self, PrimitiveError> {
79                parse_canonical_u64(bytes).map(Self)
80            }
81        }
82
83        impl From<$inner> for $name {
84            fn from(value: $inner) -> Self {
85                Self::new(value)
86            }
87        }
88
89        impl From<$name> for $inner {
90            fn from(value: $name) -> Self {
91                value.get()
92            }
93        }
94    };
95}
96
97id_type!(
98    ChainId,
99    u64,
100    "Ethereum chain identifier.\n\nThis type enforces canonical integer encoding only. EIP-155 assigns chain ID 0 to unsigned legacy transactions; signed transaction validation must reject chain ID 0 independently."
101);
102
103impl ChainId {
104    /// Creates a signed-transaction chain ID from a canonical RLP integer
105    /// payload.
106    ///
107    /// Rejects `0`, which EIP-155 reserves for unsigned legacy transactions.
108    pub fn try_from_signed_canonical_be_slice(bytes: &[u8]) -> Result<Self, PrimitiveError> {
109        let chain_id = Self::try_from_canonical_be_slice(bytes)?;
110        if chain_id.get() == 0 {
111            return Err(PrimitiveError::ReservedLegacyType);
112        }
113        Ok(chain_id)
114    }
115}
116
117id_type!(BlockNumber, u64, "Ethereum execution-layer block number.");
118id_type!(Gas, u64, "Gas quantity.");
119id_type!(Nonce, u64, "Account transaction nonce.");
120id_type!(UnixTimestamp, u64, "Block timestamp as Unix seconds.");
121
122/// Primitive constructor failures.
123#[non_exhaustive]
124#[derive(Clone, Copy, Debug, Eq, PartialEq)]
125pub enum PrimitiveError {
126    /// Integer bytes were not in shortest-form canonical big-endian encoding.
127    NonCanonicalInteger,
128    /// Integer bytes exceed the primitive's fixed-width range.
129    IntegerTooLarge,
130    /// Transaction type exceeds the EIP-2718 single-byte typed envelope range.
131    TransactionTypeTooLarge,
132    /// Zero is reserved for the legacy transaction domain, not a typed envelope.
133    ReservedLegacyType,
134    /// A codec error unexpected for the selected primitive helper was returned.
135    UnexpectedCodecError,
136}
137
138impl fmt::Display for PrimitiveError {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        match self {
141            Self::NonCanonicalInteger => f.write_str("integer is not canonical shortest-form"),
142            Self::IntegerTooLarge => f.write_str("integer exceeds primitive width"),
143            Self::TransactionTypeTooLarge => {
144                f.write_str("transaction type exceeds EIP-2718 typed envelope range")
145            }
146            Self::ReservedLegacyType => {
147                f.write_str("zero is reserved for the legacy transaction domain")
148            }
149            Self::UnexpectedCodecError => {
150                f.write_str("codec returned an unexpected error for this primitive")
151            }
152        }
153    }
154}
155
156#[cfg(feature = "std")]
157impl std::error::Error for PrimitiveError {}
158
159/// Fixed-width Ethereum address bytes.
160///
161/// Equality is constant-time because recovered sender checks appear in
162/// authentication and authorization paths.
163///
164/// The [`Hash`] implementation is for ordinary map/set use. Do not rely on
165/// hash-map lookup timing for secret or side-channel-sensitive access-control
166/// paths; use explicit indexed structures or constant-time scans there.
167#[derive(Clone, Copy, Debug)]
168pub struct Address([u8; 20]);
169
170impl Address {
171    /// Creates an address from raw bytes.
172    #[must_use]
173    pub const fn from_bytes(bytes: [u8; 20]) -> Self {
174        Self(bytes)
175    }
176
177    /// Returns the raw address bytes.
178    #[must_use]
179    pub const fn to_bytes(self) -> [u8; 20] {
180        self.0
181    }
182
183    /// Compares two addresses in constant time.
184    ///
185    /// Returns [`Choice`] so compound comparisons can use `&` and `|` without
186    /// short-circuiting. Convert to `bool` only at the final trust boundary.
187    #[must_use]
188    pub fn ct_eq(&self, other: &Self) -> Choice {
189        self.0.ct_eq(&other.0)
190    }
191}
192
193impl PartialEq for Address {
194    fn eq(&self, other: &Self) -> bool {
195        bool::from(self.ct_eq(other))
196    }
197}
198
199impl Eq for Address {}
200
201impl Hash for Address {
202    fn hash<H: Hasher>(&self, state: &mut H) {
203        self.0.hash(state);
204    }
205}
206
207impl From<[u8; 20]> for Address {
208    fn from(bytes: [u8; 20]) -> Self {
209        Self::from_bytes(bytes)
210    }
211}
212
213impl From<Address> for [u8; 20] {
214    fn from(value: Address) -> Self {
215        value.to_bytes()
216    }
217}
218
219/// Fixed-width 256-bit hash bytes.
220///
221/// All equality for this type is constant-time because hashes appear in
222/// cryptographic verification paths.
223///
224/// The [`Hash`] implementation is for ordinary map/set use. Do not rely on
225/// hash-map lookup timing for secret or side-channel-sensitive verification
226/// paths; use explicit indexed structures or constant-time scans there.
227#[derive(Clone, Copy, Debug)]
228pub struct B256([u8; 32]);
229
230impl B256 {
231    /// Creates a hash from raw bytes.
232    #[must_use]
233    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
234        Self(bytes)
235    }
236
237    /// Returns the raw hash bytes.
238    #[must_use]
239    pub const fn to_bytes(self) -> [u8; 32] {
240        self.0
241    }
242
243    /// Compares two hashes in constant time.
244    ///
245    /// Returns [`Choice`] so compound comparisons can use `&` and `|` without
246    /// short-circuiting. Convert to `bool` only at the final trust boundary.
247    #[must_use]
248    pub fn ct_eq(&self, other: &Self) -> Choice {
249        self.0.ct_eq(&other.0)
250    }
251}
252
253impl PartialEq for B256 {
254    fn eq(&self, other: &Self) -> bool {
255        bool::from(self.ct_eq(other))
256    }
257}
258
259impl Eq for B256 {}
260
261impl Hash for B256 {
262    fn hash<H: Hasher>(&self, state: &mut H) {
263        self.0.hash(state);
264    }
265}
266
267impl From<[u8; 32]> for B256 {
268    fn from(bytes: [u8; 32]) -> Self {
269        Self::from_bytes(bytes)
270    }
271}
272
273impl From<B256> for [u8; 32] {
274    fn from(value: B256) -> Self {
275        value.to_bytes()
276    }
277}
278
279/// Wei amount encoded as an unsigned 256-bit big-endian integer.
280#[derive(Clone, Copy, Debug)]
281pub struct Wei([u8; 32]);
282
283impl Wei {
284    /// Zero wei.
285    pub const ZERO: Self = Self([0_u8; 32]);
286
287    /// Creates a wei amount from unsigned 256-bit big-endian bytes.
288    #[must_use]
289    pub const fn from_be_bytes(bytes: [u8; 32]) -> Self {
290        Self(bytes)
291    }
292
293    /// Creates a wei amount from a `u128`.
294    #[must_use]
295    pub const fn from_u128(value: u128) -> Self {
296        let [
297            b0,
298            b1,
299            b2,
300            b3,
301            b4,
302            b5,
303            b6,
304            b7,
305            b8,
306            b9,
307            b10,
308            b11,
309            b12,
310            b13,
311            b14,
312            b15,
313        ] = value.to_be_bytes();
314        Self([
315            0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8,
316            0_u8, 0_u8, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15,
317        ])
318    }
319
320    /// Returns unsigned 256-bit big-endian bytes.
321    #[must_use]
322    pub const fn to_be_bytes(self) -> [u8; 32] {
323        self.0
324    }
325
326    /// Creates a wei amount from a canonical RLP integer payload.
327    ///
328    /// The empty payload represents zero. Non-empty payloads must be
329    /// shortest-form unsigned big-endian bytes without a leading zero.
330    pub fn try_from_canonical_be_slice(bytes: &[u8]) -> Result<Self, PrimitiveError> {
331        canonical_u256_bytes(bytes).map(Self)
332    }
333
334    /// Compares two wei values in constant time.
335    ///
336    /// Wei is usually public, but fixed-width constant-time equality keeps
337    /// proof and verification paths from accidentally choosing a weaker API.
338    #[must_use]
339    pub fn ct_eq(&self, other: &Self) -> Choice {
340        self.0.ct_eq(&other.0)
341    }
342}
343
344impl PartialEq for Wei {
345    fn eq(&self, other: &Self) -> bool {
346        bool::from(self.ct_eq(other))
347    }
348}
349
350impl Eq for Wei {}
351
352impl Hash for Wei {
353    fn hash<H: Hasher>(&self, state: &mut H) {
354        self.0.hash(state);
355    }
356}
357
358impl From<[u8; 32]> for Wei {
359    fn from(bytes: [u8; 32]) -> Self {
360        Self::from_be_bytes(bytes)
361    }
362}
363
364impl From<Wei> for [u8; 32] {
365    fn from(value: Wei) -> Self {
366        value.to_be_bytes()
367    }
368}
369
370/// EIP-2718 transaction envelope type.
371#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
372pub struct TransactionType(u8);
373
374impl TransactionType {
375    /// Legacy transaction type used by APIs that need an explicit domain value.
376    pub const LEGACY: Self = Self(0);
377    /// Largest typed transaction value admitted by EIP-2718.
378    pub const MAX_TYPED: u8 = 0x7f;
379
380    /// Creates a typed EIP-2718 transaction type.
381    ///
382    /// `0` is reserved for the legacy transaction domain. Encoders must handle
383    /// legacy transactions separately instead of prepending a zero type byte.
384    pub const fn try_new_typed(value: u8) -> Result<Self, PrimitiveError> {
385        match value {
386            0 => Err(PrimitiveError::ReservedLegacyType),
387            1..=Self::MAX_TYPED => Ok(Self(value)),
388            _ => Err(PrimitiveError::TransactionTypeTooLarge),
389        }
390    }
391
392    /// Creates a transaction type and accepts the legacy domain marker.
393    ///
394    /// Use this when an API explicitly accepts both legacy and typed
395    /// transaction domains. Use [`Self::try_new_typed`] when parsing an
396    /// EIP-2718 typed-envelope byte.
397    pub const fn try_new_with_legacy(value: u8) -> Result<Self, PrimitiveError> {
398        if value > Self::MAX_TYPED {
399            return Err(PrimitiveError::TransactionTypeTooLarge);
400        }
401        Ok(Self(value))
402    }
403
404    /// Returns the raw transaction type byte.
405    #[must_use]
406    pub const fn get(self) -> u8 {
407        self.0
408    }
409}
410
411impl TryFrom<u8> for TransactionType {
412    type Error = PrimitiveError;
413
414    /// Parses a typed EIP-2718 transaction type byte.
415    ///
416    /// Rejects `0` because it belongs to the legacy transaction domain. Use
417    /// [`TransactionType::try_new_with_legacy`] when the caller explicitly
418    /// accepts both legacy and typed transaction domains.
419    fn try_from(value: u8) -> Result<Self, Self::Error> {
420        Self::try_new_typed(value)
421    }
422}
423
424impl From<TransactionType> for u8 {
425    fn from(value: TransactionType) -> Self {
426        value.get()
427    }
428}