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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum PrimitiveError {
52 TransactionTypeTooLarge,
54 ReservedLegacyType,
56}
57
58#[derive(Clone, Copy, Debug)]
63pub struct Address([u8; 20]);
64
65impl Address {
66 #[must_use]
68 pub const fn from_bytes(bytes: [u8; 20]) -> Self {
69 Self(bytes)
70 }
71
72 #[must_use]
74 pub const fn to_bytes(self) -> [u8; 20] {
75 self.0
76 }
77
78 #[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#[derive(Clone, Copy, Debug)]
123pub struct B256([u8; 32]);
124
125impl B256 {
126 #[must_use]
128 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
129 Self(bytes)
130 }
131
132 #[must_use]
134 pub const fn to_bytes(self) -> [u8; 32] {
135 self.0
136 }
137
138 #[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#[derive(Clone, Copy, Debug)]
176pub struct Wei([u8; 32]);
177
178impl Wei {
179 pub const ZERO: Self = Self([0_u8; 32]);
181
182 #[must_use]
184 pub const fn from_be_bytes(bytes: [u8; 32]) -> Self {
185 Self(bytes)
186 }
187
188 #[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 #[must_use]
217 pub const fn to_be_bytes(self) -> [u8; 32] {
218 self.0
219 }
220
221 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
259pub struct TransactionType(u8);
260
261impl TransactionType {
262 pub const LEGACY: Self = Self(0);
264 pub const MAX_TYPED: u8 = 0x7f;
266
267 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 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 pub const fn try_new_with_legacy(value: u8) -> Result<Self, PrimitiveError> {
293 Self::try_new(value)
294 }
295
296 #[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}