Skip to content

Commit 09fb29b

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 27b432f commit 09fb29b

File tree

5 files changed

+161
-20
lines changed

5 files changed

+161
-20
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

+115-13
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
RefreshSingleTxTXOs(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
}
@@ -3383,6 +3420,10 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
33833420
}
33843421
walletInstance->m_attaching_chain = false;
33853422

3423+
// Remove TXOs that have already been spent
3424+
// We do this here as we need to have an attached chain to figure out what has actually been spent.
3425+
walletInstance->PruneSpentTXOs();
3426+
33863427
return true;
33873428
}
33883429

@@ -4173,9 +4214,9 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
41734214
return util::Error{_("Error: Unable to read wallet's best block locator record")};
41744215
}
41754216

4176-
// Update m_txos to match the descriptors remaining in this wallet
4217+
// Clear m_txos and m_unusable_txos. These will be updated next to match the descriptors remaining in this wallet
41774218
m_txos.clear();
4178-
RefreshAllTXOs();
4219+
m_unusable_txos.clear();
41794220

41804221
// Check if the transactions in the wallet are still ours. Either they belong here, or they belong in the watchonly wallet.
41814222
// We need to go through these in the tx insertion order so that lookups to spends works.
@@ -4203,6 +4244,9 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
42034244
}
42044245
}
42054246
for (const auto& [_pos, wtx] : wtxOrdered) {
4247+
// First update the UTXOs
4248+
wtx->m_txos.clear();
4249+
RefreshSingleTxTXOs(*wtx);
42064250
// Check it is the watchonly wallet's
42074251
// solvable_wallet doesn't need to be checked because transactions for those scripts weren't being watched for
42084252
bool is_mine = IsMine(*wtx->tx) || IsFromMe(*wtx->tx);
@@ -4216,6 +4260,7 @@ util::Result<void> CWallet::ApplyMigrationData(WalletBatch& local_wallet_batch,
42164260
if (!new_tx) return false;
42174261
ins_wtx.SetTx(to_copy_wtx.tx);
42184262
ins_wtx.CopyFrom(to_copy_wtx);
4263+
data.watchonly_wallet->RefreshSingleTxTXOs(ins_wtx);
42194264
return true;
42204265
})) {
42214266
return util::Error{strprintf(_("Error: Could not add watchonly tx %s to watchonly wallet"), wtx->GetHash().GetHex())};
@@ -4676,26 +4721,34 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const
46764721
return std::nullopt;
46774722
}
46784723

