Filter stale closed hop sends in chanmon fuzz
What changed, and why it matters
This change only modifies a fuzzing test harness, not the production Lightning Dev Kit code. It adds a filter so that simulated multi-hop payments skip test scenarios where the harness's view of a closed channel is temporarily out of sync with a node's own channel list. This prevents the fuzzer from generating misleading test cases where a payment appears to stall because the test state is inconsistent. It does not fix a vulnerability in real LDK software and cannot be exploited by users or attackers.
No security action required. Treat as a normal fuzzing-harness reliability improvement. Reviewers may verify that the filter correctly excludes the intended stale-state scenarios without reducing fuzz coverage of legitimate edge cases.
Security signals we found
Change is confined to fuzz test harness code (`fuzz/src/chanmon_consistency.rs`)
No modifications to production networking, routing, or channel state machine logic
Commentary describes a test-state inconsistency, not a real-world protocol failure
No cryptographic, authorization, or input-validation changes
Evidence from the diff
The commit adds has_stale_closed_channel_between and calls it in send_hop and send_mpp_hop within fuzz/src/chanmon_consistency.rs. The fuzz harness tracks channel closures independently of the simulated nodes. Because LDK uses non-strict forwarding, a middle node may select a parallel channel that the harness considers closed but the node still lists as existing. In that window, downstream HTLCs may not be committed and the source payment can remain pending, causing the fuzz target to behave inconsistently. The patch skips those API sends until the stale listing clears, while keeping existing open-channel-id checks for fully dropped channels. This is a test-only consistency fix.
Changed components
fuzz/src/chanmon_consistency.rsInspect captured patch +34 / −0
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index 5501489..c8444a9 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -2855,6 +2855,20 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> {
self.link_between(source_idx, dest_idx).first_channel_id()
}
+ // API calls are filtered before we make them if the harness knows they would
+ // target stale state. The open-channel filters below still handle tracked-
+ // closed channel ids after both peers have dropped them from list_channels.
+ fn has_stale_closed_channel_between(&self, source_idx: usize, dest_idx: usize) -> bool {
+ let channel_ids = self.channel_ids_between(source_idx, dest_idx);
+ let source_channels = self.nodes[source_idx].list_channels();
+ let dest_channels = self.nodes[dest_idx].list_channels();
+ channel_ids.iter().any(|channel_id| {
+ self.close_tracker.is_closed_or_closing(channel_id)
+ && (source_channels.iter().any(|chan| chan.channel_id == *channel_id)
+ || dest_channels.iter().any(|chan| chan.channel_id == *channel_id))
+ })
+ }
+
fn send_on_channel(
&mut self, source_idx: usize, dest_idx: usize, dest_chan_id: ChannelId, amt: u64,
) -> bool {
@@ -2874,6 +2888,16 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> {
}
fn send_hop(&mut self, source_idx: usize, middle_idx: usize, dest_idx: usize, amt: u64) {
+ // Even if we route over an open SCID, the middle node's non-strict
+ // forwarding can pick a parallel channel that the harness has already
+ // tracked closed but the node still lists. In that window, the downstream
+ // HTLC may never get committed, so close cleanup has nothing to fail back
+ // and the source payment can remain pending.
+ if self.has_stale_closed_channel_between(source_idx, middle_idx)
+ || self.has_stale_closed_channel_between(middle_idx, dest_idx)
+ {
+ return;
+ }
let middle_chan_id = self.first_channel_id_between(source_idx, middle_idx);
let dest_chan_id = self.first_channel_id_between(middle_idx, dest_idx);
if !self.close_tracker.is_open(&middle_chan_id)
@@ -2929,6 +2953,16 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> {
&mut self, source_idx: usize, middle_idx: usize, dest_idx: usize, channels: MppHopChannels,
amt: u64,
) {
+ // Even if we route over an open SCID, the middle node's non-strict
+ // forwarding can pick a parallel channel that the harness has already
+ // tracked closed but the node still lists. In that window, the downstream
+ // HTLC may never get committed, so close cleanup has nothing to fail back
+ // and the source payment can remain pending.
+ if self.has_stale_closed_channel_between(source_idx, middle_idx)
+ || self.has_stale_closed_channel_between(middle_idx, dest_idx)
+ {
+ return;
+ }
let middle_chan_ids = self.channel_ids_between(source_idx, middle_idx);
let dest_chan_ids = self.channel_ids_between(middle_idx, dest_idx);
let middle_first_chan_id = middle_chan_ids[0];
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.