forked from bitcoin/bitcoin
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
knst
wants to merge
5
commits into
dashpay:develop
Choose a base branch
from
knst:mnemonic-descriptors
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
8181458
refactor: drop -usehd=1 from some functional tests so far as it is de…
knst 7280161
fix: expand text commentary for RPC encryptwallet
knst f33f584
chore: add todo to wallet_descriptor.py for importdescriptor RPC
knst e3df697
feat: implement mnemonic for descriptor wallets
knst 29106cc
test: make functional test wallet_mnemonicbits.py works for descripto…
knst File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -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<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; | ||
} | ||
|
||
|
@@ -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<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); | ||
} | ||
} | ||
|
||
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,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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid duplication with |
||
{ | ||
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; | ||
} | ||
|
||
|
@@ -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<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); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.