0
0
Fork 0
mirror of https://github.com/bitcoin/bitcoin.git synced 2025-03-06 14:19:59 -05:00

Merge bitcoin/bitcoin#24699: wallet: Improve AvailableCoins performance by reducing duplicated operations

bc886fcb31 Change mapWallet to be a std::unordered_map (Andrew Chow)
272356024d Change getWalletTxs to return a set instead of a vector (Andrew Chow)
97532867cf Change mapTxSpends to be a std::unordered_multimap (Andrew Chow)
1f798fe85b wallet: Cache SigningProviders (Andrew Chow)
8a105ecd1a 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 bc886fcb31
  furszy:
    diff re-reACK bc886fcb

Tree-SHA512: fd710fe1224ef67d2bb83d6ac9e7428d9f76a67f14085915f9d80e1a492d2c51cb912edfcaad1db11c2edf8d2d97eb7ddd95bfb364587fb1f143490fd72c9ec1
This commit is contained in:
Andrew Chow 2022-08-05 15:21:23 -04:00
commit 59bd6b6d37
No known key found for this signature in database
GPG key ID: 17565732E08E5E41
9 changed files with 62 additions and 36 deletions

View file

@ -183,7 +183,7 @@ public:
virtual WalletTx getWalletTx(const uint256& txid) = 0; virtual WalletTx getWalletTx(const uint256& txid) = 0;
//! Get list of all wallet transactions. //! Get list of all wallet transactions.
virtual std::vector<WalletTx> getWalletTxs() = 0; virtual std::set<WalletTx> getWalletTxs() = 0;
//! Try to get updated status for a particular transaction, if possible without blocking. //! Try to get updated status for a particular transaction, if possible without blocking.
virtual bool tryGetTxStatus(const uint256& txid, virtual bool tryGetTxStatus(const uint256& txid,
@ -395,6 +395,8 @@ struct WalletTx
int64_t time; int64_t time;
std::map<std::string, std::string> value_map; std::map<std::string, std::string> value_map;
bool is_coinbase; bool is_coinbase;
bool operator<(const WalletTx& a) const { return tx->GetHash() < a.tx->GetHash(); }
}; };
//! Updated transaction status. //! Updated transaction status.

View file

@ -21,7 +21,7 @@ namespace wallet {
//! mined, or conflicts with a mined transaction. Return a feebumper::Result. //! mined, or conflicts with a mined transaction. Return a feebumper::Result.
static feebumper::Result PreconditionChecks(const CWallet& wallet, const CWalletTx& wtx, std::vector<bilingual_str>& errors) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) static feebumper::Result PreconditionChecks(const CWallet& wallet, const CWalletTx& wtx, std::vector<bilingual_str>& errors) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
{ {
if (wallet.HasWalletSpend(wtx.GetHash())) { if (wallet.HasWalletSpend(wtx.tx)) {
errors.push_back(Untranslated("Transaction has descendants in the wallet")); errors.push_back(Untranslated("Transaction has descendants in the wallet"));
return feebumper::Result::INVALID_PARAMETER; return feebumper::Result::INVALID_PARAMETER;
} }

View file

@ -320,13 +320,12 @@ public:
} }
return {}; return {};
} }
std::vector<WalletTx> getWalletTxs() override std::set<WalletTx> getWalletTxs() override
{ {
LOCK(m_wallet->cs_wallet); LOCK(m_wallet->cs_wallet);
std::vector<WalletTx> result; std::set<WalletTx> result;
result.reserve(m_wallet->mapWallet.size());
for (const auto& entry : m_wallet->mapWallet) { for (const auto& entry : m_wallet->mapWallet) {
result.emplace_back(MakeWalletTx(*m_wallet, entry.second)); result.emplace(MakeWalletTx(*m_wallet, entry.second));
} }
return result; return result;
} }

View file

