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