Skip to content

feat(wallet): add TxBuilder::avoid_reuse to skip reused-address UTXOs - #541

Open
tvpeter wants to merge 2 commits into
bitcoindevkit:masterfrom
tvpeter:feat/add-avoid-reuse
Open

tvpeter wants to merge 2 commits into
bitcoindevkit:masterfrom
tvpeter:feat/add-avoid-reuse

Conversation

@tvpeter

@tvpeter tvpeter commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Description

Problem:

An adversary can attack a wallet's privacy through forced address reuse: after observing one of the addresses the user have already spent from, they can send small outputs to it. If the users' wallet later select those coins into a transaction, the adversary learns which inputs the user controls and the destinations they pay to, linking their UTXOs.

The TxBuilder has no way to automatically prevent spending outputs in an address that has already been spent from. This is one of the open privacy items tracked in #28, and mirrors Bitcoin Core's avoid_reuse wallet flag (bitcoin/bitcoin#13756).

Approach:

Add an opt-in transaction builder method:

let mut builder = wallet.build_tx();
builder
    .add_recipient(addr.script_pubkey(), Amount::from_sat(20_000))
    .avoid_reuse();
let psbt = builder.finish()?;

Tradeoff:

  • An address that received change and was later used to receive outputs again will be flagged, and its outputs excluded from automatic selection (they remain spendable via add_utxo()). This can lead to InsufficientFunds when the only funds available are on reused addresses.

Notes to the reviewers

  • An address (scriptPubKey) is treated as reused when the wallet has indexed more than one output to it. Since each (keychain, derivation_index) maps 1:1 to a scriptPubKey, outputs are counted per index from spk_index().outpoints() (which includes already-spent outputs, so the spend then dust attack is caught).
  • Every UTXO on a reused address is added to the existing unspendable set. They can still be spent when selected explicitly via add_utxo().

Changelog notice

  • Add TxBuilder::avoid_reuse to prevent TxBuilder from selecting reused address UTXOs

Before submitting

- Add `TxBuilder::avoid_reuse` to exclude UTXOs
on reused addresses from automatic coin selection
(privacy, addresses bitcoindevkit#28 / Core #13756)
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.97%. Comparing base (6fc6846) to head (b67a148).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #541      +/-   ##
==========================================
+ Coverage   81.91%   81.97%   +0.06%     
==========================================
  Files          25       25              
  Lines        6535     6557      +22     
  Branches      302      302              
==========================================
+ Hits         5353     5375      +22     
  Misses       1075     1075              
  Partials      107      107              
Flag Coverage Δ
rust 81.97% <100.00%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Dmenec Dmenec left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

cACK, nice feature :)

AFAIK Core only flags an address as used once the wallet has spent from it from then on, any coin sent to it is avoided. The PR description describes it the same way, but the implementation flags any address with more than one received output, even if it was never spent from.

Small test showing it:

let addr = wallet.reveal_next_address(KeychainKind::External).address;

// sent 2 outputs to the same address
receive_output_to_address(&mut wallet, addr.clone(), Amount::from_sat(100_000), ReceiveTo::Mempool(0));
receive_output_to_address(&mut wallet, addr, Amount::from_sat(546), ReceiveTo::Mempool(0));

let mut builder = wallet.build_tx();
builder.add_recipient(recipient.script_pubkey(), Amount::from_sat(10_000)).avoid_reuse();

// the 100k sats are excluded
assert!(matches!(
    builder.finish(),
    Err(CreateTxError::CoinSelection(e)) if e.available == Amount::ZERO
));

Not sure if this was intentional. If it was, I'd document that it differs from Core. I would follow Core's approach as anyone could grief UTXOs knowing this feature.

Comment thread src/wallet/tx_builder.rs
// `(keychain, derivation index)` maps 1:1 to a script pubkey, so a count > 1 means the
// address received funds more than once, i.e. it was reused.
let mut output_counts: HashMap<(KeychainKind, u32), usize> = HashMap::new();
for &((keychain, index), _) in self.wallet.spk_index().outpoints() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

wallet.list_output() may fit better here. It is canonical and includes both spent and unspent outputs.

I think in any case it should be canonicalized, for cases like someone paying you and bumping fees, which right now would be seen as a double payment to this address, excluding your coin.

Maybe we could add some tests trying replaced payments.

Comment thread src/wallet/tx_builder.rs
.unwrap()
.add_utxo(reused_outpoint_2)
.unwrap();
assert!(builder.finish().is_ok());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: could assert the reused coins are actually selected

Suggested change
assert!(builder.finish().is_ok());
let psbt = builder.finish().unwrap();
let selected: Vec<OutPoint> = psbt
.unsigned_tx
.input
.iter()
.map(|i| i.previous_output)
.collect();
assert!(selected.contains(&reused_outpoint));
assert!(selected.contains(&reused_outpoint_2));

@Chibey-max

Copy link
Copy Markdown

Should “reused address” mean “received more than one output,” or should it mean “the wallet has already spent from this address, and then later received more coins there”?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants