Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 98 additions & 19 deletions lightning/src/chain/channelmonitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use crate::chain::package::{
use crate::chain::transaction::{OutPoint, TransactionData};
use crate::chain::{BestBlock, 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,
Expand Down Expand Up @@ -688,6 +688,8 @@ pub(crate) enum ChannelMonitorUpdateStep {
channel_parameters: ChannelTransactionParameters,
holder_commitment_tx: HolderCommitmentTransaction,
counterparty_commitment_tx: CommitmentTransaction,
contributed_inputs: Vec<BitcoinOutPoint>,
contributed_outputs: Vec<ScriptBuf>,
},
RenegotiatedFundingLocked {
funding_txid: Txid,
Expand Down Expand Up @@ -773,6 +775,8 @@ impl_writeable_tlv_based_enum_upgradable!(ChannelMonitorUpdateStep,
(1, channel_parameters, (required: ReadableArgs, None)),
(3, holder_commitment_tx, required),
(5, counterparty_commitment_tx, required),
(7, contributed_inputs, optional_vec),
(9, contributed_outputs, optional_vec),
},
(12, RenegotiatedFundingLocked) => {
(1, funding_txid, required),
Expand Down Expand Up @@ -1166,6 +1170,10 @@ struct FundingScope {
// transaction for which we have deleted claim information on some watchtowers.
current_holder_commitment_tx: HolderCommitmentTransaction,
prev_holder_commitment_tx: Option<HolderCommitmentTransaction>,

/// Inputs and outputs we contributed when negotiating the corresponding funding transaction.
contributed_inputs: Option<Vec<BitcoinOutPoint>>,
contributed_outputs: Option<Vec<ScriptBuf>>,
}

impl FundingScope {
Expand Down Expand Up @@ -1194,6 +1202,8 @@ impl_writeable_tlv_based!(FundingScope, {
(7, current_holder_commitment_tx, required),
(9, prev_holder_commitment_tx, option),
(11, counterparty_claimable_outpoints, required),
(13, contributed_inputs, option),
(15, contributed_outputs, option),
});

#[derive(Clone, PartialEq)]
Expand Down Expand Up @@ -1755,6 +1765,8 @@ pub(crate) fn write_chanmon_internal<Signer: EcdsaChannelSigner, W: Writer>(
(34, channel_monitor.alternative_funding_confirmed, option),
(35, channel_monitor.is_manual_broadcast, required),
(37, channel_monitor.funding_seen_onchain, required),
(39, channel_monitor.funding.contributed_inputs, option),
(41, channel_monitor.funding.contributed_outputs, option),
});

Ok(())
Expand Down Expand Up @@ -1904,6 +1916,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {

current_holder_commitment_tx: initial_holder_commitment_tx,
prev_holder_commitment_tx: None,

contributed_inputs: None,
contributed_outputs: None,
},
pending_funding: vec![],

Expand Down Expand Up @@ -3958,6 +3973,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
&mut self, logger: &WithContext<L>, channel_parameters: &ChannelTransactionParameters,
alternative_holder_commitment_tx: &HolderCommitmentTransaction,
alternative_counterparty_commitment_tx: &CommitmentTransaction,
contributed_inputs: &[BitcoinOutPoint], contributed_outputs: &[ScriptBuf],
) -> Result<(), ()> {
let alternative_counterparty_commitment_txid =
alternative_counterparty_commitment_tx.trust().txid();
Expand Down Expand Up @@ -4024,6 +4040,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
counterparty_claimable_outpoints,
current_holder_commitment_tx: alternative_holder_commitment_tx.clone(),
prev_holder_commitment_tx: None,
contributed_inputs: Some(contributed_inputs.to_vec()),
contributed_outputs: Some(contributed_outputs.to_vec()),
Comment on lines +4043 to +4044
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines unconditionally wrap in Some(...), even when the slices are empty. This creates a semantic difference from None:

  • Nonequeue_discard_funding_event emits FundingInfo::OutPoint (fallback for unknown contributions)
  • Some(vec![]) → goes to the else branch, accumulates nothing, and no event is emitted

This is correct for the non-contributor case (nothing to discard). However, if a RenegotiatedFunding monitor update from a prior LDK version (without fields 7/9) is replayed after upgrade, optional_vec deserialization yields empty Vecs, which become Some(vec![]) here. The resulting funding would silently produce no DiscardFunding event when discarded, instead of the OutPoint fallback.

If splicing hasn't shipped in a stable release yet, this is a non-issue in practice. But for defensive correctness, consider normalizing:

Suggested change
contributed_inputs: Some(contributed_inputs.to_vec()),
contributed_outputs: Some(contributed_outputs.to_vec()),
contributed_inputs: if contributed_inputs.is_empty() { None } else { Some(contributed_inputs.to_vec()) },
contributed_outputs: if contributed_outputs.is_empty() { None } else { Some(contributed_outputs.to_vec()) },

This would cause non-contributors to get a (harmless) OutPoint event, so it's a tradeoff. At minimum, a comment explaining the Some(vec![]) vs None distinction in queue_discard_funding_event would help future readers.

};
let alternative_funding_outpoint = alternative_funding.funding_outpoint();

Expand Down Expand Up @@ -4080,6 +4098,58 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
Ok(())
}

fn queue_discard_funding_event(
&mut self, discarded_funding: impl Iterator<Item = FundingScope>,
) {
let (mut discarded_inputs, mut discarded_outputs) = (new_hash_set(), new_hash_set());
for funding in discarded_funding {
if funding.contributed_inputs.is_none() && funding.contributed_outputs.is_none() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: consider adding a comment clarifying the None vs Some(vec![]) semantics, since they produce different behavior:

  • Both None → fallback FundingInfo::OutPoint event (contribution data unavailable)
  • Either Some(...)Contribution path (tracked but possibly empty after filtering → no event if nothing remains)

This distinction is intentional but non-obvious to a reader.

self.pending_events.push(Event::DiscardFunding {
channel_id: self.channel_id,
funding_info: FundingInfo::OutPoint { outpoint: funding.funding_outpoint() },
});
} else {
// Filter out any inputs/outputs that were already included in the locked funding
// transaction.
if let Some(mut maybe_discarded_inputs) = funding.contributed_inputs {
maybe_discarded_inputs.retain(|input| {
let is_input_in_locked_funding = self
.funding
.contributed_inputs
.as_ref()
.map(|inputs| inputs.contains(input))
// The recently locked funding did not contain any contributed inputs.
.unwrap_or(false);
!is_input_in_locked_funding
});
discarded_inputs.extend(maybe_discarded_inputs);
}
if let Some(mut maybe_discarded_outputs) = funding.contributed_outputs {
maybe_discarded_outputs.retain(|output| {
let is_output_in_locked_funding = self
.funding
.contributed_outputs
.as_ref()
.map(|outputs| outputs.contains(output))
// The recently locked funding did not contain any contributed outputs.
.unwrap_or(false);
!is_output_in_locked_funding
});
discarded_outputs.extend(maybe_discarded_outputs);
}
}
}
if !discarded_inputs.is_empty() || !discarded_outputs.is_empty() {
self.pending_events.push(Event::DiscardFunding {
channel_id: self.channel_id,
funding_info: FundingInfo::Contribution {
inputs: discarded_inputs.into_iter().collect(),
outputs: discarded_outputs.into_iter().collect(),
Comment on lines +4104 to +4147
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The discarded_inputs and discarded_outputs HashSets use RandomState, so when collected into Vecs at lines 4146-4147, the element ordering is non-deterministic across runs. This means FundingInfo::Contribution { inputs, outputs } can have different orderings each time.

Currently this is likely safe because the tests only exercise single-element cases, but any test with multiple contributed inputs or outputs and an assert_eq! on the Contribution variant would be flaky. Consider either:

  • Sorting the Vecs before constructing the event, or
  • Documenting that the ordering is unspecified and tests should use order-independent assertions (e.g., convert to a set before comparing).

},
});
}
}

fn promote_funding(&mut self, new_funding_txid: Txid) -> Result<(), ()> {
let prev_funding_txid = self.funding.funding_txid();

Expand Down Expand Up @@ -4110,18 +4180,20 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
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
Expand Down Expand Up @@ -4238,11 +4310,13 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
},
ChannelMonitorUpdateStep::RenegotiatedFunding {
channel_parameters, holder_commitment_tx, counterparty_commitment_tx,
contributed_inputs, contributed_outputs,
} => {
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,
contributed_inputs, contributed_outputs,
) {
ret = Err(());
}
Expand Down Expand Up @@ -5809,15 +5883,14 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
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 {} => {
Expand Down Expand Up @@ -6694,6 +6767,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
let mut alternative_funding_confirmed = None;
let mut is_manual_broadcast = RequiredWrapper(None);
let mut funding_seen_onchain = RequiredWrapper(None);
let mut current_funding_contributed_inputs = None;
let mut current_funding_contributed_outputs = None;
read_tlv_fields!(reader, {
(1, funding_spend_confirmed, option),
(3, htlcs_resolved_on_chain, optional_vec),
Expand All @@ -6716,6 +6791,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
(34, alternative_funding_confirmed, option),
(35, is_manual_broadcast, (default_value, false)),
(37, funding_seen_onchain, (default_value, true)),
(39, current_funding_contributed_inputs, option),
(41, current_funding_contributed_outputs, option),
});
// Note that `payment_preimages_with_info` was added (and is always written) in LDK 0.1, so
// we can use it to determine if this monitor was last written by LDK 0.1 or later.
Expand Down Expand Up @@ -6830,6 +6907,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP

current_holder_commitment_tx,
prev_holder_commitment_tx,
contributed_inputs: current_funding_contributed_inputs,
contributed_outputs: current_funding_contributed_outputs,
},
pending_funding: pending_funding.unwrap_or(vec![]),
is_manual_broadcast: is_manual_broadcast.0.unwrap(),
Expand Down
6 changes: 3 additions & 3 deletions lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,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)]
Expand Down Expand Up @@ -81,8 +81,8 @@ pub enum FundingInfo {
Contribution {
/// UTXOs spent as inputs contributed to the funding transaction.
inputs: Vec<OutPoint>,
/// Outputs contributed to the funding transaction.
outputs: Vec<TxOut>,
/// Output scripts contributed to the funding transaction.
outputs: Vec<ScriptBuf>,
},
}

