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: ACKbc886fcb31
furszy: diff re-reACKbc886fcb
Tree-SHA512: fd710fe1224ef67d2bb83d6ac9e7428d9f76a67f14085915f9d80e1a492d2c51cb912edfcaad1db11c2edf8d2d97eb7ddd95bfb364587fb1f143490fd72c9ec1
This commit is contained in:
commit
59bd6b6d37
9 changed files with 62 additions and 36 deletions
|
@ -183,7 +183,7 @@ public:
|
|||
virtual WalletTx getWalletTx(const uint256& txid) = 0;
|
||||
|
||||
//! 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.
|
||||
virtual bool tryGetTxStatus(const uint256& txid,
|
||||
|
@ -395,6 +395,8 @@ struct WalletTx
|
|||
int64_t time;
|
||||
std::map<std::string, std::string> value_map;
|
||||
bool is_coinbase;
|
||||
|
||||
bool operator<(const WalletTx& a) const { return tx->GetHash() < a.tx->GetHash(); }
|
||||
};
|
||||
|
||||
//! Updated transaction status.
|
||||
|
|
|
@ -21,7 +21,7 @@ namespace wallet {
|
|||
//! 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)
|
||||
{
|
||||
if (wallet.HasWalletSpend(wtx.GetHash())) {
|
||||
if (wallet.HasWalletSpend(wtx.tx)) {
|
||||
errors.push_back(Untranslated("Transaction has descendants in the wallet"));
|
||||
return feebumper::Result::INVALID_PARAMETER;
|
||||
}
|
||||
|
|
|
@ -320,13 +320,12 @@ public:
|
|||
}
|
||||
return {};
|
||||
}
|
||||
std::vector<WalletTx> getWalletTxs() override
|
||||
std::set<WalletTx> getWalletTxs() override
|
||||
{
|
||||
LOCK(m_wallet->cs_wallet);
|
||||
std::vector<WalletTx> result;
|
||||
result.reserve(m_wallet->mapWallet.size());
|
||||
std::set<WalletTx> result;
|
||||
for (const auto& entry : m_wallet->mapWallet) {
|
||||
result.emplace_back(MakeWalletTx(*m_wallet, entry.second));
|
||||
result.emplace(MakeWalletTx(*m_wallet, entry.second));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -2075,10 +2075,21 @@ std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvid
|
|||
std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvider(int32_t index, bool include_private) const
|
||||
{
|
||||
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::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) {
|
||||
FlatSigningProvider master_provider;
|
||||
|
|
|
@ -547,6 +547,8 @@ private:
|
|||
|
||||
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
|
||||
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.
|
||||
|
|
|
@ -213,7 +213,10 @@ CoinsResult AvailableCoins(const CWallet& wallet,
|
|||
|
||||
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));
|
||||
|
||||
// Filter by spendable outputs only
|
||||
|
@ -243,7 +246,6 @@ CoinsResult AvailableCoins(const CWallet& wallet,
|
|||
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);
|
||||
switch (type) {
|
||||
case TxoutType::WITNESS_UNKNOWN:
|
||||
|
|
|
@ -843,16 +843,16 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
|
|||
|
||||
{
|
||||
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);
|
||||
BOOST_CHECK(wallet->HasWalletSpend(prev_hash));
|
||||
BOOST_CHECK(wallet->HasWalletSpend(prev_tx));
|
||||
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u);
|
||||
|
||||
std::vector<uint256> vHashIn{ block_hash }, vHashOut;
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -414,7 +414,7 @@ std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& b
|
|||
const CWalletTx* CWallet::GetWalletTx(const uint256& hash) const
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
std::map<uint256, CWalletTx>::const_iterator it = mapWallet.find(hash);
|
||||
const auto it = mapWallet.find(hash);
|
||||
if (it == mapWallet.end())
|
||||
return nullptr;
|
||||
return &(it->second);
|
||||
|
@ -551,7 +551,7 @@ std::set<uint256> CWallet::GetConflicts(const uint256& txid) const
|
|||
std::set<uint256> result;
|
||||
AssertLockHeld(cs_wallet);
|
||||
|
||||
std::map<uint256, CWalletTx>::const_iterator it = mapWallet.find(txid);
|
||||
const auto it = mapWallet.find(txid);
|
||||
if (it == mapWallet.end())
|
||||
return result;
|
||||
const CWalletTx& wtx = it->second;
|
||||
|
@ -569,11 +569,17 @@ std::set<uint256> CWallet::GetConflicts(const uint256& txid) const
|
|||
return result;
|
||||
}
|
||||
|
||||
bool CWallet::HasWalletSpend(const uint256& txid) const
|
||||
bool CWallet::HasWalletSpend(const CTransactionRef& tx) const
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
auto iter = mapTxSpends.lower_bound(COutPoint(txid, 0));
|
||||
return (iter != mapTxSpends.end() && iter->first.hash == txid);
|
||||
const uint256& txid = tx->GetHash();
|
||||
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()
|
||||
|
@ -636,7 +642,7 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const
|
|||
|
||||
for (TxSpends::const_iterator it = range.first; it != range.second; ++it) {
|
||||
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()) {
|
||||
int depth = GetTxDepthInMainChain(mit->second);
|
||||
if (depth > 0 || (depth == 0 && !mit->second.isAbandoned()))
|
||||
|
@ -1197,12 +1203,13 @@ bool CWallet::AbandonTransaction(const uint256& hashTx)
|
|||
batch.WriteTx(wtx);
|
||||
NotifyTransactionChanged(wtx.GetHash(), CT_UPDATED);
|
||||
// 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));
|
||||
while (iter != mapTxSpends.end() && iter->first.hash == now) {
|
||||
if (!done.count(iter->second)) {
|
||||
todo.insert(iter->second);
|
||||
for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
|
||||
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(COutPoint(now, i));
|
||||
for (TxSpends::const_iterator iter = range.first; iter != range.second; ++iter) {
|
||||
if (!done.count(iter->second)) {
|
||||
todo.insert(iter->second);
|
||||
}
|
||||
}
|
||||
iter++;
|
||||
}
|
||||
// If a transaction changes 'conflicted' state, that changes the balance
|
||||
// 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();
|
||||
batch.WriteTx(wtx);
|
||||
// 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));
|
||||
while (iter != mapTxSpends.end() && iter->first.hash == now) {
|
||||
if (!done.count(iter->second)) {
|
||||
todo.insert(iter->second);
|
||||
}
|
||||
iter++;
|
||||
for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
|
||||
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(COutPoint(now, i));
|
||||
for (TxSpends::const_iterator iter = range.first; iter != range.second; ++iter) {
|
||||
if (!done.count(iter->second)) {
|
||||
todo.insert(iter->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If a transaction changes 'conflicted' state, that changes the balance
|
||||
// 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);
|
||||
std::map<uint256, CWalletTx>::const_iterator mi = mapWallet.find(txin.prevout.hash);
|
||||
const auto mi = mapWallet.find(txin.prevout.hash);
|
||||
if (mi != mapWallet.end())
|
||||
{
|
||||
const CWalletTx& prev = (*mi).second;
|
||||
|
@ -1959,7 +1967,7 @@ bool CWallet::SignTransaction(CMutableTransaction& tx) const
|
|||
// Build coins map
|
||||
std::map<COutPoint, Coin> coins;
|
||||
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()) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include <policy/feerate.h>
|
||||
#include <psbt.h>
|
||||
#include <tinyformat.h>
|
||||
#include <util/hasher.h>
|
||||
#include <util/message.h>
|
||||
#include <util/result.h>
|
||||
#include <util/strencodings.h>
|
||||
|
@ -37,6 +38,7 @@
|
|||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/signals2/signal.hpp>
|
||||
|
@ -259,7 +261,7 @@ private:
|
|||
* detect and report conflicts (double-spends or
|
||||
* 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);
|
||||
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);
|
||||
|
@ -390,7 +392,7 @@ public:
|
|||
|
||||
/** Map from txid to CWalletTx for all transactions this wallet is
|
||||
* 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;
|
||||
TxItems wtxOrdered;
|
||||
|
@ -712,7 +714,7 @@ public:
|
|||
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
|
||||
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)
|
||||
void Flush();
|
||||
|
|
Loading…
Add table
Reference in a new issue