Skip to content

Privacy: randomized coin selection (BnB changeless + single random draw) - #3408

Open
bc1cindy wants to merge 7 commits into
cake-tech:devfrom
bc1cindy:feat/bnb-srd-coin-selection
Open

Privacy: randomized coin selection (BnB changeless + single random draw)#3408
bc1cindy wants to merge 7 commits into
cake-tech:devfrom
bc1cindy:feat/bnb-srd-coin-selection

Conversation

@bc1cindy

@bc1cindy bc1cindy commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

coin selection walked unspentCoins in a fixed order (address generation order, then coin age), so the input set was deterministic, a fingerprint an analyst can exploit, and always produced residual change.

selection now works like Core/bdk (BranchAndBound<SingleRandomDraw>):

  • branch-and-bound: searches over effective values for an input set whose excess over amount + fee stays below dust → the change output is dropped and the residue goes to fee (changeless tx, overpay bounded by dust).
  • single random draw fallback: no match → the pool is shuffled with Random.secure() before the existing accumulate-until-covered loop.
  • the RBF fee-bump top-up and payjoin receiver candidates are also shuffled.

closes #3407

related to payjoin/rust-payjoin#1597

@bc1cindy
bc1cindy force-pushed the feat/bnb-srd-coin-selection branch from 7af4ac5 to 08d24a5 Compare July 18, 2026 20:32
bc1cindy added 7 commits July 22, 2026 09:21
The greedy selection walked the unspent pool in a predictable order (address generation order, then per-address server order), a fingerprint an analyst can exploit. Shuffle the pool before the accumulate-until-covered loop so the input set is chosen non-deterministically (single random draw). MWEB coins are still kept last.

Branch-and-bound (changeless exact match) is a larger follow-up.
Pure, wallet-independent coin selector: branch-and-bound for an exact
changeless match within the cost-of-change window, single-random-draw
fallback (shuffled, injectable RNG) when no exact match exists, and
selectCoins tying them together over effective values.

This is the basis for replacing the greedy accumulation in _createUTXOS
so sends produce no change when possible and a non-deterministic
selection otherwise. Validated with dart test (9 cases); wiring into
_createUTXOS is a separate step.
Before the shuffled greedy walk, run branch-and-bound over effective
values (value minus per-input fee cost) looking for an input set whose
excess over amount plus fee stays below dust. When found, order those
inputs first and cap the walk at their count: the caller then computes
a change below dust, drops the change output, and absorbs the residue
into the fee, producing a changeless transaction with no residual
change fingerprint.

When no match exists, selection falls back to the shuffled pool, which
the greedy walk turns into a single random draw. BnB is skipped for
sendAll, forced input counts and pools holding MWEB coins.

changelessMatch filters non-positive effective values and maps indices
back to the original pool.
Assert the end-to-end arithmetic the wallet performs around a
branch-and-bound match: with the 68/34/10 vBytes model, the leftover
the caller absorbs into the fee is never negative (no re-selection
loop) and never exceeds the dust limit (bounded fee overpay).

Also pin termination: a large pool with no possible match returns null
through the maxTries cap instead of exploring the full search tree.
The selection shuffle and the single-random-draw fallback exist to be
unpredictable, so seed them from Random.secure() instead of the default
PRNG, matching what Core and bdk use for coin selection. Pool sizes are
small, so the cost is negligible. Tests keep injecting a seeded Random
for determinism.
Two secondary paths still consumed unspentCoins in wallet scan order
(address generation order, then per-address age), the same predictable
order removed from the main selection path:

- replaceByFee walked the unused-UTXO list in scan order when the fee
  bump needed extra inputs
- the payjoin receiver handed input candidates to the payjoin library
  in scan order, letting ties inside its selection mirror that order

Shuffle both with a secure RNG so no input-selection path exposes the
wallet's address or coin age ordering.
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.

Coin selection follows a predictable order (privacy fingerprint)

1 participant