Support manually selecting inputs consuming their entire value - #4575

Merged
jkczyz merged 3 commits into
lightningdevkit:mainfrom
wpaulino:funding-contribution-builder-manual-inputs
May 19, 2026
Merged

Support manually selecting inputs consuming their entire value#4575
jkczyz merged 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.

@wpaulinowpaulino added this to the 0.3 milestone Apr 23, 2026
@wpaulinowpaulino self-assigned this Apr 23, 2026
@ldk-reviews-bot

ldk-reviews-bot commented Apr 23, 2026

Copy link
Copy Markdown

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

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

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

Copy link
Copy Markdown
Collaborator

I've completed a thorough review of the entire PR diff and verified the current code state. All previously flagged issues have been checked, and I've searched for any new issues.

Review Summary

Previously Flagged Issues — Status

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

  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. This drops the change output and miscalculates the fee buffer.

  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 code is otherwise correct. Specifically verified:

  • FundingInputs/FundingInputMode enums and their serialization
  • Builder add_input/add_inputs/remove_input methods correctly enforce mode exclusivity
  • splice_in_inputs convenience method correctly appends to prior manual inputs
  • ManuallySelectedInputsInsufficient error correctly does NOT fall through to coin selection in both sync and async builders
  • Fee buffer calculation for manual inputs in compute_feerate_adjustment is correct for positive and negative net_value_without_fee
  • validate_inputs duplicate detection covers the combined set (prior + new) of manually selected inputs
  • validate_contribution_parameters runs before build_from_prior_contribution, catching duplicates and MAX_MONEY violations
  • request_matches_prior correctly compares outpoints for manual inputs and value for coin-selected inputs
  • net_value_without_fee and net_value_at_feerate arithmetic is sound for manual inputs
  • Spliceable balance check in try_build_without_coin_selection correctly gates net-negative manual contributions

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 12e5994 to 412ec3dCompareApril 23, 2026 17:19
@codecov

codecovBot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing linesPatch %Lines
lightning/src/ln/funding.rs96.07%15 Missing and 14 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4575 +/- ##
==========================================
+ Coverage 86.15% 86.58% +0.43% 
==========================================
Files 157 159 +2 Lines 109096 110419 +1323 Branches 109096 110419 +1323 ==========================================
+ Hits 93989 95611 +1622 + Misses 12485 12275 -210 + Partials 2622 2533 -89 
FlagCoverage Δ
fuzzing-fake-hashes6.57% <0.00%> (?)
fuzzing-real-hashes23.17% <3.67%> (?)
tests86.23% <96.14%> (+0.07%)⬆️

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.

@Alkamal01

Copy link
Copy Markdown
Contributor

The root problem seems to be relying on == Some(CoinSelected) for persisted data. For legacy contributions, None effectively behaved like coin selection when inputs were present, so switching to != Some(Manual) (with an inputs non-empty check where needed) would preserve the old behavior.

One additional edge case in request_matches_prior: (Some(CoinSelected), None) currently falls into _ => false, so an RBF with the same value on a legacy prior won't be treated as a match and skips the feerate-adjustment path. Not strictly a correctness issue, but it becomes inconsistent if the other branches get fixed.

So overall, it'd be good to make the handling of None consistent across all three paths.

@TheBlueMattTheBlueMatt left a comment

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.

All LGTM. One question.

Comment threadlightning/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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 412ec3d to 228cae8CompareApril 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.

TheBlueMatt
TheBlueMatt previously approved these changes Apr 29, 2026

@TheBlueMattTheBlueMatt left a comment

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.

thanks

Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated

@elnoshelnosh left a comment

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.

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
ContributorAuthor

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 threadlightning/src/ln/funding.rs Outdated
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch 3 times, most recently from c3da7fe to 6e16493CompareMay 6, 2026 19:27
Comment threadlightning/src/ln/funding.rs Outdated
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase again :/

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 6e16493 to c240f1bCompareMay 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
ContributorAuthor

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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from c240f1b to ed757b2CompareMay 18, 2026 20:13
@TheBlueMatt
TheBlueMatt removed their request for review May 18, 2026 20:37
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from ed757b2 to a9fa610CompareMay 18, 2026 22:04
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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from a9fa610 to dcca939CompareMay 19, 2026 18:00
@wpaulino
wpaulino requested a review from TheBlueMattMay 19, 2026 18:00
@jkczyz
jkczyz merged commit e47a231 into lightningdevkit:mainMay 19, 2026
24 checks passed
@wpaulino
wpaulino deleted the funding-contribution-builder-manual-inputs branch May 19, 2026 20:08
Sign up for freeto 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.

7 participants

@wpaulino@ldk-reviews-bot@ldk-claude-review-bot@Alkamal01@TheBlueMatt@jkczyz@elnosh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Support manually selecting inputs consuming their entire value - #4575

Merged
jkczyz merged 3 commits into
lightningdevkit:mainfrom
wpaulino:funding-contribution-builder-manual-inputs
May 19, 2026
Merged

Support manually selecting inputs consuming their entire value#4575
jkczyz merged 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.

@wpaulinowpaulino added this to the 0.3 milestone Apr 23, 2026
@wpaulinowpaulino self-assigned this Apr 23, 2026
@ldk-reviews-bot

ldk-reviews-bot commented Apr 23, 2026

Copy link
Copy Markdown

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

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

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

Copy link
Copy Markdown
Collaborator

I've completed a thorough review of the entire PR diff and verified the current code state. All previously flagged issues have been checked, and I've searched for any new issues.

Review Summary

Previously Flagged Issues — Status

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

  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. This drops the change output and miscalculates the fee buffer.

  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 code is otherwise correct. Specifically verified:

  • FundingInputs/FundingInputMode enums and their serialization
  • Builder add_input/add_inputs/remove_input methods correctly enforce mode exclusivity
  • splice_in_inputs convenience method correctly appends to prior manual inputs
  • ManuallySelectedInputsInsufficient error correctly does NOT fall through to coin selection in both sync and async builders
  • Fee buffer calculation for manual inputs in compute_feerate_adjustment is correct for positive and negative net_value_without_fee
  • validate_inputs duplicate detection covers the combined set (prior + new) of manually selected inputs
  • validate_contribution_parameters runs before build_from_prior_contribution, catching duplicates and MAX_MONEY violations
  • request_matches_prior correctly compares outpoints for manual inputs and value for coin-selected inputs
  • net_value_without_fee and net_value_at_feerate arithmetic is sound for manual inputs
  • Spliceable balance check in try_build_without_coin_selection correctly gates net-negative manual contributions

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 12e5994 to 412ec3dCompareApril 23, 2026 17:19
@codecov

codecovBot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing linesPatch %Lines
lightning/src/ln/funding.rs96.07%15 Missing and 14 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4575 +/- ##
==========================================
+ Coverage 86.15% 86.58% +0.43% 
==========================================
Files 157 159 +2 Lines 109096 110419 +1323 Branches 109096 110419 +1323 ==========================================
+ Hits 93989 95611 +1622 + Misses 12485 12275 -210 + Partials 2622 2533 -89 
FlagCoverage Δ
fuzzing-fake-hashes6.57% <0.00%> (?)
fuzzing-real-hashes23.17% <3.67%> (?)
tests86.23% <96.14%> (+0.07%)⬆️

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.

@Alkamal01

Copy link
Copy Markdown
Contributor

The root problem seems to be relying on == Some(CoinSelected) for persisted data. For legacy contributions, None effectively behaved like coin selection when inputs were present, so switching to != Some(Manual) (with an inputs non-empty check where needed) would preserve the old behavior.

One additional edge case in request_matches_prior: (Some(CoinSelected), None) currently falls into _ => false, so an RBF with the same value on a legacy prior won't be treated as a match and skips the feerate-adjustment path. Not strictly a correctness issue, but it becomes inconsistent if the other branches get fixed.

So overall, it'd be good to make the handling of None consistent across all three paths.

@TheBlueMattTheBlueMatt left a comment

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.

All LGTM. One question.

Comment threadlightning/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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 412ec3d to 228cae8CompareApril 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.

TheBlueMatt
TheBlueMatt previously approved these changes Apr 29, 2026

@TheBlueMattTheBlueMatt left a comment

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.

thanks

Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated

@elnoshelnosh left a comment

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.

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
ContributorAuthor

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 threadlightning/src/ln/funding.rs Outdated
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch 3 times, most recently from c3da7fe to 6e16493CompareMay 6, 2026 19:27
Comment threadlightning/src/ln/funding.rs Outdated
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase again :/

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 6e16493 to c240f1bCompareMay 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
ContributorAuthor

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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from c240f1b to ed757b2CompareMay 18, 2026 20:13
@TheBlueMatt
TheBlueMatt removed their request for review May 18, 2026 20:37
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from ed757b2 to a9fa610CompareMay 18, 2026 22:04
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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from a9fa610 to dcca939CompareMay 19, 2026 18:00
@wpaulino
wpaulino requested a review from TheBlueMattMay 19, 2026 18:00
@jkczyz
jkczyz merged commit e47a231 into lightningdevkit:mainMay 19, 2026
24 checks passed
@wpaulino
wpaulino deleted the funding-contribution-builder-manual-inputs branch May 19, 2026 20:08
Sign up for freeto 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.

7 participants

@wpaulino@ldk-reviews-bot@ldk-claude-review-bot@Alkamal01@TheBlueMatt@jkczyz@elnosh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support manually selecting inputs consuming their entire value - #4575

