This commit is contained in:
Ava Chow 2025-04-29 12:07:58 +02:00 committed by GitHub
commit 430480b4be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 657 additions and 384 deletions

View file

@ -71,10 +71,17 @@ void generateFakeBlock(const CChainParams& params,
coinbase_tx.vin[0].prevout.SetNull(); coinbase_tx.vin[0].prevout.SetNull();
coinbase_tx.vout.resize(2); coinbase_tx.vout.resize(2);
coinbase_tx.vout[0].scriptPubKey = coinbase_out_script; 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.vin[0].scriptSig = CScript() << ++tip.tip_height << OP_0;
coinbase_tx.vout[1].scriptPubKey = coinbase_out_script; // extra output coinbase_tx.vout[1].scriptPubKey = coinbase_out_script; // extra output
coinbase_tx.vout[1].nValue = 1 * COIN; 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.vtx = {MakeTransactionRef(std::move(coinbase_tx))};
block.nVersion = VERSIONBITS_LAST_OLD_BLOCK_VERSION; block.nVersion = VERSIONBITS_LAST_OLD_BLOCK_VERSION;
@ -129,14 +136,14 @@ static void WalletCreateTx(benchmark::Bench& bench, const OutputType output_type
// Check available balance // Check available balance
auto bal = WITH_LOCK(wallet.cs_wallet, return wallet::AvailableCoins(wallet).GetTotalAmount()); // Cache 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; wallet::CCoinControl coin_control;
coin_control.m_allow_other_inputs = allow_other_inputs; coin_control.m_allow_other_inputs = allow_other_inputs;
CAmount target = 0; CAmount target = 0;
if (preset_inputs) { if (preset_inputs) {
// Select inputs, each has 49 BTC // Select inputs, each has 48 BTC
wallet::CoinFilterParams filter_coins; wallet::CoinFilterParams filter_coins;
filter_coins.max_count = preset_inputs->num_of_internal_inputs; filter_coins.max_count = preset_inputs->num_of_internal_inputs;
const auto& res = WITH_LOCK(wallet.cs_wallet, const auto& res = WITH_LOCK(wallet.cs_wallet,
@ -189,7 +196,7 @@ static void AvailableCoins(benchmark::Bench& bench, const std::vector<OutputType
// Check available balance // Check available balance
auto bal = WITH_LOCK(wallet.cs_wallet, return wallet::AvailableCoins(wallet).GetTotalAmount()); // Cache 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([&] { bench.epochIterations(2).run([&] {
LOCK(wallet.cs_wallet); LOCK(wallet.cs_wallet);

View file

@ -4,6 +4,7 @@
#include <consensus/amount.h> #include <consensus/amount.h>
#include <consensus/consensus.h> #include <consensus/consensus.h>
#include <util/check.h>
#include <wallet/receive.h> #include <wallet/receive.h>
#include <wallet/transaction.h> #include <wallet/transaction.h>
#include <wallet/wallet.h> #include <wallet/wallet.h>
@ -145,52 +146,6 @@ CAmount CachedTxGetChange(const CWallet& wallet, const CWalletTx& wtx)
return wtx.nChangeCached; 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, void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
std::list<COutputEntry>& listReceived, std::list<COutputEntry>& listReceived,
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter, std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter,
@ -248,25 +203,38 @@ void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
} }
bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter) bool CheckIsFromMeMap(const std::map<isminefilter, bool>& from_me_map, const isminefilter& filter)
{ {
return (CachedTxGetDebit(wallet, wtx, filter) > 0); for (const auto& [from_me_filter, from_me] : from_me_map) {
if ((filter & from_me_filter) && from_me) {
return true;
}
}
return false;
} }
// NOLINTNEXTLINE(misc-no-recursion) // NOLINTNEXTLINE(misc-no-recursion)
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents) bool CachedTxIsTrusted(const CWallet& wallet, const TxState& state, const uint256& txid, std::set<uint256>& trusted_parents)
{ {
AssertLockHeld(wallet.cs_wallet); AssertLockHeld(wallet.cs_wallet);
if (wtx.isConfirmed()) return true;
if (wtx.isBlockConflicted()) return false; // This wtx is already trusted
// using wtx's cached debit if (trusted_parents.contains(txid)) return true;
if (!wallet.m_spend_zero_conf_change || !CachedTxIsFromMe(wallet, wtx, ISMINE_ALL)) return false;
if (std::holds_alternative<TxStateConfirmed>(state)) return true;
if (std::holds_alternative<TxStateBlockConflicted>(state)) return false;
// Don't trust unconfirmed transactions from us unless they are in the mempool. // Don't trust unconfirmed transactions from us unless they are in the mempool.
if (!wtx.InMempool()) return false; if (!std::holds_alternative<TxStateInMempool>(state)) return false;
const CWalletTx* wtx = wallet.GetWalletTx(txid);
assert(wtx);
// using wtx's cached debit
if (!wallet.m_spend_zero_conf_change || !CheckIsFromMeMap(wtx->m_from_me, ISMINE_ALL)) return false;
// Trusted if all inputs are from us and are in the mempool: // Trusted if all inputs are from us and are in the mempool:
for (const CTxIn& txin : wtx.tx->vin) for (const CTxIn& txin : wtx->tx->vin)
{ {
// Transactions not sent by us: not trusted // Transactions not sent by us: not trusted
const CWalletTx* parent = wallet.GetWalletTx(txin.prevout.hash); const CWalletTx* parent = wallet.GetWalletTx(txin.prevout.hash);
@ -277,12 +245,23 @@ bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uin
// If we've already trusted this parent, continue // If we've already trusted this parent, continue
if (trusted_parents.count(parent->GetHash())) continue; if (trusted_parents.count(parent->GetHash())) continue;
// Recurse to check that the parent is also trusted // Recurse to check that the parent is also trusted
if (!CachedTxIsTrusted(wallet, *parent, trusted_parents)) return false; if (!CachedTxIsTrusted(wallet, parent->GetState(), parent->GetHash(), trusted_parents)) return false;
trusted_parents.insert(parent->GetHash()); trusted_parents.insert(parent->GetHash());
} }
return true; return true;
} }
bool CachedTxIsTrusted(const CWallet& wallet, const TxState& state, const uint256& txid)
{
std::set<uint256> trusted_parents;
return CachedTxIsTrusted(wallet, state, txid, trusted_parents);
}
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents)
{
return CachedTxIsTrusted(wallet, wtx.GetState(), wtx.GetHash(), trusted_parents);
}
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx) bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx)
{ {
std::set<uint256> trusted_parents; std::set<uint256> trusted_parents;
@ -293,27 +272,42 @@ bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx)
Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse) Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse)
{ {
Balance ret; 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); LOCK(wallet.cs_wallet);
std::set<uint256> trusted_parents; for (const auto& [outpoint, txo] : wallet.GetTXOs()) {
for (const auto& entry : wallet.mapWallet) Assert(MoneyRange(txo.GetTxOut().nValue));
{
const CWalletTx& wtx = entry.second; const bool is_trusted{CachedTxIsTrusted(wallet, txo.GetState(), outpoint.hash)};
const bool is_trusted{CachedTxIsTrusted(wallet, wtx, trusted_parents)}; const int tx_depth{wallet.GetTxStateDepthInMainChain(txo.GetState())};
const int tx_depth{wallet.GetTxDepthInMainChain(wtx)}; Assert(tx_depth >= 0);
const CAmount tx_credit_mine{CachedTxGetAvailableCredit(wallet, wtx, ISMINE_SPENDABLE | reuse_filter)}; Assert(!wallet.IsSpent(outpoint, /*min_depth=*/1));
const CAmount tx_credit_watchonly{CachedTxGetAvailableCredit(wallet, wtx, ISMINE_WATCH_ONLY | reuse_filter)};
if (is_trusted && tx_depth >= min_depth) { if (!wallet.IsSpent(outpoint) && (allow_used_addresses || !wallet.IsSpentKey(txo.GetTxOut().scriptPubKey))) {
ret.m_mine_trusted += tx_credit_mine; // Get the amounts for mine and watchonly
ret.m_watchonly_trusted += tx_credit_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.IsTXOInImmatureCoinBase(txo) && std::holds_alternative<TxStateConfirmed>(txo.GetState())) {
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 && tx_depth == 0 && std::get_if<TxStateInMempool>(&txo.GetState())) {
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; return ret;
@ -325,32 +319,19 @@ std::map<CTxDestination, CAmount> GetAddressBalances(const CWallet& wallet)
{ {
LOCK(wallet.cs_wallet); LOCK(wallet.cs_wallet);
std::set<uint256> trusted_parents; for (const auto& [outpoint, txo] : wallet.GetTXOs()) {
for (const auto& walletEntry : wallet.mapWallet) if (!CachedTxIsTrusted(wallet, txo.GetState(), outpoint.hash)) continue;
{ if (wallet.IsTXOInImmatureCoinBase(txo)) continue;
const CWalletTx& wtx = walletEntry.second;
if (!CachedTxIsTrusted(wallet, wtx, trusted_parents)) int nDepth = wallet.GetTxStateDepthInMainChain(txo.GetState());
continue; if (nDepth < (CheckIsFromMeMap(txo.GetTxFromMe(), ISMINE_ALL) ? 0 : 1)) continue;
if (wallet.IsTxImmatureCoinBase(wtx)) CTxDestination addr;
continue; Assume(wallet.IsMine(txo.GetTxOut()));
if(!ExtractDestination(txo.GetTxOut().scriptPubKey, addr)) continue;
int nDepth = wallet.GetTxDepthInMainChain(wtx); CAmount n = wallet.IsSpent(outpoint) ? 0 : txo.GetTxOut().nValue;
if (nDepth < (CachedTxIsFromMe(wallet, wtx, ISMINE_ALL) ? 0 : 1)) balances[addr] += n;
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;
}
} }
} }

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 //! filter decides which addresses will count towards the debit
CAmount CachedTxGetDebit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter); CAmount CachedTxGetDebit(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter);
CAmount CachedTxGetChange(const CWallet& wallet, const CWalletTx& wtx); 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 struct COutputEntry
{ {
CTxDestination destination; CTxDestination destination;
@ -44,10 +40,13 @@ void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
std::list<COutputEntry>& listSent, std::list<COutputEntry>& listSent,
CAmount& nFee, const isminefilter& filter, CAmount& nFee, const isminefilter& filter,
bool include_change); bool include_change);
bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter); bool CachedTxIsTrusted(const CWallet& wallet, const TxState& state, const uint256& txid, std::set<uint256>& trusted_parents) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
bool CachedTxIsTrusted(const CWallet& wallet, const TxState& state, const uint256& txid) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uint256>& trusted_parents) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx); bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx);
bool CheckIsFromMeMap(const std::map<isminefilter, bool>& from_me_map, const isminefilter& filter);
struct Balance { struct Balance {
CAmount m_mine_trusted{0}; //!< Trusted, at depth=GetBalance.min_depth or more CAmount m_mine_trusted{0}; //!< Trusted, at depth=GetBalance.min_depth or more
CAmount m_mine_untrusted_pending{0}; //!< Untrusted, but in mempool (pending) CAmount m_mine_untrusted_pending{0}; //!< Untrusted, but in mempool (pending)

View file

@ -312,6 +312,7 @@ RPCHelpMan addmultisigaddress()
// Store destination in the addressbook // Store destination in the addressbook
pwallet->SetAddressBook(dest, label, AddressPurpose::SEND); pwallet->SetAddressBook(dest, label, AddressPurpose::SEND);
pwallet->RefreshAllTXOs();
// Make the descriptor // Make the descriptor
std::unique_ptr<Descriptor> descriptor = InferDescriptor(GetScriptForDestination(dest), spk_man); std::unique_ptr<Descriptor> descriptor = InferDescriptor(GetScriptForDestination(dest), spk_man);
@ -371,6 +372,7 @@ RPCHelpMan keypoolrefill()
if (pwallet->GetKeyPoolSize() < kpSize) { if (pwallet->GetKeyPoolSize() < kpSize) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error refreshing keypool."); throw JSONRPCError(RPC_WALLET_ERROR, "Error refreshing keypool.");
} }
pwallet->RefreshAllTXOs();
return UniValue::VNULL; return UniValue::VNULL;
}, },
@ -402,6 +404,7 @@ RPCHelpMan newkeypool()
LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true); LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true);
spk_man.NewKeyPool(); spk_man.NewKeyPool();
pwallet->RefreshAllTXOs();
return UniValue::VNULL; return UniValue::VNULL;
}, },

