From 25b3e3222d04ee04079a237d6464b03534a6b531 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Thu, 15 Jun 2023 14:10:23 +0300 Subject: [PATCH 1/6] feat(rpc): add `wipewallettxes` --- src/rpc/client.cpp | 1 + src/wallet/rpcwallet.cpp | 49 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 4bcb47bec9bb..92cfc0320de0 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -197,6 +197,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "echojson", 9, "arg9" }, { "rescanblockchain", 0, "start_height"}, { "rescanblockchain", 1, "stop_height"}, + { "wipewallettxes", 0, "keep_confirmed"}, { "createwallet", 1, "disable_private_keys"}, { "createwallet", 2, "blank"}, { "createwallet", 4, "avoid_reuse"}, diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index d9e79b1e08d2..e2082b813041 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3550,6 +3550,54 @@ static UniValue rescanblockchain(const JSONRPCRequest& request) return response; } +static UniValue wipewallettxes(const JSONRPCRequest& request) +{ + RPCHelpMan{"wipewallettxes", + "\nWipe wallet transactions.\n" + "Note: Use \"rescanblockchain\" to initiate the scanning progress and recover wallet transactions.\n", + { + {"keep_confirmed", RPCArg::Type::BOOL, /* default */ "false", "Do not wipe confirmed transactions"}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("wipewallettxes", "") + + HelpExampleRpc("wipewallettxes", "") + }, + }.Check(request); + + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + CWallet* const pwallet = wallet.get(); + + WalletRescanReserver reserver(pwallet); + if (!reserver.reserve()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort rescan or wait."); + } + + LOCK(pwallet->cs_wallet); + + std::vector vHash; + std::vector vHashOut; + + bool keep_confirmed{false}; + if (!request.params[0].isNull()) { + keep_confirmed = request.params[0].get_bool(); + } + + for (auto& [txid, wtx] : pwallet->mapWallet) { + if (keep_confirmed && wtx.m_confirm.status == CWalletTx::CONFIRMED) continue; + vHash.push_back(txid); + } + + if (pwallet->ZapSelectTx(vHash, vHashOut) != DBErrors::LOAD_OK) { + throw JSONRPCError(RPC_WALLET_ERROR, "Could not properly delete transactions."); + } + + CHECK_NONFATAL(vHashOut.size() == vHash.size()); + + return NullUniValue; +} + class DescribeWalletAddressVisitor { public: @@ -4169,6 +4217,7 @@ static const CRPCCommand commands[] = { "wallet", "walletpassphrase", &walletpassphrase, {"passphrase","timeout","mixingonly"} }, { "wallet", "walletprocesspsbt", &walletprocesspsbt, {"psbt","sign","sighashtype","bip32derivs"} }, { "wallet", "walletcreatefundedpsbt", &walletcreatefundedpsbt, {"inputs","outputs","locktime","options","bip32derivs"} }, + { "wallet", "wipewallettxes", &wipewallettxes, {"keep_confirmed"} }, }; // clang-format on From 4c1c07ce7f9e09333253aea637fd69539218fcef Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Wed, 21 Jun 2023 02:39:13 +0300 Subject: [PATCH 2/6] test: add test for `wipewallettxes` rpc --- test/functional/rpc_wipewallettxes.py | 41 +++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 2 files changed, 42 insertions(+) create mode 100644 test/functional/rpc_wipewallettxes.py diff --git a/test/functional/rpc_wipewallettxes.py b/test/functional/rpc_wipewallettxes.py new file mode 100644 index 000000000000..ff1d252d4371 --- /dev/null +++ b/test/functional/rpc_wipewallettxes.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test transaction wiping using the wipewallettxes RPC.""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error + + +class WipeWalletTxesTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + self.log.info("Test that wipewallettxes removes txes and rescanblockchain is able to recover them") + self.nodes[0].generate(101) + txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) + self.nodes[0].generate(1) + assert_equal(self.nodes[0].getwalletinfo()["txcount"], 103) + self.nodes[0].wipewallettxes() + assert_equal(self.nodes[0].getwalletinfo()["txcount"], 0) + self.nodes[0].rescanblockchain() + assert_equal(self.nodes[0].getwalletinfo()["txcount"], 103) + + self.log.info("Test that wipewallettxes removes txes but keeps confirmed ones when asked to") + txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 1) + assert_equal(self.nodes[0].getwalletinfo()["txcount"], 104) + self.nodes[0].wipewallettxes(True) + assert_equal(self.nodes[0].getwalletinfo()["txcount"], 103) + self.nodes[0].rescanblockchain() + assert_equal(self.nodes[0].getwalletinfo()["txcount"], 103) + assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", self.nodes[0].gettransaction, txid) + + +if __name__ == '__main__': + WipeWalletTxesTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index c80f369d6f42..8bcb4622fe66 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -246,6 +246,7 @@ 'wallet_create_tx.py', 'p2p_fingerprint.py', 'rpc_platform_filter.py', + 'rpc_wipewallettxes.py', 'feature_dip0020_activation.py', 'feature_uacomment.py', 'wallet_coinbase_category.py', From d2f1a948ca4d4b6a1ae99b1fa2e008d6d721fdf8 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Tue, 20 Jun 2023 20:00:23 +0300 Subject: [PATCH 3/6] feat: add `wipetxes` command to dash-wallet --- src/bitcoin-wallet.cpp | 1 + src/wallet/wallettool.cpp | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/bitcoin-wallet.cpp b/src/bitcoin-wallet.cpp index 07f2a5b15fea..c2f87e15538e 100644 --- a/src/bitcoin-wallet.cpp +++ b/src/bitcoin-wallet.cpp @@ -31,6 +31,7 @@ static void SetupWalletToolArgs(ArgsManager& argsman) // Hidden argsman.AddArg("salvage", "Attempt to recover private keys from a corrupt wallet", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS); + argsman.AddArg("wipetxes", "Wipe all transactions from a wallet", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS); } static bool WalletAppInit(int argc, char* argv[]) diff --git a/src/wallet/wallettool.cpp b/src/wallet/wallettool.cpp index e574a2a827c8..de8b6d2be473 100644 --- a/src/wallet/wallettool.cpp +++ b/src/wallet/wallettool.cpp @@ -114,7 +114,7 @@ bool ExecuteWalletToolFunc(const std::string& command, const std::string& name) WalletShowInfo(wallet_instance.get()); wallet_instance->Close(); } - } else if (command == "info" || command == "salvage") { + } else if (command == "info" || command == "salvage" || command == "wipetxes") { if (command == "info") { std::shared_ptr wallet_instance = MakeWallet(name, path, /* create= */ false); if (!wallet_instance) return false; @@ -137,6 +137,32 @@ bool ExecuteWalletToolFunc(const std::string& command, const std::string& name) #else tfm::format(std::cerr, "Salvage command is not available as BDB support is not compiled"); return false; +#endif + } else if (command == "wipetxes") { +#ifdef USE_BDB + std::shared_ptr wallet_instance = MakeWallet(name, path, /* create= */ false); + if (wallet_instance == nullptr) return false; + + std::vector vHash; + std::vector vHashOut; + + LOCK(wallet_instance->cs_wallet); + + for (auto& [txid, _] : wallet_instance->mapWallet) { + vHash.push_back(txid); + } + + if (wallet_instance->ZapSelectTx(vHash, vHashOut) != DBErrors::LOAD_OK) { + tfm::format(std::cerr, "Could not properly delete transactions"); + wallet_instance->Close(); + return false; + } + + wallet_instance->Close(); + return vHashOut.size() == vHash.size(); +#else + tfm::format(std::cerr, "Wipetxes command is not available as BDB support is not compiled"); + return false; #endif } } else { From d793d5846a824d88b80dee1f1b4f8aff176a1e51 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Tue, 20 Jun 2023 20:00:42 +0300 Subject: [PATCH 4/6] test: add test for `wipetxes` command --- test/functional/tool_wallet.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py index 86ec0305b0d8..2f4ef5e9f536 100755 --- a/test/functional/tool_wallet.py +++ b/test/functional/tool_wallet.py @@ -228,6 +228,31 @@ def test_salvage(self): self.assert_tool_output('', '-wallet=salvage', 'salvage') + def test_wipe(self): + out = textwrap.dedent('''\ + Wallet info + =========== + Encrypted: no + HD (hd seed available): yes + Keypool Size: 2 + Transactions: 1 + Address Book: 1 + ''') + self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info') + + self.assert_tool_output('', '-wallet=' + self.default_wallet_name, 'wipetxes') + + out = textwrap.dedent('''\ + Wallet info + =========== + Encrypted: no + HD (hd seed available): yes + Keypool Size: 2 + Transactions: 0 + Address Book: 1 + ''') + self.assert_tool_output(out, '-wallet=' + self.default_wallet_name, 'info') + def run_test(self): self.wallet_path = os.path.join(self.nodes[0].datadir, self.chain, 'wallets', self.default_wallet_name, self.wallet_data_filename) self.test_invalid_tool_commands_and_args() @@ -238,6 +263,7 @@ def run_test(self): self.test_getwalletinfo_on_different_wallet() if self.is_bdb_compiled(): self.test_salvage() + self.test_wipe() if __name__ == '__main__': ToolWalletTest().main() From cdb153a7ffa6b974a3c64e44d7e2dacf682e367e Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Thu, 22 Jun 2023 13:31:09 +0300 Subject: [PATCH 5/6] feat(wallet): let WalletRescanReserver reset fAbortRescan --- src/wallet/wallet.cpp | 1 - src/wallet/wallet.h | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 922fde44c18a..416903d18853 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1918,7 +1918,6 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc WalletLogPrintf("Rescan started from block %s...\n", start_block.ToString()); - fAbortRescan = false; ShowProgress(strprintf("%s " + _("Rescanning...").translated, GetDisplayName()), 0); // show rescan progress in GUI as dialog or on splashscreen, if -rescan on startup uint256 tip_hash = WITH_LOCK(cs_wallet, return GetLastBlockHash()); uint256 end_hash = tip_hash; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index d2b30f138ab1..2f422006c086 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -668,7 +668,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati bool Unlock(const CKeyingMaterial& vMasterKeyIn, bool fForMixingOnly = false, bool accept_no_keys = false); - std::atomic fAbortRescan{false}; + std::atomic fAbortRescan{false}; // reset by WalletRescanReserver::reserve() std::atomic fScanningWallet{false}; // controlled by WalletRescanReserver std::atomic m_scanning_start{0}; std::atomic m_scanning_progress{0}; @@ -1384,6 +1384,7 @@ class WalletRescanReserver } m_wallet->m_scanning_start = GetTimeMillis(); m_wallet->m_scanning_progress = 0; + m_wallet->fAbortRescan = false; m_could_reserve = true; return true; } From feac8201728821826b280898ce622b2ce370b0e9 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Thu, 22 Jun 2023 13:33:09 +0300 Subject: [PATCH 6/6] feat(rpc): make `wipewallettxes` interactive in qt Also add logs in ZapSelectTx --- src/wallet/rpcwallet.cpp | 40 ++++++++++++++++++++++++++++++---------- src/wallet/wallet.cpp | 5 +++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index e2082b813041..729b674ae385 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3576,24 +3576,44 @@ static UniValue wipewallettxes(const JSONRPCRequest& request) LOCK(pwallet->cs_wallet); - std::vector vHash; - std::vector vHashOut; - bool keep_confirmed{false}; if (!request.params[0].isNull()) { keep_confirmed = request.params[0].get_bool(); } - for (auto& [txid, wtx] : pwallet->mapWallet) { - if (keep_confirmed && wtx.m_confirm.status == CWalletTx::CONFIRMED) continue; - vHash.push_back(txid); - } + const size_t WALLET_SIZE{pwallet->mapWallet.size()}; + const size_t STEPS{20}; + const size_t BATCH_SIZE = std::max(WALLET_SIZE / STEPS, size_t(1000)); + + pwallet->ShowProgress(strprintf("%s " + _("Wiping wallet transactions...").translated, pwallet->GetDisplayName()), 0); + + for (size_t progress = 0; progress < STEPS; ++progress) { + std::vector vHashIn; + std::vector vHashOut; + size_t count{0}; + + for (auto& [txid, wtx] : pwallet->mapWallet) { + if (progress < STEPS - 1 && ++count > BATCH_SIZE) break; + if (keep_confirmed && wtx.m_confirm.status == CWalletTx::CONFIRMED) continue; + vHashIn.push_back(txid); + } + + if (vHashIn.size() > 0 && pwallet->ZapSelectTx(vHashIn, vHashOut) != DBErrors::LOAD_OK) { + pwallet->ShowProgress(strprintf("%s " + _("Wiping wallet transactions...").translated, pwallet->GetDisplayName()), 100); + throw JSONRPCError(RPC_WALLET_ERROR, "Could not properly delete transactions."); + } + + CHECK_NONFATAL(vHashOut.size() == vHashIn.size()); + + if (pwallet->IsAbortingRescan() || pwallet->chain().shutdownRequested()) { + pwallet->ShowProgress(strprintf("%s " + _("Wiping wallet transactions...").translated, pwallet->GetDisplayName()), 100); + throw JSONRPCError(RPC_MISC_ERROR, "Wiping was aborted by user."); + } - if (pwallet->ZapSelectTx(vHash, vHashOut) != DBErrors::LOAD_OK) { - throw JSONRPCError(RPC_WALLET_ERROR, "Could not properly delete transactions."); + pwallet->ShowProgress(strprintf("%s " + _("Wiping wallet transactions...").translated, pwallet->GetDisplayName()), std::max(1, std::min(99, int(progress * 100 / STEPS)))); } - CHECK_NONFATAL(vHashOut.size() == vHash.size()); + pwallet->ShowProgress(strprintf("%s " + _("Wiping wallet transactions...").translated, pwallet->GetDisplayName()), 100); return NullUniValue; } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 416903d18853..b44caa01fe0c 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3875,6 +3875,9 @@ void CWallet::AutoLockMasternodeCollaterals() DBErrors CWallet::ZapSelectTx(std::vector& vHashIn, std::vector& vHashOut) { AssertLockHeld(cs_wallet); + + WalletLogPrintf("ZapSelectTx started for %d transactions...\n", vHashIn.size()); + DBErrors nZapSelectTxRet = WalletBatch(*database).ZapSelectTx(vHashIn, vHashOut); for (uint256 hash : vHashOut) { const auto& it = mapWallet.find(hash); @@ -3898,6 +3901,8 @@ DBErrors CWallet::ZapSelectTx(std::vector& vHashIn, std::vector