Skip to content

Commit fa465c4

Browse files
authored
fix(inspect): finalize journal on error to prevent EIP-2929 warm set leak (bluealloy#3780)
* fix(inspect): finalize journal on error to prevent EIP-2929 leak `InspectEvm::inspect_tx` (and its sibling inspect/system-call/commit variants) used `self.X(tx)?` to short-circuit before calling `finalize()`. On error the handler calls `discard_tx`, which reverts account values but keeps the entries in the state map and increments `transaction_id`, so the EIP-2929 warm set and touched accounts leaked into the next transaction. This mirrors the same latent bug in `ExecuteEvm::transact_commit` and `ExecuteEvm::replay`. Fix all of them to finalize the journal on the error path, matching the existing correct pattern in `ExecuteEvm::transact` and `transact_many`. Adds regression tests asserting the journal is empty after a failed `inspect_tx` / `inspect_tx_commit`. Closes bluealloy#3779 ---------
1 parent ae95b77 commit fa465c4

3 files changed

Lines changed: 180 additions & 18 deletions

File tree

crates/handler/src/api.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,17 @@ pub trait ExecuteCommitEvm: ExecuteEvm {
139139
}
140140

141141
/// Transact the transaction and commit to the state.
142+
///
143+
/// # Outcome of Error
144+
///
145+
/// If the transaction fails, the journal is finalized (not committed) so it
146+
/// does not leak into the next transaction.
142147
#[inline]
143148
fn transact_commit(&mut self, tx: Self::Tx) -> Result<Self::ExecutionResult, Self::Error> {
144-
let output = self.transact_one(tx)?;
149+
let output = self.transact_one(tx).inspect_err(|_| {
150+
// finalize (clear) the journal on error; do not commit it.
151+
let _ = self.finalize();
152+
})?;
145153
self.commit_inner();
146154
Ok(output)
147155
}
@@ -201,10 +209,17 @@ where
201209

202210
#[inline]
203211
fn replay(&mut self) -> Result<ResultAndState<HaltReason>, Self::Error> {
204-
MainnetHandler::default().run(self).map(|result| {
205-
let state = self.finalize();
206-
ResultAndState::new(result, state)
207-
})
212+
MainnetHandler::default()
213+
.run(self)
214+
// finalize (clear) the journal on error; on success the `map`
215+
// branch below finalizes it.
216+
.inspect_err(|_| {
217+
let _ = self.finalize();
218+
})
219+
.map(|result| {
220+
let state = self.finalize();
221+
ResultAndState::new(result, state)
222+
})
208223
}
209224
}
210225

crates/inspector/src/inspect.rs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,36 @@ pub trait InspectEvm: ExecuteEvm {
2121
fn inspect_one_tx(&mut self, tx: Self::Tx) -> Result<Self::ExecutionResult, Self::Error>;
2222

2323
/// Inspect the EVM and finalize the state.
24+
///
25+
/// # Outcome of Error
26+
///
27+
/// If the transaction fails, the journal is finalized (cleared) so that the
28+
/// next transaction starts from a clean state. This mirrors [`ExecuteEvm::transact`].
2429
fn inspect_tx(
2530
&mut self,
2631
tx: Self::Tx,
2732
) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
28-
let output = self.inspect_one_tx(tx)?;
33+
// finalize the journal unconditionally, even on error, so the
34+
// EIP-2929 warm set and state do not leak into the next transaction.
35+
let output_or_error = self.inspect_one_tx(tx);
2936
let state = self.finalize();
37+
let output = output_or_error?;
3038
Ok(ExecResultAndState::new(output, state))
3139
}
3240

3341
/// Inspect the EVM with the given inspector and transaction, and finalize the state.
42+
///
43+
/// # Outcome of Error
44+
///
45+
/// Same as [`InspectEvm::inspect_tx`]: the journal is finalized on error.
3446
fn inspect(
3547
&mut self,
3648
tx: Self::Tx,
3749
inspector: Self::Inspector,
3850
) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
39-
let output = self.inspect_one(tx, inspector)?;
51+
let output_or_error = self.inspect_one(tx, inspector);
4052
let state = self.finalize();
53+
let output = output_or_error?;
4154
Ok(ExecResultAndState::new(output, state))
4255
}
4356

@@ -59,20 +72,34 @@ pub trait InspectEvm: ExecuteEvm {
5972
pub trait InspectCommitEvm: InspectEvm + ExecuteCommitEvm {
6073
/// Inspect the EVM with the current inspector and previous transaction by replaying, similar to [`InspectEvm::inspect_tx`]
6174
/// and commit the state diff to the database.
75+
///
76+
/// # Outcome of Error
77+
///
78+
/// If the transaction fails, the journal is finalized (not committed) so it
79+
/// does not leak into the next transaction.
6280
fn inspect_tx_commit(&mut self, tx: Self::Tx) -> Result<Self::ExecutionResult, Self::Error> {
63-
let output = self.inspect_one_tx(tx)?;
81+
let output = self.inspect_one_tx(tx).inspect_err(|_| {
82+
// finalize (clear) the journal on error; do not commit it.
83+
let _ = self.finalize();
84+
})?;
6485
self.commit_inner();
6586
Ok(output)
6687
}
6788

6889
/// Inspect the EVM with the given transaction and inspector similar to [`InspectEvm::inspect`]
6990
/// and commit the state diff to the database.
91+
///
92+
/// # Outcome of Error
93+
///
94+
/// Same as [`InspectCommitEvm::inspect_tx_commit`]: the journal is finalized on error.
7095
fn inspect_commit(
7196
&mut self,
7297
tx: Self::Tx,
7398
inspector: Self::Inspector,
7499
) -> Result<Self::ExecutionResult, Self::Error> {
75-
let output = self.inspect_one(tx, inspector)?;
100+
let output = self.inspect_one(tx, inspector).inspect_err(|_| {
101+
let _ = self.finalize();
102+
})?;
76103
self.commit_inner();
77104
Ok(output)
78105
}
@@ -109,28 +136,38 @@ pub trait InspectSystemCallEvm: InspectEvm + SystemCallEvm {
109136
/// Inspect a system call and finalize the state.
110137
///
111138
/// Similar to [`InspectEvm::inspect_tx`] but for system calls.
139+
///
140+
/// # Outcome of Error
141+
///
142+
/// Same as [`InspectEvm::inspect_tx`]: the journal is finalized on error.
112143
fn inspect_system_call(
113144
&mut self,
114145
system_contract_address: Address,
115146
data: Bytes,
116147
) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
117-
let output = self.inspect_one_system_call(system_contract_address, data)?;
148+
let output_or_error = self.inspect_one_system_call(system_contract_address, data);
118149
let state = self.finalize();
150+
let output = output_or_error?;
119151
Ok(ExecResultAndState::new(output, state))
120152
}
121153

122154
/// Inspect a system call with a custom caller and finalize the state.
123155
///
124156
/// Similar to [`InspectEvm::inspect_tx`] but for system calls with a custom caller.
157+
///
158+
/// # Outcome of Error
159+
///
160+
/// Same as [`InspectEvm::inspect_tx`]: the journal is finalized on error.
125161
fn inspect_system_call_with_caller(
126162
&mut self,
127163
caller: Address,
128164
system_contract_address: Address,
129165
data: Bytes,
130166
) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
131-
let output =
132-
self.inspect_one_system_call_with_caller(caller, system_contract_address, data)?;
167+
let output_or_error =
168+
self.inspect_one_system_call_with_caller(caller, system_contract_address, data);
133169
let state = self.finalize();
170+
let output = output_or_error?;
134171
Ok(ExecResultAndState::new(output, state))
135172
}
136173

@@ -150,15 +187,20 @@ pub trait InspectSystemCallEvm: InspectEvm + SystemCallEvm {
150187
/// Inspect a system call with a given inspector and finalize the state.
151188
///
152189
/// Similar to [`InspectEvm::inspect`] but for system calls.
190+
///
191+
/// # Outcome of Error
192+
///
193+
/// Same as [`InspectEvm::inspect_tx`]: the journal is finalized on error.
153194
fn inspect_system_call_with_inspector(
154195
&mut self,
155196
system_contract_address: Address,
156197
data: Bytes,
157198
inspector: Self::Inspector,
158199
) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
159-
let output =
160-
self.inspect_one_system_call_with_inspector(system_contract_address, data, inspector)?;
200+
let output_or_error =
201+
self.inspect_one_system_call_with_inspector(system_contract_address, data, inspector);
161202
let state = self.finalize();
203+
let output = output_or_error?;
162204
Ok(ExecResultAndState::new(output, state))
163205
}
164206

@@ -179,20 +221,25 @@ pub trait InspectSystemCallEvm: InspectEvm + SystemCallEvm {
179221
/// Inspect a system call with a given inspector and finalize the state.
180222
///
181223
/// Similar to [`InspectEvm::inspect`] but for system calls.
224+
///
225+
/// # Outcome of Error
226+
///
227+
/// Same as [`InspectEvm::inspect_tx`]: the journal is finalized on error.
182228
fn inspect_system_call_with_inspector_and_caller(
183229
&mut self,
184230
caller: Address,
185231
system_contract_address: Address,
186232
data: Bytes,
187233
inspector: Self::Inspector,
188234
) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
189-
let output = self.inspect_one_system_call_with_inspector_and_caller(
235+
let output_or_error = self.inspect_one_system_call_with_inspector_and_caller(
190236
caller,
191237
system_contract_address,
192238
data,
193239
inspector,
194-
)?;
240+
);
195241
let state = self.finalize();
242+
let output = output_or_error?;
196243
Ok(ExecResultAndState::new(output, state))
197244
}
198245
}

