Skip to main content

pathfinder_common/
lib.rs

1//! Contains core functions and types that are widely used but have no real
2//! home of their own.
3//!
4//! This includes many trivial wrappers around [Felt] which help by providing
5//! additional type safety.
6use std::fmt::Display;
7use std::ops::Rem;
8use std::str::FromStr;
9
10use anyhow::Context;
11use fake::Dummy;
12use pathfinder_crypto::hash::HashChain;
13use pathfinder_crypto::Felt;
14use primitive_types::H160;
15use serde::{Deserialize, Serialize};
16
17pub mod casm_class;
18pub mod class_definition;
19pub mod consensus_info;
20pub mod consts;
21pub mod event;
22pub mod hash;
23mod header;
24pub mod integration_testing;
25mod l1;
26mod l2;
27mod macros;
28pub mod prelude;
29pub mod receipt;
30pub mod signature;
31pub mod state_update;
32pub mod test_utils;
33pub mod transaction;
34pub mod trie;
35
36pub use header::{BlockHeader, BlockHeaderBuilder, L1DataAvailabilityMode, SignedBlockHeader};
37pub use l1::{L1BlockHash, L1BlockNumber, L1TransactionHash};
38pub use l2::{
39    ConsensusFinalizedBlockHeader,
40    ConsensusFinalizedL2Block,
41    DeclaredClass,
42    L2Block,
43    L2BlockToCommit,
44};
45pub use signature::BlockCommitmentSignature;
46pub use state_update::{FoundStorageValue, StateUpdate};
47
48impl ContractAddress {
49    /// The contract at 0x1 is special. It was never deployed and therefore
50    /// has no class hash. It does however receive storage changes.
51    ///
52    /// It is used by starknet to store values for smart contracts to access
53    /// using syscalls. For example the block hash.
54    pub const ONE: ContractAddress = contract_address!("0x1");
55    /// The contract at 0x2 was introduced in Starknet version 0.13.4. It is
56    /// used for stateful compression:
57    /// - storage key 0 points to the global counter, which is the base for
58    ///   index values in the next block,
59    /// - other storage k-v pairs store the mapping of key to index,
60    /// - the global counter starts at value 0x80 in the first block from
61    ///   0.13.4,
62    /// - keys of value lower than 0x80 are not indexed.
63    pub const TWO: ContractAddress = contract_address!("0x2");
64    /// Useful for iteration over the system contracts
65    pub const SYSTEM: [ContractAddress; 2] = [ContractAddress::ONE, ContractAddress::TWO];
66}
67
68// Bytecode and entry point list of a class
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct ContractClass {
71    // A base64 encoding of the gzip-compressed JSON representation of program.
72    pub program: String,
73    // A JSON representation of the entry points
74    // We don't actually process this value, just serialize/deserialize
75    // from an already validated JSON.
76    // This is kept as a Value to avoid dependency on sequencer API types.
77    pub entry_points_by_type: serde_json::Value,
78}
79
80impl EntryPoint {
81    /// Returns a new EntryPoint which has been truncated to fit from Keccak256
82    /// digest of input.
83    ///
84    /// See: <https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/contract-classes/>
85    pub fn hashed(input: &[u8]) -> Self {
86        use sha3::Digest;
87        EntryPoint(truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(
88            input,
89        ))))
90    }
91
92    /// The constructor [EntryPoint], defined as the truncated keccak of
93    /// b"constructor".
94    pub const CONSTRUCTOR: Self =
95        entry_point!("0x028FFE4FF0F226A9107253E17A904099AA4F63A02A5621DE0576E5AA71BC5194");
96}
97
98impl StateCommitment {
99    /// Calculates global state commitment by combining the storage and class
100    /// commitment.
101    ///
102    /// See
103    /// <https://github.com/starkware-libs/cairo-lang/blob/12ca9e91bbdc8a423c63280949c7e34382792067/src/starkware/starknet/core/os/state.cairo#L125>
104    /// for details.
105    ///
106    /// Starting from Starknet 0.14.0, the state commitment always uses the
107    /// Poseidon hash formula, even when `class_commitment` is zero. For older
108    /// versions, when `class_commitment` is zero, the state commitment equals
109    /// the storage commitment directly.
110    pub fn calculate(
111        storage_commitment: StorageCommitment,
112        class_commitment: ClassCommitment,
113        version: StarknetVersion,
114    ) -> Self {
115        if class_commitment == ClassCommitment::ZERO
116            && storage_commitment == StorageCommitment::ZERO
117        {
118            return StateCommitment::ZERO;
119        }
120
121        if class_commitment == ClassCommitment::ZERO && version < StarknetVersion::V_0_14_0 {
122            return Self(storage_commitment.0);
123        }
124
125        const GLOBAL_STATE_VERSION: Felt = felt_bytes!(b"STARKNET_STATE_V0");
126
127        StateCommitment(
128            pathfinder_crypto::hash::poseidon::poseidon_hash_many(&[
129                GLOBAL_STATE_VERSION.into(),
130                storage_commitment.0.into(),
131                class_commitment.0.into(),
132            ])
133            .into(),
134        )
135    }
136}
137
138impl StorageAddress {
139    pub fn from_name(input: &[u8]) -> Self {
140        use sha3::Digest;
141        Self(truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(
142            input,
143        ))))
144    }
145
146    pub fn from_map_name_and_key(name: &[u8], key: Felt) -> Self {
147        use sha3::Digest;
148
149        let intermediate = truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(name)));
150        let value = pathfinder_crypto::hash::pedersen_hash(intermediate, key);
151
152        let value = primitive_types::U256::from_big_endian(value.as_be_bytes());
153        let max_address = primitive_types::U256::from_str_radix(
154            "0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
155            16,
156        )
157        .unwrap();
158
159        let value = value.rem(max_address);
160        let mut b = [0u8; 32];
161        value.to_big_endian(&mut b);
162        Self(Felt::from_be_slice(&b).expect("Truncated value should fit into a felt"))
163    }
164}
165
166/// A Starknet block number.
167#[derive(Copy, Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
168pub struct BlockNumber(u64);
169
170macros::i64_backed_u64::new_get_partialeq!(BlockNumber);
171macros::i64_backed_u64::serdes!(BlockNumber);
172
173impl From<BlockNumber> for Felt {
174    fn from(x: BlockNumber) -> Self {
175        Felt::from(x.0)
176    }
177}
178
179impl std::iter::Iterator for BlockNumber {
180    type Item = BlockNumber;
181
182    fn next(&mut self) -> Option<Self::Item> {
183        Some(*self + 1)
184    }
185}
186
187/// The timestamp of a Starknet block.
188#[derive(Copy, Debug, Clone, PartialEq, Eq, Default)]
189pub struct BlockTimestamp(u64);
190
191macros::i64_backed_u64::new_get_partialeq!(BlockTimestamp);
192macros::i64_backed_u64::serdes!(BlockTimestamp);
193
194/// A Starknet transaction index.
195#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
196pub struct TransactionIndex(u64);
197
198macros::i64_backed_u64::new_get_partialeq!(TransactionIndex);
199macros::i64_backed_u64::serdes!(TransactionIndex);
200
201/// Starknet gas price.
202#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
203pub struct GasPrice(pub u128);
204
205/// A hex representation of a [GasPrice].
206#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
207pub struct GasPriceHex(pub GasPrice);
208
209/// Starknet resource bound: amount.
210#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
211pub struct ResourceAmount(pub u64);
212
213// Transaction tip: the prioritization metric determines the sorting order of
214// transactions in the mempool.
215#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
216pub struct Tip(pub u64);
217
218// A hex representation of a [Tip].
219#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Dummy)]
220pub struct TipHex(pub Tip);
221
222/// Starknet resource bound: price per unit.
223#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
224pub struct ResourcePricePerUnit(pub u128);
225
226/// Starknet transaction version.
227#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
228pub struct TransactionVersion(pub Felt);
229
230impl TransactionVersion {
231    /// Checks if version is zero, handling QUERY_VERSION_BASE.
232    pub fn is_zero(&self) -> bool {
233        self.without_query_version() == 0
234    }
235
236    /// Returns the transaction version without QUERY_VERSION_BASE.
237    ///
238    /// QUERY_VERSION_BASE (2**128) is a large constant that gets
239    /// added to the real version to make sure transactions constructed for
240    /// call or estimateFee cannot be submitted for inclusion on the chain.
241    pub fn without_query_version(&self) -> u128 {
242        let lower = &self.0.as_be_bytes()[16..];
243        u128::from_be_bytes(lower.try_into().expect("slice should be the right length"))
244    }
245
246    pub const fn with_query_version(self) -> Self {
247        let mut bytes = self.0.to_be_bytes();
248        bytes[15] |= 0b0000_0001;
249
250        let felt = match Felt::from_be_bytes(bytes) {
251            Ok(x) => x,
252            Err(_) => panic!("Adding query bit to transaction version failed."),
253        };
254        Self(felt)
255    }
256
257    pub const fn has_query_version(&self) -> bool {
258        self.0.as_be_bytes()[15] & 0b0000_0001 != 0
259    }
260
261    pub fn with_query_only(self, query_only: bool) -> Self {
262        if query_only {
263            self.with_query_version()
264        } else {
265            Self(self.without_query_version().into())
266        }
267    }
268
269    pub const ZERO: Self = Self(Felt::ZERO);
270    pub const ONE: Self = Self(Felt::from_u64(1));
271    pub const TWO: Self = Self(Felt::from_u64(2));
272    pub const THREE: Self = Self(Felt::from_u64(3));
273    pub const ZERO_WITH_QUERY_VERSION: Self = Self::ZERO.with_query_version();
274    pub const ONE_WITH_QUERY_VERSION: Self = Self::ONE.with_query_version();
275    pub const TWO_WITH_QUERY_VERSION: Self = Self::TWO.with_query_version();
276    pub const THREE_WITH_QUERY_VERSION: Self = Self::THREE.with_query_version();
277}
278
279/// A way of identifying a specific block that has been finalized.
280///
281/// Useful in contexts that do not work with pending blocks.
282#[derive(Debug, Copy, Clone, PartialEq, Eq)]
283pub enum BlockId {
284    Number(BlockNumber),
285    Hash(BlockHash),
286    Latest,
287}
288
289impl BlockId {
290    pub fn is_latest(&self) -> bool {
291        self == &Self::Latest
292    }
293}
294
295impl BlockNumber {
296    pub const GENESIS: BlockNumber = BlockNumber::new_or_panic(0);
297    /// The maximum [BlockNumber] we can support. Restricted to `u64::MAX/2` to
298    /// match Sqlite's maximum integer value.
299    pub const MAX: BlockNumber = BlockNumber::new_or_panic(i64::MAX as u64);
300
301    /// Returns the parent's [BlockNumber] or [None] if the current number is
302    /// genesis.
303    pub fn parent(&self) -> Option<Self> {
304        if self == &Self::GENESIS {
305            None
306        } else {
307            Some(*self - 1)
308        }
309    }
310
311    pub fn is_zero(&self) -> bool {
312        self == &Self::GENESIS
313    }
314
315    pub fn checked_add(&self, rhs: u64) -> Option<Self> {
316        Self::new(self.0.checked_add(rhs)?)
317    }
318
319    pub fn checked_sub(&self, rhs: u64) -> Option<Self> {
320        self.0.checked_sub(rhs).map(Self)
321    }
322
323    pub fn saturating_sub(&self, rhs: u64) -> Self {
324        Self(self.0.saturating_sub(rhs))
325    }
326}
327
328impl std::ops::Add<u64> for BlockNumber {
329    type Output = BlockNumber;
330
331    fn add(self, rhs: u64) -> Self::Output {
332        Self(self.0 + rhs)
333    }
334}
335
336impl std::ops::AddAssign<u64> for BlockNumber {
337    fn add_assign(&mut self, rhs: u64) {
338        self.0 += rhs;
339    }
340}
341
342impl std::ops::Sub<u64> for BlockNumber {
343    type Output = BlockNumber;
344
345    fn sub(self, rhs: u64) -> Self::Output {
346        Self(self.0 - rhs)
347    }
348}
349
350impl std::ops::SubAssign<u64> for BlockNumber {
351    fn sub_assign(&mut self, rhs: u64) {
352        self.0 -= rhs;
353    }
354}
355
356/// An Ethereum address.
357#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)]
358pub struct EthereumAddress(pub H160);
359
360impl<T> Dummy<T> for EthereumAddress {
361    fn dummy_with_rng<R: rand::Rng + ?Sized>(_: &T, rng: &mut R) -> Self {
362        Self(H160::random_using(rng))
363    }
364}
365
366#[derive(Debug, thiserror::Error)]
367#[error("expected slice length of 16 or less, got {0}")]
368pub struct FromSliceError(usize);
369
370impl GasPrice {
371    pub const ZERO: GasPrice = GasPrice(0u128);
372
373    /// Returns the big-endian representation of this [GasPrice].
374    pub fn to_be_bytes(&self) -> [u8; 16] {
375        self.0.to_be_bytes()
376    }
377
378    /// Constructs [GasPrice] from an array of bytes. Big endian byte order is
379    /// assumed.
380    pub fn from_be_bytes(src: [u8; 16]) -> Self {
381        Self(u128::from_be_bytes(src))
382    }
383
384    /// Constructs [GasPrice] from a slice of bytes. Big endian byte order is
385    /// assumed.
386    pub fn from_be_slice(src: &[u8]) -> Result<Self, FromSliceError> {
387        if src.len() > 16 {
388            return Err(FromSliceError(src.len()));
389        }
390
391        let mut buf = [0u8; 16];
392        buf[16 - src.len()..].copy_from_slice(src);
393
394        Ok(Self::from_be_bytes(buf))
395    }
396}
397
398impl From<u64> for GasPrice {
399    fn from(src: u64) -> Self {
400        Self(u128::from(src))
401    }
402}
403
404impl TryFrom<Felt> for GasPrice {
405    type Error = anyhow::Error;
406
407    fn try_from(src: Felt) -> Result<Self, Self::Error> {
408        anyhow::ensure!(
409            src.as_be_bytes()[0..16] == [0; 16],
410            "Gas price fits into u128"
411        );
412
413        let mut bytes = [0u8; 16];
414        bytes.copy_from_slice(&src.as_be_bytes()[16..]);
415        Ok(Self(u128::from_be_bytes(bytes)))
416    }
417}
418
419impl From<BlockNumber> for BlockId {
420    fn from(number: BlockNumber) -> Self {
421        Self::Number(number)
422    }
423}
424
425impl From<BlockHash> for BlockId {
426    fn from(hash: BlockHash) -> Self {
427        Self::Hash(hash)
428    }
429}
430
431/// Ethereum network chains running Starknet.
432#[derive(Debug, Clone, Copy, PartialEq, Eq)]
433pub enum EthereumChain {
434    Mainnet,
435    Sepolia,
436    Other(primitive_types::U256),
437}
438
439/// Starknet chain.
440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
441pub enum Chain {
442    Mainnet,
443    SepoliaTestnet,
444    SepoliaIntegration,
445    Custom,
446}
447
448#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
449pub struct ChainId(pub Felt);
450
451impl ChainId {
452    /// Convenience function for the constants because unwrap() is not const.
453    const fn from_slice_unwrap(slice: &[u8]) -> Self {
454        Self(match Felt::from_be_slice(slice) {
455            Ok(v) => v,
456            Err(_) => panic!("Bad value"),
457        })
458    }
459
460    /// A hex string representation, eg.: `"0x534e5f4d41494e"` stands for
461    /// Mainnet (`SN_MAIN`)
462    pub fn to_hex_str(&self) -> std::borrow::Cow<'static, str> {
463        self.0.to_hex_str()
464    }
465
466    /// A human readable representation, eg.: `"SN_MAIN"` stands for Mainnet
467    pub fn as_str(&self) -> &str {
468        std::str::from_utf8(self.0.as_be_bytes())
469            .expect("valid utf8")
470            .trim_start_matches('\0')
471    }
472
473    pub const MAINNET: Self = Self::from_slice_unwrap(b"SN_MAIN");
474    pub const SEPOLIA_TESTNET: Self = Self::from_slice_unwrap(b"SN_SEPOLIA");
475    pub const SEPOLIA_INTEGRATION: Self = Self::from_slice_unwrap(b"SN_INTEGRATION_SEPOLIA");
476}
477
478impl std::fmt::Display for Chain {
479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480        match self {
481            Chain::Mainnet => f.write_str("Mainnet"),
482            Chain::SepoliaTestnet => f.write_str("Testnet/Sepolia"),
483            Chain::SepoliaIntegration => f.write_str("Integration/Sepolia"),
484            Chain::Custom => f.write_str("Custom"),
485        }
486    }
487}
488
489#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Dummy)]
490pub struct StarknetVersion(u8, u8, u8, u8);
491
492impl StarknetVersion {
493    pub const fn new(a: u8, b: u8, c: u8, d: u8) -> Self {
494        StarknetVersion(a, b, c, d)
495    }
496
497    pub fn as_u32(&self) -> u32 {
498        u32::from_le_bytes([self.0, self.1, self.2, self.3])
499    }
500
501    pub fn from_u32(version: u32) -> Self {
502        let [a, b, c, d] = version.to_le_bytes();
503        StarknetVersion(a, b, c, d)
504    }
505
506    pub const V_0_13_2: Self = Self::new(0, 13, 2, 0);
507
508    // TODO: version at which block hash definition changes taken from
509    // Starkware implementation but might yet change
510    pub const V_0_13_4: Self = Self::new(0, 13, 4, 0);
511    // A version at which the state commitment formula changed to always use the
512    // Poseidon hash, even when `class_commitment` is zero.
513    pub const V_0_14_0: Self = Self::new(0, 14, 0, 0);
514}
515
516impl FromStr for StarknetVersion {
517    type Err = anyhow::Error;
518
519    fn from_str(s: &str) -> Result<Self, Self::Err> {
520        if s.is_empty() {
521            return Ok(StarknetVersion::new(0, 0, 0, 0));
522        }
523
524        let parts: Vec<_> = s.split('.').collect();
525        anyhow::ensure!(
526            parts.len() == 3 || parts.len() == 4,
527            "Invalid version string, expected 3 or 4 parts but got {}",
528            parts.len()
529        );
530
531        let a = parts[0].parse()?;
532        let b = parts[1].parse()?;
533        let c = parts[2].parse()?;
534        let d = parts.get(3).map(|x| x.parse()).transpose()?.unwrap_or(0);
535
536        Ok(StarknetVersion(a, b, c, d))
537    }
538}
539
540impl Display for StarknetVersion {
541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542        if self.0 == 0 && self.1 == 0 && self.2 == 0 && self.3 == 0 {
543            return Ok(());
544        }
545        if self.3 == 0 {
546            write!(f, "{}.{}.{}", self.0, self.1, self.2)
547        } else {
548            write!(f, "{}.{}.{}.{}", self.0, self.1, self.2, self.3)
549        }
550    }
551}
552
553macros::felt_newtypes!(
554    [
555        AccountDeploymentDataElem,
556        BlockHash,
557        ByteCodeOffset,
558        BlockCommitmentSignatureElem,
559        CallParam,
560        CallResultValue,
561        ClassCommitment,
562        ClassCommitmentLeafHash,
563        ConstructorParam,
564        ContractAddressSalt,
565        ContractNonce,
566        ContractStateHash,
567        ContractRoot,
568        EntryPoint,
569        EventCommitment,
570        EventData,
571        EventKey,
572        Fee,
573        L1ToL2MessageNonce,
574        L1ToL2MessagePayloadElem,
575        L2ToL1MessagePayloadElem,
576        PaymasterDataElem,
577        ProofFactElem,
578        ProposalCommitment,
579        PublicKey,
580        SequencerAddress,
581        StateCommitment,
582        StateDiffCommitment,
583        StorageCommitment,
584        StorageValue,
585        TransactionCommitment,
586        ReceiptCommitment,
587        TransactionHash,
588        TransactionNonce,
589        TransactionSignatureElem,
590    ];
591    [
592        CasmHash,
593        ClassHash,
594        ContractAddress,
595        SierraHash,
596        StorageAddress,
597    ]
598);
599
600macros::fmt::thin_display!(BlockNumber);
601macros::fmt::thin_display!(BlockTimestamp);
602
603impl ContractAddress {
604    pub fn deployed_contract_address(
605        constructor_calldata: impl Iterator<Item = CallParam>,
606        contract_address_salt: &ContractAddressSalt,
607        class_hash: &ClassHash,
608    ) -> Self {
609        let constructor_calldata_hash = constructor_calldata
610            .fold(HashChain::default(), |mut h, param| {
611                h.update(param.0);
612                h
613            })
614            .finalize();
615
616        let contract_address = [
617            Felt::from_be_slice(b"STARKNET_CONTRACT_ADDRESS").expect("prefix is convertible"),
618            Felt::ZERO,
619            contract_address_salt.0,
620            class_hash.0,
621            constructor_calldata_hash,
622        ]
623        .into_iter()
624        .fold(HashChain::default(), |mut h, e| {
625            h.update(e);
626            h
627        })
628        .finalize();
629
630        // Contract addresses are _less than_ 2**251 - 256
631        const MAX_CONTRACT_ADDRESS: Felt =
632            felt!("0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00");
633        let contract_address = if contract_address >= MAX_CONTRACT_ADDRESS {
634            contract_address - MAX_CONTRACT_ADDRESS
635        } else {
636            contract_address
637        };
638
639        ContractAddress::new_or_panic(contract_address)
640    }
641
642    pub fn is_system_contract(&self) -> bool {
643        (*self == ContractAddress::ONE) || (*self == ContractAddress::TWO)
644    }
645}
646
647impl From<ContractAddress> for Vec<u8> {
648    fn from(value: ContractAddress) -> Self {
649        value.0.to_be_bytes().to_vec()
650    }
651}
652
653#[derive(Clone, Debug, PartialEq)]
654pub enum AllowedOrigins {
655    Any,
656    List(Vec<String>),
657}
658
659impl<S> From<S> for AllowedOrigins
660where
661    S: ToString,
662{
663    fn from(value: S) -> Self {
664        let s = value.to_string();
665
666        if s == "*" {
667            Self::Any
668        } else {
669            Self::List(vec![s])
670        }
671    }
672}
673
674/// See:
675/// <https://github.com/starkware-libs/cairo-lang/blob/64a7f6aed9757d3d8d6c28bd972df73272b0cb0a/src/starkware/starknet/public/abi.py#L21-L26>
676pub fn truncated_keccak(mut plain: [u8; 32]) -> Felt {
677    // python code masks with (2**250 - 1) which starts 0x03 and is followed by 31
678    // 0xff in be truncation is needed not to overflow the field element.
679    plain[0] &= 0x03;
680    Felt::from_be_bytes(plain).expect("cannot overflow: smaller than modulus")
681}
682
683/// Calculate class commitment tree leaf hash value.
684///
685/// See: <https://docs.starknet.io/documentation/starknet_versions/upcoming_versions/#state_commitment>
686pub fn calculate_class_commitment_leaf_hash(
687    compiled_class_hash: CasmHash,
688) -> ClassCommitmentLeafHash {
689    const CONTRACT_CLASS_HASH_VERSION: pathfinder_crypto::Felt =
690        felt_bytes!(b"CONTRACT_CLASS_LEAF_V0");
691    ClassCommitmentLeafHash(
692        pathfinder_crypto::hash::poseidon_hash(
693            CONTRACT_CLASS_HASH_VERSION.into(),
694            compiled_class_hash.0.into(),
695        )
696        .into(),
697    )
698}
699
700/// A SNOS stwo proof, serialized as a base64-encoded string of big-endian
701/// packed `u32` values.
702#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
703pub struct Proof(pub Vec<u32>);
704
705impl Proof {
706    pub fn is_empty(&self) -> bool {
707        self.0.is_empty()
708    }
709}
710
711impl serde::Serialize for Proof {
712    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
713        use base64::Engine;
714
715        let bytes: Vec<u8> = self.0.iter().flat_map(|v| v.to_be_bytes()).collect();
716        let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
717        serializer.serialize_str(&encoded)
718    }
719}
720
721impl<'de> serde::Deserialize<'de> for Proof {
722    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
723        use base64::Engine;
724
725        let s = String::deserialize(deserializer)?;
726        if s.is_empty() {
727            return Ok(Proof::default());
728        }
729        let bytes = base64::engine::general_purpose::STANDARD
730            .decode(&s)
731            .map_err(serde::de::Error::custom)?;
732        if bytes.len() % 4 != 0 {
733            return Err(serde::de::Error::custom(format!(
734                "proof base64 decoded length {} is not a multiple of 4",
735                bytes.len()
736            )));
737        }
738        let values = bytes
739            .chunks_exact(4)
740            .map(|chunk| u32::from_be_bytes(chunk.try_into().unwrap()))
741            .collect();
742        Ok(Proof(values))
743    }
744}
745
746#[cfg(test)]
747mod tests {
748    use crate::{felt, CallParam, ClassHash, ContractAddress, ContractAddressSalt};
749
750    #[test]
751    fn constructor_entry_point() {
752        use sha3::{Digest, Keccak256};
753
754        use crate::{truncated_keccak, EntryPoint};
755
756        let mut keccak = Keccak256::default();
757        keccak.update(b"constructor");
758        let expected = EntryPoint(truncated_keccak(<[u8; 32]>::from(keccak.finalize())));
759
760        assert_eq!(EntryPoint::CONSTRUCTOR, expected);
761    }
762
763    mod starknet_version {
764        use std::str::FromStr;
765
766        use super::super::StarknetVersion;
767
768        #[test]
769        fn valid_version_parsing() {
770            let cases = [
771                ("1.2.3.4", "1.2.3.4", StarknetVersion::new(1, 2, 3, 4)),
772                ("1.2.3", "1.2.3", StarknetVersion::new(1, 2, 3, 0)),
773                ("1.2.3.0", "1.2.3", StarknetVersion::new(1, 2, 3, 0)),
774                ("", "", StarknetVersion::new(0, 0, 0, 0)),
775            ];
776
777            for (input, output, actual) in cases.iter() {
778                let version = StarknetVersion::from_str(input).unwrap();
779                assert_eq!(version, *actual);
780                assert_eq!(version.to_string(), *output);
781            }
782        }
783
784        #[test]
785        fn invalid_version_parsing() {
786            assert!(StarknetVersion::from_str("1.2").is_err());
787            assert!(StarknetVersion::from_str("1").is_err());
788            assert!(StarknetVersion::from_str("1.2.a").is_err());
789        }
790    }
791
792    #[test]
793    fn deployed_contract_address() {
794        let expected_contract_address = ContractAddress(felt!(
795            "0x2fab82e4aef1d8664874e1f194951856d48463c3e6bf9a8c68e234a629a6f50"
796        ));
797        let actual_contract_address = ContractAddress::deployed_contract_address(
798            std::iter::once(CallParam(felt!(
799                "0x5cd65f3d7daea6c63939d659b8473ea0c5cd81576035a4d34e52fb06840196c"
800            ))),
801            &ContractAddressSalt(felt!("0x0")),
802            &ClassHash(felt!(
803                "0x2338634f11772ea342365abd5be9d9dc8a6f44f159ad782fdebd3db5d969738"
804            )),
805        );
806        assert_eq!(actual_contract_address, expected_contract_address);
807    }
808
809    mod proof_serde {
810        use super::super::Proof;
811
812        #[test]
813        fn round_trip() {
814            let proof = Proof(vec![0, 123, 456]);
815            let json = serde_json::to_string(&proof).unwrap();
816            assert_eq!(json, r#""AAAAAAAAAHsAAAHI""#);
817            let deserialized: Proof = serde_json::from_str(&json).unwrap();
818            assert_eq!(deserialized, proof);
819        }
820
821        #[test]
822        fn empty_string_deserializes_to_default() {
823            let proof: Proof = serde_json::from_str(r#""""#).unwrap();
824            assert_eq!(proof, Proof::default());
825        }
826
827        #[test]
828        fn invalid_base64_returns_error() {
829            let result = serde_json::from_str::<Proof>(r#""not-valid-base64!@#""#);
830            assert!(result.is_err());
831        }
832
833        #[test]
834        fn non_multiple_of_4_length_returns_error() {
835            // 3 bytes is not a multiple of 4
836            let result = serde_json::from_str::<Proof>(r#""AAAA""#); // decodes to 3 bytes
837            assert!(result.is_err());
838        }
839
840        #[test]
841        fn empty_proof_serializes_to_empty_string() {
842            let proof = Proof::default();
843            let json = serde_json::to_string(&proof).unwrap();
844            assert_eq!(json, r#""""#);
845        }
846    }
847}