Skip to content

Open and accept zero reserve channels#4428

Open
tankyleo wants to merge 6 commits intolightningdevkit:mainfrom
tankyleo:2026-02-zero-reserve
Open

Open and accept zero reserve channels#4428
tankyleo wants to merge 6 commits intolightningdevkit:mainfrom
tankyleo:2026-02-zero-reserve

Conversation

@tankyleo
Copy link
Contributor

@tankyleo tankyleo commented Feb 19, 2026

Fixes #1801

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Feb 19, 2026

👋 Thanks for assigning @carlaKC 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 moved this to Goal: Merge in Weekly Goals Feb 19, 2026
@tankyleo tankyleo self-assigned this Feb 19, 2026
@tankyleo tankyleo force-pushed the 2026-02-zero-reserve branch from ffa1657 to 5fa3a7c Compare February 26, 2026 09:56
@tankyleo tankyleo marked this pull request as ready for review February 26, 2026 10:45
@tankyleo tankyleo requested a review from TheBlueMatt February 26, 2026 10:46
@tankyleo tankyleo added this to the 0.3 milestone Feb 26, 2026
@codecov
Copy link

codecov bot commented Feb 26, 2026

Codecov Report

❌ Patch coverage is 78.74818% with 146 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.91%. Comparing base (14e522f) to head (5fa3a7c).
⚠️ Report is 25 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 70.64% 121 Missing and 2 partials ⚠️
lightning/src/ln/channelmanager.rs 81.70% 15 Missing ⚠️
lightning/src/ln/functional_test_utils.rs 94.33% 6 Missing ⚠️
lightning/src/sign/tx_builder.rs 97.46% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4428      +/-   ##
==========================================
- Coverage   85.94%   85.91%   -0.03%     
==========================================
  Files         159      159              
  Lines      104644   105103     +459     
  Branches   104644   105103     +459     
