Skip to content

backport: wallet coin selection prerequisites for #7400#7479

Draft
thepastaclaw wants to merge 6 commits into
dashpay:developfrom
thepastaclaw:backport-7400-prereq-coin-selection
Draft

backport: wallet coin selection prerequisites for #7400#7479
thepastaclaw wants to merge 6 commits into
dashpay:developfrom
thepastaclaw:backport-7400-prereq-coin-selection

Conversation

@thepastaclaw

Copy link
Copy Markdown

Issue being fixed or feature implemented

Dash PR #7400 currently combines its target wallet changes with a longer Bitcoin Core prerequisite chain. This extracts the first independently mergeable prerequisite batch so the backports can be reviewed and landed in smaller units.

This branch was rebuilt from upstream/develop at 87692f7b1506f8454e1b9c75c8fcf260b2c0dcd4. It contains four logical Bitcoin Core backports, within the requested limit of at most five per prerequisite PR. It does not modify or replace #7400; that PR must remain unchanged until the prerequisite PRs merge.

What was done?

Backported:

The four Bitcoin backport commits retain matching stable patch IDs to their counterparts extracted from the exact #7400 source head (80450061d0e0e5f22c334c2975fcb7531c7229bb). bitcoin#26699 intentionally travels with bitcoin#25685 because omitting it makes manual coin-control balance checks exclude the selected coins.

Dash-specific adaptations included with the backports:

  • Preserve Dash's CoinJoin coin types and fully mixed no-change behavior.
  • Preserve FundSpecialTx selected-input handling in rpc/evo.cpp.
  • Adapt the wallet: Faster transaction creation by removing pre-set-inputs fetching responsibility from Coin Selection bitcoin/bitcoin#25685 wallet benchmark to Dash's current interfaces::BlockInfo notification API.
  • Correct fee targeting when the final vin CompactSize prefix widens at 253 inputs, or when adding change widens the vout CompactSize prefix at 253 outputs. The retry covers change-bearing and exact no-change transactions, while retaining the final fee invariant check.
  • Add regression coverage for 252/253-input transactions, exact no-change selection, and 252 recipients plus change.

Dash's existing watch-only GUI architecture differs from upstream: it selects watch-only coins directly through the wallet interface rather than the upstream cached-balance path. The upstream-only legacy watch-only PSBT GUI regression test was therefore not carried over.

Later sequencing, after this PR merges:

  1. Open a second prerequisite PR with wallet: bugfix, load a wallet with an unknown/corrupt descriptor causes a fatal error bitcoin/bitcoin#26021, wallet: bugfix, invalid crypted key "checksum_valid" set bitcoin/bitcoin#26532, wallet: bugfix, invalid CoinsResult cached total amount bitcoin/bitcoin#26560, wallet: Coin Selection, return accurate error messages bitcoin/bitcoin#26661, and wallet: group outputs only once, decouple it from Coin Selection bitcoin/bitcoin#25806, including the associated Dash cursor-close and test-utility adaptations.
  2. After both prerequisite PRs merge, rebase backport: Bitcoin Core v0.26 backports (batch 2 rebuild) #7400 and retain its reduced target stack: rpc: add descriptorprocesspsbt rpc bitcoin/bitcoin#25796, Introduce MockableDatabase for wallet unit tests bitcoin/bitcoin#26715, walletdb: Remove unused CreateMockWalletDatabase bitcoin/bitcoin#27665, and wallet, bench: Move commonly used functions to their own file and fix a bug bitcoin/bitcoin#27666.

No later prerequisite PR has been opened yet.

How Has This Been Tested?

Tested on macOS arm64 using the repository's autotools build configured against the existing Dash depends prefix.

Build targets:

  • test/test_dash
  • dashd
  • bench/bench_dash
  • qt/test/test_dash-qt
  • dash-cli
  • dash-tx
  • dash-wallet

