diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index 8d40b23b6..fa00c8245 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -169,6 +169,7 @@ testScripts = [ 'listsinceblock.py', 'p2p-leaktests.py', 'replace-by-fee.py', + 'rescan.py', 'wallet_create_tx.py', 'liststucktransactions.py', 'addnode.py', diff --git a/qa/rpc-tests/rescan.py b/qa/rpc-tests/rescan.py new file mode 100644 index 000000000..c75232d36 --- /dev/null +++ b/qa/rpc-tests/rescan.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2016 The Bitcoin Core developers +# Copyright (c) 2022 The Dogecoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + + +class RescanTest(BitcoinTestFramework): + + def __init__(self): + super().__init__() + self.setup_clean_chain = True + self.num_nodes = 3 + + # only sync the first two nodes; use the third for prune testing + def sync_all(self): + syncable = self.nodes[:2] + sync_blocks(syncable) + sync_mempools(syncable) + + def setup_network(self, split=False): + self.nodes = start_nodes(self.num_nodes - 1, self.options.tmpdir, [['-spendzeroconfchange=0'], None]) + self.nodes.append(start_node(2, self.options.tmpdir, ["-prune=1"])) + connect_nodes_bi(self.nodes,0,1) + self.is_network_split=False + self.sync_all() + + def run_test(self): + print("Mining blocks...") + self.nodes[0].generate(101) + + self.sync_all() + + # address + address1 = self.nodes[0].getnewaddress() + # pubkey + address2 = self.nodes[0].getnewaddress() + address2_pubkey = self.nodes[0].validateaddress(address2)['pubkey'] # Using pubkey + # privkey + address3 = self.nodes[0].getnewaddress() + address3_privkey = self.nodes[0].dumpprivkey(address3) # Using privkey + + self.sync_all() + + # Node 1 sync test + assert_equal(self.nodes[1].getblockcount(), 101) + + # Send funds to self + txnid1 = self.nodes[0].sendtoaddress(address1, 10) + rawtxn1 = self.nodes[0].gettransaction(txnid1)['hex'] + + txnid2 = self.nodes[0].sendtoaddress(address2, 5) + rawtxn2 = self.nodes[0].gettransaction(txnid2)['hex'] + + txnid3 = self.nodes[0].sendtoaddress(address3, 2.5) + rawtxn3 = self.nodes[0].gettransaction(txnid3)['hex'] + + self.nodes[0].generate(1) + + # Import with affiliated address with no rescan + self.nodes[1].importaddress(address2, "add2", False) + balance2 = self.nodes[1].getbalance("add2", 0, True) + assert_equal(balance2, Decimal('0')) + + self.nodes[1].rescan() + balance2 = self.nodes[1].getbalance("add2", 0, True) + assert_equal(balance2, Decimal('5')) + + # Import with private key with no rescan + self.nodes[1].importprivkey(address3_privkey, "add3", False) + + # add more blocks + self.nodes[1].generate(102) + balance4 = self.nodes[1].getbalance("add3", 0, False) + assert_equal(balance4, Decimal('0')) + self.nodes[1].rescan(200) + balance4 = self.nodes[1].getbalance("add3", 0, False) + assert_equal(balance4, Decimal('0')) + result = self.nodes[1].rescan(2) + balance4 = self.nodes[1].getbalance("add3", 0, True) + assert_equal(balance4, Decimal('2.5')) + + assert_equal(result["before"], { + "balance": Decimal('21000000'), + "txcount": 103 + }) + + assert_equal(result["after"], { + "balance": Decimal('21000002.5'), + "txcount": 104 + }) + + assert_equal(result["blocks_scanned"], 202) + assert("time_elapsed" in result) + + # 2100000 from mining, 7.5 otherwise + # note that 5 are from a watch-only address + balance4 = self.nodes[1].getbalance("*", 0, True) + assert_equal(balance4, Decimal('21000007.5')) + + try: + self.nodes[1].rescan(-100) + raise AssertionError("rescan should throw JSON exception given a negative block height") + except JSONRPCException as e: + assert("Block height out of range" in e.error["message"]) + + try: + currentheight = self.nodes[1].getblockcount() + self.nodes[1].rescan(currentheight + 1 ) + raise AssertionError("rescan should throw JSON exception given a block height too high") + except JSONRPCException as e: + assert("Block height out of range" in e.error["message"]) + + try: + pruned_node = self.nodes[2] + pruned_node.rescan(1) + raise AssertionError("rescan should throw JSON error when used on a pruned node") + except JSONRPCException as e: + assert("Currently works only on non-pruned nodes" in e.error["message"]) + + + +if __name__ == '__main__': + RescanTest().main() diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 8389703b9..5c20880c6 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -125,6 +125,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "getmempooldescendants", 1, "verbose" }, { "bumpfee", 1, "options" }, { "setmaxconnections", 0, "maxconnectioncount" }, + { "rescan", 0, "height" }, // Echo with conversion (For testing only) { "echojson", 0, "arg0" }, { "echojson", 1, "arg1" }, diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index b2fbc6534..c0e701c3c 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -807,6 +807,83 @@ UniValue movecmd(const JSONRPCRequest& request) return true; } +UniValue rescan(const JSONRPCRequest& request) +{ + if (!EnsureWalletIsAvailable(request.fHelp)) + return NullUniValue; + + const int nParams = request.params.size(); + + if (request.fHelp || nParams > 1 || fPruneMode) + throw runtime_error( + "rescan ( \"height\" )\n" + "\nRescan the wallet for transactions\n" + "\nWARNING: this operation may take a long time!\n" + "\nCurrently works only on non-pruned nodes\n" + "\nArguments:\n" + "1. \"height\" (number, optional) The block height from which to start rescanning\n" + "2. \"label\" (string, optional, default=\"\") An optional label\n" + "3. rescan (boolean, optional, default=true) Rescan the wallet for transactions\n" + "\nResult:\n" + "{\n" + " \"before\":\n" + " {\n" + " \"balance\" : (numeric) The total amount in " + CURRENCY_UNIT + " received by the address before the rescan\n" + " \"txcount\" : (numeric) The number of transactions received by addresses in the wallet before the rescan\n" + " },\n" + " \"after\":\n" + " {\n" + " \"balance\" : (numeric) The total amount in " + CURRENCY_UNIT + " received by the address after the rescan\n" + " \"txcount\" : (numeric) The number of transactions received by addresses in the wallet after the rescan\n" + " },\n" + " \"blocks_scanned\" : (numeric) The number of blocks scanned during the rescan\n" + " \"time_elapsed\" : (numeric) The number of seconds it took to rescan the blocks (may be zero)\n" + "}\n" + "\nNote: This call can take minutes to complete.\n" + "\nExamples:\n" + "\nRescan from block height 122345\n" + + HelpExampleCli("rescan", "122345") + + "\nRescan from the first block\n" + + HelpExampleCli("rescan", "") + + "\nAs a JSON-RPC call\n" + + HelpExampleRpc("rescan", "122345") + ); + + + CBlockIndex* pblockindex = chainActive.Genesis(); + int64_t nHeight = 0; + + if (nParams == 1) { + nHeight = request.params[0].get_int(); + + if (nHeight < 0 || nHeight > chainActive.Height()) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Block height out of range"); + + pblockindex = chainActive[nHeight]; + } + + UniValue beforeObj(UniValue::VOBJ); + beforeObj.pushKV("balance", ValueFromAmount(pwalletMain->GetBalance())); + beforeObj.pushKV("txcount", (int)pwalletMain->mapWallet.size()); + + int64_t beforeTime = GetTime(); + + pwalletMain->ScanForWalletTransactions(pblockindex, true); + + UniValue afterObj(UniValue::VOBJ); + afterObj.pushKV("balance", ValueFromAmount(pwalletMain->GetBalance())); + afterObj.pushKV("txcount", (int)pwalletMain->mapWallet.size()); + + UniValue ret(UniValue::VOBJ); + ret.pushKV("before", beforeObj); + ret.pushKV("after", afterObj); + + ret.pushKV("blocks_scanned", chainActive.Height() - nHeight); + ret.pushKV("time_elapsed", GetTime() - beforeTime); + + return ret; +} + UniValue sendfrom(const JSONRPCRequest& request) { @@ -3183,6 +3260,7 @@ static const CRPCCommand commands[] = { "wallet", "listunspent", &listunspent, false, {"minconf","maxconf","addresses","include_unsafe","query_options"} }, { "wallet", "lockunspent", &lockunspent, true, {"unlock","transactions"} }, { "wallet", "move", &movecmd, false, {"fromaccount","toaccount","amount","minconf","comment"} }, + { "wallet", "rescan", &rescan, false, {"height"} }, { "wallet", "sendfrom", &sendfrom, false, {"fromaccount","toaddress","amount","minconf","comment","comment_to"} }, { "wallet", "sendmany", &sendmany, false, {"fromaccount","amounts","minconf","comment","subtractfeefrom"} }, { "wallet", "sendtoaddress", &sendtoaddress, false, {"address","amount","comment","comment_to","subtractfeefromamount"} },