#[path = "support/mock.rs"]
mod mock;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use alloy_eips::BlockId;
use alloy_network::Ethereum;
use alloy_primitives::{Address, B256, Bytes, Log as PrimitiveLog, U256, keccak256};
use alloy_rpc_types_eth::{Filter, Log};
use anyhow::Result;
use evm_fork_cache::StateUpdate;
use evm_fork_cache::cache::StorageBatchFetchFn;
use evm_fork_cache::events::StateView;
use evm_fork_cache::reactive::{
AppliedReport, BlockRef, ChainStatus, HandlerError, HandlerId, HandlerOutcome, HookSignal,
InputSource, LogInterest, ReactiveConfig, ReactiveContext, ReactiveEffect, ReactiveHandler,
ReactiveHook, ReactiveInput, ReactiveInputBatch, ReactiveInputRecord, ReactiveInterest,
ReactiveReport, ReactiveRuntime, ReportTag, RouteKeySpec, StateEffectQuality,
};
const TARGET_SLOT: u64 = 7;
struct PostStateWriter {
emitter: Address,
slot: U256,
}
impl ReactiveHandler<Ethereum> for PostStateWriter {
fn id(&self) -> HandlerId {
HandlerId::new("post-state-writer")
}
fn interests(&self) -> Vec<ReactiveInterest> {
vec![ReactiveInterest::Logs(LogInterest {
provider_filter: Filter::new().address(self.emitter),
local_matcher: None,
route_key: Some(RouteKeySpec::EmitterAddress),
})]
}
fn handle(
&self,
_ctx: &ReactiveContext,
input: &ReactiveInput<Ethereum>,
_state: &dyn StateView,
) -> Result<HandlerOutcome, HandlerError> {
let ReactiveInput::Log(log) = input else {
return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
};
let data = &log.inner.data.data;
if data.len() != 32 {
return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
}
let value = U256::from_be_slice(data);
Ok(HandlerOutcome {
effects: vec![
ReactiveEffect::StateUpdate(StateUpdate::slot(log.address(), self.slot, value)),
ReactiveEffect::Hook(HookSignal {
namespace: "demo".into(),
kind: "slot.write".into(),
labels: vec![ReportTag::new("slot", self.slot.to_string())],
payload: None,
}),
],
quality: StateEffectQuality::ExactFromInput,
tags: vec![],
})
}
}
#[derive(Default)]
struct AppliedLog {
writes: Arc<Mutex<Vec<U256>>>,
signals: Arc<Mutex<Vec<String>>>,
}
impl ReactiveHook<Ethereum> for AppliedLog {
fn on_report(&self, report: Arc<ReactiveReport<Ethereum>>) {
if let ReactiveReport::Applied(AppliedReport {
diff, hook_signals, ..
}) = report.as_ref()
{
self.writes
.lock()
.unwrap()
.extend(diff.slots.iter().map(|change| change.new));
self.signals.lock().unwrap().extend(
hook_signals
.iter()
.map(|signal| format!("{}:{}", signal.namespace, signal.kind)),
);
}
}
}
fn value_log(
emitter: Address,
topic: B256,
value: U256,
block: &BlockRef,
log_index: u64,
removed: bool,
) -> Log {
Log {
inner: PrimitiveLog::new_unchecked(
emitter,
vec![topic],
Bytes::copy_from_slice(&value.to_be_bytes::<32>()),
),
block_hash: Some(block.hash),
block_number: Some(block.number),
block_timestamp: block.timestamp,
transaction_hash: Some(B256::repeat_byte(0xfe)),
transaction_index: Some(0),
log_index: Some(log_index),
removed,
}
}
fn block_ref(number: u64, hash: u8, parent: u8) -> BlockRef {
BlockRef {
number,
hash: B256::repeat_byte(hash),
parent_hash: Some(B256::repeat_byte(parent)),
timestamp: Some(1_700_000_000 + number),
}
}
fn included(block: BlockRef, log_index: u64) -> ReactiveContext {
ReactiveContext {
chain_id: Some(1),
source: InputSource::Batch,
chain_status: ChainStatus::Included {
block: block.clone(),
confirmations: 0,
},
block: Some(block),
transaction_index: Some(0),
log_index: Some(log_index),
}
}
fn reorged(dropped: BlockRef, log_index: u64) -> ReactiveContext {
ReactiveContext {
chain_id: Some(1),
source: InputSource::Batch,
chain_status: ChainStatus::Reorged {
dropped_from: dropped.clone(),
},
block: Some(dropped),
transaction_index: Some(0),
log_index: Some(log_index),
}
}
fn one(input: ReactiveInput<Ethereum>, ctx: ReactiveContext) -> ReactiveInputBatch<Ethereum> {
ReactiveInputBatch::new(vec![ReactiveInputRecord::new(input, ctx)])
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
let mut cache = mock::offline_cache().await?;
let emitter = Address::repeat_byte(0x42);
mock::install_mock_erc20(&mut cache, emitter);
let slot = U256::from(TARGET_SLOT);
cache
.db_mut()
.insert_account_storage(emitter, slot, U256::from(10))?;
let fetches = Arc::new(AtomicUsize::new(0));
let counter = fetches.clone();
let fetcher: StorageBatchFetchFn =
Arc::new(move |requests: Vec<(Address, U256)>, _block: BlockId| {
counter.fetch_add(requests.len(), Ordering::Relaxed);
requests
.into_iter()
.map(|(a, s)| (a, s, Ok(U256::ZERO)))
.collect()
});
cache.set_storage_batch_fetcher(fetcher);
let hook = Arc::new(AppliedLog::default());
let mut runtime = ReactiveRuntime::<Ethereum>::new(ReactiveConfig::default());
runtime.register_handler(Arc::new(PostStateWriter { emitter, slot }))?;
runtime.register_hook(hook.clone())?;
let topic = keccak256(b"PostState(uint256)");
println!(
"slot {} starts at {:?} (the on-chain value before any event)",
TARGET_SLOT,
cache.cached_storage_value(emitter, slot)
);
let canonical = block_ref(100, 0x64, 0x63);
fetches.store(0, Ordering::Relaxed);
let report = runtime.ingest_batch(
&mut cache,
one(
ReactiveInput::Log(value_log(
emitter,
topic,
U256::from(20),
&canonical,
0,
false,
)),
included(canonical.clone(), 0),
),
)?;
println!("\n=== block {} ingested ===", canonical.number);
println!(
" applied {} update(s) with {} RPC fetch(es) <- events carry the post-state",
report.applied.len(),
fetches.load(Ordering::Relaxed),
);
println!(
" slot {} = {:?}",
TARGET_SLOT,
cache.cached_storage_value(emitter, slot)
);
let report = runtime.ingest_batch(
&mut cache,
one(
ReactiveInput::Log(value_log(
emitter,
topic,
U256::from(20),
&canonical,
0,
true,
)),
reorged(canonical.clone(), 0),
),
)?;
let reorg = report
.reports
.iter()
.find_map(|report| match report.as_ref() {
ReactiveReport::Reorg(reorg) => Some(reorg),
_ => None,
})
.expect("a removed log emits a reorg report");
println!("\n=== reorg: block {} dropped ===", canonical.number);
println!(" rolled back {} write(s):", reorg.rollback_updates.len());
for change in &reorg.rollback_diff.slots {
println!(" slot rolled {} -> {}", change.old, change.new);
}
println!(
" slot {} restored to {:?} <- automatic, journaled recovery",
TARGET_SLOT,
cache.cached_storage_value(emitter, slot)
);
println!(
"\nhook observed: writes={:?}, signals={:?}",
hook.writes.lock().unwrap(),
hook.signals.lock().unwrap()
);
Ok(())
}