Skip to content

Commit b1821d8

Browse files
committed
Merge bitcoin/bitcoin#27286: wallet: Keep track of the wallet's own transaction outputs in memory
215e599 wallet: Remove unused CachedTxGet{Available,Immature}Credit (Ava Chow) 49675de wallet: Have GetDebit use the wallet's TXO set (Ava Chow) 17d453c wallet: Recompute wallet TXOs after descriptor migration (Ava Chow) 764016e wallet: Retrieve TXO directly in FetchSelectedInputs (Ava Chow) c1801b7 wallet: Use wallet's TXO set in AvailableCoins (Ava Chow) dde7cbe wallet: Change balance calculation to use m_txos (Ava Chow) 96e7a89 wallet: Recalculate the wallet's txos after any imports (Ava Chow) ae888c3 wallet: Exit IsTrustedTx early if wtx is already in trusted_parents (Ava Chow) ae0876e wallet: Keep track of transaction outputs owned by the wallet (Ava Chow) 0f269bc walletdb: Load Txs last (Ava Chow) 5cc32ee test: Test for balance update due to untracked output becoming spendable (Ava Chow) 8222341 wallet: MarkDirty after AddWalletDescriptor (Ava Chow) e02f2d3 bench: Have AvailableCoins benchmark include a lot of unrelated utxos (Ava Chow) Pull request description: Currently, the wallet is not actually aware about its own transaction outputs. Instead, it will iterate all of the transactions stored in `mapWallet`, and then all of the outputs of those transactions, in order to figure out what belongs to it for the purposes of coin selection and balance calculation. For balance calculation, there is caching that results in it only iterating all of the transactions, but not all of the outputs. However when the cache is dirty, everything is iterated. This is especially problematic for wallets that have a lot of transactions, or transactions that have a lot of unrelated outputs (as may occur with coinjoins or batched payments). This PR helps to resolve this issue by making the wallet track all of the outputs that belong to it in a new member `m_txos`. Note that this includes outputs that may have already been spent. Both balance calculation (`GetBalance`) and coin selection (`AvailableCoins`) are updated to iterate `m_txos`. This is generally faster since it ignores all of the unrelated outputs, and it is not slower as in the worst case of wallets containing only single output transactions, it's exactly the same number of outputs iterated. `m_txos` is memory only, and it is populated during wallet loading. When each transaction is loaded, all of its outputs are checked to see if it is `IsMine`, and if so, an entry added to `m_txos`. When new transactions are received, the same procedure is done. Since imports can change the `IsMine` status of a transaction (although they can only be "promoted" from watchonly to spendable), all of the import RPCs will be a bit slower as they re-iterate all transactions and all outputs to update `m_txos`. Each output in `m_txos` is stored in a new `WalletTXO` class. This class contains references to the parent `CWalletTx` and the `CTxOut` itself. It also caches the `IsMine` value of the txout. This should be safe as `IsMine` should not change unless there are imports. This allows us to have additional performance improvements in places that use these `WalletTXO`s as they can use the cached `IsMine` rather than repeatedly calling `IsMine` which can be expensive. The existing `WalletBalance` benchmark demonstrates the performance improvement that this PR makes. The existing `WalletAvailableCoins` benchmark doesn't as all of the outputs used in that benchmark belong to the test wallet. I've updated that benchmark to have a bunch of unrelated outputs in each transaction so that the difference is demonstrated. This is part of a larger project to have the wallet actually track and store a set of its UTXOs. Built on #24914 as it requires loading the txs last in order for `m_txos` to be built correctly. *** ## Benchmarks: Master: | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 34,590,013.00 | 28.91 | 0.0% | 812,669,269.00 | 148,360,642.50 | 5.478 | 18,356,853.00 | 0.2% | 0.76 | `WalletAvailableCoins` | 3,193.46 | 313,139.91 | 0.4% | 96,868.06 | 13,731.82 | 7.054 | 26,238.01 | 0.1% | 0.01 | `WalletBalanceClean` | 26,871.18 | 37,214.59 | 3.3% | 768,179.50 | 115,544.39 | 6.648 | 154,171.09 | 0.1% | 0.01 | `WalletBalanceDirty` | 3,177.30 | 314,732.47 | 0.2% | 96,868.06 | 13,646.20 | 7.099 | 26,238.01 | 0.1% | 0.01 | `WalletBalanceMine` | 10.73 | 93,186,952.53 | 0.1% | 157.00 | 46.14 | 3.403 | 36.00 | 0.0% | 0.01 | `WalletBalanceWatch` | 590,497,920.00 | 1.69 | 0.1% |12,761,692,005.00 |2,536,899,595.00 | 5.030 | 129,124,399.00 | 0.7% | 6.50 | `WalletCreateEncrypted` | 182,929,529.00 | 5.47 | 0.0% |4,199,271,397.00 | 785,477,302.00 | 5.346 | 75,363,377.00 | 1.1% | 2.01 | `WalletCreatePlain` | 699,337.20 | 1,429.93 | 0.7% | 18,054,294.00 | 3,005,072.20 | 6.008 | 387,756.60 | 0.3% | 0.04 | `WalletCreateTxUseOnlyPresetInputs` | 32,068,583.80 | 31.18 | 0.5% | 562,026,110.00 | 137,457,635.60 | 4.089 | 90,667,459.40 | 0.3% | 1.78 | `WalletCreateTxUsePresetInputsAndCoinSelection` | 36.62 | 27,306,578.40 | 0.5% | 951.00 | 157.05 | 6.056 | 133.00 | 0.0% | 0.01 | `WalletIsMineDescriptors` | 35.00 | 28,569,989.42 | 0.7% | 937.00 | 150.33 | 6.233 | 129.00 | 0.0% | 0.01 | `WalletIsMineMigratedDescriptors` | 203,284,889.00 | 4.92 | 0.0% |4,622,691,895.00 | 872,875,275.00 | 5.296 | 90,345,002.00 | 1.2% | 1.02 | `WalletLoadingDescriptors` | 1,165,766,084.00 | 0.86 | 0.0% |24,139,316,211.00 |5,005,218,705.00 | 4.823 |2,664,455,775.00 | 0.1% | 1.17 | `WalletMigration` PR: | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- | 33,975,750.50 | 29.43 | 0.1% | 794,719,150.50 | 145,763,550.00 | 5.452 | 16,036,630.50 | 0.2% | 0.75 | `WalletAvailableCoins` | 2,442.01 | 409,498.46 | 0.2% | 60,782.04 | 10,500.60 | 5.788 | 9,492.01 | 0.3% | 0.01 | `WalletBalanceClean` | 2,763.12 | 361,909.21 | 0.2% | 61,493.05 | 11,859.48 | 5.185 | 9,625.01 | 0.2% | 0.01 | `WalletBalanceDirty` | 2,347.98 | 425,898.72 | 0.3% | 60,782.04 | 10,082.73 | 6.028 | 9,492.01 | 0.2% | 0.01 | `WalletBalanceMine` | 11.67 | 85,654,630.36 | 0.2% | 176.00 | 50.18 | 3.508 | 40.00 | 0.0% | 0.01 | `WalletBalanceWatch` | 590,119,519.00 | 1.69 | 0.1% |12,754,398,258.00 |2,534,998,522.00 | 5.031 | 129,078,027.00 | 0.7% | 6.50 | `WalletCreateEncrypted` | 183,124,790.00 | 5.46 | 0.1% |4,199,212,926.00 | 786,323,886.00 | 5.340 | 75,354,437.00 | 1.1% | 2.02 | `WalletCreatePlain` | 669,643.00 | 1,493.33 | 0.1% | 17,213,904.20 | 2,877,336.40 | 5.983 | 394,292.80 | 0.3% | 0.04 | `WalletCreateTxUseOnlyPresetInputs` | 26,205,987.80 | 38.16 | 0.8% | 365,551,340.80 | 112,376,905.20 | 3.253 | 65,684,276.20 | 0.4% | 1.44 | `WalletCreateTxUsePresetInputsAndCoinSelection` | 34.75 | 28,778,846.38 | 0.1% | 937.00 | 149.41 | 6.271 | 129.00 | 0.0% | 0.01 | `WalletIsMineDescriptors` | 29.91 | 33,428,072.85 | 0.2% | 920.00 | 128.63 | 7.152 | 126.00 | 0.0% | 0.01 | `WalletIsMineMigratedDescriptors` | 202,437,985.00 | 4.94 | 0.1% |4,626,686,256.00 | 869,439,274.00 | 5.321 | 90,961,305.00 | 1.1% | 1.02 | `WalletLoadingDescriptors` | 1,158,394,152.00 | 0.86 | 0.0% |24,143,589,972.00 |4,971,946,380.00 | 4.856 |2,665,355,654.00 | 0.1% | 1.16 | `WalletMigration` ACKs for top commit: davidgumberg: untested reACK 215e599 murchandamus: reACK 215e599 ishaanam: reACK 215e599 w0xlt: reACK bitcoin/bitcoin@215e599 Tree-SHA512: d6b929de56f67930678db654e46f15fb71008390189c701a026b2d76af8f14a7c9769e49835ce7e2b6515d2934a77aad8de0b1a82231a2e1de5337de25db9629
2 parents ed7a841 + 215e599 commit b1821d8

File tree

12 files changed

+297
-306
lines changed

12 files changed

+297
-306
lines changed

src/bench/wallet_create_tx.cpp

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,17 @@ void generateFakeBlock(const CChainParams& params,
7171
coinbase_tx.vin[0].prevout.SetNull();
7272
coinbase_tx.vout.resize(2);
7373
coinbase_tx.vout[0].scriptPubKey = coinbase_out_script;
74-
coinbase_tx.vout[0].nValue = 49 * COIN;
74+
coinbase_tx.vout[0].nValue = 48 * COIN;
7575
coinbase_tx.vin[0].scriptSig = CScript() << ++tip.tip_height << OP_0;
7676
coinbase_tx.vout[1].scriptPubKey = coinbase_out_script; // extra output
7777
coinbase_tx.vout[1].nValue = 1 * COIN;
78+
79+
// Fill the coinbase with outputs that don't belong to the wallet in order to benchmark
80+
// AvailableCoins' behavior with unnecessary TXOs
81+
for (int i = 0; i < 50; ++i) {
82+
coinbase_tx.vout.emplace_back(1 * COIN / 50, CScript(OP_TRUE));
83+
}
84+
7885
block.vtx = {MakeTransactionRef(std::move(coinbase_tx))};
7986

8087
block.nVersion = VERSIONBITS_LAST_OLD_BLOCK_VERSION;
@@ -129,14 +136,14 @@ static void WalletCreateTx(benchmark::Bench& bench, const OutputType output_type
129136

130137
// Check available balance
131138
auto bal = WITH_LOCK(wallet.cs_wallet, return wallet::AvailableCoins(wallet).GetTotalAmount()); // Cache
132-
assert(bal == 50 * COIN * (chain_size - COINBASE_MATURITY));
139+
assert(bal == 49 * COIN * (chain_size - COINBASE_MATURITY));
133140

134141
wallet::CCoinControl coin_control;
135142
coin_control.m_allow_other_inputs = allow_other_inputs;
136143

137144
CAmount target = 0;
138145
if (preset_inputs) {
139-
// Select inputs, each has 49 BTC
146+
// Select inputs, each has 48 BTC
140147
wallet::CoinFilterParams filter_coins;
141148
filter_coins.max_count = preset_inputs->num_of_internal_inputs;
142149
const auto& res = WITH_LOCK(wallet.cs_wallet,
@@ -152,7 +159,7 @@ static void WalletCreateTx(benchmark::Bench& bench, const OutputType output_type
152159
if (coin_control.m_allow_other_inputs) target += 50 * COIN;
153160
std::vector<wallet::CRecipient> recipients = {{dest, target, true}};
154161

155-
bench.epochIterations(5).run([&] {
162+
bench.run([&] {
156163
LOCK(wallet.cs_wallet);
157164
const auto& tx_res = CreateTransaction(wallet, recipients, /*change_pos=*/std::nullopt, coin_control);
158165
assert(tx_res);
@@ -189,9 +196,9 @@ static void AvailableCoins(benchmark::Bench& bench, const std::vector<OutputType
189196

190197
// Check available balance
191198
auto bal = WITH_LOCK(wallet.cs_wallet, return wallet::AvailableCoins(wallet).GetTotalAmount()); // Cache
192-
assert(bal == 50 * COIN * (chain_size - COINBASE_MATURITY));
199+
assert(bal == 49 * COIN * (chain_size - COINBASE_MATURITY));
193200

194-
bench.epochIterations(2).run([&] {
201+
bench.run([&] {
195202
LOCK(wallet.cs_wallet);
196203
const auto& res = wallet::AvailableCoins(wallet);
197204
assert(res.All().size() == (chain_size - COINBASE_MATURITY) * 2);

src/wallet/receive.cpp

Lines changed: 44 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <consensus/amount.h>
66
#include <consensus/consensus.h>
7+
#include <util/check.h>
78
#include <wallet/receive.h>
89
#include <wallet/transaction.h>
910
#include <wallet/wallet.h>
@@ -145,52 +146,6 @@ CAmount CachedTxGetChange(const CWallet& wallet, const CWalletTx& wtx)
145146
return wtx.nChangeCached;
146147
}
147148

148-
CAmount CachedTxGetImmatureCredit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter)
149-
{
150-
AssertLockHeld(wallet.cs_wallet);
151-
152-
if (wallet.IsTxImmatureCoinBase(wtx) && wtx.isConfirmed()) {
153-
return GetCachableAmount(wallet, wtx, CWalletTx::IMMATURE_CREDIT, filter);
154-
}
155-
156-
return 0;
157-
}
158-
159-
CAmount CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter)
160-
{
161-
AssertLockHeld(wallet.cs_wallet);
162-
163-
// Avoid caching ismine for NO or ALL cases (could remove this check and simplify in the future).
164-
bool allow_cache = (filter & ISMINE_ALL) && (filter & ISMINE_ALL) != ISMINE_ALL;
165-
166-
// Must wait until coinbase is safely deep enough in the chain before valuing it
167-
if (wallet.IsTxImmatureCoinBase(wtx))
168-
return 0;
169-
170-
if (allow_cache && wtx.m_amounts[CWalletTx::AVAILABLE_CREDIT].m_cached[filter]) {
171-
return wtx.m_amounts[CWalletTx::AVAILABLE_CREDIT].m_value[filter];
172-
}
173-
174-
bool allow_used_addresses = (filter & ISMINE_USED) || !wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
175-
CAmount nCredit = 0;
176-
Txid hashTx = wtx.GetHash();
177-
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) {
178-
const CTxOut& txout = wtx.tx->vout[i];
179-
if (!wallet.IsSpent(COutPoint(hashTx, i)) && (allow_used_addresses || !wallet.IsSpentKey(txout.scriptPubKey))) {
180-
nCredit += OutputGetCredit(wallet, txout, filter);
181-
if (!MoneyRange(nCredit))
182-
throw std::runtime_error(std::string(__func__) + " : value out of range");
183-
}
184-
}
185-
186-
if (allow_cache) {
187-
wtx.m_amounts[CWalletTx::AVAILABLE_CREDIT].Set(filter, nCredit);
188-
wtx.m_is_cache_empty = false;
189-
}
190-
191-
return nCredit;
192-
}
193-
194149
void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
195150
std::list<COutputEntry>& listReceived,
196151
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter,
@@ -257,6 +212,10 @@ bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminef
257212
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<Txid>& trusted_parents)
258213
{
259214
AssertLockHeld(wallet.cs_wallet);
215+
216+
// This wtx is already trusted
217+
if (trusted_parents.contains(wtx.GetHash())) return true;
218+
260219
if (wtx.isConfirmed()) return true;
261220
if (wtx.isBlockConflicted()) return false;
262221
// using wtx's cached debit
@@ -293,27 +252,41 @@ bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx)
293252
Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse)
294253
{
295254
Balance ret;
296-
isminefilter reuse_filter = avoid_reuse ? ISMINE_NO : ISMINE_USED;
255+
bool allow_used_addresses = !avoid_reuse || !wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
297256
{
298257
LOCK(wallet.cs_wallet);
299258
std::set<Txid> trusted_parents;
300-
for (const auto& entry : wallet.mapWallet)
301-
{
302-
const CWalletTx& wtx = entry.second;
259+
for (const auto& [outpoint, txo] : wallet.GetTXOs()) {
260+
const CWalletTx& wtx = txo.GetWalletTx();
261+
303262
const bool is_trusted{CachedTxIsTrusted(wallet, wtx, trusted_parents)};
304263
const int tx_depth{wallet.GetTxDepthInMainChain(wtx)};
305-
const CAmount tx_credit_mine{CachedTxGetAvailableCredit(wallet, wtx, ISMINE_SPENDABLE | reuse_filter)};
306-
const CAmount tx_credit_watchonly{CachedTxGetAvailableCredit(wallet, wtx, ISMINE_WATCH_ONLY | reuse_filter)};
307-
if (is_trusted && tx_depth >= min_depth) {
308-
ret.m_mine_trusted += tx_credit_mine;
309-
ret.m_watchonly_trusted += tx_credit_watchonly;
310-
}
311-
if (!is_trusted && tx_depth == 0 && wtx.InMempool()) {
312-
ret.m_mine_untrusted_pending += tx_credit_mine;
313-
ret.m_watchonly_untrusted_pending += tx_credit_watchonly;
264+
265+
if (!wallet.IsSpent(outpoint) && (allow_used_addresses || !wallet.IsSpentKey(txo.GetTxOut().scriptPubKey))) {
266+
// Get the amounts for mine and watchonly
267+
CAmount credit_mine = 0;
268+
CAmount credit_watchonly = 0;
269+
if (txo.GetIsMine() == ISMINE_SPENDABLE) {
270+
credit_mine = txo.GetTxOut().nValue;
271+
} else if (txo.GetIsMine() == ISMINE_WATCH_ONLY) {
272+
credit_watchonly = txo.GetTxOut().nValue;
273+
} else {
274+
// We shouldn't see any other isminetypes
275+
Assume(false);
276+
}
277+
278+
// Set the amounts in the return object
279+
if (wallet.IsTxImmatureCoinBase(wtx) && wtx.isConfirmed()) {
280+
ret.m_mine_immature += credit_mine;
281+
ret.m_watchonly_immature += credit_watchonly;
282+
} else if (is_trusted && tx_depth >= min_depth) {
283+
ret.m_mine_trusted += credit_mine;
284+
ret.m_watchonly_trusted += credit_watchonly;
285+
} else if (!is_trusted && wtx.InMempool()) {
286+
ret.m_mine_untrusted_pending += credit_mine;
287+
ret.m_watchonly_untrusted_pending += credit_watchonly;
288+
}
314289
}
315-
ret.m_mine_immature += CachedTxGetImmatureCredit(wallet, wtx, ISMINE_SPENDABLE);
316-
ret.m_watchonly_immature += CachedTxGetImmatureCredit(wallet, wtx, ISMINE_WATCH_ONLY);
317290
}
318291
}
319292
return ret;
@@ -326,31 +299,21 @@ std::map<CTxDestination, CAmount> GetAddressBalances(const CWallet& wallet)
326299
{
327300
LOCK(wallet.cs_wallet);
328301
std::set<Txid> trusted_parents;
329-
for (const auto& walletEntry : wallet.mapWallet)
330-
{
331-
const CWalletTx& wtx = walletEntry.second;
332-
333-
if (!CachedTxIsTrusted(wallet, wtx, trusted_parents))
334-
continue;
302+
for (const auto& [outpoint, txo] : wallet.GetTXOs()) {
303+
const CWalletTx& wtx = txo.GetWalletTx();
335304

336-
if (wallet.IsTxImmatureCoinBase(wtx))
337-
continue;
305+
if (!CachedTxIsTrusted(wallet, wtx, trusted_parents)) continue;
306+
if (wallet.IsTxImmatureCoinBase(wtx)) continue;
338307

339308
int nDepth = wallet.GetTxDepthInMainChain(wtx);
340-
if (nDepth < (CachedTxIsFromMe(wallet, wtx, ISMINE_ALL) ? 0 : 1))
341-
continue;
309+
if (nDepth < (CachedTxIsFromMe(wallet, wtx, ISMINE_ALL) ? 0 : 1)) continue;
342310

343-
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) {
344-
const auto& output = wtx.tx->vout[i];
345-
CTxDestination addr;
346-
if (!wallet.IsMine(output))
347-
continue;
348-
if(!ExtractDestination(output.scriptPubKey, addr))
349-
continue;
311+
CTxDestination addr;
312+
Assume(wallet.IsMine(txo.GetTxOut()));
313+
if(!ExtractDestination(txo.GetTxOut().scriptPubKey, addr)) continue;
350314

351-
CAmount n = wallet.IsSpent(COutPoint(walletEntry.first, i)) ? 0 : output.nValue;
352-
balances[addr] += n;
353-
}
315+
CAmount n = wallet.IsSpent(outpoint) ? 0 : txo.GetTxOut().nValue;
316+
balances[addr] += n;
354317
}
355318
}
356319

src/wallet/receive.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ CAmount CachedTxGetCredit(const CWallet& wallet, const CWalletTx& wtx, const ism
3030
//! filter decides which addresses will count towards the debit
3131
CAmount CachedTxGetDebit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter);
3232
CAmount CachedTxGetChange(const CWallet& wallet, const CWalletTx& wtx);
33-
CAmount CachedTxGetImmatureCredit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter)
34-
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
35-
CAmount CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter = ISMINE_SPENDABLE)
36-
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
3733
struct COutputEntry
3834
{
3935
CTxDestination destination;

src/wallet/rpc/addresses.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ RPCHelpMan keypoolrefill()
250250
if (pwallet->GetKeyPoolSize() < kpSize) {
251251
throw JSONRPCError(RPC_WALLET_ERROR, "Error refreshing keypool.");
252252
}
253+
pwallet->RefreshAllTXOs();
253254

254255
return UniValue::VNULL;
255256
},

src/wallet/rpc/backup.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ RPCHelpMan importdescriptors()
394394
}
395395
}
396396
pwallet->ConnectScriptPubKeyManNotifiers();
397+
pwallet->RefreshAllTXOs();
397398
}
398399

399400
// Rescan the blockchain using the lowest timestamp

0 commit comments

Comments
 (0)