Skip to content

Support manually selecting inputs consuming their entire value#4575

Open
wpaulino wants to merge 3 commits into
lightningdevkit:mainfrom
wpaulino:funding-contribution-builder-manual-inputs
Open

Support manually selecting inputs consuming their entire value#4575
wpaulino wants to merge 3 commits into
lightningdevkit:mainfrom
wpaulino:funding-contribution-builder-manual-inputs

Conversation

@wpaulino
Copy link
Copy Markdown
Contributor

This commit introduces an alternative way of splicing in funds without coin selection by requiring the full UTXO to be provided. Each UTXO's entire value (minus fees) is allocated towards the channel, which provides unified balance wallets a more intuitive API when splicing funds into the channel, as they don't particularly care about maintaining a portion of their balance onchain.

To simplify the implementation, we require that contributions are not allowed to mix coin-selected inputs with manually-selected ones. Users will need to start a fresh contribution if they want to change the funding input mode.

@wpaulino wpaulino added this to the 0.3 milestone Apr 23, 2026
@wpaulino wpaulino requested review from TheBlueMatt and jkczyz April 23, 2026 17:08
@wpaulino wpaulino self-assigned this Apr 23, 2026
@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Apr 23, 2026

👋 I see @TheBlueMatt was un-assigned.
If you'd like another reviewer assignment, please click here.

@wpaulino wpaulino marked this pull request as ready for review April 23, 2026 17:08
);

if !self.inputs.is_empty() {
if !self.inputs.is_empty() && self.input_mode == Some(FundingInputMode::CoinSelected) {
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.

Bug: Backwards compatibility issue with persisted FundingContribution objects.

FundingContribution is persisted in PendingFunding.contributions (channel.rs:2917). When deserializing contributions created before this PR, input_mode will be None (it's a new option TLV field). Old coin-selected contributions with inputs will have input_mode == None, causing this condition to be false even though they should take the coin-selected branch.

This causes two problems for old persisted coin-selected contributions:

  1. Wrong fee buffer calculation: Uses holder_balance + net_value_without_fee instead of estimated_fee + change_value, potentially allowing or rejecting feerate adjustments incorrectly.
  2. Change output silently dropped: compute_feerate_adjustment returns None for change, and at_feerate sets change_output = None, losing the change value.

This is reachable via for_acceptor_at_feerate / for_initiator_at_feerate called on contributions loaded from pending_splice.contributions (channel.rs lines 12504, 12944, 13127, 13145).

Fix: use self.input_mode != Some(FundingInputMode::Manual) instead of self.input_mode == Some(FundingInputMode::CoinSelected) to preserve old behavior for contributions where input_mode is None:

Suggested change
if !self.inputs.is_empty() && self.input_mode == Some(FundingInputMode::CoinSelected) {
if !self.inputs.is_empty() && self.input_mode != Some(FundingInputMode::Manual) {

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.

There is no backwards compatibility concern because the serialized object has not been included in a release yet.

Comment thread lightning/src/ln/funding.rs Outdated
if let Some(PriorContribution { contribution: prior_contribution, .. }) =
self.prior_contribution.as_ref()
{
if prior_contribution.input_mode == Some(FundingInputMode::CoinSelected)
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.

Minor: Same backwards-compat pattern as line 880. Old persisted coin-selected contributions will have input_mode == None, so this guard won't fire for them. In practice this is mostly mitigated by the check at line 1309 (value_added > 0 && manually_selected_inputs non-empty), but it could miss edge cases where the old prior had value_added() == 0 (inputs exactly covered outputs + fees).

Consider using != Some(FundingInputMode::Manual) combined with a non-empty inputs check:

Suggested change
if prior_contribution.input_mode == Some(FundingInputMode::CoinSelected)
if prior_contribution.input_mode != Some(FundingInputMode::Manual)
&& !prior_contribution.inputs.is_empty()

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

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

I've completed a thorough re-review of the entire PR diff and the current code. The previously flagged .take() bug (now line 1229) has been fixed to .clone(). The three other previously flagged issues remain open. I found no new issues.

Review Summary

Previously Flagged Issues — Status

  1. FIXEDlightning/src/ln/funding.rs:1229.take() destroying funding_inputs on coin selection fallback path is now .clone().

  2. Still openlightning/src/ln/funding.rs:933 — Backwards-compat: old persisted coin-selected contributions have input_mode == None, causing them to take the manual-inputs fee buffer branch instead of the coin-selected branch in compute_feerate_adjustment.

  3. Still openlightning/src/ln/funding.rs:1401-1412 — Backwards-compat: old coin-selected priors with input_mode == None get funding_inputs = None in builder init, silently discarding their inputs when the request doesn't match and amend_without_coin_selection is called.

  4. Still openlightning/src/ln/funding.rs:1375-1381 — Duplicate-output validation (by script_pubkey) shadows MAX_MONEY sum tests since funding_output_sats() always produces the same script_pubkey.

No New Issues Found

The new FundingInputs/FundingInputMode enums, builder add_input/add_inputs/remove_input methods, splice_in_inputs convenience method, fee buffer calculation for manual inputs, duplicate input validation, ManuallySelectedInputsInsufficient error propagation (properly not falling through to coin selection), and all new tests are correct and consistent.

@wpaulino wpaulino force-pushed the funding-contribution-builder-manual-inputs branch from 12e5994 to 412ec3d Compare April 23, 2026 17:19
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 96.14874% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.54%. Comparing base (f408b17) to head (a9fa610).
⚠️ Report is 26 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/funding.rs 96.07% 15 Missing and 14 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4575      +/-   ##
==========================================
+ Coverage   86.15%   86.54%   +0.38%     
==========================================
  Files         157      159       +2     
  Lines      109096   110371    +1275     
  Branches   109096   110371    +1275     
==========================================
+ Hits        93989    95517    +1528     
+ Misses      12485    12316     -169     
+ Partials     2622     2538      -84     
Flag Coverage Δ
fuzzing-fake-hashes 5.73% <0.00%> (?)
fuzzing-real-hashes 23.11% <3.67%> (?)
tests 86.20% <96.14%> (+0.05%) ⬆️

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
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 LGTM. One question.

Comment thread lightning/src/ln/funding.rs Outdated
@ldk-reviews-bot
Copy link
Copy Markdown

🔔 1st Reminder

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

@wpaulino wpaulino force-pushed the funding-contribution-builder-manual-inputs branch from 412ec3d to 228cae8 Compare April 28, 2026 22:00
@ldk-reviews-bot
Copy link
Copy Markdown

🔔 2nd Reminder

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

@wpaulino wpaulino requested a review from TheBlueMatt April 29, 2026 16:58
TheBlueMatt
TheBlueMatt previously approved these changes Apr 29, 2026
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.

thanks

Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Copy link
Copy Markdown
Contributor

@elnosh elnosh left a comment

Choose a reason for hiding this comment

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

Not that familiar with splicing-related code in the codebase but looking at this for learning purposes. After much staring, changes make sense although agree with @jkczyz comment on making an enum since those fields (value_added and manually_selected_inputs) are mutually exclusive in the ways inputs can be provided.

Comment on lines +1128 to +1129
/// used if the request cannot be satisfied by reusing a prior contribution, by using only
/// manually selected inputs, or by building a pure splice-out directly.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure I follow this comment. If we have a builder with either sync or async CoinSelectionSource then manually added inputs are not allowed.

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.

Manually selected inputs are still allowed as long as the added_value is 0.

Comment thread lightning/src/ln/funding.rs Outdated
@wpaulino wpaulino force-pushed the funding-contribution-builder-manual-inputs branch 3 times, most recently from c3da7fe to 6e16493 Compare May 6, 2026 19:27
Comment thread lightning/src/ln/funding.rs Outdated
@TheBlueMatt
Copy link
Copy Markdown
Collaborator

Needs rebase again :/

@wpaulino wpaulino force-pushed the funding-contribution-builder-manual-inputs branch from 6e16493 to c240f1b Compare May 13, 2026 18:40
Comment on lines +1373 to +1378
for (idx, output) in self.outputs.iter().enumerate() {
if self.outputs[..idx]
.iter()
.any(|existing_output| existing_output.script_pubkey == output.script_pubkey)
{
return Err(FundingContributionError::InvalidSpliceValue);
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 / test gap: this new duplicate-output check fires before the value_removed > MAX_MONEY accumulation below. Since funding_output_sats() always returns the same script_pubkey (new_p2wpkh(&WPubkeyHash::all_zeros())), the existing tests test_build_funding_contribution_validates_max_money ("splice_out with multiple outputs summing > MAX_MONEY", lines 2723-2734) and test_funding_builder_validates_mixed_request_max_money ("outputs summing > MAX_MONEY", lines 2755-2769) now hit this duplicate check first. They still pass because both paths yield InvalidSpliceValue, but the MAX_MONEY sum-of-outputs path has lost test coverage.

Consider using distinct script_pubkey values for each output in those tests.

TheBlueMatt
TheBlueMatt previously approved these changes May 17, 2026
let value_added =
inner.funding_inputs.as_ref().map_or(Amount::ZERO, FundingInputs::value_added);

match inner.build_without_coin_selection() {
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.

Its quite awkward that we call this here and it takes several fields from inner internally but then we reuse inner a few lines down as well as later. It probably makes sense to pay the penalty of either calling prepare_coin_selection_request upfront so we can make build_without_coin_selection take mut self without ref or cloneing internally to avoid ripping out parts of inner before we reuse it. Seems like too much opportunity for stuff to go wrong later.

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.

Alright just decided to clone internally

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.

Sorry, this wasn't the only place, we also take the prior_contribution in try_build_without_coin_selection and mutate inner that way too.

@wpaulino wpaulino force-pushed the funding-contribution-builder-manual-inputs branch from c240f1b to ed757b2 Compare May 18, 2026 20:13
@wpaulino wpaulino requested review from TheBlueMatt and jkczyz May 18, 2026 20:14
@TheBlueMatt TheBlueMatt removed their request for review May 18, 2026 20:37
wpaulino added 3 commits May 18, 2026 15:03
This commit introduces an alternative way of splicing in funds without
coin selection by requiring the full UTXO to be provided. Each UTXO's
entire value (minus fees) is allocated towards the channel, which
provides unified balance wallets a more intuitive API when splicing
funds into the channel, as they don't particularly care about
maintaining a portion of their balance onchain.

To simplify the implementation, we require that contributions are not
allowed to mix coin-selected inputs with manually-selected ones. Users
will need to start a fresh contribution if they want to change the
funding input mode.
There's no reason not to do so, and it allows us to fail earlier when
the user's net contribution exceeds their spliceable balance.
While this is already enforced when we get to the interactive
negotiation phase, we choose to fail early anyway.
@wpaulino wpaulino force-pushed the funding-contribution-builder-manual-inputs branch from ed757b2 to a9fa610 Compare May 18, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants