feat: introduce wipewallettxes RPC and wipetxes command for dash-wallet tool (#5451)

## Issue being fixed or feature implemented
Given the hard fork that happened on testnet, there is now lots of the
transactions that were made on the fork that is no longer valid. Some
transactions could be relayed and mined again but some like coinjoin
mixing won't be relayed because of 0 fee and transactions spending
coinbases from the forked branch are no longer valid at all.

## What was done?
Introduce `wipewallettxes` RPC and `wipetxes` command for `dash-wallet`
tool to be able to get rid of some/all txes in the wallet.

## How Has This Been Tested?
run tests, use rpc/command on testnet wallet

## Breaking Changes
n/a

## Checklist:
- [x] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have added or updated relevant unit/integration/functional/e2e
tests
- [ ] I have made corresponding changes to the documentation
- [x] I have assigned this pull request to a milestone _(for repository
code-owners and collaborators only)_
This commit is contained in:
UdjinM6 2023-06-27 21:51:40 +03:00
parent 49e024338a
commit ff60d10934
No known key found for this signature in database
GPG Key ID: 83592BD1400D58D9
9 changed files with 173 additions and 3 deletions

View File

@ -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[])

View File

@ -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"},

View File

@ -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<CWallet> 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<uint256> vHashIn;
std::vector<uint256> 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

View File

@ -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<uint256>& vHashIn, std::vector<uint256>& 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<uint256>& vHashIn, std::vector<uint256
MarkDirty();
WalletLogPrintf("ZapSelectTx completed for %d transactions.\n", vHashOut.size());
return DBErrors::LOAD_OK;
}

View File

@ -654,7 +654,7 @@ private:
bool Unlock(const CKeyingMaterial& vMasterKeyIn, bool fForMixingOnly = false, bool accept_no_keys = false);
std::atomic<bool> fAbortRescan{false};
std::atomic<bool> fAbortRescan{false}; // reset by WalletRescanReserver::reserve()
std::atomic<bool> fScanningWallet{false}; // controlled by WalletRescanReserver
std::atomic<int64_t> m_scanning_start{0};
std::atomic<double> 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;
}

View File

@ -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<CWallet> 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<CWallet> wallet_instance = MakeWallet(name, path, /* create= */ false);
if (wallet_instance == nullptr) return false;
std::vector<uint256> vHash;
std::vector<uint256> 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 {

View File

@ -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()

View File

@ -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',

View File

@ -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()