Skip to content

Commit 11d6c64

Browse files
committed
fix(anvil): fix latest read isolation
1 parent 9edbda9 commit 11d6c64

3 files changed

Lines changed: 126 additions & 29 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
anvil: patch
3+
---
4+
5+
Fixed live calls and pending blocks observing state, execution environment, and fees from different blocks during mining.

crates/anvil/src/eth/backend/mem/mod.rs

Lines changed: 104 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5398,6 +5398,8 @@ where
53985398
let current_excess_blob_gas_and_price = self.excess_blob_gas_and_price();
53995399

54005400
let mut evm_env = self.evm_env.read().clone();
5401+
#[cfg(feature = "monad")]
5402+
let execution_chain_id = evm_env.cfg_env.chain_id;
54015403
let hardfork = self.hardfork();
54025404

54035405
if evm_env.block_env.basefee == 0 {
@@ -5504,6 +5506,30 @@ where
55045506
self.cheats.consume_next_block_prevrandao(pending);
55055507
}
55065508

5509+
let header = &block_info.block.header;
5510+
let next_block_base_fee = self.fees.get_next_block_base_fee_per_gas(
5511+
header.gas_used,
5512+
header.gas_limit,
5513+
header.base_fee_per_gas.unwrap_or_default(),
5514+
);
5515+
let next_block_excess_blob_gas = self.fees.get_next_block_blob_excess_gas(
5516+
header.excess_blob_gas.unwrap_or_default(),
5517+
header.blob_gas_used.unwrap_or_default(),
5518+
);
5519+
let next_block_blob_fees = BlobExcessGasAndPrice::new(
5520+
next_block_excess_blob_gas,
5521+
self.fees.blob_params().update_fraction as u64,
5522+
);
5523+
5524+
// Live execution readers acquire the database read lock before cloning the block
5525+
// environment and next-block fee state. Publish the complete snapshot under the
5526+
// database write lock so readers observe either the parent or newly mined state.
5527+
// The database is always the outer lock, and no path holds an environment or fee
5528+
// guard while waiting for it, so these acquisitions cannot form a lock cycle.
5529+
evm_env.block_env.difficulty = U256::ZERO;
5530+
*self.evm_env.write() = evm_env;
5531+
self.fees.set_next_block_fees(next_block_base_fee, next_block_blob_fees);
5532+
55075533
(block_info, included, invalid, not_yet_valid, block_hash, parent_state)
55085534
};
55095535

@@ -5555,7 +5581,7 @@ where
55555581
&mut storage,
55565582
block_hash,
55575583
participants,
5558-
evm_env.cfg_env.chain_id,
5584+
execution_chain_id,
55595585
hardfork,
55605586
);
55615587
}
@@ -5593,12 +5619,6 @@ where
55935619

55945620
self.time.mark_block_created();
55955621

5596-
// we intentionally set the difficulty to `0` for newer blocks
5597-
evm_env.block_env.difficulty = U256::from(0);
5598-
5599-
// update env with new values
5600-
*self.evm_env.write() = evm_env;
5601-
56025622
let timestamp = utc_from_secs(header.timestamp);
56035623

56045624
node_info!(" Block Number: {}", block_number);
@@ -5614,24 +5634,6 @@ where
56145634

56155635
(outcome, header, block_hash)
56165636
};
5617-
let next_block_base_fee = self.fees.get_next_block_base_fee_per_gas(
5618-
header.gas_used,
5619-
header.gas_limit,
5620-
header.base_fee_per_gas.unwrap_or_default(),
5621-
);
5622-
let next_block_excess_blob_gas = self.fees.get_next_block_blob_excess_gas(
5623-
header.excess_blob_gas.unwrap_or_default(),
5624-
header.blob_gas_used.unwrap_or_default(),
5625-
);
5626-
5627-
// update next base fee
5628-
self.fees.set_base_fee(next_block_base_fee);
5629-
5630-
self.fees.set_blob_excess_gas_and_price(BlobExcessGasAndPrice::new(
5631-
next_block_excess_blob_gas,
5632-
self.fees.blob_params().update_fraction as u64,
5633-
));
5634-
56355637
// notify all listeners
56365638
self.notify_on_new_block(header.into_inner(), block_hash);
56375639

