Skip to content

FundingBuilder adjusts a prior contribution against its old max_feerate rather than the caller's #4762

Closed

Description

@jkczyz

When a FundingBuilder seeded from a template's prior contribution is built at a higher feerate (an RBF), it adjusts the prior contribution in place and only then stores the caller's max_feerate on the result. The adjustment itself still runs against the prior contribution's max_feerate, so a bump above the original ceiling fails or falls back even though the caller supplied a new ceiling at or above the requested feerate.

Observed at d7a9806.

Where

  • FundingBuilderInner::build_from_prior_contribution (lightning/src/ln/funding.rs, around line 1475): contribution.for_initiator_at_feerate(self.feerate, ..) runs first; adjusted.max_feerate = self.max_feerate is applied in the map afterwards.
  • FundingContribution::amend_without_coin_selection (around line 1074): new_contribution_at_current_feerate.at_feerate(target_feerate, .., true) runs first; max_feerate is overwritten on the next line.
  • Both go through compute_feerate_adjustment (around line 1173), which returns FeeRateAdjustmentError::FeeRateTooHigh when target_feerate > self.max_feerate and the fee at the target exceeds the contribution's current estimate. For an initiator adjusting the same inputs and outputs to a higher feerate, the fee always exceeds the current estimate, so the error is certain once the target passes the old maximum.

max_feerate is documented as the highest feerate we tolerate if we end up as the acceptor (FundingTemplate::with_prior_contribution docs, FundingContribution::max_feerate), and the comment on the check in compute_feerate_adjustment describes the acceptor case. The channel's own call to for_initiator_at_feerate in maybe_adjust_for_rbf is an adjustment the user did not request, where that ceiling makes sense. The builder's RBF path is different: the caller asked for feerate and supplied max_feerate >= feerate, which validate_contribution_parameters has already checked before the adjustment runs.

Effect by contribution type

  • Manually selected inputs: build returns ManuallySelectedInputsInsufficient (there is no fallback for this mode), although the inputs could pay the higher fee by adding less to the channel, which is exactly what the in-place adjustment would do. In ldk-node, splice_in_with_all now builds such a contribution, and bump_channel_funding_fee at a target above 1.5x the original estimate failed with this error (Build splice_in_with_all from manually selected inputs ldk-node#1176). ldk-node works around it by rebuilding via without_prior_contribution (lightningdevkit/ldk-node@c875410), which loses the record of inherited inputs.
  • Coin-selected inputs: the error maps to MissingCoinSelectionSource and the async/sync builders fall back to coin selection, re-selecting inputs and creating a new change output even when the existing change could absorb the fee.
  • Splice-out with no inputs: the same fallback "changes the funding source: wallet inputs are selected to cover the outputs and fees instead of deducting them from the channel balance" (rbf_prior_contribution docs). A bump above the original ceiling silently starts spending the wallet.

Reproduction

With ldk-node at the PR above, before its workaround commit: splice_in_with_all at 1000 sat/kwu, then bump_channel_funding_fee at 2000 sat/kwu returns ChannelSplicingFailed, and the node log shows "Failed to RBF channel: The request cannot be satisfied using the manually selected inputs". A unit test in funding.rs would be: build a manual-input contribution with max_feerate 1.5x its feerate, put it in a template as the prior with a min_rbf_feerate, then with_prior_contribution(2x, 3x).build(); it returns ManuallySelectedInputsInsufficient instead of a contribution at the higher feerate.

Proposed fix

Set the caller's max_feerate on the prior contribution (or on new_contribution_at_current_feerate) before calling the initiator adjustment, in both places. validate_contribution_parameters guarantees feerate <= max_feerate, so the ceiling check then passes for the initiator path while the fee-buffer and spliceable-balance checks still apply. Alternatively, skip the ceiling check when is_initiator is set and the call comes from the builder, leaving maybe_adjust_for_rbf's behavior unchanged.

Activity

  1. jkczyz commented on Oct 19, 2026

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions