Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mnemonic support for descriptor wallets #6570

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/serialize.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <utility>
#include <vector>

#include <support/allocators/secure.h>
#include <prevector.h>
#include <span.h>

Expand Down Expand Up @@ -816,9 +817,16 @@ struct VectorFormatter
/**
* string
*/
template<typename Stream, typename C> void Serialize(Stream& os, const std::basic_string<C>& str);
template<typename Stream, typename C> void Unserialize(Stream& is, std::basic_string<C>& str);
template<typename Stream, typename A, typename B, typename C> void Serialize(Stream& os, const std::basic_string<A, B, C>& str);
template<typename Stream, typename A, typename B, typename C> void Unserialize(Stream& is, std::basic_string<A, B, C>& str);

/**
* SecureString
*/
/*
template<typename Stream, typename C> void Serialize(Stream& os, const SecureString& str);
template<typename Stream, typename C> 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.
Expand Down Expand Up @@ -947,16 +955,16 @@ struct DefaultFormatter
/**
* string
*/
template<typename Stream, typename C>
void Serialize(Stream& os, const std::basic_string<C>& str)
template<typename Stream, typename A, typename B, typename C>
void Serialize(Stream& os, const std::basic_string<A, B, C>& str)
{
WriteCompactSize(os, str.size());
if (!str.empty())
os.write(MakeByteSpan(str));
}

template<typename Stream, typename C>
void Unserialize(Stream& is, std::basic_string<C>& str)
template<typename Stream, typename A, typename B, typename C>
void Unserialize(Stream& is, std::basic_string<A, B, C>& str)
{
unsigned int nSize = ReadCompactSize(is);
str.resize(nSize);
Expand Down
10 changes: 10 additions & 0 deletions src/wallet/rpcdump.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/rpcwallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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).";
},
};
}
Expand Down
140 changes: 129 additions & 11 deletions src/wallet/scriptpubkeyman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <util/strencodings.h>
#include <util/system.h>
#include <util/translation.h>
#include <wallet/bip39.h>
#include <wallet/scriptpubkeyman.h>

bool LegacyScriptPubKeyMan::GetNewDestination(CTxDestination& dest, bilingual_str& error)
Expand Down Expand Up @@ -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;
Expand All @@ -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<unsigned char> crypted_secret;
if (!EncryptSecret(master_key, secret, pubkey.GetHash(), crypted_secret)) {
return false;
}
std::vector<unsigned char> crypted_mnemonic;
std::vector<unsigned char> 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;
}

Expand Down Expand Up @@ -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));
Expand All @@ -2025,22 +2047,37 @@ bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const
}

std::vector<unsigned char> crypted_secret;
std::vector<unsigned char> crypted_mnemonic;
std::vector<unsigned char> 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);
Comment on lines +2050 to +2076
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor to reduce duplication in encryption logic.
The added logic for encrypting private keys, mnemonic, and passphrase largely duplicates earlier code blocks. Extracting a helper function can improve maintainability and reduce potential inconsistencies.

}
}

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));
Expand All @@ -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());
Expand All @@ -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)) {
Expand Down Expand Up @@ -2354,21 +2401,34 @@ 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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid duplication with AddDescriptorKeyWithDB.
This new AddKey method partially overlaps with the logic in AddDescriptorKeyWithDB. Consider consolidating them to reduce the overhead of multiple mnemonic-handling paths.

{
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<unsigned char>& crypted_key)
bool DescriptorScriptPubKeyMan::AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector<unsigned char>& crypted_key, const std::vector<unsigned char>& mnemonic,const std::vector<unsigned char>& mnemonic_passphrase)
{
LOCK(cs_desc_man);
if (!m_map_keys.empty()) {
return false;
}

m_map_crypted_keys[key_id] = make_pair(pubkey, crypted_key);
m_crypted_mnemonics[key_id] = make_pair(mnemonic, mnemonic_passphrase);
return true;
}

Expand Down Expand Up @@ -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
Expand All @@ -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<unsigned char>& crypted_mnemonic = mnemonic.first;
const std::vector<unsigned char>& 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);
Expand Down
17 changes: 13 additions & 4 deletions src/wallet/scriptpubkeyman.h
Original file line number Diff line number Diff line change
Expand Up @@ -510,17 +510,25 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
using CryptedKeyMap = std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>>;
using KeyMap = std::map<CKeyID, CKey>;

using Mnemonic = std::pair<SecureString, SecureString>;
using MnemonicMap = std::map<CKeyID, Mnemonic>;
using CryptedMnemonic = std::pair<std::vector<unsigned char>, std::vector<unsigned char>>;
using CryptedMnemonicMap = std::map<CKeyID, CryptedMnemonic>;

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;

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);

Expand Down Expand Up @@ -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;

Expand All @@ -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<unsigned char>& 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<unsigned char>& crypted_key, const std::vector<unsigned char>& mnemonic,const std::vector<unsigned char>& mnemonic_passphrase);

bool HasWalletDescriptor(const WalletDescriptor& desc) const;
void UpdateWalletDescriptor(WalletDescriptor& descriptor);
Expand All @@ -601,6 +609,7 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
const std::vector<CScript> GetScriptPubKeys() const;

bool GetDescriptorString(std::string& out, const bool priv) const;
bool GetMnemonicString(SecureString& mnemonic_out, SecureString& mnemonic_passphrase_out) const;

void UpgradeDescriptorCache();
};
Expand Down
Loading
Loading