Skip to content

Commit 38f5651

Browse files
authored
Merge pull request lightningdevkit#4570 from jkczyz/2026-04-splice-transaction-type
Include interactive funding candidates on broadcast
2 parents 416dfad + 1d36f7b commit 38f5651

6 files changed

Lines changed: 246 additions & 77 deletions

File tree

lightning/src/chain/chaininterface.rs

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
1616
use core::{cmp, ops::Deref};
1717

18+
use crate::ln::funding::FundingContribution;
1819
use crate::ln::types::ChannelId;
1920
use crate::prelude::*;
2021

22+
use bitcoin::hash_types::Txid;
2123
use bitcoin::secp256k1::PublicKey;
2224
use bitcoin::transaction::Transaction;
2325

@@ -104,19 +106,76 @@ pub enum TransactionType {
104106
/// A single sweep transaction may aggregate outputs from multiple channels.
105107
channels: Vec<(PublicKey, ChannelId)>,
106108
},
107-
/// A splice transaction modifying an existing channel's funding.
109+
/// An interactively-negotiated funding transaction.
108110
///
109-
/// A transaction of this type will be broadcast as a result of a [`ChannelManager::splice_channel`] operation.
111+
/// A transaction of this type will be broadcast as a result of a
112+
/// [`ChannelManager::splice_channel`] operation, or (once supported) V2 (dual-funded) channel
113+
/// establishment. The same variant is used for batches of either or both.
110114
///
111115
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
112-
Splice {
113-
/// The `node_id` of the channel counterparty.
114-
counterparty_node_id: PublicKey,
115-
/// The ID of the channel being spliced.
116-
channel_id: ChannelId,
116+
InteractiveFunding {
117+
/// Every negotiated candidate for this funding in order: the original negotiation
118+
/// followed by any RBF replacements. The last entry is the candidate being broadcast.
119+
candidates: Vec<FundingCandidate>,
117120
},
118121
}
119122

123+
/// A single negotiated candidate within a [`TransactionType::InteractiveFunding`] broadcast.
124+
///
125+
/// The candidate is identified by its [`Txid`] and lists the channels participating in it. A
126+
/// single candidate funds more than one channel only when batching splices and/or V2 channel
127+
/// openings (not yet implemented).
128+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
129+
pub struct FundingCandidate {
130+
/// The txid of this candidate.
131+
pub txid: Txid,
132+
/// The channels participating in this candidate.
133+
pub channels: Vec<ChannelFunding>,
134+
}
135+
136+
/// Information about a single channel's participation in a [`FundingCandidate`].
137+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
138+
pub struct ChannelFunding {
139+
/// The `node_id` of the channel counterparty.
140+
pub counterparty_node_id: PublicKey,
141+
/// The ID of the channel.
142+
pub channel_id: ChannelId,
143+
/// Whether this channel is being newly established or is an existing channel being spliced.
144+
pub purpose: FundingPurpose,
145+
/// The local node's contribution to this channel in this candidate, or `None` if we did
146+
/// not contribute (e.g., a pure acceptor with zero value added, or a leading RBF round
147+
/// before we began contributing).
148+
pub contribution: Option<FundingContribution>,
149+
}
150+
151+
/// The role of a channel within a [`FundingCandidate`].
152+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
153+
pub enum FundingPurpose {
154+
/// The channel is being newly established (V2 dual-funded open).
155+
Establishment,
156+
/// An existing channel is being spliced.
157+
Splice,
158+
}
159+
160+
// Needed so downstream consumers can persist these without needing to define wrapper types
161+
// mirroring the type structure.
162+
impl_writeable_tlv_based!(FundingCandidate, {
163+
(1, txid, required),
164+
(3, channels, required_vec),
165+
});
166+
167+
impl_writeable_tlv_based!(ChannelFunding, {
168+
(1, counterparty_node_id, required),
169+
(3, channel_id, required),
170+
(5, purpose, required),
171+
(7, contribution, option),
172+
});
173+
174+
impl_writeable_tlv_based_enum!(FundingPurpose,
175+
(0, Establishment) => {},
176+
(2, Splice) => {},
177+
);
178+
120179
// TODO: Define typed abstraction over feerates to handle their conversions.
121180
pub(crate) fn compute_feerate_sat_per_1000_weight(fee_sat: u64, weight: u64) -> u32 {
122181
(fee_sat * 1000 / weight).try_into().unwrap_or(u32::max_value())

lightning/src/ln/channel.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ use bitcoin::{secp256k1, sighash, FeeRate, Sequence, TxIn};
2828

2929
use crate::blinded_path::message::BlindedMessagePath;
3030
use crate::chain::chaininterface::{
31-
ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, TransactionType,
31+
ChannelFunding, ConfirmationTarget, FeeEstimator, FundingCandidate, FundingPurpose,
32+
LowerBoundedFeeEstimator, TransactionType,
3233
};
3334
use crate::chain::channelmonitor::{
3435
ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, CommitmentHTLCData,
@@ -9415,10 +9416,34 @@ where
94159416
);
94169417
}
94179418

