Introduce FundingContributionBuilder API#4516
Introduce FundingContributionBuilder API#4516wpaulino wants to merge 5 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.08% +0.08%
==========================================
Files 163 163
Lines 108635 108936 +301
Branches 108635 108936 +301
==========================================
+ Hits 94511 94869 +358
+ Misses 11647 11587 -60
- Partials 2477 2480 +3
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. |
lightning/src/ln/funding.rs
Outdated
| .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 APICorrection from prior reviewMy prior comment #2 (" New issues (this pass)
Previously flagged issues (still present)
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.
19b230b to
c11af28
Compare
| impl_writeable_tlv_based!(FundingContribution, { | ||
| (1, value_added, required), | ||
| (3, estimated_fee, required), | ||
| (5, inputs, optional_vec), | ||
| (7, outputs, optional_vec), | ||
| (9, change_output, option), | ||
| (11, feerate, required), | ||
| (13, max_feerate, required), | ||
| (15, is_splice, required), | ||
| (1, estimated_fee, required), | ||
| (3, inputs, optional_vec), | ||
| (5, outputs, optional_vec), | ||
| (7, change_output, option), | ||
| (9, feerate, required), | ||
| (11, max_feerate, required), | ||
| (13, is_splice, required), | ||
| }); |
There was a problem hiding this comment.
Breaking serialization: TLV tags renumbered for a persisted type
FundingContribution is serialized as part of PendingFunding.contributions (channel.rs:2926), which is persisted in FundedChannel (channel.rs:15814, tag 64). Renumbering the TLV tags means any previously serialized FundingContribution will fail to deserialize:
- Old tag 1 =
value_added(Amount), new tag 1 =estimated_fee(Amount) — same type, wrong semantics - Old tag 3 =
estimated_fee(Amount), new tag 3 =inputs(Vec) — different types, will fail
If a node with a pending splice upgrades, it will be unable to read its channel data.
Options:
- Keep the old tag numbering and make
value_addedoptional (defaulting to derived) for backward compatibility - Use new, higher tag numbers (e.g., start at 17+) so old fields are simply ignored by the new code
- If splicing serialization is considered unstable/unreleased, document this as a known breaking change
There was a problem hiding this comment.
This serialization has not been included in a release so it should be safe to break.
|
linter, fuzz-sanity, and check_commits are failing. |
| holder_balance.ok_or(FeeRateAdjustmentError::FeeBufferInsufficient { | ||
| source: "channel balance", | ||
| available: Amount::ZERO, | ||
| required: target_fee, | ||
| })?; |
There was a problem hiding this comment.
Could you add a test for this error path?
There was a problem hiding this comment.
Hmm... this comment was made on the second commit, but it shows up in a weird place on the third commit.
There was a problem hiding this comment.
Added a unit test since a functional test would be pretty hard to discern. It's also quite the edge case, you shouldn't have a net-negative contribution in the first place if you didn't have a balance, so either the contribution is stale/invalid or the balance is.
| /// The value that will be added to the channel after fees. See [`Self::net_value`] for the net | ||
| /// value contribution to the channel. | ||
| fn value_added(&self) -> Amount { | ||
| let total_input_value = self.inputs.iter().map(|i| i.utxo.output.value).sum::<Amount>(); |
There was a problem hiding this comment.
Should we check if this overflows?
There was a problem hiding this comment.
We check it when we select the inputs.
There was a problem hiding this comment.
Where is that? I don't see it checked after calling select_confirmed_utxo. Maybe we shouldn't care if the user's CoinSelectionSource is broken?
There was a problem hiding this comment.
Ah somehow this part didn't make it into this commit and was in my follow-up for manual input support. Should be good now.
| return Err(FeeRateAdjustmentError::FeeBufferInsufficient { | ||
| source: "channel balance - withdrawal outputs", | ||
| available: holder_balance.checked_sub(value_removed).unwrap_or(Amount::ZERO), | ||
| source: "channel balance", |
There was a problem hiding this comment.
Isn't the previous wording correct?
There was a problem hiding this comment.
Only when there aren't any inputs because there may be some that are partially contributing to the net-negative contribution.
lightning/src/ln/funding.rs
Outdated
| .to_signed() | ||
| .expect("value_added is validated to not exceed Amount::MAX_MONEY"); | ||
| let value_removed = self | ||
| .expect("total_input_value is validated to not exceed Amount::MAX_MONEY"); |
There was a problem hiding this comment.
I believe the validation was removed?
There was a problem hiding this comment.
Everything should be in validate_contribution_parameters, inputs are validated separately when we do coin selection.
There was a problem hiding this comment.
I meant can't the sum overflow? We avoided the problem earlier with the explicit value_added. But maybe it was already a problem elsewhere?
lightning/src/ln/funding.rs
Outdated
| let estimated_fee = self | ||
| .estimated_fee | ||
| .to_signed() | ||
| .expect("total_input_value is validated to not exceed Amount::MAX_MONEY"); |
It does not require coin selection, so the wallet argument is not necessary.
This simplifies the callsites such that we no longer need to check for presense of the variable, and will also become useful when we want to adjust prior contributions regardless of whether we had a non-zero holder balance.
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. Doing so also highlighted that there was an incorrect assumption in how feerates are computed/adjusted. Ultimately, we don't care whether a contribution has inputs or not. Instead, we look at whether it's contributing a positive or negative amount to the channel, and enforce different constraints for each.
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.
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.
| /// Returns `Err` if the contribution cannot accommodate the target feerate. | ||
| fn compute_feerate_adjustment( | ||
| &self, target_feerate: FeeRate, holder_balance: Amount, is_initiator: bool, | ||
| &self, target_feerate: FeeRate, holder_balance: Option<Amount>, is_initiator: bool, |
There was a problem hiding this comment.
Huh? We always have a holder_balance, there's just cases where we failed to compute one but that is a "this mostly shouldn't happen" case and should just return an error anyway because our channel is kinda borked. We shouldn't convert everything to an option.
There was a problem hiding this comment.
Happy to drop the commit, but then we probably should not allow splice_channel to return a FundingTemplate when we can't compute the balance so that PriorContribution::holder_balance is not an option.
| 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?
Looking for initial feedback on the design, still needs to be cleaned up a good bit.