Skip to content

Commit d43a539

Browse files
authored
Implement forking support with lazy loading backend (#426)
1 parent 6af0332 commit d43a539

File tree

14 files changed

+1531
-213
lines changed

14 files changed

+1531
-213
lines changed

crates/anvil-polkadot/src/cmd.rs

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
use crate::config::{
2-
AccountGenerator, AnvilNodeConfig, CHAIN_ID, DEFAULT_MNEMONIC, SubstrateNodeConfig,
2+
AccountGenerator, AnvilNodeConfig, CHAIN_ID, DEFAULT_MNEMONIC, ForkChoice, SubstrateNodeConfig,
33
};
44
use alloy_genesis::Genesis;
55
use alloy_primitives::{U256, utils::Unit};
66
use alloy_signer_local::coins_bip39::{English, Mnemonic};
77
use anvil_server::ServerConfig;
88
use clap::Parser;
9+
use core::fmt;
910
use foundry_common::shell;
1011
use foundry_config::Chain;
1112
use rand_08::{SeedableRng, rngs::StdRng};
12-
use std::{net::IpAddr, path::PathBuf, time::Duration};
13+
use std::{net::IpAddr, path::PathBuf, str::FromStr, time::Duration};
1314

1415
#[derive(Clone, Debug, Parser)]
1516
pub struct NodeArgs {
@@ -138,7 +139,16 @@ impl NodeArgs {
138139
.disable_code_size_limit(self.evm.disable_code_size_limit)
139140
.with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
140141
.with_memory_limit(self.evm.memory_limit)
141-
.with_revive_rpc_block_limit(self.revive_rpc_block_limit);
142+
.with_fork_choice(self.evm.fork_block_number.map(ForkChoice::Block).or_else(|| {
143+
self.evm
144+
.fork_url
145+
.as_ref()
146+
.and_then(|f| f.block)
147+
.map(|num| ForkChoice::Block(num as i128))
148+
}))
149+
.with_eth_rpc_url(self.evm.fork_url.map(|fork| fork.url))
150+
.fork_request_timeout(self.evm.fork_request_timeout.map(Duration::from_millis))
151+
.fork_request_retries(self.evm.fork_request_retries);
142152

143153
let substrate_node_config = SubstrateNodeConfig::new(&anvil_config);
144154

@@ -174,6 +184,44 @@ impl NodeArgs {
174184
#[derive(Clone, Debug, Parser)]
175185
#[command(next_help_heading = "EVM options")]
176186
pub struct AnvilEvmArgs {
187+
/// Fetch state over a remote endpoint instead of starting from an empty state.
188+
///
189+
/// If you want to fetch state from a specific block number, add a block number like `http://localhost:8545@1400000` or use the `--fork-block-number` argument.
190+
#[arg(
191+
long,
192+
short,
193+
visible_alias = "rpc-url",
194+
value_name = "URL",
195+
help_heading = "Fork config"
196+
)]
197+
pub fork_url: Option<ForkUrl>,
198+
199+
/// Fetch state from a specific block number over a remote endpoint.
200+
///
201+
/// If negative, the given value is subtracted from the `latest` block number.
202+
///
203+
/// See --fork-url.
204+
#[arg(
205+
long,
206+
requires = "fork_url",
207+
value_name = "BLOCK",
208+
help_heading = "Fork config",
209+
allow_hyphen_values = true
210+
)]
211+
pub fork_block_number: Option<i128>,
212+
213+
/// Timeout in ms for requests sent to remote JSON-RPC server in forking mode.
214+
///
215+
/// Default value 45000
216+
#[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")]
217+
pub fork_request_timeout: Option<u64>,
218+
219+
/// Number of retry requests for spurious networks (timed out requests)
220+
///
221+
/// Default value 5
222+
#[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")]
223+
pub fork_request_retries: Option<u32>,
224+
177225
/// The block gas limit.
178226
#[arg(long, alias = "block-gas-limit", help_heading = "Environment config")]
179227
pub gas_limit: Option<u128>,
@@ -245,6 +293,46 @@ pub struct AnvilEvmArgs {
245293
pub memory_limit: Option<u64>,
246294
}
247295

296+
/// Represents the input URL for a fork with an optional trailing block number:
297+
/// `http://localhost:8545@1000000`
298+
#[derive(Clone, Debug, PartialEq, Eq)]
299+
pub struct ForkUrl {
300+
/// The endpoint url
301+
pub url: String,
302+
/// Optional trailing block
303+
pub block: Option<u64>,
304+
}
305+
306+
impl fmt::Display for ForkUrl {
307+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308+
self.url.fmt(f)?;
309+
if let Some(block) = self.block {
310+
write!(f, "@{block}")?;
311+
}
312+
Ok(())
313+
}
314+
}
315+
316+
impl FromStr for ForkUrl {
317+
type Err = String;
318+
319+
fn from_str(s: &str) -> Result<Self, Self::Err> {
320+
if let Some((url, block)) = s.rsplit_once('@') {
321+
if block == "latest" {
322+
return Ok(Self { url: url.to_string(), block: None });
323+
}
324+
// this will prevent false positives for auths `user:[email protected]`
325+
if !block.is_empty() && !block.contains(':') && !block.contains('.') {
326+
let block: u64 = block
327+
.parse()
328+
.map_err(|_| format!("Failed to parse block number: `{block}`"))?;
329+
return Ok(Self { url: url.to_string(), block: Some(block) });
330+
}
331+
}
332+
Ok(Self { url: s.to_string(), block: None })
333+
}
334+
}
335+
248336
/// Clap's value parser for genesis. Loads a genesis.json file.
249337
fn read_genesis_file(path: &str) -> Result<Genesis, String> {
250338
foundry_common::fs::read_json_file(path.as_ref()).map_err(|err| err.to_string())

crates/anvil-polkadot/src/config.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use alloy_signer_local::{
88
};
99
use anvil_server::ServerConfig;
1010
use eyre::{Context, Result};
11-
use foundry_common::{duration_since_unix_epoch, sh_println};
11+
use foundry_common::{REQUEST_TIMEOUT, duration_since_unix_epoch, sh_println};
1212
use polkadot_sdk::{
1313
pallet_revive::evm::Account,
1414
sc_cli::{
@@ -326,6 +326,14 @@ pub struct AnvilNodeConfig {
326326
pub memory_limit: Option<u64>,
327327
/// Do not print log messages.
328328
pub silent: bool,
329+
/// url of the rpc server that should be used for any rpc calls
330+
pub eth_rpc_url: Option<String>,
331+
/// pins the block number or transaction hash for the state fork
332+
pub fork_choice: Option<ForkChoice>,
333+
/// Timeout in for requests sent to remote JSON-RPC server in forking mode
334+
pub fork_request_timeout: Duration,
335+
/// Number of request retries for spurious networks
336+
pub fork_request_retries: u32,
329337
}
330338

331339
impl AnvilNodeConfig {
@@ -548,6 +556,10 @@ impl Default for AnvilNodeConfig {
548556
disable_default_create2_deployer: false,
549557
memory_limit: None,
550558
silent: false,
559+
eth_rpc_url: None,
560+
fork_choice: None,
561+
fork_request_timeout: REQUEST_TIMEOUT,
562+
fork_request_retries: 5,
551563
}
552564
}
553565
}
@@ -855,6 +867,69 @@ impl AnvilNodeConfig {
855867
self.silent = silent;
856868
self
857869
}
870+
871+
/// Sets the `eth_rpc_url` to use when forking
872+
#[must_use]
873+
pub fn with_eth_rpc_url<U: Into<String>>(mut self, eth_rpc_url: Option<U>) -> Self {
874+
self.eth_rpc_url = eth_rpc_url.map(Into::into);
875+
self
876+
}
877+
878+
/// Sets the `fork_choice` to use to fork off from based on a block number
879+
#[must_use]
880+
pub fn with_fork_block_number<U: Into<u64>>(self, fork_block_number: Option<U>) -> Self {
881+
self.with_fork_choice(fork_block_number.map(Into::into))
882+
}
883+
884+
/// Sets the `fork_choice` to use to fork off from
885+
#[must_use]
886+
pub fn with_fork_choice<U: Into<ForkChoice>>(mut self, fork_choice: Option<U>) -> Self {
887+
self.fork_choice = fork_choice.map(Into::into);
888+
self
889+
}
890+
891+
/// Sets the `fork_request_timeout` to use for requests
892+
#[must_use]
893+
pub fn fork_request_timeout(mut self, fork_request_timeout: Option<Duration>) -> Self {
894+
if let Some(fork_request_timeout) = fork_request_timeout {
895+
self.fork_request_timeout = fork_request_timeout;
896+
}
897+
self
898+
}
899+
900+
/// Sets the `fork_request_retries` to use for spurious networks
901+
#[must_use]
902+
pub fn fork_request_retries(mut self, fork_request_retries: Option<u32>) -> Self {
903+
if let Some(fork_request_retries) = fork_request_retries {
904+
self.fork_request_retries = fork_request_retries;
905+
}
906+
self
907+
}
908+
}
909+
910+
/// Fork delimiter used to specify which block to fork from.
911+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
912+
pub enum ForkChoice {
913+
/// Block number to fork from.
914+
///
915+
/// If negative, the given value is subtracted from the `latest` block number.
916+
Block(i128),
917+
}
918+
919+
impl ForkChoice {
920+
/// Returns the block number to fork from
921+
pub fn block_number(&self) -> Option<i128> {
922+
match self {
923+
Self::Block(block_number) => Some(*block_number),
924+
}
925+
}
926+
}
927+
928+
/// Convert a decimal block number into a ForkChoice
929+
impl From<u64> for ForkChoice {
930+
fn from(block: u64) -> Self {
931+
Self::Block(block as i128)
932+
}
858933
}
859934

860935
/// Can create dev accounts

crates/anvil-polkadot/src/substrate_node/genesis.rs

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ pub struct DevelopmentGenesisBlockBuilder<Block: BlockT, B, E> {
171171
commit_genesis_state: bool,
172172
backend: Arc<B>,
173173
executor: E,
174+
checkpoint: Option<Block>,
174175
_phantom: PhantomData<Block>,
175176
}
176177

@@ -217,6 +218,40 @@ impl<Block: BlockT, B: Backend<Block>, E: RuntimeVersionOf>
217218
commit_genesis_state,
218219
backend,
219220
executor,
221+
checkpoint: None,
222+
_phantom: PhantomData::<Block>,
223+
})
224+
}
225+
226+
pub fn new_with_checkpoint(
227+
build_genesis_storage: &dyn BuildStorage,
228+
commit_genesis_state: bool,
229+
backend: Arc<B>,
230+
executor: E,
231+
checkpoint: Block,
232+
) -> sp_blockchain::Result<Self> {
233+
let genesis_storage =
234+
build_genesis_storage.build_storage().map_err(sp_blockchain::Error::Storage)?;
235+
236+
// Extract genesis number from checkpoint header
237+
let genesis_number: u32 = (*checkpoint.header().number()).try_into().map_err(|_| {
238+
sp_blockchain::Error::Application(
239+
format!(
240+
"Checkpoint block number {} is too large for u32 (max: {})",
241+
checkpoint.header().number(),
242+
u32::MAX
243+
)
244+
.into(),
245+
)
246+
})?;
247+
248+
Ok(Self {
249+
genesis_number,
250+
genesis_storage,
251+
commit_genesis_state,
252+
backend,
253+
executor,
254+
checkpoint: Some(checkpoint),
220255
_phantom: PhantomData::<Block>,
221256
})
222257
}
@@ -234,30 +269,47 @@ impl<Block: BlockT, B: Backend<Block>, E: RuntimeVersionOf> BuildGenesisBlock<Bl
234269
commit_genesis_state,
235270
backend,
236271
executor,
272+
checkpoint,
237273
_phantom,
238274
} = self;
239275