==========================================
+ Hits        89934    90298     +364     
- Misses      12204    12300      +96     
+ Partials     2506     2505       -1     
Flag Coverage Δ
tests 85.91% <78.74%> (-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.

Copy link
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.

chanmon_consistency needs to be updated to have a 0-reserve channel or two (I believe we now have three channels between each pair of peers, so we can just do it on a subset of them, in fact we could have three separate channel types for better coverage).

/// Creates a new outbound channel to the given remote node and with the given value.
///
/// The only difference between this method and [`ChannelManager::create_channel`] is that this method sets
/// the reserve the counterparty must keep at all times in the channel to zero. This allows the counterparty to
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: If that's the only difference let's say create_channel_to_trusted_peer_0_reserve? Nice to be explicit, imo.


let channel_value_satoshis =
our_funding_contribution_sats.saturating_add(msg.common_fields.funding_satoshis);
// TODO(zero_reserve): support reading and writing the `disable_channel_reserve` field
Copy link
Collaborator

Choose a reason for hiding this comment

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

Two questions. Shouldn't we check that if a channel has the 0-reserve feature bit and if it is fail if the user isn't accepting 0-reserve? Also why shouldn't we just set it now? I'm not sure we need to bother with a staging bit, really, honestly...

@TheBlueMatt
Copy link
Collaborator

Needs rebase now :/

@tankyleo tankyleo force-pushed the 2026-02-zero-reserve branch from 471ba8f to 253db4d Compare March 5, 2026 10:55
@tankyleo tankyleo requested a review from TheBlueMatt March 5, 2026 10:57
@tankyleo
Copy link
Contributor Author

tankyleo commented Mar 5, 2026

Needs rebase now :/

Let me know if you prefer I rebase first

@TheBlueMatt
Copy link
Collaborator

Feel free to go ahead and rebase and squash, yea.

@tankyleo tankyleo force-pushed the 2026-02-zero-reserve branch 2 times, most recently from 5fa3a7c to 43be438 Compare March 5, 2026 17:21
@tankyleo
Copy link
Contributor Author

tankyleo commented Mar 5, 2026

Squash diff (do not click compare just above, I pushed the wrong branch, and later corrected it):

https://github.com/lightningdevkit/rust-lightning/compare/253db4d9a05cda43f74c2835448126d3205b27b8..43be438f70ba28ad7918e407026a78763ac2b368

tankyleo added 6 commits March 5, 2026 17:32
The goal is to prevent any commitments with no outputs, since these are
not broadcastable.
This new flag sets 0-reserve for the channel opener.
This new method sets 0-reserve for the channel accepter.
`ChannelContext::do_accept_channel_checks`,
`ChannelContext::new_for_outbound_channel`,
`ChannelContext::new_for_inbound_channel`,
`InboundV1Channel::new`,
`OutboundV1Channel::new`.
@tankyleo tankyleo force-pushed the 2026-02-zero-reserve branch from 43be438 to 7fde002 Compare March 5, 2026 17:36
@tankyleo
Copy link
Contributor Author

tankyleo commented Mar 5, 2026

git range-diff main 43be438 7fde002

1:  bfba6e993 ! 1:  b916c3596 Add inbound and outbound checks for zero reserve channels
    @@ lightning/src/ln/channel.rs: impl FundingScope {

                // New reserve values are based on the new channel value and are v2-specific
     -          let counterparty_selected_channel_reserve_satoshis =
    --                  Some(get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS));
    +-                  get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS);
     -          let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
     -                  post_channel_value,
     -                  context.counterparty_dust_limit_satoshis,
    @@ lightning/src/ln/channel.rs: impl FundingScope {
     +                  == 0
     +          {
     +                  // If we previously had a 0-value reserve, continue with the same reserve
    -+                  Some(0)
    ++                  0
     +          } else {
    -+                  Some(get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS))
    ++                  get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS)
     +          };
     +          let holder_selected_channel_reserve_satoshis =
     +                  if prev_funding.holder_selected_channel_reserve_satoshis == 0 {
    @@ lightning/src/sign/tx_builder.rs: fn get_next_commitment_stats(
        );

     @@ lightning/src/sign/tx_builder.rs: fn get_available_balances(
    -   let fee_spike_buffer_htlc =
                if channel_type.supports_anchor_zero_fee_commitments() { 0 } else { 1 };

    +   // Note that the feerate is 0 in zero-fee commitment channels, so this statement is a noop
     -  let local_feerate = feerate_per_kw
     +  let spiked_feerate = feerate_per_kw
                * if is_outbound_from_holder && !channel_type.supports_anchors_zero_fee_htlc_tx() {
2:  1f8ca6c66 = 2:  53fc9a354 Add 0-reserve to `accept_inbound_channel_from_trusted_peer`
3:  1b94f7cc3 = 3:  da0520818 Add `ChannelManager::create_channel_to_trusted_peer_0reserve`
4:  19b8d7943 = 4:  1905f94e7 Shakedown zero reserve channels
5:  de2366da4 = 5:  4e6a7527a Format `ChannelManager::create_channel_internal` and...
6:  43be438f7 = 6:  7fde002e7 Update `chanmon_consistency` to include 0FC and 0-reserve channels

@ldk-reviews-bot
Copy link

✅ Added second reviewer: @joostjager

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @TheBlueMatt @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @TheBlueMatt @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 2nd Reminder

Hey @TheBlueMatt @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@tankyleo tankyleo requested review from carlaKC and removed request for joostjager March 10, 2026 14:33
@ldk-reviews-bot
Copy link

🔔 3rd Reminder

Hey @TheBlueMatt @carlaKC! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Copy link
Contributor

@carlaKC carlaKC left a comment

Choose a reason for hiding this comment

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

First pass - haven't gone through the tests yet.

Comment on lines +639 to +642
let dust_limit_msat = dust_limit_satoshis.saturating_mul(1000);
if holder_balance_msat.saturating_sub(max_dust_htlc_msat) < dust_limit_msat
&& counterparty_balance_msat < dust_limit_msat
&& nondust_htlc_count == 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we re-use check_no_outputs here? Could also return early if we don't need any action.

We'd need to change it to a saturating_sub, but I think that's okay because we have a checked_sub check of our balances on 321 after calling check_no_outputs anyway?

Comment on lines +644 to +645
// If we are allowed to send non-dust HTLCs, set the min HTLC to the smallest non-dust HTLC...
if available_capacity_msat >= min_nondust_htlc_sat.saturating_mul(1000) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: to the largest non-dust htlc?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm min_nondust_htlc_sat is right on the dust limit, so that's the smallest non-dust HTLC we can send ? The largest non-dust HTLC would be the available_capacity_msat, which we leave untouched.

Comment on lines +660 to +665
// 1) The dust_limit_satoshis plus the fee of the exisiting commitment at the spiked feerate.
// 2) The fee of the commitment with an additional non-dust HTLC, aka the fee spike buffer HTLC.
// In this case we don't mind the holder balance output dropping below the dust limit, as
// this additional non-dust HTLC will create the single remaining output on the commitment.
let min_balance_msat =
cmp::max(dust_limit_satoshis + tx_fee_sat, fee_spike_buffer_sat) * 1000;
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment for (1) says "at the spiked feerate" but isn't tx_fee_sat is just our current feerate?

A more descriptive name would be helpful here (if you can thing of something less gross than max_output_preserving_fee?).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment for (1) says "at the spiked feerate" but isn't tx_fee_sat is just our current feerate?

See the callsites of the function; we always use the spiked_feerate as the feerate_per_kw parameter. I admit this is confusing, should I rename the function parameter to spiked_feerate ?

A more descriptive name would be helpful here (if you can thing of something less gross than max_output_preserving_fee?).

How is current_spiked_tx_fee_sat ? It would be consistent with the spiked_feerate style. With max_output_preserving_fee you want to highlight that we want to maintain an output on the commitment transaction even with a 2x increase in the feerate ?

Comment on lines +5058 to +5060
.map_err(|()| {
ChannelError::close(String::from("Balance exhausted on local commitment"))
})?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like we should add an advisory on the option_zero_reserve spec about this? Otherwise a spec-compliant peer could send us a htlc that fails check_no_outputs and gets FC'ed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The intention of making this check in all cases, and not just if funding.is_outbound(), is to check this requirement from the spec:

  - receiving an `amount_msat` that results in a commitment transaction without any output:
    - SHOULD send a `warning` and close the connection, or send an
      `error` and fail the channel.

It seems to me if a peer fails check_no_outputs for our local commitment, they are not compliant with the current spec right ?

/// If it does not confirm before we decide to close the channel, or if the funding transaction
/// does not pay to the correct script the correct amount, *you will lose funds*.
///
/// # Zero-reserve
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we be setting the option_zero_reserve feature in lightning/bolts#1140?

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.

Support 0 counterparty channel reserve

4 participants