@ -2075,10 +2075,21 @@ std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvid
std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvider(int32_t index, bool include_private) const std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvider(int32_t index, bool include_private) const
{ {
AssertLockHeld(cs_desc_man); AssertLockHeld(cs_desc_man);
// Get the scripts, keys, and key origins for this script
std::unique_ptr<FlatSigningProvider> out_keys = std::make_unique<FlatSigningProvider>(); std::unique_ptr<FlatSigningProvider> out_keys = std::make_unique<FlatSigningProvider>();
std::vector<CScript> scripts_temp;
if (!m_wallet_descriptor.descriptor->ExpandFromCache(index, m_wallet_descriptor.cache, scripts_temp, *out_keys)) return nullptr; // Fetch SigningProvider from cache to avoid re-deriving
auto it = m_map_signing_providers.find(index);
if (it != m_map_signing_providers.end()) {
*out_keys = Merge(*out_keys, it->second);
} else {
// Get the scripts, keys, and key origins for this script
std::vector<CScript> scripts_temp;
if (!m_wallet_descriptor.descriptor->ExpandFromCache(index, m_wallet_descriptor.cache, scripts_temp, *out_keys)) return nullptr;
// Cache SigningProvider so we don't need to re-derive if we need this SigningProvider again
m_map_signing_providers[index] = *out_keys;
}
if (HavePrivateKeys() && include_private) { if (HavePrivateKeys() && include_private) {
FlatSigningProvider master_provider; FlatSigningProvider master_provider;

View file

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

View file

@ -213,7 +213,10 @@ CoinsResult AvailableCoins(const CWallet& wallet,
std::unique_ptr<SigningProvider> provider = wallet.GetSolvingProvider(output.scriptPubKey); std::unique_ptr<SigningProvider> provider = wallet.GetSolvingProvider(output.scriptPubKey);
bool solvable = provider ? IsSolvable(*provider, output.scriptPubKey) : false; int input_bytes = CalculateMaximumSignedInputSize(output, COutPoint(), provider.get(), coinControl);
// Because CalculateMaximumSignedInputSize just uses ProduceSignature and makes a dummy signature,
// it is safe to assume that this input is solvable if input_bytes is greater -1.
bool solvable = input_bytes > -1;
bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable)); bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable));
// Filter by spendable outputs only // Filter by spendable outputs only
@ -243,7 +246,6 @@ CoinsResult AvailableCoins(const CWallet& wallet,
type = Solver(output.scriptPubKey, return_values_unused); type = Solver(output.scriptPubKey, return_values_unused);
} }
int input_bytes = CalculateMaximumSignedInputSize(output, COutPoint(), provider.get(), coinControl);
COutput coin(outpoint, output, nDepth, input_bytes, spendable, solvable, safeTx, wtx.GetTxTime(), tx_from_me, feerate); COutput coin(outpoint, output, nDepth, input_bytes, spendable, solvable, safeTx, wtx.GetTxTime(), tx_from_me, feerate);
switch (type) { switch (type) {
case TxoutType::WITNESS_UNKNOWN: case TxoutType::WITNESS_UNKNOWN:

View file

@ -843,16 +843,16 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
{ {
auto block_hash = block_tx.GetHash(); auto block_hash = block_tx.GetHash();
auto prev_hash = m_coinbase_txns[0]->GetHash(); auto prev_tx = m_coinbase_txns[0];
LOCK(wallet->cs_wallet); LOCK(wallet->cs_wallet);
BOOST_CHECK(wallet->HasWalletSpend(prev_hash)); BOOST_CHECK(wallet->HasWalletSpend(prev_tx));
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u); BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u);
std::vector<uint256> vHashIn{ block_hash }, vHashOut; std::vector<uint256> vHashIn{ block_hash }, vHashOut;
BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vHashIn, vHashOut), DBErrors::LOAD_OK); BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vHashIn, vHashOut), DBErrors::LOAD_OK);
BOOST_CHECK(!wallet->HasWalletSpend(prev_hash)); BOOST_CHECK(!wallet->HasWalletSpend(prev_tx));
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u); BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u);
} }

View file

