eth-valkyoth-primitives 0.3.0

Core no_std Ethereum protocol primitives.
Documentation
#![no_std]
#![forbid(unsafe_code)]
//! Core `no_std` Ethereum primitive types used across the `eth` workspace.

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 {
            /// Creates a new value.
            #[must_use]
            pub const fn new(value: $inner) -> Self {
                Self(value)
            }

            /// Returns the raw integer 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.");

/// Primitive constructor failures.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PrimitiveError {
    /// Transaction type exceeds the EIP-2718 single-byte typed envelope range.
    TransactionTypeTooLarge,
    /// Zero is reserved for the legacy transaction domain, not a typed envelope.
    ReservedLegacyType,
}

/// Fixed-width Ethereum address bytes.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Address([u8; 20]);

impl Address {
    /// Creates an address from raw bytes.
    #[must_use]
    pub const fn from_bytes(bytes: [u8; 20]) -> Self {
        Self(bytes)
    }

    /// Returns the raw address bytes.
    #[must_use]
    pub const fn to_bytes(self) -> [u8; 20] {
        self.0
    }
}

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()
    }
}

/// Fixed-width 256-bit hash bytes.
///
/// All equality for this type is constant-time because hashes appear in
/// cryptographic verification paths.
#[derive(Clone, Copy, Debug)]
pub struct B256([u8; 32]);

impl B256 {
    /// Creates a hash from raw bytes.
    #[must_use]
    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
        Self(bytes)
    }

    /// Returns the raw hash bytes.
    #[must_use]
    pub const fn to_bytes(self) -> [u8; 32] {
        self.0
    }

    /// Compares two hashes in constant time.
    ///
    /// Returns [`Choice`] so compound comparisons can use `&` and `|` without
    /// short-circuiting. Convert to `bool` only at the final trust boundary.
    #[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()
    }
}

/// Wei amount encoded as an unsigned 256-bit big-endian integer.
#[derive(Clone, Copy, Debug)]
pub struct Wei([u8; 32]);

impl Wei {
    /// Zero wei.
    pub const ZERO: Self = Self([0_u8; 32]);

    /// Creates a wei amount from unsigned 256-bit big-endian bytes.
    #[must_use]
    pub const fn from_be_bytes(bytes: [u8; 32]) -> Self {
        Self(bytes)
    }

    /// Creates a wei amount from a `u128`.
    #[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,
        ])
    }

    /// Returns unsigned 256-bit big-endian bytes.
    #[must_use]
    pub const fn to_be_bytes(self) -> [u8; 32] {
        self.0
    }

    /// Compares two wei values in constant time.
    ///
    /// Wei is usually public, but fixed-width constant-time equality keeps
    /// proof and verification paths from accidentally choosing a weaker API.
    #[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()
    }
}

/// EIP-2718 transaction envelope type.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TransactionType(u8);

impl TransactionType {
    /// Legacy transaction type used by APIs that need an explicit domain value.
    pub const LEGACY: Self = Self(0);
    /// Largest typed transaction value admitted by EIP-2718.
    pub const MAX_TYPED: u8 = 0x7f;

    /// Creates a transaction type after checking the EIP-2718 range.
    pub const fn try_new(value: u8) -> Result<Self, PrimitiveError> {
        if value > Self::MAX_TYPED {
            return Err(PrimitiveError::TransactionTypeTooLarge);
        }
        Ok(Self(value))
    }

    /// Creates a typed EIP-2718 transaction type.
    ///
    /// `0` is reserved for the legacy transaction domain. Encoders must handle
    /// legacy transactions separately instead of prepending a zero type byte.
    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),
        }
    }

    /// Returns the raw transaction type byte.
    #[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(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 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));
    }

    #[test]
    fn transaction_type_round_trips() {
        assert_eq!(TransactionType::try_new(2).map(u8::from), Ok(2));
    }
}