Store splice contributions with their negotiated candidates
What changed, and why it matters
This commit refactors how a Lightning node keeps track of money it adds during splice and RBF (fee-bump) negotiations. Previously, the node's own contributions were stored in a separate list that had to be manually aligned with negotiated funding candidates, which the commit message says was 'easy to get wrong.' The change bundles each contribution with the candidate it belongs to and adds explicit checks to prevent misattribution. It also carefully preserves backward compatibility so older LDK versions can still load simple single splices but will refuse to load more complex RBF states they cannot safely handle.
Review the new TLV serialization logic and invariant assertions for edge cases, especially around deserialization of partially written or corrupted state. Ensure tests cover single-splice backward compatibility with LDK 0.2, RBF refusal by older versions, and recovery after funding negotiation reset. Consider whether the debug_assert invariants should be enforced in release builds for state loaded from disk or received from a peer.
Security signals we found
Refactor of splice/RBF contribution tracking to prevent misattribution between negotiation rounds
Added invariant assertions that contributions form a suffix of negotiated candidates
Serialization compatibility design to make older LDK versions refuse RBF states they cannot operate
Validation that an in-flight contribution is only present alongside an active funding negotiation
Validation that the RBF gate TLV is consistent with reconstructed RBF state on deserialization
Evidence from the diff
The patch restructures PendingFunding in rust-lightning: it replaces the standalone contributions Vec with a NegotiatedCandidate struct pairing each FundingScope with its optional FundingContribution, and adds a separate negotiation_contribution field for the in-flight round. It adds a contributions_form_suffix invariant and debug asserts to ensure a contribution, once present, is carried forward through later RBF rounds. Serialization is redesigned with TLV fields so LDK 0.2 can load a single non-RBF splice (legacy TLV 3) while refusing RBF via an even gate TLV 14. Odd TLVs carry the full candidate list, in-flight contribution, and last feerate for current readers. Several accessors are updated to iterate over candidate.funding, and reset_pending_splice_state now removes the in-flight contribution rather than popping from a list.
Changed components
lightning/src/ln/channel.rsPendingFunding struct and serializationNegotiatedCandidate structSplice/RBF negotiation state machineChannel persistence/serialization formatInspect captured patch +229 / −113
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index fb5a7de..3242ac8 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -84,7 +84,7 @@ use crate::util::config::{
use crate::util::errors::APIError;
use crate::util::logger::{Level as LoggerLevel, Logger, Record, WithContext};
use crate::util::scid_utils::{block_from_scid, scid_from_parts};
-use crate::util::ser::{Readable, ReadableArgs, RequiredWrapper, Writeable, Writer};
+use crate::util::ser::{Iterable, Readable, ReadableArgs, RequiredWrapper, Writeable, Writer};
use crate::util::wallet_utils::{ConfirmedUtxo, Input};
use crate::{impl_readable_for_vec, impl_writeable_for_vec};
@@ -2964,9 +2964,20 @@ impl FundingScope {
struct PendingFunding {
funding_negotiation: Option<FundingNegotiation>,
+ /// Our contribution to the funding negotiation round currently in progress, if we are
+ /// contributing to it. Set when the round starts, moved into the [`NegotiatedCandidate`]
+ /// when negotiation completes, and dropped in
+ /// [`FundedChannel::reset_pending_splice_state`] if the round is abandoned.
+ ///
+ /// When the counterparty initiates an RBF and a prior round included our contribution, this
+ /// is set to that contribution adjusted to the new feerate (or the RBF is rejected if the
+ /// adjustment fails, in which case no round starts). This ensures a splice we contributed to
+ /// never loses our contribution in subsequent rounds.
+ negotiation_contribution: Option<FundingContribution>,
+
/// Funding candidates that have been negotiated but have not reached enough confirmations
/// by both counterparties to have exchanged `splice_locked` and be promoted.
- negotiated_candidates: Vec<FundingScope>,
+ negotiated_candidates: Vec<NegotiatedCandidate>,
/// The funding txid used in the `splice_locked` sent to the counterparty.
sent_funding_txid: Option<Txid>,
@@ -2977,21 +2988,26 @@ struct PendingFunding {
/// The feerate used in the last successfully negotiated funding transaction.
/// Used for validating the minimum feerate increase rule on RBF attempts.
last_funding_feerate_sat_per_1000_weight: Option<u32>,
+}
- /// The funding contributions from splice/RBF rounds where we contributed.
- ///
- /// A new entry is appended when we contribute to a negotiation round (either as initiator
- /// or acceptor). Rounds where we don't contribute (e.g., counterparty-only splice) do not
- /// add an entry. Once non-empty, every subsequent round appends: when the counterparty
- /// initiates an RBF, the last entry is adjusted to the new feerate and appended as a new
- /// entry (or the RBF is rejected if the adjustment fails, in which case no round starts).
- ///
- /// If the round aborts, the last entry is popped in
- /// [`FundedChannel::reset_pending_splice_state`], restoring the prior round's contribution
- /// as the most recent entry.
- contributions: Vec<FundingContribution>,
+/// A funding candidate that has been negotiated, together with our contribution, if any, to the
+/// negotiation round that produced it.
+#[derive(Debug)]
+struct NegotiatedCandidate {
+ funding: FundingScope,
+
+ /// Our contribution to the negotiation round that produced this candidate, or `None` if only
+ /// the counterparty contributed. Once a candidate includes our contribution, every later
+ /// candidate does as well: RBF rounds carry the contribution forward (possibly adjusted to a
+ /// new feerate) rather than dropping it, preserving the splice intention.
+ contribution: Option<FundingContribution>,
}
+impl_ser_tlv_based!(NegotiatedCandidate, {
+ (1, funding, required),
+ (3, contribution, option),
+});
+
#[derive(Debug)]
enum FundingNegotiation {
AwaitingAck {
@@ -3050,20 +3066,44 @@ impl Writeable for PendingFundingWriteable<'_> {
Some(FundingNegotiation::AwaitingSignatures { .. })
)
);
- let contributions_len = if self.reset_funding_negotiation
- && self.pending_funding.funding_negotiation.is_some()
- {
- self.pending_funding.contributions.len().saturating_sub(1)
- } else {
- self.pending_funding.contributions.len()
- };
+ // The in-flight round's contribution is only written if its negotiation survives
+ // serialization round trips. It goes in an odd TLV that LDK 0.2 skips (0.2 never tracked
+ // contributions), so a single in-flight splice we contributed to stays loadable there.
+ let negotiation_contribution = funding_negotiation
+ .is_some()
+ .then(|| self.pending_funding.negotiation_contribution.as_ref())
+ .flatten();
+ let candidates = &self.pending_funding.negotiated_candidates;
+ debug_assert!(
+ self.pending_funding.contributions_form_suffix(),
+ "contributions must form a suffix of the negotiated candidates",
+ );
+ // TLV 3 exposes only the first candidate's funding: the single-splice view LDK 0.2
+ // understands. The authoritative candidate list -- each funding bundled with its
+ // contribution -- goes in the odd TLV 11, which current reads and 0.2 skips. A single
+ // non-contributory splice is fully captured by TLV 3 alone, so the bundle is then omitted.
+ // When a single splice does carry a contribution, 0.2 skips it (and operates the splice
+ // without it), so it need not block 0.2 from loading.
+ //
+ // The even TLV 14 is the only thing that makes 0.2 refuse, and it's written exactly when
+ // there is more than one negotiation round (RBF) -- the one thing 0.2 cannot operate. The
+ // odd contribution fields are safe despite being load-bearing for RBF: this gate makes 0.2
+ // refuse the whole channel in that case, so no reader ever skips them when they matter.
+ let first_funding = Iterable(candidates.iter().take(1).map(|candidate| &candidate.funding));
+ let any_contribution = candidates.iter().any(|candidate| candidate.contribution.is_some());
+ let negotiated_candidates =
+ (candidates.len() > 1 || any_contribution).then(|| Iterable(candidates.iter()));
+ let is_rbf = candidates.len() + usize::from(funding_negotiation.is_some()) > 1;
+ let rbf_gate = is_rbf.then_some(());
write_tlv_fields!(writer, {
(1, funding_negotiation, upgradable_option),
- (3, self.pending_funding.negotiated_candidates, required_vec),
+ (3, first_funding, required),
(5, self.pending_funding.sent_funding_txid, option),
(7, self.pending_funding.received_funding_txid, option),
- (8, self.pending_funding.last_funding_feerate_sat_per_1000_weight, option),
- (10, self.pending_funding.contributions[..contributions_len], optional_vec),
+ (9, self.pending_funding.last_funding_feerate_sat_per_1000_weight, option),
+ (11, negotiated_candidates, option),
+ (13, negotiation_contribution, option),
+ (14, rbf_gate, option),
});
Ok(())
}
@@ -3071,14 +3111,58 @@ impl Writeable for PendingFundingWriteable<'_> {
impl Readable for PendingFunding {
fn read<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
- Ok(_decode_and_build!(reader, Self, {
+ let mut funding_negotiation = None;
+ let mut legacy_negotiated_candidates: Option<Vec<FundingScope>> = None;
+ let mut sent_funding_txid = None;
+ let mut received_funding_txid = None;
+ let mut last_funding_feerate_sat_per_1000_weight = None;
+ let mut negotiated_candidates: Option<Vec<NegotiatedCandidate>> = None;
+ let mut negotiation_contribution: Option<FundingContribution> = None;
+ let mut rbf_gate: Option<()> = None;
+
+ read_tlv_fields!(reader, {
(1, funding_negotiation, upgradable_option),
- (3, negotiated_candidates, required_vec),
+ (3, legacy_negotiated_candidates, optional_vec),
(5, sent_funding_txid, option),
(7, received_funding_txid, option),
- (8, last_funding_feerate_sat_per_1000_weight, option),
- (10, contributions, optional_vec),
- }))
+ (9, last_funding_feerate_sat_per_1000_weight, option),
+ (11, negotiated_candidates, optional_vec),
+ (13, negotiation_contribution, option),
+ (14, rbf_gate, option),
+ });
+
+ // TLV 11 (the candidate list, each funding bundled with its contribution) is authoritative
+ // when present. It is omitted for a single non-contributory splice (TLV 3 holds its
+ // funding) and for data written by LDK 0.2 (which only ever wrote TLV 3 and tracked no
+ // contributions); in both cases the candidates carry no contribution.
+ let negotiated_candidates = negotiated_candidates.unwrap_or_else(|| {
+ legacy_negotiated_candidates
+ .unwrap_or_default()
+ .into_iter()
+ .map(|funding| NegotiatedCandidate { funding, contribution: None })
+ .collect()
+ });
+ // An in-flight contribution is only written alongside a surviving negotiation round, so a
+ // contribution without one is invalid.
+ if funding_negotiation.is_none() && negotiation_contribution.is_some() {
+ return Err(DecodeError::InvalidValue);
+ }
+ // TLV 14 (the RBF gate) is written exactly when there is more than one negotiation round, so
+ // pre-RBF readers (LDK 0.2) refuse an RBF they cannot operate. Current reconstructs RBF state
+ // from the candidate list, but a gate inconsistent with that state is invalid.
+ let is_rbf = negotiated_candidates.len() + usize::from(funding_negotiation.is_some()) > 1;
+ if rbf_gate.is_some() != is_rbf {
+ return Err(DecodeError::InvalidValue);
+ }
+
+ Ok(PendingFunding {
+ funding_negotiation,
+ negotiation_contribution,
+ negotiated_candidates,
+ sent_funding_txid,
+ received_funding_txid,
+ last_funding_feerate_sat_per_1000_weight,
+ })
}
}
@@ -3175,6 +3259,16 @@ impl FundingNegotiation {
}
impl PendingFunding {
+ /// Whether our contributions form a suffix of the negotiated candidates: once a round includes
+ /// our contribution, every later round carries it forward (so the splice intention is never
+ /// lost).
+ fn contributions_form_suffix(&self) -> bool {
+ self.negotiated_candidates
+ .iter()
+ .skip_while(|candidate| candidate.contribution.is_none())
+ .all(|candidate| candidate.contribution.is_some())
+ }
+
fn awaiting_ack_context(
&self, msg_name: &str,
) -> Result<(&FundingNegotiationContext, &PublicKey), ChannelError> {
@@ -3228,22 +3322,42 @@ impl PendingFunding {
feerate_sat_per_kw >= min_feerate
}
+ /// All stored contributions: those of the negotiated candidates followed by the in-flight
+ /// negotiation round's, if any.
+ fn contributions(&self) -> impl Iterator<Item = &FundingContribution> + '_ {
+ self.negotiated_candidates
+ .iter()
+ .filter_map(|candidate| candidate.contribution.as_ref())
+ .chain(self.negotiation_contribution.as_ref())
+ }
+
fn contributed_inputs(&self) -> impl Iterator<Item = bitcoin::OutPoint> + '_ {
- self.contributions.iter().flat_map(|c| c.contributed_inputs())
+ self.contributions().flat_map(|c| c.contributed_inputs())
}
fn contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ {
- self.contributions.iter().flat_map(|c| c.contributed_outputs())
+ self.contributions().flat_map(|c| c.contributed_outputs())
}
fn prior_contributed_inputs(&self) -> impl Iterator<Item = bitcoin::OutPoint> + '_ {
- let len = self.contributions.len();
- self.contributions[..len.saturating_sub(1)].iter().flat_map(|c| c.contributed_inputs())
+ self.negotiated_candidates
+ .iter()
+ .filter_map(|candidate| candidate.contribution.as_ref())
+ .flat_map(|c| c.contributed_inputs())
}
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())
+ self.negotiated_candidates
+ .iter()
+ .filter_map(|candidate| candidate.contribution.as_ref())
+ .flat_map(|c| c.contributed_outputs())
+ }
+
+ /// Our most recent contribution across rounds, including any round still under negotiation.
+ fn latest_contribution(&self) -> Option<&FundingContribution> {
+ self.negotiation_contribution.as_ref().or_else(|| {
+ self.negotiated_candidates.last().and_then(|candidate| candidate.contribution.as_ref())
+ })
}
fn check_get_splice_locked<SP: SignerProvider>(
@@ -3251,7 +3365,7 @@ impl PendingFunding {
) -> Option<msgs::SpliceLocked> {
debug_assert!(confirmed_funding_index < self.negotiated_candidates.len());
- let funding = &self.negotiated_candidates[confirmed_funding_index];
+ let funding = &self.negotiated_candidates[confirmed_funding_index].funding;
if !context.check_funding_meets_minimum_depth(funding, height) {
return None;
}
@@ -7274,8 +7388,9 @@ where
/// Builds a [`SpliceFundingFailed`] from a contribution, filtering out inputs/outputs
/// that are still committed to a prior splice round.
fn splice_funding_failed_for(&self, contribution: FundingContribution) -> SpliceFundingFailed {
- // The contribution was never pushed to `contributions`, so `contributed_inputs()` and
- // `contributed_outputs()` return only prior rounds' entries for filtering.
+ // The contribution was never stored in the pending splice state, so
+ // `contributed_inputs()` and `contributed_outputs()` return only prior rounds' entries
+ // for filtering.
splice_funding_failed_for!(self, contribution, contributed_inputs, contributed_outputs)
}
@@ -7318,12 +7433,15 @@ where
})
}
- fn pending_funding(&self) -> &[FundingScope] {
- if let Some(pending_splice) = &self.pending_splice {
- pending_splice.negotiated_candidates.as_slice()
- } else {
- &[]
- }
+ fn negotiated_candidates(&self) -> &[NegotiatedCandidate] {
+ self.pending_splice
+ .as_ref()
+ .map(|pending_splice| pending_splice.negotiated_candidates.as_slice())
+ .unwrap_or(&[])
+ }
+
+ fn pending_funding(&self) -> impl ExactSizeIterator<Item = &FundingScope> + '_ {
+ self.negotiated_candidates().iter().map(|candidate| &candidate.funding)
}
fn funding_and_pending_funding_iter_mut(&mut self) -> impl Iterator<Item = &mut FundingScope> {
@@ -7332,7 +7450,8 @@ where
.as_mut()
.map(|pending_splice| pending_splice.negotiated_candidates.as_mut_slice())
.unwrap_or(&mut [])
- .iter_mut(),
+ .iter_mut()
+ .map(|candidate| &mut candidate.funding),
)
}
@@ -7419,7 +7538,7 @@ where
"reset_pending_splice_state requires an active funding negotiation"
);
pending_splice.funding_negotiation.take();
- let contribution = pending_splice.contributions.pop();
+ let contribution = pending_splice.negotiation_contribution.take();
if let Some(ref contribution) = contribution {
debug_assert!(
pending_splice
@@ -7430,13 +7549,13 @@ where
);
}
- // After pop, `contributed_inputs()` / `contributed_outputs()` return only prior
- // rounds for filtering.
+ // With the in-flight contribution taken, `contributed_inputs()` /
+ // `contributed_outputs()` return only prior rounds' entries for filtering.
let splice_funding_failed = contribution.map(|contribution| {
splice_funding_failed_for!(self, contribution, contributed_inputs, contributed_outputs)
});
- if self.pending_funding().is_empty() {
+ if self.negotiated_candidates().is_empty() {
self.pending_splice.take();
}
@@ -7458,7 +7577,7 @@ where
pending_splice.funding_negotiation.is_some(),
"maybe_splice_funding_failed requires an active funding negotiation"
);
- let contribution = pending_splice.contributions.last().cloned()?;
+ let contribution = pending_splice.negotiation_contribution.clone()?;
Some(splice_funding_failed_for!(
self,
contribution,
@@ -8090,7 +8209,7 @@ where
}
core::iter::once(&self.funding)
- .chain(self.pending_funding().iter())
+ .chain(self.pending_funding())
.try_for_each(|funding| self.context.validate_update_add_htlc(funding, msg, fee_estimator))?;
// Now update local state:
@@ -8522,7 +8641,7 @@ where
let funding_contribution = self
.pending_splice
.as_ref()
- .and_then(|pending_splice| pending_splice.contributions.last())
+ .and_then(|pending_splice| pending_splice.negotiation_contribution.as_ref())
.cloned();
log_info!(
@@ -8585,7 +8704,7 @@ where
) -> Result<Option<ChannelMonitorUpdate>, ChannelError> {
self.commitment_signed_check_state()?;
- if !self.pending_funding().is_empty() {
+ if !self.negotiated_candidates().is_empty() {
return Err(ChannelError::close(
"Got a single commitment_signed message when expecting a batch".to_owned(),
));
@@ -8662,7 +8781,7 @@ where
// pending splice transaction has confirmed since receiving the batch.
let mut commitment_txs = Vec::with_capacity(self.pending_funding().len() + 1);
let mut htlc_data = None;
- for funding in core::iter::once(&self.funding).chain(self.pending_funding().iter()) {
+ for funding in core::iter::once(&self.funding).chain(self.pending_funding()) {
let funding_txid =
funding.get_funding_txid().expect("Funding txid must be known for pending scope");
let msg = messages.get(&funding_txid).ok_or_else(|| {
@@ -9542,7 +9661,14 @@ where
.map(|signing_session| signing_session.has_local_contribution())
.unwrap_or(false);
- pending_splice.negotiated_candidates.push(funding);
+ let contribution = pending_splice.negotiation_contribution.take();
+ pending_splice
+ .negotiated_candidates
+ .push(NegotiatedCandidate { funding, contribution });
+ debug_assert!(
+ pending_splice.contributions_form_suffix(),
+ "a round following one we contributed to must carry our contribution",
+ );
let splice_negotiated = SpliceFundingNegotiated {
funding_txo: funding_txo.into_bitcoin_outpoint(),
@@ -9566,29 +9692,21 @@ where
);
}
- let contrib_offset = pending_splice
- .negotiated_candidates
- .len()
- .saturating_sub(pending_splice.contributions.len());
let candidates = pending_splice
.negotiated_candidates
.iter()
- .enumerate()
- .map(|(i, funding)| {
- let txid = funding
+ .map(|candidate| {
+ let txid = candidate
+ .funding
.get_funding_txid()
.expect("negotiated candidates should have a funding txid");
- let contribution = i
- .checked_sub(contrib_offset)
- .and_then(|j| pending_splice.contributions.get(j))
- .cloned();
FundingCandidate {
txid,
channels: vec![ChannelFunding {
counterparty_node_id: self.context.counterparty_node_id,
channel_id: self.context.channel_id,
purpose: FundingPurpose::Splice,
- contribution,
+ contribution: candidate.contribution.clone(),
}],
}
})
@@ -9749,7 +9867,7 @@ where
debug_assert!(!self.funding.get_channel_type().supports_anchor_zero_fee_commitments());
let can_send_update_fee = core::iter::once(&self.funding)
- .chain(self.pending_funding().iter())
+ .chain(self.pending_funding())
.all(|funding| self.context.can_send_update_fee(funding, feerate_per_kw, fee_estimator, logger));
if !can_send_update_fee {
return None;
@@ -10100,7 +10218,7 @@ where
}
core::iter::once(&self.funding)
- .chain(self.pending_funding().iter())
+ .chain(self.pending_funding())
.try_for_each(|funding| FundedChannel::<SP>::check_remote_fee(funding.get_channel_type(), fee_estimator, msg.feerate_per_kw, Some(self.context.feerate_per_kw), logger))?;
self.context.pending_update_fee = Some((msg.feerate_per_kw, FeeUpdateState::RemoteAnnounced));
@@ -10808,7 +10926,6 @@ where
// for this `txid`.
let inferred_splice_locked = msg.my_current_funding_locked.as_ref().and_then(|funding_locked| {
self.pending_funding()
- .iter()
.find(|funding| funding.get_funding_txid() == Some(funding_locked.txid))
.and_then(|_| {
self.pending_splice.as_ref().and_then(|pending_splice| {
@@ -11605,7 +11722,7 @@ where
);
core::iter::once(&self.funding)
- .chain(self.pending_funding().iter())
+ .chain(self.pending_funding())
.try_for_each(|funding| self.context.can_accept_incoming_htlc(funding, dust_exposure_limiting_feerate, &logger))
}
@@ -11923,6 +12040,7 @@ where
let funding = pending_splice
.negotiated_candidates
.iter_mut()
+ .map(|candidate| &mut candidate.funding)
.find(|funding| funding.get_funding_txid() == Some(splice_txid))
.unwrap();
@@ -11937,9 +12055,12 @@ where
.funding_transaction
.as_ref()
.expect("Promoted splice funding should have a funding transaction");
- let contributions = core::mem::take(&mut pending_splice.contributions);
- contributions
+ let candidates = core::mem::take(&mut pending_splice.negotiated_candidates);
+ let negotiation_contribution = pending_splice.negotiation_contribution.take();
+ candidates
.into_iter()
+ .filter_map(|candidate| candidate.contribution)
+ .chain(negotiation_contribution)
.filter_map(|contribution| {
contribution.into_unique_contributions(
promoted_tx.input.iter().map(|i| i.previous_output),
@@ -12028,7 +12149,9 @@ where
let mut confirmed_funding_index = None;
let mut funding_already_confirmed = false;
- for (index, funding) in pending_splice.negotiated_candidates.iter_mut().enumerate() {
+ let candidates =
+ pending_splice.negotiated_candidates.iter_mut().map(|candidate| &mut candidate.funding);
+ for (index, funding) in candidates.enumerate() {
if self.context.check_for_funding_tx_confirmed(
funding, block_hash, height, index_in_block, &mut confirmed_tx, logger,
)? {
@@ -12188,7 +12311,8 @@ where
if let Some(pending_splice) = &mut self.pending_splice {
let mut confirmed_funding_index = None;
- for (index, funding) in pending_splice.negotiated_candidates.iter().enumerate() {
+ let candidates = pending_splice.negotiated_candidates.iter().map(|candidate| &candidate.funding);
+ for (index, funding) in candidates.enumerate() {
if funding.funding_tx_confirmation_height != 0 {
if confirmed_funding_index.is_some() {
let err_reason = "splice tx of another pending funding already confirmed";
@@ -12200,7 +12324,8 @@ where
}
if let Some(confirmed_funding_index) = confirmed_funding_index {
- let funding = &mut pending_splice.negotiated_candidates[confirmed_funding_index];
+ let funding =
+ &mut pending_splice.negotiated_candidates[confirmed_funding_index].funding;
// Check if the splice funding transaction was unconfirmed
if funding.get_funding_tx_confirmations(height) == 0 {
@@ -12256,7 +12381,7 @@ where
pub fn get_relevant_txids(&self) -> impl Iterator<Item = (Txid, u32, Option<BlockHash>)> + '_ {
core::iter::once(&self.funding)
- .chain(self.pending_funding().iter())
+ .chain(self.pending_funding())
.map(|funding| {
(
funding.get_funding_txid(),
@@ -12706,15 +12831,7 @@ where
);
let min_rbf_feerate = prev_feerate.map(min_rbf_feerate);
let prior = if pending_splice.last_funding_feerate_sat_per_1000_weight.is_some() {
- if let Some(prior) = self
- .pending_splice
- .as_ref()
- .and_then(|pending_splice| pending_splice.contributions.last())
- {
- Some(prior.clone())
- } else {
- None
- }
+ pending_splice.latest_contribution().cloned()
} else {
None
};
@@ -12981,7 +13098,9 @@ where
}
}
- fn send_splice_init(&mut self, context: FundingNegotiationContext) -> msgs::SpliceInit {
+ fn send_splice_init(
+ &mut self, context: FundingNegotiationContext, contribution: FundingContribution,
+ ) -> msgs::SpliceInit {
debug_assert!(self.pending_splice.is_none());
// Rotate the funding pubkey using the prev_funding_txid as a tweak
let prev_funding_txid = self.funding.get_funding_txid();
@@ -13004,11 +13123,11 @@ where
FundingNegotiation::AwaitingAck { context, new_holder_funding_key: funding_pubkey };
self.pending_splice = Some(PendingFunding {
funding_negotiation: Some(funding_negotiation),
+ negotiation_contribution: Some(contribution),
negotiated_candidates: vec![],
sent_funding_txid: None,
received_funding_txid: None,
last_funding_feerate_sat_per_1000_weight: None,
- contributions: vec![],
});
msgs::SpliceInit {
@@ -13021,7 +13140,9 @@ where
}
}
- fn send_tx_init_rbf(&mut self, context: FundingNegotiationContext) -> msgs::TxInitRbf {
+ fn send_tx_init_rbf(
+ &mut self, context: FundingNegotiationContext, contribution: FundingContribution,
+ ) -> msgs::TxInitRbf {
let pending_splice =
self.pending_splice.as_mut().expect("pending_splice should exist for RBF");
debug_assert!(!pending_splice.negotiated_candidates.is_empty());
@@ -13030,6 +13151,7 @@ where
.negotiated_candidates
.first()
.unwrap()
+ .funding
.get_holder_pubkeys()
.funding_pubkey;
@@ -13039,6 +13161,7 @@ where
pending_splice.funding_negotiation =
Some(FundingNegotiation::AwaitingAck { context, new_holder_funding_key });
+ pending_splice.negotiation_contribution = Some(contribution);
msgs::TxInitRbf {
channel_id: self.context.channel_id,
@@ -13376,11 +13499,11 @@ where
);
self.pending_splice = Some(PendingFunding {
funding_negotiation: Some(funding_negotiation),
+ negotiation_contribution: adjusted_contribution,
negotiated_candidates: Vec::new(),
received_funding_txid: None,
sent_funding_txid: None,
last_funding_feerate_sat_per_1000_weight: None,
- contributions: adjusted_contribution.into_iter().collect(),
});
Ok(msgs::SpliceAck {
@@ -13462,8 +13585,8 @@ where
// Reuse funding pubkeys from the last negotiated candidate since all RBF candidates
// for the same splice share the same funding output script.
Ok((
- last_candidate.get_holder_pubkeys().clone(),
- *last_candidate.counterparty_funding_pubkey(),
+ last_candidate.funding.get_holder_pubkeys().clone(),
+ *last_candidate.funding.counterparty_funding_pubkey(),
))
}
@@ -13487,7 +13610,7 @@ where
} else if let Some(prior) = self
.pending_splice
.as_ref()
- .and_then(|pending_splice| pending_splice.contributions.last())
+ .and_then(|pending_splice| pending_splice.latest_contribution())
{
let net_value = holder_balance
.ok_or_else(|| ChannelError::Abort(AbortReason::InsufficientRbfFeerate))
@@ -13534,16 +13657,14 @@ where
self.pending_splice
.as_mut()
.expect("pending_splice is Some")
- .contributions
- .push(adjusted_contribution.clone());
+ .negotiation_contribution = Some(adjusted_contribution.clone());
adjusted_contribution.into_tx_parts()
} else if prior_net_value.is_some() {
let prior_contribution = self
.pending_splice
.as_ref()
.expect("pending_splice is Some")
- .contributions
- .last()
+ .latest_contribution()
.expect("prior_net_value was Some")
.clone();
let adjusted_contribution = prior_contribution
@@ -13552,8 +13673,7 @@ where
self.pending_splice
.as_mut()
.expect("pending_splice is Some")
- .contributions
- .push(adjusted_contribution.clone());
+ .negotiation_contribution = Some(adjusted_contribution.clone());
adjusted_contribution.into_tx_parts()
} else {
Default::default()
@@ -13613,8 +13733,8 @@ where
"No pending splice available to RBF".into(),
))
})?;
- let holder_pubkeys = last_candidate.get_holder_pubkeys().clone();
- let counterparty_funding_pubkey = *last_candidate.counterparty_funding_pubkey();
+ let holder_pubkeys = last_candidate.funding.get_holder_pubkeys().clone();
+ let counterparty_funding_pubkey = *last_candidate.funding.counterparty_funding_pubkey();
let new_funding = self
.validate_splice_contributions(
@@ -13877,7 +13997,7 @@ where
if !pending_splice
.negotiated_candidates
.iter()
- .any(|funding| funding.get_funding_txid() == Some(msg.splice_txid))
+ .any(|candidate| candidate.funding.get_funding_txid() == Some(msg.splice_txid))
{
let err = "unknown splice funding txid";
return Err(ChannelError::close(err.to_string()));
@@ -14075,7 +14195,7 @@ where
&self, fee_estimator: &LowerBoundedFeeEstimator<F>,
) -> Result<AvailableBalances, ()> {
let init = self.context.get_available_balances_for_scope(&self.funding, fee_estimator)?;
- self.pending_funding().iter().try_fold(init, |acc, funding| {
+ self.pending_funding().try_fold(init, |acc, funding| {
let e = self.context.get_available_balances_for_scope(funding, fee_estimator)?;
Ok(AvailableBalances {
inbound_capacity_msat: acc.inbound_capacity_msat.min(e.inbound_capacity_msat),
@@ -14137,7 +14257,7 @@ where
}
self.context.resend_order = RAACommitmentOrder::RevokeAndACKFirst;
- let update = if self.pending_funding().is_empty() {
+ let update = if self.negotiated_candidates().is_empty() {
let (htlcs_ref, counterparty_commitment_tx) =
self.build_commitment_no_state_update(&self.funding, logger);
let htlc_outputs = htlcs_ref
@@ -14168,7 +14288,7 @@ where
} else {
let mut htlc_data = None;
let commitment_txs = core::iter::once(&self.funding)
- .chain(self.pending_funding().iter())
+ .chain(self.pending_funding())
.map(|funding| {
let (htlcs_ref, counterparty_commitment_tx) =
self.build_commitment_no_state_update(funding, logger);
@@ -14230,7 +14350,7 @@ where
&self, logger: &L,
) -> Result<Vec<msgs::CommitmentSigned>, ChannelError> {
core::iter::once(&self.funding)
- .chain(self.pending_funding().iter())
+ .chain(self.pending_funding())
.map(|funding| self.send_commitment_no_state_update_for_funding(funding, logger))
.collect::<Result<Vec<_>, ChannelError>>()
}
@@ -14699,16 +14819,12 @@ where
),
));
}
- let tx_init_rbf = self.send_tx_init_rbf(context);
- self.pending_splice.as_mut().unwrap()
- .contributions.push(prior_contribution);
+ let tx_init_rbf = self.send_tx_init_rbf(context, prior_contribution);
return Ok(Some(StfuResponse::TxInitRbf(tx_init_rbf)));
}
- let splice_init = self.send_splice_init(context);
+ let splice_init = self.send_splice_init(context, prior_contribution);
debug_assert!(self.pending_splice.is_some());
- self.pending_splice.as_mut().unwrap()
- .contributions.push(prior_contribution);
return Ok(Some(StfuResponse::SpliceInit(splice_init)));
},
#[cfg(any(test, fuzzing, feature = "_test_utils"))]
@@ -16371,7 +16487,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
// resumed on reestablishment, but keep any already-negotiated candidates.
let reset_funding_negotiation = self.should_reset_pending_splice_state(true);
let should_persist_pending_splice =
- !reset_funding_negotiation || !self.pending_funding().is_empty();
+ !reset_funding_negotiation || !self.negotiated_candidates().is_empty();
let pending_splice = should_persist_pending_splice
.then(|| ())
.and_then(|_| self.pending_splice.as_ref())
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.