diff --git a/.changelog/anvil-atomic-fork-reset.md b/.changelog/anvil-atomic-fork-reset.md new file mode 100644 index 0000000000000..1fd6503d313ed --- /dev/null +++ b/.changelog/anvil-atomic-fork-reset.md @@ -0,0 +1,6 @@ +--- +anvil: patch +--- + +Made fork resets atomic so failed or concurrent resets cannot expose partially updated chain state, +and reject resets that would change the node's fixed execution-network family. diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 8680c3696a02a..489f26e51d7fb 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -762,7 +762,7 @@ impl EthApi { let _mining = self.backend.lock_mining().await; self.backend.commit_fork_reset(staged).await?; self.reset_instance_id(); - self.pool.clear(); + self.pool.reset(); self.fee_history_cache.lock().clear(); } else { let _lifecycle = self.lifecycle_lock.write().await; @@ -772,7 +772,7 @@ impl EthApi { let staged = self.backend.prepare_memory_reset().await?; self.backend.commit_memory_reset(staged).await?; self.reset_instance_id(); - self.pool.clear(); + self.pool.reset(); self.fee_history_cache.lock().clear(); } Ok(()) @@ -4561,11 +4561,13 @@ impl EthApi { /// Mines exactly one block pub async fn mine_one(&self) -> Result<()> { - let transactions = self.pool.ready_transactions().collect::>(); - let outcome = self.backend.mine_block(transactions).await?; + let batch = self.pool.mining_batch(None); + let Some((generation, outcome)) = self.backend.mine_pool_batch(batch).await? else { + return Ok(()); + }; trace!(target: "node", blocknumber = ?outcome.block_number, "mined block"); - self.pool.on_mined_block(outcome); + self.pool.on_mined_block_at_generation(generation, outcome); Ok(()) } @@ -5048,6 +5050,18 @@ mod tests { use super::*; use crate::{NodeConfig, spawn}; + #[tokio::test(flavor = "multi_thread")] + async fn reset_discards_previously_selected_mining_batch() { + let (api, _) = spawn(NodeConfig::test()).await; + let batch = api.pool.mining_batch(None); + + api.anvil_reset(None).await.unwrap(); + let best_number = api.backend.best_number(); + + assert!(api.backend.mine_pool_batch(batch).await.unwrap().is_none()); + assert_eq!(api.backend.best_number(), best_number); + } + #[tokio::test(flavor = "multi_thread")] async fn memory_reset_stages_live_fees_after_active_mining() { let (api, _handle) = spawn(NodeConfig::test()).await; diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index d79ffeabd7305..1de53811723d2 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -25,7 +25,9 @@ use crate::{ state::{state_root, storage_root, trie_accounts}, storage::MinedTransactionReceipt, }, - notifications::{ChainNotification, ChainNotifications, NewBlockNotification}, + notifications::{ + ChainNotification, ChainNotifications, FeeHistoryNotification, NewBlockNotification, + }, replay::{ ExecutedHistoricalReplay, HistoricalReplayTransaction, PreparedForkTransactionReplay, execute_historical_replay, @@ -38,7 +40,7 @@ use crate::{ error::{BlockchainError, ErrDetail, InvalidTransactionError}, fees::{FeeDetails, FeeManager, MIN_SUGGESTED_PRIORITY_FEE}, macros::node_info, - pool::transactions::PoolTransaction, + pool::{MiningBatch, transactions::PoolTransaction}, preserve_simulation_request_fields, }, mem::{ @@ -208,7 +210,7 @@ use std::{ path::{Path, PathBuf}, sync::{ Arc, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, }, time::Duration, }; @@ -1029,6 +1031,8 @@ pub struct Backend { /// Listeners for new blocks that get notified when a new block was imported or when logs were /// removed from the canonical chain due to a reorg. new_block_listeners: Arc>>>, + /// Internal block notifications with reset lifecycle metadata. + fee_history_listeners: Arc>>>, /// Keeps track of active state snapshots at a specific block. active_state_snapshots: Arc>>, enable_steps_tracing: bool, @@ -1047,6 +1051,8 @@ pub struct Backend { precompile_factory: Option>, /// Prevent race conditions during mining mining: Arc>, + /// Generation used to reject work selected before a reset. + reset_generation: Arc, /// Disable pool balance checks disable_pool_balance_checks: bool, /// Keeps startup fork-cache rollback armed until startup initialization completes. @@ -1072,6 +1078,7 @@ impl Clone for Backend { fees: self.fees.clone(), genesis: self.genesis.clone(), new_block_listeners: self.new_block_listeners.clone(), + fee_history_listeners: self.fee_history_listeners.clone(), active_state_snapshots: self.active_state_snapshots.clone(), enable_steps_tracing: self.enable_steps_tracing, print_logs: self.print_logs, @@ -1083,6 +1090,7 @@ impl Clone for Backend { slots_in_an_epoch: self.slots_in_an_epoch, precompile_factory: self.precompile_factory.clone(), mining: self.mining.clone(), + reset_generation: self.reset_generation.clone(), disable_pool_balance_checks: self.disable_pool_balance_checks, startup_fork_cache_user: self.startup_fork_cache_user.clone(), } @@ -2108,6 +2116,22 @@ impl Backend { rx } + pub(crate) fn fee_history_notifications( + &self, + ) -> futures::channel::mpsc::UnboundedReceiver { + let (tx, rx) = unbounded(); + self.fee_history_listeners.lock().push(tx); + rx + } + + pub(crate) fn reset_generation(&self) -> Arc { + Arc::clone(&self.reset_generation) + } + + pub(crate) fn current_reset_generation(&self) -> u64 { + self.reset_generation.load(Ordering::Acquire) + } + /// Returns the number of new-block listeners. Closed listeners are pruned lazily on the next /// new block notification. pub fn new_block_listeners_count(&self) -> usize { @@ -2120,8 +2144,18 @@ impl Backend { // sender half for the set self.new_block_listeners.lock().retain(|tx| !tx.is_closed()); - let notification = - ChainNotification::Block(NewBlockNotification { hash, header: Arc::new(header) }); + let header = Arc::new(header); + let fee_history_notification = FeeHistoryNotification { + hash, + header: Arc::clone(&header), + generation: self.current_reset_generation(), + blob_params: self.fees.blob_params(), + }; + self.fee_history_listeners + .lock() + .retain(|tx| tx.unbounded_send(fee_history_notification.clone()).is_ok()); + + let notification = ChainNotification::Block(NewBlockNotification { hash, header }); self.new_block_listeners .lock() @@ -2183,9 +2217,10 @@ impl Backend { let _mining_guard = self.mining.lock().await; let next_number = highest.checked_add(1)?; if let Some(block) = self.get_block(next_number) { + let blob_params = self.simulation_blob_params_at_timestamp(block.header.timestamp); Some(( block.header.base_fee_per_gas.unwrap_or_default() as u128, - block.header.blob_fee(self.blob_params()).unwrap_or_default(), + block.header.blob_fee(blob_params).unwrap_or_default(), )) } else if highest == self.best_number() { Some((self.fees().base_fee() as u128, self.fees().base_fee_per_blob_gas())) @@ -4364,6 +4399,7 @@ impl Backend { time: TimeManager::new(start_timestamp), cheats: Default::default(), new_block_listeners: Default::default(), + fee_history_listeners: Default::default(), fees, genesis, active_state_snapshots: Arc::new(Mutex::new(Default::default())), @@ -4377,6 +4413,7 @@ impl Backend { slots_in_an_epoch, precompile_factory, mining: Arc::new(tokio::sync::Mutex::new(())), + reset_generation: Arc::new(AtomicU64::new(0)), disable_pool_balance_checks, startup_fork_cache_user, }; @@ -5384,6 +5421,19 @@ where self.do_mine_block(pool_transactions).await } + /// Mines a pool batch unless it was selected before the latest reset. + pub(crate) async fn mine_pool_batch( + &self, + batch: MiningBatch, + ) -> Result)>, BlockchainError> { + let _mining_guard = self.mining.lock().await; + if batch.generation != self.current_reset_generation() { + return Ok(None); + } + let outcome = self.do_mine_block_locked(batch.transactions).await?; + Ok(Some((batch.generation, outcome))) + } + /// Replays a transaction-hash fork prefix before the live pool and miner are created. pub(crate) async fn apply_fork_transaction_replay( &self, @@ -5782,6 +5832,13 @@ where pool_transactions: Vec>>, ) -> Result, BlockchainError> { let _mining_guard = self.mining.lock().await; + self.do_mine_block_locked(pool_transactions).await + } + + async fn do_mine_block_locked( + &self, + pool_transactions: Vec>>, + ) -> Result, BlockchainError> { trace!(target: "backend", "creating new block with {} transactions", pool_transactions.len()); let (outcome, header, block_hash) = { diff --git a/crates/anvil/src/eth/backend/notifications.rs b/crates/anvil/src/eth/backend/notifications.rs index 5093424491745..c7670489ee49a 100644 --- a/crates/anvil/src/eth/backend/notifications.rs +++ b/crates/anvil/src/eth/backend/notifications.rs @@ -1,6 +1,7 @@ //! Notifications emitted from the backed use alloy_consensus::Header; +use alloy_eips::eip7840::BlobParams; use alloy_primitives::B256; use alloy_rpc_types::Log; use futures::channel::mpsc::UnboundedReceiver; @@ -37,5 +38,13 @@ pub struct NewBlockNotification { pub header: Arc
, } +#[derive(Clone, Debug)] +pub(crate) struct FeeHistoryNotification { + pub(crate) hash: B256, + pub(crate) header: Arc
, + pub(crate) generation: u64, + pub(crate) blob_params: BlobParams, +} + /// Type alias for a receiver that receives [ChainNotification] pub type ChainNotifications = UnboundedReceiver; diff --git a/crates/anvil/src/eth/fees.rs b/crates/anvil/src/eth/fees.rs index f32c431d54d75..cf9eb562b7c18 100644 --- a/crates/anvil/src/eth/fees.rs +++ b/crates/anvil/src/eth/fees.rs @@ -2,7 +2,10 @@ use std::{ collections::BTreeMap, fmt, pin::Pin, - sync::{Arc, LazyLock}, + sync::{ + Arc, LazyLock, + atomic::{AtomicU64, Ordering}, + }, task::{Context, Poll}, }; @@ -16,7 +19,7 @@ use revm::{context_interface::block::BlobExcessGasAndPrice, primitives::hardfork use tempo_hardfork::{TempoHardfork, constants::gas::tempo_t7_next_block_base_fee}; use crate::eth::{ - backend::{info::StorageInfo, notifications::ChainNotifications}, + backend::{info::StorageInfo, notifications::FeeHistoryNotification}, error::BlockchainError, }; @@ -280,10 +283,10 @@ pub struct FeeHistoryService where N::ReceiptEnvelope: TxReceipt, { - /// Live fee rules, including blob parameters replaced by fork resets. - fees: FeeManager, - /// incoming notifications about new blocks - new_blocks: ChainNotifications, + /// Current reset generation. + generation: Arc, + /// New blocks with the fee rules active when each block was mined. + new_blocks: futures::channel::mpsc::UnboundedReceiver, /// contains all fee history related entries cache: FeeHistoryCache, /// number of items to consider @@ -296,14 +299,14 @@ impl FeeHistoryService where N::ReceiptEnvelope: TxReceipt, { - pub const fn new( - fees: FeeManager, - new_blocks: ChainNotifications, + pub(crate) const fn new( + generation: Arc, + new_blocks: futures::channel::mpsc::UnboundedReceiver, cache: FeeHistoryCache, storage_info: StorageInfo, ) -> Self { Self { - fees, + generation, new_blocks, cache, fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE, @@ -317,45 +320,30 @@ where } /// Inserts a new cache entry for the given block - pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &impl BlockHeader) { - let (result, block_number) = self.create_cache_entry(hash, header); - self.insert_cache_entry(result, block_number); - } - - /// Create a new history entry for the block - fn create_cache_entry( + pub(crate) fn insert_cache_entry_for_block( &self, hash: B256, header: &impl BlockHeader, - ) -> (FeeHistoryCacheItem, Option) { - create_fee_history_cache_item(hash, header, &self.storage_info, self.fees.blob_params()) - } - - fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option) { - insert_fee_history_cache_item(&self.cache, item, block_number, self.fee_history_limit); + blob_params: BlobParams, + ) { + let (item, block_number) = + create_fee_history_cache_item(hash, header, &self.storage_info, blob_params); + self.insert_cache_entry(item, block_number, self.generation.load(Ordering::Acquire)); } -} -/// Inserts an entry into the fee history cache and trims it back to `fee_history_limit`. -/// -/// Used by the async [`FeeHistoryService`]. The `eth_feeHistory` fallback applies the same bounded -/// insertion policy to a batch under one lock. -pub(crate) fn insert_fee_history_cache_item( - cache: &FeeHistoryCache, - item: FeeHistoryCacheItem, - block_number: Option, - fee_history_limit: u64, -) { - if let Some(block_number) = block_number { - trace!(target: "fees", "insert new history item={:?} for {}", item, block_number); - let mut cache = cache.lock(); + fn insert_cache_entry( + &self, + item: FeeHistoryCacheItem, + block_number: Option, + generation: u64, + ) { + let Some(block_number) = block_number else { return }; + let mut cache = self.cache.lock(); + if generation != self.generation.load(Ordering::Acquire) { + return; + } cache.insert(block_number, item); - - // Trim to the cache limit by dropping the oldest entries (smallest block numbers). - // `pop_first` is saturating and correct regardless of insertion order, unlike the - // previous index math which could underflow when the `eth_feeHistory` fallback inserts - // entries out of order. - while cache.len() as u64 > fee_history_limit { + while cache.len() as u64 > self.fee_history_limit { cache.pop_first(); } } @@ -486,10 +474,16 @@ where let pin = self.get_mut(); while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) { - // add the imported block. - if let Some(block) = notification.as_new_block() { - pin.insert_cache_entry_for_block(block.hash, block.header.as_ref()); + if notification.generation != pin.generation.load(Ordering::Acquire) { + continue; } + let (item, block_number) = create_fee_history_cache_item( + notification.hash, + notification.header.as_ref(), + &pin.storage_info, + notification.blob_params, + ); + pin.insert_cache_entry(item, block_number, notification.generation); } Poll::Pending diff --git a/crates/anvil/src/eth/miner.rs b/crates/anvil/src/eth/miner.rs index 80084fdac64f0..f564ace1d61c0 100644 --- a/crates/anvil/src/eth/miner.rs +++ b/crates/anvil/src/eth/miner.rs @@ -1,6 +1,6 @@ //! Mines transactions -use crate::eth::pool::{Pool, transactions::PoolTransaction}; +use crate::eth::pool::{MiningBatch, Pool, transactions::PoolTransaction}; use alloy_primitives::TxHash; use futures::{ channel::mpsc::Receiver, @@ -34,6 +34,9 @@ pub struct Miner { /// /// This will register the task so we can manually wake it up if the mining mode was changed inner: Arc, + /// Transactions included into the pool before any others are. + /// Done once on startup. + force_transactions: Option<(u64, Vec>>)>, /// Transaction type handled by the associated pool. transaction: PhantomData T>, } @@ -44,6 +47,7 @@ impl Clone for Miner { mode: self.mode.clone(), generation: self.generation.clone(), inner: self.inner.clone(), + force_transactions: self.force_transactions.clone(), transaction: PhantomData, } } @@ -51,7 +55,13 @@ impl Clone for Miner { impl fmt::Debug for Miner { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Miner").field("mode", &self.mode).finish_non_exhaustive() + f.debug_struct("Miner") + .field("mode", &self.mode) + .field( + "force_transactions", + &self.force_transactions.as_ref().map(|(_, txs)| txs.len()), + ) + .finish_non_exhaustive() } } @@ -62,10 +72,32 @@ impl Miner { mode: Arc::new(RwLock::new(mode)), generation: Default::default(), inner: Default::default(), + force_transactions: None, transaction: PhantomData, } } + /// Provide transactions that will cause a block to be mined with transactions + /// as soon as the miner is polled. + /// Providing an empty list of transactions will cause the miner to mine an empty block assuming + /// there are not other transactions in the pool. + pub fn with_forced_transactions( + self, + force_transactions: Option>>, + ) -> Self { + self.with_forced_transactions_at_generation(force_transactions, 0) + } + + pub(crate) fn with_forced_transactions_at_generation( + mut self, + force_transactions: Option>>, + generation: u64, + ) -> Self { + self.force_transactions = + force_transactions.map(|tx| (generation, tx.into_iter().map(Arc::new).collect())); + self + } + /// Returns the write lock of the mining mode pub fn mode_write(&self) -> RwLockWriteGuard<'_, RawRwLock, MiningMode> { self.mode.write() @@ -143,15 +175,25 @@ impl Miner { cx: &mut Context<'_>, ) -> Poll> { self.inner.register(cx); + let mode_generation = self.generation.load(Ordering::Relaxed); + if let Some((generation, mut transactions)) = self.force_transactions.take() { + let mut batch = pool.mining_batch(None); + if generation == batch.generation { + transactions.append(&mut batch.transactions); + } + return Poll::Ready(MiningWork { + batch: MiningBatch { generation, transactions }, + generation: mode_generation, + }); + } let mut mode = self.mode.write(); - let generation = self.generation.load(Ordering::Relaxed); - mode.poll(pool, cx).map(|transactions| MiningWork { transactions, generation }) + mode.poll_batch(pool, cx).map(|batch| MiningWork { batch, generation: mode_generation }) } } /// Transactions selected by a specific mining mode generation. pub(crate) struct MiningWork { - pub(crate) transactions: Vec>>, + pub(crate) batch: MiningBatch, pub(crate) generation: u64, } @@ -227,6 +269,10 @@ impl MiningMode { pool: &Arc>, cx: &mut Context<'_>, ) -> Poll>>> { + self.poll_batch(pool, cx).map(|batch| batch.transactions) + } + + fn poll_batch(&mut self, pool: &Arc>, cx: &mut Context<'_>) -> Poll> { match self { Self::None => Poll::Pending, Self::Auto(miner) => miner.poll(pool, cx), @@ -238,12 +284,16 @@ impl MiningMode { match (auto_txs, fixed_txs) { // Both auto and fixed transactions are ready, combine them (Poll::Ready(mut auto_txs), Poll::Ready(fixed_txs)) => { - for tx in fixed_txs { + for tx in fixed_txs.transactions { // filter unique transactions - if auto_txs.iter().any(|auto_tx| auto_tx.hash() == tx.hash()) { + if auto_txs + .transactions + .iter() + .any(|auto_tx| auto_tx.hash() == tx.hash()) + { continue; } - auto_txs.push(tx); + auto_txs.transactions.push(tx); } Poll::Ready(auto_txs) } @@ -279,14 +329,10 @@ impl FixedBlockTimeMiner { Self { interval } } - fn poll( - &mut self, - pool: &Arc>, - cx: &mut Context<'_>, - ) -> Poll>>> { + fn poll(&mut self, pool: &Arc>, cx: &mut Context<'_>) -> Poll> { if self.interval.poll_tick(cx).is_ready() { // drain the pool - return Poll::Ready(pool.ready_transactions().collect()); + return Poll::Ready(pool.mining_batch(None)); } Poll::Pending } @@ -311,11 +357,7 @@ pub struct ReadyTransactionMiner { } impl ReadyTransactionMiner { - fn poll( - &mut self, - pool: &Arc>, - cx: &mut Context<'_>, - ) -> Poll>>> { + fn poll(&mut self, pool: &Arc>, cx: &mut Context<'_>) -> Poll> { // always drain the notification stream so that we're woken up as soon as there's a new tx let mut saw_new_ready = false; while let Poll::Ready(Some(_hash)) = self.rx.poll_next_unpin(cx) { @@ -342,18 +384,17 @@ impl ReadyTransactionMiner { } self.coalesce = None; - let transactions = - pool.ready_transactions().take(self.max_transactions).collect::>(); + let batch = pool.mining_batch(Some(self.max_transactions)); // there are pending transactions if we didn't drain the pool - self.has_pending_txs = Some(transactions.len() >= self.max_transactions); + self.has_pending_txs = Some(batch.transactions.len() >= self.max_transactions); - if transactions.is_empty() { + if batch.transactions.is_empty() { self.has_pending_txs = Some(false); return Poll::Pending; } - Poll::Ready(transactions) + Poll::Ready(batch) } } @@ -364,11 +405,62 @@ impl fmt::Debug for ReadyTransactionMiner { .finish_non_exhaustive() } } - #[cfg(test)] mod tests { use super::*; - use futures::{channel::mpsc, future::poll_fn}; + use alloy_primitives::{Address, hex}; + use alloy_rlp::Decodable; + use anvil_core::eth::transaction::PendingTransaction; + use foundry_primitives::FoundryTxEnvelope; + use futures::{channel::mpsc, future::poll_fn, task::noop_waker}; + + fn forced_tx() -> PoolTransaction { + let raw = hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap(); + let tx = FoundryTxEnvelope::decode(&mut &raw[..]).unwrap(); + let sender: Address = "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5".parse().unwrap(); + let pending = PendingTransaction::with_impersonated(tx, sender); + PoolTransaction::new(pending) + } + + #[test] + fn poll_consumes_forced_transactions_before_mode_is_ready() { + let forced = forced_tx(); + let forced_hash = forced.hash(); + + let pool = Arc::new(Pool::default()); + let mut miner = Miner::new(MiningMode::None).with_forced_transactions(Some(vec![forced])); + + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + + let polled = miner.poll(&pool, &mut cx); + let txs = match polled { + Poll::Ready(work) => work.batch.transactions, + Poll::Pending => panic!("expected forced transactions to be returned immediately"), + }; + assert_eq!(txs.len(), 1); + assert_eq!(txs[0].hash(), forced_hash); + + // Forced transactions are consumed exactly once. + assert!(miner.poll(&pool, &mut cx).is_pending()); + } + + #[test] + fn forced_transactions_keep_their_original_generation() { + let pool = Arc::new(Pool::default()); + let forced = forced_tx(); + let mut miner = Miner::new(MiningMode::None) + .with_forced_transactions_at_generation(Some(vec![forced]), pool.generation()); + pool.reset(); + + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + let Poll::Ready(work) = miner.poll(&pool, &mut cx) else { + panic!("expected forced transactions to be returned immediately") + }; + + assert_ne!(work.batch.generation, pool.generation()); + } #[test] fn stale_failure_resumes_replacement_autominer() { diff --git a/crates/anvil/src/eth/pool/mod.rs b/crates/anvil/src/eth/pool/mod.rs index 5790d27e25292..d0174f2704245 100644 --- a/crates/anvil/src/eth/pool/mod.rs +++ b/crates/anvil/src/eth/pool/mod.rs @@ -42,7 +42,14 @@ use alloy_rpc_types::txpool::TxpoolStatus; use anvil_core::eth::transaction::PendingTransaction; use futures::channel::mpsc::{Receiver, Sender, channel}; use parking_lot::{Mutex, RwLock}; -use std::{collections::VecDeque, fmt, sync::Arc}; +use std::{ + collections::VecDeque, + fmt, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; pub mod transactions; @@ -52,17 +59,45 @@ pub struct Pool { inner: RwLock>, /// listeners for new ready transactions transaction_listener: Mutex>>, + /// Generation shared with the backend and reset consumers. + generation: Arc, +} + +/// Transactions selected from one generation of the pool. +pub struct MiningBatch { + pub generation: u64, + pub transactions: Vec>>, } impl Default for Pool { fn default() -> Self { - Self { inner: RwLock::new(PoolInner::default()), transaction_listener: Default::default() } + Self::new(Arc::new(AtomicU64::new(0))) } } // == impl Pool == impl Pool { + pub(crate) fn new(generation: Arc) -> Self { + Self { + inner: RwLock::new(PoolInner::default()), + transaction_listener: Default::default(), + generation, + } + } + + pub(crate) fn generation(&self) -> u64 { + self.generation.load(Ordering::Acquire) + } + + /// Selects transactions while capturing the pool generation under the same lock. + pub fn mining_batch(&self, limit: Option) -> MiningBatch { + let pool = self.inner.read(); + let transactions = pool.ready_transactions().take(limit.unwrap_or(usize::MAX)).collect(); + let generation = self.generation.load(Ordering::Acquire); + MiningBatch { generation, transactions } + } + /// Returns an iterator that yields all transactions that are currently ready pub fn ready_transactions(&self) -> TransactionsIterator { self.inner.read().ready_transactions() @@ -112,6 +147,13 @@ impl Pool { pool.clear(); } + /// Clears the pool and advances its reset generation. + pub(crate) fn reset(&self) { + let mut pool = self.inner.write(); + pool.clear(); + self.generation.fetch_add(1, Ordering::AcqRel); + } + /// Remove the given transactions from the pool pub fn remove_invalid(&self, tx_hashes: Vec) -> Vec>> { self.inner.write().remove_invalid(tx_hashes) @@ -192,14 +234,27 @@ impl Pool { /// /// This will remove the transactions from the pool. pub fn on_mined_block(self: &Arc, outcome: MinedBlockOutcome) -> PruneResult { - let MinedBlockOutcome { block_number, included, invalid, not_yet_valid } = outcome; + self.on_mined_block_at_generation(self.generation(), outcome) + } - // remove invalid transactions from the pool - self.remove_invalid(invalid.into_iter().map(|tx| tx.hash()).collect()); + pub(crate) fn on_mined_block_at_generation( + self: &Arc, + generation: u64, + outcome: MinedBlockOutcome, + ) -> PruneResult { + let MinedBlockOutcome { block_number, included, invalid, not_yet_valid } = outcome; - // prune all the markers the mined transactions provide - let res = self - .prune_markers(block_number, included.into_iter().flat_map(|tx| tx.provides.clone())); + let mut pool = self.inner.write(); + if generation != self.generation.load(Ordering::Acquire) { + return PruneResult { promoted: Vec::new(), failed: Vec::new(), pruned: Vec::new() }; + } + pool.remove_invalid(invalid.into_iter().map(|tx| tx.hash()).collect()); + debug!(target: "txpool", ?block_number, "pruning transactions"); + let res = pool.prune_markers(included.into_iter().flat_map(|tx| tx.provides.clone())); + drop(pool); + for tx in &res.promoted { + self.notify_ready(tx); + } trace!(target: "txpool", "pruned transaction markers {:?}", res); // Re-notify the miner about not-yet-valid transactions so they'll be retried. diff --git a/crates/anvil/src/lib.rs b/crates/anvil/src/lib.rs index c2b0ce90f2c8b..1a73e9079033d 100644 --- a/crates/anvil/src/lib.rs +++ b/crates/anvil/src/lib.rs @@ -177,7 +177,7 @@ pub async fn try_spawn(mut config: NodeConfig) -> Result<(EthApi .. } = config.clone(); - let pool = Arc::new(Pool::default()); + let pool = Arc::new(Pool::new(backend.reset_generation())); let mode = if let Some(block_time) = block_time { if mixed_mining { @@ -213,14 +213,18 @@ pub async fn try_spawn(mut config: NodeConfig) -> Result<(EthApi let fee_history_cache = Arc::new(Mutex::new(Default::default())); let fee_history_service = FeeHistoryService::new( - backend.fees().clone(), - backend.new_block_notifications(), + backend.reset_generation(), + backend.fee_history_notifications(), Arc::clone(&fee_history_cache), StorageInfo::new(Arc::clone(&backend)), ); // create an entry for the best block if let Some(header) = backend.get_block(backend.best_number()).map(|block| block.header) { - fee_history_service.insert_cache_entry_for_block(header.hash_slow(), &header); + fee_history_service.insert_cache_entry_for_block( + header.hash_slow(), + &header, + backend.fees().blob_params(), + ); } let filters = Filters::default(); diff --git a/crates/anvil/src/service.rs b/crates/anvil/src/service.rs index 56f7a885d703a..4b2cc13ed781f 100644 --- a/crates/anvil/src/service.rs +++ b/crates/anvil/src/service.rs @@ -3,8 +3,11 @@ use crate::{ NodeResult, eth::{ - backend::validate::TransactionValidator, error::BlockchainError, fees::FeeHistoryService, - miner::Miner, pool::Pool, + backend::validate::TransactionValidator, + error::BlockchainError, + fees::FeeHistoryService, + miner::{Miner, MiningWork}, + pool::Pool, }, filter::Filters, mem::{Backend, storage::MinedBlockOutcome}, @@ -86,9 +89,9 @@ where // advance block production until pending while let Poll::Ready(Some(result)) = pin.block_producer.poll_next_unpin(cx) { match result { - BlockProduction::Mined(outcome) => { + BlockProduction::Mined(generation, outcome) => { trace!(target: "node", "mined block {}", outcome.block_number); - pin.pool.on_mined_block(outcome); + pin.pool.on_mined_block_at_generation(generation, outcome); } BlockProduction::Failed(generation) => { pin.miner.handle_failed_candidate(generation); @@ -124,11 +127,14 @@ where } } -type MiningResult = - (Result::TxEnvelope>, BlockchainError>, Arc>, u64); +type MiningResult = ( + Result::TxEnvelope>)>, BlockchainError>, + Arc>, + u64, +); enum BlockProduction { - Mined(MinedBlockOutcome), + Mined(u64, MinedBlockOutcome), Failed(u64), } @@ -140,7 +146,7 @@ struct BlockProducer { /// Single active future that mines a new block block_mining: Option>>, /// backlog of sets of transactions ready to be mined - queued: VecDeque>, + queued: VecDeque>, } impl BlockProducer @@ -179,8 +185,8 @@ where let mining = tokio::task::spawn_blocking(move || { handle.block_on(async move { trace!(target: "miner", "creating new block"); - let block = backend.mine_block(work.transactions).await; - if let Ok(block) = &block { + let block = backend.mine_pool_batch(work.batch).await; + if let Ok(Some((_, block))) = &block { trace!(target: "miner", "created new block: {}", block.block_number); } (block, backend, generation) @@ -193,15 +199,20 @@ where if let Some(mut mining) = pin.block_mining.take() { if let Poll::Ready(res) = mining.poll_unpin(cx) { return match res { - Ok((Ok(outcome), backend, _)) => { + Ok((Ok(Some((generation, outcome))), backend, _)) => { pin.idle_backend = Some(backend); - Poll::Ready(Some(BlockProduction::Mined(outcome))) + Poll::Ready(Some(BlockProduction::Mined(generation, outcome))) } - Ok((Err(error), backend, generation)) => { + Ok((Err(error), backend, mode_generation)) => { pin.idle_backend = Some(backend); pin.queued.clear(); warn!(target: "miner", %error, "failed to finalize block"); - Poll::Ready(Some(BlockProduction::Failed(generation))) + Poll::Ready(Some(BlockProduction::Failed(mode_generation))) + } + Ok((Ok(None), backend, _)) => { + pin.idle_backend = Some(backend); + cx.waker().wake_by_ref(); + Poll::Pending } Err(err) => { panic!("miner task failed: {err}"); diff --git a/crates/anvil/tests/it/anvil_api.rs b/crates/anvil/tests/it/anvil_api.rs index d0936b84b7c70..70da8ce7c936c 100644 --- a/crates/anvil/tests/it/anvil_api.rs +++ b/crates/anvil/tests/it/anvil_api.rs @@ -1679,6 +1679,7 @@ async fn test_safe_and_finalized_use_configured_slots_in_epoch() { async fn test_anvil_reset_non_fork() { let (api, handle) = spawn(NodeConfig::test()).await; let provider = handle.http_provider(); + let snapshot = api.evm_snapshot().await.unwrap(); // Get initial state let init_block = provider.get_block(BlockId::latest()).await.unwrap().unwrap(); @@ -1713,6 +1714,7 @@ async fn test_anvil_reset_non_fork() { // Reset to fresh in-memory state (non-fork) api.anvil_reset(None).await.unwrap(); + assert!(!api.backend.list_state_snapshots().contains_key(&snapshot)); // Check instance id has changed let instance_id_after = api.instance_id(); @@ -1721,7 +1723,6 @@ async fn test_anvil_reset_non_fork() { // Check we're back at genesis let block_after_reset = provider.get_block(BlockId::latest()).await.unwrap().unwrap(); assert_eq!(block_after_reset.header.number, 0); - // Check accounts are restored to initial state let balance_after_reset = provider.get_balance(init_accounts[0]).await.unwrap(); assert_eq!(balance_after_reset, init_balance); @@ -1736,14 +1737,45 @@ async fn test_anvil_reset_non_fork() { assert_eq!(new_block.header.number, 1); } +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_reset_clears_runtime_pool() { + let (api, handle) = spawn(NodeConfig::test().with_no_mining(true)).await; + let from = api.accounts().unwrap()[0]; + let tx = TransactionRequest::default().with_from(from).with_to(Address::random()); + let _pending = handle.http_provider().send_transaction(tx.into()).await.unwrap(); + assert_eq!(api.txpool_status().await.unwrap().pending, 1); + + api.anvil_reset(None).await.unwrap(); + + assert_eq!(api.txpool_status().await.unwrap().pending, 0); +} + #[tokio::test(flavor = "multi_thread")] async fn test_anvil_reset_fork_to_non_fork() { - let (api, handle) = spawn(fork_config()).await; + let remote_account = Address::random(); + let remote_balance = U256::from(123_456u64); + let configured_account = Address::random(); + let configured_balance = U256::from(654_321u64); + let (source_api, source_handle) = spawn( + NodeConfig::test() + .with_funded_accounts([(remote_account, remote_balance)].into_iter().collect()), + ) + .await; + source_api.mine_one().await.unwrap(); + let (api, handle) = spawn( + NodeConfig::test() + .with_funded_accounts([(configured_account, configured_balance)].into_iter().collect()) + .with_eth_rpc_url(Some(source_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)), + ) + .await; let provider = handle.http_provider(); // Verify we're in fork mode let metadata = api.anvil_metadata().await.unwrap(); assert!(metadata.forked_network.is_some()); + assert_eq!(provider.get_balance(remote_account).await.unwrap(), remote_balance); + assert_eq!(provider.get_balance(configured_account).await.unwrap(), configured_balance); // Mine some blocks for _ in 0..3 { @@ -1760,6 +1792,8 @@ async fn test_anvil_reset_fork_to_non_fork() { // Check we're at block 0 let block = provider.get_block(BlockId::latest()).await.unwrap().unwrap(); assert_eq!(block.header.number, 0); + assert_eq!(provider.get_balance(remote_account).await.unwrap(), U256::ZERO); + assert_eq!(provider.get_balance(configured_account).await.unwrap(), configured_balance); // Verify we can still mine blocks api.mine_one().await.unwrap(); @@ -1767,6 +1801,346 @@ async fn test_anvil_reset_fork_to_non_fork() { assert_eq!(new_block.header.number, 1); } +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_reset_fork_to_non_fork_restores_configured_hardfork() { + let (source_api, source_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::Berlin.into())) + .with_genesis_timestamp(Some(1_618_481_223u64)), + ) + .await; + source_api.evm_set_next_block_timestamp(1_618_481_224u64).unwrap(); + source_api.mine_one().await.unwrap(); + + let (api, _) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(source_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)), + ) + .await; + assert_eq!(api.backend.hardfork(), EthereumHardfork::Berlin.into()); + assert!(!api.backend.is_eip1559()); + assert_eq!(api.backend.base_fee(), 0); + + api.anvil_reset(None).await.unwrap(); + + assert_eq!(api.backend.hardfork(), NodeConfig::test().get_hardfork()); + assert!(api.backend.is_eip1559()); + assert_ne!(api.backend.base_fee(), 0); + + let (api, _) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(source_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::Berlin.into())), + ) + .await; + + api.anvil_reset(None).await.unwrap(); + + assert_eq!(api.backend.hardfork(), EthereumHardfork::Berlin.into()); + assert!(!api.backend.is_eip1559()); + assert_eq!(api.backend.base_fee(), 0); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_reset_fork_to_non_fork_restores_configured_fees() { + let (source_api, source_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::London.into())) + .with_base_fee(Some(1_000u64)), + ) + .await; + source_api.mine_one().await.unwrap(); + + let (api, handle) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(source_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)) + .with_base_fee(Some(5_000u64)) + .with_gas_limit(Some(25_000_000u64)), + ) + .await; + + api.anvil_reset(None).await.unwrap(); + + assert_eq!(api.backend.base_fee(), 5_000); + assert_eq!(api.backend.gas_limit(), 25_000_000); + let genesis = handle.http_provider().get_block(BlockId::latest()).await.unwrap().unwrap(); + assert_eq!(genesis.header.base_fee_per_gas, Some(5_000)); + + let (api, _) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(source_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::Berlin.into())) + .with_gas_price(Some(6_000u128)), + ) + .await; + api.anvil_reset(None).await.unwrap(); + assert_eq!(api.gas_price(), 6_000); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_reset_fork_refreshes_inferred_chain_id() { + let (source_a_api, source_a_handle) = spawn(NodeConfig::test().with_chain_id(Some(1u64))).await; + source_a_api.mine_one().await.unwrap(); + let (source_b_api, source_b_handle) = + spawn(NodeConfig::test().with_chain_id(Some(56u64))).await; + source_b_api.mine_one().await.unwrap(); + + let (api, handle) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(source_a_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)), + ) + .await; + assert_eq!(api.chain_id(), 1); + + api.anvil_reset(Some(Forking { + json_rpc_url: Some(source_b_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + assert_eq!(api.chain_id(), 56); + let sender = api.accounts().unwrap()[0]; + handle + .http_provider() + .send_transaction(WithOtherFields::new( + TransactionRequest::default() + .with_from(sender) + .with_to(Address::random()) + .with_value(U256::from(1u64)), + )) + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + api.anvil_reset(None).await.unwrap(); + assert_eq!(api.chain_id(), 31337); + handle + .http_provider() + .send_transaction(WithOtherFields::new( + TransactionRequest::default() + .with_from(sender) + .with_to(Address::random()) + .with_value(U256::from(1u64)), + )) + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + let (api, _) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(source_a_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)) + .with_chain_id(Some(31337u64)), + ) + .await; + api.anvil_reset(Some(Forking { + json_rpc_url: Some(source_b_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + assert_eq!(api.chain_id(), 31337); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_reset_fork_refreshes_inferred_fees() { + let (london_a_api, london_a_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::London.into())) + .with_base_fee(Some(1_000u64)) + .with_gas_limit(Some(10_000_000u64)) + .with_genesis_timestamp(Some(1_628_166_823u64)), + ) + .await; + london_a_api.evm_set_next_block_timestamp(1_628_166_824u64).unwrap(); + london_a_api.mine_one().await.unwrap(); + + let (london_b_api, london_b_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::London.into())) + .with_base_fee(Some(2_000u64)) + .with_gas_limit(Some(20_000_000u64)) + .with_genesis_timestamp(Some(1_628_166_823u64)), + ) + .await; + london_b_api.evm_set_next_block_timestamp(1_628_166_824u64).unwrap(); + london_b_api.mine_one().await.unwrap(); + + let (berlin_a_api, berlin_a_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::Berlin.into())) + .with_gas_price(Some(3_000u128)) + .with_genesis_timestamp(Some(1_618_481_223u64)), + ) + .await; + berlin_a_api.evm_set_next_block_timestamp(1_618_481_224u64).unwrap(); + berlin_a_api.mine_one().await.unwrap(); + + let (berlin_b_api, berlin_b_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::Berlin.into())) + .with_gas_price(Some(4_000u128)) + .with_genesis_timestamp(Some(1_618_481_223u64)), + ) + .await; + berlin_b_api.evm_set_next_block_timestamp(1_618_481_224u64).unwrap(); + berlin_b_api.mine_one().await.unwrap(); + + let (api, _) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(london_a_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)), + ) + .await; + let london_a_base_fee = api.backend.base_fee(); + assert_eq!(api.backend.gas_limit(), 10_000_000); + + api.anvil_reset(Some(Forking { + json_rpc_url: Some(london_b_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + let london_b_base_fee = api.backend.base_fee(); + assert_ne!(london_b_base_fee, london_a_base_fee); + assert_eq!(api.backend.gas_limit(), 20_000_000); + + api.anvil_reset(Some(Forking { + json_rpc_url: Some(berlin_a_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + assert_eq!(api.gas_price(), 3_000); + + api.anvil_reset(Some(Forking { + json_rpc_url: Some(berlin_b_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + assert_eq!(api.gas_price(), 4_000); + + api.anvil_reset(Some(Forking { + json_rpc_url: Some(london_b_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + assert_eq!(api.backend.base_fee(), london_b_base_fee); + + let (api, _) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(london_a_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)) + .with_base_fee(Some(5_000u64)) + .with_gas_limit(Some(25_000_000u64)), + ) + .await; + api.anvil_reset(Some(Forking { + json_rpc_url: Some(london_b_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + assert_eq!(api.backend.base_fee(), 5_000); + assert_eq!(api.backend.gas_limit(), 25_000_000); + + let (api, _) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(berlin_a_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)) + .with_gas_price(Some(6_000u128)), + ) + .await; + api.anvil_reset(Some(Forking { + json_rpc_url: Some(berlin_b_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + assert_eq!(api.gas_price(), 6_000); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_anvil_failed_fork_reset_preserves_live_state() { + let (source_api, source_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::London.into())) + .with_base_fee(Some(1_000u64)) + .with_genesis_timestamp(Some(1_628_166_823u64)), + ) + .await; + source_api.evm_set_next_block_timestamp(1_628_166_824u64).unwrap(); + source_api.mine_one().await.unwrap(); + + let (other_api, other_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::London.into())), + ) + .await; + other_api.mine_one().await.unwrap(); + + let (api, _) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(source_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)), + ) + .await; + let node_info = api.anvil_node_info().await.unwrap(); + let fork = api.get_fork().unwrap(); + let fork_url = fork.eth_rpc_url(); + let fork_block_number = fork.block_number(); + let fork_block_hash = fork.block_hash(); + let hardfork = api.backend.hardfork(); + let spec_id = api.backend.spec_id(); + let base_fee = api.backend.base_fee(); + let gas_price = api.gas_price(); + let instance_id = api.instance_id(); + let snapshot = api.evm_snapshot().await.unwrap(); + let overridden = Address::random(); + let overridden_balance = U256::from(123_456u64); + api.anvil_set_balance(overridden, overridden_balance).await.unwrap(); + + let result = api + .anvil_reset(Some(Forking { + json_rpc_url: Some(other_handle.http_endpoint()), + block_number: Some(999u64), + })) + .await; + assert!(result.is_err()); + + assert_eq!(api.anvil_node_info().await.unwrap(), node_info); + let fork = api.get_fork().unwrap(); + assert_eq!(fork.eth_rpc_url(), fork_url); + assert_eq!(fork.block_number(), fork_block_number); + assert_eq!(fork.block_hash(), fork_block_hash); + assert_eq!(api.backend.hardfork(), hardfork); + assert_eq!(api.backend.spec_id(), spec_id); + assert_eq!(api.backend.base_fee(), base_fee); + assert_eq!(api.gas_price(), gas_price); + assert_eq!(api.instance_id(), instance_id); + assert!(api.backend.list_state_snapshots().contains_key(&snapshot)); + assert_eq!(api.balance(overridden, None).await.unwrap(), overridden_balance); +} + #[tokio::test(flavor = "multi_thread")] async fn can_get_node_info_tempo_t0() { let config = NodeConfig::test_tempo().with_hardfork(Some(TempoHardfork::T0.into())); diff --git a/crates/anvil/tests/it/eip2935.rs b/crates/anvil/tests/it/eip2935.rs index 4ee7b68a0a983..9668b412450e6 100644 --- a/crates/anvil/tests/it/eip2935.rs +++ b/crates/anvil/tests/it/eip2935.rs @@ -248,6 +248,32 @@ async fn eip2935_contract_deployed_at_genesis() { assert!(!code.is_empty(), "EIP-2935 history storage contract should be deployed at genesis"); } +#[tokio::test(flavor = "multi_thread")] +async fn reset_restores_ethereum_system_contracts() { + let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Prague.into())); + let (api, handle) = spawn(node_config).await; + let provider = http_provider(&handle.http_endpoint()); + let addresses = [ + BEACON_ROOTS_ADDRESS, + HISTORY_STORAGE_ADDRESS, + WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, + CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, + ]; + + for address in addresses { + assert!(!provider.get_code_at(address).await.unwrap().is_empty(), "missing {address}"); + } + + api.anvil_reset(None).await.unwrap(); + + for address in addresses { + assert!( + !provider.get_code_at(address).await.unwrap().is_empty(), + "reset removed {address}" + ); + } +} + #[tokio::test(flavor = "multi_thread")] async fn eip2935_stores_parent_block_hash() { let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Prague.into())); diff --git a/crates/anvil/tests/it/eip4844.rs b/crates/anvil/tests/it/eip4844.rs index 5ea503a6c9bc7..f220860f05179 100644 --- a/crates/anvil/tests/it/eip4844.rs +++ b/crates/anvil/tests/it/eip4844.rs @@ -17,12 +17,15 @@ use alloy_network::{ }; use alloy_primitives::{Address, Bytes, U256, b256}; use alloy_provider::{Provider, ProviderBuilder}; -use alloy_rpc_types::{Authorization, BlockId, TransactionRequest}; +use alloy_rpc_types::{ + Authorization, BlockId, BlockNumberOrTag, TransactionRequest, anvil::Forking, +}; use alloy_serde::WithOtherFields; use alloy_signer::SignerSync; use anvil::{NodeConfig, spawn}; use foundry_evm::hardfork::EthereumHardfork; use foundry_test_utils::rpc; +use revm::context_interface::block::BlobExcessGasAndPrice; use serde_json::{Value, json}; #[tokio::test(flavor = "multi_thread")] @@ -125,6 +128,137 @@ async fn can_send_eip4844_transaction_fork() { let _blobs = api.anvil_get_blob_by_tx_hash(tx_hash).unwrap().unwrap(); } +#[tokio::test(flavor = "multi_thread")] +async fn fork_reset_updates_fee_history_blob_params() { + let excess_blob_gas = 50_000_000u64; + let mut cancun_config = NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::Cancun.into())) + .with_genesis_timestamp(Some(1_710_338_135u64)); + cancun_config.blob_excess_gas_and_price = Some(BlobExcessGasAndPrice::new( + excess_blob_gas, + BlobParams::cancun().update_fraction as u64, + )); + let (cancun_api, cancun_handle) = spawn(cancun_config).await; + cancun_api.mine_one().await.unwrap(); + + let mut prague_config = NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::Prague.into())) + .with_genesis_timestamp(Some(1_746_612_311u64)); + prague_config.blob_excess_gas_and_price = Some(BlobExcessGasAndPrice::new( + excess_blob_gas, + BlobParams::prague().update_fraction as u64, + )); + let (prague_api, prague_handle) = spawn(prague_config).await; + prague_api.mine_one().await.unwrap(); + + let (api, handle) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(cancun_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)) + .with_no_storage_caching(true), + ) + .await; + let provider = handle.http_provider(); + let accounts = provider.get_accounts().await.unwrap(); + + api.anvil_reset(Some(Forking { + json_rpc_url: Some(prague_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + + let sidecar: BlobTransactionSidecar = + SidecarBuilder::::from_slice(b"Prague blob").build().unwrap(); + let receipt = provider + .send_transaction(WithOtherFields::new( + TransactionRequest::default() + .with_from(accounts[0]) + .with_to(accounts[1]) + .with_max_fee_per_blob_gas(api.blob_base_fee().unwrap().to::() + 1) + .with_blob_sidecar_4844(sidecar), + )) + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + let fee_history = + api.fee_history(U256::from(1u64), BlockNumberOrTag::Latest, vec![]).await.unwrap(); + let block = provider.get_block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap(); + + assert_eq!(receipt.blob_gas_used, Some(DATA_GAS_PER_BLOB)); + assert_eq!( + fee_history.blob_gas_used_ratio, + vec![DATA_GAS_PER_BLOB as f64 / BlobParams::prague().max_blob_gas_per_block() as f64] + ); + assert_eq!( + fee_history.base_fee_per_blob_gas[0], + BlobParams::prague().calc_blob_fee(block.header.excess_blob_gas.unwrap()) + ); + + api.anvil_reset(Some(Forking { + json_rpc_url: Some(cancun_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + + let sidecar: BlobTransactionSidecar = + SidecarBuilder::::from_slice(b"Cancun blob").build().unwrap(); + let receipt = provider + .send_transaction(WithOtherFields::new( + TransactionRequest::default() + .with_from(accounts[0]) + .with_to(accounts[1]) + .with_max_fee_per_blob_gas(api.blob_base_fee().unwrap().to::() + 1) + .with_blob_sidecar_4844(sidecar), + )) + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + let fee_history = + api.fee_history(U256::from(1u64), BlockNumberOrTag::Latest, vec![]).await.unwrap(); + let block = provider.get_block_by_number(BlockNumberOrTag::Latest).await.unwrap().unwrap(); + + assert_eq!(receipt.blob_gas_used, Some(DATA_GAS_PER_BLOB)); + assert_eq!( + fee_history.blob_gas_used_ratio, + vec![DATA_GAS_PER_BLOB as f64 / BlobParams::cancun().max_blob_gas_per_block() as f64] + ); + assert_eq!( + fee_history.base_fee_per_blob_gas[0], + BlobParams::cancun().calc_blob_fee(block.header.excess_blob_gas.unwrap()) + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn fork_reset_updates_active_bpo_blob_params() { + let bpo1_timestamp = EthereumHardfork::Bpo1.mainnet_activation_timestamp().unwrap(); + let (source_api, source_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::Bpo1.into())) + .with_genesis_timestamp(Some(bpo1_timestamp)), + ) + .await; + source_api.mine_one().await.unwrap(); + + let (api, _) = spawn(NodeConfig::test()).await; + api.anvil_reset(Some(Forking { + json_rpc_url: Some(source_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + + assert_eq!(api.backend.blob_params(), BlobParams::bpo1()); +} + #[tokio::test(flavor = "multi_thread")] async fn can_send_eip4844_transaction_eth_send_transaction() { let node_config = NodeConfig::test() diff --git a/crates/anvil/tests/it/fork.rs b/crates/anvil/tests/it/fork.rs index d88da31c71514..24ffb25b2b1ef 100644 --- a/crates/anvil/tests/it/fork.rs +++ b/crates/anvil/tests/it/fork.rs @@ -1463,28 +1463,17 @@ async fn can_reset_fork_to_new_fork() { let (api, handle) = spawn(NodeConfig::test().with_eth_rpc_url(Some(eth_rpc_url))).await; let provider = handle.http_provider(); - let op = address!("0xC0d3c0d3c0D3c0D3C0d3C0D3C0D3c0d3c0d30007"); // L2CrossDomainMessenger - Dead on mainnet. - - let tx = TransactionRequest::default().with_to(op).with_input("0x54fd4d50"); - - let tx = WithOtherFields::new(tx); - - let mainnet_call_output = provider.call(tx).await.unwrap(); - - assert_eq!(mainnet_call_output, Bytes::new()); // 0x - - let optimism = next_rpc_endpoint(NamedChain::Optimism); - - api.anvil_reset(Some(Forking { - json_rpc_url: Some(optimism.clone()), - block_number: Some(124659890), - })) - .await - .unwrap(); + assert_eq!(provider.get_chain_id().await.unwrap(), 1); - let code = provider.get_code_at(op).await.unwrap(); + // Reset within the same execution family. Network-family changes are rejected before commit + // because the backend's network semantics are fixed for the lifetime of the node. + let sepolia = next_rpc_endpoint(NamedChain::Sepolia); + api.anvil_reset(Some(Forking { json_rpc_url: Some(sepolia), block_number: Some(1) })) + .await + .unwrap(); - assert_ne!(code, Bytes::new()); + assert_eq!(provider.get_chain_id().await.unwrap(), 11_155_111); + assert_eq!(provider.get_block_number().await.unwrap(), 1); } #[tokio::test(flavor = "multi_thread")] diff --git a/crates/anvil/tests/it/optimism.rs b/crates/anvil/tests/it/optimism.rs index e1470c3c7cc28..c50cbe6883590 100644 --- a/crates/anvil/tests/it/optimism.rs +++ b/crates/anvil/tests/it/optimism.rs @@ -2,7 +2,7 @@ use crate::utils::{http_provider, http_provider_with_signer}; use alloy_consensus::{Eip658Value, Receipt, proofs::calculate_receipt_root}; -use alloy_eips::eip2718::Encodable2718; +use alloy_eips::{calc_next_block_base_fee, eip1559::BaseFeeParams, eip2718::Encodable2718}; use alloy_network::{EthereumWallet, NetworkTransactionBuilder, TransactionBuilder}; use alloy_primitives::{Address, Bloom, TxHash, TxKind, U256, b256}; use alloy_provider::Provider; @@ -16,6 +16,117 @@ use op_alloy_consensus::{OpDepositReceipt, OpDepositReceiptWithBloom, TxDeposit} use op_alloy_rpc_types::OpTransactionFields; use serde_json::{Value, json}; +const CANYON_TIMESTAMP: u64 = 1_704_992_401; + +#[tokio::test(flavor = "multi_thread")] +async fn test_inferred_optimism_fork_reset_to_memory_stays_coherent() { + let (source_api, source_handle) = spawn( + NodeConfig::test() + .with_networks(NetworkConfigs::with_optimism()) + .with_chain_id(Some(10u64)), + ) + .await; + source_api.mine_one().await.unwrap(); + + let (api, _) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(source_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)), + ) + .await; + assert!(api.backend.is_optimism()); + + api.anvil_reset(None).await.unwrap(); + + assert!(api.backend.is_optimism()); + assert!(matches!(api.backend.hardfork(), foundry_evm::hardfork::FoundryHardfork::Optimism(_))); + assert_eq!(api.backend.spec_id(), api.backend.hardfork().into()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_reset_refreshes_optimism_base_fee_params() { + let pre_canyon_timestamp = CANYON_TIMESTAMP - 2; + let post_canyon_timestamp = CANYON_TIMESTAMP + 1; + let (pre_canyon_api, pre_canyon_handle) = spawn( + NodeConfig::test() + .with_networks(NetworkConfigs::with_optimism()) + .with_chain_id(Some(10u64)) + .with_base_fee(Some(INITIAL_BASE_FEE)) + .with_genesis_timestamp(Some(pre_canyon_timestamp)), + ) + .await; + pre_canyon_api.mine_one().await.unwrap(); + let (post_canyon_api, post_canyon_handle) = spawn( + NodeConfig::test() + .with_networks(NetworkConfigs::with_optimism()) + .with_chain_id(Some(10u64)) + .with_base_fee(Some(INITIAL_BASE_FEE)) + .with_genesis_timestamp(Some(post_canyon_timestamp)), + ) + .await; + post_canyon_api.mine_one().await.unwrap(); + + let (api, _) = spawn( + NodeConfig::test() + .with_networks(NetworkConfigs::with_optimism()) + .with_genesis_timestamp(Some(post_canyon_timestamp)) + .with_eth_rpc_url(Some(pre_canyon_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)), + ) + .await; + + let parent = + pre_canyon_handle.http_provider().get_block(BlockId::number(1)).await.unwrap().unwrap(); + let expected = calc_next_block_base_fee( + parent.header.gas_used, + parent.header.gas_limit, + parent.header.base_fee_per_gas.unwrap(), + BaseFeeParams::optimism(), + ); + assert_eq!(api.backend.base_fee(), expected); + + api.anvil_reset(Some(Forking { + json_rpc_url: Some(post_canyon_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + + let parent = + post_canyon_handle.http_provider().get_block(BlockId::number(1)).await.unwrap().unwrap(); + let expected = calc_next_block_base_fee( + parent.header.gas_used, + parent.header.gas_limit, + parent.header.base_fee_per_gas.unwrap(), + BaseFeeParams::optimism_canyon(), + ); + assert_eq!(api.backend.base_fee(), expected); + + api.anvil_reset(Some(Forking { + json_rpc_url: Some(pre_canyon_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + + let parent = + pre_canyon_handle.http_provider().get_block(BlockId::number(1)).await.unwrap().unwrap(); + let expected = calc_next_block_base_fee( + parent.header.gas_used, + parent.header.gas_limit, + parent.header.base_fee_per_gas.unwrap(), + BaseFeeParams::optimism(), + ); + assert_eq!(api.backend.base_fee(), expected); + + api.anvil_reset(None).await.unwrap(); + let gas_limit = api.backend.gas_limit(); + api.mine_one().await.unwrap(); + let expected = + calc_next_block_base_fee(0, gas_limit, INITIAL_BASE_FEE, BaseFeeParams::optimism_canyon()); + assert_eq!(api.backend.base_fee(), expected); +} + #[tokio::test(flavor = "multi_thread")] async fn inferred_optimism_forks_allow_non_monad_source_resets() { let (_optimism_api, optimism_handle) = spawn(NodeConfig::test().with_optimism()).await; diff --git a/crates/anvil/tests/it/simulate.rs b/crates/anvil/tests/it/simulate.rs index 5a535655bd897..589de351d23c8 100644 --- a/crates/anvil/tests/it/simulate.rs +++ b/crates/anvil/tests/it/simulate.rs @@ -14,6 +14,7 @@ use alloy_primitives::{Address, B256, Bloom, Bytes, Log, TxKind, U256, address}; use alloy_provider::Provider; use alloy_rpc_types::{ BlockNumberOrTag, BlockOverrides, + anvil::Forking, request::TransactionRequest, simulate::{SimBlock, SimulatePayload}, state::{AccountOverride, StateOverridesBuilder}, @@ -916,14 +917,124 @@ async fn test_simulate_scopes_block_overrides_and_derives_base_fee_rpc() { } #[tokio::test(flavor = "multi_thread")] -async fn test_simulate_pre_london_blocks_keep_base_fee_disabled_rpc() { - let (_api, handle) = - spawn(NodeConfig::test().with_hardfork(Some(EthereumHardfork::Berlin.into()))).await; +async fn test_fork_simulate_auto_detects_pre_london_base_fee_rpc() { + let (berlin_api, berlin_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::Berlin.into())) + .with_genesis_timestamp(Some(1_618_481_223u64)), + ) + .await; + berlin_api.evm_set_next_block_timestamp(1_618_481_224u64).unwrap(); + berlin_api.mine_one().await.unwrap(); + + let (api, handle) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(berlin_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)), + ) + .await; + let endpoint = handle.http_endpoint(); + let response = rpc_request( + &endpoint, + "eth_simulateV1", + json!([{ + "blockStateCalls": [ + {"blockOverrides": {"baseFeePerGas": "0x3e8"}}, + {} + ], + "validation": true + }, "latest"]), + ) + .await; + + assert!(response.get("error").is_none(), "{response}"); + let blocks = response["result"].as_array().unwrap(); + assert_eq!(blocks.len(), 2); + assert!(blocks.iter().all(|block| block.get("baseFeePerGas").is_none())); + + let (london_api, london_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::London.into())) + .with_genesis_timestamp(Some(1_628_166_823u64)), + ) + .await; + london_api.evm_set_next_block_timestamp(1_628_166_824u64).unwrap(); + london_api.mine_one().await.unwrap(); + + api.anvil_reset(Some(Forking { + json_rpc_url: Some(london_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + + let response = rpc_request( + &endpoint, + "eth_simulateV1", + json!([{ + "blockStateCalls": [ + {"blockOverrides": {"baseFeePerGas": "0x3e8"}}, + {} + ], + "validation": true + }, "latest"]), + ) + .await; + + assert!(response.get("error").is_none(), "{response}"); + let blocks = response["result"].as_array().unwrap(); + assert_eq!(blocks.len(), 2); + assert_eq!(blocks[0]["baseFeePerGas"], "0x3e8"); + assert_eq!(blocks[1]["baseFeePerGas"], "0x36b"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_fork_simulate_preserves_explicit_fee_spec_on_reset_rpc() { + let (berlin_api, berlin_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::Berlin.into())) + .with_genesis_timestamp(Some(1_618_481_223u64)), + ) + .await; + berlin_api.evm_set_next_block_timestamp(1_618_481_224u64).unwrap(); + berlin_api.mine_one().await.unwrap(); + + let (api, handle) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(berlin_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::Berlin.into())), + ) + .await; + + let (london_api, london_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(1u64)) + .with_hardfork(Some(EthereumHardfork::London.into())) + .with_genesis_timestamp(Some(1_628_166_823u64)), + ) + .await; + london_api.evm_set_next_block_timestamp(1_628_166_824u64).unwrap(); + london_api.mine_one().await.unwrap(); + + api.anvil_reset(Some(Forking { + json_rpc_url: Some(london_handle.http_endpoint()), + block_number: Some(1u64), + })) + .await + .unwrap(); + let response = rpc_request( &handle.http_endpoint(), "eth_simulateV1", json!([{ - "blockStateCalls": [{}, {}], + "blockStateCalls": [ + {"blockOverrides": {"baseFeePerGas": "0x3e8"}}, + {} + ], "validation": true }, "latest"]), ) diff --git a/crates/anvil/tests/it/tempo.rs b/crates/anvil/tests/it/tempo.rs index 673899e7a5267..89346b9446f5b 100644 --- a/crates/anvil/tests/it/tempo.rs +++ b/crates/anvil/tests/it/tempo.rs @@ -324,6 +324,34 @@ async fn test_tempo_fork_detects_hardfork_from_fork_timestamp() { assert_eq!(latest_block.header.beneficiary, TIP_FEE_MANAGER_ADDRESS); } +#[tokio::test(flavor = "multi_thread")] +async fn test_inferred_tempo_fork_reset_to_memory_stays_coherent() { + let fork_timestamp = TempoHardfork::T3.mainnet_activation_timestamp().unwrap(); + let (source_api, source_handle) = spawn( + NodeConfig::test() + .with_chain_id(Some(4217u64)) + .with_genesis_timestamp(Some(fork_timestamp)), + ) + .await; + source_api.mine_one().await.unwrap(); + + let (api, handle) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(source_handle.http_endpoint())) + .with_fork_block_number(Some(1u64)), + ) + .await; + assert!(api.backend.is_tempo()); + + api.anvil_reset(None).await.unwrap(); + + assert!(api.backend.is_tempo()); + assert!(matches!(api.backend.hardfork(), foundry_evm::hardfork::FoundryHardfork::Tempo(_))); + assert_eq!(api.backend.spec_id(), api.backend.hardfork().into()); + let genesis = handle.http_provider().get_block(BlockId::latest()).await.unwrap().unwrap(); + assert_eq!(genesis.header.beneficiary, Address::ZERO); +} + #[tokio::test(flavor = "multi_thread")] async fn test_tempo_reset_to_fork_uses_fee_manager_beneficiary() { let (_source_api, source_handle) = spawn(NodeConfig::test()).await;