diff --git a/crates/blockifier/src/execution/stack_trace.rs b/crates/blockifier/src/execution/stack_trace.rs index 88bdf72c7f..ae797ce952 100644 --- a/crates/blockifier/src/execution/stack_trace.rs +++ b/crates/blockifier/src/execution/stack_trace.rs @@ -1,4 +1,5 @@ use std::fmt::{Display, Formatter}; +use std::sync::LazyLock; use cairo_vm::types::relocatable::Relocatable; use cairo_vm::vm::errors::cairo_run_errors::CairoRunError; @@ -163,6 +164,16 @@ pub struct Cairo1RevertFrame { pub selector: EntryPointSelector, } +pub static MIN_CAIRO1_FRAME_LENGTH: LazyLock = LazyLock::new(|| { + let frame = Cairo1RevertFrame { + contract_address: ContractAddress::default(), + class_hash: Some(ClassHash::default()), + selector: EntryPointSelector::default(), + }; + // +1 for newline. + format!("{}", frame).len() + 1 +}); + impl From<&&CallInfo> for Cairo1RevertFrame { fn from(callinfo: &&CallInfo) -> Self { Self { @@ -194,16 +205,71 @@ pub struct Cairo1RevertStack { pub last_retdata: Retdata, } +impl Cairo1RevertStack { + pub const TRUNCATION_SEPARATOR: &'static str = "\n..."; +} + +pub static MIN_CAIRO1_FRAMES_STACK_LENGTH: LazyLock = LazyLock::new(|| { + // Two frames (first and last) + separator. + 2 * *MIN_CAIRO1_FRAME_LENGTH + Cairo1RevertStack::TRUNCATION_SEPARATOR.len() +}); + impl Display for Cairo1RevertStack { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // Total string length is limited by TRACE_LENGTH_CAP. + + // Prioritize the failure reason felts over the frames. + // If the failure reason is too long to include a minimal frame trace, display only the + // failure reason (truncated if necessary). + let failure_reason = format_panic_data(&self.last_retdata.0); + if failure_reason.len() >= TRACE_LENGTH_CAP - *MIN_CAIRO1_FRAMES_STACK_LENGTH { + let output = if failure_reason.len() <= TRACE_LENGTH_CAP { + failure_reason + } else { + failure_reason + .chars() + .take(TRACE_LENGTH_CAP - Self::TRUNCATION_SEPARATOR.len()) + .collect::() + + Self::TRUNCATION_SEPARATOR + }; + return write!(f, "{}", output); + } + + let untruncated_string = self + .stack + .iter() + .map(|frame| frame.to_string()) + .chain([failure_reason.clone()]) + .join("\n"); + if untruncated_string.len() <= TRACE_LENGTH_CAP { + return write!(f, "{}", untruncated_string); + } + + // If the number of frames is too large, drop frames above the last frame (two frames are + // not too many, as checked above with MIN_CAIRO1_FRAMES_STACK_LENGTH). + let n_frames_to_drop = (untruncated_string.len() - TRACE_LENGTH_CAP + + Self::TRUNCATION_SEPARATOR.len()) + .div_ceil(*MIN_CAIRO1_FRAME_LENGTH); + + // If the number of frames is not as expected, fall back to the failure reason. + let final_string = + match (self.stack.get(..self.stack.len() - n_frames_to_drop - 1), self.stack.last()) { + (Some(frames), Some(last_frame)) => frames + .iter() + .map(|frame| frame.to_string()) + .chain([ + String::from(Self::TRUNCATION_SEPARATOR), + last_frame.to_string(), + failure_reason, + ]) + .join("\n"), + _ => failure_reason, + }; write!( f, "{}", - self.stack - .iter() - .map(|frame| frame.to_string()) - .chain([format_panic_data(&self.last_retdata.0)]) - .join("\n") + // Truncate again as a failsafe. + final_string.chars().take(TRACE_LENGTH_CAP).collect::() ) } } diff --git a/crates/blockifier/src/execution/stack_trace_test.rs b/crates/blockifier/src/execution/stack_trace_test.rs index d3a20f94d0..864bb19b7c 100644 --- a/crates/blockifier/src/execution/stack_trace_test.rs +++ b/crates/blockifier/src/execution/stack_trace_test.rs @@ -14,6 +14,9 @@ use starknet_api::{calldata, felt, invoke_tx_args}; use crate::abi::abi_utils::selector_from_name; use crate::abi::constants::CONSTRUCTOR_ENTRY_POINT_NAME; use crate::context::{BlockContext, ChainInfo}; +use crate::execution::call_info::{CallExecution, CallInfo, Retdata}; +use crate::execution::stack_trace::{extract_trailing_cairo1_revert_trace, TRACE_LENGTH_CAP}; +use crate::execution::syscalls::hint_processor::ENTRYPOINT_FAILED_ERROR; use crate::test_utils::contracts::FeatureContract; use crate::test_utils::initial_test_state::{fund_account, test_state}; use crate::test_utils::{create_calldata, CairoVersion, BALANCE}; @@ -818,3 +821,33 @@ Error in contract (contract address: {expected_address:#064x}, class hash: {:#06 invoke_deploy_tx.execute(state, &block_context, true, true).unwrap().revert_error.unwrap(); assert_eq!(error.to_string(), expected_error); } + +#[rstest] +// Assume each frame is over 100 chars (contract address + selector are already 128 chars). +#[case::too_many_frames(TRACE_LENGTH_CAP / 100, 1)] +// Each (large) felt should require at least 30 chars. +#[case::too_much_retdata(1, TRACE_LENGTH_CAP / 30)] +#[case::both_too_much(TRACE_LENGTH_CAP / 200, TRACE_LENGTH_CAP / 60)] +fn test_cairo1_revert_error_truncation(#[case] n_frames: usize, #[case] n_retdata: usize) { + let mut retdata = Retdata(vec![felt!("0xbeefbeefbeefbeefbeefbeefbeefbeef"); n_retdata]); + let mut next_call_info = CallInfo { + execution: CallExecution { retdata: retdata.clone(), failed: true, ..Default::default() }, + ..Default::default() + }; + for _ in 1..n_frames { + retdata.0.push(felt!(ENTRYPOINT_FAILED_ERROR)); + next_call_info = CallInfo { + inner_calls: vec![next_call_info], + execution: CallExecution { + retdata: retdata.clone(), + failed: true, + ..Default::default() + }, + ..Default::default() + }; + } + assert!( + format!("{}", extract_trailing_cairo1_revert_trace(&next_call_info)).len() + <= TRACE_LENGTH_CAP + ); +}