#![cfg(feature = "reactive")]
mod common;
use alloy_network::Ethereum;
use alloy_primitives::B256;
use anyhow::Result;
use common::setup_cache;
use evm_fork_cache::reactive::{
BlockRef, CanonicalSequenceMutation, CanonicalSequenceState, ChainControl, ChainStatus,
InputSource, ReactiveConfig, ReactiveContext, ReactiveError, ReactiveInput, ReactiveInputBatch,
ReactiveInputRecord, ReactiveRuntime, validate_canonical_sequence,
validate_canonical_sequence_diagnostic,
};
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 state() -> (CanonicalSequenceState, BlockRef, BlockRef) {
let parent = block(2, B256::repeat_byte(0x02), B256::repeat_byte(0x01));
let head = block(3, B256::repeat_byte(0x03), B256::repeat_byte(0x02));
(
CanonicalSequenceState::new(vec![parent, head], Some(head), None, None),
parent,
head,
)
}
fn controls(controls: impl IntoIterator<Item = ChainControl>) -> ReactiveInputBatch<Ethereum> {
ReactiveInputBatch::<Ethereum>::new(Vec::new()).with_chain_controls(controls)
}
#[test]
fn coverage_defaults_to_unknown_and_is_only_ever_set_explicitly() {
let (state, _, head) = state();
assert_eq!(state.log_coverage_head(), None);
let seeded = state.clone().with_log_coverage_head(Some(head));
assert_eq!(seeded.log_coverage_head(), Some(&head));
assert_eq!(
seeded.with_log_coverage_head(None).log_coverage_head(),
None
);
}
#[test]
fn attestation_advances_only_the_log_watermark() {
let (state, _, head) = state();
let validation =
validate_canonical_sequence(&state, &controls([ChainControl::LogCoverage(head)]))
.expect("attesting the canonical head is valid");
assert_eq!(validation.next_state().log_coverage_head(), Some(&head));
assert_eq!(
validation.next_state().coverage_head(),
Some(&head),
"canonical coverage must be unchanged by an attestation"
);
assert!(
validation.mutations().iter().any(
|mutation| matches!(mutation, CanonicalSequenceMutation::LogCoverage(b) if *b == head)
),
"the watermark must be replayable by an external checkpoint owner"
);
assert!(
!validation
.mutations()
.iter()
.any(|mutation| matches!(mutation, CanonicalSequenceMutation::Canonical(_))),
"an attestation must not stage canonical progress"
);
}
#[test]
fn attestation_may_repeat_but_never_retreat() {
let (state, parent, head) = state();
let attested = state.with_log_coverage_head(Some(head));
validate_canonical_sequence(&attested, &controls([ChainControl::LogCoverage(head)]))
.expect("re-attesting the same block is idempotent");
let error = validate_canonical_sequence_diagnostic(
&attested,
&controls([ChainControl::LogCoverage(parent)]),
)
.expect_err("a regressing watermark must be rejected");
assert!(
format!("{error:?}").contains("log coverage"),
"the error must name the rejected watermark: {error:?}"
);
}
#[test]
fn attestation_cannot_outrun_canonical_coverage() {
let (state, _, head) = state();
let ahead = block(
head.number + 1,
B256::repeat_byte(0x04),
B256::repeat_byte(0x03),
);
assert!(matches!(
validate_canonical_sequence(&state, &controls([ChainControl::LogCoverage(ahead)])),
Err(ReactiveError::InvalidChainControl { .. })
));
}
#[test]
fn attestation_identity_must_agree_with_retained_history() {
let (state, _, head) = state();
let impostor = block(
head.number,
B256::repeat_byte(0xee),
B256::repeat_byte(0x02),
);
assert!(matches!(
validate_canonical_sequence(&state, &controls([ChainControl::LogCoverage(impostor)])),
Err(ReactiveError::InvalidChainControl { .. })
));
}
#[test]
fn attesting_a_retained_ancestor_is_valid_and_lags_behind_coverage() {
let (state, parent, head) = state();
let validation =
validate_canonical_sequence(&state, &controls([ChainControl::LogCoverage(parent)]))
.expect("a lagging attestation is valid");
assert_eq!(validation.next_state().log_coverage_head(), Some(&parent));
assert_eq!(validation.next_state().coverage_head(), Some(&head));
}
#[test]
fn watermark_survives_a_state_snapshot_round_trip() {
let (state, _, head) = state();
let attested =
validate_canonical_sequence(&state, &controls([ChainControl::LogCoverage(head)]))
.expect("valid attestation")
.next_state()
.clone();
let restored = CanonicalSequenceState::new(
attested.retained_canonical_history().to_vec(),
attested.coverage_head().copied(),
attested.safe_head().copied(),
attested.finalized_head().copied(),
)
.with_log_coverage_head(attested.log_coverage_head().copied());
assert_eq!(restored.log_coverage_head(), Some(&head));
restored.validate().expect("restored state is coherent");
}
fn header_record(point: BlockRef) -> ReactiveInputRecord<Ethereum> {
ReactiveInputRecord::new(
ReactiveInput::BlockHeader(alloy_rpc_types_eth::Header {
hash: point.hash,
inner: alloy_consensus::Header {
number: point.number,
parent_hash: point.parent_hash.unwrap_or_default(),
timestamp: point.timestamp.unwrap_or_default(),
..alloy_consensus::Header::default()
},
total_difficulty: None,
size: None,
}),
ReactiveContext {
chain_id: None,
source: InputSource::Subscription,
chain_status: ChainStatus::Included {
block: point,
confirmations: 0,
},
block: Some(point),
transaction_index: None,
log_index: None,
},
)
}
#[tokio::test]
async fn runtime_exposes_the_attested_watermark_only_once_attested() -> Result<()> {
let head = block(41, B256::repeat_byte(0x41), B256::repeat_byte(0x40));
let mut cache = setup_cache().await?;
let mut runtime = ReactiveRuntime::<Ethereum>::new(ReactiveConfig::default());
runtime.ingest_batch(
&mut cache,
ReactiveInputBatch::new(vec![header_record(head)]),
)?;
assert_eq!(
runtime.log_coverage_head(),
None,
"coverage progress must never imply log completeness"
);
runtime.ingest_batch(
&mut cache,
ReactiveInputBatch::<Ethereum>::new(Vec::new())
.with_chain_id(1)
.with_chain_controls([ChainControl::LogCoverage(head)]),
)?;
assert_eq!(runtime.log_coverage_head(), Some(&head));
Ok(())
}