From 05d20afccd2f5e8605ee01c6bb1efc15dddf7398 Mon Sep 17 00:00:00 2001 From: chromatic Date: Wed, 4 May 2022 21:46:36 -0700 Subject: [PATCH] Add optional height to importprivkey RPC This allows users to avoid rescanning the entire chain when importing a new private key, if they provide the height of the block from which to start. Note that any transactions to or from the corresponding wallet will only be indexed if they occur at or after the given height. The height argument is `height`, consistent with the `height` argument to `rescan`. --- qa/rpc-tests/import-rescan.py | 27 ++++++++++++++++++++++++--- src/rpc/client.cpp | 1 + src/wallet/rpcdump.cpp | 25 ++++++++++++++++++++----- src/wallet/rpcwallet.cpp | 2 +- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/qa/rpc-tests/import-rescan.py b/qa/rpc-tests/import-rescan.py index 7f2c32174..92a671994 100755 --- a/qa/rpc-tests/import-rescan.py +++ b/qa/rpc-tests/import-rescan.py @@ -1,17 +1,18 @@ #!/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. """Test rescan behavior of importaddress, importpubkey, importprivkey, and importmulti RPCs with different types of keys and rescan options. In the first part of the test, node 1 creates an address for each type of -import RPC call and node 0 sends BTC to it. Then other nodes import the +import RPC call and node 0 sends Doge to it. Then other nodes import the addresses, and the test makes listtransactions and getbalance calls to confirm that the importing node either did or did not execute rescans picking up the send transactions. -In the second part of the test, node 0 sends more BTC to each address, and the +In the second part of the test, node 0 sends more Doge to each address, and the test makes more listtransactions and getbalance calls to confirm that the importing nodes pick up the new transactions regardless of whether rescans happened previously. @@ -114,6 +115,9 @@ class ImportRescanTest(BitcoinTestFramework): super().__init__() self.num_nodes = 2 + len(IMPORT_NODES) + def first_node(self): + return self.nodes[0] + def setup_network(self): extra_args = [["-debug=1"] for _ in range(self.num_nodes)] for i, import_node in enumerate(IMPORT_NODES, 2): @@ -125,8 +129,11 @@ class ImportRescanTest(BitcoinTestFramework): connect_nodes(self.nodes[i], 0) def run_test(self): + self.test_argument_validation() + # Create one transaction on node 0 with a unique amount and label for # each possible type of wallet import RPC. + for i, variant in enumerate(IMPORT_VARIANTS): variant.label = "label {} {}".format(i, variant) variant.address = self.nodes[1].validateaddress(self.nodes[1].getnewaddress(variant.label)) @@ -179,6 +186,21 @@ class ImportRescanTest(BitcoinTestFramework): else: variant.check() + def test_argument_validation(self): + node = self.first_node() + + try: + node.importprivkey("") + except JSONRPCException as e: + assert("Invalid private key encoding" in e.error["message"]) + + address = node.validateaddress(node.getnewaddress("some label")) + privkey = node.dumpprivkey(address["address"]) + + try: + node.importprivkey(privkey, "", True, str(node.getblockcount() + 1)) + except JSONRPCException as e: + assert("Block height out of range" in e.error["message"]) def try_rpc(func, *args, **kwargs): try: @@ -186,6 +208,5 @@ def try_rpc(func, *args, **kwargs): except JSONRPCException as e: return None, e.error - if __name__ == "__main__": ImportRescanTest().main() diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 376b3aa92..4d50ca053 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -103,6 +103,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "lockunspent", 0, "unlock" }, { "lockunspent", 1, "transactions" }, { "importprivkey", 2, "rescan" }, + { "importprivkey", 3, "height" }, { "importaddress", 2, "rescan" }, { "importaddress", 3, "p2sh" }, { "importpubkey", 2, "rescan" }, diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 0155518e8..6beb21a3a 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -83,8 +83,8 @@ UniValue importprivkey(const JSONRPCRequest& request) { if (!EnsureWalletIsAvailable(request.fHelp)) return NullUniValue; - - if (request.fHelp || request.params.size() < 1 || request.params.size() > 3) + + if (request.fHelp || request.params.size() < 1 || request.params.size() > 4) throw runtime_error( "importprivkey \"dogecoinprivkey\" ( \"label\" ) ( rescan )\n" "\nAdds a private key (as returned by dumpprivkey) to your wallet.\n" @@ -92,6 +92,7 @@ UniValue importprivkey(const JSONRPCRequest& request) "1. \"dogecoinprivkey\" (string, required) The private key (see dumpprivkey)\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" "\nDump a private key\n" @@ -102,6 +103,8 @@ UniValue importprivkey(const JSONRPCRequest& request) + HelpExampleCli("importprivkey", "\"mykey\" \"testing\" false") + "\nImport using default blank label and without rescan\n" + HelpExampleCli("importprivkey", "\"mykey\" \"\" false") + + "\nImport using default blank label, with rescan, from a specific block height\n" + + HelpExampleCli("importprivkey", "\"mykey\" \"\" true 3760036") + "\nAs a JSON-RPC call\n" + HelpExampleRpc("importprivkey", "\"mykey\", \"testing\", false") ); @@ -148,11 +151,23 @@ UniValue importprivkey(const JSONRPCRequest& request) if (!pwalletMain->AddKeyPubKey(key, pubkey)) throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet"); - // whenever a key is imported, we need to scan the whole chain - pwalletMain->UpdateTimeFirstKey(1); if (fRescan) { - pwalletMain->ScanForWalletTransactions(chainActive.Genesis(), true); + 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); } } diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 63e144ae7..03dfc4b78 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -3244,7 +3244,7 @@ static const CRPCCommand commands[] = { "wallet", "getunconfirmedbalance", &getunconfirmedbalance, false, {} }, { "wallet", "getwalletinfo", &getwalletinfo, false, {} }, { "wallet", "importmulti", &importmulti, true, {"requests","options"} }, - { "wallet", "importprivkey", &importprivkey, true, {"privkey","label","rescan"} }, + { "wallet", "importprivkey", &importprivkey, true, {"privkey","label","rescan", "height"} }, { "wallet", "importwallet", &importwallet, true, {"filename"} }, { "wallet", "importaddress", &importaddress, true, {"address","label","rescan","p2sh"} }, { "wallet", "importprunedfunds", &importprunedfunds, true, {"rawtransaction","txoutproof"} },