This commit is contained in:
Ava Chow 2024-04-29 04:31:43 +02:00 committed by GitHub
commit 2cf175aea6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 683 additions and 381 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,
@ -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)
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);
if (wtx.isConfirmed()) return true;
if (wtx.isBlockConflicted()) return false;
// using wtx's cached debit
if (!wallet.m_spend_zero_conf_change || !CachedTxIsFromMe(wallet, wtx, ISMINE_ALL)) return false;
// This wtx is already trusted
if (trusted_parents.contains(txid)) return true;
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.
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:
for (const CTxIn& txin : wtx.tx->vin)
for (const CTxIn& txin : wtx->tx->vin)
{
// Transactions not sent by us: not trusted
const CWalletTx* parent = wallet.GetWalletTx(txin.prevout.hash);
@ -283,6 +251,17 @@ bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uin
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)
{
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 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.GetState(), outpoint.hash)};
const int tx_depth{wallet.GetTxStateDepthInMainChain(txo.GetState())};
Assert(tx_depth >= 0);
Assert(!wallet.IsSpent(outpoint, /*min_depth=*/1));
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.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;
@ -325,32 +319,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.GetState(), outpoint.hash)) continue;
if (wallet.IsTXOInImmatureCoinBase(txo)) continue;
if (!CachedTxIsTrusted(wallet, wtx, trusted_parents))
continue;
int nDepth = wallet.GetTxStateDepthInMainChain(txo.GetState());
if (nDepth < (CheckIsFromMeMap(txo.GetTxFromMe(), 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;
@ -44,10 +40,13 @@ void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx,
std::list<COutputEntry>& listSent,
CAmount& nFee, const isminefilter& filter,
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);
bool CheckIsFromMeMap(const std::map<isminefilter, bool>& from_me_map, const isminefilter& filter);
struct Balance {
CAmount m_mine_trusted{0}; //!< Trusted, at depth=GetBalance.min_depth or more
CAmount m_mine_untrusted_pending{0}; //!< Untrusted, but in mempool (pending)

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

@ -330,7 +330,7 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM
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
if (!filter_label.has_value())
@ -779,10 +779,11 @@ RPCHelpMan gettransaction()
CAmount nCredit = CachedTxGetCredit(*pwallet, wtx, filter);
CAmount nDebit = CachedTxGetDebit(*pwallet, wtx, filter);
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));
if (CachedTxIsFromMe(*pwallet, wtx, filter))
if (from_me)
entry.pushKV("fee", ValueFromAmount(nFee));
WalletTxToJSON(*pwallet, wtx, entry);

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,145 @@ 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 CTxOut& output = txo.GetTxOut();
if (wallet.IsTxImmatureCoinBase(wtx) && !params.include_immature_coinbase)
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) {
if (tx_safe_cache.contains(outpoint.hash) && !tx_safe_cache.at(outpoint.hash).first) {
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;
}
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++) {
const CTxOut& output = wtx.tx->vout[i];
const COutPoint outpoint(Txid::FromUint256(txid), i);
isminetype mine = wallet.IsMine(output);
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;
// Skip manually selected coins (the caller can fetch them directly)
if (coinControl && coinControl->HasSelected() && coinControl->IsSelected(outpoint))
continue;
bool safeTx = CachedTxIsTrusted(wallet, wtx, trusted_parents);
if (wallet.IsLockedCoin(outpoint) && params.skip_locked)
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;
}
if (wallet.IsSpent(outpoint))
continue;
// 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;
}
isminetype mine = wallet.IsMine(output);
if (mine == ISMINE_NO) {
if (only_safe && !safeTx) {
continue;
}
if (!allow_used_addresses && wallet.IsSpentKey(output.scriptPubKey)) {
if (nDepth < min_depth || nDepth > max_depth) {
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);
// 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));
bool tx_from_me = CheckIsFromMeMap(txo.GetTxFromMe(), ISMINE_ALL);
// Filter by spendable outputs only
if (!spendable && params.only_spendable) continue;
std::unique_ptr<SigningProvider> provider = wallet.GetSolvingProvider(output.scriptPubKey);
// Obtain script type
std::vector<std::vector<uint8_t>> script_solutions;
TxoutType type = Solver(output.scriptPubKey, script_solutions);
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));
// 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;
}
// Filter by spendable outputs only
if (!spendable && params.only_spendable) continue;
result.Add(GetOutputType(type, is_from_p2sh),
COutput(outpoint, output, nDepth, input_bytes, spendable, solvable, safeTx, wtx.GetTxTime(), tx_from_me, feerate));
// Obtain script type
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.
if (params.min_sum_amount != MAX_MONEY) {
if (result.GetTotalAmount() >= params.min_sum_amount) {
return result;
}
}
result.Add(GetOutputType(type, is_from_p2sh),
COutput(outpoint, output, nDepth, input_bytes, spendable, solvable, tx_safe, txo.GetTxTime(), tx_from_me, feerate));
// Checks the maximum number of UTXO's.
if (params.max_count > 0 && result.Size() >= params.max_count) {
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) {
return result;
}
}
if (feerate.has_value()) {

View File

@ -7,6 +7,7 @@
#include <script/solver.h>
#include <validation.h>
#include <wallet/coincontrol.h>
#include <wallet/context.h>
#include <wallet/spend.h>
#include <wallet/test/util.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)
{
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
// 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)
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);
auto available_coins = AvailableCoins(*wallet);

View File

@ -17,18 +17,17 @@
#include <memory>
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());
{
LOCK2(wallet->cs_wallet, ::cs_main);
wallet->SetLastBlockProcessed(cchain.Height(), cchain.Tip()->GetBlockHash());
}
bilingual_str error;
std::vector<bilingual_str> warnings;
auto wallet = CWallet::Create(context, "", CreateMockableWalletDatabase(), WALLET_FLAG_DESCRIPTORS, error, warnings);
// Allow the fallback fee with it's default
wallet->m_allow_fallback_fee = true;
{
LOCK(wallet->cs_wallet);
wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
wallet->SetupDescriptorScriptPubKeyMans();
FlatSigningProvider provider;
std::string error;
std::unique_ptr<Descriptor> desc = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false);
@ -38,11 +37,13 @@ std::unique_ptr<CWallet> CreateSyncedWallet(interfaces::Chain& chain, CChain& cc
}
WalletRescanReserver reserver(*wallet);
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.last_scanned_block == cchain.Tip()->GetBlockHash());
assert(*result.last_scanned_height == cchain.Height());
int tip_height = context.chain->getHeight().value();
assert(*result.last_scanned_height == tip_height);
assert(result.last_scanned_block == context.chain->getBlockHash(tip_height));
assert(result.last_failed_block.IsNull());
return wallet;
}

View File

@ -38,7 +38,7 @@ static const DatabaseFormat DATABASE_FORMATS[] = {
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(std::unique_ptr<WalletDatabase> database, WalletContext& context, uint64_t create_flags);

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;
@ -379,7 +350,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
// reorg events. Without this, AddToWallet asserts false when the same
// transaction is confirmed in different blocks.
wtx.m_state = state;
wtx.SetState(state);
return true;
})->nTimeSmart;
}
@ -546,7 +517,10 @@ public:
ListCoinsTestingSetup()
{
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()
@ -570,17 +544,16 @@ public:
blocktx = CMutableTransaction(*wallet->mapWallet.at(tx->GetHash()).tx);
}
CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
m_node.validation_signals->SyncWithValidationInterfaceQueue();
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());
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;
}
std::unique_ptr<CWallet> wallet;
std::shared_ptr<CWallet> wallet;
};
BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup)
@ -643,9 +616,10 @@ BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup)
void TestCoinsResult(ListCoinsTest& context, OutputType out_type, CAmount amount,
std::map<OutputType, size_t>& expected_coins_sizes)
{
LOCK(context.wallet->cs_wallet);
util::Result<CTxDestination> dest = Assert(context.wallet->GetNewDestination(out_type, ""));
CWalletTx& wtx = context.AddTx(CRecipient{*dest, amount, /*fSubtractFeeFromAmount=*/true});
LOCK(context.wallet->cs_wallet);
CoinFilterParams filter;
filter.skip_locked = false;
CoinsResult available_coins = AvailableCoins(*context.wallet, nullptr, std::nullopt, filter);
@ -925,65 +899,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

@ -32,7 +32,7 @@ int64_t CWalletTx::GetTxTime() const
void CWalletTx::updateState(interfaces::Chain& chain)
{
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
// while the wallet was shutdown, change tx status to UNCONFIRMED
// 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
// while offline is covered by the rescan logic.
if (!chain.findBlock(hash, FoundBlock().inActiveChain(active).height(height)) || !active) {
state = TxStateInactive{};
SetState(TxStateInactive{});
}
};
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>()) {
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)
{
*this = _tx;
m_txos.clear();
}
void CWalletTx::SetState(const TxState& state)
{
m_state = state;
for (auto [_, txo] : m_txos) {
txo.SetState(state);
}
}
} // namespace wallet

