Skip to content

Commit 8aafedc

Browse files
authored
feat: use the actual class info (#269)
Currently when creating the `ClassInfo` struct from Blockifier, the values used aren't accurate. First, the sierra version is actually the CASM compiler version. The sierra version is encoded into the `sierra_program` field of a Starknet contract class artifact. The `sierra_program` is a list of field elements and the first six elements are reserved for the compilers - first 3 for sierra compiler version and last 3 for casm compiler version. Fyi, Blockifier performs a sierra version check when executing a contract execution to determine what resource to [track]. Second, we are using mock values for both sierra program length and abi length. I'm not exactly sure what's the specification of the ABI length, but I've referenced with the official StarkWare's [implementation] and it seems to be the length of the ABI in its RPC format i.e., a string of JSON. The implementation for getting the ABI length is not optimized and would require to be computed every time the class is fetched from database -> class cache. These values are important for ensuring deterministic executions as they affect transaction execution as well as fees. [track]: https://github.com/dojoengine/sequencer/blob/3c206344dc64b5a0f150ea94cd2623b2a9a7da63/crates/blockifier/src/execution/contract_class.rs#L258 [implementation]: https://github.com/dojoengine/sequencer/blob/3c206344dc64b5a0f150ea94cd2623b2a9a7da63/crates/apollo_rpc/src/v0_8/api/mod.rs#L453-L477
1 parent 699a81e commit 8aafedc

File tree

5 files changed

+155
-45
lines changed

5 files changed

+155
-45
lines changed

crates/executor/src/implementation/blockifier/cache.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
use std::str::FromStr;
21
use std::sync::{Arc, OnceLock};
32

43
use blockifier::execution::contract_class::{CompiledClassV1, RunnableCompiledClass};
54
use katana_primitives::class::{ClassHash, CompiledClass, ContractClass};
65
use quick_cache::sync::Cache;
7-
use starknet_api::contract_class::SierraVersion;
86

97
use super::utils::to_class;
108

@@ -256,11 +254,12 @@ impl ClassCache {
256254
#[cfg(feature = "native")]
257255
let entry_points = sierra.entry_points_by_type.clone();
258256

257+
let version = class.sierra_version();
258+
259259
let CompiledClass::Class(casm) = class.compile().unwrap() else {
260260
unreachable!("cant be legacy")
261261
};
262262

263-
let version = SierraVersion::from_str(&casm.compiler_version).unwrap();
264263
let compiled = CompiledClassV1::try_from((casm, version.clone())).unwrap();
265264

266265
#[cfg(feature = "native")]

crates/executor/src/implementation/blockifier/call.rs

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::str::FromStr;
22
use std::sync::Arc;
33

4+
use blockifier::blockifier_versioned_constants::VersionedConstants;
5+
use blockifier::bouncer::n_steps_to_sierra_gas;
46
use blockifier::context::{BlockContext, TransactionContext};
57
use blockifier::execution::call_info::CallInfo;
68
use blockifier::execution::entry_point::{
@@ -15,7 +17,7 @@ use blockifier::state::cached_state::CachedState;
1517
use blockifier::state::state_api::StateReader;
1618
use blockifier::transaction::objects::{DeprecatedTransactionInfo, TransactionInfo};
1719
use cairo_vm::vm::runners::cairo_runner::RunResources;
18-
use katana_primitives::execution::FunctionCall;
20+
use katana_primitives::execution::{FunctionCall, TrackedResource};
1921
use katana_primitives::Felt;
2022
use starknet_api::core::EntryPointSelector;
2123
use starknet_api::execution_resources::GasAmount;
@@ -25,6 +27,8 @@ use super::utils::to_blk_address;
2527
use crate::ExecutionError;
2628

2729
/// Perform a function call on a contract and retrieve the return values.
30+
///
31+
/// The `max_gas` is the maximum amount of Sierra gas to allocate for the call.
2832
pub fn execute_call<S: StateReader>(
2933
request: FunctionCall,
3034
state: &mut CachedState<S>,
@@ -39,10 +43,10 @@ fn execute_call_inner<S: StateReader>(
3943
request: FunctionCall,
4044
state: &mut CachedState<S>,
4145
block_context: Arc<BlockContext>,
42-
max_gas: u64,
46+
max_sierra_gas: u64,
4347
) -> EntryPointExecutionResult<CallInfo> {
4448
let call = CallEntryPoint {
45-
initial_gas: max_gas,
49+
initial_gas: max_sierra_gas,
4650
calldata: Calldata(Arc::new(request.calldata)),
4751
storage_address: to_blk_address(request.contract_address),
4852
entry_point_selector: EntryPointSelector(request.entry_point_selector),
@@ -67,20 +71,30 @@ fn execute_call_inner<S: StateReader>(
6771
tx_info: TransactionInfo::Deprecated(DeprecatedTransactionInfo::default()),
6872
});
6973

70-
let sierra_revert_tracker = SierraGasRevertTracker::new(GasAmount(max_gas));
74+
let sierra_revert_tracker = SierraGasRevertTracker::new(GasAmount(max_sierra_gas));
7175
let mut ctx = EntryPointExecutionContext::new_invoke(
7276
tx_context,
7377
limit_steps_by_resources,
7478
sierra_revert_tracker,
7579
);
7680

77-
// manually override the run resources
78-
// If `initial_gas` can't fit in a usize, use the maximum.
79-
ctx.vm_run_resources = RunResources::new(max_gas.try_into().unwrap_or(usize::MAX));
81+
let versioned_constants = block_context.versioned_constants();
82+
let l2_gas_per_step = versioned_constants.os_constants.gas_costs.base.step_gas_cost;
83+
84+
// Convert the sierra gas to cairo steps for the run resources.
85+
//
86+
// If values can't fit in a usize, use the maximum. This is to mimic the behaviour of
87+
// blockifier.
88+
//
89+
// For reference: https://github.com/dojoengine/sequencer/blob/5d737b9c90a14bdf4483d759d1a1d4ce64aa9fd2/crates/blockifier/src/execution/entry_point.rs#L382C1-L451
90+
let n_steps = max_sierra_gas.saturating_div(l2_gas_per_step);
91+
let n_steps = n_steps.try_into().unwrap_or(usize::MAX);
92+
ctx.vm_run_resources = RunResources::new(n_steps);
93+
8094
let mut remaining_gas = call.initial_gas;
8195
let call_info = call.execute(state, &mut ctx, &mut remaining_gas)?;
8296

83-
// In Starknet 0.13.4 calls return error as return data (Ok variant) instead of returning as an
97+
// Calls return error as the return data (Ok variant) instead of returning as an
8498
// Err. Refer to: https://github.com/dojoengine/sequencer/blob/5d737b9c90a14bdf4483d759d1a1d4ce64aa9fd2/crates/blockifier/src/execution/execution_utils.rs#L74
8599
if call_info.execution.failed {
86100
match call_info.execution.retdata.0.as_slice() {
@@ -102,6 +116,36 @@ fn execute_call_inner<S: StateReader>(
102116
Ok(call_info)
103117
}
104118

119+
/// Returns the Sierra gas consumed by the contract call execution.
120+
///
121+
/// Depending on the the Sierra compiler version of the class, the resources used for the contract
122+
/// call execution may either be tracked as Cairo steps or Sierra gas. Note that `blockifier`
123+
/// doesn't store the actual tracked resource consumed value in a single variable of the `CallInfo`
124+
/// struct but as separate variables i.e., `gas_consumed` of [`CallExecution`] and `n_steps` of
125+
/// [`ExecutionResources`] for Sierra gas and Cairo steps respectively.
126+
///
127+
/// For reference: https://github.com/dojoengine/sequencer/blob/5d737b9c90a14bdf4483d759d1a1d4ce64aa9fd2/crates/blockifier/src/execution/entry_point_execution.rs#L367-L433
128+
///
129+
/// [`CallExecution`]: blockifier::execution::call_info::CallExecution
130+
/// [`ExecutionResources`]: cairo_vm::vm::runners::cairo_runner::ExecutionResources
131+
pub fn get_call_sierra_gas_consumed(
132+
info: &CallInfo,
133+
versioned_constant: &VersionedConstants,
134+
) -> u64 {
135+
match info.tracked_resource {
136+
// If the execution is tracked using Cairo steps, the sierra gas isn't counted
137+
// ie `info.execution.gas_consumed` is set to 0. So, we have to convert the cairo
138+
// steps to sierra gas.
139+
//
140+
// https://github.com/dojoengine/sequencer/blob/5d737b9c90a14bdf4483d759d1a1d4ce64aa9fd2/crates/blockifier/src/execution/entry_point_execution.rs#L475-L479
141+
TrackedResource::CairoSteps => {
142+
n_steps_to_sierra_gas(info.resources.n_steps, versioned_constant).0
143+
}
144+
145+
TrackedResource::SierraGas => info.execution.gas_consumed,
146+
}
147+
}
148+
105149
#[cfg(test)]
106150
mod tests {
107151
use std::str::FromStr;
@@ -118,7 +162,7 @@ mod tests {
118162
use katana_provider::test_utils;
119163
use starknet::macros::selector;
120164

121-
use super::execute_call_inner;
165+
use super::{execute_call_inner, get_call_sierra_gas_consumed};
122166
use crate::implementation::blockifier::state::StateProviderDb;
123167

124168
#[test]
@@ -160,8 +204,10 @@ mod tests {
160204
// ~900,000 gas
161205
req.calldata = vec![felt!("460")];
162206
let info = execute_call_inner(req.clone(), &mut state, ctx.clone(), max_gas_1).unwrap();
207+
let gas_consumed = get_call_sierra_gas_consumed(&info, ctx.versioned_constants());
208+
163209
assert!(!info.execution.failed);
164-
assert!(max_gas_1 >= info.execution.gas_consumed);
210+
assert!(max_gas_1 >= gas_consumed);
165211

166212
req.calldata = vec![felt!("600")];
167213
let result = execute_call_inner(req.clone(), &mut state, ctx.clone(), max_gas_1);
@@ -170,14 +216,16 @@ mod tests {
170216

171217
let max_gas_2 = 10_000_000;
172218
{
173-
// rougly equivalent to 9,000,000 gas
219+
// roughly equivalent to 9,000,000 gas
174220
req.calldata = vec![felt!("4600")];
175221
let info = execute_call_inner(req.clone(), &mut state, ctx.clone(), max_gas_2).unwrap();
222+
let gas_consumed = get_call_sierra_gas_consumed(&info, ctx.versioned_constants());
223+
176224
assert!(!info.execution.failed);
177-
assert!(max_gas_2 >= info.execution.gas_consumed);
178-
assert!(max_gas_1 < info.execution.gas_consumed);
225+
assert!(max_gas_2 >= gas_consumed);
226+
assert!(max_gas_1 < gas_consumed);
179227

180-
req.calldata = vec![felt!("5000")];
228+
req.calldata = vec![felt!("6000")];
181229
let result = execute_call_inner(req.clone(), &mut state, ctx.clone(), max_gas_2);
182230
assert!(result.is_err(), "should fail due to out of run resources")
183231
}
@@ -186,9 +234,11 @@ mod tests {
186234
{
187235
req.calldata = vec![felt!("47000")];
188236
let info = execute_call_inner(req.clone(), &mut state, ctx.clone(), max_gas_3).unwrap();
237+
let gas_consumed = get_call_sierra_gas_consumed(&info, ctx.versioned_constants());
238+
189239
assert!(!info.execution.failed);
190-
assert!(max_gas_3 >= info.execution.gas_consumed);
191-
assert!(max_gas_2 < info.execution.gas_consumed);
240+
assert!(max_gas_3 >= gas_consumed);
241+
assert!(max_gas_2 < gas_consumed);
192242

193243
req.calldata = vec![felt!("60000")];
194244
let result = execute_call_inner(req.clone(), &mut state, ctx.clone(), max_gas_3);

crates/executor/src/implementation/blockifier/utils.rs

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ pub fn to_executor_tx(mut tx: ExecutableTxWithHash, mut flags: ExecutionFlags) -
326326
},
327327

328328
ExecutableTx::Declare(tx) => {
329-
let compiled = tx.class.as_ref().clone().compile().expect("failed to compile");
329+
let class = tx.class.as_ref().clone();
330330

331331
let tx = match tx.transaction {
332332
DeclareTx::V0(tx) => ApiDeclareTransaction::V0(DeclareTransactionV0V1 {
@@ -384,7 +384,7 @@ pub fn to_executor_tx(mut tx: ExecutableTxWithHash, mut flags: ExecutionFlags) -
384384
};
385385

386386
let tx_hash = TransactionHash(hash);
387-
let class_info = to_class_info(compiled).unwrap();
387+
let class_info = to_class_info(class).unwrap();
388388
Transaction::Account(AccountTransaction {
389389
tx: ExecTx::Declare(DeclareTransaction { class_info, tx_hash, tx }),
390390
execution_flags: flags.into(),
@@ -623,33 +623,27 @@ pub fn to_blk_chain_id(chain_id: katana_primitives::chain::ChainId) -> ChainId {
623623
}
624624
}
625625

626-
pub fn to_class_info(class: class::CompiledClass) -> Result<ClassInfo, ProgramError> {
626+
pub fn to_class_info(class: class::ContractClass) -> Result<ClassInfo, ProgramError> {
627627
use starknet_api::contract_class::ContractClass;
628628

629-
// TODO: @kariy not sure of the variant that must be used in this case. Should we change the
630-
// return type to include this case of error for contract class conversions?
631-
match class {
632-
class::CompiledClass::Legacy(legacy) => {
633-
// For cairo 0, the sierra_program_length must be 0.
634-
Ok(ClassInfo::new(&ContractClass::V0(legacy), 0, 0, SierraVersion::DEPRECATED).unwrap())
629+
let sierra_version = class.sierra_version();
630+
let sierra_program_len = class.sierra_program_len();
631+
let abi_len = class.abi_len();
632+
633+
let compiled = class.compile().unwrap();
634+
635+
match compiled {
636+
class::CompiledClass::Legacy(casm) => {
637+
// It's safe to unwrap here because `ClassInfo::new` will only fail if the
638+
// sierra_program_len != 0 for legacy classes and `class.sierra_program_len()` will
639+
// always return 0 for legacy classes.
640+
let casm = ContractClass::V0(casm);
641+
Ok(ClassInfo::new(&casm, sierra_program_len, abi_len, sierra_version).unwrap())
635642
}
636643

637-
class::CompiledClass::Class(sierra) => {
638-
// NOTE:
639-
//
640-
// Right now, we're using dummy values for the sierra class info (ie
641-
// sierra_program_length, and abi_length). This value affects the fee
642-
// calculation so we should use the correct values based on the sierra class itself.
643-
//
644-
// Make sure these values are the same over on `snos` when it re-executes the
645-
// transactions as otherwise the fees would be different.
646-
647-
let version = SierraVersion::from_str(&sierra.compiler_version).unwrap();
648-
let class = ContractClass::V1((sierra, version.clone()));
649-
let sierra_program_length = 1;
650-
let abi_length = 0;
651-
652-
Ok(ClassInfo::new(&class, sierra_program_length, abi_length, version).unwrap())
644+
class::CompiledClass::Class(casm) => {
645+
let class = ContractClass::V1((casm, sierra_version.clone()));
646+
Ok(ClassInfo::new(&class, sierra_program_len, abi_len, sierra_version).unwrap())
653647
}
654648
}
655649
}

crates/primitives/src/class.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ use std::str::FromStr;
22

33
use cairo_lang_starknet_classes::abi;
44
use cairo_lang_starknet_classes::casm_contract_class::StarknetSierraCompilationError;
5-
use cairo_lang_starknet_classes::contract_class::{ContractEntryPoint, ContractEntryPoints};
5+
use cairo_lang_starknet_classes::contract_class::{
6+
version_id_from_serialized_sierra_program, ContractEntryPoint, ContractEntryPoints,
7+
};
68
use serde_json_pythonic::to_string_pythonic;
79
use starknet::macros::short_string;
10+
use starknet_api::contract_class::SierraVersion;
811
use starknet_types_core::hash::{Poseidon, StarkHash};
912

1013
use crate::utils::{normalize_address, starknet_keccak};
@@ -98,6 +101,58 @@ impl ContractClass {
98101
_ => None,
99102
}
100103
}
104+
105+
/// Returns the version of the Sierra program of this class.
106+
pub fn sierra_version(&self) -> SierraVersion {
107+
match self {
108+
Self::Class(class) => {
109+
// The sierra program is an array of field elements and the first six elements are
110+
// reserved for the compilers version. The array is structured as follows:
111+
//
112+
// ┌──────────────────────────────────────┐
113+
// │ Idx │ Content │
114+
// ┌──────────────────────────────────────┐
115+
// │ 0 │ Sierra major version │
116+
// │ 1 │ Sierra minor version │
117+
// │ 2 │ Sierra patch version │
118+
// │ 3 │ CASM compiler major version │
119+
// │ 4 │ CASM compiler minor version │
120+
// │ 5 │ CASM compiler patch version │
121+
// │ 6+ │ Program data │
122+
// └──────────────────────────────────────┘
123+
//
124+
125+
let version = version_id_from_serialized_sierra_program(&class.sierra_program)
126+
.map(|(sierra_id, _)| sierra_id)
127+
.expect("invalid sierra program: failed to get version id from sierra program");
128+
129+
SierraVersion::new(
130+
version.major.try_into().unwrap(),
131+
version.minor.try_into().unwrap(),
132+
version.patch.try_into().unwrap(),
133+
)
134+
}
135+
136+
Self::Legacy(..) => SierraVersion::DEPRECATED,
137+
}
138+
}
139+
140+
/// Returns the length of the Sierra program.
141+
pub fn sierra_program_len(&self) -> usize {
142+
match self {
143+
Self::Class(class) => class.sierra_program.len(),
144+
// For cairo 0, the sierra_program_length must be 0.
145+
Self::Legacy(..) => 0,
146+
}
147+
}
148+
149+
// TODO(kariy): document the actual definition of the ABI length here.
150+
pub fn abi_len(&self) -> usize {
151+
match self {
152+
Self::Class(class) => to_string_pythonic(&class.abi.as_ref()).unwrap().len(),
153+
Self::Legacy(..) => 0,
154+
}
155+
}
101156
}
102157

103158
#[derive(Debug, thiserror::Error)]

crates/rpc/rpc/src/starknet/config.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ pub struct StarknetApiConfig {
1010
/// If `None`, the maximum keys size is bounded by [`u64::MAX`].
1111
pub max_proof_keys: Option<u64>,
1212

13+
/// Maximum Sierra gas for contract calls.
14+
///
15+
/// The maximum amount of execution resources allocated for a contract call via `starknet_call`
16+
/// method. If `None,` defaults to `1,000,000,000`.
17+
///
18+
/// ## Implementation Details
19+
///
20+
/// If the contract call execution is tracked using Cairo steps (eg., the class is using an old
21+
/// sierra compiler version), this Sierra gas value must be converted to Cairo steps. Check out
22+
/// the [`call`] module for more information.
23+
///
24+
/// [`call`]: katana_executor::implementation::blockifier::call
1325
pub max_call_gas: Option<u64>,
1426

1527
/// The maximum number of concurrent `estimate_fee` requests allowed.

0 commit comments

Comments
 (0)