4724+
using TXOMap = std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>;
46794725
void CWallet::RefreshSingleTxTXOs(const CWalletTx& wtx)
46804726
{
46814727
AssertLockHeld(cs_wallet);
46824728
for (uint32_t i = 0; i < wtx.tx->vout.size(); ++i) {
46834729
const CTxOut& txout = wtx.tx->vout.at(i);
46844730
COutPoint outpoint(wtx.GetHash(), i);
46854731

4686-
auto it = m_txos.find(outpoint);
4687-
46884732
isminetype ismine = IsMine(txout);
46894733
if (ismine == ISMINE_NO) {
46904734
continue;
46914735
}
46924736

4693-
if (it != m_txos.end()) {
4694-
it->second.SetIsMine(ismine);
4695-
it->second.SetState(wtx.GetState());
4737+
auto it = wtx.m_txos.find(i);
4738+
if (it != wtx.m_txos.end()) {
4739+
it->second->SetIsMine(ismine);
4740+
it->second->SetState(wtx.GetState());
46964741
} else {
4697-
auto [txo_it, _] = m_txos.emplace(outpoint, WalletTXO{txout, ismine, wtx.GetState(), wtx.IsCoinBase(), wtx.m_from_me, wtx.GetTxTime()});
4698-
wtx.m_txos.emplace(i, &txo_it->second);
4742+
TXOMap::iterator txo_it;
4743+
bool txos_inserted = false;
4744+
if (m_last_block_processed_height >= 0 && IsSpent(outpoint, /*min_depth=*/1)) {
4745+
std::tie(txo_it, txos_inserted) = m_unusable_txos.emplace(outpoint, WalletTXO{txout, ismine, wtx.GetState(), wtx.IsCoinBase(), wtx.m_from_me, wtx.GetTxTime()});
4746+
assert(txos_inserted);
4747+
} else {
4748+
std::tie(txo_it, txos_inserted) = m_txos.emplace(outpoint, WalletTXO{txout, ismine, wtx.GetState(), wtx.IsCoinBase(), wtx.m_from_me, wtx.GetTxTime()});
4749+
}
4750+
auto [_, wtx_inserted] = wtx.m_txos.emplace(i, &txo_it->second);
4751+
assert(wtx_inserted);
46994752
}
47004753
}
47014754
}
@@ -4712,9 +4765,58 @@ std::optional<WalletTXO> CWallet::GetTXO(const COutPoint& outpoint) const
47124765
{
47134766
AssertLockHeld(cs_wallet);
47144767
const auto& it = m_txos.find(outpoint);
4715-
if (it == m_txos.end()) {
4716-
return std::nullopt;
4768+
if (it != m_txos.end()) {
4769+
return it->second;
47174770
}
4718-
return it->second;
4771+
const auto& u_it = m_unusable_txos.find(outpoint);
4772+
if (u_it != m_unusable_txos.end()) {
4773+
return u_it->second;
4774+
}
4775+
return std::nullopt;
4776+
}
4777+
4778+
void CWallet::PruneSpentTXOs()
4779+
{
4780+
AssertLockHeld(cs_wallet);
4781+
auto it = m_txos.begin();
4782+
while (it != m_txos.end()) {
4783+
if (std::get_if<TxStateBlockConflicted>(&it->second.GetState()) || IsSpent(it->first, /*min_depth=*/1)) {
4784+
it = MarkTXOUnusable(it->first).first;
4785+
} else {
4786+
it++;
4787+
}
4788+
}
4789+
}
4790+
4791+
std::pair<TXOMap::iterator, TXOMap::iterator> CWallet::MarkTXOUnusable(const COutPoint& outpoint)
4792+
{
4793+
AssertLockHeld(cs_wallet);
4794+
auto txos_it = m_txos.find(outpoint);
4795+
auto unusable_txos_it = m_unusable_txos.end();
4796+
if (txos_it != m_txos.end()) {
4797+
auto next_txo_it = std::next(txos_it);
4798+
auto nh = m_txos.extract(txos_it);
4799+
txos_it = next_txo_it;
4800+
auto [position, inserted, _] = m_unusable_txos.insert(std::move(nh));
4801+
unusable_txos_it = position;
4802+
assert(inserted);
4803+
}
4804+
return {txos_it, unusable_txos_it};
4805+
}
4806+
4807+
std::pair<TXOMap::iterator, TXOMap::iterator> CWallet::MarkTXOUsable(const COutPoint& outpoint)
4808+
{
4809+
AssertLockHeld(cs_wallet);
4810+
auto txos_it = m_txos.end();
4811+
auto unusable_txos_it = m_unusable_txos.find(outpoint);
4812+
if (unusable_txos_it != m_unusable_txos.end()) {
4813+
auto next_unusable_txo_it = std::next(unusable_txos_it);
4814+
auto nh = m_unusable_txos.extract(unusable_txos_it);
4815+
unusable_txos_it = next_unusable_txo_it;
4816+
auto [position, inserted, _] = m_txos.insert(std::move(nh));
4817+
assert(inserted);
4818+
txos_it = position;
4819+
}
4820+
return {unusable_txos_it, txos_it};
47194821
}
47204822
} // namespace wallet

src/wallet/wallet.h

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

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

434438
/**
435439
* Catch wallet up to current chain, scanning new blocks, updating the best
@@ -510,13 +514,16 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
510514

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

513-
const std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>& GetTXOs() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return m_txos; };
517+
const TXOMap& GetTXOs() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return m_txos; };
514518
std::optional<WalletTXO> GetTXO(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
515519

516520
/** Cache outputs that belong to the wallet from a single transaction */
517521
void RefreshSingleTxTXOs(const CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
518522
/** Cache outputs that belong to the wallt for all tranasctions in the wallet */
519523
void RefreshAllTXOs() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
524+
void PruneSpentTXOs() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
525+
std::pair<TXOMap::iterator, TXOMap::iterator> MarkTXOUnusable(const COutPoint& outpoint) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
526+
std::pair<TXOMap::iterator, TXOMap::iterator> MarkTXOUsable(const COutPoint& outpoint) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
520527

521528
/**
522529
* 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)