Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/anvil/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ tempfile.workspace = true
itertools.workspace = true
rand_08.workspace = true
eyre.workspace = true
url.workspace = true
ethereum_ssz.workspace = true

# cli
Expand Down
90 changes: 70 additions & 20 deletions crates/anvil/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ const DEFAULT_DUMP_INTERVAL: Duration = Duration::from_secs(60);

impl NodeArgs {
pub fn into_node_config(self) -> eyre::Result<NodeConfig> {
self.evm.validate_fork_opts()?;
let genesis_balance = Unit::ETHER.wei().saturating_mul(U256::from(self.balance));
let compute_units_per_second =
if self.evm.no_rate_limit { Some(u64::MAX) } else { self.evm.compute_units_per_second };
Expand Down Expand Up @@ -260,7 +261,7 @@ impl NodeArgs {
_ => self
.evm
.fork_url
.as_ref()
.first()
.and_then(|f| f.block)
.map(|num| ForkChoice::Block(num as i128)),
})
Expand All @@ -270,7 +271,11 @@ impl NodeArgs {
.fork_request_retries(self.evm.fork_request_retries)
.fork_retry_backoff(self.evm.fork_retry_backoff.map(Duration::from_millis))
.fork_compute_units_per_second(compute_units_per_second)
.with_eth_rpc_url(self.evm.fork_url.map(|fork| fork.url))
.with_eth_rpc_url(if self.evm.fork_url.is_empty() {
None
} else {
Some(self.evm.fork_url.into_iter().map(|f| f.url).collect::<Vec<_>>())
})
.with_base_fee(self.evm.block_base_fee_per_gas)
.disable_min_priority_fee(self.evm.disable_min_priority_fee)
.with_no_storage_caching(self.evm.no_storage_caching)
Expand Down Expand Up @@ -433,29 +438,28 @@ pub struct AnvilEvmArgs {
value_name = "URL",
help_heading = "Fork config"
)]
pub fork_url: Option<ForkUrl>,
pub fork_url: Vec<ForkUrl>,

/// Headers to use for the rpc client, e.g. "User-Agent: test-agent"
///
/// See --fork-url.
#[arg(
long = "fork-header",
value_name = "HEADERS",
help_heading = "Fork config",
requires = "fork_url"
help_heading = "Fork config"
)]
pub fork_headers: Vec<String>,

/// Timeout in ms for requests sent to remote JSON-RPC server in forking mode.
///
/// Default value 45000
#[arg(id = "timeout", long = "timeout", help_heading = "Fork config", requires = "fork_url")]
#[arg(id = "timeout", long = "timeout", help_heading = "Fork config")]
pub fork_request_timeout: Option<u64>,

/// Number of retry requests for spurious networks (timed out requests)
///
/// Default value 5
#[arg(id = "retries", long = "retries", help_heading = "Fork config", requires = "fork_url")]
#[arg(id = "retries", long = "retries", help_heading = "Fork config")]
pub fork_request_retries: Option<u32>,

