diff --git a/src/serialize.h b/src/serialize.h index 62b4f4170b9a7..43fdfcbd2aa55 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -816,9 +817,16 @@ struct VectorFormatter /** * string */ -template void Serialize(Stream& os, const std::basic_string& str); -template void Unserialize(Stream& is, std::basic_string& str); +template void Serialize(Stream& os, const std::basic_string& str); +template void Unserialize(Stream& is, std::basic_string& str); +/** + * SecureString + */ +/* +template void Serialize(Stream& os, const SecureString& str); +template void Unserialize(Stream& is, SecureString& str); +*/ /** * prevector * prevectors of unsigned char are a special case and are intended to be serialized as a single opaque blob. @@ -947,16 +955,16 @@ struct DefaultFormatter /** * string */ -template -void Serialize(Stream& os, const std::basic_string& str) +template +void Serialize(Stream& os, const std::basic_string& str) { WriteCompactSize(os, str.size()); if (!str.empty()) os.write(MakeByteSpan(str)); } -template -void Unserialize(Stream& is, std::basic_string& str) +template +void Unserialize(Stream& is, std::basic_string& str) { unsigned int nSize = ReadCompactSize(is); str.resize(nSize); diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index d88c1aa4e1596..ff92193cb675e 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -1964,6 +1964,8 @@ RPCHelpMan listdescriptors() { {RPCResult::Type::OBJ, "", "", { {RPCResult::Type::STR, "desc", "Descriptor string representation"}, + {RPCResult::Type::STR, "mnemonic", "The mnemonic for this Descriptor wallet (bip39, english words). Presented only if private=true and created with mnemonic"}, + {RPCResult::Type::STR, "mnemonicpassphrase", "The mnemonic passphrase for this Descriptor wallet (bip39). Presented only if private=true and created with mnemonic"}, {RPCResult::Type::NUM, "timestamp", "The creation time of the descriptor"}, {RPCResult::Type::BOOL, "active", "Whether this descriptor is currently used to generate new addresses"}, {RPCResult::Type::BOOL, "internal", /*optional=*/true, "True if this descriptor is used to generate change addresses. False if this descriptor is used to generate receiving addresses; defined only for active descriptors"}, @@ -2010,6 +2012,14 @@ RPCHelpMan listdescriptors() if (!desc_spk_man->GetDescriptorString(descriptor, priv)) { throw JSONRPCError(RPC_WALLET_ERROR, "Can't get descriptor string."); } + if (priv) { + SecureString mnemonic; + SecureString mnemonic_passphrase; + if (desc_spk_man->GetMnemonicString(mnemonic, mnemonic_passphrase) && !mnemonic.empty()) { + spk.pushKV("mnemonic", mnemonic.c_str()); + spk.pushKV("mnemonicpassphrase", mnemonic_passphrase.c_str()); + } + } spk.pushKV("desc", descriptor); spk.pushKV("timestamp", wallet_descriptor.creation_time); const bool active = active_spk_mans.count(desc_spk_man) != 0; diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 172bb82fefd57..087a7165b5ad6 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2034,7 +2034,7 @@ static RPCHelpMan encryptwallet() throw JSONRPCError(RPC_WALLET_ENCRYPTION_FAILED, "Error: Failed to encrypt the wallet."); } - return "wallet encrypted; The keypool has been flushed and a new HD seed was generated (if you are using HD). You need to make a new backup."; + return "wallet encrypted; The keypool has been flushed and a new HD seed was generated (if you are using HD). You need to make a new backup or write down the new seed (mnemonic)."; }, }; } diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 8b03952ac0c18..e5841947a7692 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include bool LegacyScriptPubKeyMan::GetNewDestination(CTxDestination& dest, bilingual_str& error) @@ -1844,6 +1845,7 @@ bool DescriptorScriptPubKeyMan::CheckDecryptionKey(const CKeyingMaterial& master keyFail = true; break; } + // TODO: test for mnemonics keyPass = true; if (m_decryption_thoroughly_checked) break; @@ -1870,15 +1872,34 @@ bool DescriptorScriptPubKeyMan::Encrypt(const CKeyingMaterial& master_key, Walle { const CKey &key = key_in.second; CPubKey pubkey = key.GetPubKey(); + assert(pubkey.GetID() == key_in.first); + const auto mnemonic_in = m_mnemonics.find(key_in.first); CKeyingMaterial secret(key.begin(), key.end()); std::vector crypted_secret; if (!EncryptSecret(master_key, secret, pubkey.GetHash(), crypted_secret)) { return false; } + std::vector crypted_mnemonic; + std::vector crypted_mnemonic_passphrase; + if (mnemonic_in != m_mnemonics.end()) { + const Mnemonic mnemonic = mnemonic_in->second; + + CKeyingMaterial mnemonic_secret(mnemonic.first.begin(), mnemonic.first.end()); + CKeyingMaterial mnemonic_passphrase_secret(mnemonic.second.begin(), mnemonic.second.end()); + if (!EncryptSecret(master_key, mnemonic_secret, pubkey.GetHash(), crypted_mnemonic)) { + return false; + } + if (!EncryptSecret(master_key, mnemonic_passphrase_secret, pubkey.GetHash(), crypted_mnemonic_passphrase)) { + return false; + } + } + m_map_crypted_keys[pubkey.GetID()] = make_pair(pubkey, crypted_secret); - batch->WriteCryptedDescriptorKey(GetID(), pubkey, crypted_secret); + m_crypted_mnemonics[pubkey.GetID()] = make_pair(crypted_mnemonic, crypted_mnemonic_passphrase); + batch->WriteCryptedDescriptorKey(GetID(), pubkey, crypted_secret, crypted_mnemonic, crypted_mnemonic_passphrase); } m_map_keys.clear(); + m_mnemonics.clear(); return true; } @@ -2003,12 +2024,13 @@ void DescriptorScriptPubKeyMan::AddDescriptorKey(const CKey& key, const CPubKey { LOCK(cs_desc_man); WalletBatch batch(m_storage.GetDatabase()); - if (!AddDescriptorKeyWithDB(batch, key, pubkey)) { + // TODO: add mnemonic here too + if (!AddDescriptorKeyWithDB(batch, key, pubkey, "", "")) { throw std::runtime_error(std::string(__func__) + ": writing descriptor private key failed"); } } -bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey) +bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey, const SecureString& mnemonic, const SecureString& mnemonic_passphrase) { AssertLockHeld(cs_desc_man); assert(!m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); @@ -2025,22 +2047,37 @@ bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const } std::vector crypted_secret; + std::vector crypted_mnemonic; + std::vector crypted_mnemonic_passphrase; CKeyingMaterial secret(key.begin(), key.end()); + CKeyingMaterial mnemonic_secret(mnemonic.begin(), mnemonic.end()); + CKeyingMaterial mnemonic_passphrase_secret(mnemonic_passphrase.begin(), mnemonic_passphrase.end()); if (!m_storage.WithEncryptionKey([&](const CKeyingMaterial& encryption_key) { - return EncryptSecret(encryption_key, secret, pubkey.GetHash(), crypted_secret); + if (!EncryptSecret(encryption_key, secret, pubkey.GetHash(), crypted_secret)) return false; + if (!mnemonic.empty()) { + if (!EncryptSecret(encryption_key, mnemonic_secret, pubkey.GetHash(), crypted_mnemonic)) { + return false; + } + if (!EncryptSecret(encryption_key, mnemonic_passphrase_secret, pubkey.GetHash(), crypted_mnemonic_passphrase)) { + return false; + } + } + return true; })) { return false; } m_map_crypted_keys[pubkey.GetID()] = make_pair(pubkey, crypted_secret); - return batch.WriteCryptedDescriptorKey(GetID(), pubkey, crypted_secret); + m_crypted_mnemonics[pubkey.GetID()] = make_pair(crypted_mnemonic, crypted_mnemonic_passphrase); + return batch.WriteCryptedDescriptorKey(GetID(), pubkey, crypted_secret, crypted_mnemonic, crypted_mnemonic_passphrase); } else { m_map_keys[pubkey.GetID()] = key; - return batch.WriteDescriptorKey(GetID(), pubkey, key.GetPrivKey()); + m_mnemonics[pubkey.GetID()] = make_pair(mnemonic, mnemonic_passphrase); + return batch.WriteDescriptorKey(GetID(), pubkey, key.GetPrivKey(), mnemonic, mnemonic_passphrase); } } -bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_key, bool internal) +bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_key, const SecureString& secure_mnemonic, const SecureString& secure_mnemonic_passphrase, bool internal) { LOCK(cs_desc_man); assert(m_storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)); @@ -2050,6 +2087,16 @@ bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_ return false; } + if (!secure_mnemonic.empty()) { + // TODO: remove duplicated code with AddKey() + SecureVector seed_key_tmp; + CMnemonic::ToSeed(secure_mnemonic, secure_mnemonic_passphrase, seed_key_tmp); + + CExtKey master_key_tmp; + master_key_tmp.SetSeed(MakeByteSpan(seed_key_tmp)); + assert(master_key == master_key_tmp); + } + int64_t creation_time = GetTime(); std::string xpub = EncodeExtPubKey(master_key.Neuter()); @@ -2070,7 +2117,7 @@ bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(const CExtKey& master_ // Store the master private key, and descriptor WalletBatch batch(m_storage.GetDatabase()); - if (!AddDescriptorKeyWithDB(batch, master_key.key, master_key.key.GetPubKey())) { + if (!AddDescriptorKeyWithDB(batch, master_key.key, master_key.key.GetPubKey(), secure_mnemonic, secure_mnemonic_passphrase)) { throw std::runtime_error(std::string(__func__) + ": writing descriptor master private key failed"); } if (!batch.WriteDescriptor(GetID(), m_wallet_descriptor)) { @@ -2354,14 +2401,26 @@ void DescriptorScriptPubKeyMan::SetCache(const DescriptorCache& cache) } } -bool DescriptorScriptPubKeyMan::AddKey(const CKeyID& key_id, const CKey& key) +bool DescriptorScriptPubKeyMan::AddKey(const CKeyID& key_id, const CKey& key, const SecureString& mnemonic, const SecureString& mnemonic_passphrase) { LOCK(cs_desc_man); + if (!mnemonic.empty()) { + // TODO: remove duplicated code with AddKey() + SecureVector seed_key_tmp; + CMnemonic::ToSeed(mnemonic, mnemonic_passphrase, seed_key_tmp); + + CExtKey master_key_tmp; + master_key_tmp.SetSeed(MakeByteSpan(seed_key_tmp)); + assert(key == master_key_tmp.key); + } + m_map_keys[key_id] = key; + m_mnemonics[key_id] = make_pair(mnemonic, mnemonic_passphrase); + return true; } -bool DescriptorScriptPubKeyMan::AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector& crypted_key) +bool DescriptorScriptPubKeyMan::AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector& crypted_key, const std::vector& mnemonic,const std::vector& mnemonic_passphrase) { LOCK(cs_desc_man); if (!m_map_keys.empty()) { @@ -2369,6 +2428,7 @@ bool DescriptorScriptPubKeyMan::AddCryptedKey(const CKeyID& key_id, const CPubKe } m_map_crypted_keys[key_id] = make_pair(pubkey, crypted_key); + m_crypted_mnemonics[key_id] = make_pair(mnemonic, mnemonic_passphrase); return true; } @@ -2410,7 +2470,6 @@ bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out, const bool FlatSigningProvider provider; provider.keys = GetKeys(); - if (priv) { // For the private version, always return the master key to avoid // exposing child private keys. The risk implications of exposing child @@ -2421,6 +2480,65 @@ bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out, const bool return m_wallet_descriptor.descriptor->ToNormalizedString(provider, out, &m_wallet_descriptor.cache); } +bool DescriptorScriptPubKeyMan::GetMnemonicString(SecureString& mnemonic_out, SecureString& mnemonic_passphrase_out) const +{ + LOCK(cs_desc_man); + + mnemonic_out.clear(); + mnemonic_passphrase_out.clear(); + + if (m_mnemonics.empty() && m_crypted_mnemonics.empty()) { + WalletLogPrintf("%s: Descriptor wallet has no mnemonic defined\n", __func__); + return false; + } + if (m_storage.IsLocked(false)) return false; + + if (m_mnemonics.size() + m_crypted_mnemonics.size() > 1) { + WalletLogPrintf("%s: ERROR: One descriptor has multiple mnemonics. Can't match it\n", __func__); + return false; + } + if (m_storage.HasEncryptionKeys() && !m_storage.IsLocked(true)) { + if (!m_crypted_mnemonics.empty() && m_map_crypted_keys.size() != 1) { + WalletLogPrintf("%s: ERROR: can't choose encryption key for mnemonic out of %lld\n", __func__, m_map_crypted_keys.size()); + return false; + } + const CPubKey& pubkey = m_map_crypted_keys.begin()->second.first; + const auto mnemonic = m_crypted_mnemonics.begin()->second; + const std::vector& crypted_mnemonic = mnemonic.first; + const std::vector& crypted_mnemonic_passphrase = mnemonic.second; + + SecureVector mnemonic_v; + SecureVector mnemonic_passphrase_v; + if (!m_storage.WithEncryptionKey([&](const CKeyingMaterial& encryption_key) { + return DecryptSecret(encryption_key, crypted_mnemonic, pubkey.GetHash(), mnemonic_v); + })) { + LogPrintf("can't decrypt mnemonic pubkey %s crypted: %s\n", pubkey.GetHash().ToString(), HexStr(crypted_mnemonic)); + return false; + } + if (!crypted_mnemonic_passphrase.empty()) { + if (!m_storage.WithEncryptionKey([&](const CKeyingMaterial& encryption_key) { + return DecryptSecret(encryption_key, crypted_mnemonic_passphrase, pubkey.GetHash(), mnemonic_passphrase_v); + })) { + LogPrintf("can't decrypt mnemonic-passphrase\n"); + return false; + } + } + + std::copy(mnemonic_v.begin(), mnemonic_v.end(), std::back_inserter(mnemonic_out)); + std::copy(mnemonic_passphrase_v.begin(), mnemonic_passphrase_v.end(), std::back_inserter(mnemonic_passphrase_out)); + + return true; + } + if (m_mnemonics.empty()) return false; + + const auto mnemonic_it = m_mnemonics.begin(); + + mnemonic_out = mnemonic_it->second.first; + mnemonic_passphrase_out = mnemonic_it->second.second; + + return true; +} + void DescriptorScriptPubKeyMan::UpgradeDescriptorCache() { LOCK(cs_desc_man); diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h index 7568a6b684f46..5891de095fd4e 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -510,6 +510,11 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan using CryptedKeyMap = std::map>>; using KeyMap = std::map; + using Mnemonic = std::pair; + using MnemonicMap = std::map; + using CryptedMnemonic = std::pair, std::vector>; + using CryptedMnemonicMap = std::map; + ScriptPubKeyMap m_map_script_pub_keys GUARDED_BY(cs_desc_man); PubKeyMap m_map_pubkeys GUARDED_BY(cs_desc_man); int32_t m_max_cached_index = -1; @@ -517,10 +522,13 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan KeyMap m_map_keys GUARDED_BY(cs_desc_man); CryptedKeyMap m_map_crypted_keys GUARDED_BY(cs_desc_man); + MnemonicMap m_mnemonics GUARDED_BY(cs_desc_man); + CryptedMnemonicMap m_crypted_mnemonics GUARDED_BY(cs_desc_man); + //! keeps track of whether Unlock has run a thorough check before bool m_decryption_thoroughly_checked = false; - bool AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey) EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man); + bool AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey, const SecureString& mnemonic, const SecureString& mnemonic_passphrase) EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man); KeyMap GetKeys() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man); @@ -562,7 +570,7 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan bool IsHDEnabled() const override; //! Setup descriptors based on the given CExtkey - bool SetupDescriptorGeneration(const CExtKey& master_key, bool internal); + bool SetupDescriptorGeneration(const CExtKey& master_key, const SecureString& secure_mnemonic, const SecureString& secure_mnemonic_passphrase, bool internal); bool HavePrivateKeys() const override; @@ -588,8 +596,8 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan void SetCache(const DescriptorCache& cache); - bool AddKey(const CKeyID& key_id, const CKey& key); - bool AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector& crypted_key); + bool AddKey(const CKeyID& key_id, const CKey& key, const SecureString& mnemonic, const SecureString& mnemonic_passphrase); + bool AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector& crypted_key, const std::vector& mnemonic,const std::vector& mnemonic_passphrase); bool HasWalletDescriptor(const WalletDescriptor& desc) const; void UpdateWalletDescriptor(WalletDescriptor& descriptor); @@ -601,6 +609,7 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan const std::vector GetScriptPubKeys() const; bool GetDescriptorString(std::string& out, const bool priv) const; + bool GetMnemonicString(SecureString& mnemonic_out, SecureString& mnemonic_passphrase_out) const; void UpgradeDescriptorCache(); }; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index da659efbad914..ca48e4fe83fbb 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -39,6 +39,7 @@ #ifdef USE_BDB #include #endif +#include // TODO(refactor): move dependency it to scriptpubkeyman.cpp #include #include #include @@ -335,7 +336,7 @@ std::shared_ptr CreateWallet(interfaces::Chain& chain, interfaces::Coin // Set a seed for the wallet if (wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { LOCK(wallet->cs_wallet); - wallet->SetupDescriptorScriptPubKeyMans(); + wallet->SetupDescriptorScriptPubKeyMans("", ""); } else { // TODO: drop this condition after removing option to create non-HD wallets // related backport bitcoin#11250 @@ -724,7 +725,7 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) // If we are using descriptors, make new descriptors with a new seed if (IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS) && !IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET)) { - SetupDescriptorScriptPubKeyMans(); + SetupDescriptorScriptPubKeyMans("", ""); } else if (auto spk_man = GetLegacyScriptPubKeyMan()) { // if we are not using HD, generate new keypool if (spk_man->IsHDEnabled()) { @@ -3354,7 +3355,11 @@ std::shared_ptr CWallet::Create(interfaces::Chain* chain, interfaces::C LOCK(walletInstance->cs_wallet); if (walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { - walletInstance->SetupDescriptorScriptPubKeyMans(); + SecureString mnemonic = gArgs.GetArg("-mnemonic", "").c_str(); + SecureString mnemonic_passphrase = gArgs.GetArg("-mnemonicpassphrase", "").c_str(); + gArgs.ForceRemoveArg("mnemonic"); + gArgs.ForceRemoveArg("mnemonicpassphrase"); + walletInstance->SetupDescriptorScriptPubKeyMans(mnemonic, mnemonic_passphrase); // SetupDescriptorScriptPubKeyMans already calls SetupGeneration for us so we don't need to call SetupGeneration separately } else { // Top up the keypool // Legacy wallets need SetupGeneration here. @@ -3691,17 +3696,16 @@ bool CWallet::UpgradeToHD(const SecureString& secureMnemonic, const SecureString return false; } - if (IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { - error = Untranslated("Use RPC 'importdescriptors' to add new descriptors to Descriptor Wallets"); - return false; - } - WalletLogPrintf("Upgrading wallet to HD\n"); SetMinVersion(FEATURE_HD); - if (!GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase, secureWalletPassphrase)) { - error = Untranslated("Failed to generate HD wallet"); - return false; + if (IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { + SetupDescriptorScriptPubKeyMans(secureMnemonic, secureMnemonicPassphrase); + } else { + if (!GenerateNewHDChain(secureMnemonic, secureMnemonicPassphrase, secureWalletPassphrase)) { + error = Untranslated("Failed to generate HD wallet"); + return false; + } } return true; } @@ -4300,15 +4304,18 @@ void CWallet::LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc) m_spk_managers[id] = std::move(spk_manager); } -void CWallet::SetupDescriptorScriptPubKeyMans() +void CWallet::SetupDescriptorScriptPubKeyMans(const SecureString& mnemonic_arg, const SecureString mnemonic_passphrase) { AssertLockHeld(cs_wallet); // Make a seed - CKey seed_key; - seed_key.MakeNewKey(true); - CPubKey seed = seed_key.GetPubKey(); - assert(seed_key.VerifyPubKey(seed)); + // TODO: remove duplicated code with CHDChain::SetMnemonic + const SecureString mnemonic = mnemonic_arg.empty() ? CMnemonic::Generate(gArgs.GetIntArg("-mnemonicbits", CHDChain::DEFAULT_MNEMONIC_BITS)) : mnemonic_arg; + if (!CMnemonic::Check(mnemonic)) { + throw std::runtime_error(std::string(__func__) + ": invalid mnemonic: `" + std::string(mnemonic.c_str()) + "`"); + } + SecureVector seed_key; + CMnemonic::ToSeed(mnemonic, mnemonic_passphrase, seed_key); // Get the extended key CExtKey master_key; @@ -4325,7 +4332,7 @@ void CWallet::SetupDescriptorScriptPubKeyMans() throw std::runtime_error(std::string(__func__) + ": Could not encrypt new descriptors"); } } - spk_manager->SetupDescriptorGeneration(master_key, internal); + spk_manager->SetupDescriptorGeneration(master_key, mnemonic, mnemonic_passphrase, internal); uint256 id = spk_manager->GetID(); m_spk_managers[id] = std::move(spk_manager); AddActiveScriptPubKeyMan(id, internal); @@ -4406,6 +4413,10 @@ ScriptPubKeyMan* CWallet::AddWalletDescriptor(WalletDescriptor& desc, const Flat return nullptr; } + // TODO: implement mnemonic for AddDescriptor() + SecureString mnemonic; + SecureString mnemonic_passphrase; + LOCK(cs_wallet); auto spk_man = GetDescriptorScriptPubKeyMan(desc); if (spk_man) { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index b22eecd2a3c48..169f92fda1f6e 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1067,7 +1067,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati void DeactivateScriptPubKeyMan(uint256 id, bool internal); //! Create new DescriptorScriptPubKeyMans and add them to the wallet - void SetupDescriptorScriptPubKeyMans() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + void SetupDescriptorScriptPubKeyMans(const SecureString& mnemonic, const SecureString mnemonic_passphrase) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); //! Return the DescriptorScriptPubKeyMan for a WalletDescriptor if it is already in the wallet DescriptorScriptPubKeyMan* GetDescriptorScriptPubKeyMan(const WalletDescriptor& desc) const; diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 5ef0fce47937a..6fabdac6f21d0 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -241,7 +241,7 @@ bool WalletBatch::EraseActiveScriptPubKeyMan(bool internal) return EraseIC(key); } -bool WalletBatch::WriteDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const CPrivKey& privkey) +bool WalletBatch::WriteDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const CPrivKey& privkey, const SecureString& mnemonic, const SecureString& mnemonic_passphrase) { // hash pubkey/privkey to accelerate wallet load std::vector key; @@ -249,19 +249,19 @@ bool WalletBatch::WriteDescriptorKey(const uint256& desc_id, const CPubKey& pubk key.insert(key.end(), pubkey.begin(), pubkey.end()); key.insert(key.end(), privkey.begin(), privkey.end()); - return WriteIC(std::make_pair(DBKeys::WALLETDESCRIPTORKEY, std::make_pair(desc_id, pubkey)), std::make_pair(privkey, Hash(key)), false); + return WriteIC(std::make_pair(DBKeys::WALLETDESCRIPTORKEY, std::make_pair(desc_id, pubkey)), std::make_pair(std::make_pair(privkey, Hash(key)), std::make_pair(mnemonic, mnemonic_passphrase)), false); } -bool WalletBatch::WriteCryptedDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const std::vector& secret) +bool WalletBatch::WriteCryptedDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const std::vector& secret, const std::vector& mnemonic, const std::vector& mnemonic_passphrase) { - if (!WriteIC(std::make_pair(DBKeys::WALLETDESCRIPTORCKEY, std::make_pair(desc_id, pubkey)), secret, false)) { + if (!WriteIC(std::make_pair(DBKeys::WALLETDESCRIPTORCKEY, std::make_pair(desc_id, pubkey)), std::make_pair(secret, std::make_pair(mnemonic, mnemonic_passphrase)), false)) { return false; } EraseIC(std::make_pair(DBKeys::WALLETDESCRIPTORKEY, std::make_pair(desc_id, pubkey))); return true; } -bool WalletBatch::WriteDescriptor(const uint256& desc_id, const WalletDescriptor& descriptor) +bool WalletBatch::WriteDescriptor(const uint256& desc_id, const WalletDescriptor& descriptor/*, const SecureString& mnemonic, const SecureString& mnemonic_passphrase*/) { return WriteIC(make_pair(DBKeys::WALLETDESCRIPTOR, desc_id), descriptor); } @@ -335,6 +335,8 @@ class CWalletScanState { std::map m_descriptor_caches; std::map, CKey> m_descriptor_keys; std::map, std::pair>> m_descriptor_crypt_keys; + std::map, std::pair> mnemonics; + std::map, std::pair, std::vector>> crypted_mnemonics; bool tx_corrupt{false}; CWalletScanState() { @@ -704,6 +706,21 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, return false; } wss.m_descriptor_keys.insert(std::make_pair(std::make_pair(desc_id, pubkey.GetID()), key)); + + SecureString mnemonic; + SecureString mnemonic_passphrase; + // it's okay if wallet doesn't have mnemonic. + // The wallet may be created in an older version of Dash Core or by importing descriptor + try + { + ssValue >> mnemonic; + ssValue >> mnemonic_passphrase; + } + catch (const std::ios_base::failure&) {} + + if (!mnemonic.empty()) { + wss.mnemonics.insert(std::make_pair(std::make_pair(desc_id, pubkey.GetID()), std::make_pair(mnemonic, mnemonic_passphrase))); + } } else if (strType == DBKeys::WALLETDESCRIPTORCKEY) { uint256 desc_id; CPubKey pubkey; @@ -720,6 +737,22 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, wss.m_descriptor_crypt_keys.insert(std::make_pair(std::make_pair(desc_id, pubkey.GetID()), std::make_pair(pubkey, privkey))); wss.fIsEncrypted = true; + + // TODO : remove copy-paste with plain-text key + std::vector mnemonic; + std::vector mnemonic_passphrase; + // it's okay if wallet doesn't have mnemonic. + // The wallet may be created in an older version of Dash Core or by importing descriptor + try + { + ssValue >> mnemonic; + ssValue >> mnemonic_passphrase; + } + catch (const std::ios_base::failure&) {} + if (!mnemonic.empty()) { + wss.crypted_mnemonics.insert(std::make_pair(std::make_pair(desc_id, pubkey.GetID()), std::make_pair(mnemonic, mnemonic_passphrase))); + } + } else if (strType == DBKeys::LOCKED_UTXO) { uint256 hash; uint32_t n; @@ -864,11 +897,22 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet) // Set the descriptor keys for (auto desc_key_pair : wss.m_descriptor_keys) { auto spk_man = pwallet->GetScriptPubKeyMan(desc_key_pair.first.first); - ((DescriptorScriptPubKeyMan*)spk_man)->AddKey(desc_key_pair.first.second, desc_key_pair.second); + auto it = wss.mnemonics.find(desc_key_pair.first); + if (it == wss.mnemonics.end()) { + ((DescriptorScriptPubKeyMan*)spk_man)->AddKey(desc_key_pair.first.second, desc_key_pair.second, "", ""); + } else { + ((DescriptorScriptPubKeyMan*)spk_man)->AddKey(desc_key_pair.first.second, desc_key_pair.second, it->second.first, it->second.second); + } } + for (auto desc_key_pair : wss.m_descriptor_crypt_keys) { auto spk_man = pwallet->GetScriptPubKeyMan(desc_key_pair.first.first); - ((DescriptorScriptPubKeyMan*)spk_man)->AddCryptedKey(desc_key_pair.first.second, desc_key_pair.second.first, desc_key_pair.second.second); + auto it = wss.crypted_mnemonics.find(desc_key_pair.first); + if (it == wss.crypted_mnemonics.end()) { + ((DescriptorScriptPubKeyMan*)spk_man)->AddCryptedKey(desc_key_pair.first.second, desc_key_pair.second.first, desc_key_pair.second.second, {}, {}); + } else { + ((DescriptorScriptPubKeyMan*)spk_man)->AddCryptedKey(desc_key_pair.first.second, desc_key_pair.second.first, desc_key_pair.second.second, it->second.first, it->second.second); + } } if (fNoncriticalErrors && result == DBErrors::LOAD_OK) diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 8a980405ef139..57e42f611e126 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -212,8 +212,8 @@ class WalletBatch /** Write a CGovernanceObject to the database */ bool WriteGovernanceObject(const Governance::Object& obj); - bool WriteDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const CPrivKey& privkey); - bool WriteCryptedDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const std::vector& secret); + bool WriteDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const CPrivKey& privkey, const SecureString& mnemonic, const SecureString& mnemonic_passphrase); + bool WriteCryptedDescriptorKey(const uint256& desc_id, const CPubKey& pubkey, const std::vector& secret, const std::vector& mnemonic, const std::vector& mnemonic_passphrase); bool WriteDescriptor(const uint256& desc_id, const WalletDescriptor& descriptor); bool WriteDescriptorDerivedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index, uint32_t der_index); bool WriteDescriptorParentCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index); diff --git a/src/wallet/wallettool.cpp b/src/wallet/wallettool.cpp index 9ad08dea5ffc8..eae3a0ee1756c 100644 --- a/src/wallet/wallettool.cpp +++ b/src/wallet/wallettool.cpp @@ -49,7 +49,7 @@ static void WalletCreate(CWallet* wallet_instance, uint64_t wallet_creation_flag spk_man->GenerateNewHDChain(/*secureMnemonic=*/"", /*secureMnemonicPassphrase=*/""); } } else { - wallet_instance->SetupDescriptorScriptPubKeyMans(); + wallet_instance->SetupDescriptorScriptPubKeyMans("", ""); } tfm::format(std::cout, "Topping up keypool...\n"); diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index d3735a50fda89..52a6538590ac3 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -294,7 +294,7 @@ 'wallet_upgradewallet.py --legacy-wallet', 'wallet_importdescriptors.py --descriptors', 'wallet_mnemonicbits.py --legacy-wallet', - # 'wallet_mnemonicbits.py --descriptors', # TODO : implement mnemonics for descriptor wallets + 'wallet_mnemonicbits.py --descriptors', 'rpc_bind.py --ipv4', 'rpc_bind.py --ipv6', 'rpc_bind.py --nonloopback', diff --git a/test/functional/wallet_descriptor.py b/test/functional/wallet_descriptor.py index ca7d8875c1dd9..86a55792ec7dd 100755 --- a/test/functional/wallet_descriptor.py +++ b/test/functional/wallet_descriptor.py @@ -103,6 +103,7 @@ def run_test(self): self.log.info("Test that unlock is needed when deriving only hardened keys in an encrypted wallet") send_wrpc.walletpassphrase('pass', 10) + # TODO: implement import descriptors by mnemonic send_wrpc.importdescriptors([{ "desc": "pkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/*h)#y4dfsj7n", "timestamp": "now", diff --git a/test/functional/wallet_dump.py b/test/functional/wallet_dump.py index 947b8f1cf83d1..816a812df8201 100755 --- a/test/functional/wallet_dump.py +++ b/test/functional/wallet_dump.py @@ -87,7 +87,7 @@ class WalletDumpTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.disable_mocktime = True - self.extra_args = [["-keypool=90", "-usehd=1"]] + self.extra_args = [["-keypool=90"]] self.rpc_timeout = 120 def skip_test_if_missing_module(self): diff --git a/test/functional/wallet_importmulti.py b/test/functional/wallet_importmulti.py index 606ccb1ffc6b5..8883303305d41 100755 --- a/test/functional/wallet_importmulti.py +++ b/test/functional/wallet_importmulti.py @@ -37,7 +37,6 @@ class ImportMultiTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True - self.extra_args = [['-usehd=1']] * self.num_nodes def skip_test_if_missing_module(self): self.skip_if_no_wallet() diff --git a/test/functional/wallet_keypool_hd.py b/test/functional/wallet_keypool_hd.py index 7ef2e16401038..7bc95e80f66a1 100755 --- a/test/functional/wallet_keypool_hd.py +++ b/test/functional/wallet_keypool_hd.py @@ -18,7 +18,6 @@ class KeyPoolTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 - self.extra_args = [['-usehd=1']] def skip_test_if_missing_module(self): self.skip_if_no_wallet() diff --git a/test/functional/wallet_mnemonicbits.py b/test/functional/wallet_mnemonicbits.py index 791db4fe33248..a843125a36027 100755 --- a/test/functional/wallet_mnemonicbits.py +++ b/test/functional/wallet_mnemonicbits.py @@ -24,7 +24,42 @@ def run_test(self): self.stop_node(0) self.nodes[0].assert_start_raises_init_error(['-mnemonicbits=123'], "Error: Invalid '-mnemonicbits'. Allowed values: 128, 160, 192, 224, 256.") self.start_node(0) - assert_equal(len(self.nodes[0].dumphdinfo()["mnemonic"].split()), 12) # 12 words by default + + mnemonic_pre = self.nodes[0].listdescriptors(True)['descriptors'][1]["mnemonic"] if self.options.descriptors else self.nodes[0].dumphdinfo()["mnemonic"] + + self.nodes[0].encryptwallet('pass') + self.nodes[0].walletpassphrase('pass', 100) + if self.options.descriptors: + assert "mnemonic" not in self.nodes[0].listdescriptors()['descriptors'][0] + assert "mnemonic" in self.nodes[0].listdescriptors(True)['descriptors'][0] + + descriptors = self.nodes[0].listdescriptors(True)['descriptors'] + assert_equal(descriptors[0]['mnemonic'], descriptors[1]['mnemonic']) + + mnemonic_count = 0 + found_in_encrypted = 0 + for desc in descriptors: + if 'mnemonic' not in desc: + # skip imported coinbase private key + continue + assert_equal(len(desc['mnemonic'].split()), 12) + mnemonic_count += 1 + if desc['mnemonic'] == mnemonic_pre: + found_in_encrypted += 1 + assert not desc['active'] + else: + assert desc['active'] + # there should 5 descriptors in total + # one of them imported private key for coinbase without mnemonic + # encryption of descriptor wallet creates new private keys, + # it should be 2 active and 2 inactive mnemonics + assert_equal(found_in_encrypted, 2) + assert_equal(mnemonic_count, 4) + assert_equal(len(descriptors), 5) + else: + assert_equal(len(self.nodes[0].dumphdinfo()["mnemonic"].split()), 12) # 12 words by default + # legacy HD wallets could have only one chain + assert_equal(mnemonic_pre, self.nodes[0].dumphdinfo()["mnemonic"]) self.log.info("Can have multiple wallets with different mnemonic length loaded at the same time") self.restart_node(0, extra_args=["-mnemonicbits=160"]) @@ -34,16 +69,27 @@ def run_test(self): self.restart_node(0, extra_args=["-mnemonicbits=224"]) self.nodes[0].createwallet("wallet_224") self.restart_node(0, extra_args=["-mnemonicbits=256"]) + self.nodes[0].get_wallet_rpc(self.default_wallet_name).walletpassphrase('pass', 100) self.nodes[0].loadwallet("wallet_160") self.nodes[0].loadwallet("wallet_192") self.nodes[0].loadwallet("wallet_224") - self.nodes[0].createwallet("wallet_256", False, True) # blank - self.nodes[0].get_wallet_rpc("wallet_256").upgradetohd() - assert_equal(len(self.nodes[0].get_wallet_rpc(self.default_wallet_name).dumphdinfo()["mnemonic"].split()), 12) # 12 words by default - assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_160").dumphdinfo()["mnemonic"].split()), 15) # 15 words - assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_192").dumphdinfo()["mnemonic"].split()), 18) # 18 words - assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_224").dumphdinfo()["mnemonic"].split()), 21) # 21 words - assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_256").dumphdinfo()["mnemonic"].split()), 24) # 24 words + if self.options.descriptors: + self.nodes[0].createwallet("wallet_256", False, True, "", False, True) # blank Descriptors + self.nodes[0].get_wallet_rpc("wallet_256").upgradetohd() + # first descriptor is private key with no mnemonic for CbTx (see node.importprivkey), we use number#1 here instead + assert_equal(len(self.nodes[0].get_wallet_rpc(self.default_wallet_name).listdescriptors(True)["descriptors"][1]["mnemonic"].split()), 12) # 12 words by default + assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_160").listdescriptors(True)["descriptors"][0]["mnemonic"].split()), 15) # 15 words + assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_192").listdescriptors(True)["descriptors"][0]["mnemonic"].split()), 18) # 18 words + assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_224").listdescriptors(True)["descriptors"][0]["mnemonic"].split()), 21) # 21 words + assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_256").listdescriptors(True)["descriptors"][0]["mnemonic"].split()), 24) # 24 words + else: + self.nodes[0].createwallet("wallet_256", False, True) # blank HD legacy + self.nodes[0].get_wallet_rpc("wallet_256").upgradetohd() + assert_equal(len(self.nodes[0].get_wallet_rpc(self.default_wallet_name).dumphdinfo()["mnemonic"].split()), 12) # 12 words by default + assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_160").dumphdinfo()["mnemonic"].split()), 15) # 15 words + assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_192").dumphdinfo()["mnemonic"].split()), 18) # 18 words + assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_224").dumphdinfo()["mnemonic"].split()), 21) # 21 words + assert_equal(len(self.nodes[0].get_wallet_rpc("wallet_256").dumphdinfo()["mnemonic"].split()), 24) # 24 words if __name__ == '__main__':