From 88c6c52ec857d3630c837862142c36251149e016 Mon Sep 17 00:00:00 2001 From: Joe Parks <26990067+jowparks@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:39:14 -0700 Subject: [PATCH 1/2] fix(exex): skip missing notification files when filling the WAL block cache `WalInner::commit` consumes the next file ID before writing the notification, so a failed write (e.g. a full disk) leaves a permanent hole in the file ID range. On the next boot `fill_block_cache` walks `min..=max` and fails with `WalError::FileNotFound`, which propagates out of `Wal::new` in `ExExLauncher::launch` and aborts node startup. The node then crash-loops until the WAL directory is deleted by hand. Log a warning and skip the missing file instead. Other `WalError` variants still abort as before. --- crates/exex/exex/src/wal/mod.rs | 52 +++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/crates/exex/exex/src/wal/mod.rs b/crates/exex/exex/src/wal/mod.rs index 0836e15b55a..f6a4ab8af5e 100644 --- a/crates/exex/exex/src/wal/mod.rs +++ b/crates/exex/exex/src/wal/mod.rs @@ -23,7 +23,7 @@ use alloy_eips::BlockNumHash; use alloy_primitives::B256; use parking_lot::{RwLock, RwLockReadGuard}; use reth_exex_types::ExExNotification; -use reth_tracing::tracing::{debug, instrument}; +use reth_tracing::tracing::{debug, instrument, warn}; /// WAL is a write-ahead log (WAL) that stores the notifications sent to ExExes. /// @@ -120,7 +120,21 @@ where let mut notifications_size = 0; for entry in self.storage.iter_notifications(files_range) { - let (file_id, size, notification) = entry?; + // `commit` consumes the next file ID before writing, so a failed write (e.g. a full + // disk) leaves a permanent hole in the ID range. Aborting here would make the node + // unbootable until the WAL directory is deleted by hand. + let (file_id, size, notification) = match entry { + Ok(entry) => entry, + Err(WalError::FileNotFound(file_id)) => { + warn!( + target: "exex::wal", + ?file_id, + "Notification file is missing from the WAL, skipping" + ); + continue + } + Err(err) => return Err(err), + }; notifications_size += size; @@ -238,12 +252,13 @@ mod tests { use crate::wal::{cache::CachedBlock, error::WalResult, Wal}; use alloy_primitives::B256; use itertools::Itertools; + use reth_ethereum_primitives::EthPrimitives; use reth_exex_types::ExExNotification; use reth_provider::Chain; use reth_testing_utils::generators::{ self, random_block, random_block_range, BlockParams, BlockRangeParams, }; - use std::{collections::BTreeMap, sync::Arc}; + use std::{collections::BTreeMap, fs, sync::Arc}; fn read_notifications(wal: &Wal) -> WalResult> { wal.inner.storage.files_range()?.map_or(Ok(Vec::new()), |range| { @@ -522,4 +537,35 @@ mod tests { Ok(()) } + + #[test] + fn test_fill_block_cache_skips_missing_notification_file() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let mut rng = generators::rng(); + + let temp_dir = tempfile::tempdir()?; + let wal = Wal::::new(&temp_dir)?; + + let blocks = random_block_range(&mut rng, 0..=2, BlockRangeParams::default()) + .into_iter() + .map(|block| block.try_recover()) + .collect::, _>>()?; + + for block in &blocks { + wal.commit(&ExExNotification::ChainCommitted { + new: Arc::new(Chain::new(vec![block.clone()], Default::default(), BTreeMap::new())), + })?; + } + + fs::remove_file(temp_dir.path().join("1.wal"))?; + + let wal = Wal::::new(&temp_dir)?; + assert_eq!( + wal.inner.block_cache().blocks_sorted(), + [(blocks[2].number, 2), (blocks[0].number, 0)] + ); + + Ok(()) + } } From 12ea322f2f55eb97d332f7ebbd30f968f669ef66 Mon Sep 17 00:00:00 2001 From: Joe Parks <26990067+jowparks@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:55:28 -0700 Subject: [PATCH 2/2] chore(net): fix clippy lints surfaced by a newer toolchain `needless_range_loop` in `testnet.rs` and `needless_bool` in `fetcher.rs` fail CI on this branch independently of the WAL change. --- crates/net/network/src/test_utils/testnet.rs | 3 +-- crates/net/network/src/transactions/fetcher.rs | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/net/network/src/test_utils/testnet.rs b/crates/net/network/src/test_utils/testnet.rs index 4c52db50499..05ecb7a0b42 100644 --- a/crates/net/network/src/test_utils/testnet.rs +++ b/crates/net/network/src/test_utils/testnet.rs @@ -386,8 +386,7 @@ impl TestnetHandle { // add all peers to each other for (idx, handle) in self.peers.iter().enumerate().take(self.peers.len() - 1) { - for idx in (idx + 1)..self.peers.len() { - let neighbour = &self.peers[idx]; + for neighbour in self.peers.iter().skip(idx + 1) { handle.network.add_peer(*neighbour.peer_id(), neighbour.local_addr()); } } diff --git a/crates/net/network/src/transactions/fetcher.rs b/crates/net/network/src/transactions/fetcher.rs index f0230c21fd0..fee72ce45d7 100644 --- a/crates/net/network/src/transactions/fetcher.rs +++ b/crates/net/network/src/transactions/fetcher.rs @@ -184,10 +184,7 @@ impl TransactionFetcher { #[inline] pub fn is_idle(&self, peer_id: &PeerId) -> bool { let Some(inflight_count) = self.active_peers.peek(peer_id) else { return true }; - if *inflight_count < self.info.max_inflight_requests_per_peer { - return true - } - false + *inflight_count < self.info.max_inflight_requests_per_peer } /// Returns any idle peer for the given hash.