Unit and benchmark coverage:

  • test_dash --run_test=coinselector_tests,spend_tests,wallet_tests — 34 test cases passed
  • WalletCreateTxUseOnlyPresetInputs benchmark passed
  • WalletCreateTxUsePresetInputsAndCoinSelection benchmark passed

Functional coverage, run serially:

  • rpc_psbt.py --descriptors
  • rpc_psbt.py --legacy-wallet
  • wallet_fundrawtransaction.py --descriptors
  • wallet_fundrawtransaction.py --legacy-wallet
  • wallet_fundrawtransaction.py --legacy-wallet --nohd
  • wallet_send.py --descriptors
  • wallet_send.py --legacy-wallet

All listed variants passed.

Runtime RPC verification against the built dashd/dash-cli:

  • A 251-recipient send produced 252 outputs including change.
  • A 252-recipient send crossed the CompactSize boundary and produced 253 outputs including change.
  • Transactions restricted to exactly 252 and 253 manually selected inputs both completed and decoded with the requested input counts.
  • An insufficient selected-input probe returned a normal Insufficient funds RPC error rather than an internal fee-accounting error.

Additional checks:

  • qt/test/test_dash-qt -platform minimal passed; the existing macOS/minimal guards skipped the WalletTests, AppTests, and AddressBookTests bodies.
  • test/lint/lint-whitespace.py
  • test/lint/lint-circular-dependencies.py
  • test/lint/lint-python.py
  • test/lint/lint-include-guards.py
  • test/lint/lint-includes.py
  • git diff --check
  • All six commits have valid signatures from the configured signing key.

Breaking Changes

None.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation (not applicable)
  • I have assigned this pull request to a milestone

4fef534 wallet: use GetChange() when computing waste (S3RK)
87e0ef9 wallet: use GetChange() in tx building (S3RK)
15e97a6 wallet: add SelectionResult::GetChange (S3RK)
72cad28 wallet: calculate and store min_viable_change (S3RK)
e3210a7 wallet: account for preselected inputs in target (S3RK)
f8e7963 wallet: add SelectionResult::Merge (S3RK)
06f558e wallet: accurate SelectionResult::m_target (S3RK)
c8cf08e wallet: ensure m_min_change_target always covers change fee (S3RK)

Pull request description:

  Benefits:
  1. more accurate waste calculation for knapsack. Waste calculation is now consistent with tx building code. Before we always assumed change for knapsack even when the solution is changeless4.
  2. simpler tx building code. Only create change output when it's needed
  3. makes it easier to correctly account for fees for CPFP inputs (should be done in a follow up)

  In the first three commits we fix the code to accurately track selection target in `SelectionResult::m_target`
  Then we introduce new variable `min_change` that represents the minimum viable change amount
  Then we introduce `SelectionResult::GetChange()` which incapsulates dropping change for fee logic and uses correct values of `SelectionResult::m_target`
  Then we use `SelectionResult::GetChange()` in both tx building and waste calculation code

  This PR is a refactoring and shouldn't change the behaviour.
  There is only one known small change (arguably a bug fix). Before we dropped change output if it's smaller than `cost_of_change` after paying change fees. This is incorrect as `cost_of_change` already includes `change_fee`.

ACKs for top commit:
  achow101:
    ACK 4fef534
  Xekyo:
    crACK 4fef534
  furszy:
    Code review ACK 4fef534
  w0xlt:
    ACK bitcoin@4fef534

Tree-SHA512: 31a7455d4129bc39a444da0f16ad478d690d4d9627b2b8fdb5605facc6488171926bf02f5d7d9a545b2b59efafcf5bb3d404005e4da15c7b44b3f7d441afb941

(cherry picked from commit 2bd9aa5)
…g target

d0d9cf7 test: Check external coin effective value is used in CoinSelection (Aurèle Oulès)
76b79c1 wallet: Use correct effective value when checking target (Aurèle Oulès)

