diff --git a/crates/starknet-types-core/Cargo.toml b/crates/starknet-types-core/Cargo.toml index 3e59bfa..c214156 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.12.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..7775b3e --- /dev/null +++ b/crates/starknet-types-core/src/contract_address.rs @@ -0,0 +1,158 @@ +//! 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, PATRICIA_KEY_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 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 + } +} + +impl From for ContractAddress { + fn from(value: PatriciaKey) -> Self { + ContractAddress(value) + } +} + +impl TryFrom for ContractAddress { + type Error = PatriciaKeyFromFeltError; + + fn try_from(value: Felt) -> Result { + Ok(ContractAddress(PatriciaKey::try_from(value)?)) + } +} + +impl Felt { + /// Validates that a Felt value represents a valid Starknet contract address. + pub fn is_valid_contract_address(&self) -> bool { + self < &PATRICIA_KEY_UPPER_BOUND + } +} + +// impl TryFrom for ContractAddress { +// type Error = ContactAddressFromFeltError; + +// fn try_from(value: Felt) -> Result { +// if value == Felt::ZERO { +// return Err(ContactAddressFromFeltError::Zero); +// } +// if value == Felt::ONE { +// return Err(ContactAddressFromFeltError::One); +// } +// if value >= ADDRESS_UPPER_BOUND { +// return Err(ContactAddressFromFeltError::TooBig); +// } + +// Ok(ContractAddress(value)) +// } +// } + +// #[derive(Debug)] +// pub enum ContractAddressFromStrError { +// BadFelt(::Err), +// BadAddress(ContactAddressFromFeltError), +// } + +// impl core::fmt::Display for ContractAddressFromStrError { +// fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +// match self { +// ContractAddressFromStrError::BadFelt(e) => write!(f, "invalid felt string: {e}"), +// ContractAddressFromStrError::BadAddress(e) => write!(f, "invalid address value: {e}"), +// } +// } +// } + +impl FromStr for ContractAddress { + type Err = PatriciaKeyFromStrError; + + fn from_str(s: &str) -> Result { + Ok(ContractAddress(PatriciaKey::from_str(s)?)) + } +} + +impl ContractAddress { + pub const fn from_hex_unchecked(s: &'static str) -> ContractAddress { + let felt = PatriciaKey::from_hex_unchecked(s); + + ContractAddress(felt) + } +} + +#[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(Felt::ZERO).is_err()); + assert!(ContractAddress::try_from(Felt::ONE).is_err()); + 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..0c5cf46 --- /dev/null +++ b/crates/starknet-types-core/src/patricia_key.rs @@ -0,0 +1,110 @@ +//! 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: Felt = + Felt::from_hex_unchecked("0x800000000000000000000000000000000000000000000000000000000000000"); + +#[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 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 { + write!( + f, + "invalid felt value for patricia key. Upper non-inclusinve bound is 2^251 got {:#x}", + 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 { + 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 { + pub const fn from_hex_unchecked(s: &'static str) -> PatriciaKey { + let felt = Felt::from_hex_unchecked(s); + + PatriciaKey(felt) + } +} 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..7bb0e99 --- /dev/null +++ b/crates/starknet-types-core/src/regular_contract_address.rs @@ -0,0 +1,207 @@ +use core::str::FromStr; + +use crate::{ + contract_address::ContractAddress, + felt::Felt, + patricia_key::{ + PatriciaKey, PatriciaKeyFromFeltError, PatriciaKeyFromStrError, PATRICIA_KEY_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 RegularContractAddress(ContractAddress); + +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 [link](https://docs.starknet.io/architecture-and-concepts/network-architecture/starknet-state/#special_addresses) +/// +/// Making the regular contract address range be [2, 2^251) +#[derive(Debug, Clone, Copy)] +pub enum RegularContractAddressFromContractAddressError { + Zero, + One, +} + +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" + ) + } + } + } +} + +#[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); + } + + Ok(RegularContractAddress(value)) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum RegularContractAddressFromFeltError { + TooBig(PatriciaKeyFromFeltError), + SpecialAddress(RegularContractAddressFromContractAddressError), +} + +impl core::fmt::Display for RegularContractAddressFromFeltError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RegularContractAddressFromFeltError::TooBig(e) => { + write!(f, "invalid contract address: {}", e) + } + RegularContractAddressFromFeltError::SpecialAddress(e) => { + write!(f, "got 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 two starknet special constract address `0x0` and `0x1`. + /// + /// https://docs.starknet.io/learn/protocol/state#special-addresses + pub fn is_regular_contract_address(&self) -> bool { + self >= &Felt::TWO && self < &PATRICIA_KEY_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::TooBig)?; + + Ok(RegularContractAddress(contract_address)) + } +} + +#[derive(Debug)] +pub enum RegularContractAddressFromStrError { + BadContractAddress(PatriciaKeyFromStrError), + SpecialContractAddressZero, + SpecialContractAddressOne, +} + +impl core::fmt::Display for RegularContractAddressFromStrError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RegularContractAddressFromStrError::BadContractAddress(e) => { + write!(f, "invalid felt string: {e}") + } + RegularContractAddressFromStrError::SpecialContractAddressZero => write!( + f, + "address 0x0 is reserved as the default caller address and has no storage" + ), + RegularContractAddressFromStrError::SpecialContractAddressOne => write!( + f, + "address 0x1 is reserved as storage space for block mapping" + ), + } + } +} + +#[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::BadContractAddress)?; + + if AsRef::::as_ref(&contract_address) == &Felt::ZERO { + return Err(RegularContractAddressFromStrError::SpecialContractAddressZero); + } + if AsRef::::as_ref(&contract_address) == &Felt::ONE { + return Err(RegularContractAddressFromStrError::SpecialContractAddressOne); + } + + Ok(RegularContractAddress(contract_address)) + } +} + +impl RegularContractAddress { + pub const fn from_hex_unchecked(s: &'static str) -> RegularContractAddress { + let contract_address = ContractAddress::from_hex_unchecked(s); + + RegularContractAddress(contract_address) + } +}