wallet: batch and simplify ZapSelectTx process

The goal of the function is to erase the wallet transactions that
match the inputted hashes. There is no need to traverse the database,
reading record by record, to then perform single entry removals for
each of them.

To ensure consistency and improve performance, this change-set removes
all tx records within a single atomic db batch operation, as well as
it cleans up code, improves error handling and simplifies the
transactions removal process entirely.

This optimizes the removal of watch-only transactions during the wallet
migration process and the 'removeprunedfunds' RPC command.
This commit is contained in:
furszy 2023-07-16 13:31:38 -03:00
parent 595d50a103
commit 83b762845f
No known key found for this signature in database
GPG Key ID: 5DD23CCC686AA623
7 changed files with 44 additions and 113 deletions

View File

@ -394,14 +394,8 @@ RPCHelpMan removeprunedfunds()
uint256 hash(ParseHashV(request.params[0], "txid")); uint256 hash(ParseHashV(request.params[0], "txid"));
std::vector<uint256> vHash; std::vector<uint256> vHash;
vHash.push_back(hash); vHash.push_back(hash);
std::vector<uint256> vHashOut; if (auto res = pwallet->ZapSelectTx(vHash); !res) {
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
if (pwallet->ZapSelectTx(vHash, vHashOut) != DBErrors::LOAD_OK) {
throw JSONRPCError(RPC_WALLET_ERROR, "Could not properly delete the transaction.");
}
if(vHashOut.empty()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Transaction does not exist in wallet.");
} }
return UniValue::VNULL; return UniValue::VNULL;

View File

@ -918,8 +918,8 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
BOOST_CHECK(wallet->HasWalletSpend(prev_tx)); BOOST_CHECK(wallet->HasWalletSpend(prev_tx));
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u); BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u);
std::vector<uint256> vHashIn{ block_hash }, vHashOut; std::vector<uint256> vHashIn{ block_hash };
BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vHashIn, vHashOut), DBErrors::LOAD_OK); BOOST_CHECK(wallet->ZapSelectTx(vHashIn));
BOOST_CHECK(!wallet->HasWalletSpend(prev_tx)); BOOST_CHECK(!wallet->HasWalletSpend(prev_tx));
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u); BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u);

View File

@ -2311,12 +2311,41 @@ DBErrors CWallet::LoadWallet()
return nLoadWalletRet; return nLoadWalletRet;
} }
DBErrors CWallet::ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256>& vHashOut) util::Result<void> CWallet::ZapSelectTx(std::vector<uint256>& txs_to_remove)
{ {
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
DBErrors nZapSelectTxRet = WalletBatch(GetDatabase()).ZapSelectTx(vHashIn, vHashOut); WalletBatch batch(GetDatabase());
for (const uint256& hash : vHashOut) { if (!batch.TxnBegin()) return util::Error{_("Error starting db txn for wallet transactions removal")};
const auto& it = mapWallet.find(hash);
// Check for transaction existence and remove entries from disk
using TxIterator = std::unordered_map<uint256, CWalletTx, SaltedTxidHasher>::const_iterator;
std::vector<TxIterator> erased_txs;
bilingual_str str_err;
for (const uint256& hash : txs_to_remove) {
auto it_wtx = mapWallet.find(hash);
if (it_wtx == mapWallet.end()) {
str_err = strprintf(_("Transaction %s does not belong to this wallet"), hash.GetHex());
break;
}
if (!batch.EraseTx(hash)) {
str_err = strprintf(_("Failure removing transaction: %s"), hash.GetHex());
break;
}
erased_txs.emplace_back(it_wtx);
}
// Roll back removals in case of an error
if (!str_err.empty()) {
batch.TxnAbort();
return util::Error{str_err};
}
// Dump changes to disk
if (!batch.TxnCommit()) return util::Error{_("Error committing db txn for wallet transactions removal")};
// Update the in-memory state and notify upper layers about the removals
for (const auto& it : erased_txs) {
const uint256 hash{it->first};
wtxOrdered.erase(it->second.m_it_wtxOrdered); wtxOrdered.erase(it->second.m_it_wtxOrdered);
for (const auto& txin : it->second.tx->vin) for (const auto& txin : it->second.tx->vin)
mapTxSpends.erase(txin.prevout); mapTxSpends.erase(txin.prevout);
@ -2324,12 +2353,9 @@ DBErrors CWallet::ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256
NotifyTransactionChanged(hash, CT_DELETED); NotifyTransactionChanged(hash, CT_DELETED);
} }
if (nZapSelectTxRet != DBErrors::LOAD_OK)
return nZapSelectTxRet;
MarkDirty(); MarkDirty();
return DBErrors::LOAD_OK; return {}; // all good
} }
bool CWallet::SetAddressBookWithDB(WalletBatch& batch, const CTxDestination& address, const std::string& strName, const std::optional<AddressPurpose>& new_purpose) bool CWallet::SetAddressBookWithDB(WalletBatch& batch, const CTxDestination& address, const std::string& strName, const std::optional<AddressPurpose>& new_purpose)
@ -3925,13 +3951,8 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
watchonly_batch.reset(); // Flush watchonly_batch.reset(); // Flush
// Do the removes // Do the removes
if (txids_to_delete.size() > 0) { if (txids_to_delete.size() > 0) {
std::vector<uint256> deleted_txids; if (auto res = ZapSelectTx(txids_to_delete); !res) {
if (ZapSelectTx(txids_to_delete, deleted_txids) != DBErrors::LOAD_OK) { error = _("Error: Could not delete watchonly transactions. ") + util::ErrorString(res);
error = _("Error: Could not delete watchonly transactions");
return false;
}
if (deleted_txids != txids_to_delete) {
error = _("Error: Not all watchonly txs could be deleted");
return false; return false;
} }
} }

