Skip to main content

cairo_lang_runner/
lib.rs

1//! Basic runner for running a Sierra program on the VM.
2use std::collections::HashMap;
3
4use cairo_lang_casm::hints::Hint;
5use cairo_lang_runnable_utils::builder::{BuildError, EntryCodeConfig, RunnableBuilder};
6use cairo_lang_sierra::extensions::NamedType;
7use cairo_lang_sierra::extensions::enm::EnumType;
8use cairo_lang_sierra::extensions::gas::{CostTokenType, GasBuiltinType};
9use cairo_lang_sierra::ids::{ConcreteTypeId, GenericTypeId};
10use cairo_lang_sierra::program::{Function, GenericArg};
11use cairo_lang_sierra_to_casm::metadata::MetadataComputationConfig;
12use cairo_lang_starknet::contract::ContractInfo;
13use cairo_lang_utils::casts::IntoOrPanic;
14use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
15use cairo_lang_utils::{extract_matches, require};
16use cairo_vm::hint_processor::hint_processor_definition::HintProcessor;
17use cairo_vm::serde::deserialize_program::HintParams;
18use cairo_vm::types::builtin_name::BuiltinName;
19use cairo_vm::vm::errors::cairo_run_errors::CairoRunError;
20use cairo_vm::vm::runners::cairo_runner::{ExecutionResources, RunResources};
21use cairo_vm::vm::vm_core::VirtualMachine;
22use casm_run::hint_to_hint_params;
23pub use casm_run::{CairoHintProcessor, StarknetState};
24use num_bigint::BigInt;
25use num_traits::ToPrimitive;
26use profiling::ProfilingInfo;
27use starknet_types_core::felt::Felt as Felt252;
28use thiserror::Error;
29
30use crate::casm_run::{RunFunctionResult, StarknetHintProcessor};
31use crate::profiling::ProfilerConfig;
32
33pub mod casm_run;
34pub mod clap;
35pub mod profiling;
36pub mod short_string;
37
38const MAX_STACK_TRACE_DEPTH_DEFAULT: usize = 100;
39
40#[derive(Debug, Error)]
41pub enum RunnerError {
42    #[error(transparent)]
43    BuildError(#[from] BuildError),
44    #[error("Not enough gas to call function.")]
45    NotEnoughGasToCall,
46    #[error("Function param {param_index} only partially contains argument {arg_index}.")]
47    ArgumentUnaligned { param_index: usize, arg_index: usize },
48    #[error("Function expects arguments of size {expected} and received {actual} instead.")]
49    ArgumentsSizeMismatch { expected: usize, actual: usize },
50    #[error(transparent)]
51    CairoRunError(#[from] Box<CairoRunError>),
52}
53
54/// The full result of a run with Starknet state.
55pub struct RunResultStarknet {
56    pub gas_counter: Option<Felt252>,
57    pub memory: Vec<Option<Felt252>>,
58    pub value: RunResultValue,
59    pub starknet_state: StarknetState,
60    pub used_resources: StarknetExecutionResources,
61    /// The profiling info of the run, if requested.
62    pub profiling_info: Option<ProfilingInfo>,
63}
64
65/// The full result of a run.
66#[derive(Debug, Eq, PartialEq, Clone)]
67pub struct RunResult {
68    pub gas_counter: Option<Felt252>,
69    pub memory: Vec<Option<Felt252>>,
70    pub value: RunResultValue,
71    pub used_resources: ExecutionResources,
72    /// The profiling info of the run, if requested.
73    pub profiling_info: Option<ProfilingInfo>,
74}
75
76/// The execution resources in a run.
77/// Extends [ExecutionResources] by including the used syscalls for Starknet.
78#[derive(Debug, Eq, PartialEq, Clone, Default)]
79pub struct StarknetExecutionResources {
80    /// The basic execution resources.
81    pub basic_resources: ExecutionResources,
82    /// The used syscalls.
83    pub syscalls: HashMap<String, usize>,
84}
85
86impl std::ops::AddAssign<StarknetExecutionResources> for StarknetExecutionResources {
87    /// Adds the resources of `other` to `self`.
88    fn add_assign(&mut self, other: Self) {
89        self.basic_resources += &other.basic_resources;
90        for (k, v) in other.syscalls {
91            *self.syscalls.entry(k).or_default() += v;
92        }
93    }
94}
95
96/// The ran function return value.
97#[derive(Debug, Eq, PartialEq, Clone)]
98pub enum RunResultValue {
99    /// Run ended successfully, returning the memory of the non-implicit returns.
100    Success(Vec<Felt252>),
101    /// Run panicked, returning the carried error data.
102    Panic(Vec<Felt252>),
103}
104
105/// Returns the approximated gas cost for each token type.
106pub fn token_gas_cost(token_type: CostTokenType) -> usize {
107    match token_type {
108        CostTokenType::Const => 1,
109        CostTokenType::Step
110        | CostTokenType::Hole
111        | CostTokenType::RangeCheck
112        | CostTokenType::RangeCheck96 => {
113            panic!("Token type {token_type:?} has no gas cost.")
114        }
115        CostTokenType::Pedersen => 4050,
116        CostTokenType::Poseidon => 491,
117        CostTokenType::Bitwise => 583,
118        CostTokenType::EcOp => 4085,
119        CostTokenType::AddMod => 230,
120        CostTokenType::MulMod => 604,
121        CostTokenType::Blake => 3334,
122    }
123}
124
125/// An argument to a Sierra function run.
126#[derive(Debug, Clone)]
127pub enum Arg {
128    Value(Felt252),
129    Array(Vec<Arg>),
130}
131impl Arg {
132    /// Returns the size of the argument in the VM.
133    pub fn size(&self) -> usize {
134        match self {
135            Self::Value(_) => 1,
136            Self::Array(_) => 2,
137        }
138    }
139}
140impl From<Felt252> for Arg {
141    fn from(value: Felt252) -> Self {
142        Self::Value(value)
143    }
144}
145
146/// Builds `hints_dict` required in `cairo_vm::types::program::Program` from instructions.
147pub fn build_hints_dict(
148    hints: &[(usize, Vec<Hint>)],
149) -> (HashMap<usize, Vec<HintParams>>, HashMap<String, Hint>) {
150    let mut hints_dict: HashMap<usize, Vec<HintParams>> = HashMap::new();
151    let mut string_to_hint: HashMap<String, Hint> = HashMap::new();
152
153    for (offset, offset_hints) in hints {
154        // Register hint with string for the hint processor.
155        for hint in offset_hints {
156            string_to_hint.insert(hint.representing_string(), hint.clone());
157        }
158        // Add hint, associated with the instruction offset.
159        hints_dict.insert(*offset, offset_hints.iter().map(hint_to_hint_params).collect());
160    }
161    (hints_dict, string_to_hint)
162}
163
164/// A struct representing a prepared execution context for starting a function within a given
165/// Starknet state.
166///
167/// Pass fields from this object to
168/// [`SierraCasmRunner::run_function_with_prepared_starknet_context`] to run the function.
169/// For typical use-cases you should use [`SierraCasmRunner::run_function_with_starknet_context`],
170/// which does all the preparation, running, and result composition for you.
171pub struct PreparedStarknetContext {
172    pub hints_dict: HashMap<usize, Vec<HintParams>>,
173    pub bytecode: Vec<BigInt>,
174    pub builtins: Vec<BuiltinName>,
175}
176
177/// Runner enabling running a Sierra program on the VM.
178pub struct SierraCasmRunner {
179    /// Builder for runnable functions.
180    builder: RunnableBuilder,
181    /// Mapping from class_hash to contract info.
182    starknet_contracts_info: OrderedHashMap<Felt252, ContractInfo>,
183    /// Whether to run the profiler when running using this runner.
184    run_profiler: Option<ProfilingInfoCollectionConfig>,
185}
186impl SierraCasmRunner {
187    pub fn new(
188        sierra_program: cairo_lang_sierra::program::Program,
189        metadata_config: Option<MetadataComputationConfig>,
190        starknet_contracts_info: OrderedHashMap<Felt252, ContractInfo>,
191        run_profiler: Option<ProfilingInfoCollectionConfig>,
192    ) -> Result<Self, RunnerError> {
193        Ok(Self {
194            builder: RunnableBuilder::new(sierra_program, metadata_config)?,
195            starknet_contracts_info,
196            run_profiler,
197        })
198    }
199
200    /// Runs the VM starting from a function in the context of a given Starknet state.
201    pub fn run_function_with_starknet_context(
202        &self,
203        func: &Function,
204        args: Vec<Arg>,
205        available_gas: Option<usize>,
206        starknet_state: StarknetState,
207    ) -> Result<RunResultStarknet, RunnerError> {
208        let (mut hint_processor, ctx) =
209            self.prepare_starknet_context(func, args, available_gas, starknet_state)?;
210        self.run_function_with_prepared_starknet_context(func, &mut hint_processor, ctx)
211    }
212
213    /// Runs the VM starting from a function in the context of a given Starknet state and a
214    /// (possibly) amended hint processor.
215    pub fn run_function_with_prepared_starknet_context(
216        &self,
217        func: &Function,
218        hint_processor: &mut dyn StarknetHintProcessor,
219        PreparedStarknetContext { hints_dict, bytecode, builtins }: PreparedStarknetContext,
220    ) -> Result<RunResultStarknet, RunnerError> {
221        let RunResult { gas_counter, memory, value, used_resources, profiling_info } =
222            self.run_function(func, hint_processor, hints_dict, bytecode.iter(), builtins)?;
223        let mut all_used_resources = hint_processor.take_syscalls_used_resources();
224        all_used_resources.basic_resources += &used_resources;
225        Ok(RunResultStarknet {
226            gas_counter,
227            memory,
228            value,
229            starknet_state: hint_processor.take_starknet_state(),
230            used_resources: all_used_resources,
231            profiling_info,
232        })
233    }
234
235    /// Extract inner type if `ty` is a panic wrapper
236    fn inner_type_from_panic_wrapper(
237        &self,
238        ty: &GenericTypeId,
239        func: &Function,
240    ) -> Option<ConcreteTypeId> {
241        let generic_args = &func
242            .signature
243            .ret_types
244            .iter()
245            .find_map(|rt| {
246                let long_id = self.builder.type_long_id(rt);
247                (long_id.generic_id == *ty).then_some(long_id)
248            })
249            .unwrap()
250            .generic_args;
251
252        if *ty == EnumType::ID
253            && matches!(&generic_args[0], GenericArg::UserType(ut)
254                if ut.debug_name.as_ref().unwrap().starts_with("core::panics::PanicResult::"))
255        {
256            return Some(extract_matches!(&generic_args[1], GenericArg::Type).clone());
257        }
258        None
259    }
260
261    /// Runs the VM starting from a function with a custom hint processor. The function may have
262    /// implicits, but no other ref params. The cost of the function is deducted from
263    /// `available_gas` before the execution begins.
264    pub fn run_function<'a, Bytecode>(
265        &self,
266        func: &Function,
267        hint_processor: &mut dyn HintProcessor,
268        hints_dict: HashMap<usize, Vec<HintParams>>,
269        bytecode: Bytecode,
270        builtins: Vec<BuiltinName>,
271    ) -> Result<RunResult, RunnerError>
272    where
273        Bytecode: ExactSizeIterator<Item = &'a BigInt> + Clone,
274    {
275        let return_types =
276            self.builder.generic_id_and_size_from_concrete(&func.signature.ret_types);
277        let data_len = bytecode.len();
278        let RunFunctionResult { ap, mut used_resources, memory, relocated_trace } =
279            casm_run::run_function(
280                bytecode,
281                builtins,
282                |vm| initialize_vm(vm, data_len),
283                hint_processor,
284                hints_dict,
285            )?;
286
287        // The execution from the header created by self.builder.create_entry_code().
288        // We expect the last trace entry to be the `ret` instruction at the end of the header.
289        let header_end = relocated_trace.last().unwrap().pc;
290        used_resources.n_steps -=
291            relocated_trace.iter().position(|e| e.pc > header_end).unwrap() - 1;
292        used_resources.n_steps -=
293            relocated_trace.iter().rev().position(|e| e.pc > header_end).unwrap() - 1;
294
295        let (results_data, gas_counter) = self.get_results_data(&return_types, &memory, ap);
296        assert!(results_data.len() <= 1);
297
298        let value = match results_data.into_iter().next() {
299            // No result type - no panic.
300            None => RunResultValue::Success(vec![]),
301            Some((ty, values)) => {
302                let inner_ty = self
303                    .inner_type_from_panic_wrapper(&ty, func)
304                    .map(|it| self.builder.type_size(&it));
305                Self::handle_main_return_value(inner_ty, values, &memory)
306            }
307        };
308
309        let Self { builder, starknet_contracts_info: _, run_profiler } = self;
310
311        // The real program starts right after the header.
312        let load_offset = header_end + 1;
313
314        let profiling_info = run_profiler.as_ref().map(|config| {
315            ProfilingInfo::from_trace(builder, load_offset, config, &relocated_trace)
316        });
317
318        Ok(RunResult { gas_counter, memory, value, used_resources, profiling_info })
319    }
320
321    /// Prepares context for running a function in the context of a given Starknet state.
322    ///
323    /// The returned hint processor instance is set up for interpreting and executing the hints
324    /// provided during the Cairo program's execution. Can be customized by wrapping into a custom
325    /// hint processor implementation and passing that to the `run_function` method.
326    pub fn prepare_starknet_context(
327        &self,
328        func: &Function,
329        args: Vec<Arg>,
330        available_gas: Option<usize>,
331        starknet_state: StarknetState,
332    ) -> Result<(CairoHintProcessor<'_>, PreparedStarknetContext), RunnerError> {
333        let (assembled_program, builtins) =
334            self.builder.assemble_function_program(func, EntryCodeConfig::testing())?;
335        let (hints_dict, string_to_hint) = build_hints_dict(&assembled_program.hints);
336        let user_args = self.prepare_args(func, available_gas, args)?;
337        let hint_processor = CairoHintProcessor {
338            runner: Some(self),
339            user_args,
340            starknet_state,
341            string_to_hint,
342            run_resources: RunResources::default(),
343            syscalls_used_resources: Default::default(),
344            no_temporary_segments: true,
345            markers: Default::default(),
346            panic_traceback: Default::default(),
347        };
348        Ok((
349            hint_processor,
350            PreparedStarknetContext { hints_dict, bytecode: assembled_program.bytecode, builtins },
351        ))
352    }
353
354    /// Groups the args by parameters, and additionally adds `gas` as the first if required.
355    fn prepare_args(
356        &self,
357        func: &Function,
358        available_gas: Option<usize>,
359        args: Vec<Arg>,
360    ) -> Result<Vec<Vec<Arg>>, RunnerError> {
361        let mut user_args = vec![];
362        if let Some(gas) = self
363            .requires_gas_builtin(func)
364            .then_some(self.get_initial_available_gas(func, available_gas)?)
365        {
366            user_args.push(vec![Arg::Value(Felt252::from(gas))]);
367        }
368        let mut expected_arguments_size = 0;
369        let actual_args_size = args_size(&args);
370        let mut arg_iter = args.into_iter().enumerate();
371        for (param_index, (_, param_size)) in self
372            .builder
373            .generic_id_and_size_from_concrete(&func.signature.param_types)
374            .into_iter()
375            .filter(|(ty, _)| self.builder.is_user_arg_type(ty))
376            .enumerate()
377        {
378            let mut curr_arg = vec![];
379            let param_size: usize = param_size.into_or_panic();
380            expected_arguments_size += param_size;
381            let mut taken_size = 0;
382            while taken_size < param_size {
383                let Some((arg_index, arg)) = arg_iter.next() else {
384                    break;
385                };
386                taken_size += arg.size();
387                if taken_size > param_size {
388                    return Err(RunnerError::ArgumentUnaligned { param_index, arg_index });
389                }
390                curr_arg.push(arg);
391            }
392            user_args.push(curr_arg);
393        }
394        if expected_arguments_size != actual_args_size {
395            return Err(RunnerError::ArgumentsSizeMismatch {
396                expected: expected_arguments_size,
397                actual: actual_args_size,
398            });
399        }
400        Ok(user_args)
401    }
402
403    /// Handling the main return value to create a `RunResultValue`.
404    pub fn handle_main_return_value(
405        inner_type_size: Option<i16>,
406        values: Vec<Felt252>,
407        cells: &[Option<Felt252>],
408    ) -> RunResultValue {
409        if let Some(inner_type_size) = inner_type_size {
410            // The function includes a panic wrapper.
411            if values[0] == Felt252::from(0) {
412                // The run resulted successfully, returning the inner value.
413                let inner_ty_size = inner_type_size as usize;
414                let skip_size = values.len() - inner_ty_size;
415                RunResultValue::Success(values.into_iter().skip(skip_size).collect())
416            } else {
417                // The run resulted in a panic, returning the error data.
418                let err_data_start = values[values.len() - 2].to_usize().unwrap();
419                let err_data_end = values[values.len() - 1].to_usize().unwrap();
420                RunResultValue::Panic(
421                    cells[err_data_start..err_data_end]
422                        .iter()
423                        .cloned()
424                        .map(Option::unwrap)
425                        .collect(),
426                )
427            }
428        } else {
429            // No panic wrap - so always successful.
430            RunResultValue::Success(values)
431        }
432    }
433
434    /// Returns the final values and type of all `func`s returning variables.
435    pub fn get_results_data(
436        &self,
437        return_types: &[(GenericTypeId, i16)],
438        cells: &[Option<Felt252>],
439        mut ap: usize,
440    ) -> (Vec<(GenericTypeId, Vec<Felt252>)>, Option<Felt252>) {
441        let mut results_data = vec![];
442        for (ty, ty_size) in return_types.iter().rev() {
443            let size = *ty_size as usize;
444            let values: Vec<Felt252> =
445                ((ap - size)..ap).map(|index| cells[index].unwrap()).collect();
446            ap -= size;
447            results_data.push((ty.clone(), values));
448        }
449
450        // Handling implicits.
451        let mut gas_counter = None;
452        results_data.retain_mut(|(ty, values)| {
453            let generic_ty = ty;
454            if *generic_ty == GasBuiltinType::ID {
455                gas_counter = Some(values.remove(0));
456                assert!(values.is_empty());
457                false
458            } else {
459                self.builder.is_user_arg_type(generic_ty)
460            }
461        });
462
463        (results_data, gas_counter)
464    }
465
466    /// Finds the first function ending with `name_suffix`.
467    pub fn find_function(&self, name_suffix: &str) -> Result<&Function, RunnerError> {
468        Ok(self.builder.find_function(name_suffix)?)
469    }
470
471    /// Returns whether the gas builtin is required in the given function.
472    fn requires_gas_builtin(&self, func: &Function) -> bool {
473        func.signature
474            .param_types
475            .iter()
476            .any(|ty| self.builder.type_long_id(ty).generic_id == GasBuiltinType::ID)
477    }
478
479    /// Returns the initial value for the gas counter.
480    /// If `available_gas` is None returns 0.
481    pub fn get_initial_available_gas(
482        &self,
483        func: &Function,
484        available_gas: Option<usize>,
485    ) -> Result<usize, RunnerError> {
486        let Some(available_gas) = available_gas else {
487            return Ok(0);
488        };
489
490        // In case we don't have any costs - it means no gas equations were solved (and we are in
491        // the case of no gas checking enabled) - so the gas builtin is irrelevant, and we
492        // can return any value.
493        let Some(required_gas) = self.initial_required_gas(func) else {
494            return Ok(0);
495        };
496
497        available_gas.checked_sub(required_gas).ok_or(RunnerError::NotEnoughGasToCall)
498    }
499
500    pub fn initial_required_gas(&self, func: &Function) -> Option<usize> {
501        let gas_info = &self.builder.metadata().gas_info;
502        require(!gas_info.function_costs.is_empty())?;
503        Some(
504            gas_info.function_costs[&func.id]
505                .iter()
506                .map(|(token_type, val)| val.into_or_panic::<usize>() * token_gas_cost(*token_type))
507                .sum(),
508        )
509    }
510}
511
512/// Configuration for the profiling info collection phase.
513#[derive(Debug, Eq, PartialEq, Clone)]
514pub struct ProfilingInfoCollectionConfig {
515    /// The maximum depth of the stack trace to collect.
516    pub max_stack_trace_depth: usize,
517    /// If this flag is set, in addition to the Sierra statement weights and stack trace weights
518    /// the runner will also collect weights for Sierra statements taking into account current call
519    /// stack and collapsing recursive calls (which also includes loops).
520    /// The resulting dictionary can be pretty huge hence this feature is optional and disabled by
521    /// default.
522    pub collect_scoped_sierra_statement_weights: bool,
523}
524
525impl ProfilingInfoCollectionConfig {
526    pub fn set_max_stack_trace_depth(&mut self, max_stack_depth: usize) -> &mut Self {
527        self.max_stack_trace_depth = max_stack_depth;
528        self
529    }
530
531    pub fn from_profiler_config(profiler_config: &ProfilerConfig) -> Self {
532        match profiler_config {
533            ProfilerConfig::Cairo | ProfilerConfig::Sierra => Self::default(),
534            ProfilerConfig::Scoped => ProfilingInfoCollectionConfig {
535                collect_scoped_sierra_statement_weights: true,
536                ..Self::default()
537            },
538        }
539    }
540}
541
542impl Default for ProfilingInfoCollectionConfig {
543    // TODO(yuval): consider changing this setting to use flags.
544    /// Gets the max_stack_trace_depth according to the environment variable
545    /// `MAX_STACK_TRACE_DEPTH`, if set.
546    fn default() -> Self {
547        Self {
548            max_stack_trace_depth: if let Ok(max) = std::env::var("MAX_STACK_TRACE_DEPTH") {
549                if max.is_empty() {
550                    MAX_STACK_TRACE_DEPTH_DEFAULT
551                } else {
552                    max.parse::<usize>().expect("MAX_STACK_TRACE_DEPTH env var is not numeric")
553                }
554            } else {
555                MAX_STACK_TRACE_DEPTH_DEFAULT
556            },
557            collect_scoped_sierra_statement_weights: false,
558        }
559    }
560}
561
562/// Initializes a VM by adding a new segment with builtins cost and a necessary pointer at the end
563/// of the program, as well as placing the arguments at the initial AP values.
564pub fn initialize_vm(vm: &mut VirtualMachine, data_len: usize) -> Result<(), Box<CairoRunError>> {
565    // Create the builtin cost segment, with dummy values.
566    let builtin_cost_segment = vm.add_memory_segment();
567    for token_type in CostTokenType::iter_precost() {
568        vm.insert_value(
569            (builtin_cost_segment + (token_type.offset_in_builtin_costs() as usize)).unwrap(),
570            Felt252::from(token_gas_cost(*token_type)),
571        )
572        .map_err(|e| Box::new(e.into()))?;
573    }
574    // Put a pointer to the builtin cost segment at the end of the program (after the
575    // additional `ret` statement).
576    vm.insert_value((vm.get_pc() + data_len).unwrap(), builtin_cost_segment)
577        .map_err(|e| Box::new(e.into()))?;
578    Ok(())
579}
580
581/// The size in memory of the arguments.
582fn args_size(args: &[Arg]) -> usize {
583    args.iter().map(Arg::size).sum()
584}