From af090f3ce94e46e05130a4e97beffdafd3c13eb8 Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Tue, 18 Aug 2026 00:55:58 +0200 Subject: [PATCH] refactor(indexer): make the cache window a NonZeroU32 --- lez/indexer/core/src/config.rs | 19 +++--- lez/indexer/core/src/cross_zone_verifier.rs | 65 +++++++++------------ lez/indexer/core/src/lib.rs | 4 +- test_fixtures/src/config.rs | 4 +- 4 files changed, 45 insertions(+), 47 deletions(-) diff --git a/lez/indexer/core/src/config.rs b/lez/indexer/core/src/config.rs index 7e26f87a3..9d9d11305 100644 --- a/lez/indexer/core/src/config.rs +++ b/lez/indexer/core/src/config.rs @@ -1,4 +1,4 @@ -use std::{fs::File, io::BufReader, path::Path, time::Duration}; +use std::{fs::File, io::BufReader, num::NonZeroU32, path::Path, time::Duration}; use anyhow::{Context as _, Result}; use common::{HashType, config::BasicAuth}; @@ -36,12 +36,12 @@ pub struct IndexerConfig { #[serde(default)] pub cross_zone_accept_unverified: Vec, /// Peer-block bodies the cross-zone verifier keeps behind each peer's - /// verified tip. `None` takes the built-in default of 1024. `u32::MAX` is - /// effectively unbounded: the escape hatch for a deployment whose - /// dispatches routinely reach further back than any fixed window, paid - /// for in unbounded memory. `0` is rejected at startup. - #[serde(default)] - pub peer_block_cache_window: Option, + /// verified tip. Omitted means 1024. `u32::MAX` is effectively unbounded: + /// the escape hatch for a deployment whose dispatches routinely reach + /// further back than any fixed window, paid for in unbounded memory. Zero + /// is unrepresentable and rejected at config parse. + #[serde(default = "default_peer_block_cache_window")] + pub peer_block_cache_window: NonZeroU32, /// Bridge-lock holdings to seed into genesis, mirroring the sequencer's /// `SupplyBridgeLockHolding` actions. They are not produced by any /// transaction, so the indexer must seed them to match the sequencer's state. @@ -79,3 +79,8 @@ impl IndexerConfig { }) } } + +/// The window applied when the config omits `peer_block_cache_window`. +pub(crate) const fn default_peer_block_cache_window() -> NonZeroU32 { + NonZeroU32::new(1024).expect("1024 is nonzero") +} diff --git a/lez/indexer/core/src/cross_zone_verifier.rs b/lez/indexer/core/src/cross_zone_verifier.rs index 03df2f2f2..03cfd5b9e 100644 --- a/lez/indexer/core/src/cross_zone_verifier.rs +++ b/lez/indexer/core/src/cross_zone_verifier.rs @@ -1,6 +1,7 @@ use std::{ collections::{HashMap, HashSet}, fmt::{self, Display, Formatter}, + num::{NonZeroU32, NonZeroU64}, sync::{ Arc, Mutex, MutexGuard, PoisonError, atomic::{AtomicU64, Ordering}, @@ -52,10 +53,6 @@ const PEER_BLOCK_WAIT_TIMEOUT: Duration = Duration::from_secs(300); /// sleep can overshoot, understates it. const PEER_BLOCK_POLL_INTERVAL: Duration = Duration::from_secs(1); -/// Peer-block bodies kept behind each verified run tip when -/// [`IndexerConfig::peer_block_cache_window`] is not configured. -const DEFAULT_BODY_WINDOW: u64 = 1024; - /// Why a cross-zone dispatch could not be verified. /// /// A forgery is terminal and must stop the block applying; an unavailable peer @@ -246,17 +243,17 @@ struct PeerBlocks { impl Default for PeerBlocks { fn default() -> Self { - Self::new(None) + Self::new(crate::config::default_peer_block_cache_window()) } } impl PeerBlocks { - /// `configured` is [`IndexerConfig::peer_block_cache_window`]; zero never - /// reaches here, [`CrossZoneVerifier::start`] rejects it. - fn new(configured: Option) -> Self { + /// `configured` is [`IndexerConfig::peer_block_cache_window`]; zero is + /// unrepresentable there. + fn new(configured: NonZeroU32) -> Self { Self { chains: Arc::default(), - window: configured.map_or(DEFAULT_BODY_WINDOW, u64::from), + window: NonZeroU64::from(configured).get(), } } @@ -648,10 +645,6 @@ impl CrossZoneVerifier { /// Returns `None` when cross-zone messaging is disabled. pub fn start(config: &IndexerConfig) -> Option { let cross_zone = config.cross_zone.as_ref()?; - assert!( - config.peer_block_cache_window != Some(0), - "peer_block_cache_window must not be 0: omit it for the default 1024, use u32::MAX for effectively unbounded" - ); let self_zone: ZoneId = *config.channel_id.as_ref(); let peers = PeerBlocks::new(config.peer_block_cache_window); let watch = PeerWatch::default(); @@ -1372,10 +1365,15 @@ mod tests { } } - /// A verifier whose peer cache keeps `window` bodies behind the run tip. - fn verifier_with_window(window: u32) -> CrossZoneVerifier { + /// `n` as a test cache window. + fn window(n: u32) -> NonZeroU32 { + NonZeroU32::new(n).expect("test windows are nonzero") + } + + /// A verifier whose peer cache keeps `n` bodies behind the run tip. + fn verifier_with_window(n: u32) -> CrossZoneVerifier { CrossZoneVerifier { - peers: PeerBlocks::new(Some(window)), + peers: PeerBlocks::new(window(n)), ..verifier() } } @@ -2399,7 +2397,7 @@ mod tests { #[tokio::test] async fn advancing_the_prefix_evicts_bodies_deeper_than_the_window() { - let peers = PeerBlocks::new(Some(1)); + let peers = PeerBlocks::new(window(1)); let chain = linked_chain(4); cache_chain_with_slots(&peers, chain.clone()).await; @@ -2436,7 +2434,7 @@ mod tests { #[tokio::test] async fn exactly_window_bodies_stay_behind_the_tip() { - let peers = PeerBlocks::new(Some(2)); + let peers = PeerBlocks::new(window(2)); cache_chain_with_slots(&peers, linked_chain(5)).await; // Tip 5, window 2: the floor is 3, so ids 3 and 4 are the two bodies @@ -2461,7 +2459,7 @@ mod tests { #[tokio::test] async fn a_u32_max_window_never_evicts() { - let peers = PeerBlocks::new(Some(u32::MAX)); + let peers = PeerBlocks::new(window(u32::MAX)); cache_chain_with_slots(&peers, linked_chain(6)).await; for block_id in GENESIS_BLOCK_ID..=6 { @@ -2475,28 +2473,23 @@ mod tests { } } + /// Zero is unrepresentable in the config type, so a zero window dies at + /// parse, and an omitted field takes the 1024 default. #[test] - #[should_panic(expected = "peer_block_cache_window must not be 0")] - fn start_rejects_a_zero_window() { - let config = IndexerConfig { - consensus_info_polling_interval: Duration::from_secs(1), - bedrock_config: crate::config::ClientConfig { - addr: "http://localhost:8080".parse().expect("valid url"), - auth: None, - }, - channel_id: ChannelId::from([1; 32]), - cross_zone: Some(cross_zone_inbox_core::CrossZoneConfig { peers: Vec::new() }), - cross_zone_accept_unverified: Vec::new(), - peer_block_cache_window: Some(0), - bridge_lock_holdings: Vec::new(), - allow_chain_reset: false, - }; - let _ = CrossZoneVerifier::start(&config); + fn a_zero_window_is_rejected_at_config_parse() { + assert!( + serde_json::from_str::("0").is_err(), + "zero must not deserialize into the window type" + ); + assert_eq!( + crate::config::default_peer_block_cache_window(), + window(1024) + ); } #[tokio::test] async fn eviction_leaves_the_run_and_its_admission_rules_intact() { - let peers = PeerBlocks::new(Some(1)); + let peers = PeerBlocks::new(window(1)); let chain = linked_chain(4); cache_chain_with_slots(&peers, chain.iter().take(3).cloned().collect()).await; // Tip 3, window 1: id 1 is demoted. diff --git a/lez/indexer/core/src/lib.rs b/lez/indexer/core/src/lib.rs index 072c5eae7..c282159dd 100644 --- a/lez/indexer/core/src/lib.rs +++ b/lez/indexer/core/src/lib.rs @@ -585,7 +585,7 @@ impl IndexerCore { #[cfg(test)] mod tests { - use std::time::Duration; + use std::{num::NonZeroU32, time::Duration}; use common::{HashType, block::HashableBlockData}; use logos_blockchain_zone_sdk::Slot; @@ -656,7 +656,7 @@ mod tests { allow_chain_reset: false, cross_zone: None, cross_zone_accept_unverified, - peer_block_cache_window: None, + peer_block_cache_window: NonZeroU32::new(1024).expect("1024 is nonzero"), bridge_lock_holdings: Vec::new(), }; IndexerCore::open(config, dir).expect("open core") diff --git a/test_fixtures/src/config.rs b/test_fixtures/src/config.rs index abcf54034..d67938d33 100644 --- a/test_fixtures/src/config.rs +++ b/test_fixtures/src/config.rs @@ -1,4 +1,4 @@ -use std::{net::SocketAddr, path::PathBuf, time::Duration}; +use std::{net::SocketAddr, num::NonZeroU32, path::PathBuf, time::Duration}; use anyhow::{Context as _, Result}; use bytesize::ByteSize; @@ -258,7 +258,7 @@ pub fn indexer_config( }, channel_id, cross_zone, - peer_block_cache_window: None, + peer_block_cache_window: NonZeroU32::new(1024).expect("1024 is nonzero"), bridge_lock_holdings: Vec::new(), allow_chain_reset: false, })