Merged
jkczyz merged 3 commits into
lightningdevkit:mainfrom
wpaulino:funding-contribution-builder-manual-inputs
May 19, 2026
Merged

Support manually selecting inputs consuming their entire value#4575
jkczyz merged 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.

@wpaulinowpaulino added this to the 0.3 milestone Apr 23, 2026
@wpaulinowpaulino self-assigned this Apr 23, 2026
@ldk-reviews-bot

ldk-reviews-bot commented Apr 23, 2026

Copy link
Copy Markdown

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

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

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

Copy link
Copy Markdown
Collaborator

I've completed a thorough review of the entire PR diff and verified the current code state. All previously flagged issues have been checked, and I've searched for any new issues.

Review Summary

Previously Flagged Issues — Status

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

  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. This drops the change output and miscalculates the fee buffer.

  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 code is otherwise correct. Specifically verified:

  • FundingInputs/FundingInputMode enums and their serialization
  • Builder add_input/add_inputs/remove_input methods correctly enforce mode exclusivity
  • splice_in_inputs convenience method correctly appends to prior manual inputs
  • ManuallySelectedInputsInsufficient error correctly does NOT fall through to coin selection in both sync and async builders
  • Fee buffer calculation for manual inputs in compute_feerate_adjustment is correct for positive and negative net_value_without_fee
  • validate_inputs duplicate detection covers the combined set (prior + new) of manually selected inputs
  • validate_contribution_parameters runs before build_from_prior_contribution, catching duplicates and MAX_MONEY violations
  • request_matches_prior correctly compares outpoints for manual inputs and value for coin-selected inputs
  • net_value_without_fee and net_value_at_feerate arithmetic is sound for manual inputs
  • Spliceable balance check in try_build_without_coin_selection correctly gates net-negative manual contributions

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 12e5994 to 412ec3dCompareApril 23, 2026 17:19
@codecov

codecovBot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing linesPatch %Lines
lightning/src/ln/funding.rs96.07%15 Missing and 14 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4575 +/- ##
==========================================
+ Coverage 86.15% 86.58% +0.43% 
==========================================
Files 157 159 +2 Lines 109096 110419 +1323 Branches 109096 110419 +1323 ==========================================
+ Hits 93989 95611 +1622 + Misses 12485 12275 -210 + Partials 2622 2533 -89 
FlagCoverage Δ
fuzzing-fake-hashes6.57% <0.00%> (?)
fuzzing-real-hashes23.17% <3.67%> (?)
tests86.23% <96.14%> (+0.07%)⬆️

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.

@Alkamal01

Copy link
Copy Markdown
Contributor

The root problem seems to be relying on == Some(CoinSelected) for persisted data. For legacy contributions, None effectively behaved like coin selection when inputs were present, so switching to != Some(Manual) (with an inputs non-empty check where needed) would preserve the old behavior.

One additional edge case in request_matches_prior: (Some(CoinSelected), None) currently falls into _ => false, so an RBF with the same value on a legacy prior won't be treated as a match and skips the feerate-adjustment path. Not strictly a correctness issue, but it becomes inconsistent if the other branches get fixed.

So overall, it'd be good to make the handling of None consistent across all three paths.

@TheBlueMattTheBlueMatt left a comment

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.

All LGTM. One question.

Comment threadlightning/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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 412ec3d to 228cae8CompareApril 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.

TheBlueMatt
TheBlueMatt previously approved these changes Apr 29, 2026

@TheBlueMattTheBlueMatt left a comment

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.

thanks

Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated

@elnoshelnosh left a comment

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.

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
ContributorAuthor

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 threadlightning/src/ln/funding.rs Outdated
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch 3 times, most recently from c3da7fe to 6e16493CompareMay 6, 2026 19:27
Comment threadlightning/src/ln/funding.rs Outdated
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase again :/

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 6e16493 to c240f1bCompareMay 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
ContributorAuthor

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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from c240f1b to ed757b2CompareMay 18, 2026 20:13
@TheBlueMatt
TheBlueMatt removed their request for review May 18, 2026 20:37
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from ed757b2 to a9fa610CompareMay 18, 2026 22:04
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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from a9fa610 to dcca939CompareMay 19, 2026 18:00
@wpaulino
wpaulino requested a review from TheBlueMattMay 19, 2026 18:00
@jkczyz
jkczyz merged commit e47a231 into lightningdevkit:mainMay 19, 2026
24 checks passed
@wpaulino
wpaulino deleted the funding-contribution-builder-manual-inputs branch May 19, 2026 20:08
Sign up for freeto 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.

7 participants

@wpaulino@ldk-reviews-bot@ldk-claude-review-bot@Alkamal01@TheBlueMatt@jkczyz@elnosh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support manually selecting inputs consuming their entire value - #4575

Merged
jkczyz merged 3 commits into
lightningdevkit:mainfrom
wpaulino:funding-contribution-builder-manual-inputs
May 19, 2026
Merged

Support manually selecting inputs consuming their entire value#4575
jkczyz merged 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.

@wpaulinowpaulino added this to the 0.3 milestone Apr 23, 2026
@wpaulinowpaulino self-assigned this Apr 23, 2026
@ldk-reviews-bot

ldk-reviews-bot commented Apr 23, 2026

Copy link
Copy Markdown

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

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

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

Copy link
Copy Markdown
Collaborator

I've completed a thorough review of the entire PR diff and verified the current code state. All previously flagged issues have been checked, and I've searched for any new issues.

Review Summary

Previously Flagged Issues — Status

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

  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. This drops the change output and miscalculates the fee buffer.

  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 code is otherwise correct. Specifically verified:

  • FundingInputs/FundingInputMode enums and their serialization
  • Builder add_input/add_inputs/remove_input methods correctly enforce mode exclusivity
  • splice_in_inputs convenience method correctly appends to prior manual inputs
  • ManuallySelectedInputsInsufficient error correctly does NOT fall through to coin selection in both sync and async builders
  • Fee buffer calculation for manual inputs in compute_feerate_adjustment is correct for positive and negative net_value_without_fee
  • validate_inputs duplicate detection covers the combined set (prior + new) of manually selected inputs
  • validate_contribution_parameters runs before build_from_prior_contribution, catching duplicates and MAX_MONEY violations
  • request_matches_prior correctly compares outpoints for manual inputs and value for coin-selected inputs
  • net_value_without_fee and net_value_at_feerate arithmetic is sound for manual inputs
  • Spliceable balance check in try_build_without_coin_selection correctly gates net-negative manual contributions

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 12e5994 to 412ec3dCompareApril 23, 2026 17:19
@codecov

codecovBot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing linesPatch %Lines
lightning/src/ln/funding.rs96.07%15 Missing and 14 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4575 +/- ##
==========================================
+ Coverage 86.15% 86.58% +0.43% 
==========================================
Files 157 159 +2 Lines 109096 110419 +1323 Branches 109096 110419 +1323 ==========================================
+ Hits 93989 95611 +1622 + Misses 12485 12275 -210 + Partials 2622 2533 -89 
FlagCoverage Δ
fuzzing-fake-hashes6.57% <0.00%> (?)
fuzzing-real-hashes23.17% <3.67%> (?)
tests86.23% <96.14%> (+0.07%)⬆️

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.

@Alkamal01

Copy link
Copy Markdown
Contributor

The root problem seems to be relying on == Some(CoinSelected) for persisted data. For legacy contributions, None effectively behaved like coin selection when inputs were present, so switching to != Some(Manual) (with an inputs non-empty check where needed) would preserve the old behavior.

One additional edge case in request_matches_prior: (Some(CoinSelected), None) currently falls into _ => false, so an RBF with the same value on a legacy prior won't be treated as a match and skips the feerate-adjustment path. Not strictly a correctness issue, but it becomes inconsistent if the other branches get fixed.

So overall, it'd be good to make the handling of None consistent across all three paths.

@TheBlueMattTheBlueMatt left a comment

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.

All LGTM. One question.

Comment threadlightning/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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 412ec3d to 228cae8CompareApril 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.

TheBlueMatt
TheBlueMatt previously approved these changes Apr 29, 2026

@TheBlueMattTheBlueMatt left a comment

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.

thanks

Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated

@elnoshelnosh left a comment

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.

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
ContributorAuthor

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 threadlightning/src/ln/funding.rs Outdated
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch 3 times, most recently from c3da7fe to 6e16493CompareMay 6, 2026 19:27
Comment threadlightning/src/ln/funding.rs Outdated
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase again :/

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 6e16493 to c240f1bCompareMay 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
ContributorAuthor

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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from c240f1b to ed757b2CompareMay 18, 2026 20:13
@TheBlueMatt
TheBlueMatt removed their request for review May 18, 2026 20:37
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from ed757b2 to a9fa610CompareMay 18, 2026 22:04
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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from a9fa610 to dcca939CompareMay 19, 2026 18:00
@wpaulino
wpaulino requested a review from TheBlueMattMay 19, 2026 18:00
@jkczyz
jkczyz merged commit e47a231 into lightningdevkit:mainMay 19, 2026
24 checks passed
@wpaulino
wpaulino deleted the funding-contribution-builder-manual-inputs branch May 19, 2026 20:08
Sign up for freeto 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.

7 participants

@wpaulino@ldk-reviews-bot@ldk-claude-review-bot@Alkamal01@TheBlueMatt@jkczyz@elnosh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Support manually selecting inputs consuming their entire value - #4575

Merged
jkczyz merged 3 commits into
lightningdevkit:mainfrom
wpaulino:funding-contribution-builder-manual-inputs
May 19, 2026
Merged

Support manually selecting inputs consuming their entire value#4575
jkczyz merged 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.

@wpaulinowpaulino added this to the 0.3 milestone Apr 23, 2026
@wpaulinowpaulino self-assigned this Apr 23, 2026
@ldk-reviews-bot

ldk-reviews-bot commented Apr 23, 2026

Copy link
Copy Markdown

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

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

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

Copy link
Copy Markdown
Collaborator

I've completed a thorough review of the entire PR diff and verified the current code state. All previously flagged issues have been checked, and I've searched for any new issues.

Review Summary

Previously Flagged Issues — Status

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

  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. This drops the change output and miscalculates the fee buffer.

  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 code is otherwise correct. Specifically verified:

  • FundingInputs/FundingInputMode enums and their serialization
  • Builder add_input/add_inputs/remove_input methods correctly enforce mode exclusivity
  • splice_in_inputs convenience method correctly appends to prior manual inputs
  • ManuallySelectedInputsInsufficient error correctly does NOT fall through to coin selection in both sync and async builders
  • Fee buffer calculation for manual inputs in compute_feerate_adjustment is correct for positive and negative net_value_without_fee
  • validate_inputs duplicate detection covers the combined set (prior + new) of manually selected inputs
  • validate_contribution_parameters runs before build_from_prior_contribution, catching duplicates and MAX_MONEY violations
  • request_matches_prior correctly compares outpoints for manual inputs and value for coin-selected inputs
  • net_value_without_fee and net_value_at_feerate arithmetic is sound for manual inputs
  • Spliceable balance check in try_build_without_coin_selection correctly gates net-negative manual contributions

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 12e5994 to 412ec3dCompareApril 23, 2026 17:19
@codecov

codecovBot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing linesPatch %Lines
lightning/src/ln/funding.rs96.07%15 Missing and 14 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4575 +/- ##
==========================================
+ Coverage 86.15% 86.58% +0.43% 
==========================================
Files 157 159 +2 Lines 109096 110419 +1323 Branches 109096 110419 +1323 ==========================================
+ Hits 93989 95611 +1622 + Misses 12485 12275 -210 + Partials 2622 2533 -89 
FlagCoverage Δ
fuzzing-fake-hashes6.57% <0.00%> (?)
fuzzing-real-hashes23.17% <3.67%> (?)
tests86.23% <96.14%> (+0.07%)⬆️

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.

@Alkamal01

Copy link
Copy Markdown
Contributor

The root problem seems to be relying on == Some(CoinSelected) for persisted data. For legacy contributions, None effectively behaved like coin selection when inputs were present, so switching to != Some(Manual) (with an inputs non-empty check where needed) would preserve the old behavior.

One additional edge case in request_matches_prior: (Some(CoinSelected), None) currently falls into _ => false, so an RBF with the same value on a legacy prior won't be treated as a match and skips the feerate-adjustment path. Not strictly a correctness issue, but it becomes inconsistent if the other branches get fixed.

So overall, it'd be good to make the handling of None consistent across all three paths.

@TheBlueMattTheBlueMatt left a comment

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.

All LGTM. One question.

Comment threadlightning/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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 412ec3d to 228cae8CompareApril 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.

TheBlueMatt
TheBlueMatt previously approved these changes Apr 29, 2026

@TheBlueMattTheBlueMatt left a comment

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.

thanks

Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated

@elnoshelnosh left a comment

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.

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
ContributorAuthor

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 threadlightning/src/ln/funding.rs Outdated
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch 3 times, most recently from c3da7fe to 6e16493CompareMay 6, 2026 19:27
Comment threadlightning/src/ln/funding.rs Outdated
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase again :/

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 6e16493 to c240f1bCompareMay 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
ContributorAuthor

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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from c240f1b to ed757b2CompareMay 18, 2026 20:13
@TheBlueMatt
TheBlueMatt removed their request for review May 18, 2026 20:37
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from ed757b2 to a9fa610CompareMay 18, 2026 22:04
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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from a9fa610 to dcca939CompareMay 19, 2026 18:00
@wpaulino
wpaulino requested a review from TheBlueMattMay 19, 2026 18:00
@jkczyz
jkczyz merged commit e47a231 into lightningdevkit:mainMay 19, 2026
24 checks passed
@wpaulino
wpaulino deleted the funding-contribution-builder-manual-inputs branch May 19, 2026 20:08
Sign up for freeto 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.

7 participants

@wpaulino@ldk-reviews-bot@ldk-claude-review-bot@Alkamal01@TheBlueMatt@jkczyz@elnosh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support manually selecting inputs consuming their entire value - #4575

Merged
jkczyz merged 3 commits into
lightningdevkit:mainfrom
wpaulino:funding-contribution-builder-manual-inputs
May 19, 2026
Merged