Pull request description:

  Fixes bitcoin#26185. The following assert failed because it was not checked in the parent function.

  https://github.com/bitcoin/bitcoin/blob/2bd9aa5a44b88c866c4d98f8a7bf7154049cba31/src/wallet/coinselection.cpp#L391

ACKs for top commit:
  glozow:
    reACK d0d9cf7
  furszy:
    ACK d0d9cf7

Tree-SHA512: e126daba1115e9d143f2a582c6953e7ea55e96853b6e819c7744fd7a23668f7d9854681d43ef55d8774655bc54e7e87c1c9fccd746d9e30fbf3caa82ef808ae9

(cherry picked from commit cda6c79)
…pre-set-inputs fetching responsibility from Coin Selection

3fcb545 bench: benchmark transaction creation process (furszy)
a8a7534 wallet: SelectCoins, return early if target is covered by preset-inputs (furszy)
f41712a wallet: simplify preset inputs selection target check (furszy)
5baedc3 wallet: remove fetch pre-selected-inputs responsibility from SelectCoins (furszy)
295852f wallet: encapsulate pre-selected-inputs lookup into its own function (furszy)
37e7887 wallet: skip manually selected coins from 'AvailableCoins' result (furszy)
94c0766 wallet: skip available coins fetch if "other inputs" are disallowed (furszy)

Pull request description:

  #### # Context (Current Flow on Master)

  In the transaction creation process, in order to select which coins the new transaction will spend,
  we first obtain all the available coins known by the wallet, which means walking-through the
  wallet txes map, gathering the ones that fulfill certain spendability requirements in a vector.

  This coins vector is then provided to the Coin Selection process, which first checks if the user
  has manually selected any input (which could be internal, aka known by the wallet, or external),
  and if it does, it fetches them by searching each of them inside the wallet and/or inside the
  Coin Control external tx data.

  Then, after finding the pre-selected-inputs and gathering them in a vector, the Coin Selection
  process walks-through the entire available coins vector once more just to erase coins that are
  in both vectors. So the Coin Selection process doesn’t pick them twice (duplicate inputs inside
  the same transaction).

  #### # Process Workflow Changes

  Now, a new method, `FetchCoins` will be responsible for:
  1) Lookup the user pre-selected-inputs (which can be internal or external).
  2) And, fetch the available coins in the wallet (excluding the already fetched ones).

  Which will occur prior to the Coin Selection process. Which allows us to never include the
  pre-selected-inputs inside the available coins vector in the first place, as well as doing other
  nice improvements (written below).

  So, Coin Selection can perform its main responsibility without mixing it with having to fetch
  internal/external coins nor any slow and unneeded duplicate coins verification.

  #### # Summarizing the Improvements:

  1) If any pre-selected-input lookup fail, the process will return the error right away.
      (before, the wallet was fetching all the wallet available coins, walking through the
      entire txes map, and then failing for an invalid pre-selected-input inside SelectCoins)

  2) The pre-selected-inputs lookup failure causes are properly described on the return error.
      (before, we were returning an "Insufficient Funds" error for everything, even if the failure
      was due a not solvable external input)

  3) **Faster Coin Selection**: no longer need to "remove the pre-set inputs from the available coins
      vector so that Coin Selection doesn't pick them" (which meant to loop-over the entire
      available coins vector at Coin Selection time, erasing duplicate coins that were pre-selected).

      Now, the available coins vector, which is built after the pre-selected-inputs fetching,
      doesn’t include the already selected inputs in the first place.

  4) **Faster transaction creation** for transactions that only use manually selected inputs.

      We now will return early, as soon as we finish fetching the pre-selected-inputs and
      not perform the resources expensive calculation of walking-through the entire wallet
      txes map to obtain the available coins (coins that we will not use).

  ---------------------------

  Added a new bench (f6d0bb2) measuring the transaction creation process, for a wallet with ~250k UTXO, only using the pre-selected-inputs inside coin control. Setting `m_allow_other_inputs=false` to disallow the wallet to include coins automatically.

  #### Result on this PR (tip f6d0bb2d):

  |               ns/op |                op/s |    err% |     total | benchmark
  |--------------------:|--------------------:|--------:|----------:|:----------
  |        1,048,675.00 |              953.58 |    0.3% |      0.06 | `WalletCreateTransaction`

  vs

  #### Result on master (tip 4a4289e):

  |               ns/op |                op/s |    err% |     total | benchmark
  |--------------------:|--------------------:|--------:|----------:|:----------
  |       96,373,458.20 |               10.38 |    0.2% |      5.30 | `WalletCreateTransaction`

  The benchmark took to run in master: **96.37 milliseconds**, while in this PR: **1 millisecond**  🚀 .

