Use CoinSelection::change_output when splicing
What changed, and why it matters
This commit changes how Lightning Dev Kit handles 'change' money during Bitcoin channel splicing. Previously, the user could provide a change address or LDK would generate one itself. Now, the wallet's coin-selection logic decides whether a change output is needed and what it looks like. The commit keeps a fallback for older stored data that may not include a change script. It is a code-quality and consistency improvement rather than a clear security fix, though it reduces the chance of fee or change-output mistakes during splicing.
Treat as a normal refactor/API change. Reviewers should verify that CoinSelection::change_output correctly accounts for dust limits and fees in all splicing paths, and that the legacy ChangeStrategy::LegacyUserProvided fallback is only triggered for legitimately old serialized state. No immediate security response is indicated by the commit itself.
Security signals we found
Change output handling moved from caller-provided/generated script to wallet coin-selection result
Legacy fallback retained for older serialized SpliceInstruction without change script
Serialization format changed: FundingContribution TLV field 9 now stores change_output (TxOut) instead of change_script (ScriptBuf)
No explicit security framing, CVE, or advisory referenced in commit message or diff
Evidence from the diff
The patch refactors splicing funding contribution construction so that change outputs come from CoinSelection::change_output instead of a caller-supplied change_script or a SignerProvider-generated destination script. FundingTemplate’s splice_in, splice_in_and_out, and related methods lose their change_script parameter. FundingContribution now stores an optional change_output TxOut (serialized as TLV field 9) and exposes it via into_tx_parts. A new ChangeStrategy enum distinguishes CoinSelection-provided change from legacy user-provided/generated change, and is carried through FundingNegotiation::AwaitingAck. Legacy behavior is preserved for serialized SpliceInstructions that lack a change script. Tests and fuzz harnesses are updated to match the new API.
Changed components
lightning/src/ln/funding.rslightning/src/ln/channel.rslightning/src/ln/interactivetxs.rslightning/src/ln/splicing_tests.rslightning-tests/src/upgrade_downgrade_tests.rsfuzz/src/chanmon_consistency.rsfuzz/src/full_stack.rsInspect captured patch +186 / −211
diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs
index 70dda13..ced89f5 100644
--- a/fuzz/src/chanmon_consistency.rs
+++ b/fuzz/src/chanmon_consistency.rs
@@ -2083,7 +2083,7 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
Ok(funding_template) => {
let wallet = WalletSync::new(&wallets[0], Arc::clone(&loggers[0]));
if let Ok(contribution) =
- funding_template.splice_in_sync(None, Amount::from_sat(10_000), &wallet)
+ funding_template.splice_in_sync(Amount::from_sat(10_000), &wallet)
{
let _ = nodes[0].funding_contributed(
&chan_a_id,
@@ -2109,7 +2109,7 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
Ok(funding_template) => {
let wallet = WalletSync::new(&wallets[1], Arc::clone(&loggers[1]));
if let Ok(contribution) =
- funding_template.splice_in_sync(None, Amount::from_sat(10_000), &wallet)
+ funding_template.splice_in_sync(Amount::from_sat(10_000), &wallet)
{
let _ = nodes[1].funding_contributed(
&chan_a_id,
@@ -2135,7 +2135,7 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
Ok(funding_template) => {
let wallet = WalletSync::new(&wallets[1], Arc::clone(&loggers[1]));
if let Ok(contribution) =
- funding_template.splice_in_sync(None, Amount::from_sat(10_000), &wallet)
+ funding_template.splice_in_sync(Amount::from_sat(10_000), &wallet)
{
let _ = nodes[1].funding_contributed(
&chan_b_id,
@@ -2161,7 +2161,7 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
Ok(funding_template) => {
let wallet = WalletSync::new(&wallets[2], Arc::clone(&loggers[2]));
if let Ok(contribution) =
- funding_template.splice_in_sync(None, Amount::from_sat(10_000), &wallet)
+ funding_template.splice_in_sync(Amount::from_sat(10_000), &wallet)
{
let _ = nodes[2].funding_contributed(
&chan_b_id,
diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs
index 2163ca0..6adb8f3 100644
--- a/fuzz/src/full_stack.rs
+++ b/fuzz/src/full_stack.rs
@@ -1038,11 +1038,9 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger + MaybeSend + MaybeSync>
FeeRate::from_sat_per_kwu(253),
) {
let wallet_sync = WalletSync::new(&wallet, Arc::clone(&logger));
- if let Ok(contribution) = funding_template.splice_in_sync(
- None,
- Amount::from_sat(splice_in_sats.min(900_000)),
- &wallet_sync,
- ) {
+ if let Ok(contribution) = funding_template
+ .splice_in_sync(Amount::from_sat(splice_in_sats.min(900_000)), &wallet_sync)
+ {
let _ = channelmanager.funding_contributed(
&chan_id,
&counterparty,
diff --git a/lightning-tests/src/upgrade_downgrade_tests.rs b/lightning-tests/src/upgrade_downgrade_tests.rs
index dde1941..f18e0e5 100644
--- a/lightning-tests/src/upgrade_downgrade_tests.rs
+++ b/lightning-tests/src/upgrade_downgrade_tests.rs
@@ -458,7 +458,7 @@ fn do_test_0_1_htlc_forward_after_splice(fail_htlc: bool) {
}];
let channel_id = ChannelId(chan_id_bytes_a);
let funding_contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs);
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+ let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
for node in nodes.iter() {
mine_transaction(node, &splice_tx);
connect_blocks(node, ANTI_REORG_DELAY - 1);
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index e000ebc..db85a26 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -2908,6 +2908,7 @@ impl_writeable_tlv_based!(PendingFunding, {
enum FundingNegotiation {
AwaitingAck {
context: FundingNegotiationContext,
+ change_strategy: ChangeStrategy,
new_holder_funding_key: PublicKey,
},
ConstructingTransaction {
@@ -6680,10 +6681,17 @@ pub(super) struct FundingNegotiationContext {
/// The funding outputs we will be contributing to the channel.
#[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled.
pub our_funding_outputs: Vec<TxOut>,
+}
+
+/// How the funding transaction's change is determined.
+#[derive(Debug)]
+pub(super) enum ChangeStrategy {
+ /// The change output, if any, is included in the FundingContribution's outputs.
+ FromCoinSelection,
+
/// The change output script. This will be used if needed or -- if not set -- generated using
/// `SignerProvider::get_destination_script`.
- #[allow(dead_code)] // TODO(splicing): Remove once splicing is enabled.
- pub change_script: Option<ScriptBuf>,
+ LegacyUserProvided(Option<ScriptBuf>),
}
impl FundingNegotiationContext {
@@ -6691,7 +6699,7 @@ impl FundingNegotiationContext {
/// If error occurs, it is caused by our side, not the counterparty.
fn into_interactive_tx_constructor<SP: SignerProvider, ES: EntropySource>(
mut self, context: &ChannelContext<SP>, funding: &FundingScope, signer_provider: &SP,
- entropy_source: &ES, holder_node_id: PublicKey,
+ entropy_source: &ES, holder_node_id: PublicKey, change_strategy: ChangeStrategy,
) -> Result<InteractiveTxConstructor, NegotiationError> {
debug_assert_eq!(
self.shared_funding_input.is_some(),
@@ -6712,46 +6720,15 @@ impl FundingNegotiationContext {
script_pubkey: funding.get_funding_redeemscript().to_p2wsh(),
};
- // Optionally add change output
- let change_value_opt = if !self.our_funding_inputs.is_empty() {
- match calculate_change_output_value(
- &self,
- self.shared_funding_input.is_some(),
- &shared_funding_output.script_pubkey,
- context.holder_dust_limit_satoshis,
- ) {
- Ok(change_value_opt) => change_value_opt,
- Err(reason) => {
- return Err(self.into_negotiation_error(reason));
- },
- }
- } else {
- None
- };
-
- if let Some(change_value) = change_value_opt {
- let change_script = if let Some(script) = self.change_script {
- script
- } else {
- match signer_provider.get_destination_script(context.channel_keys_id) {
- Ok(script) => script,
- Err(_) => {
- let reason = AbortReason::InternalError("Error getting change script");
- return Err(self.into_negotiation_error(reason));
- },
- }
- };
- let mut change_output = TxOut { value: change_value, script_pubkey: change_script };
- let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu();
- let change_output_fee =
- fee_for_weight(self.funding_feerate_sat_per_1000_weight, change_output_weight);
- let change_value_decreased_with_fee =
- change_value.to_sat().saturating_sub(change_output_fee);
- // Check dust limit again
- if change_value_decreased_with_fee > context.holder_dust_limit_satoshis {
- change_output.value = Amount::from_sat(change_value_decreased_with_fee);
- self.our_funding_outputs.push(change_output);
- }
+ match self.calculate_change_output(
+ context,
+ signer_provider,
+ &shared_funding_output,
+ change_strategy,
+ ) {
+ Ok(Some(change_output)) => self.our_funding_outputs.push(change_output),
+ Ok(None) => {},
+ Err(reason) => return Err(self.into_negotiation_error(reason)),
}
let constructor_args = InteractiveTxConstructorArgs {
@@ -6773,6 +6750,52 @@ impl FundingNegotiationContext {
InteractiveTxConstructor::new(constructor_args)
}
+ fn calculate_change_output<SP: SignerProvider>(
+ &self, context: &ChannelContext<SP>, signer_provider: &SP, shared_funding_output: &TxOut,
+ change_strategy: ChangeStrategy,
+ ) -> Result<Option<TxOut>, AbortReason> {
+ if self.our_funding_inputs.is_empty() {
+ return Ok(None);
+ }
+
+ let change_script = match change_strategy {
+ ChangeStrategy::FromCoinSelection => return Ok(None),
+ ChangeStrategy::LegacyUserProvided(change_script) => change_script,
+ };
+
+ let change_value = calculate_change_output_value(
+ &self,
+ self.shared_funding_input.is_some(),
+ &shared_funding_output.script_pubkey,
+ context.holder_dust_limit_satoshis,
+ )?;
+
+ if let Some(change_value) = change_value {
+ let change_script = match change_script {
+ Some(script) => script,
+ None => match signer_provider.get_destination_script(context.channel_keys_id) {
+ Ok(script) => script,
+ Err(_) => {
+ return Err(AbortReason::InternalError("Error getting change script"))
+ },
+ },
+ };
+ let mut change_output = TxOut { value: change_value, script_pubkey: change_script };
+ let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu();
+ let change_output_fee =
+ fee_for_weight(self.funding_feerate_sat_per_1000_weight, change_output_weight);
+ let change_value_decreased_with_fee =
+ change_value.to_sat().saturating_sub(change_output_fee);
+ // Check dust limit again
+ if change_value_decreased_with_fee > context.holder_dust_limit_satoshis {
+ change_output.value = Amount::from_sat(change_value_decreased_with_fee);
+ return Ok(Some(change_output));
+ }
+ }
+
+ Ok(None)
+ }
+
fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError {
let (contributed_inputs, contributed_outputs) = self.into_contributed_inputs_and_outputs();
NegotiationError { reason, contributed_inputs, contributed_outputs }
@@ -12235,14 +12258,13 @@ where
shared_funding_input: Some(prev_funding_input),
our_funding_inputs,
our_funding_outputs,
- change_script,
};
- self.send_splice_init_internal(context)
+ self.send_splice_init_internal(context, ChangeStrategy::LegacyUserProvided(change_script))
}
fn send_splice_init_internal(
- &mut self, context: FundingNegotiationContext,
+ &mut self, context: FundingNegotiationContext, change_strategy: ChangeStrategy,
) -> msgs::SpliceInit {
debug_assert!(self.pending_splice.is_none());
// Rotate the funding pubkey using the prev_funding_txid as a tweak
@@ -12263,8 +12285,11 @@ where
let funding_contribution_satoshis = context.our_funding_contribution.to_sat();
let locktime = context.funding_tx_locktime.to_consensus_u32();
- let funding_negotiation =
- FundingNegotiation::AwaitingAck { context, new_holder_funding_key: funding_pubkey };
+ let funding_negotiation = FundingNegotiation::AwaitingAck {
+ context,
+ change_strategy,
+ new_holder_funding_key: funding_pubkey,
+ };
self.pending_splice = Some(PendingFunding {
funding_negotiation: Some(funding_negotiation),
negotiated_candidates: vec![],
@@ -12490,7 +12515,6 @@ where
shared_funding_input: Some(prev_funding_input),
our_funding_inputs: Vec::new(),
our_funding_outputs: Vec::new(),
- change_script: None,
};
let mut interactive_tx_constructor = funding_negotiation_context
@@ -12500,6 +12524,8 @@ where
signer_provider,
entropy_source,
holder_node_id.clone(),
+ // ChangeStrategy doesn't matter when no inputs are contributed
+ ChangeStrategy::FromCoinSelection,
)
.map_err(|err| {
ChannelError::WarnAndDisconnect(format!(
@@ -12550,11 +12576,11 @@ where
let pending_splice =
self.pending_splice.as_mut().expect("We should have returned an error earlier!");
// TODO: Good candidate for a let else statement once MSRV >= 1.65
- let funding_negotiation_context =
- if let Some(FundingNegotiation::AwaitingAck { context, .. }) =
+ let (funding_negotiation_context, change_strategy) =
+ if let Some(FundingNegotiation::AwaitingAck { context, change_strategy, .. }) =
pending_splice.funding_negotiation.take()
{
- context
+ (context, change_strategy)
} else {
panic!("We should have returned an error earlier!");
};
@@ -12566,6 +12592,7 @@ where
signer_provider,
entropy_source,
holder_node_id.clone(),
+ change_strategy,
)
.map_err(|err| {
ChannelError::WarnAndDisconnect(format!(
@@ -12596,7 +12623,7 @@ where
let (funding_negotiation_context, new_holder_funding_key) = match &pending_splice
.funding_negotiation
{
- Some(FundingNegotiation::AwaitingAck { context, new_holder_funding_key }) => {
+ Some(FundingNegotiation::AwaitingAck { context, new_holder_funding_key, .. }) => {
(context, new_holder_funding_key)
},
Some(FundingNegotiation::ConstructingTransaction { .. })
@@ -13523,7 +13550,7 @@ where
},
};
let funding_feerate_per_kw = contribution.feerate().to_sat_per_kwu() as u32;
- let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts();
+ let (our_funding_inputs, our_funding_outputs) = contribution.into_tx_parts();
let context = FundingNegotiationContext {
is_initiator,
@@ -13533,10 +13560,9 @@ where
shared_funding_input: Some(prev_funding_input),
our_funding_inputs,
our_funding_outputs,
- change_script,
};
- let splice_init = self.send_splice_init_internal(context);
+ let splice_init = self.send_splice_init_internal(context, ChangeStrategy::FromCoinSelection);
return Ok(Some(StfuResponse::SpliceInit(splice_init)));
},
#[cfg(any(test, fuzzing))]
@@ -14320,7 +14346,6 @@ impl<SP: SignerProvider> PendingV2Channel<SP> {
shared_funding_input: None,
our_funding_inputs: funding_inputs,
our_funding_outputs: Vec::new(),
- change_script: None,
};
let chan = Self {
funding,
@@ -14467,7 +14492,6 @@ impl<SP: SignerProvider> PendingV2Channel<SP> {
shared_funding_input: None,
our_funding_inputs: our_funding_inputs.clone(),
our_funding_outputs: Vec::new(),
- change_script: None,
};
let shared_funding_output = TxOut {
value: Amount::from_sat(funding.get_value_satoshis()),
diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs
index e369bd8..06b972d 100644
--- a/lightning/src/ln/funding.rs
+++ b/lightning/src/ln/funding.rs
@@ -19,7 +19,7 @@ use bitcoin::{
use core::ops::Deref;
use crate::events::bump_transaction::sync::CoinSelectionSourceSync;
-use crate::events::bump_transaction::{CoinSelectionSource, Input, Utxo};
+use crate::events::bump_transaction::{CoinSelection, CoinSelectionSource, Input, Utxo};
use crate::ln::chan_utils::{
make_funding_redeemscript, BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT,
FUNDING_TRANSACTION_WITNESS_WEIGHT,
@@ -57,18 +57,15 @@ pub struct FundingTemplate {
impl FundingTemplate {
/// Constructs a [`FundingTemplate`] for a splice using the provided shared input.
- pub(super) fn new(
- shared_input: Option<Input>, feerate: FeeRate, is_initiator: bool,
- ) -> Self {
+ pub(super) fn new(shared_input: Option<Input>, feerate: FeeRate, is_initiator: bool) -> Self {
Self { shared_input, feerate, is_initiator }
}
}
macro_rules! build_funding_contribution {
- ($value_added:expr, $outputs:expr, $change_script:expr, $shared_input:expr, $feerate:expr, $is_initiator:expr, $wallet:ident, $($await:tt)*) => {{
+ ($value_added:expr, $outputs:expr, $shared_input:expr, $feerate:expr, $is_initiator:expr, $wallet:ident, $($await:tt)*) => {{
let value_added: Amount = $value_added;
let outputs: Vec<TxOut> = $outputs;
- let change_script: Option<ScriptBuf> = $change_script;
let shared_input: Option<Input> = $shared_input;
let feerate: FeeRate = $feerate;
let is_initiator: bool = $is_initiator;
@@ -76,8 +73,8 @@ macro_rules! build_funding_contribution {
let value_removed = outputs.iter().map(|txout| txout.value).sum();
let is_splice = shared_input.is_some();
- let inputs = if value_added == Amount::ZERO {
- vec![]
+ let coin_selection = if value_added == Amount::ZERO {
+ CoinSelection { confirmed_utxos: vec![], change_output: None }
} else {
// Used for creating a redeem script for the new funding txo, since the funding pubkeys
// are unknown at this point. Only needed when selecting which UTXOs to include in the
@@ -98,18 +95,19 @@ macro_rules! build_funding_contribution {
let claim_id = None;
let must_spend = shared_input.map(|input| vec![input]).unwrap_or_default();
- let selection = if outputs.is_empty() {
+ if outputs.is_empty() {
let must_pay_to = &[shared_output];
$wallet.select_confirmed_utxos(claim_id, must_spend, must_pay_to, feerate.to_sat_per_kwu() as u32, u64::MAX)$(.$await)*?
} else {
let must_pay_to: Vec<_> = outputs.iter().cloned().chain(core::iter::once(shared_output)).collect();
$wallet.select_confirmed_utxos(claim_id, must_spend, &must_pay_to, feerate.to_sat_per_kwu() as u32, u64::MAX)$(.$await)*?
- };
- selection.confirmed_utxos
+ }
};
// NOTE: Must NOT fail after UTXO selection
+ let CoinSelection { confirmed_utxos: inputs, change_output } = coin_selection;
+
let estimated_fee = estimate_transaction_fee(&inputs, &outputs, is_initiator, is_splice, feerate);
let contribution = FundingContribution {
@@ -117,7 +115,7 @@ macro_rules! build_funding_contribution {
estimated_fee,
inputs,
outputs,
- change_script,
+ change_output,
feerate,
is_initiator,
is_splice,
@@ -130,13 +128,8 @@ macro_rules! build_funding_contribution {
impl FundingTemplate {
/// Creates a [`FundingContribution`] for adding funds to a channel using `wallet` to perform
/// coin selection.
- ///
- /// An optional `change_script` may be given to use as a change output. If `None` and change is
- /// needed, one will be generated using [`SignerProvider::get_destination_script`].
- ///
- /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script
pub async fn splice_in<W: Deref + MaybeSend>(
- self, change_script: Option<ScriptBuf>, value_added: Amount, wallet: W,
+ self, value_added: Amount, wallet: W,
) -> Result<FundingContribution, ()>
where
W::Target: CoinSelectionSource + MaybeSend,
@@ -145,18 +138,13 @@ impl FundingTemplate {
return Err(());
}
let FundingTemplate { shared_input, feerate, is_initiator } = self;
- build_funding_contribution!(value_added, vec![], change_script, shared_input, feerate, is_initiator, wallet, await)
+ build_funding_contribution!(value_added, vec![], shared_input, feerate, is_initiator, wallet, await)
}
/// Creates a [`FundingContribution`] for adding funds to a channel using `wallet` to perform
/// coin selection.
- ///
- /// An optional `change_script` may be given to use as a change output. If `None` and change is
- /// needed, one will be generated using [`SignerProvider::get_destination_script`].
- ///
- /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script
pub fn splice_in_sync<W: Deref>(
- self, change_script: Option<ScriptBuf>, value_added: Amount, wallet: W,
+ self, value_added: Amount, wallet: W,
) -> Result<FundingContribution, ()>
where
W::Target: CoinSelectionSourceSync,
@@ -168,7 +156,6 @@ impl FundingTemplate {
build_funding_contribution!(
value_added,
vec![],
- change_script,
shared_input,
feerate,
is_initiator,
@@ -188,7 +175,7 @@ impl FundingTemplate {
return Err(());
}
let FundingTemplate { shared_input, feerate, is_initiator } = self;
- build_funding_contribution!(Amount::ZERO, outputs, None, shared_input, feerate, is_initiator, wallet, await)
+ build_funding_contribution!(Amount::ZERO, outputs, shared_input, feerate, is_initiator, wallet, await)
}
/// Creates a [`FundingContribution`] for removing funds from a channel using `wallet` to
@@ -206,7 +193,6 @@ impl FundingTemplate {
build_funding_contribution!(
Amount::ZERO,
outputs,
- None,
shared_input,
feerate,
is_initiator,
@@ -216,14 +202,8 @@ impl FundingTemplate {
/// Creates a [`FundingContribution`] for both adding and removing funds from a channel using
/// `wallet` to perform coin selection.
- ///
- /// An optional `change_script` may be given to use as a change output. If `None` and change is
- /// needed, one will be generated using [`SignerProvider::get_destination_script`].
- ///
- /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script
pub async fn splice_in_and_out<W: Deref + MaybeSend>(
- self, change_script: Option<ScriptBuf>, value_added: Amount, outputs: Vec<TxOut>,
- wallet: W,
+ self, value_added: Amount, outputs: Vec<TxOut>, wallet: W,
) -> Result<FundingContribution, ()>
where
W::Target: CoinSelectionSource + MaybeSend,
@@ -232,19 +212,13 @@ impl FundingTemplate {
return Err(());
}
let FundingTemplate { shared_input, feerate, is_initiator } = self;
- build_funding_contribution!(value_added, outputs, change_script, shared_input, feerate, is_initiator, wallet, await)
+ build_funding_contribution!(value_added, outputs, shared_input, feerate, is_initiator, wallet, await)
}
/// Creates a [`FundingContribution`] for both adding and removing funds from a channel using
/// `wallet` to perform coin selection.
- ///
- /// An optional `change_script` may be given to use as a change output. If `None` and change is
- /// needed, one will be generated using [`SignerProvider::get_destination_script`].
- ///
- /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script
pub fn splice_in_and_out_sync<W: Deref>(
- self, change_script: Option<ScriptBuf>, value_added: Amount, outputs: Vec<TxOut>,
- wallet: W,
+ self, value_added: Amount, outputs: Vec<TxOut>, wallet: W,
) -> Result<FundingContribution, ()>
where
W::Target: CoinSelectionSourceSync,
@@ -256,7 +230,6 @@ impl FundingTemplate {
build_funding_contribution!(
value_added,
outputs,
- change_script,
shared_input,
feerate,
is_initiator,
@@ -334,11 +307,8 @@ pub struct FundingContribution {
/// will be the amount that is removed.
outputs: Vec<TxOut>,
- /// An optional change output script. This will be used if needed or, when not set,
- /// generated using [`SignerProvider::get_destination_script`].
- ///
- /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script
- change_script: Option<ScriptBuf>,
+ /// The output where any change will be sent.
+ change_output: Option<TxOut>,
/// The fee rate used to select `inputs`.
feerate: FeeRate,
@@ -356,7 +326,7 @@ impl_writeable_tlv_based!(FundingContribution, {
(3, estimated_fee, required),
(5, inputs, optional_vec),
(7, outputs, optional_vec),
- (9, change_script, option),
+ (9, change_output, option),
(11, feerate, required),
(13, is_initiator, required),
(15, is_splice, required),
@@ -375,13 +345,20 @@ impl FundingContribution {
self.is_splice
}
- pub(super) fn into_tx_parts(self) -> (Vec<FundingTxInput>, Vec<TxOut>, Option<ScriptBuf>) {
- let FundingContribution { inputs, outputs, change_script, .. } = self;
- (inputs, outputs, change_script)
+ pub(super) fn into_tx_parts(self) -> (Vec<FundingTxInput>, Vec<TxOut>) {
+ let FundingContribution { inputs, mut outputs, change_output, .. } = self;
+
+ if let Some(change_output) = change_output {
+ outputs.push(change_output);
+ }
+
+ (inputs, outputs)
}
pub(super) fn into_contributed_inputs_and_outputs(self) -> (Vec<OutPoint>, Vec<TxOut>) {
- (self.inputs.into_iter().map(|input| input.utxo.outpoint).collect(), self.outputs)
+ let (inputs, outputs) = self.into_tx_parts();
+
+ (inputs.into_iter().map(|input| input.utxo.outpoint).collect(), outputs)
}
/// The net value contributed to a channel by the splice. If negative, more value will be
@@ -723,7 +700,7 @@ mod tests {
funding_input_sats(100_000),
],
outputs: vec![],
- change_script: None,
+ change_output: None,
is_initiator: true,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2000),
@@ -744,7 +721,7 @@ mod tests {
outputs: vec![
funding_output_sats(200_000),
],
- change_script: None,
+ change_output: None,
is_initiator: true,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2000),
@@ -765,7 +742,7 @@ mod tests {
outputs: vec![
funding_output_sats(400_000),
],
- change_script: None,
+ change_output: None,
is_initiator: true,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2000),
@@ -786,7 +763,7 @@ mod tests {
outputs: vec![
funding_output_sats(400_000),
],
- change_script: None,
+ change_output: None,
is_initiator: true,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(90000),
@@ -810,7 +787,7 @@ mod tests {
funding_input_sats(100_000),
],
outputs: vec![],
- change_script: None,
+ change_output: None,
is_initiator: true,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2000),
@@ -835,7 +812,7 @@ mod tests {
funding_input_sats(100_000),
],
outputs: vec![],
- change_script: None,
+ change_output: None,
is_initiator: true,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2000),
@@ -854,7 +831,7 @@ mod tests {
funding_input_sats(100_000),
],
outputs: vec![],
- change_script: None,
+ change_output: None,
is_initiator: true,
is_splice: true,
feerate: FeeRate::from_sat_per_kwu(2200),
@@ -879,7 +856,7 @@ mod tests {
funding_input_sats(100_000),
],
outputs: vec![],
- change_script: None,
+ change_output: None,
is_initiator: false,
is_splice: false,
feerate: FeeRate::from_sat_per_kwu(2000),
diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs
index 3c47658..c5db1bc 100644
--- a/lightning/src/ln/interactivetxs.rs
+++ b/lightning/src/ln/interactivetxs.rs
@@ -3435,7 +3435,6 @@ mod tests {
shared_funding_input: None,
our_funding_inputs: inputs,
our_funding_outputs: outputs,
- change_script: None,
};
let gross_change =
total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap();
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 31c13e1..cc422d6 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -29,12 +29,9 @@ use crate::util::ser::Writeable;
use crate::sync::Arc;
-use bitcoin::hashes::Hash;
use bitcoin::secp256k1::ecdsa::Signature;
use bitcoin::secp256k1::PublicKey;
-use bitcoin::{
- Amount, FeeRate, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut, WPubkeyHash,
-};
+use bitcoin::{Amount, FeeRate, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut};
#[test]
fn test_splicing_not_supported_api_error() {
@@ -109,7 +106,7 @@ fn test_v1_splice_in_negative_insufficient_inputs() {
.unwrap();
let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger);
- assert!(funding_template.splice_in_sync(None, splice_in_value, &wallet).is_err());
+ assert!(funding_template.splice_in_sync(splice_in_value, &wallet).is_err());
}
pub fn negotiate_splice_tx<'a, 'b, 'c, 'd>(
@@ -131,21 +128,19 @@ pub fn initiate_splice_in<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
value_added: Amount,
) -> FundingContribution {
- let change_script = Some(initiator.wallet_source.get_change_script().unwrap());
- do_initiate_splice_in(initiator, acceptor, channel_id, value_added, change_script)
+ do_initiate_splice_in(initiator, acceptor, channel_id, value_added)
}
pub fn do_initiate_splice_in<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
- value_added: Amount, change_script: Option<ScriptBuf>,
+ value_added: Amount,
) -> FundingContribution {
let node_id_acceptor = acceptor.node.get_our_node_id();
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
let funding_template =
initiator.node.splice_channel(&channel_id, &node_id_acceptor, feerate).unwrap();
let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger);
- let funding_contribution =
- funding_template.splice_in_sync(change_script, value_added, &wallet).unwrap();
+ let funding_contribution = funding_template.splice_in_sync(value_added, &wallet).unwrap();
initiator
.node
.funding_contributed(&channel_id, &node_id_acceptor, funding_contribution.clone(), None)
@@ -174,29 +169,20 @@ pub fn initiate_splice_in_and_out<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
value_added: Amount, outputs: Vec<TxOut>,
) -> FundingContribution {
- let change_script = Some(initiator.wallet_source.get_change_script().unwrap());
- do_initiate_splice_in_and_out(
- initiator,
- acceptor,
- channel_id,
- value_added,
- outputs,
- change_script,
- )
+ do_initiate_splice_in_and_out(initiator, acceptor, channel_id, value_added, outputs)
}
pub fn do_initiate_splice_in_and_out<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
- value_added: Amount, outputs: Vec<TxOut>, change_script: Option<ScriptBuf>,
+ value_added: Amount, outputs: Vec<TxOut>,
) -> FundingContribution {
let node_id_acceptor = acceptor.node.get_our_node_id();
let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64);
let funding_template =
initiator.node.splice_channel(&channel_id, &node_id_acceptor, feerate).unwrap();
let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger);
- let funding_contribution = funding_template
- .splice_in_and_out_sync(change_script, value_added, outputs, &wallet)
- .unwrap();
+ let funding_contribution =
+ funding_template.splice_in_and_out_sync(value_added, outputs, &wallet).unwrap();
initiator
.node
.funding_contributed(&channel_id, &node_id_acceptor, funding_contribution.clone(), None)
@@ -245,8 +231,7 @@ pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>(
})
.map(|channel| channel.funding_txo.unwrap())
.unwrap();
- let (initiator_inputs, initiator_outputs, initiator_change_script) =
- initiator_contribution.into_tx_parts();
+ let (initiator_inputs, initiator_outputs) = initiator_contribution.into_tx_parts();
let mut expected_initiator_inputs = initiator_inputs
.iter()
.map(|input| input.utxo.outpoint)
@@ -256,7 +241,6 @@ pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>(
.into_iter()
.map(|output| output.script_pubkey)
.chain(core::iter::once(new_funding_script))
- .chain(initiator_change_script.into_iter())
.collect::<Vec<_>>();
let mut acceptor_sent_tx_complete = false;
@@ -408,7 +392,7 @@ pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>(
pub fn splice_channel<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
funding_contribution: FundingContribution,
-) -> Transaction {
+) -> (Transaction, ScriptBuf) {
let node_id_initiator = initiator.node.get_our_node_id();
let node_id_acceptor = acceptor.node.get_our_node_id();
@@ -419,7 +403,7 @@ pub fn splice_channel<'a, 'b, 'c, 'd>(
acceptor,
channel_id,
funding_contribution,
- new_funding_script,
+ new_funding_script.clone(),
);
let (splice_tx, splice_locked) = sign_interactive_funding_tx(initiator, acceptor, false);
assert!(splice_locked.is_none());
@@ -427,7 +411,7 @@ pub fn splice_channel<'a, 'b, 'c, 'd>(
expect_splice_pending_event(initiator, &node_id_acceptor);
expect_splice_pending_event(acceptor, &node_id_initiator);
- splice_tx
+ (splice_tx, new_funding_script)
}
pub fn lock_splice_after_blocks<'a, 'b, 'c, 'd>(
@@ -738,7 +722,7 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) {
// Attempt a splice negotiation that completes, (i.e. `tx_signatures` are exchanged). Reconnecting
// should not abort the negotiation or reset the splice state.
let funding_contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs);
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+ let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
if reload {
let encoded_monitor_0 = get_monitor!(nodes[0], channel_id).encode();
@@ -843,23 +827,22 @@ fn test_splice_in() {
let added_value = Amount::from_sat(initial_channel_value_sat * 2);
let utxo_value = added_value * 3 / 4;
- let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros());
- let fees = Amount::from_sat(321);
+ let fees = Amount::from_sat(322);
provide_utxo_reserves(&nodes, 2, utxo_value);
- let funding_contribution = do_initiate_splice_in(
- &nodes[0],
- &nodes[1],
- channel_id,
- added_value,
- Some(change_script.clone()),
- );
+ let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+ let (splice_tx, new_funding_script) =
+ splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
let expected_change = utxo_value * 2 - added_value - fees;
assert_eq!(
- splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value,
+ splice_tx
+ .output
+ .iter()
+ .find(|txout| txout.script_pubkey != new_funding_script)
+ .unwrap()
+ .value,
expected_change,
);
@@ -904,7 +887,7 @@ fn test_splice_out() {
];
let funding_contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs);
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+ let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
mine_transaction(&nodes[0], &splice_tx);
mine_transaction(&nodes[1], &splice_tx);
@@ -940,11 +923,10 @@ fn test_splice_in_and_out() {
let added_value = Amount::from_sat(htlc_limit_msat / 1000);
let removed_value = added_value * 2;
let utxo_value = added_value * 3 / 4;
- let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros());
let fees = if cfg!(feature = "grind_signatures") {
- Amount::from_sat(383)
+ Amount::from_sat(385)
} else {
- Amount::from_sat(384)
+ Amount::from_sat(385)
};
assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000);
@@ -961,19 +943,20 @@ fn test_splice_in_and_out() {
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
},
];
- let funding_contribution = do_initiate_splice_in_and_out(
- &nodes[0],
- &nodes[1],
- channel_id,
- added_value,
- outputs,
- Some(change_script.clone()),
- );
+ let funding_contribution =
+ do_initiate_splice_in_and_out(&nodes[0], &nodes[1], channel_id, added_value, outputs);
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+ let (splice_tx, new_funding_script) =
+ splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
let expected_change = utxo_value * 2 - added_value - fees;
assert_eq!(
- splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value,
+ splice_tx
+ .output
+ .iter()
+ .filter(|txout| txout.value != removed_value / 2)
+ .find(|txout| txout.script_pubkey != new_funding_script)
+ .unwrap()
+ .value,
expected_change,
);
@@ -995,11 +978,10 @@ fn test_splice_in_and_out() {
let added_value = Amount::from_sat(initial_channel_value_sat * 2);
let removed_value = added_value / 2;
let utxo_value = added_value * 3 / 4;
- let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros());
let fees = if cfg!(feature = "grind_signatures") {
- Amount::from_sat(383)
+ Amount::from_sat(385)
} else {
- Amount::from_sat(384)
+ Amount::from_sat(385)
};
// Clear UTXOs so that the change output from the previous splice isn't considered
@@ -1017,19 +999,20 @@ fn test_splice_in_and_out() {
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
},
];
- let funding_contribution = do_initiate_splice_in_and_out(
- &nodes[0],
- &nodes[1],
- channel_id,
- added_value,
- outputs,
- Some(change_script.clone()),
- );
+ let funding_contribution =
+ do_initiate_splice_in_and_out(&nodes[0], &nodes[1], channel_id, added_value, outputs);
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
+ let (splice_tx, new_funding_script) =
+ splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
let expected_change = utxo_value * 2 - added_value - fees;
assert_eq!(
- splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value,
+ splice_tx
+ .output
+ .iter()
+ .filter(|txout| txout.value != removed_value / 2)
+ .find(|txout| txout.script_pubkey != new_funding_script)
+ .unwrap()
+ .value,
expected_change,
);
@@ -1102,9 +1085,8 @@ fn test_fails_initiating_concurrent_splices() {
let added_value = Amount::from_sat(initial_channel_value_sat);
let acceptor_template = nodes[1].node.splice_channel(&channel_id, &node_0_id, feerate).unwrap();
let acceptor_wallet = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger);
- let change_script = Some(nodes[1].wallet_source.get_change_script().unwrap());
let acceptor_contribution =
- acceptor_template.splice_in_sync(change_script, added_value, &acceptor_wallet).unwrap();
+ acceptor_template.splice_in_sync(added_value, &acceptor_wallet).unwrap();
nodes[1]
.node
.funding_contributed(&channel_id, &node_0_id, acceptor_contribution, None)
@@ -1189,14 +1171,9 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs:
let (preimage1, payment_hash1, ..) = route_payment(&nodes[0], &[&nodes[1]], payment_amount);
let splice_in_amount = initial_channel_capacity / 2;
- let initiator_contribution = do_initiate_splice_in(
- &nodes[0],
- &nodes[1],
- channel_id,
- Amount::from_sat(splice_in_amount),
- Some(nodes[0].wallet_source.get_change_script().unwrap()),
- );
- let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);
+ let initiator_contribution =
+ do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, Amount::from_sat(splice_in_amount));
+ let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);
let (preimage2, payment_hash2, ..) = route_payment(&nodes[0], &[&nodes[1]], payment_amount);
let htlc_expiry = nodes[0].best_block_info().1 + TEST_FINAL_CLTV + LATENCY_GRACE_PERIOD_BLOCKS;
@@ -2211,7 +2188,7 @@ fn do_test_splice_with_inflight_htlc_forward_and_resolution(expire_scid_pre_forw
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
}];
let contribution = initiate_splice_out(&nodes[0], &nodes[1], channel_id_0_1, outputs_0_1);
- let splice_tx_0_1 = splice_channel(&nodes[0], &nodes[1], channel_id_0_1, contribution);
+ let (splice_tx_0_1, _) = splice_channel(&nodes[0], &nodes[1], channel_id_0_1, contribution);
for node in &nodes {
mine_transaction(node, &splice_tx_0_1);
}
@@ -2221,7 +2198,7 @@ fn do_test_splice_with_inflight_htlc_forward_and_resolution(expire_scid_pre_forw
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
}];
let contribution = initiate_splice_out(&nodes[1], &nodes[2], channel_id_1_2, outputs_1_2);
- let splice_tx_1_2 = splice_channel(&nodes[1], &nodes[2], channel_id_1_2, contribution);
+ let (splice_tx_1_2, _) = splice_channel(&nodes[1], &nodes[2], channel_id_1_2, contribution);
for node in &nodes {
mine_transaction(node, &splice_tx_1_2);
}
Why this scored 32/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.