View File

@ -19,6 +19,7 @@
#include <cstdint>
#include <map>
#include <utility>
#include <unordered_map>
#include <variant>
#include <vector>
@ -169,6 +170,8 @@ public:
}
};
class WalletTXO;
/**
* 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.
@ -216,16 +219,16 @@ public:
*/
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
* 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
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
@ -237,6 +240,8 @@ public:
mutable bool fChangeCached;
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)
{
Init();
@ -249,15 +254,18 @@ public:
fTimeReceivedIsTxTime = false;
nTimeReceived = 0;
nTimeSmart = 0;
fFromMe = false;
fChangeCached = false;
nChangeCached = 0;
nOrderPos = -1;
m_from_me.clear();
}
CTransactionRef tx;
private:
TxState m_state;
public:
// Set of mempool transactions that conflict
// directly with the transaction, or that conflict
// 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_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);
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>
@ -294,10 +302,14 @@ public:
std::vector<uint256> dummy_vector1; //!< Used to be vMerkleBranch
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;
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});
@ -322,8 +334,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;
}
@ -337,6 +347,8 @@ public:
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); }
void SetState(const TxState& state);
const TxState& GetState() const { return m_state; }
//! Update transaction state when attaching to a chain, filling in heights
//! of conflicted and confirmed blocks
@ -369,6 +381,41 @@ struct WalletTxOrderComparator {
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
#endif // BITCOIN_WALLET_TRANSACTION_H

View File

@ -128,12 +128,17 @@ static void UpdateWalletSetting(interfaces::Chain& chain,
* immediately knows the transaction's status: Whether it can be considered
* 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())) {
tx.m_state = TxStateInMempool();
state = TxStateInMempool();
} else if (tx.state<TxStateInMempool>()) {
tx.m_state = TxStateInactive();
state = TxStateInactive();
}
if (state) {
tx.SetState(*state);
}
}
@ -733,7 +738,6 @@ void CWallet::SyncMetaData(std::pair<TxSpends::iterator, TxSpends::iterator> ran
// fTimeReceivedIsTxTime not copied on purpose
// nTimeReceived not copied on purpose
copyTo->nTimeSmart = copyFrom->nTimeSmart;
copyTo->fFromMe = copyFrom->fFromMe;
// nOrderPos not copied on purpose
// cached members not copied on purpose
}
@ -743,7 +747,7 @@ void CWallet::SyncMetaData(std::pair<TxSpends::iterator, TxSpends::iterator> ran
* Outpoint is spent if any non-conflicted transaction
* 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;
range = mapTxSpends.equal_range(outpoint);
@ -753,8 +757,14 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const
const auto mit = mapWallet.find(wtxid);
if (mit != mapWallet.end()) {
const auto& wtx = mit->second;
if (!wtx.isAbandoned() && !wtx.isBlockConflicted() && !wtx.isMempoolConflicted())
return true; // Spent
int depth = GetTxDepthInMainChain(wtx);
if (depth == 0) {
if (min_depth == 0 && !wtx.isAbandoned() && !wtx.isMempoolConflicted()) {
return true;
}
} else if (depth >= min_depth) {
return true;
}
}
}
return false;
@ -984,7 +994,7 @@ bool CWallet::MarkReplaced(const uint256& originalHash, const uint256& newHash)
wtx.mapValue["replaced_by_txid"] = newHash.ToString();
// Refresh mempool status without waiting for transactionRemovedFromMempool or transactionAddedToMempool
RefreshMempoolStatus(wtx, chain());
RefreshMempoolStatus(*this, wtx, chain());
WalletBatch batch(GetDatabase());
@ -1083,16 +1093,20 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
// Update birth time when tx time is older than it.
MaybeUpdateBirthTime(wtx.GetTxTime());
for (auto filter : {ISMINE_SPENDABLE, ISMINE_WATCH_ONLY}) {
wtx.m_from_me[filter] = GetDebit(*wtx.tx, filter) > 0;
}
}
if (!fInsertedNew)
{
if (state.index() != wtx.m_state.index()) {
wtx.m_state = state;
if (state.index() != wtx.GetState().index()) {
wtx.SetState(state);
fUpdated = true;
} else {
assert(TxStateSerializedIndex(wtx.m_state) == TxStateSerializedIndex(state));
assert(TxStateSerializedBlockHash(wtx.m_state) == TxStateSerializedBlockHash(state));
assert(TxStateSerializedIndex(wtx.GetState()) == TxStateSerializedIndex(state));
assert(TxStateSerializedBlockHash(wtx.GetState()) == TxStateSerializedBlockHash(state));
}
// 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
@ -1114,7 +1128,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
while (!txs.empty()) {
CWalletTx* desc_tx = txs.back();
txs.pop_back();
desc_tx->m_state = inactive_state;
desc_tx->SetState(inactive_state);
// Break caches since we have changed the state
desc_tx->MarkDirty();
batch.WriteTx(*desc_tx);
@ -1143,6 +1157,23 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
// Break debit/credit balance caches:
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
RefreshWalletTxTXOs(wtx);
// Notify UI of new or updated transaction
NotifyTransactionChanged(hash, fInsertedNew ? CT_NEW : CT_UPDATED);
@ -1206,6 +1237,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;
}
@ -1310,7 +1343,7 @@ bool CWallet::AbandonTransaction(const uint256& hashTx)
assert(!wtx.InMempool());
// If already conflicted or abandoned, no need to set abandoned
if (!wtx.isBlockConflicted() && !wtx.isAbandoned()) {
wtx.m_state = TxStateInactive{/*abandoned=*/true};
wtx.SetState(TxStateInactive{/*abandoned=*/true});
return TxUpdate::NOTIFY_CHANGED;
}
return TxUpdate::UNCHANGED;
@ -1346,7 +1379,7 @@ void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, c
if (conflictconfirms < GetTxDepthInMainChain(wtx)) {
// Block is 'more conflicted' than current confirm; update.
// 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::UNCHANGED;
@ -1383,12 +1416,20 @@ void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash,
if (batch) batch->WriteTx(wtx);
// 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) {
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) {
if (!done.count(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) {
@ -1398,6 +1439,21 @@ void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash,
// If a transaction changes its tx state, that usually changes the balance
// available of the outputs it spends. So force those to be recomputed
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);
}
}
}
}
@ -1419,7 +1475,7 @@ void CWallet::transactionAddedToMempool(const CTransactionRef& tx) {
auto it = mapWallet.find(tx->GetHash());
if (it != mapWallet.end()) {
RefreshMempoolStatus(it->second, chain());
RefreshMempoolStatus(*this, it->second, chain());
}
const Txid& txid = tx->GetHash();
@ -1441,7 +1497,7 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe
LOCK(cs_wallet);
auto it = mapWallet.find(tx->GetHash());
if (it != mapWallet.end()) {
RefreshMempoolStatus(it->second, chain());
RefreshMempoolStatus(*this, it->second, chain());
}
// Handle transactions that were removed from the mempool because they
// conflict with transactions in a newly connected block.
@ -1525,7 +1581,10 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
int disconnect_height = block.height;
for (const CTransactionRef& ptx : Assert(block.data)->vtx) {
Assert(block.data);
// 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;
SyncTransaction(ptx, TxStateInactive{});
for (const CTxIn& tx_in : ptx->vin) {
@ -1543,7 +1602,7 @@ void CWallet::blockDisconnected(const interfaces::BlockInfo& block)
auto try_updating_state = [&](CWalletTx& tx) {
if (!tx.isBlockConflicted()) return TxUpdate::UNCHANGED;
if (tx.state<TxStateBlockConflicted>()->conflicting_block_height >= disconnect_height) {
tx.m_state = TxStateInactive{};
tx.SetState(TxStateInactive{});
return TxUpdate::CHANGED;
}
return TxUpdate::UNCHANGED;
@ -1574,16 +1633,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;
}
@ -2026,7 +2079,6 @@ bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx, std::string& err_string
// If transaction was previously in the mempool, it should be updated when
// TransactionRemovedFromMempool fires.
bool ret = chain().broadcastTransaction(wtx.tx, m_default_max_tx_fee, relay, err_string);
if (ret) wtx.m_state = TxStateInMempool{};
return ret;
}
@ -2310,7 +2362,6 @@ void CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::ve
wtx.mapValue = std::move(mapValue);
wtx.vOrderForm = std::move(orderForm);
wtx.fTimeReceivedIsTxTime = true;
wtx.fFromMe = true;
return true;
});
@ -2401,6 +2452,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);
}
@ -3327,6 +3381,10 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
}
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;
}
@ -3403,13 +3461,13 @@ CKeyPool::CKeyPool(const CPubKey& vchPubKeyIn, bool internalIn)
m_pre_split = false;
}
int CWallet::GetTxDepthInMainChain(const CWalletTx& wtx) const
int CWallet::GetTxStateDepthInMainChain(const TxState& state) const
{
AssertLockHeld(cs_wallet);
if (auto* conf = wtx.state<TxStateConfirmed>()) {
if (auto* conf = std::get_if<TxStateConfirmed>(&state)) {
assert(conf->confirmed_block_height >= 0);
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);
return -1 * (GetLastBlockHeight() - conf->conflicting_block_height + 1);
} else {
@ -3417,6 +3475,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
{
AssertLockHeld(cs_wallet);
@ -3424,9 +3496,7 @@ int CWallet::GetTxBlocksToMaturity(const CWalletTx& wtx) const
if (!wtx.IsCoinBase()) {
return 0;
}
int chain_depth = GetTxDepthInMainChain(wtx);
assert(chain_depth >= 0); // coinbase tx should not be conflicted
return std::max(0, (COINBASE_MATURITY+1) - chain_depth);
return GetTxStateBlocksToMaturity(wtx.GetState());
}
bool CWallet::IsTxImmatureCoinBase(const CWalletTx& wtx) const
@ -3437,6 +3507,24 @@ bool CWallet::IsTxImmatureCoinBase(const CWalletTx& wtx) const
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
{
return HasEncryptionKeys();
@ -4062,6 +4150,10 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
return false;
}
// 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.
// We need to go through these in the tx insertion order so that lookups to spends works.
std::vector<uint256> txids_to_delete;
@ -4086,6 +4178,9 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
}
}
for (const auto& [_pos, wtx] : wtxOrdered) {
// First update the UTXOs
wtx->m_txos.clear();
RefreshWalletTxTXOs(*wtx);
// 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
bool is_mine = IsMine(*wtx->tx) || IsFromMe(*wtx->tx);
@ -4099,6 +4194,7 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
if (!new_tx) return false;
ins_wtx.SetTx(to_copy_wtx.tx);
ins_wtx.CopyFrom(to_copy_wtx);
data.watchonly_wallet->RefreshWalletTxTXOs(ins_wtx);
return true;
})) {
error = strprintf(_("Error: Could not add watchonly tx %s to watchonly wallet"), wtx->GetHash().GetHex());
@ -4556,4 +4652,103 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const
}
return std::nullopt;
}
using TXOMap = std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>;
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);
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) {
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 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

View File

@ -426,6 +426,13 @@ 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
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
* block locator and m_last_block_processed, and registering for
@ -505,6 +512,15 @@ public:
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);
void RefreshWalletTxTXOs(const CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_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:
* <0 : conflicts with a transaction this deep in the blockchain
@ -518,6 +534,7 @@ public:
* the height of the last block processed, or the heights of blocks
* 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);
/**
@ -525,13 +542,16 @@ public:
* 0 : is not a coinbase transaction, or is a mature coinbase transaction
* >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 GetTXOBlocksToMaturity(const WalletTXO& txo) 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
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.
bool IsSpentKey(const CScript& scriptPubKey) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

View File

@ -1040,6 +1040,10 @@ static DBErrors LoadTxRecords(CWallet* pwallet, DatabaseBatch& batch, std::vecto
if (wtx.GetHash() != hash)
return false;
if (wtx.m_from_me.empty()) {
upgraded_txs.push_back(hash);
}
// Undo serialize changes in 31600
if (31404 <= wtx.fTimeReceivedIsTxTime && wtx.fTimeReceivedIsTxTime <= 31703)
{
@ -1074,6 +1078,16 @@ static DBErrors LoadTxRecords(CWallet* pwallet, DatabaseBatch& batch, std::vecto
});
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
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) {
@ -1187,14 +1201,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
@ -13,7 +14,9 @@ from test_framework.util import (
assert_equal,
assert_is_hash_string,
assert_raises_rpc_error,
find_vout_for_address,
)
from test_framework.wallet_util import get_generate_key
def create_transactions(node, address, amt, fees):
@ -312,7 +315,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 +375,60 @@ 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)
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__':
WalletTest().main()