Skip to content

Commit 5f73b70

Browse files
authored
Merge pull request #3979 from jkczyz/2025-07-splice-out
Add splice-out support
2 parents 6f78d57 + ce203f2 commit 5f73b70

File tree

10 files changed

+724
-383
lines changed

10 files changed

+724
-383
lines changed

lightning/src/ln/channel.rs

Lines changed: 353 additions & 180 deletions
Large diffs are not rendered by default.

lightning/src/ln/channelmanager.rs

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ use bitcoin::hashes::{Hash, HashEngine, HmacEngine};
3030

3131
use bitcoin::secp256k1::Secp256k1;
3232
use bitcoin::secp256k1::{PublicKey, SecretKey};
33-
use bitcoin::{secp256k1, Sequence};
34-
#[cfg(splicing)]
35-
use bitcoin::{ScriptBuf, TxIn, Weight};
33+
use bitcoin::{secp256k1, Sequence, SignedAmount};
3634

3735
use crate::blinded_path::message::{
3836
AsyncPaymentsContext, BlindedMessagePath, MessageForwardNode, OffersContext,
@@ -66,6 +64,8 @@ use crate::ln::channel::{
6664
UpdateFulfillCommitFetch, WithChannelContext,
6765
};
6866
use crate::ln::channel_state::ChannelDetails;
67+
#[cfg(splicing)]
68+
use crate::ln::funding::SpliceContribution;
6969
use crate::ln::inbound_payment;
7070
use crate::ln::interactivetxs::{HandleTxCompleteResult, InteractiveTxMessageSendResult};
7171
use crate::ln::msgs;
@@ -4452,14 +4452,13 @@ where
44524452
#[cfg(splicing)]
44534453
#[rustfmt::skip]
44544454
pub fn splice_channel(
4455-
&self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, our_funding_contribution_satoshis: i64,
4456-
our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, change_script: Option<ScriptBuf>,
4457-
funding_feerate_per_kw: u32, locktime: Option<u32>,
4455+
&self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
4456+
contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option<u32>,
44584457
) -> Result<(), APIError> {
44594458
let mut res = Ok(());
44604459
PersistenceNotifierGuard::optionally_notify(self, || {
44614460
let result = self.internal_splice_channel(
4462-
channel_id, counterparty_node_id, our_funding_contribution_satoshis, our_funding_inputs, change_script, funding_feerate_per_kw, locktime
4461+
channel_id, counterparty_node_id, contribution, funding_feerate_per_kw, locktime
44634462
);
44644463
res = result;
44654464
match res {
@@ -4474,9 +4473,7 @@ where
44744473
#[cfg(splicing)]
44754474
fn internal_splice_channel(
44764475
&self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
4477-
our_funding_contribution_satoshis: i64,
4478-
our_funding_inputs: Vec<(TxIn, Transaction, Weight)>, change_script: Option<ScriptBuf>,
4479-
funding_feerate_per_kw: u32, locktime: Option<u32>,
4476+
contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option<u32>,
44804477
) -> Result<(), APIError> {
44814478
let per_peer_state = self.per_peer_state.read().unwrap();
44824479

@@ -4497,13 +4494,8 @@ where
44974494
hash_map::Entry::Occupied(mut chan_phase_entry) => {
44984495
let locktime = locktime.unwrap_or_else(|| self.current_best_block().height);
44994496
if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() {
4500-
let msg = chan.splice_channel(
4501-
our_funding_contribution_satoshis,
4502-
our_funding_inputs,
4503-
change_script,
4504-
funding_feerate_per_kw,
4505-
locktime,
4506-
)?;
4497+
let msg =
4498+
chan.splice_channel(contribution, funding_feerate_per_kw, locktime)?;
45074499
peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceInit {
45084500
node_id: *counterparty_node_id,
45094501
msg,
@@ -9395,7 +9387,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
93959387

93969388
// Inbound V2 channels with contributed inputs are not considered unfunded.
93979389
if let Some(unfunded_chan) = chan.as_unfunded_v2() {
9398-
if unfunded_chan.funding_negotiation_context.our_funding_contribution_satoshis > 0 {
9390+
if unfunded_chan.funding_negotiation_context.our_funding_contribution > SignedAmount::ZERO {
93999391
continue;
94009392
}
94019393
}

lightning/src/ln/dual_funding_tests.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ use {
1919
crate::ln::channel::PendingV2Channel,
2020
crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint, RevocationBasepoint},
2121
crate::ln::functional_test_utils::*,
22+
crate::ln::funding::FundingTxInput,
2223
crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent},
2324
crate::ln::msgs::{CommitmentSigned, TxAddInput, TxAddOutput, TxComplete, TxSignatures},
2425
crate::ln::types::ChannelId,
2526
crate::prelude::*,
26-
crate::util::ser::TransactionU16LenLimited,
2727
crate::util::test_utils,
2828
bitcoin::Witness,
2929
};
@@ -49,10 +49,7 @@ fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession)
4949
let initiator_funding_inputs: Vec<_> = create_dual_funding_utxos_with_prev_txs(
5050
&nodes[0],
5151
&[session.initiator_input_value_satoshis],
52-
)
53-
.into_iter()
54-
.map(|(txin, tx, _)| (txin, TransactionU16LenLimited::new(tx).unwrap()))
55-
.collect();
52+
);
5653

5754
// Alice creates a dual-funded channel as initiator.
5855
let funding_satoshis = session.funding_input_sats;
@@ -86,15 +83,16 @@ fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession)
8683
&RevocationBasepoint::from(open_channel_v2_msg.common_fields.revocation_basepoint),
8784
);
8885

86+
let FundingTxInput { sequence, prevtx, .. } = &initiator_funding_inputs[0];
8987
let tx_add_input_msg = TxAddInput {
9088
channel_id,
9189
serial_id: 2, // Even serial_id from initiator.
92-
prevtx: Some(initiator_funding_inputs[0].1.clone()),
90+
prevtx: Some(prevtx.clone()),
9391
prevtx_out: 0,
94-
sequence: initiator_funding_inputs[0].0.sequence.0,
92+
sequence: sequence.0,
9593
shared_input_txid: None,
9694
};
97-
let input_value = tx_add_input_msg.prevtx.as_ref().unwrap().as_transaction().output
95+
let input_value = tx_add_input_msg.prevtx.as_ref().unwrap().output
9896
[tx_add_input_msg.prevtx_out as usize]
9997
.value;
10098
assert_eq!(input_value.to_sat(), session.initiator_input_value_satoshis);

lightning/src/ln/functional_test_utils.rs

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::ln::channelmanager::{
2626
AChannelManager, ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId,
2727
RAACommitmentOrder, RecipientOnionFields, MIN_CLTV_EXPIRY_DELTA,
2828
};
29+
use crate::ln::funding::FundingTxInput;
2930
use crate::ln::msgs;
3031
use crate::ln::msgs::{
3132
BaseMessageHandler, ChannelMessageHandler, MessageSendEvent, RoutingMessageHandler,
@@ -61,13 +62,11 @@ use bitcoin::pow::CompactTarget;
6162
use bitcoin::script::ScriptBuf;
6263
use bitcoin::secp256k1::{PublicKey, SecretKey};
6364
use bitcoin::transaction::{self, Version as TxVersion};
64-
use bitcoin::transaction::{Sequence, Transaction, TxIn, TxOut};
65-
use bitcoin::witness::Witness;
66-
use bitcoin::{WPubkeyHash, Weight};
65+
use bitcoin::transaction::{Transaction, TxIn, TxOut};
66+
use bitcoin::WPubkeyHash;
6767

6868
use crate::io;
6969
use crate::prelude::*;
70-
use crate::sign::P2WPKH_WITNESS_WEIGHT;
7170
use crate::sync::{Arc, LockTestExt, Mutex, RwLock};
7271
use alloc::rc::Rc;
7372
use core::cell::RefCell;
@@ -1440,7 +1439,7 @@ fn internal_create_funding_transaction<'a, 'b, 'c>(
14401439
/// Return the inputs (with prev tx), and the total witness weight for these inputs
14411440
pub fn create_dual_funding_utxos_with_prev_txs(
14421441
node: &Node<'_, '_, '_>, utxo_values_in_satoshis: &[u64],
1443-
) -> Vec<(TxIn, Transaction, Weight)> {
1442+
) -> Vec<FundingTxInput> {
14441443
// Ensure we have unique transactions per node by using the locktime.
14451444
let tx = Transaction {
14461445
version: TxVersion::TWO,
@@ -1460,22 +1459,12 @@ pub fn create_dual_funding_utxos_with_prev_txs(
14601459
.collect(),
14611460
};
14621461

1463-
let mut inputs = vec![];
1464-
for i in 0..utxo_values_in_satoshis.len() {
1465-
inputs.push((
1466-
TxIn {
1467-
previous_output: OutPoint { txid: tx.compute_txid(), index: i as u16 }
1468-
.into_bitcoin_outpoint(),
1469-
script_sig: ScriptBuf::new(),
1470-
sequence: Sequence::ZERO,
1471-
witness: Witness::new(),
1472-
},
1473-
tx.clone(),
1474-
Weight::from_wu(P2WPKH_WITNESS_WEIGHT),
1475-
));
1476-
}
1477-
1478-
inputs
1462+
tx.output
1463+
.iter()
1464+
.enumerate()
1465+
.map(|(index, _)| index as u32)
1466+
.map(|vout| FundingTxInput::new_p2wpkh(tx.clone(), vout).unwrap())
1467+
.collect()
14791468
}
14801469

14811470
pub fn sign_funding_transaction<'a, 'b, 'c>(

lightning/src/ln/funding.rs

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// This file is Copyright its original authors, visible in version control
2+
// history.
3+
//
4+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5+
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7+
// You may not use this file except in accordance with one or both of these
8+
// licenses.
9+
10+
//! Types pertaining to funding channels.
11+
12+
#[cfg(splicing)]
13+
use bitcoin::{Amount, ScriptBuf, SignedAmount, TxOut};
14+
use bitcoin::{Script, Sequence, Transaction, Weight};
15+
16+
use crate::events::bump_transaction::{Utxo, EMPTY_SCRIPT_SIG_WEIGHT};
17+
use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT};
18+
19+
/// The components of a splice's funding transaction that are contributed by one party.
20+
#[cfg(splicing)]
21+
pub enum SpliceContribution {
22+
/// When funds are added to a channel.
23+
SpliceIn {
24+
/// The amount to contribute to the splice.
25+
value: Amount,
26+
27+
/// The inputs included in the splice's funding transaction to meet the contributed amount.
28+
/// Any excess amount will be sent to a change output.
29+
inputs: Vec<FundingTxInput>,
30+
31+
/// An optional change output script. This will be used if needed or, when not set,
32+
/// generated using [`SignerProvider::get_destination_script`].
33+
change_script: Option<ScriptBuf>,
34+
},
35+
/// When funds are removed from a channel.
36+
SpliceOut {
37+
/// The outputs to include in the splice's funding transaction. The total value of all
38+
/// outputs will be the amount that is removed.
39+
outputs: Vec<TxOut>,
40+
},
41+
}
42+
43+
#[cfg(splicing)]
44+
impl SpliceContribution {
45+
pub(super) fn value(&self) -> SignedAmount {
46+
match self {
47+
SpliceContribution::SpliceIn { value, .. } => {
48+
value.to_signed().unwrap_or(SignedAmount::MAX)
49+
},
50+
SpliceContribution::SpliceOut { outputs } => {
51+
let value_removed = outputs
52+
.iter()
53+
.map(|txout| txout.value)
54+
.sum::<Amount>()
55+
.to_signed()
56+
.unwrap_or(SignedAmount::MAX);
57+
-value_removed
58+
},
59+
}
60+
}
61+
62+
pub(super) fn inputs(&self) -> &[FundingTxInput] {
63+
match self {
64+
SpliceContribution::SpliceIn { inputs, .. } => &inputs[..],
65+
SpliceContribution::SpliceOut { .. } => &[],
66+
}
67+
}
68+
69+
pub(super) fn outputs(&self) -> &[TxOut] {
70+
match self {
71+
SpliceContribution::SpliceIn { .. } => &[],
72+
SpliceContribution::SpliceOut { outputs } => &outputs[..],
73+
}
74+
}
75+
76+
pub(super) fn into_tx_parts(self) -> (Vec<FundingTxInput>, Vec<TxOut>, Option<ScriptBuf>) {
77+
match self {
78+
SpliceContribution::SpliceIn { inputs, change_script, .. } => {
79+
(inputs, vec![], change_script)
80+
},
81+
SpliceContribution::SpliceOut { outputs } => (vec![], outputs, None),
82+
}
83+
}
84+
}
85+
86+
/// An input to contribute to a channel's funding transaction either when using the v2 channel
87+
/// establishment protocol or when splicing.
88+
#[derive(Clone)]
89+
pub struct FundingTxInput {
90+
/// The unspent [`TxOut`] that the input spends.
91+
///
92+
/// [`TxOut`]: bitcoin::TxOut
93+
pub(super) utxo: Utxo,
94+
95+
/// The sequence number to use in the [`TxIn`].
96+
///
97+
/// [`TxIn`]: bitcoin::TxIn
98+
pub(super) sequence: Sequence,
99+
100+
/// The transaction containing the unspent [`TxOut`] referenced by [`utxo`].
101+
///
102+
/// [`TxOut`]: bitcoin::TxOut
103+
/// [`utxo`]: Self::utxo
104+
pub(super) prevtx: Transaction,
105+
}
106+
107+
impl FundingTxInput {
108+
fn new<F: FnOnce(&bitcoin::Script) -> bool>(
109+
prevtx: Transaction, vout: u32, witness_weight: Weight, script_filter: F,
110+
) -> Result<Self, ()> {
111+
Ok(FundingTxInput {
112+
utxo: Utxo {
113+
outpoint: bitcoin::OutPoint { txid: prevtx.compute_txid(), vout },
114+
output: prevtx
115+
.output
116+
.get(vout as usize)
117+
.filter(|output| script_filter(&output.script_pubkey))
118+
.ok_or(())?
119+
.clone(),
120+
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + witness_weight.to_wu(),
121+
},
122+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
123+
prevtx,
124+
})
125+
}
126+
127+
/// Creates an input spending a P2WPKH output from the given `prevtx` at index `vout`.
128+
///
129+
/// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden
130+
/// by [`set_sequence`].
131+
///
132+
/// Returns `Err` if no such output exists in `prevtx` at index `vout`.
133+
///
134+
/// [`TxIn::sequence`]: bitcoin::TxIn::sequence
135+
/// [`set_sequence`]: Self::set_sequence
136+
pub fn new_p2wpkh(prevtx: Transaction, vout: u32) -> Result<Self, ()> {
137+
let witness_weight = Weight::from_wu(P2WPKH_WITNESS_WEIGHT);
138+
FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2wpkh)
139+
}
140+
141+
/// Creates an input spending a P2WSH output from the given `prevtx` at index `vout`.
142+
///
143+
/// Requires passing the weight of witness needed to satisfy the output's script.
144+
///
145+
/// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden
146+
/// by [`set_sequence`].
147+
///
148+
/// Returns `Err` if no such output exists in `prevtx` at index `vout`.
149+
///
150+
/// [`TxIn::sequence`]: bitcoin::TxIn::sequence
151+
/// [`set_sequence`]: Self::set_sequence
152+
pub fn new_p2wsh(prevtx: Transaction, vout: u32, witness_weight: Weight) -> Result<Self, ()> {
153+
FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2wsh)
154+
}
155+
156+
/// Creates an input spending a P2TR output from the given `prevtx` at index `vout`.
157+
///
158+
/// This is meant for inputs spending a taproot output using the key path. See
159+
/// [`new_p2tr_script_spend`] for when spending using a script path.
160+
///
161+
/// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden
162+
/// by [`set_sequence`].
163+
///
164+
/// Returns `Err` if no such output exists in `prevtx` at index `vout`.
165+
///
166+
/// [`new_p2tr_script_spend`]: Self::new_p2tr_script_spend
167+
///
168+
/// [`TxIn::sequence`]: bitcoin::TxIn::sequence
169+
/// [`set_sequence`]: Self::set_sequence
170+
pub fn new_p2tr_key_spend(prevtx: Transaction, vout: u32) -> Result<Self, ()> {
171+
let witness_weight = Weight::from_wu(P2TR_KEY_PATH_WITNESS_WEIGHT);
172+
FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2tr)
173+
}
174+
175+
/// Creates an input spending a P2TR output from the given `prevtx` at index `vout`.
176+
///
177+
/// Requires passing the weight of witness needed to satisfy a script path of the taproot
178+
/// output. See [`new_p2tr_key_spend`] for when spending using the key path.
179+
///
180+
/// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden
181+
/// by [`set_sequence`].
182+
///
183+
/// Returns `Err` if no such output exists in `prevtx` at index `vout`.
184+
///
185+
/// [`new_p2tr_key_spend`]: Self::new_p2tr_key_spend
186+
///
187+
/// [`TxIn::sequence`]: bitcoin::TxIn::sequence
188+
/// [`set_sequence`]: Self::set_sequence
189+
pub fn new_p2tr_script_spend(
190+
prevtx: Transaction, vout: u32, witness_weight: Weight,
191+
) -> Result<Self, ()> {
192+
FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2tr)
193+
}
194+
195+
/// The sequence number to use in the [`TxIn`].
196+
///
197+
/// [`TxIn`]: bitcoin::TxIn
198+
pub fn sequence(&self) -> Sequence {
199+
self.sequence
200+
}
201+
202+
/// Sets the sequence number to use in the [`TxIn`].
203+
///
204+
/// [`TxIn`]: bitcoin::TxIn
205+
pub fn set_sequence(&mut self, sequence: Sequence) {
206+
self.sequence = sequence;
207+
}
208+
}

0 commit comments

Comments
 (0)