@ -414,7 +414,7 @@ std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& b
const CWalletTx* CWallet::GetWalletTx(const uint256& hash) const const CWalletTx* CWallet::GetWalletTx(const uint256& hash) const
{ {
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
std::map<uint256, CWalletTx>::const_iterator it = mapWallet.find(hash); const auto it = mapWallet.find(hash);
if (it == mapWallet.end()) if (it == mapWallet.end())
return nullptr; return nullptr;
return &(it->second); return &(it->second);
@ -551,7 +551,7 @@ std::set<uint256> CWallet::GetConflicts(const uint256& txid) const
std::set<uint256> result; std::set<uint256> result;
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
std::map<uint256, CWalletTx>::const_iterator it = mapWallet.find(txid); const auto it = mapWallet.find(txid);
if (it == mapWallet.end()) if (it == mapWallet.end())
return result; return result;
const CWalletTx& wtx = it->second; const CWalletTx& wtx = it->second;
@ -569,11 +569,17 @@ std::set<uint256> CWallet::GetConflicts(const uint256& txid) const
return result; return result;
} }
bool CWallet::HasWalletSpend(const uint256& txid) const bool CWallet::HasWalletSpend(const CTransactionRef& tx) const
{ {
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
auto iter = mapTxSpends.lower_bound(COutPoint(txid, 0)); const uint256& txid = tx->GetHash();
return (iter != mapTxSpends.end() && iter->first.hash == txid); for (unsigned int i = 0; i < tx->vout.size(); ++i) {
auto iter = mapTxSpends.find(COutPoint(txid, i));
if (iter != mapTxSpends.end()) {
return true;
}
}
return false;
} }
void CWallet::Flush() void CWallet::Flush()
@ -636,7 +642,7 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const
for (TxSpends::const_iterator it = range.first; it != range.second; ++it) { for (TxSpends::const_iterator it = range.first; it != range.second; ++it) {
const uint256& wtxid = it->second; const uint256& wtxid = it->second;
std::map<uint256, CWalletTx>::const_iterator mit = mapWallet.find(wtxid); const auto mit = mapWallet.find(wtxid);
if (mit != mapWallet.end()) { if (mit != mapWallet.end()) {
int depth = GetTxDepthInMainChain(mit->second); int depth = GetTxDepthInMainChain(mit->second);
if (depth > 0 || (depth == 0 && !mit->second.isAbandoned())) if (depth > 0 || (depth == 0 && !mit->second.isAbandoned()))
@ -1197,12 +1203,13 @@ bool CWallet::AbandonTransaction(const uint256& hashTx)
batch.WriteTx(wtx); batch.WriteTx(wtx);
NotifyTransactionChanged(wtx.GetHash(), CT_UPDATED); NotifyTransactionChanged(wtx.GetHash(), CT_UPDATED);
// Iterate over all its outputs, and mark transactions in the wallet that spend them abandoned too // Iterate over all its outputs, and mark transactions in the wallet that spend them abandoned too
TxSpends::const_iterator iter = mapTxSpends.lower_bound(COutPoint(now, 0)); for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
while (iter != mapTxSpends.end() && iter->first.hash == now) { std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(COutPoint(now, i));
if (!done.count(iter->second)) { for (TxSpends::const_iterator iter = range.first; iter != range.second; ++iter) {
todo.insert(iter->second); if (!done.count(iter->second)) {
todo.insert(iter->second);
}
} }
iter++;
} }
// If a transaction changes 'conflicted' state, that changes the balance // If a transaction changes 'conflicted' state, that changes the balance
// available of the outputs it spends. So force those to be recomputed // 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
wtx.MarkDirty(); wtx.MarkDirty();
batch.WriteTx(wtx); batch.WriteTx(wtx);
// Iterate over all its outputs, and mark transactions in the wallet that spend them conflicted too // Iterate over all its outputs, and mark transactions in the wallet that spend them conflicted too
TxSpends::const_iterator iter = mapTxSpends.lower_bound(COutPoint(now, 0)); for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
while (iter != mapTxSpends.end() && iter->first.hash == now) { std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(COutPoint(now, i));
if (!done.count(iter->second)) { for (TxSpends::const_iterator iter = range.first; iter != range.second; ++iter) {
todo.insert(iter->second); if (!done.count(iter->second)) {
} todo.insert(iter->second);
iter++; }
}
} }
// If a transaction changes 'conflicted' state, that changes the balance // If a transaction changes 'conflicted' state, that changes the balance
// available of the outputs it spends. So force those to be recomputed // 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
{ {
{ {
LOCK(cs_wallet); LOCK(cs_wallet);
std::map<uint256, CWalletTx>::const_iterator mi = mapWallet.find(txin.prevout.hash); const auto mi = mapWallet.find(txin.prevout.hash);
if (mi != mapWallet.end()) if (mi != mapWallet.end())
{ {
const CWalletTx& prev = (*mi).second; const CWalletTx& prev = (*mi).second;
@ -1959,7 +1967,7 @@ bool CWallet::SignTransaction(CMutableTransaction& tx) const
// Build coins map // Build coins map
std::map<COutPoint, Coin> coins; std::map<COutPoint, Coin> coins;
for (auto& input : tx.vin) { for (auto& input : tx.vin) {
std::map<uint256, CWalletTx>::const_iterator mi = mapWallet.find(input.prevout.hash); const auto mi = mapWallet.find(input.prevout.hash);
if(mi == mapWallet.end() || input.prevout.n >= mi->second.tx->vout.size()) { if(mi == mapWallet.end() || input.prevout.n >= mi->second.tx->vout.size()) {
return false; return false;
} }

View file

@ -14,6 +14,7 @@
#include <policy/feerate.h> #include <policy/feerate.h>
#include <psbt.h> #include <psbt.h>
#include <tinyformat.h> #include <tinyformat.h>
#include <util/hasher.h>
#include <util/message.h> #include <util/message.h>
#include <util/result.h> #include <util/result.h>
#include <util/strencodings.h> #include <util/strencodings.h>
@ -37,6 +38,7 @@
#include <stdint.h> #include <stdint.h>
#include <string> #include <string>
#include <utility> #include <utility>
#include <unordered_map>
#include <vector> #include <vector>
#include <boost/signals2/signal.hpp> #include <boost/signals2/signal.hpp>
@ -259,7 +261,7 @@ private:
* detect and report conflicts (double-spends or * detect and report conflicts (double-spends or
* mutated transactions where the mutant gets mined). * mutated transactions where the mutant gets mined).
*/ */
typedef std::multimap<COutPoint, uint256> TxSpends; typedef std::unordered_multimap<COutPoint, uint256, SaltedOutpointHasher> TxSpends;
TxSpends mapTxSpends GUARDED_BY(cs_wallet); TxSpends mapTxSpends GUARDED_BY(cs_wallet);
void AddToSpends(const COutPoint& outpoint, const uint256& wtxid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void AddToSpends(const COutPoint& outpoint, const uint256& wtxid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void AddToSpends(const CWalletTx& wtx, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void AddToSpends(const CWalletTx& wtx, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
@ -390,7 +392,7 @@ public:
/** Map from txid to CWalletTx for all transactions this wallet is /** Map from txid to CWalletTx for all transactions this wallet is
* interested in, including received and sent transactions. */ * interested in, including received and sent transactions. */
std::map<uint256, CWalletTx> mapWallet GUARDED_BY(cs_wallet); std::unordered_map<uint256, CWalletTx, SaltedTxidHasher> mapWallet GUARDED_BY(cs_wallet);
typedef std::multimap<int64_t, CWalletTx*> TxItems; typedef std::multimap<int64_t, CWalletTx*> TxItems;
TxItems wtxOrdered; TxItems wtxOrdered;
@ -712,7 +714,7 @@ public:
std::set<uint256> GetConflicts(const uint256& txid) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); std::set<uint256> GetConflicts(const uint256& txid) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
//! Check if a given transaction has any of its outputs spent by another transaction in the wallet //! Check if a given transaction has any of its outputs spent by another transaction in the wallet
bool HasWalletSpend(const uint256& txid) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool HasWalletSpend(const CTransactionRef& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
//! Flush wallet (bitdb flush) //! Flush wallet (bitdb flush)
void Flush(); void Flush();