Skip to content

blockifier: move execution utils logic to casm_estimation #8693

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: avivg/blockifier/encode_and_blake_hash_resources_in_trait
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 129 additions & 2 deletions crates/blockifier/src/execution/casm_hash_estimation.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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::<Vec<_>>()
);

resources.to_sierra_gas(
|resources| vm_resources_to_sierra_gas(resources, versioned_constants),
Some(blake_opcode_gas),
)
}
47 changes: 46 additions & 1 deletion crates/blockifier/src/execution/casm_hash_estimation_test.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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");
}
9 changes: 4 additions & 5 deletions crates/blockifier/src/execution/contract_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
132 changes: 1 addition & 131 deletions crates/blockifier/src/execution/execution_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<CairoArg>;

pub const SEGMENT_ARENA_BUILTIN_SIZE: usize = 3;
Expand Down Expand Up @@ -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::<Vec<_>>()
);

resources.to_sierra_gas(
|resources| vm_resources_to_sierra_gas(resources, versioned_constants),
Some(blake_opcode_gas),
)
}
Loading
Loading