diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index 59bd0d7776b6b6..9b775742a2ee34 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -502,71 +502,145 @@ static RPCHelpMan unloadwallet() static RPCHelpMan sethdseed() { return RPCHelpMan{"sethdseed", - "\nSet or generate a new HD wallet seed. Non-HD wallets will not be upgraded to being a HD wallet. Wallets that are already\n" - "HD will have a new HD seed set so that new keys added to the keypool will be derived from this new seed.\n" - "\nNote that you will need to MAKE A NEW BACKUP of your wallet after setting the HD wallet seed." + HELP_REQUIRING_PASSPHRASE + - "Note: This command is only compatible with legacy wallets.\n", - { - {"newkeypool", RPCArg::Type::BOOL, RPCArg::Default{true}, "Whether to flush old unused addresses, including change addresses, from the keypool and regenerate it.\n" - "If true, the next address from getnewaddress and change address from getrawchangeaddress will be from this new seed.\n" - "If false, addresses (including change addresses if the wallet already had HD Chain Split enabled) from the existing\n" - "keypool will be used until it has been depleted."}, - {"seed", RPCArg::Type::STR, RPCArg::DefaultHint{"random seed"}, "The WIF private key to use as the new HD seed.\n" - "The seed value can be retrieved using the dumpwallet command. It is the private key marked hdseed=1"}, - }, - RPCResult{RPCResult::Type::NONE, "", ""}, - RPCExamples{ - HelpExampleCli("sethdseed", "") + "Set or generate a new HD wallet seed or key.\n" + "\nLegacy wallets can only have a seed set. Non-HD Legacy wallets will not be upgraded to being a HD wallet." + "Legacy wallets that are already HD will have a new HD seed set so that new keys added to the keypool will be derived from this new seed.\n" + "\nDescriptor wallets can have either a HD seed or a HD key set. The seed or key will only be used for new automatically generated descriptors that are created when newkeypool is true, and with `createwalletdescriptor`.\n" + "For descriptor wallets, this is a backwards incompatible operation - your wallet will no longer be able to be loaded in older versions" + "\nNote that you will need to MAKE A NEW BACKUP of your wallet after setting the HD wallet seed." + + HELP_REQUIRING_PASSPHRASE, + { + {"newkeypool", RPCArg::Type::BOOL, RPCArg::Default{true}, "Whether to flush old unused addresses, including change addresses, from the keypool and regenerate it. For descriptor wallets, this will generate completely new receiving and change descriptors for all address types.\n" + "If true, the next address from getnewaddress and change address from getrawchangeaddress will be from this new seed.\n" + "If false, addresses (including change addresses if the wallet already had HD Chain Split enabled) from the existing\n" + "keypool will be used until it has been depleted (for Legacy wallets), or until the active descriptors are replaced (for Descriptor wallets)."}, + {"seed", RPCArg::Type::STR, RPCArg::DefaultHint{"random seed"}, "The BIP 32 HD seed encoded as a WIF private key or hex string. Descriptor wallets can also accept a BIP 32 extended private key (xprv) to set as the wallet's HD key.\n" + "For Legacy wallets, the seed value can be retrieved using the dumpwallet command. It is the private key marked hdseed=1"}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("sethdseed", "") + HelpExampleCli("sethdseed", "false") + HelpExampleCli("sethdseed", "true \"wifkey\"") + HelpExampleRpc("sethdseed", "true, \"wifkey\"") - }, + }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return UniValue::VNULL; + { + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; - LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true); + if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a HD seed to a wallet with private keys disabled"); + } - if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a HD seed to a wallet with private keys disabled"); - } + LOCK(pwallet->cs_wallet); - LOCK2(pwallet->cs_wallet, spk_man.cs_KeyStore); + // Do not do anything to non-HD wallets + if (!pwallet->CanSupportFeature(FEATURE_HD)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set an HD seed on a non-HD wallet. Use the upgradewallet RPC in order to upgrade a non-HD wallet to HD"); + } - // Do not do anything to non-HD wallets - if (!pwallet->CanSupportFeature(FEATURE_HD)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set an HD seed on a non-HD wallet. Use the upgradewallet RPC in order to upgrade a non-HD wallet to HD"); - } + EnsureWalletIsUnlocked(*pwallet); - EnsureWalletIsUnlocked(*pwallet); + bool flush_key_pool = true; + if (!request.params[0].isNull()) { + flush_key_pool = request.params[0].get_bool(); + } - bool flush_key_pool = true; - if (!request.params[0].isNull()) { - flush_key_pool = request.params[0].get_bool(); - } + std::string seed_str; + std::optional> seed_bytes; + if (!request.params[1].isNull()) { + seed_str = request.params[1].get_str(); + seed_bytes.emplace(); + // First try decoding as WIF + CKey seed = DecodeSecret(seed_str); + if (seed.IsValid()) { + seed_bytes->assign(seed.begin(), seed.end()); + } else { + // Next try decoding as hex + if (IsHex(seed_str)) { + seed_bytes = ParseHex(seed_str); + } + } + } - CPubKey master_pub_key; - if (request.params[1].isNull()) { - master_pub_key = spk_man.GenerateNewSeed(); - } else { - CKey key = DecodeSecret(request.params[1].get_str()); - if (!key.IsValid()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key"); - } + if (pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { + if (pwallet->GetActiveHDPubKey() != std::nullopt) { + throw JSONRPCError(RPC_WALLET_ERROR, "The wallet already has an active HD key set. Create a new wallet if you want to rotate your keys."); + } + CExtKey master_key; + if (seed_bytes.has_value()) { + if (!seed_bytes->empty()) { + master_key.SetSeed(MakeByteSpan(seed_bytes.value())); + } else { + // Try decoding as xprv + master_key = DecodeExtKey(seed_str); + if (!master_key.key.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Key must be a BIP 32 extended private key, or the BIP 32 seed as a WIF private key or a hex string."); + } + } + } else { + // Generate a new key if none was specified + CKey seed_key; + seed_key.MakeNewKey(true); + CPubKey seed = seed_key.GetPubKey(); + CHECK_NONFATAL(seed_key.VerifyPubKey(seed)); + master_key.SetSeed(seed_key); + } - if (HaveKey(spk_man, key)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Already have this key (either as an HD seed or as a loose private key)"); - } + // Set the HAS_HDKEY_RECORDS flag + // This is a backwards incompatible upgrade + pwallet->SetWalletFlag(WALLET_FLAG_HAS_HDKEY_RECORDS); + + // Write the key + ok = pwallet->SetActiveHDKey(master_key); + CHECK_NONFATAL(ok); + + if (flush_key_pool) { + std::optional active_xpub = pwallet->GetActiveHDPubKey(); + CHECK_NONFATAL(active_xpub.has_value()); + std::optional active_hd_key = pwallet->GetActiveHDPrivKey(); + CHECK_NONFATAL(active_hd_key.has_value()); + + // Generate new descriptors + for (bool internal : {false, true}) { + for (OutputType t : OUTPUT_TYPES) { + WalletDescriptor w_desc = DescriptorScriptPubKeyMan::GenerateWalletDescriptor(active_hd_key.value(), t, internal); + uint256 w_id = DescriptorID(*w_desc.descriptor); + if (pwallet->GetScriptPubKeyMan(w_id)) { + // Skip this descriptor as it already exists, don't try generating a new DescSPKM + continue; + } + pwallet->SetupDescriptorScriptPubKeyMan(*active_hd_key, t, internal); + } + } + } + } else { + LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true); - master_pub_key = spk_man.DeriveNewSeed(key); - } + LOCK(spk_man.cs_KeyStore); + + CPubKey master_pub_key; + if (seed_bytes.has_value()) { + CKey seed; + seed.Set(seed_bytes->begin(), seed_bytes->end(), true); + if (!seed.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key, seed must either be a WIF private key or hex string representing 32 bytes"); + } + if (HaveKey(spk_man, seed)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Already have this key (either as an HD seed or as a loose private key)"); + } + master_pub_key = spk_man.DeriveNewSeed(seed); + } else { + master_pub_key = spk_man.GenerateNewSeed(); + } - spk_man.SetHDSeed(master_pub_key); - if (flush_key_pool) spk_man.NewKeyPool(); + spk_man.SetHDSeed(master_pub_key); + if (flush_key_pool) spk_man.NewKeyPool(); + } - return UniValue::VNULL; -}, + return UniValue::VNULL; + }, }; } diff --git a/test/functional/wallet_descriptor.py b/test/functional/wallet_descriptor.py index e9321b72e20695..3f2fd87d7749d0 100755 --- a/test/functional/wallet_descriptor.py +++ b/test/functional/wallet_descriptor.py @@ -121,7 +121,6 @@ def run_test(self): assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.dumpprivkey, recv_wrpc.getnewaddress()) assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.dumpwallet, 'wallet.dump') assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.importwallet, 'wallet.dump') - assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", recv_wrpc.rpc.sethdseed) self.log.info("Test encryption") # Get the master fingerprint before encrypt