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::{CostTokenMap, 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_sierra_type_size::ProgramRegistryInfo;
30use cairo_lang_utils::bigint::{BigUintAsHex, deserialize_big_uint, serialize_big_uint};
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, CostTokenMap::from_iter([(CostTokenType::Const, ENTRY_POINT_COST)])))
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 program_info = ProgramRegistryInfo::new(&program).map_err(|err| {
432 StarknetSierraCompilationError::CompilationError(Box::new(
433 CompilationError::ProgramRegistryError(err),
434 ))
435 })?;
436 let metadata = calc_metadata(&program, &program_info, metadata_computation_config)?;
437 let cairo_program = cairo_lang_sierra_to_casm::compiler::compile(
438 &program,
439 &program_info,
440 &metadata,
441 SierraToCasmConfig { gas_usage_check: true, max_bytecode_size },
442 )?;
443
444 let AssembledCairoProgram { bytecode, hints } = cairo_program.assemble();
445 let bytecode = bytecode
446 .iter()
447 .map(|big_int| {
448 let (_q, reminder) = big_int.magnitude().div_rem(&prime);
449 BigUintAsHex {
450 value: if big_int.is_negative() { &prime - reminder } else { reminder },
451 }
452 })
453 .collect_vec();
454
455 let bytecode_segment_lengths =
456 if sierra_version.minor >= CONTRACT_SEGMENTATION_MINOR_VERSION {
457 Some(compute_bytecode_segment_lengths(&program, &cairo_program, bytecode.len())?)
458 } else {
459 None
460 };
461
462 let builtin_types = UnorderedHashSet::<GenericTypeId>::from_iter([
463 RangeCheckType::id(),
464 BitwiseType::id(),
465 PedersenType::id(),
466 EcOpType::id(),
467 PoseidonType::id(),
468 SegmentArenaType::id(),
469 GasBuiltinType::id(),
470 SystemType::id(),
471 RangeCheck96Type::id(),
472 AddModType::id(),
473 MulModType::id(),
474 ]);
475
476 let as_casm_entry_point = |contract_entry_point: ContractEntryPoint| {
477 let Some(function) = program.funcs.get(contract_entry_point.function_idx) else {
478 return Err(StarknetSierraCompilationError::EntryPointError);
479 };
480 let statement_id = function.entry_point;
481
482 let (panic_result, output_builtins) = function
484 .signature
485 .ret_types
486 .split_last()
487 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignature)?;
488 let (builtins, [gas_ty, system_ty]) = output_builtins
489 .split_last_chunk()
490 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignature)?;
491 let (input_span, input_builtins) = function
492 .signature
493 .param_types
494 .split_last()
495 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignatureMissingArgs)?;
496 require(input_builtins == output_builtins)
497 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignature)?;
498
499 let type_resolver = TypeResolver { type_decl: &program.type_declarations };
500 require(type_resolver.is_felt252_span(input_span))
501 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignature)?;
502
503 require(type_resolver.is_valid_entry_point_return_type(panic_result))
504 .ok_or(StarknetSierraCompilationError::InvalidEntryPointSignature)?;
505
506 for type_id in input_builtins {
507 if !builtin_types.contains(type_resolver.get_generic_id(type_id)) {
508 return Err(StarknetSierraCompilationError::InvalidBuiltinType(
509 type_id.clone(),
510 ));
511 }
512 }
513
514 if *type_resolver.get_generic_id(system_ty) != SystemType::id()
516 || *type_resolver.get_generic_id(gas_ty) != GasBuiltinType::id()
517 {
518 return Err(
519 StarknetSierraCompilationError::InvalidEntryPointSignatureWrongBuiltinsOrder,
520 );
521 }
522
523 let builtins = builtins
524 .iter()
525 .map(|type_id| match type_resolver.get_generic_id(type_id).0.as_str() {
526 "RangeCheck96" => "range_check96".to_string(),
527 name => name.to_case(Case::Snake),
528 })
529 .collect_vec();
530
531 let code_offset = cairo_program
532 .debug_info
533 .sierra_statement_info
534 .get(statement_id.0)
535 .ok_or(StarknetSierraCompilationError::EntryPointError)?
536 .start_offset;
537 assert_eq!(
538 metadata.gas_info.function_costs[&function.id],
539 CostTokenMap::from_iter([(CostTokenType::Const, ENTRY_POINT_COST as i64)]),
540 "Unexpected entry point cost."
541 );
542 Ok::<CasmContractEntryPoint, StarknetSierraCompilationError>(CasmContractEntryPoint {
543 selector: contract_entry_point.selector,
544 offset: code_offset,
545 builtins,
546 })
547 };
548
549 let as_casm_entry_points = |contract_entry_points: Vec<ContractEntryPoint>| {
550 let mut entry_points = vec![];
551 for contract_entry_point in contract_entry_points {
552 entry_points.push(as_casm_entry_point(contract_entry_point)?);
553 }
554 Ok::<Vec<CasmContractEntryPoint>, StarknetSierraCompilationError>(entry_points)
555 };
556
557 let pythonic_hints = if add_pythonic_hints {
558 Some(
559 hints
560 .iter()
561 .map(|(pc, hints)| {
562 (*pc, hints.iter().map(|hint| hint.get_pythonic_hint()).collect_vec())
563 })
564 .collect_vec(),
565 )
566 } else {
567 None
568 };
569
570 let compiler_version = current_compiler_version_id().to_string();
571 let casm_contract_class = Self {
572 prime,
573 compiler_version,
574 bytecode,
575 bytecode_segment_lengths,
576 hints,
577 pythonic_hints,
578 entry_points_by_type: CasmContractEntryPoints {
579 external: as_casm_entry_points(contract_class.entry_points_by_type.external)?,
580 l1_handler: as_casm_entry_points(contract_class.entry_points_by_type.l1_handler)?,
581 constructor: as_casm_entry_points(contract_class.entry_points_by_type.constructor)?,
582 },
583 };
584
585 Ok((casm_contract_class, cairo_program.debug_info))
586 }
587}
588
589#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
590pub struct CasmContractEntryPoint {
591 #[serde(serialize_with = "serialize_big_uint", deserialize_with = "deserialize_big_uint")]
593 pub selector: BigUint,
594 pub offset: usize,
596 pub builtins: Vec<String>,
598}
599
600#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
601pub struct CasmContractEntryPoints {
602 #[serde(rename = "EXTERNAL")]
603 pub external: Vec<CasmContractEntryPoint>,
604 #[serde(rename = "L1_HANDLER")]
605 pub l1_handler: Vec<CasmContractEntryPoint>,
606 #[serde(rename = "CONSTRUCTOR")]
607 pub constructor: Vec<CasmContractEntryPoint>,
608}