Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions .changelog/exact-fork-ancestry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
forge: patch
chisel: patch
anvil: patch
---

Pinned fork state reads and `BLOCKHASH` ancestry to the exact resolved block across reorgs.
3 changes: 2 additions & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ solar-lint = { git = "https://github.com/paradigmxyz/solar", rev = "a38f69e4f2e2
## foundry-core
foundry-compilers = { git = "https://github.com/foundry-rs/foundry-core", rev = "94b10d5c0108da0a4e58eb05fefe24b717a5c8f0" }
foundry-block-explorers = { git = "https://github.com/foundry-rs/foundry-core", rev = "94b10d5c0108da0a4e58eb05fefe24b717a5c8f0" }
foundry-fork-db = { git = "https://github.com/foundry-rs/foundry-core", rev = "94b10d5c0108da0a4e58eb05fefe24b717a5c8f0" }
foundry-fork-db = { git = "https://github.com/foundry-rs/foundry-core", rev = "66248879ff7f5b82be4b00fb30e3cd6df8a325c8" }

## alloy-core
# alloy-dyn-abi = { path = "../../alloy-rs/core/crates/dyn-abi" }
Expand Down
55 changes: 38 additions & 17 deletions crates/anvil/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use alloy_evm::EvmEnv;
use alloy_genesis::Genesis;
use alloy_network::{AnyNetwork, AnyRpcBlock, BlockResponse, TransactionResponse};
use alloy_primitives::{
Address, BlockNumber, TxHash, U256, hex, keccak256, map::HashMap, utils::Unit,
Address, B256, BlockNumber, TxHash, U256, hex, keccak256, map::HashMap, utils::Unit,
};
use alloy_provider::Provider;
use alloy_rpc_types::BlockNumberOrTag;
Expand All @@ -38,7 +38,7 @@ use foundry_common::{
};
use foundry_config::Config;
use foundry_evm::{
backend::{BlockchainDb, BlockchainDbMeta, SharedBackend},
backend::{BlockchainDb, BlockchainDbMeta, ForkBlock, SharedBackend},
constants::DEFAULT_CREATE2_DEPLOYER,
hardfork::FoundryHardfork,
hardforks::latest_active_tempo_hardfork,
Expand Down Expand Up @@ -108,6 +108,18 @@ const BANNER: &str = r"
\__,_| |_| |_| \_/ |_| |_|
";

fn fork_source_id(urls: &[String], headers: &[String]) -> B256 {
let mut encoded = Vec::new();
for parts in [urls, headers] {
encoded.extend_from_slice(&(parts.len() as u64).to_be_bytes());
for part in parts {
encoded.extend_from_slice(&(part.len() as u64).to_be_bytes());
encoded.extend_from_slice(part.as_bytes());
}
}
keccak256(encoded)
}

/// Configurations of the EVM node
#[derive(Clone, Debug)]
pub struct NodeConfig {
Expand Down Expand Up @@ -1599,15 +1611,11 @@ latest block number: {latest_block}"
self.networks,
);

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_for_rpc(fork_block_number, &eth_rpc_url),
)
} else {
BlockchainDb::new(meta, self.block_cache_path_for_rpc(fork_block_number, &eth_rpc_url))
};
let source_id = fork_source_id(&self.fork_urls, &self.fork_headers);
let meta = BlockchainDbMeta::new(cache_block_env, eth_rpc_url.clone())
.with_fork_identity(block_hash, source_id);
let block_chain_db =
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
// configured. This ensures bootstrap used only the primary endpoint for consistency,
Expand All @@ -1630,12 +1638,14 @@ latest block number: {latest_block}"

// This will spawn the background thread that will use the provider to fetch
// blockchain data from the other client
let backend = SharedBackend::spawn_backend(
Arc::clone(&provider),
block_chain_db.clone(),
Some(fork_block_number.into()),
)
.await;
let anchor = ForkBlock::with_rpc_number(
evm_env.block_env.number.saturating_to(),
fork_block_number,
block_hash,
);
let (backend, handler) =
SharedBackend::new_with_anchor(Arc::clone(&provider), block_chain_db.clone(), anchor);
tokio::spawn(handler);

