diff --git a/src/bitcoin-wallet.cpp b/src/bitcoin-wallet.cpp index fcdf22d329..c891a3cbe5 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/rpc/client.cpp b/src/rpc/client.cpp index 4fb93be805..1bbff08c87 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -196,6 +196,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 992963f47c..9bcb468b07 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3564,6 +3564,74 @@ 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); + + bool keep_confirmed{false}; + if (!request.params[0].isNull()) { + keep_confirmed = request.params[0].get_bool(); + } + + 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."); + } + + pwallet->ShowProgress(strprintf("%s " + _("Wiping wallet transactions...").translated, pwallet->GetDisplayName()), std::max(1, std::min(99, int(progress * 100 / STEPS)))); + } + + pwallet->ShowProgress(strprintf("%s " + _("Wiping wallet transactions...").translated, pwallet->GetDisplayName()), 100); + + return NullUniValue; +} + class DescribeWalletAddressVisitor { public: @@ -4166,6 +4234,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 diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index d4cb379eda..0d15f78d5b 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1884,7 +1884,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; // The way the 'block_height' is initialized is just a workaround for the gcc bug #47679 since version 4.6.0. @@ -3667,6 +3666,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); @@ -3690,6 +3692,8 @@ DBErrors CWallet::ZapSelectTx(std::vector& vHashIn, std::vector 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}; @@ -1306,6 +1306,7 @@ public: } m_wallet->m_scanning_start = GetTimeMillis(); m_wallet->m_scanning_progress = 0; + m_wallet->fAbortRescan = false; m_could_reserve = true; return true; } diff --git a/src/wallet/wallettool.cpp b/src/wallet/wallettool.cpp index 8044aad7e0..c51a217a2c 100644 --- a/src/wallet/wallettool.cpp +++ b/src/wallet/wallettool.cpp @@ -112,7 +112,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; @@ -135,6 +135,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 { diff --git a/test/functional/rpc_wipewallettxes.py b/test/functional/rpc_wipewallettxes.py new file mode 100644 index 0000000000..ff1d252d43 --- /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 f2fac09566..485fb03cbf 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -239,6 +239,7 @@ BASE_SCRIPTS = [ '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', diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py index 86ec0305b0..2f4ef5e9f5 100755 --- a/test/functional/tool_wallet.py +++ b/test/functional/tool_wallet.py @@ -228,6 +228,31 @@ class ToolWalletTest(BitcoinTestFramework): 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 @@ class ToolWalletTest(BitcoinTestFramework): self.test_getwalletinfo_on_different_wallet() if self.is_bdb_compiled(): self.test_salvage() + self.test_wipe() if __name__ == '__main__': ToolWalletTest().main()