240-
let genesis_state_version =
241-
resolve_state_version_from_wasm::<_, HashingFor<Block>>(&genesis_storage, &executor)?;
242-
let mut op = backend.begin_operation()?;
243-
let state_root =
244-
op.set_genesis_state(genesis_storage, commit_genesis_state, genesis_state_version)?;
245-
let extrinsics_root = <<<Block as BlockT>::Header as HeaderT>::Hashing as HashT>::trie_root(
246-
Vec::new(),
247-
genesis_state_version,
248-
);
249-
let genesis_block = Block::new(
250-
<<Block as BlockT>::Header as HeaderT>::new(
251-
genesis_number.into(),
252-
extrinsics_root,
253-
state_root,
254-
Default::default(),
276+
// If we have a checkpoint (fork mode), use it as the genesis block
277+
if let Some(checkpoint) = checkpoint {
278+
tracing::info!(
279+
"Using checkpoint block as genesis: number={}, hash={:?}",
280+
checkpoint.header().number(),
281+
checkpoint.header().hash()
282+
);
283+
284+
let op = backend.begin_operation()?;
285+
Ok((checkpoint, op))
286+
} else {
287+
// Normal mode: create new genesis block
288+
let genesis_state_version = resolve_state_version_from_wasm::<_, HashingFor<Block>>(
289+
&genesis_storage,
290+
&executor,
291+
)?;
292+
let mut op = backend.begin_operation()?;
293+
let state_root =
294+
op.set_genesis_state(genesis_storage, commit_genesis_state, genesis_state_version)?;
295+
let extrinsics_root =
296+
<<<Block as BlockT>::Header as HeaderT>::Hashing as HashT>::trie_root(
297+
Vec::new(),
298+
genesis_state_version,
299+
);
300+
let genesis_block = Block::new(
301+
<<Block as BlockT>::Header as HeaderT>::new(
302+
genesis_number.into(),
303+
extrinsics_root,
304+
state_root,
305+
Default::default(),
306+
Default::default(),
307+
),
255308
Default::default(),
256-
),
257-
Default::default(),
258-
);
309+
);
259310

260-
Ok((genesis_block, op))
311+
Ok((genesis_block, op))
312+
}
261313
}
262314
}
263315

