diff --git a/lightning/src/chain/chaininterface.rs b/lightning/src/chain/chaininterface.rs index 806e947c153..147341ae1d7 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; @@ -25,7 +27,7 @@ use bitcoin::transaction::Transaction; /// /// This is used to provide context about the type of transaction being broadcast, which may be /// useful for logging, filtering, or prioritization purposes. -#[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum TransactionType { /// A funding transaction establishing a new channel. /// @@ -104,19 +106,74 @@ 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. + /// + /// The `Vec` contains every negotiated candidate for this funding in order: the original + /// negotiation followed by any RBF replacements. The last entry is the candidate being + /// broadcast. /// /// [`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(Vec), +} + +/// A single negotiated candidate within an [`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, PartialEq, Eq)] +pub struct FundingCandidate { + /// The txid of this candidate. + pub txid: Txid, + /// The channels participating in this candidate. + pub channels: Vec, } +/// Information about a single channel's participation in a [`FundingCandidate`]. +#[derive(Clone, Debug, 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, +} + +/// The role of a channel within a [`FundingCandidate`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FundingPurpose { + /// The channel is being newly established (V2 dual-funded open). + Establishment, + /// An existing channel is being spliced. + Splice, +} + +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 10801edef01..e4fda070252 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, @@ -9394,10 +9395,32 @@ 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() + .filter_map(|(i, funding)| { + let txid = funding.get_funding_txid()?; + let contribution = i + .checked_sub(contrib_offset) + .and_then(|j| pending_splice.contributions.get(j)) + .cloned(); + Some(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 73d9a67f50f..27fb23ca4fc 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11136,7 +11136,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 20366fe772a..8c125cf3bcb 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -578,10 +578,6 @@ impl_writeable_tlv_based!(FundingContribution, { }); impl FundingContribution { - pub(super) fn feerate(&self) -> FeeRate { - self.feerate - } - pub(super) fn is_splice(&self) -> bool { self.is_splice } @@ -610,6 +606,16 @@ impl FundingContribution { .unwrap_or(Amount::ZERO) } + /// Returns the estimated on-chain fee this contribution is responsible for paying. + pub fn estimated_fee(&self) -> Amount { + self.estimated_fee + } + + /// Returns the inputs included in this contribution. + pub fn inputs(&self) -> &[FundingTxInput] { + &self.inputs + } + /// Returns the outputs (e.g., withdrawal destinations) included in this contribution. /// /// This does not include the change output; see [`FundingContribution::change_output`]. @@ -625,6 +631,17 @@ impl FundingContribution { self.change_output.as_ref() } + /// Returns the fee rate used to select `inputs` (the minimum feerate). + pub fn feerate(&self) -> FeeRate { + self.feerate + } + + /// Returns the maximum fee rate this contribution will accept as acceptor before rejecting + /// the splice. + pub fn max_feerate(&self) -> FeeRate { + self.max_feerate + } + /// Tries to satisfy a new request using only this contribution's existing inputs. /// /// For input-backed contributions, this reuses the current inputs, adjusts the explicit diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index fa22ccb61c7..bf3856b6056 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; @@ -492,13 +492,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, ) -> (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, ) -> (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(); @@ -598,17 +608,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) @@ -630,7 +652,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); @@ -1311,7 +1333,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); @@ -1516,7 +1538,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()); @@ -1584,7 +1606,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()); @@ -1632,7 +1654,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); @@ -2294,7 +2316,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); @@ -4349,8 +4371,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); @@ -4387,7 +4415,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). @@ -4403,7 +4431,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); @@ -4424,7 +4457,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); @@ -4544,7 +4578,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); @@ -4559,7 +4593,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); @@ -5054,7 +5089,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()); @@ -5126,7 +5165,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()); @@ -5190,7 +5233,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); @@ -5357,8 +5400,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); @@ -5394,8 +5438,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); @@ -5481,8 +5530,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); @@ -5613,8 +5663,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); @@ -5699,7 +5750,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); @@ -5719,7 +5771,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); @@ -5769,7 +5822,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) @@ -5782,7 +5835,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); @@ -5803,7 +5857,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()); @@ -5818,7 +5872,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()); @@ -5836,7 +5890,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()); @@ -5850,7 +5904,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], @@ -5896,7 +5950,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) @@ -5909,7 +5963,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); @@ -5929,7 +5984,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()); @@ -5943,7 +5998,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()); @@ -5954,7 +6009,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], @@ -6019,8 +6074,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); @@ -6731,7 +6787,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 @@ -6755,11 +6811,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. @@ -6802,7 +6864,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 @@ -6826,11 +6888,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.