#![cfg(feature = "reactive")]
mod common;
use alloy_consensus::Header;
use alloy_primitives::{Address, B256};
use anyhow::Result;
use common::setup_cache;
use evm_fork_cache::cache::BlockContextRequirements;
fn header(number: u64, basefee: Option<u64>) -> Header {
Header {
number,
timestamp: 1_700_000_000 + number,
base_fee_per_gas: basefee,
beneficiary: Address::repeat_byte(0xcb),
gas_limit: 30_000_000,
mix_hash: B256::repeat_byte(0xab),
..Default::default()
}
}
#[test]
fn strict_requirements_reject_header_missing_basefee() {
let reqs = BlockContextRequirements::strict();
assert!(
reqs.validate_header(&header(100, Some(7))).is_ok(),
"a complete header must satisfy strict requirements"
);
let err = reqs
.validate_header(&header(100, None))
.expect_err("strict must reject a header with no base fee");
assert!(
err.to_string().to_lowercase().contains("basefee")
|| err.to_string().to_lowercase().contains("base fee"),
"the error must name the missing base-fee field, got: {err}"
);
}
#[test]
fn lenient_requirements_accept_incomplete_header() {
let reqs = BlockContextRequirements::lenient();
assert!(reqs.validate_header(&header(100, None)).is_ok());
assert!(reqs.validate_header(&header(100, Some(7))).is_ok());
}
#[test]
fn per_field_requirements_allow_opting_out_of_basefee() {
let mut reqs = BlockContextRequirements::strict();
reqs.require_basefee = false;
assert!(
reqs.validate_header(&header(100, None)).is_ok(),
"opting out of the base-fee requirement must accept a header without one"
);
}
#[tokio::test]
async fn advance_block_refreshes_all_block_env_fields() -> Result<()> {
let mut cache = setup_cache().await?;
let h = header(12_345, Some(42));
cache
.advance_block(&h)
.expect("lenient advance_block over a complete header succeeds");
assert_eq!(cache.block_number(), Some(12_345));
assert_eq!(cache.basefee(), Some(42));
assert_eq!(cache.coinbase(), Some(Address::repeat_byte(0xcb)));
assert_eq!(cache.prevrandao(), Some(B256::repeat_byte(0xab)));
assert_eq!(cache.block_gas_limit(), Some(30_000_000));
assert_eq!(cache.timestamp(), Some(1_700_000_000 + 12_345));
assert_eq!(
cache.block(),
alloy_eips::BlockId::number(12_345),
"advance_block must re-pin RPC fetches to the advanced block"
);
Ok(())
}
#[tokio::test]
async fn advance_block_strict_rejects_incomplete_header() -> Result<()> {
let mut cache = setup_cache().await?;
cache.set_block_context_requirements(BlockContextRequirements::strict());
let err = cache
.advance_block(&header(200, None))
.expect_err("strict advance_block must reject a header with no base fee");
assert!(
err.to_string().to_lowercase().contains("basefee")
|| err.to_string().to_lowercase().contains("base fee"),
"the error must name the missing base-fee field, got: {err}"
);
cache
.advance_block(&header(200, Some(9)))
.expect("strict advance_block over a complete header succeeds");
assert_eq!(cache.basefee(), Some(9));
Ok(())
}
use std::sync::Arc;
use alloy_network::Ethereum;
use alloy_provider::RootProvider;
use alloy_provider::network::AnyNetwork;
use alloy_rpc_client::RpcClient;
use alloy_transport::mock::Asserter;
use evm_fork_cache::EvmCacheBuilder;
use evm_fork_cache::reactive::{
ChainStatus, InputSource, ReactiveConfig, ReactiveContext, ReactiveInput, ReactiveInputBatch,
ReactiveInputRecord, ReactiveReport, ReactiveRuntime,
};
fn mock_provider() -> Arc<RootProvider<AnyNetwork>> {
let client = RpcClient::mocked(Asserter::new());
Arc::new(RootProvider::<AnyNetwork>::new(client))
}
#[tokio::test]
async fn try_build_strict_fails_when_header_unavailable() {
let result = EvmCacheBuilder::new(mock_provider())
.strict_block_context(true)
.try_build()
.await;
let err = match result {
Ok(_) => panic!("strict try_build over a header-less mock provider must error"),
Err(err) => err,
};
assert!(
err.to_string().to_lowercase().contains("fetch failed"),
"expected a fetch-failure error, got: {err}"
);
}
#[tokio::test]
async fn try_build_lenient_succeeds_without_header() -> Result<()> {
let cache = EvmCacheBuilder::new(mock_provider())
.strict_block_context(false)
.try_build()
.await?;
assert_eq!(cache.block_number(), None);
let _cache = EvmCacheBuilder::new(mock_provider()).try_build().await?;
Ok(())
}
fn rpc_header(number: u64, basefee: Option<u64>) -> alloy_rpc_types_eth::Header {
alloy_rpc_types_eth::Header::new(header(number, basefee))
}
fn included_header_context(number: u64) -> ReactiveContext {
let block = evm_fork_cache::reactive::BlockRef {
number,
hash: B256::repeat_byte(0x11),
parent_hash: Some(B256::repeat_byte(0x10)),
timestamp: Some(1_700_000_000 + number),
};
ReactiveContext {
chain_id: Some(1),
source: InputSource::Batch,
chain_status: ChainStatus::Included {
block: block.clone(),
confirmations: 0,
},
block: Some(block),
transaction_index: None,
log_index: None,
}
}
#[tokio::test]
async fn reactive_ingest_of_canonical_header_refreshes_block_env() -> Result<()> {
let mut cache = setup_cache().await?;
let mut runtime = ReactiveRuntime::<Ethereum>::new(ReactiveConfig::default());
let input = ReactiveInput::BlockHeader(rpc_header(7_777, Some(123)));
let batch = ReactiveInputBatch::new(vec![ReactiveInputRecord::new(
input,
included_header_context(7_777),
)]);
let report = runtime.ingest_batch(&mut cache, batch)?;
assert_eq!(cache.block_number(), Some(7_777));
assert_eq!(cache.basefee(), Some(123));
assert_eq!(cache.timestamp(), Some(1_700_000_000 + 7_777));
assert!(
!report
.reports
.iter()
.any(|r| matches!(r.as_ref(), ReactiveReport::Error(_))),
"a lenient canonical drive must not surface an error report"
);
Ok(())
}
#[tokio::test]
async fn reactive_ingest_of_pending_header_does_not_refresh_block_env() -> Result<()> {
let mut cache = setup_cache().await?;
let mut runtime = ReactiveRuntime::<Ethereum>::new(ReactiveConfig::default());
let ctx = ReactiveContext {
chain_id: Some(1),
source: InputSource::Subscription,
chain_status: ChainStatus::Pending,
block: None,
transaction_index: None,
log_index: None,
};
let input = ReactiveInput::BlockHeader(rpc_header(9_999, Some(55)));
let batch = ReactiveInputBatch::new(vec![ReactiveInputRecord::new(input, ctx)]);
runtime.ingest_batch(&mut cache, batch)?;
assert_eq!(cache.block_number(), None);
assert_eq!(cache.basefee(), None);
Ok(())
}
#[tokio::test]
async fn reactive_strict_drive_surfaces_error_report_for_incomplete_header() -> Result<()> {
let mut cache = setup_cache().await?;
cache.set_block_context_requirements(BlockContextRequirements::strict());
let mut runtime = ReactiveRuntime::<Ethereum>::new(ReactiveConfig::default());
let input = ReactiveInput::BlockHeader(rpc_header(4_242, None));
let batch = ReactiveInputBatch::new(vec![ReactiveInputRecord::new(
input,
included_header_context(4_242),
)]);
let report = runtime.ingest_batch(&mut cache, batch)?;
let error_message = report
.reports
.iter()
.find_map(|r| match r.as_ref() {
ReactiveReport::Error(e) => Some(e.message.clone()),
_ => None,
})
.expect("strict drive over an incomplete header must surface an error report");
assert!(
error_message.to_lowercase().contains("basefee"),
"the error report must name the missing base-fee field, got: {error_message}"
);
Ok(())
}