View file

@ -206,6 +206,7 @@ RPCHelpMan importprivkey()
pwallet->ImportScripts({GetScriptForDestination(WitnessV0KeyHash(vchAddress))}, /*timestamp=*/0); pwallet->ImportScripts({GetScriptForDestination(WitnessV0KeyHash(vchAddress))}, /*timestamp=*/0);
} }
} }
pwallet->RefreshAllTXOs();
} }
if (fRescan) { if (fRescan) {
RescanWallet(*pwallet, reserver); RescanWallet(*pwallet, reserver);
@ -306,6 +307,7 @@ RPCHelpMan importaddress()
} else { } else {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address or script"); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address or script");
} }
pwallet->RefreshAllTXOs();
} }
if (fRescan) if (fRescan)
{ {
@ -472,6 +474,8 @@ RPCHelpMan importpubkey()
pwallet->ImportScriptPubKeys(strLabel, script_pub_keys, /*have_solving_data=*/true, /*apply_label=*/true, /*timestamp=*/1); pwallet->ImportScriptPubKeys(strLabel, script_pub_keys, /*have_solving_data=*/true, /*apply_label=*/true, /*timestamp=*/1);
pwallet->ImportPubKeys({{pubKey.GetID(), false}}, {{pubKey.GetID(), pubKey}} , /*key_origins=*/{}, /*add_keypool=*/false, /*timestamp=*/1); pwallet->ImportPubKeys({{pubKey.GetID(), false}}, {{pubKey.GetID(), pubKey}} , /*key_origins=*/{}, /*add_keypool=*/false, /*timestamp=*/1);
pwallet->RefreshAllTXOs();
} }
if (fRescan) if (fRescan)
{ {
@ -619,6 +623,7 @@ RPCHelpMan importwallet()
progress++; progress++;
} }
pwallet->RefreshAllTXOs();
pwallet->chain().showProgress("", 100, false); // hide progress dialog in GUI pwallet->chain().showProgress("", 100, false); // hide progress dialog in GUI
} }
pwallet->chain().showProgress("", 100, false); // hide progress dialog in GUI pwallet->chain().showProgress("", 100, false); // hide progress dialog in GUI
@ -1410,6 +1415,8 @@ RPCHelpMan importmulti()
nLowestTimestamp = timestamp; nLowestTimestamp = timestamp;
} }
} }
pwallet->RefreshAllTXOs();
} }
if (fRescan && fRunScan && requests.size()) { if (fRescan && fRunScan && requests.size()) {
int64_t scannedTime = pwallet->RescanFromTime(nLowestTimestamp, reserver, /*update=*/true); int64_t scannedTime = pwallet->RescanFromTime(nLowestTimestamp, reserver, /*update=*/true);
@ -1722,6 +1729,7 @@ RPCHelpMan importdescriptors()
} }
} }
pwallet->ConnectScriptPubKeyManNotifiers(); pwallet->ConnectScriptPubKeyManNotifiers();
pwallet->RefreshAllTXOs();
} }
// Rescan the blockchain using the lowest timestamp // Rescan the blockchain using the lowest timestamp

View file

@ -331,7 +331,7 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM
CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine, include_change); CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine, include_change);
bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY); bool involvesWatchonly = CheckIsFromMeMap(wtx.m_from_me, ISMINE_WATCH_ONLY);
// Sent // Sent
if (!filter_label.has_value()) if (!filter_label.has_value())
@ -780,10 +780,11 @@ RPCHelpMan gettransaction()
CAmount nCredit = CachedTxGetCredit(*pwallet, wtx, filter); CAmount nCredit = CachedTxGetCredit(*pwallet, wtx, filter);
CAmount nDebit = CachedTxGetDebit(*pwallet, wtx, filter); CAmount nDebit = CachedTxGetDebit(*pwallet, wtx, filter);
CAmount nNet = nCredit - nDebit; CAmount nNet = nCredit - nDebit;
CAmount nFee = (CachedTxIsFromMe(*pwallet, wtx, filter) ? wtx.tx->GetValueOut() - nDebit : 0); bool from_me = CheckIsFromMeMap(wtx.m_from_me, filter);
CAmount nFee = (from_me ? wtx.tx->GetValueOut() - nDebit : 0);
entry.pushKV("amount", ValueFromAmount(nNet - nFee)); entry.pushKV("amount", ValueFromAmount(nNet - nFee));
if (CachedTxIsFromMe(*pwallet, wtx, filter)) if (from_me)
entry.pushKV("fee", ValueFromAmount(nFee)); entry.pushKV("fee", ValueFromAmount(nFee));
WalletTxToJSON(*pwallet, wtx, entry); WalletTxToJSON(*pwallet, wtx, entry);

View file

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

View file

@ -276,12 +276,8 @@ util::Result<PreSelectedInputs> FetchSelectedInputs(const CWallet& wallet, const
input_bytes = GetVirtualTransactionSize(input_bytes, 0, 0); input_bytes = GetVirtualTransactionSize(input_bytes, 0, 0);
} }
CTxOut txout; CTxOut txout;
if (auto ptr_wtx = wallet.GetWalletTx(outpoint.hash)) { if (auto txo = wallet.GetTXO(outpoint)) {
// Clearly invalid input, fail txout = txo->GetTxOut();
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 (input_bytes == -1) { if (input_bytes == -1) {
input_bytes = CalculateMaximumSignedInputSize(txout, &wallet, &coin_control); input_bytes = CalculateMaximumSignedInputSize(txout, &wallet, &coin_control);
} }
@ -329,137 +325,145 @@ CoinsResult AvailableCoins(const CWallet& wallet,
std::vector<COutPoint> outpoints; std::vector<COutPoint> outpoints;
std::set<uint256> trusted_parents; std::set<uint256> trusted_parents;
for (const auto& entry : wallet.mapWallet) // 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;
const uint256& txid = entry.first; for (const auto& [outpoint, txo] : wallet.GetTXOs()) {
const CWalletTx& wtx = entry.second; const CTxOut& output = txo.GetTxOut();
if (wallet.IsTxImmatureCoinBase(wtx) && !params.include_immature_coinbase) if (tx_safe_cache.contains(outpoint.hash) && !tx_safe_cache.at(outpoint.hash).first) {
continue;
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;
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; continue;
} }
if (nDepth < min_depth || nDepth > max_depth) { // 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;
int nDepth = wallet.GetTxStateDepthInMainChain(txo.GetState());
Assert(nDepth >= 0);
Assert(!wallet.IsSpent(outpoint, /*min_depth=*/1));
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; continue;
} }
bool tx_from_me = CachedTxIsFromMe(wallet, wtx, ISMINE_ALL); if (wallet.IsTXOInImmatureCoinBase(txo) && !params.include_immature_coinbase)
continue;
for (unsigned int i = 0; i < wtx.tx->vout.size(); i++) { isminetype mine = wallet.IsMine(output);
const CTxOut& output = wtx.tx->vout[i];
const COutPoint outpoint(Txid::FromUint256(txid), i);
if (output.nValue < params.min_amount || output.nValue > params.max_amount) assert(mine != ISMINE_NO);
if (!tx_safe_cache.contains(outpoint.hash)) {
tx_safe_cache[outpoint.hash] = {false, false};
const CWalletTx& wtx = *wallet.GetWalletTx(outpoint.hash);
// 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; continue;
// Skip manually selected coins (the caller can fetch them directly) bool safeTx = CachedTxIsTrusted(wallet, wtx, trusted_parents);
if (coinControl && coinControl->HasSelected() && coinControl->IsSelected(outpoint))
continue;
if (wallet.IsLockedCoin(outpoint) && params.skip_locked) // We should not consider coins from transactions that are replacing
continue; // 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;
}
if (wallet.IsSpent(outpoint)) // Similarly, we should not consider coins from transactions that
continue; // 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;
}
isminetype mine = wallet.IsMine(output); if (only_safe && !safeTx) {
if (mine == ISMINE_NO) {
continue; continue;
} }
if (!allow_used_addresses && wallet.IsSpentKey(output.scriptPubKey)) { if (nDepth < min_depth || nDepth > max_depth) {
continue; continue;
} }
std::unique_ptr<SigningProvider> provider = wallet.GetSolvingProvider(output.scriptPubKey); tx_safe_cache[outpoint.hash] = {true, safeTx};
}
const auto& [tx_ok, tx_safe] = tx_safe_cache.at(outpoint.hash);
if (!Assume(tx_ok)) {
continue;
}
int input_bytes = CalculateMaximumSignedInputSize(output, COutPoint(), provider.get(), can_grind_r, coinControl); bool tx_from_me = CheckIsFromMeMap(txo.GetTxFromMe(), ISMINE_ALL);
// 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 std::unique_ptr<SigningProvider> provider = wallet.GetSolvingProvider(output.scriptPubKey);
if (!spendable && params.only_spendable) continue;
// Obtain script type int input_bytes = CalculateMaximumSignedInputSize(output, COutPoint(), provider.get(), can_grind_r, coinControl);
std::vector<std::vector<uint8_t>> script_solutions; // Because CalculateMaximumSignedInputSize infers a solvable descriptor to get the satisfaction size,
TxoutType type = Solver(output.scriptPubKey, script_solutions); // 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));
// If the output is P2SH and solvable, we want to know if it is // Filter by spendable outputs only
// a P2SH (legacy) or one of P2SH-P2WPKH, P2SH-P2WSH (P2SH-Segwit). We can determine if (!spendable && params.only_spendable) continue;
// 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), // Obtain script type
COutput(outpoint, output, nDepth, input_bytes, spendable, solvable, safeTx, wtx.GetTxTime(), tx_from_me, feerate)); std::vector<std::vector<uint8_t>> script_solutions;
TxoutType type = Solver(output.scriptPubKey, script_solutions);
outpoints.push_back(outpoint); // 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;
}
// Checks the sum amount of all UTXO's. result.Add(GetOutputType(type, is_from_p2sh),
if (params.min_sum_amount != MAX_MONEY) { COutput(outpoint, output, nDepth, input_bytes, spendable, solvable, tx_safe, txo.GetTxTime(), tx_from_me, feerate));
if (result.GetTotalAmount() >= params.min_sum_amount) {
return result;
}
}
// Checks the maximum number of UTXO's. outpoints.push_back(outpoint);
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; 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()) { if (feerate.has_value()) {

View file

@ -7,6 +7,7 @@
#include <script/solver.h> #include <script/solver.h>
#include <validation.h> #include <validation.h>
#include <wallet/coincontrol.h> #include <wallet/coincontrol.h>
#include <wallet/context.h>
#include <wallet/spend.h> #include <wallet/spend.h>
#include <wallet/test/util.h> #include <wallet/test/util.h>
#include <wallet/test/wallet_test_fixture.h> #include <wallet/test/wallet_test_fixture.h>
@ -19,7 +20,10 @@ BOOST_FIXTURE_TEST_SUITE(spend_tests, WalletTestingSetup)
BOOST_FIXTURE_TEST_CASE(SubtractFee, TestChain100Setup) BOOST_FIXTURE_TEST_CASE(SubtractFee, TestChain100Setup)
{ {
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
auto wallet = CreateSyncedWallet(*m_node.chain, WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain()), coinbaseKey); WalletContext context;
context.chain = m_node.chain.get();
context.args = m_node.args;
auto wallet = CreateSyncedWallet(context, coinbaseKey);
// Check that a subtract-from-recipient transaction slightly less than the // Check that a subtract-from-recipient transaction slightly less than the
// coinbase input amount does not create a change output (because it would // coinbase input amount does not create a change output (because it would
@ -67,7 +71,10 @@ BOOST_FIXTURE_TEST_CASE(wallet_duplicated_preset_inputs_test, TestChain100Setup)
// Add 4 spendable UTXO, 50 BTC each, to the wallet (total balance 200 BTC) // Add 4 spendable UTXO, 50 BTC each, to the wallet (total balance 200 BTC)
for (int i = 0; i < 4; i++) CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); for (int i = 0; i < 4; i++) CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
auto wallet = CreateSyncedWallet(*m_node.chain, WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain()), coinbaseKey); WalletContext context;
context.chain = m_node.chain.get();
context.args = m_node.args;
auto wallet = CreateSyncedWallet(context, coinbaseKey);
LOCK(wallet->cs_wallet); LOCK(wallet->cs_wallet);
auto available_coins = AvailableCoins(*wallet); auto available_coins = AvailableCoins(*wallet);

View file

@ -17,18 +17,17 @@
#include <memory> #include <memory>
namespace wallet { namespace wallet {
std::unique_ptr<CWallet> CreateSyncedWallet(interfaces::Chain& chain, CChain& cchain, const CKey& key) std::shared_ptr<CWallet> CreateSyncedWallet(WalletContext& context, const CKey& key)
{ {
auto wallet = std::make_unique<CWallet>(&chain, "", CreateMockableWalletDatabase()); bilingual_str error;
{ std::vector<bilingual_str> warnings;
LOCK2(wallet->cs_wallet, ::cs_main); auto wallet = CWallet::Create(context, "", CreateMockableWalletDatabase(), WALLET_FLAG_DESCRIPTORS, error, warnings);
wallet->SetLastBlockProcessed(cchain.Height(), cchain.Tip()->GetBlockHash());
} // Allow the fallback fee with it's default
wallet->m_allow_fallback_fee = true;
{ {
LOCK(wallet->cs_wallet); LOCK(wallet->cs_wallet);
wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
wallet->SetupDescriptorScriptPubKeyMans();
FlatSigningProvider provider; FlatSigningProvider provider;
std::string error; std::string error;
auto descs = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false); auto descs = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false);
@ -40,11 +39,13 @@ std::unique_ptr<CWallet> CreateSyncedWallet(interfaces::Chain& chain, CChain& cc
} }
WalletRescanReserver reserver(*wallet); WalletRescanReserver reserver(*wallet);
reserver.reserve(); reserver.reserve();
CWallet::ScanResult result = wallet->ScanForWalletTransactions(cchain.Genesis()->GetBlockHash(), /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false); CWallet::ScanResult result = wallet->ScanForWalletTransactions(context.chain->getBlockHash(0), /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
assert(result.status == CWallet::ScanResult::SUCCESS); assert(result.status == CWallet::ScanResult::SUCCESS);
assert(result.last_scanned_block == cchain.Tip()->GetBlockHash()); int tip_height = context.chain->getHeight().value();
assert(*result.last_scanned_height == cchain.Height()); assert(*result.last_scanned_height == tip_height);
assert(result.last_scanned_block == context.chain->getBlockHash(tip_height));
assert(result.last_failed_block.IsNull()); assert(result.last_failed_block.IsNull());
return wallet; return wallet;
} }

View file

@ -30,7 +30,7 @@ static const DatabaseFormat DATABASE_FORMATS[] = {
const std::string ADDRESS_BCRT1_UNSPENDABLE = "bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj"; const std::string ADDRESS_BCRT1_UNSPENDABLE = "bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3xueyj";
std::unique_ptr<CWallet> CreateSyncedWallet(interfaces::Chain& chain, CChain& cchain, const CKey& key); std::shared_ptr<CWallet> CreateSyncedWallet(WalletContext& chain, const CKey& key);
std::shared_ptr<CWallet> TestLoadWallet(WalletContext& context); std::shared_ptr<CWallet> TestLoadWallet(WalletContext& context);
std::shared_ptr<CWallet> TestLoadWallet(std::unique_ptr<WalletDatabase> database, WalletContext& context, uint64_t create_flags); std::shared_ptr<CWallet> TestLoadWallet(std::unique_ptr<WalletDatabase> database, WalletContext& context, uint64_t create_flags);

View file

@ -229,35 +229,6 @@ BOOST_FIXTURE_TEST_CASE(write_wallet_settings_concurrently, TestingSetup)
/*num_expected_wallets=*/0); /*num_expected_wallets=*/0);
} }
// 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) static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lockTime, int64_t mockTime, int64_t blockTime)
{ {
CMutableTransaction tx; CMutableTransaction tx;
@ -279,7 +250,7 @@ static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lock
// Assign wtx.m_state to simplify test and avoid the need to simulate // Assign wtx.m_state to simplify test and avoid the need to simulate
// reorg events. Without this, AddToWallet asserts false when the same // reorg events. Without this, AddToWallet asserts false when the same
// transaction is confirmed in different blocks. // transaction is confirmed in different blocks.
wtx.m_state = state; wtx.SetState(state);
return true; return true;
})->nTimeSmart; })->nTimeSmart;
} }
@ -367,7 +338,10 @@ public:
ListCoinsTestingSetup() ListCoinsTestingSetup()
{ {
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
wallet = CreateSyncedWallet(*m_node.chain, WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain()), coinbaseKey); WalletContext context;
context.chain = m_node.chain.get();
context.args = m_node.args;
wallet = CreateSyncedWallet(context, coinbaseKey);
} }
~ListCoinsTestingSetup() ~ListCoinsTestingSetup()
@ -391,17 +365,16 @@ public:
blocktx = CMutableTransaction(*wallet->mapWallet.at(tx->GetHash()).tx); blocktx = CMutableTransaction(*wallet->mapWallet.at(tx->GetHash()).tx);
} }
CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
m_node.validation_signals->SyncWithValidationInterfaceQueue();
LOCK(wallet->cs_wallet); LOCK(wallet->cs_wallet);
LOCK(Assert(m_node.chainman)->GetMutex());
wallet->SetLastBlockProcessed(wallet->GetLastBlockHeight() + 1, m_node.chainman->ActiveChain().Tip()->GetBlockHash());
auto it = wallet->mapWallet.find(tx->GetHash()); auto it = wallet->mapWallet.find(tx->GetHash());
BOOST_CHECK(it != wallet->mapWallet.end()); BOOST_CHECK(it != wallet->mapWallet.end());
it->second.m_state = TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/1}; BOOST_CHECK(it->second.state<TxStateConfirmed>());
return it->second; return it->second;
} }
std::unique_ptr<CWallet> wallet; std::shared_ptr<CWallet> wallet;
}; };
BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup) BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup)
@ -464,9 +437,10 @@ BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup)
void TestCoinsResult(ListCoinsTest& context, OutputType out_type, CAmount amount, void TestCoinsResult(ListCoinsTest& context, OutputType out_type, CAmount amount,
std::map<OutputType, size_t>& expected_coins_sizes) std::map<OutputType, size_t>& expected_coins_sizes)
{ {
LOCK(context.wallet->cs_wallet);
util::Result<CTxDestination> dest = Assert(context.wallet->GetNewDestination(out_type, "")); util::Result<CTxDestination> dest = Assert(context.wallet->GetNewDestination(out_type, ""));
CWalletTx& wtx = context.AddTx(CRecipient{*dest, amount, /*fSubtractFeeFromAmount=*/true}); CWalletTx& wtx = context.AddTx(CRecipient{*dest, amount, /*fSubtractFeeFromAmount=*/true});
LOCK(context.wallet->cs_wallet);
CoinFilterParams filter; CoinFilterParams filter;
filter.skip_locked = false; filter.skip_locked = false;
CoinsResult available_coins = AvailableCoins(*context.wallet, nullptr, std::nullopt, filter); CoinsResult available_coins = AvailableCoins(*context.wallet, nullptr, std::nullopt, filter);
@ -736,65 +710,5 @@ BOOST_FIXTURE_TEST_CASE(RemoveTxs, TestChain100Setup)
TestUnloadWallet(std::move(wallet)); 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(m_rng.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() BOOST_AUTO_TEST_SUITE_END()
} // namespace wallet } // namespace wallet

View file

@ -32,7 +32,7 @@ int64_t CWalletTx::GetTxTime() const
void CWalletTx::updateState(interfaces::Chain& chain) void CWalletTx::updateState(interfaces::Chain& chain)
{ {
bool active; bool active;
auto lookup_block = [&](const uint256& hash, int& height, TxState& state) { auto lookup_block = [&](const uint256& hash, int& height) {
// If tx block (or conflicting block) was reorged out of chain // If tx block (or conflicting block) was reorged out of chain
// while the wallet was shutdown, change tx status to UNCONFIRMED // while the wallet was shutdown, change tx status to UNCONFIRMED
// and reset block height, hash, and index. ABANDONED tx don't have // and reset block height, hash, and index. ABANDONED tx don't have
@ -40,18 +40,27 @@ void CWalletTx::updateState(interfaces::Chain& chain)
// transaction was reorged out while online and then reconfirmed // transaction was reorged out while online and then reconfirmed
// while offline is covered by the rescan logic. // while offline is covered by the rescan logic.
if (!chain.findBlock(hash, FoundBlock().inActiveChain(active).height(height)) || !active) { if (!chain.findBlock(hash, FoundBlock().inActiveChain(active).height(height)) || !active) {
state = TxStateInactive{}; SetState(TxStateInactive{});
} }
}; };
if (auto* conf = state<TxStateConfirmed>()) { if (auto* conf = state<TxStateConfirmed>()) {
lookup_block(conf->confirmed_block_hash, conf->confirmed_block_height, m_state); lookup_block(conf->confirmed_block_hash, conf->confirmed_block_height);
} else if (auto* conf = state<TxStateBlockConflicted>()) { } else if (auto* conf = state<TxStateBlockConflicted>()) {
lookup_block(conf->conflicting_block_hash, conf->conflicting_block_height, m_state); lookup_block(conf->conflicting_block_hash, conf->conflicting_block_height);
} }
} }
void CWalletTx::CopyFrom(const CWalletTx& _tx) void CWalletTx::CopyFrom(const CWalletTx& _tx)
{ {
*this = _tx; *this = _tx;
m_txos.clear();
}
void CWalletTx::SetState(const TxState& state)
{
m_state = state;
for (auto [_, txo] : m_txos) {
txo->SetState(state);
}
} }
} // namespace wallet } // namespace wallet

View file

@ -19,6 +19,7 @@
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <utility> #include <utility>
#include <unordered_map>
#include <variant> #include <variant>
#include <vector> #include <vector>
@ -169,6 +170,8 @@ public:
} }
}; };
class WalletTXO;
/** /**
* A transaction with a bunch of additional info that only the owner cares about. * A transaction with a bunch of additional info that only the owner cares about.
* It includes any unrecorded transactions needed to link it back to the block chain. * It includes any unrecorded transactions needed to link it back to the block chain.
@ -216,16 +219,16 @@ public:
*/ */
unsigned int nTimeSmart; unsigned int nTimeSmart;
/** /**
* From me flag is set to 1 for transactions that were created by the wallet * From me flags are set to 1 for transactions that were created by the wallet
* on this bitcoin node, and set to 0 for transactions that were created * on this bitcoin node, and set to 0 for transactions that were created
* externally and came in through the network or sendrawtransaction RPC. * externally and came in through the network or sendrawtransaction RPC.
*/ */
bool fFromMe; std::map<isminefilter, bool> m_from_me;
int64_t nOrderPos; //!< position in ordered transaction list int64_t nOrderPos; //!< position in ordered transaction list
std::multimap<int64_t, CWalletTx*>::const_iterator m_it_wtxOrdered; std::multimap<int64_t, CWalletTx*>::const_iterator m_it_wtxOrdered;
// memory only // 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]; mutable CachableAmount m_amounts[AMOUNTTYPE_ENUM_ELEMENTS];
/** /**
* This flag is true if all m_amounts caches are empty. This is particularly * This flag is true if all m_amounts caches are empty. This is particularly
@ -237,6 +240,8 @@ public:
mutable bool fChangeCached; mutable bool fChangeCached;
mutable CAmount nChangeCached; mutable CAmount nChangeCached;
mutable std::unordered_map<uint32_t, WalletTXO*> m_txos;
CWalletTx(CTransactionRef tx, const TxState& state) : tx(std::move(tx)), m_state(state) CWalletTx(CTransactionRef tx, const TxState& state) : tx(std::move(tx)), m_state(state)
{ {
Init(); Init();
@ -249,15 +254,18 @@ public:
fTimeReceivedIsTxTime = false; fTimeReceivedIsTxTime = false;
nTimeReceived = 0; nTimeReceived = 0;
nTimeSmart = 0; nTimeSmart = 0;
fFromMe = false;
fChangeCached = false; fChangeCached = false;
nChangeCached = 0; nChangeCached = 0;
nOrderPos = -1; nOrderPos = -1;
m_from_me.clear();
} }
CTransactionRef tx; CTransactionRef tx;
private:
TxState m_state; TxState m_state;
public:
// Set of mempool transactions that conflict // Set of mempool transactions that conflict
// directly with the transaction, or that conflict // directly with the transaction, or that conflict
// with an ancestor transaction. This set will be // with an ancestor transaction. This set will be
@ -281,10 +289,10 @@ public:
std::vector<uint8_t> dummy_vector1; //!< Used to be vMerkleBranch std::vector<uint8_t> dummy_vector1; //!< Used to be vMerkleBranch
std::vector<uint8_t> dummy_vector2; //!< Used to be vtxPrev std::vector<uint8_t> dummy_vector2; //!< Used to be vtxPrev
bool dummy_bool = false; //!< Used to be fSpent bool dummy_bool = false; //!< Used to be fSpent and fFromMe
uint256 serializedHash = TxStateSerializedBlockHash(m_state); uint256 serializedHash = TxStateSerializedBlockHash(m_state);
int serializedIndex = TxStateSerializedIndex(m_state); int serializedIndex = TxStateSerializedIndex(m_state);
s << TX_WITH_WITNESS(tx) << serializedHash << dummy_vector1 << serializedIndex << dummy_vector2 << mapValueCopy << vOrderForm << fTimeReceivedIsTxTime << nTimeReceived << fFromMe << dummy_bool; s << TX_WITH_WITNESS(tx) << serializedHash << dummy_vector1 << serializedIndex << dummy_vector2 << mapValueCopy << vOrderForm << fTimeReceivedIsTxTime << nTimeReceived << dummy_bool << dummy_bool << m_from_me;
} }
template<typename Stream> template<typename Stream>
@ -294,10 +302,14 @@ public:
std::vector<uint256> dummy_vector1; //!< Used to be vMerkleBranch std::vector<uint256> dummy_vector1; //!< Used to be vMerkleBranch
std::vector<CMerkleTx> dummy_vector2; //!< Used to be vtxPrev std::vector<CMerkleTx> dummy_vector2; //!< Used to be vtxPrev
bool dummy_bool; //! Used to be fSpent bool dummy_bool; //! Used to be fSpent and fFromMe
uint256 serialized_block_hash; uint256 serialized_block_hash;
int serializedIndex; int serializedIndex;
s >> TX_WITH_WITNESS(tx) >> serialized_block_hash >> dummy_vector1 >> serializedIndex >> dummy_vector2 >> mapValue >> vOrderForm >> fTimeReceivedIsTxTime >> nTimeReceived >> fFromMe >> dummy_bool; s >> TX_WITH_WITNESS(tx) >> serialized_block_hash >> dummy_vector1 >> serializedIndex >> dummy_vector2 >> mapValue >> vOrderForm >> fTimeReceivedIsTxTime >> nTimeReceived >> dummy_bool >> dummy_bool;
if (!s.eof()) {
s >> m_from_me;
}
m_state = TxStateInterpretSerialized({serialized_block_hash, serializedIndex}); m_state = TxStateInterpretSerialized({serialized_block_hash, serializedIndex});
@ -322,8 +334,6 @@ public:
{ {
m_amounts[DEBIT].Reset(); m_amounts[DEBIT].Reset();
m_amounts[CREDIT].Reset(); m_amounts[CREDIT].Reset();
m_amounts[IMMATURE_CREDIT].Reset();
m_amounts[AVAILABLE_CREDIT].Reset();
fChangeCached = false; fChangeCached = false;
m_is_cache_empty = true; m_is_cache_empty = true;
} }
@ -337,6 +347,8 @@ public:
template<typename T> const T* state() const { return std::get_if<T>(&m_state); } template<typename T> const T* state() const { return std::get_if<T>(&m_state); }
template<typename T> T* state() { return std::get_if<T>(&m_state); } template<typename T> T* state() { return std::get_if<T>(&m_state); }
void SetState(const TxState& state);
const TxState& GetState() const { return m_state; }
//! Update transaction state when attaching to a chain, filling in heights //! Update transaction state when attaching to a chain, filling in heights
//! of conflicted and confirmed blocks //! of conflicted and confirmed blocks
@ -369,6 +381,41 @@ struct WalletTxOrderComparator {
return a->nOrderPos < b->nOrderPos; return a->nOrderPos < b->nOrderPos;
} }
}; };
class WalletTXO
{
private:
const CTxOut& m_output;
isminetype m_ismine;
TxState m_tx_state;
bool m_tx_coinbase;
std::map<isminefilter, bool> m_tx_from_me;
int64_t m_tx_time;
public:
WalletTXO(const CTxOut& output, const isminetype ismine, const TxState& state, bool coinbase, const std::map<isminefilter, bool>& tx_from_me, int64_t tx_time)
: m_output(output),
m_ismine(ismine),
m_tx_state(state),
m_tx_coinbase(coinbase),
m_tx_from_me(tx_from_me),
m_tx_time(tx_time)
{}
const CTxOut& GetTxOut() const { return m_output; }
isminetype GetIsMine() const { return m_ismine; }
void SetIsMine(isminetype ismine) { m_ismine = ismine; }
const TxState& GetState() const { return m_tx_state; }
void SetState(const TxState& state) { m_tx_state = state; }
bool IsTxCoinBase() const { return m_tx_coinbase; }
const std::map<isminefilter, bool>& GetTxFromMe() const { return m_tx_from_me; }
int64_t GetTxTime() const { return m_tx_time; }
};
} // namespace wallet } // namespace wallet
#endif // BITCOIN_WALLET_TRANSACTION_H #endif // BITCOIN_WALLET_TRANSACTION_H

View file

@ -137,12 +137,17 @@ static void UpdateWalletSetting(interfaces::Chain& chain,
* immediately knows the transaction's status: Whether it can be considered * immediately knows the transaction's status: Whether it can be considered
* trusted and is eligible to be abandoned ... * trusted and is eligible to be abandoned ...
*/ */
static void RefreshMempoolStatus(CWalletTx& tx, interfaces::Chain& chain) static void RefreshMempoolStatus(CWallet& wallet, CWalletTx& tx, interfaces::Chain& chain) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
{ {
AssertLockHeld(wallet.cs_wallet);
std::optional<TxState> state;
if (chain.isInMempool(tx.GetHash())) { if (chain.isInMempool(tx.GetHash())) {
tx.m_state = TxStateInMempool(); state = TxStateInMempool();
} else if (tx.state<TxStateInMempool>()) { } else if (tx.state<TxStateInMempool>()) {
tx.m_state = TxStateInactive(); state = TxStateInactive();
}
if (state) {
tx.SetState(*state);
} }
} }
@ -754,7 +759,6 @@ void CWallet::SyncMetaData(std::pair<TxSpends::iterator, TxSpends::iterator> ran
// fTimeReceivedIsTxTime not copied on purpose // fTimeReceivedIsTxTime not copied on purpose
// nTimeReceived not copied on purpose // nTimeReceived not copied on purpose
copyTo->nTimeSmart = copyFrom->nTimeSmart; copyTo->nTimeSmart = copyFrom->nTimeSmart;
copyTo->fFromMe = copyFrom->fFromMe;
// nOrderPos not copied on purpose // nOrderPos not copied on purpose
// cached members not copied on purpose // cached members not copied on purpose
} }
@ -764,7 +768,7 @@ void CWallet::SyncMetaData(std::pair<TxSpends::iterator, TxSpends::iterator> ran
* Outpoint is spent if any non-conflicted transaction * Outpoint is spent if any non-conflicted transaction
* spends it: * spends it:
*/ */
bool CWallet::IsSpent(const COutPoint& outpoint) const bool CWallet::IsSpent(const COutPoint& outpoint, int min_depth) const
{ {
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range; std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range;
range = mapTxSpends.equal_range(outpoint); range = mapTxSpends.equal_range(outpoint);
@ -774,8 +778,14 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const
const auto mit = mapWallet.find(wtxid); const auto mit = mapWallet.find(wtxid);
if (mit != mapWallet.end()) { if (mit != mapWallet.end()) {
const auto& wtx = mit->second; const auto& wtx = mit->second;
if (!wtx.isAbandoned() && !wtx.isBlockConflicted() && !wtx.isMempoolConflicted()) int depth = GetTxDepthInMainChain(wtx);
return true; // Spent if (depth == 0) {
if (min_depth == 0 && !wtx.isAbandoned() && !wtx.isMempoolConflicted()) {
return true;
}
} else if (depth >= min_depth) {
return true;
}
} }
} }
return false; return false;
@ -1005,7 +1015,7 @@ bool CWallet::MarkReplaced(const uint256& originalHash, const uint256& newHash)
wtx.mapValue["replaced_by_txid"] = newHash.ToString(); wtx.mapValue["replaced_by_txid"] = newHash.ToString();
// Refresh mempool status without waiting for transactionRemovedFromMempool or transactionAddedToMempool // Refresh mempool status without waiting for transactionRemovedFromMempool or transactionAddedToMempool
RefreshMempoolStatus(wtx, chain()); RefreshMempoolStatus(*this, wtx, chain());
WalletBatch batch(GetDatabase()); WalletBatch batch(GetDatabase());
@ -1103,16 +1113,20 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
// Update birth time when tx time is older than it. // Update birth time when tx time is older than it.
MaybeUpdateBirthTime(wtx.GetTxTime()); MaybeUpdateBirthTime(wtx.GetTxTime());
for (auto filter : {ISMINE_SPENDABLE, ISMINE_WATCH_ONLY}) {
wtx.m_from_me[filter] = GetDebit(*wtx.tx, filter) > 0;
}
} }
if (!fInsertedNew) if (!fInsertedNew)
{ {
if (state.index() != wtx.m_state.index()) { if (state.index() != wtx.GetState().index()) {
wtx.m_state = state; wtx.SetState(state);
fUpdated = true; fUpdated = true;
} else { } else {
assert(TxStateSerializedIndex(wtx.m_state) == TxStateSerializedIndex(state)); assert(TxStateSerializedIndex(wtx.GetState()) == TxStateSerializedIndex(state));
assert(TxStateSerializedBlockHash(wtx.m_state) == TxStateSerializedBlockHash(state)); assert(TxStateSerializedBlockHash(wtx.GetState()) == TxStateSerializedBlockHash(state));
} }
// If we have a witness-stripped version of this transaction, and we // If we have a witness-stripped version of this transaction, and we
// see a new version with a witness, then we must be upgrading a pre-segwit // see a new version with a witness, then we must be upgrading a pre-segwit
@ -1134,7 +1148,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
while (!txs.empty()) { while (!txs.empty()) {
CWalletTx* desc_tx = txs.back(); CWalletTx* desc_tx = txs.back();
txs.pop_back(); txs.pop_back();
desc_tx->m_state = inactive_state; desc_tx->SetState(inactive_state);
// Break caches since we have changed the state // Break caches since we have changed the state
desc_tx->MarkDirty(); desc_tx->MarkDirty();
batch.WriteTx(*desc_tx); batch.WriteTx(*desc_tx);
@ -1163,6 +1177,23 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
// Break debit/credit balance caches: // Break debit/credit balance caches:
wtx.MarkDirty(); wtx.MarkDirty();
// Remove or add back the inputs from m_txos to match the state of this tx.
if (wtx.isConfirmed())
{
// When a transaction becomes confirmed, we can remove all of the txos that were spent
// in its inputs as they are no longer relevant.
for (const CTxIn& txin : wtx.tx->vin) {
MarkTXOUnusable(txin.prevout);
}
} else if (wtx.isInactive()) {
// When a transaction becomes inactive, we need to mark its inputs as usable again
for (const CTxIn& txin : wtx.tx->vin) {
MarkTXOUsable(txin.prevout);
}
}
// Cache the outputs that belong to the wallet
RefreshSingleTxTXOs(wtx);
// Notify UI of new or updated transaction // Notify UI of new or updated transaction
NotifyTransactionChanged(hash, fInsertedNew ? CT_NEW : CT_UPDATED); NotifyTransactionChanged(hash, fInsertedNew ? CT_NEW : CT_UPDATED);
@ -1226,6 +1257,8 @@ bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx
// Update birth time when tx time is older than it. // Update birth time when tx time is older than it.
MaybeUpdateBirthTime(wtx.GetTxTime()); MaybeUpdateBirthTime(wtx.GetTxTime());
// Make sure the tx outputs are known by the wallet
RefreshSingleTxTXOs(wtx);
return true; return true;
} }
@ -1333,7 +1366,7 @@ bool CWallet::AbandonTransaction(CWalletTx& tx)
assert(!wtx.InMempool()); assert(!wtx.InMempool());
// If already conflicted or abandoned, no need to set abandoned // If already conflicted or abandoned, no need to set abandoned
if (!wtx.isBlockConflicted() && !wtx.isAbandoned()) { if (!wtx.isBlockConflicted() && !wtx.isAbandoned()) {
wtx.m_state = TxStateInactive{/*abandoned=*/true}; wtx.SetState(TxStateInactive{/*abandoned=*/true});
return TxUpdate::NOTIFY_CHANGED; return TxUpdate::NOTIFY_CHANGED;
} }
return TxUpdate::UNCHANGED; return TxUpdate::UNCHANGED;
@ -1369,7 +1402,7 @@ void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, c
if (conflictconfirms < GetTxDepthInMainChain(wtx)) { if (conflictconfirms < GetTxDepthInMainChain(wtx)) {
// Block is 'more conflicted' than current confirm; update. // Block is 'more conflicted' than current confirm; update.
// Mark transaction as conflicted with this block. // Mark transaction as conflicted with this block.
wtx.m_state = TxStateBlockConflicted{hashBlock, conflicting_height}; wtx.SetState(TxStateBlockConflicted{hashBlock, conflicting_height});
return TxUpdate::CHANGED; return TxUpdate::CHANGED;
} }
return TxUpdate::UNCHANGED; return TxUpdate::UNCHANGED;
@ -1406,12 +1439,20 @@ void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash,
if (batch) batch->WriteTx(wtx); if (batch) batch->WriteTx(wtx);
// Iterate over all its outputs, and update those tx states as well (if applicable) // Iterate over all its outputs, and update those tx states as well (if applicable)
for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) { for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(COutPoint(Txid::FromUint256(now), i)); COutPoint outpoint{Txid::FromUint256(now), i};
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(outpoint);
for (TxSpends::const_iterator iter = range.first; iter != range.second; ++iter) { for (TxSpends::const_iterator iter = range.first; iter != range.second; ++iter) {
if (!done.count(iter->second)) { if (!done.count(iter->second)) {
todo.insert(iter->second); todo.insert(iter->second);
} }
} }
if (wtx.state<TxStateBlockConflicted>() || wtx.state<TxStateConfirmed>()) {
// If the state applied is conflicted or confirmed, the outputs are unusable
MarkTXOUnusable(outpoint);
} else {
// Otherwise make the outputs usable
MarkTXOUsable(outpoint);
}
} }
if (update_state == TxUpdate::NOTIFY_CHANGED) { if (update_state == TxUpdate::NOTIFY_CHANGED) {
@ -1421,6 +1462,21 @@ void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash,
// If a transaction changes its tx state, that usually changes the balance // If a transaction changes its tx state, that usually 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
MarkInputsDirty(wtx.tx); MarkInputsDirty(wtx.tx);
// Make the non-conflicted inputs usable again
for (unsigned int i = 0; i < wtx.tx->vin.size(); ++i) {
const CTxIn& txin = wtx.tx->vin.at(i);
auto unusable_txo_it = m_unusable_txos.find(txin.prevout);
if (unusable_txo_it == m_unusable_txos.end()) {
continue;
}
if (std::get_if<TxStateBlockConflicted>(&unusable_txo_it->second.GetState()) ||
std::get_if<TxStateConfirmed>(&unusable_txo_it->second.GetState())) {
continue;
}
MarkTXOUsable(txin.prevout);
}
} }
} }
} }
@ -1442,7 +1498,7 @@ void CWallet::transactionAddedToMempool(const CTransactionRef& tx) {
auto it = mapWallet.find(tx->GetHash()); auto it = mapWallet.find(tx->GetHash());
if (it != mapWallet.end()) { if (it != mapWallet.end()) {
RefreshMempoolStatus(it->second, chain()); RefreshMempoolStatus(*this, it->second, chain());
} }
const Txid& txid = tx->GetHash(); const Txid& txid = tx->GetHash();
@ -1464,7 +1520,7 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe
LOCK(cs_wallet); LOCK(cs_wallet);
auto it = mapWallet.find(tx->GetHash()); auto it = mapWallet.find(tx->GetHash());
if (it != mapWallet.end()) { if (it != mapWallet.end()) {
RefreshMempoolStatus(it->second, chain()); RefreshMempoolStatus(*this, it->second, chain());
} }
// Handle transactions that were removed from the mempool because they // Handle transactions that were removed from the mempool because they
// conflict with transactions in a newly connected block. // conflict with transactions in a newly connected block.
@ -1548,11 +1604,13 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
int disconnect_height = block.height; int disconnect_height = block.height;
for (size_t index = 0; index < block.data->vtx.size(); index++) { Assert(block.data);
const CTransactionRef& ptx = block.data->vtx[index]; // Iterate the block backwards so that we can undo the UTXO changes in the correct order
for (auto it = block.data->vtx.rbegin(); it != block.data->vtx.rend(); ++it) {
const CTransactionRef& ptx = *it;
// Coinbase transactions are not only inactive but also abandoned, // Coinbase transactions are not only inactive but also abandoned,
// meaning they should never be relayed standalone via the p2p protocol. // meaning they should never be relayed standalone via the p2p protocol.
SyncTransaction(ptx, TxStateInactive{/*abandoned=*/index == 0}); SyncTransaction(ptx, TxStateInactive{/*abandoned=*/ptx->IsCoinBase()});
for (const CTxIn& tx_in : ptx->vin) { for (const CTxIn& tx_in : ptx->vin) {
// No other wallet transactions conflicted with this transaction // No other wallet transactions conflicted with this transaction
@ -1569,7 +1627,7 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
auto try_updating_state = [&](CWalletTx& tx) { auto try_updating_state = [&](CWalletTx& tx) {
if (!tx.isBlockConflicted()) return TxUpdate::UNCHANGED; if (!tx.isBlockConflicted()) return TxUpdate::UNCHANGED;
if (tx.state<TxStateBlockConflicted>()->conflicting_block_height >= disconnect_height) { if (tx.state<TxStateBlockConflicted>()->conflicting_block_height >= disconnect_height) {
tx.m_state = TxStateInactive{}; tx.SetState(TxStateInactive{});
return TxUpdate::CHANGED; return TxUpdate::CHANGED;
} }
return TxUpdate::UNCHANGED; return TxUpdate::UNCHANGED;
@ -1600,16 +1658,10 @@ void CWallet::BlockUntilSyncedToCurrentChain() const {
// and a not-"is mine" (according to the filter) input. // and a not-"is mine" (according to the filter) input.
CAmount CWallet::GetDebit(const CTxIn &txin, const isminefilter& filter) const CAmount CWallet::GetDebit(const CTxIn &txin, const isminefilter& filter) const
{ {
{ LOCK(cs_wallet);
LOCK(cs_wallet); auto txo = GetTXO(txin.prevout);
const auto mi = mapWallet.find(txin.prevout.hash); if (txo && (txo->GetIsMine() & filter)) {
if (mi != mapWallet.end()) return txo->GetTxOut().nValue;
{
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;
}
} }
return 0; return 0;
} }
@ -2068,7 +2120,9 @@ bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string
// If transaction was previously in the mempool, it should be updated when // If transaction was previously in the mempool, it should be updated when
// TransactionRemovedFromMempool fires. // TransactionRemovedFromMempool fires.
bool ret = chain().broadcastTransaction(wtx.tx, m_default_max_tx_fee, relay, err_string); bool ret = chain().broadcastTransaction(wtx.tx, m_default_max_tx_fee, relay, err_string);
if (ret) wtx.m_state = TxStateInMempool{}; if (ret) {
wtx.SetState(TxStateInMempool{});
}
return ret; return ret;
} }
@ -2352,7 +2406,6 @@ void CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::ve
wtx.mapValue = std::move(mapValue); wtx.mapValue = std::move(mapValue);
wtx.vOrderForm = std::move(orderForm); wtx.vOrderForm = std::move(orderForm);
wtx.fTimeReceivedIsTxTime = true; wtx.fTimeReceivedIsTxTime = true;
wtx.fFromMe = true;
return true; return true;
}); });
@ -2447,6 +2500,9 @@ util::Result<void> CWallet::RemoveTxs(WalletBatch& batch, std::vector<uint256>&
wtxOrdered.erase(it->second.m_it_wtxOrdered); wtxOrdered.erase(it->second.m_it_wtxOrdered);
for (const auto& txin : it->second.tx->vin) for (const auto& txin : it->second.tx->vin)
mapTxSpends.erase(txin.prevout); 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); mapWallet.erase(it);
NotifyTransactionChanged(hash, CT_DELETED); NotifyTransactionChanged(hash, CT_DELETED);
} }
@ -3386,6 +3442,10 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
} }
walletInstance->m_attaching_chain = false; walletInstance->m_attaching_chain = false;
// Remove TXOs that have already been spent
// We do this here as we need to have an attached chain to figure out what has actually been spent.
walletInstance->PruneSpentTXOs();
return true; return true;
} }
@ -3470,13 +3530,13 @@ CKeyPool::CKeyPool(const CPubKey& vchPubKeyIn, bool internalIn)
m_pre_split = false; m_pre_split = false;
} }
int CWallet::GetTxDepthInMainChain(const CWalletTx& wtx) const int CWallet::GetTxStateDepthInMainChain(const TxState& state) const
{ {
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
if (auto* conf = wtx.state<TxStateConfirmed>()) { if (auto* conf = std::get_if<TxStateConfirmed>(&state)) {
assert(conf->confirmed_block_height >= 0); assert(conf->confirmed_block_height >= 0);
return GetLastBlockHeight() - conf->confirmed_block_height + 1; return GetLastBlockHeight() - conf->confirmed_block_height + 1;
} else if (auto* conf = wtx.state<TxStateBlockConflicted>()) { } else if (auto* conf = std::get_if<TxStateBlockConflicted>(&state)) {
assert(conf->conflicting_block_height >= 0); assert(conf->conflicting_block_height >= 0);
return -1 * (GetLastBlockHeight() - conf->conflicting_block_height + 1); return -1 * (GetLastBlockHeight() - conf->conflicting_block_height + 1);
} else { } else {
@ -3484,6 +3544,20 @@ int CWallet::GetTxDepthInMainChain(const CWalletTx& wtx) const
} }
} }
int CWallet::GetTxDepthInMainChain(const CWalletTx& wtx) const
{
AssertLockHeld(cs_wallet);
return GetTxStateDepthInMainChain(wtx.GetState());
}
int CWallet::GetTxStateBlocksToMaturity(const TxState& state) const
{
AssertLockHeld(cs_wallet);
int chain_depth = GetTxStateDepthInMainChain(state);
assert(chain_depth >= 0); // coinbase tx should not be conflicted
return std::max(0, (COINBASE_MATURITY+1) - chain_depth);
}
int CWallet::GetTxBlocksToMaturity(const CWalletTx& wtx) const int CWallet::GetTxBlocksToMaturity(const CWalletTx& wtx) const
{ {
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
@ -3491,9 +3565,7 @@ int CWallet::GetTxBlocksToMaturity(const CWalletTx& wtx) const
if (!wtx.IsCoinBase()) { if (!wtx.IsCoinBase()) {
return 0; return 0;
} }
int chain_depth = GetTxDepthInMainChain(wtx); return GetTxStateBlocksToMaturity(wtx.GetState());
assert(chain_depth >= 0); // coinbase tx should not be conflicted
return std::max(0, (COINBASE_MATURITY+1) - chain_depth);
} }
bool CWallet::IsTxImmatureCoinBase(const CWalletTx& wtx) const bool CWallet::IsTxImmatureCoinBase(const CWalletTx& wtx) const
@ -3504,6 +3576,24 @@ bool CWallet::IsTxImmatureCoinBase(const CWalletTx& wtx) const
return GetTxBlocksToMaturity(wtx) > 0; return GetTxBlocksToMaturity(wtx) > 0;
} }
int CWallet::GetTXOBlocksToMaturity(const WalletTXO& txo) const
{
AssertLockHeld(cs_wallet);
if (!txo.IsTxCoinBase()) {
return 0;
}
return GetTxStateBlocksToMaturity(txo.GetState());
}
bool CWallet::IsTXOInImmatureCoinBase(const WalletTXO& txo) const
{
AssertLockHeld(cs_wallet);
// note GetBlocksToMaturity is 0 for non-coinbase tx
return GetTXOBlocksToMaturity(txo) > 0;
}
bool CWallet::IsCrypted() const bool CWallet::IsCrypted() const
{ {
return HasEncryptionKeys(); return HasEncryptionKeys();
@ -4162,6 +4252,10 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
return util::Error{_("Error: Unable to read wallet's best block locator record")}; return util::Error{_("Error: Unable to read wallet's best block locator record")};
} }
// Clear m_txos and m_unusable_txos. These will be updated next to match the descriptors remaining in this wallet
m_txos.clear();
m_unusable_txos.clear();
// Check if the transactions in the wallet are still ours. Either they belong here, or they belong in the watchonly wallet. // 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. // We need to go through these in the tx insertion order so that lookups to spends works.
std::vector<uint256> txids_to_delete; std::vector<uint256> txids_to_delete;
@ -4188,6 +4282,9 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
} }
} }
for (const auto& [_pos, wtx] : wtxOrdered) { for (const auto& [_pos, wtx] : wtxOrdered) {
// First update the UTXOs
wtx->m_txos.clear();
RefreshSingleTxTXOs(*wtx);
// Check it is the watchonly wallet's // Check it is the watchonly wallet's
// solvable_wallet doesn't need to be checked because transactions for those scripts weren't being watched for // solvable_wallet doesn't need to be checked because transactions for those scripts weren't being watched for
bool is_mine = IsMine(*wtx->tx) || IsFromMe(*wtx->tx); bool is_mine = IsMine(*wtx->tx) || IsFromMe(*wtx->tx);
@ -4201,6 +4298,7 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
if (!new_tx) return false; if (!new_tx) return false;
ins_wtx.SetTx(to_copy_wtx.tx); ins_wtx.SetTx(to_copy_wtx.tx);
ins_wtx.CopyFrom(to_copy_wtx); ins_wtx.CopyFrom(to_copy_wtx);
data.watchonly_wallet->RefreshSingleTxTXOs(ins_wtx);
return true; return true;
})) { })) {
return util::Error{strprintf(_("Error: Could not add watchonly tx %s to watchonly wallet"), wtx->GetHash().GetHex())}; return util::Error{strprintf(_("Error: Could not add watchonly tx %s to watchonly wallet"), wtx->GetHash().GetHex())};
@ -4686,4 +4784,103 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const
} }
return std::nullopt; return std::nullopt;
} }
using TXOMap = std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>;
void CWallet::RefreshSingleTxTXOs(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);
COutPoint outpoint(wtx.GetHash(), i);
isminetype ismine = IsMine(txout);
if (ismine == ISMINE_NO) {
continue;
}
auto it = wtx.m_txos.find(i);
if (it != wtx.m_txos.end()) {
it->second->SetIsMine(ismine);
it->second->SetState(wtx.GetState());
} else {
TXOMap::iterator txo_it;
bool txos_inserted = false;
if (m_last_block_processed_height >= 0 && IsSpent(outpoint, /*min_depth=*/1)) {
std::tie(txo_it, txos_inserted) = m_unusable_txos.emplace(outpoint, WalletTXO{txout, ismine, wtx.GetState(), wtx.IsCoinBase(), wtx.m_from_me, wtx.GetTxTime()});
assert(txos_inserted);
} else {
std::tie(txo_it, txos_inserted) = m_txos.emplace(outpoint, WalletTXO{txout, ismine, wtx.GetState(), wtx.IsCoinBase(), wtx.m_from_me, wtx.GetTxTime()});
}
auto [_, wtx_inserted] = wtx.m_txos.emplace(i, &txo_it->second);
assert(wtx_inserted);
}
}
}
void CWallet::RefreshAllTXOs()
{
AssertLockHeld(cs_wallet);
for (const auto& [_, wtx] : mapWallet) {
RefreshSingleTxTXOs(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 it->second;
}
const auto& u_it = m_unusable_txos.find(outpoint);
if (u_it != m_unusable_txos.end()) {
return u_it->second;
}
return std::nullopt;
}
void CWallet::PruneSpentTXOs()
{
AssertLockHeld(cs_wallet);
auto it = m_txos.begin();
while (it != m_txos.end()) {
if (std::get_if<TxStateBlockConflicted>(&it->second.GetState()) || IsSpent(it->first, /*min_depth=*/1)) {
it = MarkTXOUnusable(it->first).first;
} else {
it++;
}
}
}
std::pair<TXOMap::iterator, TXOMap::iterator> CWallet::MarkTXOUnusable(const COutPoint& outpoint)
{
AssertLockHeld(cs_wallet);
auto txos_it = m_txos.find(outpoint);
auto unusable_txos_it = m_unusable_txos.end();
if (txos_it != m_txos.end()) {
auto next_txo_it = std::next(txos_it);
auto nh = m_txos.extract(txos_it);
txos_it = next_txo_it;
auto [position, inserted, _] = m_unusable_txos.insert(std::move(nh));
unusable_txos_it = position;
assert(inserted);
}
return {txos_it, unusable_txos_it};
}
std::pair<TXOMap::iterator, TXOMap::iterator> CWallet::MarkTXOUsable(const COutPoint& outpoint)
{
AssertLockHeld(cs_wallet);
auto txos_it = m_txos.end();
auto unusable_txos_it = m_unusable_txos.find(outpoint);
if (unusable_txos_it != m_unusable_txos.end()) {
auto next_unusable_txo_it = std::next(unusable_txos_it);
auto nh = m_unusable_txos.extract(unusable_txos_it);
unusable_txos_it = next_unusable_txo_it;
auto [position, inserted, _] = m_txos.insert(std::move(nh));
assert(inserted);
txos_it = position;
}
return {unusable_txos_it, txos_it};
}
} // namespace wallet } // namespace wallet

