Refactor `BroadcasterInterface` to include `TransactionType`
What changed, and why it matters
This commit is a straightforward API refactor: it adds a TransactionType label (Funding, Close, Claim, Sweep, etc.) to every place in the Lightning Dev Kit that broadcasts a Bitcoin transaction. The actual transactions being broadcast do not change; only the metadata passed alongside them changes. There is no security vulnerability being fixed or introduced here.
No security action required. Treat as a normal API-breaking change: downstream implementers of BroadcasterInterface must update their method signature to accept the new TransactionType tuple.
Security signals we found
API-only refactor with no change to transaction construction, signing, or validation
All broadcast call sites updated consistently to include TransactionType metadata
No new cryptographic operations, no new network inputs, no new permissions
No mention of security fixes, CVEs, or vulnerability disclosures in commit message
Evidence from the diff
The change introduces a TransactionType enum in chaininterface.rs and updates BroadcasterInterface::broadcast_transactions to accept &[(&Transaction, TransactionType)] instead of &[&Transaction]. All call sites (channel funding, cooperative closes, force closes, anchor bumps, claim transactions, sweeps, and splice operations) are updated to supply the appropriate variant. The commit also threads ChannelId into OnchainTxHandler so that Claim broadcasts can be tagged. No transaction logic, signing, or validation behavior is modified.
Changed components
lightning/src/chain/chaininterface.rslightning/src/chain/channelmonitor.rslightning/src/chain/onchaintx.rslightning/src/events/bump_transaction/mod.rslightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/util/sweep.rslightning-liquidity/src/lsps2/service.rsInspect captured patch +305 / −110
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index f87af5c..f363da5 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<Vec<Transaction>>,
}
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 e73db74..0b0f097 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<Vec<Transaction>>,
}
impl BroadcasterInterface for TestBroadcaster {
- fn broadcast_transactions(&self, txs: &[&Transaction]) {
- let owned_txs: Vec<Transaction> = txs.iter().map(|tx| (*tx).clone()).collect();
+ fn broadcast_transactions(&self, txs: &[(&Transaction, TransactionType)]) {
+ let owned_txs: Vec<Transaction> = 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 00f68af..4c688d3 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 7e71d96..758fd1a 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<ChannelId>,
+ },
+ /// 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<ChannelId>,
+ },
+ /// 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:
/// <https://github.com/bitcoin/bitcoin/blob/master/doc/policy/packages.md>
- 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<T: BroadcasterInterface + ?Sized, B: Deref<Target = T>> 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 80d0ef1..2d2fdd1 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -1879,8 +1879,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
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<SP::EcdsaSigner> = ReadableArgs::read(
+ let mut onchain_tx_handler: OnchainTxHandler<SP::EcdsaSigner> = 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 cfee63b..8de99eb 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<ChannelSigner: EcdsaChannelSigner> {
+ 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<ChannelSigner: EcdsaChannelSigner> PartialEq for OnchainTxHandler<ChannelSi
#[rustfmt::skip]
fn eq(&self, other: &Self) -> 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<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
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<Vec<Option<(usize, Signature)>>> = Readable::read(reader)?;
- let channel_parameters = ReadableArgs::<Option<u64>>::read(reader, Some(channel_value_satoshis))?;
+ let channel_parameters: ChannelTransactionParameters = ReadableArgs::<Option<u64>>::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<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
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<secp256k1::All>,
) -> Self {
OnchainTxHandler {
+ channel_id,
channel_value_satoshis,
channel_keys_id,
destination_script,
@@ -511,7 +533,7 @@ impl<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
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<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
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<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
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<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
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 1b3496c..6b6581e 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 042b388..a47241d 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<msgs::TxSignatures>,
- /// The fully-signed funding transaction to be broadcast.
- pub funding_tx: Option<Transaction>,
+ /// 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<SpliceFundingNegotiated>,
@@ -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 129a4d5..775d1ea 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<SP>, funding_tx: &Transaction, logger: &L,
+ &self, channel: &mut FundedChannel<SP>, funding_tx: &Transaction,
+ transaction_type: Option<TransactionType>, 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 2aef218..f0c2d6d 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<Transaction>, 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<ChannelId>)>, 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 34f5d5f..f5c73ca 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
// <https://github.com/bitcoin/bitcoin/blob/master/doc/policy/packages.md>.
@@ -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<Transaction> = txs.iter().map(|tx| (*tx).clone()).collect();
+ let owned_txs: Vec<Transaction> = txs.iter().map(|(tx, _)| (*tx).clone()).collect();
self.txn_broadcasted.lock().unwrap().extend(owned_txs);
}
}
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.