Skip to content

Commit

Permalink
Merge bitcoin#28987: wallet: simplify and batch zap wallet txes process
Browse files Browse the repository at this point in the history
9a3c5c8 scripted-diff: rename ZapSelectTx to RemoveTxs (furszy)
83b7628 wallet: batch and simplify ZapSelectTx process (furszy)
595d50a wallet: migration, remove extra NotifyTransactionChanged call (furszy)
a2b071f wallet: ZapSelectTx, remove db rewrite code (furszy)

Pull request description:

  Work decoupled from bitcoin#28574. Brother of bitcoin#28894.

  Includes two different, yet interconnected, performance and code improvements to the zap wallet transactions process.

  1) As the goal of the `ZapSelectTx` function is to erase tx records that match any of the inputted hashes. There is no need to traverse the whole database record by record. We could just check if the tx exist, and remove it directly by calling `EraseTx()`.

  2) Instead of performing single write operations per removed tx record, this PR batches them all within a single atomic db txn.

  Moreover, these changes will enable us to consolidate all individual write operations that take place during the wallet migration process into a single db txn in the future.

ACKs for top commit:
  achow101:
    ACK 9a3c5c8
  josibake:
    ACK bitcoin@9a3c5c8

Tree-SHA512: fb2ecc48224c400ab3b1fbb32e174b5b13bf03794717727f80f01f55fb183883b067a68c0a127b2de8885564da15425d021a96541953bf38a72becc2e9929ccf
  • Loading branch information
achow101 committed Feb 12, 2024
2 parents c6398c6 + 9a3c5c8 commit 6ff0aa0
Show file tree
Hide file tree
Showing 7 changed files with 45 additions and 128 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->RemoveTxs(vHash); !res) {
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
}

return UniValue::VNULL;
Expand Down
6 changes: 3 additions & 3 deletions src/wallet/test/wallet_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@ BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup)
UnloadWallet(std::move(wallet));
}

BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
BOOST_FIXTURE_TEST_CASE(RemoveTxs, TestChain100Setup)
{
m_args.ForceSetArg("-unsafesqlitesync", "1");
WalletContext context;
Expand All @@ -913,8 +913,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->RemoveTxs(vHashIn));

BOOST_CHECK(!wallet->HasWalletSpend(prev_tx));
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u);
Expand Down
65 changes: 36 additions & 29 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2320,35 +2320,51 @@ DBErrors CWallet::LoadWallet()
return nLoadWalletRet;
}

DBErrors CWallet::ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256>& vHashOut)
util::Result<void> CWallet::RemoveTxs(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::NEED_REWRITE)
{
if (GetDatabase().Rewrite("\x04pool"))
{
for (const auto& spk_man_pair : m_spk_managers) {
spk_man_pair.second->RewriteDB();
}
}
}

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 @@ -4025,19 +4041,10 @@ 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");
if (auto res = RemoveTxs(txids_to_delete); !res) {
error = _("Error: Could not delete watchonly transactions. ") + util::ErrorString(res);
return false;
}
if (deleted_txids != txids_to_delete) {
error = _("Error: Not all watchonly txs could be deleted");
return false;
}
// Tell the GUI of each tx
for (const uint256& txid : deleted_txids) {
NotifyTransactionChanged(txid, CT_UPDATED);
}
}

// Pair external wallets with their corresponding db handler
Expand Down
4 changes: 3 additions & 1 deletion src/wallet/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,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> RemoveTxs(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 6ff0aa0

Please sign in to comment.