use std::collections::HashMap;
use cairo_lang_casm::hints::Hint;
use cairo_lang_runnable_utils::builder::{BuildError, EntryCodeConfig, RunnableBuilder};
use cairo_lang_sierra::extensions::NamedType;
use cairo_lang_sierra::extensions::enm::EnumType;
use cairo_lang_sierra::extensions::gas::{CostTokenType, GasBuiltinType};
use cairo_lang_sierra::ids::{ConcreteTypeId, GenericTypeId};
use cairo_lang_sierra::program::{Function, GenericArg};
use cairo_lang_sierra_to_casm::metadata::MetadataComputationConfig;
use cairo_lang_starknet::contract::ContractInfo;
use cairo_lang_utils::casts::IntoOrPanic;
use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
use cairo_lang_utils::{extract_matches, require};
use cairo_vm::hint_processor::hint_processor_definition::HintProcessor;
use cairo_vm::serde::deserialize_program::HintParams;
use cairo_vm::types::builtin_name::BuiltinName;
use cairo_vm::vm::errors::cairo_run_errors::CairoRunError;
use cairo_vm::vm::runners::cairo_runner::{ExecutionResources, RunResources};
use cairo_vm::vm::vm_core::VirtualMachine;
use casm_run::hint_to_hint_params;
pub use casm_run::{CairoHintProcessor, StarknetState};
use num_bigint::BigInt;
use num_traits::ToPrimitive;
use profiling::ProfilingInfo;
use starknet_types_core::felt::Felt as Felt252;
use thiserror::Error;
use crate::casm_run::{RunFunctionResult, StarknetHintProcessor};
use crate::profiling::ProfilerConfig;
pub mod casm_run;
pub mod clap;
pub mod profiling;
pub mod short_string;
const MAX_STACK_TRACE_DEPTH_DEFAULT: usize = 100;
#[derive(Debug, Error)]
pub enum RunnerError {
#[error(transparent)]
BuildError(#[from] BuildError),
#[error("Not enough gas to call function.")]
NotEnoughGasToCall,
#[error("Function param {param_index} only partially contains argument {arg_index}.")]
ArgumentUnaligned { param_index: usize, arg_index: usize },
#[error("Function expects arguments of size {expected} and received {actual} instead.")]
ArgumentsSizeMismatch { expected: usize, actual: usize },
#[error(transparent)]
CairoRunError(#[from] Box<CairoRunError>),
}
pub struct RunResultStarknet {
pub gas_counter: Option<Felt252>,
pub memory: Vec<Option<Felt252>>,
pub value: RunResultValue,
pub starknet_state: StarknetState,
pub used_resources: StarknetExecutionResources,
pub profiling_info: Option<ProfilingInfo>,
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct RunResult {
pub gas_counter: Option<Felt252>,
pub memory: Vec<Option<Felt252>>,
pub value: RunResultValue,
pub used_resources: ExecutionResources,
pub profiling_info: Option<ProfilingInfo>,
}
#[derive(Debug, Eq, PartialEq, Clone, Default)]
pub struct StarknetExecutionResources {
pub basic_resources: ExecutionResources,
pub syscalls: HashMap<String, usize>,
}
impl std::ops::AddAssign<StarknetExecutionResources> for StarknetExecutionResources {
fn add_assign(&mut self, other: Self) {
self.basic_resources += &other.basic_resources;
for (k, v) in other.syscalls {
*self.syscalls.entry(k).or_default() += v;
}
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum RunResultValue {
Success(Vec<Felt252>),
Panic(Vec<Felt252>),
}
pub fn token_gas_cost(token_type: CostTokenType) -> usize {
match token_type {
CostTokenType::Const => 1,
CostTokenType::Step
| CostTokenType::Hole
| CostTokenType::RangeCheck
| CostTokenType::RangeCheck96 => {
panic!("Token type {token_type:?} has no gas cost.")
}
CostTokenType::Pedersen => 4050,
CostTokenType::Poseidon => 491,
CostTokenType::Bitwise => 583,
CostTokenType::EcOp => 4085,
CostTokenType::AddMod => 230,
CostTokenType::MulMod => 604,
CostTokenType::Blake => 3334,
}
}
#[derive(Debug, Clone)]
pub enum Arg {
Value(Felt252),
Array(Vec<Arg>),
}
impl Arg {
pub fn size(&self) -> usize {
match self {
Self::Value(_) => 1,
Self::Array(_) => 2,
}
}
}
impl From<Felt252> for Arg {
fn from(value: Felt252) -> Self {
Self::Value(value)
}
}
pub fn build_hints_dict(
hints: &[(usize, Vec<Hint>)],
) -> (HashMap<usize, Vec<HintParams>>, HashMap<String, Hint>) {
let mut hints_dict: HashMap<usize, Vec<HintParams>> = HashMap::new();
let mut string_to_hint: HashMap<String, Hint> = HashMap::new();
for (offset, offset_hints) in hints {
for hint in offset_hints {
string_to_hint.insert(hint.representing_string(), hint.clone());
}
hints_dict.insert(*offset, offset_hints.iter().map(hint_to_hint_params).collect());
}
(hints_dict, string_to_hint)
}
pub struct PreparedStarknetContext {
pub hints_dict: HashMap<usize, Vec<HintParams>>,
pub bytecode: Vec<BigInt>,
pub builtins: Vec<BuiltinName>,
}
pub struct SierraCasmRunner {
builder: RunnableBuilder,
starknet_contracts_info: OrderedHashMap<Felt252, ContractInfo>,
run_profiler: Option<ProfilingInfoCollectionConfig>,
}
impl SierraCasmRunner {
pub fn new(
sierra_program: cairo_lang_sierra::program::Program,
metadata_config: Option<MetadataComputationConfig>,
starknet_contracts_info: OrderedHashMap<Felt252, ContractInfo>,
run_profiler: Option<ProfilingInfoCollectionConfig>,
) -> Result<Self, RunnerError> {
Ok(Self {
builder: RunnableBuilder::new(sierra_program, metadata_config)?,
starknet_contracts_info,
run_profiler,
})
}
pub fn run_function_with_starknet_context(
&self,
func: &Function,
args: Vec<Arg>,
available_gas: Option<usize>,
starknet_state: StarknetState,
) -> Result<RunResultStarknet, RunnerError> {
let (mut hint_processor, ctx) =
self.prepare_starknet_context(func, args, available_gas, starknet_state)?;
self.run_function_with_prepared_starknet_context(func, &mut hint_processor, ctx)
}
pub fn run_function_with_prepared_starknet_context(
&self,
func: &Function,
hint_processor: &mut dyn StarknetHintProcessor,
PreparedStarknetContext { hints_dict, bytecode, builtins }: PreparedStarknetContext,
) -> Result<RunResultStarknet, RunnerError> {
let RunResult { gas_counter, memory, value, used_resources, profiling_info } =
self.run_function(func, hint_processor, hints_dict, bytecode.iter(), builtins)?;
let mut all_used_resources = hint_processor.take_syscalls_used_resources();
all_used_resources.basic_resources += &used_resources;
Ok(RunResultStarknet {
gas_counter,
memory,
value,
starknet_state: hint_processor.take_starknet_state(),
used_resources: all_used_resources,
profiling_info,
})
}
fn inner_type_from_panic_wrapper(
&self,
ty: &GenericTypeId,
func: &Function,
) -> Option<ConcreteTypeId> {
let generic_args = &func
.signature
.ret_types
.iter()
.find_map(|rt| {
let long_id = self.builder.type_long_id(rt);
(long_id.generic_id == *ty).then_some(long_id)
})
.unwrap()
.generic_args;
if *ty == EnumType::ID
&& matches!(&generic_args[0], GenericArg::UserType(ut)
if ut.debug_name.as_ref().unwrap().starts_with("core::panics::PanicResult::"))
{
return Some(extract_matches!(&generic_args[1], GenericArg::Type).clone());
}
None
}
pub fn run_function<'a, Bytecode>(
&self,
func: &Function,
hint_processor: &mut dyn HintProcessor,
hints_dict: HashMap<usize, Vec<HintParams>>,
bytecode: Bytecode,
builtins: Vec<BuiltinName>,
) -> Result<RunResult, RunnerError>
where
Bytecode: ExactSizeIterator<Item = &'a BigInt> + Clone,
{
let return_types =
self.builder.generic_id_and_size_from_concrete(&func.signature.ret_types);
let data_len = bytecode.len();
let RunFunctionResult { ap, mut used_resources, memory, relocated_trace } =
casm_run::run_function(
bytecode,
builtins,
|vm| initialize_vm(vm, data_len),
hint_processor,
hints_dict,
)?;
let header_end = relocated_trace.last().unwrap().pc;
used_resources.n_steps -=
relocated_trace.iter().position(|e| e.pc > header_end).unwrap() - 1;
used_resources.n_steps -=
relocated_trace.iter().rev().position(|e| e.pc > header_end).unwrap() - 1;
let (results_data, gas_counter) = self.get_results_data(&return_types, &memory, ap);
assert!(results_data.len() <= 1);
let value = match results_data.into_iter().next() {
None => RunResultValue::Success(vec![]),
Some((ty, values)) => {
let inner_ty = self
.inner_type_from_panic_wrapper(&ty, func)
.map(|it| self.builder.type_size(&it));
Self::handle_main_return_value(inner_ty, values, &memory)
}
};
let Self { builder, starknet_contracts_info: _, run_profiler } = self;
let load_offset = header_end + 1;
let profiling_info = run_profiler.as_ref().map(|config| {
ProfilingInfo::from_trace(builder, load_offset, config, &relocated_trace)
});
Ok(RunResult { gas_counter, memory, value, used_resources, profiling_info })
}
pub fn prepare_starknet_context(
&self,
func: &Function,
args: Vec<Arg>,
available_gas: Option<usize>,
starknet_state: StarknetState,
) -> Result<(CairoHintProcessor<'_>, PreparedStarknetContext), RunnerError> {
let (assembled_program, builtins) =
self.builder.assemble_function_program(func, EntryCodeConfig::testing())?;
let (hints_dict, string_to_hint) = build_hints_dict(&assembled_program.hints);
let user_args = self.prepare_args(func, available_gas, args)?;
let hint_processor = CairoHintProcessor {
runner: Some(self),
user_args,
starknet_state,
string_to_hint,
run_resources: RunResources::default(),
syscalls_used_resources: Default::default(),
no_temporary_segments: true,
markers: Default::default(),
panic_traceback: Default::default(),
};
Ok((
hint_processor,
PreparedStarknetContext { hints_dict, bytecode: assembled_program.bytecode, builtins },
))
}
fn prepare_args(
&self,
func: &Function,
available_gas: Option<usize>,
args: Vec<Arg>,
) -> Result<Vec<Vec<Arg>>, RunnerError> {
let mut user_args = vec![];
if let Some(gas) = self
.requires_gas_builtin(func)
.then_some(self.get_initial_available_gas(func, available_gas)?)
{
user_args.push(vec![Arg::Value(Felt252::from(gas))]);
}
let mut expected_arguments_size = 0;
let actual_args_size = args_size(&args);
let mut arg_iter = args.into_iter().enumerate();
for (param_index, (_, param_size)) in self
.builder
.generic_id_and_size_from_concrete(&func.signature.param_types)
.into_iter()
.filter(|(ty, _)| self.builder.is_user_arg_type(ty))
.enumerate()
{
let mut curr_arg = vec![];
let param_size: usize = param_size.into_or_panic();
expected_arguments_size += param_size;
let mut taken_size = 0;
while taken_size < param_size {
let Some((arg_index, arg)) = arg_iter.next() else {
break;
};
taken_size += arg.size();
if taken_size > param_size {
return Err(RunnerError::ArgumentUnaligned { param_index, arg_index });
}
curr_arg.push(arg);
}
user_args.push(curr_arg);
}
if expected_arguments_size != actual_args_size {
return Err(RunnerError::ArgumentsSizeMismatch {
expected: expected_arguments_size,
actual: actual_args_size,
});
}
Ok(user_args)
}
pub fn handle_main_return_value(
inner_type_size: Option<i16>,
values: Vec<Felt252>,
cells: &[Option<Felt252>],
) -> RunResultValue {
if let Some(inner_type_size) = inner_type_size {
if values[0] == Felt252::from(0) {
let inner_ty_size = inner_type_size as usize;
let skip_size = values.len() - inner_ty_size;
RunResultValue::Success(values.into_iter().skip(skip_size).collect())
} else {
let err_data_start = values[values.len() - 2].to_usize().unwrap();
let err_data_end = values[values.len() - 1].to_usize().unwrap();
RunResultValue::Panic(
cells[err_data_start..err_data_end]
.iter()
.cloned()
.map(Option::unwrap)
.collect(),
)
}
} else {
RunResultValue::Success(values)
}
}
pub fn get_results_data(
&self,
return_types: &[(GenericTypeId, i16)],
cells: &[Option<Felt252>],
mut ap: usize,
) -> (Vec<(GenericTypeId, Vec<Felt252>)>, Option<Felt252>) {
let mut results_data = vec![];
for (ty, ty_size) in return_types.iter().rev() {
let size = *ty_size as usize;
let values: Vec<Felt252> =
((ap - size)..ap).map(|index| cells[index].unwrap()).collect();
ap -= size;
results_data.push((ty.clone(), values));
}
let mut gas_counter = None;
results_data.retain_mut(|(ty, values)| {
let generic_ty = ty;
if *generic_ty == GasBuiltinType::ID {
gas_counter = Some(values.remove(0));
assert!(values.is_empty());
false
} else {
self.builder.is_user_arg_type(generic_ty)
}
});
(results_data, gas_counter)
}
pub fn find_function(&self, name_suffix: &str) -> Result<&Function, RunnerError> {
Ok(self.builder.find_function(name_suffix)?)
}
fn requires_gas_builtin(&self, func: &Function) -> bool {
func.signature
.param_types
.iter()
.any(|ty| self.builder.type_long_id(ty).generic_id == GasBuiltinType::ID)
}
pub fn get_initial_available_gas(
&self,
func: &Function,
available_gas: Option<usize>,
) -> Result<usize, RunnerError> {
let Some(available_gas) = available_gas else {
return Ok(0);
};
let Some(required_gas) = self.initial_required_gas(func) else {
return Ok(0);
};
available_gas.checked_sub(required_gas).ok_or(RunnerError::NotEnoughGasToCall)
}
pub fn initial_required_gas(&self, func: &Function) -> Option<usize> {
let gas_info = &self.builder.metadata().gas_info;
require(!gas_info.function_costs.is_empty())?;
Some(
gas_info.function_costs[&func.id]
.iter()
.map(|(token_type, val)| val.into_or_panic::<usize>() * token_gas_cost(*token_type))
.sum(),
)
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct ProfilingInfoCollectionConfig {
pub max_stack_trace_depth: usize,
pub collect_scoped_sierra_statement_weights: bool,
}
impl ProfilingInfoCollectionConfig {
pub fn set_max_stack_trace_depth(&mut self, max_stack_depth: usize) -> &mut Self {
self.max_stack_trace_depth = max_stack_depth;
self
}
pub fn from_profiler_config(profiler_config: &ProfilerConfig) -> Self {
match profiler_config {
ProfilerConfig::Cairo | ProfilerConfig::Sierra => Self::default(),
ProfilerConfig::Scoped => ProfilingInfoCollectionConfig {
collect_scoped_sierra_statement_weights: true,
..Self::default()
},
}
}
}
impl Default for ProfilingInfoCollectionConfig {
fn default() -> Self {
Self {
max_stack_trace_depth: if let Ok(max) = std::env::var("MAX_STACK_TRACE_DEPTH") {
if max.is_empty() {
MAX_STACK_TRACE_DEPTH_DEFAULT
} else {
max.parse::<usize>().expect("MAX_STACK_TRACE_DEPTH env var is not numeric")
}
} else {
MAX_STACK_TRACE_DEPTH_DEFAULT
},
collect_scoped_sierra_statement_weights: false,
}
}
}
pub fn initialize_vm(vm: &mut VirtualMachine, data_len: usize) -> Result<(), Box<CairoRunError>> {
let builtin_cost_segment = vm.add_memory_segment();
for token_type in CostTokenType::iter_precost() {
vm.insert_value(
(builtin_cost_segment + (token_type.offset_in_builtin_costs() as usize)).unwrap(),
Felt252::from(token_gas_cost(*token_type)),
)
.map_err(|e| Box::new(e.into()))?;
}
vm.insert_value((vm.get_pc() + data_len).unwrap(), builtin_cost_segment)
.map_err(|e| Box::new(e.into()))?;
Ok(())
}
fn args_size(args: &[Arg]) -> usize {
args.iter().map(Arg::size).sum()
}