Skip to content

Commit a89fc9a

Browse files
committed
wallet: Move definintely unusable TXOs to a separate container
Definitely unusable TXOs are those that are spent by a confirmed transaction or were produced by a now conflicted transaction. However, we still need them for GetDebit, so we store them in a separate m_unusable_txos container. MarkConflicted, AbandonTransaction, and loading (via PruneSpentTXOs) will ensure that these unusable TXOs are properly moved.
1 parent 00aaef0 commit a89fc9a

File tree

5 files changed

+159
-18
lines changed

5 files changed

+159
-18
lines changed

src/wallet/receive.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@ Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse)
280280

281281
const bool is_trusted{CachedTxIsTrusted(wallet, txo.GetState(), outpoint.hash)};
282282
const int tx_depth{wallet.GetTxStateDepthInMainChain(txo.GetState())};
283+
Assert(tx_depth >= 0);
284+
Assert(!wallet.IsSpent(outpoint, /*min_depth=*/1));
283285

284286
if (!wallet.IsSpent(outpoint) && (allow_used_addresses || !wallet.IsSpentKey(txo.GetTxOut().scriptPubKey))) {
285287
// Get the amounts for mine and watchonly

src/wallet/spend.cpp

+4-5
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,10 @@ CoinsResult AvailableCoins(const CWallet& wallet,
336336
if (wallet.IsLockedCoin(outpoint) && params.skip_locked)
337337
continue;
338338

339+
int nDepth = wallet.GetTxStateDepthInMainChain(txo.GetState());
340+
Assert(nDepth >= 0);
341+
Assert(!wallet.IsSpent(outpoint, /*min_depth=*/1));
342+
339343
if (wallet.IsSpent(outpoint))
340344
continue;
341345

@@ -353,11 +357,6 @@ CoinsResult AvailableCoins(const CWallet& wallet,
353357

354358
assert(mine != ISMINE_NO);
355359

356-
int nDepth = wallet.GetTxStateDepthInMainChain(txo.GetState());
357-
if (nDepth < 0)
358-
continue;
359-
360-
// Perform tx level checks if we haven't already come across outputs from this tx before.
361360
if (!tx_safe_cache.contains(outpoint.hash)) {
362361
tx_safe_cache[outpoint.hash] = {false, false};
363362
const CWalletTx& wtx = *wallet.GetWalletTx(outpoint.hash);

src/wallet/wallet.cpp

+113-11
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,20 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
11711171
// Break debit/credit balance caches:
11721172
wtx.MarkDirty();
11731173

1174+
// Remove or add back the inputs from m_txos to match the state of this tx.
1175+
if (wtx.isConfirmed())
1176+
{
1177+
// When a transaction becomes confirmed, we can remove all of the txos that were spent
1178+
// in its inputs as they are no longer relevant.
1179+
for (const CTxIn& txin : wtx.tx->vin) {
1180+
MarkTXOUnusable(txin.prevout);
1181+
}
1182+
} else if (wtx.isInactive()) {
1183+
// When a transaction becomes inactive, we need to mark its inputs as usable again
1184+
for (const CTxIn& txin : wtx.tx->vin) {
1185+
MarkTXOUsable(txin.prevout);
1186+
}
1187+
}
11741188
// Cache the outputs that belong to the wallet
11751189
RefreshWalletTxTXOs(wtx);
11761190

@@ -1416,12 +1430,20 @@ void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash,
14161430
if (batch) batch->WriteTx(wtx);
14171431
// Iterate over all its outputs, and update those tx states as well (if applicable)
14181432
for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
1419-
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(COutPoint(Txid::FromUint256(now), i));
1433+
COutPoint outpoint{Txid::FromUint256(now), i};
1434+
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(outpoint);
14201435
for (TxSpends::const_iterator iter = range.first; iter != range.second; ++iter) {
14211436
if (!done.count(iter->second)) {
14221437
todo.insert(iter->second);
14231438
}
14241439
}
1440+
if (wtx.state<TxStateBlockConflicted>() || wtx.state<TxStateConfirmed>()) {
1441+
// If the state applied is conflicted or confirmed, the outputs are unusable
1442+
MarkTXOUnusable(outpoint);
1443+
} else {
1444+
// Otherwise make the outputs usable
1445+
MarkTXOUsable(outpoint);
1446+
}
14251447
}
14261448

14271449
if (update_state == TxUpdate::NOTIFY_CHANGED) {
@@ -1431,6 +1453,21 @@ void CWallet::RecursiveUpdateTxState(WalletBatch* batch, const uint256& tx_hash,
14311453
// If a transaction changes its tx state, that usually changes the balance
14321454
// available of the outputs it spends. So force those to be recomputed
14331455
MarkInputsDirty(wtx.tx);
1456+
// Make the non-conflicted inputs usable again
1457+
for (unsigned int i = 0; i < wtx.tx->vin.size(); ++i) {
1458+
const CTxIn& txin = wtx.tx->vin.at(i);
1459+
auto unusable_txo_it = m_unusable_txos.find(txin.prevout);
1460+
if (unusable_txo_it == m_unusable_txos.end()) {
1461+
continue;
1462+
}
1463+
1464+
if (std::get_if<TxStateBlockConflicted>(&unusable_txo_it->second.GetState()) ||
1465+
std::get_if<TxStateConfirmed>(&unusable_txo_it->second.GetState())) {
1466+
continue;
1467+
}
1468+
1469+
MarkTXOUsable(txin.prevout);
1470+
}
14341471
}
14351472
}
14361473
}
@@ -3369,6 +3406,10 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
33693406
}
33703407
walletInstance->m_attaching_chain = false;
33713408

3409+
// Remove TXOs that have already been spent
3410+
// We do this here as we need to have an attached chain to figure out what has actually been spent.
3411+
walletInstance->PruneSpentTXOs();
3412+
33723413
return true;
33733414
}
33743415

