Skip to content

Commit 269a586

Browse files
ron-starkwareclaude
andcommitted
blockifier: omit cairo-vm-specific VM frames from SierraGas revert traces
For SierraGas-mode (Cairo 1) contracts the choice of execution backend (cairo-vm CASM vs cairo-native) is meant to be a free implementation detail, but blockifier's stack-trace formatter was committing the backend choice into the rendered revert reason: cairo-vm produces a `VmException` with a PC and a Cairo traceback, cairo-native does not, and `stack_trace.rs` faithfully rendered the former and skipped the latter. Because the revert reason is hashed verbatim into the receipt commitment (and thus the block hash), the same Cairo 1 contract reverting on the same input produced two different receipts depending on which backend was hot in the contract class manager's cache at the moment of execution. This is what bites echonet replays of mainnet: any mainnet block where a Cairo 1 contract reverts can mismatch echonet's receipt commitment iff echonet ran that class via native while mainnet ran it via CASM (or vice versa). This patch flips the `strip_vm_frames_in_sierra_gas` flag (introduced as a no-op in the preceding commit) from false to true at protocol v0.14.3, and teaches the stack-trace formatter to honor it: * The flag is set in `blockifier_versioned_constants_0_14_3.json`. Pre-v0.14.3 versions remain at false, so historical receipts replay byte-identically. * `extract_entry_point_execution_error_into_stack_trace` reads the annotation from the `AnnotatedEntryPointExecutionError` carried by the outer `TransactionExecutionError` field, and inside the `CairoRunError` arm skips the `Error at pc=` / `Cairo traceback` block when the frame is `SierraGas`-tracked AND the strip policy is on. Cairo 0 (`CairoSteps`) frames are unaffected at every version. * Cairo 0 traces are unchanged at every protocol version. Native traces are unchanged. A SierraGas-mode contract at v0.14.3+ now renders identically whether it ran via cairo-vm CASM or cairo-native. Verified by a new parameterized test (`test_revert_text_is_backend_invariant_for_sierra_gas`) that runs the same Cairo 1 deploy-faulty-ctor flow on both backends and asserts byte-identical revert text, plus three unit tests covering the four `(TrackedResource, strip)` cells of the rendering matrix. Real-data confirmation: with `strip_vm_frames_in_sierra_gas: true` forced into the 0.14.1 json locally, `--compare-native` on mainnet block 6481044 reports matched=1 (formerly matched=0 between backends). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c1fef32 commit 269a586

7 files changed

Lines changed: 206 additions & 33 deletions

File tree

crates/blockifier/resources/blockifier_versioned_constants_0_14_3.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@
121121
"enable_reverts": true,
122122
"enable_casm_hash_migration": true,
123123
"block_casm_hash_v1_declares": true,
124-
"strip_vm_frames_in_sierra_gas": false,
124+
"strip_vm_frames_in_sierra_gas": true,
125125
"min_sierra_version_for_sierra_gas": "1.7.0",
126126
"enable_tip": true,
127127
"segment_arena_cells": false,

crates/blockifier/resources/versioned_constants_diff_regression/0.14.2_0.14.3.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
~ /os_resources/execute_syscalls/Sha512ProcessBlock/n_steps: 4737
77
~ /os_resources/execute_syscalls/StorageRead/n_steps: 240
88
~ /os_resources/execute_syscalls/StorageWrite/n_steps: 599
9+
~ /strip_vm_frames_in_sierra_gas: true

crates/blockifier/src/execution/stack_trace.rs

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ use starknet_api::core::{ClassHash, ContractAddress, EntryPointSelector};
1010
use starknet_api::execution_utils::format_panic_data;
1111

