Skip to content

Commit 6fc554f

Browse files
committed
wallet: Reset reused transactions cache
If a destination is reused we mark the cache of the other transactions going to that destination dirty so they are not accidentally reported as trusted when the cache is hit.
1 parent 6196e93 commit 6fc554f

File tree

3 files changed

+63
-5
lines changed

3 files changed

+63
-5
lines changed

src/wallet/wallet.cpp

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ bool CWallet::MarkReplaced(const uint256& originalHash, const uint256& newHash)
708708
return success;
709709
}
710710

711-
void CWallet::SetUsedDestinationState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used)
711+
void CWallet::SetUsedDestinationState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used, std::set<CTxDestination>& tx_destinations)
712712
{
713713
AssertLockHeld(cs_wallet);
714714
const CWalletTx* srctx = GetWalletTx(hash);
@@ -718,7 +718,9 @@ void CWallet::SetUsedDestinationState(WalletBatch& batch, const uint256& hash, u
718718
if (ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst)) {
719719
if (IsMine(dst)) {
720720
if (used && !GetDestData(dst, "used", nullptr)) {
721-
AddDestData(batch, dst, "used", "p"); // p for "present", opposite of absent (null)
721+
if (AddDestData(batch, dst, "used", "p")) { // p for "present", opposite of absent (null)
722+
tx_destinations.insert(dst);
723+
}
722724
} else if (!used && GetDestData(dst, "used", nullptr)) {
723725
EraseDestData(batch, dst, "used");
724726
}
@@ -765,10 +767,14 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
765767

766768
if (IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) {
767769
// Mark used destinations
770+
std::set<CTxDestination> tx_destinations;
771+
768772
for (const CTxIn& txin : wtxIn.tx->vin) {
769773
const COutPoint& op = txin.prevout;
770-
SetUsedDestinationState(batch, op.hash, op.n, true);
774+
SetUsedDestinationState(batch, op.hash, op.n, true, tx_destinations);
771775
}
776+
777+
MarkDestinationsDirty(tx_destinations);
772778
}
773779

774780
// Inserts only if not already there, returns tx inserted or tx found
@@ -3162,6 +3168,21 @@ int64_t CWallet::GetOldestKeyPoolTime()
31623168
return oldestKey;
31633169
}
31643170

3171+
void CWallet::MarkDestinationsDirty(const std::set<CTxDestination>& destinations) {
3172+
for (auto& entry : mapWallet) {
3173+
CWalletTx& wtx = entry.second;
3174+
3175+
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) {
3176+
CTxDestination dst;
3177+
3178+
if (ExtractDestination(wtx.tx->vout[i].scriptPubKey, dst) && destinations.count(dst)) {
3179+
wtx.MarkDirty();
3180+
break;
3181+
}
3182+
}
3183+
}
3184+
}
3185+
31653186
std::map<CTxDestination, CAmount> CWallet::GetAddressBalances(interfaces::Chain::Lock& locked_chain)
31663187
{
31673188
std::map<CTxDestination, CAmount> balances;

src/wallet/wallet.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@ class CWallet final : public WalletStorage, private interfaces::Chain::Notificat
803803

804804
// Whether this or any known UTXO with the same single key has been spent.
805805
bool IsUsedDestination(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
806-
void SetUsedDestinationState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
806+
void SetUsedDestinationState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used, std::set<CTxDestination>& tx_destinations) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
807807

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

@@ -963,6 +963,12 @@ class CWallet final : public WalletStorage, private interfaces::Chain::Notificat
963963

964964
std::set<CTxDestination> GetLabelAddresses(const std::string& label) const;
965965

966+
/**
967+
* Marks all outputs in each one of the destinations dirty, so their cache is
968+
* reset and does not return outdated information.
969+
*/
970+
void MarkDestinationsDirty(const std::set<CTxDestination>& destinations) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
971+
966972
bool GetNewDestination(const OutputType type, const std::string label, CTxDestination& dest, std::string& error);
967973
bool GetNewChangeDestination(const OutputType type, CTxDestination& dest, std::string& error);
968974

test/functional/wallet_avoidreuse.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ def run_test(self):
9191
self.test_fund_send_fund_send("p2sh-segwit")
9292
reset_balance(self.nodes[1], self.nodes[0].getnewaddress())
9393
self.test_fund_send_fund_send("bech32")
94-
94+
reset_balance(self.nodes[1], self.nodes[0].getnewaddress())
95+
self.test_getbalances_used()
9596

9697
def test_persistence(self):
9798
'''Test that wallet files persist the avoid_reuse flag.'''
@@ -257,5 +258,35 @@ def test_fund_send_fund_send(self, second_addr_type):
257258
assert_approx(self.nodes[1].getbalance(), 1, 0.001)
258259
assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 11, 0.001)
259260

261+
def test_getbalances_used(self):
262+
'''
263+
getbalances and listunspent should pick up on reused addresses
264+
immediately, even for address reusing outputs created before the first
265+
transaction was spending from that address
266+
'''
267+
self.log.info("Test getbalances used category")
268+
269+
# node under test should be completely empty
270+
assert_equal(self.nodes[1].getbalance(avoid_reuse=False), 0)
271+
272+
new_addr = self.nodes[1].getnewaddress()
273+
ret_addr = self.nodes[0].getnewaddress()
274+
275+
# send multiple transactions, reusing one address
276+
for _ in range(11):
277+
self.nodes[0].sendtoaddress(new_addr, 1)
278+
279+
self.nodes[0].generate(1)
280+
self.sync_all()
281+
282+
# send transaction that should not use all the available outputs
283+
# per the current coin selection algorithm
284+
self.nodes[1].sendtoaddress(ret_addr, 5)
285+
286+
# getbalances and listunspent should show the remaining outputs
287+
# in the reused address as used/reused
288+
assert_unspent(self.nodes[1], total_count=2, total_sum=6, reused_count=1, reused_sum=1)
289+
assert_balances(self.nodes[1], mine={"used": 1, "trusted": 5})
290+
260291
if __name__ == '__main__':
261292
AvoidReuseTest().main()

0 commit comments

Comments
 (0)