Skip to content

Commit 59bd6b6

Browse files
committed
Merge bitcoin/bitcoin#24699: wallet: Improve AvailableCoins performance by reducing duplicated operations
bc886fc Change mapWallet to be a std::unordered_map (Andrew Chow) 2723560 Change getWalletTxs to return a set instead of a vector (Andrew Chow) 9753286 Change mapTxSpends to be a std::unordered_multimap (Andrew Chow) 1f798fe wallet: Cache SigningProviders (Andrew Chow) 8a105ec wallet: Use CalculateMaximumSignedInputSize to indicate solvability (Andrew Chow) Pull request description: 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. ACKs for top commit: Xekyo: ACK bc886fc furszy: diff re-reACK bc886fc Tree-SHA512: fd710fe1224ef67d2bb83d6ac9e7428d9f76a67f14085915f9d80e1a492d2c51cb912edfcaad1db11c2edf8d2d97eb7ddd95bfb364587fb1f143490fd72c9ec1
2 parents 35305c7 + bc886fc commit 59bd6b6

File tree

9 files changed

+62
-36
lines changed

9 files changed

+62
-36
lines changed

src/interfaces/wallet.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ class Wallet
183183
virtual WalletTx getWalletTx(const uint256& txid) = 0;
184184

185185
//! Get list of all wallet transactions.
186-
virtual std::vector<WalletTx> getWalletTxs() = 0;
186+
virtual std::set<WalletTx> getWalletTxs() = 0;
187187

188188
//! Try to get updated status for a particular transaction, if possible without blocking.
189189
virtual bool tryGetTxStatus(const uint256& txid,
@@ -395,6 +395,8 @@ struct WalletTx
395395
int64_t time;
396396
std::map<std::string, std::string> value_map;
397397
bool is_coinbase;
398+
399+
bool operator<(const WalletTx& a) const { return tx->GetHash() < a.tx->GetHash(); }
398400
};
399401

400402
//! Updated transaction status.

src/wallet/feebumper.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ namespace wallet {
2121
//! mined, or conflicts with a mined transaction. Return a feebumper::Result.
2222
static feebumper::Result PreconditionChecks(const CWallet& wallet, const CWalletTx& wtx, std::vector<bilingual_str>& errors) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
2323
{
24-
if (wallet.HasWalletSpend(wtx.GetHash())) {
24+
if (wallet.HasWalletSpend(wtx.tx)) {
2525
errors.push_back(Untranslated("Transaction has descendants in the wallet"));
2626
return feebumper::Result::INVALID_PARAMETER;
2727
}

src/wallet/interfaces.cpp

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,12 @@ class WalletImpl : public Wallet
320320
}
321321
return {};
322322
}
323-
std::vector<WalletTx> getWalletTxs() override
323+
std::set<WalletTx> getWalletTxs() override
324324
{
325325
LOCK(m_wallet->cs_wallet);
326-
std::vector<WalletTx> result;
327-
result.reserve(m_wallet->mapWallet.size());
326+
std::set<WalletTx> result;
328327
for (const auto& entry : m_wallet->mapWallet) {
329-
result.emplace_back(MakeWalletTx(*m_wallet, entry.second));
328+
result.emplace(MakeWalletTx(*m_wallet, entry.second));
330329
}
331330
return result;
332331
}

src/wallet/scriptpubkeyman.cpp

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2075,10 +2075,21 @@ std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvid
20752075
std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvider(int32_t index, bool include_private) const
20762076
{
20772077
AssertLockHeld(cs_desc_man);
2078-
// Get the scripts, keys, and key origins for this script
2078+
20792079
std::unique_ptr<FlatSigningProvider> out_keys = std::make_unique<FlatSigningProvider>();
2080-
std::vector<CScript> scripts_temp;
2081-
if (!m_wallet_descriptor.descriptor->ExpandFromCache(index, m_wallet_descriptor.cache, scripts_temp, *out_keys)) return nullptr;
2080+
2081+
// Fetch SigningProvider from cache to avoid re-deriving
2082+
auto it = m_map_signing_providers.find(index);
2083+
if (it != m_map_signing_providers.end()) {
2084+
*out_keys = Merge(*out_keys, it->second);
2085+
} else {
2086+
// Get the scripts, keys, and key origins for this script
2087+
std::vector<CScript> scripts_temp;
2088+
if (!m_wallet_descriptor.descriptor->ExpandFromCache(index, m_wallet_descriptor.cache, scripts_temp, *out_keys)) return nullptr;
2089+
2090+
// Cache SigningProvider so we don't need to re-derive if we need this SigningProvider again
2091+
m_map_signing_providers[index] = *out_keys;
2092+
}
20822093

20832094
if (HavePrivateKeys() && include_private) {
20842095
FlatSigningProvider master_provider;

src/wallet/scriptpubkeyman.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,8 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
547547

548548
KeyMap GetKeys() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
549549

550+
// Cached FlatSigningProviders to avoid regenerating them each time they are needed.
551+
mutable std::map<int32_t, FlatSigningProvider> m_map_signing_providers;
550552
// Fetch the SigningProvider for the given script and optionally include private keys
551553
std::unique_ptr<FlatSigningProvider> GetSigningProvider(const CScript& script, bool include_private = false) const;
552554
// Fetch the SigningProvider for the given pubkey and always include private keys. This should only be called by signing code.

src/wallet/spend.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,10 @@ CoinsResult AvailableCoins(const CWallet& wallet,
213213

214214
std::unique_ptr<SigningProvider> provider = wallet.GetSolvingProvider(output.scriptPubKey);
215215

216-
bool solvable = provider ? IsSolvable(*provider, output.scriptPubKey) : false;
216+
int input_bytes = CalculateMaximumSignedInputSize(output, COutPoint(), provider.get(), coinControl);
217+
// Because CalculateMaximumSignedInputSize just uses ProduceSignature and makes a dummy signature,
218+
// it is safe to assume that this input is solvable if input_bytes is greater -1.
219+
bool solvable = input_bytes > -1;
217220
bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable));
218221

219222
// Filter by spendable outputs only
@@ -243,7 +246,6 @@ CoinsResult AvailableCoins(const CWallet& wallet,
243246
type = Solver(output.scriptPubKey, return_values_unused);
244247
}
245248

246-
int input_bytes = CalculateMaximumSignedInputSize(output, COutPoint(), provider.get(), coinControl);
247249
COutput coin(outpoint, output, nDepth, input_bytes, spendable, solvable, safeTx, wtx.GetTxTime(), tx_from_me, feerate);
248250
switch (type) {
249251
case TxoutType::WITNESS_UNKNOWN:

src/wallet/test/wallet_tests.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -843,16 +843,16 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
843843

844844
{
845845
auto block_hash = block_tx.GetHash();
846-
auto prev_hash = m_coinbase_txns[0]->GetHash();
846+
auto prev_tx = m_coinbase_txns[0];
847847

848848
LOCK(wallet->cs_wallet);
849-
BOOST_CHECK(wallet->HasWalletSpend(prev_hash));
849+
BOOST_CHECK(wallet->HasWalletSpend(prev_tx));
850850
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u);
851851

852852
std::vector<uint256> vHashIn{ block_hash }, vHashOut;
853853
BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vHashIn, vHashOut), DBErrors::LOAD_OK);
854854

855-
BOOST_CHECK(!wallet->HasWalletSpend(prev_hash));
855+
BOOST_CHECK(!wallet->HasWalletSpend(prev_tx));
856856
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u);
857857
}
858858

src/wallet/wallet.cpp

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& b
414414
const CWalletTx* CWallet::GetWalletTx(const uint256& hash) const
415415
{
416416
AssertLockHeld(cs_wallet);
417-
std::map<uint256, CWalletTx>::const_iterator it = mapWallet.find(hash);
417+
const auto it = mapWallet.find(hash);
418418
if (it == mapWallet.end())
419419
return nullptr;
420420
return &(it->second);
@@ -551,7 +551,7 @@ std::set<uint256> CWallet::GetConflicts(const uint256& txid) const
551551
std::set<uint256> result;
552552
AssertLockHeld(cs_wallet);
553553

554-
std::map<uint256, CWalletTx>::const_iterator it = mapWallet.find(txid);
554+
const auto it = mapWallet.find(txid);
555555
if (it == mapWallet.end())
556556
return result;
557557
const CWalletTx& wtx = it->second;
@@ -569,11 +569,17 @@ std::set<uint256> CWallet::GetConflicts(const uint256& txid) const
569569
return result;
570570
}
571571

572-
bool CWallet::HasWalletSpend(const uint256& txid) const
572+
bool CWallet::HasWalletSpend(const CTransactionRef& tx) const
573573
{
574574
AssertLockHeld(cs_wallet);
575-
auto iter = mapTxSpends.lower_bound(COutPoint(txid, 0));
576-
return (iter != mapTxSpends.end() && iter->first.hash == txid);
575+
const uint256& txid = tx->GetHash();
576+
for (unsigned int i = 0; i < tx->vout.size(); ++i) {
577+
auto iter = mapTxSpends.find(COutPoint(txid, i));
578+
if (iter != mapTxSpends.end()) {
579+
return true;
580+
}
581+
}
582+
return false;
577583
}
578584

579585
void CWallet::Flush()
@@ -636,7 +642,7 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const
636642

637643
for (TxSpends::const_iterator it = range.first; it != range.second; ++it) {
638644
const uint256& wtxid = it->second;
639-
std::map<uint256, CWalletTx>::const_iterator mit = mapWallet.find(wtxid);
645+
const auto mit = mapWallet.find(wtxid);
640646
if (mit != mapWallet.end()) {
641647
int depth = GetTxDepthInMainChain(mit->second);
642648
if (depth > 0 || (depth == 0 && !mit->second.isAbandoned()))
@@ -1197,12 +1203,13 @@ bool CWallet::AbandonTransaction(const uint256& hashTx)
11971203
batch.WriteTx(wtx);
11981204
NotifyTransactionChanged(wtx.GetHash(), CT_UPDATED);
11991205
// Iterate over all its outputs, and mark transactions in the wallet that spend them abandoned too
1200-
TxSpends::const_iterator iter = mapTxSpends.lower_bound(COutPoint(now, 0));
1201-
while (iter != mapTxSpends.end() && iter->first.hash == now) {
1202-
if (!done.count(iter->second)) {
1203-
todo.insert(iter->second);
1206+
for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
1207+
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(COutPoint(now, i));
1208+
for (TxSpends::const_iterator iter = range.first; iter != range.second; ++iter) {
1209+
if (!done.count(iter->second)) {
1210+
todo.insert(iter->second);
1211+
}
12041212
}
1205-
iter++;
12061213
}
12071214
// If a transaction changes 'conflicted' state, that changes the balance
12081215
// available of the outputs it spends. So force those to be recomputed
@@ -1248,12 +1255,13 @@ void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, c
12481255
wtx.MarkDirty();
12491256
batch.WriteTx(wtx);
12501257
// Iterate over all its outputs, and mark transactions in the wallet that spend them conflicted too
1251-
TxSpends::const_iterator iter = mapTxSpends.lower_bound(COutPoint(now, 0));
1252-
while (iter != mapTxSpends.end() && iter->first.hash == now) {
1253-
if (!done.count(iter->second)) {
1254-
todo.insert(iter->second);
1255-
}
1256-
iter++;
1258+
for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
1259+
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(COutPoint(now, i));
1260+
for (TxSpends::const_iterator iter = range.first; iter != range.second; ++iter) {
1261+
if (!done.count(iter->second)) {
1262+
todo.insert(iter->second);
1263+
}
1264+
}
12571265
}
12581266
// If a transaction changes 'conflicted' state, that changes the balance
12591267
// available of the outputs it spends. So force those to be recomputed
@@ -1370,7 +1378,7 @@ CAmount CWallet::GetDebit(const CTxIn &txin, const isminefilter& filter) const
13701378
{
13711379
{
13721380
LOCK(cs_wallet);
1373-
std::map<uint256, CWalletTx>::const_iterator mi = mapWallet.find(txin.prevout.hash);
1381+
const auto mi = mapWallet.find(txin.prevout.hash);
13741382
if (mi != mapWallet.end())
13751383
{
13761384
const CWalletTx& prev = (*mi).second;
@@ -1959,7 +1967,7 @@ bool CWallet::SignTransaction(CMutableTransaction& tx) const
19591967
// Build coins map
19601968
std::map<COutPoint, Coin> coins;
19611969
for (auto& input : tx.vin) {
1962-
std::map<uint256, CWalletTx>::const_iterator mi = mapWallet.find(input.prevout.hash);
1970+
const auto mi = mapWallet.find(input.prevout.hash);
19631971
if(mi == mapWallet.end() || input.prevout.n >= mi->second.tx->vout.size()) {
19641972
return false;
19651973
}

src/wallet/wallet.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <policy/feerate.h>
1515
#include <psbt.h>
1616
#include <tinyformat.h>
17+
#include <util/hasher.h>
1718
#include <util/message.h>
1819
#include <util/result.h>
1920
#include <util/strencodings.h>
@@ -37,6 +38,7 @@
3738
#include <stdint.h>
3839
#include <string>
3940
#include <utility>
41+
#include <unordered_map>
4042
#include <vector>
4143

4244
#include <boost/signals2/signal.hpp>
@@ -259,7 +261,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
259261
* detect and report conflicts (double-spends or
260262
* mutated transactions where the mutant gets mined).
261263
*/
262-
typedef std::multimap<COutPoint, uint256> TxSpends;
264+
typedef std::unordered_multimap<COutPoint, uint256, SaltedOutpointHasher> TxSpends;
263265
TxSpends mapTxSpends GUARDED_BY(cs_wallet);
264266
void AddToSpends(const COutPoint& outpoint, const uint256& wtxid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
265267
void AddToSpends(const CWalletTx& wtx, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
@@ -390,7 +392,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
390392

391393
/** Map from txid to CWalletTx for all transactions this wallet is
392394
* interested in, including received and sent transactions. */
393-
std::map<uint256, CWalletTx> mapWallet GUARDED_BY(cs_wallet);
395+
std::unordered_map<uint256, CWalletTx, SaltedTxidHasher> mapWallet GUARDED_BY(cs_wallet);
394396

395397
typedef std::multimap<int64_t, CWalletTx*> TxItems;
396398
TxItems wtxOrdered;
@@ -712,7 +714,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
712714
std::set<uint256> GetConflicts(const uint256& txid) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
713715

714716
//! Check if a given transaction has any of its outputs spent by another transaction in the wallet
715-
bool HasWalletSpend(const uint256& txid) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
717+
bool HasWalletSpend(const CTransactionRef& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
716718

717719
//! Flush wallet (bitdb flush)
718720
void Flush();

0 commit comments

Comments
 (0)