Support manually selecting inputs consuming their entire value#4575
jkczyz merged 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.

@wpaulinowpaulino added this to the 0.3 milestone Apr 23, 2026
@wpaulinowpaulino self-assigned this Apr 23, 2026
@ldk-reviews-bot

ldk-reviews-bot commented Apr 23, 2026

Copy link
Copy Markdown

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

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

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

Copy link
Copy Markdown
Collaborator

I've completed a thorough review of the entire PR diff and verified the current code state. All previously flagged issues have been checked, and I've searched for any new issues.

Review Summary

Previously Flagged Issues — Status

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

  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. This drops the change output and miscalculates the fee buffer.

  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 code is otherwise correct. Specifically verified:

  • FundingInputs/FundingInputMode enums and their serialization
  • Builder add_input/add_inputs/remove_input methods correctly enforce mode exclusivity
  • splice_in_inputs convenience method correctly appends to prior manual inputs
  • ManuallySelectedInputsInsufficient error correctly does NOT fall through to coin selection in both sync and async builders
  • Fee buffer calculation for manual inputs in compute_feerate_adjustment is correct for positive and negative net_value_without_fee
  • validate_inputs duplicate detection covers the combined set (prior + new) of manually selected inputs
  • validate_contribution_parameters runs before build_from_prior_contribution, catching duplicates and MAX_MONEY violations
  • request_matches_prior correctly compares outpoints for manual inputs and value for coin-selected inputs
  • net_value_without_fee and net_value_at_feerate arithmetic is sound for manual inputs
  • Spliceable balance check in try_build_without_coin_selection correctly gates net-negative manual contributions

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 12e5994 to 412ec3dCompareApril 23, 2026 17:19
@codecov

codecovBot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing linesPatch %Lines
lightning/src/ln/funding.rs96.07%15 Missing and 14 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4575 +/- ##
==========================================
+ Coverage 86.15% 86.58% +0.43% 
==========================================
Files 157 159 +2 Lines 109096 110419 +1323 Branches 109096 110419 +1323 ==========================================
+ Hits 93989 95611 +1622 + Misses 12485 12275 -210 + Partials 2622 2533 -89 
FlagCoverage Δ
fuzzing-fake-hashes6.57% <0.00%> (?)
fuzzing-real-hashes23.17% <3.67%> (?)
tests86.23% <96.14%> (+0.07%)⬆️

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.

@Alkamal01

Copy link
Copy Markdown
Contributor

The root problem seems to be relying on == Some(CoinSelected) for persisted data. For legacy contributions, None effectively behaved like coin selection when inputs were present, so switching to != Some(Manual) (with an inputs non-empty check where needed) would preserve the old behavior.

One additional edge case in request_matches_prior: (Some(CoinSelected), None) currently falls into _ => false, so an RBF with the same value on a legacy prior won't be treated as a match and skips the feerate-adjustment path. Not strictly a correctness issue, but it becomes inconsistent if the other branches get fixed.

So overall, it'd be good to make the handling of None consistent across all three paths.

@TheBlueMattTheBlueMatt left a comment

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.

All LGTM. One question.

Comment threadlightning/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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 412ec3d to 228cae8CompareApril 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.

TheBlueMatt
TheBlueMatt previously approved these changes Apr 29, 2026

@TheBlueMattTheBlueMatt left a comment

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.

thanks

Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated

@elnoshelnosh left a comment

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.

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
ContributorAuthor

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 threadlightning/src/ln/funding.rs Outdated
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch 3 times, most recently from c3da7fe to 6e16493CompareMay 6, 2026 19:27
Comment threadlightning/src/ln/funding.rs Outdated
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase again :/

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 6e16493 to c240f1bCompareMay 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
ContributorAuthor

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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from c240f1b to ed757b2CompareMay 18, 2026 20:13
@TheBlueMatt
TheBlueMatt removed their request for review May 18, 2026 20:37
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from ed757b2 to a9fa610CompareMay 18, 2026 22:04
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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from a9fa610 to dcca939CompareMay 19, 2026 18:00
@wpaulino
wpaulino requested a review from TheBlueMattMay 19, 2026 18:00
@jkczyz
jkczyz merged commit e47a231 into lightningdevkit:mainMay 19, 2026
24 checks passed
@wpaulino
wpaulino deleted the funding-contribution-builder-manual-inputs branch May 19, 2026 20:08
Sign up for freeto 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.

7 participants

@wpaulino@ldk-reviews-bot@ldk-claude-review-bot@Alkamal01@TheBlueMatt@jkczyz@elnosh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support manually selecting inputs consuming their entire value - #4575

Merged
jkczyz merged 3 commits into
lightningdevkit:mainfrom
wpaulino:funding-contribution-builder-manual-inputs
May 19, 2026
Merged