Expand Down
58 changes: 34 additions & 24 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3083,7 +3083,7 @@ impl PendingFunding {
self.contributions.iter().flat_map(|c| c.contributed_inputs())
}

fn contributed_outputs(&self) -> impl Iterator<Item = &TxOut> + '_ {
fn contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ {
self.contributions.iter().flat_map(|c| c.contributed_outputs())
}

Expand All @@ -3092,7 +3092,7 @@ impl PendingFunding {
self.contributions[..len.saturating_sub(1)].iter().flat_map(|c| c.contributed_inputs())
}

fn prior_contributed_outputs(&self) -> impl Iterator<Item = &TxOut> + '_ {
fn prior_contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ {
let len = self.contributions.len();
self.contributions[..len.saturating_sub(1)].iter().flat_map(|c| c.contributed_outputs())
}
Expand Down Expand Up @@ -3141,7 +3141,7 @@ pub(crate) enum QuiescentAction {

pub(super) enum QuiescentError {
DoNothing,
DiscardFunding { inputs: Vec<bitcoin::OutPoint>, outputs: Vec<bitcoin::TxOut> },
DiscardFunding { inputs: Vec<bitcoin::OutPoint>, outputs: Vec<ScriptBuf> },
FailSplice(SpliceFundingFailed),
}

Expand Down Expand Up @@ -6516,23 +6516,27 @@ impl FundingNegotiationContext {
}
}