crates/inspector/src/inspector_tests.rs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#[cfg(test)]
22
mod tests {
3-
use crate::{InspectEvm, InspectSystemCallEvm, InspectorEvent, TestInspector};
3+
use crate::{
4+
InspectCommitEvm, InspectEvm, InspectSystemCallEvm, InspectorEvent, TestInspector,
5+
};
46
use context::{Context, TxEnv};
57
use database::{BenchmarkDB, BENCH_CALLER, BENCH_TARGET};
6-
use handler::{MainBuilder, MainContext};
8+
use handler::{ExecuteEvm, MainBuilder, MainContext};
79
use primitives::{address, Address, Bytes, TxKind, U256};
810
use state::{bytecode::opcode, AccountInfo, Bytecode};
911

@@ -772,4 +774,102 @@ mod tests {
772774
"system_call state_gas_spent must match between inspect and non-inspect paths",
773775
);
774776
}
777+
778+
/// Regression test for https://github.com/bluealloy/revm/issues/3779
779+
///
780+
/// `inspect_tx` used to short-circuit on `inspect_one_tx` returning `Err`
781+
/// and skip `finalize()`, leaving the journal (EIP-2929 warm set, touched
782+
/// accounts) in a dirty state that leaked into the next transaction. The
783+
/// handler's error path calls `discard_tx`, which reverts account *values*
784+
/// but keeps the account *entries* in the state map, so a subsequent
785+
/// `finalize()` drains those leftovers — making the leak observable.
786+
///
787+
/// Here a nonce mismatch triggers an `InvalidTransaction` *after* the
788+
/// caller and coinbase have been loaded and warmed in `load_accounts`.
789+
/// After `inspect_tx` returns `Err`, the journal must already be cleared,
790+
/// i.e. the drained state must be empty.
791+
#[test]
792+
fn test_inspect_tx_finalizes_journal_on_error() {
793+
use database::{CacheDB, EmptyDB};
794+
795+
// Caller account exists in the DB with nonce = 1.
796+
let mut db = CacheDB::<EmptyDB>::default();
797+
db.insert_account_info(
798+
BENCH_CALLER,
799+
AccountInfo {
800+
balance: U256::from(1_000_000_000_000_000_000u64),
801+
nonce: 1,
802+
..Default::default()
803+
},
804+
);
805+
let ctx = Context::mainnet().with_db(db);
806+
let mut evm = ctx.build_mainnet_with_inspector(TestInspector::new());
807+
808+
// Send a tx with nonce = 0 -> InvalidTransaction::NonceTooLow, raised
809+
// after load_accounts warmed the caller/coinbase.
810+
let result = evm.inspect_tx(
811+
TxEnv::builder()
812+
.caller(BENCH_CALLER)
813+
.kind(TxKind::Call(BENCH_TARGET))
814+
.gas_limit(100_000)
815+
.nonce(0)
816+
.build()
817+
.unwrap(),
818+
);
819+
assert!(
820+
result.is_err(),
821+
"tx with stale nonce must error so the error path is exercised"
822+
);
823+
824+
// `inspect_tx` must have finalized the journal on error, so draining
825+
// it now yields an empty state. Before the fix this contained the
826+
// warmed caller/coinbase accounts and was non-empty.
827+
let leftover = evm.finalize();
828+
assert!(
829+
leftover.is_empty(),
830+
"journal must be cleared after a failed inspect_tx, but found {} leftover accounts",
831+
leftover.len(),
832+
);
833+
}
834+
835+
/// Companion to [`Self::test_inspect_tx_finalizes_journal_on_error`] for the
836+
/// `inspect_tx_commit` path: on error the journal must be finalized (not
837+
/// committed), so a subsequent drain yields an empty state.
838+
#[test]
839+
fn test_inspect_tx_commit_finalizes_journal_on_error() {
840+
use database::{CacheDB, EmptyDB};
841+
842+
let mut db = CacheDB::<EmptyDB>::default();
843+
db.insert_account_info(
844+
BENCH_CALLER,
845+
AccountInfo {
846+
balance: U256::from(1_000_000_000_000_000_000u64),
847+
nonce: 1,
848+
..Default::default()
849+
},
850+
);
851+
let ctx = Context::mainnet().with_db(db);
852+
let mut evm = ctx.build_mainnet_with_inspector(TestInspector::new());
853+
854+
let result = evm.inspect_tx_commit(
855+
TxEnv::builder()
856+
.caller(BENCH_CALLER)
857+
.kind(TxKind::Call(BENCH_TARGET))
858+
.gas_limit(100_000)
859+
.nonce(0)
860+
.build()
861+
.unwrap(),
862+
);
863+
assert!(
864+
result.is_err(),
865+
"tx with stale nonce must error so the error path is exercised"
866+
);
867+
868+
let leftover = evm.finalize();
869+
assert!(
870+
leftover.is_empty(),
871+
"journal must be cleared after a failed inspect_tx_commit, but found {} leftover accounts",
872+
leftover.len(),
873+
);
874+
}
775875
}

0 commit comments

Comments
 (0)