Skip to content

Conversation

achow101
Copy link
Member

While running my coin selection simulations, I noticed that towards the end of the simulation, the wallet would become slow to make new transactions. The wallet generally performs much more slowly when there are a large number of transactions and/or a large number of keys. The improvements here are focused on wallets with a large number of transactions as that is what the simulations produce.

Most of the slowdown I observed was due to DescriptorScriptPubKeyMan::GetSigningProvider re-deriving keys every time it is called. To avoid this, it will now cache the SigningProvider produced so that repeatedly fetching the SigningProvider for the same script will not result in the same key being derived over and over. This has a side effect of making the function non-const, which makes a lot of other functions non-const as well. This helps with wallets with lots of address reuse (as my coin selection simulations are), but not if addresses are not reused as keys will end up needing to be derived the first time GetSigningProvider is called for a script.

The GetSigningProvider problem was also exacerbated by unnecessarily fetching a SigningProvider for the same script multiple times. A SigningProvider is retrieved to be used inside of IsSolvable. A few lines later, we use GetTxSpendSize which fetches a SigningProvider and then calls CalculateMaximumSignedInputSize. We can avoid a second call to GetSigningProvider by using CalculateMaximumSignedInputSize directly with the SigningProvider already retrieved for IsSolvable.

There is an additional slowdown where ProduceSignature with a dummy signer is called twice for each output. The first time is IsSolvable checks that ProduceSignature succeeds, thereby informing whether we have solving data. The second is CalculateMaximumSignedInputSize which returns -1 if ProduceSignature fails, and returns the input size otherwise. We can reduce this to one call of ProduceSignature by using CalculateMaximumSignedInputSize's result to set solvable.

Lastly, a lot of time is spent looking in mapWallet and mapTxSpends to determine whether an output is already spent. The performance of these lookups is slightly improved by changing those maps to use std::unordered_map and std::unordered_multimap respectively.

@DrahtBot
Copy link
Contributor

DrahtBot commented Mar 29, 2022

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

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #25695 (tidy: add modernize-use-using by fanquake)
  • #25664 (refactor: Redefine IsSolvable() using descriptors by darosior)
  • #25659 (wallet: simplify ListCoins implementation by furszy)
  • #23417 (wallet, spkm: Move key management from DescriptorScriptPubKeyMan to wallet level KeyManager by achow101)
  • #22693 (RPC/Wallet: Add "use_txids" to output of getaddressinfo by luke-jr)

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.

@josibake
Copy link
Member

Concept ACK

very nice, cursory glance looks good. do you have any benchmarks on how much this improves performance when dealing with large wallets with many transactions?

@achow101
Copy link
Member Author

achow101 commented Mar 29, 2022

do you have any benchmarks on how much this improves performance when dealing with large wallets with many transactions?

One of the simulation scenarios I ran has 10050 deposits and 4950 spends. It's runtime went from 4 hr 18 min to 38 min after the changes in this PR (plus one additional change to the simulation script which would apply a speedup that I'm not sure about, but would not dominate the total runtime).

Copy link
Contributor

@promag promag left a comment

Choose a reason for hiding this comment

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

Concept ACK

for (const auto& entry : m_wallet->mapWallet) {
result.emplace_back(MakeWalletTx(*m_wallet, entry.second));
result.emplace(MakeWalletTx(*m_wallet, entry.second));
Copy link
Contributor

Choose a reason for hiding this comment

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

d3a6bba

This seems to be a bad change for the GUI when loading a big wallet.

The order by hash requirement comes from TransactionTablePriv::updateWallet implementation, definitely something to improve/refactor.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it won't be too bad, but fixing that seems like a bigger change than suitable for this PR.

Copy link
Contributor

@w0xlt w0xlt left a comment

Choose a reason for hiding this comment

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

Code Review ACK e6e77b3

Copy link
Contributor

@w0xlt w0xlt left a comment

Choose a reason for hiding this comment

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

reACK 91c3f77

Copy link
Member

@furszy furszy left a comment

Choose a reason for hiding this comment

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

Code reviewed 91c3f77.

Two things to add:

  • About the signing providers cache (fcc2160):

    Seems that this going to cache the private keys when the wallet is unencrypted/unlocked and then keep them available in memory when the wallet gets encrypted/locked. --> update: meh nah.. could actually assert that the provider in the cache does not have a sk stored.

    Even when the changes are small, wouldn't hurt to add test coverage for it (like getting the signing provider without including the private keys, then get it with them, check cache update, etc).
    Maybe, if you are ok, I could work on it (not sure how much it will take me to do it but.. I can give it a shot to start getting deeper over the descriptors architecture).

  • I do agree with @promag comment for 3b8b47b but.. yeah, better to work on it on a future PR as well.

@@ -189,7 +189,7 @@ void AvailableCoins(const CWallet& wallet, std::vector<COutput>& vCoins, const C

bool solvable = provider ? IsSolvable(*provider, wtx.tx->vout[i].scriptPubKey) : false;
bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable));
int input_bytes = GetTxSpendSize(wallet, wtx, i, (coinControl && coinControl->fAllowWatchOnly));
int input_bytes = CalculateMaximumSignedInputSize(wtx.tx->vout[i], provider.get(), /*use_max_sig=*/(coinControl && coinControl->fAllowWatchOnly));
Copy link
Member

Choose a reason for hiding this comment

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

Seeing af54709:
nit: would be good to extract wtx.tx->vout[I] into its own ref variable at the top of the vout for loop (line 165). We are accessing the same value in the vector several times.

Copy link
Member Author

Choose a reason for hiding this comment

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

Will do this if I have to touch again.

@@ -187,10 +187,13 @@ void AvailableCoins(const CWallet& wallet, std::vector<COutput>& vCoins, const C

std::unique_ptr<SigningProvider> provider = wallet.GetSolvingProvider(wtx.tx->vout[i].scriptPubKey);

bool solvable = provider ? IsSolvable(*provider, wtx.tx->vout[i].scriptPubKey) : false;
Copy link
Member

Choose a reason for hiding this comment

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

Self-note for 7d64540:

The difference, aside from the clear speedup, is that we are no longer going to call VerifyScript after producing the dummy signature (which if would had failed in the past, the node would had crashed for the IsSolvable assertion).

@achow101
Copy link
Member Author

I wonder how this would perform by splitting up the COutPoint with this data structure:

using TxSpends = std::unordered_map<uint256, std::unordered_multimap<uint32_t, uint256>, SaltedOutpointHasher>;

This would get rid of the O(n) iterations of the whole vout.size(), which should therefor have a lot less lookups.

Also I'm not sure how fast the unordered_multimap is, maybe it's better to use a vector:

using TxSpends = std::unordered_map<uint256, std::unordered_map<uint32_t, std::vector<uint256>>, SaltedOutpointHasher>;

Hmm, that would probably be better. I'll try that. I don't think there's really a benchmark for this so it'll be hard to measure the effect. I think the second solution is also faster as getting the range from the multimap appears to be O(n) on average too.

Seems that this going to cache the private keys when the wallet is unencrypted/unlocked and then keep them available in memory when the wallet gets encrypted/locked. --> update: meh nah.. could actually assert that the provider in the cache does not have a sk stored.

This was considered and it specifically only caches the SigningProvider before private keys are added by making a copy.

Even when the changes are small, wouldn't hurt to add test coverage for it (like getting the signing provider without including the private keys, then get it with them, check cache update, etc).
Maybe, if you are ok, I could work on it (not sure how much it will take me to do it but.. I can give it a shot to start getting deeper over the descriptors architecture).

Feel free to try. You can reach out to me if you have any questions.

@achow101
Copy link
Member Author

I wonder how this would perform by splitting up the COutPoint with this data structure:

Actually there are a lot of other places that mapTxSpends is used and splitting it up requires making a ton of changes throughout the wallet to deal with it. The vast majority of the usage of mapTxSpends is to do lookups by outpoint.

@furszy
Copy link
Member

furszy commented Apr 26, 2022

If you like it, feel free to cherry-pick it: furszy/bitcoin@42fbb69

Copy link
Member

@furszy furszy left a comment

Choose a reason for hiding this comment

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

code ACK 91c3f77

@fjahr
Copy link
Contributor

fjahr commented Jul 26, 2022

Code review re-ACK 1f61630

Copy link
Contributor

@w0xlt w0xlt left a comment

Choose a reason for hiding this comment

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

reACK 1f61630

In AvailableCoins, we need to know whether we can solve for an output.
This was done by using IsSolvable, which just calls ProduceSignature and
produces a dummy signature. However, we already do that in order to get
the size of the input by using CalculateMaximumSignedInputSize. As this
function returns -1 if ProduceSignature fails, we can just remove the
use of IsSolvable and check that input_bytes is not -1 to determine
the solvability of an output.
@achow101 achow101 force-pushed the faster-available-coins branch from 1f61630 to 38ead65 Compare July 29, 2022 15:18
@josibake
Copy link
Member

reACK 38ead65

verified rebase with git range-diff master 1f61630 38ead65

@fjahr
Copy link
Contributor

fjahr commented Jul 29, 2022

re-ACK 38ead65

Copy link
Member

@furszy furszy left a comment

Choose a reason for hiding this comment

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

diff ACK 38ead65

In order to avoid constantly re-deriving the same keys in
DescriptorScriptPubKeyMan, cache the SigningProviders generated inside
of GetSigningProvider.
For some reason, the primary consumer of getWalletTxs requires the
transactions to be in hash order when it is processing them. std::map
will iterate in hash order so the transactions end up in that order when
placed into the vector. To ensure this order when mapWallet is no longer
ordered, the vector is replaced with a set which will maintain the hash
order.
@achow101 achow101 force-pushed the faster-available-coins branch from 38ead65 to bc886fc Compare August 3, 2022 19:38
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.

ACK bc886fc

Copy link
Member

@furszy furszy left a comment

Choose a reason for hiding this comment

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

diff re-reACK bc886fc

@achow101 achow101 merged commit 59bd6b6 into bitcoin:master Aug 5, 2022
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Aug 6, 2022
@bitcoinhodler
Copy link
Contributor

I filed #26406 to report an instability triggered by bc886fc

@murchandamus
Copy link
Contributor

I filed #26406 to report an instability triggered by bc886fc

Reading the linked issue, this has meanwhile been resolved.

achow101 added a commit that referenced this pull request Jan 4, 2023
3a4f8bc bench: add benchmark for wallet 'AvailableCoins' function. (furszy)

Pull request description:

  #### Rationale

  `AvailableCoins` is part of several important flows for the wallet; from RPC commands that create transactions like `fundrawtransaction`, `send`, `walletcreatefundedpsbt`, get the available balance, list the available coins with `listunspent` etc. to GUI connected processes that perform the same or similar actions: tx creation, available balance calculation, present the spendable coins in the coin control dialog.

  As we are improving this process in #24699, #25005 and there are more structural changes coming on the way. This benchmark aims to ensure us that, at least, there are no regressions (obviously performance improvements are great but, at least for me, this heads into the direction of having a base metric to compare future structural changes).

  #### Implementation Notes

  There are 5 new benchmarks, one per wallet supported output type (LEGACY, P2SH_SEGWIT, BECH32, BECH32M), plus a multi-output-type wallet benchmark which contains outputs from all the descriptor types.

  The test, by default, fills-up the wallet with 1k transactions, 2k outputs. Mainly to not consume much time if the user just want to verify that no substantial regressions were introduced. But, my expectation for those who are focused on this process is to use a much higher number locally to really note the differences across commits.

ACKs for top commit:
  achow101:
    ACK 3a4f8bc
  hernanmarino:
    ACK 3a4f8bc
  aureleoules:
    ACK 3a4f8bc

Tree-SHA512: d0bb4c165f1efa181b47cb31561e6217eff9135bcd1b6761a7292f9018e456d13d18a1b886c2e2268d35c52f9e1fd8e0f252972424e5c5f00c280620b79c5a1b
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Jan 4, 2023
… function.

3a4f8bc bench: add benchmark for wallet 'AvailableCoins' function. (furszy)

Pull request description:

  #### Rationale

  `AvailableCoins` is part of several important flows for the wallet; from RPC commands that create transactions like `fundrawtransaction`, `send`, `walletcreatefundedpsbt`, get the available balance, list the available coins with `listunspent` etc. to GUI connected processes that perform the same or similar actions: tx creation, available balance calculation, present the spendable coins in the coin control dialog.

  As we are improving this process in bitcoin#24699, bitcoin#25005 and there are more structural changes coming on the way. This benchmark aims to ensure us that, at least, there are no regressions (obviously performance improvements are great but, at least for me, this heads into the direction of having a base metric to compare future structural changes).

  #### Implementation Notes

  There are 5 new benchmarks, one per wallet supported output type (LEGACY, P2SH_SEGWIT, BECH32, BECH32M), plus a multi-output-type wallet benchmark which contains outputs from all the descriptor types.

  The test, by default, fills-up the wallet with 1k transactions, 2k outputs. Mainly to not consume much time if the user just want to verify that no substantial regressions were introduced. But, my expectation for those who are focused on this process is to use a much higher number locally to really note the differences across commits.

ACKs for top commit:
  achow101:
    ACK 3a4f8bc
  hernanmarino:
    ACK 3a4f8bc
  aureleoules:
    ACK 3a4f8bc

Tree-SHA512: d0bb4c165f1efa181b47cb31561e6217eff9135bcd1b6761a7292f9018e456d13d18a1b886c2e2268d35c52f9e1fd8e0f252972424e5c5f00c280620b79c5a1b
hebasto added a commit to bitcoin-core/gui that referenced this pull request Feb 3, 2023
c497a19 Fix comment about how wallet txs are sorted (John Moffett)

Pull request description:

  The wallet transactions in the node are not sorted by txid (or any hash) since bitcoin/bitcoin#24699.

  This is how they're stored in memory now:

  https://github.com/bitcoin-core/gui/blob/835212cd1d8f8fc7f19775f5ff8cc21c099122b2/src/wallet/wallet.h#L397-L399

ACKs for top commit:
  achow101:
    ACK c497a19
  jarolrod:
    ACK c497a19

Tree-SHA512: e72559991688452ef254474d4235dc75fac655bce04909c3a0eece907360f4c6f57707db9b4373a4bd2271b23c57e863684c33e0728adf48e477c5499cdfdad7
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Feb 3, 2023
… sorted

c497a19 Fix comment about how wallet txs are sorted (John Moffett)

Pull request description:

  The wallet transactions in the node are not sorted by txid (or any hash) since bitcoin#24699.

  This is how they're stored in memory now:

  https://github.com/bitcoin-core/gui/blob/835212cd1d8f8fc7f19775f5ff8cc21c099122b2/src/wallet/wallet.h#L397-L399

ACKs for top commit:
  achow101:
    ACK c497a19
  jarolrod:
    ACK c497a19

Tree-SHA512: e72559991688452ef254474d4235dc75fac655bce04909c3a0eece907360f4c6f57707db9b4373a4bd2271b23c57e863684c33e0728adf48e477c5499cdfdad7
@bitcoin bitcoin locked and limited conversation to collaborators Oct 28, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants