From b33526ef5660c064d40728b4d0cb66ca20ca0acc Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 21 May 2026 08:39:41 +0200 Subject: [PATCH 1/3] Add explicit chanmon manager persistence commands Add chanmon_consistency commands to persist each node's ChannelManager state explicitly. This lets the fuzz target exercise delayed manager persistence instead of checkpointing it after every command. --- fuzz/src/chanmon_consistency.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index ce148157df9..95874c340cd 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -2860,6 +2860,7 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> { } fn restart_node(&mut self, node_idx: usize, v: u8, router: &'a FuzzRouter) { + self.nodes[node_idx].checkpoint_manager_persistence(); match node_idx { 0 => { self.ab_link.disconnect_for_reload(0, &self.nodes, &mut self.queues); @@ -3116,6 +3117,16 @@ pub fn do_test(data: &[u8], out: Out) { 0x88 => harness.nodes[2].bump_fee_estimate(harness.chan_type), 0x89 => harness.nodes[2].reset_fee_estimate(), + 0x90 => { + harness.nodes[0].checkpoint_manager_persistence(); + }, + 0x91 => { + harness.nodes[1].checkpoint_manager_persistence(); + }, + 0x92 => { + harness.nodes[2].checkpoint_manager_persistence(); + }, + 0xa0 => { if !cfg!(splicing) { break 'fuzz_loop; @@ -3370,8 +3381,6 @@ pub fn do_test(data: &[u8], out: Out) { }, _ => break 'fuzz_loop, } - - harness.checkpoint_manager_persistences(); } harness.finish(); } From bbeba3e2afdd94437acf6046dab8d1974b39255a Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 22 May 2026 10:46:32 +0200 Subject: [PATCH 2/3] Refactor chanmon payment tracker helpers Move node-local payment tracking mutations onto NodePayments. Pending and resolved payment state are updated the same way. The owner of that state now owns the helper methods. --- fuzz/src/chanmon_consistency.rs | 85 +++++++++++++++++---------------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 95874c340cd..39b166b8d29 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -1485,6 +1485,42 @@ impl NodePayments { fn new() -> Self { Self { pending: Vec::new(), resolved: new_hash_map() } } + + fn add_pending(&mut self, payment_id: PaymentId) { + self.pending.push(payment_id); + } + + fn mark_sent(&mut self, sent_id: PaymentId, payment_hash: PaymentHash) { + let idx_opt = self.pending.iter().position(|id| *id == sent_id); + if let Some(idx) = idx_opt { + self.pending.remove(idx); + self.resolved.insert(sent_id, Some(payment_hash)); + } else { + assert!(self.resolved.contains_key(&sent_id)); + } + } + + fn mark_resolved_without_hash(&mut self, payment_id: PaymentId) { + let idx_opt = self.pending.iter().position(|id| *id == payment_id); + if let Some(idx) = idx_opt { + self.pending.remove(idx); + self.resolved.insert(payment_id, None); + } else if !self.resolved.contains_key(&payment_id) { + // Some resolutions can arrive immediately, before the send helper records + // the payment as pending. Track them so later duplicate events are accepted. + self.resolved.insert(payment_id, None); + } + } + + fn mark_successful_probe(&mut self, payment_id: PaymentId) { + let idx_opt = self.pending.iter().position(|id| *id == payment_id); + if let Some(idx) = idx_opt { + self.pending.remove(idx); + self.resolved.insert(payment_id, None); + } else { + assert!(self.resolved.contains_key(&payment_id)); + } + } } struct PaymentTracker { @@ -1590,7 +1626,7 @@ impl PaymentTracker { }, }; if succeeded { - self.nodes[source_idx].pending.push(id); + self.nodes[source_idx].add_pending(id); } succeeded } @@ -1667,7 +1703,7 @@ impl PaymentTracker { }, }; if succeeded { - self.nodes[source_idx].pending.push(id); + self.nodes[source_idx].add_pending(id); } } @@ -1736,7 +1772,7 @@ impl PaymentTracker { Ok(()) => Self::check_payment_send_events(source, id), }; if succeeded { - self.nodes[source_idx].pending.push(id); + self.nodes[source_idx].add_pending(id); } } @@ -1836,7 +1872,7 @@ impl PaymentTracker { Ok(()) => Self::check_payment_send_events(source, id), }; if succeeded { - self.nodes[source_idx].pending.push(id); + self.nodes[source_idx].add_pending(id); } } @@ -1853,41 +1889,6 @@ impl PaymentTracker { } } - fn mark_sent(&mut self, node_idx: usize, sent_id: PaymentId, payment_hash: PaymentHash) { - let node = &mut self.nodes[node_idx]; - let idx_opt = node.pending.iter().position(|id| *id == sent_id); - if let Some(idx) = idx_opt { - node.pending.remove(idx); - node.resolved.insert(sent_id, Some(payment_hash)); - } else { - assert!(node.resolved.contains_key(&sent_id)); - } - } - - fn mark_resolved_without_hash(&mut self, node_idx: usize, payment_id: PaymentId) { - let node = &mut self.nodes[node_idx]; - let idx_opt = node.pending.iter().position(|id| *id == payment_id); - if let Some(idx) = idx_opt { - node.pending.remove(idx); - node.resolved.insert(payment_id, None); - } else if !node.resolved.contains_key(&payment_id) { - // Some resolutions can arrive immediately, before the send helper records - // the payment as pending. Track them so later duplicate events are accepted. - node.resolved.insert(payment_id, None); - } - } - - fn mark_successful_probe(&mut self, node_idx: usize, payment_id: PaymentId) { - let node = &mut self.nodes[node_idx]; - let idx_opt = node.pending.iter().position(|id| *id == payment_id); - if let Some(idx) = idx_opt { - node.pending.remove(idx); - node.resolved.insert(payment_id, None); - } else { - assert!(node.resolved.contains_key(&payment_id)); - } - } - fn assert_all_resolved(&self) { for (idx, node) in self.nodes.iter().enumerate() { assert!( @@ -2725,17 +2726,17 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> { } }, events::Event::PaymentSent { payment_id, payment_hash, .. } => { - payments.mark_sent(node_idx, payment_id.unwrap(), payment_hash); + payments.nodes[node_idx].mark_sent(payment_id.unwrap(), payment_hash); }, // Even though we don't explicitly send probes, because probes are detected based on // hashing the payment hash+preimage, it is rather trivial for the fuzzer to build // payments that accidentally end up looking like probes. events::Event::ProbeSuccessful { payment_id, .. } => { - payments.mark_successful_probe(node_idx, payment_id); + payments.nodes[node_idx].mark_successful_probe(payment_id); }, events::Event::PaymentFailed { payment_id, .. } | events::Event::ProbeFailed { payment_id, .. } => { - payments.mark_resolved_without_hash(node_idx, payment_id); + payments.nodes[node_idx].mark_resolved_without_hash(payment_id); }, events::Event::PaymentClaimed { .. } => {}, events::Event::PaymentPathSuccessful { .. } => {}, From c18fab55a83bde8ac038f4e29c12c845da54ef01 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 22 May 2026 11:05:28 +0200 Subject: [PATCH 3/3] Track chanmon payment persistence generations Stamp pending payments with the first manager generation. On deferred reload, drop payments born after the loaded snapshot. This keeps tracker state aligned with explicit persistence. --- fuzz/src/chanmon_consistency.rs | 88 ++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 39b166b8d29..7f03ceaa50c 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -819,6 +819,7 @@ struct HarnessNode<'a> { persistence_style: ChannelMonitorUpdateStatus, deferred: bool, serialized_manager: Vec, + serialized_manager_generation: u64, height: u32, last_htlc_clear_fee: u32, } @@ -917,6 +918,7 @@ impl<'a> HarnessNode<'a> { persistence_style, deferred, serialized_manager: Vec::new(), + serialized_manager_generation: 0, height: 0, last_htlc_clear_fee: 253, } @@ -976,6 +978,7 @@ impl<'a> HarnessNode<'a> { if self.node.get_and_clear_needs_persistence() { let pending_monitor_writes = self.monitor.pending_operation_count(); self.serialized_manager = self.node.encode(); + self.serialized_manager_generation += 1; if self.deferred { self.monitor.flush(pending_monitor_writes, &self.logger); } else { @@ -991,6 +994,7 @@ impl<'a> HarnessNode<'a> { fn force_checkpoint_manager_persistence(&mut self) { let pending_monitor_writes = self.monitor.pending_operation_count(); self.serialized_manager = self.node.encode(); + self.serialized_manager_generation += 1; self.node.get_and_clear_needs_persistence(); if self.deferred { self.monitor.flush(pending_monitor_writes, &self.logger); @@ -999,6 +1003,10 @@ impl<'a> HarnessNode<'a> { } } + fn next_manager_persistence_generation(&self) -> u64 { + self.serialized_manager_generation + 1 + } + fn bump_fee_estimate(&mut self, chan_type: ChanType) { let mut max_feerate = self.last_htlc_clear_fee; if matches!(chan_type, ChanType::Legacy) { @@ -1098,7 +1106,8 @@ impl<'a> HarnessNode<'a> { fn reload( &mut self, use_old_mons: u8, out: &Out, router: &'a FuzzRouter, chan_type: ChanType, - ) { + ) -> u64 { + let loaded_manager_generation = self.serialized_manager_generation; let logger = Self::build_logger(self.node_id, out); let persister = Self::build_persister(self.persistence_style); let chain_monitor = Self::build_chain_monitor( @@ -1170,6 +1179,7 @@ impl<'a> HarnessNode<'a> { // even if the reloaded ChannelManager does not need persistence. Always checkpoint here so // those registrations can be flushed against the manager snapshot they belong to. self.force_checkpoint_manager_persistence(); + loaded_manager_generation } } @@ -1476,8 +1486,14 @@ impl PeerLink { } } +struct PendingPayment { + payment_id: PaymentId, + payment_hash: PaymentHash, + first_persisted_manager_generation: u64, +} + struct NodePayments { - pending: Vec, + pending: Vec, resolved: HashMap>, } @@ -1486,12 +1502,19 @@ impl NodePayments { Self { pending: Vec::new(), resolved: new_hash_map() } } - fn add_pending(&mut self, payment_id: PaymentId) { - self.pending.push(payment_id); + fn add_pending( + &mut self, payment_id: PaymentId, payment_hash: PaymentHash, + first_persisted_manager_generation: u64, + ) { + self.pending.push(PendingPayment { + payment_id, + payment_hash, + first_persisted_manager_generation, + }); } fn mark_sent(&mut self, sent_id: PaymentId, payment_hash: PaymentHash) { - let idx_opt = self.pending.iter().position(|id| *id == sent_id); + let idx_opt = self.pending.iter().position(|pending| pending.payment_id == sent_id); if let Some(idx) = idx_opt { self.pending.remove(idx); self.resolved.insert(sent_id, Some(payment_hash)); @@ -1501,7 +1524,7 @@ impl NodePayments { } fn mark_resolved_without_hash(&mut self, payment_id: PaymentId) { - let idx_opt = self.pending.iter().position(|id| *id == payment_id); + let idx_opt = self.pending.iter().position(|pending| pending.payment_id == payment_id); if let Some(idx) = idx_opt { self.pending.remove(idx); self.resolved.insert(payment_id, None); @@ -1513,7 +1536,7 @@ impl NodePayments { } fn mark_successful_probe(&mut self, payment_id: PaymentId) { - let idx_opt = self.pending.iter().position(|id| *id == payment_id); + let idx_opt = self.pending.iter().position(|pending| pending.payment_id == payment_id); if let Some(idx) = idx_opt { self.pending.remove(idx); self.resolved.insert(payment_id, None); @@ -1521,6 +1544,21 @@ impl NodePayments { assert!(self.resolved.contains_key(&payment_id)); } } + + fn sync_pending_with_manager_generation( + &mut self, loaded_manager_generation: u64, + ) -> Vec { + let mut rolled_back_payment_hashes = Vec::new(); + let pending = mem::take(&mut self.pending); + for pending_payment in pending { + if pending_payment.first_persisted_manager_generation > loaded_manager_generation { + rolled_back_payment_hashes.push(pending_payment.payment_hash); + } else { + self.pending.push(pending_payment); + } + } + rolled_back_payment_hashes + } } struct PaymentTracker { @@ -1626,7 +1664,11 @@ impl PaymentTracker { }, }; if succeeded { - self.nodes[source_idx].add_pending(id); + self.nodes[source_idx].add_pending( + id, + hash, + source.next_manager_persistence_generation(), + ); } succeeded } @@ -1703,7 +1745,11 @@ impl PaymentTracker { }, }; if succeeded { - self.nodes[source_idx].add_pending(id); + self.nodes[source_idx].add_pending( + id, + hash, + source.next_manager_persistence_generation(), + ); } } @@ -1772,7 +1818,11 @@ impl PaymentTracker { Ok(()) => Self::check_payment_send_events(source, id), }; if succeeded { - self.nodes[source_idx].add_pending(id); + self.nodes[source_idx].add_pending( + id, + hash, + source.next_manager_persistence_generation(), + ); } } @@ -1872,7 +1922,11 @@ impl PaymentTracker { Ok(()) => Self::check_payment_send_events(source, id), }; if succeeded { - self.nodes[source_idx].add_pending(id); + self.nodes[source_idx].add_pending( + id, + hash, + source.next_manager_persistence_generation(), + ); } } @@ -2861,7 +2915,9 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> { } fn restart_node(&mut self, node_idx: usize, v: u8, router: &'a FuzzRouter) { - self.nodes[node_idx].checkpoint_manager_persistence(); + if !self.nodes[node_idx].deferred { + self.nodes[node_idx].checkpoint_manager_persistence(); + } match node_idx { 0 => { self.ab_link.disconnect_for_reload(0, &self.nodes, &mut self.queues); @@ -2875,7 +2931,13 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> { }, _ => panic!("invalid node index"), } - self.nodes[node_idx].reload(v, &self.out, router, self.chan_type); + let loaded_manager_generation = + self.nodes[node_idx].reload(v, &self.out, router, self.chan_type); + let rolled_back_payment_hashes = self.payments.nodes[node_idx] + .sync_pending_with_manager_generation(loaded_manager_generation); + for payment_hash in rolled_back_payment_hashes { + self.payments.claimed_payment_hashes.remove(&payment_hash); + } } fn settle_all(&mut self) {