Skip to content

Introduce FundingContributionBuilder API#4516

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

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

Conversation

@wpaulino
Copy link
Copy Markdown
Contributor

Looking for initial feedback on the design, still needs to be cleaned up a good bit.

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

❌ Patch coverage is 90.15152% with 78 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.26%. Comparing base (688544d) to head (0968836).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/funding.rs 90.15% 65 Missing and 13 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4516      +/-   ##
==========================================
+ Coverage   86.14%   86.26%   +0.11%     
==========================================
  Files         160      160              
  Lines      108046   109045     +999     
  Branches   108046   109045     +999     
==========================================
+ Hits        93080    94071     +991     
+ Misses      12346    12332      -14     
- Partials     2620     2642      +22     
Flag Coverage Δ
tests 86.26% <90.15%> (+0.11%) ⬆️

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

return Ok(contribution);
}

Err(FundingContributionError::MissingCoinSelectionSource)
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.

Could we have the compiler enforce this by requiring a call to with_coin_selection_source yielding a type with compatible methods? I'd include the convenience methods like splice_in assuming we reuse FundingTemplate instead of adding another type.

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.

Or maybe we just drop the convenience methods?

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.

Discussed offline: we enforce this at compile-time already for add/remove_value, but we cannot do it elsewhere as it's possible that an RBF builder without an attached wallet may require coin selection if the new feerate is too large for the prior contribution's input set.

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

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

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

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

Review Summary

New Issues Found (this pass)

  1. lightning/src/ln/funding.rs:418 — When request matches prior exactly but holder_balance is None, the early return skips amend_without_coin_selection, which could succeed for input-backed contributions without needing holder_balance. This forces unnecessary coin selection.

  2. lightning/src/ln/funding.rs:508InvalidSpliceValue error message is unhelpful when a user creates a builder from a fresh template and forgets to call add_value or add_output.

Previously Flagged Issues (still present)

  1. lightning/src/ln/funding.rs:836-841splice_in/splice_in_sync additive semantics double value_added during RBF when the template carries a prior contribution. The do_initiate_rbf_splice_in test helper (splicing_tests.rs:252) calls splice_in_sync(value_added, ...) on RBF templates, resulting in prior.value_added + value_added.

  2. lightning/src/ln/funding.rs:618-627saturating_add/saturating_sub for bitcoin amounts silently produces nonsensical values on overflow/underflow. checked_add/checked_sub with error propagation would be safer.

  3. lightning/src/ln/funding.rs:661-777add_value/remove_value/add_output/remove_outputs methods duplicated across AsyncFundingBuilder and SyncFundingBuilder impl blocks with identical docs and implementations. Could be deduplicated.

  4. lightning/src/ln/funding.rs:727,804 — Unnecessary return keyword in final expression position.

  5. lightning/src/ln/funding.rs:908-920rbf_prior_contribution now requires a prior contribution, removing the old fee-bump-only capability without directing users to the builder alternative.

Inline Comments Posted

  • lightning/src/ln/funding.rs:418 — Missed amend_without_coin_selection opportunity for input-backed prior contributions with holder_balance: None
  • lightning/src/ln/funding.rs:508 — Unhelpful error message for empty builder requests

let FundingTemplate { shared_input, min_rbf_feerate, .. } = self;
build_funding_contribution!(value_added, vec![], shared_input, min_rbf_feerate, min_feerate, max_feerate, false, wallet, await)

Err(FundingContributionError::MissingCoinSelectionSource)
Copy link
Copy Markdown
Contributor

@jkczyz jkczyz Mar 30, 2026

Choose a reason for hiding this comment

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

Can we get this when it should be InvalidSpliceValue? For example, with zero added value and no outputs?

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.

That should have already been checked in validate_contribution_parameters above.

Comment on lines +813 to 815
for output in outputs {
builder = builder.add_output(output);
}
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.

Could we make a private add_outputs that simply moves the input Vec?


/// Creates a [`FundingContribution`] for both adding and removing funds from a channel using
/// `wallet` to perform coin selection.
impl FundingTemplate {
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.

Hmm... I think the diff may be saner if these methods were kept in the original impl block.

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.

The methods haven't moved, rather the diffing logic isn't happy with FundingBuilder being added above FundingTemplate.

Comment on lines +335 to +343
struct FundingBuilderContents {
shared_input: Option<Input>,
min_rbf_feerate: Option<FeeRate>,
prior_contribution: Option<PriorContribution>,
value_added: Amount,
outputs: Vec<TxOut>,
feerate: FeeRate,
max_feerate: FeeRate,
}
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.

Is this struct necessary? Seems having all this on FundingBuilder could simplify things. Plus, you may be able to simplify error handling if value_added was only for builder types having a CoinSelectionSource. Then value_added could be a field on the parameter type.

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.

Removed FundingBuilderContents. value_added will need to remain here as we plan to add explicit input support.

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.

}

#[doc(hidden)]
pub mod builder_states {
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.

Rather than using a generic, it seems like the code would be basically just as clean, but bindings would be happier, if we kept the internal struct and just exposed three separate builder structs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think what I pushed should work, let me know otherwise.

It does not require coin selection, so the wallet argument is not
necessary.
This commit introduces a `FundingBuilder`, following the builder
pattern, to iteratively construct a `FundingContribution`. It supports
adding/removing value (which may trigger a coin selection attempt),
adding/remove outputs, and amending prior contributions (which was
previously not possible).

A `FundingBuilder` may be obtained in two ways: via
`FundingTemplate::contribution_builder` (which resumes from the prior
contribution to amend if available) or
`FundingTemplate::without_prior_contribution_builder` to start from
scratch and fully replace the prior contribution.

When `FundingBuilder::build` is called, we attempt to build the
contribution iteratively such that coin selection is our last resort.

The splice-related convenience methods on `FundingTemplate` now map to
the `FundingBuilder` under the hood.
@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
return Some(adjusted);
}
}
return None;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bug: When the request matches the prior exactly but holder_balance is None, this early return at line 418 skips amend_without_coin_selection entirely. For input-backed contributions, amend_without_coin_selection does not need holder_balance (lines 1157-1211 don't use it), so it could succeed. This means rbf_prior_contribution_sync(None, max_feerate, wallet) unnecessarily re-runs coin selection for input-backed prior contributions when holder_balance is None — potentially yielding a different input set than the prior had.

This is covered by the test at line 2890 (test_rbf_sync_holder_balance_none_falls_through_to_coin_selection), which expects coin selection, so this appears intentional. But worth noting that the amend_without_coin_selection path could handle this case without requiring a wallet.

}

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

The validation here catches value_added == 0 && outputs.is_empty(), which blocks the RBF "same request" path for splice-out prior contributions that had value_added == 0 and outputs == []. While this shouldn't arise in normal use (a prior contribution with both zero is meaningless), there's a subtle interaction: if someone calls with_prior_contribution(...) on a fresh template (no prior) without calling any mutation methods, they'd hit this error. The error message "Invalid splice value" doesn't hint that they need to call add_value or add_output first. Consider a more descriptive variant like EmptyContribution or adding a hint to the InvalidSpliceValue display message.

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