Skip to content

Commit a3db1a9

Browse files
blockifier: move execution utils logic to casm_estimation
1 parent 56419e6 commit a3db1a9

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

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)