Skip to content

Commit bcb4cdc

Browse files
committed
Merge #17621: IsUsedDestination should count any known single-key address
0950245 IsUsedDestination should count any known single-key address (Gregory Sanders) Pull request description: This plugs the privacy leak detailed at bitcoin/bitcoin#17605, at least for the single-key case. ACKs for top commit: meshcollider: Code Review ACK 0950245 Tree-SHA512: e1d68281675f05072b3087171cba1df9416a69c9ccf70c72e8555e55eadda2d0fd339e5a894e3a3438ff94b9e3827fb19b8b701faade70c08756b19ff157ee0c
2 parents e8e7995 + 0950245 commit bcb4cdc

File tree

4 files changed

+47
-15
lines changed

4 files changed

+47
-15
lines changed

src/wallet/rpcwallet.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2927,7 +2927,7 @@ static UniValue listunspent(const JSONRPCRequest& request)
29272927
CTxDestination address;
29282928
const CScript& scriptPubKey = out.tx->tx->vout[out.i].scriptPubKey;
29292929
bool fValidAddress = ExtractDestination(scriptPubKey, address);
2930-
bool reused = avoid_reuse && pwallet->IsUsedDestination(address);
2930+
bool reused = avoid_reuse && pwallet->IsUsedDestination(out.tx->GetHash(), out.i);
29312931

29322932
if (destinations.size() && (!fValidAddress || !destinations.count(address)))
29332933
continue;

src/wallet/wallet.cpp

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -718,17 +718,33 @@ void CWallet::SetUsedDestinationState(WalletBatch& batch, const uint256& hash, u
718718
}
719719
}
720720

721-
bool CWallet::IsUsedDestination(const CTxDestination& dst) const
722-
{
723-
LOCK(cs_wallet);
724-
return IsMine(dst) && GetDestData(dst, "used", nullptr);
725-
}
726-
727721
bool CWallet::IsUsedDestination(const uint256& hash, unsigned int n) const
728722
{
723+
AssertLockHeld(cs_wallet);
729724
CTxDestination dst;
730725
const CWalletTx* srctx = GetWalletTx(hash);
731-
return srctx && ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst) && IsUsedDestination(dst);
726+
if (srctx) {
727+
assert(srctx->tx->vout.size() > n);
728+
LegacyScriptPubKeyMan* spk_man = GetLegacyScriptPubKeyMan();
729+
// When descriptor wallets arrive, these additional checks are
730+
// likely superfluous and can be optimized out
731+
assert(spk_man != nullptr);
732+
for (const auto& keyid : GetAffectedKeys(srctx->tx->vout[n].scriptPubKey, *spk_man)) {
733+
WitnessV0KeyHash wpkh_dest(keyid);
734+
if (GetDestData(wpkh_dest, "used", nullptr)) {
735+
return true;
736+
}
737+
ScriptHash sh_wpkh_dest(wpkh_dest);
738+
if (GetDestData(sh_wpkh_dest, "used", nullptr)) {
739+
return true;
740+
}
741+
PKHash pkh_dest(keyid);
742+
if (GetDestData(pkh_dest, "used", nullptr)) {
743+
return true;
744+
}
745+
}
746+
}
747+
return false;
732748
}
733749

734750
bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)

src/wallet/wallet.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -798,9 +798,8 @@ class CWallet final : public WalletStorage, private interfaces::Chain::Notificat
798798

799799
bool IsSpent(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
800800

801-
// Whether this or any UTXO with the same CTxDestination has been spent.
802-
bool IsUsedDestination(const CTxDestination& dst) const;
803-
bool IsUsedDestination(const uint256& hash, unsigned int n) const;
801+
// Whether this or any known UTXO with the same single key has been spent.
802+
bool IsUsedDestination(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
804803
void SetUsedDestinationState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
805804

806805
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, bool single_coin) const;

test/functional/wallet_avoidreuse.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ def run_test(self):
8686
reset_balance(self.nodes[1], self.nodes[0].getnewaddress())
8787
self.test_fund_send_fund_senddirty()
8888
reset_balance(self.nodes[1], self.nodes[0].getnewaddress())
89-
self.test_fund_send_fund_send()
89+
self.test_fund_send_fund_send("legacy")
90+
reset_balance(self.nodes[1], self.nodes[0].getnewaddress())
91+
self.test_fund_send_fund_send("p2sh-segwit")
92+
reset_balance(self.nodes[1], self.nodes[0].getnewaddress())
93+
self.test_fund_send_fund_send("bech32")
94+
9095

9196
def test_persistence(self):
9297
'''Test that wallet files persist the avoid_reuse flag.'''
@@ -182,7 +187,7 @@ def test_fund_send_fund_senddirty(self):
182187
assert_approx(self.nodes[1].getbalance(), 5, 0.001)
183188
assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 5, 0.001)
184189

185-
def test_fund_send_fund_send(self):
190+
def test_fund_send_fund_send(self, second_addr_type):
186191
'''
187192
Test the simple case where [1] generates a new address A, then
188193
[0] sends 10 BTC to A.
@@ -193,7 +198,7 @@ def test_fund_send_fund_send(self):
193198
'''
194199
self.log.info("Test fund send fund send")
195200

196-
fundaddr = self.nodes[1].getnewaddress()
201+
fundaddr = self.nodes[1].getnewaddress(label="", address_type="legacy")
197202
retaddr = self.nodes[0].getnewaddress()
198203

199204
self.nodes[0].sendtoaddress(fundaddr, 10)
@@ -214,7 +219,19 @@ def test_fund_send_fund_send(self):
214219
# getbalances should show no used, 5 btc trusted
215220
assert_balances(self.nodes[1], mine={"used": 0, "trusted": 5})
216221

217-
self.nodes[0].sendtoaddress(fundaddr, 10)
222+
# For the second send, we transmute it to a related single-key address
223+
# to make sure it's also detected as re-use
224+
fund_spk = self.nodes[0].getaddressinfo(fundaddr)["scriptPubKey"]
225+
fund_decoded = self.nodes[0].decodescript(fund_spk)
226+
if second_addr_type == "p2sh-segwit":
227+
new_fundaddr = fund_decoded["segwit"]["p2sh-segwit"]
228+
elif second_addr_type == "bech32":
229+
new_fundaddr = fund_decoded["segwit"]["addresses"][0]
230+
else:
231+
new_fundaddr = fundaddr
232+
assert_equal(second_addr_type, "legacy")
233+
234+
self.nodes[0].sendtoaddress(new_fundaddr, 10)
218235
self.nodes[0].generate(1)
219236
self.sync_all()
220237

0 commit comments

Comments
 (0)