Skip to content

Build splice_in_with_all from manually selected inputs - #1176

Open
jkczyz wants to merge 2 commits into
lightningdevkit:mainfrom
jkczyz:2026-10-splice-in-with-all-manual-inputs
Open

jkczyz wants to merge 2 commits into
lightningdevkit:mainfrom
jkczyz:2026-10-splice-in-with-all-manual-inputs

Conversation

@jkczyz

@jkczyz jkczyz commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #1168.

Node::splice_in_with_all estimated the maximum amount with a wallet drain and then ran coin selection for that exact amount. The resulting contribution had at most the anchor reserve as a change output, so bump_channel_funding_fee had nothing else to pay a higher fee from. With anchor channels enabled, the bump shrank the change output and spent the reserve on the splice fee. Without a reserve there was no change output at all, so the bump failed with ChannelSplicingFailed. The drain estimate also ran at the funding feerate estimate while the contribution could be built at a pending splice's higher RBF minimum.

This PR instead hands LDK the wallet's confirmed UTXOs as manually selected inputs (FundingBuilder::add_inputs). They are consumed in full with no change output, the amount added to the channel is whatever remains after fees at the template's feerate, and a later fee bump is paid by adding a little less to the channel. When an anchor reserve is required, it is kept as an explicit output back to one of the wallet's own addresses, which stays fixed across bumps.

The reserve address is marked used in BDK as soon as it is handed out, since the splice transaction only reaches the wallet once negotiation completes and until then BDK would offer the same address as change elsewhere. The mark is dropped again if the contribution cannot be built, or via DiscardFunding if LDK rejects it.

Insufficient-funds checks stay in front of LDK, which would otherwise build a near-empty contribution. The drain-based splice estimate is removed along with the shared-input handling in the drain helpers that only it used.

The first commit extracts the low -incrementalrelayfee bitcoind setup used by the splice RBF tests into a shared helper, since the new tests need it too.

Tests

  • splice_in_with_all_bump_fee_retains_anchor_reserve: splice in with all funds with an anchor reserve, bump the fee, and check the bump succeeds, the amount added to the channel drops by exactly the fee increase, and the on-chain balance after confirmation still holds the reserve.
  • splice_in_with_all_bump_fee_without_anchor_reserve: same without a reserve, where the bump previously failed outright.
  • A wallet unit test for the confirmed-input listing (unconfirmed outputs excluded) and one for the address reservation (skipped while reserved, offered again once released).

Both integration tests fail on main: with a reserve the bump leaves the spliced-in amount unchanged while the fee rises, i.e. it is paid from the reserve; without a reserve bump_channel_funding_fee returns ChannelSplicingFailed.

🤖 Generated with Claude Code

@jkczyz
jkczyz requested a review from tnull October 16, 2026 21:20
@ldk-reviews-bot

ldk-reviews-bot commented Oct 16, 2026 •

Copy link
Copy Markdown

👋 Thanks for assigning @tnull 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.

jkczyz and others added 2 commits October 16, 2026 16:26
The RBF splice test builds its own bitcoind with a lower
incrementalrelayfee so that LDK's minimum RBF bump of 25 sat/kwu
satisfies BIP125's absolute fee increase requirement. Upcoming tests
that fee-bump a splice_in_with_all splice need the same setup, so make
it a helper next to setup_bitcoind_and_electrsd.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
splice_in_with_all estimated the maximum amount with a wallet drain and
then ran coin selection for that exact amount. The resulting
contribution had at most the anchor reserve as a change output, so
bump_channel_funding_fee had nothing else to pay a higher fee from.
With anchor channels enabled, the bump shrank the change output and so
spent the reserve on the splice fee. Without a reserve there was no
change output at all; LDK keeps the value added fixed for coin-selected
contributions, so the bump fell back to coin selection, which failed for
want of further inputs. The drain estimate also ran at the funding
feerate estimate while the contribution could be built at a pending
splice's higher RBF minimum, so coin selection for that amount could
eat into the reserve or fail.

Instead, hand LDK the wallet's confirmed UTXOs as manually selected
inputs. They are consumed in full with no change output, the amount
added to the channel is whatever remains after fees at the template's
feerate, and a later fee bump is paid by adding a little less to the
channel. When an anchor reserve is required, it is kept as an explicit
output back to one of the wallet's own addresses, which stays fixed
across bumps.

That address is marked used as soon as it is handed out, since the
splice transaction only reaches the wallet once negotiation completes
and until then BDK would offer the same address as change elsewhere.
The mark is dropped again if the contribution cannot be built or LDK
rejects it.

Insufficient-funds checks stay in front of LDK, which would otherwise
build a near-empty contribution. The drain-based splice estimate is
removed along with the shared-input handling in the drain helpers that
only it used.

Fixes lightningdevkit#1168.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jkczyz
jkczyz force-pushed the 2026-10-splice-in-with-all-manual-inputs branch from 173105a to fbaa8d4 Compare October 16, 2026 21:38

@tnull tnull left a comment

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.

Seems Leo's suggestion here might make sense to add in this PR?: #1103 (comment)

Comment thread src/lib.rs
// retained as an explicit output to one of our own addresses, which stays
// fixed across fee bumps. The address is reserved so that other change-address
// consumers skip it until the splice transaction reaches the wallet.
let reserve_output = if cur_anchor_reserve_sats > DUST_LIMIT_SATS {

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.

Codex:

  1. [P2] Preserve small configured anchor reserves — src/lib.rs:1891 (/home/tnull/workspace/ldk-node-review-pr-1176-0T4Hm0/src/lib.rs:1891). With
    per_channel_reserve_sats = 546, this condition omits the reserve output. Because the previous balance check was removed, splice_in_with_all now succeeds
    and drains the wallet to zero sats. Preserve positive reserves using the destination’s dust threshold, or reject the splice when preservation is
    impossible.

Comment thread src/lib.rs
.and_then(|builder| builder.add_outputs(reserve_output.into_iter().collect()).build())
.map_err(|e| {
log_error!(self.logger, "Failed to splice channel: {}", e);
Error::ChannelSplicingFailed

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.

Codex:

  1. [P2] Report fee shortfalls as InsufficientFunds — src/lib.rs:1970 (/home/tnull/workspace/ldk-node-review-pr-1176-0T4Hm0/src/lib.rs:1970). With 26,000 sats
    confirmed, a 25,000-sat reserve, no local channel balance, and a 2,500 sat/kwu feerate, LDK rejects the contribution before the subsequent insufficient-
    funds check. This mapping returns ChannelSplicingFailed, preventing callers from identifying the funding shortage. Map ManuallySelectedInputsInsufficient
    to InsufficientFunds.

@tnull tnull left a comment

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.

CI is failing, not sure if flake:

thread 'splice_rounds_discarded_while_the_channel_is_listed_fail_at_close' (307885) panicked at tests/integration_tests_rust.rs:4488:5:
assertion `left == right` failed: the bump reused an input of the first round
  left: [OutPoint { txid: ec9c78fceb249288b98f6b1772c1b2b648178638e4799baeae8a6b086af0c555, vout: 1 }, OutPoint { txid: ec9c78fceb249288b98f6b1772c1b2b648178638e4799baeae8a6b086af0c555, vout: 0 }]
 right: [OutPoint { txid: ec9c78fceb249288b98f6b1772c1b2b648178638e4799baeae8a6b086af0c555, vout: 1 }]
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    splice_rounds_discarded_while_the_channel_is_listed_fail_at_close

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.

bump_channel_funding_fee can't bump a splice_in_with_all splice without spending the anchor reserve

3 participants