Skip to content

Commit 9015c77

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 7aa77bc commit 9015c77

File tree

5 files changed

+153
-15
lines changed

5 files changed

+153
-15
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-4
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,10 @@ CoinsResult AvailableCoins(const CWallet& wallet,
332332
if (wallet.IsLockedCoin(outpoint) && params.skip_locked)
333333
continue;
334334

335+
int nDepth = wallet.GetTxStateDepthInMainChain(txo.GetState());
336+
Assert(nDepth >= 0);
337+
Assert(!wallet.IsSpent(outpoint, /*min_depth=*/1));
338+
335339
if (wallet.IsSpent(outpoint))
336340
continue;
337341

@@ -349,10 +353,6 @@ CoinsResult AvailableCoins(const CWallet& wallet,
349353

350354
assert(mine != ISMINE_NO);
351355

352-
int nDepth = wallet.GetTxStateDepthInMainChain(txo.GetState());
353-
if (nDepth < 0)
354-
continue;
355-
356356
if (tx_safe_cache.count(outpoint.hash) == 0 ) {
357357
tx_safe_cache[outpoint.hash] = {false, false};
358358
const CWalletTx& wtx = *wallet.GetWalletTx(outpoint.hash);

src/wallet/wallet.cpp

+107-9
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,20 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
11561156
// Break debit/credit balance caches:
11571157
wtx.MarkDirty();
11581158

1159+
// Remove or add back the inputs from m_txos to match the state of this tx.
1160+
if (wtx.isConfirmed())
1161+
{
1162+
// When a transaction becomes confirmed, we can remove all of the txos that were spent
1163+
// in its inputs as they are no longer relevant.
1164+
for (const CTxIn& txin : wtx.tx->vin) {
1165+
MarkTXOUnusable(txin.prevout);
1166+
}
1167+
} else if (wtx.isInactive()) {
1168+
// When a transaction becomes inactive, we need to mark its inputs as usable again
1169+
for (const CTxIn& txin : wtx.tx->vin) {
1170+
MarkTXOUsable(txin.prevout);
1171+
}
1172+
}
11591173
// Get the outputs that belong to the wallet
11601174
RefreshWalletTxTXOs(wtx);
11611175

@@ -1398,12 +1412,20 @@ void CWallet::RecursiveUpdateTxState(const uint256& tx_hash, const TryUpdatingSt
13981412
batch.WriteTx(wtx);
13991413
// Iterate over all its outputs, and update those tx states as well (if applicable)
14001414
for (unsigned int i = 0; i < wtx.tx->vout.size(); ++i) {
1401-
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(COutPoint(Txid::FromUint256(now), i));
1415+
COutPoint outpoint{Txid::FromUint256(now), i};
1416+
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> range = mapTxSpends.equal_range(outpoint);
14021417
for (TxSpends::const_iterator iter = range.first; iter != range.second; ++iter) {
14031418
if (!done.count(iter->second)) {
14041419
todo.insert(iter->second);
14051420
}
14061421
}
1422+
if (wtx.state<TxStateConflicted>() || wtx.state<TxStateConfirmed>()) {
1423+
// If the state applied is conflicted or confirmed, the outputs are unusable
1424+
MarkTXOUnusable(outpoint);
1425+
} else {
1426+
// Otherwise make the outputs usable
1427+
MarkTXOUsable(outpoint);
1428+
}
14071429
}
14081430

14091431
if (update_state == TxUpdate::NOTIFY_CHANGED) {
@@ -1413,6 +1435,21 @@ void CWallet::RecursiveUpdateTxState(const uint256& tx_hash, const TryUpdatingSt
14131435
// If a transaction changes its tx state, that usually changes the balance
14141436
// available of the outputs it spends. So force those to be recomputed
14151437
MarkInputsDirty(wtx.tx);
1438+
// Make the non-conflicted inputs usable again
1439+
for (unsigned int i = 0; i < wtx.tx->vin.size(); ++i) {
1440+
const CTxIn& txin = wtx.tx->vin.at(i);
1441+
auto unusable_txo_it = m_unusable_txos.find(txin.prevout);
1442+
if (unusable_txo_it == m_unusable_txos.end()) {
1443+
continue;
1444+
}
1445+
1446+
if (std::get_if<TxStateConflicted>(&unusable_txo_it->second.GetState()) ||
1447+
std::get_if<TxStateConfirmed>(&unusable_txo_it->second.GetState())) {
1448+
continue;
1449+
}
1450+
1451+
MarkTXOUsable(txin.prevout);
1452+
}
14161453
}
14171454
}
14181455
}
@@ -3248,6 +3285,10 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
32483285
}
32493286
walletInstance->m_attaching_chain = false;
32503287

3288+
// Remove TXOs that have already been spent
3289+
// We do this here as we need to have an attached chain to figure out what has actually been spent.
3290+
walletInstance->PruneSpentTXOs();
3291+
32513292
return true;
32523293
}
32533294

@@ -4398,26 +4439,34 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& walle
43984439
return res;
43994440
}
44004441

