starknet_core/types/contract/
mod.rs

1use alloc::{format, string::*, vec::*};
2
3use serde::{de::Visitor, ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer};
4use serde_json_pythonic::to_string_pythonic;
5use serde_with::serde_as;
6use starknet_crypto::{poseidon_hash_many, PoseidonHasher};
7
8use crate::{
9    serde::unsigned_field_element::UfeHex,
10    types::{EntryPointsByType, Felt, FlattenedSierraClass, SierraEntryPoint},
11    utils::{
12        cairo_short_string_to_felt, normalize_address, starknet_keccak, CairoShortStringToFeltError,
13    },
14};
15
16/// Module containing types related to artifacts of contracts compiled with a Cairo 0.x compiler.
17pub mod legacy;
18
19/// Cairo string for `CONTRACT_CLASS_V0.1.0`
20const PREFIX_CONTRACT_CLASS_V0_1_0: Felt = Felt::from_raw([
21    37302452645455172,
22    18446734822722598327,
23    15539482671244488427,
24    5800711240972404213,
25]);
26
27/// Cairo string for `COMPILED_CLASS_V1`
28const PREFIX_COMPILED_CLASS_V1: Felt = Felt::from_raw([
29    324306817650036332,
30    18446744073709549462,
31    1609463842841646376,
32    2291010424822318237,
33]);
34
35/// Cairo contract artifact in a representation identical to the compiler output.
36#[derive(Debug, Clone, Serialize)]
37#[serde(untagged)]
38#[allow(clippy::large_enum_variant)]
39pub enum ContractArtifact {
40    /// Sierra (Cairo 1) class.
41    SierraClass(SierraClass),
42    /// Cairo assembly (CASM) class compiled from a Sierra class.
43    CompiledClass(CompiledClass),
44    /// Legacy (Cairo 0) class
45    LegacyClass(legacy::LegacyContractClass),
46}
47
48/// A Sierra (Cairo 1) contract class in a representation identical to the compiler output.
49#[serde_as]
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
52pub struct SierraClass {
53    /// Sierra bytecode.
54    #[serde_as(as = "Vec<UfeHex>")]
55    pub sierra_program: Vec<Felt>,
56    /// Sierra class debug info.
57    pub sierra_program_debug_info: SierraClassDebugInfo,
58    /// Sierra version.
59    pub contract_class_version: String,
60    /// Contract entrypoints.
61    pub entry_points_by_type: EntryPointsByType,
62    /// Contract ABI.
63    pub abi: Vec<AbiEntry>,
64}
65
66/// A Cairo assembly (CASM) class compiled from a Sierra class.
67///
68/// The Sierra to CASM process is needed as the Cairo VM can only execute CASM, not Sierra bytecode.
69/// However, if direct deployment of CASM were allowed, unsafe (unprovable) programs could be
70/// deployed to a public network allowing for DOS attacks. Instead, only Sierra, an intermedia
71/// representation that always compiles to safe (provable) CASM, is allowed to be deployed.
72#[serde_as]
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
75pub struct CompiledClass {
76    /// The STARK field prime.
77    pub prime: String,
78    /// Version of the compiler used to compile this contract.
79    pub compiler_version: String,
80    /// Cairo assembly bytecode.
81    #[serde_as(as = "Vec<UfeHex>")]
82    pub bytecode: Vec<Felt>,
83    /// Represents the structure of the bytecode segments, using a nested list of segment lengths.
84    /// For example, [2, [3, 4]] represents a bytecode with 2 segments, the first is a leaf of
85    /// length 2 and the second is a node with 2 children of lengths 3 and 4.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub bytecode_segment_lengths: Option<IntOrList>,
88    /// Hints for non-determinism.
89    pub hints: Vec<Hint>,
90    /// Same as `hints` but represented in Python code, which can be generated by the compiler but
91    /// is off by default.
92    pub pythonic_hints: Option<Vec<PythonicHint>>,
93    /// Contract entrypoints.
94    pub entry_points_by_type: CompiledClassEntrypointList,
95}
96
97/// Debug information optionally generated by the compiler for mapping IDs to human-readable names.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
100pub struct SierraClassDebugInfo {
101    /// Mapping from type IDs to names.
102    pub type_names: Vec<(u64, String)>,
103    /// Mapping from libfunc IDs to names.
104    pub libfunc_names: Vec<(u64, String)>,
105    /// Mapping from user function IDs to names.
106    pub user_func_names: Vec<(u64, String)>,
107}
108
109/// Cairo assembly (CASM) contract entrypoints by types.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
112#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
113pub struct CompiledClassEntrypointList {
114    /// Entrypoints of type `EXTERNAL` used for invocations from outside contracts.
115    pub external: Vec<CompiledClassEntrypoint>,
116    /// Entrypoints of type `L1_HANDLER` used for handling L1-to-L2 messages.
117    pub l1_handler: Vec<CompiledClassEntrypoint>,
118    /// Entrypoints of type `CONSTRUCTOR` used during contract deployment.
119    pub constructor: Vec<CompiledClassEntrypoint>,
120}
121
122/// Sierra (Cairo 1) contract ABI item.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(tag = "type", rename_all = "snake_case")]
125#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
126pub enum AbiEntry {
127    /// Function ABI entry.
128    Function(AbiFunction),
129    /// Event ABI entry.
130    Event(AbiEvent),
131    /// Struct ABI entry.
132    Struct(AbiStruct),
133    /// Enum ABI entry.
134    Enum(AbiEnum),
135    /// Constructor ABI entry.
136    Constructor(AbiConstructor),
137    /// Impl ABI entry.
138    Impl(AbiImpl),
139    /// Interface ABI entry.
140    Interface(AbiInterface),
141    /// L1 handler ABI entry.
142    L1Handler(AbiFunction),
143}
144
145/// Cairo assembly (CASM) hints for introducing non-determinism into a Cairo program.
146///
147/// Unlike legacy (Cairo 0) hints which are arbitrary Python code, hints compiled from Sierra
148/// bytecode come from a predefined list of hint specifications (e.g. `TestLessThanOrEqual`).
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct Hint {
151    /// ID of the hint.
152    pub id: u64,
153    // For convenience we just treat it as an opaque JSON value here, unless a use case justifies
154    // implementing the structure. (We no longer need the hints for the class hash anyways.)
155    /// Declarative specification of the hint.
156    pub code: Vec<serde_json::Value>,
157}
158
159/// Same as [`Hint`] but is represented as Python code instead of a declarative specification.
160#[derive(Debug, Clone)]
161pub struct PythonicHint {
162    /// ID of the hint.
163    pub id: u64,
164    /// Python code representation of the hint.
165    pub code: Vec<String>,
166}
167
168/// An Cairo assembly (CASM) contract entrypoint for translating a selector to a bytecode offset.
169#[serde_as]
170#[derive(Debug, Clone, Serialize, Deserialize)]
171#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
172pub struct CompiledClassEntrypoint {
173    /// Selector of the entrypoint, usually computed as the Starknet Keccak of the function name.
174    #[serde_as(as = "UfeHex")]
175    pub selector: Felt,
176    /// Offset in the bytecode.
177    pub offset: u64,
178    /// The list of Cairo builtins used with this entrypoint.
179    pub builtins: Vec<String>,
180}
181
182/// Sierra (Cairo 1) contract ABI representation of a function.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
185pub struct AbiFunction {
186    /// Name of the function.
187    pub name: String,
188    /// Inputs to the function.
189    pub inputs: Vec<AbiNamedMember>,
190    /// Outputs of the function.
191    pub outputs: Vec<AbiOutput>,
192    /// State mutability of the function.
193    ///
194    /// Note that this is currently not enforced by the compiler. It's therefore only as accurate as
195    /// the code author annotating them is.
196    pub state_mutability: StateMutability,
197}
198
199/// Sierra (Cairo 1) contract ABI representation of an event.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
202#[serde(untagged)]
203pub enum AbiEvent {
204    /// Cairo 2.x ABI event entry.
205    Typed(TypedAbiEvent),
206    /// Cairo 1.x ABI event entry.
207    Untyped(UntypedAbiEvent),
208}
209
210/// Cairo 2.x ABI event entry.
211#[derive(Debug, Clone, Deserialize)]
212#[serde(tag = "kind", rename_all = "snake_case")]
213#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
214pub enum TypedAbiEvent {
215    /// An event definition that's a struct.
216    Struct(AbiEventStruct),
217    /// An event definition that's an enum.
218    Enum(AbiEventEnum),
219}
220
221/// Cairo 1.x ABI event entry.
222#[derive(Debug, Clone, Serialize, Deserialize)]
223#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
224pub struct UntypedAbiEvent {
225    /// Name of the event.
226    pub name: String,
227    /// Fields of the event.
228    pub inputs: Vec<AbiNamedMember>,
229}
230
231/// An event definition that's a struct.
232#[derive(Debug, Clone, Serialize, Deserialize)]
233#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
234pub struct AbiEventStruct {
235    /// Name of the event struct.
236    pub name: String,
237    /// Fields of the event struct.
238    pub members: Vec<EventField>,
239}
240
241/// An event definition that's an enum.
242#[derive(Debug, Clone, Serialize, Deserialize)]
243#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
244pub struct AbiEventEnum {
245    /// Name of the event enum.
246    pub name: String,
247    /// Variants of the event enum.
248    pub variants: Vec<EventField>,
249}
250
251/// Sierra (Cairo 1) contract ABI representation of a struct.
252#[derive(Debug, Clone, Serialize, Deserialize)]
253#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
254pub struct AbiStruct {
255    /// Name of the struct.
256    pub name: String,
257    /// Fields of the struct.
258    pub members: Vec<AbiNamedMember>,
259}
260
261/// Sierra (Cairo 1) contract ABI representation of a constructor.
262#[derive(Debug, Clone, Serialize, Deserialize)]
263#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
264pub struct AbiConstructor {
265    /// Name of the constructor.
266    pub name: String,
267    /// Inputs to the constructor.
268    pub inputs: Vec<AbiNamedMember>,
269}
270
271/// Sierra (Cairo 1) contract ABI representation of an interface implementation.
272#[derive(Debug, Clone, Serialize, Deserialize)]
273#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
274pub struct AbiImpl {
275    /// Name of the interface implementation.
276    pub name: String,
277    /// Name of the interface being implemented.
278    pub interface_name: String,
279}
280
281/// Sierra (Cairo 1) contract ABI representation of an interface.
282#[derive(Debug, Clone, Serialize, Deserialize)]
283#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
284pub struct AbiInterface {
285    /// Name of the interface.
286    pub name: String,
287    /// The shape of the interface.
288    pub items: Vec<AbiEntry>,
289}
290
291/// Sierra (Cairo 1) contract ABI representation of an enum.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
294pub struct AbiEnum {
295    /// Name of the enum.
296    pub name: String,
297    /// Variants of the enum.
298    pub variants: Vec<AbiNamedMember>,
299}
300
301/// A name and type pair for describing a struct field.
302#[derive(Debug, Clone, Serialize, Deserialize)]
303#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
304pub struct AbiNamedMember {
305    /// Name of the field.
306    pub name: String,
307    /// Type of the field.
308    pub r#type: String,
309}
310
311/// An output from a contract function.
312#[derive(Debug, Clone, Serialize, Deserialize)]
313#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
314pub struct AbiOutput {
315    /// Type of the output.
316    pub r#type: String,
317}
318
319/// Struct field or enum variant of an event type.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
322pub struct EventField {
323    /// Name of the field or variant.
324    pub name: String,
325    /// Type of the field or variant.
326    pub r#type: String,
327    /// The role this field or variant plays as part of an event emission.
328    pub kind: EventFieldKind,
329}
330
331/// State mutability of a function.
332///
333/// Note that this is currently not enforced by the compiler. It's therefore only as accurate as the
334/// code author annotating them is.
335#[derive(Debug, Clone, Serialize, Deserialize)]
336#[serde(rename_all = "snake_case")]
337pub enum StateMutability {
338    /// The function is *annotated* as potentially state-changing.
339    External,
340    /// The function is *annotated* as view-only, without changing the state.
341    View,
342}
343
344/// The role an event struct field or enum variant plays during an event emission.
345#[derive(Debug, Clone, Serialize, Deserialize)]
346#[serde(rename_all = "snake_case")]
347pub enum EventFieldKind {
348    /// Emitted as part of the event keys. Annotated with `#[key]`.
349    Key,
350    /// Emitted as part of the event data. Annotated with `#[data]`.
351    Data,
352    /// Serialized as a nested event.
353    Nested,
354    /// Serialize as a flat event. Annotated with `#[flat]`.
355    Flat,
356}
357
358/// A node in a bytecode segment length tree.
359#[derive(Debug, Clone)]
360pub enum IntOrList {
361    /// A leave node of the tree representing a segment.
362    Int(u64),
363    /// A branch node of the tree.
364    List(Vec<IntOrList>),
365}
366
367struct IntOrListVisitor;
368
369/// Internal structure used for post-Sierra-1.5.0 CASM hash calculation.
370enum BytecodeSegmentStructure {
371    BytecodeLeaf(BytecodeLeaf),
372    BytecodeSegmentedNode(BytecodeSegmentedNode),
373}
374
375/// Internal structure used for post-Sierra-1.5.0 CASM hash calculation.
376///
377/// Represents a leaf in the bytecode segment tree.
378struct BytecodeLeaf {
379    // NOTE: change this to a slice?
380    data: Vec<Felt>,
381}
382
383/// Internal structure used for post-Sierra-1.5.0 CASM hash calculation.
384///
385/// Represents an internal node in the bytecode segment tree. Each child can be loaded into memory
386/// or skipped.
387struct BytecodeSegmentedNode {
388    segments: Vec<BytecodeSegment>,
389}
390
391/// Internal structure used for post-Sierra-1.5.0 CASM hash calculation.
392///
393/// Represents a child of [`BytecodeSegmentedNode`].
394struct BytecodeSegment {
395    segment_length: u64,
396    #[allow(unused)]
397    is_used: bool,
398    inner_structure: alloc::boxed::Box<BytecodeSegmentStructure>,
399}
400
401mod errors {
402    use alloc::string::*;
403    use core::fmt::{Display, Formatter, Result};
404
405    /// Errors computing a class hash.
406    #[derive(Debug)]
407    pub enum ComputeClassHashError {
408        /// An invalid Cairo builtin name is found. This can happen when computing hashes for legacy
409        /// (Cairo 0) or Cairo assembly (CASM) classes.
410        InvalidBuiltinName,
411        /// The total bytecode length is different than the implied length from
412        /// `bytecode_segment_lengths`. This can happen when computing Cairo assembly (CASM) class
413        /// hashes.
414        BytecodeSegmentLengthMismatch(BytecodeSegmentLengthMismatchError),
415        /// A bytecode segment specified in `bytecode_segment_lengths` is invalid. This can happen
416        /// when computing Cairo assembly (CASM) class hashes.
417        InvalidBytecodeSegment(InvalidBytecodeSegmentError),
418        /// The bytecode segments specified in `bytecode_segment_lengths` do not cover the full
419        /// range of bytecode. This can happen when computing Cairo assembly (CASM) class hashes.
420        PcOutOfRange(PcOutOfRangeError),
421        /// Json serialization error. This can happen when serializing the contract ABI, as required
422        /// for computing hashes for legacy (Cairo 0) and Sierra (Cairo 1) classes.
423        Json(JsonError),
424    }
425
426    /// Errors compressing a legacy (Cairo 0) class.
427    #[cfg(feature = "std")]
428    #[derive(Debug)]
429    pub enum CompressProgramError {
430        /// Json serialization error when serializing the contract ABI.
431        Json(JsonError),
432        /// Gzip encoding error.
433        Io(std::io::Error),
434    }
435
436    /// Json serialization error represented as a string message. This is done instead of exposing
437    /// `serde_json` error types to avoid leaking implementation details and ease error handling.
438    #[derive(Debug)]
439    pub struct JsonError {
440        pub(crate) message: String,
441    }
442
443    /// The total bytecode length is different than the implied length from
444    /// `bytecode_segment_lengths`.
445    #[derive(Debug)]
446    pub struct BytecodeSegmentLengthMismatchError {
447        /// The total length implied by `bytecode_segment_lengths`.
448        pub segment_length: usize,
449        /// The actual bytecode length.
450        pub bytecode_length: usize,
451    }
452
453    /// The bytecode segment is invalid as it's not consecutive with the prior segment.
454    #[derive(Debug)]
455    pub struct InvalidBytecodeSegmentError {
456        /// The expected program counter implied from a previous processed segment.
457        pub visited_pc: u64,
458        /// The actual starting program counter as specified by the segment bytecode offset.
459        pub segment_start: u64,
460    }
461
462    /// The bytecode segments specified in `bytecode_segment_lengths` do not cover the full range of
463    /// bytecode.
464    #[derive(Debug)]
465    pub struct PcOutOfRangeError {
466        /// The first out-of-range program counter.
467        pub pc: u64,
468    }
469
470    #[cfg(feature = "std")]
471    impl std::error::Error for ComputeClassHashError {}
472
473    impl Display for ComputeClassHashError {
474        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
475            match self {
476                Self::InvalidBuiltinName => write!(f, "invalid builtin name"),
477                Self::BytecodeSegmentLengthMismatch(inner) => write!(f, "{inner}"),
478                Self::InvalidBytecodeSegment(inner) => write!(f, "{inner}"),
479                Self::PcOutOfRange(inner) => write!(f, "{inner}"),
480                Self::Json(inner) => write!(f, "json serialization error: {inner}"),
481            }
482        }
483    }
484
485    #[cfg(feature = "std")]
486    impl std::error::Error for CompressProgramError {}
487
488    #[cfg(feature = "std")]
489    impl Display for CompressProgramError {
490        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
491            match self {
492                Self::Json(inner) => write!(f, "json serialization error: {inner}"),
493                Self::Io(inner) => write!(f, "compression io error: {inner}"),
494            }
495        }
496    }
497
498    #[cfg(feature = "std")]
499    impl std::error::Error for JsonError {}
500
501    impl Display for JsonError {
502        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
503            write!(f, "{}", self.message)
504        }
505    }
506
507    #[cfg(feature = "std")]
508    impl std::error::Error for BytecodeSegmentLengthMismatchError {}
509
510    impl Display for BytecodeSegmentLengthMismatchError {
511        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
512            write!(
513                f,
514                "invalid bytecode segment structure length: {}, bytecode length: {}.",
515                self.segment_length, self.bytecode_length,
516            )
517        }
518    }
519
520    #[cfg(feature = "std")]
521    impl std::error::Error for InvalidBytecodeSegmentError {}
522
523    impl Display for InvalidBytecodeSegmentError {
524        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
525            write!(
526                f,
527                "invalid segment structure: PC {} was visited, \
528                but the beginning of the segment ({}) was not",
529                self.visited_pc, self.segment_start
530            )
531        }
532    }
533
534    #[cfg(feature = "std")]
535    impl std::error::Error for PcOutOfRangeError {}
536
537    impl Display for PcOutOfRangeError {
538        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
539            write!(f, "PC {} is out of range", self.pc)
540        }
541    }
542}
543pub use errors::{
544    BytecodeSegmentLengthMismatchError, ComputeClassHashError, InvalidBytecodeSegmentError,
545    JsonError, PcOutOfRangeError,
546};
547
548#[cfg(feature = "std")]
549pub use errors::CompressProgramError;
550
551impl SierraClass {
552    /// Computes the class hash of the Sierra (Cairo 1) class.
553    pub fn class_hash(&self) -> Result<Felt, ComputeClassHashError> {
554        // Technically we don't have to use the Pythonic JSON style here. Doing this just to align
555        // with the official `cairo-lang` CLI.
556        //
557        // TODO: add an `AbiFormatter` trait and let users choose which one to use.
558        let abi_str = to_string_pythonic(&self.abi).map_err(|err| {
559            ComputeClassHashError::Json(JsonError {
560                message: format!("{err}"),
561            })
562        })?;
563
564        let mut hasher = PoseidonHasher::new();
565        hasher.update(PREFIX_CONTRACT_CLASS_V0_1_0);
566
567        // Hashes entry points
568        hasher.update(hash_sierra_entrypoints(&self.entry_points_by_type.external));
569        hasher.update(hash_sierra_entrypoints(
570            &self.entry_points_by_type.l1_handler,
571        ));
572        hasher.update(hash_sierra_entrypoints(
573            &self.entry_points_by_type.constructor,
574        ));
575
576        // Hashes ABI
577        hasher.update(starknet_keccak(abi_str.as_bytes()));
578
579        // Hashes Sierra program
580        hasher.update(poseidon_hash_many(&self.sierra_program));
581
582        Ok(normalize_address(hasher.finalize()))
583    }
584
585    /// Flattens the Sierra (Cairo 1) class by serializing the ABI using a Pythonic JSON
586    /// representation. The Pythoic JSON format is used for achieving the exact same output as that
587    /// of `cairo-lang`.
588    ///
589    /// The flattening is necessary for class declaration as the network does not deal with
590    /// structured contract ABI. Instead, any ABI is treated as an opaque string.
591    ///
592    /// Therefore, a process is needed for transforming a structured ABI into a string. This process
593    /// is called "flattening" in this library. The word is chosen instead of "serializing" as the
594    /// latter implies that there must be a way to "deserialize" the resulting string back to its
595    /// original, structured form. However, that would not be guaranteed.
596    ///
597    /// While it's true that the in *this* particular implementation, the string can *indeed* be
598    /// deserialized back to the original form, it's easy to imagine other flattening strategies
599    /// that are more destructive. In the future, this library will also allow custom flattening
600    /// implementations where the Pythoic style one presented here would merely be one of them.
601    pub fn flatten(self) -> Result<FlattenedSierraClass, JsonError> {
602        let abi = to_string_pythonic(&self.abi).map_err(|err| JsonError {
603            message: format!("{err}"),
604        })?;
605
606        Ok(FlattenedSierraClass {
607            sierra_program: self.sierra_program,
608            entry_points_by_type: self.entry_points_by_type,
609            abi,
610            contract_class_version: self.contract_class_version,
611        })
612    }
613}
614
615impl FlattenedSierraClass {
616    /// Computes the class hash of the flattened Sierra (Cairo 1) class.
617    pub fn class_hash(&self) -> Felt {
618        let mut hasher = PoseidonHasher::new();
619        hasher.update(PREFIX_CONTRACT_CLASS_V0_1_0);
620
621        // Hashes entry points
622        hasher.update(hash_sierra_entrypoints(&self.entry_points_by_type.external));
623        hasher.update(hash_sierra_entrypoints(
624            &self.entry_points_by_type.l1_handler,
625        ));
626        hasher.update(hash_sierra_entrypoints(
627            &self.entry_points_by_type.constructor,
628        ));
629
630        // Hashes ABI
631        hasher.update(starknet_keccak(self.abi.as_bytes()));
632
633        // Hashes Sierra program
634        hasher.update(poseidon_hash_many(&self.sierra_program));
635
636        normalize_address(hasher.finalize())
637    }
638}
639
640impl CompiledClass {
641    /// Computes the class hash of the Cairo assembly (CASM) class.
642    pub fn class_hash(&self) -> Result<Felt, ComputeClassHashError> {
643        let mut hasher = PoseidonHasher::new();
644        hasher.update(PREFIX_COMPILED_CLASS_V1);
645
646        // Hashes entry points
647        hasher.update(
648            Self::hash_entrypoints(&self.entry_points_by_type.external)
649                .map_err(|_| ComputeClassHashError::InvalidBuiltinName)?,
650        );
651        hasher.update(
652            Self::hash_entrypoints(&self.entry_points_by_type.l1_handler)
653                .map_err(|_| ComputeClassHashError::InvalidBuiltinName)?,
654        );
655        hasher.update(
656            Self::hash_entrypoints(&self.entry_points_by_type.constructor)
657                .map_err(|_| ComputeClassHashError::InvalidBuiltinName)?,
658        );
659
660        // Hashes bytecode
661        hasher.update(
662            if let Some(bytecode_segment_lengths) = self.bytecode_segment_lengths.clone() {
663                // `bytecode_segment_lengths` was added since Sierra 1.5.0 and changed hash calculation.
664                // This implementation here is basically a direct translation of the Python code from
665                // `cairo-lang` v0.13.1. The goal was simply to have a working implementation as quickly
666                // as possible. There should be some optimizations to be made here.
667                // TODO: review how this can be optimized
668
669                // NOTE: this looks extremely inefficient. Maybe just use a number for tracking instead?
670                let mut rev_visited_pcs: Vec<u64> =
671                    (0..(self.bytecode.len() as u64)).rev().collect();
672
673                let (res, total_len) = Self::create_bytecode_segment_structure_inner(
674                    &self.bytecode,
675                    &bytecode_segment_lengths,
676                    &mut rev_visited_pcs,
677                    &mut 0,
678                )?;
679
680                if total_len != self.bytecode.len() as u64 {
681                    return Err(ComputeClassHashError::BytecodeSegmentLengthMismatch(
682                        BytecodeSegmentLengthMismatchError {
683                            segment_length: total_len as usize,
684                            bytecode_length: self.bytecode.len(),
685                        },
686                    ));
687                }
688                if !rev_visited_pcs.is_empty() {
689                    return Err(ComputeClassHashError::PcOutOfRange(PcOutOfRangeError {
690                        pc: rev_visited_pcs[rev_visited_pcs.len() - 1],
691                    }));
692                }
693
694                res.hash()
695            } else {
696                // Pre-Sierra-1.5.0 compiled classes
697                poseidon_hash_many(&self.bytecode)
698            },
699        );
700
701        Ok(hasher.finalize())
702    }
703
704    fn hash_entrypoints(
705        entrypoints: &[CompiledClassEntrypoint],
706    ) -> Result<Felt, CairoShortStringToFeltError> {
707        let mut hasher = PoseidonHasher::new();
708
709        for entry in entrypoints {
710            hasher.update(entry.selector);
711            hasher.update(entry.offset.into());
712
713            let mut builtin_hasher = PoseidonHasher::new();
714            for builtin in &entry.builtins {
715                builtin_hasher.update(cairo_short_string_to_felt(builtin)?)
716            }
717
718            hasher.update(builtin_hasher.finalize());
719        }
720
721        Ok(hasher.finalize())
722    }
723
724    // Direct translation of `_create_bytecode_segment_structure_inner` from `cairo-lang` v0.13.1.
725    //
726    // `visited_pcs` should be given in reverse order, and is consumed by the function. Returns the
727    // BytecodeSegmentStructure and the total length of the processed segment.
728    fn create_bytecode_segment_structure_inner(
729        bytecode: &[Felt],
730        bytecode_segment_lengths: &IntOrList,
731        visited_pcs: &mut Vec<u64>,
732        bytecode_offset: &mut u64,
733    ) -> Result<(BytecodeSegmentStructure, u64), ComputeClassHashError> {
734        match bytecode_segment_lengths {
735            IntOrList::Int(bytecode_segment_lengths) => {
736                let segment_end = *bytecode_offset + bytecode_segment_lengths;
737
738                // Remove all the visited PCs that are in the segment.
739                while !visited_pcs.is_empty()
740                    && *bytecode_offset <= visited_pcs[visited_pcs.len() - 1]
741                    && visited_pcs[visited_pcs.len() - 1] < segment_end
742                {
743                    visited_pcs.pop();
744                }
745
746                Ok((
747                    BytecodeSegmentStructure::BytecodeLeaf(BytecodeLeaf {
748                        data: bytecode[(*bytecode_offset as usize)..(segment_end as usize)]
749                            .to_vec(),
750                    }),
751                    *bytecode_segment_lengths,
752                ))
753            }
754            IntOrList::List(bytecode_segment_lengths) => {
755                let mut res = Vec::new();
756                let mut total_len = 0;
757
758                for item in bytecode_segment_lengths {
759                    let visited_pc_before = if !visited_pcs.is_empty() {
760                        Some(visited_pcs[visited_pcs.len() - 1])
761                    } else {
762                        None
763                    };
764
765                    let (current_structure, item_len) =
766                        Self::create_bytecode_segment_structure_inner(
767                            bytecode,
768                            item,
769                            visited_pcs,
770                            bytecode_offset,
771                        )?;
772
773                    let visited_pc_after = if !visited_pcs.is_empty() {
774                        Some(visited_pcs[visited_pcs.len() - 1])
775                    } else {
776                        None
777                    };
778                    let is_used = visited_pc_after != visited_pc_before;
779
780                    if let Some(visited_pc_before) = visited_pc_before {
781                        if is_used && visited_pc_before != *bytecode_offset {
782                            return Err(ComputeClassHashError::InvalidBytecodeSegment(
783                                InvalidBytecodeSegmentError {
784                                    visited_pc: visited_pc_before,
785                                    segment_start: *bytecode_offset,
786                                },
787                            ));
788                        }
789                    }
790
791                    res.push(BytecodeSegment {
792                        segment_length: item_len,
793                        is_used,
794                        inner_structure: alloc::boxed::Box::new(current_structure),
795                    });
796
797                    *bytecode_offset += item_len;
798                    total_len += item_len;
799                }
800
801                Ok((
802                    BytecodeSegmentStructure::BytecodeSegmentedNode(BytecodeSegmentedNode {
803                        segments: res,
804                    }),
805                    total_len,
806                ))
807            }
808        }
809    }
810}
811
812impl BytecodeSegmentStructure {
813    fn hash(&self) -> Felt {
814        match self {
815            Self::BytecodeLeaf(inner) => inner.hash(),
816            Self::BytecodeSegmentedNode(inner) => inner.hash(),
817        }
818    }
819}
820
821impl BytecodeLeaf {
822    fn hash(&self) -> Felt {
823        poseidon_hash_many(&self.data)
824    }
825}
826
827impl BytecodeSegmentedNode {
828    fn hash(&self) -> Felt {
829        let mut hasher = PoseidonHasher::new();
830        for node in &self.segments {
831            hasher.update(node.segment_length.into());
832            hasher.update(node.inner_structure.hash());
833        }
834        hasher.finalize() + Felt::ONE
835    }
836}
837
838// We need to manually implement this because `raw_value` doesn't work with `untagged`:
839//   https://github.com/serde-rs/serde/issues/1183
840impl<'de> Deserialize<'de> for ContractArtifact {
841    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
842    where
843        D: Deserializer<'de>,
844    {
845        let temp_value = serde_json::Value::deserialize(deserializer)?;
846        if let Ok(value) = SierraClass::deserialize(&temp_value) {
847            return Ok(Self::SierraClass(value));
848        }
849        if let Ok(value) = CompiledClass::deserialize(&temp_value) {
850            return Ok(Self::CompiledClass(value));
851        }
852        if let Ok(value) = legacy::LegacyContractClass::deserialize(&temp_value) {
853            return Ok(Self::LegacyClass(value));
854        }
855        Err(serde::de::Error::custom(
856            "data did not match any variant of enum ContractArtifact",
857        ))
858    }
859}
860
861impl Serialize for PythonicHint {
862    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
863    where
864        S: Serializer,
865    {
866        let mut seq = serializer.serialize_seq(Some(2))?;
867        seq.serialize_element(&self.id)?;
868        seq.serialize_element(&self.code)?;
869        seq.end()
870    }
871}
872
873impl<'de> Deserialize<'de> for PythonicHint {
874    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
875    where
876        D: Deserializer<'de>,
877    {
878        let temp_value = serde_json::Value::deserialize(deserializer)?;
879        if let serde_json::Value::Array(mut array) = temp_value {
880            if array.len() != 2 {
881                return Err(serde::de::Error::custom("length mismatch"));
882            }
883
884            let code = array.pop().unwrap();
885            let code = Vec::<String>::deserialize(code).map_err(|err| {
886                serde::de::Error::custom(format!("unable to deserialize Location: {err}"))
887            })?;
888
889            let id = array.pop().unwrap();
890            let id = match id {
891                serde_json::Value::Number(id) => id
892                    .as_u64()
893                    .ok_or_else(|| serde::de::Error::custom("id value out of range"))?,
894                _ => return Err(serde::de::Error::custom("unexpected value type")),
895            };
896
897            Ok(Self { id, code })
898        } else {
899            Err(serde::de::Error::custom("expected sequence"))
900        }
901    }
902}
903
904// Manually implementing this so we can put `kind` in the middle:
905impl Serialize for TypedAbiEvent {
906    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
907    where
908        S: Serializer,
909    {
910        #[derive(Serialize)]
911        struct StructRef<'a> {
912            name: &'a str,
913            kind: &'static str,
914            members: &'a [EventField],
915        }
916
917        #[derive(Serialize)]
918        struct EnumRef<'a> {
919            name: &'a str,
920            kind: &'static str,
921            variants: &'a [EventField],
922        }
923
924        match self {
925            Self::Struct(inner) => StructRef::serialize(
926                &StructRef {
927                    name: &inner.name,
928                    kind: "struct",
929                    members: &inner.members,
930                },
931                serializer,
932            ),
933            Self::Enum(inner) => EnumRef::serialize(
934                &EnumRef {
935                    name: &inner.name,
936                    kind: "enum",
937                    variants: &inner.variants,
938                },
939                serializer,
940            ),
941        }
942    }
943}
944
945impl Serialize for IntOrList {
946    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
947    where
948        S: Serializer,
949    {
950        match self {
951            Self::Int(int) => serializer.serialize_u64(*int),
952            Self::List(list) => {
953                let mut seq = serializer.serialize_seq(Some(list.len()))?;
954                for item in list {
955                    seq.serialize_element(item)?;
956                }
957                seq.end()
958            }
959        }
960    }
961}
962
963impl<'de> Visitor<'de> for IntOrListVisitor {
964    type Value = IntOrList;
965
966    fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
967        write!(formatter, "number or list")
968    }
969
970    fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
971    where
972        E: serde::de::Error,
973    {
974        Ok(IntOrList::Int(v))
975    }
976
977    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
978    where
979        A: serde::de::SeqAccess<'de>,
980    {
981        let mut items = Vec::new();
982        while let Some(element) = seq.next_element::<IntOrList>()? {
983            items.push(element);
984        }
985        Ok(IntOrList::List(items))
986    }
987}
988
989impl<'de> Deserialize<'de> for IntOrList {
990    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
991    where
992        D: Deserializer<'de>,
993    {
994        deserializer.deserialize_any(IntOrListVisitor)
995    }
996}
997
998fn hash_sierra_entrypoints(entrypoints: &[SierraEntryPoint]) -> Felt {
999    let mut hasher = PoseidonHasher::new();
1000
1001    for entry in entrypoints {
1002        hasher.update(entry.selector);
1003        hasher.update(entry.function_idx.into());
1004    }
1005
1006    hasher.finalize()
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011    use super::*;
1012
1013    #[derive(serde::Deserialize)]
1014    struct ContractHashes {
1015        sierra_class_hash: String,
1016        compiled_class_hash: String,
1017    }
1018
1019    #[test]
1020    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1021    fn test_sierra_class_deser() {
1022        for raw_artifact in [
1023            include_str!("../../../test-data/contracts/cairo1/artifacts/abi_types_sierra.txt"),
1024            include_str!("../../../test-data/contracts/cairo1/artifacts/erc20_sierra.txt"),
1025            include_str!("../../../test-data/contracts/cairo2/artifacts/abi_types_sierra.txt"),
1026            include_str!("../../../test-data/contracts/cairo2/artifacts/erc20_sierra.txt"),
1027            include_str!("../../../test-data/contracts/cairo2.6/artifacts/erc20_sierra.txt"),
1028            include_str!("../../../test-data/contracts/cairo2.6/artifacts/trivial_sierra.txt"),
1029        ] {
1030            let direct_deser = serde_json::from_str::<SierraClass>(raw_artifact).unwrap();
1031            let via_contract_artifact = match serde_json::from_str::<ContractArtifact>(raw_artifact)
1032            {
1033                Ok(ContractArtifact::SierraClass(class)) => class,
1034                _ => panic!("Unexpected result"),
1035            };
1036
1037            // Class should be identical however it's deserialized
1038            assert_eq!(
1039                direct_deser.class_hash().unwrap(),
1040                via_contract_artifact.class_hash().unwrap()
1041            );
1042        }
1043    }
1044
1045    #[test]
1046    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1047    fn test_compiled_class_deser() {
1048        for raw_artifact in [
1049            include_str!("../../../test-data/contracts/cairo1/artifacts/abi_types_compiled.txt"),
1050            include_str!("../../../test-data/contracts/cairo1/artifacts/erc20_compiled.txt"),
1051            include_str!("../../../test-data/contracts/cairo2/artifacts/abi_types_compiled.txt"),
1052            include_str!("../../../test-data/contracts/cairo2/artifacts/erc20_compiled.txt"),
1053            include_str!("../../../test-data/contracts/cairo2.6/artifacts/erc20_compiled.txt"),
1054            include_str!("../../../test-data/contracts/cairo2.6/artifacts/trivial_compiled.txt"),
1055        ] {
1056            let direct_deser = serde_json::from_str::<CompiledClass>(raw_artifact).unwrap();
1057            let via_contract_artifact = match serde_json::from_str::<ContractArtifact>(raw_artifact)
1058            {
1059                Ok(ContractArtifact::CompiledClass(class)) => class,
1060                _ => panic!("Unexpected result"),
1061            };
1062
1063            // Class should be identical however it's deserialized
1064            assert_eq!(
1065                direct_deser.class_hash().unwrap(),
1066                via_contract_artifact.class_hash().unwrap()
1067            );
1068        }
1069    }
1070
1071    #[test]
1072    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1073    fn test_legacy_class_deser() {
1074        match serde_json::from_str::<ContractArtifact>(include_str!(
1075            "../../../test-data/contracts/cairo0/artifacts/oz_account.txt"
1076        )) {
1077            Ok(ContractArtifact::LegacyClass(_)) => {}
1078            _ => panic!("Unexpected result"),
1079        }
1080    }
1081
1082    #[test]
1083    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1084    fn test_sierra_class_hash() {
1085        for (raw_artifact, raw_hashes) in [
1086            (
1087                include_str!("../../../test-data/contracts/cairo1/artifacts/erc20_sierra.txt"),
1088                include_str!("../../../test-data/contracts/cairo1/artifacts/erc20.hashes.json"),
1089            ),
1090            (
1091                include_str!("../../../test-data/contracts/cairo1/artifacts/abi_types_sierra.txt"),
1092                include_str!("../../../test-data/contracts/cairo1/artifacts/abi_types.hashes.json"),
1093            ),
1094            (
1095                include_str!("../../../test-data/contracts/cairo2/artifacts/erc20_sierra.txt"),
1096                include_str!("../../../test-data/contracts/cairo2/artifacts/erc20.hashes.json"),
1097            ),
1098            (
1099                include_str!("../../../test-data/contracts/cairo2/artifacts/abi_types_sierra.txt"),
1100                include_str!("../../../test-data/contracts/cairo2/artifacts/abi_types.hashes.json"),
1101            ),
1102        ] {
1103            let sierra_class = serde_json::from_str::<SierraClass>(raw_artifact).unwrap();
1104            let computed_hash = sierra_class.class_hash().unwrap();
1105
1106            let hashes: ContractHashes = serde_json::from_str(raw_hashes).unwrap();
1107            let expected_hash = Felt::from_hex(&hashes.sierra_class_hash).unwrap();
1108
1109            assert_eq!(computed_hash, expected_hash);
1110        }
1111    }
1112
1113    #[test]
1114    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1115    fn test_compiled_class_hash() {
1116        for (raw_artifact, raw_hashes) in [
1117            (
1118                include_str!("../../../test-data/contracts/cairo1/artifacts/erc20_compiled.txt"),
1119                include_str!("../../../test-data/contracts/cairo1/artifacts/erc20.hashes.json"),
1120            ),
1121            (
1122                include_str!(
1123                    "../../../test-data/contracts/cairo1/artifacts/abi_types_compiled.txt"
1124                ),
1125                include_str!("../../../test-data/contracts/cairo1/artifacts/abi_types.hashes.json"),
1126            ),
1127            (
1128                include_str!("../../../test-data/contracts/cairo2/artifacts/erc20_compiled.txt"),
1129                include_str!("../../../test-data/contracts/cairo2/artifacts/erc20.hashes.json"),
1130            ),
1131            (
1132                include_str!(
1133                    "../../../test-data/contracts/cairo2/artifacts/abi_types_compiled.txt"
1134                ),
1135                include_str!("../../../test-data/contracts/cairo2/artifacts/abi_types.hashes.json"),
1136            ),
1137            (
1138                include_str!("../../../test-data/contracts/cairo2.6/artifacts/erc20_compiled.txt"),
1139                include_str!("../../../test-data/contracts/cairo2.6/artifacts/erc20.hashes.json"),
1140            ),
1141            (
1142                include_str!(
1143                    "../../../test-data/contracts/cairo2.6/artifacts/trivial_compiled.txt"
1144                ),
1145                include_str!("../../../test-data/contracts/cairo2.6/artifacts/trivial.hashes.json"),
1146            ),
1147        ] {
1148            let compiled_class = serde_json::from_str::<CompiledClass>(raw_artifact).unwrap();
1149            let computed_hash = compiled_class.class_hash().unwrap();
1150
1151            let hashes: ContractHashes = serde_json::from_str(raw_hashes).unwrap();
1152            let expected_hash = Felt::from_hex(&hashes.compiled_class_hash).unwrap();
1153
1154            assert_eq!(computed_hash, expected_hash);
1155        }
1156    }
1157}