@@ -4152,9 +4193,9 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
41524193
return false;
41534194
}
41544195

4155-
// Update m_txos to match the descriptors remaining in this wallet
4196+
// Clear m_txos and m_unusable_txos. These will be updated next to match the descriptors remaining in this wallet
41564197
m_txos.clear();
4157-
RefreshAllTXOs();
4198+
m_unusable_txos.clear();
41584199

41594200
// Check if the transactions in the wallet are still ours. Either they belong here, or they belong in the watchonly wallet.
41604201
// We need to go through these in the tx insertion order so that lookups to spends works.
@@ -4180,6 +4221,9 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
41804221
}
41814222
}
41824223
for (const auto& [_pos, wtx] : wtxOrdered) {
4224+
// First update the UTXOs
4225+
wtx->m_txos.clear();
4226+
RefreshWalletTxTXOs(*wtx);
41834227
// Check it is the watchonly wallet's
41844228
// solvable_wallet doesn't need to be checked because transactions for those scripts weren't being watched for
41854229
bool is_mine = IsMine(*wtx->tx) || IsFromMe(*wtx->tx);
@@ -4193,6 +4237,7 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
41934237
if (!new_tx) return false;
41944238
ins_wtx.SetTx(to_copy_wtx.tx);
41954239
ins_wtx.CopyFrom(to_copy_wtx);
4240+
data.watchonly_wallet->RefreshWalletTxTXOs(ins_wtx);
41964241
return true;
41974242
})) {
41984243
error = strprintf(_("Error: Could not add watchonly tx %s to watchonly wallet"), wtx->GetHash().GetHex());
@@ -4667,26 +4712,34 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const
46674712
return std::nullopt;
46684713
}
46694714

