Introduce FundingContributionBuilder API#4516
Introduce FundingContributionBuilder API#4516wpaulino wants to merge 6 commits intolightningdevkit:mainfrom
Conversation
|
👋 Thanks for assigning @TheBlueMatt as a reviewer! |
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
0968836 to
2da45ef
Compare
|
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( |
There was a problem hiding this comment.
Nit: unnecessary return in final expression position. Same on line 740 for the sync variant.
| return Ok(FundingContribution::new( | |
| Ok(FundingContribution::new( |
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:
Correctness: Diagnostic / DX: Code quality: Cross-cutting concerns
|
TheBlueMatt
left a comment
There was a problem hiding this comment.
I didn't look too deeply at the tests but basically LGTM.
2da45ef to
19b230b
Compare
|
Had to rebase due to a small import conflict. |
TheBlueMatt
left a comment
There was a problem hiding this comment.
needs rebase again tho
jkczyz
left a comment
There was a problem hiding this comment.
Code looks good, but I think merging some impl blocks will help the diff.
|
linter, fuzz-sanity, and check_commits are failing. |
It does not require coin selection, so the wallet argument is not necessary.
c11af28 to
de4ccfb
Compare
| } | ||
| } | ||
|
|
||
| if self.value_added == Amount::ZERO && self.outputs.is_empty() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fee-bump-only contributions would have the prior contribution's value_added and outputs so this is nonsense.
| /// 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 }, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Does this need to consider dust?
There was a problem hiding this comment.
new_change is returned from compute_feerate_adjustment so it should have already been considered there.
There was a problem hiding this comment.
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.
| target_feerate, | ||
| ); | ||
| let net_value_without_fee = self.net_value_without_fee(); | ||
| if net_value_without_fee.is_positive() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
de4ccfb to
97eb0fc
Compare
97eb0fc to
fefea5c
Compare
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.
fefea5c to
33f510c
Compare
jkczyz
left a comment
There was a problem hiding this comment.
PR description could probably be completed now.
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.
33f510c to
fa10899
Compare
| /// - **input-backed contributions**: when wallet inputs are selected, those inputs fund both | ||
| /// the requested value added to the channel and any explicit withdrawal outputs |
There was a problem hiding this comment.
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?
| /// them as needed. The withdrawal `outputs` are funded by the selected wallet inputs and do | ||
| /// not reduce the requested `value_added` to the channel. |
There was a problem hiding this comment.
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. |
| 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"); |
There was a problem hiding this comment.
I see that the total input value is checked in validate_inputs, but I don't see where we validate the outputs.
This PR refactors splice contribution construction around a new
FundingBuilderAPI and tightens the semantics of how splice value is funded. It replaces the olderFundingTemplatecontribution helpers with a builder flow that can reuse, amend, or replace prior contributions for fresh splices and RBF attempts.