/// Fetch state from a specific block number over a remote endpoint.
Expand All @@ -465,7 +469,6 @@ pub struct AnvilEvmArgs {
/// See --fork-url.
#[arg(
long,
requires = "fork_url",
value_name = "BLOCK",
help_heading = "Fork config",
allow_hyphen_values = true
Expand All @@ -477,7 +480,6 @@ pub struct AnvilEvmArgs {
/// See --fork-url.
#[arg(
long,
requires = "fork_url",
value_name = "TRANSACTION",
help_heading = "Fork config",
conflicts_with = "fork_block_number"
Expand All @@ -487,7 +489,7 @@ pub struct AnvilEvmArgs {
/// Initial retry backoff on encountering errors.
///
/// See --fork-url.
#[arg(long, requires = "fork_url", value_name = "BACKOFF", help_heading = "Fork config")]
#[arg(long, value_name = "BACKOFF", help_heading = "Fork config")]
pub fork_retry_backoff: Option<u64>,

/// Specify chain id to skip fetching it from remote endpoint. This enables offline-start mode.
Expand All @@ -498,8 +500,7 @@ pub struct AnvilEvmArgs {
#[arg(
long,
help_heading = "Fork config",
value_name = "CHAIN",
requires = "fork_block_number"
value_name = "CHAIN"
)]
pub fork_chain_id: Option<Chain>,

Expand All @@ -510,7 +511,6 @@ pub struct AnvilEvmArgs {
/// See also --fork-url and <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
#[arg(
long,
requires = "fork_url",
alias = "cups",
value_name = "CUPS",
help_heading = "Fork config"
Expand All @@ -524,7 +524,6 @@ pub struct AnvilEvmArgs {
/// See also --fork-url and <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
#[arg(
long,
requires = "fork_url",
value_name = "NO_RATE_LIMITS",
help_heading = "Fork config",
visible_alias = "no-rpc-rate-limit"
Expand All @@ -538,7 +537,7 @@ pub struct AnvilEvmArgs {
/// This flag overrides the project's configuration file.
///
/// See --fork-url.
#[arg(long, requires = "fork_url", help_heading = "Fork config")]
#[arg(long, help_heading = "Fork config")]
pub no_storage_caching: bool,

/// The block gas limit.
Expand Down Expand Up @@ -631,12 +630,63 @@ pub struct AnvilEvmArgs {
/// of the project configuration file.
/// Does nothing if the fork-url is not a configured alias.
impl AnvilEvmArgs {
/// Validates that fork-dependent options are only used when `--fork-url` is provided.
pub fn validate_fork_opts(&self) -> eyre::Result<()> {
if self.fork_url.is_empty() {
let mut invalid = Vec::new();
if !self.fork_headers.is_empty() {
invalid.push("--fork-header");
}
if self.fork_request_timeout.is_some() {
invalid.push("--timeout");
}
if self.fork_request_retries.is_some() {
invalid.push("--retries");
}
if self.fork_block_number.is_some() {
invalid.push("--fork-block-number");
}
if self.fork_transaction_hash.is_some() {
invalid.push("--fork-transaction-hash");
}
if self.fork_retry_backoff.is_some() {
invalid.push("--fork-retry-backoff");
}
if self.compute_units_per_second.is_some() {
invalid.push("--compute-units-per-second");
}
if self.no_rate_limit {
invalid.push("--no-rate-limit");
}
if self.no_storage_caching {
invalid.push("--no-storage-caching");
}
if self.fork_chain_id.is_some() {
invalid.push("--fork-chain-id");
}
if !invalid.is_empty() {
eyre::bail!(
"The following options require --fork-url: {}",
invalid.join(", ")
);
}
}

// --fork-chain-id requires --fork-block-number (original upstream constraint)
if self.fork_chain_id.is_some() && self.fork_block_number.is_none() {
eyre::bail!("--fork-chain-id requires --fork-block-number");
}

Ok(())
}

pub fn resolve_rpc_alias(&mut self) {
if let Some(fork_url) = &self.fork_url
&& let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil)
&& let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url)
{
self.fork_url = Some(ForkUrl { url: url.to_string(), block: fork_url.block });
if let Ok(config) = Config::load_with_providers(FigmentProviders::Anvil) {
for fork_url in &mut self.fork_url {
if let Some(Ok(url)) = config.get_rpc_url_with_alias(&fork_url.url) {
fork_url.url = url.to_string();
}
}
}
}
}
Expand Down
35 changes: 22 additions & 13 deletions crates/anvil/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ pub struct NodeConfig {
pub port: u16,
/// maximum number of transactions in a block
pub max_transactions: usize,
/// url of the rpc server that should be used for any rpc calls
pub eth_rpc_url: Option<String>,
/// urls of the rpc server that should be used for any rpc calls
pub eth_rpc_url: Option<Vec<String>>,
/// pins the block number or transaction hash for the state fork
pub fork_choice: Option<ForkChoice>,
/// headers to use with `eth_rpc_url`
Expand Down Expand Up @@ -857,8 +857,8 @@ impl NodeConfig {

/// Sets the `eth_rpc_url` to use when forking
#[must_use]
pub fn with_eth_rpc_url<U: Into<String>>(mut self, eth_rpc_url: Option<U>) -> Self {
self.eth_rpc_url = eth_rpc_url.map(Into::into);
pub fn with_eth_rpc_url(mut self, eth_rpc_url: Option<Vec<String>>) -> Self {
self.eth_rpc_url = eth_rpc_url;
self
}

Expand Down Expand Up @@ -1142,8 +1142,8 @@ impl NodeConfig {
);

let (db, fork): (Arc<TokioRwLock<Box<dyn Db>>>, Option<ClientFork>) =
if let Some(eth_rpc_url) = self.eth_rpc_url.clone() {
self.setup_fork_db(eth_rpc_url, &mut evm_env, &fees).await?
if let Some(eth_rpc_urls) = self.eth_rpc_url.clone() {
self.setup_fork_db(eth_rpc_urls, &mut evm_env, &fees).await?
} else {
(Arc::new(TokioRwLock::new(Box::<MemDb>::default())), None)
};
Expand Down Expand Up @@ -1223,11 +1223,11 @@ impl NodeConfig {
/// - mutating some members of `self`
pub async fn setup_fork_db(
&mut self,
eth_rpc_url: String,
eth_rpc_urls: Vec<String>,
evm_env: &mut EvmEnv,
fees: &FeeManager,
) -> Result<(Arc<TokioRwLock<Box<dyn Db>>>, Option<ClientFork>)> {
let (db, config) = self.setup_fork_db_config(eth_rpc_url, evm_env, fees).await?;
let (db, config) = self.setup_fork_db_config(eth_rpc_urls, evm_env, fees).await?;
let db: Arc<TokioRwLock<Box<dyn Db>>> = Arc::new(TokioRwLock::new(Box::new(db)));
let fork = ClientFork::new(config, Arc::clone(&db));
Ok((db, Some(fork)))
Expand All @@ -1240,18 +1240,25 @@ impl NodeConfig {
/// - mutating some members of `self`
pub async fn setup_fork_db_config(
&mut self,
eth_rpc_url: String,
eth_rpc_urls: Vec<String>,
evm_env: &mut EvmEnv,
fees: &FeeManager,
) -> Result<(ForkedDatabase<AnyNetwork>, ClientForkConfig)> {
debug!(target: "node", ?eth_rpc_url, "setting up fork db");
let primary_url = eth_rpc_urls.first().expect("at least one fork URL required").clone();
let additional_urls: Vec<url::Url> = eth_rpc_urls[1..]
.iter()
.map(|u| url::Url::parse(u).wrap_err_with(|| format!("invalid fork URL: {u}")))
.collect::<Result<Vec<_>>>()?;

debug!(target: "node", ?primary_url, additional_count = additional_urls.len(), "setting up fork db");
let provider = Arc::new(
ProviderBuilder::new(&eth_rpc_url)
ProviderBuilder::new(&primary_url)
.timeout(self.fork_request_timeout)
.initial_backoff(self.fork_retry_backoff.as_millis() as u64)
.compute_units_per_second(self.compute_units_per_second)
.max_retry(self.fork_request_retries)
.headers(self.fork_headers.clone())
.additional_urls(additional_urls)
.build()
.wrap_err("failed to establish provider to fork url")?,
);
Expand Down Expand Up @@ -1399,7 +1406,7 @@ latest block number: {latest_block}"
self.networks,
);

let meta = BlockchainDbMeta::new(evm_env.block_env.clone(), eth_rpc_url.clone());
let meta = BlockchainDbMeta::new(evm_env.block_env.clone(), primary_url.clone());
let block_chain_db = if self.fork_chain_id.is_some() {
BlockchainDb::new_skip_check(meta, self.block_cache_path(fork_block_number))
} else {
Expand All @@ -1415,8 +1422,10 @@ latest block number: {latest_block}"
)
.await;

let additional_rpc_urls: Vec<String> = eth_rpc_urls[1..].to_vec();
let config = ClientForkConfig {
eth_rpc_url,
eth_rpc_url: primary_url,
additional_rpc_urls,
block_number: fork_block_number,
block_hash,
transaction_hash: self.fork_choice.and_then(|fc| fc.transaction_hash()),
Expand Down
12 changes: 12 additions & 0 deletions crates/anvil/src/eth/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use super::{
use crate::{
ClientFork, LoggingManager, Miner, MiningMode, StorageInfo,
eth::{
multicall_prefetch,
backend::{
self,
db::SerializableState,
Expand Down Expand Up @@ -2305,6 +2306,17 @@ impl EthApi<FoundryNetwork> {
return Ok(fork.call(&request, Some(number.into())).await?);
}

// Prefetch storage for Multicall3 calls in fork mode
if let Some(fork) = self.get_fork() {
if multicall_prefetch::is_multicall3_aggregate(&request) {
let block_id = match &block_request {
BlockRequest::Number(n) => BlockId::Number(BlockNumber::Number(*n)),
_ => BlockId::default(),
};
multicall_prefetch::prefetch_multicall(&fork, &request, block_id).await;
}
}

let fees = FeeDetails::new(
request.gas_price,
request.max_fee_per_gas,
Expand Down
30 changes: 20 additions & 10 deletions crates/anvil/src/eth/backend/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ impl<N: Network> ClientFork<N> {
self.config.read().chain_id
}

fn provider(&self) -> Arc<RetryProvider<N>> {
pub fn provider(&self) -> Arc<RetryProvider<N>> {
self.config.read().provider.clone()
}

Expand Down Expand Up @@ -634,6 +634,8 @@ impl ClientFork {
#[derive(Clone, Debug)]
pub struct ClientForkConfig<N: Network = AnyNetwork> {
pub eth_rpc_url: String,
/// Additional RPC URLs for load balancing
pub additional_rpc_urls: Vec<String>,
/// The block number of the forked block
pub block_number: u64,
/// The hash of the forked block
Expand Down Expand Up @@ -672,16 +674,24 @@ impl<N: Network> ClientForkConfig<N> {
///
/// This will fail if no new provider could be established (erroneous URL)
fn update_url(&mut self, url: String) -> Result<(), BlockchainError> {
// let interval = self.provider.get_interval();
let mut builder = ProviderBuilder::<N>::new(url.as_str())
.timeout(self.timeout)
.max_retry(self.retries)
.initial_backoff(self.backoff.as_millis() as u64)
.compute_units_per_second(self.compute_units_per_second);

if !self.additional_rpc_urls.is_empty() {
let additional: Vec<url::Url> = self.additional_rpc_urls.iter()
.map(|u| url::Url::parse(u).map_err(|e| {
BlockchainError::InvalidUrl(format!("invalid fork URL: {u}: {e}"))
}))
.collect::<Result<Vec<_>, _>>()?;
builder = builder.additional_urls(additional);
}

self.provider = Arc::new(
ProviderBuilder::<N>::new(url.as_str())
.timeout(self.timeout)
// .timeout_retry(self.retries)
.max_retry(self.retries)
.initial_backoff(self.backoff.as_millis() as u64)
.compute_units_per_second(self.compute_units_per_second)
.build()
.map_err(|e| BlockchainError::InvalidUrl(format!("{url}: {e}")))?, /* .interval(interval), */
builder.build()
.map_err(|e| BlockchainError::InvalidUrl(format!("{url}: {e}")))?,
);
trace!(target: "fork", "Updated rpc url {}", url);
self.eth_rpc_url = url;
Expand Down
Loading