let config = ClientForkConfig {
fork_urls: self.fork_urls.clone(),
Expand Down Expand Up @@ -1989,6 +1999,17 @@ async fn find_latest_fork_block<P: Provider<AnyNetwork>>(
mod tests {
use super::*;

#[test]
fn fork_source_identity_includes_all_urls_and_headers() {
let urls = ["http://primary".to_string(), "http://fallback".to_string()];
let headers = ["Authorization: secret".to_string()];
let identity = fork_source_id(&urls, &headers);

assert_ne!(identity, fork_source_id(&urls[..1], &headers));
assert_ne!(identity, fork_source_id(&urls, &[]));
assert_ne!(identity, fork_source_id(&[urls[1].clone(), urls[0].clone()], &headers));
}

#[test]
fn test_prune_history() {
let config = PruneStateHistoryConfig::default();
Expand Down
23 changes: 0 additions & 23 deletions crates/anvil/src/eth/backend/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,29 +433,6 @@ impl<N: Network> ClientFork<N> {
self.provider().raw_request("trace_blockOpcodeGas".into(), (block_id,)).await
}

/// Reset the fork to a fresh forked state, and optionally update the fork config
pub async fn reset(
&self,
urls: Vec<String>,
block_number: impl Into<BlockId>,
) -> Result<(), BlockchainError> {
let block_number = block_number.into();
self.prepare_reset(urls.clone(), block_number).await?;
{
self.database
.write()
.await
.maybe_reset(urls.clone(), block_number)
.map_err(BlockchainError::Internal)?;
}

let number = self.block_number();
let block_hash = self.block_hash();
self.database.write().await.insert_block_hash(U256::from(number), block_hash);

Ok(())
}

/// Updates the fork configuration for a reset without modifying the current database.
pub(crate) async fn prepare_reset(
&self,
Expand Down
52 changes: 12 additions & 40 deletions crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3361,48 +3361,18 @@ impl<N: Network> Backend<N> {
)));
}
}
let fork_url = target_rpc_url
.clone()
.ok_or_else(|| BlockchainError::InvalidUrl("fork URL is missing".to_string()))?;
let fork = self
.reset_block_number(fork_url, fork_block_number, forking.json_rpc_url.is_some())
.await?;
let fork_block = fork
Comment thread
mablr marked this conversation as resolved.
Outdated
.block_by_number(fork_block_number)
.block_by_hash(fork.block_hash())
.await?
.ok_or(BlockchainError::BlockNotFound)?;
// update all settings related to the forked block
{
if let Some(fork_url) = forking.json_rpc_url {
self.reset_block_number(fork_url, fork_block_number, true).await?;
} else {
// If rpc url is unspecified, then update the fork with the new block number and
// existing rpc url, this updates the cache path
if let Some(fork_url) = target_rpc_url.clone() {
self.reset_block_number(fork_url, fork_block_number, false).await?;
}

let gas_limit = self.node_config.read().await.fork_gas_limit(&fork_block);
let mut env = self.evm_env.write();

env.cfg_env.chain_id = fork.chain_id();
env.block_env = BlockEnv {
number: U256::from(fork_block_number),
timestamp: U256::from(fork_block.header.timestamp()),
gas_limit,
difficulty: fork_block.header.difficulty(),
prevrandao: Some(fork_block.header.mix_hash().unwrap_or_default()),
// Keep previous `beneficiary` and `basefee` value
beneficiary: env.block_env.beneficiary,
basefee: env.block_env.basefee,
..env.block_env.clone()
};

// this is the base fee of the current block, but we need the base fee of
// the next block
let next_block_base_fee = self.fees.get_next_block_base_fee_per_gas(
fork_block.header.gas_used(),
gas_limit,
fork_block.header.base_fee_per_gas().unwrap_or_default(),
);

self.fees.set_base_fee(next_block_base_fee);
}

// reset the time to the timestamp of the forked block
self.time.reset(fork_block.header.timestamp());
// drop any pending next-block prevrandao override so it does not leak into a block
Expand Down Expand Up @@ -3504,8 +3474,10 @@ impl<N: Network> Backend<N> {
fork_url: String,
fork_block_number: u64,
validated_url: bool,
) -> Result<(), BlockchainError> {
) -> Result<ClientFork, BlockchainError> {
let mut node_config = self.node_config.read().await.clone();
// Recompute the next block's base fee from the newly resolved fork block.
node_config.base_fee.take();
node_config.fork_choice = Some(ForkChoice::Block(fork_block_number as i128));
let override_chain_id =
self.get_fork().and_then(|fork| fork.config.read().override_chain_id);
Expand All @@ -3523,10 +3495,10 @@ impl<N: Network> Backend<N> {
*self.db.write().await = Box::new(forked_db);
let fork = ClientFork::new(client_fork_config, Arc::clone(&self.db));
*self.node_config.write().await = node_config;
*self.fork.write() = Some(fork);
*self.fork.write() = Some(fork.clone());
*self.evm_env.write() = evm_env;

Ok(())
Ok(fork)
}

/// Reverts the state to the state snapshot identified by the given `id`.
Expand Down
40 changes: 0 additions & 40 deletions crates/anvil/tests/it/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2550,46 +2550,6 @@ async fn test_fork_reset_after_set_rpc_url_does_not_reuse_old_cache() {
let _ = std::fs::remove_dir_all(cache_dir);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_client_fork_reset_then_backend_reset_rebuilds_database() {
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()));
let (api, handle) = spawn(fork_config).await;
let provider = handle.http_provider();

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

let fork = api.get_fork().unwrap();
fork.reset(vec![second_origin_handle.http_endpoint()], fork.block_number()).await.unwrap();
api.anvil_reset(Some(Forking::default())).await.unwrap();

assert_eq!(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_get_account() {
let (_api, handle) = spawn(fork_config()).await;
Expand Down
1 change: 1 addition & 0 deletions crates/cheatcodes/src/evm/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ fn create_fork_request<FEN: FoundryEvmNetwork>(
&& ccx.state.config.rpc_storage_caching.enable_for_endpoint(&url),
url,
evm_opts,
resolved: None,
};
Ok(fork)
}
Expand Down
1 change: 1 addition & 0 deletions crates/chisel/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ async fn run_command_with_network<FEN: FoundryEvmNetwork>(
no_vm: args.no_vm,
evm_opts,
backend: None,
resolved_fork: None,
calldata: None,
ir_minimum: args.ir_minimum,
})?;
Expand Down
6 changes: 5 additions & 1 deletion crates/chisel/src/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,10 @@ impl<FEN: FoundryEvmNetwork> ChiselDispatcher<FEN> {

pub(crate) fn set_fork(&mut self, url: Option<String>) -> Result<()> {
let Some(url) = url else {
self.source_mut().config.evm_opts.fork_url = None;
let config = &mut self.source_mut().config;
config.evm_opts.fork_url = None;
config.backend = None;
config.resolved_fork = None;
sh_println!("Now using local environment.")?;
return Ok(());
};
Expand All @@ -393,6 +396,7 @@ impl<FEN: FoundryEvmNetwork> ChiselDispatcher<FEN> {
// Clear the backend so that it is re-instantiated with the new fork
// upon the next execution of the session source.
self.source_mut().config.backend = None;
self.source_mut().config.resolved_fork = None;

Ok(())
}
Expand Down
28 changes: 20 additions & 8 deletions crates/chisel/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,20 +193,32 @@ impl<FEN: FoundryEvmNetwork> SessionSource<FEN> {
}

async fn build_runner(&mut self, final_pc: usize) -> Result<ChiselRunner<FEN>> {
let (evm_env, tx_env, fork_block) =
self.config.evm_opts.env::<SpecFor<FEN>, BlockEnvFor<FEN>, TxEnvFor<FEN>>().await?;

let backend = match self.config.backend.clone() {
Some(backend) => backend,
let (evm_env, tx_env, backend) = match self.config.backend.clone() {
Some(backend) => {
let (evm_env, tx_env) = self
.config
.evm_opts
.env_with_resolved_fork::<SpecFor<FEN>, BlockEnvFor<FEN>, TxEnvFor<FEN>>(
self.config.resolved_fork.as_ref(),
)
.await?;
(evm_env, tx_env, backend)
}
None => {
let fork = self.config.evm_opts.get_fork(
let (evm_env, tx_env, resolved_fork) = self
.config
.evm_opts
.env_resolved::<SpecFor<FEN>, BlockEnvFor<FEN>, TxEnvFor<FEN>>()
.await?;
let fork = self.config.evm_opts.get_fork_resolved(
&self.config.foundry_config,
evm_env.cfg_env.chain_id,
fork_block,
resolved_fork.as_ref(),
);
let backend = Backend::spawn(fork)?;
self.config.backend = Some(backend.clone());
backend
self.config.resolved_fork = resolved_fork;
(evm_env, tx_env, backend)
}
};

Expand Down
4 changes: 4 additions & 0 deletions crates/chisel/src/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use foundry_config::{Config, SolcReq};
use foundry_evm::{
backend::Backend,
core::{bytecode::InstIter, evm::FoundryEvmNetwork},
fork::ResolvedFork,
opts::EvmOpts,
};
use semver::Version;
Expand Down Expand Up @@ -284,6 +285,9 @@ pub struct SessionSourceConfig<FEN: FoundryEvmNetwork> {
/// In-memory REVM db for the session's runner.
#[serde(skip)]
pub backend: Option<Backend<FEN>>,
/// Exact fork identity used to construct the cached backend.
#[serde(skip)]
pub resolved_fork: Option<ResolvedFork>,
/// Optionally enable traces for the REPL contract execution
pub traces: bool,
/// Optionally set calldata for the REPL contract execution
Expand Down
Loading
Loading