1use std::cmp::Ordering;
2use std::sync::LazyLock;
3
4use cairo_lang_casm::assembler::AssembledCairoProgram;
5use cairo_lang_casm::hints::{Hint, PythonicHint};
6use cairo_lang_sierra::extensions::NamedType;
7use cairo_lang_sierra::extensions::array::ArrayType;
8use cairo_lang_sierra::extensions::bitwise::BitwiseType;
9use cairo_lang_sierra::extensions::circuit::{AddModType, MulModType};
10use cairo_lang_sierra::extensions::ec::EcOpType;
11use cairo_lang_sierra::extensions::enm::EnumType;
12use cairo_lang_sierra::extensions::felt252::Felt252Type;
13use cairo_lang_sierra::extensions::gas::{CostTokenType, GasBuiltinType};
14use cairo_lang_sierra::extensions::pedersen::PedersenType;
15use cairo_lang_sierra::extensions::poseidon::PoseidonType;
16use cairo_lang_sierra::extensions::range_check::{RangeCheck96Type, RangeCheckType};
17use cairo_lang_sierra::extensions::segment_arena::SegmentArenaType;
18use cairo_lang_sierra::extensions::snapshot::SnapshotType;
19use cairo_lang_sierra::extensions::starknet::syscalls::SystemType;
20use cairo_lang_sierra::extensions::structure::StructType;
21use cairo_lang_sierra::ids::{ConcreteTypeId, GenericTypeId};
22use cairo_lang_sierra::program::{ConcreteTypeLongId, GenericArg, TypeDeclaration};
23use cairo_lang_sierra_to_casm::compiler::{
24 CairoProgramDebugInfo, CompilationError, SierraToCasmConfig,
25};
26use cairo_lang_sierra_to_casm::metadata::{
27 MetadataComputationConfig, MetadataError, calc_metadata,
28};
29use cairo_lang_utils::bigint::{BigUintAsHex, deserialize_big_uint, serialize_big_uint};
30use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
31use cairo_lang_utils::require;
32use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
33use cairo_lang_utils::unordered_hash_set::UnorderedHashSet;
34use convert_case::{Case, Casing};
35use itertools::{Itertools, chain};
36use num_bigint::BigUint;
37use num_integer::Integer;
38use num_traits::Signed;
39use serde::{Deserialize, Serialize};
40use starknet_types_core::felt::Felt as Felt252;
41use starknet_types_core::hash::{Poseidon, StarkHash};
42use thiserror::Error;
43
44use crate::allowed_libfuncs::AllowedLibfuncsError;
45use crate::compiler_version::{
46 CONTRACT_SEGMENTATION_MINOR_VERSION, VersionId, current_compiler_version_id,
47 current_sierra_version_id,
48};
49use crate::contract_class::{ContractClass, ContractEntryPoint};
50use crate::contract_segmentation::{
51 NestedIntList, SegmentationError, compute_bytecode_segment_lengths,
52};
53use crate::felt252_serde::{Felt252SerdeError, sierra_from_felt252s};
54use crate::keccak::starknet_keccak;
55
56#[cfg(test)]
57#[path = "casm_contract_class_test.rs"]
58mod test;
59
60pub const ENTRY_POINT_COST: i32 = 10000;
62
63static CONSTRUCTOR_ENTRY_POINT_SELECTOR: LazyLock<BigUint> =
64 LazyLock::new(|| starknet_keccak(b"constructor"));
65
66#[derive(Error, Debug, Eq, PartialEq)]
67pub enum StarknetSierraCompilationError {
68 #[error(transparent)]
69 CompilationError(#[from] Box<CompilationError>),
70 #[error(transparent)]
71 Felt252SerdeError(#[from] Felt252SerdeError),
72 #[error(transparent)]
73 MetadataError(#[from] MetadataError),
74 #[error(transparent)]
75 AllowedLibfuncsError(#[from] AllowedLibfuncsError),
76 #[error(transparent)]
77 SegmentationError(#[from] SegmentationError),
78 #[error("Invalid entry point.")]
79 EntryPointError,
80 #[error("Missing arguments in the entry point.")]
81 InvalidEntryPointSignatureMissingArgs,
82 #[error("Invalid entry point signature.")]
83 InvalidEntryPointSignature,
84 #[error("Invalid constructor entry point.")]
85 InvalidConstructorEntryPoint,
86 #[error("{0} is not a supported builtin type.")]
87 InvalidBuiltinType(ConcreteTypeId),
88 #[error("Invalid entry point signature - builtins are not in the expected order.")]
89 InvalidEntryPointSignatureWrongBuiltinsOrder,
90 #[error("Entry points not sorted by selectors.")]
91 EntryPointsOutOfOrder,
92 #[error("Duplicate entry point selector {selector}.")]
93 DuplicateEntryPointSelector { selector: BigUint },
94 #[error("Duplicate entry point function index {index}.")]
95 DuplicateEntryPointSierraFunction { index: usize },
96 #[error("Out of range value in serialization.")]
97 ValueOutOfRange,
98 #[error(
99 "Cannot compile Sierra version {version_in_contract} with the current compiler (sierra \
100 version: {version_of_compiler})"
101 )]
102 UnsupportedSierraVersion { version_in_contract: VersionId, version_of_compiler: VersionId },
103}
104
105fn skip_if_none<T>(opt_field: &Option<T>) -> bool {
106 opt_field.is_none()
107}
108
109#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
111pub struct CasmContractClass {
112 #[serde(serialize_with = "serialize_big_uint", deserialize_with = "deserialize_big_uint")]
113 pub prime: BigUint,
114 pub compiler_version: String,
115 pub bytecode: Vec<BigUintAsHex>,
116 #[serde(skip_serializing_if = "skip_if_none")]
117 pub bytecode_segment_lengths: Option<NestedIntList>,
118 pub hints: Vec<(usize, Vec<Hint>)>,
119
120 #[serde(skip_serializing_if = "skip_if_none")]
122 pub pythonic_hints: Option<Vec<(usize, Vec<String>)>>,
123 pub entry_points_by_type: CasmContractEntryPoints,
124}
125impl CasmContractClass {
126 pub fn compiled_class_hash(&self) -> Felt252 {
128 self.compiled_class_hash_inner::<Poseidon>()
129 }
130
131 pub fn get_bytecode_segment_lengths(&self) -> NestedIntList {
135 self.bytecode_segment_lengths.clone().unwrap_or(NestedIntList::Leaf(self.bytecode.len()))
136 }
137
138 fn entry_points_hash<H: StarkHash>(&self, entry_points: &[CasmContractEntryPoint]) -> Felt252 {
140 let mut entry_point_hash_elements = vec![];
141 for entry_point in entry_points {
142 entry_point_hash_elements.push(Felt252::from(&entry_point.selector));
143 entry_point_hash_elements.push(Felt252::from(entry_point.offset));
144 entry_point_hash_elements.push(H::hash_array(
145 &entry_point
146 .builtins
147 .iter()
148 .map(|builtin| Felt252::from_bytes_be_slice(builtin.as_bytes()))
149 .collect_vec(),
150 ));
151 }
152 H::hash_array(&entry_point_hash_elements)
153 }
154
155 fn compute_bytecode_hash<H: StarkHash>(&self) -> Felt252 {
157 let mut bytecode_iter = self.bytecode.iter().map(|big_uint| Felt252::from(&big_uint.value));
158
159 let (len, bytecode_hash) =
160 bytecode_hash_node::<H>(&mut bytecode_iter, &self.get_bytecode_segment_lengths());
161 assert_eq!(len, self.bytecode.len());
162
163 bytecode_hash
164 }
165
166 fn compiled_class_hash_inner<H: StarkHash>(&self) -> Felt252 {
170 let external_funcs_hash = self.entry_points_hash::<H>(&self.entry_points_by_type.external);
172 let l1_handlers_hash = self.entry_points_hash::<H>(&self.entry_points_by_type.l1_handler);
173 let constructors_hash = self.entry_points_hash::<H>(&self.entry_points_by_type.constructor);
174 let bytecode_hash = self.compute_bytecode_hash::<H>();
175
176 H::hash_array(&[
178 Felt252::from_bytes_be_slice(b"COMPILED_CLASS_V1"),
179 external_funcs_hash,
180 l1_handlers_hash,
181 constructors_hash,
182 bytecode_hash,
183 ])
184 }
185}
186
187fn bytecode_hash_node<H: StarkHash>(
192 iter: &mut impl Iterator<Item = Felt252>,
193 node: &NestedIntList,
194) -> (usize, Felt252) {
195 match node {
196 NestedIntList::Leaf(len) => {
197 let data = &iter.take(*len).collect_vec();
198 assert_eq!(data.len(), *len);
199 (*len, H::hash_array(data))
200 }
201 NestedIntList::Node(nodes) => {
202 let inner_nodes =
204 nodes.iter().map(|node| bytecode_hash_node::<H>(iter, node)).collect_vec();
205 let hash = H::hash_array(
206 &inner_nodes.iter().flat_map(|(len, hash)| [(*len).into(), *hash]).collect_vec(),
207 ) + 1;
208 (inner_nodes.iter().map(|(len, _)| len).sum(), hash)
209 }
210 }
211}
212
213pub struct TypeResolver<'a> {
215 type_decl: &'a [TypeDeclaration],
216}
217
218impl TypeResolver<'_> {
219 fn get_long_id(&self, type_id: &ConcreteTypeId) -> &ConcreteTypeLongId {
220 &self.type_decl[type_id.id as usize].long_id
221 }
222
223 fn get_generic_id(&self, type_id: &ConcreteTypeId) -> &GenericTypeId {
224 &self.get_long_id(type_id).generic_id
225 }
226
227 fn is_felt252_array_snapshot(&self, ty: &ConcreteTypeId) -> bool {
228 let long_id = self.get_long_id(ty);
229 if long_id.generic_id != SnapshotType::id() {
230 return false;
231 }
232
233 let [GenericArg::Type(inner_ty)] = long_id.generic_args.as_slice() else {
234 return false;
235 };
236
237 self.is_felt252_array(inner_ty)
238 }
239
240 fn is_felt252_array(&self, ty: &ConcreteTypeId) -> bool {
241 let long_id = self.get_long_id(ty);
242 if long_id.generic_id != ArrayType::id() {
243 return false;
244 }
245
246 let [GenericArg::Type(element_ty)] = long_id.generic_args.as_slice() else {
247 return false;
248 };
249
250 *self.get_generic_id(element_ty) == Felt252Type::id()
251 }
252
253 fn is_felt252_span(&self, ty: &ConcreteTypeId) -> bool {
254 let long_id = self.get_long_id(ty);
255 if long_id.generic_id != StructType::ID {
256 return false;
257 }
258
259 let [GenericArg::UserType(_), GenericArg::Type(element_ty)] =
260 long_id.generic_args.as_slice()
261 else {
262 return false;
263 };
264
265 self.is_felt252_array_snapshot(element_ty)
266 }
267
268 fn is_valid_entry_point_return_type(&self, ty: &ConcreteTypeId) -> bool {
269 let Some((result_tuple_ty, err_ty)) = self.extract_result_ty(ty) else {
271 return false;
272 };
273
274 let Some(result_ty) = self.extract_struct1(result_tuple_ty) else {
276 return false;
277 };
278 if !self.is_felt252_span(result_ty) {
279 return false;
280 }
281
282 if self.is_felt252_array(err_ty) {
285 return true;
286 }
287
288 let Some((_panic_ty, err_data_ty)) = self.extract_struct2(err_ty) else {
290 return false;
291 };
292
293 self.is_felt252_array(err_data_ty)
295 }
296
297 fn extract_result_ty(&self, ty: &ConcreteTypeId) -> Option<(&ConcreteTypeId, &ConcreteTypeId)> {
299 let long_id = self.get_long_id(ty);
300 require(long_id.generic_id == EnumType::id())?;
301 let [GenericArg::UserType(_), GenericArg::Type(result_tuple_ty), GenericArg::Type(err_ty)] =
302 long_id.generic_args.as_slice()
303 else {
304 return None;
305 };
306 Some((result_tuple_ty, err_ty))
307 }
308
309 fn extract_struct1(&self, ty: &ConcreteTypeId) -> Option<&ConcreteTypeId> {
311 let long_id = self.get_long_id(ty);
312 require(long_id.generic_id == StructType::id())?;
313 let [GenericArg::UserType(_), GenericArg::Type(ty0)] = long_id.generic_args.as_slice()
314 else {
315 return None;
316 };
317 Some(ty0)
318 }
319
320 fn extract_struct2(&self, ty: &ConcreteTypeId) -> Option<(&ConcreteTypeId, &ConcreteTypeId)> {
322 let long_id = self.get_long_id(ty);
323 require(long_id.generic_id == StructType::id())?;
324 let [GenericArg::UserType(_), GenericArg::Type(ty0), GenericArg::Type(ty1)] =
325 long_id.generic_args.as_slice()
326 else {
327 return None;
328 };
329 Some((ty0, ty1))
330 }
331}
332
333impl CasmContractClass {
334 pub fn from_contract_class(
335 contract_class: ContractClass,
336 add_pythonic_hints: bool,
337 max_bytecode_size: usize,
338 ) -> Result<Self, StarknetSierraCompilationError> {
339 Ok(Self::from_contract_class_with_debug_info(
340 contract_class,
341 add_pythonic_hints,
342 max_bytecode_size,
343 )?
344 .0)
345 }
346
347 pub fn from_contract_class_with_debug_info(
348 contract_class: ContractClass,
349 add_pythonic_hints: bool,
350 max_bytecode_size: usize,
351 ) -> Result<(Self, CairoProgramDebugInfo), StarknetSierraCompilationError> {
352 let prime = Felt252::prime();
353 for felt252 in &contract_class.sierra_program {
354 if felt252.value >= prime {
355 return Err(StarknetSierraCompilationError::ValueOutOfRange);
356 }
357 }
358
359 let (sierra_version, _, program) = sierra_from_felt252s(&contract_class.sierra_program)?;
360 let current_sierra_version = current_sierra_version_id();
361 if !(sierra_version.major == current_sierra_version.major
362 && sierra_version.minor <= current_sierra_version.minor)
363 {
364 return Err(StarknetSierraCompilationError::UnsupportedSierraVersion {
365 version_in_contract: sierra_version,
366 version_of_compiler: current_sierra_version,
367 });
368 }
369
370 match &contract_class.entry_points_by_type.constructor.as_slice() {
371 [] => {}
372 [ContractEntryPoint { selector, .. }]
373 if selector == &*CONSTRUCTOR_ENTRY_POINT_SELECTOR => {}
374 _ => {
375 return Err(StarknetSierraCompilationError::InvalidConstructorEntryPoint);
376 }
377 };
378
379 for entry_points in [
380 &contract_class.entry_points_by_type.constructor,
381 &contract_class.entry_points_by_type.external,
382 &contract_class.entry_points_by_type.l1_handler,
383 ] {
384 for (prev, next) in entry_points.iter().tuple_windows() {
385 match prev.selector.cmp(&next.selector) {
386 Ordering::Less => {}
387 Ordering::Equal => {
388 return Err(StarknetSierraCompilationError::DuplicateEntryPointSelector {
389 selector: prev.selector.clone(),
390 });
391 }
392 Ordering::Greater => {
393 return Err(StarknetSierraCompilationError::EntryPointsOutOfOrder);
394 }
395 }
396 }
397 }
398
399 let entrypoint_function_indices = chain!(
400 &contract_class.entry_points_by_type.constructor,
401 &contract_class.entry_points_by_type.external,
402 &contract_class.entry_points_by_type.l1_handler,
403 )
404 .map(|entrypoint| entrypoint.function_idx);
405 let mut function_idx_usages = UnorderedHashMap::<usize, usize>::default();
407 for index in entrypoint_function_indices.clone() {
408 let usages = function_idx_usages.entry(index).or_default();
409 *usages += 1;
410 const MAX_SIERRA_FUNCTION_USAGES: usize = 2;
411 if *usages > MAX_SIERRA_FUNCTION_USAGES {
412 return Err(StarknetSierraCompilationError::DuplicateEntryPointSierraFunction {
413 index,
414 });
415 }
416 }
417 let entrypoint_ids = entrypoint_function_indices.map(|idx| program.funcs[idx].id.clone());
418 assert_eq!(sierra_version.major, 1);
421 let no_eq_solver = sierra_version.minor >= 4;
422 let metadata_computation_config = MetadataComputationConfig {
423 function_set_costs: entrypoint_ids
424 .map(|id| (id, [(CostTokenType::Const, ENTRY_POINT_COST)].into()))
425 .collect(),
426 linear_gas_solver: no_eq_solver,
427 linear_ap_change_solver: no_eq_solver,
428 skip_non_linear_solver_comparisons: false,
429 compute_runtime_costs: false,
430 };
431 let metadata = calc_metadata(&program, metadata_computation_config)?;
432
433 let cairo_program = cairo_lang_sierra_to_casm::compiler::compile(
434 &program,
435 &metadata,
436 SierraToCasmConfig { gas_usage_check: true, max_bytecode_size },
437 )?;
438
439 let AssembledCairoProgram { bytecode, hints } = cairo_program.assemble();
440 let bytecode = bytecode
441 .iter()
442 .map(|big_int| {
443 let (_q, reminder) = big_int.magnitude().div_rem(&prime);
444 BigUintAsHex {
445 value: if big_int.is_negative() { &prime - reminder } else { reminder },
446 }
447 })
448 .collect_vec();
449
450 let bytecode_segment_lengths =
451 if sierra_version.minor >= CONTRACT_SEGMENTATION_MINOR_VERSION {
452 Some(compute_bytecode_segment_lengths(&program, &cairo_program, bytecode.len())?)
453 } else {
454 None
455 };
456
457 let builtin_types = UnorderedHashSet::<GenericTypeId>::from_iter([
458 RangeCheckType::id(),
459 BitwiseType::id(),
460 PedersenType::id(),
461 EcOpType::id(),
462 PoseidonType::id(),
463 SegmentArenaType::id(),
464 GasBuiltinType::id(),
465 SystemType::id(),
466 RangeCheck96Type::id(),
467 AddModType::id(),
468 MulModType::id(),
469 ]);
470
471 let as_casm_entry_point = |contract_entry_point: ContractEntryPoint| {
472 let Some(function) = program.funcs.get(contract_entry_point.function_idx) else {
473 return Err(StarknetSierraCompilationError::EntryPointError);
474 };
475 let statement_id = function.entry_point;
476
477 let (panic_result, output_builtins) = function
479 .signature
480 .ret_types
481 .split_last()
482 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignature)?;
483 let (builtins, [gas_ty, system_ty]) = output_builtins
484 .split_last_chunk()
485 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignature)?;
486 let (input_span, input_builtins) = function
487 .signature
488 .param_types
489 .split_last()
490 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignatureMissingArgs)?;
491 require(input_builtins == output_builtins)
492 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignature)?;
493
494 let type_resolver = TypeResolver { type_decl: &program.type_declarations };
495 require(type_resolver.is_felt252_span(input_span))
496 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignature)?;
497
498 require(type_resolver.is_valid_entry_point_return_type(panic_result))
499 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignature)?;
500
501 for type_id in input_builtins {
502 if !builtin_types.contains(type_resolver.get_generic_id(type_id)) {
503 return Err(StarknetSierraCompilationError::InvalidBuiltinType(
504 type_id.clone(),
505 ));
506 }
507 }
508
509 if *type_resolver.get_generic_id(system_ty) != SystemType::id()
511 || *type_resolver.get_generic_id(gas_ty) != GasBuiltinType::id()
512 {
513 return Err(
514 StarknetSierraCompilationError::InvalidEntryPointSignatureWrongBuiltinsOrder,
515 );
516 }
517
518 let builtins = builtins
519 .iter()
520 .map(|type_id| match type_resolver.get_generic_id(type_id).0.as_str() {
521 "RangeCheck96" => "range_check96".to_string(),
522 name => name.to_case(Case::Snake),
523 })
524 .collect_vec();
525
526 let code_offset = cairo_program
527 .debug_info
528 .sierra_statement_info
529 .get(statement_id.0)
530 .ok_or(StarknetSierraCompilationError::EntryPointError)?
531 .start_offset;
532 assert_eq!(
533 metadata.gas_info.function_costs[&function.id],
534 OrderedHashMap::from_iter([(CostTokenType::Const, ENTRY_POINT_COST as i64)]),
535 "Unexpected entry point cost."
536 );
537 Ok::<CasmContractEntryPoint, StarknetSierraCompilationError>(CasmContractEntryPoint {
538 selector: contract_entry_point.selector,
539 offset: code_offset,
540 builtins,
541 })
542 };
543
544 let as_casm_entry_points = |contract_entry_points: Vec<ContractEntryPoint>| {
545 let mut entry_points = vec![];
546 for contract_entry_point in contract_entry_points {
547 entry_points.push(as_casm_entry_point(contract_entry_point)?);
548 }
549 Ok::<Vec<CasmContractEntryPoint>, StarknetSierraCompilationError>(entry_points)
550 };
551
552 let pythonic_hints = if add_pythonic_hints {
553 Some(
554 hints
555 .iter()
556 .map(|(pc, hints)| {
557 (*pc, hints.iter().map(|hint| hint.get_pythonic_hint()).collect_vec())
558 })
559 .collect_vec(),
560 )
561 } else {
562 None
563 };
564
565 let compiler_version = current_compiler_version_id().to_string();
566 let casm_contract_class = Self {
567 prime,
568 compiler_version,
569 bytecode,
570 bytecode_segment_lengths,
571 hints,
572 pythonic_hints,
573 entry_points_by_type: CasmContractEntryPoints {
574 external: as_casm_entry_points(contract_class.entry_points_by_type.external)?,
575 l1_handler: as_casm_entry_points(contract_class.entry_points_by_type.l1_handler)?,
576 constructor: as_casm_entry_points(contract_class.entry_points_by_type.constructor)?,
577 },
578 };
579
580 Ok((casm_contract_class, cairo_program.debug_info))
581 }
582}
583
584#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
585pub struct CasmContractEntryPoint {
586 #[serde(serialize_with = "serialize_big_uint", deserialize_with = "deserialize_big_uint")]
588 pub selector: BigUint,
589 pub offset: usize,
591 pub builtins: Vec<String>,
593}
594
595#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
596pub struct CasmContractEntryPoints {
597 #[serde(rename = "EXTERNAL")]
598 pub external: Vec<CasmContractEntryPoint>,
599 #[serde(rename = "L1_HANDLER")]
600 pub l1_handler: Vec<CasmContractEntryPoint>,
601 #[serde(rename = "CONSTRUCTOR")]
602 pub constructor: Vec<CasmContractEntryPoint>,
603}