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 byte string.
701#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
702pub struct Proof(pub Vec<u8>);
703
704impl Proof {
705    pub fn is_empty(&self) -> bool {
706        self.0.is_empty()
707    }
708}
709
710impl serde::Serialize for Proof {
711    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
712        use base64::Engine;
713
714        let encoded = base64::engine::general_purpose::STANDARD.encode(&self.0);
715        serializer.serialize_str(&encoded)
716    }
717}
718
719impl<'de> serde::Deserialize<'de> for Proof {
720    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
721        use base64::Engine;
722
723        let s = String::deserialize(deserializer)?;
724        if s.is_empty() {
725            return Ok(Proof::default());
726        }
727        let bytes = base64::engine::general_purpose::STANDARD
728            .decode(&s)
729            .map_err(serde::de::Error::custom)?;
730        Ok(Proof(bytes))
731    }
732}
733
734#[cfg(test)]
735mod tests {
736    use crate::{felt, CallParam, ClassHash, ContractAddress, ContractAddressSalt};
737
738    #[test]
739    fn constructor_entry_point() {
740        use sha3::{Digest, Keccak256};
741
742        use crate::{truncated_keccak, EntryPoint};
743
744        let mut keccak = Keccak256::default();
745        keccak.update(b"constructor");
746        let expected = EntryPoint(truncated_keccak(<[u8; 32]>::from(keccak.finalize())));
747
748        assert_eq!(EntryPoint::CONSTRUCTOR, expected);
749    }
750
751    mod starknet_version {
752        use std::str::FromStr;
753
754        use super::super::StarknetVersion;
755
756        #[test]
757        fn valid_version_parsing() {
758            let cases = [
759                ("1.2.3.4", "1.2.3.4", StarknetVersion::new(1, 2, 3, 4)),
760                ("1.2.3", "1.2.3", StarknetVersion::new(1, 2, 3, 0)),
761                ("1.2.3.0", "1.2.3", StarknetVersion::new(1, 2, 3, 0)),
762                ("", "", StarknetVersion::new(0, 0, 0, 0)),
763            ];
764
765            for (input, output, actual) in cases.iter() {
766                let version = StarknetVersion::from_str(input).unwrap();
767                assert_eq!(version, *actual);
768                assert_eq!(version.to_string(), *output);
769            }
770        }
771
772        #[test]
773        fn invalid_version_parsing() {
774            assert!(StarknetVersion::from_str("1.2").is_err());
775            assert!(StarknetVersion::from_str("1").is_err());
776            assert!(StarknetVersion::from_str("1.2.a").is_err());
777        }
778    }
779
780    #[test]
781    fn deployed_contract_address() {
782        let expected_contract_address = ContractAddress(felt!(
783            "0x2fab82e4aef1d8664874e1f194951856d48463c3e6bf9a8c68e234a629a6f50"
784        ));
785        let actual_contract_address = ContractAddress::deployed_contract_address(
786            std::iter::once(CallParam(felt!(
787                "0x5cd65f3d7daea6c63939d659b8473ea0c5cd81576035a4d34e52fb06840196c"
788            ))),
789            &ContractAddressSalt(felt!("0x0")),
790            &ClassHash(felt!(
791                "0x2338634f11772ea342365abd5be9d9dc8a6f44f159ad782fdebd3db5d969738"
792            )),
793        );
794        assert_eq!(actual_contract_address, expected_contract_address);
795    }
796
797    mod proof_serde {
798        use super::super::Proof;
799
800        #[test]
801        fn round_trip() {
802            let proof = Proof(vec![0, 0, 0, 0, 0, 0, 0, 123, 0, 0, 1, 200]);
803            let json = serde_json::to_string(&proof).unwrap();
804            assert_eq!(json, r#""AAAAAAAAAHsAAAHI""#);
805            let deserialized: Proof = serde_json::from_str(&json).unwrap();
806            assert_eq!(deserialized, proof);
807        }
808
809        #[test]
810        fn empty_string_deserializes_to_default() {
811            let proof: Proof = serde_json::from_str(r#""""#).unwrap();
812            assert_eq!(proof, Proof::default());
813        }
814
815        #[test]
816        fn invalid_base64_returns_error() {
817            let result = serde_json::from_str::<Proof>(r#""not-valid-base64!@#""#);
818            assert!(result.is_err());
819        }
820
821        #[test]
822        fn empty_proof_serializes_to_empty_string() {
823            let proof = Proof::default();
824            let json = serde_json::to_string(&proof).unwrap();
825            assert_eq!(json, r#""""#);
826        }
827    }
828}