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