From 75548a3692b23a8f726c936f2d8e15aa07dda5ae Mon Sep 17 00:00:00 2001 From: Aviv Greenburg Date: Tue, 19 Aug 2025 18:51:01 +0300 Subject: [PATCH] blockifier: move execution utils logic to casm_estimation --- .../src/execution/casm_hash_estimation.rs | 131 ++++++++++++++++- .../execution/casm_hash_estimation_test.rs | 47 ++++++- .../src/execution/contract_class.rs | 9 +- .../src/execution/execution_utils.rs | 132 +----------------- .../blake2s/blake2s_test.rs | 2 +- 5 files changed, 181 insertions(+), 140 deletions(-) diff --git a/crates/blockifier/src/execution/casm_hash_estimation.rs b/crates/blockifier/src/execution/casm_hash_estimation.rs index e6bced878f5..23472ecb265 100644 --- a/crates/blockifier/src/execution/casm_hash_estimation.rs +++ b/crates/blockifier/src/execution/casm_hash_estimation.rs @@ -1,16 +1,20 @@ +use std::collections::HashMap; use std::ops::AddAssign; +use cairo_vm::types::builtin_name::BuiltinName; use cairo_vm::vm::runners::cairo_runner::ExecutionResources; use starknet_api::contract_class::compiled_class_hash::HashVersion; use starknet_api::execution_resources::GasAmount; +use crate::blockifier_versioned_constants::VersionedConstants; +use crate::bouncer::vm_resources_to_sierra_gas; use crate::execution::contract_class::{ EntryPointV1, EntryPointsByType, FeltSizeCount, NestedFeltCounts, }; -use crate::execution::execution_utils::{encode_and_blake_hash_resources, poseidon_hash_many_cost}; +use crate::execution::execution_utils::poseidon_hash_many_cost; use crate::utils::u64_from_usize; #[cfg(test)] @@ -55,7 +59,8 @@ impl EstimatedExecutionResources { /// /// This is only defined for the V2 (Blake) variant. // TODO(AvivG): Consider returning 0 for V1 instead of panicking. - pub fn blake_count(&self) -> usize { + #[cfg(test)] + fn blake_count(&self) -> usize { match self { EstimatedExecutionResources::V2Hash { blake_count, .. } => *blake_count, _ => panic!("Cannot get blake count from V1Hash"), @@ -200,3 +205,125 @@ impl EstimateCasmHashResources for CasmV2HashResourceEstimate { encode_and_blake_hash_resources(felt_size_groups) } } + +// Constants used for estimating the cost of BLAKE hashing inside Starknet OS. +// These values are based on empirical measurement by running +// `encode_felt252_data_and_calc_blake_hash` on various combinations of big and small felts. +mod blake_estimation { + // Per-felt step cost (measured). + pub const STEPS_BIG_FELT: usize = 45; + pub const STEPS_SMALL_FELT: usize = 15; + + // One-time overhead. + // Overhead when input fills a full Blake message (16 u32s). + pub const BASE_STEPS_FULL_MSG: usize = 217; + // Overhead when input results in a partial message (remainder < 16 u32s). + pub const BASE_STEPS_PARTIAL_MSG: usize = 195; + // Extra steps per 2-u32 remainder in partial messages. + pub const STEPS_PER_2_U32_REMINDER: usize = 3; + // Overhead when input for `encode_felt252_data_and_calc_blake_hash` is non-empty. + pub const BASE_RANGE_CHECK_NON_EMPTY: usize = 3; + // Empty input steps. + pub const STEPS_EMPTY_INPUT: usize = 170; +} + +fn base_steps_for_blake_hash(n_u32s: usize) -> usize { + let rem_u32s = n_u32s % FeltSizeCount::U32_WORDS_PER_MESSAGE; + if rem_u32s == 0 { + blake_estimation::BASE_STEPS_FULL_MSG + } else { + // This computation is based on running blake2s with different inputs. + // Note: all inputs expand to an even number of u32s --> `rem_u32s` is always even. + blake_estimation::BASE_STEPS_PARTIAL_MSG + + (rem_u32s / 2) * blake_estimation::STEPS_PER_2_U32_REMINDER + } +} + +fn felts_steps(n_big_felts: usize, n_small_felts: usize) -> usize { + let big_steps = n_big_felts + .checked_mul(blake_estimation::STEPS_BIG_FELT) + .expect("Overflow computing big felt steps"); + let small_steps = n_small_felts + .checked_mul(blake_estimation::STEPS_SMALL_FELT) + .expect("Overflow computing small felt steps"); + big_steps.checked_add(small_steps).expect("Overflow computing total felt steps") +} + +/// Estimates the number of VM steps needed to hash the given felts with Blake in Starknet OS. +/// Each small felt unpacks into 2 u32s, and each big felt into 8 u32s. +/// Adds a base cost depending on whether the total fits exactly into full 16-u32 messages. +fn estimate_steps_of_encode_felt252_data_and_calc_blake_hash( + felt_size_groups: &FeltSizeCount, +) -> usize { + let total_u32s = felt_size_groups.encoded_u32_len(); + if total_u32s == 0 { + // The empty input case is a special case. + return blake_estimation::STEPS_EMPTY_INPUT; + } + + let base_steps = base_steps_for_blake_hash(total_u32s); + let felt_steps = felts_steps(felt_size_groups.large, felt_size_groups.small); + + base_steps.checked_add(felt_steps).expect("Overflow computing total Blake hash steps") +} + +/// Estimates resource usage for `encode_felt252_data_and_calc_blake_hash` in the Starknet OS. +/// +/// # Encoding Details +/// - Small felts → 2 `u32`s each; Big felts → 8 `u32`s each. +/// - Each felt requires one `range_check` operation. +/// +/// # Returns: +/// - `ExecutionResources`: VM resource usage (e.g., n_steps, range checks). +/// - `usize`: number of Blake opcodes used, accounted for separately as those are not reported via +/// `ExecutionResources`. +pub fn encode_and_blake_hash_resources( + felt_size_groups: &FeltSizeCount, +) -> EstimatedExecutionResources { + let n_steps = estimate_steps_of_encode_felt252_data_and_calc_blake_hash(felt_size_groups); + let builtin_instance_counter = match felt_size_groups.n_felts() { + // The empty case does not use builtins at all. + 0 => HashMap::new(), + // One `range_check` per input felt to validate its size + Overhead for the non empty case. + _ => HashMap::from([( + BuiltinName::range_check, + felt_size_groups.n_felts() + blake_estimation::BASE_RANGE_CHECK_NON_EMPTY, + )]), + }; + + let resources = ExecutionResources { n_steps, n_memory_holes: 0, builtin_instance_counter }; + + EstimatedExecutionResources::V2Hash { + resources, + blake_count: felt_size_groups.blake_opcode_count(), + } +} + +/// Converts the execution resources and blake opcode count to L2 gas. +/// +/// Used for both Stwo ("proving_gas") and Stone ("sierra_gas") estimations, which differ in +/// builtin costs. This unified logic is valid because only the `range_check` builtin is used, +/// and its cost is identical across provers (see `bouncer.get_tx_weights`). +// TODO(AvivG): Move inside blake estimation struct. +pub fn blake_execution_resources_estimation_to_gas( + resources: EstimatedExecutionResources, + versioned_constants: &VersionedConstants, + blake_opcode_gas: usize, +) -> GasAmount { + // TODO(AvivG): Remove this once gas computation is separated from resource estimation. + assert!( + resources + .resources() + .builtin_instance_counter + .keys() + .all(|&k| k == BuiltinName::range_check), + "Expected either empty builtins or only `range_check` builtin, got: {:?}. This breaks the \ + assumption that builtin costs are identical between provers.", + resources.resources().builtin_instance_counter.keys().collect::>() + ); + + resources.to_sierra_gas( + |resources| vm_resources_to_sierra_gas(resources, versioned_constants), + Some(blake_opcode_gas), + ) +} diff --git a/crates/blockifier/src/execution/casm_hash_estimation_test.rs b/crates/blockifier/src/execution/casm_hash_estimation_test.rs index a4e96559a18..73249c708f2 100644 --- a/crates/blockifier/src/execution/casm_hash_estimation_test.rs +++ b/crates/blockifier/src/execution/casm_hash_estimation_test.rs @@ -1,10 +1,19 @@ use std::collections::HashMap; +use blake2s::encode_felts_to_u32s; use cairo_vm::types::builtin_name::BuiltinName; use cairo_vm::vm::runners::cairo_runner::ExecutionResources; +use pretty_assertions::assert_eq; use rstest::rstest; +use starknet_types_core::felt::Felt; -use crate::execution::casm_hash_estimation::EstimatedExecutionResources; +use crate::execution::casm_hash_estimation::blake_estimation::STEPS_EMPTY_INPUT; +use crate::execution::casm_hash_estimation::{ + encode_and_blake_hash_resources, + estimate_steps_of_encode_felt252_data_and_calc_blake_hash, + EstimatedExecutionResources, +}; +use crate::execution::contract_class::FeltSizeCount; impl EstimatedExecutionResources { /// Constructs an `EstimatedExecutionResources` for the V1 (Poseidon) hash function. @@ -93,3 +102,39 @@ fn add_assign_estimated_resources_success( } } } + +#[test] +fn test_u32_constants() { + // Small value < 2^63, will encode to 2 u32s. + let small_felt = Felt::ONE; + // Large value >= 2^63, will encode to 8 u32s (Just above 2^63). + let big_felt = Felt::from_hex_unchecked("8000000000000001"); + + let small_u32s = encode_felts_to_u32s(vec![small_felt]); + let big_u32s = encode_felts_to_u32s(vec![big_felt]); + + // Blake estimation constants should match the actual encoding. + assert_eq!(small_u32s.len(), FeltSizeCount::U32_WORDS_PER_SMALL_FELT); + assert_eq!(big_u32s.len(), FeltSizeCount::U32_WORDS_PER_LARGE_FELT); +} + +/// Test the edge case of hashing an empty array of felt values. +#[test] +fn test_zero_inputs() { + // logic was written. + let steps = estimate_steps_of_encode_felt252_data_and_calc_blake_hash(&FeltSizeCount { + large: 0, + small: 0, + }); + assert_eq!(steps, STEPS_EMPTY_INPUT, "Unexpected base step cost for zero inputs"); + + // No opcodes should be emitted. + let opcodes = FeltSizeCount::default().blake_opcode_count(); + assert_eq!(opcodes, 0, "Expected zero BLAKE opcodes for zero inputs"); + + // Should result in base cost only (no opcode cost). + let resources = encode_and_blake_hash_resources(&FeltSizeCount::default()); + let expected = ExecutionResources { n_steps: STEPS_EMPTY_INPUT, ..Default::default() }; + assert_eq!(resources.resources(), &expected, "Unexpected resources values for zero-input hash"); + assert_eq!(resources.blake_count(), 0, "Expected zero BLAKE opcodes for zero inputs"); +} diff --git a/crates/blockifier/src/execution/contract_class.rs b/crates/blockifier/src/execution/contract_class.rs index 3de221de1cb..9e60f6cde13 100644 --- a/crates/blockifier/src/execution/contract_class.rs +++ b/crates/blockifier/src/execution/contract_class.rs @@ -44,14 +44,13 @@ use crate::blockifier_versioned_constants::VersionedConstants; use crate::bouncer::vm_resources_to_sierra_gas; use crate::execution::call_info::BuiltinCounterMap; use crate::execution::casm_hash_estimation::EstimatedExecutionResources; -use crate::execution::entry_point::{EntryPointExecutionContext, EntryPointTypeAndSelector}; -use crate::execution::errors::PreExecutionError; -use crate::execution::execution_utils::{ +use crate::execution::casm_hash_estimation::{ blake_execution_resources_estimation_to_gas, encode_and_blake_hash_resources, - poseidon_hash_many_cost, - sn_api_to_cairo_vm_program, }; +use crate::execution::entry_point::{EntryPointExecutionContext, EntryPointTypeAndSelector}; +use crate::execution::errors::PreExecutionError; +use crate::execution::execution_utils::{poseidon_hash_many_cost, sn_api_to_cairo_vm_program}; #[cfg(feature = "cairo_native")] use crate::execution::native::contract_class::NativeCompiledClassV1; use crate::transaction::errors::TransactionExecutionError; diff --git a/crates/blockifier/src/execution/execution_utils.rs b/crates/blockifier/src/execution/execution_utils.rs index dfd7231cbc1..4f8022e9712 100644 --- a/crates/blockifier/src/execution/execution_utils.rs +++ b/crates/blockifier/src/execution/execution_utils.rs @@ -18,15 +18,11 @@ use cairo_vm::vm::vm_core::VirtualMachine; use num_bigint::BigUint; use starknet_api::core::ClassHash; use starknet_api::deprecated_contract_class::Program as DeprecatedProgram; -use starknet_api::execution_resources::GasAmount; use starknet_api::transaction::fields::Calldata; use starknet_types_core::felt::Felt; -use crate::blockifier_versioned_constants::VersionedConstants; -use crate::bouncer::vm_resources_to_sierra_gas; use crate::execution::call_info::{CallExecution, CallInfo, Retdata}; -use crate::execution::casm_hash_estimation::EstimatedExecutionResources; -use crate::execution::contract_class::{FeltSizeCount, RunnableCompiledClass, TrackedResource}; +use crate::execution::contract_class::{RunnableCompiledClass, TrackedResource}; use crate::execution::entry_point::{ execute_constructor_entry_point, ConstructorContext, @@ -52,10 +48,6 @@ use crate::execution::{deprecated_entry_point_execution, entry_point_execution}; use crate::state::errors::StateError; use crate::state::state_api::State; -#[cfg(test)] -#[path = "execution_utils_test.rs"] -pub mod test; - pub type Args = Vec; pub const SEGMENT_ARENA_BUILTIN_SIZE: usize = 3; @@ -372,125 +364,3 @@ pub fn poseidon_hash_many_cost(data_length: usize) -> ExecutionResources { builtin_instance_counter: HashMap::from([(BuiltinName::poseidon, data_length / 2 + 1)]), } } - -// Constants used for estimating the cost of BLAKE hashing inside Starknet OS. -// These values are based on empirical measurement by running -// `encode_felt252_data_and_calc_blake_hash` on various combinations of big and small felts. -mod blake_estimation { - // Per-felt step cost (measured). - pub const STEPS_BIG_FELT: usize = 45; - pub const STEPS_SMALL_FELT: usize = 15; - - // One-time overhead. - // Overhead when input fills a full Blake message (16 u32s). - pub const BASE_STEPS_FULL_MSG: usize = 217; - // Overhead when input results in a partial message (remainder < 16 u32s). - pub const BASE_STEPS_PARTIAL_MSG: usize = 195; - // Extra steps per 2-u32 remainder in partial messages. - pub const STEPS_PER_2_U32_REMINDER: usize = 3; - // Overhead when input for `encode_felt252_data_and_calc_blake_hash` is non-empty. - pub const BASE_RANGE_CHECK_NON_EMPTY: usize = 3; - // Empty input steps. - pub const STEPS_EMPTY_INPUT: usize = 170; -} - -fn base_steps_for_blake_hash(n_u32s: usize) -> usize { - let rem_u32s = n_u32s % FeltSizeCount::U32_WORDS_PER_MESSAGE; - if rem_u32s == 0 { - blake_estimation::BASE_STEPS_FULL_MSG - } else { - // This computation is based on running blake2s with different inputs. - // Note: all inputs expand to an even number of u32s --> `rem_u32s` is always even. - blake_estimation::BASE_STEPS_PARTIAL_MSG - + (rem_u32s / 2) * blake_estimation::STEPS_PER_2_U32_REMINDER - } -} - -fn felts_steps(n_big_felts: usize, n_small_felts: usize) -> usize { - let big_steps = n_big_felts - .checked_mul(blake_estimation::STEPS_BIG_FELT) - .expect("Overflow computing big felt steps"); - let small_steps = n_small_felts - .checked_mul(blake_estimation::STEPS_SMALL_FELT) - .expect("Overflow computing small felt steps"); - big_steps.checked_add(small_steps).expect("Overflow computing total felt steps") -} - -/// Estimates the number of VM steps needed to hash the given felts with Blake in Starknet OS. -/// Each small felt unpacks into 2 u32s, and each big felt into 8 u32s. -/// Adds a base cost depending on whether the total fits exactly into full 16-u32 messages. -fn estimate_steps_of_encode_felt252_data_and_calc_blake_hash( - felt_size_groups: &FeltSizeCount, -) -> usize { - let total_u32s = felt_size_groups.encoded_u32_len(); - if total_u32s == 0 { - // The empty input case is a special case. - return blake_estimation::STEPS_EMPTY_INPUT; - } - - let base_steps = base_steps_for_blake_hash(total_u32s); - let felt_steps = felts_steps(felt_size_groups.large, felt_size_groups.small); - - base_steps.checked_add(felt_steps).expect("Overflow computing total Blake hash steps") -} - -/// Estimates resource usage for `encode_felt252_data_and_calc_blake_hash` in the Starknet OS. -/// -/// # Encoding Details -/// - Small felts → 2 `u32`s each; Big felts → 8 `u32`s each. -/// - Each felt requires one `range_check` operation. -/// -/// # Returns: -/// - `ExecutionResources`: VM resource usage (e.g., n_steps, range checks). -/// - `usize`: number of Blake opcodes used, accounted for separately as those are not reported via -/// `ExecutionResources`. -pub fn encode_and_blake_hash_resources( - felt_size_groups: &FeltSizeCount, -) -> EstimatedExecutionResources { - let n_steps = estimate_steps_of_encode_felt252_data_and_calc_blake_hash(felt_size_groups); - let builtin_instance_counter = match felt_size_groups.n_felts() { - // The empty case does not use builtins at all. - 0 => HashMap::new(), - // One `range_check` per input felt to validate its size + Overhead for the non empty case. - _ => HashMap::from([( - BuiltinName::range_check, - felt_size_groups.n_felts() + blake_estimation::BASE_RANGE_CHECK_NON_EMPTY, - )]), - }; - - let resources = ExecutionResources { n_steps, n_memory_holes: 0, builtin_instance_counter }; - - EstimatedExecutionResources::V2Hash { - resources, - blake_count: felt_size_groups.blake_opcode_count(), - } -} - -/// Converts the execution resources and blake opcode count to L2 gas. -/// -/// Used for both Stwo ("proving_gas") and Stone ("sierra_gas") estimations, which differ in -/// builtin costs. This unified logic is valid because only the `range_check` builtin is used, -/// and its cost is identical across provers (see `bouncer.get_tx_weights`). -// TODO(AvivG): Move inside blake estimation struct. -pub fn blake_execution_resources_estimation_to_gas( - resources: EstimatedExecutionResources, - versioned_constants: &VersionedConstants, - blake_opcode_gas: usize, -) -> GasAmount { - // TODO(AvivG): Remove this once gas computation is separated from resource estimation. - assert!( - resources - .resources() - .builtin_instance_counter - .keys() - .all(|&k| k == BuiltinName::range_check), - "Expected either empty builtins or only `range_check` builtin, got: {:?}. This breaks the \ - assumption that builtin costs are identical between provers.", - resources.resources().builtin_instance_counter.keys().collect::>() - ); - - resources.to_sierra_gas( - |resources| vm_resources_to_sierra_gas(resources, versioned_constants), - Some(blake_opcode_gas), - ) -} diff --git a/crates/starknet_os/src/hints/hint_implementation/blake2s/blake2s_test.rs b/crates/starknet_os/src/hints/hint_implementation/blake2s/blake2s_test.rs index 53bf4229b30..00a3bb19917 100644 --- a/crates/starknet_os/src/hints/hint_implementation/blake2s/blake2s_test.rs +++ b/crates/starknet_os/src/hints/hint_implementation/blake2s/blake2s_test.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; use blake2s::encode_felt252_data_and_calc_blake_hash; +use blockifier::execution::casm_hash_estimation::encode_and_blake_hash_resources; use blockifier::execution::contract_class::FeltSizeCount; -use blockifier::execution::execution_utils::encode_and_blake_hash_resources; use cairo_vm::types::builtin_name::BuiltinName; use cairo_vm::types::layout_name::LayoutName; use cairo_vm::types::relocatable::MaybeRelocatable;