use std::collections::HashMap;
use cairo_felt::Felt252;
use cairo_lang_casm::instructions::Instruction;
use cairo_lang_casm::{casm, casm_extend};
use cairo_lang_sierra::extensions::bitwise::BitwiseType;
use cairo_lang_sierra::extensions::core::{CoreLibfunc, CoreType};
use cairo_lang_sierra::extensions::ec::EcOpType;
use cairo_lang_sierra::extensions::enm::EnumType;
use cairo_lang_sierra::extensions::gas::{CostTokenType, GasBuiltinType};
use cairo_lang_sierra::extensions::pedersen::PedersenType;
use cairo_lang_sierra::extensions::poseidon::PoseidonType;
use cairo_lang_sierra::extensions::range_check::RangeCheckType;
use cairo_lang_sierra::extensions::segment_arena::SegmentArenaType;
use cairo_lang_sierra::extensions::starknet::syscalls::SystemType;
use cairo_lang_sierra::extensions::{ConcreteType, NamedType};
use cairo_lang_sierra::program::{Function, GenericArg};
use cairo_lang_sierra::program_registry::{ProgramRegistry, ProgramRegistryError};
use cairo_lang_sierra_ap_change::{calc_ap_changes, ApChangeError};
use cairo_lang_sierra_gas::gas_info::GasInfo;
use cairo_lang_sierra_to_casm::compiler::{CairoProgram, CompilationError};
use cairo_lang_sierra_to_casm::metadata::{
calc_metadata, Metadata, MetadataComputationConfig, MetadataError,
};
use cairo_lang_starknet::contract::ContractInfo;
use cairo_lang_utils::extract_matches;
use cairo_vm::serde::deserialize_program::BuiltinName;
use cairo_vm::vm::errors::vm_errors::VirtualMachineError;
pub use casm_run::StarknetState;
use itertools::chain;
use num_traits::ToPrimitive;
use thiserror::Error;
mod casm_run;
pub mod short_string;
#[derive(Debug, Error)]
pub enum RunnerError {
#[error("Not enough gas to call function.")]
NotEnoughGasToCall,
#[error("GasBuiltin is required while `available_gas` value is provided.")]
GasBuiltinRequired,
#[error(
"Failed calculating gas usage, it is likely a call for `gas::withdraw_gas` is missing."
)]
FailedGasCalculation,
#[error("Function with suffix `{suffix}` to run not found.")]
MissingFunction { suffix: String },
#[error("Function expects arguments of size {expected} and received {actual} instead.")]
ArgumentsSizeMismatch { expected: usize, actual: usize },
#[error(transparent)]
ProgramRegistryError(#[from] Box<ProgramRegistryError>),
#[error(transparent)]
SierraCompilationError(#[from] Box<CompilationError>),
#[error(transparent)]
ApChangeError(#[from] ApChangeError),
#[error(transparent)]
VirtualMachineError(#[from] Box<VirtualMachineError>),
}
pub struct RunResult {
pub gas_counter: Option<Felt252>,
pub memory: Vec<Option<Felt252>>,
pub value: RunResultValue,
pub starknet_state: StarknetState,
}
#[derive(Debug, Eq, PartialEq)]
pub enum RunResultValue {
Success(Vec<Felt252>),
Panic(Vec<Felt252>),
}
pub const DUMMY_BUILTIN_GAS_COST: usize = 10000;
#[derive(Debug)]
pub enum Arg {
Value(Felt252),
Array(Vec<Felt252>),
}
impl From<Felt252> for Arg {
fn from(value: Felt252) -> Self {
Self::Value(value)
}
}
pub struct SierraCasmRunner {
sierra_program: cairo_lang_sierra::program::Program,
metadata: Metadata,
sierra_program_registry: ProgramRegistry<CoreType, CoreLibfunc>,
casm_program: CairoProgram,
#[allow(dead_code)]
starknet_contracts_info: HashMap<Felt252, ContractInfo>,
}
impl SierraCasmRunner {
pub fn new(
sierra_program: cairo_lang_sierra::program::Program,
metadata_config: Option<MetadataComputationConfig>,
starknet_contracts_info: HashMap<Felt252, ContractInfo>,
) -> Result<Self, RunnerError> {
let gas_usage_check = metadata_config.is_some();
let metadata = create_metadata(&sierra_program, metadata_config)?;
let sierra_program_registry =
ProgramRegistry::<CoreType, CoreLibfunc>::new(&sierra_program)?;
let casm_program = cairo_lang_sierra_to_casm::compiler::compile(
&sierra_program,
&metadata,
gas_usage_check,
)?;
Ok(Self {
sierra_program,
metadata,
sierra_program_registry,
casm_program,
starknet_contracts_info,
})
}
pub fn run_function(
&self,
func: &Function,
args: &[Arg],
available_gas: Option<usize>,
starknet_state: StarknetState,
) -> Result<RunResult, RunnerError> {
let initial_gas = self.get_initial_available_gas(func, available_gas)?;
let (entry_code, builtins) = self.create_entry_code(func, args, initial_gas)?;
let footer = self.create_code_footer();
let (cells, ap, starknet_state) = casm_run::run_function(
Some(self),
chain!(entry_code.iter(), self.casm_program.instructions.iter(), footer.iter()),
builtins,
|context| {
let vm = context.vm;
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(DUMMY_BUILTIN_GAS_COST),
)
.map_err(|e| Box::new(e.into()))?;
}
vm.insert_value((vm.get_pc() + context.data_len).unwrap(), builtin_cost_segment)
.map_err(|e| Box::new(e.into()))?;
Ok(())
},
starknet_state,
)?;
let mut results_data = self.get_results_data(func, &cells, ap)?;
let mut gas_counter = None;
results_data.retain_mut(|(ty, values)| {
let info = self.get_info(ty);
let generic_ty = &info.long_id.generic_id;
if *generic_ty == GasBuiltinType::ID {
gas_counter = Some(values.remove(0));
assert!(values.is_empty());
false
} else {
*generic_ty != RangeCheckType::ID
&& *generic_ty != BitwiseType::ID
&& *generic_ty != EcOpType::ID
&& *generic_ty != PedersenType::ID
&& *generic_ty != PoseidonType::ID
&& *generic_ty != SystemType::ID
&& *generic_ty != SegmentArenaType::ID
}
});
assert!(results_data.len() <= 1);
let value = if results_data.is_empty() {
RunResultValue::Success(vec![])
} else {
let [(ty, values)] = <[_; 1]>::try_from(results_data).ok().unwrap();
self.handle_main_return_value(ty, values, &cells)?
};
Ok(RunResult { gas_counter, memory: cells, value, starknet_state })
}
fn handle_main_return_value(
&self,
ty: cairo_lang_sierra::ids::ConcreteTypeId,
values: Vec<Felt252>,
cells: &[Option<Felt252>],
) -> Result<RunResultValue, RunnerError> {
let info = self.get_info(&ty);
let long_id = &info.long_id;
Ok(
if long_id.generic_id == EnumType::ID
&& matches!(&long_id.generic_args[0], GenericArg::UserType(ut) if ut.debug_name.as_ref().unwrap().starts_with("core::PanicResult::"))
{
if values[0] != Felt252::from(0) {
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(|cell| cell.unwrap())
.collect(),
)
} else {
let inner_ty = extract_matches!(&long_id.generic_args[1], GenericArg::Type);
let inner_ty_size =
self.sierra_program_registry.get_type(inner_ty)?.info().size as usize;
let skip_size = values.len() - inner_ty_size;
RunResultValue::Success(values.into_iter().skip(skip_size).collect())
}
} else {
RunResultValue::Success(values)
},
)
}
fn get_results_data(
&self,
func: &Function,
cells: &[Option<Felt252>],
mut ap: usize,
) -> Result<Vec<(cairo_lang_sierra::ids::ConcreteTypeId, Vec<Felt252>)>, RunnerError> {
let mut results_data = vec![];
for ty in func.signature.ret_types.iter().rev() {
let size = self.sierra_program_registry.get_type(ty)?.info().size as usize;
let values: Vec<Felt252> =
((ap - size)..ap).map(|index| cells[index].clone().unwrap()).collect();
ap -= size;
results_data.push((ty.clone(), values));
}
Ok(results_data)
}
pub fn find_function(&self, name_suffix: &str) -> Result<&Function, RunnerError> {
self.sierra_program
.funcs
.iter()
.find(|f| {
if let Some(name) = &f.id.debug_name { name.ends_with(name_suffix) } else { false }
})
.ok_or_else(|| RunnerError::MissingFunction { suffix: name_suffix.to_owned() })
}
fn get_info(
&self,
ty: &cairo_lang_sierra::ids::ConcreteTypeId,
) -> &cairo_lang_sierra::extensions::types::TypeInfo {
self.sierra_program_registry.get_type(ty).unwrap().info()
}
fn create_entry_code(
&self,
func: &Function,
args: &[Arg],
initial_gas: usize,
) -> Result<(Vec<Instruction>, Vec<BuiltinName>), RunnerError> {
let mut arg_iter = args.iter().peekable();
let mut expected_arguments_size = 0;
let mut ctx = casm! {};
let builtins = vec![
BuiltinName::pedersen,
BuiltinName::range_check,
BuiltinName::bitwise,
BuiltinName::ec_op,
BuiltinName::poseidon,
];
let builtin_offset: HashMap<cairo_lang_sierra::ids::GenericTypeId, i16> = HashMap::from([
(PedersenType::ID, 7),
(RangeCheckType::ID, 6),
(BitwiseType::ID, 5),
(EcOpType::ID, 4),
(PoseidonType::ID, 3),
]);
let mut vecs = vec![];
let mut ap_offset: i16 = 0;
for arg in args {
let Arg::Array(values) = arg else { continue };
vecs.push(ap_offset);
casm_extend! {ctx,
%{ memory[ap + 0] = segments.add() %}
ap += 1;
}
for (i, v) in values.iter().enumerate() {
let arr_at = (i + 1) as i16;
casm_extend! {ctx,
[ap + 0] = (v.to_bigint());
[ap + 0] = [[ap - arr_at] + (i as i16)], ap++;
};
}
ap_offset += (1 + values.len()) as i16;
}
let after_vecs_offset = ap_offset;
if func
.signature
.param_types
.iter()
.any(|ty| self.get_info(ty).long_id.generic_id == SegmentArenaType::ID)
{
casm_extend! {ctx,
%{ memory[ap + 0] = segments.add() %}
%{ memory[ap + 1] = segments.add() %}
ap += 2;
[ap + 0] = 0, ap++;
[ap - 2] = [[ap - 3]];
[ap - 1] = [[ap - 3] + 1];
[ap - 1] = [[ap - 3] + 2];
}
ap_offset += 3;
}
for ty in func.signature.param_types.iter() {
let info = self.get_info(ty);
let generic_ty = &info.long_id.generic_id;
if let Some(offset) = builtin_offset.get(generic_ty) {
casm_extend! {ctx,
[ap + 0] = [fp - offset], ap++;
}
} else if generic_ty == &SystemType::ID {
casm_extend! {ctx,
%{ memory[ap + 0] = segments.add() %}
ap += 1;
}
} else if generic_ty == &GasBuiltinType::ID {
casm_extend! {ctx,
[ap + 0] = initial_gas, ap++;
}
} else if generic_ty == &SegmentArenaType::ID {
let offset = -ap_offset + after_vecs_offset;
casm_extend! {ctx,
[ap + 0] = [ap + offset] + 3, ap++;
}
} else if let Some(Arg::Array(_)) = arg_iter.peek() {
let values = extract_matches!(arg_iter.next().unwrap(), Arg::Array);
let offset = -ap_offset + vecs.pop().unwrap();
expected_arguments_size += 1;
casm_extend! {ctx,
[ap + 0] = [ap + (offset)], ap++;
[ap + 0] = [ap - 1] + (values.len()), ap++;
}
} else {
let arg_size = info.size;
expected_arguments_size += arg_size as usize;
for _ in 0..arg_size {
if let Some(value) = arg_iter.next() {
let value = extract_matches!(value, Arg::Value);
casm_extend! {ctx,
[ap + 0] = (value.to_bigint()), ap++;
}
}
}
};
ap_offset += info.size;
}
if expected_arguments_size != args.len() {
return Err(RunnerError::ArgumentsSizeMismatch {
expected: expected_arguments_size,
actual: args.len(),
});
}
let before_final_call = ctx.current_code_offset;
let final_call_size = 3;
let offset = final_call_size
+ self.casm_program.debug_info.sierra_statement_info[func.entry_point.0].code_offset;
casm_extend! {ctx,
call rel offset;
ret;
}
assert_eq!(before_final_call + final_call_size, ctx.current_code_offset);
Ok((ctx.instructions, builtins))
}
fn get_initial_available_gas(
&self,
func: &Function,
available_gas: Option<usize>,
) -> Result<usize, RunnerError> {
if self.metadata.gas_info.function_costs.is_empty() {
return Ok(0);
}
let Some(available_gas) = available_gas else { return Ok(0); };
let required_gas = self.metadata.gas_info.function_costs[func.id.clone()]
.iter()
.map(|(cost_token_type, val)| {
let val_usize: usize = (*val).try_into().unwrap();
let token_cost = if *cost_token_type == CostTokenType::Const {
1
} else {
DUMMY_BUILTIN_GAS_COST
};
val_usize * token_cost
})
.sum();
available_gas.checked_sub(required_gas).ok_or(RunnerError::NotEnoughGasToCall)
}
pub fn create_code_footer(&self) -> Vec<Instruction> {
casm! {
ret;
}
.instructions
}
}
fn create_metadata(
sierra_program: &cairo_lang_sierra::program::Program,
metadata_config: Option<MetadataComputationConfig>,
) -> Result<Metadata, RunnerError> {
if let Some(metadata_config) = metadata_config {
calc_metadata(sierra_program, metadata_config).map_err(|err| match err {
MetadataError::ApChangeError(err) => RunnerError::ApChangeError(err),
MetadataError::CostError(_) => RunnerError::FailedGasCalculation,
})
} else {
Ok(Metadata {
ap_change_info: calc_ap_changes(sierra_program, |_, _| 0)?,
gas_info: GasInfo {
variable_values: Default::default(),
function_costs: Default::default(),
},
})
}
}