View file

@ -428,6 +428,13 @@ private:
//! Cache of descriptor ScriptPubKeys used for IsMine. Maps ScriptPubKey to set of spkms //! Cache of descriptor ScriptPubKeys used for IsMine. Maps ScriptPubKey to set of spkms
std::unordered_map<CScript, std::vector<ScriptPubKeyMan*>, SaltedSipHasher> m_cached_spks; std::unordered_map<CScript, std::vector<ScriptPubKeyMan*>, SaltedSipHasher> m_cached_spks;
//! Set of both spent and unspent transaction outputs owned by this wallet
using TXOMap = std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>;
TXOMap m_txos GUARDED_BY(cs_wallet);
//! Set of transaction outputs that are definitely no longer usuable
//! These outputs may already be spent in a confirmed tx, or are the outputs of a conflicted tx
TXOMap m_unusable_txos GUARDED_BY(cs_wallet);
/** /**
* Catch wallet up to current chain, scanning new blocks, updating the best * Catch wallet up to current chain, scanning new blocks, updating the best
* block locator and m_last_block_processed, and registering for * block locator and m_last_block_processed, and registering for
@ -507,6 +514,17 @@ public:
std::set<uint256> GetTxConflicts(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); std::set<uint256> GetTxConflicts(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
const TXOMap& 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);
/** Cache outputs that belong to the wallet from a single transaction */
void RefreshSingleTxTXOs(const CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/** Cache outputs that belong to the wallt for all tranasctions in the wallet */
void RefreshAllTXOs() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void PruneSpentTXOs() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
std::pair<TXOMap::iterator, TXOMap::iterator> MarkTXOUnusable(const COutPoint& outpoint) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
std::pair<TXOMap::iterator, TXOMap::iterator> MarkTXOUsable(const COutPoint& outpoint) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/** /**
* Return depth of transaction in blockchain: * Return depth of transaction in blockchain:
* <0 : conflicts with a transaction this deep in the blockchain * <0 : conflicts with a transaction this deep in the blockchain
@ -520,6 +538,7 @@ public:
* the height of the last block processed, or the heights of blocks * the height of the last block processed, or the heights of blocks
* referenced in transaction, and might cause assert failures. * referenced in transaction, and might cause assert failures.
*/ */
int GetTxStateDepthInMainChain(const TxState& state) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
int GetTxDepthInMainChain(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); int GetTxDepthInMainChain(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/** /**
@ -527,13 +546,16 @@ public:
* 0 : is not a coinbase transaction, or is a mature coinbase transaction * 0 : is not a coinbase transaction, or is a mature coinbase transaction
* >0 : is a coinbase transaction which matures in this many blocks * >0 : is a coinbase transaction which matures in this many blocks
*/ */
int GetTxStateBlocksToMaturity(const TxState& state) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
int GetTxBlocksToMaturity(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); int GetTxBlocksToMaturity(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
int GetTXOBlocksToMaturity(const WalletTXO& txo) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool IsTxImmatureCoinBase(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsTxImmatureCoinBase(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool IsTXOInImmatureCoinBase(const WalletTXO& txo) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
//! check whether we support the named feature //! check whether we support the named feature
bool CanSupportFeature(enum WalletFeature wf) const override EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return IsFeatureSupported(nWalletVersion, wf); } bool CanSupportFeature(enum WalletFeature wf) const override EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return IsFeatureSupported(nWalletVersion, wf); }
bool IsSpent(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsSpent(const COutPoint& outpoint, int min_depth = 0) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
// Whether this or any known scriptPubKey with the same single key has been spent. // Whether this or any known scriptPubKey with the same single key has been spent.
bool IsSpentKey(const CScript& scriptPubKey) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsSpentKey(const CScript& scriptPubKey) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

View file

@ -1073,6 +1073,10 @@ static DBErrors LoadTxRecords(CWallet* pwallet, DatabaseBatch& batch, std::vecto
if (wtx.GetHash() != hash) if (wtx.GetHash() != hash)
return false; return false;
if (wtx.m_from_me.empty()) {
upgraded_txs.push_back(hash);
}
// Undo serialize changes in 31600 // Undo serialize changes in 31600
if (31404 <= wtx.fTimeReceivedIsTxTime && wtx.fTimeReceivedIsTxTime <= 31703) if (31404 <= wtx.fTimeReceivedIsTxTime && wtx.fTimeReceivedIsTxTime <= 31703)
{ {
@ -1107,6 +1111,16 @@ static DBErrors LoadTxRecords(CWallet* pwallet, DatabaseBatch& batch, std::vecto
}); });
result = std::max(result, tx_res.m_result); result = std::max(result, tx_res.m_result);
// Upgrade each CWalletTx with new m_from_me data
for (auto txid : upgraded_txs) {
auto it = pwallet->mapWallet.find(txid);
Assert(it != pwallet->mapWallet.end());
CWalletTx& wtx = it->second;
for (auto filter : {ISMINE_SPENDABLE, ISMINE_WATCH_ONLY}) {
wtx.m_from_me[filter] = pwallet->GetDebit(*wtx.tx, filter) > 0;
}
}
// Load locked utxo record // Load locked utxo record
LoadResult locked_utxo_res = LoadRecords(pwallet, batch, DBKeys::LOCKED_UTXO, LoadResult locked_utxo_res = LoadRecords(pwallet, batch, DBKeys::LOCKED_UTXO,
[] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet) { [] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet) {
@ -1228,14 +1242,14 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
// Load address book // Load address book
result = std::max(LoadAddressBookRecords(pwallet, *m_batch), result); 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 // Load SPKMs
result = std::max(LoadActiveSPKMs(pwallet, *m_batch), result); result = std::max(LoadActiveSPKMs(pwallet, *m_batch), result);
// Load decryption keys // Load decryption keys
result = std::max(LoadDecryptionKeys(pwallet, *m_batch), result); result = std::max(LoadDecryptionKeys(pwallet, *m_batch), result);
// Load tx records
result = std::max(LoadTxRecords(pwallet, *m_batch, upgraded_txs, any_unordered), result);
} catch (...) { } catch (...) {
// Exceptions that can be ignored or treated as non-critical are handled by the individual loading functions. // 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. // Any uncaught exceptions will be caught here and treated as critical.

View file

@ -4,6 +4,7 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php. # file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the wallet balance RPC methods.""" """Test the wallet balance RPC methods."""
from decimal import Decimal from decimal import Decimal
import time
from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE as ADDRESS_WATCHONLY from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE as ADDRESS_WATCHONLY
from test_framework.blocktools import COINBASE_MATURITY from test_framework.blocktools import COINBASE_MATURITY
@ -12,7 +13,9 @@ from test_framework.util import (
assert_equal, assert_equal,
assert_is_hash_string, assert_is_hash_string,
assert_raises_rpc_error, assert_raises_rpc_error,
find_vout_for_address,
) )
from test_framework.wallet_util import get_generate_key
def create_transactions(node, address, amt, fees): def create_transactions(node, address, amt, fees):
@ -285,5 +288,60 @@ class WalletTest(BitcoinTestFramework):
assert_equal(tx_info['lastprocessedblock']['height'], prev_height) assert_equal(tx_info['lastprocessedblock']['height'], prev_height)
assert_equal(tx_info['lastprocessedblock']['hash'], prev_hash) 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)
wallet.unloadwallet()
self.log.info("Test that the balance is unchanged by an import that makes an already spent output in an existing tx \"mine\"")
self.nodes[0].createwallet("importalreadyspent")
wallet = self.nodes[0].get_wallet_rpc("importalreadyspent")
import_change_key = get_generate_key()
addr1 = wallet.getnewaddress()
addr2 = wallet.getnewaddress()
default.importprivkey(privkey=import_change_key.privkey, rescan=False)
res = default.send(outputs=[{addr1: amount}], options={"change_address": import_change_key.p2wpkh_addr})
inputs = [{"txid":res["txid"], "vout": find_vout_for_address(default, res["txid"], import_change_key.p2wpkh_addr)}]
default.send(outputs=[{addr2: amount}], options={"inputs": inputs, "add_inputs": True})
# Mock the time forward by another day so that "now" will exclude the block we just mined
self.nodes[0].setmocktime(int(time.time()) + 86400 * 2)
# 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 * 2)
# Don't rescan to make sure that the import updates the wallet txos
# The balance should not change because the output for this key is already spent
wallet.importprivkey(privkey=import_change_key.privkey, rescan=False)
balances = wallet.getbalances()
assert_equal(balances["mine"]["trusted"], amount * 2)
if __name__ == '__main__': if __name__ == '__main__':
WalletTest(__file__).main() WalletTest(__file__).main()