4442+
using TXOMap = std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>;
44014443
void CWallet::RefreshWalletTxTXOs(const CWalletTx& wtx)
44024444
{
44034445
AssertLockHeld(cs_wallet);
44044446
for (uint32_t i = 0; i < wtx.tx->vout.size(); ++i) {
44054447
const CTxOut& txout = wtx.tx->vout.at(i);
44064448
COutPoint outpoint(wtx.GetHash(), i);
44074449

4408-
auto it = m_txos.find(outpoint);
4409-
44104450
isminetype ismine = IsMine(txout);
44114451
if (ismine == ISMINE_NO) {
44124452
continue;
44134453
}
44144454

4415-
if (it != m_txos.end()) {
4455+
auto it = wtx.m_txos.find(i);
4456+
if (it != wtx.m_txos.end()) {
44164457
it->second.SetIsMine(ismine);
44174458
it->second.SetState(wtx.GetState());
44184459
} else {
4419-
auto [txo_it, _] = m_txos.emplace(outpoint, WalletTXO{txout, ismine, wtx.GetState(), wtx.IsCoinBase(), wtx.m_from_me, wtx.GetTxTime()});
4420-
wtx.m_txos.emplace(i, txo_it->second);
4460+
TXOMap::iterator txo_it;
4461+
bool txos_inserted = false;
4462+
if (m_last_block_processed_height >= 0 && IsSpent(outpoint, /*min_depth=*/1)) {
4463+
std::tie(txo_it, txos_inserted) = m_unusable_txos.emplace(outpoint, WalletTXO{txout, ismine, wtx.GetState(), wtx.IsCoinBase(), wtx.m_from_me, wtx.GetTxTime()});
4464+
assert(txos_inserted);
4465+
} else {
4466+
std::tie(txo_it, txos_inserted) = m_txos.emplace(outpoint, WalletTXO{txout, ismine, wtx.GetState(), wtx.IsCoinBase(), wtx.m_from_me, wtx.GetTxTime()});
4467+
}
4468+
auto [_, wtx_inserted] = wtx.m_txos.emplace(i, txo_it->second);
4469+
assert(wtx_inserted);
44214470
}
44224471
}
44234472
}
@@ -4434,9 +4483,58 @@ std::optional<WalletTXO> CWallet::GetTXO(const COutPoint& outpoint) const
44344483
{
44354484
AssertLockHeld(cs_wallet);
44364485
const auto& it = m_txos.find(outpoint);
4437-
if (it == m_txos.end()) {
4438-
return std::nullopt;
4486+
if (it != m_txos.end()) {
4487+
return it->second;
44394488
}
4440-
return it->second;
4489+
const auto& u_it = m_unusable_txos.find(outpoint);
4490+
if (u_it != m_unusable_txos.end()) {
4491+
return u_it->second;
4492+
}
4493+
return std::nullopt;
4494+
}
4495+
4496+
void CWallet::PruneSpentTXOs()
4497+
{
4498+
AssertLockHeld(cs_wallet);
4499+
auto it = m_txos.begin();
4500+
while (it != m_txos.end()) {
4501+
if (std::get_if<TxStateConflicted>(&it->second.GetState()) || IsSpent(it->first, /*min_depth=*/1)) {
4502+
it = MarkTXOUnusable(it->first).first;
4503+
} else {
4504+
it++;
4505+
}
4506+
}
4507+
}
4508+
4509+
std::pair<TXOMap::iterator, TXOMap::iterator> CWallet::MarkTXOUnusable(const COutPoint& outpoint)
4510+
{
4511+
AssertLockHeld(cs_wallet);
4512+
auto txos_it = m_txos.find(outpoint);
4513+
auto unusable_txos_it = m_unusable_txos.end();
4514+
if (txos_it != m_txos.end()) {
4515+
auto next_txo_it = std::next(txos_it);
4516+
auto nh = m_txos.extract(txos_it);
4517+
txos_it = next_txo_it;
4518+
auto [position, inserted, _] = m_unusable_txos.insert(std::move(nh));
4519+
unusable_txos_it = position;
4520+
assert(inserted);
4521+
}
4522+
return {txos_it, unusable_txos_it};
4523+
}
4524+
4525+
std::pair<TXOMap::iterator, TXOMap::iterator> CWallet::MarkTXOUsable(const COutPoint& outpoint)
4526+
{
4527+
AssertLockHeld(cs_wallet);
4528+
auto txos_it = m_txos.end();
4529+
auto unusable_txos_it = m_unusable_txos.find(outpoint);
4530+
if (unusable_txos_it != m_unusable_txos.end()) {
4531+
auto next_unusable_txo_it = std::next(unusable_txos_it);
4532+
auto nh = m_unusable_txos.extract(unusable_txos_it);
4533+
unusable_txos_it = next_unusable_txo_it;
4534+
auto [position, inserted, _] = m_txos.insert(std::move(nh));
4535+
assert(inserted);
4536+
txos_it = position;
4537+
}
4538+
return {unusable_txos_it, txos_it};
44414539
}
44424540
} // namespace wallet

src/wallet/wallet.h

+9-2
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,11 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
423423
void AddActiveScriptPubKeyManWithDb(WalletBatch& batch, uint256 id, OutputType type, bool internal);
424424

425425
//! Set of transaction outputs owned by this wallet
426-
std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher> m_txos GUARDED_BY(cs_wallet);
426+
using TXOMap = std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>;
427+
TXOMap m_txos GUARDED_BY(cs_wallet);
428+
//! Set of transaction outputs that are definitely no longer usuable
429+
//! These outputs may already be spent in a confirmed tx, or are the outputs of a conflicted tx
430+
TXOMap m_unusable_txos GUARDED_BY(cs_wallet);
427431

428432
/**
429433
* Catch wallet up to current chain, scanning new blocks, updating the best
@@ -504,11 +508,14 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
504508

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

507-
const std::unordered_map<COutPoint, WalletTXO, SaltedOutpointHasher>& GetTXOs() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return m_txos; };
511+
const TXOMap& GetTXOs() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return m_txos; };
508512
std::optional<WalletTXO> GetTXO(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
509513

510514
void RefreshWalletTxTXOs(const CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
511515
void RefreshAllTXOs() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
516+
void PruneSpentTXOs() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
517+
std::pair<TXOMap::iterator, TXOMap::iterator> MarkTXOUnusable(const COutPoint& outpoint) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
518+
std::pair<TXOMap::iterator, TXOMap::iterator> MarkTXOUsable(const COutPoint& outpoint) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
512519

513520
/**
514521
* Return depth of transaction in blockchain:

test/functional/wallet_balance.py

+31
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
assert_equal,
1515
assert_is_hash_string,
1616
assert_raises_rpc_error,
17+
find_vout_for_address,
1718
)
1819
from test_framework.wallet_util import get_generate_key
1920

@@ -400,5 +401,35 @@ def test_balances(*, fee_node_1=0):
400401
balances = wallet.getbalances()
401402
assert_equal(balances["mine"]["trusted"], amount * 2)
402403

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

0 commit comments

Comments
 (0)