diff --git a/qa/rpc-tests/import-rescan.py b/qa/rpc-tests/import-rescan.py index 92a671994..5b1dc982a 100755 --- a/qa/rpc-tests/import-rescan.py +++ b/qa/rpc-tests/import-rescan.py @@ -20,7 +20,7 @@ happened previously. from test_framework.authproxy import JSONRPCException from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import (start_nodes, connect_nodes, sync_blocks, assert_equal, set_node_times) +from test_framework.util import (start_nodes, connect_nodes, sync_blocks, assert_equal, set_node_times, sync_mempools) from decimal import Decimal import collections @@ -127,10 +127,19 @@ class ImportRescanTest(BitcoinTestFramework): self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, extra_args) for i in range(1, self.num_nodes): connect_nodes(self.nodes[i], 0) + self.sync_recipient_nodes() + + def sync_recipient_nodes(self): + syncable = self.nodes[:2] + sync_blocks(syncable) + sync_mempools(syncable) def run_test(self): self.test_argument_validation() + self.test_import_types() + self.test_rescan_from_height() + def test_import_types(self): # Create one transaction on node 0 with a unique amount and label for # each possible type of wallet import RPC. @@ -202,6 +211,54 @@ class ImportRescanTest(BitcoinTestFramework): except JSONRPCException as e: assert("Block height out of range" in e.error["message"]) + try: + node.importpubkey("") + except JSONRPCException as e: + assert("Pubkey must be a hex string" in e.error["message"]) + + try: + node.importpubkey("abcdef") + except JSONRPCException as e: + assert("Pubkey is not a valid public key" in e.error["message"]) + + try: + address = self.nodes[1].getnewaddress() + pubkey = self.nodes[1].validateaddress(address)["pubkey"] + node.importpubkey(pubkey, "", True, node.getblockcount() + 1) + except JSONRPCException as e: + assert("Block height out of range" in e.error["message"]) + + def test_rescan_from_height(self): + # this height is before sending anything to the new address + orig_height = self.nodes[0].getblockcount() + + address = self.nodes[0].getnewaddress() + pubkey = self.nodes[0].validateaddress(address)["pubkey"] + self.nodes[0].sendtoaddress(address, 100) + + # generate two blocks + # the first contains the tx that sends these koinu + # the second is after it + self.nodes[0].generate(2) + new_height = self.nodes[0].getblockcount() + + self.sync_recipient_nodes() + + # no rescan, no funds seen for this pubkey + self.nodes[1].importpubkey(pubkey, "newpubkey", False) + balance = self.nodes[1].getbalance("newpubkey", 0, True) + assert_equal(balance, Decimal("0")) + + # rescan at the block *after* the tx, no funds seen for this pubkey + self.nodes[2].importpubkey(pubkey, "newpubkey", True, new_height) + balance = self.nodes[2].getbalance("newpubkey", 0, True) + assert_equal(balance, Decimal("0")) + + # rescan at the block *before* the tx, funds seen for this pubkey + self.nodes[3].importpubkey(pubkey, "newpubkey", True, orig_height) + balance = self.nodes[3].getbalance("newpubkey", 0, True) + assert_equal(balance, Decimal("100")) + def try_rpc(func, *args, **kwargs): try: return func(*args, **kwargs), None diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 4d50ca053..bdb29c2d0 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -107,6 +107,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "importaddress", 2, "rescan" }, { "importaddress", 3, "p2sh" }, { "importpubkey", 2, "rescan" }, + { "importpubkey", 3, "height" }, { "importmulti", 0, "requests" }, { "importmulti", 1, "options" }, { "verifychain", 0, "checklevel" }, diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 6beb21a3a..7ea0a0f91 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -35,6 +35,8 @@ using namespace std; void EnsureWalletIsUnlocked(); bool EnsureWalletIsAvailable(bool avoidException); +uint32_t getHeightParamFromRequest(const JSONRPCRequest& request, size_t pos); +void attemptRescanFromHeight(uint32_t nHeight); std::string static EncodeDumpTime(int64_t nTime) { return DateTimeStrFormat("%Y-%m-%dT%H:%M:%SZ", nTime); @@ -151,29 +153,42 @@ UniValue importprivkey(const JSONRPCRequest& request) if (!pwalletMain->AddKeyPubKey(key, pubkey)) throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet"); - if (fRescan) { - CBlockIndex* pblockindex = chainActive.Genesis(); - - if (request.params.size() > 3) { - int nHeight = request.params[3].get_int(); - - if (nHeight < 0 || nHeight > chainActive.Height()) - throw JSONRPCError(RPC_INVALID_PARAMETER, "Block height out of range"); - - pblockindex = chainActive[nHeight]; - } else { - // we have no implicit first height for a key, so we need to scan the whole chain - pwalletMain->UpdateTimeFirstKey(1); - } - - pwalletMain->ScanForWalletTransactions(pblockindex, true); + const uint32_t nHeight = getHeightParamFromRequest(request, 3); + attemptRescanFromHeight(nHeight); } } return NullUniValue; } +uint32_t getHeightParamFromRequest(const JSONRPCRequest& request, const size_t pos) +{ + if (request.params.size() <= pos) + return 1; + + const int nHeight = request.params[pos].get_int(); + + if (nHeight < 0 || nHeight > chainActive.Height()) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Block height out of range"); + + return nHeight; +} + +void attemptRescanFromHeight(const uint32_t nHeight) +{ + CBlockIndex* pblockindex = chainActive.Genesis(); + + // we have no implicit first height for a key, so we need to scan the whole chain + if (nHeight <= 1) + pwalletMain->UpdateTimeFirstKey(1); + else + pblockindex = chainActive[nHeight]; + + pwalletMain->ScanForWalletTransactions(pblockindex, true); + pwalletMain->ReacceptWalletTransactions(); +} + void ImportAddress(const CBitcoinAddress& address, const string& strLabel); void ImportScript(const CScript& script, const string& strLabel, bool isRedeemScript) { @@ -382,10 +397,13 @@ UniValue importpubkey(const JSONRPCRequest& request) "1. \"pubkey\" (string, required) The hex-encoded public key\n" "2. \"label\" (string, optional, default=\"\") An optional label\n" "3. rescan (boolean, optional, default=true) Rescan the wallet for transactions\n" + "4. height (numeric, optional, default=1) If rescanning, the block height from which to start\n" "\nNote: This call can take minutes to complete if rescan is true.\n" "\nExamples:\n" "\nImport a public key with rescan\n" + HelpExampleCli("importpubkey", "\"mypubkey\"") + + "\nImport a public key with rescan from a specific height\n" + + HelpExampleCli("importpubkey", "\"mypubkey\" true 123654") + "\nImport using a label without rescan\n" + HelpExampleCli("importpubkey", "\"mypubkey\" \"testing\" false") + "\nAs a JSON-RPC call\n" @@ -417,10 +435,9 @@ UniValue importpubkey(const JSONRPCRequest& request) ImportAddress(CBitcoinAddress(pubKey.GetID()), strLabel); ImportScript(GetScriptForRawPubKey(pubKey), strLabel, false); - if (fRescan) - { - pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true); - pwalletMain->ReacceptWalletTransactions(); + if (fRescan) { + const uint32_t nHeight = getHeightParamFromRequest(request, 3); + attemptRescanFromHeight(nHeight); } return NullUniValue; diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 03dfc4b78..4403a8376 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3248,7 +3248,7 @@ static const CRPCCommand commands[] = { "wallet", "importwallet", &importwallet, true, {"filename"} }, { "wallet", "importaddress", &importaddress, true, {"address","label","rescan","p2sh"} }, { "wallet", "importprunedfunds", &importprunedfunds, true, {"rawtransaction","txoutproof"} }, - { "wallet", "importpubkey", &importpubkey, true, {"pubkey","label","rescan"} }, + { "wallet", "importpubkey", &importpubkey, true, {"pubkey","label","rescan", "height"} }, { "wallet", "keypoolrefill", &keypoolrefill, true, {"newsize"} }, { "wallet", "listaccounts", &listaccounts, false, {"minconf","include_watchonly"} }, { "wallet", "listaddressgroupings", &listaddressgroupings, false, {} },