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