#![cfg(feature = "reactive")]
mod common;
use std::sync::Arc;
use alloy_network::Ethereum;
use alloy_primitives::{Address, B256, Bytes, Log as PrimitiveLog, U256};
use alloy_rpc_types_eth::{Filter, Log};
use anyhow::Result;
use common::setup_cache;
use evm_fork_cache::StateUpdate;
use evm_fork_cache::events::StateView;
use evm_fork_cache::reactive::{
BlockRef, ChainStatus, HandlerError, HandlerId, HandlerOutcome, InputSource, LogInterest,
ReactiveConfig, ReactiveContext, ReactiveEffect, ReactiveHandler, ReactiveInput,
ReactiveInputBatch, ReactiveInputRecord, ReactiveInterest, ReactiveRuntime, RouteKeySpec,
StateEffectQuality,
};
fn block(number: u64, hash: B256, parent_hash: B256) -> BlockRef {
BlockRef {
number,
hash,
parent_hash: Some(parent_hash),
timestamp: Some(1_700_000_000 + number),
}
}
fn rpc_log(address: Address, block: &BlockRef, log_index: u64) -> Log {
Log {
inner: PrimitiveLog::new_unchecked(address, vec![B256::repeat_byte(0xee)], Bytes::new()),
block_hash: Some(block.hash),
block_number: Some(block.number),
block_timestamp: block.timestamp,
transaction_hash: Some(B256::repeat_byte(0x01)),
transaction_index: Some(0),
log_index: Some(log_index),
removed: false,
}
}
fn included_context(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 batch(input: ReactiveInput<Ethereum>, ctx: ReactiveContext) -> ReactiveInputBatch<Ethereum> {
ReactiveInputBatch::new(vec![ReactiveInputRecord::new(input, ctx)])
}
struct SlotWriter {
address: Address,
slot: U256,
}
impl ReactiveHandler<Ethereum> for SlotWriter {
fn id(&self) -> HandlerId {
HandlerId::new("freshness-slot-writer")
}
fn interests(&self) -> Vec<ReactiveInterest> {
vec![ReactiveInterest::Logs(LogInterest {
provider_filter: Filter::new().address(self.address),
local_matcher: None,
route_key: Some(RouteKeySpec::EmitterAddress),
})]
}
fn handle(
&self,
_ctx: &ReactiveContext,
_input: &ReactiveInput<Ethereum>,
_state: &dyn StateView,
) -> Result<HandlerOutcome, HandlerError> {
Ok(HandlerOutcome {
effects: vec![ReactiveEffect::StateUpdate(StateUpdate::slot(
self.address,
self.slot,
U256::from(1u64),
))],
quality: StateEffectQuality::ExactFromInput,
tags: vec![],
})
}
}
struct BlockValueWriter {
address: Address,
slot: U256,
}
impl ReactiveHandler<Ethereum> for BlockValueWriter {
fn id(&self) -> HandlerId {
HandlerId::new("freshness-block-value-writer")
}
fn interests(&self) -> Vec<ReactiveInterest> {
vec![ReactiveInterest::Logs(LogInterest {
provider_filter: Filter::new().address(self.address),
local_matcher: None,
route_key: Some(RouteKeySpec::EmitterAddress),
})]
}
fn handle(
&self,
ctx: &ReactiveContext,
_input: &ReactiveInput<Ethereum>,
_state: &dyn StateView,
) -> Result<HandlerOutcome, HandlerError> {
let number = ctx.block.as_ref().map(|b| b.number).unwrap_or_default();
Ok(HandlerOutcome {
effects: vec![ReactiveEffect::StateUpdate(StateUpdate::slot(
self.address,
self.slot,
U256::from(number),
))],
quality: StateEffectQuality::ExactFromInput,
tags: vec![],
})
}
}
#[tokio::test]
async fn reactive_write_stamps_validity_through_block() -> Result<()> {
let address = Address::repeat_byte(0xf1);
let slot = U256::from(3);
let mut cache = setup_cache().await?;
let mut runtime = ReactiveRuntime::<Ethereum>::new(ReactiveConfig::default());
runtime.enable_freshness_stamping();
runtime.register_handler(Arc::new(SlotWriter { address, slot }))?;
let b10 = block(10, B256::repeat_byte(0x10), B256::repeat_byte(0x0f));
runtime.ingest_batch(
&mut cache,
batch(
ReactiveInput::Log(rpc_log(address, &b10, 10)),
included_context(b10.clone(), 10),
),
)?;
let freshness = runtime
.freshness()
.expect("stamping was enabled, so a registry is present");
assert!(
!freshness.is_volatile(address, slot, 10),
"an event-stamped slot is valid through its write block"
);
assert!(
freshness.is_volatile(address, slot, 11),
"the stamp ages to volatile after its block"
);
Ok(())
}
#[tokio::test]
async fn freshness_registry_absent_unless_enabled() -> Result<()> {
let runtime = ReactiveRuntime::<Ethereum>::new(ReactiveConfig::default());
assert!(
runtime.freshness().is_none(),
"freshness stamping is opt-in; disabled by default"
);
Ok(())
}
fn pending_context(block: BlockRef, log_index: u64) -> ReactiveContext {
ReactiveContext {
chain_id: Some(1),
source: InputSource::Batch,
chain_status: ChainStatus::Pending,
block: Some(block),
transaction_index: Some(0),
log_index: Some(log_index),
}
}
#[tokio::test]
async fn pending_write_does_not_stamp_validity() -> Result<()> {
let address = Address::repeat_byte(0xf2);
let slot = U256::from(7);
let mut cache = setup_cache().await?;
let mut runtime = ReactiveRuntime::<Ethereum>::new(ReactiveConfig::default());
runtime.enable_freshness_stamping();
runtime.register_handler(Arc::new(SlotWriter { address, slot }))?;
let b10 = block(10, B256::repeat_byte(0x10), B256::repeat_byte(0x0f));
let _ = runtime.ingest_batch(
&mut cache,
batch(
ReactiveInput::Log(rpc_log(address, &b10, 10)),
pending_context(b10.clone(), 10),
),
);
let freshness = runtime
.freshness()
.expect("stamping was enabled, so a registry is present");
assert!(
freshness.is_volatile(address, slot, 10),
"a pending write must not stamp canonical freshness"
);
Ok(())
}
#[tokio::test]
async fn later_canonical_stamp_wins() -> Result<()> {
let address = Address::repeat_byte(0xf3);
let slot = U256::from(9);
let mut cache = setup_cache().await?;
let mut runtime = ReactiveRuntime::<Ethereum>::new(ReactiveConfig::default());
runtime.enable_freshness_stamping();
runtime.register_handler(Arc::new(BlockValueWriter { address, slot }))?;
let b10 = block(10, B256::repeat_byte(0x10), B256::repeat_byte(0x0f));
runtime.ingest_batch(
&mut cache,
batch(
ReactiveInput::Log(rpc_log(address, &b10, 10)),
included_context(b10.clone(), 10),
),
)?;
assert!(
!runtime.freshness().unwrap().is_volatile(address, slot, 10),
"valid through its first write block"
);
assert!(
runtime.freshness().unwrap().is_volatile(address, slot, 11),
"the first stamp is volatile once past block 10"
);
let b11 = block(11, B256::repeat_byte(0x11), b10.hash);
runtime.ingest_batch(
&mut cache,
batch(
ReactiveInput::Log(rpc_log(address, &b11, 11)),
included_context(b11.clone(), 11),
),
)?;
let freshness = runtime
.freshness()
.expect("stamping was enabled, so a registry is present");
assert!(
!freshness.is_volatile(address, slot, 11),
"the later stamp wins: valid through block 11"
);
assert!(
freshness.is_volatile(address, slot, 12),
"the later stamp ages to volatile after block 11"
);
Ok(())
}