1use std::collections::HashMap;
2
3use alloy_primitives::Address;
4use revm::{Database, database::State};
5use revm_database::{AccountStatus as CacheAccountStatus, TransitionAccount};
6use revm_state::AccountInfo;
7
8#[derive(Clone, Debug)]
9struct Entry {
10 previous_info: Option<AccountInfo>,
11 previous_status: CacheAccountStatus,
12}
13
14#[derive(Default, Debug)]
25pub struct StateOverlay {
26 entries: HashMap<Address, Entry>,
27}
28
29impl StateOverlay {
30 pub fn new() -> Self {
31 Self::default()
32 }
33
34 pub fn reset_tx(&mut self) {
35 self.entries.clear();
36 }
37
38 pub fn record_pre_touch<DB: Database>(&mut self, state: &mut State<DB>, addr: Address) {
39 if self.entries.contains_key(&addr) {
40 return;
41 }
42 let _ = state.load_cache_account(addr);
43 let cache_entry = state.cache.accounts.get(&addr);
44 let previous_info = cache_entry
45 .and_then(|c| c.account.as_ref())
46 .map(|a| a.info.clone());
47 let previous_status = cache_entry
48 .map(|c| c.status)
49 .unwrap_or(CacheAccountStatus::LoadedNotExisting);
50 self.entries.insert(
51 addr,
52 Entry {
53 previous_info,
54 previous_status,
55 },
56 );
57 }
58
59 pub fn drain_and_apply<DB: Database>(
60 &mut self,
61 state: &mut State<DB>,
62 zombies: &rustc_hash::FxHashSet<Address>,
63 ) {
64 if self.entries.is_empty() {
65 return;
66 }
67 let entries: Vec<(Address, Entry)> = self.entries.drain().collect();
68
69 let mut existing_transitions: Vec<(Address, TransitionAccount)> = Vec::new();
70
71 for (addr, entry) in entries {
72 let current_info = state
73 .cache
74 .accounts
75 .get(&addr)
76 .and_then(|c| c.account.as_ref())
77 .map(|a| a.info.clone());
78
79 if current_info == entry.previous_info {
80 continue;
81 }
82
83 let pre_empty = entry
84 .previous_info
85 .as_ref()
86 .map(|i| i.is_empty())
87 .unwrap_or(true);
88 let cur_empty = current_info.as_ref().map(|i| i.is_empty()).unwrap_or(true);
89 if pre_empty && cur_empty {
90 if zombies.contains(&addr) && current_info.is_some() {
98 let previous_status = if entry.previous_info.is_none() {
99 CacheAccountStatus::LoadedNotExisting
100 } else {
101 entry.previous_status
102 };
103 if let Some(cached) = state.cache.accounts.get_mut(&addr) {
104 cached.status = CacheAccountStatus::InMemoryChange;
105 }
106 existing_transitions.push((
107 addr,
108 TransitionAccount {
109 info: current_info.clone(),
110 status: CacheAccountStatus::InMemoryChange,
111 previous_info: entry.previous_info,
112 previous_status,
113 storage: Default::default(),
114 storage_was_destroyed: false,
115 },
116 ));
117 }
118 continue;
119 }
120
121 let base_status = state
127 .bundle_state
128 .state
129 .get(&addr)
130 .map(|b| b.status)
131 .unwrap_or(entry.previous_status);
132 let live_in_bundle = state
133 .bundle_state
134 .state
135 .get(&addr)
136 .is_some_and(|b| b.info.is_some());
137
138 let was_non_existing = !live_in_bundle
139 && (entry.previous_info.is_none()
140 || matches!(
141 base_status,
142 CacheAccountStatus::LoadedNotExisting
143 | CacheAccountStatus::LoadedEmptyEIP161
144 ));
145
146 if was_non_existing && !cur_empty {
147 if let Some(cached) = state.cache.accounts.get_mut(&addr) {
155 cached.status = CacheAccountStatus::InMemoryChange;
156 }
157 existing_transitions.push((
158 addr,
159 TransitionAccount {
160 info: current_info.clone(),
161 status: CacheAccountStatus::InMemoryChange,
162 previous_info: entry.previous_info,
163 previous_status: entry.previous_status,
164 storage: Default::default(),
165 storage_was_destroyed: false,
166 },
167 ));
168 continue;
169 }
170
171 let new_status = if cur_empty {
174 match base_status {
175 CacheAccountStatus::LoadedNotExisting => continue,
176 CacheAccountStatus::DestroyedAgain | CacheAccountStatus::DestroyedChanged => {
177 CacheAccountStatus::DestroyedAgain
178 }
179 _ => CacheAccountStatus::Destroyed,
180 }
181 } else {
182 match base_status {
183 CacheAccountStatus::Loaded => CacheAccountStatus::Changed,
184 CacheAccountStatus::LoadedNotExisting
185 | CacheAccountStatus::LoadedEmptyEIP161 => CacheAccountStatus::InMemoryChange,
186 CacheAccountStatus::DestroyedAgain
187 | CacheAccountStatus::Destroyed
188 | CacheAccountStatus::DestroyedChanged => CacheAccountStatus::DestroyedChanged,
189 other => other,
190 }
191 };
192
193 let goes_destroyed = matches!(
194 new_status,
195 CacheAccountStatus::Destroyed | CacheAccountStatus::DestroyedAgain
196 );
197 let transition_info = if goes_destroyed {
198 None
199 } else {
200 current_info.clone()
201 };
202 let storage_was_destroyed = goes_destroyed && !pre_empty;
203
204 if let Some(cached) = state.cache.accounts.get_mut(&addr) {
205 cached.status = new_status;
206 if goes_destroyed {
207 cached.account = None;
208 }
209 }
210
211 existing_transitions.push((
212 addr,
213 TransitionAccount {
214 info: transition_info,
215 status: new_status,
216 previous_info: entry.previous_info,
217 previous_status: entry.previous_status,
218 storage: Default::default(),
219 storage_was_destroyed,
220 },
221 ));
222 }
223
224 if !existing_transitions.is_empty() {
225 state.apply_transition(existing_transitions);
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use alloy_primitives::{U256, address};
233 use revm::database::{EmptyDB, State};
234 use revm_database::states::{
235 bundle_state::BundleRetention, cache_account::CacheAccount, plain_account::PlainAccount,
236 };
237
238 use super::*;
239
240 fn make_state() -> State<EmptyDB> {
241 State::builder()
242 .with_database(EmptyDB::default())
243 .with_bundle_update()
244 .build()
245 }
246
247 #[test]
248 fn fresh_account_credit_lands_in_persisted_bundle() {
249 let mut state = make_state();
250 let mut overlay = StateOverlay::new();
251 let recipient = address!("000000000000000000000000000000000000beef");
252 let credit = U256::from(0x6a94d74f430000u64);
253
254 overlay.record_pre_touch(&mut state, recipient);
255 let entry = state.cache.accounts.get_mut(&recipient).unwrap();
256 entry.account = Some(PlainAccount {
257 info: AccountInfo {
258 balance: credit,
259 ..Default::default()
260 },
261 storage: Default::default(),
262 });
263
264 overlay.drain_and_apply(&mut state, &Default::default());
265 state.merge_transitions(BundleRetention::Reverts);
266
267 let bundled = state
268 .bundle_state
269 .state
270 .get(&recipient)
271 .and_then(|a| a.info.as_ref())
272 .map(|i| i.balance);
273 assert_eq!(bundled, Some(credit));
274 }
275
276 #[test]
277 fn existing_account_change_lands_in_persisted_bundle() {
278 let mut state = make_state();
279 let mut overlay = StateOverlay::new();
280 let acct = address!("000000000000000000000000000000000000c0de");
281
282 state.cache.accounts.insert(
283 acct,
284 CacheAccount {
285 account: Some(PlainAccount {
286 info: AccountInfo {
287 balance: U256::from(10u64),
288 ..Default::default()
289 },
290 storage: Default::default(),
291 }),
292 status: CacheAccountStatus::Loaded,
293 },
294 );
295
296 overlay.record_pre_touch(&mut state, acct);
297 if let Some(p) = state
298 .cache
299 .accounts
300 .get_mut(&acct)
301 .and_then(|c| c.account.as_mut())
302 {
303 p.info.balance = U256::from(42u64);
304 }
305
306 overlay.drain_and_apply(&mut state, &Default::default());
307 state.merge_transitions(BundleRetention::Reverts);
308
309 let bundled = state
310 .bundle_state
311 .state
312 .get(&acct)
313 .and_then(|a| a.info.as_ref())
314 .map(|i| i.balance);
315 assert_eq!(bundled, Some(U256::from(42u64)));
316 }
317
318 fn credit<DB: Database>(
319 state: &mut State<DB>,
320 overlay: &mut StateOverlay,
321 a: Address,
322 by: U256,
323 ) {
324 overlay.record_pre_touch(state, a);
325 let c = state.cache.accounts.get_mut(&a).unwrap();
326 match c.account.as_mut() {
327 Some(acct) => acct.info.balance += by,
328 None => {
329 c.account = Some(PlainAccount {
330 info: AccountInfo {
331 balance: by,
332 ..Default::default()
333 },
334 storage: Default::default(),
335 })
336 }
337 }
338 overlay.drain_and_apply(state, &Default::default());
339 state.merge_transitions(BundleRetention::Reverts);
340 overlay.reset_tx();
341 }
342
343 #[test]
344 fn repro_fresh_credit_then_recredit_across_merges() {
345 let mut state = make_state();
346 let mut overlay = StateOverlay::new();
347 let a = address!("00000000000000000000000000000000feed0001");
348 credit(&mut state, &mut overlay, a, U256::from(1_000_000u64));
349 credit(&mut state, &mut overlay, a, U256::from(2_000_000u64));
350 let bal = state
351 .bundle_state
352 .state
353 .get(&a)
354 .and_then(|x| x.info.as_ref())
355 .map(|i| i.balance);
356 assert_eq!(bal, Some(U256::from(3_000_000u64)));
357 }
358
359 #[test]
364 fn recredit_with_drifted_cache_status_stays_changed() {
365 let mut state = make_state();
366 let mut overlay = StateOverlay::new();
367 let a = address!("00000000000000000000000000000000feed0002");
368
369 state.cache.accounts.insert(
371 a,
372 CacheAccount {
373 account: Some(PlainAccount {
374 info: AccountInfo {
375 balance: U256::from(10u64),
376 ..Default::default()
377 },
378 storage: Default::default(),
379 }),
380 status: CacheAccountStatus::Loaded,
381 },
382 );
383 credit(&mut state, &mut overlay, a, U256::from(1_000_000u64));
384 assert_eq!(
385 state.bundle_state.state.get(&a).map(|b| b.status),
386 Some(CacheAccountStatus::Changed)
387 );
388
389 state.cache.accounts.get_mut(&a).unwrap().status = CacheAccountStatus::LoadedEmptyEIP161;
394
395 credit(&mut state, &mut overlay, a, U256::from(2_000_000u64));
396 let acct = state.bundle_state.state.get(&a).unwrap();
397 assert_eq!(acct.status, CacheAccountStatus::Changed);
398 assert_eq!(
399 acct.info.as_ref().map(|i| i.balance),
400 Some(U256::from(3_000_010u64))
401 );
402 }
403
404 #[test]
405 fn repro_loaded_credit_then_recredit_across_merges() {
406 let mut state = make_state();
407 let mut overlay = StateOverlay::new();
408 let a = address!("00000000000000000000000000000000feed0003");
409 state.cache.accounts.insert(
411 a,
412 CacheAccount {
413 account: Some(PlainAccount {
414 info: AccountInfo {
415 balance: U256::from(10u64),
416 ..Default::default()
417 },
418 storage: Default::default(),
419 }),
420 status: CacheAccountStatus::Loaded,
421 },
422 );
423 credit(&mut state, &mut overlay, a, U256::from(1_000_000u64));
424 credit(&mut state, &mut overlay, a, U256::from(1_000_000u64));
425 let bal = state
426 .bundle_state
427 .state
428 .get(&a)
429 .and_then(|x| x.info.as_ref())
430 .map(|i| i.balance);
431 assert_eq!(bal, Some(U256::from(2_000_010u64)));
432 }
433
434 #[test]
435 fn created_then_emptied_in_same_tx_produces_no_bundle_entry() {
436 let mut state = make_state();
437 let mut overlay = StateOverlay::new();
438 let transient = address!("0000000000000000000000000000000000007a17");
439
440 overlay.record_pre_touch(&mut state, transient);
441 let entry = state.cache.accounts.get_mut(&transient).unwrap();
442 entry.account = Some(PlainAccount {
443 info: AccountInfo {
444 balance: U256::ZERO,
445 ..Default::default()
446 },
447 storage: Default::default(),
448 });
449
450 overlay.drain_and_apply(&mut state, &Default::default());
451 state.merge_transitions(BundleRetention::Reverts);
452
453 assert!(!state.bundle_state.state.contains_key(&transient));
454 }
455
456 #[test]
457 fn zombie_resurrection_reverts_to_absent() {
458 let addr = address!("00000000000000000000000000000000deadbeef");
463 for stale in [
464 CacheAccountStatus::InMemoryChange,
465 CacheAccountStatus::Loaded,
466 CacheAccountStatus::LoadedNotExisting,
467 CacheAccountStatus::LoadedEmptyEIP161,
468 ] {
469 let mut state = make_state();
470 let mut overlay = StateOverlay::new();
471 state.cache.accounts.insert(
472 addr,
473 CacheAccount {
474 account: None,
475 status: stale,
476 },
477 );
478 overlay.record_pre_touch(&mut state, addr);
479 let entry = state.cache.accounts.get_mut(&addr).unwrap();
480 entry.account = Some(PlainAccount {
481 info: AccountInfo::default(),
482 storage: Default::default(),
483 });
484 entry.status = CacheAccountStatus::InMemoryChange;
485
486 let zombies: rustc_hash::FxHashSet<Address> = std::iter::once(addr).collect();
487 overlay.drain_and_apply(&mut state, &zombies);
488 state.merge_transitions(BundleRetention::Reverts);
489
490 let forward = state
491 .bundle_state
492 .state
493 .get(&addr)
494 .and_then(|a| a.info.as_ref())
495 .cloned();
496 assert!(
497 forward.as_ref().is_some_and(|i| i.is_empty()),
498 "stale={stale:?}: zombie must persist present-empty forward, got {forward:?}"
499 );
500
501 state.bundle_state.revert(usize::MAX);
502 let reverted = state
503 .bundle_state
504 .state
505 .get(&addr)
506 .and_then(|a| a.info.as_ref())
507 .cloned();
508 assert!(
509 reverted.is_none(),
510 "stale={stale:?}: unwound zombie must be absent, got {reverted:?}"
511 );
512 }
513 }
514}