Skip to content

Support manually selecting inputs consuming their entire value#4575

Open
wpaulino wants to merge 2 commits intolightningdevkit:mainfrom
wpaulino:funding-contribution-builder-manual-inputs
Open

Support manually selecting inputs consuming their entire value#4575
wpaulino wants to merge 2 commits intolightningdevkit: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

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

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

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 thoroughly re-reviewed the entire PR diff. The two previously flagged backwards-compatibility issues (lines 866 and 1310) remain the only actionable issues. No new bugs, security problems, or logic errors found.

Review Summary

Previously Flagged Issues (still open)

Both backwards-compatibility issues from the prior review are still present and unresolved:

  1. lightning/src/ln/funding.rs:866 — Old persisted FundingContribution objects have input_mode == None, but the guard uses == Some(CoinSelected). This routes old coin-selected contributions with inputs into the manual/no-inputs branch, which uses net_value_without_fee + spliceable_balance as the fee buffer instead of estimated_fee + change_value, and always returns change_output: None, silently losing the change.

  2. lightning/src/ln/funding.rs:1310 — Same pattern: old persisted coin-selected priors with input_mode == None bypass the guard that prevents mixing manual inputs onto a coin-selected prior, because the check uses == Some(CoinSelected) instead of != Some(ManuallySelected).

New Issues

No new issues found. The rest of the implementation is correct:

  • ManuallySelected flow through amend_without_coin_selection correctly replaces inputs, drops change, and adjusts feerate.
  • ManuallySelectedInputsInsufficient does not fall through to coin selection in AsyncFundingBuilder::build / SyncFundingBuilder::build (only MissingCoinSelectionSource triggers fallback).
  • splice_in_inputs correctly appends new inputs to prior manual inputs via builder initialization + add_inputs.
  • spliceable_balance validation in try_build_without_coin_selection correctly handles net-negative contributions.
  • FundingInputMode TLV serialization (tag 15, option) correctly yields None for legacy contributions.
  • validate_inputs is correctly called for manually selected inputs.
  • request_matches_prior correctly compares outpoints for manual mode and value_added for coin-selected mode.
  • Moving spliceable_balance computation to the top of splice_channel is safe since it's now needed unconditionally.
  • compute_feerate_adjustment correctly models manual inputs as adding/offsetting channel balance for fee purposes.

@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 94.84536% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.31%. Comparing base (b64efcd) to head (c3da7fe).
⚠️ Report is 19 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/funding.rs 96.28% 12 Missing and 9 partials ⚠️
lightning/src/ln/channel.rs 43.75% 8 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4575      +/-   ##
==========================================
+ Coverage   86.22%   86.31%   +0.09%     
==========================================
  Files         159      159              
  Lines      109170   109681     +511     
  Branches   109170   109681     +511     
==========================================
+ Hits        94136    94676     +540     
+ Misses      12424    12385      -39     
- Partials     2610     2620      +10     
Flag Coverage Δ
tests 86.31% <94.84%> (+0.09%) ⬆️

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 on lines 1095 to +1096
value_added: Amount,
manually_selected_inputs: Vec<FundingTxInput>,
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.

Seems making these an enum would simplify some of the checks? Or is there a good argument for keeping these separate fields? It would essentially be changing FundingInputs to own the manual inputs and using that here.

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.

I kept them separate so we don't expose errors on the add/remove_value/input builder methods. Instead, we detect when the user is attempting both at build time, since technically they could have provided a coin selection source to the builder but still opted to manually select inputs.

Comment thread lightning/src/ln/funding.rs Outdated
(Some(value_added), contribution.inputs, Some(FundingInputMode::CoinSelected))
},
FundingInputs::ManuallySelected { inputs } => {
(None, inputs.to_vec(), Some(FundingInputMode::Manual))
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.

FWIW, with the enum approach we could avoid this allocation.

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.

return Err(FundingContributionError::InvalidSpliceValue);
}

validate_inputs(&self.manually_selected_inputs)?;
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.

shouldn't happen but wouldn't hurt for this to check for duplicate inputs provided?

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.

We already fail this when we get to the negotiation stage. Not sure it's worth failing early given how unlikely this would be, but happy to add it if others agree.

@wpaulino wpaulino force-pushed the funding-contribution-builder-manual-inputs branch from 228cae8 to 17c864b Compare May 5, 2026 23:36
wpaulino added 2 commits May 5, 2026 16:40
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.
@wpaulino wpaulino force-pushed the funding-contribution-builder-manual-inputs branch from 17c864b to c3da7fe Compare May 5, 2026 23:43
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