Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #541 +/- ##
==========================================
+ Coverage 81.66% 81.70% +0.03%
==========================================
Files 25 25
Lines 6339 6351 +12
Branches 302 303 +1
==========================================
+ Hits 5177 5189 +12
Misses 1055 1055
Partials 107 107
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Dmenec
left a comment
There was a problem hiding this comment.
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.
| .unwrap() | ||
| .add_utxo(reused_outpoint_2) | ||
| .unwrap(); | ||
| assert!(builder.finish().is_ok()); |
There was a problem hiding this comment.
nit: could assert the reused coins are actually selected
| 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)); |
|
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”? |
921c4c1 to
3a7773a
Compare
Thank you so much for this review. I have updated the approach to use the wallet's canonical history ( |
|
The CI is failing because of a transitive dependency |
3a7773a to
ac4ed32
Compare
noahjoeris
left a comment
There was a problem hiding this comment.
Interesting!
Why did you add it to TxBuilder and not the new create_psbt (bdk-tx) path?
I thought that since the feature is non-breaking and tiny, and adds some privacy improvement, shipping it on the next TxBuilder stable release won't be bad. But, I'll certainly add it to |
noahjoeris
left a comment
There was a problem hiding this comment.
cACK
Maybe we should add a more explicit warning? something like:
WARNING: Coins on spks the wallet already spent from are not used, but still counted in
balance(), so it can fail withInsufficientFundsdespite enough balance.
- Add an opt-in TxBuilder method that keeps automatic coin selection from spending UTXOs in an address the wallet has already spent from. Once a wallet spends from an address, that address becomes publicly linked to the wallet. An adversary can exploit this by sending coins to it and hoping the wallet later merges them into a payment thereby linking their UTXOs. `avoid_reuse` defends against this by excluding such coins from selection. Details: - Semantics follow Bitcoin Core's avoid_reuse wallet flag (bitcoin/bitcoin#13756): an address is avoided only once it has been spent from. Coins on a never-spent address stay selectable. - Detection uses the wallet's canonical history (Wallet::list_output), so outputs from replaced transactions (e.g. RBF) are not counted as reuse. - Implemented via the existing unspendable set, so avoided coins can still be spent when selected explicitly with TxBuilder::add_utxo. - Opt-in and off by default; existing behavior is unchanged. Modeled as a per-transaction TxBuilder option rather than a persisted wallet flag. - Tests cover three cases: a never-spent address is kept, a spent-from address is excluded (spendable via explicit selection), and a fee-bumped incoming payment is not mistaken for reuse. Fixes bitcoin/bitcoin#13756 item in bitcoindevkit#28.
ac4ed32 to
4413067
Compare
I have updated the method docs to include a warning and an example. |
| /// | ||
| /// # Warning | ||
| /// | ||
| /// Avoided UTXOs are still included in [`Wallet::balance`]. Coin selection may therefore return |
There was a problem hiding this comment.
I'm thinking of how we could allow Balance to be aware of it. With the new chain's Balance refactor (bitcoindevkit/bdk#2246) we could create our own fold over Eligibility in the wallet and put the coins sitting on dirty addresses in a separate bucket. Core does something similar, getbalances reports a used balance when avoid_reuse is enabled.
There was a problem hiding this comment.
Great idea worth exploring. Unlike Core, where avoid_reuse is a persisted wallet flag, this is a per TxBuilder option. So introducing a new balance bucket would therefore require either always reporting the bucket as part of the normal Balance or adding something like balance_with_avoid_reuse() or accepting a boolean parameter in the balance (defaulting to off). Any of these approaches might work but this is definitely worth discussing.
|
tACK 4413067 Looks good! last nit: Core’s and wallet’s in the doc use |
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_reusewallet flag (bitcoin/bitcoin#13756).Approach:
Add an opt-in transaction builder method:
Tradeoff:
Notes to the reviewers
add_utxo().Changelog notice
TxBuilder::avoid_reuseto prevent TxBuilder from selecting reused address UTXOsBefore submitting