Add FundingContribution to SpliceFailed event
What changed, and why it matters
This commit changes the information returned to users when a Lightning channel splice attempt fails. Instead of reporting an abandoned funding transaction outpoint and channel type, the library now returns the full funding contribution object from the failed round. This lets users retry the splice more easily or decide whether to bump the fee. It also suppresses empty 'discard your inputs' events when nothing actually needs discarding. There is no direct security vulnerability here; it is a usability and API-correctness improvement.
Treat as a normal API/behavior change. Review downstream event handlers that match on Event::SpliceFailed, because the abandoned_funding_txo and channel_type fields are removed and replaced by contribution. Ensure persistence compatibility if older serialized events are replayed: the reader maps missing fields to None and defaults reason to Unknown, which is safe. No security patch or incident response is indicated.
Security signals we found
API surface change: public Event::SpliceFailed fields altered
Public method FundingContribution::feerate() newly exposed
Serialization format change for Event::SpliceFailed (TLV field renumbering)
Behavioral change: empty DiscardFunding events suppressed
No new cryptographic operations, network parsing, or permission checks introduced
Evidence from the diff
The patch refactors Event::SpliceFailed to carry an Option
Changed components
lightning/src/events/mod.rslightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/functional_test_utils.rslightning/src/ln/splicing_tests.rsInspect captured patch +201 / −234
diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs
index 0c99ee0..5a52be0 100644
--- a/lightning/src/events/mod.rs
+++ b/lightning/src/events/mod.rs
@@ -25,6 +25,7 @@ use crate::blinded_path::payment::{
use crate::chain::transaction;
use crate::ln::channel::FUNDING_CONF_DEADLINE_BLOCKS;
use crate::ln::channelmanager::{InterceptId, PaymentId};
+use crate::ln::funding::FundingContribution;
use crate::ln::msgs;
use crate::ln::onion_utils::LocalHTLCFailureReason;
use crate::ln::outbound_payment::RecipientOnionFields;
@@ -1664,19 +1665,20 @@ pub enum Event {
/// The witness script that is used to lock the channel's funding output to commitment transactions.
new_funding_redeem_script: ScriptBuf,
},
- /// Used to indicate that a splice for the given `channel_id` has failed.
+ /// Used to indicate that a splice negotiation round for the given `channel_id` has failed.
///
- /// This event may be emitted if a splice fails after it has been initiated but prior to signing
- /// any negotiated funding transaction.
+ /// Each splice attempt (initial or RBF) resolves to either [`Event::SplicePending`] on
+ /// success or this event on failure. Prior successfully negotiated splice transactions are
+ /// unaffected.
///
- /// Any UTXOs contributed to be spent by the funding transaction may be reused and will be
- /// given in `contributed_inputs`.
+ /// Any UTXOs contributed to the failed round that are not committed to a prior negotiated
+ /// splice transaction will be returned via a preceding [`Event::DiscardFunding`].
///
/// # Failure Behavior and Persistence
/// This event will eventually be replayed after failures-to-handle (i.e., the event handler
/// returning `Err(ReplayEvent ())`) and will be persisted across restarts.
SpliceFailed {
- /// The `channel_id` of the channel for which the splice failed.
+ /// The `channel_id` of the channel for which the splice negotiation round failed.
channel_id: ChannelId,
/// The `user_channel_id` value passed in to [`ChannelManager::create_channel`] for outbound
/// channels, or to [`ChannelManager::accept_inbound_channel`] for inbound channels.
@@ -1686,12 +1688,17 @@ pub enum Event {
user_channel_id: u128,
/// The `node_id` of the channel counterparty.
counterparty_node_id: PublicKey,
- /// The outpoint of the channel's splice funding transaction, if one was created.
- abandoned_funding_txo: Option<OutPoint>,
- /// The features that this channel will operate with, if available.
- channel_type: Option<ChannelTypeFeatures>,
/// The reason the splice negotiation failed.
reason: NegotiationFailureReason,
+ /// The funding contribution from the failed negotiation round, if available. This can be
+ /// fed back to [`ChannelManager::funding_contributed`] to retry with the same parameters.
+ /// Alternatively, call [`ChannelManager::splice_channel`] to obtain a fresh
+ /// [`FundingTemplate`] and build a new contribution.
+ ///
+ /// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed
+ /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
+ /// [`FundingTemplate`]: crate::ln::funding::FundingTemplate
+ contribution: Option<FundingContribution>,
},
/// Used to indicate to the user that they can abandon the funding transaction and recycle the
/// inputs for another purpose.
@@ -2483,18 +2490,16 @@ impl Writeable for Event {
ref channel_id,
ref user_channel_id,
ref counterparty_node_id,
- ref abandoned_funding_txo,
- ref channel_type,
ref reason,
+ ref contribution,
} => {
52u8.write(writer)?;
write_tlv_fields!(writer, {
(1, channel_id, required),
- (3, channel_type, option),
(5, user_channel_id, required),
(7, counterparty_node_id, required),
- (9, abandoned_funding_txo, option),
(11, reason, required),
+ (13, contribution, option),
});
},
// Note that, going forward, all new events must only write data inside of
@@ -3135,20 +3140,18 @@ impl MaybeReadable for Event {
let mut f = || {
_init_and_read_len_prefixed_tlv_fields!(reader, {
(1, channel_id, required),
- (3, channel_type, option),
(5, user_channel_id, required),
(7, counterparty_node_id, required),
- (9, abandoned_funding_txo, option),
(11, reason, upgradable_option),
+ (13, contribution, option),
});
Ok(Some(Event::SpliceFailed {
channel_id: channel_id.0.unwrap(),
user_channel_id: user_channel_id.0.unwrap(),
counterparty_node_id: counterparty_node_id.0.unwrap(),
- abandoned_funding_txo,
- channel_type,
reason: reason.unwrap_or(NegotiationFailureReason::Unknown),
+ contribution,
}))
};
f()
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index ad643a1..473fc6b 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -7051,17 +7051,33 @@ pub struct SpliceFundingNegotiated {
/// Information about a splice funding negotiation that has failed.
pub struct SpliceFundingFailed {
- /// The outpoint of the channel's splice funding transaction, if one was created.
- pub funding_txo: Option<bitcoin::OutPoint>,
+ /// UTXOs spent as inputs contributed to the splice transaction. Excludes inputs already
+ /// contributed in prior rounds, which may be included in `contribution`.
+ contributed_inputs: Vec<bitcoin::OutPoint>,
- /// The features that this channel will operate with, if available.
- pub channel_type: Option<ChannelTypeFeatures>,
+ /// Outputs contributed to the splice transaction. Excludes outputs already contributed
+ /// in prior rounds, which may be included in `contribution`.
+ contributed_outputs: Vec<bitcoin::TxOut>,
- /// UTXOs spent as inputs contributed to the splice transaction.
- pub contributed_inputs: Vec<bitcoin::OutPoint>,
+ /// The funding contribution from the failed round, if available.
+ contribution: Option<FundingContribution>,
+}
- /// Outputs contributed to the splice transaction.
- pub contributed_outputs: Vec<bitcoin::TxOut>,
+impl SpliceFundingFailed {
+ /// Splits into the funding info for `DiscardFunding` (if there are inputs or outputs to
+ /// discard) and the contribution for `SpliceFailed`.
+ pub(super) fn into_parts(self) -> (Option<FundingInfo>, Option<FundingContribution>) {
+ let funding_info =
+ if !self.contributed_inputs.is_empty() || !self.contributed_outputs.is_empty() {
+ Some(FundingInfo::Contribution {
+ inputs: self.contributed_inputs,
+ outputs: self.contributed_outputs,
+ })
+ } else {
+ None
+ };
+ (funding_info, self.contribution)
+ }
}
macro_rules! maybe_create_splice_funding_failed {
@@ -7071,15 +7087,6 @@ macro_rules! maybe_create_splice_funding_failed {
.and_then(|funding_negotiation| {
let is_initiator = funding_negotiation.is_initiator();
- let funding_txo = funding_negotiation
- .as_funding()
- .and_then(|funding| funding.get_funding_txo())
- .map(|txo| txo.into_bitcoin_outpoint());
-
- let channel_type = funding_negotiation
- .as_funding()
- .map(|funding| funding.get_channel_type().clone());
-
let (mut contributed_inputs, mut contributed_outputs) = match funding_negotiation {
FundingNegotiation::AwaitingAck { context, .. } => {
context.$contributed_inputs_and_outputs()
@@ -7110,12 +7117,10 @@ macro_rules! maybe_create_splice_funding_failed {
return None;
}
- Some(SpliceFundingFailed {
- funding_txo,
- channel_type,
- contributed_inputs,
- contributed_outputs,
- })
+ let contribution =
+ $pending_splice_ref.and_then(|ps| ps.contributions.last().cloned());
+
+ Some(SpliceFundingFailed { contributed_inputs, contributed_outputs, contribution })
})
}};
}
@@ -7146,6 +7151,7 @@ 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 {
+ let cloned_contribution = contribution.clone();
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() {
@@ -7156,10 +7162,9 @@ where
}
}
SpliceFundingFailed {
- funding_txo: None,
- channel_type: None,
contributed_inputs: inputs,
contributed_outputs: outputs,
+ contribution: Some(cloned_contribution),
}
}
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index ef2ce9a..2d6aaa5 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -61,8 +61,8 @@ use crate::ln::channel::QuiescentError;
use crate::ln::channel::{
self, hold_time_since, Channel, ChannelError, ChannelUpdateStatus, DisconnectResult,
FundedChannel, FundingTxSigned, InboundV1Channel, InteractiveTxMsgError, OutboundHop,
- OutboundV1Channel, PendingV2Channel, ReconnectionMsg, ShutdownResult, SpliceFundingFailed,
- StfuResponse, UpdateFulfillCommitFetch, WithChannelContext,
+ OutboundV1Channel, PendingV2Channel, ReconnectionMsg, ShutdownResult, StfuResponse,
+ UpdateFulfillCommitFetch, WithChannelContext,
};
use crate::ln::channel_state::ChannelDetails;
use crate::ln::funding::{FundingContribution, FundingTemplate};
@@ -4161,28 +4161,27 @@ impl<
failed_htlcs = htlcs;
if let Some(splice_funding_failed) = splice_funding_failed {
+ let (funding_info, contribution) = splice_funding_failed.into_parts();
let mut pending_events = self.pending_events.lock().unwrap();
pending_events.push_back((
events::Event::SpliceFailed {
channel_id: *chan_id,
counterparty_node_id: *counterparty_node_id,
user_channel_id: chan.context().get_user_id(),
- abandoned_funding_txo: splice_funding_failed.funding_txo,
- channel_type: splice_funding_failed.channel_type,
+ contribution,
reason: events::NegotiationFailureReason::ChannelClosing,
},
None,
));
- pending_events.push_back((
- events::Event::DiscardFunding {
- channel_id: *chan_id,
- funding_info: FundingInfo::Contribution {
- inputs: splice_funding_failed.contributed_inputs,
- outputs: splice_funding_failed.contributed_outputs,
+ if let Some(funding_info) = funding_info {
+ pending_events.push_back((
+ events::Event::DiscardFunding {
+ channel_id: *chan_id,
+ funding_info,
},
- },
- None,
- ));
+ None,
+ ));
+ }
}
// We can send the `shutdown` message before updating the `ChannelMonitor`
@@ -4469,27 +4468,26 @@ impl<
));
if let Some(splice_funding_failed) = shutdown_res.splice_funding_failed.take() {
+ let (funding_info, contribution) = splice_funding_failed.into_parts();
pending_events.push_back((
events::Event::SpliceFailed {
channel_id: shutdown_res.channel_id,
counterparty_node_id: shutdown_res.counterparty_node_id,
user_channel_id: shutdown_res.user_channel_id,
- abandoned_funding_txo: splice_funding_failed.funding_txo,
- channel_type: splice_funding_failed.channel_type,
+ contribution,
reason: events::NegotiationFailureReason::ChannelClosing,
},
None,
));
- pending_events.push_back((
- events::Event::DiscardFunding {
- channel_id: shutdown_res.channel_id,
- funding_info: FundingInfo::Contribution {
- inputs: splice_funding_failed.contributed_inputs,
- outputs: splice_funding_failed.contributed_outputs,
+ if let Some(funding_info) = funding_info {
+ pending_events.push_back((
+ events::Event::DiscardFunding {
+ channel_id: shutdown_res.channel_id,
+ funding_info,
},
- },
- None,
- ));
+ None,
+ ));
+ }
}
if let Some(transaction) = shutdown_res.unbroadcasted_funding_tx {
@@ -4975,28 +4973,27 @@ impl<
});
if let Some(splice_funding_failed) = splice_funding_failed {
+ let (funding_info, contribution) = splice_funding_failed.into_parts();
let pending_events = &mut self.pending_events.lock().unwrap();
pending_events.push_back((
events::Event::SpliceFailed {
channel_id: *channel_id,
counterparty_node_id: *counterparty_node_id,
user_channel_id: chan.context.get_user_id(),
- abandoned_funding_txo: splice_funding_failed.funding_txo,
- channel_type: splice_funding_failed.channel_type,
+ contribution,
reason: events::NegotiationFailureReason::LocallyAbandoned,
},
None,
));
- pending_events.push_back((
- events::Event::DiscardFunding {
- channel_id: *channel_id,
- funding_info: FundingInfo::Contribution {
- inputs: splice_funding_failed.contributed_inputs,
- outputs: splice_funding_failed.contributed_outputs,
+ if let Some(funding_info) = funding_info {
+ pending_events.push_back((
+ events::Event::DiscardFunding {
+ channel_id: *channel_id,
+ funding_info,
},
- },
- None,
- ));
+ None,
+ ));
+ }
}
Ok(())
@@ -6676,36 +6673,22 @@ impl<
));
}
},
- QuiescentError::FailSplice(
- SpliceFundingFailed {
- funding_txo,
- channel_type,
- contributed_inputs,
- contributed_outputs,
- },
- reason,
- ) => {
+ QuiescentError::FailSplice(splice_funding_failed, reason) => {
+ let (funding_info, contribution) = splice_funding_failed.into_parts();
let pending_events = &mut self.pending_events.lock().unwrap();
pending_events.push_back((
events::Event::SpliceFailed {
channel_id,
counterparty_node_id,
user_channel_id,
- abandoned_funding_txo: funding_txo,
- channel_type,
reason,
+ contribution,
},
None,
));
- if !contributed_inputs.is_empty() || !contributed_outputs.is_empty() {
+ if let Some(funding_info) = funding_info {
pending_events.push_back((
- events::Event::DiscardFunding {
- channel_id,
- funding_info: FundingInfo::Contribution {
- inputs: contributed_inputs,
- outputs: contributed_outputs,
- },
- },
+ events::Event::DiscardFunding { channel_id, funding_info },
None,
));
}
@@ -11982,30 +11965,24 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
user_channel_id: u128,
) -> MsgHandleErrInternal {
if let Some(splice_funding_failed) = err.splice_funding_failed {
+ let (funding_info, contribution) = splice_funding_failed.into_parts();
let pending_events = &mut self.pending_events.lock().unwrap();
pending_events.push_back((
events::Event::SpliceFailed {
channel_id,
counterparty_node_id: *counterparty_node_id,
user_channel_id,
- abandoned_funding_txo: splice_funding_failed.funding_txo,
- channel_type: splice_funding_failed.channel_type.clone(),
+ contribution,
reason: events::NegotiationFailureReason::NegotiationError {
msg: format!("{:?}", err.err),
},
},
None,
));
- pending_events.push_back((
- events::Event::DiscardFunding {
- channel_id,
- funding_info: FundingInfo::Contribution {
- inputs: splice_funding_failed.contributed_inputs,
- outputs: splice_funding_failed.contributed_outputs,
- },
- },
- None,
- ));
+ if let Some(funding_info) = funding_info {
+ pending_events
+ .push_back((events::Event::DiscardFunding { channel_id, funding_info }, None));
+ }
}
MsgHandleErrInternal::from_chan_no_close(err.err, channel_id)
}
@@ -12321,14 +12298,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
}
if let Some(splice_funding_failed) = splice_failed {
+ let (funding_info, contribution) = splice_funding_failed.into_parts();
let pending_events = &mut self.pending_events.lock().unwrap();
pending_events.push_back((
events::Event::SpliceFailed {
channel_id: msg.channel_id,
counterparty_node_id: *counterparty_node_id,
user_channel_id: chan_entry.get().context().get_user_id(),
- abandoned_funding_txo: splice_funding_failed.funding_txo,
- channel_type: splice_funding_failed.channel_type,
+ contribution,
reason: events::NegotiationFailureReason::CounterpartyAborted {
msg: UntrustedString(
String::from_utf8_lossy(&msg.data).to_string(),
@@ -12337,16 +12314,15 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
},
None,
));
- pending_events.push_back((
- events::Event::DiscardFunding {
- channel_id: msg.channel_id,
- funding_info: FundingInfo::Contribution {
- inputs: splice_funding_failed.contributed_inputs,
- outputs: splice_funding_failed.contributed_outputs,
+ if let Some(funding_info) = funding_info {
+ pending_events.push_back((
+ events::Event::DiscardFunding {
+ channel_id: msg.channel_id,
+ funding_info,
},
- },
- None,
- ));
+ None,
+ ));
+ }
}
let holding_cell_res = if needs_holding_cell_release {
@@ -12474,28 +12450,27 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
dropped_htlcs = htlcs;
if let Some(splice_funding_failed) = splice_funding_failed {
+ let (funding_info, contribution) = splice_funding_failed.into_parts();
let mut pending_events = self.pending_events.lock().unwrap();
pending_events.push_back((
events::Event::SpliceFailed {
channel_id: msg.channel_id,
counterparty_node_id: *counterparty_node_id,
user_channel_id: chan.context().get_user_id(),
- abandoned_funding_txo: splice_funding_failed.funding_txo,
- channel_type: splice_funding_failed.channel_type,
+ contribution,
reason: events::NegotiationFailureReason::ChannelClosing,
},
None,
));
- pending_events.push_back((
- events::Event::DiscardFunding {
- channel_id: msg.channel_id,
- funding_info: FundingInfo::Contribution {
- inputs: splice_funding_failed.contributed_inputs,
- outputs: splice_funding_failed.contributed_outputs,
+ if let Some(funding_info) = funding_info {
+ pending_events.push_back((
+ events::Event::DiscardFunding {
+ channel_id: msg.channel_id,
+ funding_info,
},
- },
- None,
- ));
+ None,
+ ));
+ }
}
if let Some(msg) = shutdown {
@@ -15553,21 +15528,20 @@ impl<
chan.peer_disconnected_is_resumable(&&logger);
if let Some(splice_funding_failed) = splice_funding_failed {
+ let (funding_info, contribution) = splice_funding_failed.into_parts();
splice_failed_events.push(events::Event::SpliceFailed {
channel_id: chan.context().channel_id(),
counterparty_node_id,
user_channel_id: chan.context().get_user_id(),
- abandoned_funding_txo: splice_funding_failed.funding_txo,
- channel_type: splice_funding_failed.channel_type,
+ contribution,
reason: events::NegotiationFailureReason::PeerDisconnected,
});
- splice_failed_events.push(events::Event::DiscardFunding {
- channel_id: chan.context().channel_id(),
- funding_info: FundingInfo::Contribution {
- inputs: splice_funding_failed.contributed_inputs,
- outputs: splice_funding_failed.contributed_outputs,
- },
- });
+ if let Some(funding_info) = funding_info {
+ splice_failed_events.push(events::Event::DiscardFunding {
+ channel_id: chan.context().channel_id(),
+ funding_info,
+ });
+ }
}
if is_resumable {
@@ -18181,27 +18155,26 @@ impl<
for peer_state in peer_states.iter() {
for chan in peer_state.channel_by_id.values().filter_map(Channel::as_funded) {
if let Some(splice_funding_failed) = chan.maybe_splice_funding_failed() {
+ let (funding_info, contribution) = splice_funding_failed.into_parts();
events.push_back((
events::Event::SpliceFailed {
channel_id: chan.context.channel_id(),
counterparty_node_id: chan.context.get_counterparty_node_id(),
user_channel_id: chan.context.get_user_id(),
- abandoned_funding_txo: splice_funding_failed.funding_txo,
- channel_type: splice_funding_failed.channel_type,
reason: events::NegotiationFailureReason::PeerDisconnected,
+ contribution,
},
None,
));
- events.push_back((
- events::Event::DiscardFunding {
- channel_id: chan.context().channel_id(),
- funding_info: FundingInfo::Contribution {
- inputs: splice_funding_failed.contributed_inputs,
- outputs: splice_funding_failed.contributed_outputs,
+ if let Some(funding_info) = funding_info {
+ events.push_back((
+ events::Event::DiscardFunding {
+ channel_id: chan.context().channel_id(),
+ funding_info,
},
- },
- None,
- ));
+ None,
+ ));
+ }
}
}
}
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index b8ef589..df16171 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -3237,9 +3237,10 @@ pub fn expect_splice_failed_events<'a, 'b, 'c, 'd>(
let events = node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 2);
match &events[0] {
- Event::SpliceFailed { channel_id, reason, .. } => {
+ Event::SpliceFailed { channel_id, reason, contribution, .. } => {
assert_eq!(*expected_channel_id, *channel_id);
assert_eq!(expected_reason, *reason);
+ assert_eq!(contribution.as_ref(), Some(&funding_contribution));
},
_ => panic!("Unexpected event"),
}
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 34b51e2..63d0b32 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -205,13 +205,12 @@ pub fn do_initiate_splice_in<'a, 'b, 'c, 'd>(
pub fn do_initiate_rbf_splice_in<'a, 'b, 'c, 'd>(
node: &'a Node<'b, 'c, 'd>, counterparty: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
- value_added: Amount, feerate: FeeRate,
+ feerate: FeeRate,
) -> FundingContribution {
let node_id_counterparty = counterparty.node.get_our_node_id();
let funding_template = node.node.splice_channel(&channel_id, &node_id_counterparty).unwrap();
- let wallet = WalletSync::new(Arc::clone(&node.wallet_source), node.logger);
let funding_contribution =
- funding_template.splice_in_sync(value_added, feerate, FeeRate::MAX, &wallet).unwrap();
+ funding_template.with_prior_contribution(feerate, FeeRate::MAX).build().unwrap();
node.node
.funding_contributed(&channel_id, &node_id_counterparty, funding_contribution.clone(), None)
.unwrap();
@@ -220,15 +219,12 @@ pub fn do_initiate_rbf_splice_in<'a, 'b, 'c, 'd>(
pub fn do_initiate_rbf_splice_in_and_out<'a, 'b, 'c, 'd>(
node: &'a Node<'b, 'c, 'd>, counterparty: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
- value_added: Amount, outputs: Vec<TxOut>, feerate: FeeRate,
+ outputs: Vec<TxOut>, feerate: FeeRate,
) -> FundingContribution {
let node_id_counterparty = counterparty.node.get_our_node_id();
let funding_template = node.node.splice_channel(&channel_id, &node_id_counterparty).unwrap();
- let wallet = WalletSync::new(Arc::clone(&node.wallet_source), node.logger);
let funding_contribution = funding_template
- .without_prior_contribution(feerate, FeeRate::MAX)
- .with_coin_selection_source_sync(&wallet)
- .add_value(value_added)
+ .with_prior_contribution(feerate, FeeRate::MAX)
.add_outputs(outputs)
.build()
.unwrap();
@@ -238,6 +234,22 @@ pub fn do_initiate_rbf_splice_in_and_out<'a, 'b, 'c, 'd>(
funding_contribution
}
+pub fn do_initiate_splice_in_at_feerate<'a, 'b, 'c, 'd>(
+ initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
+ value_added: Amount, feerate: FeeRate,
+) -> FundingContribution {
+ let node_id_acceptor = acceptor.node.get_our_node_id();
+ let funding_template = initiator.node.splice_channel(&channel_id, &node_id_acceptor).unwrap();
+ let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger);
+ let funding_contribution =
+ funding_template.splice_in_sync(value_added, feerate, FeeRate::MAX, &wallet).unwrap();
+ initiator
+ .node
+ .funding_contributed(&channel_id, &node_id_acceptor, funding_contribution.clone(), None)
+ .unwrap();
+ funding_contribution
+}
+
pub fn initiate_splice_out<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
outputs: Vec<TxOut>,
@@ -3207,9 +3219,10 @@ fn do_abandon_splice_quiescent_action_on_shutdown(local_shutdown: bool, pending_
let events = nodes[0].node.get_and_clear_pending_events();
assert_eq!(events.len(), 2, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, reason, .. } => {
+ Event::SpliceFailed { channel_id: cid, reason, contribution, .. } => {
assert_eq!(*cid, channel_id);
assert_eq!(*reason, NegotiationFailureReason::ChannelClosing);
+ assert!(contribution.is_some());
},
other => panic!("Expected SpliceFailed, got {:?}", other),
}
@@ -4601,9 +4614,10 @@ fn test_splice_acceptor_disconnect_emits_events() {
let events = nodes[1].node.get_and_clear_pending_events();
assert_eq!(events.len(), 2, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, reason, .. } => {
+ Event::SpliceFailed { channel_id: cid, reason, contribution, .. } => {
assert_eq!(*cid, channel_id);
assert_eq!(*reason, NegotiationFailureReason::PeerDisconnected);
+ assert!(contribution.is_some());
},
other => panic!("Expected SpliceFailed, got {:?}", other),
}
@@ -4660,7 +4674,7 @@ fn test_splice_rbf_acceptor_basic() {
let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25;
let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu);
let funding_contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate);
// Steps 4-8: STFU exchange → tx_init_rbf → tx_ack_rbf.
complete_rbf_handshake(&nodes[0], &nodes[1]);
@@ -4724,8 +4738,7 @@ fn test_splice_rbf_at_high_feerate() {
// Step 2: RBF to a high feerate (1000 sat/kwu, well above the 600 crossover point).
provide_utxo_reserves(&nodes, 2, added_value * 2);
let high_feerate = FeeRate::from_sat_per_kwu(1000);
- let contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, high_feerate);
+ let contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, high_feerate);
complete_rbf_handshake(&nodes[0], &nodes[1]);
complete_interactive_funding_negotiation(
&nodes[0],
@@ -4750,8 +4763,7 @@ fn test_splice_rbf_at_high_feerate() {
let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
funding_template.min_rbf_feerate().unwrap()
};
- let contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate);
+ let contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate);
complete_rbf_handshake(&nodes[0], &nodes[1]);
complete_interactive_funding_negotiation(
&nodes[0],
@@ -4816,7 +4828,7 @@ fn test_splice_rbf_insufficient_feerate() {
// Node 0 initiates a proper RBF but we tamper the feerate to be insufficient.
provide_utxo_reserves(&nodes, 2, added_value * 2);
let _funding_contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, min_rbf_feerate);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, min_rbf_feerate);
let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
nodes[1].node.handle_stfu(node_id_0, &stfu_0);
@@ -4842,17 +4854,14 @@ fn test_splice_rbf_insufficient_feerate() {
// Node 0 echoes tx_abort and exits quiescence, freeing the holding cell.
nodes[0].node.handle_tx_abort(node_id_1, &tx_abort);
- // TODO: the RBF round's inputs are partially filtered against the prior round's committed
- // UTXOs, so the DiscardFunding carries coin-selection-dependent residue. Revisit once
- // #4514 lands to see if its semantics change what DiscardFunding contains here.
+ // The RBF round contributed the same inputs and outputs as the prior round, so after
+ // filtering against the prior round's committed UTXOs nothing remains to discard and
+ // `DiscardFunding` is suppressed; only `SpliceFailed` is emitted.
let events = nodes[0].node.get_and_clear_pending_events();
- assert_eq!(events.len(), 2, "{events:?}");
+ assert_eq!(events.len(), 1, "{events:?}");
assert!(
matches!(&events[0], Event::SpliceFailed { channel_id: cid, .. } if *cid == channel_id)
);
- assert!(
- matches!(&events[1], Event::DiscardFunding { channel_id: cid, .. } if *cid == channel_id)
- );
let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
assert_eq!(msg_events.len(), 2, "{msg_events:?}");
@@ -4885,7 +4894,7 @@ fn test_splice_rbf_insufficient_feerate() {
// Node 0 initiates another proper RBF but we tamper the feerate to the 25/24 value.
provide_utxo_reserves(&nodes, 2, added_value * 2);
let _funding_contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, min_rbf_feerate);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, min_rbf_feerate);
let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
nodes[1].node.handle_stfu(node_id_0, &stfu_0);
@@ -4903,22 +4912,19 @@ fn test_splice_rbf_insufficient_feerate() {
nodes[0].node.handle_tx_abort(node_id_1, &tx_abort);
let tx_abort_echo = get_event_msg!(nodes[0], MessageSendEvent::SendTxAbort, node_id_1);
- // TODO: same as above — revisit once #4514 lands.
+ // As above: nothing remains after filtering, so `DiscardFunding` is suppressed.
let events = nodes[0].node.get_and_clear_pending_events();
- assert_eq!(events.len(), 2);
+ assert_eq!(events.len(), 1);
assert!(
matches!(&events[0], Event::SpliceFailed { channel_id: cid, .. } if *cid == channel_id)
);
- assert!(
- matches!(&events[1], Event::DiscardFunding { channel_id: cid, .. } if *cid == channel_id)
- );
nodes[1].node.handle_tx_abort(node_id_0, &tx_abort_echo);
// Acceptor-side: prev + 25 = 278 satisfies the combined BIP125 rule and is accepted.
provide_utxo_reserves(&nodes, 2, added_value * 2);
let _funding_contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, min_rbf_feerate);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, min_rbf_feerate);
let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
nodes[1].node.handle_stfu(node_id_0, &stfu_0);
@@ -4958,8 +4964,7 @@ fn test_splice_rbf_insufficient_feerate_high() {
provide_utxo_reserves(&nodes, 2, added_value * 2);
let high_feerate = FeeRate::from_sat_per_kwu(1000);
- let contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, high_feerate);
+ let contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, high_feerate);
complete_rbf_handshake(&nodes[0], &nodes[1]);
complete_interactive_funding_negotiation(
&nodes[0],
@@ -4980,7 +4985,7 @@ fn test_splice_rbf_insufficient_feerate_high() {
provide_utxo_reserves(&nodes, 2, added_value * 2);
let min_rbf_feerate = FeeRate::from_sat_per_kwu(1041);
let _funding_contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, min_rbf_feerate);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, min_rbf_feerate);
let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
nodes[1].node.handle_stfu(node_id_0, &stfu_0);
@@ -4997,33 +5002,20 @@ fn test_splice_rbf_insufficient_feerate_high() {
nodes[0].node.handle_tx_abort(node_id_1, &tx_abort);
let tx_abort_echo = get_event_msg!(nodes[0], MessageSendEvent::SendTxAbort, node_id_1);
- // TODO: the RBF round's inputs are fully filtered against the prior round's committed
- // UTXOs, so this DiscardFunding is emitted with empty inputs and outputs. Once #4514
- // lands, a fully-drained DiscardFunding should be suppressed entirely — expect
- // `events.len() == 1`.
+ // The RBF round's inputs and outputs are fully filtered against the prior round's
+ // committed UTXOs, so `DiscardFunding` is suppressed.
let events = nodes[0].node.get_and_clear_pending_events();
- assert_eq!(events.len(), 2);
+ assert_eq!(events.len(), 1);
assert!(
matches!(&events[0], Event::SpliceFailed { channel_id: cid, .. } if *cid == channel_id)
);
- match &events[1] {
- Event::DiscardFunding {
- channel_id: cid,
- funding_info: FundingInfo::Contribution { inputs, outputs },
- } => {
- assert_eq!(*cid, channel_id);
- assert!(inputs.is_empty(), "Expected inputs filtered, got {inputs:?}");
- assert!(outputs.is_empty(), "Expected outputs filtered, got {outputs:?}");
- },
- other => panic!("Expected DiscardFunding with Contribution, got {other:?}"),
- }
nodes[1].node.handle_tx_abort(node_id_0, &tx_abort_echo);
// Feerate 1041 satisfies both rules — accepted.
provide_utxo_reserves(&nodes, 2, added_value * 2);
let _funding_contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, min_rbf_feerate);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, min_rbf_feerate);
let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
nodes[1].node.handle_stfu(node_id_0, &stfu_0);
@@ -5315,7 +5307,7 @@ fn test_splice_rbf_not_quiescence_initiator() {
let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25;
let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu);
let _funding_contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate);
// STFU exchange: node 0 initiates quiescence.
let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
@@ -5425,10 +5417,10 @@ pub fn do_test_splice_rbf_tiebreak(
// Node 0 calls splice_channel + funding_contributed.
let node_0_funding_contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate_0);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate_0);
// Node 1 calls splice_channel + funding_contributed.
- let node_1_funding_contribution = do_initiate_rbf_splice_in(
+ let node_1_funding_contribution = do_initiate_splice_in_at_feerate(
&nodes[1],
&nodes[0],
channel_id,
@@ -5838,7 +5830,7 @@ fn test_splice_rbf_acceptor_recontributes() {
let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25;
let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu);
let rbf_funding_contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate);
// Steps 6-9: STFU exchange → tx_init_rbf → tx_ack_rbf.
// Node 1 should re-contribute via our_prior_contribution.
@@ -5967,7 +5959,7 @@ fn test_splice_rbf_after_counterparty_rbf_aborted() {
let rbf_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25);
let _rbf_funding_contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate);
let tx_ack_rbf = complete_rbf_handshake(&nodes[0], &nodes[1]);
assert!(tx_ack_rbf.funding_output_contribution.is_some());
@@ -6163,7 +6155,7 @@ fn test_splice_rbf_sequential() {
let rbf_feerate_1 = FeeRate::from_sat_per_kwu(feerate_1_sat_per_kwu);
let funding_contribution_1 =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate_1);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate_1);
complete_rbf_handshake(&nodes[0], &nodes[1]);
complete_interactive_funding_negotiation(
@@ -6184,7 +6176,7 @@ fn test_splice_rbf_sequential() {
let rbf_feerate_2 = FeeRate::from_sat_per_kwu(feerate_2_sat_per_kwu);
let funding_contribution_2 =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate_2);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate_2);
complete_rbf_handshake(&nodes[0], &nodes[1]);
complete_interactive_funding_negotiation(
@@ -6511,7 +6503,7 @@ fn test_splice_rbf_acceptor_contributes_then_disconnects() {
let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25;
let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu);
let _rbf_funding_contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate);
let tx_ack_rbf = complete_rbf_handshake(&nodes[0], &nodes[1]);
assert!(
@@ -6523,24 +6515,22 @@ fn test_splice_rbf_acceptor_contributes_then_disconnects() {
nodes[0].node.peer_disconnected(node_id_1);
nodes[1].node.peer_disconnected(node_id_0);
- // The initiator should get SpliceFailed + DiscardFunding.
+ // The initiator re-used the same UTXOs as round 0. Since those UTXOs are still committed
+ // to round 0's splice, they are filtered and no DiscardFunding is emitted.
let events = nodes[0].node.get_and_clear_pending_events();
- assert_eq!(events.len(), 2, "{events:?}");
+ assert_eq!(events.len(), 1, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, reason, .. } => {
+ Event::SpliceFailed { channel_id: cid, reason, contribution, .. } => {
assert_eq!(*cid, channel_id);
assert_eq!(*reason, NegotiationFailureReason::PeerDisconnected);
+ assert!(contribution.is_some());
},
other => panic!("Expected SpliceFailed, got {:?}", other),
}
- match &events[1] {
- Event::DiscardFunding { funding_info: FundingInfo::Contribution { .. }, .. } => {},
- other => panic!("Expected DiscardFunding with Contribution, got {:?}", other),
- }
// The acceptor re-contributed the same UTXOs as round 0 (via prior contribution
// adjustment). Since those UTXOs are still committed to round 0's splice, they are
- // filtered from the DiscardFunding event. With all inputs/outputs filtered, no events
+ // filtered and no DiscardFunding is emitted. With all inputs/outputs filtered, no events
// are emitted for the acceptor.
let events = nodes[1].node.get_and_clear_pending_events();
assert_eq!(events.len(), 0, "{events:?}");
@@ -6593,7 +6583,6 @@ fn test_splice_rbf_disconnect_filters_prior_contributions() {
&nodes[0],
&nodes[1],
channel_id,
- added_value,
vec![splice_out_output.clone()],
rbf_feerate,
);
@@ -6609,9 +6598,10 @@ fn test_splice_rbf_disconnect_filters_prior_contributions() {
let events = nodes[0].node.get_and_clear_pending_events();
assert_eq!(events.len(), 2, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, reason, .. } => {
+ Event::SpliceFailed { channel_id: cid, reason, contribution, .. } => {
assert_eq!(*cid, channel_id);
assert_eq!(*reason, NegotiationFailureReason::PeerDisconnected);
+ assert!(contribution.is_some());
},
other => panic!("Expected SpliceFailed, got {:?}", other),
}
@@ -6641,7 +6631,7 @@ fn test_splice_rbf_disconnect_filters_prior_contributions() {
let rbf_feerate_2 = FeeRate::from_sat_per_kwu(feerate_1_sat_per_kwu);
let _funding_contribution_2 =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate_2);
+ do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate_2);
complete_rbf_handshake(&nodes[0], &nodes[1]);
// Disconnect again to clean up the in-progress interactive TX negotiation.
@@ -6649,18 +6639,15 @@ fn test_splice_rbf_disconnect_filters_prior_contributions() {
nodes[1].node.peer_disconnected(node_id_0);
let events = nodes[0].node.get_and_clear_pending_events();
- assert_eq!(events.len(), 2, "{events:?}");
+ assert_eq!(events.len(), 1, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, reason, .. } => {
+ Event::SpliceFailed { channel_id: cid, reason, contribution, .. } => {
assert_eq!(*cid, channel_id);
assert_eq!(*reason, NegotiationFailureReason::PeerDisconnected);
+ assert!(contribution.is_some());
},
other => panic!("Expected SpliceFailed, got {:?}", other),
}
- match &events[1] {
- Event::DiscardFunding { .. } => {},
- other => panic!("Expected DiscardFunding, got {:?}", other),
- }
let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]);
reconnect_args.send_announcement_sigs = (true, true);
@@ -7326,8 +7313,7 @@ fn test_splice_rbf_rejects_low_feerate_after_several_attempts() {
let feerate = prev_feerate + 25;
provide_utxo_reserves(&nodes, 2, added_value * 2);
let rbf_feerate = FeeRate::from_sat_per_kwu(feerate);
- let contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate);
+ let contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate);
complete_rbf_handshake(&nodes[0], &nodes[1]);
complete_interactive_funding_negotiation(
&nodes[0],
@@ -7353,8 +7339,7 @@ fn test_splice_rbf_rejects_low_feerate_after_several_attempts() {
let next_feerate = prev_feerate + 25;
provide_utxo_reserves(&nodes, 2, added_value * 2);
let rbf_feerate = FeeRate::from_sat_per_kwu(next_feerate);
- let _contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate);
+ let _contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate);
let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1);
nodes[1].node.handle_stfu(node_id_0, &stfu_0);
let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0);
@@ -7403,8 +7388,7 @@ fn test_splice_rbf_rejects_own_low_feerate_after_several_attempts() {
let feerate = prev_feerate + 25;
provide_utxo_reserves(&nodes, 2, added_value * 2);
let rbf_feerate = FeeRate::from_sat_per_kwu(feerate);
- let contribution =
- do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate);
+ let contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate);
complete_rbf_handshake(&nodes[0], &nodes[1]);
complete_interactive_funding_negotiation(
&nodes[0],
@@ -7446,9 +7430,10 @@ fn test_splice_rbf_rejects_own_low_feerate_after_several_attempts() {
let events = nodes[0].node.get_and_clear_pending_events();
assert_eq!(events.len(), 1, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, reason, .. } => {
+ Event::SpliceFailed { channel_id: cid, reason, contribution, .. } => {
assert_eq!(*cid, channel_id);
assert_eq!(*reason, NegotiationFailureReason::FeeRateTooLow);
+ assert!(contribution.is_some());
},
other => panic!("Expected SpliceFailed, got {:?}", other),
}
Why this scored 24/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.