Emit DiscardFunding event when no splice transaction confirms
What changed, and why it matters
This commit fixes a missing notification in the Lightning Dev Kit's channel monitoring logic. When a splice (a planned funding change) never confirms on the blockchain and instead the original commitment transaction confirms, the software now emits a 'DiscardFunding' event. Previously, this event was only emitted when one of the splice candidates confirmed. Without this event, wallets or users relying on it to stop watching or clean up unconfirmed splice funding transactions could leave those transactions being monitored indefinitely, potentially causing confusion, wasted resources, or delayed cleanup.
Review whether any downstream wallet or node operator depends on DiscardFunding for cleanup; ensure this patch is applied to avoid stale watch entries and missing notifications. No immediate active exploitation vector is evident, but the fix should be included in the next maintenance release.
Security signals we found
Missing event emission in on-chain monitoring path
Resource cleanup/watch-list pruning for unconfirmed splice funding
Consistency fix between commitment confirmation and alternative funding confirmation paths
Test added to assert expected DiscardFunding events
Evidence from the diff
In ChannelMonitorImpl, the OnchainEvent::FundingSpendConfirmation branch now drains self.pending_funding and emits Event::DiscardFunding for each pending splice funding outpoint when no alternative funding has been confirmed (alternative_funding_confirmed.is_none()). It also removes those outpoints from outputs_to_watch. This complements the existing AlternativeFundingConfirmation path. A test in splicing_tests.rs verifies the event is emitted for both nodes when a splice remains unconfirmed and a commitment transaction confirms instead.
Changed components
lightning/src/chain/channelmonitor.rslightning/src/ln/splicing_tests.rsInspect captured patch +54 / −2
diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 9ef44cf..0f36cf1 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -5571,6 +5571,17 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
OnchainEvent::FundingSpendConfirmation { commitment_tx_to_counterparty_output, .. } => {
self.funding_spend_confirmed = Some(entry.txid);
self.confirmed_commitment_tx_counterparty_output = commitment_tx_to_counterparty_output;
+ if self.alternative_funding_confirmed.is_none() {
+ for funding in self.pending_funding.drain(..) {
+ self.outputs_to_watch.remove(&funding.funding_txid());
+ self.pending_events.push(Event::DiscardFunding {
+ channel_id: self.channel_id,
+ funding_info: crate::events::FundingInfo::OutPoint {
+ outpoint: funding.funding_outpoint(),
+ },
+ });
+ }
+ }
},
OnchainEvent::AlternativeFundingConfirmation {} => {
// An alternative funding transaction has irrevocably confirmed and we're no
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 2cc32a4..1537a36 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -9,9 +9,11 @@
use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW;
use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};
+use crate::chain::transaction::OutPoint;
use crate::events::bump_transaction::sync::WalletSourceSync;
-use crate::events::{ClosureReason, Event, HTLCHandlingFailureType};
+use crate::events::{ClosureReason, Event, FundingInfo, HTLCHandlingFailureType};
use crate::ln::chan_utils;
+use crate::ln::channelmanager::BREAKDOWN_TIMEOUT;
use crate::ln::functional_test_utils::*;
use crate::ln::funding::{FundingTxInput, SpliceContribution};
use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent};
@@ -305,7 +307,8 @@ fn lock_splice_after_blocks<'a, 'b, 'c, 'd>(
panic!();
}
- // Remove the corresponding outputs and transactions the chain source is watching.
+ // Remove the corresponding outputs and transactions the chain source is watching for the
+ // old funding as it is no longer being tracked.
node_a
.chain_source
.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone());
@@ -560,4 +563,42 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs:
);
}
check_added_monitors(&nodes[0], 2); // Two `ReleasePaymentComplete` monitor updates
+
+ // When the splice never confirms and we see a commitment transaction broadcast and confirm for
+ // the current funding instead, we should expect to see an `Event::DiscardFunding` for the
+ // splice transaction.
+ if splice_status == SpliceStatus::Unconfirmed {
+ // Remove the corresponding outputs and transactions the chain source is watching for the
+ // splice as it is no longer being tracked.
+ connect_blocks(&nodes[0], BREAKDOWN_TIMEOUT as u32);
+ let (vout, txout) = splice_tx
+ .output
+ .iter()
+ .enumerate()
+ .find(|(_, output)| output.script_pubkey.is_p2wsh())
+ .unwrap();
+ let funding_outpoint = OutPoint { txid: splice_tx.compute_txid(), index: vout as u16 };
+ nodes[0]
+ .chain_source
+ .remove_watched_txn_and_outputs(funding_outpoint, txout.script_pubkey.clone());
+ nodes[1]
+ .chain_source
+ .remove_watched_txn_and_outputs(funding_outpoint, txout.script_pubkey.clone());
+
+ // `SpendableOutputs` events are also included here, but we don't care for them.
+ let events = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert_eq!(events.len(), if claim_htlcs { 2 } else { 4 }, "{events:?}");
+ if let Event::DiscardFunding { funding_info, .. } = &events[0] {
+ assert_eq!(*funding_info, FundingInfo::OutPoint { outpoint: funding_outpoint });
+ } else {
+ panic!();
+ }
+ let events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert_eq!(events.len(), if claim_htlcs { 2 } else { 1 }, "{events:?}");
+ if let Event::DiscardFunding { funding_info, .. } = &events[0] {
+ assert_eq!(*funding_info, FundingInfo::OutPoint { outpoint: funding_outpoint });
+ } else {
+ panic!();
+ }
+ }
}
Why this scored 42/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.