mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-13 11:35:20 +01:00
Merge 09fb29b82c
into a50af6e4c4
This commit is contained in:
commit
f96a3a3e61
18 changed files with 689 additions and 382 deletions
|
@ -71,10 +71,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;
|
||||
|
@ -129,14 +136,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,
|
||||
|
@ -189,7 +196,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);
|
||||
|
|
|
@ -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);
|
||||
|
@ -277,12 +245,23 @@ bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set<uin
|
|||
// If we've already trusted this parent, continue
|
||||
if (trusted_parents.count(parent->GetHash())) continue;
|
||||
// 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());
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -312,6 +312,7 @@ RPCHelpMan addmultisigaddress()
|
|||
|
||||
// Store destination in the addressbook
|
||||
pwallet->SetAddressBook(dest, label, AddressPurpose::SEND);
|
||||
pwallet->RefreshAllTXOs();
|
||||
|
||||
// Make the descriptor
|
||||
std::unique_ptr<Descriptor> descriptor = InferDescriptor(GetScriptForDestination(dest), spk_man);
|
||||
|
@ -371,6 +372,7 @@ RPCHelpMan keypoolrefill()
|
|||
if (pwallet->GetKeyPoolSize() < kpSize) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error refreshing keypool.");
|
||||
}
|
||||
pwallet->RefreshAllTXOs();
|
||||
|
||||
return UniValue::VNULL;
|
||||
},
|
||||
|
@ -402,6 +404,7 @@ RPCHelpMan newkeypool()
|
|||
|
||||
LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true);
|
||||
spk_man.NewKeyPool();
|
||||
pwallet->RefreshAllTXOs();
|
||||
|
||||
return UniValue::VNULL;
|
||||
},
|
||||
|
|
|
@ -206,6 +206,7 @@ RPCHelpMan importprivkey()
|
|||
pwallet->ImportScripts({GetScriptForDestination(WitnessV0KeyHash(vchAddress))}, /*timestamp=*/0);
|
||||
}
|
||||
}
|
||||
pwallet->RefreshAllTXOs();
|
||||
}
|
||||
if (fRescan) {
|
||||
RescanWallet(*pwallet, reserver);
|
||||
|
@ -306,6 +307,7 @@ RPCHelpMan importaddress()
|
|||
} else {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Bitcoin address or script");
|
||||
}
|
||||
pwallet->RefreshAllTXOs();
|
||||
}
|
||||
if (fRescan)
|
||||
{
|
||||
|
@ -472,6 +474,8 @@ RPCHelpMan importpubkey()
|
|||
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->RefreshAllTXOs();
|
||||
}
|
||||
if (fRescan)
|
||||
{
|
||||
|
@ -619,6 +623,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
|
||||
|
@ -1405,6 +1410,8 @@ RPCHelpMan importmulti()
|
|||
nLowestTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
pwallet->RefreshAllTXOs();
|
||||
}
|
||||
if (fRescan && fRunScan && requests.size()) {
|
||||
int64_t scannedTime = pwallet->RescanFromTime(nLowestTimestamp, reserver, /*update=*/true);
|
||||
|
@ -1718,6 +1725,7 @@ RPCHelpMan importdescriptors()
|
|||
}
|
||||
}
|
||||
pwallet->ConnectScriptPubKeyManNotifiers();
|
||||
pwallet->RefreshAllTXOs();
|
||||
}
|
||||
|
||||
// Rescan the blockchain using the lowest timestamp
|
||||
|
|
|
@ -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);
|
||||
|
||||
bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY);
|
||||
bool involvesWatchonly = CheckIsFromMeMap(wtx.m_from_me, ISMINE_WATCH_ONLY);
|
||||
|
||||
// Sent
|
||||
if (!filter_label.has_value())
|
||||
|
@ -780,10 +780,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);
|
||||
|
|
|
@ -577,6 +577,7 @@ static RPCHelpMan sethdseed()
|
|||
|
||||
spk_man.SetHDSeed(master_pub_key);
|
||||
if (flush_key_pool) spk_man.NewKeyPool();
|
||||
pwallet->RefreshAllTXOs();
|
||||
|
||||
return UniValue::VNULL;
|
||||
},
|
||||
|
|
|
@ -276,12 +276,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);
|
||||
}
|
||||
|
@ -329,137 +325,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()) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
auto descs = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false);
|
||||
|
@ -39,11 +38,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;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,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);
|
||||
|
|
|
@ -363,35 +363,6 @@ BOOST_FIXTURE_TEST_CASE(write_wallet_settings_concurrently, TestingSetup)
|
|||
/*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)
|
||||
{
|
||||
CMutableTransaction tx;
|
||||
|
@ -413,7 +384,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;
|
||||
}
|
||||
|
@ -582,7 +553,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()
|
||||
|
@ -606,17 +580,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)
|
||||
|
@ -679,9 +652,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);
|
||||
|
@ -961,65 +935,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(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()
|
||||
} // namespace wallet
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -137,12 +137,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -754,7 +759,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
|
||||
}
|
||||
|
@ -764,7 +768,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);
|
||||
|
@ -774,8 +778,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;
|
||||
|
@ -1005,7 +1015,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());
|
||||
|
||||
|
@ -1103,16 +1113,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
|
||||
|
@ -1134,7 +1148,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);
|
||||
|
@ -1163,6 +1177,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
|
||||
RefreshSingleTxTXOs(wtx);
|
||||
|
||||
// Notify UI of new or updated transaction
|
||||
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.
|
||||
MaybeUpdateBirthTime(wtx.GetTxTime());
|
||||
|
||||
// Make sure the tx outputs are known by the wallet
|
||||
RefreshSingleTxTXOs(wtx);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1333,7 +1366,7 @@ bool CWallet::AbandonTransaction(CWalletTx& tx)
|
|||
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;
|
||||
|
@ -1369,7 +1402,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;
|
||||
|
@ -1406,12 +1439,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) {
|
||||
|
@ -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
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1442,7 +1498,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();
|
||||
|
@ -1464,7 +1520,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.
|
||||
|
@ -1548,7 +1604,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) {
|
||||
|
@ -1566,7 +1625,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;
|
||||
|
@ -1597,16 +1656,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;
|
||||
}
|
||||
|
@ -2065,7 +2118,9 @@ 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{};
|
||||
if (ret) {
|
||||
wtx.SetState(TxStateInMempool{});
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -2349,7 +2404,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;
|
||||
});
|
||||
|
||||
|
@ -2444,6 +2498,9 @@ util::Result<void> CWallet::RemoveTxs(WalletBatch& batch, std::vector<uint256>&
|
|||
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);
|
||||
}
|
||||
|
@ -3380,6 +3437,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;
|
||||
}
|
||||
|
||||
|
@ -3464,13 +3525,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 {
|
||||
|
@ -3478,6 +3539,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);
|
||||
|
@ -3485,9 +3560,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
|
||||
|
@ -3498,6 +3571,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();
|
||||
|
@ -4152,6 +4243,10 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
|
|||
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.
|
||||
// We need to go through these in the tx insertion order so that lookups to spends works.
|
||||
std::vector<uint256> txids_to_delete;
|
||||
|
@ -4178,6 +4273,9 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
|
|||
}
|
||||
}
|
||||
for (const auto& [_pos, wtx] : wtxOrdered) {
|
||||
// First update the UTXOs
|
||||
wtx->m_txos.clear();
|
||||
RefreshSingleTxTXOs(*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);
|
||||
|
@ -4191,6 +4289,7 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
|
|||
if (!new_tx) return false;
|
||||
ins_wtx.SetTx(to_copy_wtx.tx);
|
||||
ins_wtx.CopyFrom(to_copy_wtx);
|
||||
data.watchonly_wallet->RefreshSingleTxTXOs(ins_wtx);
|
||||
return true;
|
||||
})) {
|
||||
return util::Error{strprintf(_("Error: Could not add watchonly tx %s to watchonly wallet"), wtx->GetHash().GetHex())};
|
||||
|
@ -4672,4 +4771,103 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const
|
|||
}
|
||||
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
|
||||
|
|
|
@ -428,6 +428,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
|
||||
|
@ -507,6 +514,17 @@ 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);
|
||||
|
||||
/** 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:
|
||||
* <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
|
||||
* 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);
|
||||
|
||||
/**
|
||||
|
@ -527,13 +546,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);
|
||||
|
|
|
@ -1055,6 +1055,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)
|
||||
{
|
||||
|
@ -1089,6 +1093,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) {
|
||||
|
@ -1210,14 +1224,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.
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test the wallet balance RPC methods."""
|
||||
from decimal import Decimal
|
||||
import time
|
||||
|
||||
from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE as ADDRESS_WATCHONLY
|
||||
from test_framework.blocktools import COINBASE_MATURITY
|
||||
|
@ -12,7 +13,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):
|
||||
|
@ -311,7 +314,40 @@ class WalletTest(BitcoinTestFramework):
|
|||
self.nodes[0].createwallet('w2', False, True)
|
||||
self.nodes[0].importprivkey(privkey)
|
||||
assert_equal(self.nodes[0].getbalances()['mine']['untrusted_pending'], Decimal('0.1'))
|
||||
self.nodes[0].unloadwallet("w2")
|
||||
|
||||
self.log.info("Test that an import that makes something spendable updates \"mine\" balance")
|
||||
self.nodes[0].loadwallet(self.default_wallet_name)
|
||||
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
|
||||
self.nodes[0].createwallet(wallet_name="legacyspendableupdate", descriptors=False)
|
||||
wallet = self.nodes[0].get_wallet_rpc("legacyspendableupdate")
|
||||
|
||||
import_key1 = get_generate_key()
|
||||
import_key2 = get_generate_key()
|
||||
wallet.importaddress(import_key1.p2wpkh_addr)
|
||||
wallet.importaddress(import_key2.p2wpkh_addr)
|
||||
|
||||
amount = Decimal(15)
|
||||
default.sendtoaddress(import_key1.p2wpkh_addr, amount)
|
||||
default.sendtoaddress(import_key2.p2wpkh_addr, amount)
|
||||
self.generate(self.nodes[0], 1)
|
||||
|
||||
balances = wallet.getbalances()
|
||||
assert_equal(balances["mine"]["trusted"], 0)
|
||||
assert_equal(balances["watchonly"]["trusted"], amount * 2)
|
||||
|
||||
# Rescanning should always update the txos by virtue of finding them again
|
||||
wallet.importprivkey(privkey=import_key1.privkey, rescan=True)
|
||||
balances = wallet.getbalances()
|
||||
assert_equal(balances["mine"]["trusted"], amount)
|
||||
assert_equal(balances["watchonly"]["trusted"], amount)
|
||||
|
||||
# Don't rescan to make sure that the import updates the wallet txos
|
||||
wallet.importprivkey(privkey=import_key2.privkey, rescan=False)
|
||||
balances = wallet.getbalances()
|
||||
assert_equal(balances["mine"]["trusted"], amount * 2)
|
||||
assert_equal(balances["watchonly"]["trusted"], 0)
|
||||
self.nodes[0].unloadwallet("legacyspendableupdate")
|
||||
|
||||
# Tests the lastprocessedblock JSON object in getbalances, getwalletinfo
|
||||
# and gettransaction by checking for valid hex strings and by comparing
|
||||
|
@ -338,5 +374,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(__file__).main()
|
||||
|
|
Loading…
Add table
Reference in a new issue