Skip to content

Validate next_splice_out_maximum_sat and reserved fees on both commitments#4625

Open
tankyleo wants to merge 3 commits into
lightningdevkit:mainfrom
tankyleo:2026-05-check-both-commitments
Open

Validate next_splice_out_maximum_sat and reserved fees on both commitments#4625
tankyleo wants to merge 3 commits into
lightningdevkit:mainfrom
tankyleo:2026-05-check-both-commitments

Conversation

@tankyleo
Copy link
Copy Markdown
Contributor

No description provided.

@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented May 20, 2026

👋 Thanks for assigning @TheBlueMatt 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.

@tankyleo tankyleo requested a review from wpaulino May 20, 2026 07:06
// reserve violation even though it's a dust HTLC and therefore shouldn't count towards the
// commitment transaction fee.
route_payment(&nodes[1], &[&nodes[0]], dust_amt);
// This will be dust on the remote commitment, but non-dust on the local commitment
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.

The comment is incorrect. dust_amt_on_both_commitments = dust_limit + htlc_timeout_fee - 1, which is below the dust threshold on both commitments (timeout applies on node 1's local commitment, success applies on node 0's local commitment, and success_fee > timeout_fee). The variable name dust_amt_on_both_commitments is correct — this amount is dust on both commitments, not "non-dust on the local commitment" as the comment states.

Suggested change
// This will be dust on the remote commitment, but non-dust on the local commitment
// This will be dust on both commitments

Comment thread lightning/src/ln/splicing_tests.rs Outdated
Comment on lines +9319 to +9320

let _node_id_0 = nodes[0].node.get_our_node_id();
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.

Nit: _node_id_0 and _node_id_1 are computed but never used in this test. Consider removing these lines.

Suggested change
let _node_id_0 = nodes[0].node.get_our_node_id();

Comment on lines +9407 to +9408
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);

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.

Same here — _node_id_0 and _node_id_1 are unused dead code.

Suggested change
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);

Comment on lines +527 to +531
let available_capacity_on_remote_commitment = read_available_capacity(
remote_nondust_htlc_count,
channel_constraints.counterparty_dust_limit_satoshis + real_htlc_success_tx_fee_sat,
);
cmp::min(available_capacity_on_local_commitment, available_capacity_on_remote_commitment)
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.

Behavioral change: in the original code, the remote commitment was only checked in the !is_outbound_from_holder path using feerate_per_kw (not spiked). Now, when the holder is the funder, the remote commitment fee is also checked at spiked_feerate.

For anchor channels (where spiked_feerate == feerate_per_kw), this is a no-op. For legacy non-anchor channels (where spiked_feerate == feerate_per_kw * 2), this is more conservative — the holder must be able to afford the spiked fee on the remote commitment too. That seems correct (the holder pays fees on both commitments), but the new tests only cover anchor channels, so this behavioral change for legacy channels is untested.

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

ldk-claude-review-bot commented May 20, 2026

After a thorough line-by-line review of the entire diff, including tracing through the logic of every new and refactored function, I found no new issues beyond what was flagged in the prior review pass.

Review Summary

No new issues found beyond those already posted in the prior review.

Prior comments still applicable:

  • lightning/src/sign/tx_builder.rs:531spiked_feerate now applied to the remote commitment check in adjust_capacity_for_holder_reserved_fee, which is a behavioral change for legacy (non-anchor) channels that is untested.
  • lightning/src/ln/htlc_reserve_unit_tests.rs:1068 — Incorrect comment (variable name is correct but comment contradicts it).

Verification of key changes:

  1. adjust_capacity_for_holder_reserved_fee: Correctly checks both local (timeout dust threshold) and remote (success dust threshold) commitments, taking the minimum available capacity. The hardcoded +2/+1 for fee spike buffer is equivalent to the old fee_spike_buffer_htlc for all channel types (legacy: was 1, anchor: was 1, zero-fee: was 0 but feerate=0 makes it irrelevant).

  2. adjust_capacity_for_counterparty_reserved_fee: Correctly adds a local commitment check (previously only remote was checked). Uses holder_selected_channel_reserve_satoshis for both commitment checks, which is correct per spec (reserve applies on all commitments).

  3. get_next_splice_out_maximum_sat: post_splice_delta_above_reserve_sat now correctly uses max(local_nondust, remote_nondust) instead of just local_nondust, fixing an under-estimation of the post-splice minimum balance when the remote commitment has more non-dust HTLCs.

  4. trim_splice_out_max_if_no_outputs: Called for both local and remote commitments. Ordering is correct — the second call can only tighten, never loosen the result.

  5. Test fix (htlc_timeout_tx_fee_sat replacing htlc_success_tx_fee_sat): Correct — the old test used the wrong second-stage fee type, which was masked by only checking one commitment.

  6. saturating_sub for counterparty_max_htlc_value_in_flight_msat: Good defensive fix against potential underflow.

@tankyleo tankyleo added this to the 0.3 milestone May 20, 2026
@tankyleo tankyleo self-assigned this May 20, 2026
@tankyleo tankyleo moved this to Goal: Merge in Weekly Goals May 20, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.67%. Comparing base (44ca076) to head (9fe1a36).
⚠️ Report is 22 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4625      +/-   ##
==========================================
+ Coverage   86.58%   86.67%   +0.08%     
==========================================
  Files         159      159              
  Lines      110419   110654     +235     
  Branches   110419   110654     +235     
==========================================
+ Hits        95610    95904     +294     
+ Misses      12276    12222      -54     
+ Partials     2533     2528       -5     
Flag Coverage Δ
fuzzing-fake-hashes 6.76% <0.00%> (+0.19%) ⬆️
fuzzing-real-hashes 23.38% <99.57%> (+0.20%) ⬆️
tests 86.25% <100.00%> (+0.03%) ⬆️

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 jkczyz requested review from TheBlueMatt and jkczyz May 20, 2026 18:18
Copy link
Copy Markdown
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

All these commits make sense to me (boy --color-moved --color-moved-ws=ignore-space-change is great), but they could all do with some actual comentary in the commit message.

tankyleo added 3 commits May 21, 2026 17:06
Wilmer's fuzzing runs caught a case where an advertised splice out
maximum hit the debug assertions in `get_next_splice_out_maximum`.
These debug assertions ensure that any adverstised splice out maximum
passes the validation of splice contributions.

The core issue is that we only read the local commitment when
calculating the splice out maximum, but our splice validation requires
that any splice out maximum is covered by the minimum of the holder's
balances on the local and the remote commitments.

Therefore, if a HTLC is dust on the local commitment, but non-dust on
the remote commitment, and the holder is the funder of the channel, we
advertise a splice out maximum that is not covered by the holder's
balance on the remote commitment, and fails our validation of splice
contributions.

We now read both commitments when calculating the next splice out
maximum, which fixes this issue.
The local and remote commitments may have different dust limits, which
can cause each commitment to have a different transaction fee.

Therefore when we reserve commitment transaction fees in
`get_available_balances`, we must ensure that we read the maximum of the
transaction fees on the local and the remote commitments. Otherwise, we
may have not reserved enough fees to ensure that our next proposed
channel state update is onside.
@tankyleo tankyleo force-pushed the 2026-05-check-both-commitments branch from 7d0c2e1 to 9fe1a36 Compare May 21, 2026 17:10
@tankyleo tankyleo requested a review from TheBlueMatt May 21, 2026 17:59
Copy link
Copy Markdown
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Didn't really review the tests, but the code changes look good

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Goal: Merge

Development

Successfully merging this pull request may close these issues.

4 participants