Skip to content

Use destination groups instead of coins in coin_select to improve privacy#1851

Closed
martinsaposnic wants to merge 3 commits intobitcoindevkit:masterfrom
martinsaposnic:master
Closed

Use destination groups instead of coins in coin_select to improve privacy#1851
martinsaposnic wants to merge 3 commits intobitcoindevkit:masterfrom
martinsaposnic:master

Conversation

@martinsaposnic
Copy link
Copy Markdown

In bitcoindevkit/bdk_wallet#28 this PR from Bitcoin Core was mentioned bitcoin/bitcoin#12257

This PR adds a param when building the tx called avoid_partial_spends. When true, it changes coin select to use output groups rather than outputs, where each output group corresponds to all outputs with the same destination.

It is a privacy improvement, as each time you spend some output, any other output that is publicly associated with the destination (address) will also be spent at the same time, at the cost of fee increase for cases where coin select without group restriction would find a more optimal set of coins (see example below).

For regular use without address reuse, this PR should have no effect on the user experience whatsoever; it only affects users who, for some reason, have multiple outputs with the same destination (i.e. address reuse).

Example: a node has four outputs linked to two addresses A and B:

  • 1.0 btc to A
  • 0.5 btc to A
  • 1.0 btc to B
  • 0.5 btc to B

The node sends 0.2 btc to C. Without -avoidpartialspends, the following coin selection will occur:

  • 0.5 btc to A or B is picked
  • 0.2 btc is output to C
  • 0.3 - fee is output to (unique change address)

With -avoidpartialspends, the following will instead happen:

  • Both of (0.5, 1.0) btc to A or B is picked (one or the other pair)
  • 0.2 btc is output to C
  • 1.3 - fee is output to (unique change address)

Assuming nobody sends to the address after you spend from it, you will only ever use one address once. The con is that the transaction becomes slightly larger in this case, because it is overpicking outputs to adhere to the no partial spending rule.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature

Bugfixes:

  • I'm linking the issue being fixed by this PR

@martinsaposnic martinsaposnic changed the title Use UTXO groups instead of UTXO in coin_select to improve privacy Use destination groups instead of coins in coin_select to improve privacy Feb 24, 2025
@martinsaposnic martinsaposnic marked this pull request as draft February 25, 2025 14:17
@martinsaposnic martinsaposnic marked this pull request as ready for review February 25, 2025 14:33
@martinsaposnic martinsaposnic marked this pull request as draft February 25, 2025 14:35
New flag called avoid_partial_spends is introduced
It's a parameter used when building a tx
and it's passed to coin_select.

Later commits will introduce behavior for this flag
This commit introduces many changes but they
don't _really_ change anything. Tests will still pass
as usual.

The main change is how UTXOs are treated on coin_selection.
Before, each UTXO was treated individually, but now
each UTXO is its own **group**.

For now, group_utxos_if_applies just creates groups
with a single UTXO inside of it, but future commits
will add logic to this function so it groups by pub_key
Now avoid_partial_spends will work as intended.
If passed as true, it will group utxos by pubkey,
and if selected, the grouped UTXOs will be used together.
Tests are added that check that new functionality works
@martinsaposnic martinsaposnic marked this pull request as ready for review February 25, 2025 14:43
}
fn group_utxos_if_applies(utxos: Vec<WeightedUtxo>, _: bool) -> Vec<Vec<WeightedUtxo>> {
// No grouping: every UTXO is its own group.
return utxos.into_iter().map(|u| vec![u]).collect();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

why return utxos.into_iter().map(|u| vec![u]).collect(); and not utxos.into_iter().map(|u| vec![u]).collect() ?

@notmandatory
Copy link
Copy Markdown
Member

Thanks for working on this, might take some time to get it reviewed but I'll bring it up on our next dev call (on our discord server, open to everyone if you'd like to join).

@martinsaposnic
Copy link
Copy Markdown
Author

FYI, I'm working on a feature based on this PR. Nodes with avoid_partial_spends off will still try to avoid partial spending if the new transaction's fee is not higher than the original. PR coming soon

@nymius
Copy link
Copy Markdown
Contributor

nymius commented Mar 14, 2025

Looking at past PRs, OutputGroups was also tried before, in #727. There was a structure to wrap for WeightedUtxos , named OutputGroup too.
However, maybe this is not the most valuable thing to work right now as it would be extra work to match with in the future on bdk-tx, as we want to upstream it for bdk_wallet.

@martinsaposnic
Copy link
Copy Markdown
Author

However, maybe this is not the most valuable thing to work right now as it would be extra work to match with in the future on bdk-tx, as we want to upstream it for bdk_wallet.

closing and will work in the future

@github-project-automation github-project-automation bot moved this from Needs Review to Done in BDK Wallet Mar 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

module-wallet new feature New feature or request

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants