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