Support manually selecting inputs consuming their entire value#4575
jkczyz merged 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.

@wpaulinowpaulino added this to the 0.3 milestone Apr 23, 2026
@wpaulinowpaulino self-assigned this Apr 23, 2026
@ldk-reviews-bot

ldk-reviews-bot commented Apr 23, 2026

Copy link
Copy Markdown

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

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

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

Copy link
Copy Markdown
Collaborator

I've completed a thorough review of the entire PR diff and verified the current code state. All previously flagged issues have been checked, and I've searched for any new issues.

Review Summary

Previously Flagged Issues — Status

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

  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. This drops the change output and miscalculates the fee buffer.

  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 code is otherwise correct. Specifically verified:

  • FundingInputs/FundingInputMode enums and their serialization
  • Builder add_input/add_inputs/remove_input methods correctly enforce mode exclusivity
  • splice_in_inputs convenience method correctly appends to prior manual inputs
  • ManuallySelectedInputsInsufficient error correctly does NOT fall through to coin selection in both sync and async builders
  • Fee buffer calculation for manual inputs in compute_feerate_adjustment is correct for positive and negative net_value_without_fee
  • validate_inputs duplicate detection covers the combined set (prior + new) of manually selected inputs
  • validate_contribution_parameters runs before build_from_prior_contribution, catching duplicates and MAX_MONEY violations
  • request_matches_prior correctly compares outpoints for manual inputs and value for coin-selected inputs
  • net_value_without_fee and net_value_at_feerate arithmetic is sound for manual inputs
  • Spliceable balance check in try_build_without_coin_selection correctly gates net-negative manual contributions

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 12e5994 to 412ec3dCompareApril 23, 2026 17:19
@codecov

codecovBot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing linesPatch %Lines
lightning/src/ln/funding.rs96.07%15 Missing and 14 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4575 +/- ##
==========================================
+ Coverage 86.15% 86.58% +0.43% 
==========================================
Files 157 159 +2 Lines 109096 110419 +1323 Branches 109096 110419 +1323 ==========================================
+ Hits 93989 95611 +1622 + Misses 12485 12275 -210 + Partials 2622 2533 -89 
FlagCoverage Δ
fuzzing-fake-hashes6.57% <0.00%> (?)
fuzzing-real-hashes23.17% <3.67%> (?)
tests86.23% <96.14%> (+0.07%)⬆️

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.

@Alkamal01

Copy link
Copy Markdown
Contributor

The root problem seems to be relying on == Some(CoinSelected) for persisted data. For legacy contributions, None effectively behaved like coin selection when inputs were present, so switching to != Some(Manual) (with an inputs non-empty check where needed) would preserve the old behavior.

One additional edge case in request_matches_prior: (Some(CoinSelected), None) currently falls into _ => false, so an RBF with the same value on a legacy prior won't be treated as a match and skips the feerate-adjustment path. Not strictly a correctness issue, but it becomes inconsistent if the other branches get fixed.

So overall, it'd be good to make the handling of None consistent across all three paths.

@TheBlueMattTheBlueMatt left a comment

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.

All LGTM. One question.

Comment threadlightning/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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 412ec3d to 228cae8CompareApril 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.

TheBlueMatt
TheBlueMatt previously approved these changes Apr 29, 2026

@TheBlueMattTheBlueMatt left a comment

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.

thanks

Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated

@elnoshelnosh left a comment

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.

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
ContributorAuthor

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 threadlightning/src/ln/funding.rs Outdated
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch 3 times, most recently from c3da7fe to 6e16493CompareMay 6, 2026 19:27
Comment threadlightning/src/ln/funding.rs Outdated
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase again :/

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 6e16493 to c240f1bCompareMay 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
ContributorAuthor

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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from c240f1b to ed757b2CompareMay 18, 2026 20:13
@TheBlueMatt
TheBlueMatt removed their request for review May 18, 2026 20:37
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from ed757b2 to a9fa610CompareMay 18, 2026 22:04
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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from a9fa610 to dcca939CompareMay 19, 2026 18:00
@wpaulino
wpaulino requested a review from TheBlueMattMay 19, 2026 18:00
@jkczyz
jkczyz merged commit e47a231 into lightningdevkit:mainMay 19, 2026
24 checks passed
@wpaulino
wpaulino deleted the funding-contribution-builder-manual-inputs branch May 19, 2026 20:08
Sign up for freeto 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.

7 participants

@wpaulino@ldk-reviews-bot@ldk-claude-review-bot@Alkamal01@TheBlueMatt@jkczyz@elnosh
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Support manually selecting inputs consuming their entire value - #4575

Merged
jkczyz merged 3 commits into
lightningdevkit:mainfrom
wpaulino:funding-contribution-builder-manual-inputs
May 19, 2026
Merged

