Skip to content

Commit ac61ec9

Browse files
committed
Merge bitcoin#17843: wallet: Reset reused transactions cache
6fc554f wallet: Reset reused transactions cache (Fabian Jahr) Pull request description: Fixes bitcoin#17603 (together with bitcoin#17824) `getbalances` is using the cache within `GetAvailableCredit` under certain conditions [here](https://github.com/bitcoin/bitcoin/blob/35fff5be60e853455abc24713481544e91adfedb/src/wallet/wallet.cpp#L1826). For a wallet with `avoid_reuse` activated this can lead to inconsistent reporting of `used` transactions/balances between `getbalances` and `listunspent` as pointed out in bitcoin#17603. When an address is reused before the first transaction is spending from this address, the cache is not updated even after the transaction is sent. This means the remaining outputs at the reused address are not showing up as `used` in `getbalances`. With this change, any newly incoming transaction belonging to the wallet marks all the other outputs at the same address as dirty. ACKs for top commit: kallewoof: Code review re-ACK 6fc554f promag: ACK 6fc554f. achow101: Re-ACK 6fc554f meshcollider: Code review ACK 6fc554f Tree-SHA512: c4cad2c752176d16d77b4a4202291d20baddf9f27250896a40274d74a6945e0f6b34be04c2f9b1b2e756d3ac669b794969df8f82a98e0b16f10e92f276649ea2
2 parents 002f9e9 + 6fc554f commit ac61ec9

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)