Skip to content

Add AvailableBalances::next_splice_out_maximum_sat#4550

Open
tankyleo wants to merge 6 commits intolightningdevkit:mainfrom
tankyleo:2026-04-next-splice-out-maximum
Open

Add AvailableBalances::next_splice_out_maximum_sat#4550
tankyleo wants to merge 6 commits intolightningdevkit:mainfrom
tankyleo:2026-04-next-splice-out-maximum

Conversation

@tankyleo
Copy link
Copy Markdown
Contributor

@tankyleo tankyleo commented Apr 9, 2026

This is the maximum value the holder can splice out of their channel balance, accounting for the updated v2 reserves after the splice, and the "must have at least one output" constraint on zero-reserve channels.

@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Apr 9, 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.

@ldk-reviews-bot ldk-reviews-bot requested a review from jkczyz April 9, 2026 08:13
Comment thread lightning/src/ln/channel_state.rs Outdated
Comment thread lightning/src/sign/tx_builder.rs
Comment thread lightning/src/sign/tx_builder.rs Outdated
Comment thread lightning/src/ln/channel.rs
Comment thread lightning/src/routing/router.rs Outdated
Comment thread lightning/src/routing/router.rs Outdated
@tankyleo tankyleo force-pushed the 2026-04-next-splice-out-maximum branch from 724acf9 to 3bd7a76 Compare April 9, 2026 08:29
@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

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

I've completed a thorough re-review of the entire diff, cross-referencing against the current code and my prior comments. All unit-conversion concerns from the prior review were confirmed incorrect — the code is correct in those areas. The remaining prior concerns about dust limit inconsistency, has_output asymmetry, and misleading comments are still valid but were already posted.

I found no new issues in this review pass.

Review Summary

No new issues found beyond what was flagged in the prior review.

Previously Flagged Issues (Still Valid)

  1. lightning/src/sign/tx_builder.rs:354-356get_next_splice_out_maximum_sat uses holder_dust_limit_satoshis as the floor for the v2 reserve calculation, while FundingScope::for_splice (line 2815) uses MIN_CHAN_DUST_LIMIT_SATOSHIS (354). For channels deserialized from older LDK with holder_dust_limit > 354, the splice max is more conservative than validation requires, and the unwrap_err() debug assertion in get_next_splice_out_maximum (line 13469) could panic.

  2. lightning/src/sign/tx_builder.rs:400-407has_output only checks the local commitment (using holder_dust_limit), but validate_splice_contributions checks both commitments via get_holder_counterparty_balances_floor_incl_fee. If counterparty_dust_limit > holder_dust_limit in a zero-reserve channel, the debug assertion unwrap() at channel.rs:13461 could panic.

  3. lightning/src/sign/tx_builder.rs:388,395 — Comments are semantically backwards: "free to withdraw up to its post_splice_delta_above_reserve_sat" should be "up to its balance minus post_splice_delta_above_reserve_sat", and "bump this maximum" should be "reduce/cap this maximum".

Previously Flagged Issues (Confirmed Incorrect — Not Issues)

  • channel_state.rs:628 — TLV default correctly divides outbound_capacity_msat by 1000.
  • router.rs:4153 — Correctly uses outbound_capacity_msat / 1000.
  • router.rs:9653 — Hardcoded 10_000_000 correctly matches 10_000_000_000 msat / 1000.

Cross-Cutting Note

The semantic change from "holder balance floor" to "splice-out maximum" is incomplete: several variable names remain holder_balance (e.g., channel.rs:12888 log message "Cannot compute holder balance"), and the PriorContribution field was updated but not all surrounding call sites.

@tankyleo tankyleo requested review from TheBlueMatt and wpaulino and removed request for jkczyz April 9, 2026 08:32
@tankyleo tankyleo self-assigned this Apr 9, 2026
@tankyleo tankyleo moved this to Goal: Merge in Weekly Goals Apr 9, 2026
@tankyleo
Copy link
Copy Markdown
Contributor Author

tankyleo commented Apr 9, 2026

Seems the fuzz failure produces this error on my machine

Hex:

ff1cb4a6a6ffffff02000000ffffb4a669696969696969691b00000000000000ffffffffffa2ad06301100ffffffffffffcf111f7d29000000000000001f0007

Target: chanmon_consistency

Error:

2      ERROR [lightning::ln::channelmanager:4635]      ch:8c0000 p:020000          Closed channel due to close-required error: Channel closed because of an exception: splice tx of another pending funding already confirmed
2      ERROR [lightning::ln::channelmanager:4557]      ch:8c0000 p:020000          Closing channel: Channel closed because of an exception: splice tx of another pending funding already confirmed

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt @wpaulino! 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
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt @wpaulino! 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.

@Alkamal01
Copy link
Copy Markdown

Curious about estimated_fees = 183 in do_test_splice_max_value — is that commit_tx_fee_sat(253, 0, legacy_channel) (the unspiked fee)? The test already subtracts commit_tx_fee_sat at the spiked feerate separately, so I wasn't sure what the 183 is covering.

@tankyleo
Copy link
Copy Markdown
Contributor Author

Curious about estimated_fees = 183 in do_test_splice_max_value — is that commit_tx_fee_sat(253, 0, legacy_channel) (the unspiked fee)? The test already subtracts commit_tx_fee_sat at the spiked feerate separately, so I wasn't sure what the 183 is covering.

Thanks for taking a look ! 183sats is the amount allocated to the fee of the splice out transaction at the default feerate of 253sat/kw, and will be added on top of the splice amount. Will clarify this in the test.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 2nd Reminder

Hey @TheBlueMatt @wpaulino! 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
Copy Markdown

🔔 2nd Reminder

Hey @TheBlueMatt @wpaulino! 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.

Comment thread lightning/src/ln/channel.rs Outdated
their_contribution_candidate: SignedAmount, addl_nondust_htlc_count: usize,
feerate_per_kw: u32,
) -> Result<(Amount, Amount), String> {
let htlc_candidate = None;
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.

This really feels like it could/should be substantially DRYd with get_next_local/remote_commitment_stats - what we're really asking at the callsites is "is this splice even valid" which istm should really be done by just saying "did get_next_local_commitment_stats return an Error or not" (with appropriate balance modification flags, maybe a wrapper method calling an internal impl method rather than modifying the existing methods).

Comment thread lightning/src/sign/tx_builder.rs Outdated
/// retains at least one output after the splice, which is particularly relevant for
/// zero-reserve channels.
//
// The equation to determine `percent_max_splice_out_sat` is:
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.

discussed offline but this feels super unclear to me because "percent_" as a prefix implies to me, that the value is a percent, but its not.

@TheBlueMatt
Copy link
Copy Markdown
Collaborator

Needs rebase, sorry about that. Do think its worth doing this for 0.3, yes, given it fixed splice <-> 0 reserve compat. Its also not a big deal to add a field rn.

@TheBlueMatt TheBlueMatt added this to the 0.3 milestone Apr 13, 2026
@Alkamal01
Copy link
Copy Markdown

Curious about estimated_fees = 183 in do_test_splice_max_value — is that commit_tx_fee_sat(253, 0, legacy_channel) (the unspiked fee)? The test already subtracts commit_tx_fee_sat at the spiked feerate separately, so I wasn't sure what the 183 is covering.

Thanks for taking a look ! 183sats is the amount allocated to the fee of the splice out transaction at the default feerate of 253sat/kw, and will be added on top of the splice amount. Will clarify this in the test.

Makes sense, thanks for clarifying!

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 3rd Reminder

Hey @wpaulino! 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.

@jkczyz jkczyz self-requested a review April 16, 2026 17:20
@ldk-reviews-bot
Copy link
Copy Markdown

🔔 4th Reminder

Hey @jkczyz @wpaulino! 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.

Comment thread lightning/src/ln/splicing_tests.rs Outdated
Comment on lines +6770 to +6773
// TODO: Skip 0FC channels for now as these always have an output on the commitment, the P2A
// output. We will be able to withdraw up to the dust limit of the funding script, which
// is checked in interactivetx. Still need to double check whether that's what we actually
// want.
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.

@TheBlueMatt forgot to mention this TODO here, WDYT ?

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.

Hmm, it seems like mostly-not-an-issue, no? Like, with 0FC in order to be worried about dust burns we have to actually have dust. Yea, we can't really withdraw much (or its in the P2A at which point it might as well be a burn) but its basically only when we have almost no balance anyway, so it seems like its not really worth worrying too much about?

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.

do we want to allow people to withdraw to the point where the overall channel value is around 300-400 sats ? the public API for create_channel does not allow users to open channels less than 1000 satoshis

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.

Yea, maybe not. We can just hard-code the lower-bound for both in the same constant?

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.

yes can do that

@tankyleo
Copy link
Copy Markdown
Contributor Author

git diff ccde0da fb777b8, let me know when I can rebase

diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 8931b05bc..5b165a28b 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -12401,8 +12401,8 @@
 			"build_prior_contribution requires pending_splice"
 		);
 		let prior = self.pending_splice.as_ref()?.contributions.last()?;