Support manually selecting inputs consuming their entire value#4575
jkczyz merged 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.

@wpaulinowpaulino added this to the 0.3 milestone Apr 23, 2026
@wpaulinowpaulino self-assigned this Apr 23, 2026
@ldk-reviews-bot

ldk-reviews-bot commented Apr 23, 2026

Copy link
Copy Markdown

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

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

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

Copy link
Copy Markdown
Collaborator

I've completed a thorough review of the entire PR diff and verified the current code state. All previously flagged issues have been checked, and I've searched for any new issues.

Review Summary

Previously Flagged Issues — Status

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

  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. This drops the change output and miscalculates the fee buffer.

  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 code is otherwise correct. Specifically verified:

  • FundingInputs/FundingInputMode enums and their serialization
  • Builder add_input/add_inputs/remove_input methods correctly enforce mode exclusivity
  • splice_in_inputs convenience method correctly appends to prior manual inputs
  • ManuallySelectedInputsInsufficient error correctly does NOT fall through to coin selection in both sync and async builders
  • Fee buffer calculation for manual inputs in compute_feerate_adjustment is correct for positive and negative net_value_without_fee
  • validate_inputs duplicate detection covers the combined set (prior + new) of manually selected inputs
  • validate_contribution_parameters runs before build_from_prior_contribution, catching duplicates and MAX_MONEY violations
  • request_matches_prior correctly compares outpoints for manual inputs and value for coin-selected inputs
  • net_value_without_fee and net_value_at_feerate arithmetic is sound for manual inputs
  • Spliceable balance check in try_build_without_coin_selection correctly gates net-negative manual contributions

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 12e5994 to 412ec3dCompareApril 23, 2026 17:19
@codecov

codecovBot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing linesPatch %Lines
lightning/src/ln/funding.rs96.07%15 Missing and 14 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4575 +/- ##
==========================================
+ Coverage 86.15% 86.58% +0.43% 
==========================================
Files 157 159 +2 Lines 109096 110419 +1323 Branches 109096 110419 +1323 ==========================================
+ Hits 93989 95611 +1622 + Misses 12485 12275 -210 + Partials 2622 2533 -89 
FlagCoverage Δ
fuzzing-fake-hashes6.57% <0.00%> (?)
fuzzing-real-hashes23.17% <3.67%> (?)
tests86.23% <96.14%> (+0.07%)⬆️

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.

@Alkamal01

Copy link
Copy Markdown
Contributor

The root problem seems to be relying on == Some(CoinSelected) for persisted data. For legacy contributions, None effectively behaved like coin selection when inputs were present, so switching to != Some(Manual) (with an inputs non-empty check where needed) would preserve the old behavior.

One additional edge case in request_matches_prior: (Some(CoinSelected), None) currently falls into _ => false, so an RBF with the same value on a legacy prior won't be treated as a match and skips the feerate-adjustment path. Not strictly a correctness issue, but it becomes inconsistent if the other branches get fixed.

So overall, it'd be good to make the handling of None consistent across all three paths.

@TheBlueMattTheBlueMatt left a comment

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.

All LGTM. One question.

Comment threadlightning/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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 412ec3d to 228cae8CompareApril 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.

TheBlueMatt
TheBlueMatt previously approved these changes Apr 29, 2026

@TheBlueMattTheBlueMatt left a comment

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.

thanks

Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated
Comment threadlightning/src/ln/funding.rs Outdated

@elnoshelnosh left a comment

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.

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
ContributorAuthor

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 threadlightning/src/ln/funding.rs Outdated
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch 3 times, most recently from c3da7fe to 6e16493CompareMay 6, 2026 19:27
Comment threadlightning/src/ln/funding.rs Outdated
@TheBlueMatt

Copy link
Copy Markdown
Collaborator

Needs rebase again :/

@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from 6e16493 to c240f1bCompareMay 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
ContributorAuthor

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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from c240f1b to ed757b2CompareMay 18, 2026 20:13
@TheBlueMatt
TheBlueMatt removed their request for review May 18, 2026 20:37
@wpaulino
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from ed757b2 to a9fa610CompareMay 18, 2026 22:04
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
wpaulinoforce-pushed the funding-contribution-builder-manual-inputs branch from a9fa610 to dcca939CompareMay 19, 2026 18:00
@wpaulino
wpaulino requested a review from TheBlueMattMay 19, 2026 18:00
@jkczyz
jkczyz merged commit e47a231 into lightningdevkit:mainMay 19, 2026
24 checks passed
@wpaulino
wpaulino deleted the funding-contribution-builder-manual-inputs branch May 19, 2026 20:08
Sign up for freeto 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.

7 participants

@wpaulino@ldk-reviews-bot@ldk-claude-review-bot@Alkamal01@TheBlueMatt@jkczyz@elnosh