Skip to content

Commit 6bfccae

Browse files
blockifier: move execution utils logic to casm_estimation
1 parent c26eb39 commit 6bfccae

File tree

6 files changed

+181
-196
lines changed

6 files changed

+181
-196
lines changed

crates/blockifier/src/execution/casm_hash_estimation.rs

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1+
use std::collections::HashMap;
12
use std::ops::AddAssign;
23

4+
use cairo_vm::types::builtin_name::BuiltinName;
35
use cairo_vm::vm::runners::cairo_runner::ExecutionResources;
46
use starknet_api::contract_class::compiled_class_hash::HashVersion;
57
use starknet_api::execution_resources::GasAmount;
68

9+
use crate::blockifier_versioned_constants::VersionedConstants;
10+
use crate::bouncer::vm_resources_to_sierra_gas;
711
use crate::execution::contract_class::{
812
EntryPointV1,
913
EntryPointsByType,
1014
FeltSizeCount,
1115
NestedFeltCounts,
1216
};
13-
use crate::execution::execution_utils::{encode_and_blake_hash_resources, poseidon_hash_many_cost};
17+
use crate::execution::execution_utils::poseidon_hash_many_cost;
1418
use crate::utils::u64_from_usize;
1519

1620
#[cfg(test)]
@@ -55,7 +59,8 @@ impl EstimatedExecutionResources {
5559
///
5660
/// This is only defined for the V2 (Blake) variant.
5761
// TODO(AvivG): Consider returning 0 for V1 instead of panicking.
58-
pub fn blake_count(&self) -> usize {
62+
#[cfg(test)]
63+
fn blake_count(&self) -> usize {
5964
match self {
6065
EstimatedExecutionResources::V2Hash { blake_count, .. } => *blake_count,
6166
_ => panic!("Cannot get blake count from V1Hash"),
@@ -200,3 +205,125 @@ impl EstimateCasmHashResources for CasmV2HashResourceEstimate {
200205
encode_and_blake_hash_resources(felt_size_groups)
201206
}
202207
}
208+
209+
// Constants used for estimating the cost of BLAKE hashing inside Starknet OS.
210+
// These values are based on empirical measurement by running
211+
// `encode_felt252_data_and_calc_blake_hash` on various combinations of big and small felts.
212+
mod blake_estimation {
213+
// Per-felt step cost (measured).
214+
pub const STEPS_BIG_FELT: usize = 45;
215+
pub const STEPS_SMALL_FELT: usize = 15;
216+
217+
// One-time overhead.
218+
// Overhead when input fills a full Blake message (16 u32s).
219+
pub const BASE_STEPS_FULL_MSG: usize = 217;
220+
// Overhead when input results in a partial message (remainder < 16 u32s).
221+
pub const BASE_STEPS_PARTIAL_MSG: usize = 195;
222+
// Extra steps per 2-u32 remainder in partial messages.
223+
pub const STEPS_PER_2_U32_REMINDER: usize = 3;
224+
// Overhead when input for `encode_felt252_data_and_calc_blake_hash` is non-empty.
225+
pub const BASE_RANGE_CHECK_NON_EMPTY: usize = 3;
226+
// Empty input steps.
227+
pub const STEPS_EMPTY_INPUT: usize = 170;
228+
}
229+
230+
fn base_steps_for_blake_hash(n_u32s: usize) -> usize {
231+
let rem_u32s = n_u32s % FeltSizeCount::U32_WORDS_PER_MESSAGE;
232+
if rem_u32s == 0 {
233+
blake_estimation::BASE_STEPS_FULL_MSG
234+
} else {
235+
// This computation is based on running blake2s with different inputs.
236+
// Note: all inputs expand to an even number of u32s --> `rem_u32s` is always even.
237+
blake_estimation::BASE_STEPS_PARTIAL_MSG
238+
+ (rem_u32s / 2) * blake_estimation::STEPS_PER_2_U32_REMINDER
239+
}
240+
}
241+
242+
fn felts_steps(n_big_felts: usize, n_small_felts: usize) -> usize {
243+
let big_steps = n_big_felts
244+
.checked_mul(blake_estimation::STEPS_BIG_FELT)
245+
.expect("Overflow computing big felt steps");
246+
let small_steps = n_small_felts
247+
.checked_mul(blake_estimation::STEPS_SMALL_FELT)
248+
.expect("Overflow computing small felt steps");
249+
big_steps.checked_add(small_steps).expect("Overflow computing total felt steps")
250+
}
251+
252+
/// Estimates the number of VM steps needed to hash the given felts with Blake in Starknet OS.
253+
/// Each small felt unpacks into 2 u32s, and each big felt into 8 u32s.
254+
/// Adds a base cost depending on whether the total fits exactly into full 16-u32 messages.
255+
fn estimate_steps_of_encode_felt252_data_and_calc_blake_hash(
256+
felt_size_groups: &FeltSizeCount,
257+
) -> usize {
258+
let total_u32s = felt_size_groups.encoded_u32_len();
259+
if total_u32s == 0 {
260+
// The empty input case is a special case.
261+
return blake_estimation::STEPS_EMPTY_INPUT;
262+
}
263+
264+
let base_steps = base_steps_for_blake_hash(total_u32s);
265+
let felt_steps = felts_steps(felt_size_groups.large, felt_size_groups.small);
266+
267+
base_steps.checked_add(felt_steps).expect("Overflow computing total Blake hash steps")
268+
}
269+
270+
/// Estimates resource usage for `encode_felt252_data_and_calc_blake_hash` in the Starknet OS.
271+
///
272+
/// # Encoding Details
273+
/// - Small felts → 2 `u32`s each; Big felts → 8 `u32`s each.
274+
/// - Each felt requires one `range_check` operation.
275+
///
276+
/// # Returns:
277+
/// - `ExecutionResources`: VM resource usage (e.g., n_steps, range checks).
278+
/// - `usize`: number of Blake opcodes used, accounted for separately as those are not reported via
279+
/// `ExecutionResources`.
280+
pub fn encode_and_blake_hash_resources(
281+
felt_size_groups: &FeltSizeCount,
282+
) -> EstimatedExecutionResources {
283+
let n_steps = estimate_steps_of_encode_felt252_data_and_calc_blake_hash(felt_size_groups);
284+
let builtin_instance_counter = match felt_size_groups.n_felts() {
285+
// The empty case does not use builtins at all.
286+
0 => HashMap::new(),
287+
// One `range_check` per input felt to validate its size + Overhead for the non empty case.
288+
_ => HashMap::from([(
289+
BuiltinName::range_check,
290+
felt_size_groups.n_felts() + blake_estimation::BASE_RANGE_CHECK_NON_EMPTY,
291+
)]),
292+
};
293+
294+
let resources = ExecutionResources { n_steps, n_memory_holes: 0, builtin_instance_counter };
295+
296+
EstimatedExecutionResources::V2Hash {
297+
resources,
298+
blake_count: felt_size_groups.blake_opcode_count(),
299+
}
300+
}
301+
302+
/// Converts the execution resources and blake opcode count to L2 gas.
303+
///
304+
/// Used for both Stwo ("proving_gas") and Stone ("sierra_gas") estimations, which differ in
305+
/// builtin costs. This unified logic is valid because only the `range_check` builtin is used,
306+
/// and its cost is identical across provers (see `bouncer.get_tx_weights`).
307+
// TODO(AvivG): Move inside blake estimation struct.
308+
pub fn blake_execution_resources_estimation_to_gas(
309+
resources: EstimatedExecutionResources,
310+
versioned_constants: &VersionedConstants,
311+
blake_opcode_gas: usize,
312+
) -> GasAmount {
313+
// TODO(AvivG): Remove this once gas computation is separated from resource estimation.
314+
assert!(
315+
resources
316+
.resources()
317+
.builtin_instance_counter
318+
.keys()
319+
.all(|&k| k == BuiltinName::range_check),
320+
"Expected either empty builtins or only `range_check` builtin, got: {:?}. This breaks the \
321+
assumption that builtin costs are identical between provers.",
322+
resources.resources().builtin_instance_counter.keys().collect::<Vec<_>>()
323+
);
324+
325+
resources.to_sierra_gas(
326+
|resources| vm_resources_to_sierra_gas(resources, versioned_constants),
327+
Some(blake_opcode_gas),
328+
)
329+
}

crates/blockifier/src/execution/casm_hash_estimation_test.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
use std::collections::HashMap;
22

3+
use blake2s::encode_felts_to_u32s;
34
use cairo_vm::types::builtin_name::BuiltinName;
45
use cairo_vm::vm::runners::cairo_runner::ExecutionResources;
6+
use pretty_assertions::assert_eq;
57
use rstest::rstest;
8+
use starknet_types_core::felt::Felt;
69

7-
use crate::execution::casm_hash_estimation::EstimatedExecutionResources;
10+
use crate::execution::casm_hash_estimation::blake_estimation::STEPS_EMPTY_INPUT;
11+
use crate::execution::casm_hash_estimation::{
12+
encode_and_blake_hash_resources,
13+
estimate_steps_of_encode_felt252_data_and_calc_blake_hash,
14+
EstimatedExecutionResources,
15+
};
16+
use crate::execution::contract_class::FeltSizeCount;
817

918
impl EstimatedExecutionResources {
1019
/// Constructs an `EstimatedExecutionResources` for the V1 (Poseidon) hash function.
@@ -93,3 +102,39 @@ fn add_assign_estimated_resources_success(
93102
}
94103
}
95104
}
105+
106+
#[test]
107+
fn test_u32_constants() {
108+
// Small value < 2^63, will encode to 2 u32s.
109+
let small_felt = Felt::ONE;
110+
// Large value >= 2^63, will encode to 8 u32s (Just above 2^63).
111+
let big_felt = Felt::from_hex_unchecked("8000000000000001");
112+
113+
let small_u32s = encode_felts_to_u32s(vec![small_felt]);
114+
let big_u32s = encode_felts_to_u32s(vec![big_felt]);
115+
116+
// Blake estimation constants should match the actual encoding.
117+
assert_eq!(small_u32s.len(), FeltSizeCount::U32_WORDS_PER_SMALL_FELT);
118+
assert_eq!(big_u32s.len(), FeltSizeCount::U32_WORDS_PER_LARGE_FELT);
119+
}
120+
121+
/// Test the edge case of hashing an empty array of felt values.
122+
#[test]
123+
fn test_zero_inputs() {
124+
// logic was written.
125+
let steps = estimate_steps_of_encode_felt252_data_and_calc_blake_hash(&FeltSizeCount {
126+
large: 0,
127+
small: 0,
128+
});
129+
assert_eq!(steps, STEPS_EMPTY_INPUT, "Unexpected base step cost for zero inputs");
130+
131+
// No opcodes should be emitted.
132+
let opcodes = FeltSizeCount { large: 0, small: 0 }.blake_opcode_count();
133+
assert_eq!(opcodes, 0, "Expected zero BLAKE opcodes for zero inputs");
134+
135+
// Should result in base cost only (no opcode cost).
136+
let resources = encode_and_blake_hash_resources(&FeltSizeCount { large: 0, small: 0 });
137+
let expected = ExecutionResources { n_steps: STEPS_EMPTY_INPUT, ..Default::default() };
138+
assert_eq!(resources.resources(), &expected, "Unexpected resources values for zero-input hash");
139+
assert_eq!(resources.blake_count(), 0, "Expected zero BLAKE opcodes for zero inputs");
140+
}

crates/blockifier/src/execution/contract_class.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,13 @@ use crate::abi::constants::{self};
4242
use crate::blockifier_versioned_constants::VersionedConstants;
4343
use crate::bouncer::vm_resources_to_sierra_gas;
4444
use crate::execution::call_info::BuiltinCounterMap;
45-
use crate::execution::entry_point::{EntryPointExecutionContext, EntryPointTypeAndSelector};
46-
use crate::execution::errors::PreExecutionError;
47-
use crate::execution::execution_utils::{
45+
use crate::execution::casm_hash_estimation::{
4846
blake_execution_resources_estimation_to_gas,
4947
encode_and_blake_hash_resources,
50-
poseidon_hash_many_cost,
51-
sn_api_to_cairo_vm_program,
5248
};
49+
use crate::execution::entry_point::{EntryPointExecutionContext, EntryPointTypeAndSelector};
50+
use crate::execution::errors::PreExecutionError;
51+
use crate::execution::execution_utils::{poseidon_hash_many_cost, sn_api_to_cairo_vm_program};
5352
#[cfg(feature = "cairo_native")]
5453
use crate::execution::native::contract_class::NativeCompiledClassV1;
5554
use crate::transaction::errors::TransactionExecutionError;

crates/blockifier/src/execution/execution_utils.rs

Lines changed: 1 addition & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,11 @@ use cairo_vm::vm::vm_core::VirtualMachine;
1818
use num_bigint::BigUint;
1919
use starknet_api::core::ClassHash;
2020
use starknet_api::deprecated_contract_class::Program as DeprecatedProgram;
21-
use starknet_api::execution_resources::GasAmount;
2221
use starknet_api::transaction::fields::Calldata;
2322
use starknet_types_core::felt::Felt;
2423

25-
use crate::blockifier_versioned_constants::VersionedConstants;
26-
use crate::bouncer::vm_resources_to_sierra_gas;
2724
use crate::execution::call_info::{CallExecution, CallInfo, Retdata};
28-
use crate::execution::casm_hash_estimation::EstimatedExecutionResources;
29-
use crate::execution::contract_class::{FeltSizeCount, RunnableCompiledClass, TrackedResource};
25+
use crate::execution::contract_class::{RunnableCompiledClass, TrackedResource};
3026
use crate::execution::entry_point::{
3127
execute_constructor_entry_point,
3228
ConstructorContext,
@@ -52,10 +48,6 @@ use crate::execution::{deprecated_entry_point_execution, entry_point_execution};
5248
use crate::state::errors::StateError;
5349
use crate::state::state_api::State;
5450

55-
#[cfg(test)]
56-
#[path = "execution_utils_test.rs"]
57-
pub mod test;
58-
5951
pub type Args = Vec<CairoArg>;
6052

6153
pub const SEGMENT_ARENA_BUILTIN_SIZE: usize = 3;
@@ -372,125 +364,3 @@ pub fn poseidon_hash_many_cost(data_length: usize) -> ExecutionResources {
372364
builtin_instance_counter: HashMap::from([(BuiltinName::poseidon, data_length / 2 + 1)]),
373365
}
374366
}
375-
376-
// Constants used for estimating the cost of BLAKE hashing inside Starknet OS.
377-
// These values are based on empirical measurement by running
378-
// `encode_felt252_data_and_calc_blake_hash` on various combinations of big and small felts.
379-
mod blake_estimation {
380-
// Per-felt step cost (measured).
381-
pub const STEPS_BIG_FELT: usize = 45;
382-
pub const STEPS_SMALL_FELT: usize = 15;
383-
384-
// One-time overhead.
385-
// Overhead when input fills a full Blake message (16 u32s).
386-
pub const BASE_STEPS_FULL_MSG: usize = 217;
387-
// Overhead when input results in a partial message (remainder < 16 u32s).
388-
pub const BASE_STEPS_PARTIAL_MSG: usize = 195;
389-
// Extra steps per 2-u32 remainder in partial messages.
390-
pub const STEPS_PER_2_U32_REMINDER: usize = 3;
391-
// Overhead when input for `encode_felt252_data_and_calc_blake_hash` is non-empty.
392-
pub const BASE_RANGE_CHECK_NON_EMPTY: usize = 3;
393-
// Empty input steps.
394-
pub const STEPS_EMPTY_INPUT: usize = 170;
395-
}
396-
397-
fn base_steps_for_blake_hash(n_u32s: usize) -> usize {
398-
let rem_u32s = n_u32s % FeltSizeCount::U32_WORDS_PER_MESSAGE;
399-
if rem_u32s == 0 {
400-
blake_estimation::BASE_STEPS_FULL_MSG
401-
} else {
402-
// This computation is based on running blake2s with different inputs.
403-
// Note: all inputs expand to an even number of u32s --> `rem_u32s` is always even.
404-
blake_estimation::BASE_STEPS_PARTIAL_MSG
405-
+ (rem_u32s / 2) * blake_estimation::STEPS_PER_2_U32_REMINDER
406-
}
407-
}
408-
409-
fn felts_steps(n_big_felts: usize, n_small_felts: usize) -> usize {
410-
let big_steps = n_big_felts
411-
.checked_mul(blake_estimation::STEPS_BIG_FELT)
412-
.expect("Overflow computing big felt steps");
413-
let small_steps = n_small_felts
414-
.checked_mul(blake_estimation::STEPS_SMALL_FELT)
415-
.expect("Overflow computing small felt steps");
416-
big_steps.checked_add(small_steps).expect("Overflow computing total felt steps")
417-
}
418-
419-
/// Estimates the number of VM steps needed to hash the given felts with Blake in Starknet OS.
420-
/// Each small felt unpacks into 2 u32s, and each big felt into 8 u32s.
421-
/// Adds a base cost depending on whether the total fits exactly into full 16-u32 messages.
422-
fn estimate_steps_of_encode_felt252_data_and_calc_blake_hash(
423-
felt_size_groups: &FeltSizeCount,
424-
) -> usize {
425-
let total_u32s = felt_size_groups.encoded_u32_len();
426-
if total_u32s == 0 {
427-
// The empty input case is a special case.
428-
return blake_estimation::STEPS_EMPTY_INPUT;
429-
}
430-
431-
let base_steps = base_steps_for_blake_hash(total_u32s);
432-
let felt_steps = felts_steps(felt_size_groups.large, felt_size_groups.small);
433-
434-
base_steps.checked_add(felt_steps).expect("Overflow computing total Blake hash steps")
435-
}
436-
437-
/// Estimates resource usage for `encode_felt252_data_and_calc_blake_hash` in the Starknet OS.
438-
///
439-
/// # Encoding Details
440-
/// - Small felts → 2 `u32`s each; Big felts → 8 `u32`s each.
441-
/// - Each felt requires one `range_check` operation.
442-
///
443-
/// # Returns:
444-
/// - `ExecutionResources`: VM resource usage (e.g., n_steps, range checks).
445-
/// - `usize`: number of Blake opcodes used, accounted for separately as those are not reported via
446-
/// `ExecutionResources`.
447-
pub fn encode_and_blake_hash_resources(
448-
felt_size_groups: &FeltSizeCount,
449-
) -> EstimatedExecutionResources {
450-
let n_steps = estimate_steps_of_encode_felt252_data_and_calc_blake_hash(felt_size_groups);
451-
let builtin_instance_counter = match felt_size_groups.n_felts() {
452-
// The empty case does not use builtins at all.
453-
0 => HashMap::new(),
454-
// One `range_check` per input felt to validate its size + Overhead for the non empty case.
455-
_ => HashMap::from([(
456-
BuiltinName::range_check,
457-
felt_size_groups.n_felts() + blake_estimation::BASE_RANGE_CHECK_NON_EMPTY,
458-
)]),
459-
};
460-
461-
let resources = ExecutionResources { n_steps, n_memory_holes: 0, builtin_instance_counter };
462-
463-
EstimatedExecutionResources::V2Hash {
464-
resources,
465-
blake_count: felt_size_groups.blake_opcode_count(),
466-
}
467-
}
468-
469-
/// Converts the execution resources and blake opcode count to L2 gas.
470-
///
471-
/// Used for both Stwo ("proving_gas") and Stone ("sierra_gas") estimations, which differ in
472-
/// builtin costs. This unified logic is valid because only the `range_check` builtin is used,
473-
/// and its cost is identical across provers (see `bouncer.get_tx_weights`).
474-
// TODO(AvivG): Move inside blake estimation struct.
475-
pub fn blake_execution_resources_estimation_to_gas(
476-
resources: EstimatedExecutionResources,
477-
versioned_constants: &VersionedConstants,
478-
blake_opcode_gas: usize,
479-
) -> GasAmount {
480-
// TODO(AvivG): Remove this once gas computation is separated from resource estimation.
481-
assert!(
482-
resources
483-
.resources()
484-
.builtin_instance_counter
485-
.keys()
486-
.all(|&k| k == BuiltinName::range_check),
487-
"Expected either empty builtins or only `range_check` builtin, got: {:?}. This breaks the \
488-
assumption that builtin costs are identical between provers.",
489-
resources.resources().builtin_instance_counter.keys().collect::<Vec<_>>()
490-
);
491-
492-
resources.to_sierra_gas(
493-
|resources| vm_resources_to_sierra_gas(resources, versioned_constants),
494-
Some(blake_opcode_gas),
495-
)
496-
}

0 commit comments

Comments
 (0)