wallet: Load everything into DescSPKM on construction
What changed, and why it matters
This Bitcoin Core commit refactors how descriptor wallets are loaded from disk. Instead of creating an empty wallet key manager and then gradually adding keys and cache data to it, the code now gathers all the data first and creates the key manager in one go. The change also adds a safety check that rejects wallets containing both unencrypted and encrypted keys, which previously could have coexisted during incremental loading. There is no direct evidence in the commit that this fixes an active exploit, but the stricter loading model removes a class of potential consistency bugs.
Treat as a hardening/refactoring change. Reviewers should verify that the new atomic constructor correctly preserves all previously loaded state, that the unencrypted+encrypted key check does not break legitimate wallet migration paths, and that no code paths still attempt to call the removed AddKey/AddCryptedKey/SetCache methods. No immediate emergency response is warranted absent additional vulnerability reports.
Security signals we found
Atomic loading of wallet key material instead of incremental mutation
New invariant enforced: unencrypted and encrypted keys cannot both be present in the same descriptor SPKM
Descriptor ID verification moved before SPKM instantiation
Removal of public AddKey/AddCryptedKey/SetCache APIs that allowed partial state mutation
Refactoring only; no explicit bug or CVE described in commit message
Evidence from the diff
The patch changes DescriptorScriptPubKeyMan (DescSPKM) construction so that loading happens atomically: a new protected constructor and static LoadFromStorage factory take the WalletDescriptor, keypool size, KeyMap, and CryptedKeyMap up front, then call a new Load() method. The old public AddKey, AddCryptedKey, and SetCache methods are removed. CWallet::LoadDescriptorScriptPubKeyMan now returns void and accepts keys/ckeys. WalletDB loading now collects keys and ckeys into local maps, attaches the cache to the WalletDescriptor, verifies the descriptor ID before instantiation, and only then constructs the SPKM. A runtime_error is thrown if both unencrypted and encrypted keys are present, which is now caught and reported as DBErrors::CORRUPT.
Changed components
src/wallet/scriptpubkeyman.cppsrc/wallet/scriptpubkeyman.hsrc/wallet/external_signer_scriptpubkeyman.cppsrc/wallet/external_signer_scriptpubkeyman.hsrc/wallet/wallet.cppsrc/wallet/wallet.hsrc/wallet/walletdb.cppInspect captured patch +68 / −54
diff --git a/src/wallet/external_signer_scriptpubkeyman.cpp b/src/wallet/external_signer_scriptpubkeyman.cpp
index f6668625..189f7698 100644
--- a/src/wallet/external_signer_scriptpubkeyman.cpp
+++ b/src/wallet/external_signer_scriptpubkeyman.cpp
@@ -21,6 +21,11 @@
using common::PSBTError;
namespace wallet {
+std::unique_ptr<ExternalSignerScriptPubKeyMan> ExternalSignerScriptPubKeyMan::LoadFromStorage(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
+{
+ return std::unique_ptr<ExternalSignerScriptPubKeyMan>(new ExternalSignerScriptPubKeyMan(storage, descriptor, keypool_size, keys, ckeys));
+}
+
bool ExternalSignerScriptPubKeyMan::SetupDescriptor(WalletBatch& batch, std::unique_ptr<Descriptor> desc)
{
LOCK(cs_desc_man);
diff --git a/src/wallet/external_signer_scriptpubkeyman.h b/src/wallet/external_signer_scriptpubkeyman.h
index 0ccc243c..1c3324f1 100644
--- a/src/wallet/external_signer_scriptpubkeyman.h
+++ b/src/wallet/external_signer_scriptpubkeyman.h
@@ -15,14 +15,18 @@ struct bilingual_str;
namespace wallet {
class ExternalSignerScriptPubKeyMan : public DescriptorScriptPubKeyMan
{
- public:
- ExternalSignerScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size)
- : DescriptorScriptPubKeyMan(storage, descriptor, keypool_size)
- {}
+private:
+ ExternalSignerScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
+ : DescriptorScriptPubKeyMan(storage, descriptor, keypool_size, keys, ckeys)
+ {}
+
+public:
ExternalSignerScriptPubKeyMan(WalletStorage& storage, int64_t keypool_size)
: DescriptorScriptPubKeyMan(storage, keypool_size)
{}
+ static std::unique_ptr<ExternalSignerScriptPubKeyMan> LoadFromStorage(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys);
+
/** Provide a descriptor at setup time
* Returns false if already setup or setup fails, true if setup is successful
*/
diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp
index a65dee60..5ec51a73 100644
--- a/src/wallet/scriptpubkeyman.cpp
+++ b/src/wallet/scriptpubkeyman.cpp
@@ -821,6 +821,24 @@ bool LegacyDataSPKM::DeleteRecordsWithDB(WalletBatch& batch)
return batch.EraseRecords(DBKeys::LEGACY_TYPES);
}
+DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
+ : ScriptPubKeyMan(storage),
+ m_map_keys(keys),
+ m_map_crypted_keys(ckeys),
+ m_keypool_size(keypool_size),
+ m_wallet_descriptor(descriptor)
+{
+ if (!keys.empty() && !ckeys.empty()) {
+ throw std::runtime_error("Wallet contains both unencrypted and encrypted keys");
+ }
+ Load();
+}
+
+std::unique_ptr<DescriptorScriptPubKeyMan> DescriptorScriptPubKeyMan::LoadFromStorage(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
+{
+ return std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(storage, descriptor, keypool_size, keys, ckeys));
+}
+
util::Result<CTxDestination> DescriptorScriptPubKeyMan::GetNewDestination(const OutputType type)
{
// Returns true if this descriptor supports getting new addresses. Conditions where we may be unable to fetch them (e.g. locked) are caught later
@@ -1426,11 +1444,10 @@ uint256 DescriptorScriptPubKeyMan::GetID() const
return m_wallet_descriptor.id;
}
-void DescriptorScriptPubKeyMan::SetCache(const DescriptorCache& cache)
+void DescriptorScriptPubKeyMan::Load()
{
LOCK(cs_desc_man);
std::set<CScript> new_spks;
- m_wallet_descriptor.cache = cache;
for (int32_t i = m_wallet_descriptor.range_start; i < m_wallet_descriptor.range_end; ++i) {
FlatSigningProvider out_keys;
std::vector<CScript> scripts_temp;
@@ -1460,24 +1477,6 @@ void DescriptorScriptPubKeyMan::SetCache(const DescriptorCache& cache)
m_storage.TopUpCallback(new_spks, this);
}
-bool DescriptorScriptPubKeyMan::AddKey(const CKeyID& key_id, const CKey& key)
-{
- LOCK(cs_desc_man);
- m_map_keys[key_id] = key;
- return true;
-}
-
-bool DescriptorScriptPubKeyMan::AddCryptedKey(const CKeyID& key_id, const CPubKey& pubkey, const std::vector<unsigned char>& crypted_key)
-{
- LOCK(cs_desc_man);
- if (!m_map_keys.empty()) {
- return false;
- }
-
- m_map_crypted_keys[key_id] = make_pair(pubkey, crypted_key);
- return true;
-}
-
bool DescriptorScriptPubKeyMan::HasWalletDescriptor(const WalletDescriptor& desc) const
{
LOCK(cs_desc_man);
diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h
index cf024294..2762ebc2 100644
--- a/src/wallet/scriptpubkeyman.h
+++ b/src/wallet/scriptpubkeyman.h
@@ -166,14 +166,18 @@ static const std::unordered_set<OutputType> LEGACY_OUTPUT_TYPES {
OutputType::BECH32,
};
+using KeyMap = std::map<CKeyID, CKey>;
+using CryptedKeyMap = std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>>;
+using ScriptPubKeyMap = std::map<CScript, int32_t>; // Map of scripts to descriptor range index
+using PubKeyMap = std::map<CPubKey, int32_t>; // Map of pubkeys involved in scripts to descriptor range index
+
// Manages the data for a LegacyScriptPubKeyMan.
// This is the minimum necessary to load a legacy wallet so that it can be migrated.
class LegacyDataSPKM : public ScriptPubKeyMan, public FillableSigningProvider
{
-private:
+protected:
using WatchOnlySet = std::set<CScript>;
using WatchKeyMap = std::map<CKeyID, CPubKey>;
- using CryptedKeyMap = std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>>;
CryptedKeyMap mapCryptedKeys GUARDED_BY(cs_KeyStore);
WatchOnlySet setWatchOnly GUARDED_BY(cs_KeyStore);
@@ -273,11 +277,6 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
{
friend class LegacyDataSPKM;
private:
- using ScriptPubKeyMap = std::map<CScript, int32_t>; // Map of scripts to descriptor range index
- using PubKeyMap = std::map<CPubKey, int32_t>; // Map of pubkeys involved in scripts to descriptor range index
- using CryptedKeyMap = std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>>;
- using KeyMap = std::map<CKeyID, CKey>;
-
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;
@@ -315,13 +314,19 @@ private:
// Fetch the SigningProvider for a given index and optionally include private keys. Called by the above functions.
std::unique_ptr<FlatSigningProvider> GetSigningProvider(int32_t index, bool include_private = false) const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
+ void Load();
+
protected:
+ //! Create a DescriptorScriptPubKeyMan from existing data (i.e. during loading)
+ DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys);
+
WalletDescriptor m_wallet_descriptor GUARDED_BY(cs_desc_man);
//! Same as 'TopUp' but designed for use within a batch transaction context
bool TopUpWithDB(WalletBatch& batch, unsigned int size = 0);
public:
+ //! Create a new DescriptorScriptPubKeyMan from an existing descriptor (i.e. from an import)
DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size)
: ScriptPubKeyMan(storage),
m_keypool_size(keypool_size),
@@ -332,6 +337,8 @@ public:
m_keypool_size(keypool_size)
{}
+ static std::unique_ptr<DescriptorScriptPubKeyMan> LoadFromStorage(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys);
+
mutable RecursiveMutex cs_desc_man;
util::Result<CTxDestination> GetNewDestination(OutputType type) override;
@@ -383,11 +390,6 @@ public:
uint256 GetID() const override;
- 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 HasWalletDescriptor(const WalletDescriptor& desc) const;
util::Result<void> UpdateWalletDescriptor(WalletDescriptor& descriptor);
bool CanUpdateToWalletDescriptor(const WalletDescriptor& descriptor, std::string& error);
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index c337c45d..7b94b659 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -3539,16 +3539,15 @@ void CWallet::ConnectScriptPubKeyManNotifiers()
}
}
-DescriptorScriptPubKeyMan& CWallet::LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc)
+void CWallet::LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc, const KeyMap& keys, const CryptedKeyMap& ckeys)
{
- DescriptorScriptPubKeyMan* spk_manager;
+ std::unique_ptr<DescriptorScriptPubKeyMan> spk_manager;
if (IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER)) {
- spk_manager = new ExternalSignerScriptPubKeyMan(*this, desc, m_keypool_size);
+ spk_manager = ExternalSignerScriptPubKeyMan::LoadFromStorage(*this, desc, m_keypool_size, keys, ckeys);
} else {
- spk_manager = new DescriptorScriptPubKeyMan(*this, desc, m_keypool_size);
+ spk_manager = DescriptorScriptPubKeyMan::LoadFromStorage(*this, desc, m_keypool_size, keys, ckeys);
}
- AddScriptPubKeyMan(id, std::unique_ptr<ScriptPubKeyMan>(spk_manager));
- return *spk_manager;
+ AddScriptPubKeyMan(id, std::move(spk_manager));
}
DescriptorScriptPubKeyMan& CWallet::SetupDescriptorScriptPubKeyMan(WalletBatch& batch, const CExtKey& master_key, const OutputType& output_type, bool internal)
diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h
index 360b7315..4b5df5c5 100644
--- a/src/wallet/wallet.h
+++ b/src/wallet/wallet.h
@@ -1004,7 +1004,7 @@ public:
void ConnectScriptPubKeyManNotifiers();
//! Instantiate a descriptor ScriptPubKeyMan from the WalletDescriptor and load it
- DescriptorScriptPubKeyMan& LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc);
+ void LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc, const KeyMap& keys, const CryptedKeyMap& ckeys);
//! Adds the active ScriptPubKeyMan for the specified type and internal. Writes it to the wallet file
//! @param[in] id The unique id for the ScriptPubKeyMan
diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp
index c8b7ae53..5ad29970 100644
--- a/src/wallet/walletdb.cpp
+++ b/src/wallet/walletdb.cpp
@@ -771,10 +771,8 @@ static DBErrors LoadDescriptorWalletRecords(CWallet* pwallet, DatabaseBatch& bat
strErr = strprintf("%s\nDetails: %s", strErr, e.what());
return DBErrors::UNKNOWN_DESCRIPTOR;
}
- DescriptorScriptPubKeyMan& spkm = pwallet->LoadDescriptorScriptPubKeyMan(id, desc);
- // Prior to doing anything with this spkm, verify ID compatibility
- if (id != spkm.GetID()) {
+ if (id != desc.id) {
strErr = "The descriptor ID calculated by the wallet differs from the one in DB";
return DBErrors::CORRUPT;
}
@@ -833,15 +831,14 @@ static DBErrors LoadDescriptorWalletRecords(CWallet* pwallet, DatabaseBatch& bat
});
result = std::max(result, lh_cache_res.m_result);
- // Set the cache for this descriptor
- auto spk_man = (DescriptorScriptPubKeyMan*)pwallet->GetScriptPubKeyMan(id);
- assert(spk_man);
- spk_man->SetCache(cache);
+ // Set the cache to the WalletDescriptor
+ desc.cache = cache;
// Get unencrypted keys
+ KeyMap keys;
prefix = PrefixStream(DBKeys::WALLETDESCRIPTORKEY, id);
LoadResult key_res = LoadRecords(pwallet, batch, DBKeys::WALLETDESCRIPTORKEY, prefix,
- [&id, &spk_man] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& strErr) {
+ [&id, &keys] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& strErr) {
uint256 desc_id;
CPubKey pubkey;
key >> desc_id;
@@ -873,16 +870,17 @@ static DBErrors LoadDescriptorWalletRecords(CWallet* pwallet, DatabaseBatch& bat
strErr = "Error reading wallet database: descriptor unencrypted key CPrivKey corrupt";
return DBErrors::CORRUPT;
}
- spk_man->AddKey(pubkey.GetID(), privkey);
+ keys[pubkey.GetID()] = privkey;
return DBErrors::LOAD_OK;
});
result = std::max(result, key_res.m_result);
num_keys = key_res.m_records;
// Get encrypted keys
+ CryptedKeyMap ckeys;
prefix = PrefixStream(DBKeys::WALLETDESCRIPTORCKEY, id);
LoadResult ckey_res = LoadRecords(pwallet, batch, DBKeys::WALLETDESCRIPTORCKEY, prefix,
- [&id, &spk_man] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) {
+ [&id, &ckeys] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) {
uint256 desc_id;
CPubKey pubkey;
key >> desc_id;
@@ -896,12 +894,19 @@ static DBErrors LoadDescriptorWalletRecords(CWallet* pwallet, DatabaseBatch& bat
std::vector<unsigned char> privkey;
value >> privkey;
- spk_man->AddCryptedKey(pubkey.GetID(), pubkey, privkey);
+ ckeys[pubkey.GetID()] = std::make_pair(pubkey, privkey);
return DBErrors::LOAD_OK;
});
result = std::max(result, ckey_res.m_result);
num_ckeys = ckey_res.m_records;
+ try {
+ pwallet->LoadDescriptorScriptPubKeyMan(id, desc, keys, ckeys);
+ } catch (std::runtime_error& e) {
+ strErr = e.what();
+ return DBErrors::CORRUPT;
+ }
+
return result;
});
Why this scored 26/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.