fn into_contributed_inputs_and_outputs(self) -> (Vec<bitcoin::OutPoint>, Vec<TxOut>) {
fn into_contributed_inputs_and_outputs(self) -> (Vec<bitcoin::OutPoint>, Vec<ScriptBuf>) {
let contributed_inputs =
self.our_funding_inputs.into_iter().map(|input| input.utxo.outpoint).collect();
let contributed_outputs = self.our_funding_outputs;
let contributed_outputs =
self.our_funding_outputs.into_iter().map(|output| output.script_pubkey).collect();
(contributed_inputs, contributed_outputs)
}

fn contributed_inputs(&self) -> impl Iterator<Item = bitcoin::OutPoint> + '_ {
self.our_funding_inputs.iter().map(|input| input.utxo.outpoint)
}

fn contributed_outputs(&self) -> impl Iterator<Item = &TxOut> + '_ {
self.our_funding_outputs.iter()
fn contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ {
self.our_funding_outputs.iter().map(|output| output.script_pubkey.as_script())
}

fn to_contributed_inputs_and_outputs(&self) -> (Vec<bitcoin::OutPoint>, Vec<TxOut>) {
(self.contributed_inputs().collect(), self.contributed_outputs().cloned().collect())
fn to_contributed_inputs_and_outputs(&self) -> (Vec<bitcoin::OutPoint>, Vec<ScriptBuf>) {
(
self.contributed_inputs().collect(),
self.contributed_outputs().map(|script| script.into()).collect(),
)
}
}

