use crate::eth::{
backend::{
db::{
MaybeFullDatabase, SerializableBlock, SerializableHistoricalStates,
SerializableTransaction, StateDb,
},
mem::cache::DiskStateCache,
},
error::BlockchainError,
pool::transactions::PoolTransaction,
};
use alloy_consensus::constants::EMPTY_WITHDRAWALS;
use alloy_eips::eip7685::EMPTY_REQUESTS_HASH;
use alloy_primitives::{
map::{B256HashMap, HashMap},
Bytes, B256, U256, U64,
};
use alloy_rpc_types::{
trace::{
geth::{
FourByteFrame, GethDebugBuiltInTracerType, GethDebugTracerType,
GethDebugTracingOptions, GethTrace, NoopFrame,
},
otterscan::{InternalOperation, OperationType},
parity::LocalizedTransactionTrace,
},
BlockId, BlockNumberOrTag, TransactionInfo as RethTransactionInfo,
};
use anvil_core::eth::{
block::{Block, PartialHeader},
transaction::{MaybeImpersonatedTransaction, ReceiptResponse, TransactionInfo, TypedReceipt},
};
use anvil_rpc::error::RpcError;
use foundry_evm::{
backend::MemDb,
revm::primitives::Env,
traces::{
CallKind, FourByteInspector, GethTraceBuilder, ParityTraceBuilder, TracingInspectorConfig,
},
};
use parking_lot::RwLock;
use revm::primitives::SpecId;
use std::{collections::VecDeque, fmt, path::PathBuf, sync::Arc, time::Duration};
pub const DEFAULT_HISTORY_LIMIT: usize = 500;
const MIN_HISTORY_LIMIT: usize = 10;
const MAX_ON_DISK_HISTORY_LIMIT: usize = 3_600;
pub struct InMemoryBlockStates {
states: B256HashMap<StateDb>,
on_disk_states: B256HashMap<StateDb>,
in_memory_limit: usize,
min_in_memory_limit: usize,
max_on_disk_limit: usize,
oldest_on_disk: VecDeque<B256>,
present: VecDeque<B256>,
disk_cache: DiskStateCache,
}
impl InMemoryBlockStates {
pub fn new(in_memory_limit: usize, on_disk_limit: usize) -> Self {
Self {
states: Default::default(),
on_disk_states: Default::default(),
in_memory_limit,
min_in_memory_limit: in_memory_limit.min(MIN_HISTORY_LIMIT),
max_on_disk_limit: on_disk_limit,
oldest_on_disk: Default::default(),
present: Default::default(),
disk_cache: Default::default(),
}
}
pub fn memory_only(mut self) -> Self {
self.max_on_disk_limit = 0;
self
}
pub fn disk_path(mut self, path: PathBuf) -> Self {
self.disk_cache = self.disk_cache.with_path(path);
self
}
pub fn update_interval_mine_block_time(&mut self, block_time: Duration) {
let block_time = block_time.as_secs();
if block_time <= 2 {
self.in_memory_limit = DEFAULT_HISTORY_LIMIT * 3;
self.enforce_limits();
}
}
fn is_memory_only(&self) -> bool {
self.max_on_disk_limit == 0
}
pub fn insert(&mut self, hash: B256, state: StateDb) {
if !self.is_memory_only() && self.present.len() >= self.in_memory_limit {
self.in_memory_limit =
self.in_memory_limit.saturating_sub(1).max(self.min_in_memory_limit);
}
self.enforce_limits();
self.states.insert(hash, state);
self.present.push_back(hash);
}
fn enforce_limits(&mut self) {
while self.present.len() >= self.in_memory_limit {
if let Some((hash, mut state)) = self
.present
.pop_front()
.and_then(|hash| self.states.remove(&hash).map(|state| (hash, state)))
{
if !self.is_memory_only() {
let state_snapshot = state.0.clear_into_state_snapshot();
self.disk_cache.write(hash, state_snapshot);
self.on_disk_states.insert(hash, state);
self.oldest_on_disk.push_back(hash);
}
}
}
while !self.is_memory_only() && self.oldest_on_disk.len() >= self.max_on_disk_limit {
if let Some(hash) = self.oldest_on_disk.pop_front() {
self.on_disk_states.remove(&hash);
self.disk_cache.remove(hash);
}
}
}
pub fn get(&mut self, hash: &B256) -> Option<&StateDb> {
self.states.get(hash).or_else(|| {
if let Some(state) = self.on_disk_states.get_mut(hash) {
if let Some(cached) = self.disk_cache.read(*hash) {
state.init_from_state_snapshot(cached);
return Some(state);
}
}
None
})
}
pub fn set_cache_limit(&mut self, limit: usize) {
self.in_memory_limit = limit;
}
pub fn clear(&mut self) {
self.states.clear();
self.on_disk_states.clear();
self.present.clear();
for on_disk in std::mem::take(&mut self.oldest_on_disk) {
self.disk_cache.remove(on_disk)
}
}
pub fn serialized_states(&mut self) -> SerializableHistoricalStates {
let mut states = self
.states
.iter_mut()
.map(|(hash, state)| (*hash, state.serialize_state()))
.collect::<Vec<_>>();
self.on_disk_states.iter().for_each(|(hash, _)| {
if let Some(state_snapshot) = self.disk_cache.read(*hash) {
states.push((*hash, state_snapshot));
}
});
SerializableHistoricalStates::new(states)
}
pub fn load_states(&mut self, states: SerializableHistoricalStates) {
for (hash, state_snapshot) in states {
let mut state_db = StateDb::new(MemDb::default());
state_db.init_from_state_snapshot(state_snapshot);
self.insert(hash, state_db);
}
}
}
impl fmt::Debug for InMemoryBlockStates {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InMemoryBlockStates")
.field("in_memory_limit", &self.in_memory_limit)
.field("min_in_memory_limit", &self.min_in_memory_limit)
.field("max_on_disk_limit", &self.max_on_disk_limit)
.field("oldest_on_disk", &self.oldest_on_disk)
.field("present", &self.present)
.finish_non_exhaustive()
}
}
impl Default for InMemoryBlockStates {
fn default() -> Self {
Self::new(DEFAULT_HISTORY_LIMIT, MAX_ON_DISK_HISTORY_LIMIT)
}
}
#[derive(Clone)]
pub struct BlockchainStorage {
pub blocks: B256HashMap<Block>,
pub hashes: HashMap<U64, B256>,
pub best_hash: B256,
pub best_number: U64,
pub genesis_hash: B256,
pub transactions: B256HashMap<MinedTransaction>,
pub total_difficulty: U256,
}
impl BlockchainStorage {
pub fn new(env: &Env, spec_id: SpecId, base_fee: Option<u64>, timestamp: u64) -> Self {
let is_shanghai = spec_id >= SpecId::SHANGHAI;
let is_cancun = spec_id >= SpecId::CANCUN;
let is_prague = spec_id >= SpecId::PRAGUE;
let partial_header = PartialHeader {
timestamp,
base_fee,
gas_limit: env.block.gas_limit.to::<u64>(),
beneficiary: env.block.coinbase,
difficulty: env.block.difficulty,
blob_gas_used: env.block.blob_excess_gas_and_price.as_ref().map(|_| 0),
excess_blob_gas: env.block.get_blob_excess_gas(),
parent_beacon_block_root: is_cancun.then_some(Default::default()),
withdrawals_root: is_shanghai.then_some(EMPTY_WITHDRAWALS),
requests_hash: is_prague.then_some(EMPTY_REQUESTS_HASH),
..Default::default()
};
let block = Block::new::<MaybeImpersonatedTransaction>(partial_header, vec![]);
let genesis_hash = block.header.hash_slow();
let best_hash = genesis_hash;
let best_number: U64 = U64::from(0u64);
let mut blocks = B256HashMap::default();
blocks.insert(genesis_hash, block);
let mut hashes = HashMap::default();
hashes.insert(best_number, genesis_hash);
Self {
blocks,
hashes,
best_hash,
best_number,
genesis_hash,
transactions: Default::default(),
total_difficulty: Default::default(),
}
}
pub fn forked(block_number: u64, block_hash: B256, total_difficulty: U256) -> Self {
let mut hashes = HashMap::default();
hashes.insert(U64::from(block_number), block_hash);
Self {
blocks: B256HashMap::default(),
hashes,
best_hash: block_hash,
best_number: U64::from(block_number),
genesis_hash: Default::default(),
transactions: Default::default(),
total_difficulty,
}
}
pub fn unwind_to(&mut self, block_number: u64, block_hash: B256) {
let best_num: u64 = self.best_number.try_into().unwrap_or(0);
for i in (block_number + 1)..=best_num {
if let Some(hash) = self.hashes.remove(&U64::from(i)) {
if let Some(block) = self.blocks.remove(&hash) {
self.remove_block_transactions_by_number(block.header.number);
}
}
}
self.best_hash = block_hash;
self.best_number = U64::from(block_number);
}
#[allow(unused)]
pub fn empty() -> Self {
Self {
blocks: Default::default(),
hashes: Default::default(),
best_hash: Default::default(),
best_number: Default::default(),
genesis_hash: Default::default(),
transactions: Default::default(),
total_difficulty: Default::default(),
}
}
pub fn remove_block_transactions_by_number(&mut self, num: u64) {
if let Some(hash) = self.hashes.get(&(U64::from(num))).copied() {
self.remove_block_transactions(hash);
}
}
pub fn remove_block_transactions(&mut self, block_hash: B256) {
if let Some(block) = self.blocks.get_mut(&block_hash) {
for tx in block.transactions.iter() {
self.transactions.remove(&tx.hash());
}
block.transactions.clear();
}
}
}
impl BlockchainStorage {
pub fn hash(&self, number: BlockNumberOrTag) -> Option<B256> {
let slots_in_an_epoch = U64::from(32u64);
match number {
BlockNumberOrTag::Latest => Some(self.best_hash),
BlockNumberOrTag::Earliest => Some(self.genesis_hash),
BlockNumberOrTag::Pending => None,
BlockNumberOrTag::Number(num) => self.hashes.get(&U64::from(num)).copied(),
BlockNumberOrTag::Safe => {
if self.best_number > (slots_in_an_epoch) {
self.hashes.get(&(self.best_number - (slots_in_an_epoch))).copied()
} else {
Some(self.genesis_hash) }
}
BlockNumberOrTag::Finalized => {
if self.best_number > (slots_in_an_epoch * U64::from(2)) {
self.hashes
.get(&(self.best_number - (slots_in_an_epoch * U64::from(2))))
.copied()
} else {
Some(self.genesis_hash)
}
}
}
}
pub fn serialized_blocks(&self) -> Vec<SerializableBlock> {
self.blocks.values().map(|block| block.clone().into()).collect()
}
pub fn serialized_transactions(&self) -> Vec<SerializableTransaction> {
self.transactions.values().map(|tx: &MinedTransaction| tx.clone().into()).collect()
}
pub fn load_blocks(&mut self, serializable_blocks: Vec<SerializableBlock>) {
for serializable_block in serializable_blocks.iter() {
let block: Block = serializable_block.clone().into();
let block_hash = block.header.hash_slow();
let block_number = block.header.number;
self.blocks.insert(block_hash, block);
self.hashes.insert(U64::from(block_number), block_hash);
}
}
pub fn load_transactions(&mut self, serializable_transactions: Vec<SerializableTransaction>) {
for serializable_transaction in serializable_transactions.iter() {
let transaction: MinedTransaction = serializable_transaction.clone().into();
self.transactions.insert(transaction.info.transaction_hash, transaction);
}
}
}
#[derive(Clone)]
pub struct Blockchain {
pub storage: Arc<RwLock<BlockchainStorage>>,
}
impl Blockchain {
pub fn new(env: &Env, spec_id: SpecId, base_fee: Option<u64>, timestamp: u64) -> Self {
Self {
storage: Arc::new(RwLock::new(BlockchainStorage::new(
env, spec_id, base_fee, timestamp,
))),
}
}
pub fn forked(block_number: u64, block_hash: B256, total_difficulty: U256) -> Self {
Self {
storage: Arc::new(RwLock::new(BlockchainStorage::forked(
block_number,
block_hash,
total_difficulty,
))),
}
}
pub fn hash(&self, id: BlockId) -> Option<B256> {
match id {
BlockId::Hash(h) => Some(h.block_hash),
BlockId::Number(num) => self.storage.read().hash(num),
}
}
pub fn get_block_by_hash(&self, hash: &B256) -> Option<Block> {
self.storage.read().blocks.get(hash).cloned()
}
pub fn get_transaction_by_hash(&self, hash: &B256) -> Option<MinedTransaction> {
self.storage.read().transactions.get(hash).cloned()
}
pub fn blocks_count(&self) -> usize {
self.storage.read().blocks.len()
}
}
#[derive(Clone, Debug)]
pub struct MinedBlockOutcome {
pub block_number: U64,
pub included: Vec<Arc<PoolTransaction>>,
pub invalid: Vec<Arc<PoolTransaction>>,
}
#[derive(Clone, Debug)]
pub struct MinedTransaction {
pub info: TransactionInfo,
pub receipt: TypedReceipt,
pub block_hash: B256,
pub block_number: u64,
}
impl MinedTransaction {
pub fn parity_traces(&self) -> Vec<LocalizedTransactionTrace> {
ParityTraceBuilder::new(
self.info.traces.clone(),
None,
TracingInspectorConfig::default_parity(),
)
.into_localized_transaction_traces(RethTransactionInfo {
hash: Some(self.info.transaction_hash),
index: Some(self.info.transaction_index),
block_hash: Some(self.block_hash),
block_number: Some(self.block_number),
base_fee: None,
})
}
pub fn ots_internal_operations(&self) -> Vec<InternalOperation> {
self.info
.traces
.iter()
.filter_map(|node| {
let r#type = match node.trace.kind {
_ if node.is_selfdestruct() => OperationType::OpSelfDestruct,
CallKind::Call if !node.trace.value.is_zero() => OperationType::OpTransfer,
CallKind::Create => OperationType::OpCreate,
CallKind::Create2 => OperationType::OpCreate2,
_ => return None,
};
let mut from = node.trace.caller;
let mut to = node.trace.address;
let mut value = node.trace.value;
if node.is_selfdestruct() {
from = node.trace.address;
to = node.trace.selfdestruct_refund_target.unwrap_or_default();
value = node.trace.selfdestruct_transferred_value.unwrap_or_default();
}
Some(InternalOperation { r#type, from, to, value })
})
.collect()
}
pub fn geth_trace(&self, opts: GethDebugTracingOptions) -> Result<GethTrace, BlockchainError> {
let GethDebugTracingOptions { config, tracer, tracer_config, .. } = opts;
if let Some(tracer) = tracer {
match tracer {
GethDebugTracerType::BuiltInTracer(tracer) => match tracer {
GethDebugBuiltInTracerType::FourByteTracer => {
let inspector = FourByteInspector::default();
return Ok(FourByteFrame::from(inspector).into());
}
GethDebugBuiltInTracerType::CallTracer => {
return match tracer_config.into_call_config() {
Ok(call_config) => Ok(GethTraceBuilder::new(
self.info.traces.clone(),
TracingInspectorConfig::from_geth_config(&config),
)
.geth_call_traces(
call_config,
self.receipt.cumulative_gas_used() as u64,
)
.into()),
Err(e) => Err(RpcError::invalid_params(e.to_string()).into()),
};
}
GethDebugBuiltInTracerType::PreStateTracer |
GethDebugBuiltInTracerType::NoopTracer |
GethDebugBuiltInTracerType::MuxTracer |
GethDebugBuiltInTracerType::FlatCallTracer => {}
},
GethDebugTracerType::JsTracer(_code) => {}
}
return Ok(NoopFrame::default().into());
}
Ok(GethTraceBuilder::new(
self.info.traces.clone(),
TracingInspectorConfig::from_geth_config(&config),
)
.geth_traces(
self.receipt.cumulative_gas_used() as u64,
self.info.out.clone().unwrap_or_default(),
opts.config,
)
.into())
}
}
#[derive(Clone, Debug)]
pub struct MinedTransactionReceipt {
pub inner: ReceiptResponse,
pub out: Option<Bytes>,
}
#[cfg(test)]
#[allow(clippy::needless_return)]
mod tests {
use super::*;
use crate::eth::backend::db::Db;
use alloy_primitives::{hex, Address};
use alloy_rlp::Decodable;
use anvil_core::eth::transaction::TypedTransaction;
use foundry_evm::{
backend::MemDb,
revm::{
db::DatabaseRef,
primitives::{AccountInfo, U256},
},
};
#[test]
fn test_interval_update() {
let mut storage = InMemoryBlockStates::default();
storage.update_interval_mine_block_time(Duration::from_secs(1));
assert_eq!(storage.in_memory_limit, DEFAULT_HISTORY_LIMIT * 3);
}
#[test]
fn test_init_state_limits() {
let mut storage = InMemoryBlockStates::default();
assert_eq!(storage.in_memory_limit, DEFAULT_HISTORY_LIMIT);
assert_eq!(storage.min_in_memory_limit, MIN_HISTORY_LIMIT);
assert_eq!(storage.max_on_disk_limit, MAX_ON_DISK_HISTORY_LIMIT);
storage = storage.memory_only();
assert!(storage.is_memory_only());
storage = InMemoryBlockStates::new(1, 0);
assert!(storage.is_memory_only());
assert_eq!(storage.in_memory_limit, 1);
assert_eq!(storage.min_in_memory_limit, 1);
assert_eq!(storage.max_on_disk_limit, 0);
storage = InMemoryBlockStates::new(1, 2);
assert!(!storage.is_memory_only());
assert_eq!(storage.in_memory_limit, 1);
assert_eq!(storage.min_in_memory_limit, 1);
assert_eq!(storage.max_on_disk_limit, 2);
}
#[tokio::test(flavor = "multi_thread")]
async fn can_read_write_cached_state() {
let mut storage = InMemoryBlockStates::new(1, MAX_ON_DISK_HISTORY_LIMIT);
let one = B256::from(U256::from(1));
let two = B256::from(U256::from(2));
let mut state = MemDb::default();
let addr = Address::random();
let info = AccountInfo::from_balance(U256::from(1337));
state.insert_account(addr, info);
storage.insert(one, StateDb::new(state));
storage.insert(two, StateDb::new(MemDb::default()));
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
assert_eq!(storage.on_disk_states.len(), 1);
assert!(storage.on_disk_states.contains_key(&one));
let loaded = storage.get(&one).unwrap();
let acc = loaded.basic_ref(addr).unwrap().unwrap();
assert_eq!(acc.balance, U256::from(1337u64));
}
#[tokio::test(flavor = "multi_thread")]
async fn can_decrease_state_cache_size() {
let limit = 15;
let mut storage = InMemoryBlockStates::new(limit, MAX_ON_DISK_HISTORY_LIMIT);
let num_states = 30;
for idx in 0..num_states {
let mut state = MemDb::default();
let hash = B256::from(U256::from(idx));
let addr = Address::from_word(hash);
let balance = (idx * 2) as u64;
let info = AccountInfo::from_balance(U256::from(balance));
state.insert_account(addr, info);
storage.insert(hash, StateDb::new(state));
}
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
assert_eq!(storage.on_disk_states.len(), num_states - storage.min_in_memory_limit);
assert_eq!(storage.present.len(), storage.min_in_memory_limit);
for idx in 0..num_states {
let hash = B256::from(U256::from(idx));
let addr = Address::from_word(hash);
let loaded = storage.get(&hash).unwrap();
let acc = loaded.basic_ref(addr).unwrap().unwrap();
let balance = (idx * 2) as u64;
assert_eq!(acc.balance, U256::from(balance));
}
}
#[test]
fn test_storage_dump_reload_cycle() {
let mut dump_storage = BlockchainStorage::empty();
let partial_header = PartialHeader { gas_limit: 123456, ..Default::default() };
let bytes_first = &mut &hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap()[..];
let tx: MaybeImpersonatedTransaction =
TypedTransaction::decode(&mut &bytes_first[..]).unwrap().into();
let block =
Block::new::<MaybeImpersonatedTransaction>(partial_header.clone(), vec![tx.clone()]);
let block_hash = block.header.hash_slow();
dump_storage.blocks.insert(block_hash, block);
let serialized_blocks = dump_storage.serialized_blocks();
let serialized_transactions = dump_storage.serialized_transactions();
let mut load_storage = BlockchainStorage::empty();
load_storage.load_blocks(serialized_blocks);
load_storage.load_transactions(serialized_transactions);
let loaded_block = load_storage.blocks.get(&block_hash).unwrap();
assert_eq!(loaded_block.header.gas_limit, { partial_header.gas_limit });
let loaded_tx = loaded_block.transactions.first().unwrap();
assert_eq!(loaded_tx, &tx);
}
}