From 3183fd92f4c41e7f42729a8b6046cd18f81375c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Tue, 16 Sep 2025 12:03:52 -0300 Subject: [PATCH 1/9] feat: add ContractAddress type --- crates/starknet-types-core/Cargo.toml | 2 +- .../src/contract_address.rs | 177 ++++++++++++++++++ crates/starknet-types-core/src/lib.rs | 1 + 3 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 crates/starknet-types-core/src/contract_address.rs diff --git a/crates/starknet-types-core/Cargo.toml b/crates/starknet-types-core/Cargo.toml index 0742cdd2..b97312f7 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 00000000..75bc5d98 --- /dev/null +++ b/crates/starknet-types-core/src/contract_address.rs @@ -0,0 +1,177 @@ +//! 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; + +#[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(Felt); + +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 + } +} + +impl From for Felt { + fn from(value: ContractAddress) -> Self { + value.0 + } +} + +#[derive(Debug, Copy, Clone)] +/// In Starknet, contract addresses must follow specific constraints to be considered valid: +/// - They must be greater than or equal to 2, as addresses 0 and 1 are reserved for system 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) +/// - They must be less than 2^251 (0x800000000000000000000000000000000000000000000000000000000000000) +/// +/// Making the valid addressabe range be [2, 2^251) +pub enum ContactAddressFromFeltError { + Zero, + One, + TooBig, +} + +impl core::fmt::Display for ContactAddressFromFeltError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ContactAddressFromFeltError::Zero => { + write!( + f, + "address 0x0 is reserved as the default caller address and has no storage" + ) + } + ContactAddressFromFeltError::One => { + write!( + f, + "address 0x1 is reserved as storage space for block mapping" + ) + } + ContactAddressFromFeltError::TooBig => { + write!(f, "the highest possible address is 2^251 - 1") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ContactAddressFromFeltError {} + +const ADDRESS_UPPER_BOUND: Felt = + Felt::from_hex_unwrap("0x800000000000000000000000000000000000000000000000000000000000000"); + +/// Validates that a Felt value represents a valid Starknet contract address. +/// +/// This validation is critical for preventing funds from being sent to invalid addresses, +/// which would result in permanent loss. +impl Felt { + pub fn is_valid_contract_address(&self) -> bool { + self >= &Felt::from(2u64) && self < &ADDRESS_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 = ContractAddressFromStrError; + + fn from_str(s: &str) -> Result { + let felt = Felt::from_str(s).map_err(ContractAddressFromStrError::BadFelt)?; + let contract_address = + ContractAddress::try_from(felt).map_err(ContractAddressFromStrError::BadAddress)?; + + Ok(contract_address) + } +} + +impl ContractAddress { + pub const fn from_hex_unchecked(s: &'static str) -> ContractAddress { + let felt = Felt::from_hex_unwrap(s); + + ContractAddress(felt) + } +} + +#[cfg(test)] +mod test { + #[cfg(feature = "alloc")] + pub extern crate alloc; + use proptest::prelude::*; + + use crate::{ + contract_address::{ContractAddress, ADDRESS_UPPER_BOUND}, + felt::Felt, + }; + + #[test] + fn basic_values() { + assert!(ContractAddress::try_from(Felt::ZERO).is_err()); + assert!(ContractAddress::try_from(Felt::ONE).is_err()); + assert!(ContractAddress::try_from(ADDRESS_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 bda7d420..e814d931 100644 --- a/crates/starknet-types-core/src/lib.rs +++ b/crates/starknet-types-core/src/lib.rs @@ -8,6 +8,7 @@ pub mod hash; pub mod felt; pub mod qm31; +pub mod contract_address; #[cfg(any(feature = "std", feature = "alloc"))] pub mod short_string; pub mod u256; From 41c3f69b63f712011cf6bef975ba252423b47bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Tue, 30 Sep 2025 15:41:12 -0300 Subject: [PATCH 2/9] patricia key --- .../src/contract_address.rs | 13 +-- crates/starknet-types-core/src/lib.rs | 1 + .../starknet-types-core/src/patricia_key.rs | 107 ++++++++++++++++++ 3 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 crates/starknet-types-core/src/patricia_key.rs diff --git a/crates/starknet-types-core/src/contract_address.rs b/crates/starknet-types-core/src/contract_address.rs index 75bc5d98..202ad585 100644 --- a/crates/starknet-types-core/src/contract_address.rs +++ b/crates/starknet-types-core/src/contract_address.rs @@ -7,7 +7,7 @@ use core::str::FromStr; -use crate::felt::Felt; +use crate::{felt::Felt, patricia_key::PATRICIA_KEY_UPPER_BOUND}; #[repr(transparent)] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -76,16 +76,13 @@ impl core::fmt::Display for ContactAddressFromFeltError { #[cfg(feature = "std")] impl std::error::Error for ContactAddressFromFeltError {} -const ADDRESS_UPPER_BOUND: Felt = - Felt::from_hex_unwrap("0x800000000000000000000000000000000000000000000000000000000000000"); - /// Validates that a Felt value represents a valid Starknet contract address. /// /// This validation is critical for preventing funds from being sent to invalid addresses, /// which would result in permanent loss. impl Felt { pub fn is_valid_contract_address(&self) -> bool { - self >= &Felt::from(2u64) && self < &ADDRESS_UPPER_BOUND + self >= &Felt::from(2u64) && self < &PATRICIA_KEY_UPPER_BOUND } } @@ -99,7 +96,7 @@ impl TryFrom for ContractAddress { if value == Felt::ONE { return Err(ContactAddressFromFeltError::One); } - if value >= ADDRESS_UPPER_BOUND { + if value >= PATRICIA_KEY_UPPER_BOUND { return Err(ContactAddressFromFeltError::TooBig); } @@ -149,7 +146,7 @@ mod test { use proptest::prelude::*; use crate::{ - contract_address::{ContractAddress, ADDRESS_UPPER_BOUND}, + contract_address::{ContractAddress, PATRICIA_KEY_UPPER_BOUND}, felt::Felt, }; @@ -157,7 +154,7 @@ mod test { fn basic_values() { assert!(ContractAddress::try_from(Felt::ZERO).is_err()); assert!(ContractAddress::try_from(Felt::ONE).is_err()); - assert!(ContractAddress::try_from(ADDRESS_UPPER_BOUND).is_err()); + assert!(ContractAddress::try_from(PATRICIA_KEY_UPPER_BOUND).is_err()); let felt = Felt::TWO; let contract_address = ContractAddress::try_from(felt).unwrap(); diff --git a/crates/starknet-types-core/src/lib.rs b/crates/starknet-types-core/src/lib.rs index e814d931..e2493736 100644 --- a/crates/starknet-types-core/src/lib.rs +++ b/crates/starknet-types-core/src/lib.rs @@ -9,6 +9,7 @@ pub mod felt; pub mod qm31; pub mod contract_address; +pub mod patricia_key; #[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 00000000..b04e3e04 --- /dev/null +++ b/crates/starknet-types-core/src/patricia_key.rs @@ -0,0 +1,107 @@ +//! 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`, ranging from `0x0` to `PATRICIA_KEY_UPPER_BOUND` (2^251). +//! 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_unwrap("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)] +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 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}"), + } + } +} + +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_unwrap(s); + + PatriciaKey(felt) + } +} From bdece5e1cfb9572e09920b3bd232213ec477c691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Tue, 30 Sep 2025 18:52:38 -0300 Subject: [PATCH 3/9] feat: add PatriciaKey and RegularContractAddress types --- .../src/contract_address.rs | 109 +++------ crates/starknet-types-core/src/lib.rs | 1 + .../starknet-types-core/src/patricia_key.rs | 11 +- .../src/regular_contract_address.rs | 207 ++++++++++++++++++ 4 files changed, 245 insertions(+), 83 deletions(-) create mode 100644 crates/starknet-types-core/src/regular_contract_address.rs diff --git a/crates/starknet-types-core/src/contract_address.rs b/crates/starknet-types-core/src/contract_address.rs index 202ad585..7b1487d2 100644 --- a/crates/starknet-types-core/src/contract_address.rs +++ b/crates/starknet-types-core/src/contract_address.rs @@ -7,7 +7,12 @@ use core::str::FromStr; -use crate::{felt::Felt, patricia_key::PATRICIA_KEY_UPPER_BOUND}; +use crate::{ + felt::Felt, + patricia_key::{ + PatriciaKey, PatriciaKeyFromFeltError, PatriciaKeyFromStrError, PATRICIA_KEY_UPPER_BOUND, + }, +}; #[repr(transparent)] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -17,7 +22,7 @@ use crate::{felt::Felt, patricia_key::PATRICIA_KEY_UPPER_BOUND}; feature = "parity-scale-codec", derive(parity_scale_codec::Encode, parity_scale_codec::Decode) )] -pub struct ContractAddress(Felt); +pub struct ContractAddress(PatriciaKey); impl core::fmt::Display for ContractAddress { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { @@ -27,113 +32,60 @@ impl core::fmt::Display for ContractAddress { impl AsRef for ContractAddress { fn as_ref(&self) -> &Felt { - &self.0 + self.0.as_ref() } } impl From for Felt { fn from(value: ContractAddress) -> Self { - value.0 + value.0.into() } } -#[derive(Debug, Copy, Clone)] -/// In Starknet, contract addresses must follow specific constraints to be considered valid: -/// - They must be greater than or equal to 2, as addresses 0 and 1 are reserved for system 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) -/// - They must be less than 2^251 (0x800000000000000000000000000000000000000000000000000000000000000) -/// -/// Making the valid addressabe range be [2, 2^251) -pub enum ContactAddressFromFeltError { - Zero, - One, - TooBig, +impl AsRef for ContractAddress { + fn as_ref(&self) -> &PatriciaKey { + &self.0 + } } -impl core::fmt::Display for ContactAddressFromFeltError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - ContactAddressFromFeltError::Zero => { - write!( - f, - "address 0x0 is reserved as the default caller address and has no storage" - ) - } - ContactAddressFromFeltError::One => { - write!( - f, - "address 0x1 is reserved as storage space for block mapping" - ) - } - ContactAddressFromFeltError::TooBig => { - write!(f, "the highest possible address is 2^251 - 1") - } - } +impl From for PatriciaKey { + fn from(value: ContractAddress) -> Self { + value.0 } } -#[cfg(feature = "std")] -impl std::error::Error for ContactAddressFromFeltError {} - -/// Validates that a Felt value represents a valid Starknet contract address. -/// -/// This validation is critical for preventing funds from being sent to invalid addresses, -/// which would result in permanent loss. -impl Felt { - pub fn is_valid_contract_address(&self) -> bool { - self >= &Felt::from(2u64) && self < &PATRICIA_KEY_UPPER_BOUND +impl From for ContractAddress { + fn from(value: PatriciaKey) -> Self { + ContractAddress(value) } } impl TryFrom for ContractAddress { - type Error = ContactAddressFromFeltError; + type Error = PatriciaKeyFromFeltError; fn try_from(value: Felt) -> Result { - if value == Felt::ZERO { - return Err(ContactAddressFromFeltError::Zero); - } - if value == Felt::ONE { - return Err(ContactAddressFromFeltError::One); - } - if value >= PATRICIA_KEY_UPPER_BOUND { - return Err(ContactAddressFromFeltError::TooBig); - } - - Ok(ContractAddress(value)) + Ok(ContractAddress(PatriciaKey::try_from(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 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 FromStr for ContractAddress { - type Err = ContractAddressFromStrError; + type Err = PatriciaKeyFromStrError; fn from_str(s: &str) -> Result { - let felt = Felt::from_str(s).map_err(ContractAddressFromStrError::BadFelt)?; - let contract_address = - ContractAddress::try_from(felt).map_err(ContractAddressFromStrError::BadAddress)?; - - Ok(contract_address) + Ok(ContractAddress(PatriciaKey::from_str(s)?)) } } impl ContractAddress { - pub const fn from_hex_unchecked(s: &'static str) -> ContractAddress { - let felt = Felt::from_hex_unwrap(s); + pub const fn from_hex_unwrap(s: &'static str) -> ContractAddress { + let felt = PatriciaKey::from_hex_unwrap(s); ContractAddress(felt) } @@ -146,8 +98,7 @@ mod test { use proptest::prelude::*; use crate::{ - contract_address::{ContractAddress, PATRICIA_KEY_UPPER_BOUND}, - felt::Felt, + contract_address::ContractAddress, felt::Felt, patricia_key::PATRICIA_KEY_UPPER_BOUND, }; #[test] diff --git a/crates/starknet-types-core/src/lib.rs b/crates/starknet-types-core/src/lib.rs index e2493736..c0d2e0b2 100644 --- a/crates/starknet-types-core/src/lib.rs +++ b/crates/starknet-types-core/src/lib.rs @@ -10,6 +10,7 @@ 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 index b04e3e04..d9511b7b 100644 --- a/crates/starknet-types-core/src/patricia_key.rs +++ b/crates/starknet-types-core/src/patricia_key.rs @@ -4,7 +4,7 @@ //! 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`, ranging from `0x0` to `PATRICIA_KEY_UPPER_BOUND` (2^251). +//! 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. @@ -43,14 +43,14 @@ impl From for Felt { } } -#[derive(Debug)] +#[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 bound is 2^251 got {:#x}", + "invalid felt value for patricia key. Upper non-inclusinve bound is 2^251 got {:#x}", self.0 ) } @@ -86,6 +86,9 @@ impl core::fmt::Display for PatriciaKeyFromStrError { } } +#[cfg(feature = "std")] +impl std::error::Error for PatriciaKeyFromStrError {} + impl FromStr for PatriciaKey { type Err = PatriciaKeyFromStrError; @@ -99,7 +102,7 @@ impl FromStr for PatriciaKey { } impl PatriciaKey { - pub const fn from_hex_unchecked(s: &'static str) -> PatriciaKey { + pub const fn from_hex_unwrap(s: &'static str) -> PatriciaKey { let felt = Felt::from_hex_unwrap(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 00000000..3a03d292 --- /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_unwrap(s: &'static str) -> RegularContractAddress { + let contract_address = ContractAddress::from_hex_unwrap(s); + + RegularContractAddress(contract_address) + } +} From dbe73a88a606c683a4f6c89421909d315e81a599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Mon, 6 Oct 2025 15:00:01 -0300 Subject: [PATCH 4/9] doc: RegularContractAddress --- .../src/regular_contract_address.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/starknet-types-core/src/regular_contract_address.rs b/crates/starknet-types-core/src/regular_contract_address.rs index 3a03d292..d8d7fc65 100644 --- a/crates/starknet-types-core/src/regular_contract_address.rs +++ b/crates/starknet-types-core/src/regular_contract_address.rs @@ -1,3 +1,14 @@ +//! 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. +//! 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::{ @@ -63,7 +74,7 @@ impl From for ContractAddress { /// 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) +/// * 0x1 functions as a storage space for block mapping /// /// Making the regular contract address range be [2, 2^251) #[derive(Debug, Clone, Copy)] From fe06694f9edf3a6b2bb54d1ed42bd6115143061a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Mon, 13 Oct 2025 11:44:03 -0300 Subject: [PATCH 5/9] add 0x2 0x3 to reserved addresses --- .../src/regular_contract_address.rs | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/crates/starknet-types-core/src/regular_contract_address.rs b/crates/starknet-types-core/src/regular_contract_address.rs index d8d7fc65..97f1ca09 100644 --- a/crates/starknet-types-core/src/regular_contract_address.rs +++ b/crates/starknet-types-core/src/regular_contract_address.rs @@ -3,6 +3,8 @@ //! 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. @@ -75,12 +77,16 @@ impl From for ContractAddress { /// 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 [2, 2^251) +/// 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 { @@ -98,6 +104,12 @@ impl core::fmt::Display for RegularContractAddressFromContractAddressError { "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") + } } } } @@ -115,6 +127,12 @@ impl TryFrom for RegularContractAddress { 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)) } @@ -144,11 +162,12 @@ 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`. + /// 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::TWO && self < &PATRICIA_KEY_UPPER_BOUND + self > &Felt::THREE && self < &PATRICIA_KEY_UPPER_BOUND } } @@ -159,15 +178,15 @@ impl TryFrom for RegularContractAddress { let contract_address = ContractAddress::try_from(value) .map_err(RegularContractAddressFromFeltError::TooBig)?; - Ok(RegularContractAddress(contract_address)) + RegularContractAddress::try_from(contract_address) + .map_err(RegularContractAddressFromFeltError::SpecialAddress) } } #[derive(Debug)] pub enum RegularContractAddressFromStrError { BadContractAddress(PatriciaKeyFromStrError), - SpecialContractAddressZero, - SpecialContractAddressOne, + SpecialContractAddress(RegularContractAddressFromContractAddressError), } impl core::fmt::Display for RegularContractAddressFromStrError { @@ -176,14 +195,9 @@ impl core::fmt::Display for RegularContractAddressFromStrError { 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" - ), + RegularContractAddressFromStrError::SpecialContractAddress(e) => { + write!(f, "got special contract address: {e}") + } } } } @@ -198,14 +212,8 @@ impl FromStr for RegularContractAddress { 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)) + RegularContractAddress::try_from(contract_address) + .map_err(RegularContractAddressFromStrError::SpecialContractAddress) } } From 0a1f96b0bb6f065ecb552a4f1d920607dbef6bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Mon, 13 Oct 2025 12:48:30 -0300 Subject: [PATCH 6/9] rebase and rename method --- crates/starknet-types-core/src/contract_address.rs | 13 ++++++++++--- crates/starknet-types-core/src/patricia_key.rs | 2 +- .../src/regular_contract_address.rs | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/starknet-types-core/src/contract_address.rs b/crates/starknet-types-core/src/contract_address.rs index 7b1487d2..4b756bb5 100644 --- a/crates/starknet-types-core/src/contract_address.rs +++ b/crates/starknet-types-core/src/contract_address.rs @@ -24,6 +24,13 @@ use crate::{ )] 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"); +} + impl core::fmt::Display for ContractAddress { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{}", self.0) @@ -84,10 +91,10 @@ impl FromStr for ContractAddress { } impl ContractAddress { - pub const fn from_hex_unwrap(s: &'static str) -> ContractAddress { - let felt = PatriciaKey::from_hex_unwrap(s); + pub const fn from_hex_unchecked(s: &'static str) -> ContractAddress { + let patricia_key = PatriciaKey::from_hex_unchecked(s); - ContractAddress(felt) + ContractAddress(patricia_key) } } diff --git a/crates/starknet-types-core/src/patricia_key.rs b/crates/starknet-types-core/src/patricia_key.rs index d9511b7b..54fc94d5 100644 --- a/crates/starknet-types-core/src/patricia_key.rs +++ b/crates/starknet-types-core/src/patricia_key.rs @@ -102,7 +102,7 @@ impl FromStr for PatriciaKey { } impl PatriciaKey { - pub const fn from_hex_unwrap(s: &'static str) -> PatriciaKey { + pub const fn from_hex_unchecked(s: &'static str) -> PatriciaKey { let felt = Felt::from_hex_unwrap(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 index 97f1ca09..139fc24e 100644 --- a/crates/starknet-types-core/src/regular_contract_address.rs +++ b/crates/starknet-types-core/src/regular_contract_address.rs @@ -218,8 +218,8 @@ impl FromStr for RegularContractAddress { } impl RegularContractAddress { - pub const fn from_hex_unwrap(s: &'static str) -> RegularContractAddress { - let contract_address = ContractAddress::from_hex_unwrap(s); + pub const fn from_hex_unchecked(s: &'static str) -> RegularContractAddress { + let contract_address = ContractAddress::from_hex_unchecked(s); RegularContractAddress(contract_address) } From 7b24a2aa722329270fbfb02feddaa90d449eb482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Mon, 13 Oct 2025 15:17:38 -0300 Subject: [PATCH 7/9] fix: different felt display if alloc not enabled --- crates/starknet-types-core/src/patricia_key.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/starknet-types-core/src/patricia_key.rs b/crates/starknet-types-core/src/patricia_key.rs index 54fc94d5..633c8813 100644 --- a/crates/starknet-types-core/src/patricia_key.rs +++ b/crates/starknet-types-core/src/patricia_key.rs @@ -48,11 +48,19 @@ pub struct PatriciaKeyFromFeltError(Felt); impl core::fmt::Display for PatriciaKeyFromFeltError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!( + #[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 + ); } } From 1a3ea40adde97dde8712eef7d916837148120a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Wed, 15 Oct 2025 13:58:39 -0300 Subject: [PATCH 8/9] feat: add UPPER_BOUND & LOWER_BOUND to all our new types --- .../src/contract_address.rs | 18 ++++-- .../starknet-types-core/src/patricia_key.rs | 12 ++++ .../src/regular_contract_address.rs | 55 +++++++++++++++++-- 3 files changed, 75 insertions(+), 10 deletions(-) diff --git a/crates/starknet-types-core/src/contract_address.rs b/crates/starknet-types-core/src/contract_address.rs index 4b756bb5..be717b26 100644 --- a/crates/starknet-types-core/src/contract_address.rs +++ b/crates/starknet-types-core/src/contract_address.rs @@ -9,9 +9,7 @@ use core::str::FromStr; use crate::{ felt::Felt, - patricia_key::{ - PatriciaKey, PatriciaKeyFromFeltError, PatriciaKeyFromStrError, PATRICIA_KEY_UPPER_BOUND, - }, + patricia_key::{PatriciaKey, PatriciaKeyFromFeltError, PatriciaKeyFromStrError}, }; #[repr(transparent)] @@ -29,6 +27,11 @@ impl ContractAddress { 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 + pub const UPPER_BOUND: Self = Self(PatriciaKey::UPPER_BOUND); } impl core::fmt::Display for ContractAddress { @@ -78,7 +81,7 @@ impl TryFrom for ContractAddress { 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 + self < &Felt::from(ContractAddress::UPPER_BOUND) } } @@ -91,6 +94,11 @@ impl FromStr for ContractAddress { } 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); @@ -110,8 +118,6 @@ mod test { #[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; diff --git a/crates/starknet-types-core/src/patricia_key.rs b/crates/starknet-types-core/src/patricia_key.rs index 633c8813..b9d0c087 100644 --- a/crates/starknet-types-core/src/patricia_key.rs +++ b/crates/starknet-types-core/src/patricia_key.rs @@ -25,6 +25,13 @@ pub const PATRICIA_KEY_UPPER_BOUND: Felt = )] 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 = 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) @@ -110,6 +117,11 @@ impl FromStr for PatriciaKey { } 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); diff --git a/crates/starknet-types-core/src/regular_contract_address.rs b/crates/starknet-types-core/src/regular_contract_address.rs index 139fc24e..bce9d5b7 100644 --- a/crates/starknet-types-core/src/regular_contract_address.rs +++ b/crates/starknet-types-core/src/regular_contract_address.rs @@ -16,9 +16,7 @@ use core::str::FromStr; use crate::{ contract_address::ContractAddress, felt::Felt, - patricia_key::{ - PatriciaKey, PatriciaKeyFromFeltError, PatriciaKeyFromStrError, PATRICIA_KEY_UPPER_BOUND, - }, + patricia_key::{PatriciaKey, PatriciaKeyFromFeltError, PatriciaKeyFromStrError}, }; #[repr(transparent)] @@ -31,6 +29,13 @@ use crate::{ )] 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) @@ -167,7 +172,8 @@ impl Felt { /// 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::THREE && self < &PATRICIA_KEY_UPPER_BOUND + self >= &Felt::from(RegularContractAddress::LOWER_BOUND) + && self < &Felt::from(RegularContractAddress::UPPER_BOUND) } } @@ -218,9 +224,50 @@ impl FromStr for RegularContractAddress { } 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(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()); + } + } + } +} From 4070b1c72f75ffbb652311fdc9dd0ebcffc19755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Delabrouille?= Date: Tue, 21 Oct 2025 17:45:26 -0300 Subject: [PATCH 9/9] feat: modify ContracAddress upper bound to be PATRICIA_KEY_UPPER_BOUND -256 --- .../src/contract_address.rs | 101 ++++++++++++++++-- .../starknet-types-core/src/patricia_key.rs | 40 ++++++- .../src/regular_contract_address.rs | 24 +++-- 3 files changed, 139 insertions(+), 26 deletions(-) diff --git a/crates/starknet-types-core/src/contract_address.rs b/crates/starknet-types-core/src/contract_address.rs index be717b26..1d0338d1 100644 --- a/crates/starknet-types-core/src/contract_address.rs +++ b/crates/starknet-types-core/src/contract_address.rs @@ -9,7 +9,10 @@ use core::str::FromStr; use crate::{ felt::Felt, - patricia_key::{PatriciaKey, PatriciaKeyFromFeltError, PatriciaKeyFromStrError}, + patricia_key::{ + PatriciaKey, PatriciaKeyFromFeltError, PatriciaKeyFromStrError, + STORAGE_LEAF_ADDRESS_UPPER_BOUND, + }, }; #[repr(transparent)] @@ -31,7 +34,9 @@ impl ContractAddress { /// Lower inclusive bound pub const LOWER_BOUND: Self = Self::ZERO; /// Upper non-inclusive bound - pub const UPPER_BOUND: Self = Self(PatriciaKey::UPPER_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 { @@ -64,17 +69,67 @@ impl From for PatriciaKey { } } -impl From for ContractAddress { - fn from(value: PatriciaKey) -> Self { - ContractAddress(value) +#[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 = PatriciaKeyFromFeltError; + type Error = ContractAddressFromFeltError; fn try_from(value: Felt) -> Result { - Ok(ContractAddress(PatriciaKey::try_from(value)?)) + let pk = PatriciaKey::try_from(value).map_err(ContractAddressFromFeltError::PatriciaKey)?; + let ca = ContractAddress::try_from(pk).map_err(ContractAddressFromFeltError::OutOfBound)?; + + Ok(ca) } } @@ -85,19 +140,43 @@ impl Felt { } } +#[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 = PatriciaKeyFromStrError; + type Err = ContractAddressFromStrError; fn from_str(s: &str) -> Result { - Ok(ContractAddress(PatriciaKey::from_str(s)?)) + 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 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); diff --git a/crates/starknet-types-core/src/patricia_key.rs b/crates/starknet-types-core/src/patricia_key.rs index b9d0c087..b447a983 100644 --- a/crates/starknet-types-core/src/patricia_key.rs +++ b/crates/starknet-types-core/src/patricia_key.rs @@ -12,8 +12,24 @@ use core::str::FromStr; use crate::felt::Felt; -pub const PATRICIA_KEY_UPPER_BOUND: Felt = - Felt::from_hex_unwrap("0x800000000000000000000000000000000000000000000000000000000000000"); +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)] @@ -29,7 +45,7 @@ impl PatriciaKey { /// Lower inclusive bound pub const LOWER_BOUND: Self = Self(Felt::ZERO); /// Upper non-inclusive bound - pub const UPPER_BOUND: Self = Self(PATRICIA_KEY_UPPER_BOUND); + pub const UPPER_BOUND: Self = PATRICIA_KEY_UPPER_BOUND; } impl core::fmt::Display for PatriciaKey { @@ -78,7 +94,7 @@ impl TryFrom for PatriciaKey { type Error = PatriciaKeyFromFeltError; fn try_from(value: Felt) -> Result { - if value >= PATRICIA_KEY_UPPER_BOUND { + if value >= PATRICIA_KEY_UPPER_BOUND.0 { return Err(PatriciaKeyFromFeltError(value)); } @@ -128,3 +144,19 @@ impl PatriciaKey { 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 index bce9d5b7..57165f24 100644 --- a/crates/starknet-types-core/src/regular_contract_address.rs +++ b/crates/starknet-types-core/src/regular_contract_address.rs @@ -14,9 +14,11 @@ use core::str::FromStr; use crate::{ - contract_address::ContractAddress, + contract_address::{ + ContractAddress, ContractAddressFromFeltError, ContractAddressFromStrError, + }, felt::Felt, - patricia_key::{PatriciaKey, PatriciaKeyFromFeltError, PatriciaKeyFromStrError}, + patricia_key::PatriciaKey, }; #[repr(transparent)] @@ -143,20 +145,20 @@ impl TryFrom for RegularContractAddress { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug)] pub enum RegularContractAddressFromFeltError { - TooBig(PatriciaKeyFromFeltError), + ContractAddress(ContractAddressFromFeltError), SpecialAddress(RegularContractAddressFromContractAddressError), } impl core::fmt::Display for RegularContractAddressFromFeltError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - RegularContractAddressFromFeltError::TooBig(e) => { + RegularContractAddressFromFeltError::ContractAddress(e) => { write!(f, "invalid contract address: {}", e) } RegularContractAddressFromFeltError::SpecialAddress(e) => { - write!(f, "got special contract address: {e}") + write!(f, "value is a special contract address: {e}") } } } @@ -182,7 +184,7 @@ impl TryFrom for RegularContractAddress { fn try_from(value: Felt) -> Result { let contract_address = ContractAddress::try_from(value) - .map_err(RegularContractAddressFromFeltError::TooBig)?; + .map_err(RegularContractAddressFromFeltError::ContractAddress)?; RegularContractAddress::try_from(contract_address) .map_err(RegularContractAddressFromFeltError::SpecialAddress) @@ -191,14 +193,14 @@ impl TryFrom for RegularContractAddress { #[derive(Debug)] pub enum RegularContractAddressFromStrError { - BadContractAddress(PatriciaKeyFromStrError), + ContractAddress(ContractAddressFromStrError), SpecialContractAddress(RegularContractAddressFromContractAddressError), } impl core::fmt::Display for RegularContractAddressFromStrError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - RegularContractAddressFromStrError::BadContractAddress(e) => { + RegularContractAddressFromStrError::ContractAddress(e) => { write!(f, "invalid felt string: {e}") } RegularContractAddressFromStrError::SpecialContractAddress(e) => { @@ -216,7 +218,7 @@ impl FromStr for RegularContractAddress { fn from_str(s: &str) -> Result { let contract_address = ContractAddress::from_str(s) - .map_err(RegularContractAddressFromStrError::BadContractAddress)?; + .map_err(RegularContractAddressFromStrError::ContractAddress)?; RegularContractAddress::try_from(contract_address) .map_err(RegularContractAddressFromStrError::SpecialContractAddress) @@ -253,7 +255,7 @@ mod test { 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(PATRICIA_KEY_UPPER_BOUND).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();