This commit is contained in:
Ava Chow 2024-04-29 04:32:40 +02:00 committed by GitHub
commit 5b00539ae9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 332 additions and 311 deletions

View File

@ -46,10 +46,17 @@ void generateFakeBlock(const CChainParams& params,
coinbase_tx.vin[0].prevout.SetNull();
coinbase_tx.vout.resize(2);
coinbase_tx.vout[0].scriptPubKey = coinbase_out_script;
coinbase_tx.vout[0].nValue = 49 * COIN;
coinbase_tx.vout[0].nValue = 48 * COIN;
coinbase_tx.vin[0].scriptSig = CScript() << ++tip.tip_height << OP_0;
coinbase_tx.vout[1].scriptPubKey = coinbase_out_script; // extra output
coinbase_tx.vout[1].nValue = 1 * COIN;
// Fill the coinbase with outputs that don't belong to the wallet in order to benchmark
// AvailableCoins' behavior with unnecessary TXOs
for (int i = 0; i < 50; ++i) {
coinbase_tx.vout.emplace_back(1 * COIN / 50, CScript(OP_TRUE));
}
block.vtx = {MakeTransactionRef(std::move(coinbase_tx))};
block.nVersion = VERSIONBITS_LAST_OLD_BLOCK_VERSION;
@ -104,14 +111,14 @@ static void WalletCreateTx(benchmark::Bench& bench, const OutputType output_type
// Check available balance
auto bal = WITH_LOCK(wallet.cs_wallet, return wallet::AvailableCoins(wallet).GetTotalAmount()); // Cache
assert(bal == 50 * COIN * (chain_size - COINBASE_MATURITY));
assert(bal == 49 * COIN * (chain_size - COINBASE_MATURITY));
wallet::CCoinControl coin_control;
coin_control.m_allow_other_inputs = allow_other_inputs;
CAmount target = 0;
if (preset_inputs) {
// Select inputs, each has 49 BTC
// Select inputs, each has 48 BTC
wallet::CoinFilterParams filter_coins;
filter_coins.max_count = preset_inputs->num_of_internal_inputs;
const auto& res = WITH_LOCK(wallet.cs_wallet,
@ -164,7 +171,7 @@ static void AvailableCoins(benchmark::Bench& bench, const std::vector<OutputType
// Check available balance
auto bal = WITH_LOCK(wallet.cs_wallet, return wallet::AvailableCoins(wallet).GetTotalAmount()); // Cache
assert(bal == 50 * COIN * (chain_size - COINBASE_MATURITY));
assert(bal == 49 * COIN * (chain_size - COINBASE_MATURITY));
bench.epochIterations(2).run([&] {
LOCK(wallet.cs_wallet);

View File

@ -4,6 +4,7 @@
#include <consensus/amount.h>
#include <consensus/consensus.h>
#include <util/check.h>
#include <wallet/receive.h>
#include <wallet/transaction.h>
#include <wallet/wallet.h>
@ -145,52 +146,6 @@ CAmount CachedTxGetChange(const CWallet& wallet, const CWalletTx& wtx)
return wtx.nChangeCached;
}
CAmount CachedTxGetImmatureCredit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter)
{
AssertLockHeld(wallet.cs_wallet);
if (wallet.IsTxImmatureCoinBase(wtx) && wtx.isConfirmed()) {
return GetCachableAmount(wallet, wtx, CWalletTx::IMMATURE_CREDIT, filter);
}
return 0;
}
CAmount CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter)
{
AssertLockHeld(wallet.cs_wallet);
// Avoid caching ismine for NO or ALL cases (could remove this check and simplify in the future).
bool allow_cache = (filter & ISMINE_ALL) && (filter & ISMINE_ALL) != ISMINE_ALL;
// Must wait until coinbase is safely deep enough in the chain before valuing it
if (wallet.IsTxImmatureCoinBase(wtx))
return 0;
if (allow_cache && wtx.m_amounts[CWalletTx::AVAILABLE_CREDIT].m_cached[filter]) {
return wtx.m_amounts[CWalletTx::AVAILABLE_CREDIT].m_value[filter];
}
bool allow_used_addresses = (filter & ISMINE_USED) || !wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
CAmount nCredit = 0;
Txid hashTx = wtx.GetHash();
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) {
const CTxOut& txout = wtx.tx->vout[i];
if (!wallet.IsSpent(COutPoint(hashTx, i)) && (allow_used_addresses || !wallet.IsSpentKey(txout.scriptPubKey))) {
nCredit += OutputGetCredit(wallet, txout, filter);
if (!MoneyRange(nCredit))
throw std::runtime_error(std::string(__func__) + " : value out of range");
}
}
if (allow_cache) {
wtx.m_amounts[CWalletTx::AVAILABLE_CREDIT].Set(filter, nCredit);
wtx.m_is_cache_empty = false;
}
return nCredit;
}
void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
std::list<COutputEntry>& listReceived,
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter,
@ -257,6 +212,10 @@ bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminef
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents)
{
AssertLockHeld(wallet.cs_wallet);
// This wtx is already trusted
if (trusted_parents.contains(wtx.GetHash())) return true;
if (wtx.isConfirmed()) return true;
if (wtx.isBlockConflicted()) return false;
// using wtx's cached debit
@ -293,27 +252,40 @@ bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx)
Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse)
{
Balance ret;
isminefilter reuse_filter = avoid_reuse ? ISMINE_NO : ISMINE_USED;
bool allow_used_addresses = !avoid_reuse || !wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
{
LOCK(wallet.cs_wallet);
std::set<uint256> trusted_parents;
for (const auto& entry : wallet.mapWallet)
{
const CWalletTx& wtx = entry.second;
const bool is_trusted{CachedTxIsTrusted(wallet, wtx, trusted_parents)};
const int tx_depth{wallet.GetTxDepthInMainChain(wtx)};
const CAmount tx_credit_mine{CachedTxGetAvailableCredit(wallet, wtx, ISMINE_SPENDABLE | reuse_filter)};
const CAmount tx_credit_watchonly{CachedTxGetAvailableCredit(wallet, wtx, ISMINE_WATCH_ONLY | reuse_filter)};
if (is_trusted && tx_depth >= min_depth) {
ret.m_mine_trusted += tx_credit_mine;
ret.m_watchonly_trusted += tx_credit_watchonly;
for (const auto& [outpoint, txo] : wallet.GetTXOs()) {
Assert(MoneyRange(txo.GetTxOut().nValue));
const bool is_trusted{CachedTxIsTrusted(wallet, txo.GetWalletTx())};
const int tx_depth{wallet.GetTxDepthInMainChain(txo.GetWalletTx())};
if (!wallet.IsSpent(outpoint) && (allow_used_addresses || !wallet.IsSpentKey(txo.GetTxOut().scriptPubKey))) {
// Get the amounts for mine and watchonly
CAmount credit_mine = 0;
CAmount credit_watchonly = 0;
if (txo.GetIsMine() == ISMINE_SPENDABLE) {
credit_mine = txo.GetTxOut().nValue;
} else if (txo.GetIsMine() == ISMINE_WATCH_ONLY) {
credit_watchonly = txo.GetTxOut().nValue;
} else {
// We shouldn't see any other isminetypes
Assume(false);
}
// Set the amounts in the return object
if (wallet.IsTxImmatureCoinBase(txo.GetWalletTx()) && txo.GetWalletTx().isConfirmed()) {
ret.m_mine_immature += credit_mine;
ret.m_watchonly_immature += credit_watchonly;
} else if (is_trusted && tx_depth >= min_depth) {
ret.m_mine_trusted += credit_mine;
ret.m_watchonly_trusted += credit_watchonly;
} else if (!is_trusted && txo.GetWalletTx().InMempool()) {
ret.m_mine_untrusted_pending += credit_mine;
ret.m_watchonly_untrusted_pending += credit_watchonly;
}
}
if (!is_trusted && tx_depth == 0 && wtx.InMempool()) {
ret.m_mine_untrusted_pending += tx_credit_mine;
ret.m_watchonly_untrusted_pending += tx_credit_watchonly;
}
ret.m_mine_immature += CachedTxGetImmatureCredit(wallet, wtx, ISMINE_SPENDABLE);
ret.m_watchonly_immature += CachedTxGetImmatureCredit(wallet, wtx, ISMINE_WATCH_ONLY);
}
}
return ret;
@ -325,32 +297,19 @@ std::map<CTxDestination, CAmount> GetAddressBalances(const CWallet& wallet)
{
LOCK(wallet.cs_wallet);
std::set<uint256> trusted_parents;
for (const auto& walletEntry : wallet.mapWallet)
{
const CWalletTx& wtx = walletEntry.second;
for (const auto& [outpoint, txo] : wallet.GetTXOs()) {
if (!CachedTxIsTrusted(wallet, txo.GetWalletTx())) continue;
if (wallet.IsTxImmatureCoinBase(txo.GetWalletTx())) continue;
if (!CachedTxIsTrusted(wallet, wtx, trusted_parents))
continue;
int nDepth = wallet.GetTxDepthInMainChain(txo.GetWalletTx());
if (nDepth < (CachedTxIsFromMe(wallet, txo.GetWalletTx(), ISMINE_ALL) ? 0 : 1)) continue;
if (wallet.IsTxImmatureCoinBase(wtx))
continue;
CTxDestination addr;
Assume(wallet.IsMine(txo.GetTxOut()));
if(!ExtractDestination(txo.GetTxOut().scriptPubKey, addr)) continue;
int nDepth = wallet.GetTxDepthInMainChain(wtx);
if (nDepth < (CachedTxIsFromMe(wallet, wtx, ISMINE_ALL) ? 0 : 1))
continue;
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) {
const auto& output = wtx.tx->vout[i];
CTxDestination addr;
if (!wallet.IsMine(output))
continue;
if(!ExtractDestination(output.scriptPubKey, addr))
continue;
CAmount n = wallet.IsSpent(COutPoint(Txid::FromUint256(walletEntry.first), i)) ? 0 : output.nValue;
balances[addr] += n;
}
CAmount n = wallet.IsSpent(outpoint) ? 0 : txo.GetTxOut().nValue;
balances[addr] += n;
}
}

View File

@ -29,10 +29,6 @@ CAmount CachedTxGetCredit(const CWallet& wallet, const CWalletTx& wtx, const ism
//! filter decides which addresses will count towards the debit
CAmount CachedTxGetDebit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter);
CAmount CachedTxGetChange(const CWallet& wallet, const CWalletTx& wtx);
CAmount CachedTxGetImmatureCredit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter)
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
CAmount CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter = ISMINE_SPENDABLE)
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
struct COutputEntry
{
CTxDestination destination;

View File

@ -293,6 +293,7 @@ RPCHelpMan addmultisigaddress()
CScript inner;
CTxDestination dest = AddAndGetMultisigDestination(required, pubkeys, output_type, spk_man, inner);
pwallet->SetAddressBook(dest, label, AddressPurpose::SEND);
pwallet->RefreshAllTXOs();
// Make the descriptor
std::unique_ptr<Descriptor> descriptor = InferDescriptor(GetScriptForDestination(dest), spk_man);
@ -352,6 +353,7 @@ RPCHelpMan keypoolrefill()
if (pwallet->GetKeyPoolSize() < kpSize) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error refreshing keypool.");
}
pwallet->RefreshAllTXOs();
return UniValue::VNULL;
},
@ -383,6 +385,7 @@ RPCHelpMan newkeypool()
LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true);
spk_man.NewKeyPool();
pwallet->RefreshAllTXOs();
return UniValue::VNULL;
},

View File

@ -207,6 +207,7 @@ RPCHelpMan importprivkey()
pwallet->ImportScripts({GetScriptForDestination(WitnessV0KeyHash(vchAddress))}, /*timestamp=*/0);
}
}
pwallet->RefreshAllTXOs();
}
if (fRescan) {
RescanWallet(*pwallet, reserver);
@ -307,6 +308,7 @@ RPCHelpMan importaddress()
} else {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address or script");
}
pwallet->RefreshAllTXOs();
}
if (fRescan)
{
@ -478,6 +480,8 @@ RPCHelpMan importpubkey()
pwallet->ImportScriptPubKeys(strLabel, script_pub_keys, /*have_solving_data=*/true, /*apply_label=*/true, /*timestamp=*/1);
pwallet->ImportPubKeys({pubKey.GetID()}, {{pubKey.GetID(), pubKey}} , /*key_origins=*/{}, /*add_keypool=*/false, /*internal=*/false, /*timestamp=*/1);
pwallet->RefreshAllTXOs();
}
if (fRescan)
{
@ -625,6 +629,7 @@ RPCHelpMan importwallet()
progress++;
}
pwallet->RefreshAllTXOs();
pwallet->chain().showProgress("", 100, false); // hide progress dialog in GUI
}
pwallet->chain().showProgress("", 100, false); // hide progress dialog in GUI
@ -1401,6 +1406,8 @@ RPCHelpMan importmulti()
nLowestTimestamp = timestamp;
}
}
pwallet->RefreshAllTXOs();
}
if (fRescan && fRunScan && requests.size()) {
int64_t scannedTime = pwallet->RescanFromTime(nLowestTimestamp, reserver, /*update=*/true);
@ -1693,6 +1700,7 @@ RPCHelpMan importdescriptors()
}
}
pwallet->ConnectScriptPubKeyManNotifiers();
pwallet->RefreshAllTXOs();
}
// Rescan the blockchain using the lowest timestamp

