mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-02 09:46:52 -05:00
Merge #17843: wallet: Reset reused transactions cache
6fc554f591
wallet: Reset reused transactions cache (Fabian Jahr) Pull request description: Fixes #17603 (together with #17824) `getbalances` is using the cache within `GetAvailableCredit` under certain conditions [here](35fff5be60/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 #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-ACK6fc554f591
promag: ACK6fc554f591
. achow101: Re-ACK6fc554f591
meshcollider: Code review ACK6fc554f591
Tree-SHA512: c4cad2c752176d16d77b4a4202291d20baddf9f27250896a40274d74a6945e0f6b34be04c2f9b1b2e756d3ac669b794969df8f82a98e0b16f10e92f276649ea2
This commit is contained in:
commit
ac61ec9da6
3 changed files with 63 additions and 5 deletions
|
@ -708,7 +708,7 @@ bool CWallet::MarkReplaced(const uint256& originalHash, const uint256& newHash)
|
|||
return success;
|
||||
}
|
||||
|
||||
void CWallet::SetUsedDestinationState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used)
|
||||
void CWallet::SetUsedDestinationState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used, std::set<CTxDestination>& tx_destinations)
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
const CWalletTx* srctx = GetWalletTx(hash);
|
||||
|
@ -718,7 +718,9 @@ void CWallet::SetUsedDestinationState(WalletBatch& batch, const uint256& hash, u
|
|||
if (ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst)) {
|
||||
if (IsMine(dst)) {
|
||||
if (used && !GetDestData(dst, "used", nullptr)) {
|
||||
AddDestData(batch, dst, "used", "p"); // p for "present", opposite of absent (null)
|
||||
if (AddDestData(batch, dst, "used", "p")) { // p for "present", opposite of absent (null)
|
||||
tx_destinations.insert(dst);
|
||||
}
|
||||
} else if (!used && GetDestData(dst, "used", nullptr)) {
|
||||
EraseDestData(batch, dst, "used");
|
||||
}
|
||||
|
@ -765,10 +767,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(batch, op.hash, op.n, true);
|
||||
SetUsedDestinationState(batch, op.hash, op.n, true, tx_destinations);
|
||||
}
|
||||
|
||||
MarkDestinationsDirty(tx_destinations);
|
||||
}
|
||||
|
||||
// Inserts only if not already there, returns tx inserted or tx found
|
||||
|
@ -3162,6 +3168,21 @@ int64_t CWallet::GetOldestKeyPoolTime()
|
|||
return oldestKey;
|
||||
}
|
||||
|
||||
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(interfaces::Chain::Lock& locked_chain)
|
||||
{
|
||||
std::map<CTxDestination, CAmount> balances;
|
||||
|
|
|
@ -803,7 +803,7 @@ public:
|
|||
|
||||
// Whether this or any known UTXO with the same single key has been spent.
|
||||
bool IsUsedDestination(const uint256& hash, unsigned int n) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void SetUsedDestinationState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
void SetUsedDestinationState(WalletBatch& batch, const uint256& hash, unsigned int n, bool used, std::set<CTxDestination>& tx_destinations) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
|
||||
|
||||
std::vector<OutputGroup> GroupOutputs(const std::vector<COutput>& outputs, bool single_coin) const;
|
||||
|
||||
|
@ -963,6 +963,12 @@ public:
|
|||
|
||||
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 OutputType type, const std::string label, CTxDestination& dest, std::string& error);
|
||||
bool GetNewChangeDestination(const OutputType type, CTxDestination& dest, std::string& error);
|
||||
|
||||
|
|
|
@ -91,7 +91,8 @@ class AvoidReuseTest(BitcoinTestFramework):
|
|||
self.test_fund_send_fund_send("p2sh-segwit")
|
||||
reset_balance(self.nodes[1], self.nodes[0].getnewaddress())
|
||||
self.test_fund_send_fund_send("bech32")
|
||||
|
||||
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.'''
|
||||
|
@ -257,5 +258,35 @@ class AvoidReuseTest(BitcoinTestFramework):
|
|||
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()
|
||||
|
|
Loading…
Add table
Reference in a new issue