diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -103,7 +103,7 @@ } { LOCK(cs_main); - wallet.ScanForWalletTransactions(chainActive.Genesis(), true); + wallet.ScanForWalletTransactions(chainActive.Genesis(), nullptr, true); } wallet.SetBroadcastTransactions(true); diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -123,6 +123,8 @@ {"echojson", 7, "arg7"}, {"echojson", 8, "arg8"}, {"echojson", 9, "arg9"}, + {"rescanblockchain", 0, "start_height"}, + {"rescanblockchain", 1, "stop_height"}, }; class CRPCConvertTable { diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3473,6 +3473,89 @@ true); } +UniValue rescanblockchain(const Config &config, const JSONRPCRequest &request) { + CWallet *const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() > 2) { + throw std::runtime_error( + "rescanblockchain (\"start_height\") (\"stop_height\")\n" + "\nRescan the local blockchain for wallet related transactions.\n" + "\nArguments:\n" + "1. \"start_height\" (numeric, optional) block height where the " + "rescan should start\n" + "2. \"stop_height\" (numeric, optional) the last block height " + "that should be scanned\n" + "\nResult:\n" + "{\n" + " \"start_height\" (numeric) The block height where the " + "rescan has started. If omitted, rescan started from the genesis " + "block.\n" + " \"stop_height\" (numeric) The height of the last rescanned " + "block. If omitted, rescan stopped at the chain tip.\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("rescanblockchain", "100000 120000") + + HelpExampleRpc("rescanblockchain", "100000 120000")); + } + + LOCK2(cs_main, pwallet->cs_wallet); + + CBlockIndex *pindexStart = chainActive.Genesis(); + CBlockIndex *pindexStop = nullptr; + if (!request.params[0].isNull()) { + pindexStart = chainActive[request.params[0].get_int()]; + if (!pindexStart) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid start_height"); + } + } + + if (!request.params[1].isNull()) { + pindexStop = chainActive[request.params[1].get_int()]; + if (!pindexStop) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid stop_height"); + } else if (pindexStop->nHeight < pindexStart->nHeight) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "stop_height must be greater then start_height"); + } + } + + // We can't rescan beyond non-pruned blocks, stop and throw an error + if (fPruneMode) { + CBlockIndex *block = pindexStop ? pindexStop : chainActive.Tip(); + while (block && block->nHeight >= pindexStart->nHeight) { + if (!block->nStatus.hasData()) { + throw JSONRPCError(RPC_MISC_ERROR, + "Can't rescan beyond pruned data. Use RPC " + "call getblockchaininfo to determine your " + "pruned height."); + } + block = block->pprev; + } + } + + CBlockIndex *stopBlock = + pwallet->ScanForWalletTransactions(pindexStart, pindexStop, true); + if (!stopBlock) { + if (pwallet->IsAbortingRescan()) { + throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted."); + } + // if we got a nullptr returned, ScanForWalletTransactions did rescan up + // to the requested stopindex + stopBlock = pindexStop ? pindexStop : chainActive.Tip(); + } else { + throw JSONRPCError(RPC_MISC_ERROR, + "Rescan failed. Potentially corrupted data files."); + } + + UniValue response(UniValue::VOBJ); + response.pushKV("start_height", pindexStart->nHeight); + response.pushKV("stop_height", stopBlock->nHeight); + return response; +} + // clang-format off static const ContextFreeRPCCommand commands[] = { // category name actor (function) okSafeMode @@ -3506,6 +3589,7 @@ { "wallet", "listwallets", listwallets, true, {} }, { "wallet", "lockunspent", lockunspent, true, {"unlock","transactions"} }, { "wallet", "move", movecmd, false, {"fromaccount","toaccount","amount","minconf","comment"} }, + { "wallet", "rescanblockchain", rescanblockchain, false, {"start_height", "stop_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"} }, diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -469,7 +469,8 @@ CWallet wallet(Params()); LOCK(wallet.cs_wallet); wallet.AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey()); - BOOST_CHECK_EQUAL(nullBlock, wallet.ScanForWalletTransactions(oldTip)); + BOOST_CHECK_EQUAL(nullBlock, + wallet.ScanForWalletTransactions(oldTip, nullptr)); BOOST_CHECK_EQUAL(wallet.GetImmatureBalance(), 100 * COIN); } @@ -483,7 +484,8 @@ CWallet wallet(Params()); LOCK(wallet.cs_wallet); wallet.AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey()); - BOOST_CHECK_EQUAL(oldTip, wallet.ScanForWalletTransactions(oldTip)); + BOOST_CHECK_EQUAL(oldTip, + wallet.ScanForWalletTransactions(oldTip, nullptr)); BOOST_CHECK_EQUAL(wallet.GetImmatureBalance(), 50 * COIN); } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -898,6 +898,7 @@ bool fUpdate); int64_t RescanFromTime(int64_t startTime, bool update); CBlockIndex *ScanForWalletTransactions(CBlockIndex *pindexStart, + CBlockIndex *pindexStop, bool fUpdate = false); void ReacceptWalletTransactions(); void ResendWalletTransactions(int64_t nBestBlockTime, diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1721,7 +1721,7 @@ if (startBlock) { const CBlockIndex *const failedBlock = - ScanForWalletTransactions(startBlock, update); + ScanForWalletTransactions(startBlock, nullptr, update); if (failedBlock) { return failedBlock->GetBlockTimeMax() + TIMESTAMP_WINDOW + 1; } @@ -1737,11 +1737,19 @@ * Returns null if scan was successful. Otherwise, if a complete rescan was not * possible (due to pruning or corruption), returns pointer to the most recent * block that could not be scanned. + * + * If pindexStop is not a nullptr, the scan will stop at the block-index defined + * by pindexStop. */ CBlockIndex *CWallet::ScanForWalletTransactions(CBlockIndex *pindexStart, + CBlockIndex *pindexStop, bool fUpdate) { int64_t nNow = GetTime(); + if (pindexStop) { + assert(pindexStop->nHeight >= pindexStart->nHeight); + } + CBlockIndex *pindex = pindexStart; CBlockIndex *ret = nullptr; @@ -1778,6 +1786,9 @@ } else { ret = pindex; } + if (pindex == pindexStop) { + break; + } pindex = chainActive.Next(pindex); if (GetTime() >= nNow + 60) { @@ -4298,7 +4309,7 @@ } nStart = GetTimeMillis(); - walletInstance->ScanForWalletTransactions(pindexRescan, true); + walletInstance->ScanForWalletTransactions(pindexRescan, nullptr, true); LogPrintf(" rescan %15dms\n", GetTimeMillis() - nStart); walletInstance->SetBestChain(chainActive.GetLocator()); walletInstance->dbw->IncrementUpdateCounter(); diff --git a/test/functional/timing.json b/test/functional/timing.json --- a/test/functional/timing.json +++ b/test/functional/timing.json @@ -3,29 +3,33 @@ "name": "abandonconflict.py", "time": 17 }, + { + "name": "abc-checkdatasig-activation.py", + "time": 16 + }, { "name": "abc-cmdline.py", "time": 8 }, { "name": "abc-high_priority_transaction.py", - "time": 10 + "time": 22 }, { "name": "abc-magnetic-anomaly-activation.py", - "time": 15 + "time": 10 }, { "name": "abc-mempool-accept-txn.py", - "time": 5 + "time": 4 }, { "name": "abc-p2p-compactblocks.py", - "time": 176 + "time": 184 }, { "name": "abc-p2p-fullblocktest.py", - "time": 51 + "time": 65 }, { "name": "abc-replay-protection.py", @@ -33,11 +37,11 @@ }, { "name": "abc-rpc.py", - "time": 2 + "time": 15 }, { "name": "abc-transaction-ordering.py", - "time": 13 + "time": 9 }, { "name": "assumevalid.py", @@ -45,35 +49,35 @@ }, { "name": "bip65-cltv-p2p.py", - "time": 7 + "time": 5 }, { "name": "bip68-112-113-p2p.py", - "time": 22 + "time": 21 }, { "name": "bip68-sequence.py", - "time": 40 + "time": 37 }, { "name": "bipdersig-p2p.py", - "time": 18 + "time": 5 }, { "name": "bitcoin_cli.py", - "time": 3 + "time": 14 }, { "name": "blockchain.py", - "time": 19 + "time": 9 }, { "name": "dbcrash.py", - "time": 1183 + "time": 1161 }, { "name": "decodescript.py", - "time": 4 + "time": 15 }, { "name": "disablewallet.py", @@ -81,19 +85,19 @@ }, { "name": "disconnect_ban.py", - "time": 24 + "time": 10 }, { "name": "example_test.py", - "time": 4 + "time": 14 }, { "name": "forknotify.py", - "time": 4 + "time": 20 }, { "name": "fundrawtransaction.py", - "time": 43 + "time": 50 }, { "name": "getblocktemplate_longpoll.py", @@ -101,27 +105,27 @@ }, { "name": "getchaintips.py", - "time": 7 + "time": 22 }, { "name": "httpbasics.py", - "time": 6 + "time": 19 }, { "name": "import-rescan.py", - "time": 20 + "time": 44 }, { "name": "importmulti.py", - "time": 14 + "time": 24 }, { "name": "importprunedfunds.py", - "time": 4 + "time": 3 }, { "name": "invalidateblock.py", - "time": 21 + "time": 8 }, { "name": "invalidblockrequest.py", @@ -133,35 +137,35 @@ }, { "name": "keypool-topup.py", - "time": 23 + "time": 45 }, { "name": "keypool.py", - "time": 11 + "time": 9 }, { "name": "listsinceblock.py", - "time": 5 + "time": 4 }, { "name": "listtransactions.py", - "time": 12 + "time": 8 }, { "name": "maxuploadtarget.py", - "time": 52 + "time": 49 }, { "name": "mempool_limit.py", - "time": 8 + "time": 35 }, { "name": "mempool_packages.py", - "time": 30 + "time": 62 }, { "name": "mempool_persist.py", - "time": 27 + "time": 26 }, { "name": "mempool_reorg.py", @@ -169,35 +173,35 @@ }, { "name": "mempool_resurrect_test.py", - "time": 3 + "time": 4 }, { "name": "mempool_spendcoinbase.py", - "time": 2 + "time": 3 }, { "name": "merkle_blocks.py", - "time": 5 + "time": 4 }, { "name": "minchainwork.py", - "time": 6 + "time": 16 }, { "name": "mining.py", - "time": 3 + "time": 4 }, { "name": "multi_rpc.py", - "time": 5 + "time": 6 }, { "name": "multiwallet.py", - "time": 7 + "time": 21 }, { "name": "net.py", - "time": 5 + "time": 3 }, { "name": "nulldummy.py", @@ -205,23 +209,23 @@ }, { "name": "p2p-acceptblock.py", - "time": 8 + "time": 7 }, { "name": "p2p-compactblocks.py", - "time": 27 + "time": 31 }, { "name": "p2p-feefilter.py", - "time": 31 + "time": 24 }, { "name": "p2p-fullblocktest.py", - "time": 140 + "time": 175 }, { "name": "p2p-leaktests.py", - "time": 8 + "time": 9 }, { "name": "p2p-mempool.py", @@ -229,7 +233,7 @@ }, { "name": "p2p-timeouts.py", - "time": 64 + "time": 66 }, { "name": "preciousblock.py", @@ -237,7 +241,7 @@ }, { "name": "prioritise_transaction.py", - "time": 22 + "time": 9 }, { "name": "proxy_test.py", @@ -245,23 +249,23 @@ }, { "name": "pruning.py", - "time": 1623 + "time": 1590 }, { "name": "rawtransactions.py", - "time": 15 + "time": 26 }, { "name": "receivedby.py", - "time": 21 + "time": 24 }, { "name": "reindex.py", - "time": 14 + "time": 25 }, { "name": "resendwallettransactions.py", - "time": 18 + "time": 7 }, { "name": "rest.py", @@ -269,7 +273,7 @@ }, { "name": "rpcbind_test.py", - "time": 28 + "time": 27 }, { "name": "rpcnamedargs.py", @@ -277,27 +281,27 @@ }, { "name": "sendheaders.py", - "time": 29 + "time": 30 }, { "name": "signmessages.py", - "time": 2 + "time": 3 }, { "name": "signrawtransactions.py", - "time": 2 + "time": 3 }, { "name": "txn_clone.py", - "time": 6 + "time": 7 }, { "name": "txn_clone.py --mineblock", - "time": 6 + "time": 17 }, { "name": "txn_doublespend.py", - "time": 20 + "time": 7 }, { "name": "txn_doublespend.py --mineblock", @@ -309,34 +313,34 @@ }, { "name": "wallet-accounts.py", - "time": 15 + "time": 22 }, { "name": "wallet-dump.py", - "time": 8 + "time": 7 }, { "name": "wallet-encryption.py", - "time": 24 + "time": 9 }, { "name": "wallet-hd.py", - "time": 82 + "time": 85 }, { "name": "wallet.py", - "time": 43 + "time": 47 }, { "name": "walletbackup.py", - "time": 139 + "time": 116 }, { "name": "zapwallettxes.py", - "time": 12 + "time": 34 }, { "name": "zmq_test.py", - "time": 4 + "time": 5 } ] \ No newline at end of file diff --git a/test/functional/wallet-hd.py b/test/functional/wallet-hd.py --- a/test/functional/wallet-hd.py +++ b/test/functional/wallet-hd.py @@ -10,6 +10,7 @@ connect_nodes_bi, ) import shutil +import os class WalletHDTest(BitcoinTestFramework): @@ -74,10 +75,10 @@ self.stop_node(1) # we need to delete the complete regtest directory # otherwise node1 would auto-recover all funds in flag the keypool keys as used - shutil.rmtree(tmpdir + "/node1/regtest/blocks") - shutil.rmtree(tmpdir + "/node1/regtest/chainstate") - shutil.copyfile(tmpdir + "/hd.bak", tmpdir + - "/node1/regtest/wallet.dat") + shutil.rmtree(os.path.join(tmpdir, "node1/regtest/blocks")) + shutil.rmtree(os.path.join(tmpdir, "node1/regtest/chainstate")) + shutil.copyfile(os.path.join(tmpdir, "hd.bak"), + os.path.join(tmpdir, "node1/regtest/wallet.dat")) self.start_node(1) # Assert that derivation is deterministic @@ -96,6 +97,29 @@ self.start_node(1, extra_args=self.extra_args[1] + ['-rescan']) assert_equal(self.nodes[1].getbalance(), num_hd_adds + 1) + # Try a RPC based rescan + self.stop_node(1) + shutil.rmtree(os.path.join(tmpdir, "node1/regtest/blocks")) + shutil.rmtree(os.path.join(tmpdir, "node1/regtest/chainstate")) + shutil.copyfile(os.path.join(tmpdir, "hd.bak"), + os.path.join(tmpdir, "node1/regtest/wallet.dat")) + self.start_node(1, extra_args=self.extra_args[1]) + connect_nodes_bi(self.nodes, 0, 1) + self.sync_all() + out = self.nodes[1].rescanblockchain(0, 1) + assert_equal(out['start_height'], 0) + assert_equal(out['stop_height'], 1) + out = self.nodes[1].rescanblockchain(2, 4) + assert_equal(out['start_height'], 2) + assert_equal(out['stop_height'], 4) + out = self.nodes[1].rescanblockchain(3) + assert_equal(out['start_height'], 3) + assert_equal(out['stop_height'], self.nodes[1].getblockcount()) + out = self.nodes[1].rescanblockchain() + assert_equal(out['start_height'], 0) + assert_equal(out['stop_height'], self.nodes[1].getblockcount()) + assert_equal(self.nodes[1].getbalance(), num_hd_adds + 1) + # send a tx and make sure its using the internal chain for the changeoutput txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1) outs = self.nodes[1].decoderawtransaction(