View File

@ -572,6 +572,7 @@ static RPCHelpMan sethdseed()
spk_man.SetHDSeed(master_pub_key);
if (flush_key_pool) spk_man.NewKeyPool();
pwallet->RefreshAllTXOs();
return UniValue::VNULL;
},

View File

@ -267,12 +267,8 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
input_bytes = GetVirtualTransactionSize(input_bytes, 0, 0);
}
CTxOut txout;
if (auto ptr_wtx = wallet.GetWalletTx(outpoint.hash)) {
// Clearly invalid input, fail
if (ptr_wtx->tx->vout.size() <= outpoint.n) {
return util::Error{strprintf(_("Invalid pre-selected input %s"), outpoint.ToString())};
}
txout = ptr_wtx->tx->vout.at(outpoint.n);
if (auto txo = wallet.GetTXO(outpoint)) {
txout = txo->GetTxOut();
if (input_bytes == -1) {
input_bytes = CalculateMaximumSignedInputSize(txout, &wallet, &coin_control);
}
@ -320,137 +316,146 @@ CoinsResult AvailableCoins(const CWallet& wallet,
std::vector<COutPoint> outpoints;
std::set<uint256> trusted_parents;
for (const auto& entry : wallet.mapWallet)
{
const uint256& txid = entry.first;
const CWalletTx& wtx = entry.second;
// Cache for whether each tx passes the tx level checks (first bool), and whether the transaction is "safe" (second bool)
std::unordered_map<uint256, std::pair<bool, bool>, SaltedTxidHasher> tx_safe_cache;
for (const auto& [outpoint, txo] : wallet.GetTXOs()) {
const CWalletTx& wtx = txo.GetWalletTx();
const CTxOut& output = txo.GetTxOut();
if (tx_safe_cache.contains(outpoint.hash) && !tx_safe_cache.at(outpoint.hash).first) {
continue;
}
// Skip manually selected coins (the caller can fetch them directly)
if (coinControl && coinControl->HasSelected() && coinControl->IsSelected(outpoint))
continue;
if (wallet.IsLockedCoin(outpoint) && params.skip_locked)
continue;
if (wallet.IsSpent(outpoint))
continue;
if (output.nValue < params.min_amount || output.nValue > params.max_amount)
continue;
if (!allow_used_addresses && wallet.IsSpentKey(output.scriptPubKey)) {
continue;
}
if (wallet.IsTxImmatureCoinBase(wtx) && !params.include_immature_coinbase)
continue;
isminetype mine = wallet.IsMine(output);
assert(mine != ISMINE_NO);
int nDepth = wallet.GetTxDepthInMainChain(wtx);
if (nDepth < 0)
continue;
// We should not consider coins which aren't at least in our mempool
// It's possible for these to be conflicted via ancestors which we may never be able to detect
if (nDepth == 0 && !wtx.InMempool())
continue;
// Perform tx level checks if we haven't already come across outputs from this tx before.
if (!tx_safe_cache.contains(outpoint.hash)) {
tx_safe_cache[outpoint.hash] = {false, false};
bool safeTx = CachedTxIsTrusted(wallet, wtx, trusted_parents);
// We should not consider coins which aren't at least in our mempool
// It's possible for these to be conflicted via ancestors which we may never be able to detect
if (nDepth == 0 && !wtx.InMempool())
continue;
// We should not consider coins from transactions that are replacing
// other transactions.
//
// Example: There is a transaction A which is replaced by bumpfee
// transaction B. In this case, we want to prevent creation of
// a transaction B' which spends an output of B.
//
// Reason: If transaction A were initially confirmed, transactions B
// and B' would no longer be valid, so the user would have to create
// a new transaction C to replace B'. However, in the case of a
// one-block reorg, transactions B' and C might BOTH be accepted,
// when the user only wanted one of them. Specifically, there could
// be a 1-block reorg away from the chain where transactions A and C
// were accepted to another chain where B, B', and C were all
// accepted.
if (nDepth == 0 && wtx.mapValue.count("replaces_txid")) {
safeTx = false;
bool safeTx = CachedTxIsTrusted(wallet, wtx, trusted_parents);
// We should not consider coins from transactions that are replacing
// other transactions.
//
// Example: There is a transaction A which is replaced by bumpfee
// transaction B. In this case, we want to prevent creation of
// a transaction B' which spends an output of B.
//
// Reason: If transaction A were initially confirmed, transactions B
// and B' would no longer be valid, so the user would have to create
// a new transaction C to replace B'. However, in the case of a
// one-block reorg, transactions B' and C might BOTH be accepted,
// when the user only wanted one of them. Specifically, there could
// be a 1-block reorg away from the chain where transactions A and C
// were accepted to another chain where B, B', and C were all
// accepted.
if (nDepth == 0 && wtx.mapValue.count("replaces_txid")) {
safeTx = false;
}
// Similarly, we should not consider coins from transactions that
// have been replaced. In the example above, we would want to prevent
// creation of a transaction A' spending an output of A, because if
// transaction B were initially confirmed, conflicting with A and
// A', we wouldn't want to the user to create a transaction D
// intending to replace A', but potentially resulting in a scenario
// where A, A', and D could all be accepted (instead of just B and
// D, or just A and A' like the user would want).
if (nDepth == 0 && wtx.mapValue.count("replaced_by_txid")) {
safeTx = false;
}
if (only_safe && !safeTx) {
continue;
}
if (nDepth < min_depth || nDepth > max_depth) {
continue;
}
tx_safe_cache[outpoint.hash] = {true, safeTx};
}
// Similarly, we should not consider coins from transactions that
// have been replaced. In the example above, we would want to prevent
// creation of a transaction A' spending an output of A, because if
// transaction B were initially confirmed, conflicting with A and
// A', we wouldn't want to the user to create a transaction D
// intending to replace A', but potentially resulting in a scenario
// where A, A', and D could all be accepted (instead of just B and
// D, or just A and A' like the user would want).
if (nDepth == 0 && wtx.mapValue.count("replaced_by_txid")) {
safeTx = false;
}
if (only_safe && !safeTx) {
continue;
}
if (nDepth < min_depth || nDepth > max_depth) {
const auto& [tx_ok, tx_safe] = tx_safe_cache.at(outpoint.hash);
if (!Assume(tx_ok)) {
continue;
}
bool tx_from_me = CachedTxIsFromMe(wallet, wtx, ISMINE_ALL);
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) {
const CTxOut& output = wtx.tx->vout[i];
const COutPoint outpoint(Txid::FromUint256(txid), i);
std::unique_ptr<SigningProvider> provider = wallet.GetSolvingProvider(output.scriptPubKey);
if (output.nValue < params.min_amount || output.nValue > params.max_amount)
continue;
int input_bytes = CalculateMaximumSignedInputSize(output, COutPoint(), provider.get(), can_grind_r, coinControl);
// Because CalculateMaximumSignedInputSize infers a solvable descriptor to get the satisfaction size,
// it is safe to assume that this input is solvable if input_bytes is greater than -1.
bool solvable = input_bytes > -1;
bool spendable = ((mine & ISMINE_SPENDABLE) != ISMINE_NO) || (((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) && (coinControl && coinControl->fAllowWatchOnly && solvable));
// Skip manually selected coins (the caller can fetch them directly)
if (coinControl && coinControl->HasSelected() && coinControl->IsSelected(outpoint))
continue;
// Filter by spendable outputs only
if (!spendable && params.only_spendable) continue;
if (wallet.IsLockedCoin(outpoint) && params.skip_locked)
continue;
// Obtain script type
std::vector<std::vector<uint8_t>> script_solutions;
TxoutType type = Solver(output.scriptPubKey, script_solutions);
if (wallet.IsSpent(outpoint))
continue;
// If the output is P2SH and solvable, we want to know if it is
// a P2SH (legacy) or one of P2SH-P2WPKH, P2SH-P2WSH (P2SH-Segwit). We can determine
// this from the redeemScript. If the output is not solvable, it will be classified
// as a P2SH (legacy), since we have no way of knowing otherwise without the redeemScript
bool is_from_p2sh{false};
if (type == TxoutType::SCRIPTHASH && solvable) {
CScript script;
if (!provider->GetCScript(CScriptID(uint160(script_solutions[0])), script)) continue;
type = Solver(script, script_solutions);
is_from_p2sh = true;
}
isminetype mine = wallet.IsMine(output);
result.Add(GetOutputType(type, is_from_p2sh),
COutput(outpoint, output, nDepth, input_bytes, spendable, solvable, tx_safe, wtx.GetTxTime(), tx_from_me, feerate));
if (mine == ISMINE_NO) {
continue;
}
outpoints.push_back(outpoint);
if (!allow_used_addresses && wallet.IsSpentKey(output.scriptPubKey)) {
continue;
}
std::unique_ptr<SigningProvider> provider = wallet.GetSolvingProvider(output.scriptPubKey);
int input_bytes = CalculateMaximumSignedInputSize(output, COutPoint(), provider.get(), can_grind_r, coinControl);
// Because CalculateMaximumSignedInputSize infers a solvable descriptor to get the satisfaction size,
// it is safe to assume that this input is solvable if input_bytes is greater than -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
if (!spendable && params.only_spendable) continue;
// Obtain script type
std::vector<std::vector<uint8_t>> script_solutions;
TxoutType type = Solver(output.scriptPubKey, script_solutions);
// If the output is P2SH and solvable, we want to know if it is
// a P2SH (legacy) or one of P2SH-P2WPKH, P2SH-P2WSH (P2SH-Segwit). We can determine
// this from the redeemScript. If the output is not solvable, it will be classified
// as a P2SH (legacy), since we have no way of knowing otherwise without the redeemScript
bool is_from_p2sh{false};
if (type == TxoutType::SCRIPTHASH && solvable) {
CScript script;
if (!provider->GetCScript(CScriptID(uint160(script_solutions[0])), script)) continue;
type = Solver(script, script_solutions);
is_from_p2sh = true;
}
result.Add(GetOutputType(type, is_from_p2sh),
COutput(outpoint, output, nDepth, input_bytes, spendable, solvable, safeTx, wtx.GetTxTime(), tx_from_me, feerate));
outpoints.push_back(outpoint);
// Checks the sum amount of all UTXO's.
if (params.min_sum_amount != MAX_MONEY) {
if (result.GetTotalAmount() >= params.min_sum_amount) {
return result;
}
}
// Checks the maximum number of UTXO's.
if (params.max_count > 0 && result.Size() >= params.max_count) {
// Checks the sum amount of all UTXO's.
if (params.min_sum_amount != MAX_MONEY) {
if (result.GetTotalAmount() >= params.min_sum_amount) {
return result;
}
}
// Checks the maximum number of UTXO's.
if (params.max_count > 0 && result.Size() >= params.max_count) {
return result;
}
}
if (feerate.has_value()) {

View File

@ -329,35 +329,6 @@ BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup)
}
}
// Check that GetImmatureCredit() returns a newly calculated value instead of
// the cached value after a MarkDirty() call.
//
// This is a regression test written to verify a bugfix for the immature credit
// function. Similar tests probably should be written for the other credit and
// debit functions.
BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup)
{
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
LOCK(wallet.cs_wallet);
LOCK(Assert(m_node.chainman)->GetMutex());
CWalletTx wtx{m_coinbase_txns.back(), TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/0}};
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
wallet.SetupDescriptorScriptPubKeyMans();
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
// Call GetImmatureCredit() once before adding the key to the wallet to
// cache the current immature credit amount, which is 0.
BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx, ISMINE_SPENDABLE), 0);
// Invalidate the cached value, add the key, and make sure a new immature
// credit amount is calculated.
wtx.MarkDirty();
AddKey(wallet, coinbaseKey);
BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx, ISMINE_SPENDABLE), 50*COIN);
}
static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lockTime, int64_t mockTime, int64_t blockTime)
{
CMutableTransaction tx;
@ -925,65 +896,5 @@ BOOST_FIXTURE_TEST_CASE(RemoveTxs, TestChain100Setup)
TestUnloadWallet(std::move(wallet));
}
/**
* Checks a wallet invalid state where the inputs (prev-txs) of a new arriving transaction are not marked dirty,
* while the transaction that spends them exist inside the in-memory wallet tx map (not stored on db due a db write failure).
*/
BOOST_FIXTURE_TEST_CASE(wallet_sync_tx_invalid_state_test, TestingSetup)
{
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
{
LOCK(wallet.cs_wallet);
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
wallet.SetupDescriptorScriptPubKeyMans();
}
// Add tx to wallet
const auto op_dest{*Assert(wallet.GetNewDestination(OutputType::BECH32M, ""))};
CMutableTransaction mtx;
mtx.vout.emplace_back(COIN, GetScriptForDestination(op_dest));
mtx.vin.emplace_back(Txid::FromUint256(g_insecure_rand_ctx.rand256()), 0);
const auto& tx_id_to_spend = wallet.AddToWallet(MakeTransactionRef(mtx), TxStateInMempool{})->GetHash();
{
// Cache and verify available balance for the wtx
LOCK(wallet.cs_wallet);
const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 1 * COIN);
}
// Now the good case:
// 1) Add a transaction that spends the previously created transaction
// 2) Verify that the available balance of this new tx and the old one is updated (prev tx is marked dirty)
mtx.vin.clear();
mtx.vin.emplace_back(tx_id_to_spend, 0);
wallet.transactionAddedToMempool(MakeTransactionRef(mtx));
const auto good_tx_id{mtx.GetHash()};
{
// Verify balance update for the new tx and the old one
LOCK(wallet.cs_wallet);
const CWalletTx* new_wtx = wallet.GetWalletTx(good_tx_id.ToUint256());
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *new_wtx), 1 * COIN);
// Now the old wtx
const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 0 * COIN);
}
// Now the bad case:
// 1) Make db always fail
// 2) Try to add a transaction that spends the previously created transaction and
// verify that we are not moving forward if the wallet cannot store it
GetMockableDatabase(wallet).m_pass = false;
mtx.vin.clear();
mtx.vin.emplace_back(good_tx_id, 0);
BOOST_CHECK_EXCEPTION(wallet.transactionAddedToMempool(MakeTransactionRef(mtx)),
std::runtime_error,
HasReason("DB error adding transaction to wallet, write failed"));
}
BOOST_AUTO_TEST_SUITE_END()
} // namespace wallet

View File

@ -225,7 +225,7 @@ public:
std::multimap<int64_t, CWalletTx*>::const_iterator m_it_wtxOrdered;
// memory only
enum AmountType { DEBIT, CREDIT, IMMATURE_CREDIT, AVAILABLE_CREDIT, AMOUNTTYPE_ENUM_ELEMENTS };
enum AmountType { DEBIT, CREDIT, AMOUNTTYPE_ENUM_ELEMENTS };
mutable CachableAmount m_amounts[AMOUNTTYPE_ENUM_ELEMENTS];
/**
* This flag is true if all m_amounts caches are empty. This is particularly
@ -322,8 +322,6 @@ public:
{
m_amounts[DEBIT].Reset();
m_amounts[CREDIT].Reset();
m_amounts[IMMATURE_CREDIT].Reset();
m_amounts[AVAILABLE_CREDIT].Reset();
fChangeCached = false;
m_is_cache_empty = true;
}
@ -369,6 +367,28 @@ struct WalletTxOrderComparator {
return a->nOrderPos < b->nOrderPos;
}
};
class WalletTXO
{
private:
const CWalletTx& m_wtx;
const CTxOut& m_output;
isminetype m_ismine;
public:
WalletTXO(const CWalletTx& wtx, const CTxOut& output, const isminetype ismine)
: m_wtx(wtx),
m_output(output),
m_ismine(ismine)
{}
const CWalletTx& GetWalletTx() const { return m_wtx; }
const CTxOut& GetTxOut() const { return m_output; }
isminetype GetIsMine() const { return m_ismine; }
void SetIsMine(isminetype ismine) { m_ismine = ismine; }
};
} // namespace wallet
#endif // BITCOIN_WALLET_TRANSACTION_H

View File

@ -1143,6 +1143,9 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
// Break debit/credit balance caches:
wtx.MarkDirty();
// Cache the outputs that belong to the wallet
RefreshWalletTxTXOs(wtx);
// Notify UI of new or updated transaction
NotifyTransactionChanged(hash, fInsertedNew ? CT_NEW : CT_UPDATED);
@ -1206,6 +1209,8 @@ bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx
// Update birth time when tx time is older than it.
MaybeUpdateBirthTime(wtx.GetTxTime());
// Make sure the tx outputs are known by the wallet
RefreshWalletTxTXOs(wtx);
return true;
}
@ -1574,16 +1579,10 @@ void CWallet::BlockUntilSyncedToCurrentChain() const {
// and a not-"is mine" (according to the filter) input.
CAmount CWallet::GetDebit(const CTxIn &txin, const isminefilter& filter) const
{
{
LOCK(cs_wallet);
const auto mi = mapWallet.find(txin.prevout.hash);
if (mi != mapWallet.end())
{
const CWalletTx& prev = (*mi).second;
if (txin.prevout.n < prev.tx->vout.size())
if (IsMine(prev.tx->vout[txin.prevout.n]) & filter)
return prev.tx->vout[txin.prevout.n].nValue;
}
LOCK(cs_wallet);
auto txo = GetTXO(txin.prevout);
if (txo && (txo->GetIsMine() & filter)) {
return txo->GetTxOut().nValue;
}
return 0;
}
@ -2401,6 +2400,9 @@ util::Result<void> CWallet::RemoveTxs(std::vector<uint256>& txs_to_remove)
wtxOrdered.erase(it->second.m_it_wtxOrdered);
for (const auto& txin : it->second.tx->vin)
mapTxSpends.erase(txin.prevout);
for (unsigned int i = 0; i < it->second.tx->vout.size(); ++i) {
m_txos.erase(COutPoint(Txid::FromUint256(hash), i));
}
mapWallet.erase(it);
NotifyTransactionChanged(hash, CT_DELETED);
}
@ -4062,6 +4064,10 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
return false;
}
// Update m_txos to match the descriptors remaining in this wallet
m_txos.clear();
RefreshAllTXOs();
// Check if the transactions in the wallet are still ours. Either they belong here, or they belong in the watchonly wallet.
// We need to go through these in the tx insertion order so that lookups to spends works.
std::vector<uint256> txids_to_delete;
@ -4556,4 +4562,40 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const
}
return std::nullopt;
}
void CWallet::RefreshWalletTxTXOs(const CWalletTx& wtx)
{
AssertLockHeld(cs_wallet);
for (uint32_t i = 0; i < wtx.tx->vout.size(); ++i) {
const CTxOut& txout = wtx.tx->vout.at(i);
isminetype ismine = IsMine(txout);
if (ismine == ISMINE_NO) {
continue;
}
COutPoint outpoint(wtx.GetHash(), i);
if (m_txos.contains(outpoint)) {
m_txos.at(outpoint).SetIsMine(ismine);
} else {
m_txos.emplace(outpoint, WalletTXO{wtx, txout, ismine});
}
}
}
void CWallet::RefreshAllTXOs()
{
AssertLockHeld(cs_wallet);
for (const auto& [_, wtx] : mapWallet) {
RefreshWalletTxTXOs(wtx);
}
}
std::optional<WalletTXO> CWallet::GetTXO(const COutPoint& outpoint) const
{
AssertLockHeld(cs_wallet);
const auto& it = m_txos.find(outpoint);
if (it == m_txos.end()) {
return std::nullopt;
}
return it->second;
}
} // namespace wallet

View File

@ -426,6 +426,9 @@ private:
//! Cache of descriptor ScriptPubKeys used for IsMine. Maps ScriptPubKey to set of spkms
std::unordered_map<CScript, std::vector<ScriptPubKeyMan*>, SaltedSipHasher> m_cached_spks;
//! Set of both spent and unspent transaction outputs owned by this wallet
std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher> m_txos GUARDED_BY(cs_wallet);
/**
* Catch wallet up to current chain, scanning new blocks, updating the best
* block locator and m_last_block_processed, and registering for
@ -505,6 +508,12 @@ public:
std::set<uint256> GetTxConflicts(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
const std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>& GetTXOs() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return m_txos; };
std::optional<WalletTXO> GetTXO(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void RefreshWalletTxTXOs(const CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void RefreshAllTXOs() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/**
* Return depth of transaction in blockchain:
* <0 : conflicts with a transaction this deep in the blockchain

View File

@ -1187,14 +1187,14 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
// Load address book
result = std::max(LoadAddressBookRecords(pwallet, *m_batch), result);
// Load tx records
result = std::max(LoadTxRecords(pwallet, *m_batch, upgraded_txs, any_unordered), result);
// Load SPKMs
result = std::max(LoadActiveSPKMs(pwallet, *m_batch), result);
// Load decryption keys
result = std::max(LoadDecryptionKeys(pwallet, *m_batch), result);
// Load tx records
result = std::max(LoadTxRecords(pwallet, *m_batch, upgraded_txs, any_unordered), result);
} catch (...) {
// Exceptions that can be ignored or treated as non-critical are handled by the individual loading functions.
// Any uncaught exceptions will be caught here and treated as critical.

View File

@ -5,6 +5,7 @@
"""Test the wallet balance RPC methods."""
from decimal import Decimal
import struct
import time
from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE as ADDRESS_WATCHONLY
from test_framework.blocktools import COINBASE_MATURITY
@ -14,6 +15,7 @@ from test_framework.util import (
assert_is_hash_string,
assert_raises_rpc_error,
)
from test_framework.wallet_util import get_generate_key
def create_transactions(node, address, amt, fees):
@ -312,7 +314,40 @@ class WalletTest(BitcoinTestFramework):
self.nodes[0].createwallet('w2', False, True)
self.nodes[0].importprivkey(privkey)
assert_equal(self.nodes[0].getbalances()['mine']['untrusted_pending'], Decimal('0.1'))
self.nodes[0].unloadwallet("w2")
self.log.info("Test that an import that makes something spendable updates \"mine\" balance")
self.nodes[0].loadwallet(self.default_wallet_name)
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
self.nodes[0].createwallet(wallet_name="legacyspendableupdate", descriptors=False)
wallet = self.nodes[0].get_wallet_rpc("legacyspendableupdate")
import_key1 = get_generate_key()
import_key2 = get_generate_key()
wallet.importaddress(import_key1.p2wpkh_addr)
wallet.importaddress(import_key2.p2wpkh_addr)
amount = Decimal(15)
default.sendtoaddress(import_key1.p2wpkh_addr, amount)
default.sendtoaddress(import_key2.p2wpkh_addr, amount)
self.generate(self.nodes[0], 1)
balances = wallet.getbalances()
assert_equal(balances["mine"]["trusted"], 0)
assert_equal(balances["watchonly"]["trusted"], amount * 2)
# Rescanning should always update the txos by virtue of finding them again
wallet.importprivkey(privkey=import_key1.privkey, rescan=True)
balances = wallet.getbalances()
assert_equal(balances["mine"]["trusted"], amount)
assert_equal(balances["watchonly"]["trusted"], amount)
# Don't rescan to make sure that the import updates the wallet txos
wallet.importprivkey(privkey=import_key2.privkey, rescan=False)
balances = wallet.getbalances()
assert_equal(balances["mine"]["trusted"], amount * 2)
assert_equal(balances["watchonly"]["trusted"], 0)
self.nodes[0].unloadwallet("legacyspendableupdate")
# Tests the lastprocessedblock JSON object in getbalances, getwalletinfo
# and gettransaction by checking for valid hex strings and by comparing
@ -339,5 +374,30 @@ class WalletTest(BitcoinTestFramework):
assert_equal(tx_info['lastprocessedblock']['height'], prev_height)
assert_equal(tx_info['lastprocessedblock']['hash'], prev_hash)
self.log.info("Test that the balance is updated by an import that makes an untracked output in an existing tx \"mine\"")
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
self.nodes[0].createwallet("importupdate")
wallet = self.nodes[0].get_wallet_rpc("importupdate")
import_key1 = get_generate_key()
import_key2 = get_generate_key()
wallet.importprivkey(import_key1.privkey)
amount = 15
default.send([{import_key1.p2wpkh_addr: amount},{import_key2.p2wpkh_addr: amount}])
self.generate(self.nodes[0], 1)
# Mock the time forward by 1 day so that "now" will exclude the block we just mined
self.nodes[0].setmocktime(int(time.time()) + 86400)
# Mine 11 blocks to move the MTP past the block we just mined
self.generate(self.nodes[0], 11, sync_fun=self.no_op)
balances = wallet.getbalances()
assert_equal(balances["mine"]["trusted"], amount)
# Don't rescan to make sure that the import updates the wallet txos
wallet.importprivkey(privkey=import_key2.privkey, rescan=False)
balances = wallet.getbalances()
assert_equal(balances["mine"]["trusted"], amount * 2)
if __name__ == '__main__':
WalletTest().main()