Skip to content

Commit

Permalink
Merge bitcoin#17843: wallet: Reset reused transactions cache
Browse files Browse the repository at this point in the history
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
  • Loading branch information
meshcollider authored and Munkybooty committed Oct 17, 2022
1 parent 0676488 commit 3b125bf
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 4 deletions.
27 changes: 24 additions & 3 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ void CWallet::MarkDirty()
fAnonymizableTallyCachedNonDenom = false;
}

void CWallet::SetUsedDestinationState(const uint256& hash, unsigned int n, bool used)
void CWallet::SetUsedDestinationState(const uint256& hash, unsigned int n, bool used, std::set<CTxDestination>& tx_destinations)
{
const CWalletTx* srctx = GetWalletTx(hash);
if (!srctx) return;
Expand All @@ -781,7 +781,9 @@ void CWallet::SetUsedDestinationState(const uint256& hash, unsigned int n, bool
if (IsMine(dst)) {
LOCK(cs_wallet);
if (used && !GetDestData(dst, "used", nullptr)) {
AddDestData(dst, "used", "p"); // p for "present", opposite of absent (null)
if (AddDestData(dst, "used", "p")) { // p for "present", opposite of absent (null)
tx_destinations.insert(dst);
}
} else if (!used && GetDestData(dst, "used", nullptr)) {
EraseDestData(dst, "used");
}
Expand Down Expand Up @@ -812,10 +814,14 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)

if (IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) {
// Mark used destinations
std::set<CTxDestination> tx_destinations;

for (const CTxIn& txin : wtxIn.tx->vin) {
const COutPoint& op = txin.prevout;
SetUsedDestinationState(op.hash, op.n, true);
SetUsedDestinationState(op.hash, op.n, true, tx_destinations);
}

MarkDestinationsDirty(tx_destinations);
}

// Inserts only if not already there, returns tx inserted or tx found
Expand Down Expand Up @@ -3771,6 +3777,21 @@ bool CWallet::GetNewChangeDestination(CTxDestination& dest, std::string& error)
return true;
}

void CWallet::MarkDestinationsDirty(const std::set<CTxDestination>& destinations) {
for (auto& entry : mapWallet) {
CWalletTx& wtx = entry.second;

for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) {
CTxDestination dst;

if (ExtractDestination(wtx.tx->vout[i].scriptPubKey, dst) && destinations.count(dst)) {
wtx.MarkDirty();
break;
}
}
}
}

std::map<CTxDestination, CAmount> CWallet::GetAddressBalances()
{
std::map<CTxDestination, CAmount> balances;
Expand Down
8 changes: 7 additions & 1 deletion src/wallet/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -909,7 +909,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
// Whether this or any UTXO with the same CTxDestination has been spent.
bool IsUsedDestination(const CTxDestination& dst) const;
bool IsUsedDestination(const uint256& hash, unsigned int n) const;
void SetUsedDestinationState(const uint256& hash, unsigned int n, bool used);
void SetUsedDestinationState(const uint256& hash, unsigned int n, bool used, std::set<CTxDestination>& tx_destinations);

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

Expand Down Expand Up @@ -1074,6 +1074,12 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati

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

/**
* Marks all outputs in each one of the destinations dirty, so their cache is
* reset and does not return outdated information.
*/
void MarkDestinationsDirty(const std::set<CTxDestination>& destinations) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

bool GetNewDestination(const std::string label, CTxDestination& dest, std::string& error);
bool GetNewChangeDestination(CTxDestination& dest, std::string& error);

Expand Down
32 changes: 32 additions & 0 deletions test/functional/wallet_avoidreuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ def run_test(self):
self.test_fund_send_fund_senddirty()
reset_balance(self.nodes[1], self.nodes[0].getnewaddress())
self.test_fund_send_fund_send()
reset_balance(self.nodes[1], self.nodes[0].getnewaddress())
self.test_getbalances_used()

def test_persistence(self):
'''Test that wallet files persist the avoid_reuse flag.'''
Expand Down Expand Up @@ -222,5 +224,35 @@ def test_fund_send_fund_send(self):
assert_approx(self.nodes[1].getbalance(), 1, 0.001)
assert_approx(self.nodes[1].getbalance(avoid_reuse=False), 11, 0.001)

def test_getbalances_used(self):
'''
getbalances and listunspent should pick up on reused addresses
immediately, even for address reusing outputs created before the first
transaction was spending from that address
'''
self.log.info("Test getbalances used category")

# node under test should be completely empty
assert_equal(self.nodes[1].getbalance(avoid_reuse=False), 0)

new_addr = self.nodes[1].getnewaddress()
ret_addr = self.nodes[0].getnewaddress()

# send multiple transactions, reusing one address
for _ in range(11):
self.nodes[0].sendtoaddress(new_addr, 1)

self.nodes[0].generate(1)
self.sync_all()

# send transaction that should not use all the available outputs
# per the current coin selection algorithm
self.nodes[1].sendtoaddress(ret_addr, 5)

# getbalances and listunspent should show the remaining outputs
# in the reused address as used/reused
assert_unspent(self.nodes[1], total_count=2, total_sum=6, reused_count=1, reused_sum=1)
assert_balances(self.nodes[1], mine={"used": 1, "trusted": 5})

if __name__ == '__main__':
AvoidReuseTest().main()

0 comments on commit 3b125bf

Please sign in to comment.