@@ -9420,12 +9422,12 @@ pub use foundry_evm::core::evm::IntoInstructionResult;
94209422
#[cfg(test)]
94219423
mod tests {
94229424
use super::{
9423-
ForkCacheNamespace, ForkCacheSource, StagedForkCacheLease, StagedForkDbUser,
9424-
arbitrum_replay_block_number,
9425+
BlockRequest, FeeDetails, ForkCacheNamespace, ForkCacheSource, InstructionResult, Output,
9426+
StagedForkCacheLease, StagedForkDbUser, arbitrum_replay_block_number,
94259427
};
94269428
use crate::{NodeConfig, config::ForkTransactionReplay, spawn};
94279429
use alloy_network::{AnyHeader, AnyRpcBlock, AnyRpcHeader, TransactionBuilder};
9428-
use alloy_primitives::{B256, Bytes, U256};
9430+
use alloy_primitives::{Address, B256, Bytes, U256};
94299431
use alloy_provider::Provider;
94309432
use alloy_rpc_types::{Block, BlockTransactions, TransactionRequest, state::EvmOverrides};
94319433
use alloy_serde::WithOtherFields;
@@ -9435,7 +9437,7 @@ mod tests {
94359437
hardfork::{EthereumHardfork, FoundryHardfork},
94369438
};
94379439
use foundry_evm_networks::arbitrum;
9438-
use std::sync::Arc;
9440+
use std::sync::{Arc, mpsc};
94399441
use tempfile::tempdir;
94409442

94419443
fn test_cache_db(cache_path: std::path::PathBuf) -> BlockchainDb {
@@ -9559,6 +9561,79 @@ mod tests {
95599561
assert_eq!(api.backend.time().last_block_wall_time(), head_wall_time);
95609562
}
95619563

9564+
#[tokio::test(flavor = "multi_thread")]
9565+
async fn live_execution_snapshot_is_coherent_during_mining() {
9566+
let (api, handle) = spawn(NodeConfig::test().with_no_mining(true)).await;
9567+
let sender = handle.dev_wallets().next().unwrap().address();
9568+
let recipient = Address::repeat_byte(0x11);
9569+
let contract = Address::repeat_byte(0x22);
9570+
9571+
// Return the recipient balance followed by NUMBER. Before mining both are zero; after the
9572+
// queued transfer is mined both are one.
9573+
let mut code = vec![0x73];
9574+
code.extend_from_slice(recipient.as_slice());
9575+
code.extend_from_slice(&[
9576+
0x31, 0x60, 0x00, 0x52, 0x43, 0x60, 0x20, 0x52, 0x60, 0x40, 0x60, 0x00, 0xf3,
9577+
]);
9578+
api.anvil_set_code(contract, code.into()).await.unwrap();
9579+
api.send_transaction(WithOtherFields::new(
9580+
TransactionRequest::default().from(sender).to(recipient).value(U256::from(1)),
9581+
))
9582+
.await
9583+
.unwrap();
9584+
9585+
let resolved_head = api.backend.best_number();
9586+
let db_guard = api.backend.db.write().await;
9587+
9588+
// Polling while holding the database makes the lock queue deterministic: mining owns the
9589+
// first waiter and the call that already resolved `latest` owns the second.
9590+
let mining_api = api.clone();
9591+
let mut mining = Box::pin(async move { mining_api.mine_one().await });
9592+
assert!(futures::poll!(mining.as_mut()).is_pending());
9593+
9594+
let request = WithOtherFields::new(TransactionRequest::default().to(contract));
9595+
let mut call = Box::pin(api.backend.call(
9596+
request,
9597+
FeeDetails::zero(),
9598+
Some(BlockRequest::Number(resolved_head)),
9599+
EvmOverrides::default(),
9600+
));
9601+
assert!(futures::poll!(call.as_mut()).is_pending());
9602+
9603+
// Pause canonical block publication after the database snapshot is published. A separate
9604+
// thread owns this synchronous lock so the async test does not hold it across an await.
9605+
let backend = api.backend.clone();
9606+
let (storage_locked_tx, storage_locked_rx) = mpsc::channel();
9607+
let (release_storage_tx, release_storage_rx) = mpsc::channel();
9608+
let storage_thread = std::thread::spawn(move || {
9609+
let storage_guard = backend.blockchain.storage.write();
9610+
storage_locked_tx.send(()).unwrap();
9611+
release_storage_rx.recv().unwrap();
9612+
drop(storage_guard);
9613+
});
9614+
storage_locked_rx.recv().unwrap();
9615+
drop(db_guard);
9616+
let mining = tokio::spawn(mining);
9617+
let (exit, output, _, _) = call.await.unwrap();
9618+
9619+
// `with_pending_block` builds its environment from this snapshot before reading canonical
9620+
// storage. It must already contain the fee derived for block two while mining is paused.
9621+
let pending_env = api.backend.next_evm_env();
9622+
assert_eq!(pending_env.block_env.number, U256::from(2));
9623+
assert_eq!(pending_env.block_env.basefee, 875_175_000);
9624+
9625+
release_storage_tx.send(()).unwrap();
9626+
storage_thread.join().unwrap();
9627+
mining.await.unwrap().unwrap();
9628+
9629+
assert_eq!(exit, InstructionResult::Return);
9630+
let Some(Output::Call(output)) = output else { panic!("call did not return data") };
9631+
assert_eq!(output.len(), 64);
9632+
let balance = U256::from_be_slice(&output[..32]);
9633+
let block_number = U256::from_be_slice(&output[32..]);
9634+
assert_eq!((balance, block_number), (U256::from(1), U256::from(1)));
9635+
}
9636+
95629637
struct CacheFlushingDb(BlockchainDb);
95639638

95649639
impl Drop for CacheFlushingDb {

crates/anvil/src/eth/fees.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,23 @@ impl FeeManager {
131131
state.blob_excess_gas_and_price = snapshot.blob_excess_gas_and_price;
132132
}
133133

134+
/// Atomically publishes the chain-derived fee state for the next block.
135+
pub(crate) fn set_next_block_fees(
136+
&self,
137+
base_fee: u64,
138+
blob_excess_gas_and_price: BlobExcessGasAndPrice,
139+
) {
140+
trace!(
141+
target: "backend::fees",
142+
?base_fee,
143+
?blob_excess_gas_and_price,
144+
"updated next block fees"
145+
);
146+
let mut state = self.state.write();
147+
state.base_fee = base_fee;
148+
state.blob_excess_gas_and_price = blob_excess_gas_and_price;
149+
}
150+
134151
/// Returns the active Tempo hardfork, if running a Tempo chain.
135152
pub fn tempo_hardfork(&self) -> Option<TempoHardfork> {
136153
self.state.read().rules.tempo_hardfork

0 commit comments

Comments
 (0)