Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changelog/anvil-fork-cache-isolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
anvil: patch
---

Isolate persisted fork caches by RPC endpoint.
4 changes: 2 additions & 2 deletions crates/anvil/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ pub struct NodeArgs {
/// Path to the cache directory where persisted states are stored (see
/// `--max-persisted-states`).
///
/// Note: This does not affect the fork RPC cache location (`storage.json`), which is stored in
/// `~/.foundry/cache/rpc/<chain>/<block>/`.
/// Note: This does not affect the fork RPC cache location, which uses endpoint-specific files
/// under `~/.foundry/cache/rpc/<chain>/<block>/`.
#[arg(long, value_name = "PATH")]
pub cache_path: Option<PathBuf>,
}
Expand Down
30 changes: 21 additions & 9 deletions crates/anvil/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ use alloy_eips::{eip1559::BaseFeeParams, eip7840::BlobParams};
use alloy_evm::EvmEnv;
use alloy_genesis::Genesis;
use alloy_network::{AnyNetwork, BlockResponse, TransactionResponse};
use alloy_primitives::{Address, BlockNumber, TxHash, U256, hex, map::HashMap, utils::Unit};
use alloy_primitives::{
Address, BlockNumber, TxHash, U256, hex, keccak256, map::HashMap, utils::Unit,
};
use alloy_provider::Provider;
use alloy_rpc_types::BlockNumberOrTag;
use alloy_signer::Signer;
Expand Down Expand Up @@ -1068,16 +1070,23 @@ impl NodeConfig {
Ok(())
}

/// Returns the path where the cache file should be stored
/// Returns the endpoint-specific path where the cache file should be stored.
///
/// See also [ Config::foundry_block_cache_file()]
/// See also [`Config::foundry_block_cache_file`].
pub fn block_cache_path(&self, block: u64) -> Option<PathBuf> {
self.block_cache_path_for_rpc(block, self.fork_urls.first()?)
}

fn block_cache_path_for_rpc(&self, block: u64, rpc_url: &str) -> Option<PathBuf> {
if self.no_storage_caching || self.fork_urls.is_empty() {
return None;
}
let chain_id = self.get_chain_id();

Config::foundry_block_cache_file(chain_id, block)
let rpc_url_hash = hex::encode(keccak256(rpc_url));
Some(
Config::foundry_block_cache_file(chain_id, block)?
.with_file_name(format!("storage-{rpc_url_hash}.json")),
)
}

/// Sets whether to disable the default create2 deployer
Expand Down Expand Up @@ -1137,8 +1146,8 @@ impl NodeConfig {

/// Sets the path where persisted states are cached (used with `max_persisted_states`).
///
/// Note: This does not control the fork RPC cache location (`storage.json`), which uses
/// `~/.foundry/cache/rpc/<chain>/<block>/` via [`Config::foundry_block_cache_file`].
/// Note: This does not control the fork RPC cache location, which uses endpoint-specific files
/// under `~/.foundry/cache/rpc/<chain>/<block>/`.
#[must_use]
pub fn with_cache_path(mut self, cache_path: Option<PathBuf>) -> Self {
self.cache_path = cache_path;
Expand Down Expand Up @@ -1513,9 +1522,12 @@ latest block number: {latest_block}"

let meta = BlockchainDbMeta::new(cache_block_env, 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))
BlockchainDb::new_skip_check(
meta,
self.block_cache_path_for_rpc(fork_block_number, &eth_rpc_url),
)
} else {
BlockchainDb::new(meta, self.block_cache_path(fork_block_number))
BlockchainDb::new(meta, self.block_cache_path_for_rpc(fork_block_number, &eth_rpc_url))
};

// After bootstrap, rebuild the provider with round-robin if multiple URLs are
Expand Down
54 changes: 54 additions & 0 deletions crates/anvil/tests/it/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,60 @@ async fn test_fork_reset_reuses_cached_remote_state() {
let _ = std::fs::remove_dir_all(cache_dir);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_fork_reset_block_zero_does_not_reuse_cache_for_new_rpc_url() {
let address = Address::random();
let first_balance = U256::from(1337u64);
let second_balance = U256::from(42u64);
let timestamp = 1_000_000u64;
let chain_id =
u64::from_be_bytes(address.as_slice()[12..].try_into().unwrap()) % 1_000_000 + 1_000_000;
let cache_dir = Config::foundry_chain_cache_dir(chain_id).unwrap();
let _ = std::fs::remove_dir_all(&cache_dir);

async {
let first_origin = NodeConfig::test()
.with_chain_id(Some(chain_id))
.with_genesis_timestamp(Some(timestamp))
.with_funded_accounts([(address, first_balance)].into_iter().collect());
let (_first_origin_api, first_origin_handle) = spawn(first_origin).await;
let second_origin = NodeConfig::test()
.with_chain_id(Some(chain_id))
.with_genesis_timestamp(Some(timestamp))
.with_funded_accounts([(address, second_balance)].into_iter().collect());
let (_second_origin_api, second_origin_handle) = spawn(second_origin).await;
let fork_config = NodeConfig::test()
.with_chain_id(Some(chain_id))
.with_eth_rpc_url(Some(first_origin_handle.http_endpoint()))
.with_fork_block_number(Some(0u64));
let (api, handle) = spawn(fork_config).await;
let provider = handle.http_provider();

assert_eq!(provider.get_balance(address).await.unwrap(), first_balance);

api.anvil_reset(Some(Forking {
json_rpc_url: Some(second_origin_handle.http_endpoint()),
block_number: Some(0u64),
}))
.await
.unwrap();

assert_eq!(provider.get_balance(address).await.unwrap(), second_balance);

let second_fork_config = NodeConfig::test()
.with_chain_id(Some(chain_id))
.with_eth_rpc_url(Some(second_origin_handle.http_endpoint()))
.with_fork_block_number(Some(0u64));
let (_second_fork_api, second_fork_handle) = spawn(second_fork_config).await;
assert_eq!(
second_fork_handle.http_provider().get_balance(address).await.unwrap(),
second_balance
);
}
.await;
let _ = std::fs::remove_dir_all(cache_dir);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_fork_reset_does_not_reuse_cache_for_new_rpc_url() {
let address = Address::random();
Expand Down
48 changes: 43 additions & 5 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2467,16 +2467,35 @@ impl Config {
for block in chain_path.read_dir()?.flatten() {
let file_type = block.file_type()?;
let file_name = block.file_name();
let filepath = if file_type.is_dir() {
block.path().join("storage.json")
let size = if file_type.is_dir() {
let mut size = 0;
for cache_file in block.path().read_dir()?.flatten() {
let cache_file_name = cache_file.file_name();
let cache_file_name = cache_file_name.to_string_lossy();
if cache_file.file_type()?.is_file()
&& (cache_file_name == "storage.json"
|| cache_file_name
.strip_prefix("storage-")
.and_then(|name| name.strip_suffix(".json"))
.is_some_and(|hash| {
hash.len() == 64 && hash.bytes().all(|b| b.is_ascii_hexdigit())
}))
{
size += cache_file.metadata()?.len();
}
}
if size == 0 {
continue;
}
size
} else if file_type.is_file()
&& file_name.to_string_lossy().chars().all(char::is_numeric)
{
block.path()
block.metadata()?.len()
} else {
continue;
};
blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
blocks.push((file_name.to_string_lossy().into_owned(), size));
}
Ok(blocks)
}
Expand Down Expand Up @@ -5615,6 +5634,18 @@ mod tests {
writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
}

fn fake_endpoint_block_cache(
chain_path: &Path,
block_number: &str,
endpoint: &str,
size_bytes: usize,
) {
let block_path = chain_path.join(block_number);
let file_path = block_path.join(format!("storage-{endpoint}.json"));
let mut file = File::create(file_path).unwrap();
writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
}

fn fake_block_cache_block_path_as_file(
chain_path: &Path,
block_number: &str,
Expand All @@ -5628,6 +5659,13 @@ mod tests {
let chain_dir = tempdir()?;

fake_block_cache(chain_dir.path(), "1", 100);
fake_endpoint_block_cache(
chain_dir.path(),
"1",
"0000000000000000000000000000000000000000000000000000000000000000",
50,
);
fake_endpoint_block_cache(chain_dir.path(), "1", "backup", 75);
fake_block_cache(chain_dir.path(), "2", 500);
fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
// Pollution file that should not show up in the cached block
Expand All @@ -5642,7 +5680,7 @@ mod tests {
let block3 = &result.iter().find(|x| x.0 == "3").unwrap();

assert_eq!(block1.0, "1");
assert_eq!(block1.1, 100);
assert_eq!(block1.1, 150);
assert_eq!(block2.0, "2");
assert_eq!(block2.1, 500);
assert_eq!(block3.0, "3");
Expand Down
Loading