use std::fmt::Display;
use std::ops::Rem;
use std::str::FromStr;
use anyhow::Context;
use fake::Dummy;
use pathfinder_crypto::hash::HashChain;
use pathfinder_crypto::Felt;
use primitive_types::H160;
use serde::{Deserialize, Serialize};
pub mod casm_class;
pub mod class_definition;
pub mod consensus_info;
pub mod consts;
pub mod event;
pub mod hash;
mod header;
pub mod integration_testing;
mod l1;
mod l2;
mod macros;
pub mod prelude;
pub mod receipt;
pub mod signature;
pub mod state_update;
pub mod test_utils;
pub mod transaction;
pub mod trie;
pub use header::{BlockHeader, BlockHeaderBuilder, L1DataAvailabilityMode, SignedBlockHeader};
pub use l1::{L1BlockHash, L1BlockNumber, L1TransactionHash};
pub use l2::{
ConsensusFinalizedBlockHeader,
ConsensusFinalizedL2Block,
DeclaredClass,
L2Block,
L2BlockToCommit,
};
pub use signature::BlockCommitmentSignature;
pub use state_update::{FoundStorageValue, StateUpdate};
impl ContractAddress {
pub const ONE: ContractAddress = contract_address!("0x1");
pub const TWO: ContractAddress = contract_address!("0x2");
pub const SYSTEM: [ContractAddress; 2] = [ContractAddress::ONE, ContractAddress::TWO];
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContractClass {
pub program: String,
pub entry_points_by_type: serde_json::Value,
}
impl EntryPoint {
pub fn hashed(input: &[u8]) -> Self {
use sha3::Digest;
EntryPoint(truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(
input,
))))
}
pub const CONSTRUCTOR: Self =
entry_point!("0x028FFE4FF0F226A9107253E17A904099AA4F63A02A5621DE0576E5AA71BC5194");
}
impl StateCommitment {
pub fn calculate(
storage_commitment: StorageCommitment,
class_commitment: ClassCommitment,
version: StarknetVersion,
) -> Self {
if class_commitment == ClassCommitment::ZERO
&& storage_commitment == StorageCommitment::ZERO
{
return StateCommitment::ZERO;
}
if class_commitment == ClassCommitment::ZERO && version < StarknetVersion::V_0_14_0 {
return Self(storage_commitment.0);
}
const GLOBAL_STATE_VERSION: Felt = felt_bytes!(b"STARKNET_STATE_V0");
StateCommitment(
pathfinder_crypto::hash::poseidon::poseidon_hash_many(&[
GLOBAL_STATE_VERSION.into(),
storage_commitment.0.into(),
class_commitment.0.into(),
])
.into(),
)
}
}
impl StorageAddress {
pub fn from_name(input: &[u8]) -> Self {
use sha3::Digest;
Self(truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(
input,
))))
}
pub fn from_map_name_and_key(name: &[u8], key: Felt) -> Self {
use sha3::Digest;
let intermediate = truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(name)));
let value = pathfinder_crypto::hash::pedersen_hash(intermediate, key);
let value = primitive_types::U256::from_big_endian(value.as_be_bytes());
let max_address = primitive_types::U256::from_str_radix(
"0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
16,
)
.unwrap();
let value = value.rem(max_address);
let mut b = [0u8; 32];
value.to_big_endian(&mut b);
Self(Felt::from_be_slice(&b).expect("Truncated value should fit into a felt"))
}
}
#[derive(Copy, Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BlockNumber(u64);
macros::i64_backed_u64::new_get_partialeq!(BlockNumber);
macros::i64_backed_u64::serdes!(BlockNumber);
impl From<BlockNumber> for Felt {
fn from(x: BlockNumber) -> Self {
Felt::from(x.0)
}
}
impl std::iter::Iterator for BlockNumber {
type Item = BlockNumber;
fn next(&mut self) -> Option<Self::Item> {
Some(*self + 1)
}
}
#[derive(Copy, Debug, Clone, PartialEq, Eq, Default)]
pub struct BlockTimestamp(u64);
macros::i64_backed_u64::new_get_partialeq!(BlockTimestamp);
macros::i64_backed_u64::serdes!(BlockTimestamp);
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub struct TransactionIndex(u64);
macros::i64_backed_u64::new_get_partialeq!(TransactionIndex);
macros::i64_backed_u64::serdes!(TransactionIndex);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
pub struct GasPrice(pub u128);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
pub struct GasPriceHex(pub GasPrice);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
pub struct ResourceAmount(pub u64);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
pub struct Tip(pub u64);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Dummy)]
pub struct TipHex(pub Tip);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
pub struct ResourcePricePerUnit(pub u128);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
pub struct TransactionVersion(pub Felt);
impl TransactionVersion {
pub fn is_zero(&self) -> bool {
self.without_query_version() == 0
}
pub fn without_query_version(&self) -> u128 {
let lower = &self.0.as_be_bytes()[16..];
u128::from_be_bytes(lower.try_into().expect("slice should be the right length"))
}
pub const fn with_query_version(self) -> Self {
let mut bytes = self.0.to_be_bytes();
bytes[15] |= 0b0000_0001;
let felt = match Felt::from_be_bytes(bytes) {
Ok(x) => x,
Err(_) => panic!("Adding query bit to transaction version failed."),
};
Self(felt)
}
pub const fn has_query_version(&self) -> bool {
self.0.as_be_bytes()[15] & 0b0000_0001 != 0
}
pub fn with_query_only(self, query_only: bool) -> Self {
if query_only {
self.with_query_version()
} else {
Self(self.without_query_version().into())
}
}
pub const ZERO: Self = Self(Felt::ZERO);
pub const ONE: Self = Self(Felt::from_u64(1));
pub const TWO: Self = Self(Felt::from_u64(2));
pub const THREE: Self = Self(Felt::from_u64(3));
pub const ZERO_WITH_QUERY_VERSION: Self = Self::ZERO.with_query_version();
pub const ONE_WITH_QUERY_VERSION: Self = Self::ONE.with_query_version();
pub const TWO_WITH_QUERY_VERSION: Self = Self::TWO.with_query_version();
pub const THREE_WITH_QUERY_VERSION: Self = Self::THREE.with_query_version();
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum BlockId {
Number(BlockNumber),
Hash(BlockHash),
Latest,
}
impl BlockId {
pub fn is_latest(&self) -> bool {
self == &Self::Latest
}
}
impl BlockNumber {
pub const GENESIS: BlockNumber = BlockNumber::new_or_panic(0);
pub const MAX: BlockNumber = BlockNumber::new_or_panic(i64::MAX as u64);
pub fn parent(&self) -> Option<Self> {
if self == &Self::GENESIS {
None
} else {
Some(*self - 1)
}
}
pub fn is_zero(&self) -> bool {
self == &Self::GENESIS
}
pub fn checked_add(&self, rhs: u64) -> Option<Self> {
Self::new(self.0.checked_add(rhs)?)
}
pub fn checked_sub(&self, rhs: u64) -> Option<Self> {
self.0.checked_sub(rhs).map(Self)
}
pub fn saturating_sub(&self, rhs: u64) -> Self {
Self(self.0.saturating_sub(rhs))
}
}
impl std::ops::Add<u64> for BlockNumber {
type Output = BlockNumber;
fn add(self, rhs: u64) -> Self::Output {
Self(self.0 + rhs)
}
}
impl std::ops::AddAssign<u64> for BlockNumber {
fn add_assign(&mut self, rhs: u64) {
self.0 += rhs;
}
}
impl std::ops::Sub<u64> for BlockNumber {
type Output = BlockNumber;
fn sub(self, rhs: u64) -> Self::Output {
Self(self.0 - rhs)
}
}
impl std::ops::SubAssign<u64> for BlockNumber {
fn sub_assign(&mut self, rhs: u64) {
self.0 -= rhs;
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct EthereumAddress(pub H160);
impl<T> Dummy<T> for EthereumAddress {
fn dummy_with_rng<R: rand::Rng + ?Sized>(_: &T, rng: &mut R) -> Self {
Self(H160::random_using(rng))
}
}
#[derive(Debug, thiserror::Error)]
#[error("expected slice length of 16 or less, got {0}")]
pub struct FromSliceError(usize);
impl GasPrice {
pub const ZERO: GasPrice = GasPrice(0u128);
pub fn to_be_bytes(&self) -> [u8; 16] {
self.0.to_be_bytes()
}
pub fn from_be_bytes(src: [u8; 16]) -> Self {
Self(u128::from_be_bytes(src))
}
pub fn from_be_slice(src: &[u8]) -> Result<Self, FromSliceError> {
if src.len() > 16 {
return Err(FromSliceError(src.len()));
}
let mut buf = [0u8; 16];
buf[16 - src.len()..].copy_from_slice(src);
Ok(Self::from_be_bytes(buf))
}
}
impl From<u64> for GasPrice {
fn from(src: u64) -> Self {
Self(u128::from(src))
}
}
impl TryFrom<Felt> for GasPrice {
type Error = anyhow::Error;
fn try_from(src: Felt) -> Result<Self, Self::Error> {
anyhow::ensure!(
src.as_be_bytes()[0..16] == [0; 16],
"Gas price fits into u128"
);
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&src.as_be_bytes()[16..]);
Ok(Self(u128::from_be_bytes(bytes)))
}
}
impl From<BlockNumber> for BlockId {
fn from(number: BlockNumber) -> Self {
Self::Number(number)
}
}
impl From<BlockHash> for BlockId {
fn from(hash: BlockHash) -> Self {
Self::Hash(hash)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EthereumChain {
Mainnet,
Sepolia,
Other(primitive_types::U256),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Chain {
Mainnet,
SepoliaTestnet,
SepoliaIntegration,
Custom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub struct ChainId(pub Felt);
impl ChainId {
const fn from_slice_unwrap(slice: &[u8]) -> Self {
Self(match Felt::from_be_slice(slice) {
Ok(v) => v,
Err(_) => panic!("Bad value"),
})
}
pub fn to_hex_str(&self) -> std::borrow::Cow<'static, str> {
self.0.to_hex_str()
}
pub fn as_str(&self) -> &str {
std::str::from_utf8(self.0.as_be_bytes())
.expect("valid utf8")
.trim_start_matches('\0')
}
pub const MAINNET: Self = Self::from_slice_unwrap(b"SN_MAIN");
pub const SEPOLIA_TESTNET: Self = Self::from_slice_unwrap(b"SN_SEPOLIA");
pub const SEPOLIA_INTEGRATION: Self = Self::from_slice_unwrap(b"SN_INTEGRATION_SEPOLIA");
}
impl std::fmt::Display for Chain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Chain::Mainnet => f.write_str("Mainnet"),
Chain::SepoliaTestnet => f.write_str("Testnet/Sepolia"),
Chain::SepoliaIntegration => f.write_str("Integration/Sepolia"),
Chain::Custom => f.write_str("Custom"),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Dummy)]
pub struct StarknetVersion(u8, u8, u8, u8);
impl StarknetVersion {
pub const fn new(a: u8, b: u8, c: u8, d: u8) -> Self {
StarknetVersion(a, b, c, d)
}
pub fn as_u32(&self) -> u32 {
u32::from_le_bytes([self.0, self.1, self.2, self.3])
}
pub fn from_u32(version: u32) -> Self {
let [a, b, c, d] = version.to_le_bytes();
StarknetVersion(a, b, c, d)
}
pub const V_0_13_2: Self = Self::new(0, 13, 2, 0);
pub const V_0_13_4: Self = Self::new(0, 13, 4, 0);
pub const V_0_14_0: Self = Self::new(0, 14, 0, 0);
}
impl FromStr for StarknetVersion {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Ok(StarknetVersion::new(0, 0, 0, 0));
}
let parts: Vec<_> = s.split('.').collect();
anyhow::ensure!(
parts.len() == 3 || parts.len() == 4,
"Invalid version string, expected 3 or 4 parts but got {}",
parts.len()
);
let a = parts[0].parse()?;
let b = parts[1].parse()?;
let c = parts[2].parse()?;
let d = parts.get(3).map(|x| x.parse()).transpose()?.unwrap_or(0);
Ok(StarknetVersion(a, b, c, d))
}
}
impl Display for StarknetVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.0 == 0 && self.1 == 0 && self.2 == 0 && self.3 == 0 {
return Ok(());
}
if self.3 == 0 {
write!(f, "{}.{}.{}", self.0, self.1, self.2)
} else {
write!(f, "{}.{}.{}.{}", self.0, self.1, self.2, self.3)
}
}
}
macros::felt_newtypes!(
[
AccountDeploymentDataElem,
BlockHash,
ByteCodeOffset,
BlockCommitmentSignatureElem,
CallParam,
CallResultValue,
ClassCommitment,
ClassCommitmentLeafHash,
ConstructorParam,
ContractAddressSalt,
ContractNonce,
ContractStateHash,
ContractRoot,
EntryPoint,
EventCommitment,
EventData,
EventKey,
Fee,
L1ToL2MessageNonce,
L1ToL2MessagePayloadElem,
L2ToL1MessagePayloadElem,
PaymasterDataElem,
ProofFactElem,
ProposalCommitment,
PublicKey,
SequencerAddress,
StateCommitment,
StateDiffCommitment,
StorageCommitment,
StorageValue,
TransactionCommitment,
ReceiptCommitment,
TransactionHash,
TransactionNonce,
TransactionSignatureElem,
];
[
CasmHash,
ClassHash,
ContractAddress,
SierraHash,
StorageAddress,
]
);
macros::fmt::thin_display!(BlockNumber);
macros::fmt::thin_display!(BlockTimestamp);
impl ContractAddress {
pub fn deployed_contract_address(
constructor_calldata: impl Iterator<Item = CallParam>,
contract_address_salt: &ContractAddressSalt,
class_hash: &ClassHash,
) -> Self {
let constructor_calldata_hash = constructor_calldata
.fold(HashChain::default(), |mut h, param| {
h.update(param.0);
h
})
.finalize();
let contract_address = [
Felt::from_be_slice(b"STARKNET_CONTRACT_ADDRESS").expect("prefix is convertible"),
Felt::ZERO,
contract_address_salt.0,
class_hash.0,
constructor_calldata_hash,
]
.into_iter()
.fold(HashChain::default(), |mut h, e| {
h.update(e);
h
})
.finalize();
const MAX_CONTRACT_ADDRESS: Felt =
felt!("0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00");
let contract_address = if contract_address >= MAX_CONTRACT_ADDRESS {
contract_address - MAX_CONTRACT_ADDRESS
} else {
contract_address
};
ContractAddress::new_or_panic(contract_address)
}
pub fn is_system_contract(&self) -> bool {
(*self == ContractAddress::ONE) || (*self == ContractAddress::TWO)
}
}
impl From<ContractAddress> for Vec<u8> {
fn from(value: ContractAddress) -> Self {
value.0.to_be_bytes().to_vec()
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum AllowedOrigins {
Any,
List(Vec<String>),
}
impl<S> From<S> for AllowedOrigins
where
S: ToString,
{
fn from(value: S) -> Self {
let s = value.to_string();
if s == "*" {
Self::Any
} else {
Self::List(vec![s])
}
}
}
pub fn truncated_keccak(mut plain: [u8; 32]) -> Felt {
plain[0] &= 0x03;
Felt::from_be_bytes(plain).expect("cannot overflow: smaller than modulus")
}
pub fn calculate_class_commitment_leaf_hash(
compiled_class_hash: CasmHash,
) -> ClassCommitmentLeafHash {
const CONTRACT_CLASS_HASH_VERSION: pathfinder_crypto::Felt =
felt_bytes!(b"CONTRACT_CLASS_LEAF_V0");
ClassCommitmentLeafHash(
pathfinder_crypto::hash::poseidon_hash(
CONTRACT_CLASS_HASH_VERSION.into(),
compiled_class_hash.0.into(),
)
.into(),
)
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Proof(pub Vec<u8>);
impl Proof {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl serde::Serialize for Proof {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use base64::Engine;
let encoded = base64::engine::general_purpose::STANDARD.encode(&self.0);
serializer.serialize_str(&encoded)
}
}
impl<'de> serde::Deserialize<'de> for Proof {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use base64::Engine;
let s = String::deserialize(deserializer)?;
if s.is_empty() {
return Ok(Proof::default());
}
let bytes = base64::engine::general_purpose::STANDARD
.decode(&s)
.map_err(serde::de::Error::custom)?;
Ok(Proof(bytes))
}
}
#[cfg(test)]
mod tests {
use crate::{felt, CallParam, ClassHash, ContractAddress, ContractAddressSalt};
#[test]
fn constructor_entry_point() {
use sha3::{Digest, Keccak256};
use crate::{truncated_keccak, EntryPoint};
let mut keccak = Keccak256::default();
keccak.update(b"constructor");
let expected = EntryPoint(truncated_keccak(<[u8; 32]>::from(keccak.finalize())));
assert_eq!(EntryPoint::CONSTRUCTOR, expected);
}
mod starknet_version {
use std::str::FromStr;
use super::super::StarknetVersion;
#[test]
fn valid_version_parsing() {
let cases = [
("1.2.3.4", "1.2.3.4", StarknetVersion::new(1, 2, 3, 4)),
("1.2.3", "1.2.3", StarknetVersion::new(1, 2, 3, 0)),
("1.2.3.0", "1.2.3", StarknetVersion::new(1, 2, 3, 0)),
("", "", StarknetVersion::new(0, 0, 0, 0)),
];
for (input, output, actual) in cases.iter() {
let version = StarknetVersion::from_str(input).unwrap();
assert_eq!(version, *actual);
assert_eq!(version.to_string(), *output);
}
}
#[test]
fn invalid_version_parsing() {
assert!(StarknetVersion::from_str("1.2").is_err());
assert!(StarknetVersion::from_str("1").is_err());
assert!(StarknetVersion::from_str("1.2.a").is_err());
}
}
#[test]
fn deployed_contract_address() {
let expected_contract_address = ContractAddress(felt!(
"0x2fab82e4aef1d8664874e1f194951856d48463c3e6bf9a8c68e234a629a6f50"
));
let actual_contract_address = ContractAddress::deployed_contract_address(
std::iter::once(CallParam(felt!(
"0x5cd65f3d7daea6c63939d659b8473ea0c5cd81576035a4d34e52fb06840196c"
))),
&ContractAddressSalt(felt!("0x0")),
&ClassHash(felt!(
"0x2338634f11772ea342365abd5be9d9dc8a6f44f159ad782fdebd3db5d969738"
)),
);
assert_eq!(actual_contract_address, expected_contract_address);
}
mod proof_serde {
use super::super::Proof;
#[test]
fn round_trip() {
let proof = Proof(vec![0, 0, 0, 0, 0, 0, 0, 123, 0, 0, 1, 200]);
let json = serde_json::to_string(&proof).unwrap();
assert_eq!(json, r#""AAAAAAAAAHsAAAHI""#);
let deserialized: Proof = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, proof);
}
#[test]
fn empty_string_deserializes_to_default() {
let proof: Proof = serde_json::from_str(r#""""#).unwrap();
assert_eq!(proof, Proof::default());
}
#[test]
fn invalid_base64_returns_error() {
let result = serde_json::from_str::<Proof>(r#""not-valid-base64!@#""#);
assert!(result.is_err());
}
#[test]
fn empty_proof_serializes_to_empty_string() {
let proof = Proof::default();
let json = serde_json::to_string(&proof).unwrap();
assert_eq!(json, r#""""#);
}
}
}