Expose interactive funding candidates on broadcast
What changed, and why it matters
This commit is a routine API improvement for the Lightning Dev Kit. It changes the information reported when a splice (or future dual-funded) transaction is broadcast so that downstream wallet software can see all negotiated versions of the transaction and each party's contribution. There is no indication of a security bug being fixed; it is a feature/refactoring change to expose richer data to consumers of the library.
No security action required. Reviewers may want to confirm that the new Writeable/Readable implementations are backward-compatible and that downstream consumers handle the renamed TransactionType variant, but these are API-compatibility concerns, not security issues.
Security signals we found
No security-relevant signal: public API refactor to expose more metadata in broadcast callback
Serialization added for new public types (FundingCandidate, ChannelFunding, FundingPurpose) using existing TLV macros
No change to cryptographic validation, fee policy, or transaction signing logic
Evidence from the diff
The patch replaces TransactionType::Splice with TransactionType::InteractiveFunding, embedding a Vec
Changed components
lightning/src/chain/chaininterface.rslightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/funding.rslightning/src/ln/splicing_tests.rslightning/src/util/wallet_utils.rsInspect captured patch +225 / −73
diff --git a/lightning/src/chain/chaininterface.rs b/lightning/src/chain/chaininterface.rs
index 806e947..bb5f6de 100644
--- a/lightning/src/chain/chaininterface.rs
+++ b/lightning/src/chain/chaininterface.rs
@@ -15,9 +15,11 @@
use core::{cmp, ops::Deref};
+use crate::ln::funding::FundingContribution;
use crate::ln::types::ChannelId;
use crate::prelude::*;
+use bitcoin::hash_types::Txid;
use bitcoin::secp256k1::PublicKey;
use bitcoin::transaction::Transaction;
@@ -104,19 +106,76 @@ pub enum TransactionType {
/// A single sweep transaction may aggregate outputs from multiple channels.
channels: Vec<(PublicKey, ChannelId)>,
},
- /// A splice transaction modifying an existing channel's funding.
+ /// An interactively-negotiated funding transaction.
///
- /// A transaction of this type will be broadcast as a result of a [`ChannelManager::splice_channel`] operation.
+ /// A transaction of this type will be broadcast as a result of a
+ /// [`ChannelManager::splice_channel`] operation, or (once supported) V2 (dual-funded) channel
+ /// establishment. The same variant is used for batches of either or both.
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
- Splice {
- /// The `node_id` of the channel counterparty.
- counterparty_node_id: PublicKey,
- /// The ID of the channel being spliced.
- channel_id: ChannelId,
+ InteractiveFunding {
+ /// Every negotiated candidate for this funding in order: the original negotiation
+ /// followed by any RBF replacements. The last entry is the candidate being broadcast.
+ candidates: Vec<FundingCandidate>,
},
}
+/// A single negotiated candidate within a [`TransactionType::InteractiveFunding`] broadcast.
+///
+/// The candidate is identified by its [`Txid`] and lists the channels participating in it. A
+/// single candidate funds more than one channel only when batching splices and/or V2 channel
+/// openings (not yet implemented).
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct FundingCandidate {
+ /// The txid of this candidate.
+ pub txid: Txid,
+ /// The channels participating in this candidate.
+ pub channels: Vec<ChannelFunding>,
+}
+
+/// Information about a single channel's participation in a [`FundingCandidate`].
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct ChannelFunding {
+ /// The `node_id` of the channel counterparty.
+ pub counterparty_node_id: PublicKey,
+ /// The ID of the channel.
+ pub channel_id: ChannelId,
+ /// Whether this channel is being newly established or is an existing channel being spliced.
+ pub purpose: FundingPurpose,
+ /// The local node's contribution to this channel in this candidate, or `None` if we did
+ /// not contribute (e.g., a pure acceptor with zero value added, or a leading RBF round
+ /// before we began contributing).
+ pub contribution: Option<FundingContribution>,
+}
+
+/// The role of a channel within a [`FundingCandidate`].
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub enum FundingPurpose {
+ /// The channel is being newly established (V2 dual-funded open).
+ Establishment,
+ /// An existing channel is being spliced.
+ Splice,
+}
+
+// Needed so downstream consumers can persist these without needing to define wrapper types
+// mirroring the type structure.
+impl_writeable_tlv_based!(FundingCandidate, {
+ (1, txid, required),
+ (3, channels, required_vec),
+});
+
+impl_writeable_tlv_based!(ChannelFunding, {
+ (1, counterparty_node_id, required),
+ (3, channel_id, required),
+ (5, purpose, required),
+ (7, contribution, option),
+});
+
+impl_writeable_tlv_based_enum!(FundingPurpose,
+ (0, Establishment) => {},
+ (2, Splice) => {},
+);
+
// TODO: Define typed abstraction over feerates to handle their conversions.
pub(crate) fn compute_feerate_sat_per_1000_weight(fee_sat: u64, weight: u64) -> u32 {
(fee_sat * 1000 / weight).try_into().unwrap_or(u32::max_value())
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index e6397ae..5b6d04a 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -28,7 +28,8 @@ use bitcoin::{secp256k1, sighash, FeeRate, Sequence, TxIn};
use crate::blinded_path::message::BlindedMessagePath;
use crate::chain::chaininterface::{
- ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, TransactionType,
+ ChannelFunding, ConfirmationTarget, FeeEstimator, FundingCandidate, FundingPurpose,
+ LowerBoundedFeeEstimator, TransactionType,
};
use crate::chain::channelmonitor::{
ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, CommitmentHTLCData,
@@ -9382,10 +9383,34 @@ where
);
}
- let tx_type = TransactionType::Splice {
- counterparty_node_id: self.context.counterparty_node_id,
- channel_id: self.context.channel_id,
- };
+ 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
+ .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,
+ }],
+ }
+ })
+ .collect();
+ let tx_type = TransactionType::InteractiveFunding { candidates };
funding_tx_signed.funding_tx = Some((funding_tx, tx_type));
funding_tx_signed.splice_negotiated = Some(splice_negotiated);
funding_tx_signed.splice_locked = splice_locked;
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 1f32423..7aa8201 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -11124,7 +11124,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
} else if let Some((splice_tx, tx_type)) = funding_tx_signed
.as_mut()
.and_then(|v| v.funding_tx.take())
- .filter(|(_, tx_type)| matches!(tx_type, TransactionType::Splice { .. }))
+ .filter(|(_, tx_type)| matches!(tx_type, TransactionType::InteractiveFunding { .. }))
{
log_info!(logger, "Broadcasting signed splice transaction with txid {}", splice_tx.compute_txid());
self.tx_broadcaster.broadcast_transactions(&[(&splice_tx, tx_type)]);
diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs
index 20366fe..aa5a854 100644
--- a/lightning/src/ln/funding.rs
+++ b/lightning/src/ln/funding.rs
@@ -539,7 +539,7 @@ enum FundingInputs {
}
/// The components of a funding transaction contributed by one party.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct FundingContribution {
/// The estimate fees responsible to be paid for the contribution.
estimated_fee: Amount,
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index b2cb1ed..635ad31 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -9,7 +9,7 @@
#![cfg_attr(not(test), allow(unused_imports))]
-use crate::chain::chaininterface::{TransactionType, FEERATE_FLOOR_SATS_PER_KW};
+use crate::chain::chaininterface::{FundingPurpose, TransactionType, FEERATE_FLOOR_SATS_PER_KW};
use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::ChannelMonitorUpdateStatus;
@@ -493,13 +493,23 @@ pub fn complete_interactive_funding_negotiation_for_both<'a, 'b, 'c, 'd>(
pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, is_0conf: bool,
+ expected_replaced_txid: Option<Txid>,
) -> (Transaction, Option<(msgs::SpliceLocked, PublicKey)>) {
- sign_interactive_funding_tx_with_acceptor_contribution(initiator, acceptor, is_0conf, false)
+ sign_interactive_funding_tx_with_acceptor_contribution(
+ initiator,
+ acceptor,
+ is_0conf,
+ false,
+ expected_replaced_txid,
+ )
}
+/// `expected_replaced_txid` is the expected txid of the prior negotiated candidate in the
+/// `TransactionType::InteractiveFunding` broadcast: `None` for a first splice attempt; `Some(txid)`
+/// for an RBF replacing that prior negotiated candidate.
pub fn sign_interactive_funding_tx_with_acceptor_contribution<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, is_0conf: bool,
- acceptor_has_contribution: bool,
+ acceptor_has_contribution: bool, expected_replaced_txid: Option<Txid>,
) -> (Transaction, Option<(msgs::SpliceLocked, PublicKey)>) {
let node_id_initiator = initiator.node.get_our_node_id();
let node_id_acceptor = acceptor.node.get_our_node_id();
@@ -599,17 +609,29 @@ pub fn sign_interactive_funding_tx_with_acceptor_contribution<'a, 'b, 'c, 'd>(
assert_eq!(initiator_txn[0].0, acceptor_txn[0].0);
let (tx, initiator_tx_type) = initiator_txn.remove(0);
let (_, acceptor_tx_type) = acceptor_txn.remove(0);
- // Verify transaction types are Splice for both nodes
- assert!(
- matches!(initiator_tx_type, TransactionType::Splice { .. }),
- "Expected TransactionType::Splice, got {:?}",
- initiator_tx_type
- );
- assert!(
- matches!(acceptor_tx_type, TransactionType::Splice { .. }),
- "Expected TransactionType::Splice, got {:?}",
- acceptor_tx_type
- );
+ // Verify transaction types are InteractiveFunding for both nodes. The initiator always
+ // contributes; the acceptor contributes iff the flag says so. Both parties must observe
+ // the same prior candidate txid as the caller declares.
+ let assert_broadcast =
+ |label: &str, tx_type: &TransactionType, contribution_expected: bool| {
+ let candidates = match tx_type {
+ TransactionType::InteractiveFunding { candidates } => candidates,
+ other => panic!("Expected TransactionType::InteractiveFunding, got {other:?}"),
+ };
+ let last = candidates.last().expect("at least one candidate");
+ assert_eq!(last.txid, tx.compute_txid(), "{label} last candidate txid mismatch");
+ let last_channel = last.channels.first().expect("at least one channel");
+ assert!(matches!(last_channel.purpose, FundingPurpose::Splice));
+ assert_eq!(
+ last_channel.contribution.is_some(),
+ contribution_expected,
+ "{label} contribution presence mismatch",
+ );
+ let prior_txid = candidates.len().checked_sub(2).map(|i| candidates[i].txid);
+ assert_eq!(prior_txid, expected_replaced_txid, "{label} replaced_txid mismatch");
+ };
+ assert_broadcast("initiator", &initiator_tx_type, true);
+ assert_broadcast("acceptor", &acceptor_tx_type, acceptor_has_contribution);
tx
};
(tx, splice_locked)
@@ -631,7 +653,7 @@ pub fn splice_channel<'a, 'b, 'c, 'd>(
funding_contribution,
new_funding_script.clone(),
);
- let (splice_tx, splice_locked) = sign_interactive_funding_tx(initiator, acceptor, false);
+ let (splice_tx, splice_locked) = sign_interactive_funding_tx(initiator, acceptor, false, None);
assert!(splice_locked.is_none());
expect_splice_pending_event(initiator, &node_id_acceptor);
@@ -1312,7 +1334,7 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
}),
);
- let (splice_tx, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ let (splice_tx, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false, None);
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_1_id);
@@ -1517,7 +1539,7 @@ fn do_test_splice_tiebreak(
// Sign (acceptor has contribution) and broadcast.
let (tx, splice_locked) = sign_interactive_funding_tx_with_acceptor_contribution(
- &nodes[0], &nodes[1], false, true,
+ &nodes[0], &nodes[1], false, true, None,
);
assert!(splice_locked.is_none());
@@ -1585,7 +1607,7 @@ fn do_test_splice_tiebreak(
// Sign (no acceptor contribution) and broadcast.
let (tx, splice_locked) = sign_interactive_funding_tx_with_acceptor_contribution(
- &nodes[0], &nodes[1], false, false,
+ &nodes[0], &nodes[1], false, false, None,
);
assert!(splice_locked.is_none());
@@ -1633,7 +1655,7 @@ fn do_test_splice_tiebreak(
);
let (new_splice_tx, splice_locked) =
- sign_interactive_funding_tx(&nodes[1], &nodes[0], false);
+ sign_interactive_funding_tx(&nodes[1], &nodes[0], false, None);
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[1], &node_id_0);
@@ -2513,7 +2535,7 @@ fn do_test_propose_splice_while_disconnected(use_0conf: bool) {
new_funding_script,
);
let (splice_tx, splice_locked) = sign_interactive_funding_tx_with_acceptor_contribution(
- &nodes[0], &nodes[1], use_0conf, true,
+ &nodes[0], &nodes[1], use_0conf, true, None,
);
expect_splice_pending_event(&nodes[0], &node_id_1);
expect_splice_pending_event(&nodes[1], &node_id_0);
@@ -4568,8 +4590,14 @@ fn test_splice_rbf_acceptor_basic() {
new_funding_script.clone(),
);
- // Step 10: Sign and broadcast.
- let (rbf_tx, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ // Step 10: Sign and broadcast. The prior candidate in the broadcast's
+ // `TransactionType::InteractiveFunding` must point at the first splice tx it is replacing.
+ let (rbf_tx, splice_locked) = sign_interactive_funding_tx(
+ &nodes[0],
+ &nodes[1],
+ false,
+ Some(first_splice_tx.compute_txid()),
+ );
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
@@ -4606,7 +4634,7 @@ fn test_splice_rbf_at_high_feerate() {
// Step 1: Complete a splice-in at floor feerate.
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
- let (_first_splice_tx, new_funding_script) =
+ let (first_splice_tx, new_funding_script) =
splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
// Step 2: RBF to a high feerate (1000 sat/kwu, well above the 600 crossover point).
@@ -4622,7 +4650,12 @@ fn test_splice_rbf_at_high_feerate() {
contribution,
new_funding_script.clone(),
);
- let (_, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ let (rbf_tx_1, splice_locked) = sign_interactive_funding_tx(
+ &nodes[0],
+ &nodes[1],
+ false,
+ Some(first_splice_tx.compute_txid()),
+ );
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
expect_splice_pending_event(&nodes[1], &node_id_0);
@@ -4643,7 +4676,8 @@ fn test_splice_rbf_at_high_feerate() {
contribution,
new_funding_script,
);
- let (_, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ let (_, splice_locked) =
+ sign_interactive_funding_tx(&nodes[0], &nodes[1], false, Some(rbf_tx_1.compute_txid()));
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
expect_splice_pending_event(&nodes[1], &node_id_0);
@@ -4835,7 +4869,7 @@ fn test_splice_rbf_insufficient_feerate_high() {
// Complete a splice-in at floor feerate, then RBF to 1000 sat/kwu.
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
- let (_splice_tx, new_funding_script) =
+ let (splice_tx, new_funding_script) =
splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
provide_utxo_reserves(&nodes, 2, added_value * 2);
@@ -4850,7 +4884,8 @@ fn test_splice_rbf_insufficient_feerate_high() {
contribution,
new_funding_script,
);
- let (_, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ let (_, splice_locked) =
+ sign_interactive_funding_tx(&nodes[0], &nodes[1], false, Some(splice_tx.compute_txid()));
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
expect_splice_pending_event(&nodes[1], &node_id_0);
@@ -5378,7 +5413,11 @@ pub fn do_test_splice_rbf_tiebreak(
// Sign (acceptor has contribution) and broadcast.
let (rbf_tx, splice_locked) = sign_interactive_funding_tx_with_acceptor_contribution(
- &nodes[0], &nodes[1], false, true,
+ &nodes[0],
+ &nodes[1],
+ false,
+ true,
+ Some(first_splice_tx.compute_txid()),
);
assert!(splice_locked.is_none());
@@ -5450,7 +5489,11 @@ pub fn do_test_splice_rbf_tiebreak(
// Sign (acceptor has no contribution) and broadcast.
let (rbf_tx, splice_locked) = sign_interactive_funding_tx_with_acceptor_contribution(
- &nodes[0], &nodes[1], false, false,
+ &nodes[0],
+ &nodes[1],
+ false,
+ false,
+ Some(first_splice_tx.compute_txid()),
);
assert!(splice_locked.is_none());
@@ -5514,7 +5557,7 @@ pub fn do_test_splice_rbf_tiebreak(
// Sign (no acceptor contribution) and broadcast.
let (new_splice_tx, splice_locked) =
- sign_interactive_funding_tx(&nodes[1], &nodes[0], false);
+ sign_interactive_funding_tx(&nodes[1], &nodes[0], false, None);
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[1], &node_id_0);
@@ -5696,8 +5739,9 @@ fn test_splice_rbf_acceptor_recontributes() {
new_funding_script.clone(),
);
- let (first_splice_tx, splice_locked) =
- sign_interactive_funding_tx_with_acceptor_contribution(&nodes[0], &nodes[1], false, true);
+ let (first_splice_tx, splice_locked) = sign_interactive_funding_tx_with_acceptor_contribution(
+ &nodes[0], &nodes[1], false, true, None,
+ );
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
@@ -5733,8 +5777,13 @@ fn test_splice_rbf_acceptor_recontributes() {
);
// Step 11: Sign (acceptor has contribution) and broadcast.
- let (rbf_tx, splice_locked) =
- sign_interactive_funding_tx_with_acceptor_contribution(&nodes[0], &nodes[1], false, true);
+ let (rbf_tx, splice_locked) = sign_interactive_funding_tx_with_acceptor_contribution(
+ &nodes[0],
+ &nodes[1],
+ false,
+ true,
+ Some(first_splice_tx.compute_txid()),
+ );
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
@@ -5820,8 +5869,9 @@ fn test_splice_rbf_after_counterparty_rbf_aborted() {
new_funding_script,
);
- let (_first_splice_tx, splice_locked) =
- sign_interactive_funding_tx_with_acceptor_contribution(&nodes[0], &nodes[1], false, true);
+ let (_first_splice_tx, splice_locked) = sign_interactive_funding_tx_with_acceptor_contribution(
+ &nodes[0], &nodes[1], false, true, None,
+ );
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
@@ -5952,8 +6002,9 @@ fn test_splice_rbf_recontributes_feerate_too_high() {
new_funding_script.clone(),
);
- let (_first_splice_tx, splice_locked) =
- sign_interactive_funding_tx_with_acceptor_contribution(&nodes[0], &nodes[1], false, true);
+ let (_first_splice_tx, splice_locked) = sign_interactive_funding_tx_with_acceptor_contribution(
+ &nodes[0], &nodes[1], false, true, None,
+ );
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
@@ -6038,7 +6089,8 @@ fn test_splice_rbf_sequential() {
funding_contribution_1,
new_funding_script.clone(),
);
- let (splice_tx_1, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ let (splice_tx_1, splice_locked) =
+ sign_interactive_funding_tx(&nodes[0], &nodes[1], false, Some(splice_tx_0.compute_txid()));
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
expect_splice_pending_event(&nodes[1], &node_id_0);
@@ -6058,7 +6110,8 @@ fn test_splice_rbf_sequential() {
funding_contribution_2,
new_funding_script.clone(),
);
- let (rbf_tx_final, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ let (rbf_tx_final, splice_locked) =
+ sign_interactive_funding_tx(&nodes[0], &nodes[1], false, Some(splice_tx_1.compute_txid()));
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
expect_splice_pending_event(&nodes[1], &node_id_0);
@@ -6108,7 +6161,7 @@ fn test_splice_rbf_amends_prior_net_positive_contribution_request() {
script_pubkey: ScriptBuf::new_p2wsh(&WScriptHash::all_zeros()),
};
- let run_rbf_round = |contribution: FundingContribution| {
+ let run_rbf_round = |contribution: FundingContribution, replaced_txid: Txid| {
nodes[0]
.node
.funding_contributed(&channel_id, &node_id_1, contribution.clone(), None)
@@ -6121,7 +6174,8 @@ fn test_splice_rbf_amends_prior_net_positive_contribution_request() {
contribution,
new_funding_script.clone(),
);
- let (tx, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ let (tx, splice_locked) =
+ sign_interactive_funding_tx(&nodes[0], &nodes[1], false, Some(replaced_txid));
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
expect_splice_pending_event(&nodes[1], &node_id_0);
@@ -6142,7 +6196,7 @@ fn test_splice_rbf_amends_prior_net_positive_contribution_request() {
contribution_1.change_output().unwrap().value
< initial_contribution.change_output().unwrap().value
);
- let splice_tx_1 = run_rbf_round(contribution_1.clone());
+ let splice_tx_1 = run_rbf_round(contribution_1.clone(), splice_tx_0.compute_txid());
let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_1.outputs());
@@ -6157,7 +6211,7 @@ fn test_splice_rbf_amends_prior_net_positive_contribution_request() {
assert_eq!(inputs_2, initial_inputs);
assert_eq!(contribution_2.outputs(), contribution_1.outputs());
assert!(contribution_2.net_value() < contribution_1.net_value());
- let splice_tx_2 = run_rbf_round(contribution_2.clone());
+ let splice_tx_2 = run_rbf_round(contribution_2.clone(), splice_tx_1.compute_txid());
let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_2.outputs());
@@ -6175,7 +6229,7 @@ fn test_splice_rbf_amends_prior_net_positive_contribution_request() {
contribution_3.change_output().unwrap().value
> contribution_2.change_output().unwrap().value
);
- let splice_tx_3 = run_rbf_round(contribution_3.clone());
+ let splice_tx_3 = run_rbf_round(contribution_3.clone(), splice_tx_2.compute_txid());
let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_3.outputs());
@@ -6189,7 +6243,7 @@ fn test_splice_rbf_amends_prior_net_positive_contribution_request() {
contribution_4.change_output().unwrap().value
< contribution_3.change_output().unwrap().value
);
- let rbf_tx_final = run_rbf_round(contribution_4);
+ let rbf_tx_final = run_rbf_round(contribution_4, splice_tx_3.compute_txid());
lock_rbf_splice_after_blocks(
&nodes[0],
@@ -6235,7 +6289,7 @@ fn test_splice_rbf_amends_prior_net_negative_contribution_request() {
let (splice_tx_0, new_funding_script) =
splice_channel(&nodes[0], &nodes[1], channel_id, initial_contribution.clone());
- let run_rbf_round = |contribution: FundingContribution| {
+ let run_rbf_round = |contribution: FundingContribution, replaced_txid: Txid| {
nodes[0]
.node
.funding_contributed(&channel_id, &node_id_1, contribution.clone(), None)
@@ -6248,7 +6302,8 @@ fn test_splice_rbf_amends_prior_net_negative_contribution_request() {
contribution,
new_funding_script.clone(),
);
- let (tx, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ let (tx, splice_locked) =
+ sign_interactive_funding_tx(&nodes[0], &nodes[1], false, Some(replaced_txid));
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
expect_splice_pending_event(&nodes[1], &node_id_0);
@@ -6268,7 +6323,7 @@ fn test_splice_rbf_amends_prior_net_negative_contribution_request() {
assert!(inputs_1.is_empty());
assert_eq!(contribution_1.outputs(), &[first_output.clone(), second_output.clone()]);
assert!(contribution_1.net_value() < initial_contribution.net_value());
- let splice_tx_1 = run_rbf_round(contribution_1.clone());
+ let splice_tx_1 = run_rbf_round(contribution_1.clone(), splice_tx_0.compute_txid());
let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_1.outputs());
@@ -6282,7 +6337,7 @@ fn test_splice_rbf_amends_prior_net_negative_contribution_request() {
assert!(inputs_2.is_empty());
assert_eq!(contribution_2.outputs(), std::slice::from_ref(&second_output));
assert!(contribution_2.net_value() > contribution_1.net_value());
- let splice_tx_2 = run_rbf_round(contribution_2.clone());
+ let splice_tx_2 = run_rbf_round(contribution_2.clone(), splice_tx_1.compute_txid());
let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap();
assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_2.outputs());
@@ -6293,7 +6348,7 @@ fn test_splice_rbf_amends_prior_net_negative_contribution_request() {
assert_eq!(contribution_3.outputs(), contribution_2.outputs());
assert!(contribution_3.net_value() < contribution_2.net_value());
assert!(contribution_3.change_output().is_none());
- let rbf_tx_final = run_rbf_round(contribution_3);
+ let rbf_tx_final = run_rbf_round(contribution_3, splice_tx_2.compute_txid());
lock_rbf_splice_after_blocks(
&nodes[0],
@@ -6358,8 +6413,9 @@ fn test_splice_rbf_acceptor_contributes_then_disconnects() {
new_funding_script.clone(),
);
- let (_first_splice_tx, splice_locked) =
- sign_interactive_funding_tx_with_acceptor_contribution(&nodes[0], &nodes[1], false, true);
+ let (_first_splice_tx, splice_locked) = sign_interactive_funding_tx_with_acceptor_contribution(
+ &nodes[0], &nodes[1], false, true, None,
+ );
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
@@ -7160,7 +7216,7 @@ fn test_splice_rbf_rejects_low_feerate_after_several_attempts() {
// Round 0: Initial splice-in at floor feerate (253).
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
- let (_, new_funding_script) =
+ let (mut prev_splice_tx, new_funding_script) =
splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
// Bump the fee estimator on node 1 (the RBF receiver) early so the feerate check
@@ -7184,11 +7240,17 @@ fn test_splice_rbf_rejects_low_feerate_after_several_attempts() {
contribution,
new_funding_script.clone(),
);
- let (_, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ let (rbf_tx, splice_locked) = sign_interactive_funding_tx(
+ &nodes[0],
+ &nodes[1],
+ false,
+ Some(prev_splice_tx.compute_txid()),
+ );
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
expect_splice_pending_event(&nodes[1], &node_id_0);
prev_feerate = feerate;
+ prev_splice_tx = rbf_tx;
}
// Round 11: RBF at minimum bump. Should be rejected because feerate < fee estimator.
@@ -7231,7 +7293,7 @@ fn test_splice_rbf_rejects_own_low_feerate_after_several_attempts() {
// Round 0: Initial splice-in at floor feerate (253).
let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value);
- let (_, new_funding_script) =
+ let (mut prev_splice_tx, new_funding_script) =
splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
// Bump node 0's fee estimator early so the feerate check would reject once the
@@ -7255,11 +7317,17 @@ fn test_splice_rbf_rejects_own_low_feerate_after_several_attempts() {
contribution,
new_funding_script.clone(),
);
- let (_, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ let (rbf_tx, splice_locked) = sign_interactive_funding_tx(
+ &nodes[0],
+ &nodes[1],
+ false,
+ Some(prev_splice_tx.compute_txid()),
+ );
assert!(splice_locked.is_none());
expect_splice_pending_event(&nodes[0], &node_id_1);
expect_splice_pending_event(&nodes[1], &node_id_0);
prev_feerate = feerate;
+ prev_splice_tx = rbf_tx;
}
// Round 11: Our own RBF at minimum bump. funding_contributed should reject it.
@@ -7319,7 +7387,7 @@ fn test_no_disconnect_after_splice_completes() {
funding_contribution,
new_funding_script,
);
- let (_, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false);
+ let (_, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false, None);
assert!(splice_locked.is_none());
let node_id_0 = nodes[0].node.get_our_node_id();
diff --git a/lightning/src/util/wallet_utils.rs b/lightning/src/util/wallet_utils.rs
index b0fdb60..cd79b36 100644
--- a/lightning/src/util/wallet_utils.rs
+++ b/lightning/src/util/wallet_utils.rs
@@ -149,7 +149,7 @@ impl Utxo {
///
/// Can be used as an input to contribute to a channel's funding transaction either when using the
/// v2 channel establishment protocol or when splicing.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct ConfirmedUtxo {
/// The unspent [`TxOut`] found in [`prevtx`].
///
Why this scored 18/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.