diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 0f36cf14e60..200a7d7a71c 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, @@ -2312,19 +2327,33 @@ 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] + /// + /// 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 + &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, + false, + ); } /// Unsafe test-only version of `broadcast_latest_holder_commitment_txn` used by our test framework @@ -3941,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, @@ -3954,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, @@ -4268,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()); @@ -5218,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; @@ -5239,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); @@ -5455,8 +5503,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) @@ -5490,13 +5540,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. @@ -5734,7 +5789,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; @@ -5795,16 +5850,23 @@ 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); } } /// 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; } @@ -5813,7 +5875,7 @@ impl ChannelMonitorImpl { } } if matches { - matched_txn.insert(tx.compute_txid()); + matched_txn.insert(txid); } matches }).map(|(_, tx)| *tx).collect() @@ -6454,6 +6516,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), @@ -6474,6 +6538,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. @@ -6591,6 +6657,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, @@ -6810,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); @@ -6913,7 +7598,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]); @@ -7173,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, + best_block, dummy_key, channel_id, false, ); let chan_id = monitor.inner.lock().unwrap().channel_id(); 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(),