perf(multiproof): optimize state batching with in-place merge

- Merge EvmState in-place as messages arrive instead of accumulating to Vec
- Add storage.reserve() to reduce HashMap rehashing during merge
- Remove unused accumulated_state_updates Vec and STATE_UPDATE_BATCH_PREALLOC
- Inline on_hashed_state_update_without_added_removed into on_hashed_state_update

This reduces allocations and memory pressure during state update batching.
This commit is contained in:
yongkangc
2025-12-19 03:10:09 +00:00
parent e34201a4d9
commit a3c55a53d1

View File

@@ -242,70 +242,6 @@ pub(crate) fn evm_state_to_hashed_post_state(update: EvmState) -> HashedPostStat
hashed_state
}
/// Estimates the number of proof targets for an `EvmState` (accounts + storage slots).
fn estimate_evm_state_targets(state: &EvmState) -> usize {
state
.iter()
.filter(|(_, account)| account.is_touched())
.map(|(_, account)| {
1 + account.storage.iter().filter(|(_, slot)| slot.is_changed()).count()
})
.sum()
}
/// Merges `overlay` into `base` while preserving `original_value` from `base`.
///
/// `EvmState::extend` overwrites `original_value`, breaking `is_changed()` when batching:
///
/// - TX1: slot X changes 0->10 (original=0, present=10, is_changed=true)
/// - TX2: reads slot X (original=10, present=10, is_changed=false)
/// - After extend: original=10, present=10, is_changed=false (WRONG)
///
/// This function keeps base's `original_value`, so `is_changed()` stays correct:
///
/// - After merge: original=0, present=10, is_changed=true (CORRECT)
fn merge_evm_state(base: &mut EvmState, overlay: EvmState) {
for (addr, overlay_account) in overlay {
match base.get_mut(&addr) {
Some(base_account) => {
// Merge account info - overlay wins for latest state
base_account.info = overlay_account.info;
// Merge status flags
base_account.status |= overlay_account.status;
// Update transaction_id to latest
base_account.transaction_id = overlay_account.transaction_id;
if base_account.is_selfdestructed() {
// Selfdestructed account has its storage cleared, so we can
// replace it entirely with overlay's storage
base_account.storage = overlay_account.storage;
} else {
// Merge storage: preserve base's original_value, take overlay's present_value
for (slot, overlay_slot) in overlay_account.storage {
match base_account.storage.get_mut(&slot) {
Some(base_slot) => {
// Key insight: keep base's original_value, update present_value
base_slot.present_value = overlay_slot.present_value;
// Update cold/transaction tracking from overlay
base_slot.transaction_id = overlay_slot.transaction_id;
base_slot.is_cold = overlay_slot.is_cold;
}
None => {
// Slot is new in overlay - use overlay's values as-is
base_account.storage.insert(slot, overlay_slot);
}
}
}
}
}
None => {
// Account is new in overlay - insert as-is
base.insert(addr, overlay_account);
}
}
}
}
/// A pending multiproof task, either [`StorageMultiproofInput`] or [`MultiproofInput`].
#[derive(Debug)]
enum PendingMultiproofTask {
@@ -1638,6 +1574,70 @@ where
1
}
/// Estimates the number of proof targets for an `EvmState` (accounts + storage slots).
fn estimate_evm_state_targets(state: &EvmState) -> usize {
state
.iter()
.filter(|(_, account)| account.is_touched())
.map(|(_, account)| {
1 + account.storage.iter().filter(|(_, slot)| slot.is_changed()).count()
})
.sum()
}
/// Merges `overlay` into `base` while preserving `original_value` from `base`.
///
/// `EvmState::extend` overwrites `original_value`, breaking `is_changed()` when batching:
///
/// - TX1: slot X changes 0->10 (original=0, present=10, is_changed=true)
/// - TX2: reads slot X (original=10, present=10, is_changed=false)
/// - After extend: original=10, present=10, is_changed=false (WRONG)
///
/// This function keeps base's `original_value`, so `is_changed()` stays correct:
///
/// - After merge: original=0, present=10, is_changed=true (CORRECT)
fn merge_evm_state(base: &mut EvmState, overlay: EvmState) {
for (addr, overlay_account) in overlay {
match base.get_mut(&addr) {
Some(base_account) => {
// Merge account info - overlay wins for latest state
base_account.info = overlay_account.info;
// Merge status flags
base_account.status |= overlay_account.status;
// Update transaction_id to latest
base_account.transaction_id = overlay_account.transaction_id;
if base_account.is_selfdestructed() {
// Selfdestructed account has its storage cleared, so we can
// replace it entirely with overlay's storage
base_account.storage = overlay_account.storage;
} else {
// Merge storage: preserve base's original_value, take overlay's present_value
for (slot, overlay_slot) in overlay_account.storage {
match base_account.storage.get_mut(&slot) {
Some(base_slot) => {
// Key insight: keep base's original_value, update present_value
base_slot.present_value = overlay_slot.present_value;
// Update cold/transaction tracking from overlay
base_slot.transaction_id = overlay_slot.transaction_id;
base_slot.is_cold = overlay_slot.is_cold;
}
None => {
// Slot is new in overlay - use overlay's values as-is
base_account.storage.insert(slot, overlay_slot);
}
}
}
}
}
None => {
// Account is new in overlay - insert as-is
base.insert(addr, overlay_account);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;