Skip to content

Open and accept zero reserve channels#4428

Merged
TheBlueMatt merged 12 commits intolightningdevkit:mainfrom
tankyleo:2026-02-zero-reserve
Mar 26, 2026
Merged

Open and accept zero reserve channels#4428
TheBlueMatt merged 12 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 80.11696% with 170 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.17%. Comparing base (d830f10) to head (32a67f8).
⚠️ Report is 22 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 71.12% 148 Missing and 3 partials ⚠️
lightning/src/ln/channelmanager.rs 84.78% 14 Missing ⚠️
lightning/src/ln/msgs.rs 94.87% 4 Missing ⚠️
lightning/src/sign/tx_builder.rs 99.37% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4428      +/-   ##
==========================================
- Coverage   86.18%   86.17%   -0.02%     
==========================================
  Files         160      160              
  Lines      107536   108046     +510     
  Branches   107536   108046     +510     
==========================================
+ Hits        92680    93105     +425     
- Misses      12231    12321      +90     
+ Partials     2625     2620       -5     
Flag Coverage Δ
tests 86.17% <80.11%> (-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.

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

/// 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed offline, I'll hold off on signaling for now pending further spec discussions

@tankyleo tankyleo requested a review from carlaKC March 13, 2026 12:50
@TheBlueMatt
Copy link
Collaborator

I think the changes here LGTM. Feel free to squash from my side, once @carlaKC says good I think we're good.

The floor for *our* selected reserve is *their* dust limit.
The goal is to prevent any commitments with no outputs, since these are
not broadcastable.
@tankyleo tankyleo force-pushed the 2026-02-zero-reserve branch 2 times, most recently from 91565f1 to 2967e6f Compare March 25, 2026 23:23
@tankyleo
Copy link
Contributor Author

git diff bbe7a62 2967e6f, all significant changes in commit 5eb530d

diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 8f8ab4a9b..fa394ee6e 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -15586,6 +15586,10 @@
 		}
 		let is_manual_broadcast = Some(self.context.is_manual_broadcast);

+		let has_0reserve =
+			(self.funding.counterparty_selected_channel_reserve_satoshis.is_some_and(|r| r == 0)
+				|| self.funding.holder_selected_channel_reserve_satoshis == 0)
+				.then_some(());
 		let holder_commitment_point_previous_revoked =
 			self.holder_commitment_point.previous_revoked_point();
 		let holder_commitment_point_last_revoked =
@@ -15655,6 +15659,7 @@
 			// 65 was previously used for quiescent_action
 			(67, pending_outbound_held_htlc_flags, optional_vec), // Added in 0.2
 			(69, holding_cell_held_htlc_flags, optional_vec), // Added in 0.2
+			(70, has_0reserve, option), // Added in 0.3 to prevent downgrades
 			(71, holder_commitment_point_previous_revoked, option), // Added in 0.3
 			(73, holder_commitment_point_last_revoked, option), // Added in 0.3
 			(75, inbound_committed_update_adds, optional_vec),
@@ -16028,6 +16033,7 @@
 		let mut malformed_htlcs: Option<Vec<(u64, u16, [u8; 32])>> = None;
 		let mut monitor_pending_update_adds: Option<Vec<msgs::UpdateAddHTLC>> = None;

+		let mut _has_0reserve: Option<()> = None;
 		let mut holder_commitment_point_previous_revoked_opt: Option<PublicKey> = None;
 		let mut holder_commitment_point_last_revoked_opt: Option<PublicKey> = None;
 		let mut holder_commitment_point_current_opt: Option<PublicKey> = None;
@@ -16098,6 +16104,7 @@
 			// 65 quiescent_action: Added in 0.2; removed in 0.3
 			(67, pending_outbound_held_htlc_flags_opt, optional_vec), // Added in 0.2
 			(69, holding_cell_held_htlc_flags_opt, optional_vec), // Added in 0.2
+			(70, _has_0reserve, option), // Added in 0.3 to prevent downgrades
 			(71, holder_commitment_point_previous_revoked_opt, option), // Added in 0.3
 			(73, holder_commitment_point_last_revoked_opt, option), // Added in 0.3
 			(75, inbound_committed_update_adds_opt, optional_vec),
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 432070129..184bc4052 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -3555,7 +3555,7 @@
 	///
 	/// Note that there is no guarantee that the counterparty accepts such a channel themselves.
 	ZeroReserve,
-	/// Sets combination of [`TrustedChannelFeatures::ZeroConf`] and [`TrustedChannelFeatures::ZeroReserve`]
+	/// Sets the combination of [`TrustedChannelFeatures::ZeroConf`] and [`TrustedChannelFeatures::ZeroReserve`]
 	ZeroConfZeroReserve,
 }

