diff --git a/crates/starknet-types-core/Cargo.toml b/crates/starknet-types-core/Cargo.toml index 0742cdd..b97312f 100644 --- a/crates/starknet-types-core/Cargo.toml +++ b/crates/starknet-types-core/Cargo.toml @@ -24,7 +24,7 @@ serde = { version = "1", optional = true, default-features = false, features = [ "alloc", "derive" ] } lambdaworks-crypto = { version = "0.13.0", default-features = false, optional = true } -parity-scale-codec = { version = "3.6", default-features = false, optional = true } +parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"], optional = true } lazy_static = { version = "1.5", default-features = false, optional = true } zeroize = { version = "1.8.1", default-features = false, optional = true } subtle = { version = "2.6.1", default-features = false, optional = true } diff --git a/crates/starknet-types-core/src/contract_address.rs b/crates/starknet-types-core/src/contract_address.rs new file mode 100644 index 0000000..1d0338d --- /dev/null +++ b/crates/starknet-types-core/src/contract_address.rs @@ -0,0 +1,217 @@ +//! A starknet contract address +//! +//! In starknet valid contract addresses exists as a subset of the type `Felt`. +//! Therefore some checks must be done in order to produce protocol valid addresses. +//! This module provides this logic as a type `ContractAddress`, that can garantee the validity of the address. +//! It also comes with some quality of life methods. + +use core::str::FromStr; + +use crate::{ + felt::Felt, + patricia_key::{ + PatriciaKey, PatriciaKeyFromFeltError, PatriciaKeyFromStrError, + STORAGE_LEAF_ADDRESS_UPPER_BOUND, + }, +}; + +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[cfg_attr( + feature = "parity-scale-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode) +)] +pub struct ContractAddress(PatriciaKey); + +impl ContractAddress { + pub const ZERO: Self = Self::from_hex_unchecked("0x0"); + pub const ONE: Self = Self::from_hex_unchecked("0x1"); + pub const TWO: Self = Self::from_hex_unchecked("0x2"); + pub const THREE: Self = Self::from_hex_unchecked("0x3"); + + /// Lower inclusive bound + pub const LOWER_BOUND: Self = Self::ZERO; + /// Upper non-inclusive bound + /// + /// For consistency with other merkle leaf bounds, [ContractAddress] is also bounded by [STORAGE_LEAF_ADDRESS_UPPER_BOUND] + pub const UPPER_BOUND: Self = Self(STORAGE_LEAF_ADDRESS_UPPER_BOUND); +} + +impl core::fmt::Display for ContractAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl AsRef for ContractAddress { + fn as_ref(&self) -> &Felt { + self.0.as_ref() + } +} + +impl From for Felt { + fn from(value: ContractAddress) -> Self { + value.0.into() + } +} + +impl AsRef for ContractAddress { + fn as_ref(&self) -> &PatriciaKey { + &self.0 + } +} + +impl From for PatriciaKey { + fn from(value: ContractAddress) -> Self { + value.0 + } +} + +#[derive(Debug)] +pub enum ContractAddressFromPatriciaKeyError { + OutOfBound, +} + +impl core::fmt::Display for ContractAddressFromPatriciaKeyError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ContractAddressFromPatriciaKeyError::OutOfBound => write!( + f, + "value out of bound, upper non-inclusive bound is {}", + ContractAddress::UPPER_BOUND + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ContractAddressFromPatriciaKeyError {} + +impl TryFrom for ContractAddress { + type Error = ContractAddressFromPatriciaKeyError; + + fn try_from(value: PatriciaKey) -> Result { + if value >= STORAGE_LEAF_ADDRESS_UPPER_BOUND { + Err(ContractAddressFromPatriciaKeyError::OutOfBound) + } else { + Ok(ContractAddress(value)) + } + } +} + +#[derive(Debug)] +pub enum ContractAddressFromFeltError { + PatriciaKey(PatriciaKeyFromFeltError), + OutOfBound(ContractAddressFromPatriciaKeyError), +} + +impl core::fmt::Display for ContractAddressFromFeltError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ContractAddressFromFeltError::OutOfBound(e) => { + write!(f, "invalid value for contract address: {e}") + } + ContractAddressFromFeltError::PatriciaKey(e) => { + write!(f, "invalid patricia key value: {e}") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ContractAddressFromFeltError {} +impl TryFrom for ContractAddress { + type Error = ContractAddressFromFeltError; + + fn try_from(value: Felt) -> Result { + let pk = PatriciaKey::try_from(value).map_err(ContractAddressFromFeltError::PatriciaKey)?; + let ca = ContractAddress::try_from(pk).map_err(ContractAddressFromFeltError::OutOfBound)?; + + Ok(ca) + } +} + +impl Felt { + /// Validates that a Felt value represents a valid Starknet contract address. + pub fn is_valid_contract_address(&self) -> bool { + self < &Felt::from(ContractAddress::UPPER_BOUND) + } +} + +#[derive(Debug)] +pub enum ContractAddressFromStrError { + PatriciaKey(PatriciaKeyFromStrError), + OutOfBound(ContractAddressFromPatriciaKeyError), +} + +impl core::fmt::Display for ContractAddressFromStrError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ContractAddressFromStrError::PatriciaKey(e) => { + write!(f, "invalid patricia key: {e}") + } + ContractAddressFromStrError::OutOfBound(e) => { + write!(f, "invalid value for contract address: {e}") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ContractAddressFromStrError {} + +impl FromStr for ContractAddress { + type Err = ContractAddressFromStrError; + + fn from_str(s: &str) -> Result { + let pk = PatriciaKey::from_str(s).map_err(ContractAddressFromStrError::PatriciaKey)?; + let ca = ContractAddress::try_from(pk).map_err(ContractAddressFromStrError::OutOfBound)?; + + Ok(ca) + } +} + +impl ContractAddress { + /// Create a new [ContractAddress] from an hex encoded string without checking it is a valid value. + /// + /// Should NEVER be used on user inputs, as it can cause erroneous execution if dynamically initialized with bad values. + /// Should mostly be used at compilation time on hardcoded static string. + pub const fn from_hex_unchecked(s: &'static str) -> ContractAddress { + let patricia_key = PatriciaKey::from_hex_unchecked(s); + + ContractAddress(patricia_key) + } +} + +#[cfg(test)] +mod test { + #[cfg(feature = "alloc")] + pub extern crate alloc; + use proptest::prelude::*; + + use crate::{ + contract_address::ContractAddress, felt::Felt, patricia_key::PATRICIA_KEY_UPPER_BOUND, + }; + + #[test] + fn basic_values() { + assert!(ContractAddress::try_from(PATRICIA_KEY_UPPER_BOUND).is_err()); + + let felt = Felt::TWO; + let contract_address = ContractAddress::try_from(felt).unwrap(); + assert_eq!(Felt::from(contract_address), felt); + } + + proptest! { + #[test] + fn is_valid_match_try_into(ref x in any::()) { + if x.is_valid_contract_address() { + prop_assert!(ContractAddress::try_from(*x).is_ok()); + } else { + prop_assert!(ContractAddress::try_from(*x).is_err()); + } + } + } +} diff --git a/crates/starknet-types-core/src/lib.rs b/crates/starknet-types-core/src/lib.rs index bda7d42..c0d2e0b 100644 --- a/crates/starknet-types-core/src/lib.rs +++ b/crates/starknet-types-core/src/lib.rs @@ -8,6 +8,9 @@ pub mod hash; pub mod felt; pub mod qm31; +pub mod contract_address; +pub mod patricia_key; +pub mod regular_contract_address; #[cfg(any(feature = "std", feature = "alloc"))] pub mod short_string; pub mod u256; diff --git a/crates/starknet-types-core/src/patricia_key.rs b/crates/starknet-types-core/src/patricia_key.rs new file mode 100644 index 0000000..b447a98 --- /dev/null +++ b/crates/starknet-types-core/src/patricia_key.rs @@ -0,0 +1,162 @@ +//! The key of one of starknet state tree +//! +//! https://docs.starknet.io/learn/protocol/state +//! The state of the starknet blockchain (contracts declared, contracts deployed, storage of each contract), +//! is represented as multiple binary Merkle-Patricia trees. +//! Those trees have an height of 251, which means that they contains at most 2^251 values. +//! The keys to those values are represented as `Felt`, with range [0, PATRICIA_KEY_UPPER_BOUND). +//! Therefore not every `Felt` is a valid `PatriciaKey`, +//! and we can use the `PatriciaKey` type to enfoce type safety in our code. + +use core::str::FromStr; + +use crate::felt::Felt; + +pub const PATRICIA_KEY_UPPER_BOUND: PatriciaKey = PatriciaKey(Felt::from_hex_unwrap( + "0x800000000000000000000000000000000000000000000000000000000000000", +)); + +/// The index upper bound for a Starknet tree +/// +/// Equal to `0x800000000000000000000000000000000000000000000000000000000000000 - 256`. +/// +/// In Starknet users are allowed to store up to 256 felts in a tree leaf. +/// Therfore, storage addresses can be used as "pointers" to some specific felt stored in a leaf: +/// ValueAddress = LeafAddress + IndexInsideTheLeaf +/// So, all leaf addresses are modulo this value. +pub const STORAGE_LEAF_ADDRESS_UPPER_BOUND: PatriciaKey = PatriciaKey(Felt::from_raw([ + 576459263475590224, + 18446744073709255680, + 160989183, + 18446743986131443745, +])); + +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[cfg_attr( + feature = "parity-scale-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode) +)] +pub struct PatriciaKey(Felt); + +impl PatriciaKey { + /// Lower inclusive bound + pub const LOWER_BOUND: Self = Self(Felt::ZERO); + /// Upper non-inclusive bound + pub const UPPER_BOUND: Self = PATRICIA_KEY_UPPER_BOUND; +} + +impl core::fmt::Display for PatriciaKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl AsRef for PatriciaKey { + fn as_ref(&self) -> &Felt { + &self.0 + } +} + +impl From for Felt { + fn from(value: PatriciaKey) -> Self { + value.0 + } +} + +#[derive(Debug, Clone, Copy)] +pub struct PatriciaKeyFromFeltError(Felt); + +impl core::fmt::Display for PatriciaKeyFromFeltError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #[cfg(feature = "alloc")] + return write!( + f, + "invalid felt value for patricia key. Upper non-inclusinve bound is 2^251 got {:#x}", + self.0 + ); + + #[cfg(not(feature = "alloc"))] + return write!( + f, + "invalid felt value for patricia key. Upper non-inclusinve bound is 2^251 got {}", + self.0 + ); + } +} + +#[cfg(feature = "std")] +impl std::error::Error for PatriciaKeyFromFeltError {} + +impl TryFrom for PatriciaKey { + type Error = PatriciaKeyFromFeltError; + + fn try_from(value: Felt) -> Result { + if value >= PATRICIA_KEY_UPPER_BOUND.0 { + return Err(PatriciaKeyFromFeltError(value)); + } + + Ok(PatriciaKey(value)) + } +} + +#[derive(Debug)] +pub enum PatriciaKeyFromStrError { + BadFelt(::Err), + BadKey(PatriciaKeyFromFeltError), +} + +impl core::fmt::Display for PatriciaKeyFromStrError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + PatriciaKeyFromStrError::BadFelt(e) => write!(f, "invalid felt string: {e}"), + PatriciaKeyFromStrError::BadKey(e) => write!(f, "invalid address value: {e}"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for PatriciaKeyFromStrError {} + +impl FromStr for PatriciaKey { + type Err = PatriciaKeyFromStrError; + + fn from_str(s: &str) -> Result { + let felt = Felt::from_str(s).map_err(PatriciaKeyFromStrError::BadFelt)?; + let contract_address = + PatriciaKey::try_from(felt).map_err(PatriciaKeyFromStrError::BadKey)?; + + Ok(contract_address) + } +} + +impl PatriciaKey { + /// Create a new [PatriciaKey] from an hex encoded string without checking it is a valid value. + /// + /// Should NEVER be used on user inputs, + /// as it can cause erroneous execution if dynamically initialized with bad values. + /// Should mostly be used at compilation time on hardcoded static string. + pub const fn from_hex_unchecked(s: &'static str) -> PatriciaKey { + let felt = Felt::from_hex_unwrap(s); + + PatriciaKey(felt) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + felt::Felt, + patricia_key::{PATRICIA_KEY_UPPER_BOUND, STORAGE_LEAF_ADDRESS_UPPER_BOUND}, + }; + + #[test] + fn enforce_max_storage_leaf_address() { + assert_eq!( + PATRICIA_KEY_UPPER_BOUND.0 - Felt::from(256), + STORAGE_LEAF_ADDRESS_UPPER_BOUND.into(), + ); + } +} diff --git a/crates/starknet-types-core/src/regular_contract_address.rs b/crates/starknet-types-core/src/regular_contract_address.rs new file mode 100644 index 0000000..57165f2 --- /dev/null +++ b/crates/starknet-types-core/src/regular_contract_address.rs @@ -0,0 +1,275 @@ +//! A regular Starknet contract address +//! +//! This excludes the two special values reserved by the protocol: 0x0 and 0x1. +//! 0x0 is the default caller address used for external calls. Nothing is ever stored there. +//! 0x1 is used for block hash mapping. +//! 0x2 is used for alias. +//! 0x3 is reserved without used for now. +//! See: https://docs.starknet.io/learn/protocol/state#special-addresses +//! +//! Most user applications should not interact with those special addresses. +//! Doing so would be a bug or invalid input. +//! `RegularContractAddress` enforces this at the type level. + +use core::str::FromStr; + +use crate::{ + contract_address::{ + ContractAddress, ContractAddressFromFeltError, ContractAddressFromStrError, + }, + felt::Felt, + patricia_key::PatriciaKey, +}; + +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[cfg_attr( + feature = "parity-scale-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode) +)] +pub struct RegularContractAddress(ContractAddress); + +impl RegularContractAddress { + /// Lower inclusive bound + pub const LOWER_BOUND: Self = Self::from_hex_unchecked("0x4"); + /// Upper non-inclusive bound + pub const UPPER_BOUND: Self = Self(ContractAddress::UPPER_BOUND); +} + +impl core::fmt::Display for RegularContractAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl AsRef for RegularContractAddress { + fn as_ref(&self) -> &Felt { + self.0.as_ref() + } +} + +impl From for Felt { + fn from(value: RegularContractAddress) -> Self { + value.0.into() + } +} + +impl AsRef for RegularContractAddress { + fn as_ref(&self) -> &PatriciaKey { + self.0.as_ref() + } +} + +impl From for PatriciaKey { + fn from(value: RegularContractAddress) -> Self { + value.0.into() + } +} + +impl AsRef for RegularContractAddress { + fn as_ref(&self) -> &ContractAddress { + &self.0 + } +} + +impl From for ContractAddress { + fn from(value: RegularContractAddress) -> Self { + value.0 + } +} + +/// In Starknet, contract addresses must follow specific constraints to be less than 2^251 (0x800000000000000000000000000000000000000000000000000000000000000) to be valid. +/// But there is also two special addressed for the protocol use: +/// * 0x0 acts as the default caller address for external calls and has no storage +/// * 0x1 functions as a storage space for block mapping +/// * 0x2 is an alias +/// * 0x3 is an reserved but not used +/// +/// Making the regular contract address range be [4, 2^251) +#[derive(Debug, Clone, Copy)] +pub enum RegularContractAddressFromContractAddressError { + Zero, + One, + Two, + Three, +} + +impl core::fmt::Display for RegularContractAddressFromContractAddressError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RegularContractAddressFromContractAddressError::Zero => { + write!( + f, + "address 0x0 is reserved as the default caller address and has no storage" + ) + } + RegularContractAddressFromContractAddressError::One => { + write!( + f, + "address 0x1 is reserved as storage space for block mapping" + ) + } + RegularContractAddressFromContractAddressError::Two => { + write!(f, "address 0x2 is reserved as alias") + } + RegularContractAddressFromContractAddressError::Three => { + write!(f, "address 0x3 is reserved for future uses") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RegularContractAddressFromContractAddressError {} + +impl TryFrom for RegularContractAddress { + type Error = RegularContractAddressFromContractAddressError; + + fn try_from(value: ContractAddress) -> Result { + if AsRef::::as_ref(&value) == &Felt::ZERO { + return Err(RegularContractAddressFromContractAddressError::Zero); + } + if AsRef::::as_ref(&value) == &Felt::ONE { + return Err(RegularContractAddressFromContractAddressError::One); + } + if AsRef::::as_ref(&value) == &Felt::TWO { + return Err(RegularContractAddressFromContractAddressError::Two); + } + if AsRef::::as_ref(&value) == &Felt::THREE { + return Err(RegularContractAddressFromContractAddressError::Three); + } + + Ok(RegularContractAddress(value)) + } +} + +#[derive(Debug)] +pub enum RegularContractAddressFromFeltError { + ContractAddress(ContractAddressFromFeltError), + SpecialAddress(RegularContractAddressFromContractAddressError), +} + +impl core::fmt::Display for RegularContractAddressFromFeltError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RegularContractAddressFromFeltError::ContractAddress(e) => { + write!(f, "invalid contract address: {}", e) + } + RegularContractAddressFromFeltError::SpecialAddress(e) => { + write!(f, "value is a special contract address: {e}") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RegularContractAddressFromFeltError {} + +impl Felt { + /// Validates that a Felt value represents a valid Starknet contract address, + /// excluding the starknet special constract address `0x0`, `0x1`, `0x2` and `0x3`. + /// + /// https://docs.starknet.io/learn/protocol/state#special-addresses + /// https://github.com/starkware-libs/sequencer/blob/ecd4779abef7bf345938a69f18ef70b6239d3a50/crates/blockifier/resources/blockifier_versioned_constants_0_15_0.json#L92-L97 + pub fn is_regular_contract_address(&self) -> bool { + self >= &Felt::from(RegularContractAddress::LOWER_BOUND) + && self < &Felt::from(RegularContractAddress::UPPER_BOUND) + } +} + +impl TryFrom for RegularContractAddress { + type Error = RegularContractAddressFromFeltError; + + fn try_from(value: Felt) -> Result { + let contract_address = ContractAddress::try_from(value) + .map_err(RegularContractAddressFromFeltError::ContractAddress)?; + + RegularContractAddress::try_from(contract_address) + .map_err(RegularContractAddressFromFeltError::SpecialAddress) + } +} + +#[derive(Debug)] +pub enum RegularContractAddressFromStrError { + ContractAddress(ContractAddressFromStrError), + SpecialContractAddress(RegularContractAddressFromContractAddressError), +} + +impl core::fmt::Display for RegularContractAddressFromStrError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RegularContractAddressFromStrError::ContractAddress(e) => { + write!(f, "invalid felt string: {e}") + } + RegularContractAddressFromStrError::SpecialContractAddress(e) => { + write!(f, "got special contract address: {e}") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RegularContractAddressFromStrError {} + +impl FromStr for RegularContractAddress { + type Err = RegularContractAddressFromStrError; + + fn from_str(s: &str) -> Result { + let contract_address = ContractAddress::from_str(s) + .map_err(RegularContractAddressFromStrError::ContractAddress)?; + + RegularContractAddress::try_from(contract_address) + .map_err(RegularContractAddressFromStrError::SpecialContractAddress) + } +} + +impl RegularContractAddress { + /// Create a new [RegularContractAddress] from an hex encoded string without checking it is a valid value. + /// + /// Should NEVER be used on user inputs, + /// as it can cause erroneous execution if dynamically initialized with bad values. + /// Should mostly be used at compilation time on hardcoded static string. + pub const fn from_hex_unchecked(s: &'static str) -> RegularContractAddress { + let contract_address = ContractAddress::from_hex_unchecked(s); + + RegularContractAddress(contract_address) + } +} + +#[cfg(test)] +mod test { + #[cfg(feature = "alloc")] + pub extern crate alloc; + use proptest::prelude::*; + + use crate::{ + felt::Felt, patricia_key::PATRICIA_KEY_UPPER_BOUND, + regular_contract_address::RegularContractAddress, + }; + + #[test] + fn basic_values() { + assert!(RegularContractAddress::try_from(Felt::ZERO).is_err()); + assert!(RegularContractAddress::try_from(Felt::ONE).is_err()); + assert!(RegularContractAddress::try_from(Felt::TWO).is_err()); + assert!(RegularContractAddress::try_from(Felt::THREE).is_err()); + assert!(RegularContractAddress::try_from(Felt::from(PATRICIA_KEY_UPPER_BOUND)).is_err()); + + let felt = Felt::from_hex_unwrap("0xcaffe"); + let contract_address = RegularContractAddress::try_from(felt).unwrap(); + assert_eq!(Felt::from(contract_address), felt); + } + + proptest! { + #[test] + fn is_valid_match_try_into(ref x in any::()) { + if x.is_regular_contract_address() { + prop_assert!(RegularContractAddress::try_from(*x).is_ok()); + } else { + prop_assert!(RegularContractAddress::try_from(*x).is_err()); + } + } + } +}