Defer claimable tracking until funding tx confirms
What changed, and why it matters
This patch fixes a logic bug in rust-lightning's channel monitor for manually-broadcast funding transactions. Previously, the monitor could try to claim outputs and watch for spending transactions before the funding transaction had actually appeared on the blockchain, which could lead to wasted work, incorrect state, or spurious errors. The fix defers all claimable-output tracking until the funding transaction is confirmed on-chain, and avoids generating duplicate claims when a block is processed.
Review whether pre-fix monitors could enter an inconsistent state from attempting to claim non-existent outputs; consider adding regression tests for manual-broadcast funding flows and monitor restart/reorg behavior.
Security signals we found
Pre-confirmation output claiming in manual-broadcast channels
Potential duplicate claim generation during block processing
State consistency fix in channel monitor claim tracking
Evidence from the diff
In ChannelMonitorImpl, generate_claimable_outpoints_and_watch_outputs now returns empty (claimable_outpoints, watch_outputs) when is_manual_broadcast is true and funding_seen_onchain is false. A second change in the block-processing path skips calling should_broadcast_holder_commitment_txn and re-generating claims if claimable_outpoints is already non-empty, preventing duplicate claim generation that could occur after transactions_confirmed already populated the vectors.
Changed components
lightning/src/chain/channelmonitor.rsChannelMonitorImplgenerate_claimable_outpoints_and_watch_outputsblock-connected processing pathInspect captured patch +17 / −8
diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 175db8a..bbf2e5f 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -3985,7 +3985,13 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}
claimable_outpoints.append(&mut new_outpoints);
}
- (claimable_outpoints, watch_outputs)
+ // In manual-broadcast mode, if we have not yet observed the funding transaction on-chain,
+ // return empty vectors.
+ if self.is_manual_broadcast && !self.funding_seen_onchain {
+ return (Vec::new(), Vec::new());
+ } else {
+ (claimable_outpoints, watch_outputs)
+ }
}
#[rustfmt::skip]
@@ -5642,13 +5648,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
log_trace!(logger, "Processing {} matched transactions for block at height {}.", txn_matched.len(), conf_height);
debug_assert!(self.best_block.height >= conf_height);
- let should_broadcast = self.should_broadcast_holder_commitment_txn(logger);
- if let Some(payment_hash) = should_broadcast {
- let reason = ClosureReason::HTLCsTimedOut { payment_hash: Some(payment_hash) };
- let (mut new_outpoints, mut new_outputs) =
- self.generate_claimable_outpoints_and_watch_outputs(Some(reason));
- claimable_outpoints.append(&mut new_outpoints);
- watch_outputs.append(&mut new_outputs);
+ // Only generate claims if we haven't already done so (e.g., in transactions_confirmed).
+ if claimable_outpoints.is_empty() {
+ let should_broadcast = self.should_broadcast_holder_commitment_txn(logger);
+ if let Some(payment_hash) = should_broadcast {
+ let reason = ClosureReason::HTLCsTimedOut { payment_hash: Some(payment_hash) };
+ let (mut new_outpoints, mut new_outputs) =
+ self.generate_claimable_outpoints_and_watch_outputs(Some(reason));
+ claimable_outpoints.append(&mut new_outpoints);
+ watch_outputs.append(&mut new_outputs);
+ }
}
// Find which on-chain events have reached their confirmation threshold.
Why this scored 32/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.