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