Skip to content

Commit cf892af

Browse files
jkczyzclaude
andcommitted
f - Generalize Splice variant into InteractiveFunding
Rename TransactionType::Splice to TransactionType::InteractiveFunding and reshape it as Vec<FundingCandidate> (original + RBF attempts) where each candidate carries one or more ChannelFunding entries (forward- compatible with batched splices and V2 channel establishment, neither of which is implemented yet). Per-candidate contribution is tail-aligned with negotiated_candidates so leading rounds where we didn't contribute yield None. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0453078 commit cf892af

4 files changed

Lines changed: 104 additions & 55 deletions

File tree

lightning/src/chain/chaininterface.rs

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,55 @@ pub enum TransactionType {
106106
/// A single sweep transaction may aggregate outputs from multiple channels.
107107
channels: Vec<(PublicKey, ChannelId)>,
108108
},
109-
/// A splice transaction modifying an existing channel's funding.
109+
/// An interactively-negotiated funding transaction.
110110
///
111-
/// 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.
114+
///
115+
/// The `Vec` contains every negotiated candidate for this funding in order: the original
116+
/// negotiation followed by any RBF replacements. The last entry is the candidate being
117+
/// broadcast.
112118
///
113119
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
114-
Splice {
115-
/// The `node_id` of the channel counterparty.
116-
counterparty_node_id: PublicKey,
117-
/// The ID of the channel being spliced.
118-
channel_id: ChannelId,
119-
/// The local node's contribution to this splice/RBF round, or `None` if we did not
120-
/// contribute (e.g., a pure acceptor with zero value added).
121-
contribution: Option<FundingContribution>,
122-
/// For an RBF replacement, the txid of the prior negotiated splice candidate being
123-
/// replaced. `None` for the first splice attempt.
124-
replaced_txid: Option<Txid>,
125-
},
120+
InteractiveFunding(Vec<FundingCandidate>),
121+
}
122+
123+
/// A single negotiated candidate within an [`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, 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, 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, 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,
126158
}
127159

128160
// TODO: Define typed abstraction over feerates to handle their conversions.

lightning/src/ln/channel.rs

Lines changed: 28 additions & 12 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,
@@ -9394,17 +9395,32 @@ where
93949395
);
93959396
}
93969397

9397-
let replaced_txid =
9398-
pending_splice.negotiated_candidates.len().checked_sub(2).and_then(|idx| {
9399-
pending_splice.negotiated_candidates[idx].get_funding_txid()
9400-
});
9401-
let contribution = pending_splice.contributions.last().cloned();
9402-
let tx_type = TransactionType::Splice {
9403-
counterparty_node_id: self.context.counterparty_node_id,
9404-
channel_id: self.context.channel_id,
9405-
contribution,
9406-
replaced_txid,
9407-
};
9398+
let contrib_offset = pending_splice
9399+
.negotiated_candidates
9400+
.len()
9401+
.saturating_sub(pending_splice.contributions.len());
9402+
let candidates = pending_splice
9403+
.negotiated_candidates
9404+
.iter()
9405+
.enumerate()
9406+
.filter_map(|(i, funding)| {
9407+
let txid = funding.get_funding_txid()?;
9408+
let contribution = i
9409+
.checked_sub(contrib_offset)
9410+
.and_then(|j| pending_splice.contributions.get(j))
9411+
.cloned();
9412+
Some(FundingCandidate {
9413+
txid,
9414+
channels: vec![ChannelFunding {
9415+
counterparty_node_id: self.context.counterparty_node_id,
9416+
channel_id: self.context.channel_id,
9417+
purpose: FundingPurpose::Splice,
9418+
contribution,
9419+
}],
9420+
})
9421+
})
9422+
.collect();
9423+
let tx_type = TransactionType::InteractiveFunding(candidates);
94089424
funding_tx_signed.funding_tx = Some((funding_tx, tx_type));
94099425
funding_tx_signed.splice_negotiated = Some(splice_negotiated);
94109426
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
@@ -11136,7 +11136,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1113611136
} else if let Some((splice_tx, tx_type)) = funding_tx_signed
1113711137
.as_mut()
1113811138
.and_then(|v| v.funding_tx.take())
11139-
.filter(|(_, tx_type)| matches!(tx_type, TransactionType::Splice { .. }))
11139+
.filter(|(_, tx_type)| matches!(tx_type, TransactionType::InteractiveFunding(..)))
1114011140
{
1114111141
log_info!(logger, "Broadcasting signed splice transaction with txid {}", splice_tx.compute_txid());
1114211142
self.tx_broadcaster.broadcast_transactions(&[(&splice_tx, tx_type)]);

lightning/src/ln/splicing_tests.rs

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
#![cfg_attr(not(test), allow(unused_imports))]
1111

12-
use crate::chain::chaininterface::{TransactionType, FEERATE_FLOOR_SATS_PER_KW};
12+
use crate::chain::chaininterface::{FundingPurpose, TransactionType, FEERATE_FLOOR_SATS_PER_KW};
1313
use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};
1414
use crate::chain::transaction::OutPoint;
1515
use crate::chain::ChannelMonitorUpdateStatus;
@@ -503,9 +503,9 @@ pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>(
503503
)
504504
}
505505

