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