Skip to content
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

feat(blockifier): refactor native stack config #3003

Merged
merged 1 commit into from
Jan 1, 2025
Merged
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
42 changes: 28 additions & 14 deletions crates/blockifier/src/execution/native/entry_point_execution.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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(
Expand All @@ -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).
Expand All @@ -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, || {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,32 @@ 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)]);

let depth = felt!(1000000_u128);
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();
Expand All @@ -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()
}
Expand Down
31 changes: 31 additions & 0 deletions crates/blockifier/src/versioned_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,37 @@ pub struct ArchivalDataGasCosts {
pub gas_per_code_byte: ResourceCost,
}

pub struct CairoNativeStackConfig {
pub gas_to_stack_ratio: Ratio<u64>,
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;
size.div_ceil(MB) * 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 {
// Stack size should be a multiple of page size, since `stacker::grow` works with this unit.
CairoNativeStackConfig::round_up_to_mb(red_zone + self.buffer_size)
}
}

#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct EventLimits {
pub max_data_length: usize,
Expand Down
Loading