1#![no_std]
2#![forbid(unsafe_code)]
3use 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 #[must_use]
18 pub const fn new(value: $inner) -> Self {
19 Self(value)
20 }
21
22 #[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#[non_exhaustive]
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub enum PrimitiveError {
53 TransactionTypeTooLarge,
55 ReservedLegacyType,
57}
58
59#[derive(Clone, Copy, Debug)]
68pub struct Address([u8; 20]);
69
70impl Address {
71 #[must_use]
73 pub const fn from_bytes(bytes: [u8; 20]) -> Self {
74 Self(bytes)
75 }
76
77 #[must_use]
79 pub const fn to_bytes(self) -> [u8; 20] {
80 self.0
81 }
82
83 #[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#[derive(Clone, Copy, Debug)]
128pub struct B256([u8; 32]);
129
130impl B256 {
131 #[must_use]
133 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
134 Self(bytes)
135 }
136
137 #[must_use]
139 pub const fn to_bytes(self) -> [u8; 32] {
140 self.0
141 }
142
143 #[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#[derive(Clone, Copy, Debug)]
181pub struct Wei([u8; 32]);
182
183impl Wei {
184 pub const ZERO: Self = Self([0_u8; 32]);
186
187 #[must_use]
189 pub const fn from_be_bytes(bytes: [u8; 32]) -> Self {
190 Self(bytes)
191 }
192
193 #[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 #[must_use]
222 pub const fn to_be_bytes(self) -> [u8; 32] {
223 self.0
224 }
225
226 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
264pub struct TransactionType(u8);
265
266impl TransactionType {
267 pub const LEGACY: Self = Self(0);
269 pub const MAX_TYPED: u8 = 0x7f;
271
272 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 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 pub const fn try_new_with_legacy(value: u8) -> Result<Self, PrimitiveError> {
298 Self::try_new(value)
299 }
300
301 #[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 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}