diff --git a/lightning/src/chain/chaininterface.rs b/lightning/src/chain/chaininterface.rs index 806e947c153..fcd074fc351 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. /// @@ -114,6 +116,12 @@ pub enum TransactionType { counterparty_node_id: PublicKey, /// The ID of the channel being spliced. channel_id: ChannelId, + /// The local node's contribution to this splice/RBF round, or `None` if we did not + /// contribute (e.g., a pure acceptor with zero value added). + contribution: Option, + /// For an RBF replacement, the txid of the prior negotiated splice candidate being + /// replaced. `None` for the first splice attempt. + replaced_txid: Option, }, } diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 32c0e94bdc8..ae42beb3e96 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -9358,9 +9358,16 @@ where ); } + let replaced_txid = + pending_splice.negotiated_candidates.len().checked_sub(2).and_then(|idx| { + pending_splice.negotiated_candidates[idx].get_funding_txid() + }); + let contribution = pending_splice.contributions.last().cloned(); let tx_type = TransactionType::Splice { counterparty_node_id: self.context.counterparty_node_id, channel_id: self.context.channel_id, + contribution, + replaced_txid, }; funding_tx_signed.funding_tx = Some((funding_tx, tx_type)); funding_tx_signed.splice_negotiated = Some(splice_negotiated); diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index c08a0a9f471..5df66a9164d 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -710,10 +710,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 } @@ -731,6 +727,16 @@ impl FundingContribution { self.value_added } + /// 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`]. @@ -746,6 +752,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 + } + pub(super) fn into_tx_parts(self) -> (Vec, Vec) { let FundingContribution { inputs, mut outputs, change_output, .. } = self; diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 9adccd17627..99feff4cefe 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -520,13 +520,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 value of `TransactionType::Splice.replaced_txid` on +/// the resulting 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(); @@ -626,17 +636,28 @@ 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 Splice for both nodes. The initiator always contributes; + // the acceptor contributes iff the flag says so. Both parties must observe the same + // `replaced_txid` as the caller declares. + if let TransactionType::Splice { contribution, replaced_txid, .. } = &initiator_tx_type { + assert!( + contribution.is_some(), + "Initiator always contributes; expected Some, got None" + ); + assert_eq!(*replaced_txid, expected_replaced_txid, "initiator replaced_txid mismatch"); + } else { + panic!("Expected TransactionType::Splice, got {:?}", initiator_tx_type); + } + if let TransactionType::Splice { contribution, replaced_txid, .. } = &acceptor_tx_type { + assert_eq!( + contribution.is_some(), + acceptor_has_contribution, + "Acceptor contribution presence must match `acceptor_has_contribution`", + ); + assert_eq!(*replaced_txid, expected_replaced_txid, "acceptor replaced_txid mismatch"); + } else { + panic!("Expected TransactionType::Splice, got {:?}", acceptor_tx_type); + } tx }; (tx, splice_locked) @@ -658,7 +679,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); @@ -1418,7 +1439,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); @@ -1623,7 +1644,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()); @@ -1691,7 +1712,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()); @@ -1739,7 +1760,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); @@ -2400,7 +2421,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); @@ -4459,8 +4480,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 broadcast's `TransactionType::Splice.replaced_txid` 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); @@ -4497,7 +4524,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). @@ -4513,7 +4540,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); @@ -4534,7 +4566,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); @@ -5078,7 +5111,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()); @@ -5150,7 +5187,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()); @@ -5214,7 +5255,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); @@ -5381,8 +5422,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); @@ -5418,8 +5460,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); @@ -5505,8 +5552,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); @@ -5635,8 +5683,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); @@ -5721,7 +5770,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); @@ -5741,7 +5791,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); @@ -5812,8 +5863,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); @@ -6524,7 +6576,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 @@ -6548,11 +6600,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. @@ -6595,7 +6653,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 @@ -6619,11 +6677,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.