Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/typo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Typo Check

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
typos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@master
2 changes: 1 addition & 1 deletion crates/starknet-types-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
218 changes: 218 additions & 0 deletions crates/starknet-types-core/src/contract_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
//! A starknet contract address
//!
//! In Starknet, a valid contract addresses exist 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 Rust type `ContractAddress`, that can guarantee 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<Felt> for ContractAddress {
fn as_ref(&self) -> &Felt {
self.0.as_ref()
}
}

impl From<ContractAddress> for Felt {
fn from(value: ContractAddress) -> Self {
value.0.into()
}
}

impl AsRef<PatriciaKey> for ContractAddress {
fn as_ref(&self) -> &PatriciaKey {
&self.0
}
}

impl From<ContractAddress> for PatriciaKey {
fn from(value: ContractAddress) -> Self {
value.0
}
}

#[derive(Debug)]
pub enum ContractAddressFromPatriciaKeyError {
OutOfBounds,
}

impl core::fmt::Display for ContractAddressFromPatriciaKeyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ContractAddressFromPatriciaKeyError::OutOfBounds => write!(
f,
"value out of bounds, upper non-inclusive bound is {}",
ContractAddress::UPPER_BOUND
),
}
}
}

#[cfg(feature = "std")]
impl std::error::Error for ContractAddressFromPatriciaKeyError {}

impl TryFrom<PatriciaKey> for ContractAddress {
type Error = ContractAddressFromPatriciaKeyError;

fn try_from(value: PatriciaKey) -> Result<Self, Self::Error> {
if value >= STORAGE_LEAF_ADDRESS_UPPER_BOUND {
Err(ContractAddressFromPatriciaKeyError::OutOfBounds)
} else {
Ok(ContractAddress(value))
}
}
}

#[derive(Debug)]
pub enum ContractAddressFromFeltError {
PatriciaKey(PatriciaKeyFromFeltError),
OutOfBounds(ContractAddressFromPatriciaKeyError),
}

impl core::fmt::Display for ContractAddressFromFeltError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ContractAddressFromFeltError::OutOfBounds(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<Felt> for ContractAddress {
type Error = ContractAddressFromFeltError;

fn try_from(value: Felt) -> Result<Self, Self::Error> {
let pk = PatriciaKey::try_from(value).map_err(ContractAddressFromFeltError::PatriciaKey)?;
let ca =
ContractAddress::try_from(pk).map_err(ContractAddressFromFeltError::OutOfBounds)?;

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),
OutOfBounds(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::OutOfBounds(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<Self, Self::Err> {
let pk = PatriciaKey::from_str(s).map_err(ContractAddressFromStrError::PatriciaKey)?;
let ca = ContractAddress::try_from(pk).map_err(ContractAddressFromStrError::OutOfBounds)?;

Ok(ca)
}
}

impl ContractAddress {
/// Creates 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::<Felt>()) {
if x.is_valid_contract_address() {
prop_assert!(ContractAddress::try_from(*x).is_ok());
} else {
prop_assert!(ContractAddress::try_from(*x).is_err());
}
}
}
}
2 changes: 1 addition & 1 deletion crates/starknet-types-core/src/felt/alloc_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl Felt {
/// 2. an amount of padding zeros so that the resulting string length is fixed (This amount may be 0),
/// 3. the felt value represented in hexadecimal
///
/// The resulting string is guaranted to be 66 chars long, which is enough to represent `Felt::MAX`:
/// The resulting string is guaranteed to be 66 chars long, which is enough to represent `Felt::MAX`:
/// 2 chars for the `0x` prefix and 64 chars for the padded hexadecimal felt value.
pub fn to_fixed_hex_string(&self) -> alloc::string::String {
alloc::format!("{self:#066x}")
Expand Down
10 changes: 5 additions & 5 deletions crates/starknet-types-core/src/felt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,22 +473,22 @@ impl Add<&Felt> for u64 {
[0, 0, 0, low] => self.checked_add(low),
// Now we need to compare the 3 most significant digits.
// There are two relevant cases from now on, either `rhs` behaves like a
// substraction of a `u64` or the result of the sum falls out of range.
// subtraction of a `u64` or the result of the sum falls out of range.

// The 3 MSB only match the prime for Felt::max_value(), which is -1
// in the signed field, so this is equivalent to substracting 1 to `self`.
// in the signed field, so this is equivalent to subtracting 1 to `self`.
[hi @ .., _] if hi == PRIME_DIGITS_BE_HI => self.checked_sub(1),

// For the remaining values between `[-u64::MAX..0]` (where `{0, -1}` have
// already been covered) the MSB matches that of `PRIME - u64::MAX`.
// Because we're in the negative number case, we count down. Because `0`
// and `-1` correspond to different MSBs, `0` and `1` in the LSB are less
// than `-u64::MAX`, the smallest value we can add to (read, substract its
// than `-u64::MAX`, the smallest value we can add to (read, subtract its
// magnitude from) a `u64` number, meaning we exclude them from the valid
// case.
// For the remaining range, we take the absolute value module-2 while
// correcting by substracting `1` (note we actually substract `2` because
// the absolute value itself requires substracting `1`.
// correcting by subtracting `1` (note we actually subtract `2` because
// the absolute value itself requires subtracting `1`.
[hi @ .., low] if hi == PRIME_MINUS_U64_MAX_DIGITS_BE_HI && low >= 2 => {
(self).checked_sub(u64::MAX - (low - 2))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ mod tests {
fn parity_scale_codec_serialization() {
use parity_scale_codec::{Decode, Encode};

// use an endianness-asymetric number to test that byte order is correct in serialization
// use an endianness-asymmetric number to test that byte order is correct in serialization
let initial_felt = Felt::from_hex("0xabcdef123").unwrap();

// serialize the felt
Expand Down
3 changes: 3 additions & 0 deletions crates/starknet-types-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(feature = "alloc")]
pub mod short_string;
pub mod u256;
Expand Down
Loading