#![no_std]
#![forbid(unsafe_code)]
use core::hash::{Hash, Hasher};
pub use subtle::Choice;
use subtle::ConstantTimeEq as _;
macro_rules! id_type {
($name:ident, $inner:ty, $doc:literal) => {
#[doc = $doc]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $name($inner);
impl $name {
#[must_use]
pub const fn new(value: $inner) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> $inner {
self.0
}
}
impl From<$inner> for $name {
fn from(value: $inner) -> Self {
Self::new(value)
}
}
impl From<$name> for $inner {
fn from(value: $name) -> Self {
value.get()
}
}
};
}
id_type!(ChainId, u64, "Ethereum chain identifier.");
id_type!(BlockNumber, u64, "Ethereum execution-layer block number.");
id_type!(Gas, u64, "Gas quantity.");
id_type!(Nonce, u64, "Account transaction nonce.");
id_type!(UnixTimestamp, u64, "Block timestamp as Unix seconds.");
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PrimitiveError {
TransactionTypeTooLarge,
ReservedLegacyType,
}
#[derive(Clone, Copy, Debug)]
pub struct Address([u8; 20]);
impl Address {
#[must_use]
pub const fn from_bytes(bytes: [u8; 20]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn to_bytes(self) -> [u8; 20] {
self.0
}
#[must_use]
pub fn ct_eq(&self, other: &Self) -> Choice {
self.0.ct_eq(&other.0)
}
}
impl PartialEq for Address {
fn eq(&self, other: &Self) -> bool {
bool::from(self.ct_eq(other))
}
}
impl Eq for Address {}
impl Hash for Address {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl From<[u8; 20]> for Address {
fn from(bytes: [u8; 20]) -> Self {
Self::from_bytes(bytes)
}
}
impl From<Address> for [u8; 20] {
fn from(value: Address) -> Self {
value.to_bytes()
}
}
#[derive(Clone, Copy, Debug)]
pub struct B256([u8; 32]);
impl B256 {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn to_bytes(self) -> [u8; 32] {
self.0
}
#[must_use]
pub fn ct_eq(&self, other: &Self) -> Choice {
self.0.ct_eq(&other.0)
}
}
impl PartialEq for B256 {
fn eq(&self, other: &Self) -> bool {
bool::from(self.ct_eq(other))
}
}
impl Eq for B256 {}
impl Hash for B256 {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl From<[u8; 32]> for B256 {
fn from(bytes: [u8; 32]) -> Self {
Self::from_bytes(bytes)
}
}
impl From<B256> for [u8; 32] {
fn from(value: B256) -> Self {
value.to_bytes()
}
}
#[derive(Clone, Copy, Debug)]
pub struct Wei([u8; 32]);
impl Wei {
pub const ZERO: Self = Self([0_u8; 32]);
#[must_use]
pub const fn from_be_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn from_u128(value: u128) -> Self {
let [
b0,
b1,
b2,
b3,
b4,
b5,
b6,
b7,
b8,
b9,
b10,
b11,
b12,
b13,
b14,
b15,
] = value.to_be_bytes();
Self([
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,
0_u8, 0_u8, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15,
])
}
#[must_use]
pub const fn to_be_bytes(self) -> [u8; 32] {
self.0
}
#[must_use]
pub fn ct_eq(&self, other: &Self) -> Choice {
self.0.ct_eq(&other.0)
}
}
impl PartialEq for Wei {
fn eq(&self, other: &Self) -> bool {
bool::from(self.ct_eq(other))
}
}
impl Eq for Wei {}
impl Hash for Wei {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl From<[u8; 32]> for Wei {
fn from(bytes: [u8; 32]) -> Self {
Self::from_be_bytes(bytes)
}
}
impl From<Wei> for [u8; 32] {
fn from(value: Wei) -> Self {
value.to_be_bytes()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TransactionType(u8);
impl TransactionType {
pub const LEGACY: Self = Self(0);
pub const MAX_TYPED: u8 = 0x7f;
pub const fn try_new(value: u8) -> Result<Self, PrimitiveError> {
if value > Self::MAX_TYPED {
return Err(PrimitiveError::TransactionTypeTooLarge);
}
Ok(Self(value))
}
pub const fn try_new_typed(value: u8) -> Result<Self, PrimitiveError> {
match value {
0 => Err(PrimitiveError::ReservedLegacyType),
1..=Self::MAX_TYPED => Ok(Self(value)),
_ => Err(PrimitiveError::TransactionTypeTooLarge),
}
}
pub const fn try_new_with_legacy(value: u8) -> Result<Self, PrimitiveError> {
Self::try_new(value)
}
#[must_use]
pub const fn get(self) -> u8 {
self.0
}
}
impl TryFrom<u8> for TransactionType {
type Error = PrimitiveError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
Self::try_new_typed(value)
}
}
impl From<TransactionType> for u8 {
fn from(value: TransactionType) -> Self {
value.get()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chain_id_round_trips() {
assert_eq!(u64::from(ChainId::from(1)), 1);
}
#[test]
fn block_number_round_trips() {
assert_eq!(u64::from(BlockNumber::from(2)), 2);
}
#[test]
fn gas_round_trips() {
assert_eq!(u64::from(Gas::from(21_000)), 21_000);
}
#[test]
fn nonce_round_trips() {
assert_eq!(u64::from(Nonce::from(7)), 7);
}
#[test]
fn unix_timestamp_round_trips() {
assert_eq!(u64::from(UnixTimestamp::from(1_700_000_000)), 1_700_000_000);
}
#[test]
fn address_round_trips() {
let bytes = [7_u8; 20];
assert_eq!(<[u8; 20]>::from(Address::from(bytes)), bytes);
}
#[test]
fn address_constant_time_equality_result_matches_equality() {
let left = Address::from_bytes([1_u8; 20]);
let same = Address::from_bytes([1_u8; 20]);
let different = Address::from_bytes([2_u8; 20]);
assert!(bool::from(left.ct_eq(&same)));
assert!(!bool::from(left.ct_eq(&different)));
assert!(left == same);
assert!(left != different);
}
#[test]
fn b256_constant_time_equality_result_matches_equality() {
let left = B256::from_bytes([1_u8; 32]);
let same = B256::from_bytes([1_u8; 32]);
let different = B256::from_bytes([2_u8; 32]);
assert!(bool::from(left.ct_eq(&same)));
assert!(!bool::from(left.ct_eq(&different)));
assert!(left == same);
assert!(left != different);
}
#[test]
fn b256_constant_time_choices_compose_without_short_circuit() {
let left = B256::from_bytes([1_u8; 32]);
let same = B256::from_bytes([1_u8; 32]);
let different = B256::from_bytes([2_u8; 32]);
let composed = left.ct_eq(&same) & same.ct_eq(&different);
assert!(!bool::from(composed));
}
#[test]
fn b256_round_trips() {
let bytes = [3_u8; 32];
assert_eq!(<[u8; 32]>::from(B256::from(bytes)), bytes);
}
#[test]
fn wei_round_trips() {
let bytes = [9_u8; 32];
assert_eq!(<[u8; 32]>::from(Wei::from(bytes)), bytes);
}
#[test]
fn wei_constant_time_equality_result_matches_equality() {
let left = Wei::from_be_bytes([1_u8; 32]);
let same = Wei::from_be_bytes([1_u8; 32]);
let different = Wei::from_be_bytes([2_u8; 32]);
assert!(bool::from(left.ct_eq(&same)));
assert!(!bool::from(left.ct_eq(&different)));
assert!(left == same);
assert!(left != different);
}
#[test]
fn wei_from_u128_places_bytes_at_low_end() {
let wei = Wei::from_u128(1);
let expected = [
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,
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,
0_u8, 0_u8, 0_u8, 1_u8,
];
assert_eq!(wei.to_be_bytes(), expected);
}
#[test]
fn transaction_type_accepts_eip_2718_range() {
let tx_type = TransactionType::try_new(TransactionType::MAX_TYPED);
assert_eq!(tx_type.map(TransactionType::get), Ok(0x7f));
}
#[test]
fn transaction_type_rejects_reserved_range() {
assert_eq!(
TransactionType::try_new(0x80),
Err(PrimitiveError::TransactionTypeTooLarge)
);
}
#[test]
fn typed_transaction_type_rejects_legacy_zero() {
assert_eq!(
TransactionType::try_new_typed(0),
Err(PrimitiveError::ReservedLegacyType)
);
assert_eq!(TransactionType::try_new_typed(1).map(u8::from), Ok(1));
assert_eq!(
TransactionType::try_from(0),
Err(PrimitiveError::ReservedLegacyType)
);
}
#[test]
fn transaction_type_round_trips() {
assert_eq!(TransactionType::try_new(2).map(u8::from), Ok(2));
assert_eq!(TransactionType::try_new_with_legacy(0).map(u8::from), Ok(0));
}
}