@ldk-reviews-bot
Copy link

🔔 1st 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.

TheBlueMatt
TheBlueMatt previously approved these changes Mar 26, 2026
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.

Happy to land this to get the API changes in now, but a few small tweaks we'll want to change/address before release.

value_to_remote_after_htlcs: u64, channel_type: &ChannelTypeFeatures,
fn saturating_sub_from_funder(
is_outbound_from_holder: bool, value_to_holder: u64, value_to_counterparty: u64,
subtrahend: u64,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I've never heard of a subtrahend 😂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

haha yes I looked it up do you have a suggestion ? value_to_subtract comes to mind :)

(0, self.common_fields.shutdown_scriptpubkey.as_ref().map(|s| WithoutLength(s)), option), // Don't encode length twice.
(1, self.common_fields.channel_type, option),
(2, self.require_confirmed_inputs, option),
(3, self.disable_channel_reserve, option),
Copy link
Collaborator

Choose a reason for hiding this comment

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

I assume this needs a bLIP update? Given this is for a bLIP and not a BOLT (IIRC?) should we not be using a +1000 or whatever range?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a BOLT proposal. Using the staging bit makes sense to me yes.

let is_manual_broadcast = Some(self.context.is_manual_broadcast);

let has_0reserve =
(self.funding.counterparty_selected_channel_reserve_satoshis.is_some_and(|r| r == 0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We've always supported counterparty-selected reserve of 0, so I don't think we want to break downgrade for that?

carlaKC
carlaKC previously approved these changes Mar 26, 2026
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.

lgtm!

@tankyleo
Copy link
Contributor Author

Thank you for the review ! Happy to hold off on the merge and get this PR just right :)

tankyleo and others added 10 commits March 26, 2026 15:15
We prevent downgrades from 0.3 only in the case where the
holder-selected reserve is 0, as we've had support for counterparty
selected 0-reserves in prior releases.

There is no need for this sentinel in `FundingScope` serialization code
as this would only apply to pending `FundingScope`'s.

Also, if the current scope has some zero-reserve, that reserve is
carried over to all pending scopes automatically. Therefore it is not
possible for a pending scope to have some 0-reserve without the current
one also having it.
This new flag sets 0-reserve for the channel opener.
This new method sets 0-reserve for the channel accepter.
We do not care if our balance drops below the counterparty-selected
reserve upon an inbound `update_add_htlc`. This is the counterparty's
problem.

Hence, we drop the assumption that once our balance rises above the
counterparty-selected reserve, it will always remain above this reserve
for the lifetime of a funding scope.

In the following commit, we make the assumption that the counterparty
does not complain if we push them below our selected reserve when adding
a HTLC, so we accommodate this assumption here.
Note that this currently does not match the spec as we use an odd TLV
for the `disable_channel_reserve` field in `open_channel2` and
`accept_channel2` msgs.

If the counterparty does not understand this field, that's ok as it just
means that the counterparty will not send some HTLCs we would have
accepted.

We make the assumption that the counterparty will not complain if we
send a HTLC that pushes their balance below our selected reserve; this
could happen if the counterparty is the funder of the channel. They
should not complain because if we push them below our selected reserve,
this is our problem.
`ChannelContext::do_accept_channel_checks`,
`ChannelContext::new_for_outbound_channel`,
`ChannelContext::new_for_inbound_channel`,
`InboundV1Channel::new`,
`OutboundV1Channel::new`.
Reduce line count and indentation
Convert format string arguments to inline `{var}` captures where
the argument is a simple identifier (variable or constant). Field
accesses, method calls, and expressions remain as positional args.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tankyleo tankyleo dismissed stale reviews from carlaKC and TheBlueMatt via 32a67f8 March 26, 2026 15:32
@tankyleo tankyleo force-pushed the 2026-02-zero-reserve branch from 2967e6f to 32a67f8 Compare March 26, 2026 15:32
@tankyleo tankyleo requested review from TheBlueMatt and carlaKC March 26, 2026 16:28
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.

RIP subtrahend, word of the week ✊

@TheBlueMatt TheBlueMatt merged commit 688544d into lightningdevkit:main Mar 26, 2026
24 of 44 checks passed
@github-project-automation github-project-automation bot moved this from Goal: Merge to Done in Weekly Goals Mar 26, 2026
@tankyleo tankyleo deleted the 2026-02-zero-reserve branch March 26, 2026 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Support 0 counterparty channel reserve 0-output commitment tx with 0-reserve

6 participants