From 988f1b1490030afe4911cefe2d2c2308dda6773c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 27 Jan 2026 12:34:53 +0100 Subject: [PATCH 1/3] Refactor `BroadcasterInterface` to include `TransactionType` Add a `TransactionType` enum to provide context about the type of transaction being broadcast. This information can be useful for logging, filtering, or prioritization purposes. The `TransactionType` variants are: - `Funding`: A funding transaction establishing a new channel - `CooperativeClose`: A cooperative close transaction - `UnilateralClose`: A force-close transaction - `AnchorBump`: An anchor transaction for CPFP fee-bumping - `Claim`: A transaction claiming outputs from commitment tx - `Sweep`: A transaction sweeping spendable outputs to wallet Co-Authored-By: HAL 9000 Signed-off-by: Elias Rohrer --- fuzz/src/chanmon_consistency.rs | 8 +- fuzz/src/full_stack.rs | 8 +- lightning-liquidity/src/lsps2/service.rs | 17 +-- lightning/src/chain/chaininterface.rs | 94 +++++++++++++- lightning/src/chain/channelmonitor.rs | 8 +- lightning/src/chain/onchaintx.rs | 42 +++++-- lightning/src/events/bump_transaction/mod.rs | 29 +++-- lightning/src/ln/channel.rs | 26 +++- lightning/src/ln/channelmanager.rs | 44 +++++-- lightning/src/util/sweep.rs | 125 +++++++++++-------- lightning/src/util/test_utils.rs | 14 ++- 11 files changed, 305 insertions(+), 110 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index f87af5c6ff5..f363da534ac 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -36,7 +36,9 @@ use bitcoin::WPubkeyHash; use lightning::blinded_path::message::{BlindedMessagePath, MessageContext, MessageForwardNode}; use lightning::blinded_path::payment::{BlindedPaymentPath, ReceiveTlvs}; use lightning::chain; -use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; +use lightning::chain::chaininterface::{ + TransactionType, BroadcasterInterface, ConfirmationTarget, FeeEstimator, +}; use lightning::chain::channelmonitor::{ChannelMonitor, MonitorEvent}; use lightning::chain::transaction::OutPoint; use lightning::chain::{ @@ -159,8 +161,8 @@ pub struct TestBroadcaster { txn_broadcasted: RefCell>, } impl BroadcasterInterface for TestBroadcaster { - fn broadcast_transactions(&self, txs: &[&Transaction]) { - for tx in txs { + fn broadcast_transactions(&self, txs: &[(&Transaction, TransactionType)]) { + for (tx, _broadcast_type) in txs { self.txn_broadcasted.borrow_mut().push((*tx).clone()); } } diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index e73db74fa5d..0b0f097863a 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -35,7 +35,9 @@ use lightning::ln::funding::{FundingTxInput, SpliceContribution}; use lightning::blinded_path::message::{BlindedMessagePath, MessageContext, MessageForwardNode}; use lightning::blinded_path::payment::{BlindedPaymentPath, ReceiveTlvs}; use lightning::chain; -use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; +use lightning::chain::chaininterface::{ + TransactionType, BroadcasterInterface, ConfirmationTarget, FeeEstimator, +}; use lightning::chain::chainmonitor; use lightning::chain::transaction::OutPoint; use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen}; @@ -187,8 +189,8 @@ struct TestBroadcaster { txn_broadcasted: Mutex>, } impl BroadcasterInterface for TestBroadcaster { - fn broadcast_transactions(&self, txs: &[&Transaction]) { - let owned_txs: Vec = txs.iter().map(|tx| (*tx).clone()).collect(); + fn broadcast_transactions(&self, txs: &[(&Transaction, TransactionType)]) { + let owned_txs: Vec = txs.iter().map(|(tx, _)| (*tx).clone()).collect(); self.txn_broadcasted.lock().unwrap().extend(owned_txs); } } diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 00f68aff696..4c688d39eef 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -40,7 +40,7 @@ use crate::prelude::{new_hash_map, HashMap}; use crate::sync::{Arc, Mutex, MutexGuard, RwLock}; use crate::utils::async_poll::dummy_waker; -use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::chain::chaininterface::{BroadcasterInterface, TransactionType}; use lightning::events::HTLCHandlingFailureType; use lightning::ln::channelmanager::{AChannelManager, FailureCode, InterceptId}; use lightning::ln::msgs::{ErrorAction, LightningError}; @@ -2019,23 +2019,24 @@ where // (for example when a forwarded HTLC nears expiry). Broadcasting funding after a // close could then confirm the commitment and trigger unintended on‑chain handling. // To avoid this, we check ChannelManager’s view (`is_channel_ready`) before broadcasting. - let channel_id_opt = jit_channel.get_channel_id(); - if let Some(ch_id) = channel_id_opt { + if let Some(ch_id) = jit_channel.get_channel_id() { let is_channel_ready = self .channel_manager .get_cm() .list_channels() .into_iter() .any(|cd| cd.channel_id == ch_id && cd.is_channel_ready); + if !is_channel_ready { return; } - } else { - return; - } - if let Some(funding_tx) = jit_channel.get_funding_tx() { - self.tx_broadcaster.broadcast_transactions(&[funding_tx]); + if let Some(funding_tx) = jit_channel.get_funding_tx() { + self.tx_broadcaster.broadcast_transactions(&[( + funding_tx, + TransactionType::Funding { channel_ids: vec![ch_id] }, + )]); + } } } } diff --git a/lightning/src/chain/chaininterface.rs b/lightning/src/chain/chaininterface.rs index 7e71d960e67..758fd1a74e2 100644 --- a/lightning/src/chain/chaininterface.rs +++ b/lightning/src/chain/chaininterface.rs @@ -15,10 +15,97 @@ use core::{cmp, ops::Deref}; +use crate::ln::types::ChannelId; use crate::prelude::*; use bitcoin::transaction::Transaction; +/// Represents the class of transaction being broadcast. +/// +/// This is used to provide context about the type of transaction being broadcast, which may be +/// useful for logging, filtering, or prioritization purposes. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub enum TransactionType { + /// A funding transaction establishing a new channel. + /// + /// If we initiated the channel the transaction given to + /// [`ChannelManager::funding_transaction_generated`] will be broadcast with this type. + /// + /// [`ChannelManager::funding_transaction_generated`]: crate::ln::channelmanager::ChannelManager::funding_transaction_generated + Funding { + /// The IDs of the channels being funded. + /// + /// A single funding transaction may establish multiple channels when using batch funding. + channel_ids: Vec, + }, + /// A transaction cooperatively closing a channel. + /// + /// A transaction of this type will be broadcast when cooperatively closing a channel via + /// [`ChannelManager::close_channel`] or if the counterparty closes the channel. + /// + /// [`ChannelManager::close_channel`]: crate::ln::channelmanager::ChannelManager::close_channel + CooperativeClose { + /// The ID of the channel being closed. + channel_id: ChannelId, + }, + /// A transaction being broadcast to force-close the channel. + /// + /// A transaction of this type will be broadcast when unilaterally closing a channel via + /// [`ChannelManager::force_close_broadcasting_latest_txn`] or if the counterparty force-closes + /// the channel. + /// + /// [`ChannelManager::force_close_broadcasting_latest_txn`]: crate::ln::channelmanager::ChannelManager::force_close_broadcasting_latest_txn + UnilateralClose { + /// The ID of the channel being force-closed. + channel_id: ChannelId, + }, + /// An anchor bumping transaction used for CPFP fee-bumping a closing transaction. + /// + /// This will be broadcast after an anchor channel has been closed. See + /// [`BumpTransactionEvent`] for more information. + /// + /// [`BumpTransactionEvent`]: crate::events::bump_transaction::BumpTransactionEvent + AnchorBump { + /// The ID of the channel whose closing transaction is being fee-bumped. + channel_id: ChannelId, + }, + /// A transaction which is resolving an output spendable by both us and our counterparty. + /// + /// When a channel closes via the unilateral close path, there may be transaction outputs which + /// are spendable by either our counterparty or us and represent some lightning state. In order + /// to resolve that state, the [`ChannelMonitor`] will spend any such outputs, ensuring funds + /// are only available to us prior to generating an [`Event::SpendableOutputs`]. This + /// transaction is one such transaction - resolving in-flight HTLCs or punishing our + /// counterparty if they broadcasted an outdated state. + /// + /// [`ChannelMonitor`]: crate::chain::ChannelMonitor + /// [`Event::SpendableOutputs`]: crate::events::Event::SpendableOutputs + Claim { + /// The ID of the channel from which outputs are being claimed. + channel_id: ChannelId, + }, + /// A transaction generated by the [`OutputSweeper`], sweeping [`SpendableOutputDescriptor`]s + /// to the user's wallet. + /// + /// [`OutputSweeper`]: crate::util::sweep::OutputSweeper + /// [`SpendableOutputDescriptor`]: crate::sign::SpendableOutputDescriptor + Sweep { + /// The IDs of the channels from which outputs are being swept, if known. + /// + /// A single sweep transaction may aggregate outputs from multiple channels. + channel_ids: Vec, + }, + /// A splice transaction modifying an existing channel's funding. + /// + /// A transaction of this type will be broadcast as a result of a [`ChannelManager::splice_channel`] operation. + /// + /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel + Splice { + /// The ID of the channel being spliced. + channel_id: ChannelId, + }, +} + // TODO: Define typed abstraction over feerates to handle their conversions. pub(crate) fn compute_feerate_sat_per_1000_weight(fee_sat: u64, weight: u64) -> u32 { (fee_sat * 1000 / weight).try_into().unwrap_or(u32::max_value()) @@ -45,11 +132,14 @@ pub trait BroadcasterInterface { /// /// Bitcoin transaction packages are defined in BIP 331 and here: /// - fn broadcast_transactions(&self, txs: &[&Transaction]); + /// + /// Each transaction is paired with a [`TransactionType`] indicating the class of transaction + /// being broadcast, which may be useful for logging, filtering, or prioritization. + fn broadcast_transactions(&self, txs: &[(&Transaction, TransactionType)]); } impl> BroadcasterInterface for B { - fn broadcast_transactions(&self, txs: &[&Transaction]) { + fn broadcast_transactions(&self, txs: &[(&Transaction, TransactionType)]) { self.deref().broadcast_transactions(txs) } } diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 80d0ef125fc..2d2fdd14ad2 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -1879,8 +1879,9 @@ impl ChannelMonitor { initial_holder_commitment_tx.trust().commitment_number(); let onchain_tx_handler = OnchainTxHandler::new( - channel_parameters.channel_value_satoshis, channel_keys_id, destination_script.into(), - keys, channel_parameters.clone(), initial_holder_commitment_tx.clone(), secp_ctx + channel_id, channel_parameters.channel_value_satoshis, channel_keys_id, + destination_script.into(), keys, channel_parameters.clone(), + initial_holder_commitment_tx.clone(), secp_ctx, ); let funding_outpoint = channel_parameters.funding_outpoint @@ -6491,7 +6492,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP return Err(DecodeError::InvalidValue); } } - let onchain_tx_handler: OnchainTxHandler = ReadableArgs::read( + let mut onchain_tx_handler: OnchainTxHandler = ReadableArgs::read( reader, (entropy_source, signer_provider, channel_value_satoshis, channel_keys_id) )?; @@ -6587,6 +6588,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP } let channel_id = channel_id.unwrap_or(ChannelId::v1_from_funding_outpoint(outpoint)); + onchain_tx_handler.set_channel_id(channel_id); let (current_holder_commitment_tx, current_holder_htlc_data) = { let holder_commitment_tx = onchain_tx_handler.current_holder_commitment_tx(); diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs index cfee63beefd..8de99eb8601 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -23,7 +23,9 @@ use bitcoin::transaction::OutPoint as BitcoinOutPoint; use bitcoin::transaction::Transaction; use crate::chain::chaininterface::ConfirmationTarget; -use crate::chain::chaininterface::{BroadcasterInterface, FeeEstimator, LowerBoundedFeeEstimator}; +use crate::chain::chaininterface::{ + BroadcasterInterface, FeeEstimator, LowerBoundedFeeEstimator, TransactionType, +}; use crate::chain::channelmonitor::ANTI_REORG_DELAY; use crate::chain::package::{PackageSolvingData, PackageTemplate}; use crate::chain::transaction::MaybeSignedTransaction; @@ -33,6 +35,7 @@ use crate::ln::chan_utils::{ HTLCOutputInCommitment, HolderCommitmentTransaction, }; use crate::ln::msgs::DecodeError; +use crate::ln::types::ChannelId; use crate::sign::{ecdsa::EcdsaChannelSigner, EntropySource, HTLCDescriptor, SignerProvider}; use crate::util::logger::Logger; use crate::util::ser::{ @@ -220,6 +223,7 @@ pub(crate) enum FeerateStrategy { /// do RBF bumping if possible. #[derive(Clone)] pub struct OnchainTxHandler { + channel_id: ChannelId, channel_value_satoshis: u64, // Deprecated as of 0.2. channel_keys_id: [u8; 32], // Deprecated as of 0.2. destination_script: ScriptBuf, // Deprecated as of 0.2. @@ -282,7 +286,8 @@ impl PartialEq for OnchainTxHandler bool { // `signer`, `secp_ctx`, and `pending_claim_events` are excluded on purpose. - self.channel_value_satoshis == other.channel_value_satoshis && + self.channel_id == other.channel_id && + self.channel_value_satoshis == other.channel_value_satoshis && self.channel_keys_id == other.channel_keys_id && self.destination_script == other.destination_script && self.holder_commitment == other.holder_commitment && @@ -345,6 +350,14 @@ impl OnchainTxHandler { write_tlv_fields!(writer, {}); Ok(()) } + + // `ChannelMonitor`s already track the `channel_id`, however, due to the derserialization order + // there we can't make use of `ReadableArgs` to hand it into `OnchainTxHandler`'s + // deserialization logic directly. Instead we opt to initialize it with 0s and override it + // after reading the respective field via this method. + pub(crate) fn set_channel_id(&mut self, channel_id: ChannelId) { + self.channel_id = channel_id; + } } impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP, u64, [u8; 32])> @@ -366,7 +379,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP let prev_holder_commitment = Readable::read(reader)?; let _prev_holder_htlc_sigs: Option>> = Readable::read(reader)?; - let channel_parameters = ReadableArgs::>::read(reader, Some(channel_value_satoshis))?; + let channel_parameters: ChannelTransactionParameters = ReadableArgs::>::read(reader, Some(channel_value_satoshis))?; // Read the serialized signer bytes, but don't deserialize them, as we'll obtain our signer // by re-deriving the private key material. @@ -420,10 +433,17 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP read_tlv_fields!(reader, {}); + // `ChannelMonitor`s already track the `channel_id`, however, due to the derserialization + // order there we can't make use of `ReadableArgs` to hand it in directly. Instead we opt + // to initialize it with 0s and override it after reading the respective field via + // `OnchainTxHandler::set_channel_id`. + let channel_id = ChannelId([0u8; 32]); + let mut secp_ctx = Secp256k1::new(); secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes()); Ok(OnchainTxHandler { + channel_id, channel_value_satoshis, channel_keys_id, destination_script, @@ -443,11 +463,13 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP impl OnchainTxHandler { pub(crate) fn new( - channel_value_satoshis: u64, channel_keys_id: [u8; 32], destination_script: ScriptBuf, - signer: ChannelSigner, channel_parameters: ChannelTransactionParameters, + channel_id: ChannelId, channel_value_satoshis: u64, channel_keys_id: [u8; 32], + destination_script: ScriptBuf, signer: ChannelSigner, + channel_parameters: ChannelTransactionParameters, holder_commitment: HolderCommitmentTransaction, secp_ctx: Secp256k1, ) -> Self { OnchainTxHandler { + channel_id, channel_value_satoshis, channel_keys_id, destination_script, @@ -511,7 +533,7 @@ impl OnchainTxHandler { if tx.is_fully_signed() { let log_start = if feerate_was_bumped { "Broadcasting RBF-bumped" } else { "Rebroadcasting" }; log_info!(logger, "{} onchain {}", log_start, log_tx!(tx.0)); - broadcaster.broadcast_transactions(&[&tx.0]); + broadcaster.broadcast_transactions(&[(&tx.0, TransactionType::Claim { channel_id: self.channel_id })]); } else { log_info!(logger, "Waiting for signature of unsigned onchain transaction {}", tx.0.compute_txid()); } @@ -853,7 +875,7 @@ impl OnchainTxHandler { OnchainClaim::Tx(tx) => { if tx.is_fully_signed() { log_info!(logger, "Broadcasting onchain {}", log_tx!(tx.0)); - broadcaster.broadcast_transactions(&[&tx.0]); + broadcaster.broadcast_transactions(&[(&tx.0, TransactionType::Claim { channel_id: self.channel_id })]); } else { log_info!(logger, "Waiting for signature of unsigned onchain transaction {}", tx.0.compute_txid()); } @@ -1071,7 +1093,7 @@ impl OnchainTxHandler { OnchainClaim::Tx(bump_tx) => { if bump_tx.is_fully_signed() { log_info!(logger, "Broadcasting RBF-bumped onchain {}", log_tx!(bump_tx.0)); - broadcaster.broadcast_transactions(&[&bump_tx.0]); + broadcaster.broadcast_transactions(&[(&bump_tx.0, TransactionType::Claim { channel_id: self.channel_id })]); } else { log_info!(logger, "Waiting for signature of RBF-bumped unsigned onchain transaction {}", bump_tx.0.compute_txid()); @@ -1168,7 +1190,7 @@ impl OnchainTxHandler { OnchainClaim::Tx(bump_tx) => { if bump_tx.is_fully_signed() { log_info!(logger, "Broadcasting onchain {}", log_tx!(bump_tx.0)); - broadcaster.broadcast_transactions(&[&bump_tx.0]); + broadcaster.broadcast_transactions(&[(&bump_tx.0, TransactionType::Claim { channel_id: self.channel_id })]); } else { log_info!(logger, "Waiting for signature of unsigned onchain transaction {}", bump_tx.0.compute_txid()); } @@ -1262,6 +1284,7 @@ mod tests { }; use crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint}; use crate::ln::functional_test_utils::create_dummy_block; + use crate::ln::types::ChannelId; use crate::sign::{ChannelDerivationParameters, ChannelSigner, HTLCDescriptor, InMemorySigner}; use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::test_utils::{TestBroadcaster, TestFeeEstimator, TestLogger}; @@ -1346,6 +1369,7 @@ mod tests { let holder_commit = HolderCommitmentTransaction::dummy(1000000, funding_outpoint, nondust_htlcs); let destination_script = ScriptBuf::new(); let mut tx_handler = OnchainTxHandler::new( + ChannelId::from_bytes([0; 32]), 1000000, [0; 32], destination_script.clone(), diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index 1b3496c5eab..6b6581ec749 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -18,7 +18,7 @@ use core::future::Future; use core::ops::Deref; use crate::chain::chaininterface::{ - compute_feerate_sat_per_1000_weight, fee_for_weight, BroadcasterInterface, + compute_feerate_sat_per_1000_weight, fee_for_weight, BroadcasterInterface, TransactionType, }; use crate::chain::ClaimId; use crate::io_extras::sink; @@ -761,9 +761,9 @@ where /// transaction spending an anchor output of the commitment transaction to bump its fee and /// broadcasts them to the network as a package. async fn handle_channel_close( - &self, claim_id: ClaimId, package_target_feerate_sat_per_1000_weight: u32, - commitment_tx: &Transaction, commitment_tx_fee_sat: u64, - anchor_descriptor: &AnchorDescriptor, + &self, channel_id: ChannelId, claim_id: ClaimId, + package_target_feerate_sat_per_1000_weight: u32, commitment_tx: &Transaction, + commitment_tx_fee_sat: u64, anchor_descriptor: &AnchorDescriptor, ) -> Result<(), ()> { let channel_type = &anchor_descriptor .channel_derivation_parameters @@ -784,7 +784,10 @@ where log_debug!(self.logger, "Pre-signed commitment {} already has feerate {} sat/kW above required {} sat/kW, broadcasting.", commitment_tx.compute_txid(), commitment_tx_feerate_sat_per_1000_weight, package_target_feerate_sat_per_1000_weight); - self.broadcaster.broadcast_transactions(&[&commitment_tx]); + self.broadcaster.broadcast_transactions(&[( + &commitment_tx, + TransactionType::UnilateralClose { channel_id }, + )]); return Ok(()); } @@ -951,7 +954,10 @@ where anchor_txid, commitment_tx.compute_txid() ); - self.broadcaster.broadcast_transactions(&[&commitment_tx, &anchor_tx]); + self.broadcaster.broadcast_transactions(&[ + (&commitment_tx, TransactionType::UnilateralClose { channel_id }), + (&anchor_tx, TransactionType::AnchorBump { channel_id }), + ]); return Ok(()); } } @@ -959,7 +965,7 @@ where /// Handles a [`BumpTransactionEvent::HTLCResolution`] event variant by producing a /// fully-signed, fee-bumped HTLC transaction that is broadcast to the network. async fn handle_htlc_resolution( - &self, claim_id: ClaimId, target_feerate_sat_per_1000_weight: u32, + &self, channel_id: ChannelId, claim_id: ClaimId, target_feerate_sat_per_1000_weight: u32, htlc_descriptors: &[HTLCDescriptor], tx_lock_time: LockTime, ) -> Result<(), ()> { let channel_type = &htlc_descriptors[0] @@ -1184,7 +1190,10 @@ where } log_info!(self.logger, "Broadcasting {}", log_tx!(htlc_tx)); - self.broadcaster.broadcast_transactions(&[&htlc_tx]); + self.broadcaster.broadcast_transactions(&[( + &htlc_tx, + TransactionType::UnilateralClose { channel_id }, + )]); } Ok(()) @@ -1194,6 +1203,7 @@ where pub async fn handle_event(&self, event: &BumpTransactionEvent) { match event { BumpTransactionEvent::ChannelClose { + channel_id, claim_id, package_target_feerate_sat_per_1000_weight, commitment_tx, @@ -1208,6 +1218,7 @@ where commitment_tx.compute_txid() ); self.handle_channel_close( + *channel_id, *claim_id, *package_target_feerate_sat_per_1000_weight, commitment_tx, @@ -1224,6 +1235,7 @@ where }); }, BumpTransactionEvent::HTLCResolution { + channel_id, claim_id, target_feerate_sat_per_1000_weight, htlc_descriptors, @@ -1237,6 +1249,7 @@ where log_iter!(htlc_descriptors.iter().map(|d| d.outpoint())) ); self.handle_htlc_resolution( + *channel_id, *claim_id, *target_feerate_sat_per_1000_weight, htlc_descriptors, diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 042b388e1a1..a47241d5a98 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -28,7 +28,7 @@ use bitcoin::{secp256k1, sighash, FeeRate, Sequence, TxIn}; use crate::blinded_path::message::BlindedMessagePath; use crate::chain::chaininterface::{ - fee_for_weight, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, + fee_for_weight, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, TransactionType, }; use crate::chain::channelmonitor::{ ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, CommitmentHTLCData, @@ -2103,6 +2103,8 @@ where }, }; + let channel_id = context.channel_id; + let signing_session = if let Some(signing_session) = context.interactive_tx_signing_session.as_mut() { @@ -2213,6 +2215,15 @@ where (None, None) }; + let funding_tx = funding_tx.map(|tx| { + let tx_type = if splice_negotiated.is_some() { + TransactionType::Splice { channel_id } + } else { + TransactionType::Funding { channel_ids: vec![channel_id] } + }; + (tx, tx_type) + }); + // If we have a pending splice with a buffered initial commitment signed from our // counterparty, process it now that we have provided our signatures. let counterparty_initial_commitment_signed_result = @@ -6919,8 +6930,8 @@ pub(super) struct FundingTxSigned { /// Signatures that should be sent to the counterparty, if necessary. pub tx_signatures: Option, - /// The fully-signed funding transaction to be broadcast. - pub funding_tx: Option, + /// The fully-signed funding transaction to be broadcast, along with the transaction type. + pub funding_tx: Option<(Transaction, TransactionType)>, /// Information about the completed funding negotiation. pub splice_negotiated: Option, @@ -9143,6 +9154,15 @@ where (None, None) }; + let funding_tx = funding_tx.map(|tx| { + let tx_type = if splice_negotiated.is_some() { + TransactionType::Splice { channel_id: self.context.channel_id } + } else { + TransactionType::Funding { channel_ids: vec![self.context.channel_id] } + }; + (tx, tx_type) + }); + Ok(FundingTxSigned { commitment_signed: None, counterparty_initial_commitment_signed_result: None, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 129a4d58171..775d1ea7682 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -40,6 +40,7 @@ use crate::blinded_path::NodeIdLookUp; use crate::chain; use crate::chain::chaininterface::{ BroadcasterInterface, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, + TransactionType, }; use crate::chain::channelmonitor::{ Balance, ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, MonitorEvent, @@ -6428,13 +6429,14 @@ impl< splice_negotiated, splice_locked, }) => { - if let Some(funding_tx) = funding_tx { + if let Some((funding_tx, tx_type)) = funding_tx { let funded_chan = chan.as_funded_mut().expect( "Funding transactions ready for broadcast can only exist for funded channels", ); self.broadcast_interactive_funding( funded_chan, &funding_tx, + Some(tx_type), &self.logger, ); } @@ -6562,7 +6564,8 @@ impl< } fn broadcast_interactive_funding( - &self, channel: &mut FundedChannel, funding_tx: &Transaction, logger: &L, + &self, channel: &mut FundedChannel, funding_tx: &Transaction, + transaction_type: Option, logger: &L, ) { let logger = WithChannelContext::from(logger, channel.context(), None); log_info!( @@ -6570,7 +6573,10 @@ impl< "Broadcasting signed interactive funding transaction {}", funding_tx.compute_txid() ); - self.tx_broadcaster.broadcast_transactions(&[funding_tx]); + let tx_type = transaction_type.unwrap_or_else(|| TransactionType::Funding { + channel_ids: vec![channel.context().channel_id()], + }); + self.tx_broadcaster.broadcast_transactions(&[(funding_tx, tx_type)]); { let mut pending_events = self.pending_events.lock().unwrap(); emit_channel_pending_event!(pending_events, channel); @@ -9579,6 +9585,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let removed_batch_state = funding_batch_states.remove(&txid).into_iter().flatten(); let per_peer_state = self.per_peer_state.read().unwrap(); let mut batch_funding_tx = None; + let mut batch_channel_ids = Vec::new(); for (channel_id, counterparty_node_id, _) in removed_batch_state { if let Some(peer_state_mutex) = per_peer_state.get(&counterparty_node_id) { let mut peer_state = peer_state_mutex.lock().unwrap(); @@ -9589,6 +9596,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ funded_chan.context.unbroadcasted_funding(&funded_chan.funding) }); funded_chan.set_batch_ready(); + batch_channel_ids.push(channel_id); let mut pending_events = self.pending_events.lock().unwrap(); emit_channel_pending_event!(pending_events, funded_chan); @@ -9597,7 +9605,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } if let Some(tx) = batch_funding_tx { log_info!(self.logger, "Broadcasting batch funding tx {}", tx.compute_txid()); - self.tx_broadcaster.broadcast_transactions(&[&tx]); + self.tx_broadcaster.broadcast_transactions(&[( + &tx, + TransactionType::Funding { channel_ids: batch_channel_ids }, + )]); } } } @@ -10263,7 +10274,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }; } else { log_info!(logger, "Broadcasting funding transaction with txid {}", tx.compute_txid()); - self.tx_broadcaster.broadcast_transactions(&[&tx]); + self.tx_broadcaster.broadcast_transactions(&[( + &tx, + TransactionType::Funding { channel_ids: vec![channel.context.channel_id()] }, + )]); } } @@ -11390,8 +11404,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ msg: splice_locked, }); } - if let Some(ref funding_tx) = funding_tx { - self.broadcast_interactive_funding(chan, funding_tx, &self.logger); + if let Some((ref funding_tx, ref tx_type)) = funding_tx { + self.broadcast_interactive_funding(chan, funding_tx, Some(tx_type.clone()), &self.logger); } if let Some(splice_negotiated) = splice_negotiated { self.pending_events.lock().unwrap().push_back(( @@ -11702,7 +11716,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ mem::drop(per_peer_state); if let Some((broadcast_tx, err)) = tx_err { log_info!(logger, "Broadcasting {}", log_tx!(broadcast_tx)); - self.tx_broadcaster.broadcast_transactions(&[&broadcast_tx]); + self.tx_broadcaster.broadcast_transactions(&[( + &broadcast_tx, + TransactionType::CooperativeClose { channel_id: msg.channel_id }, + )]); let _ = self.handle_error(err, *counterparty_node_id); } Ok(()) @@ -12919,7 +12936,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } if let Some(broadcast_tx) = msgs.signed_closing_tx { log_info!(logger, "Broadcasting closing tx {}", log_tx!(broadcast_tx)); - self.tx_broadcaster.broadcast_transactions(&[&broadcast_tx]); + self.tx_broadcaster.broadcast_transactions(&[( + &broadcast_tx, + TransactionType::CooperativeClose { channel_id }, + )]); } } else { // We don't know how to handle a channel_ready or signed_closing_tx for a @@ -13037,6 +13057,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ if let Some((tx, shutdown_res)) = tx_shutdown_result_opt { // We're done with this channel. We got a closing_signed and sent back // a closing_signed with a closing transaction to broadcast. + let channel_id = funded_chan.context.channel_id(); let err = self.locked_handle_funded_coop_close( &mut peer_state.closed_channel_monitor_update_ids, &mut peer_state.in_flight_monitor_updates, @@ -13046,7 +13067,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ handle_errors.push((*cp_id, Err(err))); log_info!(logger, "Broadcasting {}", log_tx!(tx)); - self.tx_broadcaster.broadcast_transactions(&[&tx]); + self.tx_broadcaster.broadcast_transactions(&[( + &tx, + TransactionType::CooperativeClose { channel_id }, + )]); false } else { true diff --git a/lightning/src/util/sweep.rs b/lightning/src/util/sweep.rs index 2aef2186323..f0c2d6d2d15 100644 --- a/lightning/src/util/sweep.rs +++ b/lightning/src/util/sweep.rs @@ -8,7 +8,9 @@ //! [`SpendableOutputDescriptor`]s, i.e., persists them in a given [`KVStoreSync`] and regularly retries //! sweeping them. -use crate::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; +use crate::chain::chaininterface::{ + BroadcasterInterface, ConfirmationTarget, FeeEstimator, TransactionType, +}; use crate::chain::channelmonitor::{ANTI_REORG_DELAY, ARCHIVAL_DELAY_BLOCKS}; use crate::chain::{self, BestBlock, Confirm, Filter, Listen, WatchedOutput}; use crate::io; @@ -525,66 +527,79 @@ where self.change_destination_source.get_change_destination_script().await?; // Sweep the outputs. - let spending_tx = self - .update_state(|sweeper_state| -> Result<(Option, bool), ()> { - let cur_height = sweeper_state.best_block.height; - let cur_hash = sweeper_state.best_block.block_hash; - - let respend_descriptors_set: HashSet<&SpendableOutputDescriptor> = sweeper_state - .outputs - .iter() - .filter(|o| filter_fn(*o, cur_height)) - .map(|o| &o.descriptor) - .collect(); - - // we first collect into a set to avoid duplicates and to "randomize" the order - // in which outputs are spent. Then we collect into a vec as that is what - // `spend_outputs` requires. - let respend_descriptors: Vec<&SpendableOutputDescriptor> = - respend_descriptors_set.into_iter().collect(); - - // Generate the spending transaction and broadcast it. - if !respend_descriptors.is_empty() { - let spending_tx = self - .spend_outputs( - &sweeper_state, - &respend_descriptors, - change_destination_script, - ) - .map_err(|e| { - log_error!(self.logger, "Error spending outputs: {:?}", e); - })?; - - log_debug!( - self.logger, - "Generating and broadcasting sweeping transaction {}", - spending_tx.compute_txid() - ); - - // As we didn't modify the state so far, the same filter_fn yields the same elements as - // above. - let respend_outputs = - sweeper_state.outputs.iter_mut().filter(|o| filter_fn(&**o, cur_height)); - for output_info in respend_outputs { - if let Some(filter) = self.chain_data_source.as_ref() { - let watched_output = output_info.to_watched_output(cur_hash); - filter.register_output(watched_output); + let spending_tx_and_chan_id = self + .update_state( + |sweeper_state| -> Result<(Option<(Transaction, Vec)>, bool), ()> { + let cur_height = sweeper_state.best_block.height; + let cur_hash = sweeper_state.best_block.block_hash; + + let respend_descriptors_set: HashSet<&SpendableOutputDescriptor> = + sweeper_state + .outputs + .iter() + .filter(|o| filter_fn(*o, cur_height)) + .map(|o| &o.descriptor) + .collect(); + + // we first collect into a set to avoid duplicates and to "randomize" the order + // in which outputs are spent. Then we collect into a vec as that is what + // `spend_outputs` requires. + let respend_descriptors: Vec<&SpendableOutputDescriptor> = + respend_descriptors_set.into_iter().collect(); + + // Generate the spending transaction and broadcast it. + if !respend_descriptors.is_empty() { + let spending_tx = self + .spend_outputs( + &sweeper_state, + &respend_descriptors, + change_destination_script, + ) + .map_err(|e| { + log_error!(self.logger, "Error spending outputs: {:?}", e); + })?; + + log_debug!( + self.logger, + "Generating and broadcasting sweeping transaction {}", + spending_tx.compute_txid() + ); + + // As we didn't modify the state so far, the same filter_fn yields the same elements as + // above. + let respend_outputs = sweeper_state + .outputs + .iter_mut() + .filter(|o| filter_fn(&**o, cur_height)); + let mut channel_ids = Vec::new(); + for output_info in respend_outputs { + if let Some(filter) = self.chain_data_source.as_ref() { + let watched_output = output_info.to_watched_output(cur_hash); + filter.register_output(watched_output); + } + + if let Some(channel_id) = output_info.channel_id { + if !channel_ids.contains(&channel_id) { + channel_ids.push(channel_id); + } + } + + output_info.status.broadcast(cur_hash, cur_height, spending_tx.clone()); + sweeper_state.dirty = true; } - output_info.status.broadcast(cur_hash, cur_height, spending_tx.clone()); - sweeper_state.dirty = true; + Ok((Some((spending_tx, channel_ids)), false)) + } else { + Ok((None, false)) } - - Ok((Some(spending_tx), false)) - } else { - Ok((None, false)) - } - }) + }, + ) .await?; // Persistence completely successfully. If we have a spending transaction, we broadcast it. - if let Some(spending_tx) = spending_tx { - self.broadcaster.broadcast_transactions(&[&spending_tx]); + if let Some((spending_tx, channel_ids)) = spending_tx_and_chan_id { + self.broadcaster + .broadcast_transactions(&[(&spending_tx, TransactionType::Sweep { channel_ids })]); } Ok(()) diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 34f5d5fe36e..f5c73ca4ca3 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -12,9 +12,9 @@ use crate::blinded_path::message::{BlindedMessagePath, MessageForwardNode}; use crate::blinded_path::payment::{BlindedPaymentPath, ReceiveTlvs}; use crate::chain; use crate::chain::chaininterface; -use crate::chain::chaininterface::ConfirmationTarget; #[cfg(any(test, feature = "_externalize_tests"))] use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; +use crate::chain::chaininterface::{ConfirmationTarget, TransactionType}; use crate::chain::chainmonitor::{ChainMonitor, Persist}; use crate::chain::channelmonitor::{ ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, MonitorEvent, @@ -1154,7 +1154,7 @@ impl TestBroadcaster { } impl chaininterface::BroadcasterInterface for TestBroadcaster { - fn broadcast_transactions(&self, txs: &[&Transaction]) { + fn broadcast_transactions(&self, txs: &[(&Transaction, TransactionType)]) { // Assert that any batch of transactions of length greater than 1 is sorted // topologically, and is a `child-with-parents` package as defined in // . @@ -1165,21 +1165,23 @@ impl chaininterface::BroadcasterInterface for TestBroadcaster { // Right now LDK only ever broadcasts packages of length 2. assert!(txs.len() <= 2); if txs.len() == 2 { - let parent_txid = txs[0].compute_txid(); + let parent_txid = txs[0].0.compute_txid(); assert!(txs[1] + .0 .input .iter() .map(|input| input.previous_output.txid) .any(|txid| txid == parent_txid)); - let child_txid = txs[1].compute_txid(); + let child_txid = txs[1].0.compute_txid(); assert!(txs[0] + .0 .input .iter() .map(|input| input.previous_output.txid) .all(|txid| txid != child_txid)); } - for tx in txs { + for (tx, _broadcast_type) in txs { let lock_time = tx.lock_time.to_consensus_u32(); assert!(lock_time < 1_500_000_000); if tx.lock_time.is_block_height() @@ -1195,7 +1197,7 @@ impl chaininterface::BroadcasterInterface for TestBroadcaster { } } } - let owned_txs: Vec = txs.iter().map(|tx| (*tx).clone()).collect(); + let owned_txs: Vec = txs.iter().map(|(tx, _)| (*tx).clone()).collect(); self.txn_broadcasted.lock().unwrap().extend(owned_txs); } } From 32a801ef18bc7d25c7c191d791ec359532bc14a4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 2 Feb 2026 15:00:50 +0100 Subject: [PATCH 2/3] Add test coverage for `TransactionType::Splice` Add parallel `txn_types` vector to `TestBroadcaster` to track `TransactionType` alongside broadcast transactions. Existing `txn_broadcast()` API remains unchanged for backward compatibility. New `txn_broadcast_with_types()` API allows tests to verify transaction types. Also add a `clear()` helper method and update test files to use it instead of directly manipulating `txn_broadcasted`, ensuring the two vectors stay in sync. Update splice tests to use the new API and verify that splice transactions are broadcast with the correct `TransactionType`. Co-Authored-By: HAL 9000 Signed-off-by: Elias Rohrer --- lightning/src/ln/chanmon_update_fail_tests.rs | 1 + lightning/src/ln/channel_open_tests.rs | 2 +- lightning/src/ln/functional_test_utils.rs | 11 ++++---- lightning/src/ln/monitor_tests.rs | 4 +-- lightning/src/ln/payment_tests.rs | 6 ++--- lightning/src/ln/shutdown_tests.rs | 6 ++--- lightning/src/ln/splicing_tests.rs | 17 ++++++++---- lightning/src/util/test_utils.rs | 26 +++++++++++++++++-- 8 files changed, 51 insertions(+), 22 deletions(-) diff --git a/lightning/src/ln/chanmon_update_fail_tests.rs b/lightning/src/ln/chanmon_update_fail_tests.rs index ff499d049d4..f602bbe8d02 100644 --- a/lightning/src/ln/chanmon_update_fail_tests.rs +++ b/lightning/src/ln/chanmon_update_fail_tests.rs @@ -86,6 +86,7 @@ fn test_monitor_and_persister_update_fail() { let persister = test_utils::TestPersister::new(); let tx_broadcaster = TestBroadcaster { txn_broadcasted: Mutex::new(Vec::new()), + txn_types: Mutex::new(Vec::new()), // Because we will connect a block at height 200 below, we need the TestBroadcaster to know // that we are at height 200 so that it doesn't think we're violating the time lock // requirements of transactions broadcasted at that point. diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs index 3a9c266aacd..b7965c4fb66 100644 --- a/lightning/src/ln/channel_open_tests.rs +++ b/lightning/src/ln/channel_open_tests.rs @@ -1699,7 +1699,7 @@ pub fn test_invalid_funding_tx() { assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx); - nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); + nodes[0].tx_broadcaster.clear(); let expected_err = "funding tx had wrong script/value or output index"; confirm_transaction_at(&nodes[1], &tx, 1); diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index cea9ea45428..4739af956cb 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -861,6 +861,7 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { txn_broadcasted: Mutex::new( self.tx_broadcaster.txn_broadcasted.lock().unwrap().clone(), ), + txn_types: Mutex::new(self.tx_broadcaster.txn_types.lock().unwrap().clone()), blocks: Arc::new(Mutex::new(self.tx_broadcaster.blocks.lock().unwrap().clone())), }; @@ -1538,7 +1539,7 @@ pub fn sign_funding_transaction<'a, 'b, 'c>( assert_eq!(node_a.tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); assert_eq!(node_a.tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx); - node_a.tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); + node_a.tx_broadcaster.clear(); // Ensure that funding_transaction_generated is idempotent. assert!(node_a @@ -1641,10 +1642,8 @@ pub fn open_zero_conf_channel_with_value<'a, 'b, 'c, 'd>( check_added_monitors(&initiator, 1); assert_eq!(initiator.tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); - assert_eq!( - initiator.tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0)[0], - tx - ); + assert_eq!(initiator.tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx); + initiator.tx_broadcaster.clear(); as_channel_ready = get_event_msg!(initiator, MessageSendEvent::SendChannelReady, receiver_node_id); @@ -2014,7 +2013,7 @@ pub fn create_unannounced_chan_between_nodes_with_value<'a, 'b, 'c, 'd>( assert_eq!(nodes[a].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); assert_eq!(nodes[a].tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx); - nodes[a].tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); + nodes[a].tx_broadcaster.clear(); let conf_height = core::cmp::max(nodes[a].best_block_info().1 + 1, nodes[b].best_block_info().1 + 1); diff --git a/lightning/src/ln/monitor_tests.rs b/lightning/src/ln/monitor_tests.rs index 04915affa20..5caff13cf71 100644 --- a/lightning/src/ln/monitor_tests.rs +++ b/lightning/src/ln/monitor_tests.rs @@ -1208,7 +1208,7 @@ fn test_no_preimage_inbound_htlc_balances() { }, a_received_htlc_balance.clone(), a_sent_htlc_balance.clone()]); mine_transaction(&nodes[0], &as_txn[0]); - nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); + nodes[0].tx_broadcaster.clear(); check_closed_broadcast!(nodes[0], true); check_added_monitors(&nodes[0], 1); check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[1].node.get_our_node_id()], 1000000); @@ -1255,7 +1255,7 @@ fn test_no_preimage_inbound_htlc_balances() { bs_pre_spend_claims.retain(|e| if let Balance::ClaimableAwaitingConfirmations { .. } = e { false } else { true }); // The next few blocks for B look the same as for A, though for the opposite HTLC - nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); + nodes[1].tx_broadcaster.clear(); connect_blocks(&nodes[1], TEST_FINAL_CLTV - (ANTI_REORG_DELAY - 1)); expect_htlc_failure_conditions(nodes[1].node.get_and_clear_pending_events(), &[HTLCHandlingFailureType::Receive { payment_hash: to_b_failed_payment_hash }]); nodes[1].node.process_pending_htlc_forwards(); diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index e41e60a46a7..c44f3d107ff 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -838,7 +838,7 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) { let as_commitment_tx = get_local_commitment_txn!(nodes[0], chan_id)[0].clone(); if confirm_before_reload { mine_transaction(&nodes[0], &as_commitment_tx); - nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); + nodes[0].tx_broadcaster.clear(); } // The ChannelMonitor should always be the latest version, as we're required to persist it @@ -893,7 +893,7 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) { &node_b_id)) }, &[node_a_id], 100000); check_added_monitors(&nodes[1], 1); assert_eq!(nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); - nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); + nodes[1].tx_broadcaster.clear(); }, _ => panic!("Unexpected event"), } @@ -954,7 +954,7 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) { } else { confirm_transaction(&nodes[0], &first_htlc_timeout_tx); } - nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); + nodes[0].tx_broadcaster.clear(); let conditions = PaymentFailedConditions::new().from_mon_update(); expect_payment_failed_conditions(&nodes[0], payment_hash, false, conditions); diff --git a/lightning/src/ln/shutdown_tests.rs b/lightning/src/ln/shutdown_tests.rs index 192bc6399e4..66f8df69dce 100644 --- a/lightning/src/ln/shutdown_tests.rs +++ b/lightning/src/ln/shutdown_tests.rs @@ -495,7 +495,7 @@ fn updates_shutdown_wait() { assert!(nodes[0].node.list_channels().is_empty()); assert_eq!(nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); - nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); + nodes[1].tx_broadcaster.clear(); close_channel(&nodes[1], &nodes[2], &chan_2.2, chan_2.3, true); assert!(nodes[1].node.list_channels().is_empty()); @@ -625,7 +625,7 @@ fn do_htlc_fail_async_shutdown(blinded_recipient: bool) { assert!(nodes[0].node.list_channels().is_empty()); assert_eq!(nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); - nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); + nodes[1].tx_broadcaster.clear(); close_channel(&nodes[1], &nodes[2], &chan_2.2, chan_2.3, true); assert!(nodes[1].node.list_channels().is_empty()); assert!(nodes[2].node.list_channels().is_empty()); @@ -842,7 +842,7 @@ fn do_test_shutdown_rebroadcast(recv_count: u8) { assert!(nodes[0].node.list_channels().is_empty()); assert_eq!(nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); - nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); + nodes[1].tx_broadcaster.clear(); close_channel(&nodes[1], &nodes[2], &chan_2.2, chan_2.3, true); assert!(nodes[1].node.list_channels().is_empty()); diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index ef524db6be3..05303cfa0b7 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -9,7 +9,7 @@ #![cfg_attr(not(test), allow(unused_imports))] -use crate::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; +use crate::chain::chaininterface::{TransactionType, FEERATE_FLOOR_SATS_PER_KW}; use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS}; use crate::chain::transaction::OutPoint; use crate::chain::ChannelMonitorUpdateStatus; @@ -333,11 +333,18 @@ pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>( check_added_monitors(&acceptor, 1); let tx = { - let mut initiator_txn = initiator.tx_broadcaster.txn_broadcast(); + let mut initiator_txn = initiator.tx_broadcaster.txn_broadcast_with_types(); assert_eq!(initiator_txn.len(), 1); - let acceptor_txn = acceptor.tx_broadcaster.txn_broadcast(); - assert_eq!(initiator_txn, acceptor_txn,); - initiator_txn.remove(0) + let acceptor_txn = acceptor.tx_broadcaster.txn_broadcast_with_types(); + assert_eq!(initiator_txn, acceptor_txn); + let (tx, tx_type) = initiator_txn.remove(0); + // Verify transaction type is Splice + assert!( + matches!(tx_type, TransactionType::Splice { .. }), + "Expected TransactionType::Splice, got {:?}", + tx_type + ); + tx }; (tx, splice_locked) } diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index f5c73ca4ca3..18d003c7993 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -1126,31 +1126,50 @@ unsafe impl Send for TestStore {} pub struct TestBroadcaster { pub txn_broadcasted: Mutex>, + pub txn_types: Mutex>, pub blocks: Arc>>, } impl TestBroadcaster { pub fn new(network: Network) -> Self { let txn_broadcasted = Mutex::new(Vec::new()); + let txn_types = Mutex::new(Vec::new()); let blocks = Arc::new(Mutex::new(vec![(genesis_block(network), 0)])); - Self { txn_broadcasted, blocks } + Self { txn_broadcasted, txn_types, blocks } } pub fn with_blocks(blocks: Arc>>) -> Self { let txn_broadcasted = Mutex::new(Vec::new()); - Self { txn_broadcasted, blocks } + let txn_types = Mutex::new(Vec::new()); + Self { txn_broadcasted, txn_types, blocks } } pub fn txn_broadcast(&self) -> Vec { + self.txn_types.lock().unwrap().clear(); self.txn_broadcasted.lock().unwrap().split_off(0) } pub fn unique_txn_broadcast(&self) -> Vec { let mut txn = self.txn_broadcasted.lock().unwrap().split_off(0); + self.txn_types.lock().unwrap().clear(); let mut seen = new_hash_set(); txn.retain(|tx| seen.insert(tx.compute_txid())); txn } + + /// Returns all broadcast transactions with their types, clearing both internal lists. + pub fn txn_broadcast_with_types(&self) -> Vec<(Transaction, TransactionType)> { + let txn = self.txn_broadcasted.lock().unwrap().split_off(0); + let types = self.txn_types.lock().unwrap().split_off(0); + assert_eq!(txn.len(), types.len(), "Transaction and type vectors out of sync"); + txn.into_iter().zip(types.into_iter()).collect() + } + + /// Clears both the transaction and type vectors. + pub fn clear(&self) { + self.txn_broadcasted.lock().unwrap().clear(); + self.txn_types.lock().unwrap().clear(); + } } impl chaininterface::BroadcasterInterface for TestBroadcaster { @@ -1198,7 +1217,10 @@ impl chaininterface::BroadcasterInterface for TestBroadcaster { } } let owned_txs: Vec = txs.iter().map(|(tx, _)| (*tx).clone()).collect(); + let owned_types: Vec = + txs.iter().map(|(_, tx_type)| tx_type.clone()).collect(); self.txn_broadcasted.lock().unwrap().extend(owned_txs); + self.txn_types.lock().unwrap().extend(owned_types); } } From 0715f4aa58733b29fa062674eda240f1a1fe73da Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 2 Feb 2026 13:14:33 +0100 Subject: [PATCH 3/3] Drop unused imports Co-Authored-By: HAL 9000 --- lightning/src/chain/mod.rs | 1 - lightning/src/util/sweep.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lightning/src/chain/mod.rs b/lightning/src/chain/mod.rs index e8baa7aad1f..bc47f1b1db6 100644 --- a/lightning/src/chain/mod.rs +++ b/lightning/src/chain/mod.rs @@ -20,7 +20,6 @@ use bitcoin::secp256k1::PublicKey; use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, MonitorEvent}; use crate::chain::transaction::{OutPoint, TransactionData}; -use crate::impl_writeable_tlv_based; use crate::ln::types::ChannelId; use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::HTLCDescriptor; diff --git a/lightning/src/util/sweep.rs b/lightning/src/util/sweep.rs index f0c2d6d2d15..6815b395d91 100644 --- a/lightning/src/util/sweep.rs +++ b/lightning/src/util/sweep.rs @@ -28,7 +28,7 @@ use crate::util::persist::{ OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE, }; use crate::util::ser::{Readable, ReadableArgs, Writeable}; -use crate::{impl_writeable_tlv_based, log_debug, log_error}; +use crate::{log_debug, log_error}; use bitcoin::block::Header; use bitcoin::locktime::absolute::LockTime;