Skip to content
Merged
Show file tree
Hide file tree
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
413 changes: 391 additions & 22 deletions client/rpc/src/eth/execute.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frame/ethereum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,7 @@ impl<T: Config> Pallet<T> {
validate,
weight_limit,
proof_size_base_cost,
None,
config.as_ref().unwrap_or_else(|| T::config()),
) {
Ok(res) => res,
Expand Down
1 change: 1 addition & 0 deletions frame/evm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ pub mod pallet {
validate,
None,
None,
None,
T::config(),
) {
Ok(info) => info,
Expand Down
5 changes: 4 additions & 1 deletion frame/evm/src/runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub mod stack;
use crate::{Config, Weight};
use alloc::vec::Vec;
use ethereum::AuthorizationList;
use fp_evm::{CallInfo, CreateInfo};
use fp_evm::{CallInfo, CreateInfo, StateOverride};
use sp_core::{H160, H256, U256};

#[derive(Debug)]
Expand Down Expand Up @@ -65,6 +65,9 @@ pub trait Runner<T: Config> {
validate: bool,
weight_limit: Option<Weight>,
proof_size_base_cost: Option<u64>,
// Per-account full storage replacement (Geth-style `state` override).
// See [`StateOverride`] for the encoding and semantics.
state_override: StateOverride,
config: &evm::Config,
) -> Result<CallInfo, RunnerError<Self::Error>>;

Expand Down
106 changes: 99 additions & 7 deletions frame/evm/src/runner/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ use sp_runtime::traits::UniqueSaturatedInto;
// Frontier
use fp_evm::{
AccessedStorage, CallInfo, CreateInfo, ExecutionInfoV2, IsPrecompileResult, Log, PrecompileSet,
Vicinity, WeightInfo, ACCOUNT_BASIC_PROOF_SIZE, ACCOUNT_CODES_KEY_SIZE,
StateOverride, Vicinity, WeightInfo, ACCOUNT_BASIC_PROOF_SIZE, ACCOUNT_CODES_KEY_SIZE,
ACCOUNT_CODES_METADATA_PROOF_SIZE, ACCOUNT_STORAGE_PROOF_SIZE, IS_EMPTY_CHECK_PROOF_SIZE,
WRITE_PROOF_SIZE,
};
Expand Down Expand Up @@ -83,6 +83,7 @@ where
weight_limit: Option<Weight>,
proof_size_base_cost: Option<u64>,
measured_proof_size_before: u64,
state_override: StateOverride,
f: F,
) -> Result<ExecutionInfoV2<R>, RunnerError<Error<T>>>
where
Expand Down Expand Up @@ -114,6 +115,7 @@ where
weight_limit,
proof_size_base_cost,
measured_proof_size_before,
state_override,
);

#[cfg(feature = "forbid-evm-reentrancy")]
Expand Down Expand Up @@ -153,6 +155,7 @@ where
weight_limit,
proof_size_base_cost,
measured_proof_size_before,
state_override,
)
});

Expand All @@ -175,6 +178,7 @@ where
weight_limit: Option<Weight>,
proof_size_base_cost: Option<u64>,
measured_proof_size_before: u64,
state_override: StateOverride,
) -> Result<ExecutionInfoV2<R>, RunnerError<Error<T>>>
where
F: FnOnce(
Expand Down Expand Up @@ -307,7 +311,13 @@ where
};

let metadata = StackSubstateMetadata::new(gas_limit, config);
let state = SubstrateStackState::new(&vicinity, metadata, maybe_weight_info, storage_limit);
let state = SubstrateStackState::new(
&vicinity,
metadata,
maybe_weight_info,
storage_limit,
state_override,
);
let mut executor = StackExecutor::new_with_precompiles(state, config, precompiles);