ACKs for top commit:
  S3RK:
    Code Review ACK 3fcb545
  achow101:
    ACK 3fcb545
  aureleoules:
    reACK 3fcb545

Tree-SHA512: 42f833e92f40c348007ca565a4c98039e6f1ff25d8322bc2b27115824744779baf0b0a38452e4e2cdcba45076473f1028079bbd0f670020481ec5d3db42e4731

(cherry picked from commit f37bd15)
…elected coins

68eed5d test,gui: add coverage for PSBT creation on legacy watch-only wallets (furszy)
306aab5 test,gui: decouple widgets and model into a MiniGui struct (furszy)
2f76ac0 test,gui: decouple chain and wallet initialization from test case (furszy)
cd98b71 gui: 'getAvailableBalance', include watch only balance (furszy)
74eac3a test: add coverage for 'useAvailableBalance' functionality (furszy)
dc1cc1c gui: bugfix, getAvailableBalance skips selected coins (furszy)

Pull request description:

  Fixes bitcoin-core/gui#688 and bitcoin#26687.

  First Issue Description (bitcoin-core/gui#688):

  The previous behavior for `getAvailableBalance`, when the coin control had selected coins, was to return the sum of them. Instead, we are currently returning the wallet's available total balance minus the selected coins total amount.

  Reason:
  Missed to update the `GetAvailableBalance` function to include the coin control selected coins on bitcoin#25685.

  Context:
  Since bitcoin#25685 we skip the selected coins inside `AvailableCoins`, the reason is that there is no need to waste resources walking through the entire wallet's txes map just to get coins that could have gotten by just doing a simple `mapWallet.find`).

  Places Where This Generates Issues (only when the user manually select coins via coin control):
  1) The GUI balance check prior the transaction creation process.
  2) The GUI "useAvailableBalance" functionality.

  Note 1:
  As the GUI uses a balance cache since bitcoin-core/gui#598, this issue does not affect the regular spending process. Only arises when the user manually select coins.

  Note 2:
  Added test coverage for the `useAvailableBalance` functionality.

  ----------------------------------

  Second Issue Description (bitcoin#26687):

  As we are using a cached balance on `WalletModel::getAvailableBalance`,
  the function needs to include the watch-only available balance for wallets
  with private keys disabled.

ACKs for top commit:
  Sjors:
    tACK 68eed5d
  achow101:
    ACK 68eed5d
  theStack:
    ACK 68eed5d

Tree-SHA512: 674f3e050024dabda2ff4a04b9ed3750cf54a040527204c920e1e38bd3d7f5fd4d096e4fd08a0fea84ee6abb5070f022b5c0d450c58fd30202ef05ebfd7af6d3

(cherry picked from commit 27dcc07)
…ing)

Dash-specific follow-up to the bitcoin#25647 backport. Coin selection estimates CompactSize prefixes before the final input and output counts are known. Wider vin-count prefixes at 253 or 65,536 inputs, or a wider vout-count prefix when change raises 252 or 65,535 recipients across a boundary, can leave the selected value a few duffs short of the accurately measured final fee.

After selecting inputs, recompute the non-input fee with the actual vin-count prefix and the prospective change output count, then retry selection if the target must increase. This keeps change-bearing and exact no-change transactions on the same path and preserves the final Fee needed > fee paid check for unrelated accounting defects.

Add regression coverage for 252 inputs, 253 inputs with change, an exact-target 253-input transaction without change, and 252 recipients plus change.
…ification)

Dash's current wallet notification interface accepts interfaces::BlockInfo after the kernel chain interface backport. Construct the notification with kernel::MakeBlockInfo while preserving the bitcoin#25685 benchmark behavior.
@thepastaclaw
thepastaclaw force-pushed the backport-7400-prereq-coin-selection branch from b4cf789 to bc52f0a Compare July 23, 2026 12:50
@thepastaclaw

thepastaclaw commented Jul 23, 2026

Copy link
Copy Markdown
Author

✅ Final review complete — no blockers (commit bc52f0a)

@thepastaclaw

Copy link
Copy Markdown
Author

CI triage for current head bc52f0a9c79c: the sole red job is linux64_ubsan-test / Test source, where unrelated feature_governance.py observed 24 votes instead of 25. The PR changes wallet coin-selection prerequisites/adaptations, and every other build/test job passed. I tried rerunning the failed jobs, but GitHub rejected it because this account lacks repository admin rights.

@knst could you rerun the failed job when convenient? No branch change looks warranted from this failure.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Final validation — Codex + Sonnet

The final tree correctly carries the four advertised wallet backports and the Dash-specific CompactSize and BlockInfo adaptations; the omitted upstream legacy watch-only PSBT GUI coverage is an explicit architecture-specific exclusion, while the applicable selected-coin balance behavior is adapted and tested in Dash. No runtime or Dash-subsystem correctness defects remain at the PR head. The only retained finding is commit-stack hygiene: the benchmark is introduced with an uncompilable wallet notification call and repaired three commits later.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed), gpt-5.6-sol — backport-reviewer (completed)
  • Verifier: gpt-5.6-sol — final-verifier (fallback)
  • Sonnet reviewers: claude-sonnet-5 — general (failed), claude-sonnet-5 — dash-core-commit-history (failed), claude-sonnet-5 — backport-reviewer (completed), claude-sonnet-5 — general (completed), claude-sonnet-5 — dash-core-commit-history (completed)

🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/bench/wallet_create_tx.cpp`:
- [SUGGESTION] src/bench/wallet_create_tx.cpp:73: bench target does not compile for three intermediate commits (fixed by final commit in this same PR)
  Commit `c392040ca08` registers this new source in `bench_bench_dash_SOURCES` but calls `wallet.blockConnected(block, pindex->nHeight)`. At that commit, `CWallet` declares only `blockConnected(const interfaces::BlockInfo&)`, so a wallet-enabled benchmark build fails at `c392040ca08` and remains broken through `0969ce0fe7e` and `cde726cc5e1`. The final commit correctly changes the call to `kernel::MakeBlockInfo`, but Dash preserves commits when merging, so fold that adaptation into `c392040ca08` or place it immediately afterward to keep the permanent history independently buildable and bisectable.


// notify wallet
const auto& pindex = WITH_LOCK(::cs_main, return context.chainman->ActiveChain().Tip());
wallet.blockConnected(kernel::MakeBlockInfo(pindex, &block));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🟡 Suggestion: bench target does not compile for three intermediate commits (fixed by final commit in this same PR)

Commit c392040ca08 registers this new source in bench_bench_dash_SOURCES but calls wallet.blockConnected(block, pindex->nHeight). At that commit, CWallet declares only blockConnected(const interfaces::BlockInfo&), so a wallet-enabled benchmark build fails at c392040ca08 and remains broken through 0969ce0fe7e and cde726cc5e1. The final commit correctly changes the call to kernel::MakeBlockInfo, but Dash preserves commits when merging, so fold that adaptation into c392040ca08 or place it immediately afterward to keep the permanent history independently buildable and bisectable.

source: ['claude', 'codex']

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.

1 participant