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, Eq, Hash, PartialEq)]
60pub struct Address([u8; 20]);
61
62impl Address {
63 #[must_use]
65 pub const fn from_bytes(bytes: [u8; 20]) -> Self {
66 Self(bytes)
67 }
68
69 #[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#[derive(Clone, Copy, Debug)]
93pub struct B256([u8; 32]);
94
95impl B256 {
96 #[must_use]
98 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
99 Self(bytes)
100 }
101
102 #[must_use]
104 pub const fn to_bytes(self) -> [u8; 32] {
105 self.0
106 }
107
108 #[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#[derive(Clone, Copy, Debug)]
146pub struct Wei([u8; 32]);
147
148impl Wei {
149 pub const ZERO: Self = Self([0_u8; 32]);
151
152 #[must_use]
154 pub const fn from_be_bytes(bytes: [u8; 32]) -> Self {
155 Self(bytes)
156 }
157
158 #[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 #[must_use]
187 pub const fn to_be_bytes(self) -> [u8; 32] {
188 self.0
189 }
190
191 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
229pub struct TransactionType(u8);
230
231impl TransactionType {
232 pub const LEGACY: Self = Self(0);
234 pub const MAX_TYPED: u8 = 0x7f;
236
237 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 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 #[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}