From 8376197c738153b0bc77d6d305cfe24679df1376 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Fri, 26 Sep 2025 12:30:29 -0300 Subject: [PATCH 1/7] Prefactor: drop #[rustfmt::skip] on broadcast_latest_holder_commitment_txn --- lightning/src/chain/channelmonitor.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 0f36cf14e60..ec593e81c3d 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -2312,19 +2312,21 @@ impl ChannelMonitor { /// close channel with their commitment transaction after a substantial amount of time. Best /// may be to contact the other node operator out-of-band to coordinate other options available /// to you. - #[rustfmt::skip] pub fn broadcast_latest_holder_commitment_txn( - &self, broadcaster: &B, fee_estimator: &F, logger: &L - ) - where + &self, broadcaster: &B, fee_estimator: &F, logger: &L, + ) where B::Target: BroadcasterInterface, F::Target: FeeEstimator, - L::Target: Logger + L::Target: Logger, { let mut inner = self.inner.lock().unwrap(); let fee_estimator = LowerBoundedFeeEstimator::new(&**fee_estimator); let logger = WithChannelMonitor::from_impl(logger, &*inner, None); - inner.queue_latest_holder_commitment_txn_for_broadcast(broadcaster, &fee_estimator, &logger); + inner.queue_latest_holder_commitment_txn_for_broadcast( + broadcaster, + &fee_estimator, + &logger, + ); } /// Unsafe test-only version of `broadcast_latest_holder_commitment_txn` used by our test framework From cdc4ebfecd601510e899f4d2b2fb65748a8e8aec Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Fri, 26 Sep 2025 11:45:21 -0300 Subject: [PATCH 2/7] Add manual-funding broadcast tracking to ChannelMonitor Adds `is_manual_broadcast` and `funding_seen_onchain` flags to track whether the channel uses manual funding broadcasts and whether we've seen the funding tx confirm. This enables deferring holder commitment broadcasts until after the funding tx is actually broadcast. For example, in LSPS2 with client_trusts_lsp=true, the LSP may defer broadcasting the funding tx until the client claims an HTLC, so we need to avoid broadcasting commitments that reference outputs that don't exist yet. --- lightning/src/chain/channelmonitor.rs | 23 +++++++++++++++++++++++ lightning/src/ln/channel.rs | 1 + 2 files changed, 24 insertions(+) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index ec593e81c3d..a107f515739 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -1187,6 +1187,15 @@ pub(crate) struct ChannelMonitorImpl { funding: FundingScope, pending_funding: Vec, + /// 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. + funding_seen_onchain: bool, + latest_update_id: u64, commitment_transaction_number_obscure_factor: u64, @@ -1725,6 +1734,8 @@ pub(crate) fn write_chanmon_internal( (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(()) @@ -1853,6 +1864,7 @@ impl ChannelMonitor { 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 { assert!(commitment_transaction_number_obscure_factor <= (1 << 48)); @@ -1899,6 +1911,9 @@ impl ChannelMonitor { }, pending_funding: vec![], + is_manual_broadcast, + funding_seen_onchain: false, + latest_update_id: 0, commitment_transaction_number_obscure_factor, @@ -6456,6 +6471,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), @@ -6476,6 +6493,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. @@ -6593,6 +6612,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 5344818a195..2444b47478a 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -3058,6 +3058,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(), From b725130140b9ef879a4bb7e308c65a3c2c016d8a Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Fri, 26 Sep 2025 11:46:54 -0300 Subject: [PATCH 3/7] Set funding_seen_onchain=true in filter_block Marks funding_seen_onchain when we see the funding tx confirm. --- lightning/src/chain/channelmonitor.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index a107f515739..23195d44f03 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -5818,10 +5818,17 @@ impl ChannelMonitorImpl { /// Filters a block's `txdata` for transactions spending watched outputs or for any child /// transactions thereof. + /// While iterating, this also tracks whether we observed the funding transaction. #[rustfmt::skip] - fn filter_block<'a>(&self, txdata: &TransactionData<'a>) -> Vec<&'a Transaction> { + fn filter_block<'a>(&mut self, txdata: &TransactionData<'a>) -> Vec<&'a Transaction> { let mut matched_txn = new_hash_set(); txdata.iter().filter(|&&(_, tx)| { + let txid = tx.compute_txid(); + if !self.funding_seen_onchain && (txid == self.funding.funding_txid() || + self.pending_funding.iter().any(|f| f.funding_txid() == txid)) + { + self.funding_seen_onchain = true; + } let mut matches = self.spends_watched_output(tx); for input in tx.input.iter() { if matches { break; } @@ -5830,7 +5837,7 @@ impl ChannelMonitorImpl { } } if matches { - matched_txn.insert(tx.compute_txid()); + matched_txn.insert(txid); } matches }).map(|(_, tx)| *tx).collect() From c665c95551ec7c407bcb07d0d745ea5d9e28b7e0 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Fri, 26 Sep 2025 11:52:01 -0300 Subject: [PATCH 4/7] Gate holder broadcast queueing on funding confirmation Don't queue holder commitment broadcasts until funding is confirmed, unless explicitly overridden via broadcast_latest_holder_commitment_txn. Attempting to broadcast commitments before funding confirms would fail mempool validation since the funding output doesn't exist yet. --- lightning/src/chain/channelmonitor.rs | 37 ++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 23195d44f03..941792e8ae8 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -2327,6 +2327,16 @@ impl ChannelMonitor { /// close channel with their commitment transaction after a substantial amount of time. Best /// may be to contact the other node operator out-of-band to coordinate other options available /// to you. + /// + /// Note: For channels using manual funding broadcast (see + /// [`crate::ln::channelmanager::ChannelManager::funding_transaction_generated_manual_broadcast`]), + /// automatic broadcasts are suppressed until the funding transaction has been observed on-chain. + /// Calling this method overrides that suppression and queues the latest holder commitment + /// transaction for broadcast even if the funding has not yet been seen on-chain. This may result + /// in unconfirmable transactions being broadcast or [`Event::BumpTransaction`] notifications for + /// transactions that cannot be confirmed until the funding transaction is visible. + /// + /// [`Event::BumpTransaction`]: crate::events::Event::BumpTransaction pub fn broadcast_latest_holder_commitment_txn( &self, broadcaster: &B, fee_estimator: &F, logger: &L, ) where @@ -2337,10 +2347,12 @@ impl ChannelMonitor { let mut inner = self.inner.lock().unwrap(); let fee_estimator = LowerBoundedFeeEstimator::new(&**fee_estimator); let logger = WithChannelMonitor::from_impl(logger, &*inner, None); + inner.queue_latest_holder_commitment_txn_for_broadcast( broadcaster, &fee_estimator, &logger, + false, ); } @@ -3958,8 +3970,15 @@ impl ChannelMonitorImpl { } #[rustfmt::skip] + /// Note: For channels where the funding transaction is being manually managed (see + /// [`crate::ln::channelmanager::ChannelManager::funding_transaction_generated_manual_broadcast`]), + /// this method returns without queuing any transactions until the funding transaction has been + /// observed on-chain, unless `require_funding_seen` is `false`. This prevents attempting to + /// broadcast unconfirmable holder commitment transactions before the funding is visible. + /// See also + /// [`crate::chain::channelmonitor::ChannelMonitor::broadcast_latest_holder_commitment_txn`]. pub(crate) fn queue_latest_holder_commitment_txn_for_broadcast( - &mut self, broadcaster: &B, fee_estimator: &LowerBoundedFeeEstimator, logger: &WithChannelMonitor + &mut self, broadcaster: &B, fee_estimator: &LowerBoundedFeeEstimator, logger: &WithChannelMonitor, require_funding_seen: bool, ) where B::Target: BroadcasterInterface, @@ -3971,6 +3990,12 @@ impl ChannelMonitorImpl { message: "ChannelMonitor-initiated commitment transaction broadcast".to_owned(), }; let (claimable_outpoints, _) = self.generate_claimable_outpoints_and_watch_outputs(Some(reason)); + // In manual-broadcast mode, if `require_funding_seen` is true and we have not yet observed + // the funding transaction on-chain, do not queue any transactions. + if require_funding_seen && self.is_manual_broadcast && !self.funding_seen_onchain { + log_info!(logger, "Not broadcasting holder commitment for manual-broadcast channel before funding appears on-chain"); + return; + } let conf_target = self.closure_conf_target(); self.onchain_tx_handler.update_claims_view_from_requests( claimable_outpoints, self.best_block.height, self.best_block.height, broadcaster, @@ -4285,7 +4310,7 @@ impl ChannelMonitorImpl { log_trace!(logger, "Avoiding commitment broadcast, already detected confirmed spend onchain"); continue; } - self.queue_latest_holder_commitment_txn_for_broadcast(broadcaster, &bounded_fee_estimator, logger); + self.queue_latest_holder_commitment_txn_for_broadcast(broadcaster, &bounded_fee_estimator, logger, true); } else if !self.holder_tx_signed { log_error!(logger, "WARNING: You have a potentially-unsafe holder commitment transaction available to broadcast"); log_error!(logger, " in channel monitor for channel {}!", &self.channel_id()); @@ -5751,7 +5776,7 @@ impl ChannelMonitorImpl { // Only attempt to broadcast the new commitment after the `block_disconnected` call above so that // it doesn't get removed from the set of pending claims. if should_broadcast_commitment { - self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, &bounded_fee_estimator, logger); + self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, &bounded_fee_estimator, logger, true); } self.best_block = fork_point; @@ -5812,7 +5837,7 @@ impl ChannelMonitorImpl { // Only attempt to broadcast the new commitment after the `transaction_unconfirmed` call above so // that it doesn't get removed from the set of pending claims. if should_broadcast_commitment { - self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, fee_estimator, logger); + self.queue_latest_holder_commitment_txn_for_broadcast(&broadcaster, fee_estimator, logger, true); } } @@ -6945,7 +6970,7 @@ mod tests { let monitor = ChannelMonitor::new( Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(), &channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), - best_block, dummy_key, channel_id, + best_block, dummy_key, channel_id, false, ); let nondust_htlcs = preimages_slice_to_htlcs!(preimages[0..10]); @@ -7205,7 +7230,7 @@ mod tests { let monitor = ChannelMonitor::new( Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(), &channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), - best_block, dummy_key, channel_id, + best_block, dummy_key, channel_id, false ); let chan_id = monitor.inner.lock().unwrap().channel_id(); From 1f9ee401cfb6cc049f4b7fcab398aa07d93e90c3 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Fri, 26 Sep 2025 11:53:33 -0300 Subject: [PATCH 5/7] Defer claimable tracking until funding tx confirms For manually-broadcast funding, we can't track claimable outputs until the funding tx is actually onchain. Otherwise we'd try to claim outputs that don't exist yet. --- lightning/src/chain/channelmonitor.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 941792e8ae8..215d307f95e 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -5497,8 +5497,10 @@ impl ChannelMonitorImpl { if should_broadcast_commitment { let (mut claimables, mut outputs) = self.generate_claimable_outpoints_and_watch_outputs(None); - claimable_outpoints.append(&mut claimables); - watch_outputs.append(&mut outputs); + if !self.is_manual_broadcast || self.funding_seen_onchain { + claimable_outpoints.append(&mut claimables); + watch_outputs.append(&mut outputs); + } } self.block_confirmed(height, block_hash, txn_matched, watch_outputs, claimable_outpoints, &broadcaster, &fee_estimator, logger) @@ -5532,13 +5534,18 @@ impl ChannelMonitorImpl { 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)); + if !self.is_manual_broadcast || self.funding_seen_onchain { + claimable_outpoints.append(&mut new_outpoints); + watch_outputs.append(&mut new_outputs); + } + } } // Find which on-chain events have reached their confirmation threshold. From ef3890342112c40435d6d442ecacc87baad1acd7 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Fri, 26 Sep 2025 11:54:09 -0300 Subject: [PATCH 6/7] Queue holder commit once funding tx confirms Sets should_broadcast_commitment=true when funding confirms. Since we skip the initial broadcast when funding_seen_onchain is false, we need to queue it once funding actually hits the chain. --- lightning/src/chain/channelmonitor.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 215d307f95e..d5c61fc96ae 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -5260,6 +5260,7 @@ impl ChannelMonitorImpl { F::Target: FeeEstimator, L::Target: Logger, { + let funding_seen_before = self.funding_seen_onchain; let txn_matched = self.filter_block(txdata); for tx in &txn_matched { let mut output_val = Amount::ZERO; @@ -5281,6 +5282,11 @@ impl ChannelMonitorImpl { let mut watch_outputs = Vec::new(); let mut claimable_outpoints = Vec::new(); + + if self.is_manual_broadcast && !funding_seen_before && self.funding_seen_onchain && self.holder_tx_signed + { + should_broadcast_commitment = true; + } 'tx_iter: for tx in &txn_matched { let txid = tx.compute_txid(); log_trace!(logger, "Transaction {} confirmed in block {}", txid , block_hash); From 400730f25974061fc2c3b01d491a89261bad0706 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Fri, 26 Sep 2025 11:54:50 -0300 Subject: [PATCH 7/7] Test manual broadcast tracking and holder commit flow Tests that holder commitment broadcasts are properly deferred until funding confirms, and that the full manual-funding flow works correctly. --- lightning/src/chain/channelmonitor.rs | 617 +++++++++++++++++++++++++- 1 file changed, 616 insertions(+), 1 deletion(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index d5c61fc96ae..200a7d7a71c 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -6880,6 +6880,621 @@ mod tests { check_added_monitors(&nodes[1], 1); } + #[test] + fn test_manual_broadcast_skips_commitment_until_funding_seen() { + let secp_ctx = Secp256k1::new(); + let logger = Arc::new(TestLogger::new()); + let broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); + let fee_estimator = Arc::new(TestFeeEstimator::new(253)); + + let dummy_key = + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + + let keys = InMemorySigner::new( + &secp_ctx, + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + [41; 32], + [0; 32], + [0; 32], + ); + + let counterparty_pubkeys = ChannelPublicKeys { + funding_pubkey: PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[44; 32]).unwrap(), + ), + revocation_basepoint: RevocationBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[45; 32]).unwrap(), + )), + payment_point: PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[46; 32]).unwrap(), + ), + delayed_payment_basepoint: DelayedPaymentBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[47; 32]).unwrap(), + )), + htlc_basepoint: HtlcBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[48; 32]).unwrap(), + )), + }; + let funding_outpoint = OutPoint { txid: Txid::all_zeros(), index: u16::MAX }; + let channel_id = ChannelId::v1_from_funding_outpoint(funding_outpoint); + let channel_parameters = ChannelTransactionParameters { + holder_pubkeys: keys.holder_channel_pubkeys.clone(), + holder_selected_contest_delay: 66, + is_outbound_from_holder: true, + counterparty_parameters: Some(CounterpartyChannelTransactionParameters { + pubkeys: counterparty_pubkeys, + selected_contest_delay: 67, + }), + funding_outpoint: Some(funding_outpoint), + splice_parent_funding_txid: None, + channel_type_features: ChannelTypeFeatures::only_static_remote_key(), + channel_value_satoshis: 0, + }; + let shutdown_pubkey = + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let shutdown_script = ShutdownScript::new_p2wpkh_from_pubkey(shutdown_pubkey); + let best_block = BestBlock::from_network(Network::Testnet); + let monitor = ChannelMonitor::new( + Secp256k1::new(), + keys, + Some(shutdown_script.into_inner()), + 0, + &ScriptBuf::new(), + &channel_parameters, + true, + 0, + HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), + best_block, + dummy_key, + channel_id, + true, + ); + + let payment_hash = PaymentHash([7; 32]); + let htlc = HTLCOutputInCommitment { + offered: true, + amount_msat: 1000, + cltv_expiry: 1, + payment_hash, + transaction_output_index: Some(0), + }; + let commit_tx = HolderCommitmentTransaction::dummy(0, funding_outpoint, vec![htlc.clone()]); + let dummy_sig = crate::crypto::utils::sign( + &secp_ctx, + &bitcoin::secp256k1::Message::from_digest([42; 32]), + &SecretKey::from_slice(&[42; 32]).unwrap(), + ); + let dummy_source = HTLCSource::dummy(); + monitor.provide_latest_holder_commitment_tx( + commit_tx, + &vec![(htlc.clone(), Some(dummy_sig), Some(dummy_source.clone()))], + ); + + // Advance height beyond expiry. no commitment should be broadcast. + let prev_hash = monitor.current_best_block().block_hash; + { + let mut blocks = broadcaster.blocks.lock().unwrap(); + blocks.push((create_dummy_block(prev_hash, 0, vec![]), 10)); + } + let header = create_dummy_header(prev_hash, 0); + monitor.best_block_updated( + &header, + 10, + Arc::clone(&broadcaster), + Arc::clone(&fee_estimator), + &logger, + ); + assert!(broadcaster.txn_broadcast().is_empty()); + + // Now simulate seeing funding on-chain. ensure the + // monitor proceeds to broadcast upon next height update. + { + let mut inner = monitor.inner.lock().unwrap(); + inner.funding_seen_onchain = true; + } + { + let mut blocks = broadcaster.blocks.lock().unwrap(); + blocks.push((create_dummy_block(header.block_hash(), 1, vec![]), 11)); + } + let header2 = create_dummy_header(header.block_hash(), 1); + monitor.best_block_updated( + &header2, + 11, + Arc::clone(&broadcaster), + Arc::clone(&fee_estimator), + &logger, + ); + assert!(!broadcaster.txn_broadcast().is_empty()); + } + + #[test] + fn test_manual_broadcast_detects_funding_and_broadcasts_on_timeout() { + let secp_ctx = Secp256k1::new(); + let logger = Arc::new(TestLogger::new()); + let broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); + let fee_estimator = TestFeeEstimator::new(253); + + let dummy_key = + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + + let keys = InMemorySigner::new( + &secp_ctx, + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + [41; 32], + [0; 32], + [0; 32], + ); + + let counterparty_pubkeys = ChannelPublicKeys { + funding_pubkey: PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[44; 32]).unwrap(), + ), + revocation_basepoint: RevocationBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[45; 32]).unwrap(), + )), + payment_point: PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[46; 32]).unwrap(), + ), + delayed_payment_basepoint: DelayedPaymentBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[47; 32]).unwrap(), + )), + htlc_basepoint: HtlcBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[48; 32]).unwrap(), + )), + }; + + let fake_prevout = bitcoin::OutPoint { txid: Txid::all_zeros(), vout: 0 }; + let funding_script = { + let holder_pubkeys = keys.holder_channel_pubkeys.clone(); + let redeem = chan_utils::make_funding_redeemscript( + &holder_pubkeys.funding_pubkey, + &counterparty_pubkeys.funding_pubkey, + ); + redeem.to_p2wsh() + }; + let funding_tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: fake_prevout, + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::new(), + }], + output: vec![TxOut { + script_pubkey: funding_script.clone(), + value: Amount::from_sat(1000), + }], + }; + let funding_txid = funding_tx.compute_txid(); + let funding_outpoint = OutPoint { txid: funding_txid, index: 0 }; + + let channel_id = ChannelId::v1_from_funding_outpoint(funding_outpoint); + let channel_parameters = ChannelTransactionParameters { + holder_pubkeys: keys.holder_channel_pubkeys.clone(), + holder_selected_contest_delay: 66, + is_outbound_from_holder: true, + counterparty_parameters: Some(CounterpartyChannelTransactionParameters { + pubkeys: counterparty_pubkeys, + selected_contest_delay: 67, + }), + funding_outpoint: Some(funding_outpoint), + splice_parent_funding_txid: None, + channel_type_features: ChannelTypeFeatures::only_static_remote_key(), + channel_value_satoshis: 0, + }; + let shutdown_pubkey = + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let shutdown_script = ShutdownScript::new_p2wpkh_from_pubkey(shutdown_pubkey); + let best_block = BestBlock::from_network(Network::Testnet); + let monitor = ChannelMonitor::new( + Secp256k1::new(), + keys, + Some(shutdown_script.into_inner()), + 0, + &ScriptBuf::new(), + &channel_parameters, + true, + 0, + HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), + best_block, + dummy_key, + channel_id, + true, + ); + + let payment_hash = PaymentHash([9; 32]); + let htlc = HTLCOutputInCommitment { + offered: true, + amount_msat: 1000, + cltv_expiry: 1, + payment_hash, + transaction_output_index: Some(0), + }; + let commit_tx = HolderCommitmentTransaction::dummy(0, funding_outpoint, vec![htlc.clone()]); + let dummy_sig = crate::crypto::utils::sign( + &secp_ctx, + &bitcoin::secp256k1::Message::from_digest([42; 32]), + &SecretKey::from_slice(&[42; 32]).unwrap(), + ); + let dummy_source = HTLCSource::dummy(); + monitor.provide_latest_holder_commitment_tx( + commit_tx, + &vec![(htlc.clone(), Some(dummy_sig), Some(dummy_source.clone()))], + ); + + // Advance height beyond expiry. no broadcast should occur. + let prev_hash = monitor.current_best_block().block_hash; + { + let mut blocks = broadcaster.blocks.lock().unwrap(); + blocks.push((create_dummy_block(prev_hash, 0, vec![]), 10)); + } + let header = create_dummy_header(prev_hash, 0); + monitor.best_block_updated(&header, 10, &*broadcaster, &fee_estimator, &logger); + assert!(broadcaster.txn_broadcast().is_empty()); + { + let inner = monitor.inner.lock().unwrap(); + assert!(!inner.funding_seen_onchain); + } + + // Now confirm the funding transaction via transactions_confirmed. + let fund_block = create_dummy_block(header.block_hash(), 1, vec![funding_tx.clone()]); + let txdata: Vec<(usize, &Transaction)> = + fund_block.txdata.iter().map(|t| (0usize, t)).collect(); + monitor.transactions_confirmed( + &fund_block.header, + &txdata, + 11, + &*broadcaster, + &fee_estimator, + &logger, + ); + { + let inner = monitor.inner.lock().unwrap(); + assert!(inner.funding_seen_onchain); + } + + // Next height update should allow broadcast. + { + let mut blocks = broadcaster.blocks.lock().unwrap(); + blocks.push((create_dummy_block(fund_block.block_hash(), 2, vec![]), 12)); + } + let header2 = create_dummy_header(fund_block.block_hash(), 2); + monitor.best_block_updated(&header2, 12, &*broadcaster, &fee_estimator, &logger); + assert!(!broadcaster.txn_broadcast().is_empty()); + } + + #[test] + fn test_manual_broadcast_no_bump_events_before_funding_seen() { + use crate::events::Event; + use crate::types::features::ChannelTypeFeatures; + + let secp_ctx = Secp256k1::new(); + let logger = Arc::new(TestLogger::new()); + let broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); + let fee_estimator = TestFeeEstimator::new(253); + + let dummy_key = + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + + let keys = InMemorySigner::new( + &secp_ctx, + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + [41; 32], + [0; 32], + [0; 32], + ); + + let counterparty_pubkeys = ChannelPublicKeys { + funding_pubkey: PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[44; 32]).unwrap(), + ), + revocation_basepoint: RevocationBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[45; 32]).unwrap(), + )), + payment_point: PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[46; 32]).unwrap(), + ), + delayed_payment_basepoint: DelayedPaymentBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[47; 32]).unwrap(), + )), + htlc_basepoint: HtlcBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[48; 32]).unwrap(), + )), + }; + + let funding_outpoint = OutPoint { txid: Txid::all_zeros(), index: u16::MAX }; + let channel_id = ChannelId::v1_from_funding_outpoint(funding_outpoint); + let channel_parameters = ChannelTransactionParameters { + holder_pubkeys: keys.holder_channel_pubkeys.clone(), + holder_selected_contest_delay: 66, + is_outbound_from_holder: true, + counterparty_parameters: Some(CounterpartyChannelTransactionParameters { + pubkeys: counterparty_pubkeys, + selected_contest_delay: 67, + }), + funding_outpoint: Some(funding_outpoint), + splice_parent_funding_txid: None, + channel_type_features: ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(), + channel_value_satoshis: 1_000_000, + }; + + let shutdown_pubkey = + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let shutdown_script = ShutdownScript::new_p2wpkh_from_pubkey(shutdown_pubkey); + let best_block = BestBlock::from_network(Network::Testnet); + let monitor = ChannelMonitor::new( + Secp256k1::new(), + keys, + Some(shutdown_script.into_inner()), + 0, + &ScriptBuf::new(), + &channel_parameters, + true, + 0, + HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), + best_block, + dummy_key, + channel_id, + true, + ); + + let payment_hash = PaymentHash([7; 32]); + let htlc = HTLCOutputInCommitment { + offered: true, + amount_msat: 1000, + cltv_expiry: 1, + payment_hash, + transaction_output_index: Some(0), + }; + let commit_tx = HolderCommitmentTransaction::dummy(0, funding_outpoint, vec![htlc.clone()]); + let dummy_sig = crate::crypto::utils::sign( + &secp_ctx, + &bitcoin::secp256k1::Message::from_digest([42; 32]), + &SecretKey::from_slice(&[42; 32]).unwrap(), + ); + let dummy_source = HTLCSource::dummy(); + monitor.provide_latest_holder_commitment_tx( + commit_tx, + &vec![(htlc.clone(), Some(dummy_sig), Some(dummy_source.clone()))], + ); + + // Advance height beyond expiry, there must be no bump events emitted. + let prev_hash = monitor.current_best_block().block_hash; + { + let mut blocks = broadcaster.blocks.lock().unwrap(); + blocks.push((create_dummy_block(prev_hash, 0, vec![]), 10)); + } + let header = create_dummy_header(prev_hash, 0); + monitor.best_block_updated(&header, 10, &*broadcaster, &fee_estimator, &logger); + let events = monitor.get_and_clear_pending_events(); + assert!( + events.iter().all(|e| !matches!(e, Event::BumpTransaction(_))), + "No BumpTransaction events should be emitted before funding is seen on-chain" + ); + } + + #[test] + fn test_manual_broadcast_reorg_resets_funding_seen() { + let secp_ctx = Secp256k1::new(); + let logger = Arc::new(TestLogger::new()); + let broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); + let fee_estimator = TestFeeEstimator::new(253); + + let dummy_key = + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + + let keys = InMemorySigner::new( + &secp_ctx, + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + [41; 32], + [0; 32], + [0; 32], + ); + + let counterparty_pubkeys = ChannelPublicKeys { + funding_pubkey: PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[44; 32]).unwrap(), + ), + revocation_basepoint: RevocationBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[45; 32]).unwrap(), + )), + payment_point: PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[46; 32]).unwrap(), + ), + delayed_payment_basepoint: DelayedPaymentBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[47; 32]).unwrap(), + )), + htlc_basepoint: HtlcBasepoint::from(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[48; 32]).unwrap(), + )), + }; + + let funding_script = { + let holder_pubkeys = keys.holder_channel_pubkeys.clone(); + let redeem = chan_utils::make_funding_redeemscript( + &holder_pubkeys.funding_pubkey, + &counterparty_pubkeys.funding_pubkey, + ); + redeem.to_p2wsh() + }; + let funding_tx = Transaction { + version: Version(2), + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: bitcoin::OutPoint { txid: Txid::all_zeros(), vout: 0 }, + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::new(), + }], + output: vec![TxOut { + script_pubkey: funding_script.clone(), + value: Amount::from_sat(1000), + }], + }; + let funding_txid = funding_tx.compute_txid(); + let funding_outpoint = OutPoint { txid: funding_txid, index: 0 }; + + let channel_id = ChannelId::v1_from_funding_outpoint(funding_outpoint); + let channel_parameters = ChannelTransactionParameters { + holder_pubkeys: keys.holder_channel_pubkeys.clone(), + holder_selected_contest_delay: 66, + is_outbound_from_holder: true, + counterparty_parameters: Some(CounterpartyChannelTransactionParameters { + pubkeys: counterparty_pubkeys, + selected_contest_delay: 67, + }), + funding_outpoint: Some(funding_outpoint), + splice_parent_funding_txid: None, + channel_type_features: ChannelTypeFeatures::only_static_remote_key(), + channel_value_satoshis: 0, + }; + let shutdown_pubkey = + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let shutdown_script = ShutdownScript::new_p2wpkh_from_pubkey(shutdown_pubkey); + let best_block = BestBlock::from_network(Network::Testnet); + let monitor = ChannelMonitor::new( + Secp256k1::new(), + keys, + Some(shutdown_script.into_inner()), + 0, + &ScriptBuf::new(), + &channel_parameters, + true, + 0, + HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), + best_block, + dummy_key, + channel_id, + true, + ); + + let payment_hash = PaymentHash([3; 32]); + let htlc = HTLCOutputInCommitment { + offered: true, + amount_msat: 1000, + cltv_expiry: 1, + payment_hash, + transaction_output_index: Some(0), + }; + let commit_tx = HolderCommitmentTransaction::dummy(0, funding_outpoint, vec![htlc.clone()]); + let dummy_sig = crate::crypto::utils::sign( + &secp_ctx, + &bitcoin::secp256k1::Message::from_digest([42; 32]), + &SecretKey::from_slice(&[42; 32]).unwrap(), + ); + let dummy_source = HTLCSource::dummy(); + monitor.provide_latest_holder_commitment_tx( + commit_tx, + &vec![(htlc.clone(), Some(dummy_sig), Some(dummy_source.clone()))], + ); + + // Bump height past expiry. no broadcast yet since funding not seen. + let prev_hash = monitor.current_best_block().block_hash; + { + let mut blocks = broadcaster.blocks.lock().unwrap(); + blocks.push((create_dummy_block(prev_hash, 0, vec![]), 10)); + } + monitor.best_block_updated( + &create_dummy_header(prev_hash, 0), + 10, + &*broadcaster, + &fee_estimator, + &logger, + ); + assert!(broadcaster.txn_broadcast().is_empty()); + + // Confirm funding, then immediately unconfirm it before a height update. gating should reset. + let fund_block = create_dummy_block(prev_hash, 1, vec![funding_tx.clone()]); + let txdata: Vec<(usize, &Transaction)> = + fund_block.txdata.iter().map(|t| (0usize, t)).collect(); + monitor.transactions_confirmed( + &fund_block.header, + &txdata, + 11, + &*broadcaster, + &fee_estimator, + &logger, + ); + monitor.transaction_unconfirmed(&funding_txid, &*broadcaster, &fee_estimator, &logger); + + // Next height update should still NOT broadcast since funding was unconfirmed. + { + let mut blocks = broadcaster.blocks.lock().unwrap(); + blocks.push((create_dummy_block(fund_block.block_hash(), 2, vec![]), 12)); + } + monitor.best_block_updated( + &create_dummy_header(fund_block.block_hash(), 2), + 12, + &*broadcaster, + &fee_estimator, + &logger, + ); + let _ = broadcaster.txn_broadcasted.lock().unwrap().split_off(0); + assert!(broadcaster.txn_broadcast().is_empty()); + + // Reconfirm funding, then height update should allow broadcast now. + let re_block = create_dummy_block(fund_block.block_hash(), 3, vec![funding_tx.clone()]); + let txdata2: Vec<(usize, &Transaction)> = + re_block.txdata.iter().map(|t| (0usize, t)).collect(); + monitor.transactions_confirmed( + &re_block.header, + &txdata2, + 13, + &*broadcaster, + &fee_estimator, + &logger, + ); + { + let mut blocks = broadcaster.blocks.lock().unwrap(); + blocks.push((create_dummy_block(re_block.block_hash(), 4, vec![]), 14)); + } + monitor.best_block_updated( + &create_dummy_header(re_block.block_hash(), 4), + 14, + &*broadcaster, + &fee_estimator, + &logger, + ); + assert!(!broadcaster.txn_broadcast().is_empty()); + } + #[test] fn test_funding_spend_refuses_updates() { do_test_funding_spend_refuses_updates(true); @@ -7243,7 +7858,7 @@ mod tests { let monitor = ChannelMonitor::new( Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(), &channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), - best_block, dummy_key, channel_id, false + best_block, dummy_key, channel_id, false, ); let chan_id = monitor.inner.lock().unwrap().channel_id();