Skip to content

Exit quiescence when splice_init and tx_init_rbf are rejected#4495

Open
jkczyz wants to merge 5 commits intolightningdevkit:mainfrom
jkczyz:2026-03-splicing-rbf-follow-ups
Open

Exit quiescence when splice_init and tx_init_rbf are rejected#4495
jkczyz wants to merge 5 commits intolightningdevkit:mainfrom
jkczyz:2026-03-splicing-rbf-follow-ups

Conversation

@jkczyz
Copy link
Copy Markdown
Contributor

@jkczyz jkczyz commented Mar 18, 2026

Summary

  • Fix quiescence not being exited when tx_init_rbf or splice_init is rejected with Abort (e.g., insufficient RBF feerate, negotiation in progress), which left the channel stuck in a quiescent state
  • Refactor both handlers to return InteractiveTxMsgError, reusing the same pattern already used by the interactive TX message handlers, with a shared handle_interactive_tx_msg_err helper in channelmanager

Test plan

  • Existing test_splice_rbf_insufficient_feerate updated to verify quiescence is properly exited after tx_abort
  • Existing test_splice_feerate_too_high updated to verify quiescence is properly exited after splice_init rejection
  • All splicing tests pass

🤖 Generated with Claude Code

Based on #4494.

@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Mar 18, 2026

I've assigned @wpaulino as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@jkczyz jkczyz changed the title Exit quiescence when splice_init and tx_init_rbf are rejected Exit quiescence when splice_init and tx_init_rbf are rejected Mar 18, 2026
@jkczyz jkczyz force-pushed the 2026-03-splicing-rbf-follow-ups branch from a5d670c to 57aad34 Compare March 18, 2026 18:57
@jkczyz jkczyz self-assigned this Mar 19, 2026
@jkczyz jkczyz mentioned this pull request Mar 19, 2026
36 tasks
@jkczyz jkczyz force-pushed the 2026-03-splicing-rbf-follow-ups branch 3 times, most recently from 8cdc480 to 959b553 Compare March 31, 2026 00:16
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 61.79775% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.22%. Comparing base (8d00139) to head (959b553).
⚠️ Report is 111 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channelmanager.rs 42.85% 30 Missing and 2 partials ⚠️
lightning/src/ln/channel.rs 92.30% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4495      +/-   ##
==========================================
- Coverage   86.24%   86.22%   -0.02%     
==========================================
  Files         160      161       +1     
  Lines      107909   108651     +742     
  Branches   107909   108651     +742     
==========================================
+ Hits        93061    93681     +620     
- Misses      12212    12339     +127     
+ Partials     2636     2631       -5     
Flag Coverage Δ
tests 86.22% <61.79%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

jkczyz and others added 4 commits March 31, 2026 17:24
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@jkczyz jkczyz force-pushed the 2026-03-splicing-rbf-follow-ups branch from 959b553 to 69a1a1c Compare March 31, 2026 22:28
@jkczyz jkczyz marked this pull request as ready for review April 2, 2026 17:03
@ldk-reviews-bot ldk-reviews-bot requested a review from wpaulino April 2, 2026 17:04
Comment on lines +13970 to +13974
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 }
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In splice_init, resolve_queued_contribution (which can return Abort(FeeRateTooHigh)) is called before validate_splice_init checks is_quiescent(). If a misbehaving peer sends splice_init during the STFU handshake (before quiescence is established), and the holder has a queued contribution with a low max_feerate, the Abort error would flow into quiescent_negotiation_errexit_quiescence(), which has debug_assert!(!is_local_stfu_sent()) / debug_assert!(!is_remote_stfu_sent()).

Those asserts would fire because the STFU flags are still set (handshake incomplete). In release builds, exit_quiescence would harmlessly return false, but in debug/test builds this would panic.

Same issue applies to tx_init_rbf at line 12762 (and line 12780).

Consider guarding with an is_quiescent() check before calling exit_quiescence:

Suggested change
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 }
}
fn quiescent_negotiation_err(&mut self, err: ChannelError) -> InteractiveTxMsgError {
let exited_quiescence = if matches!(err, ChannelError::Abort(_))
&& self.context.channel_state.is_quiescent()
{
self.exit_quiescence()
} else {
false
};
InteractiveTxMsgError { err, splice_funding_failed: None, exited_quiescence }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, though I took the opportunity to have validate_splice_init and validate_ tx_init_rbf no longer call validate_splice_contributions and FundingScope::for_splice. Rather they are called at those call sites. This let's us check if we are quiescent before checking fee rates when calling resolve_queued_contribution.

@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

ldk-claude-review-bot commented Apr 2, 2026

resolve_queued_contribution takes &self (not &mut self), so it has no mutable side effects. It's a pure computation. Good — no rollback needed.

I'm satisfied with the review. No new issues found beyond my prior comment (which is still valid but defensive). The PR is well-structured.

No issues found.

Summary

After thorough review of all files in this diff — channel.rs, channelmanager.rs, funding.rs, and splicing_tests.rs — I found no new issues beyond the one flagged in my prior review pass.

Prior comment status:

  • lightning/src/ln/channel.rs:13970-13974 — The quiescent_negotiation_err / exit_quiescence debug_assert concern remains valid as a defensive hardening suggestion, though the new commit (d275551) that defers resolve_queued_contribution until after validate_splice_init/validate_tx_init_rbf significantly reduces the practical risk. The new tests (test_splice_init_before_quiescence_sends_warning, test_tx_init_rbf_before_quiescence_sends_warning) confirm that misbehaving peers hitting these paths before quiescence get WarnAndDisconnect (not Abort), so the debug_assert shouldn't fire in practice.

Verification of key changes:

  • min_rbf_feerate correctly computes max(prev + 25, ceil(prev * 25/24)) with crossover at 600 sat/kwu
  • Asymmetric validation (stricter for own RBFs, lenient 25/24 for counterparty) is correct and tested
  • validate_splice_init/validate_tx_init_rbf refactoring preserves all validation logic and parameter passing
  • handle_interactive_tx_msg_err extraction is a clean refactor of the inline code in internal_tx_msg
  • exit_quiescence correctly had its test-only attribute removed for production use
  • resolve_queued_contribution is side-effect-free (&self), so ordering changes are safe
  • All test feerate values updated consistently from (prev * 25).div_ceil(24) to prev + 25 where prev is at/near FEERATE_FLOOR_SATS_PER_KW (253)

validate_splice_init and validate_tx_init_rbf check preconditions
including quiescence. Move them before resolve_queued_contribution
so that a misbehaving peer sending splice_init or tx_init_rbf before
quiescence is rejected early. This also moves validate_splice_contributions
and FundingScope construction into the callers since they depend on the
resolved contribution.

Have validate_tx_init_rbf return the last candidate's funding pubkeys
so the caller can construct FundingScope without re-accessing
pending_splice.

Add a debug_assert in quiescent_negotiation_err that Abort errors only
occur when the channel is quiescent, since exit_quiescence requires it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants