use crate::{
cmd::StateFile,
eth::{
backend::{
db::{Db, SerializableState},
fork::{ClientFork, ClientForkConfig},
genesis::GenesisConfig,
mem::fork_db::ForkedDatabase,
time::duration_since_unix_epoch,
},
fees::{INITIAL_BASE_FEE, INITIAL_GAS_PRICE},
pool::transactions::{PoolTransaction, TransactionOrder},
},
hardfork::{ChainHardfork, OptimismHardfork},
mem::{self, in_memory_db::MemDb},
EthereumHardfork, FeeManager, PrecompileFactory,
};
use alloy_consensus::BlockHeader;
use alloy_genesis::Genesis;
use alloy_network::{AnyNetwork, TransactionResponse};
use alloy_primitives::{hex, map::HashMap, utils::Unit, BlockNumber, TxHash, U256};
use alloy_provider::Provider;
use alloy_rpc_types::{Block, BlockNumberOrTag};
use alloy_signer::Signer;
use alloy_signer_local::{
coins_bip39::{English, Mnemonic},
MnemonicBuilder, PrivateKeySigner,
};
use alloy_transport::{Transport, TransportError};
use anvil_server::ServerConfig;
use eyre::Result;
use foundry_common::{
provider::{ProviderBuilder, RetryProvider},
ALCHEMY_FREE_TIER_CUPS, NON_ARCHIVE_NODE_WARNING, REQUEST_TIMEOUT,
};
use foundry_config::Config;
use foundry_evm::{
backend::{BlockchainDb, BlockchainDbMeta, SharedBackend},
constants::DEFAULT_CREATE2_DEPLOYER,
revm::primitives::{BlockEnv, CfgEnv, CfgEnvWithHandlerCfg, EnvWithHandlerCfg, SpecId, TxEnv},
utils::apply_chain_and_block_specific_env_changes,
};
use itertools::Itertools;
use parking_lot::RwLock;
use rand::thread_rng;
use revm::primitives::BlobExcessGasAndPrice;
use serde_json::{json, to_writer, Value};
use std::{
fmt::Write as FmtWrite,
fs::File,
net::{IpAddr, Ipv4Addr},
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use yansi::Paint;
pub const NODE_PORT: u16 = 8545;
pub const CHAIN_ID: u64 = 31337;
pub const DEFAULT_GAS_LIMIT: u128 = 30_000_000;
pub const DEFAULT_MNEMONIC: &str = "test test test test test test test test test test test junk";
pub const DEFAULT_IPC_ENDPOINT: &str =
if cfg!(unix) { "/tmp/anvil.ipc" } else { r"\\.\pipe\anvil.ipc" };
pub const VERSION_MESSAGE: &str = concat!(
env!("CARGO_PKG_VERSION"),
" (",
env!("VERGEN_GIT_SHA"),
" ",
env!("VERGEN_BUILD_TIMESTAMP"),
")"
);
const BANNER: &str = r"
_ _
(_) | |
__ _ _ __ __ __ _ | |
/ _` | | '_ \ \ \ / / | | | |
| (_| | | | | | \ V / | | | |
\__,_| |_| |_| \_/ |_| |_|
";
#[derive(Clone, Debug)]
pub struct NodeConfig {
pub chain_id: Option<u64>,
pub gas_limit: Option<u128>,
pub disable_block_gas_limit: bool,
pub gas_price: Option<u128>,
pub base_fee: Option<u64>,
pub disable_min_priority_fee: bool,
pub blob_excess_gas_and_price: Option<BlobExcessGasAndPrice>,
pub hardfork: Option<ChainHardfork>,
pub genesis_accounts: Vec<PrivateKeySigner>,
pub genesis_balance: U256,
pub genesis_timestamp: Option<u64>,
pub signer_accounts: Vec<PrivateKeySigner>,
pub block_time: Option<Duration>,
pub no_mining: bool,
pub mixed_mining: bool,
pub port: u16,
pub max_transactions: usize,
pub eth_rpc_url: Option<String>,
pub fork_choice: Option<ForkChoice>,
pub fork_headers: Vec<String>,
pub fork_chain_id: Option<U256>,
pub account_generator: Option<AccountGenerator>,
pub enable_tracing: bool,
pub no_storage_caching: bool,
pub server_config: ServerConfig,
pub host: Vec<IpAddr>,
pub transaction_order: TransactionOrder,
pub config_out: Option<String>,
pub genesis: Option<Genesis>,
pub fork_request_timeout: Duration,
pub fork_request_retries: u32,
pub fork_retry_backoff: Duration,
pub compute_units_per_second: u64,
pub ipc_path: Option<Option<String>>,
pub enable_steps_tracing: bool,
pub print_logs: bool,
pub enable_auto_impersonate: bool,
pub code_size_limit: Option<usize>,
pub prune_history: PruneStateHistoryConfig,
pub max_persisted_states: Option<usize>,
pub init_state: Option<SerializableState>,
pub transaction_block_keeper: Option<usize>,
pub disable_default_create2_deployer: bool,
pub enable_optimism: bool,
pub slots_in_an_epoch: u64,
pub memory_limit: Option<u64>,
pub precompile_factory: Option<Arc<dyn PrecompileFactory>>,
pub alphanet: bool,
pub silent: bool,
pub cache_path: Option<PathBuf>,
}
impl NodeConfig {
fn as_string(&self, fork: Option<&ClientFork>) -> String {
let mut config_string: String = String::new();
let _ = write!(config_string, "\n{}", BANNER.green());
let _ = write!(config_string, "\n {VERSION_MESSAGE}");
let _ = write!(config_string, "\n {}", "https://github.com/foundry-rs/foundry".green());
let _ = write!(
config_string,
r#"
Available Accounts
==================
"#
);
let balance = alloy_primitives::utils::format_ether(self.genesis_balance);
for (idx, wallet) in self.genesis_accounts.iter().enumerate() {
write!(config_string, "\n({idx}) {} ({balance} ETH)", wallet.address()).unwrap();
}
let _ = write!(
config_string,
r#"
Private Keys
==================
"#
);
for (idx, wallet) in self.genesis_accounts.iter().enumerate() {
let hex = hex::encode(wallet.credential().to_bytes());
let _ = write!(config_string, "\n({idx}) 0x{hex}");
}
if let Some(ref gen) = self.account_generator {
let _ = write!(
config_string,
r#"
Wallet
==================
Mnemonic: {}
Derivation path: {}
"#,
gen.phrase,
gen.get_derivation_path()
);
}
if let Some(fork) = fork {
let _ = write!(
config_string,
r#"
Fork
==================
Endpoint: {}
Block number: {}
Block hash: {:?}
Chain ID: {}
"#,
fork.eth_rpc_url(),
fork.block_number(),
fork.block_hash(),
fork.chain_id()
);
if let Some(tx_hash) = fork.transaction_hash() {
let _ = writeln!(config_string, "Transaction hash: {tx_hash}");
}
} else {
let _ = write!(
config_string,
r#"
Chain ID
==================
{}
"#,
self.get_chain_id().green()
);
}
if (SpecId::from(self.get_hardfork()) as u8) < (SpecId::LONDON as u8) {
let _ = write!(
config_string,
r#"
Gas Price
==================
{}
"#,
self.get_gas_price().green()
);
} else {
let _ = write!(
config_string,
r#"
Base Fee
==================
{}
"#,
self.get_base_fee().green()
);
}
let _ = write!(
config_string,
r#"
Gas Limit
==================
{}
"#,
{
if self.disable_block_gas_limit {
"Disabled".to_string()
} else {
self.gas_limit.map(|l| l.to_string()).unwrap_or_else(|| {
if self.fork_choice.is_some() {
"Forked".to_string()
} else {
DEFAULT_GAS_LIMIT.to_string()
}
})
}
}
.green()
);
let _ = write!(
config_string,
r#"
Genesis Timestamp
==================
{}
"#,
self.get_genesis_timestamp().green()
);
config_string
}
fn as_json(&self, fork: Option<&ClientFork>) -> Value {
let mut wallet_description = HashMap::new();
let mut available_accounts = Vec::with_capacity(self.genesis_accounts.len());
let mut private_keys = Vec::with_capacity(self.genesis_accounts.len());
for wallet in &self.genesis_accounts {
available_accounts.push(format!("{:?}", wallet.address()));
private_keys.push(format!("0x{}", hex::encode(wallet.credential().to_bytes())));
}
if let Some(ref gen) = self.account_generator {
let phrase = gen.get_phrase().to_string();
let derivation_path = gen.get_derivation_path().to_string();
wallet_description.insert("derivation_path".to_string(), derivation_path);
wallet_description.insert("mnemonic".to_string(), phrase);
};
let gas_limit = match self.gas_limit {
Some(_) | None if self.disable_block_gas_limit => Some(u64::MAX.to_string()),
Some(limit) => Some(limit.to_string()),
_ => None,
};
if let Some(fork) = fork {
json!({
"available_accounts": available_accounts,
"private_keys": private_keys,
"endpoint": fork.eth_rpc_url(),
"block_number": fork.block_number(),
"block_hash": fork.block_hash(),
"chain_id": fork.chain_id(),
"wallet": wallet_description,
"base_fee": format!("{}", self.get_base_fee()),
"gas_price": format!("{}", self.get_gas_price()),
"gas_limit": gas_limit,
})
} else {
json!({
"available_accounts": available_accounts,
"private_keys": private_keys,
"wallet": wallet_description,
"base_fee": format!("{}", self.get_base_fee()),
"gas_price": format!("{}", self.get_gas_price()),
"gas_limit": gas_limit,
"genesis_timestamp": format!("{}", self.get_genesis_timestamp()),
})
}
}
}
impl NodeConfig {
#[doc(hidden)]
pub fn test() -> Self {
Self { enable_tracing: true, port: 0, silent: true, ..Default::default() }
}
pub fn empty_state() -> Self {
Self {
genesis_accounts: vec![],
signer_accounts: vec![],
disable_default_create2_deployer: true,
..Default::default()
}
}
}
impl Default for NodeConfig {
fn default() -> Self {
let genesis_accounts = AccountGenerator::new(10).phrase(DEFAULT_MNEMONIC).gen();
Self {
chain_id: None,
gas_limit: None,
disable_block_gas_limit: false,
gas_price: None,
hardfork: None,
signer_accounts: genesis_accounts.clone(),
genesis_timestamp: None,
genesis_accounts,
genesis_balance: Unit::ETHER.wei().saturating_mul(U256::from(100u64)),
block_time: None,
no_mining: false,
mixed_mining: false,
port: NODE_PORT,
max_transactions: 1_000,
eth_rpc_url: None,
fork_choice: None,
account_generator: None,
base_fee: None,
disable_min_priority_fee: false,
blob_excess_gas_and_price: None,
enable_tracing: true,
enable_steps_tracing: false,
print_logs: true,
enable_auto_impersonate: false,
no_storage_caching: false,
server_config: Default::default(),
host: vec![IpAddr::V4(Ipv4Addr::LOCALHOST)],
transaction_order: Default::default(),
config_out: None,
genesis: None,
fork_request_timeout: REQUEST_TIMEOUT,
fork_headers: vec![],
fork_request_retries: 5,
fork_retry_backoff: Duration::from_millis(1_000),
fork_chain_id: None,
compute_units_per_second: ALCHEMY_FREE_TIER_CUPS,
ipc_path: None,
code_size_limit: None,
prune_history: Default::default(),
max_persisted_states: None,
init_state: None,
transaction_block_keeper: None,
disable_default_create2_deployer: false,
enable_optimism: false,
slots_in_an_epoch: 32,
memory_limit: None,
precompile_factory: None,
alphanet: false,
silent: false,
cache_path: None,
}
}
}
impl NodeConfig {
#[must_use]
pub fn with_memory_limit(mut self, mems_value: Option<u64>) -> Self {
self.memory_limit = mems_value;
self
}
pub fn get_base_fee(&self) -> u64 {
self.base_fee
.or_else(|| self.genesis.as_ref().and_then(|g| g.base_fee_per_gas.map(|g| g as u64)))
.unwrap_or(INITIAL_BASE_FEE)
}
pub fn get_gas_price(&self) -> u128 {
self.gas_price.unwrap_or(INITIAL_GAS_PRICE)
}
pub fn get_blob_excess_gas_and_price(&self) -> BlobExcessGasAndPrice {
if let Some(blob_excess_gas_and_price) = &self.blob_excess_gas_and_price {
blob_excess_gas_and_price.clone()
} else if let Some(excess_blob_gas) = self.genesis.as_ref().and_then(|g| g.excess_blob_gas)
{
BlobExcessGasAndPrice::new(excess_blob_gas as u64)
} else {
BlobExcessGasAndPrice::new(0)
}
}
pub fn get_hardfork(&self) -> ChainHardfork {
if self.alphanet {
return ChainHardfork::Ethereum(EthereumHardfork::PragueEOF);
}
if let Some(hardfork) = self.hardfork {
return hardfork;
}
if self.enable_optimism {
return OptimismHardfork::default().into();
}
EthereumHardfork::default().into()
}
#[must_use]
pub fn with_code_size_limit(mut self, code_size_limit: Option<usize>) -> Self {
self.code_size_limit = code_size_limit;
self
}
#[must_use]
pub fn disable_code_size_limit(mut self, disable_code_size_limit: bool) -> Self {
if disable_code_size_limit {
self.code_size_limit = Some(usize::MAX);
}
self
}
#[must_use]
pub fn with_init_state(mut self, init_state: Option<SerializableState>) -> Self {
self.init_state = init_state;
self
}
#[must_use]
pub fn with_init_state_path(mut self, path: impl AsRef<Path>) -> Self {
self.init_state = StateFile::parse_path(path).ok().and_then(|file| file.state);
self
}
#[must_use]
pub fn with_chain_id<U: Into<u64>>(mut self, chain_id: Option<U>) -> Self {
self.set_chain_id(chain_id);
self
}
pub fn get_chain_id(&self) -> u64 {
self.chain_id
.or_else(|| self.genesis.as_ref().map(|g| g.config.chain_id))
.unwrap_or(CHAIN_ID)
}
pub fn set_chain_id(&mut self, chain_id: Option<impl Into<u64>>) {
self.chain_id = chain_id.map(Into::into);
let chain_id = self.get_chain_id();
self.genesis_accounts.iter_mut().for_each(|wallet| {
*wallet = wallet.clone().with_chain_id(Some(chain_id));
});
self.signer_accounts.iter_mut().for_each(|wallet| {
*wallet = wallet.clone().with_chain_id(Some(chain_id));
})
}
#[must_use]
pub fn with_gas_limit(mut self, gas_limit: Option<u128>) -> Self {
self.gas_limit = gas_limit;
self
}
#[must_use]
pub fn disable_block_gas_limit(mut self, disable_block_gas_limit: bool) -> Self {
self.disable_block_gas_limit = disable_block_gas_limit;
self
}
#[must_use]
pub fn with_gas_price(mut self, gas_price: Option<u128>) -> Self {
self.gas_price = gas_price;
self
}
#[must_use]
pub fn set_pruned_history(mut self, prune_history: Option<Option<usize>>) -> Self {
self.prune_history = PruneStateHistoryConfig::from_args(prune_history);
self
}
#[must_use]
pub fn with_max_persisted_states<U: Into<usize>>(
mut self,
max_persisted_states: Option<U>,
) -> Self {
self.max_persisted_states = max_persisted_states.map(Into::into);
self
}
#[must_use]
pub fn with_transaction_block_keeper<U: Into<usize>>(
mut self,
transaction_block_keeper: Option<U>,
) -> Self {
self.transaction_block_keeper = transaction_block_keeper.map(Into::into);
self
}
#[must_use]
pub fn with_base_fee(mut self, base_fee: Option<u64>) -> Self {
self.base_fee = base_fee;
self
}
#[must_use]
pub fn disable_min_priority_fee(mut self, disable_min_priority_fee: bool) -> Self {
self.disable_min_priority_fee = disable_min_priority_fee;
self
}
#[must_use]
pub fn with_genesis(mut self, genesis: Option<Genesis>) -> Self {
self.genesis = genesis;
self
}
pub fn get_genesis_timestamp(&self) -> u64 {
self.genesis_timestamp
.or_else(|| self.genesis.as_ref().map(|g| g.timestamp))
.unwrap_or_else(|| duration_since_unix_epoch().as_secs())
}
#[must_use]
pub fn with_genesis_timestamp<U: Into<u64>>(mut self, timestamp: Option<U>) -> Self {
if let Some(timestamp) = timestamp {
self.genesis_timestamp = Some(timestamp.into());
}
self
}
#[must_use]
pub fn with_hardfork(mut self, hardfork: Option<ChainHardfork>) -> Self {
self.hardfork = hardfork;
self
}
#[must_use]
pub fn with_genesis_accounts(mut self, accounts: Vec<PrivateKeySigner>) -> Self {
self.genesis_accounts = accounts;
self
}
#[must_use]
pub fn with_signer_accounts(mut self, accounts: Vec<PrivateKeySigner>) -> Self {
self.signer_accounts = accounts;
self
}
#[must_use]
pub fn with_account_generator(mut self, generator: AccountGenerator) -> Self {
let accounts = generator.gen();
self.account_generator = Some(generator);
self.with_signer_accounts(accounts.clone()).with_genesis_accounts(accounts)
}
#[must_use]
pub fn with_genesis_balance<U: Into<U256>>(mut self, balance: U) -> Self {
self.genesis_balance = balance.into();
self
}
#[must_use]
pub fn with_blocktime<D: Into<Duration>>(mut self, block_time: Option<D>) -> Self {
self.block_time = block_time.map(Into::into);
self
}
#[must_use]
pub fn with_mixed_mining<D: Into<Duration>>(
mut self,
mixed_mining: bool,
block_time: Option<D>,
) -> Self {
self.block_time = block_time.map(Into::into);
self.mixed_mining = mixed_mining;
self
}
#[must_use]
pub fn with_no_mining(mut self, no_mining: bool) -> Self {
self.no_mining = no_mining;
self
}
#[must_use]
pub fn with_slots_in_an_epoch(mut self, slots_in_an_epoch: u64) -> Self {
self.slots_in_an_epoch = slots_in_an_epoch;
self
}
#[must_use]
pub fn with_port(mut self, port: u16) -> Self {
self.port = port;
self
}
#[must_use]
pub fn with_ipc(mut self, ipc_path: Option<Option<String>>) -> Self {
self.ipc_path = ipc_path;
self
}
#[must_use]
pub fn set_config_out(mut self, config_out: Option<String>) -> Self {
self.config_out = config_out;
self
}
#[must_use]
pub fn no_storage_caching(self) -> Self {
self.with_storage_caching(true)
}
#[must_use]
pub fn with_storage_caching(mut self, storage_caching: bool) -> Self {
self.no_storage_caching = storage_caching;
self
}
#[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);
self
}
#[must_use]
pub fn with_fork_block_number<U: Into<u64>>(self, fork_block_number: Option<U>) -> Self {
self.with_fork_choice(fork_block_number.map(Into::into))
}
#[must_use]
pub fn with_fork_transaction_hash<U: Into<TxHash>>(
self,
fork_transaction_hash: Option<U>,
) -> Self {
self.with_fork_choice(fork_transaction_hash.map(Into::into))
}
#[must_use]
pub fn with_fork_choice<U: Into<ForkChoice>>(mut self, fork_choice: Option<U>) -> Self {
self.fork_choice = fork_choice.map(Into::into);
self
}
#[must_use]
pub fn with_fork_chain_id(mut self, fork_chain_id: Option<U256>) -> Self {
self.fork_chain_id = fork_chain_id.map(Into::into);
self
}
#[must_use]
pub fn with_fork_headers(mut self, headers: Vec<String>) -> Self {
self.fork_headers = headers;
self
}
#[must_use]
pub fn fork_request_timeout(mut self, fork_request_timeout: Option<Duration>) -> Self {
if let Some(fork_request_timeout) = fork_request_timeout {
self.fork_request_timeout = fork_request_timeout;
}
self
}
#[must_use]
pub fn fork_request_retries(mut self, fork_request_retries: Option<u32>) -> Self {
if let Some(fork_request_retries) = fork_request_retries {
self.fork_request_retries = fork_request_retries;
}
self
}
#[must_use]
pub fn fork_retry_backoff(mut self, fork_retry_backoff: Option<Duration>) -> Self {
if let Some(fork_retry_backoff) = fork_retry_backoff {
self.fork_retry_backoff = fork_retry_backoff;
}
self
}
#[must_use]
pub fn fork_compute_units_per_second(mut self, compute_units_per_second: Option<u64>) -> Self {
if let Some(compute_units_per_second) = compute_units_per_second {
self.compute_units_per_second = compute_units_per_second;
}
self
}
#[must_use]
pub fn with_tracing(mut self, enable_tracing: bool) -> Self {
self.enable_tracing = enable_tracing;
self
}
#[must_use]
pub fn with_steps_tracing(mut self, enable_steps_tracing: bool) -> Self {
self.enable_steps_tracing = enable_steps_tracing;
self
}
#[must_use]
pub fn with_print_logs(mut self, print_logs: bool) -> Self {
self.print_logs = print_logs;
self
}
#[must_use]
pub fn with_auto_impersonate(mut self, enable_auto_impersonate: bool) -> Self {
self.enable_auto_impersonate = enable_auto_impersonate;
self
}
#[must_use]
pub fn with_server_config(mut self, config: ServerConfig) -> Self {
self.server_config = config;
self
}
#[must_use]
pub fn with_host(mut self, host: Vec<IpAddr>) -> Self {
self.host = if host.is_empty() { vec![IpAddr::V4(Ipv4Addr::LOCALHOST)] } else { host };
self
}
#[must_use]
pub fn with_transaction_order(mut self, transaction_order: TransactionOrder) -> Self {
self.transaction_order = transaction_order;
self
}
pub fn get_ipc_path(&self) -> Option<String> {
match &self.ipc_path {
Some(path) => path.clone().or_else(|| Some(DEFAULT_IPC_ENDPOINT.to_string())),
None => None,
}
}
pub fn print(&self, fork: Option<&ClientFork>) {
if self.config_out.is_some() {
let config_out = self.config_out.as_deref().unwrap();
to_writer(
&File::create(config_out).expect("Unable to create anvil config description file"),
&self.as_json(fork),
)
.expect("Failed writing json");
}
if self.silent {
return;
}
let _ = sh_println!("{}", self.as_string(fork));
}
pub fn block_cache_path(&self, block: u64) -> Option<PathBuf> {
if self.no_storage_caching || self.eth_rpc_url.is_none() {
return None;
}
let chain_id = self.get_chain_id();
Config::foundry_block_cache_file(chain_id, block)
}
#[must_use]
pub fn with_optimism(mut self, enable_optimism: bool) -> Self {
self.enable_optimism = enable_optimism;
self
}
#[must_use]
pub fn with_disable_default_create2_deployer(mut self, yes: bool) -> Self {
self.disable_default_create2_deployer = yes;
self
}
#[must_use]
pub fn with_precompile_factory(mut self, factory: impl PrecompileFactory + 'static) -> Self {
self.precompile_factory = Some(Arc::new(factory));
self
}
#[must_use]
pub fn with_alphanet(mut self, alphanet: bool) -> Self {
self.alphanet = alphanet;
self
}
#[must_use]
pub fn silent(self) -> Self {
self.set_silent(true)
}
#[must_use]
pub fn set_silent(mut self, silent: bool) -> Self {
self.silent = silent;
self
}
#[must_use]
pub fn with_cache_path(mut self, cache_path: Option<PathBuf>) -> Self {
self.cache_path = cache_path;
self
}
pub(crate) async fn setup(&mut self) -> mem::Backend {
let mut cfg =
CfgEnvWithHandlerCfg::new_with_spec_id(CfgEnv::default(), self.get_hardfork().into());
cfg.chain_id = self.get_chain_id();
cfg.limit_contract_code_size = self.code_size_limit;
cfg.disable_eip3607 = true;
cfg.disable_block_gas_limit = self.disable_block_gas_limit;
cfg.handler_cfg.is_optimism = self.enable_optimism;
if let Some(value) = self.memory_limit {
cfg.memory_limit = value;
}
let env = revm::primitives::Env {
cfg: cfg.cfg_env,
block: BlockEnv {
gas_limit: U256::from(self.gas_limit()),
basefee: U256::from(self.get_base_fee()),
..Default::default()
},
tx: TxEnv { chain_id: self.get_chain_id().into(), ..Default::default() },
};
let mut env = EnvWithHandlerCfg::new(Box::new(env), cfg.handler_cfg);
let fees = FeeManager::new(
cfg.handler_cfg.spec_id,
self.get_base_fee(),
!self.disable_min_priority_fee,
self.get_gas_price(),
self.get_blob_excess_gas_and_price(),
);
let (db, fork): (Arc<tokio::sync::RwLock<Box<dyn Db>>>, Option<ClientFork>) =
if let Some(eth_rpc_url) = self.eth_rpc_url.clone() {
self.setup_fork_db(eth_rpc_url, &mut env, &fees).await
} else {
(Arc::new(tokio::sync::RwLock::new(Box::<MemDb>::default())), None)
};
if let Some(ref genesis) = self.genesis {
env.cfg.chain_id = genesis.config.chain_id;
env.block.timestamp = U256::from(genesis.timestamp);
if let Some(base_fee) = genesis.base_fee_per_gas {
env.block.basefee = U256::from(base_fee);
}
if let Some(number) = genesis.number {
env.block.number = U256::from(number);
}
env.block.coinbase = genesis.coinbase;
}
let genesis = GenesisConfig {
timestamp: self.get_genesis_timestamp(),
balance: self.genesis_balance,
accounts: self.genesis_accounts.iter().map(|acc| acc.address()).collect(),
genesis_init: self.genesis.clone(),
};
let backend = mem::Backend::with_genesis(
db,
Arc::new(RwLock::new(env)),
genesis,
fees,
Arc::new(RwLock::new(fork)),
self.enable_steps_tracing,
self.print_logs,
self.alphanet,
self.prune_history,
self.max_persisted_states,
self.transaction_block_keeper,
self.block_time,
self.cache_path.clone(),
Arc::new(tokio::sync::RwLock::new(self.clone())),
)
.await;
if !self.disable_default_create2_deployer && self.eth_rpc_url.is_none() {
backend
.set_create2_deployer(DEFAULT_CREATE2_DEPLOYER)
.await
.expect("Failed to create default create2 deployer");
}
if let Some(state) = self.init_state.clone() {
backend.load_state(state).await.expect("Failed to load init state");
}
backend
}
pub async fn setup_fork_db(
&mut self,
eth_rpc_url: String,
env: &mut EnvWithHandlerCfg,
fees: &FeeManager,
) -> (Arc<tokio::sync::RwLock<Box<dyn Db>>>, Option<ClientFork>) {
let (db, config) = self.setup_fork_db_config(eth_rpc_url, env, fees).await;
let db: Arc<tokio::sync::RwLock<Box<dyn Db>>> =
Arc::new(tokio::sync::RwLock::new(Box::new(db)));
let fork = ClientFork::new(config, Arc::clone(&db));
(db, Some(fork))
}
pub async fn setup_fork_db_config(
&mut self,
eth_rpc_url: String,
env: &mut EnvWithHandlerCfg,
fees: &FeeManager,
) -> (ForkedDatabase, ClientForkConfig) {
let provider = Arc::new(
ProviderBuilder::new(ð_rpc_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)
.initial_backoff(1000)
.headers(self.fork_headers.clone())
.build()
.expect("Failed to establish provider to fork url"),
);
let (fork_block_number, fork_chain_id, force_transactions) = if let Some(fork_choice) =
&self.fork_choice
{
let (fork_block_number, force_transactions) =
derive_block_and_transactions(fork_choice, &provider).await.expect(
"Failed to derive fork block number and force transactions from fork choice",
);
let chain_id = if let Some(chain_id) = self.fork_chain_id {
Some(chain_id)
} else if self.hardfork.is_none() {
let chain_id =
provider.get_chain_id().await.expect("Failed to fetch network chain ID");
if alloy_chains::NamedChain::Mainnet == chain_id {
let hardfork: EthereumHardfork = fork_block_number.into();
env.handler_cfg.spec_id = hardfork.into();
self.hardfork = Some(ChainHardfork::Ethereum(hardfork));
}
Some(U256::from(chain_id))
} else {
None
};
(fork_block_number, chain_id, force_transactions)
} else {
let bn =
find_latest_fork_block(&provider).await.expect("Failed to get fork block number");
(bn, None, None)
};
let block = provider
.get_block(BlockNumberOrTag::Number(fork_block_number).into(), false.into())
.await
.expect("Failed to get fork block");
let block = if let Some(block) = block {
block
} else {
if let Ok(latest_block) = provider.get_block_number().await {
let mut message = format!(
"Failed to get block for block number: {fork_block_number}\n\
latest block number: {latest_block}"
);
if fork_block_number <= latest_block {
message.push_str(&format!("\n{NON_ARCHIVE_NODE_WARNING}"));
}
panic!("{}", message);
}
panic!("Failed to get block for block number: {fork_block_number}")
};
let gas_limit = self.fork_gas_limit(&block);
env.block = BlockEnv {
number: U256::from(fork_block_number),
timestamp: U256::from(block.header.timestamp),
difficulty: block.header.difficulty,
prevrandao: Some(block.header.mix_hash.unwrap_or_default()),
gas_limit: U256::from(gas_limit),
coinbase: env.block.coinbase,
basefee: env.block.basefee,
..Default::default()
};
if self.base_fee.is_none() {
if let Some(base_fee) = block.header.base_fee_per_gas {
self.base_fee = Some(base_fee);
env.block.basefee = U256::from(base_fee);
let next_block_base_fee = fees.get_next_block_base_fee_per_gas(
block.header.gas_used as u128,
gas_limit,
block.header.base_fee_per_gas.unwrap_or_default(),
);
fees.set_base_fee(next_block_base_fee);
}
if let (Some(blob_excess_gas), Some(blob_gas_used)) =
(block.header.excess_blob_gas, block.header.blob_gas_used)
{
env.block.blob_excess_gas_and_price =
Some(BlobExcessGasAndPrice::new(blob_excess_gas));
let next_block_blob_excess_gas = fees
.get_next_block_blob_excess_gas(blob_excess_gas as u128, blob_gas_used as u128);
fees.set_blob_excess_gas_and_price(BlobExcessGasAndPrice::new(
next_block_blob_excess_gas,
));
}
}
if self.gas_price.is_none() {
if let Ok(gas_price) = provider.get_gas_price().await {
self.gas_price = Some(gas_price);
fees.set_gas_price(gas_price);
}
}
let block_hash = block.header.hash;
let chain_id = if let Some(chain_id) = self.chain_id {
chain_id
} else {
let chain_id = if let Some(fork_chain_id) = fork_chain_id {
fork_chain_id.to()
} else {
provider.get_chain_id().await.unwrap()
};
self.set_chain_id(Some(chain_id));
env.cfg.chain_id = chain_id;
env.tx.chain_id = chain_id.into();
chain_id
};
let override_chain_id = self.chain_id;
apply_chain_and_block_specific_env_changes::<AnyNetwork>(env, &block);
let meta = BlockchainDbMeta::new(*env.env.clone(), eth_rpc_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 {
BlockchainDb::new(meta, self.block_cache_path(fork_block_number))
};
let backend = SharedBackend::spawn_backend_thread(
Arc::clone(&provider),
block_chain_db.clone(),
Some(fork_block_number.into()),
);
let config = ClientForkConfig {
eth_rpc_url,
block_number: fork_block_number,
block_hash,
transaction_hash: self.fork_choice.and_then(|fc| fc.transaction_hash()),
provider,
chain_id,
override_chain_id,
timestamp: block.header.timestamp,
base_fee: block.header.base_fee_per_gas.map(|g| g as u128),
timeout: self.fork_request_timeout,
retries: self.fork_request_retries,
backoff: self.fork_retry_backoff,
compute_units_per_second: self.compute_units_per_second,
total_difficulty: block.header.total_difficulty.unwrap_or_default(),
blob_gas_used: block.header.blob_gas_used.map(|g| g as u128),
blob_excess_gas_and_price: env.block.blob_excess_gas_and_price.clone(),
force_transactions,
};
let mut db = ForkedDatabase::new(backend, block_chain_db);
db.insert_block_hash(U256::from(config.block_number), config.block_hash);
(db, config)
}
pub(crate) fn fork_gas_limit<T: TransactionResponse, H: BlockHeader>(
&self,
block: &Block<T, H>,
) -> u128 {
if !self.disable_block_gas_limit {
if let Some(gas_limit) = self.gas_limit {
return gas_limit;
} else if block.header.gas_limit() > 0 {
return block.header.gas_limit() as u128;
}
}
u64::MAX as u128
}
pub(crate) fn gas_limit(&self) -> u128 {
if self.disable_block_gas_limit {
return u64::MAX as u128;
}
self.gas_limit.unwrap_or(DEFAULT_GAS_LIMIT)
}
}
async fn derive_block_and_transactions(
fork_choice: &ForkChoice,
provider: &Arc<RetryProvider>,
) -> eyre::Result<(BlockNumber, Option<Vec<PoolTransaction>>)> {
match fork_choice {
ForkChoice::Block(block_number) => Ok((block_number.to_owned(), None)),
ForkChoice::Transaction(transaction_hash) => {
let transaction = provider
.get_transaction_by_hash(transaction_hash.0.into())
.await?
.ok_or(eyre::eyre!("Failed to get fork transaction by hash"))?;
let transaction_block_number = transaction.block_number.unwrap();
let transaction_block = provider
.get_block_by_number(
transaction_block_number.into(),
alloy_rpc_types::BlockTransactionsKind::Full,
)
.await?
.ok_or(eyre::eyre!("Failed to get fork block by number"))?;
let filtered_transactions = transaction_block
.transactions
.as_transactions()
.ok_or(eyre::eyre!("Failed to get transactions from full fork block"))?
.iter()
.take_while_inclusive(|&transaction| transaction.tx_hash() != transaction_hash.0)
.collect::<Vec<_>>();
let force_transactions = filtered_transactions
.iter()
.map(|&transaction| PoolTransaction::try_from(transaction.clone()))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| eyre::eyre!("Err converting to pool transactions {e}"))?;
Ok((transaction_block_number.saturating_sub(1), Some(force_transactions)))
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ForkChoice {
Block(BlockNumber),
Transaction(TxHash),
}
impl ForkChoice {
pub fn block_number(&self) -> Option<BlockNumber> {
match self {
Self::Block(block_number) => Some(*block_number),
Self::Transaction(_) => None,
}
}
pub fn transaction_hash(&self) -> Option<TxHash> {
match self {
Self::Block(_) => None,
Self::Transaction(transaction_hash) => Some(*transaction_hash),
}
}
}
impl From<TxHash> for ForkChoice {
fn from(tx_hash: TxHash) -> Self {
Self::Transaction(tx_hash)
}
}
impl From<u64> for ForkChoice {
fn from(block: u64) -> Self {
Self::Block(block)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PruneStateHistoryConfig {
pub enabled: bool,
pub max_memory_history: Option<usize>,
}
impl PruneStateHistoryConfig {
pub fn is_state_history_supported(&self) -> bool {
!self.enabled || self.max_memory_history.is_some()
}
pub fn is_config_enabled(&self) -> bool {
self.enabled
}
pub fn from_args(val: Option<Option<usize>>) -> Self {
val.map(|max_memory_history| Self { enabled: true, max_memory_history }).unwrap_or_default()
}
}
#[derive(Clone, Debug)]
pub struct AccountGenerator {
chain_id: u64,
amount: usize,
phrase: String,
derivation_path: Option<String>,
}
impl AccountGenerator {
pub fn new(amount: usize) -> Self {
Self {
chain_id: CHAIN_ID,
amount,
phrase: Mnemonic::<English>::new(&mut thread_rng()).to_phrase(),
derivation_path: None,
}
}
#[must_use]
pub fn phrase(mut self, phrase: impl Into<String>) -> Self {
self.phrase = phrase.into();
self
}
fn get_phrase(&self) -> &str {
&self.phrase
}
#[must_use]
pub fn chain_id(mut self, chain_id: impl Into<u64>) -> Self {
self.chain_id = chain_id.into();
self
}
#[must_use]
pub fn derivation_path(mut self, derivation_path: impl Into<String>) -> Self {
let mut derivation_path = derivation_path.into();
if !derivation_path.ends_with('/') {
derivation_path.push('/');
}
self.derivation_path = Some(derivation_path);
self
}
fn get_derivation_path(&self) -> &str {
self.derivation_path.as_deref().unwrap_or("m/44'/60'/0'/0/")
}
}
impl AccountGenerator {
pub fn gen(&self) -> Vec<PrivateKeySigner> {
let builder = MnemonicBuilder::<English>::default().phrase(self.phrase.as_str());
let derivation_path = self.get_derivation_path();
let mut wallets = Vec::with_capacity(self.amount);
for idx in 0..self.amount {
let builder =
builder.clone().derivation_path(format!("{derivation_path}{idx}")).unwrap();
let wallet = builder.build().unwrap().with_chain_id(Some(self.chain_id));
wallets.push(wallet)
}
wallets
}
}
pub fn anvil_dir() -> Option<PathBuf> {
Config::foundry_dir().map(|p| p.join("anvil"))
}
pub fn anvil_tmp_dir() -> Option<PathBuf> {
anvil_dir().map(|p| p.join("tmp"))
}
async fn find_latest_fork_block<P: Provider<T, AnyNetwork>, T: Transport + Clone>(
provider: P,
) -> Result<u64, TransportError> {
let mut num = provider.get_block_number().await?;
for _ in 0..2 {
if let Some(block) = provider.get_block(num.into(), false.into()).await? {
if !block.header.hash.is_zero() {
break;
}
}
num = num.saturating_sub(1)
}
Ok(num)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prune_history() {
let config = PruneStateHistoryConfig::default();
assert!(config.is_state_history_supported());
let config = PruneStateHistoryConfig::from_args(Some(None));
assert!(!config.is_state_history_supported());
let config = PruneStateHistoryConfig::from_args(Some(Some(10)));
assert!(config.is_state_history_supported());
}
}