4715+
using TXOMap = std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>;
46704716
void CWallet::RefreshWalletTxTXOs(const CWalletTx& wtx)
46714717
{
46724718
AssertLockHeld(cs_wallet);
46734719
for (uint32_t i = 0; i < wtx.tx->vout.size(); ++i) {
46744720
const CTxOut& txout = wtx.tx->vout.at(i);
46754721
COutPoint outpoint(wtx.GetHash(), i);
46764722

4677-
auto it = m_txos.find(outpoint);
4678-
46794723
isminetype ismine = IsMine(txout);
46804724
if (ismine == ISMINE_NO) {
46814725
continue;
46824726
}
46834727

4684-
if (it != m_txos.end()) {
4728+
auto it = wtx.m_txos.find(i);
4729+
if (it != wtx.m_txos.end()) {
46854730
it->second.SetIsMine(ismine);
46864731
it->second.SetState(wtx.GetState());
46874732
} else {
4688-
auto [txo_it, _] = m_txos.emplace(outpoint, WalletTXO{txout, ismine, wtx.GetState(), wtx.IsCoinBase(), wtx.m_from_me, wtx.GetTxTime()});
4689-
wtx.m_txos.emplace(i, txo_it->second);
4733+
TXOMap::iterator txo_it;
4734+
bool txos_inserted = false;
4735+
if (m_last_block_processed_height >= 0 && IsSpent(outpoint, /*min_depth=*/1)) {
4736+
std::tie(txo_it, txos_inserted) = m_unusable_txos.emplace(outpoint, WalletTXO{txout, ismine, wtx.GetState(), wtx.IsCoinBase(), wtx.m_from_me, wtx.GetTxTime()});
4737+
assert(txos_inserted);
4738+
} else {
4739+
std::tie(txo_it, txos_inserted) = m_txos.emplace(outpoint, WalletTXO{txout, ismine, wtx.GetState(), wtx.IsCoinBase(), wtx.m_from_me, wtx.GetTxTime()});
4740+
}
4741+
auto [_, wtx_inserted] = wtx.m_txos.emplace(i, txo_it->second);
4742+
assert(wtx_inserted);
46904743
}
46914744
}
46924745
}
@@ -4703,9 +4756,58 @@ std::optional<WalletTXO> CWallet::GetTXO(const COutPoint& outpoint) const
47034756
{
47044757
AssertLockHeld(cs_wallet);
47054758
const auto& it = m_txos.find(outpoint);
4706-
if (it == m_txos.end()) {
4707-
return std::nullopt;
4759+
if (it != m_txos.end()) {
4760+
return it->second;
47084761
}
4709-
return it->second;
4762+
const auto& u_it = m_unusable_txos.find(outpoint);
4763+
if (u_it != m_unusable_txos.end()) {
4764+
return u_it->second;
4765+
}
4766+
return std::nullopt;
4767+
}
4768+
4769+
void CWallet::PruneSpentTXOs()
4770+
{
4771+
AssertLockHeld(cs_wallet);
4772+
auto it = m_txos.begin();
4773+
while (it != m_txos.end()) {
4774+
if (std::get_if<TxStateBlockConflicted>(&it->second.GetState()) || IsSpent(it->first, /*min_depth=*/1)) {
4775+
it = MarkTXOUnusable(it->first).first;
4776+
} else {
4777+
it++;
4778+
}
4779+
}
4780+
}
4781+
4782+
std::pair<TXOMap::iterator, TXOMap::iterator> CWallet::MarkTXOUnusable(const COutPoint& outpoint)
4783+
{
4784+
AssertLockHeld(cs_wallet);
4785+
auto txos_it = m_txos.find(outpoint);
4786+
auto unusable_txos_it = m_unusable_txos.end();
4787+
if (txos_it != m_txos.end()) {
4788+
auto next_txo_it = std::next(txos_it);
4789+
auto nh = m_txos.extract(txos_it);
4790+
txos_it = next_txo_it;
4791+
auto [position, inserted, _] = m_unusable_txos.insert(std::move(nh));
4792+
unusable_txos_it = position;
4793+
assert(inserted);
4794+
}
4795+
return {txos_it, unusable_txos_it};
4796+
}
4797+
4798+
std::pair<TXOMap::iterator, TXOMap::iterator> CWallet::MarkTXOUsable(const COutPoint& outpoint)
4799+
{
4800+
AssertLockHeld(cs_wallet);
4801+
auto txos_it = m_txos.end();
4802+
auto unusable_txos_it = m_unusable_txos.find(outpoint);
4803+
if (unusable_txos_it != m_unusable_txos.end()) {
4804+
auto next_unusable_txo_it = std::next(unusable_txos_it);
4805+
auto nh = m_unusable_txos.extract(unusable_txos_it);
4806+
unusable_txos_it = next_unusable_txo_it;
4807+
auto [position, inserted, _] = m_txos.insert(std::move(nh));
4808+
assert(inserted);
4809+
txos_it = position;
4810+
}
4811+
return {unusable_txos_it, txos_it};
47104812
}
47114813
} // namespace wallet

src/wallet/wallet.h

+9-2
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,11 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
426426
std::unordered_map<CScript, std::vector<ScriptPubKeyMan*>, SaltedSipHasher> m_cached_spks;
427427

428428
//! Set of both spent and unspent transaction outputs owned by this wallet
429-
std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher> m_txos GUARDED_BY(cs_wallet);
429+
using TXOMap = std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>;
430+
TXOMap m_txos GUARDED_BY(cs_wallet);
431+
//! Set of transaction outputs that are definitely no longer usuable
432+
//! These outputs may already be spent in a confirmed tx, or are the outputs of a conflicted tx
433+
TXOMap m_unusable_txos GUARDED_BY(cs_wallet);
430434

431435
/**
432436
* Catch wallet up to current chain, scanning new blocks, updating the best
@@ -507,11 +511,14 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
507511

508512
std::set<uint256> GetTxConflicts(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
509513

510-
const std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>& GetTXOs() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return m_txos; };
514+
const TXOMap& GetTXOs() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return m_txos; };
511515
std::optional<WalletTXO> GetTXO(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
512516

513517
void RefreshWalletTxTXOs(const CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
514518
void RefreshAllTXOs() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
519+
void PruneSpentTXOs() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
520+
std::pair<TXOMap::iterator, TXOMap::iterator> MarkTXOUnusable(const COutPoint& outpoint) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
521+
std::pair<TXOMap::iterator, TXOMap::iterator> MarkTXOUsable(const COutPoint& outpoint) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
515522

516523
/**
517524
* Return depth of transaction in blockchain:

test/functional/wallet_balance.py

+31
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
assert_equal,
1414
assert_is_hash_string,
1515
assert_raises_rpc_error,
16+
find_vout_for_address,
1617
)
1718
from test_framework.wallet_util import get_generate_key
1819

@@ -398,5 +399,35 @@ def test_balances(*, fee_node_1=0):
398399
balances = wallet.getbalances()
399400
assert_equal(balances["mine"]["trusted"], amount * 2)
400401

402+
wallet.unloadwallet()
403+
404+
self.log.info("Test that the balance is unchanged by an import that makes an already spent output in an existing tx \"mine\"")
405+
self.nodes[0].createwallet("importalreadyspent")
406+
wallet = self.nodes[0].get_wallet_rpc("importalreadyspent")
407+
408+
import_change_key = get_generate_key()
409+
addr1 = wallet.getnewaddress()
410+
addr2 = wallet.getnewaddress()
411+
412+
default.importprivkey(privkey=import_change_key.privkey, rescan=False)
413+
414+
res = default.send(outputs=[{addr1: amount}], options={"change_address": import_change_key.p2wpkh_addr})
415+
inputs = [{"txid":res["txid"], "vout": find_vout_for_address(default, res["txid"], import_change_key.p2wpkh_addr)}]
416+
default.send(outputs=[{addr2: amount}], options={"inputs": inputs, "add_inputs": True})
417+
418+
# Mock the time forward by another day so that "now" will exclude the block we just mined
419+
self.nodes[0].setmocktime(int(time.time()) + 86400 * 2)
420+
# Mine 11 blocks to move the MTP past the block we just mined
421+
self.generate(self.nodes[0], 11, sync_fun=self.no_op)
422+
423+
balances = wallet.getbalances()
424+
assert_equal(balances["mine"]["trusted"], amount * 2)
425+
426+
# Don't rescan to make sure that the import updates the wallet txos
427+
# The balance should not change because the output for this key is already spent
428+
wallet.importprivkey(privkey=import_change_key.privkey, rescan=False)
429+
balances = wallet.getbalances()
430+
assert_equal(balances["mine"]["trusted"], amount * 2)
431+
401432
if __name__ == '__main__':
402433
WalletTest(__file__).main()

0 commit comments

Comments
 (0)