Skip to content

Commit 16563c3

Browse files
blockifier: improve revert handling (#11426)
wip
1 parent 5c3c150 commit 16563c3

File tree

8 files changed

+13218
-11510
lines changed

8 files changed

+13218
-11510
lines changed

crates/blockifier/src/bouncer_test.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -775,11 +775,11 @@ fn class_hash_migration_data_from_state(
775775

776776
if should_migrate {
777777
expect![[r#"
778-
100709658
778+
102995620
779779
"#]]
780780
.assert_debug_eq(&migration_sierra_gas.0);
781781
expect![[r#"
782-
243210896
782+
248629270
783783
"#]]
784784
.assert_debug_eq(&migration_proving_gas.0);
785785
} else {

crates/blockifier/src/execution/syscalls/syscall_base.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,25 @@ impl<'state> SyscallHandlerBase<'state> {
191191

192192
match self.original_values.entry(key) {
193193
hash_map::Entry::Vacant(entry) => {
194-
entry.insert(self.state.get_storage_at(contract_address, key)?);
194+
// Check if any inner call (entries created after this handler's entry) already
195+
// captured an original value for this key. If so, use that value instead of the
196+
// current state, because the inner call captured the true original before any
197+
// writes in this execution scope.
198+
let original_value = self
199+
.context
200+
.revert_infos
201+
.0
202+
.iter()
203+
.skip(self.revert_info_idx + 1)
204+
.find_map(|info| {
205+
if info.contract_address == contract_address {
206+
info.original_values.get(&key).copied()
207+
} else {
208+
None
209+
}
210+
})
211+
.map_or_else(|| self.state.get_storage_at(contract_address, key), Ok)?;
212+
entry.insert(original_value);
195213
}
196214
hash_map::Entry::Occupied(_) => {}
197215
}

crates/blockifier/src/execution/syscalls/syscall_tests/call_contract.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use pretty_assertions::assert_eq;
1010
use rstest::rstest;
1111
use starknet_api::abi::abi_utils::selector_from_name;
1212
use starknet_api::execution_utils::format_panic_data;
13+
use starknet_api::state::StorageKey;
1314
use starknet_api::transaction::fields::Calldata;
1415
use starknet_api::{calldata as calldata_macro, felt};
1516
use test_case::test_case;
@@ -18,6 +19,7 @@ use crate::context::{BlockContext, ChainInfo};
1819
use crate::execution::contract_class::TrackedResource;
1920
use crate::execution::entry_point::CallEntryPoint;
2021
use crate::retdata;
22+
use crate::state::state_api::StateReader;
2123
use crate::test_utils::initial_test_state::test_state;
2224
use crate::test_utils::syscall::build_recurse_calldata;
2325
use crate::test_utils::{trivial_external_entry_point_new, CompilerBasedVersion, BALANCE};
@@ -501,3 +503,55 @@ fn test_empty_function_flow(#[case] runnable: RunnableCairo1) {
501503
// Contract should not fail.
502504
assert!(!call_info.execution.failed);
503505
}
506+
507+
/// Test that storage is correctly reverted when nested calls write to the same key.
508+
///
509+
/// Scenario:
510+
/// - catch_write_revert_panic calls call_write_rewrite_panic and catches/ignores the revert
511+
/// - call_write_rewrite_panic calls write_1, then writes storage = 2, then panics
512+
/// - write_1 writes storage = 1
513+
///
514+
/// After call_write_rewrite_panic's revert, storage should be 0 (original), not 1 (write_1's
515+
/// write). This tests the fix for a bug where call_write_rewrite_panic would capture write_1's
516+
/// written value (1) as the "original" instead of the true original (0), causing incorrect state
517+
/// after revert.
518+
#[cfg_attr(feature = "cairo_native", test_case(RunnableCairo1::Native; "Native"))]
519+
#[test_case(RunnableCairo1::Casm; "VM")]
520+
fn test_nested_call_storage_revert(runnable_version: RunnableCairo1) {
521+
let test_contract = FeatureContract::TestContract(CairoVersion::Cairo1(runnable_version));
522+
let chain_info = &ChainInfo::create_for_testing();
523+
let mut state = test_state(chain_info, BALANCE, &[(test_contract, 1)]);
524+
525+
let contract_address = test_contract.get_instance_address(0);
526+
let storage_key = StorageKey::try_from(felt!(123_u64)).unwrap();
527+
528+
// Call catch_write_revert_panic which:
529+
// 1. Calls call_write_rewrite_panic
530+
// 2. call_write_rewrite_panic calls write_1
531+
// 3. write_1 writes storage = 1
532+
// 4. call_write_rewrite_panic writes storage = 2
533+
// 5. call_write_rewrite_panic panics (gets reverted)
534+
// 6. catch_write_revert_panic catches the revert and reads storage (should be 0)
535+
let entry_point_call = CallEntryPoint {
536+
entry_point_selector: selector_from_name("catch_write_revert_panic"),
537+
calldata: calldata_macro![*contract_address.0.key(), *storage_key.0.key()],
538+
..trivial_external_entry_point_new(test_contract)
539+
};
540+
541+
let call_info = entry_point_call.execute_directly(&mut state).unwrap();
542+
543+
// catch_write_revert_panic should succeed (it catches call_write_rewrite_panic's revert).
544+
assert!(!call_info.execution.failed, "catch_write_revert_panic should not fail");
545+
546+
// catch_write_revert_panic returns the storage value it read after call_write_rewrite_panic's
547+
// revert. This should be 0 (the original value), not 1 (write_1's write).
548+
assert_eq!(
549+
call_info.execution.retdata.0,
550+
vec![felt!(0_u8)],
551+
"Storage should be reverted to 0, not write_1's value (1)"
552+
);
553+
554+
// Double-check by reading storage directly.
555+
let final_value = state.get_storage_at(contract_address, storage_key).unwrap();
556+
assert_eq!(final_value, felt!(0_u8), "Storage should be 0 after revert");
557+
}

0 commit comments

Comments
 (0)