Skip to content

Introduce FundingContributionBuilder API#4516

Open
wpaulino wants to merge 6 commits intolightningdevkit:mainfrom
wpaulino:funding-contribution-builder
Open

Introduce FundingContributionBuilder API#4516
wpaulino wants to merge 6 commits intolightningdevkit:mainfrom
wpaulino:funding-contribution-builder

Conversation

@wpaulino
Copy link
Copy Markdown
Contributor

@wpaulino wpaulino commented Mar 27, 2026

This PR refactors splice contribution construction around a new FundingBuilder API and tightens the semantics of how splice value is funded. It replaces the older FundingTemplate contribution helpers with a builder flow that can reuse, amend, or replace prior contributions for fresh splices and RBF attempts.

@wpaulino wpaulino added this to the 0.3 milestone Mar 27, 2026
@wpaulino wpaulino requested review from TheBlueMatt and jkczyz March 27, 2026 00:05
@wpaulino wpaulino self-assigned this Mar 27, 2026
@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Mar 27, 2026

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.07%. Comparing base (5704e8e) to head (fa10899).
⚠️ Report is 52 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4516      +/-   ##
==========================================
+ Coverage   86.99%   87.07%   +0.07%     
==========================================
  Files         163      161       -2     
  Lines      108635   109248     +613     
  Branches   108635   109248     +613     
==========================================
+ Hits        94511    95130     +619     
+ Misses      11647    11636      -11     
- Partials     2477     2482       +5     
Flag Coverage Δ
fuzzing 39.60% <40.00%> (-0.70%) ⬇️
tests 86.17% <80.00%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

basically lgtm

Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
@wpaulino wpaulino force-pushed the funding-contribution-builder branch from 0968836 to 2da45ef Compare March 30, 2026 18:54
@wpaulino wpaulino marked this pull request as ready for review March 30, 2026 18:54
@wpaulino wpaulino requested review from TheBlueMatt and jkczyz March 30, 2026 18:54
@wpaulino
Copy link
Copy Markdown
Contributor Author

Leaving the explicit input support for a follow-up as this PR is large enough already.

Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
.await
.map_err(|_| FundingContributionError::CoinSelectionFailed)?;

return Ok(FundingContribution::new(
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: unnecessary return in final expression position. Same on line 740 for the sync variant.

Suggested change
return Ok(FundingContribution::new(
Ok(FundingContribution::new(

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

ldk-claude-review-bot commented Mar 30, 2026

Review Summary — PR #4516: Introduce FundingContributionBuilder API (Final Pass)

No new inline comments this pass. The prior review passes were comprehensive and covered all significant issues found in this diff.

Previously flagged issues (all still present)

Critical:

  1. funding.rs:570-578 — Breaking TLV tag renumbering for persisted FundingContribution. Upgrading nodes with pending splices will fail to deserialize channel data.
  2. funding.rs:348splice_in/splice_in_sync additive semantics doubles value_added during RBF when a prior contribution is present.

Correctness:
3. funding.rs:647 — Wrong fee estimate used in sufficiency check after dropping change output in amend_without_coin_selection. Uses WITH-change fee instead of no-change fee, making the check too conservative.
4. funding.rs:700-714 — No sufficiency check in the no-change fallthrough of amend_without_coin_selection. Can silently return a contribution with less value_added() than requested.
5. funding.rs:636-644amend_without_coin_selection always fails for amended splice-out priors (no inputs → fee check fails).
6. funding.rs:1103-1104build_from_prior_contribution failure blocks fresh splice-out fallthrough in try_build_without_coin_selection.
7. funding.rs:1076-1080FundingInputs::None silently discards prior wallet inputs when value_added == 0.
8. funding.rs:431rbf_prior_contribution now requires a prior contribution, removing old fee-bump-only capability.
9. funding.rs:1172validate_contribution_parameters blocks reuse of fee-bump-only prior contributions.
10. channel.rs:12332-12341splice_channel now fails with APIError where it previously silently degraded to re-coin-selection.

Diagnostic / DX:
11. funding.rs:1067-1073 — All feerate adjustment failures mapped to MissingCoinSelectionSource, losing diagnostic information.
12. funding.rs:1292-1300saturating_add/saturating_sub for bitcoin amounts can silently truncate/zero.

Code quality:
13. funding.rs:1365,1465add_value/remove_value duplicated across async/sync builder impls.
14. funding.rs:1420,1519 — Unnecessary return keyword in final expression.

Cross-cutting concerns

  • The MissingCoinSelectionSource error remains overloaded: it covers "need wallet inputs", "feerate can't be accommodated", and "prior amendment failed." The wallet-backed builder silently discards prior inputs on any of these by falling through to fresh coin selection.
  • The design shift where wallet-backed contributions fund withdrawal outputs from wallet inputs (rather than channel balance) is intentional per the docs, but represents a significant semantic change from the old splice_in_and_out behavior.

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

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

I didn't look too deeply at the tests but basically LGTM.

Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs
@wpaulino wpaulino force-pushed the funding-contribution-builder branch from 2da45ef to 19b230b Compare March 31, 2026 20:00
@wpaulino
Copy link
Copy Markdown
Contributor Author

Had to rebase due to a small import conflict.

@wpaulino wpaulino requested review from TheBlueMatt and jkczyz March 31, 2026 20:29
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
TheBlueMatt
TheBlueMatt previously approved these changes Apr 1, 2026
Copy link
Copy Markdown
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

needs rebase again tho

Copy link
Copy Markdown
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Code looks good, but I think merging some impl blocks will help the diff.

Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
@TheBlueMatt
Copy link
Copy Markdown
Collaborator

linter, fuzz-sanity, and check_commits are failing.

Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs
It does not require coin selection, so the wallet argument is not
necessary.
@wpaulino wpaulino force-pushed the funding-contribution-builder branch from c11af28 to de4ccfb Compare April 8, 2026 17:51
}
}

if self.value_added == Amount::ZERO && self.outputs.is_empty() {
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.

Correctness concern: validate_contribution_parameters blocks reuse of fee-bump-only prior contributions

When a prior contribution has value_added() == 0 and empty outputs (a fee-bump-only contribution that only selected wallet inputs to pay fees), initializing a builder from it yields self.value_added = 0 and self.outputs = []. The validate_contribution_parameters check here then rejects the request with InvalidSpliceValue before build_from_prior_contribution gets a chance to reuse the prior.

This can happen when coin selection returned inputs where inputs - fee - change = 0 exactly. The old rbf code handled this case (via force_coin_selection = true); the new builder path rejects it at validation.

This may be intentional given the removal of the fee-bump-only flow, but it creates an asymmetry: a contribution that was successfully built and stored as a prior can fail to be reused with an identical request, which would be surprising to callers using rbf_prior_contribution or with_prior_contribution.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fee-bump-only contributions would have the prior contribution's value_added and outputs so this is nonsense.

Comment thread lightning/src/ln/funding.rs Outdated
/// Reuses the contribution's existing inputs while targeting at least `value_added` added to
/// the channel after fees. If dropping the change output leaves surplus value, it remains in
/// the channel contribution.
CoinSelected { value_added: Amount },
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: FundingInputs is a single-variant enum, adding indirection without benefit

FundingInputs has only one variant (CoinSelected), yet with_inputs_and_outputs and amend_without_coin_selection match on it. The target_value_added extracted from the match is Option<Amount> but is always Some(...) in practice. This makes the control flow harder to follow without adding extensibility — if new variants are expected soon, a comment noting that would help; otherwise this could be simplified to a plain struct or direct parameter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We plan to add another variant in a follow-up for manual input support.

let new_fee = new_estimated_fee
.to_signed()
.expect("total input amount cannot exceed Amount::MAX_MONEY");
let new_change = new_change
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.

Does this need to consider dust?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

new_change is returned from compute_feerate_adjustment so it should have already been considered there.

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.

I don't think that's quite sufficient? This is also called from net_value_for_acceptor_at_feerate in channel.rs's resolve_queued_contribution. The resulting our_funding_contribution is sent to our peer. Maybe its fine to be a bit off there but it does leave a good chunk of code all the way into channel.rs kinda confusing.

Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
target_feerate,
);
let net_value_without_fee = self.net_value_without_fee();
if net_value_without_fee.is_positive() {
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 weird to split on whether the net contribution was negative or not - if I have a splice where I added 1 BTC+1 sat to the channel but sent 1BTC out without change output, I'm probably more than happy to RBF by using the value of some of my inputs to change the channel's balance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point, but then we're not really abiding by the value_added they provided when we initially coin-selected the inputs. With manual input selection, this will work out of the box since there's no explicit value_added there, we're just always adding whatever is left after fees. Should we treat the coin-selected no-change case the same way?

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.

With manual input selection, this will work out of the box since there's no explicit value_added there, we're just always adding whatever is left after fees.

Hmm? There's no explicit value_added anywhere now? So I suppose you could argue this code is correct for the coin selection case but wrong for the all-input case? Do we need to track the input selection style here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's still explicit when splicing in funds via FundingBuilder::add_value. The manual input selection does have a special case in this path to allow spending up to half of the UTXO value (as long as the feerate is still within the max of course).

Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs
Comment thread lightning/src/ln/splicing_tests.rs
@wpaulino wpaulino force-pushed the funding-contribution-builder branch from de4ccfb to 97eb0fc Compare April 9, 2026 22:17
Comment thread lightning/src/ln/channel.rs
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs
@wpaulino wpaulino force-pushed the funding-contribution-builder branch from 97eb0fc to fefea5c Compare April 10, 2026 00:05
Comment thread lightning/src/ln/funding.rs
Comment thread lightning/src/ln/funding.rs
Comment thread lightning/src/ln/funding.rs
Comment thread lightning/src/ln/funding.rs
Comment thread lightning/src/ln/funding.rs Outdated
Comment thread lightning/src/ln/funding.rs
The `holder_balance` is computed by
`FundedChannel::get_holder_counterparty_balances_floor_incl_fee`, which
may unexpectedly fail due to the balance either being too high or too
low. These cases are highly unlikely to happen given we have validation
to ensure we never enter such a state to begin with. If they were to
happen, something has gone wrong with the channel and it doesn't make
sense to allow splicing anyway. Therefore, we opt to make
`PriorContribution::holder_balance` non-optional and return an error
that the channel cannot be spliced at the moment.
This commit removes `FundingContribution::value_added` as tracking it is
unnecessary -- it can just be derived from the total amount in minus
total amount out minus fees.
When a user requests to add value via coin-selected inputs, we should
strive to fulfill their request. Allowing them to remove value from the
channel is undesired as it goes against their request. While we still
allow adding outputs to enabled mixed contributions, their funds must
now always come from the set of coin-selected inputs, and must never
draw from the channel balance resulting in a smaller added value.
@wpaulino wpaulino force-pushed the funding-contribution-builder branch from fefea5c to 33f510c Compare April 15, 2026 02:51
Copy link
Copy Markdown
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

PR description could probably be completed now.

Comment thread lightning/src/ln/channelmanager.rs
This lets callers easily amend a prior contribution in place and only
re-run coin selection when the new request cannot be satisfied with the
existing inputs.
This results in a slight change of behavior: now these methods reuse and
amend the prior contribution, as opposed to always starting from a fresh
contribution, which would be the desired expected behavior by users.
@wpaulino wpaulino force-pushed the funding-contribution-builder branch from 33f510c to fa10899 Compare April 16, 2026 16:49
@wpaulino wpaulino requested a review from TheBlueMatt April 16, 2026 16:50
Copy link
Copy Markdown
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Mostly LGTM.

Comment on lines +6642 to +6643
/// - **input-backed contributions**: when wallet inputs are selected, those inputs fund both
/// the requested value added to the channel and any explicit withdrawal outputs
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.

Hope this isn't too pedantic, bit using "fund" here may be confusing for the withdrawal case. I think of "fund" more as funding a channel rather than how the contribution fees are paid for. It seems a little overloaded here when talking about withdrawal outputs.

Perhaps we can re-word similar to the second bullet where paying for fees and withdrawal outputs is mentioned?

Comment thread lightning/src/ln/funding.rs Outdated
Comment on lines +470 to +471
/// them as needed. The withdrawal `outputs` are funded by the selected wallet inputs and do
/// not reduce the requested `value_added` to the channel.
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.

Similarly here with "funded".

/// The outputs to include in the funding transaction.
///
/// When no wallet inputs are contributed, these outputs are paid from the channel balance.
/// Otherwise, they are funded by the contributed inputs.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Likewise.

Comment on lines +1116 to +1123
let total_output_value = self
.outputs
.iter()
.chain(self.change_output.iter())
.map(|txout| txout.value)
.sum::<Amount>()
.to_signed()
.expect("value_removed is validated to not exceed Amount::MAX_MONEY");

let contribution_amount = value_added - value_removed;
contribution_amount
.checked_sub(unpaid_fees)
.expect("total_output_value is validated to not exceed Amount::MAX_MONEY");
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 see that the total input value is checked in validate_inputs, but I don't see where we validate the outputs.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 1st Reminder

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants