Skip to content
Closed
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
2 changes: 1 addition & 1 deletion crates/blockifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ derive_more = { workspace = true, features = [
"from",
"into_iterator",
] }
expect-test.workspace = true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test-only crate promoted to production dependency

Medium Severity

The expect-test crate — a snapshot testing library — is moved from [dev-dependencies] to [dependencies], making it a production dependency. The Expect type and expect! macro embed source file paths, line numbers, and column numbers, and are designed exclusively for test assertions with UPDATE_EXPECT=1 auto-fixing. The _EXPECT constants are only consumed by LazyLock parsers and test assertions — they serve no production purpose. This also downgrades six compile-time const usize values to runtime-parsed LazyLock<usize> statics, introducing a panic path (.parse().expect(...)) that previously could not exist.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a3359be. Configure here.

indexmap.workspace = true
itertools.workspace = true
keccak.workspace = true
Expand Down Expand Up @@ -84,7 +85,6 @@ validator.workspace = true
assert_matches.workspace = true
blockifier_test_utils.workspace = true
criterion = { workspace = true, features = ["html_reports"] }
expect-test.workspace = true
glob.workspace = true
hex.workspace = true
itertools.workspace = true
Expand Down
62 changes: 38 additions & 24 deletions crates/blockifier/src/execution/casm_hash_estimation.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::collections::BTreeMap;
use std::sync::LazyLock;

use cairo_vm::types::builtin_name::BuiltinName;
use cairo_vm::vm::runners::cairo_runner::ExecutionResources;
use expect_test::{expect, Expect};

use crate::execution::call_info::{ExtendedExecutionResources, OpcodeName};
use crate::execution::contract_class::{
Expand Down Expand Up @@ -237,6 +239,36 @@ impl EstimateCasmHashResources for CasmV1HashResourceEstimate {
}
}

// Base number of VM steps applied when the input to Blake hashing is empty.
// Determined empirically by running `encode_felt252_data_and_calc_blake_hash` on empty input.
pub const STEPS_EMPTY_INPUT_EXPECT: Expect = expect!["169"];
pub static STEPS_EMPTY_INPUT: LazyLock<usize> =
LazyLock::new(|| STEPS_EMPTY_INPUT_EXPECT.data.parse().expect("Expected data is a usize"));