1212
use crate::execution::call_info::{CallInfo, Retdata};
13+
use crate::execution::contract_class::TrackedResource;
1314
use crate::execution::deprecated_syscalls::hint_processor::DeprecatedSyscallExecutionError;
14-
use crate::execution::errors::{ConstructorEntryPointExecutionError, EntryPointExecutionError};
15+
use crate::execution::errors::{
16+
AnnotatedEntryPointExecutionError,
17+
ConstructorEntryPointExecutionError,
18+
EntryPointExecutionError,
19+
};
1520
use crate::execution::syscalls::hint_processor::{
1621
SyscallExecutionError,
1722
ENTRYPOINT_FAILED_ERROR_FELT,
@@ -409,7 +414,7 @@ pub fn gen_tx_execution_error_trace(error: &TransactionExecutionError) -> ErrorS
409414
selector,
410415
} => gen_error_trace_from_entry_point_error(
411416
ErrorStackHeader::Execution,
412-
error.unannotated(),
417+
error,
413418
storage_address,
414419
class_hash,
415420
Some(selector),
@@ -422,7 +427,7 @@ pub fn gen_tx_execution_error_trace(error: &TransactionExecutionError) -> ErrorS
422427
selector,
423428
} => gen_error_trace_from_entry_point_error(
424429
ErrorStackHeader::Validation,
425-
error.unannotated(),
430+
error,
426431
storage_address,
427432
class_hash,
428433
Some(selector),
@@ -437,7 +442,7 @@ pub fn gen_tx_execution_error_trace(error: &TransactionExecutionError) -> ErrorS
437442
},
438443
) => gen_error_trace_from_entry_point_error(
439444
ErrorStackHeader::Constructor,
440-
error.unannotated(),
445+
error,
441446
storage_address,
442447
class_hash,
443448
constructor_selector.as_ref(),
@@ -460,7 +465,7 @@ pub fn gen_tx_execution_error_trace(error: &TransactionExecutionError) -> ErrorS
460465
/// Generate error stack from top-level entry point execution error.
461466
fn gen_error_trace_from_entry_point_error(
462467
header: ErrorStackHeader,
463-
error: &EntryPointExecutionError,
468+
error: &AnnotatedEntryPointExecutionError,
464469
storage_address: &ContractAddress,
465470
class_hash: &ClassHash,
466471
entry_point_selector: Option<&EntryPointSelector>,
@@ -486,16 +491,19 @@ fn extract_cairo_run_error_into_stack_trace(
486491
error_stack: &mut ErrorStack,
487492
depth: usize,
488493
error: &CairoRunError,
494+
omit_vm_frame: bool,
489495
) {
490496
if let CairoRunError::VmException(vm_exception) = error {
491-
error_stack.push(
492-
VmExceptionFrame {
493-
pc: vm_exception.pc,
494-
error_attr_value: vm_exception.error_attr_value.clone(),
495-
traceback: vm_exception.traceback.clone(),
496-
}
497-
.into(),
498-
);
497+
if !omit_vm_frame {
498+
error_stack.push(
499+
VmExceptionFrame {
500+
pc: vm_exception.pc,
501+
error_attr_value: vm_exception.error_attr_value.clone(),
502+
traceback: vm_exception.traceback.clone(),
503+
}
504+
.into(),
505+
);
506+
}
499507
extract_virtual_machine_error_into_stack_trace(error_stack, depth, &vm_exception.inner_exc);
500508
} else {
501509
error_stack.push(error.to_string().into());
@@ -604,17 +612,13 @@ fn extract_syscall_execution_error_into_stack_trace(
604612
}
605613
.into(),
606614
);
607-
extract_entry_point_execution_error_into_stack_trace(
608-
error_stack,
609-
depth,
610-
error.unannotated(),
611-
)
615+
extract_entry_point_execution_error_into_stack_trace(error_stack, depth, error)
612616
}
613617
SyscallExecutionError::EntryPointExecutionError(entry_point_error) => {
614618
extract_entry_point_execution_error_into_stack_trace(
615619
error_stack,
616620
depth,
617-
entry_point_error.unannotated(),
621+
entry_point_error,
618622
)
619623
}
620624
_ => {
@@ -691,17 +695,13 @@ fn extract_deprecated_syscall_execution_error_into_stack_trace(
691695
}
692696
.into(),
693697
);
694-
extract_entry_point_execution_error_into_stack_trace(
695-
error_stack,
696-
depth,
697-
error.unannotated(),
698-
)
698+
extract_entry_point_execution_error_into_stack_trace(error_stack, depth, error)
699699
}
700700
DeprecatedSyscallExecutionError::EntryPointExecutionError(entry_point_error) => {
701701
extract_entry_point_execution_error_into_stack_trace(
702702
error_stack,
703703
depth,
704-
entry_point_error.unannotated(),
704+
entry_point_error,
705705
)
706706
}
707707
_ => error_stack.push(syscall_error.to_string().into()),
@@ -711,11 +711,22 @@ fn extract_deprecated_syscall_execution_error_into_stack_trace(
711711
fn extract_entry_point_execution_error_into_stack_trace(
712712
error_stack: &mut ErrorStack,
713713
depth: usize,
714-
entry_point_error: &EntryPointExecutionError,
714+
entry_point_error: &AnnotatedEntryPointExecutionError,
715715
) {
716-
match entry_point_error {
716+
let inner = entry_point_error.unannotated();
717+
match inner {
717718
EntryPointExecutionError::CairoRunError(cairo_run_error) => {
718-
extract_cairo_run_error_into_stack_trace(error_stack, depth, cairo_run_error)
719+
// Omit the cairo-vm PC/traceback only for SierraGas frames at versions where the
720+
// strip policy is on — makes the revert reason invariant across execution
721+
// backends. Cairo 0 (CairoSteps) always emits; it has no native counterpart.
722+
let omit_vm_frame = entry_point_error.strip_vm_frames_in_sierra_gas()
723+
&& entry_point_error.tracked_resource() == TrackedResource::SierraGas;
724+
extract_cairo_run_error_into_stack_trace(
725+
error_stack,
726+
depth,
727+
cairo_run_error,
728+
omit_vm_frame,
729+
)
719730
}
720731
#[cfg(feature = "cairo_native")]
721732
EntryPointExecutionError::NativeUnrecoverableError(error) => {
@@ -724,6 +735,6 @@ fn extract_entry_point_execution_error_into_stack_trace(
724735
EntryPointExecutionError::ExecutionFailed { error_trace } => {
725736
error_stack.push(error_trace.clone().into())
726737
}
727-
_ => error_stack.push(format!("{entry_point_error}\n").into()),
738+
_ => error_stack.push(format!("{inner}\n").into()),
728739
}
729740
}

crates/blockifier/src/execution/stack_trace_regression/test_contract_ctor_frame_stack_trace_cairo1_casm.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
Transaction execution has failed:
22
0: Error in the called contract (contract address: 0x00000000000000000000000000000000000000000000000000000000c0020000, class hash: 0x0000000000000000000000000000000000000000000000000000000080020000, selector: 0x015d40a3d6ca2ac30f4031e42be28da9b056fef9bb7357ac5e85627ee876e5ad):
3-
Error at pc=0:443:
43
1: Error in the called contract (contract address: 0x00000000000000000000000000000000000000000000000000000000c0020000, class hash: 0x0000000000000000000000000000000000000000000000000000000080020000, selector: 0x02730079d734ee55315f4f141eaed376bddd8c2133523d223a344c5604e0f7f8):
5-
Error at pc=0:797:
64
2: Error in the contract class constructor (contract address: 0x0103ee82605273496eed8d9141c5b3ad967baa08be63aa5bc49ffae5eae454cc, class hash: 0x0000000000000000000000000000000000000000000000000000000080040000, selector: 0x028ffe4ff0f226a9107253e17a904099aa4f63a02a5621de0576e5aa71bc5194):
75
Execution failed. Failure reason:
86
Error in contract (contract address: 0x0103ee82605273496eed8d9141c5b3ad967baa08be63aa5bc49ffae5eae454cc, class hash: 0x0000000000000000000000000000000000000000000000000000000080040000, selector: 0x028ffe4ff0f226a9107253e17a904099aa4f63a02a5621de0576e5aa71bc5194):

crates/blockifier/src/execution/stack_trace_test.rs

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ use assert_matches::assert_matches;
22
use blockifier_test_utils::cairo_versions::{CairoVersion, RunnableCairo1};
33
use blockifier_test_utils::calldata::create_calldata;
44
use blockifier_test_utils::contracts::FeatureContract;
5+
use cairo_vm::types::relocatable::Relocatable;
6+
use cairo_vm::vm::errors::cairo_run_errors::CairoRunError;
7+
use cairo_vm::vm::errors::vm_errors::VirtualMachineError;
8+
use cairo_vm::vm::errors::vm_exception::VmException;
59
use expect_test::expect_file;
610
use pretty_assertions::assert_eq;
711
use regex::Regex;
@@ -32,15 +36,17 @@ use starknet_api::transaction::fields::{
3236
ValidResourceBounds,
3337
};
3438
use starknet_api::transaction::TransactionVersion;
35-
use starknet_api::{calldata, felt, invoke_tx_args};
39+
use starknet_api::{calldata, class_hash, contract_address, felt, invoke_tx_args};
3640
use starknet_types_core::felt::Felt;
3741

3842
use crate::context::{BlockContext, ChainInfo};
3943
use crate::execution::call_info::{CallExecution, CallInfo, Retdata};
44+
use crate::execution::contract_class::TrackedResource;
4045
use crate::execution::entry_point::CallEntryPoint;
4146
use crate::execution::errors::EntryPointExecutionError;
4247
use crate::execution::stack_trace::{
4348
extract_trailing_cairo1_revert_trace,
49+
gen_tx_execution_error_trace,
4450
Cairo1RevertHeader,
4551
Cairo1RevertSummary,
4652
MIN_CAIRO1_FRAME_LENGTH,
@@ -51,6 +57,7 @@ use crate::test_utils::initial_test_state::{fund_account, test_state};
5157
use crate::test_utils::test_templates::cairo_version;
5258
use crate::test_utils::BALANCE;
5359
use crate::transaction::account_transaction::{AccountTransaction, ExecutionFlags};
60+
use crate::transaction::errors::TransactionExecutionError;
5461
use crate::transaction::objects::TransactionInfoCreator;
5562
use crate::transaction::test_utils::{
5663
block_context,
@@ -842,6 +849,71 @@ Error in contract (contract address: {expected_address:#066x}, class hash: {:#06
842849
);
843850
}
844851

852+
/// Drives the deploy-faulty-ctor flow once on the given Cairo 1 backend and returns the
853+
/// rendered revert text. Same setup as `test_contract_ctor_frame_stack_trace` (Cairo1 cases),
854+
/// extracted so the cross-backend equality test below can run both backends in one process.
855+
#[cfg(feature = "cairo_native")]
856+
fn render_faulty_ctor_revert(
857+
block_context: &BlockContext,
858+
default_all_resource_bounds: ValidResourceBounds,
859+
runnable: RunnableCairo1,
860+
) -> String {
861+
let cairo_version = CairoVersion::Cairo1(runnable);
862+
let chain_info = &block_context.chain_info;
863+
let account = FeatureContract::AccountWithoutValidations(cairo_version);
864+
let faulty_ctor = FeatureContract::FaultyAccount(cairo_version);
865+
let state = &mut test_state(chain_info, BALANCE, &[(account, 1), (faulty_ctor, 0)]);
866+
let account_address = account.get_instance_address(0);
867+
let faulty_class_hash = faulty_ctor.get_class_hash();
868+
869+
let invoke_deploy_tx = invoke_tx_with_default_flags(invoke_tx_args! {
870+
sender_address: account_address,
871+
signature: TransactionSignature(vec![felt!(INVALID)].into()),
872+
calldata: create_calldata(
873+
account_address,
874+
DEPLOY_CONTRACT_FUNCTION_ENTRY_POINT_NAME,
875+
&[
876+
faulty_class_hash.0,
877+
felt!(7_u8), // salt
878+
felt!(1_u8), // ctor args length
879+
felt!(FELT_TRUE), // validate_constructor: true → fail
880+
]
881+
),
882+
resource_bounds: default_all_resource_bounds,
883+
nonce: Nonce(felt!(0_u8)),
884+
});
885+
invoke_deploy_tx.execute(state, block_context).unwrap().revert_error.unwrap().to_string()
886+
}
887+
888+
/// At v0.14.3+ (strip policy on), the same Cairo 1 flow must produce a byte-identical revert
889+
/// string regardless of execution backend — this is what makes `receipt_commitment` invariant
890+
/// under cairo-native vs cairo-vm CASM. Pre-patch (origin/main-v0.14.3), this assertion would
891+
/// fail: CASM emitted `Error at pc=0:443:` / `Error at pc=0:797:` lines under the outer two
892+
/// frames that native never produced, see the historical diff of
893+
/// `test_contract_ctor_frame_stack_trace_cairo1_casm.txt`.
894+
#[cfg(feature = "cairo_native")]
895+
#[rstest]
896+
fn test_revert_text_is_backend_invariant_for_sierra_gas(
897+
block_context: BlockContext,
898+
default_all_resource_bounds: ValidResourceBounds,
899+
) {
900+
let casm_revert = render_faulty_ctor_revert(
901+
&block_context,
902+
default_all_resource_bounds,
903+
RunnableCairo1::Casm,
904+
);
905+
let native_revert = render_faulty_ctor_revert(
906+
&block_context,
907+
default_all_resource_bounds,
908+
RunnableCairo1::Native,
909+
);
910+
assert_eq!(
911+
casm_revert, native_revert,
912+
"Cairo 1 revert text must be backend-invariant at v0.14.3+; CASM and Native \
913+
diverged.\nCASM:\n{casm_revert}\n\nNative:\n{native_revert}"
914+
);
915+
}
916+
845917
#[test]
846918
fn test_min_cairo1_frame_length() {
847919
let failure_hex = "0xdeadbeef";
@@ -1080,3 +1152,92 @@ fn test_cairo1_stack_extraction_not_failure_fallback() {
10801152
if stack.is_empty() && last_retdata == expected_retdata
10811153
);
10821154
}
1155+
1156+
/// Build a synthetic `EntryPointExecutionError::CairoRunError(VmException)` to feed into the
1157+
/// formatter. The exact PC/traceback strings are arbitrary; the test asserts on whether the
1158+
/// `Error at pc=` block survives the formatter, not on its contents.
1159+
fn synthetic_vm_exception_error() -> EntryPointExecutionError {
1160+
let vm_exception = VmException {
1161+
pc: Relocatable { segment_index: 0, offset: 42 },
1162+
inst_location: None,
1163+
inner_exc: VirtualMachineError::Unexpected,
1164+
error_attr_value: None,
1165+
traceback: Some(
1166+
"Cairo traceback (most recent call last):\nUnknown location (pc=0:7)\n".to_string(),
1167+
),
1168+
};
1169+
EntryPointExecutionError::CairoRunError(Box::new(CairoRunError::VmException(vm_exception)))
1170+
}
1171+
1172+
/// Wraps a synthetic CairoRunError as the top-level error of a `TransactionExecutionError`
1173+
/// with the given `(tracked_resource, strip_vm_frames_in_sierra_gas)` annotation. Returns
1174+
/// the formatted revert string.
1175+
fn render_revert_for(tracked_resource: TrackedResource, strip: bool) -> String {
1176+
let top_error = synthetic_vm_exception_error().annotated(tracked_resource, strip);
1177+
let tx_error = TransactionExecutionError::ExecutionError {
1178+
error: Box::new(top_error),
1179+
class_hash: class_hash!("0xabc"),
1180+
storage_address: contract_address!("0x123"),
1181+
selector: EntryPointSelector(felt!("0xdef")),
1182+
};
1183+
gen_tx_execution_error_trace(&tx_error).to_string()
1184+
}
1185+
1186+
/// SierraGas frame at a protocol version where the policy is active (v0.14.3+): the formatter
1187+
/// MUST omit the `Error at pc=` / `Cairo traceback` block, so the revert reason is identical
1188+
/// whether the contract ran via cairo-vm CASM or cairo-native.
1189+
#[test]
1190+
fn test_sierra_gas_frame_omits_vm_exception_block_when_strip_enabled() {
1191+
let rendered = render_revert_for(TrackedResource::SierraGas, true);
1192+
assert!(
1193+
!rendered.contains("Error at pc="),
1194+
"with strip enabled, SierraGas-mode revert must not include `Error at pc=` (got: \
1195+
{rendered:?})"
1196+
);
1197+
assert!(
1198+
!rendered.contains("Cairo traceback"),
1199+
"with strip enabled, SierraGas-mode revert must not include `Cairo traceback` (got: \
1200+
{rendered:?})"
1201+
);
1202+
// The frame preamble must still be present.
1203+
assert!(
1204+
rendered.contains("Error in the called contract"),
1205+
"frame preamble missing (got: {rendered:?})"
1206+
);
1207+
}
1208+
1209+
/// SierraGas frame at a protocol version where the policy is OFF (pre-v0.14.3): the formatter
1210+
/// MUST keep the legacy `Error at pc=` / `Cairo traceback` block, so we replay historical
1211+
/// blocks with byte-identical receipts. Replaying old blocks would break otherwise.
1212+
#[test]
1213+
fn test_sierra_gas_frame_keeps_vm_exception_block_when_strip_disabled() {
1214+
let rendered = render_revert_for(TrackedResource::SierraGas, false);
1215+
assert!(
1216+
rendered.contains("Error at pc=0:42:"),
1217+
"with strip disabled, SierraGas-mode revert must include `Error at pc=` (got: \
1218+
{rendered:?})"
1219+
);
1220+
assert!(
1221+
rendered.contains("Cairo traceback (most recent call last):"),
1222+
"with strip disabled, SierraGas-mode revert must include `Cairo traceback` (got: \
1223+
{rendered:?})"
1224+
);
1225+
}
1226+
1227+
/// CairoSteps frame (Cairo 0): the formatter MUST emit the `Error at pc=` block regardless of
1228+
/// the strip policy, since Cairo 0 has no alternative execution backend and the PC + traceback
1229+
/// are useful debug information that nothing else carries.
1230+
#[rstest]
1231+
fn test_cairo_steps_frame_keeps_vm_exception_block_under_either_policy(
1232+
#[values(true, false)] strip: bool,
1233+
) {
1234+
let rendered = render_revert_for(TrackedResource::CairoSteps, strip);
1235+
assert!(
1236+
rendered.contains("Error at pc=0:42:"),
1237+
"CairoSteps-mode revert (strip={strip}) must include `Error at pc=` (got: {rendered:?})"
1238+
);
1239+
assert!(
1240+
rendered.contains("Cairo traceback (most recent call last):"),
1241+
"CairoSteps-mode revert (strip={strip}) must include `Cairo traceback` (got: {rendered:?})"
1242+
);
1243+
}

crates/blockifier_reexecution/block_numbers_for_reexecution.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"0.13.6": 1743490,
1111
"0.14.0": 2509604,
1212
"0.14.1": 4448394,
13+
"0.14.1_sierra_gas_revert": 6481044,
1314
"0.14.2_with_proof_facts": 9023035,
1415
"first_0.13.5_rpc_v0_8": 1400000,
1516
"second_0.13.5_rpc_v0_8": 1450000,

crates/blockifier_reexecution/src/state_reader/offline_state_reader_test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ fn reexecute_block_for_testing(block_number: u64) {
3939
#[case::v_0_13_6(1743490)]
4040
#[case::v_0_14_0(2509604)]
4141
#[case::v_0_14_1(4448394)]
42+
#[case::v_0_14_1_sierra_gas_revert(6481044)]
4243
#[case::v_0_14_2_with_proof_facts(9023035)]
4344
#[case::first_v_0_13_5_rpc_v8(1400000)]
4445
#[case::second_v_0_13_5_rpc_v8(1450000)]

0 commit comments

Comments
 (0)