From 1ff1bb45c98304b18afd23a332951c60a44563d3 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 17 Mar 2026 13:23:16 -0500 Subject: [PATCH 1/4] Ensure minimum RBF feerate satisfies BIP125 The spec's 25/24 multiplier doesn't always satisfy BIP125's relay requirement of an absolute fee increase at low feerates, while a flat +25 sat/kwu increment falls below the spec's 25/24 rule above 600 sat/kwu. Use max(prev + 25, ceil(prev * 25/24)) for our own RBFs to satisfy both constraints, while still accepting the bare 25/24 rule from counterparties. Co-Authored-By: Claude Opus 4.6 (1M context) --- lightning/src/ln/channel.rs | 23 +++-- lightning/src/ln/funding.rs | 26 ++--- lightning/src/ln/splicing_tests.rs | 152 +++++++++++++++++++++-------- 3 files changed, 143 insertions(+), 58 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 3cc6a6b0d86..79d012ba686 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6484,6 +6484,19 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis)) } +/// Returns the minimum feerate for our own RBF attempts given a previous feerate. +/// +/// The spec (tx_init_rbf) requires the new feerate to be >= 25/24 of the previous feerate. +/// However, at low feerates that multiplier doesn't always satisfy BIP125's relay requirement of +/// an absolute fee increase, so we take the max of a flat +25 sat/kwu (0.1 sat/vB) increment +/// and the spec's multiplicative rule. We still accept the bare 25/24 rule from counterparties +/// in [`FundedChannel::validate_tx_init_rbf`]. +fn min_rbf_feerate(prev_feerate: u32) -> FeeRate { + let flat_increment = (prev_feerate as u64).saturating_add(25); + let spec_increment = ((prev_feerate as u64) * 25).div_ceil(24); + FeeRate::from_sat_per_kwu(cmp::max(flat_increment, spec_increment)) +} + /// Context for negotiating channels (dual-funded V2 open, splicing) #[derive(Debug)] pub(super) struct FundingNegotiationContext { @@ -12019,10 +12032,7 @@ where prev_feerate.is_some(), "pending_splice should have last_funding_feerate or funding_negotiation", ); - let min_rbf_feerate = prev_feerate.map(|f| { - let min_feerate_kwu = ((f as u64) * 25).div_ceil(24); - FeeRate::from_sat_per_kwu(min_feerate_kwu) - }); + let min_rbf_feerate = prev_feerate.map(min_rbf_feerate); let prior = if pending_splice.last_funding_feerate_sat_per_1000_weight.is_some() { self.build_prior_contribution() } else { @@ -12114,10 +12124,7 @@ where } match pending_splice.last_funding_feerate_sat_per_1000_weight { - Some(prev_feerate) => { - let min_feerate_kwu = ((prev_feerate as u64) * 25).div_ceil(24); - Ok(FeeRate::from_sat_per_kwu(min_feerate_kwu)) - }, + Some(prev_feerate) => Ok(min_rbf_feerate(prev_feerate)), None => Err(format!( "Channel {} has no prior feerate to compute RBF minimum", self.context.channel_id(), diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 0ba4ed188e6..c94b2806d60 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -218,8 +218,9 @@ impl PriorContribution { /// prior contribution logic internally — reusing an adjusted prior when possible, re-running /// coin selection when needed, or creating a fee-bump-only contribution. /// -/// Check [`FundingTemplate::min_rbf_feerate`] for the minimum feerate required (25/24 of -/// the previous feerate). Use [`FundingTemplate::prior_contribution`] to inspect the prior +/// Check [`FundingTemplate::min_rbf_feerate`] for the minimum feerate required (the greater of +/// the previous feerate + 25 sat/kwu and the spec's 25/24 rule). Use +/// [`FundingTemplate::prior_contribution`] to inspect the prior /// contribution's parameters (e.g., [`FundingContribution::value_added`], /// [`FundingContribution::outputs`]) before deciding whether to reuse it via the RBF methods /// or build a fresh contribution with different parameters using the splice methods above. @@ -232,8 +233,9 @@ pub struct FundingTemplate { /// transaction. shared_input: Option, - /// The minimum RBF feerate (25/24 of the previous feerate), if this template is for an - /// RBF attempt. `None` for fresh splices with no pending splice candidates. + /// The minimum RBF feerate (the greater of previous feerate + 25 sat/kwu and the spec's + /// 25/24 rule), if this template is for an RBF attempt. `None` for fresh splices with no + /// pending splice candidates. min_rbf_feerate: Option, /// The user's prior contribution from a previous splice negotiation, if available. @@ -2262,8 +2264,8 @@ mod tests { // When the caller's max_feerate is below the minimum RBF feerate, rbf_sync should // return Err(()). let prior_feerate = FeeRate::from_sat_per_kwu(2000); - let min_rbf_feerate = FeeRate::from_sat_per_kwu(5000); - let max_feerate = FeeRate::from_sat_per_kwu(3000); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); + let max_feerate = FeeRate::from_sat_per_kwu(2020); let prior = FundingContribution { value_added: Amount::from_sat(50_000), @@ -2276,7 +2278,7 @@ mod tests { is_splice: true, }; - // max_feerate (3000) < min_rbf_feerate (5000). + // max_feerate (2020) < min_rbf_feerate (2025). let template = FundingTemplate::new( None, Some(min_rbf_feerate), @@ -2359,8 +2361,8 @@ mod tests { // When the prior contribution's feerate is below the minimum RBF feerate and no // holder balance is available, rbf_sync should run coin selection to add inputs that // cover the higher RBF fee. - let min_rbf_feerate = FeeRate::from_sat_per_kwu(5000); let prior_feerate = FeeRate::from_sat_per_kwu(2000); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); let withdrawal = funding_output_sats(20_000); let prior = FundingContribution { @@ -2397,7 +2399,7 @@ mod tests { fn test_rbf_sync_no_prior_fee_bump_only_runs_coin_selection() { // When there is no prior contribution (e.g., acceptor), rbf_sync should run coin // selection to add inputs for a fee-bump-only contribution. - let min_rbf_feerate = FeeRate::from_sat_per_kwu(5000); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); let template = FundingTemplate::new(Some(shared_input(100_000)), Some(min_rbf_feerate), None); @@ -2419,7 +2421,7 @@ mod tests { // When the prior contribution's feerate is below the minimum RBF feerate and no // holder balance is available, rbf_sync should use the caller's max_feerate (not the // prior's) for the resulting contribution. - let min_rbf_feerate = FeeRate::from_sat_per_kwu(5000); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); let prior_max_feerate = FeeRate::from_sat_per_kwu(50_000); let callers_max_feerate = FeeRate::from_sat_per_kwu(10_000); let withdrawal = funding_output_sats(20_000); @@ -2458,8 +2460,8 @@ mod tests { // When splice_out_sync is called on a template with min_rbf_feerate set (user // choosing a fresh splice-out instead of rbf_sync), coin selection should NOT run. // Fees come from the channel balance. - let min_rbf_feerate = FeeRate::from_sat_per_kwu(5000); - let feerate = FeeRate::from_sat_per_kwu(5000); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); + let feerate = FeeRate::from_sat_per_kwu(2025); let withdrawal = funding_output_sats(20_000); let template = diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 20339e445bf..da5b79b6017 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -4322,8 +4322,8 @@ fn test_splice_rbf_acceptor_basic() { provide_utxo_reserves(&nodes, 2, added_value * 2); // Step 3: Use splice_channel API to initiate the RBF. - // Original feerate was FEERATE_FLOOR_SATS_PER_KW (253). 253 * 25 / 24 = 263.54, so 264 works. - let rbf_feerate_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24); + // Original feerate was FEERATE_FLOOR_SATS_PER_KW (253). 253 + 25 = 278. + let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu); let funding_contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate); @@ -4357,9 +4357,73 @@ fn test_splice_rbf_acceptor_basic() { ); } +#[test] +fn test_splice_rbf_at_high_feerate() { + // Test that min_rbf_feerate satisfies the spec's 25/24 rule at high feerates (above 600 + // sat/kwu, where a flat +25 increment alone would be insufficient). + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // 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) = + 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). + provide_utxo_reserves(&nodes, 2, added_value * 2); + let high_feerate = FeeRate::from_sat_per_kwu(1000); + let contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, high_feerate); + complete_rbf_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + contribution, + new_funding_script.clone(), + ); + let (_, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + // Step 3: RBF again using the template's min_rbf_feerate. The counterparty must accept it. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let rbf_feerate = { + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + funding_template.min_rbf_feerate().unwrap() + }; + let contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate); + complete_rbf_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + contribution, + new_funding_script, + ); + let (_, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], false); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); +} + #[test] fn test_splice_rbf_insufficient_feerate() { - // Test that splice_in_sync rejects a feerate that doesn't satisfy the 25/24 rule, and that the + // Test that splice_in_sync rejects a feerate that doesn't satisfy the +25 sat/kwu rule, and that the // acceptor also rejects tx_init_rbf with an insufficient feerate from a misbehaving peer. let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); @@ -4388,8 +4452,7 @@ fn test_splice_rbf_insufficient_feerate() { // Verify that the template exposes the RBF floor. let min_rbf_feerate = funding_template.min_rbf_feerate().unwrap(); - let expected_floor = - FeeRate::from_sat_per_kwu(((FEERATE_FLOOR_SATS_PER_KW as u64) * 25).div_ceil(24)); + let expected_floor = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); assert_eq!(min_rbf_feerate, expected_floor); let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); @@ -4417,6 +4480,22 @@ fn test_splice_rbf_insufficient_feerate() { let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); assert_eq!(tx_abort.channel_id, channel_id); + + // Acceptor-side: a counterparty feerate that satisfies the spec's 25/24 rule (264) is + // accepted, even though our own RBF floor (+25 sat/kwu = 278) is higher. + // After tx_abort the channel remains quiescent, so no need to re-enter quiescence. + nodes[0].node.handle_tx_abort(node_id_1, &tx_abort); + + let rbf_feerate_25_24 = ((FEERATE_FLOOR_SATS_PER_KW as u64) * 25).div_ceil(24) as u32; + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: rbf_feerate_25_24, + funding_output_contribution: Some(added_value.to_sat() as i64), + }; + + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + let _tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0); } #[test] @@ -4695,7 +4774,7 @@ fn test_splice_rbf_not_quiescence_initiator() { provide_utxo_reserves(&nodes, 2, added_value * 2); // Initiate RBF from node 0 (quiescence initiator). - let rbf_feerate_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24); + let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu); let _funding_contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate); @@ -4725,7 +4804,7 @@ fn test_splice_rbf_not_quiescence_initiator() { #[test] fn test_splice_rbf_both_contribute_tiebreak() { - let min_rbf_feerate = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24); + let min_rbf_feerate = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; let feerate = FeeRate::from_sat_per_kwu(min_rbf_feerate); let added_value = Amount::from_sat(50_000); do_test_splice_rbf_tiebreak(feerate, feerate, added_value, true); @@ -4735,7 +4814,7 @@ fn test_splice_rbf_both_contribute_tiebreak() { fn test_splice_rbf_tiebreak_higher_feerate() { // Node 0 (winner) uses a higher feerate than node 1 (loser). Node 1's change output is // adjusted (reduced) to accommodate the higher feerate. Negotiation succeeds. - let min_rbf_feerate = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24); + let min_rbf_feerate = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; do_test_splice_rbf_tiebreak( FeeRate::from_sat_per_kwu(min_rbf_feerate * 3), FeeRate::from_sat_per_kwu(min_rbf_feerate), @@ -4749,7 +4828,7 @@ fn test_splice_rbf_tiebreak_lower_feerate() { // Node 0 (winner) uses a lower feerate than node 1 (loser). Since the initiator's feerate // is below node 1's minimum, node 1 proceeds without contribution and will retry via a new // splice at its preferred feerate after the RBF locks. - let min_rbf_feerate = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24); + let min_rbf_feerate = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; do_test_splice_rbf_tiebreak( FeeRate::from_sat_per_kwu(min_rbf_feerate), FeeRate::from_sat_per_kwu(min_rbf_feerate * 3), @@ -4763,7 +4842,7 @@ fn test_splice_rbf_tiebreak_feerate_too_high() { // Node 0 (winner) uses a feerate high enough that node 1's (loser) contribution cannot // cover the fees. Node 1 proceeds without its contribution (QuiescentAction is preserved // for a future splice). The RBF completes with only node 0's inputs/outputs. - let min_rbf_feerate = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24); + let min_rbf_feerate = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; do_test_splice_rbf_tiebreak( FeeRate::from_sat_per_kwu(20_000), FeeRate::from_sat_per_kwu(min_rbf_feerate), @@ -5064,7 +5143,7 @@ fn test_splice_rbf_tiebreak_feerate_too_high_rejected() { // The target (100k) far exceeds node 1's max (3k), and the fair fee at 100k exceeds // node 1's budget, triggering TooHigh. let high_feerate = FeeRate::from_sat_per_kwu(100_000); - let min_rbf_feerate_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24); + let min_rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; let min_rbf_feerate = FeeRate::from_sat_per_kwu(min_rbf_feerate_sat_per_kwu); let node_1_max_feerate = FeeRate::from_sat_per_kwu(3_000); @@ -5194,7 +5273,7 @@ fn test_splice_rbf_acceptor_recontributes() { provide_utxo_reserves(&nodes, 2, added_value * 2); // Step 5: Only node 0 calls splice_channel + funding_contributed. - let rbf_feerate_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24); + let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu); let rbf_funding_contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate); @@ -5318,8 +5397,7 @@ fn test_splice_rbf_after_counterparty_rbf_aborted() { // is adjusted to the RBF feerate via for_acceptor_at_feerate. provide_utxo_reserves(&nodes, 2, added_value * 2); - let rbf_feerate = - FeeRate::from_sat_per_kwu((FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24)); + let rbf_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); let _rbf_funding_contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate); @@ -5482,7 +5560,7 @@ fn test_splice_rbf_sequential() { // Three consecutive RBF rounds on the same splice (initial → RBF #1 → RBF #2). // Node 0 is the quiescence initiator; node 1 is the acceptor with no contribution. // Verifies: - // - Each round satisfies the 25/24 feerate rule + // - Each round satisfies the +25 sat/kwu feerate rule // - DiscardFunding events reference the correct txids from previous rounds // - The final RBF can be mined and splice_locked successfully let chanmon_cfgs = create_chanmon_cfgs(2); @@ -5505,11 +5583,11 @@ fn test_splice_rbf_sequential() { let (splice_tx_0, new_funding_script) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); - // Feerate progression: 253 → ceil(253*25/24) = 264 → ceil(264*25/24) = 275 - let feerate_1_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24); // 264 - let feerate_2_sat_per_kwu = (feerate_1_sat_per_kwu * 25).div_ceil(24); + // Feerate progression: 253 → 253+25 = 278 → 278+25 = 303 + let feerate_1_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; // 278 + let feerate_2_sat_per_kwu = feerate_1_sat_per_kwu + 25; - // --- Round 1: RBF #1 at feerate 264. --- + // --- Round 1: RBF #1 at feerate 278. --- provide_utxo_reserves(&nodes, 2, added_value * 2); let rbf_feerate_1 = FeeRate::from_sat_per_kwu(feerate_1_sat_per_kwu); @@ -5529,7 +5607,7 @@ fn test_splice_rbf_sequential() { expect_splice_pending_event(&nodes[0], &node_id_1); expect_splice_pending_event(&nodes[1], &node_id_0); - // --- Round 2: RBF #2 at feerate 275. --- + // --- Round 2: RBF #2 at feerate 303. --- provide_utxo_reserves(&nodes, 2, added_value * 2); let rbf_feerate_2 = FeeRate::from_sat_per_kwu(feerate_2_sat_per_kwu); @@ -5625,7 +5703,7 @@ fn test_splice_rbf_acceptor_contributes_then_disconnects() { // --- Round 1: Node 0 initiates RBF; node 1 re-contributes via prior. --- provide_utxo_reserves(&nodes, 2, added_value * 2); - let rbf_feerate_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24); + let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu); let _rbf_funding_contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, rbf_feerate); @@ -5697,7 +5775,7 @@ fn test_splice_rbf_disconnect_filters_prior_contributions() { // Include a splice-out output with a different script_pubkey so the test can verify // selective filtering: the change output (same script_pubkey as round 0) is filtered, // while the splice-out output (different script_pubkey) survives. - let feerate_1_sat_per_kwu = (FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24); + let feerate_1_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; let rbf_feerate = FeeRate::from_sat_per_kwu(feerate_1_sat_per_kwu); let splice_out_output = TxOut { value: Amount::from_sat(1_000), @@ -5747,9 +5825,9 @@ fn test_splice_rbf_disconnect_filters_prior_contributions() { reconnect_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_args); - // --- Round 2: RBF at the same feerate as the failed round 1 (264). --- + // --- Round 2: RBF at the same feerate as the failed round 1 (278). --- // This should succeed because the failed round never updated the feerate floor, which - // remains at round 0's rate (253), and 264 >= ceil(253 * 25/24). + // remains at round 0's rate (253), and 278 >= 253 + 25. provide_utxo_reserves(&nodes, 1, added_value * 2); let rbf_feerate_2 = FeeRate::from_sat_per_kwu(feerate_1_sat_per_kwu); @@ -5809,8 +5887,7 @@ fn test_splice_channel_with_pending_splice_includes_rbf_floor() { // Call splice_channel again — the pending splice should cause min_rbf_feerate to be set // and the prior contribution to be available. let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); - let expected_floor = - FeeRate::from_sat_per_kwu(((FEERATE_FLOOR_SATS_PER_KW as u64) * 25).div_ceil(24)); + let expected_floor = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); assert_eq!(funding_template.min_rbf_feerate(), Some(expected_floor)); assert!(funding_template.prior_contribution().is_some()); @@ -5859,7 +5936,7 @@ fn test_funding_contributed_adjusts_feerate_for_rbf() { splice_channel(&nodes[1], &nodes[0], channel_id, node_1_contribution); // Node 0 calls funding_contributed. The contribution's feerate (floor) is below the RBF - // floor (25/24 of floor), but funding_contributed adjusts it upward. + // floor (floor + 25 sat/kwu), but funding_contributed adjusts it upward. nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution.clone(), None).unwrap(); // STFU should be sent immediately (the adjusted feerate satisfies the RBF check). @@ -5871,8 +5948,7 @@ fn test_funding_contributed_adjusts_feerate_for_rbf() { // Verify the RBF handshake proceeds. let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); let rbf_feerate = FeeRate::from_sat_per_kwu(tx_init_rbf.feerate_sat_per_1000_weight as u64); - let expected_floor = - FeeRate::from_sat_per_kwu((FEERATE_FLOOR_SATS_PER_KW as u64 * 25).div_ceil(24)); + let expected_floor = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); assert!(rbf_feerate >= expected_floor); } @@ -5897,7 +5973,7 @@ fn test_funding_contributed_rbf_adjustment_exceeds_max_feerate() { provide_utxo_reserves(&nodes, 4, added_value * 2); // Node 0 calls splice_channel and builds contribution with max_feerate = floor_feerate. - // This means the minimum RBF feerate (25/24 of floor) will exceed max_feerate, preventing adjustment. + // This means the minimum RBF feerate (floor + 25 sat/kwu) will exceed max_feerate, preventing adjustment. let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); @@ -5972,7 +6048,8 @@ fn test_funding_contributed_rbf_adjustment_insufficient_budget() { funding_template.splice_in_sync(added_value, floor_feerate, FeeRate::MAX, &wallet).unwrap(); // Node 1 initiates a splice at a HIGH feerate (10,000 sat/kwu). The minimum RBF feerate will be - // 25/24 of 10,000 = 10,417 sat/kwu — far above what node 0's tight budget can handle. + // max(10,000 + 25, ceil(10,000 * 25/24)) = 10,417 sat/kwu — far above what node 0's tight + // budget can handle. let high_feerate = FeeRate::from_sat_per_kwu(10_000); let node_1_template = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); let node_1_wallet = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); @@ -6047,7 +6124,7 @@ fn test_prior_contribution_unadjusted_when_max_feerate_too_low() { .unwrap(); let (_splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); - // Call splice_channel again — the minimum RBF feerate (25/24 of floor) exceeds the prior + // Call splice_channel again — the minimum RBF feerate (floor + 25 sat/kwu) exceeds the prior // contribution's max_feerate (floor), so adjustment fails. rbf_sync re-runs coin selection // with the caller's max_feerate. let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); @@ -6092,8 +6169,7 @@ fn test_splice_channel_during_negotiation_includes_rbf_feerate() { // Node 0 (acceptor) calls splice_channel while the negotiation is in progress. // min_rbf_feerate should be derived from the in-progress negotiation's feerate. let template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); - let expected_floor = - FeeRate::from_sat_per_kwu(((FEERATE_FLOOR_SATS_PER_KW as u64) * 25).div_ceil(24)); + let expected_floor = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); assert_eq!(template.min_rbf_feerate(), Some(expected_floor)); // No prior contribution since there are no negotiated candidates yet. rbf_sync runs @@ -6339,7 +6415,7 @@ fn test_splice_rbf_rejects_low_feerate_after_several_attempts() { // Rounds 1-10: RBF at minimum bump. Accepted (at or below threshold). let mut prev_feerate = FEERATE_FLOOR_SATS_PER_KW as u64; for _ in 0..10 { - let feerate = (prev_feerate * 25).div_ceil(24); + let feerate = prev_feerate + 25; provide_utxo_reserves(&nodes, 2, added_value * 2); let rbf_feerate = FeeRate::from_sat_per_kwu(feerate); let contribution = @@ -6360,7 +6436,7 @@ fn test_splice_rbf_rejects_low_feerate_after_several_attempts() { } // Round 11: RBF at minimum bump. Should be rejected because feerate < fee estimator. - let next_feerate = (prev_feerate * 25).div_ceil(24); + let next_feerate = prev_feerate + 25; provide_utxo_reserves(&nodes, 2, added_value * 2); let rbf_feerate = FeeRate::from_sat_per_kwu(next_feerate); let _contribution = @@ -6410,7 +6486,7 @@ fn test_splice_rbf_rejects_own_low_feerate_after_several_attempts() { // Rounds 1-10: RBF at minimum bump. Accepted (at or below threshold). let mut prev_feerate = FEERATE_FLOOR_SATS_PER_KW as u64; for _ in 0..10 { - let feerate = (prev_feerate * 25).div_ceil(24); + let feerate = prev_feerate + 25; provide_utxo_reserves(&nodes, 2, added_value * 2); let rbf_feerate = FeeRate::from_sat_per_kwu(feerate); let contribution = @@ -6431,7 +6507,7 @@ fn test_splice_rbf_rejects_own_low_feerate_after_several_attempts() { } // Round 11: Our own RBF at minimum bump. funding_contributed should reject it. - let next_feerate = (prev_feerate * 25).div_ceil(24); + let next_feerate = prev_feerate + 25; provide_utxo_reserves(&nodes, 2, added_value * 2); let rbf_feerate = FeeRate::from_sat_per_kwu(next_feerate); let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); From f517aac704e009e1e247f3f912bd31466a0cacac Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 17 Mar 2026 15:25:55 -0500 Subject: [PATCH 2/4] Exit quiescence when tx_init_rbf is rejected with Abort When tx_init_rbf is rejected with ChannelError::Abort (e.g., insufficient RBF feerate, negotiation in progress, feerate too high), the error is converted to a tx_abort message but quiescence is never exited and holding cells are never freed. This leaves the channel stuck in a quiescent state. Fix this by intercepting ChannelError::Abort before try_channel_entry! in internal_tx_init_rbf, calling exit_quiescence on the channel, and returning the error with exited_quiescence set so that handle_error frees holding cells. Also make exit_quiescence available in non-test builds by removing its cfg gate. Update tests to use the proper RBF initiation flow (with tampered feerates) so that handle_tx_abort correctly echoes the abort and exits quiescence, rather than manually crafting tx_init_rbf messages that leave node 0 without proper negotiation state. Co-Authored-By: Claude Opus 4.6 (1M context) --- lightning/src/ln/channel.rs | 2 - lightning/src/ln/channelmanager.rs | 9 ++++ lightning/src/ln/splicing_tests.rs | 72 ++++++++++++++++++++++-------- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 79d012ba686..44d7f51694d 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -13949,8 +13949,6 @@ where Some(msgs::Stfu { channel_id: self.context.channel_id, initiator }) } - #[cfg(any(test, fuzzing, feature = "_test_utils"))] - #[rustfmt::skip] pub fn exit_quiescence(&mut self) -> bool { // Make sure we either finished the quiescence handshake and are quiescent, or we never // attempted to initiate quiescence at all. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8356e5f32fc..183ce812c9b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -13151,6 +13151,15 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self.fee_estimator, &self.logger, ); + if let Err(ChannelError::Abort(_)) = &init_res { + funded_channel.exit_quiescence(); + let chan_id = funded_channel.context.channel_id(); + let res = MsgHandleErrInternal::from_chan_no_close( + init_res.unwrap_err(), + chan_id, + ); + return Err(res.with_exited_quiescence(true)); + } let tx_ack_rbf_msg = try_channel_entry!(self, peer_state, init_res, chan_entry); peer_state.pending_msg_events.push(MessageSendEvent::SendTxAckRbf { node_id: *counterparty_node_id, diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index da5b79b6017..afd4d307d32 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -4467,33 +4467,54 @@ fn test_splice_rbf_insufficient_feerate() { .is_ok()); // Acceptor-side: tx_init_rbf with an insufficient feerate is also rejected. - reenter_quiescence(&nodes[0], &nodes[1], &channel_id); + // Node 0 initiates a proper RBF but we tamper the feerate to be insufficient. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let _funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, min_rbf_feerate); - let tx_init_rbf = msgs::TxInitRbf { - channel_id, - locktime: 0, - feerate_sat_per_1000_weight: FEERATE_FLOOR_SATS_PER_KW, - funding_output_contribution: Some(added_value.to_sat() as i64), - }; + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + let mut tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + tx_init_rbf.feerate_sat_per_1000_weight = FEERATE_FLOOR_SATS_PER_KW; nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); assert_eq!(tx_abort.channel_id, channel_id); + // Node 0 echoes tx_abort and exits quiescence. + nodes[0].node.handle_tx_abort(node_id_1, &tx_abort); + let tx_abort_echo = get_event_msg!(nodes[0], MessageSendEvent::SendTxAbort, node_id_1); + + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2); + assert!( + matches!(&events[0], Event::SpliceFailed { channel_id: cid, .. } if *cid == channel_id) + ); + assert!( + matches!(&events[1], Event::DiscardFunding { channel_id: cid, .. } if *cid == channel_id) + ); + + // Node 1 handles the echo (no-op since it already aborted). + nodes[1].node.handle_tx_abort(node_id_0, &tx_abort_echo); + // Acceptor-side: a counterparty feerate that satisfies the spec's 25/24 rule (264) is // accepted, even though our own RBF floor (+25 sat/kwu = 278) is higher. - // After tx_abort the channel remains quiescent, so no need to re-enter quiescence. - nodes[0].node.handle_tx_abort(node_id_1, &tx_abort); + // Node 0 initiates another proper RBF but we tamper the feerate to the 25/24 value. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let _funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, added_value, min_rbf_feerate); - let rbf_feerate_25_24 = ((FEERATE_FLOOR_SATS_PER_KW as u64) * 25).div_ceil(24) as u32; - let tx_init_rbf = msgs::TxInitRbf { - channel_id, - locktime: 0, - feerate_sat_per_1000_weight: rbf_feerate_25_24, - funding_output_contribution: Some(added_value.to_sat() as i64), - }; + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + let mut tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + let rbf_feerate_25_24 = ((FEERATE_FLOOR_SATS_PER_KW as u64) * 25).div_ceil(24) as u32; + tx_init_rbf.feerate_sat_per_1000_weight = rbf_feerate_25_24; nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); let _tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0); } @@ -5181,10 +5202,25 @@ fn test_splice_rbf_tiebreak_feerate_too_high_rejected() { assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, high_feerate.to_sat_per_kwu() as u32); // Node 1 handles tx_init_rbf — TooHigh: target (100k) >> max (3k) and fair fee > budget. + // Node 1 exits quiescence upon rejecting with tx_abort, and since it has a pending + // QuiescentAction (from its own splice RBF attempt), it immediately re-proposes quiescence. nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); - let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); - assert_eq!(tx_abort.channel_id, channel_id); + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2); + match &msg_events[0] { + MessageSendEvent::SendTxAbort { node_id, msg } => { + assert_eq!(*node_id, node_id_0); + assert_eq!(msg.channel_id, channel_id); + }, + _ => panic!("Expected SendTxAbort, got {:?}", msg_events[0]), + }; + match &msg_events[1] { + MessageSendEvent::SendStfu { node_id, .. } => { + assert_eq!(*node_id, node_id_0); + }, + _ => panic!("Expected SendStfu, got {:?}", msg_events[1]), + }; } #[test] From 886351e4b00936a3fbdb065d7df7b560304be488 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 17 Mar 2026 15:43:46 -0500 Subject: [PATCH 3/4] Exit quiescence when splice_init is rejected with Abort The same bug fixed in the prior commit for tx_init_rbf also exists in internal_splice_init: when splice_init triggers FeeRateTooHigh in resolve_queued_contribution, the ChannelError::Abort goes through try_channel_entry! without exiting quiescence. Apply the same fix: intercept ChannelError::Abort before try_channel_entry!, call exit_quiescence, and return the error with exited_quiescence set. Co-Authored-By: Claude Opus 4.6 (1M context) --- lightning/src/ln/channelmanager.rs | 9 +++++++++ lightning/src/ln/splicing_tests.rs | 19 +++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 183ce812c9b..d69fe0a309c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -13106,6 +13106,15 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self.get_our_node_id(), &self.logger, ); + if let Err(ChannelError::Abort(_)) = &init_res { + funded_channel.exit_quiescence(); + let chan_id = funded_channel.context.channel_id(); + let res = MsgHandleErrInternal::from_chan_no_close( + init_res.unwrap_err(), + chan_id, + ); + return Err(res.with_exited_quiescence(true)); + } let splice_ack_msg = try_channel_entry!(self, peer_state, init_res, chan_entry); peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck { node_id: *counterparty_node_id, diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index afd4d307d32..736e806797c 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -1813,10 +1813,25 @@ fn test_splice_tiebreak_feerate_too_high_rejected() { let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); // Node 1 handles SpliceInit — TooHigh: target (100k) >> max (3k) and fair fee > budget. + // Node 1 exits quiescence upon rejecting with tx_abort, and since it has a pending + // QuiescentAction (from its own splice attempt), it immediately re-proposes quiescence. nodes[1].node.handle_splice_init(node_id_0, &splice_init); - let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); - assert_eq!(tx_abort.channel_id, channel_id); + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2); + match &msg_events[0] { + MessageSendEvent::SendTxAbort { node_id, msg } => { + assert_eq!(*node_id, node_id_0); + assert_eq!(msg.channel_id, channel_id); + }, + _ => panic!("Expected SendTxAbort, got {:?}", msg_events[0]), + }; + match &msg_events[1] { + MessageSendEvent::SendStfu { node_id, .. } => { + assert_eq!(*node_id, node_id_0); + }, + _ => panic!("Expected SendStfu, got {:?}", msg_events[1]), + }; } #[cfg(test)] From 69a1a1c373ca761d975f958a427fe36ca3a0c72b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 17 Mar 2026 16:31:10 -0500 Subject: [PATCH 4/4] Return InteractiveTxMsgError from splice_init and tx_init_rbf The prior two commits manually intercepted ChannelError::Abort in the channelmanager handlers for splice_init and tx_init_rbf to exit quiescence before returning, since the channel methods didn't signal this themselves. The interactive TX message handlers already solved this by returning InteractiveTxMsgError which bundles exited_quiescence into the error type. Apply the same pattern: change splice_init and tx_init_rbf to return InteractiveTxMsgError, adding a quiescent_negotiation_err helper on FundedChannel that exits quiescence for Abort errors and passes through other variants unchanged. Extract handle_interactive_tx_msg_err in channelmanager to deduplicate the error handling across internal_tx_msg, internal_splice_init, and internal_tx_init_rbf. Co-Authored-By: Claude Opus 4.6 (1M context) --- lightning/src/ln/channel.rs | 40 +++++--- lightning/src/ln/channelmanager.rs | 143 ++++++++++++++++------------- 2 files changed, 105 insertions(+), 78 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 44d7f51694d..e100783e794 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -12595,13 +12595,15 @@ where pub(crate) fn splice_init( &mut self, msg: &msgs::SpliceInit, entropy_source: &ES, holder_node_id: &PublicKey, logger: &L, - ) -> Result { + ) -> Result { let feerate = FeeRate::from_sat_per_kwu(msg.funding_feerate_per_kw as u64); - let (our_funding_contribution, holder_balance) = - self.resolve_queued_contribution(feerate, logger)?; + let (our_funding_contribution, holder_balance) = self + .resolve_queued_contribution(feerate, logger) + .map_err(|e| self.quiescent_negotiation_err(e))?; - let splice_funding = - self.validate_splice_init(msg, our_funding_contribution.unwrap_or(SignedAmount::ZERO))?; + let splice_funding = self + .validate_splice_init(msg, our_funding_contribution.unwrap_or(SignedAmount::ZERO)) + .map_err(|e| self.quiescent_negotiation_err(e))?; // Adjust for the feerate and clone so we can store it for future RBF re-use. let (adjusted_contribution, our_funding_inputs, our_funding_outputs) = @@ -12753,10 +12755,11 @@ where pub(crate) fn tx_init_rbf( &mut self, msg: &msgs::TxInitRbf, entropy_source: &ES, holder_node_id: &PublicKey, fee_estimator: &LowerBoundedFeeEstimator, logger: &L, - ) -> Result { + ) -> Result { let feerate = FeeRate::from_sat_per_kwu(msg.feerate_sat_per_1000_weight as u64); - let (queued_net_value, holder_balance) = - self.resolve_queued_contribution(feerate, logger)?; + let (queued_net_value, holder_balance) = self + .resolve_queued_contribution(feerate, logger) + .map_err(|e| self.quiescent_negotiation_err(e))?; // If no queued contribution, try prior contribution from previous negotiation. // Failing here means the RBF would erase our splice — reject it. @@ -12773,7 +12776,8 @@ where prior .net_value_for_acceptor_at_feerate(feerate, holder_balance) .map_err(|_| ChannelError::Abort(AbortReason::InsufficientRbfFeerate)) - })?; + }) + .map_err(|e| self.quiescent_negotiation_err(e))?; Some(net_value) } else { None @@ -12781,11 +12785,13 @@ where let our_funding_contribution = queued_net_value.or(prior_net_value); - let rbf_funding = self.validate_tx_init_rbf( - msg, - our_funding_contribution.unwrap_or(SignedAmount::ZERO), - fee_estimator, - )?; + let rbf_funding = self + .validate_tx_init_rbf( + msg, + our_funding_contribution.unwrap_or(SignedAmount::ZERO), + fee_estimator, + ) + .map_err(|e| self.quiescent_negotiation_err(e))?; // Consume the appropriate contribution source. let (our_funding_inputs, our_funding_outputs) = if queued_net_value.is_some() { @@ -13961,6 +13967,12 @@ where was_quiescent } + fn quiescent_negotiation_err(&mut self, err: ChannelError) -> InteractiveTxMsgError { + let exited_quiescence = + if matches!(err, ChannelError::Abort(_)) { self.exit_quiescence() } else { false }; + InteractiveTxMsgError { err, splice_funding_failed: None, exited_quiescence } + } + pub fn remove_legacy_scids_before_block(&mut self, height: u32) -> alloc::vec::Drain<'_, u64> { let end = self .funding diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d69fe0a309c..40f8d95a99b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11673,6 +11673,39 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } + fn handle_interactive_tx_msg_err( + &self, err: InteractiveTxMsgError, channel_id: ChannelId, counterparty_node_id: &PublicKey, + user_channel_id: u128, + ) -> MsgHandleErrInternal { + if let Some(splice_funding_failed) = err.splice_funding_failed { + let pending_events = &mut self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::SpliceFailed { + channel_id, + counterparty_node_id: *counterparty_node_id, + user_channel_id, + abandoned_funding_txo: splice_funding_failed.funding_txo, + channel_type: splice_funding_failed.channel_type.clone(), + }, + None, + )); + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id, + funding_info: FundingInfo::Contribution { + inputs: splice_funding_failed.contributed_inputs, + outputs: splice_funding_failed.contributed_outputs, + }, + }, + None, + )); + } + debug_assert!(!err.exited_quiescence || matches!(err.err, ChannelError::Abort(_))); + + MsgHandleErrInternal::from_chan_no_close(err.err, channel_id) + .with_exited_quiescence(err.exited_quiescence) + } + fn internal_tx_msg< HandleTxMsgFn: Fn(&mut Channel) -> Result, >( @@ -11694,38 +11727,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ peer_state.pending_msg_events.push(msg_send_event); Ok(()) }, - Err(InteractiveTxMsgError { - err, - splice_funding_failed, - exited_quiescence, - }) => { - if let Some(splice_funding_failed) = splice_funding_failed { - let pending_events = &mut self.pending_events.lock().unwrap(); - pending_events.push_back(( - events::Event::SpliceFailed { - channel_id, - counterparty_node_id: *counterparty_node_id, - user_channel_id: channel.context().get_user_id(), - abandoned_funding_txo: splice_funding_failed.funding_txo, - channel_type: splice_funding_failed.channel_type.clone(), - }, - None, - )); - pending_events.push_back(( - events::Event::DiscardFunding { - channel_id, - funding_info: FundingInfo::Contribution { - inputs: splice_funding_failed.contributed_inputs, - outputs: splice_funding_failed.contributed_outputs, - }, - }, - None, - )); - } - debug_assert!(!exited_quiescence || matches!(err, ChannelError::Abort(_))); - - Err(MsgHandleErrInternal::from_chan_no_close(err, channel_id) - .with_exited_quiescence(exited_quiescence)) + Err(err) => { + let user_channel_id = channel.context().get_user_id(); + Err(self.handle_interactive_tx_msg_err( + err, + channel_id, + counterparty_node_id, + user_channel_id, + )) }, } }, @@ -13100,27 +13109,30 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { - let init_res = funded_channel.splice_init( + let user_channel_id = funded_channel.context.get_user_id(); + match funded_channel.splice_init( msg, &self.entropy_source, &self.get_our_node_id(), &self.logger, - ); - if let Err(ChannelError::Abort(_)) = &init_res { - funded_channel.exit_quiescence(); - let chan_id = funded_channel.context.channel_id(); - let res = MsgHandleErrInternal::from_chan_no_close( - init_res.unwrap_err(), - chan_id, - ); - return Err(res.with_exited_quiescence(true)); + ) { + Ok(splice_ack_msg) => { + peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck { + node_id: *counterparty_node_id, + msg: splice_ack_msg, + }); + Ok(()) + }, + Err(err) => { + debug_assert!(err.splice_funding_failed.is_none()); + Err(self.handle_interactive_tx_msg_err( + err, + msg.channel_id, + counterparty_node_id, + user_channel_id, + )) + }, } - let splice_ack_msg = try_channel_entry!(self, peer_state, init_res, chan_entry); - peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck { - node_id: *counterparty_node_id, - msg: splice_ack_msg, - }); - Ok(()) } else { try_channel_entry!( self, @@ -13153,28 +13165,31 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }, hash_map::Entry::Occupied(mut chan_entry) => { if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { - let init_res = funded_channel.tx_init_rbf( + let user_channel_id = funded_channel.context.get_user_id(); + match funded_channel.tx_init_rbf( msg, &self.entropy_source, &self.get_our_node_id(), &self.fee_estimator, &self.logger, - ); - if let Err(ChannelError::Abort(_)) = &init_res { - funded_channel.exit_quiescence(); - let chan_id = funded_channel.context.channel_id(); - let res = MsgHandleErrInternal::from_chan_no_close( - init_res.unwrap_err(), - chan_id, - ); - return Err(res.with_exited_quiescence(true)); + ) { + Ok(tx_ack_rbf_msg) => { + peer_state.pending_msg_events.push(MessageSendEvent::SendTxAckRbf { + node_id: *counterparty_node_id, + msg: tx_ack_rbf_msg, + }); + Ok(()) + }, + Err(err) => { + debug_assert!(err.splice_funding_failed.is_none()); + Err(self.handle_interactive_tx_msg_err( + err, + msg.channel_id, + counterparty_node_id, + user_channel_id, + )) + }, } - let tx_ack_rbf_msg = try_channel_entry!(self, peer_state, init_res, chan_entry); - peer_state.pending_msg_events.push(MessageSendEvent::SendTxAckRbf { - node_id: *counterparty_node_id, - msg: tx_ack_rbf_msg, - }); - Ok(()) } else { try_channel_entry!( self,