diff --git a/common/src/network_spec.rs b/common/src/network_spec.rs index 6a81ed4d..217550d2 100644 --- a/common/src/network_spec.rs +++ b/common/src/network_spec.rs @@ -1,6 +1,11 @@ use std::{collections::HashMap, fmt::Debug, sync::Arc}; -use alloy::{eips::BlockId, network::Network, primitives::Address, rpc::types::Log}; +use alloy::{ + eips::BlockId, + network::Network, + primitives::Address, + rpc::types::{state::StateOverride, Log}, +}; use async_trait::async_trait; use revm::context::result::ExecutionResult; @@ -27,5 +32,6 @@ pub trait NetworkSpec: Network { chain_id: u64, fork_schedule: ForkSchedule, block_id: BlockId, + state_overrides: Option, ) -> Result<(ExecutionResult, HashMap), EvmError>; } diff --git a/core/src/client/api.rs b/core/src/client/api.rs index 9b348346..4c7f6ff9 100644 --- a/core/src/client/api.rs +++ b/core/src/client/api.rs @@ -1,7 +1,10 @@ use alloy::{ eips::BlockId, primitives::{Address, Bytes, B256, U256}, - rpc::types::{AccessListResult, EIP1186AccountProofResponse, Filter, Log, SyncStatus}, + rpc::types::{ + state::StateOverride, AccessListResult, EIP1186AccountProofResponse, Filter, Log, + SyncStatus, + }, }; use async_trait::async_trait; use eyre::Result; @@ -53,12 +56,23 @@ pub trait HeliosApi: Send + Sync + 'static { block_id: BlockId, ) -> Result>>; // evm - async fn call(&self, tx: &N::TransactionRequest, block_id: BlockId) -> Result; - async fn estimate_gas(&self, tx: &N::TransactionRequest, block_id: BlockId) -> Result; + async fn call( + &self, + tx: &N::TransactionRequest, + block_id: BlockId, + state_overrides: Option, + ) -> Result; + async fn estimate_gas( + &self, + tx: &N::TransactionRequest, + block_id: BlockId, + state_overrides: Option, + ) -> Result; async fn create_access_list( &self, tx: &N::TransactionRequest, block_id: BlockId, + state_overrides: Option, ) -> Result; // logs async fn get_logs(&self, filter: &Filter) -> Result>; diff --git a/core/src/client/node.rs b/core/src/client/node.rs index 6036b2a1..4523ea67 100644 --- a/core/src/client/node.rs +++ b/core/src/client/node.rs @@ -6,8 +6,8 @@ use alloy::eips::{BlockId, BlockNumberOrTag}; use alloy::network::BlockResponse; use alloy::primitives::{Address, Bytes, B256, U256}; use alloy::rpc::types::{ - AccessListItem, AccessListResult, EIP1186AccountProofResponse, EIP1186StorageProof, Filter, - Log, SyncInfo, SyncStatus, + state::StateOverride, AccessListItem, AccessListResult, EIP1186AccountProofResponse, + EIP1186StorageProof, Filter, Log, SyncInfo, SyncStatus, }; use async_trait::async_trait; use eyre::{eyre, Result}; @@ -172,7 +172,12 @@ impl, E: ExecutionProvider> He self.consensus.wait_synced().await } - async fn call(&self, tx: &N::TransactionRequest, block_id: BlockId) -> Result { + async fn call( + &self, + tx: &N::TransactionRequest, + block_id: BlockId, + state_overrides: Option, + ) -> Result { self.check_blocktag_age(&block_id).await?; let (result, ..) = N::transact( tx, @@ -181,6 +186,7 @@ impl, E: ExecutionProvider> He self.get_chain_id().await, self.fork_schedule, block_id, + state_overrides, ) .await?; @@ -195,7 +201,12 @@ impl, E: ExecutionProvider> He Ok(res) } - async fn estimate_gas(&self, tx: &N::TransactionRequest, block_id: BlockId) -> Result { + async fn estimate_gas( + &self, + tx: &N::TransactionRequest, + block_id: BlockId, + state_overrides: Option, + ) -> Result { self.check_blocktag_age(&block_id).await?; let (result, ..) = N::transact( @@ -205,6 +216,7 @@ impl, E: ExecutionProvider> He self.get_chain_id().await, self.fork_schedule, block_id, + state_overrides, ) .await?; @@ -215,6 +227,7 @@ impl, E: ExecutionProvider> He &self, tx: &N::TransactionRequest, block: BlockId, + state_overrides: Option, ) -> Result { self.check_blocktag_age(&block).await?; @@ -225,6 +238,7 @@ impl, E: ExecutionProvider> He self.get_chain_id().await, self.fork_schedule, block, + state_overrides, ) .await?; diff --git a/core/src/jsonrpc/mod.rs b/core/src/jsonrpc/mod.rs index ba161c4f..33038b49 100644 --- a/core/src/jsonrpc/mod.rs +++ b/core/src/jsonrpc/mod.rs @@ -7,7 +7,8 @@ use alloy::network::{BlockResponse, ReceiptResponse, TransactionResponse}; use alloy::primitives::{Address, Bytes, B256, U256, U64}; use alloy::rpc::json_rpc::RpcObject; use alloy::rpc::types::{ - AccessListResult, EIP1186AccountProofResponse, Filter, FilterChanges, Log, SyncStatus, + state::StateOverride, AccessListResult, EIP1186AccountProofResponse, Filter, FilterChanges, + Log, SyncStatus, }; use eyre::{eyre, Result}; use jsonrpsee::{ @@ -84,14 +85,25 @@ trait EthRpc< #[method(name = "getCode")] async fn get_code(&self, address: Address, block: BlockId) -> Result; #[method(name = "call")] - async fn call(&self, tx: TXR, block: BlockId) -> Result; + async fn call( + &self, + tx: TXR, + block: BlockId, + state_overrides: Option, + ) -> Result; #[method(name = "estimateGas")] - async fn estimate_gas(&self, tx: TXR, block: BlockId) -> Result; + async fn estimate_gas( + &self, + tx: TXR, + block: BlockId, + state_overrides: Option, + ) -> Result; #[method(name = "createAccessList")] async fn create_access_list( &self, tx: TXR, block: BlockId, + state_overrides: Option, ) -> Result; #[method(name = "chainId")] async fn chain_id(&self) -> Result; @@ -238,16 +250,22 @@ impl &self, tx: N::TransactionRequest, block: BlockId, + state_overrides: Option, ) -> Result { - convert_err(self.client.call(&tx, block).await) + convert_err(self.client.call(&tx, block, state_overrides).await) } async fn estimate_gas( &self, tx: N::TransactionRequest, block: BlockId, + state_overrides: Option, ) -> Result { - let res = self.client.estimate_gas(&tx, block).await.map(U64::from); + let res = self + .client + .estimate_gas(&tx, block, state_overrides) + .await + .map(U64::from); convert_err(res) } @@ -256,8 +274,13 @@ impl &self, tx: N::TransactionRequest, block: BlockId, + state_overrides: Option, ) -> Result { - convert_err(self.client.create_access_list(&tx, block).await) + convert_err( + self.client + .create_access_list(&tx, block, state_overrides) + .await, + ) } async fn chain_id(&self) -> Result { diff --git a/ethereum/src/evm.rs b/ethereum/src/evm.rs index 03082159..b8f025fd 100644 --- a/ethereum/src/evm.rs +++ b/ethereum/src/evm.rs @@ -4,7 +4,7 @@ use alloy::{ consensus::{BlockHeader, TxType}, eips::BlockId, network::TransactionBuilder, - rpc::types::{Block, Header, Transaction, TransactionRequest}, + rpc::types::{state::StateOverride, Block, Header, Transaction, TransactionRequest}, }; use eyre::Result; use revm::{ @@ -53,8 +53,9 @@ impl> EthereumEvm { &mut self, tx: &TransactionRequest, validate_tx: bool, + state_overrides: Option, ) -> Result<(ExecutionResult, HashMap), EvmError> { - let mut db = ProofDB::new(self.block_id, self.execution.clone()); + let mut db = ProofDB::new(self.block_id, self.execution.clone(), state_overrides); _ = db.state.prefetch_state(tx, validate_tx).await; // Track iterations for debugging diff --git a/ethereum/src/spec.rs b/ethereum/src/spec.rs index 1001d074..92ca7d3b 100644 --- a/ethereum/src/spec.rs +++ b/ethereum/src/spec.rs @@ -8,7 +8,7 @@ use alloy::{ eips::{BlockId, Encodable2718}, network::{BuildResult, Network, NetworkWallet, TransactionBuilder, TransactionBuilderError}, primitives::{Address, Bytes, ChainId, TxKind, U256}, - rpc::types::{AccessList, Log, TransactionRequest}, + rpc::types::{state::StateOverride, AccessList, Log, TransactionRequest}, }; use async_trait::async_trait; use revm::context::result::{ExecutionResult, HaltReason}; @@ -104,10 +104,11 @@ impl NetworkSpec for Ethereum { chain_id: u64, fork_schedule: ForkSchedule, block_id: BlockId, + state_overrides: Option, ) -> Result<(ExecutionResult, HashMap), EvmError> { let mut evm = EthereumEvm::new(execution, chain_id, fork_schedule, block_id); - evm.transact_inner(tx, validate_tx).await + evm.transact_inner(tx, validate_tx, state_overrides).await } } diff --git a/examples/call.rs b/examples/call.rs index 6f37f2fe..bb03a0d4 100644 --- a/examples/call.rs +++ b/examples/call.rs @@ -65,7 +65,9 @@ async fn main() -> eyre::Result<()> { ..Default::default() }; - let result = client.call(&tx, BlockNumberOrTag::Latest.into()).await?; + let result = client + .call(&tx, BlockNumberOrTag::Latest.into(), None) + .await?; info!("[HELIOS] DAI total supply: {:?}", result); Ok(()) diff --git a/helios-ts/lib.ts b/helios-ts/lib.ts index 79936960..c63471eb 100644 --- a/helios-ts/lib.ts +++ b/helios-ts/lib.ts @@ -211,13 +211,13 @@ export class HeliosProvider { return this.#client.get_proof(req.params[0], req.params[1], req.params[2]); } case "eth_call": { - return this.#client.call(req.params[0], req.params[1]); + return this.#client.call(req.params[0], req.params[1], req.params[2]); } case "eth_estimateGas": { - return this.#client.estimate_gas(req.params[0], req.params[1]); + return this.#client.estimate_gas(req.params[0], req.params[1], req.params[2]); } case "eth_createAccessList": { - return this.#client.create_access_list(req.params[0], req.params[1]); + return this.#client.create_access_list(req.params[0], req.params[1], req.params[2]); } case "eth_gasPrice": { return this.#client.gas_price(); diff --git a/helios-ts/src/ethereum.rs b/helios-ts/src/ethereum.rs index 134f8e2b..6253dbcc 100644 --- a/helios-ts/src/ethereum.rs +++ b/helios-ts/src/ethereum.rs @@ -7,7 +7,7 @@ use std::str::FromStr; use alloy::eips::{BlockId, BlockNumberOrTag}; use alloy::hex::{self, FromHex}; use alloy::primitives::{Address, B256, U256}; -use alloy::rpc::types::{Filter, TransactionRequest}; +use alloy::rpc::types::{state::StateOverride, Filter, TransactionRequest}; use eyre::Result; use url::Url; use wasm_bindgen::prelude::*; @@ -321,18 +321,32 @@ impl EthereumClient { } #[wasm_bindgen] - pub async fn call(&self, opts: JsValue, block: JsValue) -> Result { + pub async fn call( + &self, + opts: JsValue, + block: JsValue, + state_overrides: JsValue, + ) -> Result { let opts: TransactionRequest = serde_wasm_bindgen::from_value(opts)?; let block: BlockId = serde_wasm_bindgen::from_value(block)?; - let res = map_err(self.inner.call(&opts, block).await)?; + let state_overrides: Option = + serde_wasm_bindgen::from_value(state_overrides)?; + let res = map_err(self.inner.call(&opts, block, state_overrides).await)?; Ok(format!("0x{}", hex::encode(res))) } #[wasm_bindgen] - pub async fn estimate_gas(&self, opts: JsValue, block: JsValue) -> Result { + pub async fn estimate_gas( + &self, + opts: JsValue, + block: JsValue, + state_overrides: JsValue, + ) -> Result { let opts: TransactionRequest = serde_wasm_bindgen::from_value(opts)?; let block: BlockId = serde_wasm_bindgen::from_value(block)?; - Ok(map_err(self.inner.estimate_gas(&opts, block).await)? as u32) + let state_overrides: Option = + serde_wasm_bindgen::from_value(state_overrides)?; + Ok(map_err(self.inner.estimate_gas(&opts, block, state_overrides).await)? as u32) } #[wasm_bindgen] @@ -340,10 +354,17 @@ impl EthereumClient { &self, opts: JsValue, block: JsValue, + state_overrides: JsValue, ) -> Result { let opts: TransactionRequest = serde_wasm_bindgen::from_value(opts)?; let block: BlockId = serde_wasm_bindgen::from_value(block)?; - let access_list_result = map_err(self.inner.create_access_list(&opts, block).await)?; + let state_overrides: Option = + serde_wasm_bindgen::from_value(state_overrides)?; + let access_list_result = map_err( + self.inner + .create_access_list(&opts, block, state_overrides) + .await, + )?; Ok(serde_wasm_bindgen::to_value(&access_list_result)?) } diff --git a/helios-ts/src/linea.rs b/helios-ts/src/linea.rs index 3f12d1ca..a8fe0840 100644 --- a/helios-ts/src/linea.rs +++ b/helios-ts/src/linea.rs @@ -7,7 +7,7 @@ use std::str::FromStr; use alloy::eips::{BlockId, BlockNumberOrTag}; use alloy::hex; use alloy::primitives::{Address, B256, U256}; -use alloy::rpc::types::{Filter, TransactionRequest}; +use alloy::rpc::types::{state::StateOverride, Filter, TransactionRequest}; use url::Url; use wasm_bindgen::prelude::*; use web_sys::js_sys::Function; @@ -245,18 +245,32 @@ impl LineaClient { } #[wasm_bindgen] - pub async fn call(&self, opts: JsValue, block: JsValue) -> Result { + pub async fn call( + &self, + opts: JsValue, + block: JsValue, + state_overrides: JsValue, + ) -> Result { let opts: TransactionRequest = serde_wasm_bindgen::from_value(opts)?; let block: BlockId = serde_wasm_bindgen::from_value(block)?; - let res = map_err(self.inner.call(&opts, block).await)?; + let state_overrides: Option = + serde_wasm_bindgen::from_value(state_overrides)?; + let res = map_err(self.inner.call(&opts, block, state_overrides).await)?; Ok(format!("0x{}", hex::encode(res))) } #[wasm_bindgen] - pub async fn estimate_gas(&self, opts: JsValue, block: JsValue) -> Result { + pub async fn estimate_gas( + &self, + opts: JsValue, + block: JsValue, + state_overrides: JsValue, + ) -> Result { let opts: TransactionRequest = serde_wasm_bindgen::from_value(opts)?; let block: BlockId = serde_wasm_bindgen::from_value(block)?; - Ok(map_err(self.inner.estimate_gas(&opts, block).await)? as u32) + let state_overrides: Option = + serde_wasm_bindgen::from_value(state_overrides)?; + Ok(map_err(self.inner.estimate_gas(&opts, block, state_overrides).await)? as u32) } #[wasm_bindgen] @@ -264,10 +278,17 @@ impl LineaClient { &self, opts: JsValue, block: JsValue, + state_overrides: JsValue, ) -> Result { let opts: TransactionRequest = serde_wasm_bindgen::from_value(opts)?; let block: BlockId = serde_wasm_bindgen::from_value(block)?; - let access_list_result = map_err(self.inner.create_access_list(&opts, block).await)?; + let state_overrides: Option = + serde_wasm_bindgen::from_value(state_overrides)?; + let access_list_result = map_err( + self.inner + .create_access_list(&opts, block, state_overrides) + .await, + )?; Ok(serde_wasm_bindgen::to_value(&access_list_result)?) } diff --git a/helios-ts/src/opstack.rs b/helios-ts/src/opstack.rs index f86d9da5..0bcb6227 100644 --- a/helios-ts/src/opstack.rs +++ b/helios-ts/src/opstack.rs @@ -7,7 +7,7 @@ use std::str::FromStr; use alloy::eips::{BlockId, BlockNumberOrTag}; use alloy::hex; use alloy::primitives::{Address, B256, U256}; -use alloy::rpc::types::Filter; +use alloy::rpc::types::{state::StateOverride, Filter}; use url::Url; use wasm_bindgen::prelude::*; use web_sys::js_sys::Function; @@ -262,18 +262,32 @@ impl OpStackClient { } #[wasm_bindgen] - pub async fn call(&self, opts: JsValue, block: JsValue) -> Result { + pub async fn call( + &self, + opts: JsValue, + block: JsValue, + state_overrides: JsValue, + ) -> Result { let opts: OpTransactionRequest = serde_wasm_bindgen::from_value(opts)?; let block: BlockId = serde_wasm_bindgen::from_value(block)?; - let res = map_err(self.inner.call(&opts, block).await)?; + let state_overrides: Option = + serde_wasm_bindgen::from_value(state_overrides)?; + let res = map_err(self.inner.call(&opts, block, state_overrides).await)?; Ok(format!("0x{}", hex::encode(res))) } #[wasm_bindgen] - pub async fn estimate_gas(&self, opts: JsValue, block: JsValue) -> Result { + pub async fn estimate_gas( + &self, + opts: JsValue, + block: JsValue, + state_overrides: JsValue, + ) -> Result { let opts: OpTransactionRequest = serde_wasm_bindgen::from_value(opts)?; let block: BlockId = serde_wasm_bindgen::from_value(block)?; - Ok(map_err(self.inner.estimate_gas(&opts, block).await)? as u32) + let state_overrides: Option = + serde_wasm_bindgen::from_value(state_overrides)?; + Ok(map_err(self.inner.estimate_gas(&opts, block, state_overrides).await)? as u32) } #[wasm_bindgen] @@ -281,10 +295,17 @@ impl OpStackClient { &self, opts: JsValue, block: JsValue, + state_overrides: JsValue, ) -> Result { let opts: OpTransactionRequest = serde_wasm_bindgen::from_value(opts)?; let block: BlockId = serde_wasm_bindgen::from_value(block)?; - let access_list_result = map_err(self.inner.create_access_list(&opts, block).await)?; + let state_overrides: Option = + serde_wasm_bindgen::from_value(state_overrides)?; + let access_list_result = map_err( + self.inner + .create_access_list(&opts, block, state_overrides) + .await, + )?; Ok(serde_wasm_bindgen::to_value(&access_list_result)?) } diff --git a/opstack/src/evm.rs b/opstack/src/evm.rs index 86631b13..5e5f2bfa 100644 --- a/opstack/src/evm.rs +++ b/opstack/src/evm.rs @@ -4,7 +4,7 @@ use alloy::{ consensus::BlockHeader, eips::BlockId, network::TransactionBuilder, - rpc::types::{Block, Header}, + rpc::types::{state::StateOverride, Block, Header}, }; use eyre::Result; use op_alloy_consensus::OpTxType; @@ -57,8 +57,9 @@ impl> OpStackEvm { &mut self, tx: &OpTransactionRequest, validate_tx: bool, + state_overrides: Option, ) -> Result<(ExecutionResult, HashMap), EvmError> { - let mut db = ProofDB::new(self.block_id, self.execution.clone()); + let mut db = ProofDB::new(self.block_id, self.execution.clone(), state_overrides); _ = db.state.prefetch_state(tx, validate_tx).await; // Track iterations for debugging diff --git a/opstack/src/spec.rs b/opstack/src/spec.rs index 70b07fdb..1ebb6088 100644 --- a/opstack/src/spec.rs +++ b/opstack/src/spec.rs @@ -4,7 +4,7 @@ use alloy::{ consensus::{proofs::calculate_transaction_root, Receipt, ReceiptWithBloom, TxReceipt, TxType}, eips::{BlockId, Encodable2718}, primitives::{Address, Bytes, ChainId, TxKind, U256}, - rpc::types::{AccessList, Log, TransactionRequest}, + rpc::types::{state::StateOverride, AccessList, Log, TransactionRequest}, }; use async_trait::async_trait; @@ -133,10 +133,11 @@ impl NetworkSpec for OpStack { chain_id: u64, fork_schedule: ForkSchedule, block_id: BlockId, + state_overrides: Option, ) -> Result<(ExecutionResult, HashMap), EvmError> { let mut evm = OpStackEvm::new(execution, chain_id, fork_schedule, block_id); - evm.transact_inner(tx, validate_tx).await + evm.transact_inner(tx, validate_tx, state_overrides).await } } diff --git a/revm-utils/src/proof_db.rs b/revm-utils/src/proof_db.rs index d32e505e..09acd281 100644 --- a/revm-utils/src/proof_db.rs +++ b/revm-utils/src/proof_db.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, marker::PhantomData, sync::Arc}; use alloy::{ eips::{BlockId, BlockNumberOrTag}, network::{primitives::HeaderResponse, BlockResponse}, + rpc::types::state::{AccountOverride, StateOverride}, }; use eyre::Result; use revm::{ @@ -26,8 +27,12 @@ pub struct ProofDB> { } impl> ProofDB { - pub fn new(block_id: BlockId, execution: Arc) -> Self { - let state = EvmState::new(execution, block_id); + pub fn new( + block_id: BlockId, + execution: Arc, + state_overrides: Option, + ) -> Self { + let state = EvmState::new(execution, block_id, state_overrides); ProofDB { state } } } @@ -45,17 +50,19 @@ pub struct EvmState> { pub block: BlockId, pub access: Option, pub execution: Arc, + pub state_overrides: Option, pub phantom: PhantomData, } impl> EvmState { - pub fn new(execution: Arc, block: BlockId) -> Self { + pub fn new(execution: Arc, block: BlockId, state_overrides: Option) -> Self { Self { execution, block, accounts: HashMap::new(), block_hash: HashMap::new(), access: None, + state_overrides, phantom: PhantomData, } } @@ -111,7 +118,16 @@ impl> EvmState { } pub fn get_basic(&mut self, address: Address) -> Result { - if let Some(account) = self.accounts.get(&address) { + let override_opt = self + .state_overrides + .as_ref() + .and_then(|overrides| overrides.get(&address)); + + if let Some(account) = self.accounts.get_mut(&address) { + if let Some(override_opt) = override_opt { + apply_account_overrides(account, override_opt)?; + } + // Normalize code_hash for REVM compatibility: // RPC response for getProof method for non-existing (unused) EOAs // may contain B256::ZERO for code_hash, but REVM expects KECCAK_EMPTY @@ -127,6 +143,11 @@ impl> EvmState { code_hash, Bytecode::new_raw(account.code.as_ref().unwrap().clone()), )) + } else if override_opt.is_some() { + // It means we have an override but no actual account data to fall through to. + // We return error so that the account is fetched first, overrides will be applied on the next iteration. + self.access = Some(StateAccess::Basic(address)); + Err(DatabaseError::StateMissing) } else { self.access = Some(StateAccess::Basic(address)); Err(DatabaseError::StateMissing) @@ -134,11 +155,25 @@ impl> EvmState { } pub fn get_storage(&mut self, address: Address, slot: U256) -> Result { + let override_opt = self + .state_overrides + .as_ref() + .and_then(|overrides| overrides.get(&address)); + + let slot_b256 = B256::from(slot); + + if let Some(account_override) = override_opt { + if let Some(result) = apply_storage_overrides(&slot_b256, account_override)? { + return Ok(result); + } + } + if let Some(account) = self.accounts.get(&address) { - if let Some(value) = account.get_storage_value(B256::from(slot)) { + if let Some(value) = account.get_storage_value(slot_b256) { return Ok(value); } } + self.access = Some(StateAccess::Storage(address, slot)); Err(DatabaseError::StateMissing) } @@ -206,3 +241,57 @@ impl> Database for ProofDB { fn is_precompile(address: &Address) -> bool { address.le(&address!("0000000000000000000000000000000000000009")) && address.gt(&Address::ZERO) } + +fn apply_account_overrides( + account: &mut Account, + overrides: &AccountOverride, +) -> Result<(), DatabaseError> { + let balance = overrides.balance.unwrap_or(account.account.balance); + + let nonce = overrides.nonce.unwrap_or(account.account.nonce); + + let (code_hash, code) = if let Some(override_code) = overrides.code.as_ref() { + let code = Bytecode::new_raw_checked(override_code.clone()) + .map_err(|e| DatabaseError::InvalidStateOverride(e.to_string()))?; + (code.hash_slow(), code.bytes()) + } else { + ( + account.account.code_hash, + account.code.as_ref().unwrap().clone(), + ) + }; + account.account.balance = balance; + account.account.nonce = nonce; + account.account.code_hash = code_hash; + account.code = Some(code); + Ok(()) +} + +fn apply_storage_overrides( + slot: &B256, + overrides: &AccountOverride, +) -> Result, DatabaseError> { + if overrides.state.is_some() && overrides.state_diff.is_some() { + return Err(DatabaseError::InvalidStateOverride( + "Both 'state' and 'stateDiff' defined for account".to_string(), + )); + } + + if let Some(ref state) = overrides.state { + // Full state replacement - only use override values + let value = state + .get(slot) + .map(|b| U256::from_be_bytes(b.0)) + .unwrap_or(U256::ZERO); + return Ok(Some(value)); + } + + if let Some(ref state_diff) = overrides.state_diff { + if let Some(value_b256) = state_diff.get(slot) { + return Ok(Some(U256::from_be_bytes(value_b256.0))); + } + } + + // No override applies, fall through to regular storage + Ok(None) +} diff --git a/revm-utils/src/types.rs b/revm-utils/src/types.rs index df018364..3ab7d74c 100644 --- a/revm-utils/src/types.rs +++ b/revm-utils/src/types.rs @@ -5,6 +5,8 @@ use thiserror::Error; pub enum DatabaseError { #[error("state missing")] StateMissing, + #[error("invalid state override: {0}")] + InvalidStateOverride(String), #[error("should never be called")] Unimplemented, } diff --git a/tests/rpc_equivalence.rs b/tests/rpc_equivalence.rs index 629ca25c..2fe4b533 100644 --- a/tests/rpc_equivalence.rs +++ b/tests/rpc_equivalence.rs @@ -1,10 +1,12 @@ +use std::collections::HashMap; use std::env; use std::net::{SocketAddr, TcpListener}; use alloy::eips::BlockNumberOrTag; use alloy::network::ReceiptResponse; -use alloy::primitives::{address, B256, U256}; +use alloy::primitives::{address, Bytes, B256, U256}; use alloy::providers::{Provider, RootProvider}; +use alloy::rpc::types::state::{AccountOverride, StateOverride}; use alloy::rpc::types::Filter; use alloy::sol; use alloy::sol_types::SolCall; @@ -55,6 +57,10 @@ use helios_verifiable_api_server::server::{ // // EVM Execution Methods: // - eth_call (call, call_complex_contract) +// - eth_createAccessList (create_access_list) +// +// State Override Tests: +// - Combined overrides (call_with_combined_overrides) // // Log/Event Methods: // - eth_getLogs (get_logs, get_logs_by_address, get_logs_by_topic, get_logs_block_range) @@ -1336,6 +1342,102 @@ async fn test_get_proof_multiple_keys( Ok(()) } +// ========== STATE OVERRIDE TESTS ========== + +async fn test_call_with_combined_overrides( + helios: &RootProvider, + expected: &RootProvider, +) -> Result<()> { + let block_num = helios.get_block_number().await?; + let target_address = address!("0000000000000000000000000000000000000456"); + + // Combine multiple overrides: balance, nonce, code, and storage + let override_balance = U256::from(5_000_000_000_000_000_000u128); + let override_nonce = 42u64; + let override_code = Bytes::from_static(&[ + 0x30, // ADDRESS + 0x31, // BALANCE + 0x60, 0x00, // PUSH1 0x00 + 0x52, // MSTORE + 0x60, 0x00, // PUSH1 0x00 + 0x54, // SLOAD + 0x60, 0x20, // PUSH1 0x20 + 0x52, // MSTORE + 0x60, 0x40, // PUSH1 0x40 + 0x60, 0x00, // PUSH1 0x00 + 0xf3, // RETURN + ]); + + let slot_0 = B256::ZERO; + let storage_value = B256::from(U256::from(0x12345678u64)); + + let mut storage_diff = HashMap::default(); + storage_diff.insert(slot_0, storage_value); + + let mut state_override = StateOverride::default(); + state_override.insert( + target_address, + AccountOverride { + balance: Some(override_balance), + nonce: Some(override_nonce), + code: Some(override_code), + state: None, + state_diff: Some(storage_diff), + move_precompile_to: None, + }, + ); + + // Test that eth_call works with combined overrides + let call_request = json!({ + "to": format!("{:#x}", target_address), + "data": "0x" + }); + + let helios_result: String = helios + .raw_request( + "eth_call".into(), + vec![ + call_request.clone(), + json!(format!("{:#x}", block_num)), + serde_json::to_value(&state_override)?, + ], + ) + .await?; + + let expected_result: String = expected + .raw_request( + "eth_call".into(), + vec![ + call_request, + json!(format!("{:#x}", block_num)), + serde_json::to_value(&state_override)?, + ], + ) + .await?; + + ensure_eq!( + helios_result, + expected_result, + "Combined override result mismatch: expected {:?}, got {:?}", + expected_result, + helios_result + ); + + // Both should return the overridden storage value + ensure!( + helios_result.contains("12345678"), + "Combined overrides should return storage value 0x12345678" + ); + + // Both should return the overridden balance value (5 ETH = 0x4563918244F40000) + ensure!( + helios_result.to_lowercase().contains("4563918244f40000"), + "Combined overrides should return balance value 5 ETH" + ); + + Ok(()) +} + // ========== MAIN TEST RUNNER ========== #[tokio::test(flavor = "multi_thread")] @@ -1377,7 +1479,7 @@ async fn rpc_equivalence_tests() { }}; } - let test_count = 33; // Update count as we add tests + let test_count = 36; // Update count as we add tests println!( "Setup complete! Running {} mini-tests in parallel...", test_count @@ -1454,6 +1556,11 @@ async fn rpc_equivalence_tests() { // Historical Data spawn_test!(test_get_historical_block, "get_historical_block"), spawn_test!(test_get_too_old_block, "get_too_old_block"), + // State Override Tests + spawn_test!( + test_call_with_combined_overrides, + "call_with_combined_overrides" + ), ]; // Collect results diff --git a/verifiable-api/server/src/service.rs b/verifiable-api/server/src/service.rs index fa093a12..e0fa2866 100644 --- a/verifiable-api/server/src/service.rs +++ b/verifiable-api/server/src/service.rs @@ -242,6 +242,7 @@ impl VerifiableApi for ApiService { self.rpc.get_chain_id().await?, ForkSchedule::default(), block_id, + None, // state overrides not supported for execution hints ) .await?;