Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
67a4103
Fix everything to make it work with real assethub westend
dimartiro Jan 27, 2026
b69a20d
Fix clippy and fmt
dimartiro Jan 27, 2026
76e36f2
Import HashMap and use it from there
dimartiro Feb 3, 2026
6912de4
Make prefetch_storage_keys private
dimartiro Feb 3, 2026
90040dd
Remove sleep
dimartiro Feb 3, 2026
996509e
Fix test_fork_can_send_tx_from_westend
dimartiro Feb 3, 2026
4b86134
Merge branch 'feature/forking' of https://github.com/paritytech/found…
dimartiro Feb 3, 2026
a9e5976
Merge remote-tracking branch 'upstream/feature/forking' into fix-west…
dimartiro Feb 10, 2026
456a63b
Remove keys prefetch
dimartiro Feb 11, 2026
7d94ba2
Remove unnecesary test
dimartiro Feb 11, 2026
98a1f56
Refactor to improve error handling
dimartiro Feb 11, 2026
5d42a43
Check contract code
dimartiro Feb 11, 2026
9d5dc7b
Check contract methods
dimartiro Feb 11, 2026
dc7adda
Prevent double rpc call
dimartiro Feb 11, 2026
e934278
Add forking-support feature for forking tests
dimartiro Feb 12, 2026
fc95dd7
Improve error message
dimartiro Feb 12, 2026
9a3aa96
Await block import notifications instead of sleeping in send_transact…
dimartiro Feb 12, 2026
4e2383b
Remove forking-specific comments from transaction wait helpers
dimartiro Feb 12, 2026
036240a
Skip polling in send_transaction_and_wait when automine is enabled
dimartiro Feb 12, 2026
c1ee5bd
remove unnecesary clone
dimartiro Feb 13, 2026
0792cbe
Use forking-only test helpers behind forking-support feature
dimartiro Feb 13, 2026
ea6c533
Simplify map_err
dimartiro Feb 13, 2026
2f34659
Add comment explaining that we have tests for westend
dimartiro Feb 13, 2026
85d5fac
Fix automine tests
dimartiro Feb 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions crates/anvil-polkadot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,4 @@ op-alloy-rpc-types.workspace = true
[features]
default = []
asm-keccak = ["alloy-primitives/asm-keccak"]
forking-support = []
forking-tests = []
forking-support = []
6 changes: 6 additions & 0 deletions crates/anvil-polkadot/src/api_server/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ impl From<subxt::Error> for Error {
}
}

impl From<subxt::ext::subxt_rpcs::Error> for Error {
fn from(err: subxt::ext::subxt_rpcs::Error) -> Self {
Self::from(subxt::Error::from(err))
}
}

impl From<ClientError> for Error {
fn from(err: ClientError) -> Self {
match err {
Expand Down
96 changes: 65 additions & 31 deletions crates/anvil-polkadot/src/api_server/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,23 @@ 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;

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 {
Expand All @@ -113,6 +122,12 @@ pub struct ApiServer {
/// Tracks all active filters
filters: Filters,
hardcoded_chain_id: u64,
/// RPC methods for submitting transactions
rpc: LegacyRpcMethods<SrcChainConfig>,
/// 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<SrcChainConfig>,
}

/// Fetch the chain ID from the substrate chain.
Expand Down Expand Up @@ -140,7 +155,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,
Expand Down Expand Up @@ -176,6 +191,8 @@ impl ApiServer {
instance_id: B256::random(),
filters,
hardcoded_chain_id: chain_id,
rpc,
api,
})
}

Expand Down Expand Up @@ -807,8 +824,18 @@ impl ApiServer {

async fn send_raw_transaction(&self, transaction: Bytes) -> Result<H256> {
let hash = H256(keccak_256(&transaction.0));
let call = subxt_client::tx().revive().eth_transact(transaction.0);
self.eth_rpc_client.submit(call).await?;

// 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);
let tx_payload = dynamic_tx("Revive", "eth_transact", vec![payload_value]);

let ext = self.api.tx().create_unsigned(&tx_payload)?;

// Submit the extrinsic to the transaction pool
self.rpc.author_submit_extrinsic(ext.encoded()).await?;

Ok(hash)
}

Expand Down Expand Up @@ -1910,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 {
Expand Down Expand Up @@ -1948,38 +1982,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<H256> = 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
Comment thread
re-gius marked this conversation as resolved.
let tx_hashes: Vec<H256> = 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,11 @@ impl<Block: BlockT + DeserializeOwned> Blockchain<Block> {
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 {
Expand Down Expand Up @@ -266,25 +267,43 @@ impl<Block: BlockT + DeserializeOwned> HeaderBackend<Block> for Blockchain<Block

// If not found in local storage, fetch from RPC client
let header = if let Some(rpc) = self.rpc() {
rpc.block(Some(hash)).ok().flatten().map(|full| {
let block = full.block.clone();
self.storage
.write()
.blocks
.insert(hash, StoredBlock::Full(block.clone(), full.justifications));
block.header().clone()
})
match rpc.block(Some(hash)) {
Ok(Some(full)) => {
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)
}
Expand Down Expand Up @@ -418,19 +437,30 @@ impl<Block: BlockT + DeserializeOwned> sp_blockchain::Backend<Block> for Blockch
Ok(leaves)
}

fn children(&self, _parent_hash: Block::Hash) -> sp_blockchain::Result<Vec<Block::Hash>> {
unimplemented!("Not supported by the `lazy-loading` backend.")
fn children(&self, parent_hash: Block::Hash) -> sp_blockchain::Result<Vec<Block::Hash>> {
// Find all blocks whose parent_hash matches the given hash
let storage = self.storage.read();
let children: Vec<Block::Hash> = 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<Option<Vec<u8>>> {
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<Option<Vec<Vec<u8>>>> {
unimplemented!("Not supported by the `lazy-loading` backend.")
// Indexed block bodies are not supported in the lazy-loading backend
Ok(None)
}
}

Expand Down
Loading
Loading