Skip to content

Commit 83b7628

Browse files
committed
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.
1 parent 595d50a commit 83b7628

File tree

7 files changed

+44
-113
lines changed

7 files changed

+44
-113
lines changed

src/wallet/rpc/backup.cpp

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -394,14 +394,8 @@ RPCHelpMan removeprunedfunds()
394394
uint256 hash(ParseHashV(request.params[0], "txid"));
395395
std::vector<uint256> vHash;
396396
vHash.push_back(hash);
397-
std::vector<uint256> vHashOut;
398-
399-
if (pwallet->ZapSelectTx(vHash, vHashOut) != DBErrors::LOAD_OK) {
400-
throw JSONRPCError(RPC_WALLET_ERROR, "Could not properly delete the transaction.");
401-
}
402-
403-
if(vHashOut.empty()) {
404-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Transaction does not exist in wallet.");
397+
if (auto res = pwallet->ZapSelectTx(vHash); !res) {
398+
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
405399
}
406400

407401
return UniValue::VNULL;

src/wallet/test/wallet_tests.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -918,8 +918,8 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
918918
BOOST_CHECK(wallet->HasWalletSpend(prev_tx));
919919
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u);
920920

921-
std::vector<uint256> vHashIn{ block_hash }, vHashOut;
922-
BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vHashIn, vHashOut), DBErrors::LOAD_OK);
921+
std::vector<uint256> vHashIn{ block_hash };
922+
BOOST_CHECK(wallet->ZapSelectTx(vHashIn));
923923

924924
BOOST_CHECK(!wallet->HasWalletSpend(prev_tx));
925925
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u);

src/wallet/wallet.cpp

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2311,25 +2311,51 @@ DBErrors CWallet::LoadWallet()
23112311
return nLoadWalletRet;
23122312
}
23132313

2314-
DBErrors CWallet::ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256>& vHashOut)
2314+
util::Result<void> CWallet::ZapSelectTx(std::vector<uint256>& txs_to_remove)
23152315
{
23162316
AssertLockHeld(cs_wallet);
2317-
DBErrors nZapSelectTxRet = WalletBatch(GetDatabase()).ZapSelectTx(vHashIn, vHashOut);
2318-
for (const uint256& hash : vHashOut) {
2319-
const auto& it = mapWallet.find(hash);
2317+
WalletBatch batch(GetDatabase());
2318+
if (!batch.TxnBegin()) return util::Error{_("Error starting db txn for wallet transactions removal")};
2319+
2320+
// Check for transaction existence and remove entries from disk
2321+
using TxIterator = std::unordered_map<uint256, CWalletTx, SaltedTxidHasher>::const_iterator;
2322+
std::vector<TxIterator> erased_txs;
2323+
bilingual_str str_err;
2324+
for (const uint256& hash : txs_to_remove) {
2325+
auto it_wtx = mapWallet.find(hash);
2326+
if (it_wtx == mapWallet.end()) {
2327+
str_err = strprintf(_("Transaction %s does not belong to this wallet"), hash.GetHex());
2328+
break;
2329+
}
2330+
if (!batch.EraseTx(hash)) {
2331+
str_err = strprintf(_("Failure removing transaction: %s"), hash.GetHex());
2332+
break;
2333+
}
2334+
erased_txs.emplace_back(it_wtx);
2335+
}
2336+
2337+
// Roll back removals in case of an error
2338+
if (!str_err.empty()) {
2339+
batch.TxnAbort();
2340+
return util::Error{str_err};
2341+
}
2342+
2343+
// Dump changes to disk
2344+
if (!batch.TxnCommit()) return util::Error{_("Error committing db txn for wallet transactions removal")};
2345+
2346+
// Update the in-memory state and notify upper layers about the removals
2347+
for (const auto& it : erased_txs) {
2348+
const uint256 hash{it->first};
23202349
wtxOrdered.erase(it->second.m_it_wtxOrdered);
23212350
for (const auto& txin : it->second.tx->vin)
23222351
mapTxSpends.erase(txin.prevout);
23232352
mapWallet.erase(it);
23242353
NotifyTransactionChanged(hash, CT_DELETED);
23252354
}
23262355

2327-
if (nZapSelectTxRet != DBErrors::LOAD_OK)
2328-
return nZapSelectTxRet;
2329-
23302356
MarkDirty();
23312357

2332-
return DBErrors::LOAD_OK;
2358+
return {}; // all good
23332359
}
23342360

23352361
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)
39253951
watchonly_batch.reset(); // Flush
39263952
// Do the removes
39273953
if (txids_to_delete.size() > 0) {
3928-
std::vector<uint256> deleted_txids;
3929-
if (ZapSelectTx(txids_to_delete, deleted_txids) != DBErrors::LOAD_OK) {
3930-
error = _("Error: Could not delete watchonly transactions");
3931-
return false;
3932-
}
3933-
if (deleted_txids != txids_to_delete) {
3934-
error = _("Error: Not all watchonly txs could be deleted");
3954+
if (auto res = ZapSelectTx(txids_to_delete); !res) {
3955+
error = _("Error: Could not delete watchonly transactions. ") + util::ErrorString(res);
39353956
return false;
39363957
}
39373958
}

src/wallet/wallet.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
787787
void chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) override;
788788

789789
DBErrors LoadWallet();
790-
DBErrors ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256>& vHashOut) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
790+
791+
/** Erases the provided transactions from the wallet. */
792+
util::Result<void> ZapSelectTx(std::vector<uint256>& txs_to_remove) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
791793

792794
bool SetAddressBook(const CTxDestination& address, const std::string& strName, const std::optional<AddressPurpose>& purpose);
793795

src/wallet/walletdb.cpp

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,90 +1230,6 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
12301230
return result;
12311231
}
12321232

1233-
DBErrors WalletBatch::FindWalletTxHashes(std::vector<uint256>& tx_hashes)
1234-
{
1235-
DBErrors result = DBErrors::LOAD_OK;
1236-
1237-
try {
1238-
int nMinVersion = 0;
1239-
if (m_batch->Read(DBKeys::MINVERSION, nMinVersion)) {
1240-
if (nMinVersion > FEATURE_LATEST)
1241-
return DBErrors::TOO_NEW;
1242-
}
1243-
1244-
// Get cursor
1245-
std::unique_ptr<DatabaseCursor> cursor = m_batch->GetNewCursor();
1246-
if (!cursor)
1247-
{
1248-
LogPrintf("Error getting wallet database cursor\n");
1249-
return DBErrors::CORRUPT;
1250-
}
1251-
1252-
while (true)
1253-
{
1254-
// Read next record
1255-
DataStream ssKey{};
1256-
DataStream ssValue{};
1257-
DatabaseCursor::Status status = cursor->Next(ssKey, ssValue);
1258-
if (status == DatabaseCursor::Status::DONE) {
1259-
break;
1260-
} else if (status == DatabaseCursor::Status::FAIL) {
1261-
LogPrintf("Error reading next record from wallet database\n");
1262-
return DBErrors::CORRUPT;
1263-
}
1264-
1265-
std::string strType;
1266-
ssKey >> strType;
1267-
if (strType == DBKeys::TX) {
1268-
uint256 hash;
1269-
ssKey >> hash;
1270-
tx_hashes.push_back(hash);
1271-
}
1272-
}
1273-
} catch (...) {
1274-
result = DBErrors::CORRUPT;
1275-
}
1276-
1277-
return result;
1278-
}
1279-
1280-
DBErrors WalletBatch::ZapSelectTx(std::vector<uint256>& vTxHashIn, std::vector<uint256>& vTxHashOut)
1281-
{
1282-
// build list of wallet TX hashes
1283-
std::vector<uint256> vTxHash;
1284-
DBErrors err = FindWalletTxHashes(vTxHash);
1285-
if (err != DBErrors::LOAD_OK) {
1286-
return err;
1287-
}
1288-
1289-
std::sort(vTxHash.begin(), vTxHash.end());
1290-
std::sort(vTxHashIn.begin(), vTxHashIn.end());
1291-
1292-
// erase each matching wallet TX
1293-
bool delerror = false;
1294-
std::vector<uint256>::iterator it = vTxHashIn.begin();
1295-
for (const uint256& hash : vTxHash) {
1296-
while (it < vTxHashIn.end() && (*it) < hash) {
1297-
it++;
1298-
}
1299-
if (it == vTxHashIn.end()) {
1300-
break;
1301-
}
1302-
else if ((*it) == hash) {
1303-
if(!EraseTx(hash)) {
1304-
LogPrint(BCLog::WALLETDB, "Transaction was found for deletion but returned database error: %s\n", hash.GetHex());
1305-
delerror = true;
1306-
}
1307-
vTxHashOut.push_back(hash);
1308-
}
1309-
}
1310-
1311-
if (delerror) {
1312-
return DBErrors::CORRUPT;
1313-
}
1314-
return DBErrors::LOAD_OK;
1315-
}
1316-
13171233
void MaybeCompactWalletDB(WalletContext& context)
13181234
{
13191235
static std::atomic<bool> fOneThread(false);

src/wallet/walletdb.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,6 @@ class WalletBatch
275275
bool EraseActiveScriptPubKeyMan(uint8_t type, bool internal);
276276

277277
DBErrors LoadWallet(CWallet* pwallet);
278-
DBErrors FindWalletTxHashes(std::vector<uint256>& tx_hashes);
279-
DBErrors ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256>& vHashOut);
280278

281279
//! write the hdchain model (external chain child index counter)
282280
bool WriteHDChain(const CHDChain& chain);

test/functional/wallet_importprunedfunds.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def run_test(self):
120120
assert_equal(address_info['ismine'], True)
121121

122122
# Remove transactions
123-
assert_raises_rpc_error(-8, "Transaction does not exist in wallet.", w1.removeprunedfunds, txnid1)
123+
assert_raises_rpc_error(-4, f'Transaction {txnid1} does not belong to this wallet', w1.removeprunedfunds, txnid1)
124124
assert not [tx for tx in w1.listtransactions(include_watchonly=True) if tx['txid'] == txnid1]
125125

126126
wwatch.removeprunedfunds(txnid2)

0 commit comments

Comments
 (0)