From 0453078b41c50c9015baed471feebadfb8833ef6 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 17 Apr 2026 10:03:38 -0500 Subject: [PATCH 1/2] Expose splice contribution and replaced txid on broadcast `TransactionType::Splice` now carries the local contribution to the splice and, for RBF, the txid of the prior negotiated candidate being replaced. This lets LDK Node update its `PaymentDetails` and `PendingPaymentDetails` from the broadcast callback without re-deriving the contribution from the raw transaction or tracking RBF chains itself. Driving these updates from the `SplicePending` event instead is more complicated because that event races with BDK wallet syncing and doesn't carry the replaced txid; the broadcast callback is a cleaner integration point. The `Hash` derive on `TransactionType` is dropped since it isn't used anywhere in the workspace and `FundingContribution` (now embedded) transitively contains `ConfirmedUtxo`, which doesn't derive `Hash`. Co-Authored-By: Claude Opus 4.7 (1M context) --- lightning/src/chain/chaininterface.rs | 10 +- lightning/src/ln/channel.rs | 7 + lightning/src/ln/splicing_tests.rs | 179 ++++++++++++++++++-------- 3 files changed, 139 insertions(+), 57 deletions(-) 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 10801edef01..0944e983e84 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -9394,9 +9394,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/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index fa22ccb61c7..626295eb977 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -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 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(); @@ -598,17 +608,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) @@ -630,7 +651,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 +1332,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 +1537,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 +1605,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 +1653,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 +2315,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 +4370,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); @@ -4387,7 +4414,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 +4430,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 +4456,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 +4577,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 +4592,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 +5088,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 +5164,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 +5232,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 +5399,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 +5437,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 +5529,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 +5662,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 +5749,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 +5770,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 +5821,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 +5834,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 +5856,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 +5871,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 +5889,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 +5903,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 +5949,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 +5962,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 +5983,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 +5997,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 +6008,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 +6073,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 +6786,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 +6810,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 +6863,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 +6887,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. From dacf092c2bb9cddeba1b27bcd3f902a9b19410ae Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 17 Apr 2026 10:35:05 -0500 Subject: [PATCH 2/2] Expose additional FundingContribution accessors Add public getters for `estimated_fee`, `inputs`, and `max_feerate`, and elevate `feerate` from `pub(super)` to `pub`. Together with the existing `value_added`, `outputs`, and `change_output`, this gives downstream consumers of `TransactionType::Splice` (notably LDK Node, which updates `PaymentDetails` from the broadcast callback) the data they need without reaching into the raw transaction. Co-Authored-By: Claude Opus 4.7 (1M context) --- lightning/src/ln/funding.rs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) 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