crates/anvil-polkadot/src/substrate_node/lazy_loading/backend/forked_lazy_backend.rs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use super::make_composite_child_key;
2-
use crate::substrate_node::lazy_loading::{LAZY_LOADING_LOG_TARGET, rpc_client::RPCClient};
3-
use alloy_primitives::hex;
2+
use crate::substrate_node::lazy_loading::rpc_client::RPCClient;
43
use parking_lot::RwLock;
54
use polkadot_sdk::{
65
sc_client_api::StorageKey,
@@ -329,13 +328,6 @@ impl<Block: BlockT + DeserializeOwned> sp_state_machine::Backend<HashingFor<Bloc
329328
}
330329
.filter(|next_key| next_key != key);
331330

332-
tracing::trace!(
333-
target: LAZY_LOADING_LOG_TARGET,
334-
"next_storage_key: (key: {:?}, next_key: {:?})",
335-
hex::encode(key),
336-
maybe_next_key.clone().map(hex::encode)
337-
);
338-
339331
Ok(maybe_next_key)
340332
}
341333

@@ -377,14 +369,6 @@ impl<Block: BlockT + DeserializeOwned> sp_state_machine::Backend<HashingFor<Bloc
377369
}
378370
.filter(|next_key| next_key != key);
379371

380-
tracing::trace!(
381-
target: LAZY_LOADING_LOG_TARGET,
382-
"next_child_storage_key: (child_info: {:?}, key: {:?}, next_key: {:?})",
383-
child_info,
384-
hex::encode(key),
385-
maybe_next_key.clone().map(hex::encode)
386-
);
387-
388372
Ok(maybe_next_key)
389373
}
390374

0 commit comments

Comments
 (0)