-		let holder_balance = self.get_next_splice_out_maximum(&self.funding).ok();
-		Some(PriorContribution::new(prior.clone(), holder_balance))
+		let next_splice_out_maximum = self.get_next_splice_out_maximum(&self.funding).ok();
+		Some(PriorContribution::new(prior.clone(), next_splice_out_maximum))
 	}

 	/// Returns whether this channel can ever RBF, independent of splice state.
@@ -12476,13 +12476,13 @@
 			return contribution;
 		}

-		let holder_balance = match self.get_next_splice_out_maximum(&self.funding) {
+		let next_splice_out_maximum = match self.get_next_splice_out_maximum(&self.funding) {
 			Ok(balance) => balance,
 			Err(_) => return contribution,
 		};

-		if let Err(e) =
-			contribution.net_value_for_initiator_at_feerate(min_rbf_feerate, holder_balance)
+		if let Err(e) = contribution
+			.net_value_for_initiator_at_feerate(min_rbf_feerate, next_splice_out_maximum)
 		{
 			log_info!(
 				logger,
@@ -12503,7 +12503,7 @@
 			min_rbf_feerate,
 		);
 		contribution
-			.for_initiator_at_feerate(min_rbf_feerate, holder_balance)
+			.for_initiator_at_feerate(min_rbf_feerate, next_splice_out_maximum)
 			.expect("feerate compatibility already checked")
 	}

@@ -12876,7 +12876,7 @@
 	fn resolve_queued_contribution<L: Logger>(
 		&self, feerate: FeeRate, logger: &L,
 	) -> Result<(Option<SignedAmount>, Option<Amount>), ChannelError> {
-		let holder_balance = self
+		let next_splice_out_maximum = self
 			.get_next_splice_out_maximum(&self.funding)
 			.map_err(|e| {
 				log_info!(
@@ -12889,9 +12889,12 @@
 			})
 			.ok();

-		let net_value = match holder_balance.and_then(|_| self.queued_funding_contribution()) {
+		let net_value = match next_splice_out_maximum
+			.and_then(|_| self.queued_funding_contribution())
+		{
 			Some(c) => {
-				match c.net_value_for_acceptor_at_feerate(feerate, holder_balance.unwrap()) {
+				match c.net_value_for_acceptor_at_feerate(feerate, next_splice_out_maximum.unwrap())
+				{
 					Ok(net_value) => Some(net_value),
 					Err(FeeRateAdjustmentError::FeeRateTooHigh { .. }) => {
 						return Err(ChannelError::Abort(AbortReason::FeeRateTooHigh));
@@ -12911,7 +12914,7 @@
 			None => None,
 		};

-		Ok((net_value, holder_balance))
+		Ok((net_value, next_splice_out_maximum))
 	}

 	pub(crate) fn splice_init<ES: EntropySource, L: Logger>(
diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs
index d78f65d22..ade54c603 100644
--- a/lightning/src/sign/tx_builder.rs
+++ b/lightning/src/sign/tx_builder.rs
@@ -324,7 +324,7 @@
 //
 // The equation to determine `max_splice_percentage_constraint_sat` is:
 // 1) floor((c - s) / 100) == h - s - d
-// We want the maximum value of s that will satisfy this equation, therefore, we solve:
+// We want the maximum value of s that will satisfy equation 1, therefore, we solve:
 // 2) (c - s) / 100 < h - s - d + 1
 // where c: `channel_value_satoshis`
 //       s: `max_splice_percentage_constraint_sat`
@@ -343,16 +343,17 @@
 		.counterparty_selected_channel_reserve_satoshis
 		!= 0
 	{
-		let dividend = local_balance_before_fee_sat
+		let dividend_sat = local_balance_before_fee_sat
 			.saturating_mul(100)
 			.saturating_add(100)
 			.saturating_sub(post_splice_delta_above_reserve_sat.saturating_mul(100))
 			.saturating_sub(channel_value_satoshis);
-		let max_splice_percentage_constraint_sat = dividend.saturating_sub(1) / 99;
+		// Calculate the greatest integer that is strictly less than the RHS of inequality 3 above
+		let max_splice_percentage_constraint_sat = dividend_sat.saturating_sub(1) / 99;
 		let max_splice_dust_limit_constraint_sat = local_balance_before_fee_sat
 			.saturating_sub(channel_constraints.holder_dust_limit_satoshis)
 			.saturating_sub(post_splice_delta_above_reserve_sat);
-		// Take whichever constraint you hit first as you increase the value of the splice-out
+		// Both constraints must be satisfied, so take the minimum of the two maximums
 		let max_splice_out_sat =
 			cmp::min(max_splice_percentage_constraint_sat, max_splice_dust_limit_constraint_sat);
 		#[cfg(debug_assertions)]

@wpaulino
Copy link
Copy Markdown
Contributor

Needs a rebase

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 2nd Reminder

Hey @jkczyz @wpaulino! 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
Copy Markdown

🔔 6th Reminder

Hey @jkczyz @wpaulino! 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 Apr 23, 2026
@ldk-reviews-bot
Copy link
Copy Markdown

🔔 7th Reminder

Hey @jkczyz @wpaulino! 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
Copy Markdown

🔔 3rd Reminder

Hey @jkczyz @wpaulino! 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
Copy Markdown

🔔 8th Reminder

Hey @jkczyz @wpaulino! 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
Copy Markdown

🔔 4th Reminder

Hey @jkczyz @wpaulino! 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
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.

LGTM but needs another rebase :/

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 9th Reminder

Hey @jkczyz @wpaulino! 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
Copy Markdown

🔔 5th Reminder

Hey @jkczyz @wpaulino! 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.

As a result, we now validate that both commitments retain at least one
output under the new funding scope, which is crucial for zero-reserve
channels.
We previously determined this value by subtracting the htlcs, the
anchors, and the commitment transaction fee. This ignored the reserve,
as well as the at-least-one-output requirement in zero-reserve channels.

This new field now accounts for both of these constraints. It can be
seen as the total spliceable balance from the channel.
This is equivalent to the previous commit, see the debug assertions
added in the previous commit. We now also get to communicate the
exact maximum back to the user, instead of some "balance is lower
than our reserve" message, which is hard to react to.
We did not enforce this minimum when accepting 0-reserve channels. This
is because we depended on the `MIN_THEIR_CHAN_RESERVE_SATOSHIS` constant
to guarantee this minimum channel value, but this value is no longer
read in 0-reserve channels.

Note that the user's `min_funding_satoshis` value would still be
respected in this case.

When splicing 0-reserve channels, we only enforced that the commitment
transaction retained at least one output after the splice, which could
produce a channel value lower than 1000sats.

Along the way, we also now enforce this 1000sat minimum when splicing
reserve-enabled channels. We previously correctly enforced the reserves
after the splice, but this could still result in a channel value smaller
than 1000sats. This case is now rejected during splice validation.

Note that the user's `min_funding_satoshis` is not respected when
validating splice contributions, we leave this for follow-up work.
@tankyleo tankyleo force-pushed the 2026-04-next-splice-out-maximum branch from 794b9ff to 89ee5a1 Compare April 29, 2026 05:40
@tankyleo
Copy link
Copy Markdown
Contributor Author

Rebased, and appended one commit to enforce a min channel value satoshis everywhere @TheBlueMatt (create channel, accept channel, and splice channel).

@tankyleo tankyleo requested a review from TheBlueMatt April 29, 2026 05:52
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.

6 participants