pub mod erc20;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use alloy_primitives::{Address, Log, U256};
use crate::cache::EvmCache;
use crate::errors::CacheResult as Result;
use crate::freshness::SlotChange;
use crate::state_update::{PurgeScope, StateDiff, StateUpdate};
pub trait StateView {
fn storage(&self, address: Address, slot: U256) -> Option<U256>;
}
pub trait EventDecoder: Send + Sync {
fn decode(&self, log: &Log, view: &dyn StateView) -> Vec<StateUpdate>;
}
#[derive(Default)]
pub struct DecoderRegistry {
global: Vec<Arc<dyn EventDecoder>>,
per_address: HashMap<Address, Vec<Arc<dyn EventDecoder>>>,
}
impl DecoderRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, decoder: Arc<dyn EventDecoder>) -> &mut Self {
self.global.push(decoder);
self
}
pub fn register_for_address(
&mut self,
address: Address,
decoder: Arc<dyn EventDecoder>,
) -> &mut Self {
self.per_address.entry(address).or_default().push(decoder);
self
}
pub fn decode(&self, log: &Log, view: &dyn StateView) -> Vec<StateUpdate> {
let mut out = Vec::new();
if let Some(scoped) = self.per_address.get(&log.address) {
for decoder in scoped {
out.extend(decoder.decode(log, view));
}
}
for decoder in &self.global {
out.extend(decoder.decode(log, view));
}
out
}
}
#[derive(Clone, Debug)]
pub struct ReorgConfig {
pub depth: usize,
pub scope: PurgeScope,
}
impl Default for ReorgConfig {
fn default() -> Self {
Self {
depth: 64,
scope: PurgeScope::AllStorage,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct BlockDigest {
pub block: u64,
pub applied: StateDiff,
pub decoded_logs: usize,
pub touched_slots: Vec<(Address, U256)>,
}
#[derive(Clone, Debug, Default)]
pub struct ReconcileReport {
pub checked: usize,
pub mismatched: Vec<SlotChange>,
}
pub struct EventPipeline {
registry: DecoderRegistry,
reorg: ReorgConfig,
touched: VecDeque<(u64, Vec<Address>)>,
derived: VecDeque<(u64, HashSet<(Address, U256)>)>,
}
impl EventPipeline {
pub fn new(registry: DecoderRegistry) -> Self {
Self {
registry,
reorg: ReorgConfig::default(),
touched: VecDeque::new(),
derived: VecDeque::new(),
}
}
pub fn with_reorg_config(mut self, cfg: ReorgConfig) -> Self {
self.reorg = cfg;
self
}
pub fn ingest_logs(&mut self, cache: &mut EvmCache, block: u64, logs: &[Log]) -> BlockDigest {
let mut digest = BlockDigest {
block,
..Default::default()
};
let mut touched_addrs: HashSet<Address> = HashSet::new();
let mut block_derived: HashSet<(Address, U256)> = HashSet::new();
for log in logs {
let updates = self.registry.decode(log, &*cache);
if updates.is_empty() {
continue;
}
let diff = cache.apply_updates(&updates);
digest.decoded_logs += 1;
for change in &diff.slots {
touched_addrs.insert(change.address);
Self::note_touched_slot(
&mut digest,
&mut block_derived,
change.address,
change.slot,
);
}
for change in &diff.accounts {
touched_addrs.insert(change.address);
}
for record in &diff.purged {
touched_addrs.insert(record.address);
}
for skip in &diff.skipped {
touched_addrs.insert(skip.address);
Self::note_touched_slot(&mut digest, &mut block_derived, skip.address, skip.slot);
}
for skip in &diff.skipped_balances {
touched_addrs.insert(skip.address);
}
for skip in &diff.skipped_masks {
touched_addrs.insert(skip.address);
Self::note_touched_slot(&mut digest, &mut block_derived, skip.address, skip.slot);
}
for skip in &diff.skipped_accounts {
touched_addrs.insert(skip.address);
}
digest.applied.merge(diff);
}
if !touched_addrs.is_empty() {
self.touched
.push_back((block, touched_addrs.into_iter().collect()));
if !block_derived.is_empty() {
self.derived.push_back((block, block_derived));
}
self.trim_ring();
}
digest
}
pub fn reorg_to(&mut self, cache: &mut EvmCache, new_head: u64) -> StateDiff {
let mut to_purge: HashSet<Address> = HashSet::new();
for (block, addrs) in &self.touched {
if *block > new_head {
to_purge.extend(addrs.iter().copied());
}
}
self.touched.retain(|(block, _)| *block <= new_head);
self.derived.retain(|(block, _)| *block <= new_head);
let updates: Vec<StateUpdate> = to_purge
.into_iter()
.map(|addr| StateUpdate::purge(addr, self.reorg.scope.clone()))
.collect();
cache.apply_updates(&updates)
}
pub fn reconcile(
&mut self,
cache: &mut EvmCache,
slots: &[(Address, U256)],
) -> Result<ReconcileReport> {
let mismatched = cache.reconcile_slots(slots)?;
Ok(ReconcileReport {
checked: slots.len(),
mismatched,
})
}
pub fn derived_slots(&self) -> impl Iterator<Item = (Address, U256)> + '_ {
let mut seen: HashSet<(Address, U256)> = HashSet::new();
for (_, pairs) in &self.derived {
seen.extend(pairs.iter().copied());
}
seen.into_iter()
}
fn note_touched_slot(
digest: &mut BlockDigest,
block_derived: &mut HashSet<(Address, U256)>,
address: Address,
slot: U256,
) {
block_derived.insert((address, slot));
if !digest.touched_slots.contains(&(address, slot)) {
digest.touched_slots.push((address, slot));
}
}
fn trim_ring(&mut self) {
while self.touched.len() > self.reorg.depth {
self.touched.pop_front();
}
while self.derived.len() > self.reorg.depth {
self.derived.pop_front();
}
}
}
pub type ReorgSignal = Option<u64>;
pub trait LogSource {
fn next_block(
&mut self,
) -> impl std::future::Future<Output = Option<(u64, Vec<Log>, ReorgSignal)>> + Send;
}
pub async fn drive<S, F>(
pipeline: &mut EventPipeline,
cache: &mut EvmCache,
mut source: S,
mut on_block: F,
) where
S: LogSource,
F: FnMut(&BlockDigest),
{
while let Some((block, logs, reorg)) = source.next_block().await {
if let Some(new_head) = reorg {
pipeline.reorg_to(cache, new_head);
}
let digest = pipeline.ingest_logs(cache, block, &logs);
on_block(&digest);
}
}