diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 8758195cff4..dbfd849fb8c 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -190,7 +190,6 @@ static const CRPCConvertParam vRPCConvertParams[] = { "sendmany", 8 , "output_assets" }, { "sendmany", 9 , "ignoreblindfail" }, { "sendtoaddress", 9 , "ignoreblindfail" }, - { "importblindingkey", 2, "key_is_master"}, { "createrawtransaction", 4, "output_assets" }, }; diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index e785f6c8853..f679717a31f 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -764,7 +764,7 @@ UniValue dumpwallet(const JSONRPCRequest& request) } // ELEMENTS: Dump the master blinding key in hex as well if (!pwallet->blinding_derivation_key.IsNull()) { - file << ("# Master private blinding key: " + pwallet->blinding_derivation_key.GetHex() + "\n\n"); + file << ("# Master private blinding key: " + HexStr(pwallet->blinding_derivation_key) + "\n\n"); } for (std::vector >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) { const CKeyID &keyid = it->second; @@ -1343,14 +1343,13 @@ UniValue importblindingkey(const JSONRPCRequest& request) return NullUniValue; } - if (request.fHelp || request.params.size() < 2 || request.params.size() > 3) + if (request.fHelp || request.params.size() != 2) throw std::runtime_error( "importblindingkey \"address\" \"blindinghex\"\n" "\nImports a private blinding key in hex for a CT address." "\nArguments:\n" "1. \"address\" (string, required) The CT address\n" "2. \"hexkey\" (string, required) The blinding key in hex\n" - "3. \"key_is_master\" (bool, optional, default=false) If the `hexkey` is a master blinding key. Note: wallets can only have one master blinding key at a time. Funds could be permanently lost if user doesn't know what they are doing. Recommended use is only for wallet recovery using this in conjunction with `sethdseed`.\n" "\nExample:\n" + HelpExampleCli("importblindingkey", "\"my blinded CT address\" ") ); @@ -1373,11 +1372,6 @@ UniValue importblindingkey(const JSONRPCRequest& request) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid hexadecimal key length"); } - bool key_is_master = false; - if (!request.params[2].isNull()) { - key_is_master = request.params[2].get_bool(); - } - CKey key; key.Set(keydata.begin(), keydata.end(), true); if (!key.IsValid() || key.GetPubKey() != GetDestinationBlindingKey(dest)) { @@ -1386,20 +1380,56 @@ UniValue importblindingkey(const JSONRPCRequest& request) uint256 keyval; memcpy(keyval.begin(), &keydata[0], 32); - if (key_is_master) { - if (pwallet->SetMasterBlindingKey(keyval)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Failed to import master blinding key"); - } - } else { - if (!pwallet->AddSpecificBlindingKey(CScriptID(GetScriptForDestination(dest)), keyval)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Failed to import blinding key"); - } + if (!pwallet->AddSpecificBlindingKey(CScriptID(GetScriptForDestination(dest)), keyval)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to import blinding key"); } pwallet->MarkDirty(); return NullUniValue; } +UniValue importmasterblindingkey(const JSONRPCRequest& request) +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + CWallet* const pwallet = wallet.get(); + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() != 1) + throw std::runtime_error( + "importblindingkey \"address\" \"blindinghex\"\n" + "\nImports a master private blinding key in hex for a CT address." + "Note: wallets can only have one master blinding key at a time. Funds could be permanently lost if user doesn't know what they are doing. Recommended use is only for wallet recovery using this in conjunction with `sethdseed`.\n" + "\nArguments:\n" + "1. \"hexkey\" (string, required) The blinding key in hex\n" + "\nExample:\n" + + HelpExampleCli("importmasterblindingkey", "") + ); + + LOCK2(cs_main, pwallet->cs_wallet); + + if (!IsHex(request.params[0].get_str())) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid hexadecimal for key"); + } + std::vector keydata = ParseHex(request.params[0].get_str()); + if (keydata.size() != 32) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid hexadecimal key length"); + } + + uint256 keyval; + memcpy(keyval.begin(), &keydata[0], 32); + + if (!pwallet->SetMasterBlindingKey(keyval)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to import master blinding key"); + } + + pwallet->MarkDirty(); + + return NullUniValue; +} + UniValue importissuanceblindingkey(const JSONRPCRequest& request) { std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); @@ -1519,6 +1549,34 @@ UniValue dumpblindingkey(const JSONRPCRequest& request) throw JSONRPCError(RPC_WALLET_ERROR, "Blinding key for address is unknown"); } +UniValue dumpmasterblindingkey(const JSONRPCRequest& request) +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + CWallet* const pwallet = wallet.get(); + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() != 0) + throw std::runtime_error( + "dumpmasterblindingkey\n" + "\nDumps the master private blinding key in hex." + "\nResult:\n" + "\"blindingkey\" (string) The master blinding key\n" + "\nExample:\n" + + HelpExampleCli("dumpmasterblindingkey", "") + ); + + LOCK2(cs_main, pwallet->cs_wallet); + + if (!pwallet->blinding_derivation_key.IsNull()) { + return HexStr(pwallet->blinding_derivation_key); + } else { + throw JSONRPCError(RPC_WALLET_ERROR, "Master blinding key is uninitialized."); + } +} + UniValue dumpissuanceblindingkey(const JSONRPCRequest& request) { std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 3e2572c91cb..8e375e21869 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -6199,8 +6199,10 @@ UniValue getpegoutkeys(const JSONRPCRequest& request) UniValue abortrescan(const JSONRPCRequest& request); // in rpcdump.cpp UniValue dumpprivkey(const JSONRPCRequest& request); // in rpcdump.cpp UniValue importblindingkey(const JSONRPCRequest& request); // in rpcdump.cpp +UniValue importmasterblindingkey(const JSONRPCRequest& request); // in rpcdump.cpp UniValue importissuanceblindingkey(const JSONRPCRequest& request); // in rpcdump.cpp UniValue dumpblindingkey(const JSONRPCRequest& request); // in rpcdump.cpp +UniValue dumpmasterblindingkey(const JSONRPCRequest& request); // in rpcdump.cpp UniValue dumpissuanceblindingkey(const JSONRPCRequest& request); // in rpcdump.cpp UniValue importprivkey(const JSONRPCRequest& request); UniValue importaddress(const JSONRPCRequest& request); @@ -6281,9 +6283,11 @@ static const CRPCCommand commands[] = { "wallet", "sendtomainchain", &sendtomainchain, {"address", "amount", "subtractfeefromamount"} }, { "wallet", "initpegoutwallet", &initpegoutwallet, {"bitcoin_descriptor", "bip32_counter", "liquid_pak"} }, { "wallet", "getwalletpakinfo", &getwalletpakinfo, {} }, - { "wallet", "importblindingkey", &importblindingkey, {"address", "hexkey", "key_is_master"}}, + { "wallet", "importblindingkey", &importblindingkey, {"address", "hexkey"}}, + { "wallet", "importmasterblindingkey", &importmasterblindingkey, {"hexkey"}}, { "wallet", "importissuanceblindingkey", &importissuanceblindingkey, {"txid", "vin", "blindingkey"}}, { "wallet", "dumpblindingkey", &dumpblindingkey, {"address"}}, + { "wallet", "dumpmasterblindingkey", &dumpmasterblindingkey, {}}, { "wallet", "dumpissuanceblindingkey", &dumpissuanceblindingkey, {"txid", "vin"}}, { "wallet", "signblock", &signblock, {"blockhex"}}, { "wallet", "listissuances", &listissuances, {"asset"}}, diff --git a/test/functional/feature_confidential_transactions.py b/test/functional/feature_confidential_transactions.py index b51cafe2971..5e76b75576a 100755 --- a/test/functional/feature_confidential_transactions.py +++ b/test/functional/feature_confidential_transactions.py @@ -7,6 +7,8 @@ from test_framework.util import connect_nodes_bi, assert_equal from test_framework.authproxy import JSONRPCException from decimal import Decimal +import os +import re class CTTest (BitcoinTestFramework): @@ -28,8 +30,63 @@ def setup_network(self, split=False): def skip_test_if_missing_module(self): self.skip_if_no_wallet() + def test_wallet_recovery(self): + file_path = "/tmp/blind_details" + try: + os.remove(file_path) + except OSError: + pass + + # Wallet recovery requires more than just seed, but also master blinding key + # which currently is not derived from seed, see + # https://github.com/ElementsProject/elements/pull/232 + blind_addr = self.nodes[0].getnewaddress() + + self.nodes[0].dumpwallet(file_path) + + found_seed = False + found_blind = False + with open(file_path, encoding="utf8") as f: + for line in f: + if "hdseed=1" in line: + split = re.split(" ", line) + found_seed = split[0] + split = re.split("Master private blinding key: ", line) + if len(split) == 2: + assert_equal(len(split[1].rstrip()), 64) + found_blind = split[1].rstrip() + + assert_equal(found_blind, self.nodes[0].dumpmasterblindingkey()) + + # Create new wallet + self.nodes[0].createwallet("recover") + rec = self.nodes[0].get_wallet_rpc("recover") + wrong_info = rec.getaddressinfo(blind_addr) + assert("pubkey" not in wrong_info) + assert_equal(wrong_info["ismine"], False) + + # Setting seed should get us more info, still not "ours" until blinding key + rec.generatetoaddress(1, rec.getnewaddress()) # get out of IBD + rec.sethdseed(True, found_seed) + + wrong_blind_info = rec.getaddressinfo(blind_addr) + assert("pubkey" in wrong_blind_info) + assert_equal(wrong_blind_info["ismine"], False) + + # Now import master blinding key + rec.importmasterblindingkey(found_blind) + assert_equal(rec.dumpmasterblindingkey(), found_blind) + blind_info = rec.getaddressinfo(blind_addr) + assert("pubkey" in blind_info) + assert_equal(blind_info["ismine"], True) + assert_equal(rec.getaddressinfo(blind_info["unconfidential"])["confidential"], blind_addr) + self.nodes[0].unloadwallet("recover") + def run_test(self): + print("Testing wallet secret recovery") + self.test_wallet_recovery() + print("General Confidential tests") # Running balances node0 = self.nodes[0].getbalance()["bitcoin"]