// Execute the EVM call.
Expand Down Expand Up @@ -530,6 +540,7 @@ where
validate: bool,
weight_limit: Option<Weight>,
proof_size_base_cost: Option<u64>,
state_override: StateOverride,
config: &evm::Config,
) -> Result<CallInfo, RunnerError<Self::Error>> {
let measured_proof_size_before = get_proof_size().unwrap_or_default();
Expand Down Expand Up @@ -578,6 +589,7 @@ where
weight_limit,
proof_size_base_cost,
measured_proof_size_before,
state_override,
|executor| {
executor.transact_call(
source,
Expand Down Expand Up @@ -658,6 +670,7 @@ where
weight_limit,
proof_size_base_cost,
measured_proof_size_before,
None,
|executor| {
let address = executor.create_address(evm::CreateScheme::Legacy { caller: source });
T::OnCreate::on_create(source, address);
Expand Down Expand Up @@ -742,6 +755,7 @@ where
weight_limit,
proof_size_base_cost,
measured_proof_size_before,
None,
|executor| {
let address = executor.create_address(evm::CreateScheme::Create2 {
caller: source,
Expand Down Expand Up @@ -829,6 +843,7 @@ where
weight_limit,
proof_size_base_cost,
measured_proof_size_before,
None,
|executor| {
T::OnCreate::on_create(source, contract_address);
let (reason, _) = executor.transact_create_force_address(
Expand Down Expand Up @@ -973,6 +988,16 @@ pub struct SubstrateStackState<'vicinity, 'config, T> {
substate: SubstrateStackSubstate<'config>,
original_storage: BTreeMap<(H160, H256), H256>,
transient_storage: BTreeMap<(H160, H256), H256>,
/// Per-account full storage replacement. When an address is present here, storage
/// reads are served exclusively from the per-address map (missing slots return zero)
/// instead of the on-chain trie. An entry with an empty inner map represents a full
/// wipe (Geth-style `"state": {}`).
state_override: BTreeMap<H160, BTreeMap<H256, H256>>,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/// Per-frame snapshots of `state_override`, pushed on `enter` and restored
/// on `exit_revert`/`exit_discard` (Geth StateDB snapshot parity).
/// `None` entries skip the clone when the override map is empty — safe
/// because `state_override` only shrinks during execution.
state_override_journal: Vec<Option<BTreeMap<H160, BTreeMap<H256, H256>>>>,
recorded: Recorded,
weight_info: Option<WeightInfo>,
storage_meter: Option<StorageMeter>,
Expand All @@ -986,8 +1011,17 @@ impl<'vicinity, 'config, T: Config> SubstrateStackState<'vicinity, 'config, T> {
metadata: StackSubstateMetadata<'config>,
weight_info: Option<WeightInfo>,
storage_limit: Option<u64>,
state_override: StateOverride,
) -> Self {
let storage_meter = storage_limit.map(StorageMeter::new);
let state_override = state_override
.map(|per_address| {
per_address
.into_iter()
.map(|(address, slots)| (address, slots.into_iter().collect()))
.collect::<BTreeMap<H160, BTreeMap<H256, H256>>>()
})
.unwrap_or_default();
Self {
vicinity,
substate: SubstrateStackSubstate {
Expand All @@ -1000,12 +1034,26 @@ impl<'vicinity, 'config, T: Config> SubstrateStackState<'vicinity, 'config, T> {
_marker: PhantomData,
original_storage: BTreeMap::new(),
transient_storage: BTreeMap::new(),
state_override,
state_override_journal: Vec::new(),
recorded: Default::default(),
weight_info,
storage_meter,
}
}

/// Resolve a storage slot for `address` as the EVM should observe it.
///
/// If an active state override covers `address`, the slot is served from the
/// in-memory override map (missing slots return zero, matching Geth's
/// "fresh state object" semantics). Otherwise the on-chain trie is read.
fn read_effective_storage(&self, address: H160, index: H256) -> H256 {
if let Some(slots) = self.state_override.get(&address) {
return slots.get(&index).copied().unwrap_or_default();
}
<AccountStorages<T>>::get(address, index)
}

pub fn weight_info(&self) -> Option<WeightInfo> {
self.weight_info
}
Expand Down Expand Up @@ -1127,7 +1175,7 @@ where
}

fn storage(&self, address: H160, index: H256) -> H256 {
<AccountStorages<T>>::get(address, index)
self.read_effective_storage(address, index)
}

fn transient_storage(&self, address: H160, index: H256) -> H256 {
Expand All @@ -1142,7 +1190,7 @@ where
self.original_storage
.get(&(address, index))
.cloned()
.unwrap_or_else(|| self.storage(address, index)),
.unwrap_or_else(|| self.read_effective_storage(address, index)),
)
}
}
Expand All @@ -1160,18 +1208,42 @@ where
}

fn enter(&mut self, gas_limit: u64, is_static: bool) {
// Journal the override alongside the Substrate storage transaction that
// `SubstrateStackSubstate::enter` opens, so an inner frame's SSTORE or
// SELFDESTRUCT-driven `reset_storage` against an overridden account can
// be rolled back on revert/discard (Geth StateDB parity).
// `None` when empty: child frames can't mutate an empty map, so we skip
// the clone while still pairing the push/pop with the substate frame.
let snapshot = (!self.state_override.is_empty()).then(|| self.state_override.clone());
self.state_override_journal.push(snapshot);
self.substate.enter(gas_limit, is_static)
}

fn exit_commit(&mut self) -> Result<(), ExitError> {
// Keep the mutated `state_override`; just drop the snapshot.
let _ = self.state_override_journal.pop();
self.substate.exit_commit()
}

fn exit_revert(&mut self) -> Result<(), ExitError> {
if let Some(snapshot) = self
.state_override_journal
.pop()
.expect("exit_revert paired with enter; snapshot always pushed")
{
self.state_override = snapshot;
}
self.substate.exit_revert()
}

fn exit_discard(&mut self) -> Result<(), ExitError> {
if let Some(snapshot) = self
.state_override_journal
.pop()
.expect("exit_discard paired with enter; snapshot always pushed")
{
self.state_override = snapshot;
}
self.substate.exit_discard()
}

Expand All @@ -1196,15 +1268,28 @@ where
fn set_storage(&mut self, address: H160, index: H256, value: H256) {
// We cache the current value if this is the first time we modify it
// in the transaction.
use alloc::collections::btree_map::Entry::Vacant;
if let Vacant(e) = self.original_storage.entry((address, index)) {
let original = <AccountStorages<T>>::get(address, index);
use alloc::collections::btree_map::Entry;
let original = self.read_effective_storage(address, index);
if let Entry::Vacant(e) = self.original_storage.entry((address, index)) {
// No need to cache if same value.
if original != value {
e.insert(original);
}
}

// Geth-parity: if this account has an active state override, writes
// update the override map (the account's "fresh state"), not the
// underlying trie. Subsequent SLOADs will observe the new value via
// `read_effective_storage`, while the on-chain storage stays untouched.
if let Some(slots) = self.state_override.get_mut(&address) {
if value == H256::default() {
slots.remove(&index);
} else {
slots.insert(index, value);
}
return;
}

// Then we insert or remove the entry based on the value.
if value == H256::default() {
log::debug!(
Expand All @@ -1226,6 +1311,10 @@ where
}

fn reset_storage(&mut self, address: H160) {
// Geth-parity: once an account is destructed/recreated, the prior state
// override (if any) no longer applies — subsequent SLOADs must fall back
// to the (now-empty) persisted storage, returning zero until written.
self.state_override.remove(&address);
#[allow(deprecated)]
let _ = <AccountStorages<T>>::remove_prefix(address, None);
}
Expand Down Expand Up @@ -1596,6 +1685,7 @@ mod tests {
None,
None,
measured_proof_size_before,
None,
|_| {
let measured_proof_size_before2 = get_proof_size().unwrap_or_default();
let res = Runner::<Test>::execute(
Expand All @@ -1610,6 +1700,7 @@ mod tests {
None,
None,
measured_proof_size_before2,
None,
|_| (ExitReason::Succeed(ExitSucceed::Stopped), ()),
);
assert_matches!(
Expand Down Expand Up @@ -1644,6 +1735,7 @@ mod tests {
None,
None,
measured_proof_size_before,
None,
|_| (ExitReason::Succeed(ExitSucceed::Stopped), ()),
);
assert!(res.is_ok());
Expand Down
Loading
Loading