Skip to content

Commit 7e2b9a6

Browse files
blockifier: move execution utils logic to casm_estimation
1 parent 1b8280e commit 7e2b9a6

File tree

6 files changed

+173
-185
lines changed

6 files changed

+173
-185
lines changed

crates/blockifier/src/execution/casm_hash_estimation.rs

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

crates/blockifier/src/execution/casm_hash_estimation_test.rs

Lines changed: 43 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+
compute_blake_hash_steps,
13+
encode_and_blake_hash_resources,
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,36 @@ 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 = compute_blake_hash_steps(&FeltSizeCount { large: 0, small: 0 });
126+
assert_eq!(steps, STEPS_EMPTY_INPUT, "Unexpected base step cost for zero inputs");
127+
128+
// No opcodes should be emitted.
129+
let opcodes = FeltSizeCount { large: 0, small: 0 }.blake_opcode_count();
130+
assert_eq!(opcodes, 0, "Expected zero BLAKE opcodes for zero inputs");
131+
132+
// Should result in base cost only (no opcode cost).
133+
let resources = encode_and_blake_hash_resources(&FeltSizeCount { large: 0, small: 0 });
134+
let expected = ExecutionResources { n_steps: STEPS_EMPTY_INPUT, ..Default::default() };
135+
assert_eq!(resources.resources(), &expected, "Unexpected resources values for zero-input hash");
136+
assert_eq!(resources.blake_count(), 0, "Expected zero BLAKE opcodes for zero inputs");
137+
}

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 & 125 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,
@@ -372,123 +368,3 @@ pub fn poseidon_hash_many_cost(data_length: usize) -> ExecutionResources {
372368
builtin_instance_counter: HashMap::from([(BuiltinName::poseidon, data_length / 2 + 1)]),
373369
}
374370
}
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 compute_blake_hash_steps(felt_size_groups: &FeltSizeCount) -> usize {
423-
let total_u32s = felt_size_groups.encoded_u32_len();
424-
if total_u32s == 0 {
425-
// The empty input case is a special case.
426-
return blake_estimation::STEPS_EMPTY_INPUT;
427-
}
428-
429-
let base_steps = base_steps_for_blake_hash(total_u32s);
430-
let felt_steps = felts_steps(felt_size_groups.large, felt_size_groups.small);
431-
432-
base_steps.checked_add(felt_steps).expect("Overflow computing total Blake hash steps")
433-
}
434-
435-
/// Estimates resource usage for `encode_felt252_data_and_calc_blake_hash` in the Starknet OS.
436-
///
437-
/// # Encoding Details
438-
/// - Small felts → 2 `u32`s each; Big felts → 8 `u32`s each.
439-
/// - Each felt requires one `range_check` operation.
440-
///
441-
/// # Returns:
442-
/// - `ExecutionResources`: VM resource usage (e.g., n_steps, range checks).
443-
/// - `usize`: number of Blake opcodes used, accounted for separately as those are not reported via
444-
/// `ExecutionResources`.
445-
pub fn encode_and_blake_hash_resources(
446-
felt_size_groups: &FeltSizeCount,
447-
) -> EstimatedExecutionResources {
448-
let n_steps = compute_blake_hash_steps(felt_size_groups);
449-
let builtin_instance_counter = match felt_size_groups.n_felts() {
450-
// The empty case does not use builtins at all.
451-
0 => HashMap::new(),
452-
// One `range_check` per input felt to validate its size + Overhead for the non empty case.
453-
_ => HashMap::from([(
454-
BuiltinName::range_check,
455-
felt_size_groups.n_felts() + blake_estimation::BASE_RANGE_CHECK_NON_EMPTY,
456-
)]),
457-
};
458-
459-
let resources = ExecutionResources { n_steps, n_memory_holes: 0, builtin_instance_counter };
460-
461-
EstimatedExecutionResources::V2Hash {
462-
resources,
463-
blake_count: felt_size_groups.blake_opcode_count(),
464-
}
465-
}
466-
467-
/// Converts the execution resources and blake opcode count to L2 gas.
468-
///
469-
/// Used for both Stwo ("proving_gas") and Stone ("sierra_gas") estimations, which differ in
470-
/// builtin costs. This unified logic is valid because only the `range_check` builtin is used,
471-
/// and its cost is identical across provers (see `bouncer.get_tx_weights`).
472-
// TODO(AvivG): Move inside blake estimation struct.
473-
pub fn blake_execution_resources_estimation_to_gas(
474-
resources: EstimatedExecutionResources,
475-
versioned_constants: &VersionedConstants,
476-
blake_opcode_gas: usize,
477-
) -> GasAmount {
478-
// TODO(AvivG): Remove this once gas computation is separated from resource estimation.
479-
assert!(
480-
resources
481-
.resources()
482-
.builtin_instance_counter
483-
.keys()
484-
.all(|&k| k == BuiltinName::range_check),
485-
"Expected either empty builtins or only `range_check` builtin, got: {:?}. This breaks the \
486-
assumption that builtin costs are identical between provers.",
487-
resources.resources().builtin_instance_counter.keys().collect::<Vec<_>>()
488-
);
489-
490-
resources.to_sierra_gas(
491-
|resources| vm_resources_to_sierra_gas(resources, versioned_constants),
492-
Some(blake_opcode_gas),
493-
)
494-
}

0 commit comments

Comments
 (0)