View File

@ -787,7 +787,9 @@ public:
void chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) override; void chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) override;
DBErrors LoadWallet(); DBErrors LoadWallet();
DBErrors ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256>& vHashOut) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/** Erases the provided transactions from the wallet. */
util::Result<void> ZapSelectTx(std::vector<uint256>& txs_to_remove) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool SetAddressBook(const CTxDestination& address, const std::string& strName, const std::optional<AddressPurpose>& purpose); bool SetAddressBook(const CTxDestination& address, const std::string& strName, const std::optional<AddressPurpose>& purpose);

View File

@ -1230,90 +1230,6 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
return result; return result;
} }
DBErrors WalletBatch::FindWalletTxHashes(std::vector<uint256>& tx_hashes)
{
DBErrors result = DBErrors::LOAD_OK;
try {
int nMinVersion = 0;
if (m_batch->Read(DBKeys::MINVERSION, nMinVersion)) {
if (nMinVersion > FEATURE_LATEST)
return DBErrors::TOO_NEW;
}
// Get cursor
std::unique_ptr<DatabaseCursor> cursor = m_batch->GetNewCursor();
if (!cursor)
{
LogPrintf("Error getting wallet database cursor\n");
return DBErrors::CORRUPT;
}
while (true)
{
// Read next record
DataStream ssKey{};
DataStream ssValue{};
DatabaseCursor::Status status = cursor->Next(ssKey, ssValue);
if (status == DatabaseCursor::Status::DONE) {
break;
} else if (status == DatabaseCursor::Status::FAIL) {
LogPrintf("Error reading next record from wallet database\n");
return DBErrors::CORRUPT;
}
std::string strType;
ssKey >> strType;
if (strType == DBKeys::TX) {
uint256 hash;
ssKey >> hash;
tx_hashes.push_back(hash);
}
}
} catch (...) {
result = DBErrors::CORRUPT;
}
return result;
}
DBErrors WalletBatch::ZapSelectTx(std::vector<uint256>& vTxHashIn, std::vector<uint256>& vTxHashOut)
{
// build list of wallet TX hashes
std::vector<uint256> vTxHash;
DBErrors err = FindWalletTxHashes(vTxHash);
if (err != DBErrors::LOAD_OK) {
return err;
}
std::sort(vTxHash.begin(), vTxHash.end());
std::sort(vTxHashIn.begin(), vTxHashIn.end());
// erase each matching wallet TX
bool delerror = false;
std::vector<uint256>::iterator it = vTxHashIn.begin();
for (const uint256& hash : vTxHash) {
while (it < vTxHashIn.end() && (*it) < hash) {
it++;
}
if (it == vTxHashIn.end()) {
break;
}
else if ((*it) == hash) {
if(!EraseTx(hash)) {
LogPrint(BCLog::WALLETDB, "Transaction was found for deletion but returned database error: %s\n", hash.GetHex());
delerror = true;
}
vTxHashOut.push_back(hash);
}
}
if (delerror) {
return DBErrors::CORRUPT;
}
return DBErrors::LOAD_OK;
}
void MaybeCompactWalletDB(WalletContext& context) void MaybeCompactWalletDB(WalletContext& context)
{ {
static std::atomic<bool> fOneThread(false); static std::atomic<bool> fOneThread(false);

View File

@ -275,8 +275,6 @@ public:
bool EraseActiveScriptPubKeyMan(uint8_t type, bool internal); bool EraseActiveScriptPubKeyMan(uint8_t type, bool internal);
DBErrors LoadWallet(CWallet* pwallet); DBErrors LoadWallet(CWallet* pwallet);
DBErrors FindWalletTxHashes(std::vector<uint256>& tx_hashes);
DBErrors ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256>& vHashOut);
//! write the hdchain model (external chain child index counter) //! write the hdchain model (external chain child index counter)
bool WriteHDChain(const CHDChain& chain); bool WriteHDChain(const CHDChain& chain);

View File

@ -120,7 +120,7 @@ class ImportPrunedFundsTest(BitcoinTestFramework):
assert_equal(address_info['ismine'], True) assert_equal(address_info['ismine'], True)
# Remove transactions # Remove transactions
assert_raises_rpc_error(-8, "Transaction does not exist in wallet.", w1.removeprunedfunds, txnid1) assert_raises_rpc_error(-4, f'Transaction {txnid1} does not belong to this wallet', w1.removeprunedfunds, txnid1)
assert not [tx for tx in w1.listtransactions(include_watchonly=True) if tx['txid'] == txnid1] assert not [tx for tx in w1.listtransactions(include_watchonly=True) if tx['txid'] == txnid1]
wwatch.removeprunedfunds(txnid2) wwatch.removeprunedfunds(txnid2)