Expand Down Expand Up @@ -6692,8 +6696,8 @@ pub struct SpliceFundingFailed {
/// UTXOs spent as inputs contributed to the splice transaction.
pub contributed_inputs: Vec<bitcoin::OutPoint>,

/// Outputs contributed to the splice transaction.
pub contributed_outputs: Vec<bitcoin::TxOut>,
/// Output scripts contributed to the splice transaction.
pub contributed_outputs: Vec<ScriptBuf>,
}

macro_rules! maybe_create_splice_funding_failed {
Expand Down Expand Up @@ -6733,7 +6737,7 @@ macro_rules! maybe_create_splice_funding_failed {
contributed_inputs.retain(|i| *i != input);
}
for output in pending_splice.prior_contributed_outputs() {
contributed_outputs.retain(|o| o.script_pubkey != output.script_pubkey);
contributed_outputs.retain(|i| i != output);
}
}

Expand Down Expand Up @@ -6778,15 +6782,16 @@ where
fn quiescent_action_into_error(&self, action: QuiescentAction) -> QuiescentError {
match action {
QuiescentAction::Splice { contribution, .. } => {
let (mut inputs, mut outputs) = contribution.into_contributed_inputs_and_outputs();
if let Some(ref pending_splice) = self.pending_splice {
for input in pending_splice.contributed_inputs() {
inputs.retain(|i| *i != input);
}
for output in pending_splice.contributed_outputs() {
outputs.retain(|o| o.script_pubkey != output.script_pubkey);
}
}
let (inputs, outputs) = if let Some(ref pending_splice) = self.pending_splice {
contribution
.into_unique_contributions(
pending_splice.contributed_inputs(),
pending_splice.contributed_outputs(),
)
.unwrap_or((Vec::new(), Vec::new()))
} else {
contribution.into_contributed_inputs_and_outputs()
};
QuiescentError::FailSplice(SpliceFundingFailed {
funding_txo: None,
channel_type: None,
Expand Down Expand Up @@ -7920,12 +7925,12 @@ where
&mut self, msg: &msgs::CommitmentSigned, fee_estimator: &LowerBoundedFeeEstimator<F>,
logger: &L,
) -> Result<Option<ChannelMonitorUpdate>, ChannelError> {
debug_assert!(self
let signing_session = self
.context
.interactive_tx_signing_session
.as_ref()
.map(|signing_session| !signing_session.has_received_tx_signatures())
.unwrap_or(false));
.expect("Signing session must exist for negotiated pending splice");
debug_assert!(!signing_session.has_received_tx_signatures());

let pending_splice_funding = self
.pending_splice
Expand Down Expand Up @@ -7977,6 +7982,9 @@ where
);
}

let (contributed_inputs, contributed_outputs) =
signing_session.to_contributed_inputs_and_outputs();

log_info!(
logger,
"Received splice initial commitment_signed from peer with funding txid {}",
Expand All @@ -7990,6 +7998,8 @@ where
channel_parameters: pending_splice_funding.channel_transaction_parameters.clone(),
holder_commitment_tx,
counterparty_commitment_tx,
contributed_inputs,
contributed_outputs,
}],
channel_id: Some(self.context.channel_id()),
};
Expand Down
Loading
Loading