// The constants used are empirical, based on running `encode_felt252_data_and_calc_blake_hash`
// on combinations of large and small felts.
// VM steps per large felt.
pub const STEPS_PER_LARGE_FELT_EXPECT: Expect = expect!["45"];
pub static STEPS_PER_LARGE_FELT: LazyLock<usize> =
LazyLock::new(|| STEPS_PER_LARGE_FELT_EXPECT.data.parse().expect("Expected data is a usize"));
// VM steps per small felt.
pub const STEPS_PER_SMALL_FELT_EXPECT: Expect = expect!["15"];
pub static STEPS_PER_SMALL_FELT: LazyLock<usize> =
LazyLock::new(|| STEPS_PER_SMALL_FELT_EXPECT.data.parse().expect("Expected data is a usize"));
// Base overhead when input exactly fills a 16-u32 Blake message.
pub const BASE_STEPS_FULL_MSG_EXPECT: Expect = expect!["216"];
pub static BASE_STEPS_FULL_MSG: LazyLock<usize> =
LazyLock::new(|| BASE_STEPS_FULL_MSG_EXPECT.data.parse().expect("Expected data is a usize"));
// Base overhead when the input leaves a remainder (< 16 u32s) for a Blake message.
pub const BASE_STEPS_PARTIAL_MSG_EXPECT: Expect = expect!["194"];
pub static BASE_STEPS_PARTIAL_MSG: LazyLock<usize> =
LazyLock::new(|| BASE_STEPS_PARTIAL_MSG_EXPECT.data.parse().expect("Expected data is a usize"));
// Extra VM steps added per 2-u32 remainder in partial Blake messages.
pub const STEPS_PER_2_U32_REMINDER_EXPECT: Expect = expect!["3"];
pub static STEPS_PER_2_U32_REMINDER: LazyLock<usize> = LazyLock::new(|| {
STEPS_PER_2_U32_REMINDER_EXPECT.data.parse().expect("Expected data is a usize")
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compile-time constants replaced with runtime-parsed lazy statics

Medium Severity

Six constants that were previously const usize (e.g., const STEPS_EMPTY_INPUT: usize = 169) are now LazyLock<usize> statics that parse strings at runtime via .data.parse(). This converts compile-time-checked values into runtime-initialized values that can panic on first access, adds lock overhead on every dereference, and relies on #[doc(hidden)] internals of expect-test. The expect! pattern is intended for test assertions, not for defining production constants.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1286e2e. Configure here.


pub struct CasmV2HashResourceEstimate {}

impl CasmV2HashResourceEstimate {
Expand All @@ -248,23 +280,6 @@ impl CasmV2HashResourceEstimate {
// Input for Blake hash function is a sequence of 16 `u32` words.
pub const U32_WORDS_PER_MESSAGE: usize = 16;

// Base number of VM steps applied when the input to Blake hashing is empty.
// Determined empirically by running `encode_felt252_data_and_calc_blake_hash` on empty input.
pub const STEPS_EMPTY_INPUT: usize = 169;

// The constants used are empirical, based on running `encode_felt252_data_and_calc_blake_hash`
// on combinations of large and small felts.
// VM steps per large felt.
pub const STEPS_PER_LARGE_FELT: usize = 45;
// VM steps per small felt.
pub const STEPS_PER_SMALL_FELT: usize = 15;
// Base overhead when input exactly fills a 16-u32 Blake message.
pub const BASE_STEPS_FULL_MSG: usize = 216;
// Base overhead when the input leaves a remainder (< 16 u32s) for a Blake message.
pub const BASE_STEPS_PARTIAL_MSG: usize = 194;
// Extra VM steps added per 2-u32 remainder in partial Blake messages.
pub const STEPS_PER_2_U32_REMINDER: usize = 3;

/// Estimates the number of VM steps required to hash the given felts with Blake in Starknet OS.
///
/// - Each small felt unpacks into 2 `u32`s.
Expand All @@ -277,23 +292,22 @@ impl CasmV2HashResourceEstimate {
let encoded_u32_len = felt_size_groups.encoded_u32_len();
if encoded_u32_len == 0 {
// The empty input case is a special case.
return Self::STEPS_EMPTY_INPUT;
return *STEPS_EMPTY_INPUT;
}

// Adds a base cost depending on whether the total fits exactly into full 16-u32 messages.
let base_steps = if encoded_u32_len.is_multiple_of(Self::U32_WORDS_PER_MESSAGE) {
Self::BASE_STEPS_FULL_MSG
*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.
Self::BASE_STEPS_PARTIAL_MSG
+ (encoded_u32_len % Self::U32_WORDS_PER_MESSAGE / 2)
* Self::STEPS_PER_2_U32_REMINDER
*BASE_STEPS_PARTIAL_MSG
+ (encoded_u32_len % Self::U32_WORDS_PER_MESSAGE / 2) * *STEPS_PER_2_U32_REMINDER
};

base_steps
+ felt_size_groups.large * Self::STEPS_PER_LARGE_FELT
+ felt_size_groups.small * Self::STEPS_PER_SMALL_FELT
+ felt_size_groups.large * *STEPS_PER_LARGE_FELT
+ felt_size_groups.small * *STEPS_PER_SMALL_FELT
}
}

Expand Down
12 changes: 3 additions & 9 deletions crates/blockifier/src/execution/casm_hash_estimation_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::execution::call_info::OpcodeName;
use crate::execution::casm_hash_estimation::{
CasmV2HashResourceEstimate,
EstimateCasmHashResources,
STEPS_EMPTY_INPUT,
};
use crate::execution::contract_class::FeltSizeCount;

Expand All @@ -29,11 +30,7 @@ fn test_zero_inputs() {
CasmV2HashResourceEstimate::estimate_steps_of_encode_felt252_data_and_calc_blake_hash(
&FeltSizeCount::default(),
);
assert_eq!(
steps,
CasmV2HashResourceEstimate::STEPS_EMPTY_INPUT,
"Unexpected base step cost for zero inputs"
);
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();
Expand All @@ -42,10 +39,7 @@ fn test_zero_inputs() {
// Should result in base cost only (no opcode cost).
let resources =
CasmV2HashResourceEstimate::estimated_resources_of_hash_function(&FeltSizeCount::default());
let expected = ExecutionResources {
n_steps: CasmV2HashResourceEstimate::STEPS_EMPTY_INPUT,
..Default::default()
};
let expected = ExecutionResources { n_steps: *STEPS_EMPTY_INPUT, ..Default::default() };
assert_eq!(resources.vm_resources, expected, "Unexpected resources values for zero-input hash");
assert_eq!(
*resources.opcode_instance_counter.get(&OpcodeName::blake).unwrap_or(&0),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ use std::collections::HashMap;
use blockifier::execution::casm_hash_estimation::{
CasmV2HashResourceEstimate,
EstimateCasmHashResources,
BASE_STEPS_FULL_MSG_EXPECT,
BASE_STEPS_PARTIAL_MSG_EXPECT,
STEPS_EMPTY_INPUT_EXPECT,
STEPS_PER_2_U32_REMINDER,
STEPS_PER_2_U32_REMINDER_EXPECT,
STEPS_PER_LARGE_FELT,
STEPS_PER_LARGE_FELT_EXPECT,
STEPS_PER_SMALL_FELT,
STEPS_PER_SMALL_FELT_EXPECT,
};
use blockifier::execution::contract_class::FeltSizeCount;
use cairo_vm::types::builtin_name::BuiltinName;
Expand Down Expand Up @@ -84,6 +93,9 @@ fn cairo_encode_felt252_data_and_calc_blake_hash_steps(input: &[Felt]) -> usize
}

/// Asserts the estimated constants correspond to empiric measurements.
///
/// To fix the constants, it may be necessary to run with UPDATE_EXPECT=1 more than once.
///
/// Note that the resulting constants cannot be used to compute the precise number of steps required
/// for any given input. As an example, the total steps required for input [x; n] where x is a small
/// felt and n ranges from 0 to 9 is: [169, 212, 230, 248, 266, 284, 302, 320, 336, 332]. The first
Expand All @@ -102,7 +114,7 @@ fn test_blake_step_constants() {

// Test empty input.
let steps_empty = cairo_encode_felt252_data_and_calc_blake_hash_steps(&[]);
assert_eq!(steps_empty, CasmV2HashResourceEstimate::STEPS_EMPTY_INPUT);
STEPS_EMPTY_INPUT_EXPECT.assert_eq(&steps_empty.to_string());

// Start with a baseline of one full word.
let one_message_large_felts = vec![large_felt; LARGE_FELTS_PER_MESSAGE];
Expand All @@ -118,7 +130,7 @@ fn test_blake_step_constants() {
.collect::<Vec<Felt>>(),
) - baseline_steps)
/ LARGE_FELTS_PER_MESSAGE;
assert_eq!(large_felt_overhead, CasmV2HashResourceEstimate::STEPS_PER_LARGE_FELT);
STEPS_PER_LARGE_FELT_EXPECT.assert_eq(&large_felt_overhead.to_string());

// Add another full word of small felts to compute the overhead per small felt.
let one_message_small_felts = vec![small_felt; SMALL_FELTS_PER_MESSAGE];
Expand All @@ -130,12 +142,12 @@ fn test_blake_step_constants() {
.collect::<Vec<Felt>>(),
) - baseline_steps)
/ SMALL_FELTS_PER_MESSAGE;
assert_eq!(small_felt_overhead, CasmV2HashResourceEstimate::STEPS_PER_SMALL_FELT);
STEPS_PER_SMALL_FELT_EXPECT.assert_eq(&small_felt_overhead.to_string());

// Compute the full-word overhead by subtracting the overhead of one word of large felts, from
// the result of exactly one word of large felts.
let full_word_overhead = baseline_steps - (large_felt_overhead * LARGE_FELTS_PER_MESSAGE);
assert_eq!(full_word_overhead, CasmV2HashResourceEstimate::BASE_STEPS_FULL_MSG);
BASE_STEPS_FULL_MSG_EXPECT.assert_eq(&full_word_overhead.to_string());

// Compute the two-word partial overhead by computing:
// X, one full message of large felts + one half-message of small felts.
Expand All @@ -160,18 +172,18 @@ fn test_blake_step_constants() {
let remainder = one_plus_half_message_plus_small_felt_steps
- one_plus_half_message_steps
- small_felt_overhead;
assert_eq!(remainder, CasmV2HashResourceEstimate::STEPS_PER_2_U32_REMINDER);
STEPS_PER_2_U32_REMINDER_EXPECT.assert_eq(&remainder.to_string());

// Reuse the above computation to deduce the constant overhead for the case where the input does
// not fit into an exact multiple of messages.
let partial_message_overhead = one_plus_half_message_steps
// Partial message: small felt overhead.
- (SMALL_FELTS_PER_MESSAGE / 2) * CasmV2HashResourceEstimate::STEPS_PER_SMALL_FELT
- (SMALL_FELTS_PER_MESSAGE / 2) * *STEPS_PER_SMALL_FELT
// Partial message: remainder per word-pair in the partial message.
- (SMALL_FELTS_PER_MESSAGE / 2) * CasmV2HashResourceEstimate::STEPS_PER_2_U32_REMINDER
- (SMALL_FELTS_PER_MESSAGE / 2) * *STEPS_PER_2_U32_REMINDER
// Overhead of first (full) message: two large felts.
- LARGE_FELTS_PER_MESSAGE * CasmV2HashResourceEstimate::STEPS_PER_LARGE_FELT;
assert_eq!(partial_message_overhead, CasmV2HashResourceEstimate::BASE_STEPS_PARTIAL_MSG);
- LARGE_FELTS_PER_MESSAGE * *STEPS_PER_LARGE_FELT;
BASE_STEPS_PARTIAL_MSG_EXPECT.assert_eq(&partial_message_overhead.to_string());
}

/// Test that compares Cairo and Rust implementations of
Expand Down
Loading