Skip to main content

pathfinder_class_hash/
lib.rs

1//! Class hash computation for Cairo and Sierra contracts.
2//!
3//! This crate provides functionality to compute class hashes for both Cairo 0.x
4//! and Sierra (Cairo 1.x+) contracts in the Starknet ecosystem. The class hash
5//! is a unique identifier for a contract's code that is used throughout the
6//! Starknet protocol.
7//!
8//! # Class Hash Types
9//!
10//! There are two main types of class hashes:
11//!
12//! * Cairo 0.x class hashes - Computed for legacy Cairo contracts
13//! * Sierra class hashes - Computed for newer Cairo 1.x+ contracts using the
14//!   Sierra intermediate representation
15//!
16//! # Main Components
17//!
18//! * [`compute_class_hash`] - The main entry point for computing class hashes
19//!   for both Cairo and Sierra contracts
20//! * [`ComputedClassHash`] - An enum representing the computed hash for either
21//!   contract type
22//! * [`PreparedCairoContractDefinition`] - A prepared Cairo contract definition
23//!   ready for hashing
24//! * [`RawCairoContractDefinition`] - An unprepared Cairo contract definition
25//!
26//! # Implementation Details
27//!
28//! ## Cairo Class Hash
29//!
30//! The Cairo class hash computation follows these steps:
31//!
32//! 1. The contract definition is prepared by removing debug info and handling
33//!    special cases for Cairo 0.8+ attributes
34//! 2. The prepared definition is serialized to JSON with Python-compatible
35//!    formatting
36//! 3. A truncated Keccak hash is computed from the serialized JSON
37//! 4. Entry points, builtins, and bytecode are processed through hash chains
38//! 5. The final class hash is computed by combining all components
39//!
40//! ## Sierra Class Hash
41//!
42//! The Sierra class hash computation is simpler:
43//!
44//! 1. The contract version is validated
45//! 2. Entry points are processed in order
46//! 3. The ABI string is hashed
47//! 4. The Sierra program is hashed
48//! 5. All components are combined into the final hash
49//!
50//! # Compatibility
51//!
52//! This crate maintains compatibility with the official Starknet implementation
53//! and includes extensive test vectors to ensure hash computation matches the
54//! network's expectations.
55//!
56//! See the [official Starknet documentation](https://www.starknet.io/cairo-book/ch100-01-contracts-classes-and-instances.html#class-hash)
57//! for more details on class hash computation.
58
59use anyhow::{Context, Error, Result};
60use pathfinder_common::class_definition::EntryPointType::*;
61use pathfinder_common::class_definition::{
62    SerializedCairoDefinition,
63    SerializedClassDefinition,
64    SerializedOpaqueClassDefinition,
65    SerializedSierraDefinition,
66};
67use pathfinder_common::{felt_bytes, ClassHash};
68use pathfinder_crypto::hash::{HashChain, PoseidonHasher};
69use pathfinder_crypto::Felt;
70use serde::Serialize;
71use sha3::Digest;
72
73/// Computed class hash
74#[derive(Debug, PartialEq)]
75pub enum ComputedClassHash {
76    Cairo(ClassHash),
77    Sierra(ClassHash),
78}
79
80impl ComputedClassHash {
81    pub fn hash(&self) -> ClassHash {
82        match self {
83            ComputedClassHash::Cairo(h) => *h,
84            ComputedClassHash::Sierra(h) => *h,
85        }
86    }
87}
88
89/// Consumes an opaque serialized class definition and outputs the computed
90/// class hash as well as the definition reinterpreted as either a serialized
91/// Cairo or Sierra definition.
92///
93/// This function first parses the JSON blob to decide if it's a Cairo or Sierra
94/// class definition and then calls the appropriate function to compute the
95/// class hash with the parsed definition.
96pub fn compute_class_hash(
97    serialized_definition: SerializedOpaqueClassDefinition,
98) -> Result<(ComputedClassHash, SerializedClassDefinition)> {
99    let contract_definition = parse_contract_definition(&serialized_definition)
100        .context("Failed to parse contract definition")?;
101
102    match contract_definition {
103        json::ContractDefinition::Sierra(definition) => compute_sierra_class_hash(definition)
104            .map(ComputedClassHash::Sierra)
105            .context("Compute class hash")
106            .map(|hash| {
107                (
108                    hash,
109                    // It is safe to reinterpret the serialized definition as a Sierra definition
110                    // since the parsing step succeeded and confirmed it is a
111                    // Sierra definition.
112                    SerializedClassDefinition::Sierra(SerializedSierraDefinition::from(
113                        serialized_definition.into_inner(),
114                    )),
115                )
116            }),
117        json::ContractDefinition::Cairo(definition) => compute_cairo_class_hash(definition.into())
118            .map(ComputedClassHash::Cairo)
119            .context("Compute class hash")
120            .map(|hash| {
121                (
122                    hash,
123                    // It is safe to reinterpret the serialized definition as a Cairo definition
124                    // since the parsing step succeeded and confirmed it is a Cairo definition.
125                    SerializedClassDefinition::Cairo(SerializedCairoDefinition::from(
126                        serialized_definition.into_inner(),
127                    )),
128                )
129            }),
130    }
131}
132
133/// Compute class hash for a Cairo contract definition
134pub fn compute_cairo_hinted_class_hash(
135    contract_definition: &PreparedCairoContractDefinition<'_>,
136) -> Result<Felt> {
137    use std::io::Write;
138
139    // It's less efficient than tweaking the formatter to emit the encoding but I
140    // don't know how and this is an emergency issue (mainnt nodes stuck).
141    let mut string_buffer = vec![];
142
143    let mut ser =
144        serde_json::Serializer::with_formatter(&mut string_buffer, PythonDefaultFormatter);
145
146    contract_definition
147        .0
148        .serialize(&mut ser)
149        .context("Serializing contract_definition for Keccak256")?;
150
151    let raw_json_output = String::from_utf8(string_buffer).expect("Invalid UTF-8");
152
153    let mut keccak_writer = KeccakWriter::default();
154    keccak_writer
155        .write_all(raw_json_output.as_bytes())
156        .expect("Failed to write to KeccakWriter");
157
158    let KeccakWriter(hash) = keccak_writer;
159    Ok(truncated_keccak(<[u8; 32]>::from(hash.finalize())))
160}
161
162/// Parse either a Sierra or a Cairo contract definition.
163///
164/// Due to an issue in serde_json we can't use an untagged enum and simply
165/// derive a Deserialize implementation: <https://github.com/serde-rs/json/issues/559>
166fn parse_contract_definition(
167    serialized_definition: &SerializedOpaqueClassDefinition,
168) -> serde_json::Result<json::ContractDefinition<'_>> {
169    serde_json::from_slice::<json::SierraContractDefinition<'_>>(serialized_definition.as_slice())
170        .map(json::ContractDefinition::Sierra)
171        .or_else(|_| {
172            serde_json::from_slice::<json::CairoContractDefinition<'_>>(
173                serialized_definition.as_slice(),
174            )
175            .map(json::ContractDefinition::Cairo)
176        })
177}
178
179/// Helpers to compute class hashes from the parts that compose a Cairo or
180/// Sierra contract
181pub mod from_parts {
182    use std::collections::HashMap;
183
184    use anyhow::Result;
185    use pathfinder_common::class_definition::{
186        EntryPointType,
187        SelectorAndOffset,
188        SierraEntryPoints,
189    };
190    use pathfinder_common::ClassHash;
191    use pathfinder_crypto::Felt;
192
193    use super::json;
194
195    /// Compute class hash from the parts that compose a Cairo contract
196    pub fn compute_cairo_class_hash(
197        abi: &[u8],
198        program: &[u8],
199        external_entry_points: Vec<SelectorAndOffset>,
200        l1_handler_entry_points: Vec<SelectorAndOffset>,
201        constructor_entry_points: Vec<SelectorAndOffset>,
202    ) -> Result<ClassHash> {
203        let mut entry_points_by_type = HashMap::new();
204        entry_points_by_type.insert(EntryPointType::External, external_entry_points);
205        entry_points_by_type.insert(EntryPointType::L1Handler, l1_handler_entry_points);
206        entry_points_by_type.insert(EntryPointType::Constructor, constructor_entry_points);
207
208        let contract_definition = json::CairoContractDefinition {
209            abi: serde_json::from_slice(abi)?,
210            program: serde_json::from_slice(program)?,
211            entry_points_by_type,
212        };
213
214        super::compute_cairo_class_hash(contract_definition.into())
215    }
216
217    /// Compute class hash from the parts that compose a Sierra contract
218    pub fn compute_sierra_class_hash(
219        abi: &str,
220        sierra_program: Vec<Felt>,
221        contract_class_version: &str,
222        entry_points: SierraEntryPoints,
223    ) -> Result<ClassHash> {
224        let mut entry_points_by_type = HashMap::new();
225        entry_points_by_type.insert(EntryPointType::External, entry_points.external);
226        entry_points_by_type.insert(EntryPointType::L1Handler, entry_points.l1_handler);
227        entry_points_by_type.insert(EntryPointType::Constructor, entry_points.constructor);
228
229        let contract_definition = json::SierraContractDefinition {
230            abi: abi.into(),
231            sierra_program,
232            contract_class_version: contract_class_version.into(),
233            entry_points_by_type,
234        };
235
236        super::compute_sierra_class_hash(contract_definition)
237    }
238}
239
240/// An unprepared Cairo contract definition.
241///
242/// This type represents a raw, unmodified Cairo contract definition before any
243/// preprocessing for class hash computation. It serves as a type-safe way to
244/// ensure contract definitions go through the proper preparation process.
245///
246/// # Type Safety
247///
248/// This type works together with [`PreparedCairoContractDefinition`] to provide
249/// compile-time guarantees that contract definitions are properly prepared
250/// before being used in class hash computation.
251///
252/// # Implementation
253///
254/// Internally wraps a [`json::CairoContractDefinition`] and can be created from
255/// one using the [`From`] implementation.
256pub struct RawCairoContractDefinition<'a>(json::CairoContractDefinition<'a>);
257
258impl<'a> From<json::CairoContractDefinition<'a>> for RawCairoContractDefinition<'a> {
259    fn from(value: json::CairoContractDefinition<'a>) -> Self {
260        RawCairoContractDefinition(value)
261    }
262}
263
264impl<'a> RawCairoContractDefinition<'a> {
265    /// Get the inner contract definition
266    pub fn inner(&self) -> &json::CairoContractDefinition<'a> {
267        &self.0
268    }
269}
270
271/// A prepared Cairo contract definition ready for class hash computation.
272///
273/// This type represents a Cairo contract definition that has been preprocessed
274/// and is ready for class hash computation. The preparation process includes:
275/// - Removal of debug information
276/// - Handling of Cairo 0.8+ specific attributes
277/// - Proper formatting of named tuple types
278///
279/// # Type Safety
280///
281/// This type works together with [`RawCairoContractDefinition`] to provide
282/// compile-time guarantees that contract definitions are properly prepared
283/// before being used in class hash computation.
284///
285/// # Creation
286///
287/// This type can be created from a [`json::CairoContractDefinition`] using
288/// [`TryFrom`], which internally uses [`prepare_json_contract_definition`] to
289/// ensure all necessary preprocessing steps are applied. The conversion may
290/// fail if the contract definition is invalid or cannot be properly prepared.
291pub struct PreparedCairoContractDefinition<'a>(json::CairoContractDefinition<'a>);
292
293impl<'a> TryFrom<json::CairoContractDefinition<'a>> for PreparedCairoContractDefinition<'a> {
294    type Error = Error;
295
296    fn try_from(value: json::CairoContractDefinition<'a>) -> Result<Self, Self::Error> {
297        prepare_json_contract_definition(RawCairoContractDefinition::from(value))
298    }
299}
300
301impl<'a> PreparedCairoContractDefinition<'a> {
302    /// Get the inner contract definition
303    pub fn inner(&self) -> &json::CairoContractDefinition<'a> {
304        &self.0
305    }
306}
307
308/// Computes the class hash for given Cairo class definition.
309///
310/// The structure of the blob is not strictly defined, so it lives in privacy
311/// under `json` module of this module. The class hash has [official
312/// documentation][starknet-doc] and [cairo-lang
313/// has an implementation][cairo-compute] which is half-python and
314/// half-[cairo][cairo-contract].
315///
316/// Outline of the hashing is:
317///
318/// 1. class definition is serialized with python's [`sort_keys=True`
319///    option][py-sortkeys], then a truncated Keccak256 hash is calculated of
320///    the serialized json
321/// 2. a [hash chain][`HashChain`] construction is used to process in order the
322///    contract entry points, builtins, the truncated keccak hash and bytecodes
323/// 3. each of the hashchains is hash chained together to produce a final class
324///    hash
325///
326/// Hash chain construction is explained at the [official
327/// documentation][starknet-doc], but it's text explanations are much more
328/// complex than the actual implementation in `HashChain`.
329///
330/// [starknet-doc]: https://www.starknet.io/cairo-book/ch100-01-contracts-classes-and-instances.html#class-hash
331/// [cairo-compute]: https://github.com/starkware-libs/cairo-lang/blob/64a7f6aed9757d3d8d6c28bd972df73272b0cb0a/src/starkware/starknet/core/os/contract_hash.py
332/// [cairo-contract]: https://github.com/starkware-libs/cairo-lang/blob/64a7f6aed9757d3d8d6c28bd972df73272b0cb0a/src/starkware/starknet/core/os/contracts.cairo#L76-L118
333/// [py-sortkeys]: https://github.com/starkware-libs/cairo-lang/blob/64a7f6aed9757d3d8d6c28bd972df73272b0cb0a/src/starkware/starknet/core/os/contract_hash.py#L58-L71
334pub fn compute_cairo_class_hash(
335    contract_definition: RawCairoContractDefinition<'_>,
336) -> Result<ClassHash> {
337    // Prepare the contract definition for class hash computation
338    let contract_definition = prepare_json_contract_definition(contract_definition)?;
339
340    // Compute the truncated Keccak hash of the prepared contract definition
341    let truncated_keccak = compute_cairo_hinted_class_hash(&contract_definition)?;
342
343    const API_VERSION: Felt = Felt::ZERO;
344
345    let mut outer = HashChain::default();
346
347    // This wasn't in the docs, but similarly to contract_state hash, we start with
348    // this 0, so this will yield outer == H(0, 0); However, dissimilarly to
349    // contract_state hash, we do include the number of items in this
350    // class_hash.
351    outer.update(API_VERSION);
352
353    // It is important to process the different entrypoint hashchains in correct
354    // order. Each of the entrypoint lists gets updated into the `outer`
355    // hashchain.
356    //
357    // This implementation doesn't preparse the strings, which makes it a bit more
358    // noisy. Late parsing is made in an attempt to lean on the one big string
359    // allocation we've already got, but these three hash chains could be
360    // constructed at deserialization time.
361    [External, L1Handler, Constructor]
362        .iter()
363        .map(|key| {
364            contract_definition
365                .0
366                .entry_points_by_type
367                .get(key)
368                .unwrap_or(&Vec::new())
369                .iter()
370                // flatten each entry point to get a list of (selector, offset, selector, offset,
371                // ...)
372                .flat_map(|x| [x.selector.0, x.offset.0].into_iter())
373                .fold(HashChain::default(), |mut hc, next| {
374                    hc.update(next);
375                    hc
376                })
377        })
378        .for_each(|x| outer.update(x.finalize()));
379
380    fn update_hash_chain(mut hc: HashChain, next: Result<Felt, Error>) -> Result<HashChain, Error> {
381        hc.update(next?);
382        Result::<_, Error>::Ok(hc)
383    }
384
385    let builtins = contract_definition
386        .0
387        .program
388        .builtins
389        .iter()
390        .enumerate()
391        .map(|(i, s)| (i, s.as_bytes()))
392        .map(|(i, s)| {
393            Felt::from_be_slice(s).with_context(|| format!("Invalid builtin at index {i}"))
394        })
395        .try_fold(HashChain::default(), update_hash_chain)
396        .context("Failed to process contract_definition.program.builtins")?;
397
398    outer.update(builtins.finalize());
399
400    outer.update(truncated_keccak);
401
402    let bytecodes = contract_definition
403        .0
404        .program
405        .data
406        .iter()
407        .enumerate()
408        .map(|(i, s)| {
409            Felt::from_hex_str(s).with_context(|| format!("Invalid bytecode at index {i}"))
410        })
411        .try_fold(HashChain::default(), update_hash_chain)
412        .context("Failed to process contract_definition.program.data")?;
413
414    outer.update(bytecodes.finalize());
415
416    Ok(ClassHash(outer.finalize()))
417}
418
419/// Prepares a Cairo contract definition for class hash computation by applying
420/// necessary transformations.
421///
422/// This function performs several modifications to ensure compatibility with
423/// Starknet's class hash computation:
424///
425/// 1. Removes the `debug_info` field from the program
426/// 2. Handles Cairo 0.8+ specific attribute fields:
427///    - Removes empty `accessible_scopes` arrays
428///    - Removes `null` `flow_tracking_data` values
429/// 3. Ensures proper spacing in named tuple type definitions for older Cairo
430///    versions
431///
432/// # Arguments
433///
434/// * `contract_definition` - A raw Cairo contract definition to be prepared
435///
436/// # Returns
437///
438/// Returns a `Result` containing the prepared contract definition ready for
439/// class hash computation, or an error if the preparation process fails.
440///
441/// # Note
442///
443/// This preparation step is crucial for maintaining compatibility with the
444/// official Starknet implementation and ensuring consistent class hash
445/// computation across different Cairo versions.
446pub fn prepare_json_contract_definition(
447    contract_definition: RawCairoContractDefinition<'_>,
448) -> Result<PreparedCairoContractDefinition<'_>, Error> {
449    let mut contract_definition = contract_definition.0;
450    contract_definition.program.debug_info = None;
451
452    // Cairo 0.8 added "accessible_scopes" and "flow_tracking_data" attribute
453    // fields, which were not present in older contracts. They present as null /
454    // empty for older contracts and should not be included in the hash
455    // calculation in these cases.
456    //
457    // We therefore check and remove them from the definition before calculating the
458    // hash.
459    contract_definition
460        .program
461        .attributes
462        .iter_mut()
463        .try_for_each(|attr| -> anyhow::Result<()> {
464            let vals = attr
465                .as_object_mut()
466                .context("Program attribute was not an object")?;
467
468            match vals.get_mut("accessible_scopes") {
469                Some(serde_json::Value::Array(array)) if array.is_empty() => {
470                    vals.remove("accessible_scopes");
471                }
472                Some(serde_json::Value::Array(_)) | None => {}
473                Some(_other) => {
474                    anyhow::bail!(
475                        r#"A program's attribute["accessible_scopes"] was not an array type."#
476                    );
477                }
478            }
479            // We don't know what this type is supposed to be, but if its missing it is
480            // null.
481            if let Some(serde_json::Value::Null) = vals.get_mut("flow_tracking_data") {
482                vals.remove("flow_tracking_data");
483            }
484
485            Ok(())
486        })?;
487
488    fn add_extra_space_to_cairo_named_tuples(value: &mut serde_json::Value) {
489        match value {
490            serde_json::Value::Array(v) => walk_array(v),
491            serde_json::Value::Object(m) => walk_map(m),
492            _ => {}
493        }
494    }
495
496    fn walk_array(array: &mut [serde_json::Value]) {
497        for v in array.iter_mut() {
498            add_extra_space_to_cairo_named_tuples(v);
499        }
500    }
501
502    fn walk_map(object: &mut serde_json::Map<String, serde_json::Value>) {
503        for (k, v) in object.iter_mut() {
504            match v {
505                serde_json::Value::String(s) => {
506                    let new_value = add_extra_space_to_named_tuple_type_definition(k, s);
507                    if new_value.as_ref() != s {
508                        *v = serde_json::Value::String(new_value.into());
509                    }
510                }
511                _ => add_extra_space_to_cairo_named_tuples(v),
512            }
513        }
514    }
515
516    fn add_extra_space_to_named_tuple_type_definition<'a>(
517        key: &str,
518        value: &'a str,
519    ) -> std::borrow::Cow<'a, str> {
520        use std::borrow::Cow::*;
521        match key {
522            "cairo_type" | "value" => Owned(add_extra_space_before_colon(value)),
523            _ => Borrowed(value),
524        }
525    }
526
527    fn add_extra_space_before_colon(v: &str) -> String {
528        // This is required because if we receive an already correct ` : `, we will
529        // still "repair" it to `  : ` which we then fix at the end.
530        v.replace(": ", " : ").replace("  :", " :")
531    }
532
533    // Handle a backwards compatibility hack which is required if compiler_version
534    // is not present. See `insert_space` for more details.
535    if contract_definition.program.compiler_version.is_none() {
536        add_extra_space_to_cairo_named_tuples(&mut contract_definition.program.identifiers);
537        add_extra_space_to_cairo_named_tuples(&mut contract_definition.program.reference_manager);
538    }
539
540    Ok(PreparedCairoContractDefinition(contract_definition))
541}
542
543/// Computes the class hash for a Sierra class definition.
544///
545/// This matches the (not very precise) [official documentation][starknet-doc]
546/// and the [cairo-lang implementation][cairo-compute] written in Cairo.
547///
548/// Calculation is somewhat simpler than for Cairo classes, since it does _not_
549/// involve serializing JSON and calculating hashes for the JSON output.
550/// Instead, ABI is handled as a string and all other relevant parts of the
551/// class definition are transformed into Felts and hashed using Poseidon.
552///
553/// [starknet-doc]: https://www.starknet.io/cairo-book/ch100-01-contracts-classes-and-instances.html#class-hash
554/// [cairo-compute]: https://github.com/starkware-libs/cairo-lang/blob/12ca9e91bbdc8a423c63280949c7e34382792067/src/starkware/starknet/core/os/contract_class/contract_class.cairo#L42
555pub fn compute_sierra_class_hash(
556    contract_definition: json::SierraContractDefinition<'_>,
557) -> Result<ClassHash> {
558    if contract_definition.contract_class_version != "0.1.0" {
559        anyhow::bail!("Unsupported Sierra class version");
560    }
561
562    let mut hash = PoseidonHasher::default();
563
564    const SIERRA_VERSION: Felt = felt_bytes!(b"CONTRACT_CLASS_V0.1.0");
565    hash.write(SIERRA_VERSION.into());
566
567    // It is important to process the different entrypoint hashchains in correct
568    // order. Each of the entrypoint lists gets updated into the `outer`
569    // hashchain.
570    //
571    // This implementation doesn't preparse the strings, which makes it a bit more
572    // noisy. Late parsing is made in an attempt to lean on the one big string
573    // allocation we've already got, but these three hash chains could be
574    // constructed at deserialization time.
575    [External, L1Handler, Constructor]
576        .iter()
577        .map(|key| {
578            contract_definition
579                .entry_points_by_type
580                .get(key)
581                .unwrap_or(&Vec::new())
582                .iter()
583                // flatten each entry point to get a list of (selector, function_idx, selector,
584                // function_idx, ...)
585                .flat_map(|x| [x.selector.0, x.function_idx.into()].into_iter())
586                .fold(PoseidonHasher::default(), |mut hc, next| {
587                    hc.write(next.into());
588                    hc
589                })
590        })
591        .for_each(|x| hash.write(x.finish()));
592
593    let abi_truncated_keccak = {
594        let mut keccak = sha3::Keccak256::default();
595        keccak.update(contract_definition.abi.as_bytes());
596        truncated_keccak(<[u8; 32]>::from(keccak.finalize()))
597    };
598    hash.write(abi_truncated_keccak.into());
599
600    let program_hash = {
601        let program_hash = contract_definition.sierra_program.iter().fold(
602            PoseidonHasher::default(),
603            |mut hc, next| {
604                hc.write((*next).into());
605                hc
606            },
607        );
608        program_hash.finish()
609    };
610    hash.write(program_hash);
611
612    Ok(ClassHash(hash.finish().into()))
613}
614
615/// Computes a truncated Keccak hash compatible with Starknet's field element
616/// representation.
617///
618/// This function takes a 32-byte Keccak hash and truncates it to ensure it fits
619/// within Starknet's field element size (251 bits) by masking the most
620/// significant bits.
621///
622/// # Arguments
623///
624/// * `plain` - A 32-byte array containing the full Keccak hash
625///
626/// # Returns
627///
628/// Returns a `Felt` containing the truncated hash value.
629///
630/// # Implementation Note
631///
632/// The implementation masks the first byte with 0x03 to ensure the result is
633/// less than the Starknet prime field modulus. This matches the official Cairo
634/// implementation: <https://github.com/starkware-libs/cairo-lang/blob/64a7f6aed9757d3d8d6c28bd972df73272b0cb0a/src/starkware/starknet/public/abi.py#L21-L26>
635pub fn truncated_keccak(mut plain: [u8; 32]) -> Felt {
636    // python code masks with (2**250 - 1) which starts 0x03 and is followed by 31
637    // 0xff in be truncation is needed not to overflow the field element.
638    plain[0] &= 0x03;
639    Felt::from_be_bytes(plain).expect("cannot overflow: smaller than modulus")
640}
641
642/// `std::io::Write` adapter for Keccak256; we don't need the serialized version
643/// in compute_class_hash, but we need the truncated_keccak hash.
644///
645/// When debugging mismatching hashes, it might be useful to check the length of
646/// each before trying to find the wrongly serialized spot. Example length >
647/// 500kB.
648#[derive(Default)]
649struct KeccakWriter(sha3::Keccak256);
650
651impl std::io::Write for KeccakWriter {
652    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
653        self.0.update(buf);
654        Ok(buf.len())
655    }
656
657    fn flush(&mut self) -> std::io::Result<()> {
658        // noop is fine, we'll finalize after the write phase
659        Ok(())
660    }
661}
662
663/// Starkware doesn't use compact formatting for JSON but default python
664/// formatting. This is required to hash to the same value after sorted
665/// serialization.
666struct PythonDefaultFormatter;
667
668impl serde_json::ser::Formatter for PythonDefaultFormatter {
669    fn begin_array_value<W>(&mut self, writer: &mut W, first: bool) -> std::io::Result<()>
670    where
671        W: ?Sized + std::io::Write,
672    {
673        if first {
674            Ok(())
675        } else {
676            writer.write_all(b", ")
677        }
678    }
679
680    fn begin_object_key<W>(&mut self, writer: &mut W, first: bool) -> std::io::Result<()>
681    where
682        W: ?Sized + std::io::Write,
683    {
684        if first {
685            Ok(())
686        } else {
687            writer.write_all(b", ")
688        }
689    }
690
691    fn begin_object_value<W>(&mut self, writer: &mut W) -> std::io::Result<()>
692    where
693        W: ?Sized + std::io::Write,
694    {
695        writer.write_all(b": ")
696    }
697
698    // Credit: Jonathan Lei from starknet-rs (https://github.com/xJonathanLEI/starknet-rs)`
699    #[inline]
700    fn write_string_fragment<W>(&mut self, writer: &mut W, fragment: &str) -> std::io::Result<()>
701    where
702        W: ?Sized + std::io::Write,
703    {
704        let mut buf = [0, 0];
705
706        for c in fragment.chars() {
707            if c.is_ascii() {
708                writer.write_all(&[c as u8])?;
709            } else {
710                let buf = c.encode_utf16(&mut buf);
711                for i in buf {
712                    write!(writer, r"\u{i:4x}")?;
713                }
714            }
715        }
716
717        Ok(())
718    }
719}
720
721/// Helpers to parse and serialize the parts that compose a Cairo or Sierra
722/// contract
723pub mod json {
724    use std::borrow::Cow;
725    use std::collections::{BTreeMap, HashMap};
726
727    use pathfinder_common::class_definition::{
728        EntryPointType,
729        SelectorAndFunctionIndex,
730        SelectorAndOffset,
731    };
732
733    #[allow(clippy::large_enum_variant)]
734    pub enum ContractDefinition<'a> {
735        Cairo(CairoContractDefinition<'a>),
736        Sierra(SierraContractDefinition<'a>),
737    }
738
739    /// A Sierra contract definition
740    #[derive(serde::Deserialize)]
741    #[serde(deny_unknown_fields)]
742    pub struct SierraContractDefinition<'a> {
743        /// Contract ABI.
744        #[serde(borrow)]
745        pub abi: Cow<'a, str>,
746
747        /// Main program definition.
748        pub sierra_program: Vec<pathfinder_crypto::Felt>,
749
750        // Version
751        #[serde(borrow)]
752        pub contract_class_version: Cow<'a, str>,
753
754        /// The contract entry points
755        pub entry_points_by_type: HashMap<EntryPointType, Vec<SelectorAndFunctionIndex>>,
756    }
757
758    /// Our version of the cairo contract definition used to deserialize and
759    /// re-serialize a modified version for a hash of the contract
760    /// definition.
761    ///
762    /// The implementation uses `serde_json::Value` extensively for the
763    /// unknown/undefined structure, and the correctness of this
764    /// implementation depends on the following features of serde_json:
765    ///
766    /// - feature `raw_value` has to be enabled for the thrown away
767    ///   `program.debug_info`
768    /// - feature `preserve_order` has to be disabled, as we want everything
769    ///   sorted
770    /// - feature `arbitrary_precision` has to be enabled, as there are big
771    ///   integers in the input
772    ///
773    /// It would be much more efficient to have a serde_json::Value which would
774    /// only hold borrowed types.
775    #[derive(serde::Deserialize, serde::Serialize)]
776    #[serde(deny_unknown_fields)]
777    pub struct CairoContractDefinition<'a> {
778        /// Contract ABI, which has no schema definition.
779        pub abi: serde_json::Value,
780
781        /// Main program definition.
782        #[serde(borrow)]
783        pub program: CairoProgram<'a>,
784
785        /// The contract entry points.
786        ///
787        /// These are left out of the re-serialized version with the ordering
788        /// requirement to a Keccak256 hash.
789        #[serde(skip_serializing)]
790        pub entry_points_by_type: HashMap<EntryPointType, Vec<SelectorAndOffset>>,
791    }
792
793    /// A Cairo program definition
794    // It's important that this is ordered alphabetically because the fields need to
795    // be in sorted order for the keccak hashed representation.
796    #[derive(serde::Deserialize, serde::Serialize)]
797    #[serde(deny_unknown_fields)]
798    pub struct CairoProgram<'a> {
799        #[serde(skip_serializing_if = "Vec::is_empty", default)]
800        pub attributes: Vec<serde_json::Value>,
801
802        #[serde(borrow)]
803        pub builtins: Vec<Cow<'a, str>>,
804
805        // Added in Starknet 0.10, so we have to handle this not being present.
806        #[serde(borrow, skip_serializing_if = "Option::is_none")]
807        pub compiler_version: Option<Cow<'a, str>>,
808
809        #[serde(borrow)]
810        pub data: Vec<Cow<'a, str>>,
811
812        #[serde(borrow)]
813        pub debug_info: Option<&'a serde_json::value::RawValue>,
814
815        // Important that this is ordered by the numeric keys, not lexicographically
816        pub hints: BTreeMap<u64, Vec<serde_json::Value>>,
817
818        pub identifiers: serde_json::Value,
819
820        #[serde(borrow)]
821        pub main_scope: Cow<'a, str>,
822
823        // Unlike most other integers, this one is hex string. We don't need to interpret it,
824        // it just needs to be part of the hashed output.
825        #[serde(borrow)]
826        pub prime: Cow<'a, str>,
827
828        pub reference_manager: serde_json::Value,
829    }
830
831    #[cfg(test)]
832    mod test_vectors {
833        use pathfinder_common::class_definition::SerializedOpaqueClassDefinition;
834        use pathfinder_common::macro_prelude::*;
835        use starknet_gateway_test_fixtures::class_definitions::*;
836
837        use super::super::{compute_class_hash, ComputedClassHash};
838
839        fn hash(data: &[u8]) -> ComputedClassHash {
840            compute_class_hash(SerializedOpaqueClassDefinition::from_slice(data))
841                .unwrap()
842                .0
843        }
844
845        #[test]
846        fn first() {
847            assert_eq!(
848                hash(INTEGRATION_TEST),
849                ComputedClassHash::Cairo(class_hash!(
850                    "0x031da92cf5f54bcb81b447e219e2b791b23f3052d12b6c9abd04ff2e5626576"
851                ))
852            );
853        }
854
855        #[test]
856        fn second() {
857            assert_eq!(
858                hash(CONTRACT_DEFINITION),
859                ComputedClassHash::Cairo(class_hash!(
860                    "0x50b2148c0d782914e0b12a1a32abe5e398930b7e914f82c65cb7afce0a0ab9b"
861                ))
862            );
863        }
864
865        #[test]
866        fn genesis_contract() {
867            assert_eq!(
868                hash(GOERLI_GENESIS),
869                ComputedClassHash::Cairo(class_hash!(
870                    "0x10455c752b86932ce552f2b0fe81a880746649b9aee7e0d842bf3f52378f9f8"
871                ))
872            );
873        }
874
875        #[test]
876        fn cairo_0_8() {
877            // Cairo 0.8 update broke our class hash calculation by adding new attribute
878            // fields (which we now need to ignore if empty).
879            assert_eq!(
880                // Known contract which triggered a hash mismatch failure.
881                hash(CAIRO_0_8_NEW_ATTRIBUTES),
882                ComputedClassHash::Cairo(class_hash!(
883                    "0x056b96c1d1bbfa01af44b465763d1b71150fa00c6c9d54c3947f57e979ff68c3"
884                ))
885            );
886        }
887
888        #[test]
889        fn cairo_0_10() {
890            // Contract whose class triggered a deserialization issue because of the new
891            // `compiler_version` property.
892            assert_eq!(
893                hash(CAIRO_0_10_COMPILER_VERSION),
894                ComputedClassHash::Cairo(class_hash!(
895                    "0xa69700a89b1fa3648adff91c438b79c75f7dcb0f4798938a144cce221639d6"
896                ))
897            );
898        }
899
900        #[test]
901        fn cairo_0_10_part_2() {
902            // Contract who's class contains `compiler_version` property as well as
903            // `cairo_type` with tuple values. These tuple values require a
904            // space to be injected in order to achieve the correct hash.
905            assert_eq!(
906                hash(CAIRO_0_10_TUPLES_INTEGRATION),
907                ComputedClassHash::Cairo(class_hash!(
908                    "0x542460935cea188d21e752d8459d82d60497866aaad21f873cbb61621d34f7f"
909                ))
910            );
911        }
912
913        #[test]
914        fn cairo_0_10_part_3() {
915            // Contract who's class contains `compiler_version` property as well as
916            // `cairo_type` with tuple values. These tuple values require a
917            // space to be injected in order to achieve the correct hash.
918            assert_eq!(
919                hash(CAIRO_0_10_TUPLES_GOERLI),
920                ComputedClassHash::Cairo(class_hash!(
921                    "0x66af14b94491ba4e2aea1117acf0a3155c53d92fdfd9c1f1dcac90dc2d30157"
922                ))
923            );
924        }
925
926        #[test]
927        fn cairo_0_11_sierra() {
928            assert_eq!(
929                hash(CAIRO_0_11_SIERRA),
930                ComputedClassHash::Sierra(class_hash!(
931                    "0x4e70b19333ae94bd958625f7b61ce9eec631653597e68645e13780061b2136c"
932                ))
933            )
934        }
935
936        #[test]
937        fn cairo_0_11_with_decimal_entry_point_offset() {
938            assert_eq!(
939                hash(CAIRO_0_11_WITH_DECIMAL_ENTRY_POINT_OFFSET),
940                ComputedClassHash::Cairo(class_hash!(
941                    "0x0484c163658bcce5f9916f486171ac60143a92897533aa7ff7ac800b16c63311"
942                ))
943            );
944        }
945    }
946
947    #[cfg(test)]
948    mod test_serde_features {
949        #[test]
950        fn serde_json_value_sorts_maps() {
951            // this property is leaned on and the default implementation of serde_json works
952            // like this. serde_json has a feature called "preserve_order" which
953            // could get enabled by accident, and it would destroy the ability
954            // to compute_class_hash.
955
956            let input = r#"{"foo": 1, "bar": 2}"#;
957            let parsed = serde_json::from_str::<serde_json::Value>(input).unwrap();
958            let output = serde_json::to_string(&parsed).unwrap();
959
960            assert_eq!(output, r#"{"bar":2,"foo":1}"#);
961        }
962
963        #[test]
964        fn serde_json_has_arbitrary_precision() {
965            // the json has 251-bit ints, python handles them out of box, serde_json
966            // requires feature "arbitrary_precision".
967
968            // this is 2**256 - 1
969            let input = r#"{"foo":115792089237316195423570985008687907853269984665640564039457584007913129639935}"#;
970
971            let output =
972                serde_json::to_string(&serde_json::from_str::<serde_json::Value>(input).unwrap())
973                    .unwrap();
974
975            assert_eq!(input, output);
976        }
977
978        #[test]
979        fn serde_json_has_raw_value() {
980            // raw value is needed for others but here for completeness; this shouldn't
981            // compile if you the feature wasn't enabled.
982
983            #[derive(serde::Deserialize, serde::Serialize)]
984            struct Program<'a> {
985                #[serde(borrow)]
986                debug_info: Option<&'a serde_json::value::RawValue>,
987            }
988
989            let mut input = serde_json::from_str::<Program<'_>>(
990                r#"{"debug_info": {"long": {"tree": { "which": ["we dont", "care", "about", 0] }}}}"#,
991            ).unwrap();
992
993            input.debug_info = None;
994
995            let output = serde_json::to_string(&input).unwrap();
996
997            assert_eq!(output, r#"{"debug_info":null}"#);
998        }
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    #[test]
1005    fn truncated_keccak_matches_pythonic() {
1006        use pathfinder_common::felt;
1007        use sha3::{Digest, Keccak256};
1008
1009        use super::truncated_keccak;
1010        let all_set = Keccak256::digest([0xffu8; 32]);
1011        assert!(all_set[0] > 0xf);
1012        let truncated = truncated_keccak(all_set.into());
1013        assert_eq!(
1014            truncated,
1015            felt!("0x1c584056064687e149968cbab758a3376d22aedc6a55823d1b3ecbee81b8fb9")
1016        );
1017    }
1018}