Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 196 additions & 4 deletions crates/trie/parallel/src/state_root_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,19 @@ pub fn evm_state_to_hashed_post_state(update: EvmState) -> HashedPostState {
trace!(target: "trie::parallel::sparse", ?address, ?hashed_address, "Adding account to state update");

let destroyed = account.is_selfdestructed();
if account.info != account.original_info() {
let info = if destroyed { None } else { Some(account.info.into()) };
hashed_state.accounts.insert(hashed_address, info);
// EIP-161: a touched account that ends up empty is deleted, so it must be emitted
// as a removal rather than as an all-zero account. This mirrors what revm does in
// the bundle path (`CacheAccount::touch_empty_eip161`) and what the sibling
// producer for this consumer already does in `send_bal_hashed_state`.
// An address that never existed and still does not exist is not a deletion: revm
// emits no transition for `LoadedNotExisting`. Skipping it matches the bundle
// producer; every `None` here becomes a storage-trie cursor walk in `StateRoot`.
let deleted = destroyed || (account.is_empty() && !account.is_loaded_as_not_existing());
if deleted {
hashed_state.accounts.insert(hashed_address, None);
} else if account.info != account.original_info() {
// A touched but unchanged account produces no bundle transition either.
hashed_state.accounts.insert(hashed_address, Some(account.info.into()));
}

let mut changed_storage_iter = account
Expand Down Expand Up @@ -537,7 +547,7 @@ mod tests {
#[test]
fn created_selfdestruct_does_not_emit_storage() {
let address = Address::repeat_byte(0x01);
let mut account = Account::default();
let mut account = Account::new_not_existing(TransactionId::ZERO);
account.mark_touch();
assert!(account.mark_created_locally());
assert!(account.mark_selfdestructed_locally());
Expand Down Expand Up @@ -577,6 +587,188 @@ mod tests {
assert!(!hashed_state.storages.contains_key(&hashed_address));
}

/// An account drained to zero balance, with nonce 0 and no code, is EIP-161-empty and
/// canonical execution deletes it. Asserts the converter reports the deletion as `None`,
/// matching `HashedPostState::from_bundle_state`, so `write_hashed_state` removes the
/// `HashedAccounts` row instead of upserting an all-zero one.
#[test]
fn emptied_account_is_deleted() {
let address = Address::repeat_byte(0x05);
let mut account = Account::default();
// Pre-state: the account exists and holds a balance.
account.info.balance = U256::from(1);
account.set_current_info_as_original();
// This block drains it. Not selfdestructed: an ordinary value transfer out.
account.mark_touch();
account.info.balance = U256::ZERO;
assert!(account.is_empty(), "the drained account must be EIP-161-empty");
assert!(!account.is_selfdestructed());

let hashed_state =
evm_state_to_hashed_post_state(EvmState::from_iter([(address, account)]));

assert_eq!(hashed_state.accounts.get(&keccak256(address)), Some(&None));
}

/// A pre-existing EIP-161-empty account that is merely touched must be deleted. revm marks it
/// for removal (`touch_empty_eip161`) and the `BundleState` path reports `None`. Asserts the
/// converter emits the deletion even though `info == original_info()`, so an existing all-zero
/// `HashedAccounts` row can be cleared by a touch.
#[test]
fn touched_preexisting_empty_account_is_deleted() {
let address = Address::repeat_byte(0x06);
// Empty in the pre-state too: nonce 0, balance 0, no code.
let mut account = Account::default();
account.set_current_info_as_original();
account.mark_touch();
assert!(account.is_empty());

let hashed_state =
evm_state_to_hashed_post_state(EvmState::from_iter([(address, account)]));

assert_eq!(hashed_state.accounts.get(&keccak256(address)), Some(&None));
}

/// The two `HashedPostState` producers that feed the same consumer must agree.
///
/// `evm_state_to_hashed_post_state` converts the raw `EvmState` handed to the state hook;
/// `HashedPostState::from_bundle_state` converts the `BundleState` revm produces from the
/// same execution. Since "perf: avoid hashing the state twice" the engine persists whichever
/// one it gets, so a disagreement between them is a disagreement about durable state.
///
/// Asserts both producers agree for a touched account drained to EIP-161-empty, which revm
/// marks for removal in the bundle path (`CacheAccount::touch_empty_eip161`).
#[test]
fn matches_bundle_state_for_emptied_account() {
use revm::{
database::{states::bundle_state::BundleRetention, State},
state::AccountInfo,
DatabaseCommit,
};

let address = Address::repeat_byte(0x07);
let pre = AccountInfo { balance: U256::from(1), ..Default::default() };

// The EvmState the state hook observes: a funded account drained to empty.
let mut account = Account::from(pre.clone());
account.mark_touch();
account.info.balance = U256::ZERO;
let evm_state = EvmState::from_iter([(address, account)]);

// Same execution, through revm's own bundle machinery.
let mut db = State::builder().with_bundle_update().build();
db.insert_account(address, pre);
db.commit(evm_state.clone());
db.merge_transitions(BundleRetention::PlainState);
let bundle = db.take_bundle();

let from_bundle =
HashedPostState::from_bundle_state::<reth_trie::KeccakKeyHasher>(bundle.state.iter());
let from_hook = evm_state_to_hashed_post_state(evm_state);

assert_eq!(
from_hook.accounts, from_bundle.accounts,
"state-hook and bundle producers disagree about durable account state"
);
}

/// An account created during the block whose final info is empty. EIP-161 deletes it, so
/// the bundle producer reports a removal.
#[test]
fn created_empty_account_matches_bundle_state() {
use revm::{
database::{states::bundle_state::BundleRetention, State},
DatabaseCommit,
};

let address = Address::repeat_byte(0x08);
let mut account = Account::default();
account.mark_touch();
assert!(account.mark_created_locally());
assert!(account.is_empty());
let evm_state = EvmState::from_iter([(address, account)]);

let mut db = State::builder().with_bundle_update().build();
db.commit(evm_state.clone());
db.merge_transitions(BundleRetention::PlainState);
let bundle = db.take_bundle();

let from_bundle =
HashedPostState::from_bundle_state::<reth_trie::KeccakKeyHasher>(bundle.state.iter());
let from_hook = evm_state_to_hashed_post_state(evm_state);

assert_eq!(
from_hook.accounts.get(&keccak256(address)).copied().flatten(),
from_bundle.accounts.get(&keccak256(address)).copied().flatten(),
"state-hook and bundle producers disagree about a created-empty account"
);
}

/// A zero-value call to an address that never existed leaves it non-existent. revm's bundle
/// path emits no transition for it (`CacheAccount::touch_empty_eip161` returns `None` for
/// `LoadedNotExisting`). Asserts the converter also emits nothing, rather than a deletion
/// that would queue a `HashedAccounts` delete and a storage-trie wipe for an unused address.
#[test]
fn touched_never_existing_account_matches_bundle_state() {
use revm::{
database::{states::bundle_state::BundleRetention, State},
DatabaseCommit,
};

let address = Address::repeat_byte(0x09);
let mut account = Account::new_not_existing(TransactionId::default());
account.mark_touch();
let evm_state = EvmState::from_iter([(address, account)]);

let mut db = State::builder().with_bundle_update().build();
db.commit(evm_state.clone());
db.merge_transitions(BundleRetention::PlainState);
let bundle = db.take_bundle();

let from_bundle =
HashedPostState::from_bundle_state::<reth_trie::KeccakKeyHasher>(bundle.state.iter());
let from_hook = evm_state_to_hashed_post_state(evm_state);

assert_eq!(
from_hook.accounts, from_bundle.accounts,
"state-hook and bundle producers disagree about a never-existing touched account"
);
}

/// An account touched without being changed. revm's bundle producer reports nothing for it.
/// Asserts the converter agrees, so a block full of zero-value calls does not rewrite
/// unchanged `HashedAccounts` rows.
#[test]
fn touched_unchanged_account_matches_bundle_state() {
use revm::{
database::{states::bundle_state::BundleRetention, State},
state::AccountInfo,
DatabaseCommit,
};

let address = Address::repeat_byte(0x0a);
let pre = AccountInfo { balance: U256::from(7), nonce: 1, ..Default::default() };
let mut account = Account::from(pre.clone());
account.mark_touch();
assert!(!account.is_empty());
let evm_state = EvmState::from_iter([(address, account)]);

let mut db = State::builder().with_bundle_update().build();
db.insert_account(address, pre);
db.commit(evm_state.clone());
db.merge_transitions(BundleRetention::PlainState);
let bundle = db.take_bundle();

let from_bundle =
HashedPostState::from_bundle_state::<reth_trie::KeccakKeyHasher>(bundle.state.iter());
let from_hook = evm_state_to_hashed_post_state(evm_state);

assert_eq!(
from_hook.accounts, from_bundle.accounts,
"state-hook and bundle producers disagree about a touched but unchanged account"
);
}

#[derive(Default)]
struct CountingSink {
access_hints: AtomicUsize,
Expand Down
Loading