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