From 82427b203ec71524fd0763240f9a8c0c7016def0 Mon Sep 17 00:00:00 2001 From: Damian Nolan Date: Tue, 27 Oct 2026 10:01:46 +0100 Subject: [PATCH] fix(revm): bump create nonce when an earlier batch call reverts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a non-final call in an evnode batch reverts, the failure is caught at the top of the next loop iteration, which returned without advancing the executor nonce — unlike the other batch failure branches. Mirror them so a CREATE-first batch consumes its nonce on this path too. Adds a regression test for a batch whose first call reverts. --- crates/ev-revm/src/handler.rs | 71 +++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/crates/ev-revm/src/handler.rs b/crates/ev-revm/src/handler.rs index 51da1b6..a75be25 100644 --- a/crates/ev-revm/src/handler.rs +++ b/crates/ev-revm/src/handler.rs @@ -311,6 +311,7 @@ where .checkpoint_revert(batch_checkpoint); gas.rollback_state_gas(); gas.set_refunded(0); + bump_create_nonce(evm, &base_tx); return Ok(Some(frame_result)); } let mut call_tx = base_tx.clone(); @@ -1332,6 +1333,76 @@ mod tests { assert_eq!(caller_account.info.nonce, 1); } + #[test] + fn batch_execution_bumps_create_nonce_when_first_call_reverts() { + // A CREATE-first batch whose first call reverts is caught at the top of + // the loop before the remaining calls run. The executor nonce must still + // advance so the signed transaction cannot be re-included. + let caller = address!("0x0000000000000000000000000000000000000aaa"); + let recipient = address!("0x0000000000000000000000000000000000000bbb"); + + let mut state = State::builder() + .with_database(CacheDB::::default()) + .with_bundle_update() + .build(); + state.insert_account( + caller, + AccountInfo { + balance: U256::from(10_000_000_000u64), + nonce: 0, + code_hash: KECCAK_EMPTY, + code: None, + account_id: None, + }, + ); + + let mut evm_env: EvmEnv = EvmEnv::default(); + evm_env.cfg_env.chain_id = 1; + evm_env.cfg_env.spec = SpecId::CANCUN; + evm_env.block_env.basefee = 1; + evm_env.block_env.gas_limit = 30_000_000; + evm_env.block_env.number = U256::from(1); + let mut evm = EvTxEvmFactory::default().create_evm(state, evm_env); + + // First call is a CREATE whose init code reverts; the second never runs. + let calls = vec![ + Call { + to: TxKind::Create, + value: U256::ZERO, + input: Bytes::copy_from_slice(REVERT_RUNTIME.as_slice()), + }, + Call { + to: TxKind::Call(recipient), + value: U256::ZERO, + input: Bytes::new(), + }, + ]; + let tx_env = TxEnv { + caller, + gas_limit: 200_000, + gas_price: 1, + gas_priority_fee: Some(1), + chain_id: Some(1), + tx_type: TransactionType::Eip1559.into(), + ..Default::default() + }; + + let result_and_state = evm + .transact_raw(EvTxEnv::with_calls(tx_env, calls)) + .expect("batch execution should complete"); + + assert!( + matches!(result_and_state.result, ExecutionResult::Revert { .. }), + "unexpected execution result: {:?}", + result_and_state.result + ); + let caller_account = result_and_state + .state + .get(&caller) + .expect("caller should be loaded"); + assert_eq!(caller_account.info.nonce, 1); + } + #[test] fn batch_execution_bumps_create_nonce_on_later_runtime_oog() { let caller = address!("0x0000000000000000000000000000000000000aaa");