Skip to main content

cairo_lang_sierra_gas/
lib.rs

1//! Sierra gas computation.
2//!
3//! This crate provides the gas computation for the Cairo programs.
4
5use cairo_lang_eq_solver::Expr;
6use cairo_lang_sierra::extensions::circuit::{CircuitInfo, CircuitTypeConcrete, ConcreteCircuit};
7use cairo_lang_sierra::extensions::core::{
8    CoreConcreteLibfunc, CoreLibfunc, CoreType, CoreTypeConcrete,
9};
10use cairo_lang_sierra::extensions::coupon::CouponConcreteLibfunc;
11use cairo_lang_sierra::extensions::gas::{CostTokenMap, CostTokenType, GasConcreteLibfunc};
12use cairo_lang_sierra::ids::{ConcreteLibfuncId, ConcreteTypeId, FunctionId};
13use cairo_lang_sierra::program::{Program, Statement, StatementIdx};
14use cairo_lang_sierra::program_registry::{ProgramRegistry, ProgramRegistryError};
15use cairo_lang_sierra_type_size::{ProgramRegistryInfo, TypeSizeMap};
16use cairo_lang_utils::casts::IntoOrPanic;
17use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
18use cairo_lang_utils::unordered_hash_set::UnorderedHashSet;
19use compute_costs::PostCostTypeEx;
20use core_libfunc_cost_base::InvocationCostInfoProvider;
21use core_libfunc_cost_expr::CostExprMap;
22use cost_expr::Var;
23use gas_info::GasInfo;
24use generate_equations::StatementFutureCost;
25use itertools::Itertools;
26use objects::CostInfoProvider;
27use thiserror::Error;
28
29pub mod compute_costs;
30pub mod core_libfunc_cost;
31mod core_libfunc_cost_base;
32mod core_libfunc_cost_expr;
33mod cost_expr;
34pub mod gas_info;
35mod generate_equations;
36pub mod objects;
37mod starknet_libfunc_cost_base;
38
39#[cfg(test)]
40mod test;
41
42/// Error occurring while calculating the costing of a program's variables.
43#[derive(Error, Debug, Eq, PartialEq)]
44pub enum CostError {
45    #[error("error from the program registry")]
46    ProgramRegistryError(#[from] Box<ProgramRegistryError>),
47    #[error("failed solving the symbol tables")]
48    SolvingGasEquationFailed,
49    #[error("found an unexpected cycle during cost computation")]
50    UnexpectedCycle,
51    #[error("failed to enforce function cost")]
52    EnforceWalletValueFailed(StatementIdx),
53}
54
55/// Helper to implement the `InvocationCostInfoProvider` for the equation generation.
56struct InvocationCostInfoProviderForEqGen<
57    'a,
58    TokenUsages: Fn(CostTokenType) -> usize,
59    ApChangeVarValue: Fn() -> usize,
60> {
61    /// Registry for providing the sizes of the types.
62    type_sizes: &'a TypeSizeMap,
63    /// Closure providing the token usages for the invocation.
64    token_usages: TokenUsages,
65    /// Closure providing the ap changes for the invocation.
66    ap_change_var_value: ApChangeVarValue,
67}
68
69impl<TokenUsages: Fn(CostTokenType) -> usize, ApChangeVarValue: Fn() -> usize>
70    InvocationCostInfoProvider
71    for InvocationCostInfoProviderForEqGen<'_, TokenUsages, ApChangeVarValue>
72{
73    fn type_size(&self, ty: &ConcreteTypeId) -> usize {
74        self.type_sizes[ty].into_or_panic()
75    }
76
77    fn token_usages(&self, token_type: CostTokenType) -> usize {
78        (self.token_usages)(token_type)
79    }
80
81    fn ap_change_var_value(&self) -> usize {
82        (self.ap_change_var_value)()
83    }
84
85    fn circuit_info(&self, _ty: &ConcreteTypeId) -> &CircuitInfo {
86        unimplemented!("circuits are not supported for old gas solver");
87    }
88}
89
90/// Calculates gas pre-cost information for a given program - the gas costs of non-step tokens.
91///
92/// Note that this calculation is deprecated and is only done for backwards compatibility with old
93/// Sierra classes. Generally, the new [compute_precost_info] is used.
94pub fn calc_gas_precost_info(
95    program: &Program,
96    program_info: &ProgramRegistryInfo,
97    function_set_costs: OrderedHashMap<FunctionId, CostTokenMap<i32>>,
98) -> Result<GasInfo, CostError> {
99    let cost_provider = ComputeCostInfoProvider::new(program_info);
100    let mut info = calc_gas_info_inner(
101        program,
102        |statement_future_cost, idx, libfunc_id| -> Vec<CostTokenMap<Expr<Var>>> {
103            let libfunc = program_info
104                .registry
105                .get_libfunc(libfunc_id)
106                .expect("Program registry creation would have already failed.");
107            core_libfunc_cost_expr::core_libfunc_precost_expr(
108                statement_future_cost,
109                idx,
110                libfunc,
111                &cost_provider,
112            )
113        },
114        function_set_costs,
115        &program_info.registry,
116    )?;
117    // Make `withdraw_gas` and `refund` libfuncs return 0 valued variables for all tokens.
118    for (i, statement) in program.statements.iter().enumerate() {
119        let Statement::Invocation(invocation) = statement else {
120            continue;
121        };
122        let Ok(libfunc) = program_info.registry.get_libfunc(&invocation.libfunc_id) else {
123            continue;
124        };
125        let is_withdraw_gas =
126            matches!(libfunc, CoreConcreteLibfunc::Gas(GasConcreteLibfunc::WithdrawGas(_)));
127        let is_refund =
128            matches!(libfunc, CoreConcreteLibfunc::Coupon(CouponConcreteLibfunc::Refund(_)));
129        if is_withdraw_gas || is_refund {
130            for token in CostTokenType::iter_precost() {
131                // Check that the variable was not assigned a value, and set it to zero.
132                assert_eq!(info.variable_values.insert((StatementIdx(i), *token), 0), None);
133            }
134        }
135    }
136    Ok(info)
137}
138
139/// Info provider used for the computation of libfunc costs.
140struct ComputeCostInfoProvider<'a> {
141    pub program_info: &'a ProgramRegistryInfo,
142}
143
144impl<'a> ComputeCostInfoProvider<'a> {
145    fn new(program_info: &'a ProgramRegistryInfo) -> Self {
146        Self { program_info }
147    }
148}
149
150/// Implementation of [CostInfoProvider] for [ComputeCostInfoProvider].
151impl CostInfoProvider for ComputeCostInfoProvider<'_> {
152    fn type_size(&self, ty: &ConcreteTypeId) -> usize {
153        self.program_info.type_sizes[ty].into_or_panic()
154    }
155
156    fn circuit_info(&self, ty: &ConcreteTypeId) -> &CircuitInfo {
157        let CoreTypeConcrete::Circuit(CircuitTypeConcrete::Circuit(ConcreteCircuit {
158            circuit_info,
159            ..
160        })) = self.program_info.registry.get_type(ty).unwrap()
161        else {
162            panic!("Expected a circuit type, got {ty:?}.")
163        };
164        circuit_info
165    }
166}
167
168/// Calculates gas pre-cost information for a given program - the gas costs of non-step tokens.
169pub fn compute_precost_info(
170    program: &Program,
171    program_info: &ProgramRegistryInfo,
172) -> Result<GasInfo, CostError> {
173    let cost_provider = ComputeCostInfoProvider::new(program_info);
174    compute_costs::compute_costs(
175        program,
176        &(|libfunc_id| {
177            let core_libfunc = cost_provider
178                .program_info
179                .registry
180                .get_libfunc(libfunc_id)
181                .expect("Program registry creation would have already failed.");
182            core_libfunc_cost_base::core_libfunc_cost(core_libfunc, &cost_provider)
183        }),
184        &compute_costs::PreCostContext {},
185        &Default::default(),
186    )
187}
188
189/// Calculates gas postcost information for a given program - the gas costs of step token.
190///
191/// Note that this calculation is deprecated and is only done for backwards compatibility with old
192/// Sierra classes. Generally, the new [compute_postcost_info] is used.
193pub fn calc_gas_postcost_info<ApChangeVarValue: Fn(StatementIdx) -> usize>(
194    program: &Program,
195    program_info: &ProgramRegistryInfo,
196    function_set_costs: OrderedHashMap<FunctionId, CostTokenMap<i32>>,
197    precost_gas_info: &GasInfo,
198    ap_change_var_value: ApChangeVarValue,
199) -> Result<GasInfo, CostError> {
200    let mut info = calc_gas_info_inner(
201        program,
202        |statement_future_cost, idx, libfunc_id| {
203            let libfunc = program_info
204                .registry
205                .get_libfunc(libfunc_id)
206                .expect("Program registry creation would have already failed.");
207            core_libfunc_cost_expr::core_libfunc_postcost_expr(
208                statement_future_cost,
209                idx,
210                libfunc,
211                &InvocationCostInfoProviderForEqGen {
212                    type_sizes: &program_info.type_sizes,
213                    token_usages: |token_type| {
214                        precost_gas_info.variable_values[&(idx, token_type)].into_or_panic()
215                    },
216                    ap_change_var_value: || ap_change_var_value(idx),
217                },
218            )
219        },
220        function_set_costs,
221        &program_info.registry,
222    )?;
223    // Make `refund` libfuncs return 0 valued variables for all tokens.
224    for (i, statement) in program.statements.iter().enumerate() {
225        let Statement::Invocation(invocation) = statement else {
226            continue;
227        };
228        let Ok(libfunc) = program_info.registry.get_libfunc(&invocation.libfunc_id) else {
229            continue;
230        };
231        let is_refund =
232            matches!(libfunc, CoreConcreteLibfunc::Coupon(CouponConcreteLibfunc::Refund(_)));
233        if is_refund {
234            // Check that the variable was not assigned a value, and set it to zero.
235            assert_eq!(
236                info.variable_values.insert((StatementIdx(i), CostTokenType::Const), 0),
237                None
238            );
239        }
240    }
241    Ok(info)
242}
243
244/// Calculates gas information. Used for both precost and postcost.
245fn calc_gas_info_inner<
246    GetCost: Fn(&mut dyn StatementFutureCost, StatementIdx, &ConcreteLibfuncId) -> Vec<CostExprMap>,
247>(
248    program: &Program,
249    get_cost: GetCost,
250    function_set_costs: OrderedHashMap<FunctionId, CostTokenMap<i32>>,
251    registry: &ProgramRegistry<CoreType, CoreLibfunc>,
252) -> Result<GasInfo, CostError> {
253    let mut equations = generate_equations::generate_equations(program, get_cost)?;
254    let non_set_cost_func_entry_points: UnorderedHashSet<_> = program
255        .funcs
256        .iter()
257        .filter(|f| !function_set_costs.contains_key(&f.id))
258        .map(|f| f.entry_point)
259        .collect();
260    for (func_id, cost_terms) in function_set_costs {
261        for token_type in CostTokenType::iter_casm_tokens() {
262            equations.get_mut(token_type).unwrap().push(
263                Expr::from_var(Var::StatementFuture(
264                    registry.get_function(&func_id)?.entry_point,
265                    *token_type,
266                )) - Expr::from_const(cost_terms.get(token_type).copied().unwrap_or_default()),
267            );
268        }
269    }
270
271    let mut variable_values = OrderedHashMap::default();
272    let mut function_costs = OrderedHashMap::default();
273    for (token_type, token_equations) in equations {
274        // Setting up minimization vars with three ranks:
275        // 1. Minimizing function costs variables.
276        // 2. Minimizing gas withdraw variables.
277        // 3. Minimizing branch align (burn gas) variables.
278        // We use this ordering to solve several issues:
279        // * In cases where we have a function with a set cost, that calls another function, and
280        //   then several calls to branch align, the inner function's price may be increased in
281        //   order to reduce the value of the burn gas variables, although we would prefer that the
282        //   function's value would be reduced (since it may be called from another point as well).
283        //   Therefore we should optimize over function costs before optimizing over branch aligns.
284        // * In cases where we have a function with an unset cost - that call `withdraw_gas` we can
285        //   decide to make the function pricier to reduce the amount of withdrawn gas. Therefore we
286        //   should optimize over function costs before optimizing over withdraw variables.
287        // * Generally we would of course prefer optimizing over withdraw variables before branch
288        //   align variables, as they cost gas to the user.
289        let mut minimization_vars = vec![vec![], vec![], vec![]];
290        for v in token_equations.iter().flat_map(|eq| eq.var_to_coef.keys()).unique() {
291            minimization_vars[match v {
292                Var::LibfuncImplicitGasVariable(idx, _) => {
293                    match program.get_statement(*idx).unwrap() {
294                        Statement::Invocation(invocation) => {
295                            match registry.get_libfunc(&invocation.libfunc_id).unwrap() {
296                                CoreConcreteLibfunc::BranchAlign(_) => 2,
297                                CoreConcreteLibfunc::Gas(GasConcreteLibfunc::WithdrawGas(_)) => 1,
298                                CoreConcreteLibfunc::Gas(
299                                    GasConcreteLibfunc::BuiltinWithdrawGas(_),
300                                ) => 0,
301                                // TODO(orizi): Make this actually maximized.
302                                CoreConcreteLibfunc::Gas(GasConcreteLibfunc::RedepositGas(_)) => {
303                                    continue;
304                                }
305                                _ => unreachable!(
306                                    "Gas variables cannot originate from {}.",
307                                    invocation.libfunc_id
308                                ),
309                            }
310                        }
311                        Statement::Return(_) => continue,
312                    }
313                }
314                Var::StatementFuture(idx, _) if non_set_cost_func_entry_points.contains(idx) => 0,
315                Var::StatementFuture(_, _) => {
316                    continue;
317                }
318            }]
319            .push(v.clone())
320        }
321        let solution =
322            cairo_lang_eq_solver::try_solve_equations(token_equations, minimization_vars)
323                .ok_or(CostError::SolvingGasEquationFailed)?;
324        for func in &program.funcs {
325            let id = &func.id;
326            if !function_costs.contains_key(id) {
327                function_costs.insert(id.clone(), CostTokenMap::default());
328            }
329            // The `None` case is of a function that can never actually be called, as it has no
330            // return, so solver for it would not actually be calculated. (Such a function may exist
331            // by receiving a never type and matching on it) The cost of the function is considered
332            // as 0.
333            if let Some(value) = solution.get(&Var::StatementFuture(func.entry_point, token_type))
334                && *value != 0
335            {
336                function_costs.get_mut(id).unwrap().insert(token_type, *value);
337            }
338        }
339        for (var, value) in solution {
340            if let Var::LibfuncImplicitGasVariable(idx, var_token_type) = var {
341                assert_eq!(
342                    token_type, var_token_type,
343                    "Unexpected variable of type {var_token_type:?} while handling {token_type:?}."
344                );
345                variable_values.insert((idx, var_token_type), value);
346            }
347        }
348    }
349    Ok(GasInfo { variable_values, function_costs })
350}
351
352/// Calculates gas postcost information for a given program.
353///
354/// `CostType` can be either `i32` to get the total gas cost for CASM generation,
355/// or `ConstCost` to get the separate components (steps, holes, range-checks).
356pub fn compute_postcost_info<CostType: PostCostTypeEx>(
357    program: &Program,
358    program_info: &ProgramRegistryInfo,
359    get_ap_change_fn: &impl Fn(StatementIdx) -> usize,
360    precost_gas_info: &GasInfo,
361    enforced_function_costs: &OrderedHashMap<FunctionId, CostType>,
362) -> Result<GasInfo, CostError> {
363    let cost_provider = ComputeCostInfoProvider::new(program_info);
364    let specific_cost_context =
365        compute_costs::PostcostContext { get_ap_change_fn, precost_gas_info };
366    compute_costs::compute_costs(
367        program,
368        &(|libfunc_id| {
369            let core_libfunc = cost_provider
370                .program_info
371                .registry
372                .get_libfunc(libfunc_id)
373                .expect("Program registry creation would have already failed.");
374            core_libfunc_cost_base::core_libfunc_cost(core_libfunc, &cost_provider)
375        }),
376        &specific_cost_context,
377        &enforced_function_costs
378            .iter()
379            .map(|(func, val)| {
380                (
381                    cost_provider
382                        .program_info
383                        .registry
384                        .get_function(func)
385                        .expect("Program registry creation would have already failed.")
386                        .entry_point,
387                    *val,
388                )
389            })
390            .collect(),
391    )
392}