506-
/// `expected_replaced_txid` is the expected value of `TransactionType::Splice.replaced_txid` on
507-
/// the resulting broadcast: `None` for a first splice attempt; `Some(txid)` for an RBF replacing
508-
/// that prior negotiated candidate.
506+
/// `expected_replaced_txid` is the expected txid of the prior negotiated candidate in the
507+
/// `TransactionType::InteractiveFunding` broadcast: `None` for a first splice attempt; `Some(txid)`
508+
/// for an RBF replacing that prior negotiated candidate.
509509
pub fn sign_interactive_funding_tx_with_acceptor_contribution<'a, 'b, 'c, 'd>(
510510
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, is_0conf: bool,
511511
acceptor_has_contribution: bool, expected_replaced_txid: Option<Txid>,
@@ -608,28 +608,29 @@ pub fn sign_interactive_funding_tx_with_acceptor_contribution<'a, 'b, 'c, 'd>(
608608
assert_eq!(initiator_txn[0].0, acceptor_txn[0].0);
609609
let (tx, initiator_tx_type) = initiator_txn.remove(0);
610610
let (_, acceptor_tx_type) = acceptor_txn.remove(0);
611-
// Verify transaction types are Splice for both nodes. The initiator always contributes;
612-
// the acceptor contributes iff the flag says so. Both parties must observe the same
613-
// `replaced_txid` as the caller declares.
614-
if let TransactionType::Splice { contribution, replaced_txid, .. } = &initiator_tx_type {
615-
assert!(
616-
contribution.is_some(),
617-
"Initiator always contributes; expected Some, got None"
618-
);
619-
assert_eq!(*replaced_txid, expected_replaced_txid, "initiator replaced_txid mismatch");
620-
} else {
621-
panic!("Expected TransactionType::Splice, got {:?}", initiator_tx_type);
622-
}
623-
if let TransactionType::Splice { contribution, replaced_txid, .. } = &acceptor_tx_type {
624-
assert_eq!(
625-
contribution.is_some(),
626-
acceptor_has_contribution,
627-
"Acceptor contribution presence must match `acceptor_has_contribution`",
628-
);
629-
assert_eq!(*replaced_txid, expected_replaced_txid, "acceptor replaced_txid mismatch");
630-
} else {
631-
panic!("Expected TransactionType::Splice, got {:?}", acceptor_tx_type);
632-
}
611+
// Verify transaction types are InteractiveFunding for both nodes. The initiator always
612+
// contributes; the acceptor contributes iff the flag says so. Both parties must observe
613+
// the same prior candidate txid as the caller declares.
614+
let assert_broadcast =
615+
|label: &str, tx_type: &TransactionType, contribution_expected: bool| {
616+
let candidates = match tx_type {
617+
TransactionType::InteractiveFunding(candidates) => candidates,
618+
other => panic!("Expected TransactionType::InteractiveFunding, got {other:?}"),
619+
};
620+
let last = candidates.last().expect("at least one candidate");
621+
assert_eq!(last.txid, tx.compute_txid(), "{label} last candidate txid mismatch");
622+
let last_channel = last.channels.first().expect("at least one channel");
623+
assert!(matches!(last_channel.purpose, FundingPurpose::Splice));
624+
assert_eq!(
625+
last_channel.contribution.is_some(),
626+
contribution_expected,
627+
"{label} contribution presence mismatch",
628+
);
629+
let prior_txid = candidates.len().checked_sub(2).map(|i| candidates[i].txid);
630+
assert_eq!(prior_txid, expected_replaced_txid, "{label} replaced_txid mismatch");
631+
};
632+
assert_broadcast("initiator", &initiator_tx_type, true);
633+
assert_broadcast("acceptor", &acceptor_tx_type, acceptor_has_contribution);
633634
tx
634635
};
635636
(tx, splice_locked)
@@ -4370,8 +4371,8 @@ fn test_splice_rbf_acceptor_basic() {
43704371
new_funding_script.clone(),
43714372
);
43724373

4373-
// Step 10: Sign and broadcast. The broadcast's `TransactionType::Splice.replaced_txid` must
4374-
// point at the first splice tx it is replacing.
4374+
// Step 10: Sign and broadcast. The prior candidate in the broadcast's
4375+
// `TransactionType::InteractiveFunding` must point at the first splice tx it is replacing.
43754376
let (rbf_tx, splice_locked) = sign_interactive_funding_tx(
43764377
&nodes[0],
43774378
&nodes[1],

0 commit comments

Comments
 (0)