Skip to content

Commit

Permalink
wallet: batch and simplify ZapSelectTx process
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
furszy committed Feb 9, 2024
1 parent 595d50a commit 83b7628
Show file tree
Hide file tree
Showing 7 changed files with 44 additions and 113 deletions.
10 changes: 2 additions & 8 deletions src/wallet/rpc/backup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,14 +394,8 @@ RPCHelpMan removeprunedfunds()
uint256 hash(ParseHashV(request.params[0], "txid"));
std::vector<uint256> vHash;
vHash.push_back(hash);
std::vector<uint256> vHashOut;

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.");
if (auto res = pwallet->ZapSelectTx(vHash); !res) {
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
}

return UniValue::VNULL;
Expand Down
4 changes: 2 additions & 2 deletions src/wallet/test/wallet_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -918,8 +918,8 @@ BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
BOOST_CHECK(wallet->HasWalletSpend(prev_tx));
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u);

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

BOOST_CHECK(!wallet->HasWalletSpend(prev_tx));
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u);
Expand Down
51 changes: 36 additions & 15 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2311,25 +2311,51 @@ DBErrors CWallet::LoadWallet()
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);
DBErrors nZapSelectTxRet = WalletBatch(GetDatabase()).ZapSelectTx(vHashIn, vHashOut);
for (const uint256& hash : vHashOut) {
const auto& it = mapWallet.find(hash);
WalletBatch batch(GetDatabase());
if (!batch.TxnBegin()) return util::Error{_("Error starting db txn for wallet transactions removal")};

// 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);
for (const auto& txin : it->second.tx->vin)
mapTxSpends.erase(txin.prevout);
mapWallet.erase(it);
NotifyTransactionChanged(hash, CT_DELETED);
}

if (nZapSelectTxRet != DBErrors::LOAD_OK)
return nZapSelectTxRet;

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)
Expand Down Expand Up @@ -3925,13 +3951,8 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
watchonly_batch.reset(); // Flush
// Do the removes
if (txids_to_delete.size() > 0) {
std::vector<uint256> deleted_txids;
if (ZapSelectTx(txids_to_delete, deleted_txids) != DBErrors::LOAD_OK) {
error = _("Error: Could not delete watchonly transactions");
return false;
}
if (deleted_txids != txids_to_delete) {
error = _("Error: Not all watchonly txs could be deleted");
if (auto res = ZapSelectTx(txids_to_delete); !res) {
error = _("Error: Could not delete watchonly transactions. ") + util::ErrorString(res);
return false;
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/wallet/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
void chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) override;

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);

Expand Down
84 changes: 0 additions & 84 deletions src/wallet/walletdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1230,90 +1230,6 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
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)
{
static std::atomic<bool> fOneThread(false);
Expand Down
2 changes: 0 additions & 2 deletions src/wallet/walletdb.h
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,6 @@ class WalletBatch
bool EraseActiveScriptPubKeyMan(uint8_t type, bool internal);

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)
bool WriteHDChain(const CHDChain& chain);
Expand Down
2 changes: 1 addition & 1 deletion test/functional/wallet_importprunedfunds.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def run_test(self):
assert_equal(address_info['ismine'], True)

# 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]

wwatch.removeprunedfunds(txnid2)
Expand Down

0 comments on commit 83b7628

Please sign in to comment.