revm_context/journal/inner.rs
1//! Module containing the [`JournalInner`] that is part of [`crate::Journal`].
2use super::warm_addresses::WarmAddresses;
3use bytecode::Bytecode;
4use context_interface::{
5 context::{SStoreResult, SelfDestructResult, StateLoad},
6 journaled_state::{
7 account::{JournaledAccount, JournaledAccountTr},
8 entry::{JournalEntryTr, SelfdestructionRevertStatus},
9 AccountLoad, JournalCheckpoint, JournalLoadError, TransferError,
10 },
11};
12use core::mem;
13use database_interface::Database;
14use primitives::{
15 eip7708::{ETH_TRANSFER_LOG_ADDRESS, ETH_TRANSFER_LOG_TOPIC},
16 hardfork::SpecId::{self, *},
17 hash_map::Entry,
18 hints_util::unlikely,
19 Address, Bytes, HashMap, Log, LogData, StorageKey, StorageValue, B256, KECCAK_EMPTY, U256,
20};
21use state::{Account, EvmState, TransactionId, TransientStorage};
22use std::vec::Vec;
23
24/// Configuration for the journal that affects EVM execution behavior.
25///
26/// This struct bundles the spec ID and EIP-7708 configuration flags.
27#[derive(Debug, Clone, Default, PartialEq, Eq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub struct JournalCfg {
30 /// The spec ID for the EVM. Spec is required for some journal entries and needs to be set for
31 /// JournalInner to be functional.
32 ///
33 /// If spec is set it assumed that precompile addresses are set as well for this particular spec.
34 ///
35 /// This spec is used for two things:
36 ///
37 /// - [EIP-161]: Prior to this EIP, Ethereum had separate definitions for empty and non-existing accounts.
38 /// - [EIP-6780]: `SELFDESTRUCT` only in same transaction
39 ///
40 /// [EIP-161]: https://eips.ethereum.org/EIPS/eip-161
41 /// [EIP-6780]: https://eips.ethereum.org/EIPS/eip-6780
42 pub spec: SpecId,
43 /// Whether EIP-7708 (ETH transfers emit logs) is disabled.
44 pub eip7708_disabled: bool,
45 /// Whether the EIP-8246 delayed clearing of self-destructed accounts is disabled.
46 ///
47 /// When enabled, revm tracks all self-destructed addresses and, at the end of the
48 /// transaction, clears the code, storage and nonce of any that still have a remaining
49 /// balance while preserving the balance (see [EIP-8246]). This can be disabled for
50 /// performance reasons as it requires storing and iterating over all self-destructed
51 /// accounts. When disabled, this clearing can be done outside of revm when applying
52 /// accounts to database state.
53 ///
54 /// [EIP-8246]: https://eips.ethereum.org/EIPS/eip-8246
55 pub eip8246_delayed_clear_disabled: bool,
56}
57/// Inner journal state that contains journal and state changes.
58///
59/// Spec Id is a essential information for the Journal.
60#[derive(Debug, Clone, PartialEq, Eq)]
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62pub struct JournalInner<ENTRY> {
63 /// The current state
64 pub state: EvmState,
65 /// Transient storage that is discarded after every transaction.
66 ///
67 /// See [EIP-1153](https://eips.ethereum.org/EIPS/eip-1153).
68 pub transient_storage: TransientStorage,
69 /// Emitted logs
70 pub logs: Vec<Log>,
71 /// The current call stack depth
72 pub depth: usize,
73 /// The journal of state changes, one for each transaction
74 pub journal: Vec<ENTRY>,
75 /// Global transaction id that represent number of transactions executed (Including reverted ones).
76 /// It can be different from number of `journal_history` as some transaction could be
77 /// reverted or had a error on execution.
78 ///
79 /// This ID is used in `Self::state` to determine if account/storage is touched/warm/cold.
80 pub transaction_id: TransactionId,
81 /// Journal configuration containing spec ID and EIP-7708 flags.
82 pub cfg: JournalCfg,
83 /// Warm addresses containing both coinbase and current precompiles.
84 pub warm_addresses: WarmAddresses,
85 /// Addresses that were self-destructed for the first time in this transaction.
86 ///
87 /// This is used by [EIP-8246] to clear (code, storage and nonce) self-destructed accounts
88 /// that still have balance at the end of the transaction, preserving their balance.
89 ///
90 /// The vec is indexed by checkpoint - on revert, entries added after the checkpoint
91 /// are removed.
92 ///
93 /// [EIP-8246]: https://eips.ethereum.org/EIPS/eip-8246
94 pub selfdestructed_addresses: Vec<Address>,
95}
96
97impl<ENTRY: JournalEntryTr> Default for JournalInner<ENTRY> {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl<ENTRY: JournalEntryTr> JournalInner<ENTRY> {
104 /// Creates new [`JournalInner`].
105 ///
106 /// `warm_preloaded_addresses` is used to determine if address is considered warm loaded.
107 /// In ordinary case this is precompile or beneficiary.
108 pub fn new() -> JournalInner<ENTRY> {
109 Self {
110 state: HashMap::default(),
111 transient_storage: TransientStorage::default(),
112 logs: Vec::new(),
113 journal: Vec::default(),
114 transaction_id: TransactionId::ZERO,
115 depth: 0,
116 cfg: JournalCfg::default(),
117 warm_addresses: WarmAddresses::new(),
118 selfdestructed_addresses: Vec::new(),
119 }
120 }
121
122 /// Returns the logs.
123 ///
124 /// Before returning, this function applies EIP-8246 to any self-destructed
125 /// accounts that still have a non-zero balance, clearing their code, storage and
126 /// nonce while preserving the balance.
127 #[inline]
128 pub fn take_logs(&mut self) -> Vec<Log> {
129 // EIP-8246: clear self-destructed accounts that still hold a balance.
130 self.eip8246_clear_selfdestructed_accounts();
131 mem::take(&mut self.logs)
132 }
133
134 /// Prepare for next transaction, by committing the current journal to history, incrementing the transaction id
135 /// and returning the logs.
136 ///
137 /// This function is used to prepare for next transaction. It will save the current journal
138 /// and clear the journal for the next transaction.
139 ///
140 /// `commit_tx` is used even for discarding transactions so transaction_id will be incremented.
141 pub fn commit_tx(&mut self) {
142 // Clears all field from JournalInner. Doing it this way to avoid
143 // missing any field.
144 let Self {
145 state,
146 transient_storage,
147 logs,
148 depth,
149 journal,
150 transaction_id,
151 cfg,
152 warm_addresses,
153 selfdestructed_addresses,
154 } = self;
155 // Cfg and state are not changed. They are always set again before execution.
156 let _ = cfg;
157 let _ = state;
158 transient_storage.clear();
159 *depth = 0;
160
161 // Do nothing with journal history so we can skip cloning present journal.
162 journal.clear();
163
164 // Clear coinbase address warming for next tx
165 warm_addresses.clear_coinbase_and_access_list();
166 // increment transaction id.
167 transaction_id.increment();
168
169 logs.clear();
170 selfdestructed_addresses.clear();
171 }
172
173 /// Discard the current transaction, by reverting the journal entries and incrementing the transaction id.
174 pub fn discard_tx(&mut self) {
175 // if there is no journal entries, there has not been any changes.
176 let Self {
177 state,
178 transient_storage,
179 logs,
180 depth,
181 journal,
182 transaction_id,
183 cfg,
184 warm_addresses,
185 selfdestructed_addresses,
186 } = self;
187 let is_spurious_dragon_enabled = cfg.spec.is_enabled_in(SPURIOUS_DRAGON);
188 // iterate over all journals entries and revert our global state
189 journal.drain(..).rev().for_each(|entry| {
190 entry.revert(state, None, is_spurious_dragon_enabled);
191 });
192 transient_storage.clear();
193 *depth = 0;
194 logs.clear();
195 selfdestructed_addresses.clear();
196 transaction_id.increment();
197
198 // Clear coinbase address warming for next tx
199 warm_addresses.clear_coinbase_and_access_list();
200 }
201
202 /// Take the [`EvmState`] and clears the journal by resetting it to initial state.
203 ///
204 /// Note: Precompile addresses and spec are preserved and initial state of
205 /// warm_preloaded_addresses will contain precompiles addresses.
206 #[inline]
207 pub fn finalize(&mut self) -> EvmState {
208 // Clears all field from JournalInner. Doing it this way to avoid
209 // missing any field.
210 let Self {
211 state,
212 transient_storage,
213 logs,
214 depth,
215 journal,
216 transaction_id,
217 cfg,
218 warm_addresses,
219 selfdestructed_addresses,
220 } = self;
221 // Clear coinbase address warming for next tx
222 warm_addresses.clear_coinbase_and_access_list();
223 selfdestructed_addresses.clear();
224
225 let mut state = mem::take(state);
226
227 // Pre-EIP-161 normalization: adjust empty touched accounts so the database
228 // layer can always apply post-EIP-161 commit semantics (destroy empty touched
229 // accounts). For pre-Spurious Dragon blocks, we prevent destruction by either
230 // marking the account as created (materialized) or clearing the touched flag.
231 if !cfg.spec.is_enabled_in(SPURIOUS_DRAGON) {
232 for acc in state.values_mut() {
233 if acc.is_touched()
234 && acc.is_empty()
235 && !acc.is_selfdestructed()
236 && !acc.is_created()
237 {
238 if acc.is_loaded_as_not_existing() {
239 // Materialize empty account that didn't exist before.
240 acc.mark_created();
241 } else {
242 // Preserve existing empty account, don't let the DB layer destroy it.
243 acc.unmark_touch();
244 }
245 }
246 }
247 }
248
249 logs.clear();
250 transient_storage.clear();
251
252 // clear journal and journal history.
253 journal.clear();
254 *depth = 0;
255 // reset transaction id.
256 *transaction_id = TransactionId::ZERO;
257
258 state
259 }
260
261 /// Finalize self-destructed accounts in place at the end of transaction execution.
262 ///
263 /// This should be called before `take_logs()` at the end of transaction execution.
264 /// It iterates over all accounts that were self-destructed in this transaction and applies
265 /// the destruction in place: the storage is wiped to zero, the nonce, code and balance are
266 /// cleared and the self-destruct flag is removed. The cleared account is an empty touched
267 /// account, so it is removed from state by [EIP-161] state clearing without downstream
268 /// layers needing to handle the self-destruct flag.
269 ///
270 /// From Amsterdam, [EIP-8246] additionally preserves a remaining non-zero balance instead
271 /// of burning it: the balance is left unchanged and the account survives in state as a
272 /// balance-only account. A non-zero balance can remain when an account receives ETH after
273 /// being self-destructed in the same transaction, or when a contract created in the same
274 /// transaction self-destructs to itself (see [`Self::selfdestruct`]).
275 ///
276 /// In-place destruction is only possible with [EIP-6780] (Cancun): self-destructed accounts
277 /// were necessarily created in the same transaction, so they never have storage stored in
278 /// the database and clearing the in-memory storage is sufficient. Pre-Cancun this function
279 /// does nothing and destruction stays flag-based.
280 ///
281 /// The self-destruct flags are removed and the nonce, code and storage are cleared for
282 /// every tracked account: destruction is finalized here and no account leaves the journal
283 /// marked as self-destructed.
284 ///
285 /// The balance is the only field that is conditionally kept: with [EIP-8246] active
286 /// (Amsterdam and `eip8246_delayed_clear_disabled` unset) a remaining non-zero balance is
287 /// preserved instead of burned. In every other case the balance is zeroed with the rest of
288 /// the account.
289 ///
290 /// [EIP-6780]: https://eips.ethereum.org/EIPS/eip-6780
291 /// [EIP-8246]: https://eips.ethereum.org/EIPS/eip-8246
292 /// [EIP-161]: https://eips.ethereum.org/EIPS/eip-161
293 #[inline]
294 pub fn eip8246_clear_selfdestructed_accounts(&mut self) {
295 // EIP-6780 (Cancun) guarantees self-destructed accounts were created in this
296 // transaction, so they have no database storage and can be destroyed in place.
297 if !self.cfg.spec.is_enabled_in(CANCUN) {
298 return;
299 }
300 // EIP-8246 (Amsterdam): a remaining non-zero balance is kept instead of being burned,
301 // unless the delayed clearing is disabled.
302 let preserve_balance =
303 self.cfg.spec.is_enabled_in(AMSTERDAM) && !self.cfg.eip8246_delayed_clear_disabled;
304
305 for address in &self.selfdestructed_addresses {
306 let Some(account) = self.state.get_mut(address) else {
307 continue;
308 };
309
310 // Destruction is finalized here: no account leaves the journal with a
311 // self-destruct flag set.
312 account.unmark_selfdestruct();
313 account.unmark_selfdestructed_locally();
314
315 if !preserve_balance {
316 account.info.balance = U256::ZERO;
317 }
318
319 account.info.nonce = 0;
320 account.info.code_hash = KECCAK_EMPTY;
321 account.info.code = Some(Bytecode::default());
322 // Wipe storage to zero in place rather than dropping the entries: the
323 // slots were accessed during the (now self-destructed) execution and
324 // EIP-7928 records those accesses in the block access list. Keeping the
325 // entries (present_value = 0) preserves the access — read-only slots
326 // stay read-only and written slots become writes-to-zero — while the
327 // committed state is still an empty (balance-only) account.
328 for slot in account.storage.values_mut() {
329 slot.present_value = StorageValue::ZERO;
330 }
331
332 // A fully cleared account must also drop the created flag, otherwise the database
333 // layer commits it as a newly created (existing) empty account instead of removing
334 // it as an empty touched account (EIP-161, always active from Cancun).
335 if account.info.balance.is_zero() {
336 account.unmark_created();
337 account.unmark_created_locally();
338 }
339 }
340 }
341
342 /// Return reference to state.
343 #[inline]
344 pub const fn state(&mut self) -> &mut EvmState {
345 &mut self.state
346 }
347
348 /// Sets SpecId.
349 #[inline]
350 pub const fn set_spec_id(&mut self, spec: SpecId) {
351 self.cfg.spec = spec;
352 }
353
354 /// Sets EIP-7708 and EIP-8246 configuration flags.
355 #[inline]
356 pub const fn set_eip7708_config(
357 &mut self,
358 disabled: bool,
359 eip8246_delayed_clear_disabled: bool,
360 ) {
361 self.cfg.eip7708_disabled = disabled;
362 self.cfg.eip8246_delayed_clear_disabled = eip8246_delayed_clear_disabled;
363 }
364
365 /// Mark account as touched as only touched accounts will be added to state.
366 /// This is especially important for state clear where touched empty accounts needs to
367 /// be removed from state.
368 #[inline]
369 pub fn touch(&mut self, address: Address) {
370 if let Some(account) = self.state.get_mut(&address) {
371 Self::touch_account(&mut self.journal, address, account);
372 }
373 }
374
375 /// Mark account as touched.
376 #[inline]
377 fn touch_account(journal: &mut Vec<ENTRY>, address: Address, account: &mut Account) {
378 if !account.is_touched() {
379 journal.push(ENTRY::account_touched(address));
380 account.mark_touch();
381 }
382 }
383
384 /// Returns the _loaded_ [Account] for the given address.
385 ///
386 /// This assumes that the account has already been loaded.
387 ///
388 /// # Panics
389 ///
390 /// Panics if the account has not been loaded and is missing from the state set.
391 #[inline]
392 pub fn account(&self, address: Address) -> &Account {
393 self.state
394 .get(&address)
395 .expect("Account expected to be loaded") // Always assume that acc is already loaded
396 }
397
398 /// Set code and its hash to the account.
399 ///
400 /// Note: Assume account is warm and that hash is calculated from code.
401 #[inline]
402 pub fn set_code_with_hash(&mut self, address: Address, code: Bytecode, hash: B256) {
403 let account = self.state.get_mut(&address).unwrap();
404 Self::touch_account(&mut self.journal, address, account);
405
406 let had_code_hash = account.info.code_hash;
407 let had_code = account.info.code.take();
408 self.journal
409 .push(ENTRY::code_changed(address, had_code_hash, had_code));
410
411 account.info.code_hash = hash;
412 account.info.code = Some(code);
413 }
414
415 /// Use it only if you know that acc is warm.
416 ///
417 /// Assume account is warm.
418 ///
419 /// In case of EIP-7702 code with zero address, the bytecode will be erased.
420 #[inline]
421 pub fn set_code(&mut self, address: Address, code: Bytecode) {
422 if let Some(eip7702_address) = code.eip7702_address() {
423 if eip7702_address.is_zero() {
424 self.set_code_with_hash(address, Bytecode::default(), KECCAK_EMPTY);
425 return;
426 }
427 }
428
429 let hash = code.hash_slow();
430 self.set_code_with_hash(address, code, hash)
431 }
432
433 /// Add journal entry for caller accounting.
434 #[inline]
435 #[deprecated]
436 pub fn caller_accounting_journal_entry(
437 &mut self,
438 address: Address,
439 old_balance: U256,
440 bump_nonce: bool,
441 ) {
442 // account balance changed.
443 self.journal
444 .push(ENTRY::balance_changed(address, old_balance));
445 // account is touched.
446 self.journal.push(ENTRY::account_touched(address));
447
448 if bump_nonce {
449 // nonce changed.
450 self.journal.push(ENTRY::nonce_bumped(address));
451 }
452 }
453
454 /// Increments the balance of the account.
455 ///
456 /// Mark account as touched.
457 #[inline]
458 pub fn balance_incr<DB: Database>(
459 &mut self,
460 db: &mut DB,
461 address: Address,
462 balance: U256,
463 ) -> Result<(), DB::Error> {
464 let mut account = self.load_account_mut(db, address)?.data;
465 account.incr_balance(balance);
466 Ok(())
467 }
468
469 /// Increments the nonce of the account.
470 #[inline]
471 #[deprecated]
472 pub fn nonce_bump_journal_entry(&mut self, address: Address) {
473 self.journal.push(ENTRY::nonce_bumped(address));
474 }
475
476 /// Transfers balance from two accounts. Returns error if sender balance is not enough.
477 ///
478 /// # Panics
479 ///
480 /// Panics if from or to are not loaded.
481 #[inline]
482 pub fn transfer_loaded(
483 &mut self,
484 from: Address,
485 to: Address,
486 balance: U256,
487 ) -> Option<TransferError> {
488 if from == to {
489 let from_balance = self.state.get(&to).unwrap().info.balance;
490 // Check if from balance is enough to transfer the balance.
491 if balance > from_balance {
492 return Some(TransferError::OutOfFunds);
493 }
494 return None;
495 }
496
497 if balance.is_zero() {
498 Self::touch_account(&mut self.journal, to, self.state.get_mut(&to).unwrap());
499 return None;
500 }
501
502 // sub balance from
503 let from_account = self.state.get_mut(&from).unwrap();
504 Self::touch_account(&mut self.journal, from, from_account);
505 let from_balance = &mut from_account.info.balance;
506 let Some(from_balance_decr) = from_balance.checked_sub(balance) else {
507 return Some(TransferError::OutOfFunds);
508 };
509 *from_balance = from_balance_decr;
510
511 // add balance to
512 let to_account = self.state.get_mut(&to).unwrap();
513 Self::touch_account(&mut self.journal, to, to_account);
514 let to_balance = &mut to_account.info.balance;
515 let Some(to_balance_incr) = to_balance.checked_add(balance) else {
516 // Overflow of U256 balance is not possible to happen on mainnet. We don't bother to return funds from from_acc.
517 return Some(TransferError::OverflowPayment);
518 };
519 *to_balance = to_balance_incr;
520
521 // add journal entry
522 self.journal
523 .push(ENTRY::balance_transfer(from, to, balance));
524
525 // EIP-7708: emit ETH transfer log
526 self.eip7708_transfer_log(from, to, balance);
527
528 None
529 }
530
531 /// Transfers balance from two accounts. Returns error if sender balance is not enough.
532 #[inline]
533 pub fn transfer<DB: Database>(
534 &mut self,
535 db: &mut DB,
536 from: Address,
537 to: Address,
538 balance: U256,
539 ) -> Result<Option<TransferError>, DB::Error> {
540 self.load_account(db, from)?;
541 self.load_account(db, to)?;
542 Ok(self.transfer_loaded(from, to, balance))
543 }
544
545 /// Creates account or returns false if collision is detected.
546 ///
547 /// There are few steps done:
548 /// 1. Make created account warm loaded (AccessList) and this should
549 /// be done before subroutine checkpoint is created.
550 /// 2. Check if there is collision of newly created account with existing one.
551 /// 3. Mark created account as created.
552 /// 4. Add fund to created account
553 /// 5. Increment nonce of created account if SpuriousDragon is active
554 /// 6. Decrease balance of caller account.
555 ///
556 /// # Panics
557 ///
558 /// Panics if the caller is not loaded inside the EVM state.
559 /// This should have been done inside `create_inner`.
560 #[inline]
561 pub fn create_account_checkpoint(
562 &mut self,
563 caller: Address,
564 target_address: Address,
565 balance: U256,
566 spec_id: SpecId,
567 ) -> Result<JournalCheckpoint, TransferError> {
568 // Enter subroutine
569 let checkpoint = self.checkpoint();
570
571 // Newly created account is present, as we just loaded it.
572 let target_acc = self.state.get_mut(&target_address).unwrap();
573 let last_journal = &mut self.journal;
574
575 // New account can be created if:
576 // Bytecode is not empty.
577 // Nonce is not zero
578 // Account is not precompile.
579 if target_acc.info.code_hash != KECCAK_EMPTY || target_acc.info.nonce != 0 {
580 self.checkpoint_revert(checkpoint);
581 return Err(TransferError::CreateCollision);
582 }
583
584 // set account status to create.
585 let is_created_globally = target_acc.mark_created_locally();
586
587 // this entry will revert set nonce.
588 last_journal.push(ENTRY::account_created(target_address, is_created_globally));
589 target_acc.info.code = None;
590 // EIP-161: State trie clearing (invariant-preserving alternative)
591 if spec_id.is_enabled_in(SPURIOUS_DRAGON) {
592 // nonce is going to be reset to zero in AccountCreated journal entry.
593 target_acc.info.nonce = 1;
594 }
595
596 // touch account. This is important as for pre SpuriousDragon account could be
597 // saved even empty.
598 Self::touch_account(last_journal, target_address, target_acc);
599
600 // If balance is zero, we don't need to add any journal entries or emit any logs.
601 if balance.is_zero() {
602 return Ok(checkpoint);
603 }
604
605 // Add balance to created account, as we already have target here.
606 let Some(new_balance) = target_acc.info.balance.checked_add(balance) else {
607 self.checkpoint_revert(checkpoint);
608 return Err(TransferError::OverflowPayment);
609 };
610 target_acc.info.balance = new_balance;
611
612 // safe to decrement for the caller as balance check is already done.
613 let caller_account = self.state.get_mut(&caller).unwrap();
614 caller_account.info.balance -= balance;
615
616 // add journal entry of transferred balance
617 last_journal.push(ENTRY::balance_transfer(caller, target_address, balance));
618
619 // EIP-7708: emit ETH transfer log
620 self.eip7708_transfer_log(caller, target_address, balance);
621
622 Ok(checkpoint)
623 }
624
625 /// Makes a checkpoint that in case of Revert can bring back state to this point.
626 #[inline]
627 pub const fn checkpoint(&mut self) -> JournalCheckpoint {
628 let checkpoint = JournalCheckpoint {
629 log_i: self.logs.len(),
630 journal_i: self.journal.len(),
631 selfdestructed_i: self.selfdestructed_addresses.len(),
632 };
633 self.depth += 1;
634 checkpoint
635 }
636
637 /// Commits the checkpoint.
638 #[inline]
639 pub const fn checkpoint_commit(&mut self) {
640 self.depth = self.depth.saturating_sub(1);
641 }
642
643 /// Reverts all changes to state until given checkpoint.
644 #[inline]
645 pub fn checkpoint_revert(&mut self, checkpoint: JournalCheckpoint) {
646 let is_spurious_dragon_enabled = self.cfg.spec.is_enabled_in(SPURIOUS_DRAGON);
647 let state = &mut self.state;
648 let transient_storage = &mut self.transient_storage;
649 self.depth = self.depth.saturating_sub(1);
650 self.logs.truncate(checkpoint.log_i);
651 // EIP-7708: Remove selfdestructed addresses added after checkpoint
652 self.selfdestructed_addresses
653 .truncate(checkpoint.selfdestructed_i);
654
655 // iterate over last N journals sets and revert our global state
656 if checkpoint.journal_i < self.journal.len() {
657 self.journal
658 .drain(checkpoint.journal_i..)
659 .rev()
660 .for_each(|entry| {
661 entry.revert(state, Some(transient_storage), is_spurious_dragon_enabled);
662 });
663 }
664 }
665
666 /// Performs selfdestruct action.
667 /// Transfers balance from address to target. Check if target exist/is_cold
668 ///
669 /// Note: Balance will be lost if address and target are the same BUT when
670 /// current spec enables Cancun, this happens only when the account associated to address
671 /// is created in the same tx
672 ///
673 /// # References:
674 /// * <https://github.com/ethereum/go-ethereum/blob/141cd425310b503c5678e674a8c3872cf46b7086/core/vm/instructions.go#L832-L833>
675 /// * <https://github.com/ethereum/go-ethereum/blob/141cd425310b503c5678e674a8c3872cf46b7086/core/state/statedb.go#L449>
676 /// * <https://eips.ethereum.org/EIPS/eip-6780>
677 #[inline]
678 pub fn selfdestruct<DB: Database>(
679 &mut self,
680 db: &mut DB,
681 address: Address,
682 target: Address,
683 skip_cold_load: bool,
684 ) -> Result<StateLoad<SelfDestructResult>, JournalLoadError<DB::Error>> {
685 let spec = self.cfg.spec;
686 let account_load = self.load_account_optional(db, target, false, skip_cold_load)?;
687 let is_cold = account_load.is_cold;
688 let is_empty = account_load.state_clear_aware_is_empty(spec);
689
690 if address != target {
691 // Both accounts are loaded before this point, `address` as we execute its contract.
692 // and `target` at the beginning of the function.
693 let acc_balance = self.state.get(&address).unwrap().info.balance;
694
695 let target_account = self.state.get_mut(&target).unwrap();
696 Self::touch_account(&mut self.journal, target, target_account);
697 target_account.info.balance += acc_balance;
698 }
699
700 let acc = self.state.get_mut(&address).unwrap();
701 let balance = acc.info.balance;
702
703 let destroyed_status = if !acc.is_selfdestructed() {
704 SelfdestructionRevertStatus::GloballySelfdestroyed
705 } else if !acc.is_selfdestructed_locally() {
706 SelfdestructionRevertStatus::LocallySelfdestroyed
707 } else {
708 SelfdestructionRevertStatus::RepeatedSelfdestruction
709 };
710
711 let is_cancun_enabled = spec.is_enabled_in(CANCUN);
712
713 // EIP-6780 (Cancun hard-fork): selfdestruct only if contract is created in the same tx
714 let journal_entry = if acc.is_created_locally() || !is_cancun_enabled {
715 // Track first self-destruction so the account can be destroyed in place at
716 // finalization (see `eip8246_clear_selfdestructed_accounts`).
717 if destroyed_status == SelfdestructionRevertStatus::GloballySelfdestroyed {
718 self.selfdestructed_addresses.push(address);
719 }
720
721 acc.mark_selfdestructed_locally();
722
723 // `had_balance` records the balance that left the account so it can be restored
724 // on revert.
725 let had_balance = if target != address {
726 // Balance was transferred to target above; zero out the source.
727 acc.info.balance = U256::ZERO;
728 // EIP-7708: transfer log for balance moved to a different address.
729 self.eip7708_transfer_log(address, target, balance);
730 balance
731 } else if spec.is_enabled_in(AMSTERDAM) {
732 // EIP-8246: self-destruct to self no longer burns the balance. The balance is
733 // kept and the account is cleared at finalization
734 // (see `eip8246_clear_selfdestructed_accounts`).
735 U256::ZERO
736 } else {
737 // Pre-EIP-8246: self-destruct to self burns the balance.
738 acc.info.balance = U256::ZERO;
739 balance
740 };
741
742 Some(ENTRY::account_destroyed(
743 address,
744 target,
745 destroyed_status,
746 had_balance,
747 ))
748 } else if address != target {
749 acc.info.balance = U256::ZERO;
750 // EIP-7708: emit appropriate log for selfdestruct
751 // Transfer log for balance transferred to different address
752 self.eip7708_transfer_log(address, target, balance);
753 Some(ENTRY::balance_transfer(address, target, balance))
754 } else {
755 // State is not changed:
756 // * if we are after Cancun upgrade and
757 // * Selfdestruct account that is created in the same transaction and
758 // * Specify the target is same as selfdestructed account. The balance stays unchanged.
759 None
760 };
761
762 if let Some(entry) = journal_entry {
763 self.journal.push(entry);
764 };
765
766 Ok(StateLoad {
767 data: SelfDestructResult {
768 had_value: !balance.is_zero(),
769 target_exists: !is_empty,
770 previously_destroyed: destroyed_status
771 == SelfdestructionRevertStatus::RepeatedSelfdestruction,
772 },
773 is_cold,
774 })
775 }
776
777 /// Loads account into memory. return if it is cold or warm accessed
778 #[inline]
779 pub fn load_account<'a, 'db, DB: Database>(
780 &'a mut self,
781 db: &'db mut DB,
782 address: Address,
783 ) -> Result<StateLoad<&'a Account>, DB::Error>
784 where
785 'db: 'a,
786 {
787 self.load_account_optional(db, address, false, false)
788 .map_err(JournalLoadError::unwrap_db_error)
789 }
790
791 /// Loads account into memory. If account is EIP-7702 type it will additionally
792 /// load delegated account.
793 ///
794 /// It will mark both this and delegated account as warm loaded.
795 ///
796 /// Returns information about the account (If it is empty or cold loaded) and if present the information
797 /// about the delegated account (If it is cold loaded).
798 #[inline]
799 pub fn load_account_delegated<DB: Database>(
800 &mut self,
801 db: &mut DB,
802 address: Address,
803 ) -> Result<StateLoad<AccountLoad>, DB::Error> {
804 let spec = self.cfg.spec;
805 let is_eip7702_enabled = spec.is_enabled_in(SpecId::PRAGUE);
806 let account = self
807 .load_account_optional(db, address, is_eip7702_enabled, false)
808 .map_err(JournalLoadError::unwrap_db_error)?;
809 let is_empty = account.state_clear_aware_is_empty(spec);
810
811 let mut account_load = StateLoad::new(
812 AccountLoad {
813 is_delegate_account_cold: None,
814 is_empty,
815 },
816 account.is_cold,
817 );
818
819 // load delegate code if account is EIP-7702
820 if let Some(address) = account
821 .info
822 .code
823 .as_ref()
824 .and_then(Bytecode::eip7702_address)
825 {
826 let delegate_account = self
827 .load_account_optional(db, address, true, false)
828 .map_err(JournalLoadError::unwrap_db_error)?;
829 account_load.data.is_delegate_account_cold = Some(delegate_account.is_cold);
830 }
831
832 Ok(account_load)
833 }
834
835 /// Loads account and its code. If account is already loaded it will load its code.
836 ///
837 /// It will mark account as warm loaded. If not existing Database will be queried for data.
838 ///
839 /// In case of EIP-7702 delegated account will not be loaded,
840 /// [`Self::load_account_delegated`] should be used instead.
841 #[inline]
842 pub fn load_code<'a, 'db, DB: Database>(
843 &'a mut self,
844 db: &'db mut DB,
845 address: Address,
846 ) -> Result<StateLoad<&'a Account>, DB::Error>
847 where
848 'db: 'a,
849 {
850 self.load_account_optional(db, address, true, false)
851 .map_err(JournalLoadError::unwrap_db_error)
852 }
853
854 /// Loads account into memory. If account is already loaded it will be marked as warm.
855 #[inline]
856 pub fn load_account_optional<'a, 'db, DB: Database>(
857 &'a mut self,
858 db: &'db mut DB,
859 address: Address,
860 load_code: bool,
861 skip_cold_load: bool,
862 ) -> Result<StateLoad<&'a Account>, JournalLoadError<DB::Error>>
863 where
864 'db: 'a,
865 {
866 let mut load = self.load_account_mut_optional(db, address, skip_cold_load)?;
867 if load_code {
868 load.data.load_code_preserve_error()?;
869 }
870 Ok(load.map(|i| i.into_account()))
871 }
872
873 /// Loads account into memory. If account is already loaded it will be marked as warm.
874 #[inline]
875 pub fn load_account_mut<'a, 'db, DB: Database>(
876 &'a mut self,
877 db: &'db mut DB,
878 address: Address,
879 ) -> Result<StateLoad<JournaledAccount<'a, DB, ENTRY>>, DB::Error>
880 where
881 'db: 'a,
882 {
883 self.load_account_mut_optional(db, address, false)
884 .map_err(JournalLoadError::unwrap_db_error)
885 }
886
887 /// Loads account. If account is already loaded it will be marked as warm.
888 #[inline]
889 pub fn load_account_mut_optional_code<'a, 'db, DB: Database>(
890 &'a mut self,
891 db: &'db mut DB,
892 address: Address,
893 load_code: bool,
894 skip_cold_load: bool,
895 ) -> Result<StateLoad<JournaledAccount<'a, DB, ENTRY>>, JournalLoadError<DB::Error>>
896 where
897 'db: 'a,
898 {
899 let mut load = self.load_account_mut_optional(db, address, skip_cold_load)?;
900 if load_code {
901 load.data.load_code_preserve_error()?;
902 }
903 Ok(load)
904 }
905
906 /// Gets the account mut reference.
907 ///
908 /// # Load Unsafe
909 ///
910 /// Use this function only if you know what you are doing. It will not mark the account as warm or cold.
911 /// It will not bump transition_id or return if it is cold or warm loaded. This function is useful
912 /// when we know account is warm, touched and already loaded.
913 ///
914 /// It is useful when we want to access storage from account that is currently being executed.
915 #[inline]
916 pub fn get_account_mut<'a, 'db, DB: Database>(
917 &'a mut self,
918 db: &'db mut DB,
919 address: Address,
920 ) -> Option<JournaledAccount<'a, DB, ENTRY>>
921 where
922 'db: 'a,
923 {
924 let account = self.state.get_mut(&address)?;
925 Some(JournaledAccount::new(
926 address,
927 account,
928 &mut self.journal,
929 db,
930 self.warm_addresses.access_list(),
931 self.transaction_id,
932 ))
933 }
934
935 /// Loads account. If account is already loaded it will be marked as warm.
936 #[inline(never)]
937 pub fn load_account_mut_optional<'a, 'db, DB: Database>(
938 &'a mut self,
939 db: &'db mut DB,
940 address: Address,
941 skip_cold_load: bool,
942 ) -> Result<StateLoad<JournaledAccount<'a, DB, ENTRY>>, JournalLoadError<DB::Error>>
943 where
944 'db: 'a,
945 {
946 let (account, is_cold) = match self.state.entry(address) {
947 Entry::Occupied(entry) => {
948 let account = entry.into_mut();
949
950 // skip load if account is cold.
951 let mut is_cold = account.is_cold_transaction_id(self.transaction_id);
952
953 if unlikely(is_cold) {
954 is_cold = self
955 .warm_addresses
956 .check_is_cold(&address, skip_cold_load)?;
957
958 // mark it warm.
959 account.mark_warm_with_transaction_id(self.transaction_id);
960
961 // if it is cold loaded and we have selfdestructed locally it means that
962 // account was selfdestructed in previous transaction and we need to clear its information and storage.
963 if account.is_selfdestructed_locally() {
964 account.selfdestruct();
965 account.unmark_selfdestructed_locally();
966 }
967 account.set_current_info_as_original();
968
969 // unmark locally created
970 account.unmark_created_locally();
971
972 // journal loading of cold account.
973 self.journal.push(ENTRY::account_warmed(address));
974 }
975 (account, is_cold)
976 }
977 Entry::Vacant(vac) => {
978 // Precompiles, among some other account(access list and coinbase included)
979 // are warm loaded so we need to take that into account
980 let is_cold = self
981 .warm_addresses
982 .check_is_cold(&address, skip_cold_load)?;
983
984 let account = if let Some(account) = db.basic(address)? {
985 let mut account: Account = account.into();
986 account.transaction_id = self.transaction_id;
987 account
988 } else {
989 Account::new_not_existing(self.transaction_id)
990 };
991
992 // journal loading of cold account.
993 if is_cold {
994 self.journal.push(ENTRY::account_warmed(address));
995 }
996
997 (vac.insert(account), is_cold)
998 }
999 };
1000
1001 Ok(StateLoad::new(
1002 JournaledAccount::new(
1003 address,
1004 account,
1005 &mut self.journal,
1006 db,
1007 self.warm_addresses.access_list(),
1008 self.transaction_id,
1009 ),
1010 is_cold,
1011 ))
1012 }
1013
1014 /// Loads storage slot.
1015 #[inline]
1016 pub fn sload<DB: Database>(
1017 &mut self,
1018 db: &mut DB,
1019 address: Address,
1020 key: StorageKey,
1021 skip_cold_load: bool,
1022 ) -> Result<StateLoad<StorageValue>, JournalLoadError<DB::Error>> {
1023 self.load_account_mut(db, address)?
1024 .sload_concrete_error(key, skip_cold_load)
1025 .map(|s| s.map(|s| s.present_value))
1026 }
1027
1028 /// Loads storage slot.
1029 ///
1030 /// If account is not present it will return [`JournalLoadError::ColdLoadSkipped`] error.
1031 #[inline]
1032 pub fn sload_assume_account_present<DB: Database>(
1033 &mut self,
1034 db: &mut DB,
1035 address: Address,
1036 key: StorageKey,
1037 skip_cold_load: bool,
1038 ) -> Result<StateLoad<StorageValue>, JournalLoadError<DB::Error>> {
1039 let Some(mut account) = self.get_account_mut(db, address) else {
1040 return Err(JournalLoadError::ColdLoadSkipped);
1041 };
1042
1043 account
1044 .sload_concrete_error(key, skip_cold_load)
1045 .map(|s| s.map(|s| s.present_value))
1046 }
1047
1048 /// Stores storage slot.
1049 ///
1050 /// If account is not present it will load from database
1051 #[inline]
1052 pub fn sstore<DB: Database>(
1053 &mut self,
1054 db: &mut DB,
1055 address: Address,
1056 key: StorageKey,
1057 new: StorageValue,
1058 skip_cold_load: bool,
1059 ) -> Result<StateLoad<SStoreResult>, JournalLoadError<DB::Error>> {
1060 self.load_account_mut(db, address)?
1061 .sstore_concrete_error(key, new, skip_cold_load)
1062 }
1063
1064 /// Stores storage slot.
1065 ///
1066 /// And returns (original,present,new) slot value.
1067 ///
1068 /// **Note**: Account should already be present in our state.
1069 #[inline]
1070 pub fn sstore_assume_account_present<DB: Database>(
1071 &mut self,
1072 db: &mut DB,
1073 address: Address,
1074 key: StorageKey,
1075 new: StorageValue,
1076 skip_cold_load: bool,
1077 ) -> Result<StateLoad<SStoreResult>, JournalLoadError<DB::Error>> {
1078 let Some(mut account) = self.get_account_mut(db, address) else {
1079 return Err(JournalLoadError::ColdLoadSkipped);
1080 };
1081
1082 account.sstore_concrete_error(key, new, skip_cold_load)
1083 }
1084
1085 /// Read transient storage tied to the account.
1086 ///
1087 /// EIP-1153: Transient storage opcodes
1088 #[inline]
1089 pub fn tload(&mut self, address: Address, key: StorageKey) -> StorageValue {
1090 self.transient_storage.get_value(address, key)
1091 }
1092
1093 /// Store transient storage tied to the account.
1094 ///
1095 /// If values is different add entry to the journal
1096 /// so that old state can be reverted if that action is needed.
1097 ///
1098 /// EIP-1153: Transient storage opcodes
1099 #[inline]
1100 pub fn tstore(&mut self, address: Address, key: StorageKey, new: StorageValue) {
1101 let had_value = if new.is_zero() {
1102 // if new values is zero, remove entry from transient storage.
1103 // if previous values was some insert it inside journal.
1104 // If it is none nothing should be inserted.
1105 self.transient_storage.remove_value(address, key)
1106 } else {
1107 // insert values
1108 let previous_value = self
1109 .transient_storage
1110 .insert_value(address, key, new)
1111 .unwrap_or_default();
1112
1113 // check if previous value is same
1114 if previous_value != new {
1115 // if it is different, insert previous values inside journal.
1116 Some(previous_value)
1117 } else {
1118 None
1119 }
1120 };
1121
1122 if let Some(had_value) = had_value {
1123 // insert in journal only if value was changed.
1124 self.journal
1125 .push(ENTRY::transient_storage_changed(address, key, had_value));
1126 }
1127 }
1128
1129 /// Pushes log into subroutine.
1130 #[inline]
1131 pub fn log(&mut self, log: Log) {
1132 self.logs.push(log);
1133 }
1134
1135 /// Creates and pushes an EIP-7708 ETH transfer log.
1136 ///
1137 /// This emits a LOG3 with the Transfer event signature, matching ERC-20 transfer events.
1138 /// Only emitted if EIP-7708 is enabled (Amsterdam and later) and balance is non-zero.
1139 ///
1140 /// [EIP-7708](https://eips.ethereum.org/EIPS/eip-7708)
1141 #[inline]
1142 pub fn eip7708_transfer_log(&mut self, from: Address, to: Address, balance: U256) {
1143 // Only emit log if EIP-7708 is enabled and balance is non-zero
1144 if !self.cfg.spec.is_enabled_in(AMSTERDAM) || self.cfg.eip7708_disabled || balance.is_zero()
1145 {
1146 return;
1147 }
1148
1149 // Create LOG3 with Transfer(address,address,uint256) event signature
1150 // Topic[0]: Transfer event signature
1151 // Topic[1]: from address (zero-padded to 32 bytes)
1152 // Topic[2]: to address (zero-padded to 32 bytes)
1153 // Data: amount in wei (big-endian uint256)
1154 let topics = std::vec![
1155 ETH_TRANSFER_LOG_TOPIC,
1156 B256::left_padding_from(from.as_slice()),
1157 B256::left_padding_from(to.as_slice()),
1158 ];
1159 let data = Bytes::copy_from_slice(&balance.to_be_bytes::<32>());
1160
1161 self.logs.push(Log {
1162 address: ETH_TRANSFER_LOG_ADDRESS,
1163 data: LogData::new(topics, data).expect("3 topics is valid"),
1164 });
1165 }
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170 use super::*;
1171 use context_interface::journaled_state::entry::JournalEntry;
1172 use database_interface::EmptyDB;
1173 use primitives::{address, HashSet, U256};
1174 use state::AccountInfo;
1175
1176 #[test]
1177 fn test_sload_skip_cold_load() {
1178 let mut journal = JournalInner::<JournalEntry>::new();
1179 let test_address = address!("1000000000000000000000000000000000000000");
1180 let test_key = U256::from(1);
1181
1182 // Insert account into state
1183 let account_info = AccountInfo {
1184 balance: U256::from(1000),
1185 nonce: 1,
1186 code_hash: KECCAK_EMPTY,
1187 code: Some(Bytecode::default()),
1188 account_id: None,
1189 };
1190 journal
1191 .state
1192 .insert(test_address, Account::from(account_info));
1193
1194 // Add storage slot to access list (make it warm)
1195 let mut access_list = HashMap::default();
1196 let mut storage_keys = HashSet::default();
1197 storage_keys.insert(test_key);
1198 access_list.insert(test_address, storage_keys);
1199 journal.warm_addresses.set_access_list(access_list);
1200
1201 // Try to sload with skip_cold_load=true - should succeed because slot is in access list
1202 let mut db = EmptyDB::new();
1203 let result = journal.sload_assume_account_present(&mut db, test_address, test_key, true);
1204
1205 // Should succeed and return as warm
1206 assert!(result.is_ok());
1207 let state_load = result.unwrap();
1208 assert!(!state_load.is_cold); // Should be warm
1209 assert_eq!(state_load.data, U256::ZERO); // Empty slot
1210 }
1211}