Add manual-funding broadcast tracking to ChannelMonitor
What changed, and why it matters
This commit adds internal bookkeeping to LDK's channel monitor so it can tell whether a channel uses 'manual' funding-transaction broadcasts and whether that funding transaction has actually appeared on-chain yet. The goal is to stop the node from broadcasting its own commitment transaction (a recovery/closing transaction) before the funding transaction exists on-chain, which would be pointless and could leak information or waste fees. It is a defensive correctness fix, not a patch for an active exploit.
Review the follow-up commits that wire funding_seen_onchain into the broadcast-gating logic and ensure the flag is set reliably when the funding transaction is first seen, including across reorgs and monitor restarts. Verify that the default value fallback for old monitors is safe in all channel types.
Security signals we found
Prevents premature holder commitment broadcasts for channels whose funding transaction is not yet on-chain
Adds backward-compatible persistence for new channel-monitor state
Defensive fix for LSPS2 client_trusts_lsp=true scenario where LSP may defer funding broadcast
No visible input validation, cryptographic, or memory-safety changes
Evidence from the diff
The change introduces two persisted flags in ChannelMonitorImpl: is_manual_broadcast and funding_seen_onchain. is_manual_broadcast is set at monitor creation from the channel context, and funding_seen_onchain starts false and is intended to be set true once the funding transaction is observed on-chain. Serialization uses TLV fields 35 and 37; for backward compatibility, older monitors load funding_seen_onchain with a default of true so existing behavior is preserved. The immediate diff only adds storage and plumbing; the actual gating of holder commitment broadcasts based on these flags is not visible in this commit.
Changed components
lightning/src/chain/channelmonitor.rslightning/src/ln/channel.rsInspect captured patch +28 / −0
diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index 37e337c..06f5212 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -1202,6 +1202,19 @@ pub(crate) struct ChannelMonitorImpl<Signer: EcdsaChannelSigner> {
funding: FundingScope,
pending_funding: Vec<FundingScope>,
+ /// True if this channel was configured for manual funding broadcasts. Monitors written by
+ /// versions prior to LDK 0.2 load with `false` until a new update persists it.
+ is_manual_broadcast: bool,
+ /// True once we've observed either funding transaction on-chain. Older monitors prior to LDK 0.2
+ /// assume this is `true` when absent during upgrade so holder broadcasts aren't gated unexpectedly.
+ /// In manual-broadcast channels we also use this to trigger deferred holder
+ /// broadcasts once the funding transaction finally appears on-chain.
+ ///
+ /// Note: This tracks whether the funding transaction was ever broadcast, not whether it is
+ /// currently confirmed. It is never reset, even if the funding transaction is unconfirmed due
+ /// to a reorg.
+ funding_seen_onchain: bool,
+
latest_update_id: u64,
commitment_transaction_number_obscure_factor: u64,
@@ -1740,6 +1753,8 @@ pub(crate) fn write_chanmon_internal<Signer: EcdsaChannelSigner, W: Writer>(
(32, channel_monitor.pending_funding, optional_vec),
(33, channel_monitor.htlcs_resolved_to_user, required),
(34, channel_monitor.alternative_funding_confirmed, option),
+ (35, channel_monitor.is_manual_broadcast, required),
+ (37, channel_monitor.funding_seen_onchain, required),
});
Ok(())
@@ -1868,6 +1883,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
commitment_transaction_number_obscure_factor: u64,
initial_holder_commitment_tx: HolderCommitmentTransaction, best_block: BestBlock,
counterparty_node_id: PublicKey, channel_id: ChannelId,
+ is_manual_broadcast: bool,
) -> ChannelMonitor<Signer> {
assert!(commitment_transaction_number_obscure_factor <= (1 << 48));
@@ -1914,6 +1930,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
},
pending_funding: vec![],
+ is_manual_broadcast,
+ funding_seen_onchain: false,
+
latest_update_id: 0,
commitment_transaction_number_obscure_factor,
@@ -6562,6 +6581,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
let mut channel_parameters = None;
let mut pending_funding = None;
let mut alternative_funding_confirmed = None;
+ let mut is_manual_broadcast = RequiredWrapper(None);
+ let mut funding_seen_onchain = RequiredWrapper(None);
read_tlv_fields!(reader, {
(1, funding_spend_confirmed, option),
(3, htlcs_resolved_on_chain, optional_vec),
@@ -6582,6 +6603,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
(32, pending_funding, optional_vec),
(33, htlcs_resolved_to_user, option),
(34, alternative_funding_confirmed, option),
+ (35, is_manual_broadcast, (default_value, false)),
+ (37, funding_seen_onchain, (default_value, true)),
});
// Note that `payment_preimages_with_info` was added (and is always written) in LDK 0.1, so
// we can use it to determine if this monitor was last written by LDK 0.1 or later.
@@ -6695,6 +6718,10 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
prev_holder_commitment_tx,
},
pending_funding: pending_funding.unwrap_or(vec![]),
+ is_manual_broadcast: is_manual_broadcast.0.unwrap(),
+ // Older monitors prior to LDK 0.2 assume this is `true` when absent
+ // during upgrade so holder broadcasts aren't gated unexpectedly.
+ funding_seen_onchain: funding_seen_onchain.0.unwrap(),
latest_update_id,
commitment_transaction_number_obscure_factor,
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 5bfe585..c243fc6 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -3196,6 +3196,7 @@ where
funding.get_holder_selected_contest_delay(), &context.destination_script,
&funding.channel_transaction_parameters, funding.is_outbound(), obscure_factor,
holder_commitment_tx, best_block, context.counterparty_node_id, context.channel_id(),
+ context.is_manual_broadcast,
);
channel_monitor.provide_initial_counterparty_commitment_tx(
counterparty_initial_commitment_tx.clone(),
Why this scored 41/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.