mirror of
https://github.com/dashpay/dash.git
synced 2024-12-25 12:02:48 +01:00
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:
parent
49e024338a
commit
ff60d10934
@ -31,6 +31,7 @@ static void SetupWalletToolArgs(ArgsManager& argsman)
|
|||||||
|
|
||||||
// Hidden
|
// Hidden
|
||||||
argsman.AddArg("salvage", "Attempt to recover private keys from a corrupt wallet", ArgsManager::ALLOW_ANY, OptionsCategory::COMMANDS);
|
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[])
|
static bool WalletAppInit(int argc, char* argv[])
|
||||||
|
@ -196,6 +196,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||||||
{ "echojson", 9, "arg9" },
|
{ "echojson", 9, "arg9" },
|
||||||
{ "rescanblockchain", 0, "start_height"},
|
{ "rescanblockchain", 0, "start_height"},
|
||||||
{ "rescanblockchain", 1, "stop_height"},
|
{ "rescanblockchain", 1, "stop_height"},
|
||||||
|
{ "wipewallettxes", 0, "keep_confirmed"},
|
||||||
{ "createwallet", 1, "disable_private_keys"},
|
{ "createwallet", 1, "disable_private_keys"},
|
||||||
{ "createwallet", 2, "blank"},
|
{ "createwallet", 2, "blank"},
|
||||||
{ "createwallet", 4, "avoid_reuse"},
|
{ "createwallet", 4, "avoid_reuse"},
|
||||||
|
@ -3564,6 +3564,74 @@ static UniValue rescanblockchain(const JSONRPCRequest& request)
|
|||||||
return response;
|
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
|
class DescribeWalletAddressVisitor
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -4166,6 +4234,7 @@ static const CRPCCommand commands[] =
|
|||||||
{ "wallet", "walletpassphrase", &walletpassphrase, {"passphrase","timeout","mixingonly"} },
|
{ "wallet", "walletpassphrase", &walletpassphrase, {"passphrase","timeout","mixingonly"} },
|
||||||
{ "wallet", "walletprocesspsbt", &walletprocesspsbt, {"psbt","sign","sighashtype","bip32derivs"} },
|
{ "wallet", "walletprocesspsbt", &walletprocesspsbt, {"psbt","sign","sighashtype","bip32derivs"} },
|
||||||
{ "wallet", "walletcreatefundedpsbt", &walletcreatefundedpsbt, {"inputs","outputs","locktime","options","bip32derivs"} },
|
{ "wallet", "walletcreatefundedpsbt", &walletcreatefundedpsbt, {"inputs","outputs","locktime","options","bip32derivs"} },
|
||||||
|
{ "wallet", "wipewallettxes", &wipewallettxes, {"keep_confirmed"} },
|
||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
|
@ -1884,7 +1884,6 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc
|
|||||||
|
|
||||||
WalletLogPrintf("Rescan started from block %s...\n", start_block.ToString());
|
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
|
ShowProgress(strprintf("%s " + _("Rescanning...").translated, GetDisplayName()), 0); // show rescan progress in GUI as dialog or on splashscreen, if -rescan on startup
|
||||||
uint256 tip_hash;
|
uint256 tip_hash;
|
||||||
// The way the 'block_height' is initialized is just a workaround for the gcc bug #47679 since version 4.6.0.
|
// 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)
|
DBErrors CWallet::ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256>& vHashOut)
|
||||||
{
|
{
|
||||||
AssertLockHeld(cs_wallet);
|
AssertLockHeld(cs_wallet);
|
||||||
|
|
||||||
|
WalletLogPrintf("ZapSelectTx started for %d transactions...\n", vHashIn.size());
|
||||||
|
|
||||||
DBErrors nZapSelectTxRet = WalletBatch(*database).ZapSelectTx(vHashIn, vHashOut);
|
DBErrors nZapSelectTxRet = WalletBatch(*database).ZapSelectTx(vHashIn, vHashOut);
|
||||||
for (uint256 hash : vHashOut) {
|
for (uint256 hash : vHashOut) {
|
||||||
const auto& it = mapWallet.find(hash);
|
const auto& it = mapWallet.find(hash);
|
||||||
@ -3690,6 +3692,8 @@ DBErrors CWallet::ZapSelectTx(std::vector<uint256>& vHashIn, std::vector<uint256
|
|||||||
|
|
||||||
MarkDirty();
|
MarkDirty();
|
||||||
|
|
||||||
|
WalletLogPrintf("ZapSelectTx completed for %d transactions.\n", vHashOut.size());
|
||||||
|
|
||||||
return DBErrors::LOAD_OK;
|
return DBErrors::LOAD_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -654,7 +654,7 @@ private:
|
|||||||
|
|
||||||
bool Unlock(const CKeyingMaterial& vMasterKeyIn, bool fForMixingOnly = false, bool accept_no_keys = false);
|
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<bool> fScanningWallet{false}; // controlled by WalletRescanReserver
|
||||||
std::atomic<int64_t> m_scanning_start{0};
|
std::atomic<int64_t> m_scanning_start{0};
|
||||||
std::atomic<double> m_scanning_progress{0};
|
std::atomic<double> m_scanning_progress{0};
|
||||||
@ -1306,6 +1306,7 @@ public:
|
|||||||
}
|
}
|
||||||
m_wallet->m_scanning_start = GetTimeMillis();
|
m_wallet->m_scanning_start = GetTimeMillis();
|
||||||
m_wallet->m_scanning_progress = 0;
|
m_wallet->m_scanning_progress = 0;
|
||||||
|
m_wallet->fAbortRescan = false;
|
||||||
m_could_reserve = true;
|
m_could_reserve = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ bool ExecuteWalletToolFunc(const std::string& command, const std::string& name)
|
|||||||
WalletShowInfo(wallet_instance.get());
|
WalletShowInfo(wallet_instance.get());
|
||||||
wallet_instance->Close();
|
wallet_instance->Close();
|
||||||
}
|
}
|
||||||
} else if (command == "info" || command == "salvage") {
|
} else if (command == "info" || command == "salvage" || command == "wipetxes") {
|
||||||
if (command == "info") {
|
if (command == "info") {
|
||||||
std::shared_ptr<CWallet> wallet_instance = MakeWallet(name, path, /* create= */ false);
|
std::shared_ptr<CWallet> wallet_instance = MakeWallet(name, path, /* create= */ false);
|
||||||
if (!wallet_instance) return false;
|
if (!wallet_instance) return false;
|
||||||
@ -135,6 +135,32 @@ bool ExecuteWalletToolFunc(const std::string& command, const std::string& name)
|
|||||||
#else
|
#else
|
||||||
tfm::format(std::cerr, "Salvage command is not available as BDB support is not compiled");
|
tfm::format(std::cerr, "Salvage command is not available as BDB support is not compiled");
|
||||||
return false;
|
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
|
#endif
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
41
test/functional/rpc_wipewallettxes.py
Normal file
41
test/functional/rpc_wipewallettxes.py
Normal 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()
|
@ -239,6 +239,7 @@ BASE_SCRIPTS = [
|
|||||||
'wallet_create_tx.py',
|
'wallet_create_tx.py',
|
||||||
'p2p_fingerprint.py',
|
'p2p_fingerprint.py',
|
||||||
'rpc_platform_filter.py',
|
'rpc_platform_filter.py',
|
||||||
|
'rpc_wipewallettxes.py',
|
||||||
'feature_dip0020_activation.py',
|
'feature_dip0020_activation.py',
|
||||||
'feature_uacomment.py',
|
'feature_uacomment.py',
|
||||||
'wallet_coinbase_category.py',
|
'wallet_coinbase_category.py',
|
||||||
|
@ -228,6 +228,31 @@ class ToolWalletTest(BitcoinTestFramework):
|
|||||||
|
|
||||||
self.assert_tool_output('', '-wallet=salvage', 'salvage')
|
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):
|
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.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()
|
self.test_invalid_tool_commands_and_args()
|
||||||
@ -238,6 +263,7 @@ class ToolWalletTest(BitcoinTestFramework):
|
|||||||
self.test_getwalletinfo_on_different_wallet()
|
self.test_getwalletinfo_on_different_wallet()
|
||||||
if self.is_bdb_compiled():
|
if self.is_bdb_compiled():
|
||||||
self.test_salvage()
|
self.test_salvage()
|
||||||
|
self.test_wipe()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
ToolWalletTest().main()
|
ToolWalletTest().main()
|
||||||
|
Loading…
Reference in New Issue
Block a user