From 6c60441e946f643d78368fe7de83a4ecbd6d6e2f Mon Sep 17 00:00:00 2001 From: AvivYossef-starkware Date: Sun, 29 Dec 2024 17:03:56 +0200 Subject: [PATCH] feat(blockifier): refactor native stack config --- .../execution/native/entry_point_execution.rs | 42 ++++++++++++------- .../syscalls/syscall_tests/out_of_gas.rs | 18 ++++++-- crates/blockifier/src/versioned_constants.rs | 30 +++++++++++++ 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/crates/blockifier/src/execution/native/entry_point_execution.rs b/crates/blockifier/src/execution/native/entry_point_execution.rs index ac0a077b42..811314022b 100644 --- a/crates/blockifier/src/execution/native/entry_point_execution.rs +++ b/crates/blockifier/src/execution/native/entry_point_execution.rs @@ -1,6 +1,7 @@ use cairo_native::execution_result::ContractExecutionResult; use cairo_native::utils::BuiltinCosts; use cairo_vm::vm::runners::cairo_runner::ExecutionResources; +use num_rational::Ratio; use stacker; use crate::execution::call_info::{CallExecution, CallInfo, ChargedResources, Retdata}; @@ -15,6 +16,7 @@ use crate::execution::errors::{EntryPointExecutionError, PostExecutionError, Pre use crate::execution::native::contract_class::NativeCompiledClassV1; use crate::execution::native::syscall_handler::NativeSyscallHandler; use crate::state::state_api::State; +use crate::versioned_constants::CairoNativeStackConfig; // todo(rodrigo): add an `entry point not found` test for Native pub fn execute_entry_point_call( @@ -40,6 +42,17 @@ pub fn execute_entry_point_call( mul_mod: gas_costs.builtins.mul_mod, }; + // Pre-charge entry point's initial budget to ensure sufficient gas for executing a minimal + // entry point code. When redepositing is used, the entry point is aware of this pre-charge + // and adjusts the gas counter accordingly if a smaller amount of gas is required. + let initial_budget = syscall_handler.base.context.gas_costs().base.entry_point_initial_budget; + let call_initial_gas = syscall_handler + .base + .call + .initial_gas + .checked_sub(initial_budget) + .ok_or(PreExecutionError::InsufficientEntryPointGas)?; + // Grow the stack (if it's below the red zone) to handle deep Cairo recursions - // when running Cairo natively, the real stack is used and could get overflowed // (unlike the VM where the stack is simulated in the heap as a memory segment). @@ -53,20 +66,21 @@ pub fn execute_entry_point_call( // The gas upper bound is MAX_POSSIBLE_SIERRA_GAS, and sequencers must not raise it without // adjusting the stack size. // This also limits multi-threading, since each thread has its own stack. - // TODO(Aviv/Yonatan): add these numbers to overridable VC. - let stack_size_red_zone = 160 * 1024 * 1024; - let target_stack_size = stack_size_red_zone + 10 * 1024 * 1024; - - // Pre-charge entry point's initial budget to ensure sufficient gas for executing a minimal - // entry point code. When redepositing is used, the entry point is aware of this pre-charge - // and adjusts the gas counter accordingly if a smaller amount of gas is required. - let initial_budget = syscall_handler.base.context.gas_costs().base.entry_point_initial_budget; - let call_initial_gas = syscall_handler - .base - .call - .initial_gas - .checked_sub(initial_budget) - .ok_or(PreExecutionError::InsufficientEntryPointGas)?; + // If the the free stack size is in the red zone, We will grow the stack to the + // target size, relative to reaming gas. + let stack_config = CairoNativeStackConfig { + // TODO(Aviv): Take it from VC. + gas_to_stack_ratio: Ratio::new(1, 20), + max_stack_size: 200 * 1024 * 1024, + min_stack_red_zone: 2 * 1024 * 1024, + buffer_size: 5 * 1024 * 1024, + }; + let stack_size_red_zone = stack_config.get_stack_size_red_zone(call_initial_gas); + let target_stack_size = + usize::try_from(stack_config.get_target_stack_size(stack_size_red_zone)) + .unwrap_or_else(|e| panic!("Failed to convert target stack size to usize: {}", e)); + let stack_size_red_zone = usize::try_from(stack_size_red_zone) + .unwrap_or_else(|e| panic!("Failed to convert stack size red zone to usize: {}", e)); // Use `maybe_grow` and not `grow` for performance, since in happy flows, only the main call // should trigger the growth. let execution_result = stacker::maybe_grow(stack_size_red_zone, target_stack_size, || { diff --git a/crates/blockifier/src/execution/syscalls/syscall_tests/out_of_gas.rs b/crates/blockifier/src/execution/syscalls/syscall_tests/out_of_gas.rs index ed32807310..03201e5659 100644 --- a/crates/blockifier/src/execution/syscalls/syscall_tests/out_of_gas.rs +++ b/crates/blockifier/src/execution/syscalls/syscall_tests/out_of_gas.rs @@ -50,14 +50,24 @@ fn test_total_tx_limits_less_than_max_sierra_gas() { } #[cfg(feature = "cairo_native")] -#[test] +#[rstest::rstest] +#[case(MAX_POSSIBLE_SIERRA_GAS, MAX_POSSIBLE_SIERRA_GAS - 5380)] +#[case(MAX_POSSIBLE_SIERRA_GAS / 10, 349992640)] +#[case(MAX_POSSIBLE_SIERRA_GAS / 100, 34994200)] +#[case(MAX_POSSIBLE_SIERRA_GAS / 1000, 3499630)] +#[case(MAX_POSSIBLE_SIERRA_GAS / 10000, 344020)] +#[case(MAX_POSSIBLE_SIERRA_GAS / 100000, 27580)] +#[case(MAX_POSSIBLE_SIERRA_GAS / 1000000, 0)] +#[case(350, 0)] +#[case(35, 0)] +#[case(0, 0)] /// Tests that Native can handle deep recursion calls without overflowing the stack. /// Note that the recursive function must be complicated, since the compiler might transform /// simple recursions into loops. The tested function was manually tested with higher gas and /// reached stack overflow. /// /// Also, there is no need to test the VM here since it doesn't use the stack. -fn test_stack_overflow() { +fn test_stack_overflow(#[case] initial_gas: u64, #[case] gas_consumed: u64) { let test_contract = FeatureContract::TestContract(CairoVersion::Cairo1(RunnableCairo1::Native)); let mut state = test_state(&ChainInfo::create_for_testing(), BALANCE, &[(test_contract, 1)]); @@ -65,7 +75,7 @@ fn test_stack_overflow() { let entry_point_call = CallEntryPoint { calldata: calldata![depth], entry_point_selector: selector_from_name("test_stack_overflow"), - initial_gas: MAX_POSSIBLE_SIERRA_GAS, + initial_gas, ..trivial_external_entry_point_new(test_contract) }; let call_info = entry_point_call.execute_directly(&mut state).unwrap(); @@ -74,7 +84,7 @@ fn test_stack_overflow() { CallExecution { // 'Out of gas' retdata: retdata![felt!["0x4f7574206f6620676173"]], - gas_consumed: MAX_POSSIBLE_SIERRA_GAS - 5380, + gas_consumed, failed: true, ..Default::default() } diff --git a/crates/blockifier/src/versioned_constants.rs b/crates/blockifier/src/versioned_constants.rs index 928c9b6940..5a95931d93 100644 --- a/crates/blockifier/src/versioned_constants.rs +++ b/crates/blockifier/src/versioned_constants.rs @@ -483,6 +483,36 @@ pub struct ArchivalDataGasCosts { pub gas_per_code_byte: ResourceCost, } +pub struct CairoNativeStackConfig { + pub gas_to_stack_ratio: Ratio, + pub max_stack_size: u64, + pub min_stack_red_zone: u64, + pub buffer_size: u64, +} + +impl CairoNativeStackConfig { + /// Rounds up the given size to the nearest multiple of MB. + pub fn round_up_to_mb(size: u64) -> u64 { + const MB: u64 = 1024 * 1024; + if size % MB == 0 { size } else { ((size / MB) + 1) * MB } + } + + /// Returns the stack size sufficient for running Cairo Native. + /// Rounds up to the nearest multiple of MB. + pub fn get_stack_size_red_zone(&self, remaining_gas: u64) -> u64 { + let stack_size_based_on_gas = + (self.gas_to_stack_ratio * Ratio::new(remaining_gas, 1)).to_integer(); + // Ensure the computed stack size is within the allowed range. + CairoNativeStackConfig::round_up_to_mb( + stack_size_based_on_gas.clamp(self.min_stack_red_zone, self.max_stack_size), + ) + } + + pub fn get_target_stack_size(&self, red_zone: u64) -> u64 { + red_zone + self.buffer_size + } +} + #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)] pub struct EventLimits { pub max_data_length: usize,