From b9d0eb747cf1c2f7f845e94bb89a1a6b0f129a11 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 17 Mar 2026 14:44:45 -0700 Subject: [PATCH 1/2] Change FundingInfo::Contribution to expose contributed output scripts Exposing the amounts for each output isn't very helpful because it's possible that they vary across over multiple splice candidates due to RBF. This commit changes `FundingInfo::Contribution` and several of the helpers used to derive it to be based on output scripts instead. --- lightning/src/events/mod.rs | 6 ++-- lightning/src/ln/channel.rs | 14 ++++----- lightning/src/ln/funding.rs | 42 ++++++++++++++++++-------- lightning/src/ln/interactivetxs.rs | 10 +++---- lightning/src/ln/splicing_tests.rs | 48 +++++++++++++++++++----------- 5 files changed, 75 insertions(+), 45 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 0d5b8f757ab..5f4f3cc7ffd 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -52,7 +52,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::script::ScriptBuf; use bitcoin::secp256k1::PublicKey; -use bitcoin::{OutPoint, Transaction, TxOut}; +use bitcoin::{OutPoint, Transaction}; use core::ops::Deref; #[allow(unused_imports)] @@ -82,8 +82,8 @@ pub enum FundingInfo { Contribution { /// UTXOs spent as inputs contributed to the funding transaction. inputs: Vec, - /// Outputs contributed to the funding transaction. - outputs: Vec, + /// Output scripts contributed to the funding transaction. + outputs: Vec, }, } diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 18236d761a0..6967f230416 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -3133,7 +3133,7 @@ impl PendingFunding { self.contributions.iter().flat_map(|c| c.contributed_inputs()) } - fn contributed_outputs(&self) -> impl Iterator + '_ { + fn contributed_outputs(&self) -> impl Iterator + '_ { self.contributions.iter().flat_map(|c| c.contributed_outputs()) } @@ -3142,7 +3142,7 @@ impl PendingFunding { self.contributions[..len.saturating_sub(1)].iter().flat_map(|c| c.contributed_inputs()) } - fn prior_contributed_outputs(&self) -> impl Iterator + '_ { + fn prior_contributed_outputs(&self) -> impl Iterator + '_ { let len = self.contributions.len(); self.contributions[..len.saturating_sub(1)].iter().flat_map(|c| c.contributed_outputs()) } @@ -3191,7 +3191,7 @@ pub(crate) enum QuiescentAction { pub(super) enum QuiescentError { DoNothing, - DiscardFunding { inputs: Vec, outputs: Vec }, + DiscardFunding { inputs: Vec, outputs: Vec }, FailSplice(SpliceFundingFailed, NegotiationFailureReason), } @@ -6887,8 +6887,8 @@ impl FundingNegotiationContext { self.our_funding_inputs.iter().map(|input| input.utxo.outpoint) } - fn contributed_outputs(&self) -> impl Iterator + '_ { - self.our_funding_outputs.iter() + fn contributed_outputs(&self) -> impl Iterator + '_ { + self.our_funding_outputs.iter().map(|output| output.script_pubkey.as_script()) } } @@ -7046,7 +7046,7 @@ pub struct SpliceFundingFailed { /// Outputs contributed to the splice transaction. Excludes outputs already contributed /// in prior rounds, which may be included in `contribution`. - contributed_outputs: Vec, + contributed_outputs: Vec, /// The funding contribution from the failed round, if available. contribution: Option, @@ -11689,7 +11689,7 @@ where .filter_map(|contribution| { contribution.into_unique_contributions( promoted_tx.input.iter().map(|i| i.previous_output), - promoted_tx.output.iter(), + promoted_tx.output.iter().map(|o| o.script_pubkey.as_script()), ) }) .map(|(inputs, outputs)| FundingInfo::Contribution { inputs, outputs }) diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 2f4e89db926..93685cc3426 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -586,8 +586,11 @@ impl FundingContribution { self.inputs.iter().map(|input| input.utxo.outpoint) } - pub(super) fn contributed_outputs(&self) -> impl Iterator + '_ { - self.outputs.iter().chain(self.change_output.iter()) + pub(super) fn contributed_outputs(&self) -> impl Iterator + '_ { + self.outputs + .iter() + .chain(self.change_output.iter()) + .map(|output| output.script_pubkey.as_script()) } /// The value that will be added to the channel after fees. See [`Self::net_value`] for the net @@ -751,26 +754,41 @@ impl FundingContribution { (inputs, outputs) } - pub(super) fn into_contributed_inputs_and_outputs(self) -> (Vec, Vec) { - let (inputs, outputs) = self.into_tx_parts(); - - (inputs.into_iter().map(|input| input.utxo.outpoint).collect(), outputs) + pub(super) fn into_contributed_inputs_and_outputs(self) -> (Vec, Vec) { + let FundingContribution { inputs, outputs, change_output, .. } = self; + let contributed_inputs = inputs.into_iter().map(|input| input.utxo.outpoint).collect(); + let contributed_outputs = outputs.into_iter().chain(change_output.into_iter()); + (contributed_inputs, contributed_outputs.map(|output| output.script_pubkey).collect()) } pub(super) fn into_unique_contributions<'a>( self, existing_inputs: impl Iterator, - existing_outputs: impl Iterator, - ) -> Option<(Vec, Vec)> { - let (mut inputs, mut outputs) = self.into_contributed_inputs_and_outputs(); + existing_outputs: impl Iterator, + ) -> Option<(Vec, Vec)> { + let FundingContribution { mut inputs, mut outputs, mut change_output, .. } = self; for existing in existing_inputs { - inputs.retain(|input| *input != existing); + inputs.retain(|input| input.outpoint() != existing); } for existing in existing_outputs { - outputs.retain(|output| output.script_pubkey != existing.script_pubkey); + outputs.retain(|output| output.script_pubkey.as_script() != existing); + // TODO: Replace with `take_if` once our MSRV is >= 1.80. + if change_output + .as_ref() + .filter(|output| output.script_pubkey.as_script() == existing) + .is_some() + { + change_output.take(); + } } - if inputs.is_empty() && outputs.is_empty() { + if inputs.is_empty() && outputs.is_empty() && change_output.as_ref().is_none() { None } else { + let inputs = inputs.into_iter().map(|input| input.outpoint()).collect(); + let outputs = outputs + .into_iter() + .chain(change_output.into_iter()) + .map(|output| output.script_pubkey) + .collect(); Some((inputs, outputs)) } } diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index 10dae95cefa..16b2806fd5c 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -378,7 +378,7 @@ impl ConstructedTransaction { .map(|(_, (txin, _))| txin.previous_output) } - fn contributed_outputs(&self) -> impl Iterator + '_ { + fn contributed_outputs(&self) -> impl Iterator + '_ { self.tx .output .iter() @@ -386,7 +386,7 @@ impl ConstructedTransaction { .enumerate() .filter(|(_, (_, output))| output.is_local(self.holder_is_initiator)) .filter(|(index, _)| *index != self.shared_output_index as usize) - .map(|(_, (txout, _))| txout) + .map(|(_, (txout, _))| txout.script_pubkey.as_script()) } pub fn tx(&self) -> &Transaction { @@ -879,7 +879,7 @@ impl InteractiveTxSigningSession { self.unsigned_tx.contributed_inputs() } - pub(super) fn contributed_outputs(&self) -> impl Iterator + '_ { + pub(super) fn contributed_outputs(&self) -> impl Iterator + '_ { self.unsigned_tx.contributed_outputs() } } @@ -2121,11 +2121,11 @@ impl InteractiveTxConstructor { .map(|(_, input)| input.tx_in().previous_output) } - pub(super) fn contributed_outputs(&self) -> impl Iterator + '_ { + pub(super) fn contributed_outputs(&self) -> impl Iterator + '_ { self.outputs_to_contribute .iter() .filter(|(_, output)| !output.is_shared()) - .map(|(_, output)| output.tx_out()) + .map(|(_, output)| output.tx_out().script_pubkey.as_script()) } pub fn is_initiator(&self) -> bool { diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 1b6879e69f0..2887a5f8ca1 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -685,8 +685,8 @@ pub fn splice_channel<'a, 'b, 'c, 'd>( pub struct SpliceLockedResult { pub stfu: Option, - pub node_a_discarded: Vec<(Vec, Vec)>, - pub node_b_discarded: Vec<(Vec, Vec)>, + pub node_a_discarded: Vec<(Vec, Vec)>, + pub node_b_discarded: Vec<(Vec, Vec)>, } pub fn lock_splice_after_blocks<'a, 'b, 'c, 'd>( @@ -3227,7 +3227,8 @@ fn do_abandon_splice_quiescent_action_on_shutdown(local_shutdown: bool, pending_ assert!(inputs.is_empty(), "Expected empty inputs (filtered), got {:?}", inputs); // The change output was filtered (same script_pubkey as the prior splice's // change output), but the splice-out output survives (different script_pubkey). - let expected_outputs: Vec<_> = splice_out_output.into_iter().collect(); + let expected_outputs: Vec<_> = + splice_out_output.into_iter().map(|output| output.script_pubkey).collect(); assert_eq!(*outputs, expected_outputs); }, other => panic!("Expected DiscardFunding with Contribution, got {:?}", other), @@ -3924,10 +3925,6 @@ fn test_funding_contributed_splice_already_pending() { // Clear UTXOs and add a LARGER one for the second contribution to ensure // the change output will be different from the first contribution's change - // - // FIXME: Should we actually not consider the change value given DiscardFunding is meant to - // reclaim the change script pubkey? But that means for other cases we'd need to track which - // output is for change later in the pipeline. nodes[0].wallet_source.clear_utxos(); provide_utxo_reserves(&nodes, 1, splice_in_amount * 3); @@ -3941,6 +3938,13 @@ fn test_funding_contributed_splice_already_pending() { .build() .unwrap(); + // The change script should remain the same. + assert_eq!( + first_contribution.change_output().map(|output| &output.script_pubkey), + second_contribution.change_output().map(|output| &output.script_pubkey), + ); + let change_script = first_contribution.change_output().unwrap().script_pubkey.clone(); + // First funding_contributed - this sets up the quiescent action nodes[0].node.funding_contributed(&channel_id, &node_id_1, first_contribution, None).unwrap(); @@ -3950,7 +3954,9 @@ fn test_funding_contributed_splice_already_pending() { // Second funding_contributed with a different contribution - this should trigger // DiscardFunding because there's already a pending quiescent action (splice contribution). // Only inputs/outputs NOT in the existing contribution should be discarded. - let expected_inputs: Vec<_> = second_contribution.contributed_inputs().collect(); + let (expected_inputs, mut expected_outputs) = + second_contribution.clone().into_contributed_inputs_and_outputs(); + expected_outputs.retain(|output| *output != change_script); // Returns Err(APIMisuseError) and emits DiscardFunding for the non-duplicate parts of the second contribution assert_eq!( @@ -3960,8 +3966,6 @@ fn test_funding_contributed_splice_already_pending() { }) ); - // The second contribution has different outputs (second_splice_out differs from first_splice_out), - // so those outputs should NOT be filtered out - they should appear in DiscardFunding. let events = nodes[0].node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); match &events[0] { @@ -3970,10 +3974,9 @@ fn test_funding_contributed_splice_already_pending() { if let FundingInfo::Contribution { inputs, outputs } = funding_info { // The input is different, so it should be in the discard event assert_eq!(*inputs, expected_inputs); - // The splice-out output (different script_pubkey) survives filtering; - // the change output (same script_pubkey as first contribution) is filtered. - assert_eq!(outputs.len(), 1); - assert!(outputs.contains(&second_splice_out)); + // The different output should NOT be filtered out, but the change script should as + // it is the same in both contributions. + assert_eq!(*outputs, expected_outputs); } else { panic!("Expected FundingInfo::Contribution"); } @@ -4085,6 +4088,13 @@ fn do_test_funding_contributed_active_funding_negotiation(state: u8) { .build() .unwrap(); + // The change script should remain the same. + assert_eq!( + first_contribution.change_output().map(|output| &output.script_pubkey), + second_contribution.change_output().map(|output| &output.script_pubkey), + ); + let change_script = first_contribution.change_output().unwrap().script_pubkey.clone(); + // First funding_contributed - sets up the quiescent action and queues STFU nodes[0] .node @@ -4131,7 +4141,9 @@ fn do_test_funding_contributed_active_funding_negotiation(state: u8) { // Call funding_contributed with the second contribution. Inputs don't overlap (different // UTXOs) so they all survive. The splice-out output (different script_pubkey) survives // while the change output (same script_pubkey as first contribution) is filtered. - let expected_inputs: Vec<_> = second_contribution.contributed_inputs().collect(); + let (expected_inputs, mut expected_outputs) = + second_contribution.clone().into_contributed_inputs_and_outputs(); + expected_outputs.retain(|output| *output != change_script); assert_eq!( nodes[0].node.funding_contributed(&channel_id, &node_id_1, second_contribution, None), Err(APIError::APIMisuseError { @@ -4149,7 +4161,7 @@ fn do_test_funding_contributed_active_funding_negotiation(state: u8) { assert_eq!(*inputs, expected_inputs); // Only the splice-out output survives; the change output is filtered // (same script_pubkey as first contribution's change). - assert_eq!(*outputs, vec![splice_out_output]); + assert_eq!(*outputs, vec![splice_out_output.script_pubkey]); } else { panic!("Expected FundingInfo::Contribution"); } @@ -4826,7 +4838,7 @@ fn test_splice_rbf_discard_unique_contribution() { assert_eq!(result.node_a_discarded.len(), 1); let (ref inputs, ref outputs) = result.node_a_discarded[0]; assert_eq!(*inputs, round_0_inputs); - assert_eq!(*outputs, vec![splice_out_output]); + assert_eq!(*outputs, vec![splice_out_output.script_pubkey]); // Node 1 (non-contributing acceptor) has no contributions to discard. assert!(result.node_b_discarded.is_empty()); @@ -6851,7 +6863,7 @@ fn test_splice_rbf_disconnect_filters_prior_contributions() { assert!(inputs.is_empty(), "Expected empty inputs (filtered), got {:?}", inputs); // The change output was filtered (same script_pubkey as round 0's change output), // but the splice-out output survives (different script_pubkey). - assert_eq!(*outputs, vec![splice_out_output.clone()]); + assert_eq!(*outputs, vec![splice_out_output.script_pubkey.clone()]); }, other => panic!("Expected DiscardFunding with Contribution, got {:?}", other), } From 7e806f97c5f2a2c3bcbb739a6d10ad591aeeba04 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Tue, 3 Mar 2026 11:43:56 -0800 Subject: [PATCH 2/2] Produce FundingInfo::Contribution variants in ChannelMonitor Similar to the `ChannelManager`, we expose the contributed inputs and outputs of a splice via `FundingInfo::Contribution` at the `ChannelMonitor` level such that we don't lose the context when the channel closes while a splice is still pending. This relies on tracking the `FundingContribution` that was provided to the `ChannelManager` prior to negotiating the new funding transaction. If no `FundingContribution` exists, then we continue to emit the `FundingInfo::OutPoint` variant. --- lightning/src/chain/channelmonitor.rs | 87 +++++++++++++++++++++------ lightning/src/ln/channel.rs | 7 +++ lightning/src/ln/funding.rs | 6 +- lightning/src/ln/splicing_tests.rs | 20 +++++- lightning/src/util/ser.rs | 2 + 5 files changed, 98 insertions(+), 24 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index c3e20ef5e6f..42d04e0f8ce 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -44,7 +44,7 @@ use crate::chain::package::{ use crate::chain::transaction::{OutPoint, TransactionData}; use crate::chain::{BlockLocator, WatchedOutput}; use crate::events::bump_transaction::{AnchorDescriptor, BumpTransactionEvent}; -use crate::events::{ClosureReason, Event, EventHandler, ReplayEvent}; +use crate::events::{ClosureReason, Event, EventHandler, FundingInfo, ReplayEvent}; use crate::ln::chan_utils::{ self, ChannelTransactionParameters, CommitmentTransaction, CounterpartyCommitmentSecrets, HTLCClaim, HTLCOutputInCommitment, HolderCommitmentTransaction, @@ -55,6 +55,7 @@ use crate::ln::channel_keys::{ RevocationKey, }; use crate::ln::channelmanager::{HTLCSource, PaymentClaimDetails, SentHTLCId}; +use crate::ln::funding::FundingContribution; use crate::ln::msgs::DecodeError; use crate::ln::types::ChannelId; use crate::sign::{ @@ -688,6 +689,7 @@ pub(crate) enum ChannelMonitorUpdateStep { channel_parameters: ChannelTransactionParameters, holder_commitment_tx: HolderCommitmentTransaction, counterparty_commitment_tx: CommitmentTransaction, + funding_contribution: Option, }, RenegotiatedFundingLocked { funding_txid: Txid, @@ -773,6 +775,7 @@ impl_writeable_tlv_based_enum_upgradable!(ChannelMonitorUpdateStep, (1, channel_parameters, (required: ReadableArgs, None)), (3, holder_commitment_tx, required), (5, counterparty_commitment_tx, required), + (7, funding_contribution, option), }, (12, RenegotiatedFundingLocked) => { (1, funding_txid, required), @@ -1166,6 +1169,9 @@ struct FundingScope { // transaction for which we have deleted claim information on some watchtowers. current_holder_commitment_tx: HolderCommitmentTransaction, prev_holder_commitment_tx: Option, + + /// Our funding contribution when we negotiated the corresponding funding transaction. + contribution: Option, } impl FundingScope { @@ -1185,6 +1191,14 @@ impl FundingScope { fn channel_type_features(&self) -> &ChannelTypeFeatures { &self.channel_parameters.channel_type_features } + + fn contributed_inputs(&self) -> impl Iterator + '_ { + self.contribution.iter().flat_map(|contribution| contribution.contributed_inputs()) + } + + fn contributed_outputs(&self) -> impl Iterator + '_ { + self.contribution.iter().flat_map(|contribution| contribution.contributed_outputs()) + } } impl_writeable_tlv_based!(FundingScope, { @@ -1194,6 +1208,7 @@ impl_writeable_tlv_based!(FundingScope, { (7, current_holder_commitment_tx, required), (9, prev_holder_commitment_tx, option), (11, counterparty_claimable_outpoints, required), + (13, contribution, option), }); #[derive(Clone, PartialEq)] @@ -1756,6 +1771,7 @@ pub(crate) fn write_chanmon_internal( (35, channel_monitor.is_manual_broadcast, required), (37, channel_monitor.funding_seen_onchain, required), (39, channel_monitor.best_block.previous_blocks, required), + (41, channel_monitor.funding.contribution, option), }); Ok(()) @@ -1905,6 +1921,8 @@ impl ChannelMonitor { current_holder_commitment_tx: initial_holder_commitment_tx, prev_holder_commitment_tx: None, + + contribution: None, }, pending_funding: vec![], @@ -3959,6 +3977,7 @@ impl ChannelMonitorImpl { &mut self, logger: &WithContext, channel_parameters: &ChannelTransactionParameters, alternative_holder_commitment_tx: &HolderCommitmentTransaction, alternative_counterparty_commitment_tx: &CommitmentTransaction, + funding_contribution: &Option, ) -> Result<(), ()> { let alternative_counterparty_commitment_txid = alternative_counterparty_commitment_tx.trust().txid(); @@ -4025,6 +4044,7 @@ impl ChannelMonitorImpl { counterparty_claimable_outpoints, current_holder_commitment_tx: alternative_holder_commitment_tx.clone(), prev_holder_commitment_tx: None, + contribution: funding_contribution.clone(), }; let alternative_funding_outpoint = alternative_funding.funding_outpoint(); @@ -4081,6 +4101,29 @@ impl ChannelMonitorImpl { Ok(()) } + fn queue_discard_funding_event( + &mut self, discarded_funding: impl Iterator, + ) { + for funding in discarded_funding { + if let Some(contribution) = funding.contribution { + if let Some((inputs, outputs)) = contribution.into_unique_contributions( + self.funding.contributed_inputs(), + self.funding.contributed_outputs(), + ) { + self.pending_events.push(Event::DiscardFunding { + channel_id: self.channel_id, + funding_info: FundingInfo::Contribution { inputs, outputs }, + }); + } + } else { + self.pending_events.push(Event::DiscardFunding { + channel_id: self.channel_id, + funding_info: FundingInfo::OutPoint { outpoint: funding.funding_outpoint() }, + }); + } + } + } + fn promote_funding(&mut self, new_funding_txid: Txid) -> Result<(), ()> { let prev_funding_txid = self.funding.funding_txid(); @@ -4111,18 +4154,20 @@ impl ChannelMonitorImpl { let no_further_updates_allowed = self.no_further_updates_allowed(); // The swap above places the previous `FundingScope` into `pending_funding`. - for funding in self.pending_funding.drain(..) { - let funding_txid = funding.funding_txid(); - self.outputs_to_watch.remove(&funding_txid); - if no_further_updates_allowed && funding_txid != prev_funding_txid { - self.pending_events.push(Event::DiscardFunding { - channel_id: self.channel_id, - funding_info: crate::events::FundingInfo::OutPoint { - outpoint: funding.funding_outpoint(), - }, - }); - } + for funding in &self.pending_funding { + self.outputs_to_watch.remove(&funding.funding_txid()); } + let mut discarded_funding = Vec::new(); + mem::swap(&mut self.pending_funding, &mut discarded_funding); + let discarded_funding = discarded_funding + .into_iter() + // The previous funding is filtered out since it was already locked, so nothing needs to + // be discarded. + .filter(|funding| { + no_further_updates_allowed && funding.funding_txid() != prev_funding_txid + }); + self.queue_discard_funding_event(discarded_funding); + if let Some((alternative_funding_txid, _)) = self.alternative_funding_confirmed.take() { // In exceedingly rare cases, it's possible there was a reorg that caused a potential funding to // be locked in that this `ChannelMonitor` has not yet seen. Thus, we avoid a runtime assertion @@ -4239,11 +4284,13 @@ impl ChannelMonitorImpl { }, ChannelMonitorUpdateStep::RenegotiatedFunding { channel_parameters, holder_commitment_tx, counterparty_commitment_tx, + funding_contribution, } => { log_trace!(logger, "Updating ChannelMonitor with alternative holder and counterparty commitment transactions for funding txid {}", channel_parameters.funding_outpoint.unwrap().txid); if let Err(_) = self.renegotiated_funding( logger, channel_parameters, holder_commitment_tx, counterparty_commitment_tx, + funding_contribution, ) { ret = Err(()); } @@ -5810,15 +5857,14 @@ impl ChannelMonitorImpl { self.funding_spend_confirmed = Some(entry.txid); self.confirmed_commitment_tx_counterparty_output = commitment_tx_to_counterparty_output; if self.alternative_funding_confirmed.is_none() { - for funding in self.pending_funding.drain(..) { + // We saw a confirmed commitment for our currently locked funding, so + // discard all pending ones. + for funding in &self.pending_funding { self.outputs_to_watch.remove(&funding.funding_txid()); - self.pending_events.push(Event::DiscardFunding { - channel_id: self.channel_id, - funding_info: crate::events::FundingInfo::OutPoint { - outpoint: funding.funding_outpoint(), - }, - }); } + let mut discarded_funding = Vec::new(); + mem::swap(&mut self.pending_funding, &mut discarded_funding); + self.queue_discard_funding_event(discarded_funding.into_iter()); } }, OnchainEvent::AlternativeFundingConfirmation {} => { @@ -6696,6 +6742,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP let mut is_manual_broadcast = RequiredWrapper(None); let mut funding_seen_onchain = RequiredWrapper(None); let mut best_block_previous_blocks = None; + let mut current_funding_contribution = None; read_tlv_fields!(reader, { (1, funding_spend_confirmed, option), (3, htlcs_resolved_on_chain, optional_vec), @@ -6719,6 +6766,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP (35, is_manual_broadcast, (default_value, false)), (37, funding_seen_onchain, (default_value, true)), (39, best_block_previous_blocks, option), // Added and always set in 0.3 + (41, current_funding_contribution, option), }); if let Some(previous_blocks) = best_block_previous_blocks { best_block.previous_blocks = previous_blocks; @@ -6837,6 +6885,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP current_holder_commitment_tx, prev_holder_commitment_tx, + contribution: current_funding_contribution, }, pending_funding: pending_funding.unwrap_or(vec![]), is_manual_broadcast: is_manual_broadcast.0.unwrap(), diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 6967f230416..e9fde821ccc 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -8400,6 +8400,12 @@ where ); } + let funding_contribution = self + .pending_splice + .as_ref() + .and_then(|pending_splice| pending_splice.contributions.last()) + .cloned(); + log_info!( logger, "Received splice initial commitment_signed from peer with funding txid {}", @@ -8413,6 +8419,7 @@ where channel_parameters: pending_splice_funding.channel_transaction_parameters.clone(), holder_commitment_tx, counterparty_commitment_tx, + funding_contribution, }], channel_id: Some(self.context.channel_id()), }; diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 93685cc3426..3a0b4fb0630 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -582,11 +582,11 @@ impl FundingContribution { self.is_splice } - pub(super) fn contributed_inputs(&self) -> impl Iterator + '_ { + pub(crate) fn contributed_inputs(&self) -> impl Iterator + '_ { self.inputs.iter().map(|input| input.utxo.outpoint) } - pub(super) fn contributed_outputs(&self) -> impl Iterator + '_ { + pub(crate) fn contributed_outputs(&self) -> impl Iterator + '_ { self.outputs .iter() .chain(self.change_output.iter()) @@ -761,7 +761,7 @@ impl FundingContribution { (contributed_inputs, contributed_outputs.map(|output| output.script_pubkey).collect()) } - pub(super) fn into_unique_contributions<'a>( + pub(crate) fn into_unique_contributions<'a>( self, existing_inputs: impl Iterator, existing_outputs: impl Iterator, ) -> Option<(Vec, Vec)> { diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 2887a5f8ca1..9d1342acd24 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -1842,6 +1842,8 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs: 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)); + let (expected_discarded_inputs, expected_discarded_outputs) = + initiator_contribution.clone().into_contributed_inputs_and_outputs(); let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution.clone()); let (preimage2, payment_hash2, ..) = route_payment(&nodes[0], &[&nodes[1]], payment_amount); @@ -1985,14 +1987,25 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs: .chain_source .remove_watched_txn_and_outputs(funding_outpoint, txout.script_pubkey.clone()); - // `SpendableOutputs` events are also included here, but we don't care for them. let events = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events(); assert_eq!(events.len(), if claim_htlcs { 2 } else { 4 }, "{events:?}"); if let Event::DiscardFunding { funding_info, .. } = &events[0] { - assert_eq!(*funding_info, FundingInfo::OutPoint { outpoint: funding_outpoint }); + assert_eq!( + *funding_info, + FundingInfo::Contribution { + inputs: expected_discarded_inputs, + outputs: expected_discarded_outputs, + } + ); } else { panic!(); } + assert!(matches!(&events[1], Event::SpendableOutputs { .. })); + if !claim_htlcs { + assert!(matches!(&events[2], Event::SpendableOutputs { .. })); + assert!(matches!(&events[3], Event::SpendableOutputs { .. })); + } + let events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events(); assert_eq!(events.len(), if claim_htlcs { 2 } else { 1 }, "{events:?}"); if let Event::DiscardFunding { funding_info, .. } = &events[0] { @@ -2000,6 +2013,9 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs: } else { panic!(); } + if claim_htlcs { + assert!(matches!(&events[1], Event::SpendableOutputs { .. })); + } } } diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index bd2488bd8d1..88c03638c82 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1099,6 +1099,8 @@ impl_for_vec!(crate::ln::channelmanager::MonitorUpdateCompletionAction); impl_for_vec!(crate::ln::channelmanager::PaymentClaimDetails); impl_for_vec!(crate::ln::msgs::SocketAddress); impl_for_vec!((A, B), A, B); +impl_for_vec!(OutPoint); +impl_for_vec!(ScriptBuf); impl_for_vec!(SerialId); impl_for_vec!(TxInMetadata); impl_for_vec!(TxOutMetadata);