Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wallet: Keep track of the wallet's own transaction outputs in memory #27286

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

achow101
Copy link
Member

@achow101 achow101 commented Mar 20, 2023

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 WalletTXOs 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 bra/op miss% total benchmark
92,245,141.50 10.84 0.1% 988,823,975.00 66,803,340.50 0.0% 2.04 WalletAvailableCoins
5,709.90 175,134.50 0.5% 80,968.24 25,539.15 0.1% 0.01 WalletBalanceClean
139,396.17 7,173.80 0.6% 1,383,390.50 430,276.86 0.0% 0.01 WalletBalanceDirty
5,055.80 197,792.47 0.3% 80,968.10 25,539.02 0.1% 0.01 WalletBalanceMine
9.79 102,152,396.19 0.1% 161.00 37.00 0.0% 0.01 WalletBalanceWatch
1,552,736.00 644.02 1.5% 20,316,315.80 618,545.80 0.6% 0.08 WalletCreateTxUseOnlyPresetInputs
114,114,732.00 8.76 0.5% 1,291,047,717.60 320,244,602.00 0.0% 6.30 WalletCreateTxUsePresetInputsAndCoinSelection
359,315,754.00 2.78 0.1% 4,339,447,818.00 136,619,757.00 0.7% 1.80 WalletLoadingDescriptors
98,230,601.00 10.18 0.1% 537,688,964.00 97,332,266.00 0.3% 0.49 WalletLoadingLegacy

PR:

ns/op op/s err% ins/op bra/op miss% total benchmark
75,319,868.50 13.28 0.2% 863,758,229.00 30,892,593.00 0.2% 1.66 WalletAvailableCoins
2,367.62 422,364.95 1.0% 35,785.05 9,893.01 0.2% 0.01 WalletBalanceClean
2,685.58 372,359.55 0.2% 36,501.05 10,027.01 0.1% 0.01 WalletBalanceDirty
3,462.24 288,830.68 2.7% 35,785.06 9,893.01 0.3% 0.01 WalletBalanceMine
11.65 85,838,176.97 0.1% 180.00 42.00 0.0% 0.01 WalletBalanceWatch
1,563,092.60 639.76 1.5% 20,426,154.40 649,953.80 0.6% 0.09 WalletCreateTxUseOnlyPresetInputs
58,367,804.40 17.13 0.9% 587,164,005.00 107,905,843.80 0.1% 3.21 WalletCreateTxUsePresetInputsAndCoinSelection
365,302,636.00 2.74 0.2% 4,349,345,147.00 138,730,668.00 0.8% 1.83 WalletLoadingDescriptors
124,995,585.00 8.00 1.2% 801,998,316.00 103,210,721.00 0.3% 0.63 WalletLoadingLegacy

@DrahtBot
Copy link
Contributor

DrahtBot commented Mar 20, 2023

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/27286.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK murchandamus
Concept ACK remyers, jonatack
Stale ACK ishaanam

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #30221 (wallet: Ensure best block matches wallet scan state by achow101)
  • #29062 (Wallet: (Refactor) GetBalance to calculate used balance by BrandonOdiwuor)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

This was referenced Mar 21, 2023
Need to load txs last so that IsMine works.
When loading transactions to the wallet, check the outputs, and keep
track of the ones that are IsMine.
Since we track the outputs owned by the wallet with m_txos, we can now
calculate the balance of the wallet by iterating m_txos and summing up
the amounts of the unspent txos.

As ISMINE_USED is not an actual isminetype that we attach to outputs and
was just passed into `CachedTxGetAvailableCredit` for convenience, we
pull the same determining logic from that function into `GetBalances` in
order to preserve existing behavior.
Instead of iterating every transaction and every output stored in wallet
when trying to figure out what outputs can be spent, iterate the TXO set
which should be a lot smaller.
Instead of searching mapWallet for the preselected inputs, search
m_txos.

wallet_fundrawtransaction.py spends external inputs and needs the change
output to also belong to the test wallet for the oversized tx test.
When a legacy wallet has been migrated to contain descriptors, but
before the transactions have been updated to match, we need to recompute
the wallet TXOs so that the transaction update will work correctly.
Instead of looking up the previous tx in mapWallet, and then calling
IsMine on the specified output, use the TXO set and its cached IsMine
value.
These two functions are no longer used as GetBalances now uses the TXO
set rather than per-tx cached balances
One of the main issues with AvailableCoins is its performance when txs
have unrelated outputs, so update the benchmark to check the performance
of that.
@jonatack
Copy link
Member

Concept ACK, if this patchset can meaningfully improve performance and reliably refresh wallet utxos to keep the in-memory cache in sync.

@DrahtBot DrahtBot requested a review from jonatack January 24, 2025 19:48
Copy link
Contributor

@murchandamus murchandamus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReACK 48545bd

mahmoudeA

This comment was marked as spam.

@bitcoin bitcoin deleted a comment from mahmoudeA Feb 23, 2025
@bitcoin bitcoin deleted a comment from mahmoudeA Feb 23, 2025
@jonatack
Copy link
Member

jonatack commented Mar 7, 2025

I plan to review this soon.

@@ -4621,4 +4627,40 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const
}
return std::nullopt;
}

void CWallet::RefreshSingleTxTXOs(const CWalletTx& wtx)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method name looks funky. SingleTxTXO is short for single transaction transaction output?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants