wallet, rpc: Move (Un)LockCoin WalletBatch creation out of RPC

If the locked coin needs to be persisted to the wallet database,
insteead of having the RPC figure out when to create a WalletBatch and
having LockCoin's behavior depend on it, have LockCoin take whether to
persist as a parameter so it makes the batch.

Since unlocking a persisted locked coin requires a database write as
well, we need to track whether the locked coin was persisted to the
wallet database so that it can erase the locked coin when necessary.

Keeping track of whether a locked coin was persisted is also useful
information for future PRs.
This commit is contained in:
Ava Chow
2025-05-12 13:29:50 -07:00
parent 2df824f4e6
commit 6135e0553e
8 changed files with 49 additions and 47 deletions

View File

@@ -249,14 +249,12 @@ public:
bool lockCoin(const COutPoint& output, const bool write_to_db) override bool lockCoin(const COutPoint& output, const bool write_to_db) override
{ {
LOCK(m_wallet->cs_wallet); LOCK(m_wallet->cs_wallet);
std::unique_ptr<WalletBatch> batch = write_to_db ? std::make_unique<WalletBatch>(m_wallet->GetDatabase()) : nullptr; return m_wallet->LockCoin(output, write_to_db);
return m_wallet->LockCoin(output, batch.get());
} }
bool unlockCoin(const COutPoint& output) override bool unlockCoin(const COutPoint& output) override
{ {
LOCK(m_wallet->cs_wallet); LOCK(m_wallet->cs_wallet);
std::unique_ptr<WalletBatch> batch = std::make_unique<WalletBatch>(m_wallet->GetDatabase()); return m_wallet->UnlockCoin(output);
return m_wallet->UnlockCoin(output, batch.get());
} }
bool isLockedCoin(const COutPoint& output) override bool isLockedCoin(const COutPoint& output) override
{ {

View File

@@ -356,16 +356,12 @@ RPCHelpMan lockunspent()
outputs.push_back(outpt); outputs.push_back(outpt);
} }
std::unique_ptr<WalletBatch> batch = nullptr;
// Unlock is always persistent
if (fUnlock || persistent) batch = std::make_unique<WalletBatch>(pwallet->GetDatabase());
// Atomically set (un)locked status for the outputs. // Atomically set (un)locked status for the outputs.
for (const COutPoint& outpt : outputs) { for (const COutPoint& outpt : outputs) {
if (fUnlock) { if (fUnlock) {
if (!pwallet->UnlockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coin failed"); if (!pwallet->UnlockCoin(outpt)) throw JSONRPCError(RPC_WALLET_ERROR, "Unlocking coin failed");
} else { } else {
if (!pwallet->LockCoin(outpt, batch.get())) throw JSONRPCError(RPC_WALLET_ERROR, "Locking coin failed"); if (!pwallet->LockCoin(outpt, persistent)) throw JSONRPCError(RPC_WALLET_ERROR, "Locking coin failed");
} }
} }

View File

@@ -1579,7 +1579,7 @@ RPCHelpMan sendall()
const bool lock_unspents{options.exists("lock_unspents") ? options["lock_unspents"].get_bool() : false}; const bool lock_unspents{options.exists("lock_unspents") ? options["lock_unspents"].get_bool() : false};
if (lock_unspents) { if (lock_unspents) {
for (const CTxIn& txin : rawTx.vin) { for (const CTxIn& txin : rawTx.vin) {
pwallet->LockCoin(txin.prevout); pwallet->LockCoin(txin.prevout, /*persist=*/false);
} }
} }

View File

@@ -1467,7 +1467,7 @@ util::Result<CreatedTransactionResult> FundTransaction(CWallet& wallet, const CM
if (lockUnspents) { if (lockUnspents) {
for (const CTxIn& txin : res->tx->vin) { for (const CTxIn& txin : res->tx->vin) {
wallet.LockCoin(txin.prevout); wallet.LockCoin(txin.prevout, /*persist=*/false);
} }
} }

View File

@@ -458,7 +458,7 @@ BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup)
for (const auto& group : list) { for (const auto& group : list) {
for (const auto& coin : group.second) { for (const auto& coin : group.second) {
LOCK(wallet->cs_wallet); LOCK(wallet->cs_wallet);
wallet->LockCoin(coin.outpoint); wallet->LockCoin(coin.outpoint, /*persist=*/false);
} }
} }
{ {
@@ -486,7 +486,7 @@ void TestCoinsResult(ListCoinsTest& context, OutputType out_type, CAmount amount
filter.skip_locked = false; filter.skip_locked = false;
CoinsResult available_coins = AvailableCoins(*context.wallet, nullptr, std::nullopt, filter); CoinsResult available_coins = AvailableCoins(*context.wallet, nullptr, std::nullopt, filter);
// Lock outputs so they are not spent in follow-up transactions // Lock outputs so they are not spent in follow-up transactions
for (uint32_t i = 0; i < wtx.tx->vout.size(); i++) context.wallet->LockCoin({wtx.GetHash(), i}); for (uint32_t i = 0; i < wtx.tx->vout.size(); i++) context.wallet->LockCoin({wtx.GetHash(), i}, /*persist=*/false);
for (const auto& [type, size] : expected_coins_sizes) BOOST_CHECK_EQUAL(size, available_coins.coins[type].size()); for (const auto& [type, size] : expected_coins_sizes) BOOST_CHECK_EQUAL(size, available_coins.coins[type].size());
} }

View File

@@ -785,16 +785,11 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const
return false; return false;
} }
void CWallet::AddToSpends(const COutPoint& outpoint, const Txid& txid, WalletBatch* batch) void CWallet::AddToSpends(const COutPoint& outpoint, const Txid& txid)
{ {
mapTxSpends.insert(std::make_pair(outpoint, txid)); mapTxSpends.insert(std::make_pair(outpoint, txid));
if (batch) { UnlockCoin(outpoint);
UnlockCoin(outpoint, batch);
} else {
WalletBatch temp_batch(GetDatabase());
UnlockCoin(outpoint, &temp_batch);
}
std::pair<TxSpends::iterator, TxSpends::iterator> range; std::pair<TxSpends::iterator, TxSpends::iterator> range;
range = mapTxSpends.equal_range(outpoint); range = mapTxSpends.equal_range(outpoint);
@@ -802,13 +797,13 @@ void CWallet::AddToSpends(const COutPoint& outpoint, const Txid& txid, WalletBat
} }
void CWallet::AddToSpends(const CWalletTx& wtx, WalletBatch* batch) void CWallet::AddToSpends(const CWalletTx& wtx)
{ {
if (wtx.IsCoinBase()) // Coinbases don't spend anything! if (wtx.IsCoinBase()) // Coinbases don't spend anything!
return; return;
for (const CTxIn& txin : wtx.tx->vin) for (const CTxIn& txin : wtx.tx->vin)
AddToSpends(txin.prevout, wtx.GetHash(), batch); AddToSpends(txin.prevout, wtx.GetHash());
} }
bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
@@ -1058,7 +1053,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const
wtx.nOrderPos = IncOrderPosNext(&batch); wtx.nOrderPos = IncOrderPosNext(&batch);
wtx.m_it_wtxOrdered = wtxOrdered.insert(std::make_pair(wtx.nOrderPos, &wtx)); wtx.m_it_wtxOrdered = wtxOrdered.insert(std::make_pair(wtx.nOrderPos, &wtx));
wtx.nTimeSmart = ComputeTimeSmart(wtx, rescanning_old_block); wtx.nTimeSmart = ComputeTimeSmart(wtx, rescanning_old_block);
AddToSpends(wtx, &batch); AddToSpends(wtx);
// Update birth time when tx time is older than it. // Update birth time when tx time is older than it.
MaybeUpdateBirthTime(wtx.GetTxTime()); MaybeUpdateBirthTime(wtx.GetTxTime());
@@ -2622,22 +2617,34 @@ util::Result<void> CWallet::DisplayAddress(const CTxDestination& dest)
return util::Error{_("There is no ScriptPubKeyManager for this address")}; return util::Error{_("There is no ScriptPubKeyManager for this address")};
} }
bool CWallet::LockCoin(const COutPoint& output, WalletBatch* batch) void CWallet::LoadLockedCoin(const COutPoint& coin, bool persistent)
{ {
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
setLockedCoins.insert(output); m_locked_coins.emplace(coin, persistent);
if (batch) { }
return batch->WriteLockedUTXO(output);
bool CWallet::LockCoin(const COutPoint& output, bool persist)
{
AssertLockHeld(cs_wallet);
LoadLockedCoin(output, persist);
if (persist) {
WalletBatch batch(GetDatabase());
return batch.WriteLockedUTXO(output);
} }
return true; return true;
} }
bool CWallet::UnlockCoin(const COutPoint& output, WalletBatch* batch) bool CWallet::UnlockCoin(const COutPoint& output)
{ {
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
bool was_locked = setLockedCoins.erase(output); auto locked_coin_it = m_locked_coins.find(output);
if (batch && was_locked) { if (locked_coin_it != m_locked_coins.end()) {
return batch->EraseLockedUTXO(output); bool persisted = locked_coin_it->second;
m_locked_coins.erase(locked_coin_it);
if (persisted) {
WalletBatch batch(GetDatabase());
return batch.EraseLockedUTXO(output);
}
} }
return true; return true;
} }
@@ -2647,26 +2654,24 @@ bool CWallet::UnlockAllCoins()
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
bool success = true; bool success = true;
WalletBatch batch(GetDatabase()); WalletBatch batch(GetDatabase());
for (auto it = setLockedCoins.begin(); it != setLockedCoins.end(); ++it) { for (const auto& [coin, persistent] : m_locked_coins) {
success &= batch.EraseLockedUTXO(*it); if (persistent) success = success && batch.EraseLockedUTXO(coin);
} }
setLockedCoins.clear(); m_locked_coins.clear();
return success; return success;
} }
bool CWallet::IsLockedCoin(const COutPoint& output) const bool CWallet::IsLockedCoin(const COutPoint& output) const
{ {
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
return setLockedCoins.count(output) > 0; return m_locked_coins.count(output) > 0;
} }
void CWallet::ListLockedCoins(std::vector<COutPoint>& vOutpts) const void CWallet::ListLockedCoins(std::vector<COutPoint>& vOutpts) const
{ {
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
for (std::set<COutPoint>::iterator it = setLockedCoins.begin(); for (const auto& [coin, _] : m_locked_coins) {
it != setLockedCoins.end(); it++) { vOutpts.push_back(coin);
COutPoint outpt = (*it);
vOutpts.push_back(outpt);
} }
} }

View File

@@ -333,8 +333,8 @@ private:
*/ */
typedef std::unordered_multimap<COutPoint, Txid, SaltedOutpointHasher> TxSpends; typedef std::unordered_multimap<COutPoint, Txid, SaltedOutpointHasher> TxSpends;
TxSpends mapTxSpends GUARDED_BY(cs_wallet); TxSpends mapTxSpends GUARDED_BY(cs_wallet);
void AddToSpends(const COutPoint& outpoint, const Txid& txid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void AddToSpends(const COutPoint& outpoint, const Txid& txid) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void AddToSpends(const CWalletTx& wtx, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void AddToSpends(const CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/** /**
* Add a transaction to the wallet, or update it. confirm.block_* should * Add a transaction to the wallet, or update it. confirm.block_* should
@@ -497,8 +497,10 @@ public:
/** Set of Coins owned by this wallet that we won't try to spend from. A /** Set of Coins owned by this wallet that we won't try to spend from. A
* Coin may be locked if it has already been used to fund a transaction * Coin may be locked if it has already been used to fund a transaction
* that hasn't confirmed yet. We wouldn't consider the Coin spent already, * that hasn't confirmed yet. We wouldn't consider the Coin spent already,
* but also shouldn't try to use it again. */ * but also shouldn't try to use it again.
std::set<COutPoint> setLockedCoins GUARDED_BY(cs_wallet); * bool to track whether this locked coin is persisted to disk.
*/
std::map<COutPoint, bool> m_locked_coins GUARDED_BY(cs_wallet);
/** Registered interfaces::Chain::Notifications handler. */ /** Registered interfaces::Chain::Notifications handler. */
std::unique_ptr<interfaces::Handler> m_chain_notifications_handler; std::unique_ptr<interfaces::Handler> m_chain_notifications_handler;
@@ -546,8 +548,9 @@ public:
util::Result<void> DisplayAddress(const CTxDestination& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); util::Result<void> DisplayAddress(const CTxDestination& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool IsLockedCoin(const COutPoint& output) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsLockedCoin(const COutPoint& output) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool LockCoin(const COutPoint& output, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void LoadLockedCoin(const COutPoint& coin, bool persistent) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool UnlockCoin(const COutPoint& output, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool LockCoin(const COutPoint& output, bool persist) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool UnlockCoin(const COutPoint& output) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void ListLockedCoins(std::vector<COutPoint>& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void ListLockedCoins(std::vector<COutPoint>& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

View File

@@ -1072,7 +1072,7 @@ static DBErrors LoadTxRecords(CWallet* pwallet, DatabaseBatch& batch, std::vecto
uint32_t n; uint32_t n;
key >> hash; key >> hash;
key >> n; key >> n;
pwallet->LockCoin(COutPoint(hash, n)); pwallet->LoadLockedCoin(COutPoint(hash, n), /*persistent=*/true);
return DBErrors::LOAD_OK; return DBErrors::LOAD_OK;
}); });
result = std::max(result, locked_utxo_res.m_result); result = std::max(result, locked_utxo_res.m_result);