Skip to content

fix(wallet): include base tx overhead fee in manual coin selection target - #109

Open
Tyagiquamar wants to merge 5 commits into
synonymdev:mainfrom
Tyagiquamar:fix/utxo-selection-send-agreement
Open

fix(wallet): include base tx overhead fee in manual coin selection target#109
Tyagiquamar wants to merge 5 commits into
synonymdev:mainfrom
Tyagiquamar:fix/utxo-selection-send-agreement

Conversation

@Tyagiquamar

@Tyagiquamar Tyagiquamar commented Sep 1, 2026

Copy link
Copy Markdown

Problem

select_utxos_with_algorithm passed target_amount to BDK's coin_select unchanged. BDK's coin selection only adds the per-input satisfaction fee on top of that target — it does not account for the fixed transaction overhead (the recipient output plus version/marker/flag/varints/locktime). The transaction builder used by send_to_address, however, pays for the entire transaction.

As a result, a changeless BranchAndBound selection could be accepted by select_utxos_with_algorithm yet rejected by send_to_address as InsufficientFunds — short by exactly the base-overhead fee. The library effectively said "these UTXOs are enough" and then "these same UTXOs are insufficient".

Fixes #104.

Fix

Inflate the selection target by the fee for the fixed base-transaction overhead (BASE_TX_WEIGHT, ~188 WU ≈ 47 vB) before calling coin_select. The recipient-output size uses the conservative P2PKH size so the inflation is never too small for other script types. Selection and the builder now agree for the same target and fee rate.

Tests

  • New unit regression test selected_utxos_satisfy_tx_builder_for_all_algorithms builds the spend exactly like the manual-UTXO path of build_transaction_psbt and asserts the builder accepts the selection for the deterministic algorithms (BranchAndBound, LargestFirst, OldestFirst). It failed before the fix with InsufficientFunds { needed: 35722, available: 35700 } and passes after. SingleRandomDraw is omitted because its randomized selection can legitimately land on a dust-change rejection, making the test flaky.
  • New end-to-end test test_selected_utxos_are_accepted_by_send_to_address reproduces the issue's wallet layout (18 P2WPKH UTXOs) against real chain infra and asserts send_to_address accepts the selection.
  • cargo test -p bdk-wallet-aggregate --lib — 39 passed, 0 failed.
  • cargo fmt --check and cargo clippy -p bdk-wallet-aggregate --lib -- -D warnings both clean.

…rget

select_utxos_with_algorithm passed target_amount to BDK's coin_select
unchanged. BDK only adds the per-input satisfaction fee on top of that
target, not the fixed transaction overhead (recipient output + version/
locktime/varints). The transaction builder in send_to_address pays for
the whole transaction, so a changeless BranchAndBound selection could be
accepted by selection yet rejected by the builder as insufficient by the
base-overhead fee.

Inflate the selection target by the base-overhead fee so selection and
the builder agree. Add a regression test exercising the invariant for
the deterministic algorithms (BranchAndBound/LargestFirst/OldestFirst)
plus an end-to-end test using send_to_address.

Fixes synonymdev#104

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7eb66a4199

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/bdk-wallet-aggregate/src/utxo.rs Outdated
Comment thread crates/bdk-wallet-aggregate/src/utxo.rs Outdated
Comment thread tests/multi_address_types_tests.rs Outdated

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

The manual-send precheck still rejects a valid low-fee selection returned by the new path. The existing P2TR/P2WSH coverage thread also remains unresolved.

Comment thread crates/bdk-wallet-aggregate/src/utxo.rs Outdated
Remove the min_fee_buffer heuristic (max(200 vB fee, 1000 sats)) that
rejected manually selected UTXOs before the transaction builder ran.
At 250 sat/kwu the changeless branch-and-bound match for a 35,000-sat
target totals ~35,600 sats and was wrongly rejected below the 36,000-sat
clamp. The builder now decides whether the exact selected inputs can fund
the recipient plus the actual fee; a genuinely insufficient selection
still fails with InsufficientFunds.

Also pin selector/builder agreement for P2TR and P2WSH recipients (43 vB
outputs, matching the conservative 224 WU base-overhead allowance).

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

The manual-send precheck issue is fixed. The new P2TR/P2WSH test still does not protect the 224-WU estimate: at 250 sat/kwu, both 188 WU and 224 WU select the same eight inputs totaling 35,600 sats, and the 43-byte-output transaction needs about 35,598 sats. Reverting BASE_TX_WEIGHT to 188 WU would leave this test green. Could we use a boundary fixture that fails with 188 WU and passes with 224 WU?

@Tyagiquamar

Copy link
Copy Markdown
Author

Good catch. The previous fixture didn't distinguish 188 WU from 224 WU. I replaced it with a boundary case where the lower allowance selects an insufficient set while 224 WU selects a builder-valid set (at 1,000 sat/kwu, 188 WU selects 10,470 sats which is insufficient for TxBuilder needing 10,486 sats for a 43-vB output, whereas 224 WU selects 10,970 sats which TxBuilder accepts). I also mutation-checked the regression: it fails when BASE_TX_WEIGHT is reverted to 188 WU and passes at 224 WU. \cargo test -p bdk-wallet-aggregate --lib\ passes.

@Tyagiquamar
Tyagiquamar force-pushed the fix/utxo-selection-send-agreement branch from 1a3f572 to b375267 Compare September 2, 2026 12:13
@coreyphillips

coreyphillips commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

I rechecked b375267 and found a few remaining issues:

  • cargo test --test multi_address_types_tests --no-run fails because test_p2tr_recipient is not imported and the test uses private ldk_node::Error instead of NodeError. After those fixes, the UniFFI build also needs fee_rate.clone() because it is an Arc.
  • Strict Clippy fails on the new eight-argument inner selector.
  • Base-target inflation can turn valid P2WPKH change into CoinSelectionFailed. A 35,655 sat input for 35,000 sats at 250 sat/kwu yields 500 sats of selection change. BDK accepts it because the script-specific dust floor is 294 sats, but the wrapper rejects anything below 546.
  • Removing the manual precheck also removes the overflow guard. u64::MAX with selected UTXOs now panics instead of returning InsufficientFunds.
  • The 224 WU allowance still undercounts valid future-witness recipients. A v2 40-byte program needs a 246 WU skeleton, so selection can still succeed before the builder rejects it.

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

The replacement boundary fixture fixes the prior 224-WU coverage gap. The compile blockers already reported on the current head still prevent approval: #109 (comment)

@Tyagiquamar

Copy link
Copy Markdown
Author

Thanks — all five findings are addressed in 71da270:

  • The integration test source now imports test_p2tr_recipient, uses public NodeError, and clones the shared fee-rate handle.
  • The selector uses a request context, so strict Clippy passes without an allowance.
  • Recipient output weight is derived from the actual script when available; BDK now owns script-specific dust handling, including sub-546-sat P2WPKH change.
  • Checked arithmetic restores the selected-value overflow guard and maps overflow to InsufficientFunds.
  • RBF deficit selection remains overhead-free; normal transaction selection uses the 42-WU envelope plus the actual recipient TxOut weight, with the conservative fallback retained when no script is available.

Verified: aggregate tests 45/45, strict aggregate Clippy, aggregate formatting, and cargo check --lib pass. The exact integration --no-run command is blocked on Windows before the test target by electrsd 0.36.1's Unix-only build script. No CI checks are currently reported for this branch.

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

utACK

@ovitrif

ovitrif commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

@Tyagiquamar Please merge master in and push or rebase on master's tip and force-push

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.

Selections from select_utxos_with_algorithm are rejected by send_to_address (BranchAndBound always fails)

3 participants