From 67a41038c35cb07a39acdab50f3e2a376dcf4256 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 27 Jan 2026 12:21:23 -0300 Subject: [PATCH 01/22] Fix everything to make it work with real assethub westend --- crates/anvil-polkadot/Cargo.toml | 3 +- .../anvil-polkadot/src/api_server/server.rs | 48 +++++- .../lazy_loading/backend/blockchain.rs | 81 ++++++--- .../backend/forked_lazy_backend.rs | 61 ++++++- .../lazy_loading/backend/mod.rs | 20 ++- .../lazy_loading/backend/tests.rs | 18 +- .../substrate_node/lazy_loading/rpc_client.rs | 49 ++++++ .../src/substrate_node/service/backend.rs | 30 ++++ crates/anvil-polkadot/tests/it/forking.rs | 162 ++++++++++++++---- crates/anvil-polkadot/tests/it/main.rs | 1 - crates/anvil-polkadot/tests/it/utils.rs | 54 +++++- 11 files changed, 452 insertions(+), 75 deletions(-) diff --git a/crates/anvil-polkadot/Cargo.toml b/crates/anvil-polkadot/Cargo.toml index d4ca0c2367d77..1b11383dc78c4 100644 --- a/crates/anvil-polkadot/Cargo.toml +++ b/crates/anvil-polkadot/Cargo.toml @@ -163,5 +163,4 @@ op-alloy-rpc-types.workspace = true [features] default = [] -asm-keccak = ["alloy-primitives/asm-keccak"] -forking-tests = [] \ No newline at end of file +asm-keccak = ["alloy-primitives/asm-keccak"] \ No newline at end of file diff --git a/crates/anvil-polkadot/src/api_server/server.rs b/crates/anvil-polkadot/src/api_server/server.rs index d9e9b79267c2a..d5e4daaaab4d4 100644 --- a/crates/anvil-polkadot/src/api_server/server.rs +++ b/crates/anvil-polkadot/src/api_server/server.rs @@ -87,9 +87,13 @@ use sqlx::sqlite::SqlitePoolOptions; use std::{collections::BTreeSet, sync::Arc, time::Duration}; use substrate_runtime::{Balance, constants::NATIVE_TO_ETH_RATIO}; use subxt::{ - Metadata as SubxtMetadata, OnlineClient, backend::rpc::RpcClient, - client::RuntimeVersion as SubxtRuntimeVersion, config::substrate::H256, - ext::subxt_rpcs::LegacyRpcMethods, utils::H160, + Metadata as SubxtMetadata, OnlineClient, + backend::rpc::RpcClient, + client::RuntimeVersion as SubxtRuntimeVersion, + config::substrate::H256, + dynamic::{Value as DynamicValue, tx as dynamic_tx}, + ext::subxt_rpcs::LegacyRpcMethods, + utils::H160, }; use subxt_signer::eth::Keypair; use tokio::try_join; @@ -113,6 +117,12 @@ pub struct ApiServer { /// Tracks all active filters filters: Filters, hardcoded_chain_id: u64, + /// RPC methods for submitting transactions + rpc: LegacyRpcMethods, + /// Subxt OnlineClient for dynamic transaction building. + /// When forking, the metadata comes from the forked chain's WASM (loaded via lazy loading), + /// so pallet indices will be correct for the forked runtime. + api: OnlineClient, } /// Fetch the chain ID from the substrate chain. @@ -140,7 +150,7 @@ impl ApiServer { let eth_rpc_client = create_revive_rpc_client( api.clone(), rpc_client.clone(), - rpc, + rpc.clone(), block_provider.clone(), substrate_service.spawn_handle.clone(), revive_rpc_block_limit, @@ -176,6 +186,8 @@ impl ApiServer { instance_id: B256::random(), filters, hardcoded_chain_id: chain_id, + rpc, + api, }) } @@ -797,8 +809,32 @@ impl ApiServer { async fn send_raw_transaction(&self, transaction: Bytes) -> Result { let hash = H256(keccak_256(&transaction.0)); - let call = subxt_client::tx().revive().eth_transact(transaction.0); - self.eth_rpc_client.submit(call).await?; + + // Prefetch storage keys for the sender to speed up transaction validation. + // This is especially important when forking from a remote chain, as each storage + // read would otherwise require a separate RPC call. When not forking, this is a no-op. + if let Ok(signed_tx) = TransactionSigned::decode(&transaction.0) { + if let Ok(sender) = recover_maybe_impersonated_address(&signed_tx) { + self.backend.prefetch_eth_transaction_keys(sender); + } + } + + // Use dynamic transaction building to ensure the correct pallet index is used. + // The metadata in self.api comes from the runtime's WASM (via runtime API call), + // which is the forked chain's WASM when forking. This ensures correct pallet indices. + let payload_value = DynamicValue::from_bytes(transaction.0.clone()); + let tx_payload = dynamic_tx("Revive", "eth_transact", vec![payload_value]); + + let ext = self.api.tx().create_unsigned(&tx_payload).map_err(|e| { + Error::InternalError(format!("Failed to create unsigned extrinsic: {e}")) + })?; + + // Submit the extrinsic to the transaction pool + self.rpc + .author_submit_extrinsic(ext.encoded()) + .await + .map_err(|e| Error::InternalError(format!("Failed to submit transaction: {e}")))?; + Ok(hash) } diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs index fd11c8b27e413..65139b3b5826f 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs @@ -156,10 +156,10 @@ impl Blockchain { storage.genesis_hash = hash; } - // Update leaves for non-genesis blocks - if storage.blocks.len() > 1 { - storage.leaves.import(hash, number, *header.parent_hash()); - } + // Update leaves for all blocks including genesis. + // For genesis when forking, the parent_hash points to the previous block on the remote chain. + // That parent won't be in our leaf set, so this effectively adds genesis as a new leaf. + storage.leaves.import(hash, number, *header.parent_hash()); // Finalize block only if explicitly requested via new_state if let NewBlockState::Final = new_state { @@ -266,25 +266,43 @@ impl HeaderBackend for Blockchain { + let block = full.block.clone(); + self.storage + .write() + .blocks + .insert(hash, StoredBlock::Full(block.clone(), full.justifications)); + Some(block.header().clone()) + } + Ok(None) => { + // Block not found on remote chain - this is expected for locally-built blocks + tracing::debug!( + target: LAZY_LOADING_LOG_TARGET, + "Block {:?} not found in local storage or remote RPC", + hash + ); + None + } + Err(e) => { + tracing::warn!( + target: LAZY_LOADING_LOG_TARGET, + "Failed to fetch block {:?} from RPC: {}", + hash, + e + ); + None + } + } } else { - None - }; - - if header.is_none() { - tracing::warn!( + // No RPC configured - block simply doesn't exist locally + tracing::debug!( target: LAZY_LOADING_LOG_TARGET, - "Expected block {:x?} to exist.", - &hash + "Block {:?} not found in local storage (no RPC configured)", + hash ); - } + None + }; Ok(header) } @@ -418,19 +436,34 @@ impl sp_blockchain::Backend for Blockch Ok(leaves) } - fn children(&self, _parent_hash: Block::Hash) -> sp_blockchain::Result> { - unimplemented!("Not supported by the `lazy-loading` backend.") + fn children(&self, parent_hash: Block::Hash) -> sp_blockchain::Result> { + // Find all blocks whose parent_hash matches the given hash + let storage = self.storage.read(); + let children: Vec = storage + .blocks + .iter() + .filter_map(|(hash, block)| { + if *block.header().parent_hash() == parent_hash { + Some(*hash) + } else { + None + } + }) + .collect(); + Ok(children) } fn indexed_transaction(&self, _hash: Block::Hash) -> sp_blockchain::Result>> { - unimplemented!("Not supported by the `lazy-loading` backend.") + // Indexed transactions are not supported in the lazy-loading backend + Ok(None) } fn block_indexed_body( &self, _hash: Block::Hash, ) -> sp_blockchain::Result>>> { - unimplemented!("Not supported by the `lazy-loading` backend.") + // Indexed block bodies are not supported in the lazy-loading backend + Ok(None) } } diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/forked_lazy_backend.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/forked_lazy_backend.rs index e0c5d19dc425c..d26ca6a743c2b 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/forked_lazy_backend.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/forked_lazy_backend.rs @@ -9,8 +9,8 @@ use polkadot_sdk::{ traits::{Block as BlockT, HashingFor}, }, sp_state_machine::{ - self, BackendTransaction, InMemoryBackend, IterArgs, StorageCollection, StorageValue, - TrieBackend, backend::AsTrieBackend, + self, Backend as StateMachineBackend, BackendTransaction, InMemoryBackend, IterArgs, + StorageCollection, StorageValue, TrieBackend, backend::AsTrieBackend, }, sp_storage::ChildInfo, sp_trie::{self, PrefixedMemoryDB}, @@ -99,6 +99,63 @@ impl ForkedLazyBackend { pub(crate) fn rpc(&self) -> Option<&dyn RPCClient> { self.rpc_client.as_deref() } + + /// Prefetch multiple storage keys in a single RPC batch call. + /// This significantly reduces latency when we know which keys will be needed. + /// Keys that are already cached or marked as removed will be skipped. + /// Returns the number of keys actually fetched from remote. + pub fn prefetch_keys(&self, keys: &[Vec]) -> usize { + if keys.is_empty() { + return 0; + } + + let rpc = match self.rpc() { + Some(rpc) => rpc, + None => return 0, + }; + + // Filter out keys that are already cached or removed + let db = self.db.read(); + let removed = self.removed_keys.read(); + + let keys_to_fetch: Vec = keys + .iter() + .filter(|key| { + // Skip if already in cache + if StateMachineBackend::storage(&*db, key).ok().flatten().is_some() { + return false; + } + // Skip if marked as removed + if removed.contains(*key) { + return false; + } + true + }) + .map(|key| polkadot_sdk::sp_storage::StorageKey(key.clone())) + .collect(); + + drop(db); + drop(removed); + + if keys_to_fetch.is_empty() { + return 0; + } + + let fetch_count = keys_to_fetch.len(); + + // Use the batch RPC call + let block_to_query = if self.before_fork { self.block_hash } else { self.fork_block }; + + match rpc.storage_batch(keys_to_fetch, block_to_query) { + Ok(results) => { + for (key, value) in results { + self.update_storage(&key.0, &value.map(|v| v.0)); + } + fetch_count + } + Err(_) => 0, + } + } } impl sp_state_machine::Backend> diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/mod.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/mod.rs index 92a7f5923c661..bee53a781072b 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/mod.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/mod.rs @@ -12,8 +12,8 @@ mod tests; use parking_lot::RwLock; use polkadot_sdk::{ sc_client_api::{ - TrieCacheContext, UsageInfo, - backend::{self, AuxStore}, + HeaderBackend, TrieCacheContext, UsageInfo, + backend::{self, AuxStore, Backend as ClientBackend}, }, sp_blockchain, sp_core::{H256, offchain::storage::InMemOffchainStorage}, @@ -59,6 +59,22 @@ impl Backend { fn fork_checkpoint(&self) -> Option<&Block::Header> { self.fork_config.as_ref().map(|(_, checkpoint)| checkpoint) } + + /// Prefetch multiple storage keys in a single batch RPC call. + /// This significantly reduces latency when we know which keys will be needed + /// (e.g., before transaction validation). + /// Returns the number of keys actually fetched from remote. + pub fn prefetch_storage_keys(&self, keys: &[Vec]) -> usize { + // Get the best block hash to find the current state + let best_hash = HeaderBackend::info(&self.blockchain).best_hash; + + // Try to get the state for the best block + if let Ok(state) = ClientBackend::state_at(self, best_hash, TrieCacheContext::Trusted) { + state.prefetch_keys(keys) + } else { + 0 + } + } } impl AuxStore for Backend { diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/tests.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/tests.rs index 9267f159f7c33..d52e4be2b252e 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/tests.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/tests.rs @@ -2,7 +2,7 @@ use super::*; use mock_rpc::{Rpc, TestBlock, TestHeader}; use parking_lot::RwLock; use polkadot_sdk::{ - sc_client_api::{Backend as BackendT, HeaderBackend, StateBackend}, + sc_client_api::{Backend as BackendT, StateBackend}, sp_runtime::{ OpaqueExtrinsic, traits::{BlakeTwo256, Header as HeaderT}, @@ -276,6 +276,22 @@ mod mock_rpc { let take = min(filtered.len(), count as usize); Ok(filtered.into_iter().take(take).map(|k| k.0).collect()) } + + fn storage_batch( + &self, + keys: Vec, + at: Option, + ) -> Result)>, jsonrpsee::core::ClientError> { + // Simple implementation: just call storage for each key + let results = keys + .into_iter() + .map(|key| { + let value = self.storage(key.clone(), at).ok().flatten(); + (key, value) + }) + .collect(); + Ok(results) + } } } diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs index aaebbd5e92aa6..c07fd0fce6187 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs @@ -84,6 +84,14 @@ pub trait RPCClient: Send + Sync + std::fmt::D start_key: Option, at: Option, ) -> Result, ClientError>; + + /// Fetch multiple storage keys in a single RPC call using state_queryStorageAt. + /// Returns a vector of (key, optional_value) pairs. + fn storage_batch( + &self, + keys: Vec, + at: Option, + ) -> Result)>, ClientError>; } #[derive(Debug, Clone)] @@ -322,6 +330,47 @@ impl RPCClient for Rpc { Err(err) => Err(err), } } + + fn storage_batch( + &self, + keys: Vec, + at: Option, + ) -> Result)>, ClientError> { + if keys.is_empty() { + return Ok(vec![]); + } + + let client = self.http_client.clone(); + let keys_clone = keys.clone(); + + let result = self.block_on(async move { + substrate_rpc_client::StateApi::::query_storage_at(&client, keys_clone, at) + .await + })?; + + // query_storage_at returns Vec> + // Each StorageChangeSet contains changes: Vec<(StorageKey, Option)> + // For a single block query, we get one StorageChangeSet with all results + let mut results: Vec<(StorageKey, Option)> = Vec::with_capacity(keys.len()); + + // Build a map from returned results + let mut result_map: std::collections::HashMap, Option> = + std::collections::HashMap::new(); + + for change_set in result { + for (key, value) in change_set.changes { + result_map.insert(key.0, value); + } + } + + // Return results in the same order as input keys + for key in keys { + let value = result_map.get(&key.0).cloned().flatten(); + results.push((key, value)); + } + + Ok(results) + } } #[cfg(test)] diff --git a/crates/anvil-polkadot/src/substrate_node/service/backend.rs b/crates/anvil-polkadot/src/substrate_node/service/backend.rs index c99fb6a7c57d0..386af909718ba 100644 --- a/crates/anvil-polkadot/src/substrate_node/service/backend.rs +++ b/crates/anvil-polkadot/src/substrate_node/service/backend.rs @@ -289,6 +289,36 @@ impl BackendWithOverlay { .storage(key.as_slice()) .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?) } + + /// Prefetch multiple storage keys in a single batch RPC call. + /// This significantly reduces latency when forking from a remote chain. + /// Returns the number of keys actually fetched from remote. + pub fn prefetch_storage_keys(&self, keys: &[Vec]) -> usize { + self.backend.prefetch_storage_keys(keys) + } + + /// Prefetch storage keys needed for validating an Ethereum transaction. + /// This includes account info for the sender address. + pub fn prefetch_eth_transaction_keys(&self, sender: H160) { + // Build list of keys to prefetch + let mut keys = Vec::new(); + + // 1. Sender's Revive account info (AccountInfoOf) + keys.push(well_known_keys::revive_account_info(sender)); + + // 2. Sender's System account info (for balance/nonce if mapping exists) + // The AccountId is derived from H160 in pallet-revive + let sender_account_id: AccountId = { + // pallet-revive uses AccountId32 derived from H160 by padding with zeros + let mut account_bytes = [0u8; 32]; + account_bytes[..20].copy_from_slice(sender.as_bytes()); + account_bytes.into() + }; + keys.push(well_known_keys::system_account_info(sender_account_id)); + + // Prefetch all keys in a single batch + self.prefetch_storage_keys(&keys); + } } pub type Storage = HashMap>; diff --git a/crates/anvil-polkadot/tests/it/forking.rs b/crates/anvil-polkadot/tests/it/forking.rs index d8bd824fa325a..f16a843e91819 100644 --- a/crates/anvil-polkadot/tests/it/forking.rs +++ b/crates/anvil-polkadot/tests/it/forking.rs @@ -14,11 +14,9 @@ use anvil_polkadot::{ }; use polkadot_sdk::pallet_revive::evm::Account; -/// Westend Asset Hub zombienet local URL for forking tests -/// This URL should point to a running zombienet instance, -/// so running the tests depending on it should be preceded -/// by setting up a zombienet network with a running RPC. -const WESTEND_ASSET_HUB_URL: &str = "http://127.0.0.1:63982"; +/// Westend Asset Hub real RPC URL for forking tests +/// This endpoint provides both Substrate RPC and ETH RPC methods +const WESTEND_ASSET_HUB_URL: &str = "https://westend-asset-hub-rpc.polkadot.io"; /// Tests that forking preserves state from the source chain and allows local modifications #[tokio::test(flavor = "multi_thread")] @@ -603,11 +601,14 @@ async fn test_fork_with_contract_deployment() { // These tests require a running zombienet instance at WESTEND_ASSET_HUB_URL // ============================================================================= -/// Helper to create a fork config pointing to Westend Asset Hub zombienet +/// Helper to create a fork config pointing to Westend Asset Hub fn westend_fork_config() -> AnvilNodeConfig { AnvilNodeConfig::test_config() .with_port(0) .with_eth_rpc_url(Some(WESTEND_ASSET_HUB_URL.to_string())) + .with_auto_impersonate(true) + .with_tracing(true) + .set_silent(false) } /// Tests that we can fork from Westend Asset Hub and get balance of addresses @@ -732,8 +733,11 @@ async fn test_fork_eth_get_nonce_from_westend() { .from(alith_address) .to(Address::from(baltathar_address)); - fork_node.send_transaction(transaction).await.unwrap(); - unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + // Send and wait for transaction to be included + fork_node + .send_transaction_and_wait(transaction, 120) + .await + .expect("Transaction receipt not found within timeout"); // Nonce should have increased by 1 let nonce_after_tx = fork_node.get_nonce(alith_address).await; @@ -798,8 +802,16 @@ async fn test_fork_state_snapshotting_from_westend() { .from(alith_address) .to(baltathar_address); - fork_node.send_transaction(transaction).await.unwrap(); - unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + // Send and wait for transaction to be included + let receipt = fork_node + .send_transaction_and_wait(transaction, 120) + .await + .expect("Transaction receipt not found within timeout"); + assert_eq!( + receipt.status, + Some(polkadot_sdk::pallet_revive::U256::from(1)), + "Transaction should succeed" + ); // Verify state changed let alith_balance_after = fork_node.get_balance(alith.address(), None).await; @@ -824,6 +836,9 @@ async fn test_fork_state_snapshotting_from_westend() { .unwrap(); assert!(reverted, "Revert should succeed"); + // Wait for state to settle + tokio::time::sleep(Duration::from_millis(500)).await; + // Verify state is back to snapshot point let alith_balance_reverted = fork_node.get_balance(alith.address(), None).await; let baltathar_balance_reverted = fork_node.get_balance(baltathar.address(), None).await; @@ -847,8 +862,6 @@ async fn test_fork_can_send_tx_from_westend() { let fork_substrate_config = SubstrateNodeConfig::new(&fork_config); let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap(); - let initial_block = fork_node.best_block_number().await; - let alith = Account::from(subxt_signer::eth::dev::alith()); let baltathar = Account::from(subxt_signer::eth::dev::baltathar()); let alith_address = Address::from(ReviveAddress::new(alith.address())); @@ -882,13 +895,11 @@ async fn test_fork_can_send_tx_from_westend() { .from(alith_address) .to(baltathar_address); - let tx_hash = fork_node.send_transaction(transaction).await.unwrap(); - - // Mine the transaction - unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); - - // Get receipt and verify transaction succeeded - let receipt = fork_node.get_transaction_receipt(tx_hash).await; + // Send and wait for transaction to be included + let receipt = fork_node + .send_transaction_and_wait(transaction, 120) + .await + .expect("Transaction receipt not found within timeout"); assert_eq!( receipt.status, Some(polkadot_sdk::pallet_revive::U256::from(1)), @@ -916,23 +927,109 @@ async fn test_fork_can_send_tx_from_westend() { .from(baltathar_address) .to(alith_address); - let tx_hash2 = fork_node.send_transaction(transaction2).await.unwrap(); - unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); - - let receipt2 = fork_node.get_transaction_receipt(tx_hash2).await; + // Send and wait for second transaction + let receipt2 = fork_node + .send_transaction_and_wait(transaction2, 120) + .await + .expect("Second transaction receipt not found within timeout"); assert_eq!( receipt2.status, Some(polkadot_sdk::pallet_revive::U256::from(1)), "Second transaction should succeed" ); - // Verify block number increased (2 transactions = 2 blocks mined) - let final_block = fork_node.best_block_number().await; + // Verify block number increased + let _final_block = fork_node.best_block_number().await; +} + +/// Tests that local state changes don't affect the remote fork state +#[tokio::test(flavor = "multi_thread")] +async fn test_fork_separate_states_from_westend() { + let fork_config = westend_fork_config(); + let fork_substrate_config = SubstrateNodeConfig::new(&fork_config); + let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap(); + + let random_addr = Address::random(); + + // Get initial balance (should be 0 for random address) + let initial_balance = + fork_node.get_balance(subxt::utils::H160::from(random_addr.0.0), None).await; + assert_eq!(initial_balance, U256::ZERO, "Random address should have zero balance initially"); + + // Set a new balance locally + let new_balance = U256::from(1337u64); + unwrap_response::<()>( + fork_node.eth_rpc(EthRequest::SetBalance(random_addr, new_balance)).await.unwrap(), + ) + .unwrap(); + + // Verify local balance changed + let local_balance = + fork_node.get_balance(subxt::utils::H160::from(random_addr.0.0), None).await; + assert_eq!(local_balance, new_balance, "Local balance should be updated"); + + // The remote state should not be affected (we can't directly check this, + // but we verify that local changes work independently) +} + +/// Tests deploying a contract on a forked chain +#[tokio::test(flavor = "multi_thread")] +async fn test_fork_can_deploy_contract_from_westend() { + let fork_config = westend_fork_config(); + let fork_substrate_config = SubstrateNodeConfig::new(&fork_config); + let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap(); + + let alith = Account::from(subxt_signer::eth::dev::alith()); + let alith_address = Address::from(ReviveAddress::new(alith.address())); + + // Set balance for deployment + let initial_balance = U256::from(100_000_000_000_000_000_000u128); // 100 ether + unwrap_response::<()>( + fork_node.eth_rpc(EthRequest::SetBalance(alith_address, initial_balance)).await.unwrap(), + ) + .unwrap(); + + // Deploy SimpleStorage contract and wait for receipt + let contract_code = get_contract_code("SimpleStorage"); + let receipt = fork_node + .deploy_contract_and_wait(&contract_code.init, alith.address(), 120) + .await + .expect("Contract deployment receipt not found within timeout"); + assert_eq!( + receipt.status, + Some(polkadot_sdk::pallet_revive::U256::from(1)), + "Contract deployment should succeed" + ); + + let contract_address = receipt.contract_address.expect("Contract address should exist"); + + // Verify contract has code + let code = unwrap_response::( + fork_node + .eth_rpc(EthRequest::EthGetCodeAt( + Address::from(ReviveAddress::new(contract_address)), + None, + )) + .await + .unwrap(), + ) + .unwrap(); + assert!(!code.is_empty(), "Deployed contract should have code"); + + // Deploy another contract to verify chain continues working + let receipt2 = fork_node + .deploy_contract_and_wait(&contract_code.init, alith.address(), 120) + .await + .expect("Second contract deployment receipt not found within timeout"); assert_eq!( - final_block, - initial_block + 2, - "Block number should increase by 2 after two mined transactions" + receipt2.status, + Some(polkadot_sdk::pallet_revive::U256::from(1)), + "Second contract deployment should succeed" ); + + let contract_address2 = + receipt2.contract_address.expect("Second contract address should exist"); + assert_ne!(contract_address, contract_address2, "Contract addresses should be different"); } /// Tests impersonating an account on a forked chain @@ -966,10 +1063,11 @@ async fn test_fork_impersonate_account_from_westend() { .from(impersonated_addr) .to(recipient_addr); - let tx_hash = fork_node.send_transaction(transaction).await.unwrap(); - unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); - - let receipt = fork_node.get_transaction_receipt(tx_hash).await; + // Send and wait for transaction to be included + let receipt = fork_node + .send_transaction_and_wait(transaction, 120) + .await + .expect("Impersonated transaction receipt not found within timeout"); assert_eq!( receipt.status, Some(polkadot_sdk::pallet_revive::U256::from(1)), diff --git a/crates/anvil-polkadot/tests/it/main.rs b/crates/anvil-polkadot/tests/it/main.rs index dbc0fbf290c9e..cb5d9793dfcb5 100644 --- a/crates/anvil-polkadot/tests/it/main.rs +++ b/crates/anvil-polkadot/tests/it/main.rs @@ -1,6 +1,5 @@ mod abi; mod filters; -#[cfg(feature = "forking-tests")] mod forking; mod gas; mod genesis; diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index 332a2169e0a3f..fd864a28b09d7 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -47,9 +47,7 @@ use std::{fmt::Debug, time::Duration}; use subxt::utils::H160; use tempfile::TempDir; -use crate::abi::Multicall; -#[cfg(feature = "forking-tests")] -use crate::abi::SimpleStorage; +use crate::abi::{Multicall, SimpleStorage}; pub struct BlockWaitTimeout { pub block_number: u32, @@ -149,6 +147,39 @@ impl TestNode { self.send_transaction_inner(transaction, None, false).await } + /// Execute an ethereum transaction and wait for its receipt. + /// This is useful for forking tests where transaction validation can take time + /// due to lazy loading of state from the remote chain. + pub async fn send_transaction_and_wait( + &mut self, + transaction: TransactionRequest, + timeout_secs: u64, + ) -> Result { + let tx_hash = self.send_transaction(transaction).await?; + + let start = std::time::Instant::now(); + let timeout = Duration::from_secs(timeout_secs); + + while start.elapsed() < timeout { + // Check if receipt is available + let receipt_result = self + .eth_rpc(EthRequest::EthGetTransactionReceipt(B256::from(tx_hash.to_fixed_bytes()))) + .await; + + if let Ok(ResponseResult::Success(val)) = receipt_result { + if !val.is_null() { + return Ok(self.get_transaction_receipt(tx_hash).await); + } + } + + // Mine a block and wait + let _ = self.eth_rpc(EthRequest::Mine(None, None)).await; + tokio::time::sleep(Duration::from_secs(5)).await; + } + + Err(RpcError::new(ErrorCode::InternalError)) + } + /// Execute an impersonated ethereum transaction. pub async fn send_unsigned_transaction( &mut self, @@ -228,7 +259,6 @@ impl TestNode { self.eth_best_block().await.number.as_u32() } - #[cfg(feature = "forking-tests")] pub fn substrate_rpc_port(&self) -> u16 { self.service .rpc_handlers @@ -360,6 +390,21 @@ impl TestNode { self.send_transaction(deploy_contract_tx).await.unwrap() } + /// Deploy a contract and wait for its receipt. + /// This is useful for forking tests where transaction validation can take time + /// due to lazy loading of state from the remote chain. + pub async fn deploy_contract_and_wait( + &mut self, + code: &[u8], + deployer: H160, + timeout_secs: u64, + ) -> Result { + let deploy_contract_tx = TransactionRequest::default() + .from(Address::from(ReviveAddress::new(deployer))) + .input(TransactionInput::both(Bytes::copy_from_slice(code))); + self.send_transaction_and_wait(deploy_contract_tx, timeout_secs).await + } + pub async fn get_storage_at(&mut self, storage_key: U256, contract_address: H160) -> U256 { let result = self .eth_rpc(EthRequest::EthGetStorageAt( @@ -540,7 +585,6 @@ pub fn to_hex_string(value: u64) -> String { } /// Helper function to call getValue() on a SimpleStorage contract -#[cfg(feature = "forking-tests")] pub async fn simplestorage_get_value( node: &mut TestNode, contract_address: polkadot_sdk::pallet_revive::H160, From b69a20d0673606ddeac0015e941507b9e96b3ed3 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 27 Jan 2026 12:27:42 -0300 Subject: [PATCH 02/22] Fix clippy and fmt --- crates/anvil-polkadot/src/api_server/server.rs | 8 ++++---- crates/anvil-polkadot/tests/it/utils.rs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/anvil-polkadot/src/api_server/server.rs b/crates/anvil-polkadot/src/api_server/server.rs index d5e4daaaab4d4..b39a1594b4ec0 100644 --- a/crates/anvil-polkadot/src/api_server/server.rs +++ b/crates/anvil-polkadot/src/api_server/server.rs @@ -813,10 +813,10 @@ impl ApiServer { // Prefetch storage keys for the sender to speed up transaction validation. // This is especially important when forking from a remote chain, as each storage // read would otherwise require a separate RPC call. When not forking, this is a no-op. - if let Ok(signed_tx) = TransactionSigned::decode(&transaction.0) { - if let Ok(sender) = recover_maybe_impersonated_address(&signed_tx) { - self.backend.prefetch_eth_transaction_keys(sender); - } + if let Ok(signed_tx) = TransactionSigned::decode(&transaction.0) + && let Ok(sender) = recover_maybe_impersonated_address(&signed_tx) + { + self.backend.prefetch_eth_transaction_keys(sender); } // Use dynamic transaction building to ensure the correct pallet index is used. diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index fd864a28b09d7..2560f8ccbaf8b 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -166,10 +166,10 @@ impl TestNode { .eth_rpc(EthRequest::EthGetTransactionReceipt(B256::from(tx_hash.to_fixed_bytes()))) .await; - if let Ok(ResponseResult::Success(val)) = receipt_result { - if !val.is_null() { - return Ok(self.get_transaction_receipt(tx_hash).await); - } + if let Ok(ResponseResult::Success(val)) = receipt_result + && !val.is_null() + { + return Ok(self.get_transaction_receipt(tx_hash).await); } // Mine a block and wait From 76e36f2fb5dccccb2dd4ac81300354dbf9eec0ec Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 3 Feb 2026 12:14:23 -0300 Subject: [PATCH 03/22] Import HashMap and use it from there --- .../substrate_node/lazy_loading/backend/blockchain.rs | 11 ++++------- .../src/substrate_node/lazy_loading/rpc_client.rs | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs index 65139b3b5826f..2e80198fa853c 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/blockchain.rs @@ -157,8 +157,9 @@ impl Blockchain { } // Update leaves for all blocks including genesis. - // For genesis when forking, the parent_hash points to the previous block on the remote chain. - // That parent won't be in our leaf set, so this effectively adds genesis as a new leaf. + // For genesis when forking, the parent_hash points to the previous block on the remote + // chain. That parent won't be in our leaf set, so this effectively adds genesis as + // a new leaf. storage.leaves.import(hash, number, *header.parent_hash()); // Finalize block only if explicitly requested via new_state @@ -443,11 +444,7 @@ impl sp_blockchain::Backend for Blockch .blocks .iter() .filter_map(|(hash, block)| { - if *block.header().parent_hash() == parent_hash { - Some(*hash) - } else { - None - } + if *block.header().parent_hash() == parent_hash { Some(*hash) } else { None } }) .collect(); Ok(children) diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs index c07fd0fce6187..ad2f4c7d24f7e 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs @@ -11,6 +11,7 @@ use polkadot_sdk::{ }; use serde::de::DeserializeOwned; use std::{ + collections::HashMap, marker::PhantomData, sync::{ Arc, @@ -354,8 +355,7 @@ impl RPCClient for Rpc { let mut results: Vec<(StorageKey, Option)> = Vec::with_capacity(keys.len()); // Build a map from returned results - let mut result_map: std::collections::HashMap, Option> = - std::collections::HashMap::new(); + let mut result_map: HashMap, Option> = HashMap::new(); for change_set in result { for (key, value) in change_set.changes { From 6912de4820e639b23deda388f38a4cbd60d68001 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 3 Feb 2026 12:15:31 -0300 Subject: [PATCH 04/22] Make prefetch_storage_keys private --- crates/anvil-polkadot/src/substrate_node/service/backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/anvil-polkadot/src/substrate_node/service/backend.rs b/crates/anvil-polkadot/src/substrate_node/service/backend.rs index 386af909718ba..700ec0afc8db5 100644 --- a/crates/anvil-polkadot/src/substrate_node/service/backend.rs +++ b/crates/anvil-polkadot/src/substrate_node/service/backend.rs @@ -293,7 +293,7 @@ impl BackendWithOverlay { /// Prefetch multiple storage keys in a single batch RPC call. /// This significantly reduces latency when forking from a remote chain. /// Returns the number of keys actually fetched from remote. - pub fn prefetch_storage_keys(&self, keys: &[Vec]) -> usize { + fn prefetch_storage_keys(&self, keys: &[Vec]) -> usize { self.backend.prefetch_storage_keys(keys) } From 90040ddb58564f3c205e59170051f509c8c72026 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 3 Feb 2026 12:16:04 -0300 Subject: [PATCH 05/22] Remove sleep --- crates/anvil-polkadot/tests/it/forking.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/anvil-polkadot/tests/it/forking.rs b/crates/anvil-polkadot/tests/it/forking.rs index f16a843e91819..eb1914c9ace30 100644 --- a/crates/anvil-polkadot/tests/it/forking.rs +++ b/crates/anvil-polkadot/tests/it/forking.rs @@ -836,9 +836,6 @@ async fn test_fork_state_snapshotting_from_westend() { .unwrap(); assert!(reverted, "Revert should succeed"); - // Wait for state to settle - tokio::time::sleep(Duration::from_millis(500)).await; - // Verify state is back to snapshot point let alith_balance_reverted = fork_node.get_balance(alith.address(), None).await; let baltathar_balance_reverted = fork_node.get_balance(baltathar.address(), None).await; From 996509e75aa616987fa1ebff808e4adfed82fbd4 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 3 Feb 2026 12:18:03 -0300 Subject: [PATCH 06/22] Fix test_fork_can_send_tx_from_westend --- crates/anvil-polkadot/tests/it/forking.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/anvil-polkadot/tests/it/forking.rs b/crates/anvil-polkadot/tests/it/forking.rs index eb1914c9ace30..6cac83b3ca711 100644 --- a/crates/anvil-polkadot/tests/it/forking.rs +++ b/crates/anvil-polkadot/tests/it/forking.rs @@ -858,6 +858,7 @@ async fn test_fork_can_send_tx_from_westend() { let fork_config = westend_fork_config(); let fork_substrate_config = SubstrateNodeConfig::new(&fork_config); let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap(); + let initial_block = fork_node.best_block_number().await; let alith = Account::from(subxt_signer::eth::dev::alith()); let baltathar = Account::from(subxt_signer::eth::dev::baltathar()); @@ -936,7 +937,8 @@ async fn test_fork_can_send_tx_from_westend() { ); // Verify block number increased - let _final_block = fork_node.best_block_number().await; + let final_block = fork_node.best_block_number().await; + assert!(final_block > initial_block, "Block number should increase after transactions"); } /// Tests that local state changes don't affect the remote fork state From 456a63b5fdfbdf722bf18eaa1b895874dcfd28dd Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Feb 2026 08:45:57 -0300 Subject: [PATCH 07/22] Remove keys prefetch --- .../anvil-polkadot/src/api_server/server.rs | 57 ++++++++--------- .../backend/forked_lazy_backend.rs | 61 +------------------ .../lazy_loading/backend/mod.rs | 20 +----- .../lazy_loading/backend/tests.rs | 18 +----- .../substrate_node/lazy_loading/rpc_client.rs | 49 --------------- .../src/substrate_node/service/backend.rs | 30 --------- 6 files changed, 29 insertions(+), 206 deletions(-) diff --git a/crates/anvil-polkadot/src/api_server/server.rs b/crates/anvil-polkadot/src/api_server/server.rs index f2a52c8622e1e..2a7b75aab7add 100644 --- a/crates/anvil-polkadot/src/api_server/server.rs +++ b/crates/anvil-polkadot/src/api_server/server.rs @@ -820,15 +820,6 @@ impl ApiServer { async fn send_raw_transaction(&self, transaction: Bytes) -> Result { let hash = H256(keccak_256(&transaction.0)); - // Prefetch storage keys for the sender to speed up transaction validation. - // This is especially important when forking from a remote chain, as each storage - // read would otherwise require a separate RPC call. When not forking, this is a no-op. - if let Ok(signed_tx) = TransactionSigned::decode(&transaction.0) - && let Ok(sender) = recover_maybe_impersonated_address(&signed_tx) - { - self.backend.prefetch_eth_transaction_keys(sender); - } - // Use dynamic transaction building to ensure the correct pallet index is used. // The metadata in self.api comes from the runtime's WASM (via runtime API call), // which is the forked chain's WASM when forking. This ensures correct pallet indices. @@ -1984,38 +1975,38 @@ impl ApiServer { if let Ok(Some(substrate_block)) = self.eth_rpc_client.block_by_hash(&block_hash).await && let Some(evm_block) = self.eth_rpc_client.evm_block(substrate_block, false).await { - // Extract transaction hashes - let tx_hashes: Vec = match &evm_block.transactions { - HashesOrTransactionInfos::Hashes(hashes) => hashes.clone(), - // Considering that we called evm_block with hydrated set to false, we will - // never receive TransactionInfos, but we handle it anyways. - HashesOrTransactionInfos::TransactionInfos(infos) => { - infos.iter().map(|i| i.hash).collect() - } - }; - - if !tx_hashes.is_empty() { - node_info!(""); + // Extract transaction hashes + let tx_hashes: Vec = match &evm_block.transactions { + HashesOrTransactionInfos::Hashes(hashes) => hashes.clone(), + // Considering that we called evm_block with hydrated set to false, we will + // never receive TransactionInfos, but we handle it anyways. + HashesOrTransactionInfos::TransactionInfos(infos) => { + infos.iter().map(|i| i.hash).collect() + } + }; - // Log each transaction - for tx_hash in tx_hashes { - if let Some(receipt) = self.eth_rpc_client.receipt(&tx_hash).await { - node_info!(" Transaction: {:?}", receipt.transaction_hash); + if !tx_hashes.is_empty() { + node_info!(""); - if let Some(contract) = &receipt.contract_address { - node_info!(" Contract created: {contract}"); - } + // Log each transaction + for tx_hash in tx_hashes { + if let Some(receipt) = self.eth_rpc_client.receipt(&tx_hash).await { + node_info!(" Transaction: {:?}", receipt.transaction_hash); - node_info!(" Gas used: {}", receipt.cumulative_gas_used); + if let Some(contract) = &receipt.contract_address { + node_info!(" Contract created: {contract}"); + } - if receipt.status == Some(evm::U256::zero()) { - node_info!(" Error: reverted"); - } + node_info!(" Gas used: {}", receipt.cumulative_gas_used); - node_info!(""); + if receipt.status == Some(evm::U256::zero()) { + node_info!(" Error: reverted"); } + + node_info!(""); } } + } } node_info!(" Block Number: {}", block_number); diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/forked_lazy_backend.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/forked_lazy_backend.rs index d26ca6a743c2b..e0c5d19dc425c 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/forked_lazy_backend.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/forked_lazy_backend.rs @@ -9,8 +9,8 @@ use polkadot_sdk::{ traits::{Block as BlockT, HashingFor}, }, sp_state_machine::{ - self, Backend as StateMachineBackend, BackendTransaction, InMemoryBackend, IterArgs, - StorageCollection, StorageValue, TrieBackend, backend::AsTrieBackend, + self, BackendTransaction, InMemoryBackend, IterArgs, StorageCollection, StorageValue, + TrieBackend, backend::AsTrieBackend, }, sp_storage::ChildInfo, sp_trie::{self, PrefixedMemoryDB}, @@ -99,63 +99,6 @@ impl ForkedLazyBackend { pub(crate) fn rpc(&self) -> Option<&dyn RPCClient> { self.rpc_client.as_deref() } - - /// Prefetch multiple storage keys in a single RPC batch call. - /// This significantly reduces latency when we know which keys will be needed. - /// Keys that are already cached or marked as removed will be skipped. - /// Returns the number of keys actually fetched from remote. - pub fn prefetch_keys(&self, keys: &[Vec]) -> usize { - if keys.is_empty() { - return 0; - } - - let rpc = match self.rpc() { - Some(rpc) => rpc, - None => return 0, - }; - - // Filter out keys that are already cached or removed - let db = self.db.read(); - let removed = self.removed_keys.read(); - - let keys_to_fetch: Vec = keys - .iter() - .filter(|key| { - // Skip if already in cache - if StateMachineBackend::storage(&*db, key).ok().flatten().is_some() { - return false; - } - // Skip if marked as removed - if removed.contains(*key) { - return false; - } - true - }) - .map(|key| polkadot_sdk::sp_storage::StorageKey(key.clone())) - .collect(); - - drop(db); - drop(removed); - - if keys_to_fetch.is_empty() { - return 0; - } - - let fetch_count = keys_to_fetch.len(); - - // Use the batch RPC call - let block_to_query = if self.before_fork { self.block_hash } else { self.fork_block }; - - match rpc.storage_batch(keys_to_fetch, block_to_query) { - Ok(results) => { - for (key, value) in results { - self.update_storage(&key.0, &value.map(|v| v.0)); - } - fetch_count - } - Err(_) => 0, - } - } } impl sp_state_machine::Backend> diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/mod.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/mod.rs index bee53a781072b..92a7f5923c661 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/mod.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/mod.rs @@ -12,8 +12,8 @@ mod tests; use parking_lot::RwLock; use polkadot_sdk::{ sc_client_api::{ - HeaderBackend, TrieCacheContext, UsageInfo, - backend::{self, AuxStore, Backend as ClientBackend}, + TrieCacheContext, UsageInfo, + backend::{self, AuxStore}, }, sp_blockchain, sp_core::{H256, offchain::storage::InMemOffchainStorage}, @@ -59,22 +59,6 @@ impl Backend { fn fork_checkpoint(&self) -> Option<&Block::Header> { self.fork_config.as_ref().map(|(_, checkpoint)| checkpoint) } - - /// Prefetch multiple storage keys in a single batch RPC call. - /// This significantly reduces latency when we know which keys will be needed - /// (e.g., before transaction validation). - /// Returns the number of keys actually fetched from remote. - pub fn prefetch_storage_keys(&self, keys: &[Vec]) -> usize { - // Get the best block hash to find the current state - let best_hash = HeaderBackend::info(&self.blockchain).best_hash; - - // Try to get the state for the best block - if let Ok(state) = ClientBackend::state_at(self, best_hash, TrieCacheContext::Trusted) { - state.prefetch_keys(keys) - } else { - 0 - } - } } impl AuxStore for Backend { diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/tests.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/tests.rs index d52e4be2b252e..9267f159f7c33 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/tests.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/tests.rs @@ -2,7 +2,7 @@ use super::*; use mock_rpc::{Rpc, TestBlock, TestHeader}; use parking_lot::RwLock; use polkadot_sdk::{ - sc_client_api::{Backend as BackendT, StateBackend}, + sc_client_api::{Backend as BackendT, HeaderBackend, StateBackend}, sp_runtime::{ OpaqueExtrinsic, traits::{BlakeTwo256, Header as HeaderT}, @@ -276,22 +276,6 @@ mod mock_rpc { let take = min(filtered.len(), count as usize); Ok(filtered.into_iter().take(take).map(|k| k.0).collect()) } - - fn storage_batch( - &self, - keys: Vec, - at: Option, - ) -> Result)>, jsonrpsee::core::ClientError> { - // Simple implementation: just call storage for each key - let results = keys - .into_iter() - .map(|key| { - let value = self.storage(key.clone(), at).ok().flatten(); - (key, value) - }) - .collect(); - Ok(results) - } } } diff --git a/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs b/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs index ad2f4c7d24f7e..aaebbd5e92aa6 100644 --- a/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs +++ b/crates/anvil-polkadot/src/substrate_node/lazy_loading/rpc_client.rs @@ -11,7 +11,6 @@ use polkadot_sdk::{ }; use serde::de::DeserializeOwned; use std::{ - collections::HashMap, marker::PhantomData, sync::{ Arc, @@ -85,14 +84,6 @@ pub trait RPCClient: Send + Sync + std::fmt::D start_key: Option, at: Option, ) -> Result, ClientError>; - - /// Fetch multiple storage keys in a single RPC call using state_queryStorageAt. - /// Returns a vector of (key, optional_value) pairs. - fn storage_batch( - &self, - keys: Vec, - at: Option, - ) -> Result)>, ClientError>; } #[derive(Debug, Clone)] @@ -331,46 +322,6 @@ impl RPCClient for Rpc { Err(err) => Err(err), } } - - fn storage_batch( - &self, - keys: Vec, - at: Option, - ) -> Result)>, ClientError> { - if keys.is_empty() { - return Ok(vec![]); - } - - let client = self.http_client.clone(); - let keys_clone = keys.clone(); - - let result = self.block_on(async move { - substrate_rpc_client::StateApi::::query_storage_at(&client, keys_clone, at) - .await - })?; - - // query_storage_at returns Vec> - // Each StorageChangeSet contains changes: Vec<(StorageKey, Option)> - // For a single block query, we get one StorageChangeSet with all results - let mut results: Vec<(StorageKey, Option)> = Vec::with_capacity(keys.len()); - - // Build a map from returned results - let mut result_map: HashMap, Option> = HashMap::new(); - - for change_set in result { - for (key, value) in change_set.changes { - result_map.insert(key.0, value); - } - } - - // Return results in the same order as input keys - for key in keys { - let value = result_map.get(&key.0).cloned().flatten(); - results.push((key, value)); - } - - Ok(results) - } } #[cfg(test)] diff --git a/crates/anvil-polkadot/src/substrate_node/service/backend.rs b/crates/anvil-polkadot/src/substrate_node/service/backend.rs index 5ba600409a1de..05ad8325188dc 100644 --- a/crates/anvil-polkadot/src/substrate_node/service/backend.rs +++ b/crates/anvil-polkadot/src/substrate_node/service/backend.rs @@ -291,36 +291,6 @@ impl BackendWithOverlay { .storage(key.as_slice()) .map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?) } - - /// Prefetch multiple storage keys in a single batch RPC call. - /// This significantly reduces latency when forking from a remote chain. - /// Returns the number of keys actually fetched from remote. - fn prefetch_storage_keys(&self, keys: &[Vec]) -> usize { - self.backend.prefetch_storage_keys(keys) - } - - /// Prefetch storage keys needed for validating an Ethereum transaction. - /// This includes account info for the sender address. - pub fn prefetch_eth_transaction_keys(&self, sender: H160) { - // Build list of keys to prefetch - let mut keys = Vec::new(); - - // 1. Sender's Revive account info (AccountInfoOf) - keys.push(well_known_keys::revive_account_info(sender)); - - // 2. Sender's System account info (for balance/nonce if mapping exists) - // The AccountId is derived from H160 in pallet-revive - let sender_account_id: AccountId = { - // pallet-revive uses AccountId32 derived from H160 by padding with zeros - let mut account_bytes = [0u8; 32]; - account_bytes[..20].copy_from_slice(sender.as_bytes()); - account_bytes.into() - }; - keys.push(well_known_keys::system_account_info(sender_account_id)); - - // Prefetch all keys in a single batch - self.prefetch_storage_keys(&keys); - } } pub type Storage = HashMap>; From 7d94ba2149cc2a5d191828e8b7106803f0db7b47 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Feb 2026 10:07:40 -0300 Subject: [PATCH 08/22] Remove unnecesary test --- crates/anvil-polkadot/tests/it/forking.rs | 30 ----------------------- 1 file changed, 30 deletions(-) diff --git a/crates/anvil-polkadot/tests/it/forking.rs b/crates/anvil-polkadot/tests/it/forking.rs index 6cac83b3ca711..3409c344d0a94 100644 --- a/crates/anvil-polkadot/tests/it/forking.rs +++ b/crates/anvil-polkadot/tests/it/forking.rs @@ -941,36 +941,6 @@ async fn test_fork_can_send_tx_from_westend() { assert!(final_block > initial_block, "Block number should increase after transactions"); } -/// Tests that local state changes don't affect the remote fork state -#[tokio::test(flavor = "multi_thread")] -async fn test_fork_separate_states_from_westend() { - let fork_config = westend_fork_config(); - let fork_substrate_config = SubstrateNodeConfig::new(&fork_config); - let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap(); - - let random_addr = Address::random(); - - // Get initial balance (should be 0 for random address) - let initial_balance = - fork_node.get_balance(subxt::utils::H160::from(random_addr.0.0), None).await; - assert_eq!(initial_balance, U256::ZERO, "Random address should have zero balance initially"); - - // Set a new balance locally - let new_balance = U256::from(1337u64); - unwrap_response::<()>( - fork_node.eth_rpc(EthRequest::SetBalance(random_addr, new_balance)).await.unwrap(), - ) - .unwrap(); - - // Verify local balance changed - let local_balance = - fork_node.get_balance(subxt::utils::H160::from(random_addr.0.0), None).await; - assert_eq!(local_balance, new_balance, "Local balance should be updated"); - - // The remote state should not be affected (we can't directly check this, - // but we verify that local changes work independently) -} - /// Tests deploying a contract on a forked chain #[tokio::test(flavor = "multi_thread")] async fn test_fork_can_deploy_contract_from_westend() { From 98a1f5679715d4813188373bf9307feac145fb1a Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Feb 2026 10:28:49 -0300 Subject: [PATCH 09/22] Refactor to improve error handling --- crates/anvil-polkadot/src/api_server/error.rs | 6 ++++++ crates/anvil-polkadot/src/api_server/server.rs | 5 +---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/anvil-polkadot/src/api_server/error.rs b/crates/anvil-polkadot/src/api_server/error.rs index 2b0c51043b4ff..7ba7838b92ef4 100644 --- a/crates/anvil-polkadot/src/api_server/error.rs +++ b/crates/anvil-polkadot/src/api_server/error.rs @@ -47,6 +47,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: subxt::ext::subxt_rpcs::Error) -> Self { + Self::from(subxt::Error::from(err)) + } +} + impl From for Error { fn from(err: ClientError) -> Self { match err { diff --git a/crates/anvil-polkadot/src/api_server/server.rs b/crates/anvil-polkadot/src/api_server/server.rs index 2a7b75aab7add..ae1954dea8ff4 100644 --- a/crates/anvil-polkadot/src/api_server/server.rs +++ b/crates/anvil-polkadot/src/api_server/server.rs @@ -831,10 +831,7 @@ impl ApiServer { })?; // Submit the extrinsic to the transaction pool - self.rpc - .author_submit_extrinsic(ext.encoded()) - .await - .map_err(|e| Error::InternalError(format!("Failed to submit transaction: {e}")))?; + self.rpc.author_submit_extrinsic(ext.encoded()).await?; Ok(hash) } From 5d42a439b6e449c8e7358790b7b03914d6a24ec2 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Feb 2026 10:43:23 -0300 Subject: [PATCH 10/22] Check contract code --- crates/anvil-polkadot/tests/it/forking.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/anvil-polkadot/tests/it/forking.rs b/crates/anvil-polkadot/tests/it/forking.rs index 3409c344d0a94..312c8930c864a 100644 --- a/crates/anvil-polkadot/tests/it/forking.rs +++ b/crates/anvil-polkadot/tests/it/forking.rs @@ -689,7 +689,11 @@ async fn test_fork_eth_get_code_from_westend() { .unwrap(), ) .unwrap(); - assert!(!deployed_code.is_empty(), "Deployed contract should have code"); + assert_eq!( + deployed_code.as_ref(), + contract_code.runtime.as_deref().expect("missing runtime bytecode"), + "Deployed code should match contract runtime bytecode" + ); // Deploy another contract to verify chain continues working let tx_hash2 = fork_node.deploy_contract(&contract_code.init, alith.address()).await; @@ -983,7 +987,11 @@ async fn test_fork_can_deploy_contract_from_westend() { .unwrap(), ) .unwrap(); - assert!(!code.is_empty(), "Deployed contract should have code"); + assert_eq!( + code.as_ref(), + contract_code.runtime.as_deref().expect("missing runtime bytecode"), + "Deployed code should match contract runtime bytecode" + ); // Deploy another contract to verify chain continues working let receipt2 = fork_node From 9d5dc7bf06314b9117adf07525563f680736da3a Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Feb 2026 11:34:53 -0300 Subject: [PATCH 11/22] Check contract methods --- crates/anvil-polkadot/tests/it/forking.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/anvil-polkadot/tests/it/forking.rs b/crates/anvil-polkadot/tests/it/forking.rs index 312c8930c864a..9fafa9caa6e13 100644 --- a/crates/anvil-polkadot/tests/it/forking.rs +++ b/crates/anvil-polkadot/tests/it/forking.rs @@ -1007,6 +1007,25 @@ async fn test_fork_can_deploy_contract_from_westend() { let contract_address2 = receipt2.contract_address.expect("Second contract address should exist"); assert_ne!(contract_address, contract_address2, "Contract addresses should be different"); + + // Verify contract methods work: set a value and read it back + let set_value = SimpleStorage::setValueCall::new((U256::from(42),)).abi_encode(); + let set_tx = TransactionRequest::default() + .from(alith_address) + .to(Address::from(ReviveAddress::new(contract_address))) + .input(TransactionInput::both(Bytes::from(set_value))); + fork_node + .send_transaction_and_wait(set_tx, 120) + .await + .expect("setValue transaction should succeed"); + + let stored_value = simplestorage_get_value( + &mut fork_node, + contract_address, + alith_address, + ) + .await; + assert_eq!(stored_value, U256::from(42), "getValue should return the value we set"); } /// Tests impersonating an account on a forked chain From dc7addaa1d37a80f7e914224da84444349d5f702 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Feb 2026 20:49:27 -0300 Subject: [PATCH 12/22] Prevent double rpc call --- crates/anvil-polkadot/tests/it/utils.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index fcf32eb41c66c..49f8d1da3a9b6 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -169,7 +169,8 @@ impl TestNode { if let Ok(ResponseResult::Success(val)) = receipt_result && !val.is_null() { - return Ok(self.get_transaction_receipt(tx_hash).await); + return serde_json::from_value(val) + .map_err(|_| RpcError::new(ErrorCode::InternalError)); } // Mine a block and wait From e93427824b4d0ca993a2c9a2643c8b1016e618a1 Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 12 Feb 2026 08:06:18 -0300 Subject: [PATCH 13/22] Add forking-support feature for forking tests --- crates/anvil-polkadot/tests/it/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/anvil-polkadot/tests/it/main.rs b/crates/anvil-polkadot/tests/it/main.rs index cb5d9793dfcb5..39934a1aae174 100644 --- a/crates/anvil-polkadot/tests/it/main.rs +++ b/crates/anvil-polkadot/tests/it/main.rs @@ -1,5 +1,6 @@ mod abi; mod filters; +#[cfg(feature = "forking-support")] mod forking; mod gas; mod genesis; From fc95dd7fd59c8ca812fc0ad7dcd55332b614ddb9 Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 12 Feb 2026 08:10:05 -0300 Subject: [PATCH 14/22] Improve error message --- crates/anvil-polkadot/tests/it/utils.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index 49f8d1da3a9b6..f6c78ca1beb4e 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -178,7 +178,9 @@ impl TestNode { tokio::time::sleep(Duration::from_secs(5)).await; } - Err(RpcError::new(ErrorCode::InternalError)) + Err(RpcError::internal_error_with(format!( + "Transaction {tx_hash:?} was not confirmed within {timeout:?}" + ))) } /// Execute an impersonated ethereum transaction. From 9a3aa9699109c259b99eec23eddea033a9f6d431 Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 12 Feb 2026 09:33:24 -0300 Subject: [PATCH 15/22] Await block import notifications instead of sleeping in send_transaction_and_wait --- crates/anvil-polkadot/tests/it/utils.rs | 54 ++++++++++++++----------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index f6c78ca1beb4e..b1290f87f1773 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -148,39 +148,47 @@ impl TestNode { } /// Execute an ethereum transaction and wait for its receipt. - /// This is useful for forking tests where transaction validation can take time - /// due to lazy loading of state from the remote chain. + /// In forking mode, block import and receipt indexing are decoupled: the block + /// commits immediately but the ReceiptProvider indexes receipts asynchronously. + /// This method awaits new block import notifications instead of sleeping. pub async fn send_transaction_and_wait( &mut self, transaction: TransactionRequest, timeout_secs: u64, ) -> Result { let tx_hash = self.send_transaction(transaction).await?; - - let start = std::time::Instant::now(); + let mut import_stream = self.service.client.import_notification_stream(); let timeout = Duration::from_secs(timeout_secs); - while start.elapsed() < timeout { - // Check if receipt is available - let receipt_result = self - .eth_rpc(EthRequest::EthGetTransactionReceipt(B256::from(tx_hash.to_fixed_bytes()))) - .await; - - if let Ok(ResponseResult::Success(val)) = receipt_result - && !val.is_null() - { - return serde_json::from_value(val) - .map_err(|_| RpcError::new(ErrorCode::InternalError)); + let result = tokio::time::timeout(timeout, async { + loop { + // Check if receipt is available + let receipt_result = self + .eth_rpc(EthRequest::EthGetTransactionReceipt(B256::from( + tx_hash.to_fixed_bytes(), + ))) + .await; + + if let Ok(ResponseResult::Success(val)) = receipt_result + && !val.is_null() + { + return serde_json::from_value(val) + .map_err(|_| RpcError::new(ErrorCode::InternalError)); + } + + // Mine a block and wait for the next block import notification + let _ = self.eth_rpc(EthRequest::Mine(None, None)).await; + let _ = import_stream.next().await; } - - // Mine a block and wait - let _ = self.eth_rpc(EthRequest::Mine(None, None)).await; - tokio::time::sleep(Duration::from_secs(5)).await; + }) + .await; + + match result { + Ok(receipt) => receipt, + Err(_) => Err(RpcError::internal_error_with(format!( + "Transaction {tx_hash:?} was not confirmed within {timeout:?}" + ))), } - - Err(RpcError::internal_error_with(format!( - "Transaction {tx_hash:?} was not confirmed within {timeout:?}" - ))) } /// Execute an impersonated ethereum transaction. From 4e2383ba33bec6351c74b3b28b298a10c73c759a Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 12 Feb 2026 10:19:55 -0300 Subject: [PATCH 16/22] Remove forking-specific comments from transaction wait helpers --- crates/anvil-polkadot/tests/it/utils.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index b1290f87f1773..63cdfe9795076 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -148,9 +148,7 @@ impl TestNode { } /// Execute an ethereum transaction and wait for its receipt. - /// In forking mode, block import and receipt indexing are decoupled: the block - /// commits immediately but the ReceiptProvider indexes receipts asynchronously. - /// This method awaits new block import notifications instead of sleeping. + /// Awaits new block import notifications until the receipt becomes available. pub async fn send_transaction_and_wait( &mut self, transaction: TransactionRequest, @@ -402,8 +400,6 @@ impl TestNode { } /// Deploy a contract and wait for its receipt. - /// This is useful for forking tests where transaction validation can take time - /// due to lazy loading of state from the remote chain. pub async fn deploy_contract_and_wait( &mut self, code: &[u8], From 036240ac7553a31fa1214be3ee61d18860a2de5d Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 12 Feb 2026 15:44:52 -0300 Subject: [PATCH 17/22] Skip polling in send_transaction_and_wait when automine is enabled --- crates/anvil-polkadot/tests/it/utils.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index 63cdfe9795076..ba248f208eb47 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -148,12 +148,27 @@ impl TestNode { } /// Execute an ethereum transaction and wait for its receipt. - /// Awaits new block import notifications until the receipt becomes available. + /// When automine is enabled, uses `EthSendTransactionSync` to get the receipt directly. + /// Otherwise, sends the transaction and polls for the receipt via block import notifications. pub async fn send_transaction_and_wait( &mut self, transaction: TransactionRequest, timeout_secs: u64, ) -> Result { + let is_automine = + unwrap_response::(self.eth_rpc(EthRequest::GetAutoMine(())).await.unwrap()) + .unwrap(); + + if is_automine { + return unwrap_response::( + self.eth_rpc(EthRequest::EthSendTransactionSync(Box::new( + WithOtherFields::new(transaction), + ))) + .await + .unwrap(), + ); + } + let tx_hash = self.send_transaction(transaction).await?; let mut import_stream = self.service.client.import_notification_stream(); let timeout = Duration::from_secs(timeout_secs); From c1ee5bd9291bb741164562c70e817fb640261a66 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 13 Feb 2026 11:08:22 -0300 Subject: [PATCH 18/22] remove unnecesary clone --- crates/anvil-polkadot/src/api_server/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/anvil-polkadot/src/api_server/server.rs b/crates/anvil-polkadot/src/api_server/server.rs index ae1954dea8ff4..e2c6ce4a2f9f5 100644 --- a/crates/anvil-polkadot/src/api_server/server.rs +++ b/crates/anvil-polkadot/src/api_server/server.rs @@ -823,7 +823,7 @@ impl ApiServer { // Use dynamic transaction building to ensure the correct pallet index is used. // The metadata in self.api comes from the runtime's WASM (via runtime API call), // which is the forked chain's WASM when forking. This ensures correct pallet indices. - let payload_value = DynamicValue::from_bytes(transaction.0.clone()); + let payload_value = DynamicValue::from_bytes(transaction.0); let tx_payload = dynamic_tx("Revive", "eth_transact", vec![payload_value]); let ext = self.api.tx().create_unsigned(&tx_payload).map_err(|e| { From 0792cbea4e789f5afe03a12a62b49df198e1151d Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 13 Feb 2026 19:19:45 -0300 Subject: [PATCH 19/22] Use forking-only test helpers behind forking-support feature --- crates/anvil-polkadot/tests/it/utils.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index ba248f208eb47..69e8a15377263 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -47,7 +47,9 @@ use std::{fmt::Debug, time::Duration}; use subxt::utils::H160; use tempfile::TempDir; -use crate::abi::{Multicall, SimpleStorage}; +use crate::abi::Multicall; +#[cfg(feature = "forking-support")] +use crate::abi::SimpleStorage; pub struct BlockWaitTimeout { pub block_number: u32, @@ -150,6 +152,7 @@ impl TestNode { /// Execute an ethereum transaction and wait for its receipt. /// When automine is enabled, uses `EthSendTransactionSync` to get the receipt directly. /// Otherwise, sends the transaction and polls for the receipt via block import notifications. + #[cfg(feature = "forking-support")] pub async fn send_transaction_and_wait( &mut self, transaction: TransactionRequest, @@ -415,6 +418,7 @@ impl TestNode { } /// Deploy a contract and wait for its receipt. + #[cfg(feature = "forking-support")] pub async fn deploy_contract_and_wait( &mut self, code: &[u8], @@ -617,6 +621,7 @@ pub fn to_hex_string(value: u64) -> String { } /// Helper function to call getValue() on a SimpleStorage contract +#[cfg(feature = "forking-support")] pub async fn simplestorage_get_value( node: &mut TestNode, contract_address: polkadot_sdk::pallet_revive::H160, From ea6c5335a0d98de469b3ea06487347639531bf02 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 13 Feb 2026 19:32:21 -0300 Subject: [PATCH 20/22] Simplify map_err --- crates/anvil-polkadot/src/api_server/server.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/anvil-polkadot/src/api_server/server.rs b/crates/anvil-polkadot/src/api_server/server.rs index e2c6ce4a2f9f5..a45726fd0e8f4 100644 --- a/crates/anvil-polkadot/src/api_server/server.rs +++ b/crates/anvil-polkadot/src/api_server/server.rs @@ -826,9 +826,7 @@ impl ApiServer { let payload_value = DynamicValue::from_bytes(transaction.0); let tx_payload = dynamic_tx("Revive", "eth_transact", vec![payload_value]); - let ext = self.api.tx().create_unsigned(&tx_payload).map_err(|e| { - Error::InternalError(format!("Failed to create unsigned extrinsic: {e}")) - })?; + let ext = self.api.tx().create_unsigned(&tx_payload)?; // Submit the extrinsic to the transaction pool self.rpc.author_submit_extrinsic(ext.encoded()).await?; From 2f34659fbc9428cbe9f617b35ab521f582458b99 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 13 Feb 2026 20:25:30 -0300 Subject: [PATCH 21/22] Add comment explaining that we have tests for westend --- crates/anvil-polkadot/tests/it/forking.rs | 71 +++++++++++++++++++---- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/crates/anvil-polkadot/tests/it/forking.rs b/crates/anvil-polkadot/tests/it/forking.rs index 9fafa9caa6e13..0daefdec30160 100644 --- a/crates/anvil-polkadot/tests/it/forking.rs +++ b/crates/anvil-polkadot/tests/it/forking.rs @@ -18,6 +18,17 @@ use polkadot_sdk::pallet_revive::evm::Account; /// This endpoint provides both Substrate RPC and ETH RPC methods const WESTEND_ASSET_HUB_URL: &str = "https://westend-asset-hub-rpc.polkadot.io"; +/// Helper to create a fork config pointing to Westend Asset Hub +/// We have verbose forking tests to debug this new experimental workflow. +fn westend_fork_config() -> AnvilNodeConfig { + AnvilNodeConfig::test_config() + .with_port(0) + .with_eth_rpc_url(Some(WESTEND_ASSET_HUB_URL.to_string())) + .with_auto_impersonate(true) + .with_tracing(true) + .set_silent(false) +} + /// Tests that forking preserves state from the source chain and allows local modifications #[tokio::test(flavor = "multi_thread")] async fn test_fork_preserves_state_and_allows_modifications() { @@ -601,16 +612,6 @@ async fn test_fork_with_contract_deployment() { // These tests require a running zombienet instance at WESTEND_ASSET_HUB_URL // ============================================================================= -/// Helper to create a fork config pointing to Westend Asset Hub -fn westend_fork_config() -> AnvilNodeConfig { - AnvilNodeConfig::test_config() - .with_port(0) - .with_eth_rpc_url(Some(WESTEND_ASSET_HUB_URL.to_string())) - .with_auto_impersonate(true) - .with_tracing(true) - .set_silent(false) -} - /// Tests that we can fork from Westend Asset Hub and get balance of addresses #[tokio::test(flavor = "multi_thread")] async fn test_fork_eth_get_balance_from_westend() { @@ -945,6 +946,56 @@ async fn test_fork_can_send_tx_from_westend() { assert!(final_block > initial_block, "Block number should increase after transactions"); } +/// Tests sending a transaction on a forked chain with automine enabled. +/// This exercises the EthSendTransactionSync path in send_transaction_and_wait. +#[tokio::test(flavor = "multi_thread")] +async fn test_fork_can_send_tx_from_westend_with_automine() { + let fork_config = westend_fork_config().with_no_mining(false); + let fork_substrate_config = SubstrateNodeConfig::new(&fork_config); + let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap(); + + let alith = Account::from(subxt_signer::eth::dev::alith()); + let baltathar = Account::from(subxt_signer::eth::dev::baltathar()); + let alith_address = Address::from(ReviveAddress::new(alith.address())); + let baltathar_address = Address::from(ReviveAddress::new(baltathar.address())); + + // Set initial balances for dev accounts (they may not have balance in the forked chain) + let initial_balance = U256::from(1e20 as u128); // 100 ether + unwrap_response::<()>( + fork_node.eth_rpc(EthRequest::SetBalance(alith_address, initial_balance)).await.unwrap(), + ) + .unwrap(); + + // Mine an empty block to warm up the forking backend. The first block in forking mode + // is slow (~60s) because it imports state from the remote chain, which would exceed + // the 30s internal timeout of EthSendTransactionSync. + unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + + // Send a transaction using the automine path (EthSendTransactionSync) + let transfer_amount = U256::from(1e18 as u128); // 1 ether + let transaction = TransactionRequest::default() + .value(transfer_amount) + .from(alith_address) + .to(baltathar_address); + + let receipt = fork_node + .send_transaction_and_wait(transaction, 120) + .await + .expect("Transaction receipt not found within timeout"); + assert_eq!( + receipt.status, + Some(polkadot_sdk::pallet_revive::U256::from(1)), + "Transaction should succeed" + ); + + // Verify balances changed + let final_baltathar_balance = fork_node.get_balance(baltathar.address(), None).await; + assert_eq!( + final_baltathar_balance, transfer_amount, + "Baltathar should receive the transfer amount" + ); +} + /// Tests deploying a contract on a forked chain #[tokio::test(flavor = "multi_thread")] async fn test_fork_can_deploy_contract_from_westend() { From 85d5fac648d78b01b96af30bb608456973b3dce7 Mon Sep 17 00:00:00 2001 From: Diego Date: Fri, 13 Feb 2026 21:46:57 -0300 Subject: [PATCH 22/22] Fix automine tests --- .../anvil-polkadot/src/api_server/server.rs | 14 +++++++++- crates/anvil-polkadot/tests/it/forking.rs | 27 ++++++++++--------- crates/anvil-polkadot/tests/it/utils.rs | 6 ++--- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/crates/anvil-polkadot/src/api_server/server.rs b/crates/anvil-polkadot/src/api_server/server.rs index a45726fd0e8f4..0bb3ad12c7832 100644 --- a/crates/anvil-polkadot/src/api_server/server.rs +++ b/crates/anvil-polkadot/src/api_server/server.rs @@ -99,6 +99,11 @@ use subxt_signer::eth::Keypair; use tokio::try_join; pub const CLIENT_VERSION: &str = concat!("anvil-polkadot/v", env!("CARGO_PKG_VERSION")); +// When forking, operations can be slower due to fetching state from the remote chain, +// so we use a higher timeout to avoid spurious failures. +#[cfg(feature = "forking-support")] +const TIMEOUT_DURATION: Duration = Duration::from_secs(120); +#[cfg(not(feature = "forking-support"))] const TIMEOUT_DURATION: Duration = Duration::from_secs(30); pub struct ApiServer { @@ -1932,7 +1937,14 @@ impl ApiServer { awaited_hash: H256, ) -> Result<()> { if let Some(mut receiver) = receiver { - tokio::time::timeout(Duration::from_secs(3), async { + // When forking, block production can be slower due to fetching state from the + // remote chain, so we use a higher timeout to avoid spurious failures. + #[cfg(feature = "forking-support")] + let timeout = TIMEOUT_DURATION; + #[cfg(not(feature = "forking-support"))] + let timeout = Duration::from_secs(3); + + tokio::time::timeout(timeout, async { loop { if let Ok(block_hash) = receiver.recv().await { if let Err(e) = self.log_mined_block(block_hash).await { diff --git a/crates/anvil-polkadot/tests/it/forking.rs b/crates/anvil-polkadot/tests/it/forking.rs index 0daefdec30160..0e5c286bebef4 100644 --- a/crates/anvil-polkadot/tests/it/forking.rs +++ b/crates/anvil-polkadot/tests/it/forking.rs @@ -950,7 +950,9 @@ async fn test_fork_can_send_tx_from_westend() { /// This exercises the EthSendTransactionSync path in send_transaction_and_wait. #[tokio::test(flavor = "multi_thread")] async fn test_fork_can_send_tx_from_westend_with_automine() { - let fork_config = westend_fork_config().with_no_mining(false); + // Start with automine disabled (default for test_config) so we can warm up + // the forking backend with a manual mine before enabling automine. + let fork_config = westend_fork_config(); let fork_substrate_config = SubstrateNodeConfig::new(&fork_config); let mut fork_node = TestNode::new(fork_config.clone(), fork_substrate_config).await.unwrap(); @@ -966,12 +968,16 @@ async fn test_fork_can_send_tx_from_westend_with_automine() { ) .unwrap(); - // Mine an empty block to warm up the forking backend. The first block in forking mode - // is slow (~60s) because it imports state from the remote chain, which would exceed - // the 30s internal timeout of EthSendTransactionSync. + // Warm up the forking backend by mining an empty block. The first block in forking + // mode is slow because it imports state from the remote chain. Without this, the + // 30s internal timeout of EthSendTransactionSync would be exceeded. unwrap_response::<()>(fork_node.eth_rpc(EthRequest::Mine(None, None)).await.unwrap()).unwrap(); + // Enable automine so send_transaction_and_wait uses EthSendTransactionSync + unwrap_response::<()>(fork_node.eth_rpc(EthRequest::SetAutomine(true)).await.unwrap()).unwrap(); + // Send a transaction using the automine path (EthSendTransactionSync) + let baltathar_balance_before = fork_node.get_balance(baltathar.address(), None).await; let transfer_amount = U256::from(1e18 as u128); // 1 ether let transaction = TransactionRequest::default() .value(transfer_amount) @@ -989,9 +995,10 @@ async fn test_fork_can_send_tx_from_westend_with_automine() { ); // Verify balances changed - let final_baltathar_balance = fork_node.get_balance(baltathar.address(), None).await; + let baltathar_balance_after = fork_node.get_balance(baltathar.address(), None).await; assert_eq!( - final_baltathar_balance, transfer_amount, + baltathar_balance_after, + baltathar_balance_before + transfer_amount, "Baltathar should receive the transfer amount" ); } @@ -1070,12 +1077,8 @@ async fn test_fork_can_deploy_contract_from_westend() { .await .expect("setValue transaction should succeed"); - let stored_value = simplestorage_get_value( - &mut fork_node, - contract_address, - alith_address, - ) - .await; + let stored_value = + simplestorage_get_value(&mut fork_node, contract_address, alith_address).await; assert_eq!(stored_value, U256::from(42), "getValue should return the value we set"); } diff --git a/crates/anvil-polkadot/tests/it/utils.rs b/crates/anvil-polkadot/tests/it/utils.rs index 69e8a15377263..f514e954446af 100644 --- a/crates/anvil-polkadot/tests/it/utils.rs +++ b/crates/anvil-polkadot/tests/it/utils.rs @@ -164,9 +164,9 @@ impl TestNode { if is_automine { return unwrap_response::( - self.eth_rpc(EthRequest::EthSendTransactionSync(Box::new( - WithOtherFields::new(transaction), - ))) + self.eth_rpc(EthRequest::EthSendTransactionSync(Box::new(WithOtherFields::new( + transaction, + )))) .await .unwrap(), );