9418-
let tx_type = TransactionType::Splice {
9419-
counterparty_node_id: self.context.counterparty_node_id,
9420-
channel_id: self.context.channel_id,
9421-
};
9419+
let contrib_offset = pending_splice
9420+
.negotiated_candidates
9421+
.len()
9422+
.saturating_sub(pending_splice.contributions.len());
9423+
let candidates = pending_splice
9424+
.negotiated_candidates
9425+
.iter()
9426+
.enumerate()
9427+
.map(|(i, funding)| {
9428+
let txid = funding
9429+
.get_funding_txid()
9430+
.expect("negotiated candidates should have a funding txid");
9431+
let contribution = i
9432+
.checked_sub(contrib_offset)
9433+
.and_then(|j| pending_splice.contributions.get(j))
9434+
.cloned();
9435+
FundingCandidate {
9436+
txid,
9437+
channels: vec![ChannelFunding {
9438+
counterparty_node_id: self.context.counterparty_node_id,
9439+
channel_id: self.context.channel_id,
9440+
purpose: FundingPurpose::Splice,
9441+
contribution,
9442+
}],
9443+
}
9444+
})
9445+
.collect();
9446+
let tx_type = TransactionType::InteractiveFunding { candidates };
94229447
funding_tx_signed.funding_tx = Some((funding_tx, tx_type));
94239448
funding_tx_signed.splice_negotiated = Some(splice_negotiated);
94249449
funding_tx_signed.splice_locked = splice_locked;

lightning/src/ln/channelmanager.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11126,7 +11126,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1112611126
} else if let Some((splice_tx, tx_type)) = funding_tx_signed
1112711127
.as_mut()
1112811128
.and_then(|v| v.funding_tx.take())
11129-
.filter(|(_, tx_type)| matches!(tx_type, TransactionType::Splice { .. }))
11129+
.filter(|(_, tx_type)| matches!(tx_type, TransactionType::InteractiveFunding { .. }))
1113011130
{
1113111131
log_info!(logger, "Broadcasting signed splice transaction with txid {}", splice_tx.compute_txid());
1113211132
self.tx_broadcaster.broadcast_transactions(&[(&splice_tx, tx_type)]);

lightning/src/ln/funding.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ enum FundingInputs {
539539
}
540540

541541
/// The components of a funding transaction contributed by one party.
542-
#[derive(Debug, Clone, PartialEq, Eq)]
542+
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
543543
pub struct FundingContribution {
544544
/// The estimate fees responsible to be paid for the contribution.
545545
estimated_fee: Amount,
@@ -578,10 +578,6 @@ impl_writeable_tlv_based!(FundingContribution, {
578578
});
579579

580580
impl FundingContribution {
581-
pub(super) fn feerate(&self) -> FeeRate {
582-
self.feerate
583-
}
584-
585581
pub(super) fn is_splice(&self) -> bool {
586582
self.is_splice
587583
}
@@ -610,6 +606,16 @@ impl FundingContribution {
610606
.unwrap_or(Amount::ZERO)
611607
}
612608

609+
/// Returns the estimated on-chain fee this contribution is responsible for paying.
610+
pub fn estimated_fee(&self) -> Amount {
611+
self.estimated_fee
612+
}
613+
614+
/// Returns the inputs included in this contribution.
615+
pub fn inputs(&self) -> &[FundingTxInput] {
616+
&self.inputs
617+
}
618+
613619
/// Returns the outputs (e.g., withdrawal destinations) included in this contribution.
614620
///
615621
/// This does not include the change output; see [`FundingContribution::change_output`].
@@ -625,6 +631,17 @@ impl FundingContribution {
625631
self.change_output.as_ref()
626632
}
627633

634+
/// Returns the fee rate used to select `inputs` (the minimum feerate).
635+
pub fn feerate(&self) -> FeeRate {
636+
self.feerate
637+
}
638+
639+
/// Returns the maximum fee rate this contribution will accept as acceptor before rejecting
640+
/// the splice.
641+
pub fn max_feerate(&self) -> FeeRate {
642+
self.max_feerate
643+
}
644+
628645
/// Tries to satisfy a new request using only this contribution's existing inputs.
629646
///
630647
/// For input-backed contributions, this reuses the current inputs, adjusts the explicit

0 commit comments

Comments
 (0)