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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum PrimitiveError {
52    /// Transaction type exceeds the EIP-2718 single-byte typed envelope range.
53    TransactionTypeTooLarge,
54    /// Zero is reserved for the legacy transaction domain, not a typed envelope.
55    ReservedLegacyType,
56}
57
58/// Fixed-width Ethereum address bytes.
59#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
60pub struct Address([u8; 20]);
61
62impl Address {
63    /// Creates an address from raw bytes.
64    #[must_use]
65    pub const fn from_bytes(bytes: [u8; 20]) -> Self {
66        Self(bytes)
67    }
68
69    /// Returns the raw address bytes.
70    #[must_use]
71    pub const fn to_bytes(self) -> [u8; 20] {
72        self.0
73    }
74}
75
76impl From<[u8; 20]> for Address {
77    fn from(bytes: [u8; 20]) -> Self {
78        Self::from_bytes(bytes)
79    }
80}
81
82impl From<Address> for [u8; 20] {
83    fn from(value: Address) -> Self {
84        value.to_bytes()
85    }
86}
87
88/// Fixed-width 256-bit hash bytes.
89///
90/// All equality for this type is constant-time because hashes appear in
91/// cryptographic verification paths.
92#[derive(Clone, Copy, Debug)]
93pub struct B256([u8; 32]);
94
95impl B256 {
96    /// Creates a hash from raw bytes.
97    #[must_use]
98    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
99        Self(bytes)
100    }
101
102    /// Returns the raw hash bytes.
103    #[must_use]
104    pub const fn to_bytes(self) -> [u8; 32] {
105        self.0
106    }
107
108    /// Compares two hashes in constant time.
109    ///
110    /// Returns [`Choice`] so compound comparisons can use `&` and `|` without
111    /// short-circuiting. Convert to `bool` only at the final trust boundary.
112    #[must_use]
113    pub fn ct_eq(&self, other: &Self) -> Choice {
114        self.0.ct_eq(&other.0)
115    }
116}
117
118impl PartialEq for B256 {
119    fn eq(&self, other: &Self) -> bool {
120        bool::from(self.ct_eq(other))
121    }
122}
123
124impl Eq for B256 {}
125
126impl Hash for B256 {
127    fn hash<H: Hasher>(&self, state: &mut H) {
128        self.0.hash(state);
129    }
130}
131
132impl From<[u8; 32]> for B256 {
133    fn from(bytes: [u8; 32]) -> Self {
134        Self::from_bytes(bytes)
135    }
136}
137
138impl From<B256> for [u8; 32] {
139    fn from(value: B256) -> Self {
140        value.to_bytes()
141    }
142}
143
144/// Wei amount encoded as an unsigned 256-bit big-endian integer.
145#[derive(Clone, Copy, Debug)]
146pub struct Wei([u8; 32]);
147
148impl Wei {
149    /// Zero wei.
150    pub const ZERO: Self = Self([0_u8; 32]);
151
152    /// Creates a wei amount from unsigned 256-bit big-endian bytes.
153    #[must_use]
154    pub const fn from_be_bytes(bytes: [u8; 32]) -> Self {
155        Self(bytes)
156    }
157
158    /// Creates a wei amount from a `u128`.
159    #[must_use]
160    pub const fn from_u128(value: u128) -> Self {
161        let [
162            b0,
163            b1,
164            b2,
165            b3,
166            b4,
167            b5,
168            b6,
169            b7,
170            b8,
171            b9,
172            b10,
173            b11,
174            b12,
175            b13,
176            b14,
177            b15,
178        ] = value.to_be_bytes();
179        Self([
180            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,
181            0_u8, 0_u8, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15,
182        ])
183    }
184
185    /// Returns unsigned 256-bit big-endian bytes.
186    #[must_use]
187    pub const fn to_be_bytes(self) -> [u8; 32] {
188        self.0
189    }
190
191    /// Compares two wei values in constant time.
192    ///
193    /// Wei is usually public, but fixed-width constant-time equality keeps
194    /// proof and verification paths from accidentally choosing a weaker API.
195    #[must_use]
196    pub fn ct_eq(&self, other: &Self) -> Choice {
197        self.0.ct_eq(&other.0)
198    }
199}
200
201impl PartialEq for Wei {
202    fn eq(&self, other: &Self) -> bool {
203        bool::from(self.ct_eq(other))
204    }
205}
206
207impl Eq for Wei {}
208
209impl Hash for Wei {
210    fn hash<H: Hasher>(&self, state: &mut H) {
211        self.0.hash(state);
212    }
213}
214
215impl From<[u8; 32]> for Wei {
216    fn from(bytes: [u8; 32]) -> Self {
217        Self::from_be_bytes(bytes)
218    }
219}
220
221impl From<Wei> for [u8; 32] {
222    fn from(value: Wei) -> Self {
223        value.to_be_bytes()
224    }
225}
226
227/// EIP-2718 transaction envelope type.
228#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
229pub struct TransactionType(u8);
230
231impl TransactionType {
232    /// Legacy transaction type used by APIs that need an explicit domain value.
233    pub const LEGACY: Self = Self(0);
234    /// Largest typed transaction value admitted by EIP-2718.
235    pub const MAX_TYPED: u8 = 0x7f;
236
237    /// Creates a transaction type after checking the EIP-2718 range.
238    pub const fn try_new(value: u8) -> Result<Self, PrimitiveError> {
239        if value > Self::MAX_TYPED {
240            return Err(PrimitiveError::TransactionTypeTooLarge);
241        }
242        Ok(Self(value))
243    }
244
245    /// Creates a typed EIP-2718 transaction type.
246    ///
247    /// `0` is reserved for the legacy transaction domain. Encoders must handle
248    /// legacy transactions separately instead of prepending a zero type byte.
249    pub const fn try_new_typed(value: u8) -> Result<Self, PrimitiveError> {
250        match value {
251            0 => Err(PrimitiveError::ReservedLegacyType),
252            1..=Self::MAX_TYPED => Ok(Self(value)),
253            _ => Err(PrimitiveError::TransactionTypeTooLarge),
254        }
255    }
256
257    /// Returns the raw transaction type byte.
258    #[must_use]
259    pub const fn get(self) -> u8 {
260        self.0
261    }
262}
263
264impl TryFrom<u8> for TransactionType {
265    type Error = PrimitiveError;
266
267    fn try_from(value: u8) -> Result<Self, Self::Error> {
268        Self::try_new(value)
269    }
270}
271
272impl From<TransactionType> for u8 {
273    fn from(value: TransactionType) -> Self {
274        value.get()
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn chain_id_round_trips() {
284        assert_eq!(u64::from(ChainId::from(1)), 1);
285    }
286
287    #[test]
288    fn block_number_round_trips() {
289        assert_eq!(u64::from(BlockNumber::from(2)), 2);
290    }
291
292    #[test]
293    fn gas_round_trips() {
294        assert_eq!(u64::from(Gas::from(21_000)), 21_000);
295    }
296
297    #[test]
298    fn nonce_round_trips() {
299        assert_eq!(u64::from(Nonce::from(7)), 7);
300    }
301
302    #[test]
303    fn unix_timestamp_round_trips() {
304        assert_eq!(u64::from(UnixTimestamp::from(1_700_000_000)), 1_700_000_000);
305    }
306
307    #[test]
308    fn address_round_trips() {
309        let bytes = [7_u8; 20];
310        assert_eq!(<[u8; 20]>::from(Address::from(bytes)), bytes);
311    }
312
313    #[test]
314    fn b256_constant_time_equality_result_matches_equality() {
315        let left = B256::from_bytes([1_u8; 32]);
316        let same = B256::from_bytes([1_u8; 32]);
317        let different = B256::from_bytes([2_u8; 32]);
318        assert!(bool::from(left.ct_eq(&same)));
319        assert!(!bool::from(left.ct_eq(&different)));
320        assert!(left == same);
321        assert!(left != different);
322    }
323
324    #[test]
325    fn b256_constant_time_choices_compose_without_short_circuit() {
326        let left = B256::from_bytes([1_u8; 32]);
327        let same = B256::from_bytes([1_u8; 32]);
328        let different = B256::from_bytes([2_u8; 32]);
329        let composed = left.ct_eq(&same) & same.ct_eq(&different);
330
331        assert!(!bool::from(composed));
332    }
333
334    #[test]
335    fn b256_round_trips() {
336        let bytes = [3_u8; 32];
337        assert_eq!(<[u8; 32]>::from(B256::from(bytes)), bytes);
338    }
339
340    #[test]
341    fn wei_round_trips() {
342        let bytes = [9_u8; 32];
343        assert_eq!(<[u8; 32]>::from(Wei::from(bytes)), bytes);
344    }
345
346    #[test]
347    fn wei_constant_time_equality_result_matches_equality() {
348        let left = Wei::from_be_bytes([1_u8; 32]);
349        let same = Wei::from_be_bytes([1_u8; 32]);
350        let different = Wei::from_be_bytes([2_u8; 32]);
351
352        assert!(bool::from(left.ct_eq(&same)));
353        assert!(!bool::from(left.ct_eq(&different)));
354        assert!(left == same);
355        assert!(left != different);
356    }
357
358    #[test]
359    fn wei_from_u128_places_bytes_at_low_end() {
360        let wei = Wei::from_u128(1);
361        let expected = [
362            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,
363            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,
364            0_u8, 0_u8, 0_u8, 1_u8,
365        ];
366        assert_eq!(wei.to_be_bytes(), expected);
367    }
368
369    #[test]
370    fn transaction_type_accepts_eip_2718_range() {
371        let tx_type = TransactionType::try_new(TransactionType::MAX_TYPED);
372        assert_eq!(tx_type.map(TransactionType::get), Ok(0x7f));
373    }
374
375    #[test]
376    fn transaction_type_rejects_reserved_range() {
377        assert_eq!(
378            TransactionType::try_new(0x80),
379            Err(PrimitiveError::TransactionTypeTooLarge)
380        );
381    }
382
383    #[test]
384    fn typed_transaction_type_rejects_legacy_zero() {
385        assert_eq!(
386            TransactionType::try_new_typed(0),
387            Err(PrimitiveError::ReservedLegacyType)
388        );
389        assert_eq!(TransactionType::try_new_typed(1).map(u8::from), Ok(1));
390    }
391
392    #[test]
393    fn transaction_type_round_trips() {
394        assert_eq!(TransactionType::try_new(2).map(u8::from), Ok(2));
395    }
396}