Skip to content

Commit 722776c

Browse files
committed
Merge bitcoin/bitcoin#21329: descriptor wallet: Cache last hardened xpub and use in normalized descriptors
e6cf0ed wallet, rpc: listdescriptors does not need unlocked (Andrew Chow) 3280704 Pass in DescriptorCache to ToNormalizedString (Andrew Chow) 7a26ff1 Change DescriptorImpl::ToStringHelper to use an enum (Andrew Chow) 75530c9 Remove priv option for ToNormalizedString (Andrew Chow) 74fede3 wallet: Upgrade existing descriptor caches (Andrew Chow) 432ba9e wallet: Store last hardened xpub cache (Andrew Chow) d87b544 descriptors: Cache last hardened xpub (Andrew Chow) cacc391 Move DescriptorCache writing to WalletBatch (Andrew Chow) 0b4c8ef Refactor Cache merging and writing (Andrew Chow) 976b53b Revert "Cache parent xpub inside of BIP32PubkeyProvider" (Andrew Chow) Pull request description: Currently fetching a normalized descriptor requires the wallet to be unlocked as it needs the private keys to derive the last hardened xpub. This is not very user friendly as normalized descriptors shouldn't require and don't involve the private keys except for derivation. We solve this problem by caching the last hardened xpub (which has to be derived at some point when generating the address pool). However the last hardened xpub was not already being cached. We only cached the immediate parent xpub and derived child keys. For example, with a descriptor derivation path of `/84'/0'/0'/0/*`, the parent xpub that is cached is `m/84'/0'/0'/0`, and the child keys of `m/84'/0'/0'/0/i` (note that child keys would not be cached in this case). This parent xpub is not suitable for the normalized descriptor form as we want the key at `m/84'/0'/0'`. So this PR adds another field to `DescriptorCache` to cache the last hardened xpub so that we can use them for normalized descriptors. Since `DescriptorCache` is changing, existing descriptor wallets need to be upgraded to use this new cache. The upgrade will occur in the background either at loading time (if the wallet is not encrypted) or at unlocking time in the same manner that `UpgradeKeyMetadata` operates. It will use a new wallet flag `WALLET_FLAG_LAST_HARDENED_XPUB_CACHED` to indicate whether the descriptor wallet has the last hardened xpub cache. Lastly `listdescriptors` will not require the wallet to be locked and `getaddressinfo`'s `parent_desc` will always be output (assuming the upgrade has occurred). ACKs for top commit: fjahr: tACK e6cf0ed S3RK: reACK e6cf0ed jonatack: Semi ACK e6cf0ed reviewed, debug-built and ran unit tests and some of the descriptor functional tests at each commit. I'm not very familiar with this code and it could be clearer to the uninitiated IMHO, so I'm not confident enough to give a full ACK. Various minor suggestions follow, most of them for readability, feel free to pick and choose. meshcollider: Code review + functional test run ACK e6cf0ed Tree-SHA512: ac27aade8644525cd65bfcaf27ff32afb974085b1451faf4ff68c6671a690bd6a41d4f39a33cbf461ae0fbe85995c0a4c08dbd36171da1c1d2a1d00053ad298d
2 parents 3fc20ab + e6cf0ed commit 722776c

File tree

13 files changed

+272
-96
lines changed

13 files changed

+272
-96
lines changed

src/script/descriptor.cpp

Lines changed: 131 additions & 52 deletions
Large diffs are not rendered by default.

src/script/descriptor.h

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class DescriptorCache {
2222
std::unordered_map<uint32_t, ExtPubKeyMap> m_derived_xpubs;
2323
/** Map key expression index -> parent xpub */
2424
ExtPubKeyMap m_parent_xpubs;
25+
/** Map key expression index -> last hardened xpub */
26+
ExtPubKeyMap m_last_hardened_xpubs;
2527

2628
public:
2729
/** Cache a parent xpub
@@ -50,11 +52,30 @@ class DescriptorCache {
5052
* @param[in] xpub The CExtPubKey to get from cache
5153
*/
5254
bool GetCachedDerivedExtPubKey(uint32_t key_exp_pos, uint32_t der_index, CExtPubKey& xpub) const;
55+
/** Cache a last hardened xpub
56+
*
57+
* @param[in] key_exp_pos Position of the key expression within the descriptor
58+
* @param[in] xpub The CExtPubKey to cache
59+
*/
60+
void CacheLastHardenedExtPubKey(uint32_t key_exp_pos, const CExtPubKey& xpub);
61+
/** Retrieve a cached last hardened xpub
62+
*
63+
* @param[in] key_exp_pos Position of the key expression within the descriptor
64+
* @param[in] xpub The CExtPubKey to get from cache
65+
*/
66+
bool GetCachedLastHardenedExtPubKey(uint32_t key_exp_pos, CExtPubKey& xpub) const;
5367

5468
/** Retrieve all cached parent xpubs */
5569
const ExtPubKeyMap GetCachedParentExtPubKeys() const;
5670
/** Retrieve all cached derived xpubs */
5771
const std::unordered_map<uint32_t, ExtPubKeyMap> GetCachedDerivedExtPubKeys() const;
72+
/** Retrieve all cached last hardened xpubs */
73+
const ExtPubKeyMap GetCachedLastHardenedExtPubKeys() const;
74+
75+
/** Combine another DescriptorCache into this one.
76+
* Returns a cache containing the items from the other cache unknown to current cache
77+
*/
78+
DescriptorCache MergeAndDiff(const DescriptorCache& other);
5879
};
5980

6081
/** \brief Interface for parsed descriptor objects.
@@ -94,7 +115,7 @@ struct Descriptor {
94115
virtual bool ToPrivateString(const SigningProvider& provider, std::string& out) const = 0;
95116

96117
/** Convert the descriptor to a normalized string. Normalized descriptors have the xpub at the last hardened step. This fails if the provided provider does not have the private keys to derive that xpub. */
97-
virtual bool ToNormalizedString(const SigningProvider& provider, std::string& out, bool priv) const = 0;
118+
virtual bool ToNormalizedString(const SigningProvider& provider, std::string& out, const DescriptorCache* cache = nullptr) const = 0;
98119

99120
/** Expand a descriptor at a specified position.
100121
*

src/test/descriptor_tests.cpp

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,10 @@ void DoCheck(const std::string& prv, const std::string& pub, const std::string&
124124

125125
// Check that private can produce the normalized descriptors
126126
std::string norm1;
127-
BOOST_CHECK(parse_priv->ToNormalizedString(keys_priv, norm1, false));
127+
BOOST_CHECK(parse_priv->ToNormalizedString(keys_priv, norm1));
128128
BOOST_CHECK(EqualDescriptor(norm1, norm_pub));
129-
BOOST_CHECK(parse_pub->ToNormalizedString(keys_priv, norm1, false));
129+
BOOST_CHECK(parse_pub->ToNormalizedString(keys_priv, norm1));
130130
BOOST_CHECK(EqualDescriptor(norm1, norm_pub));
131-
BOOST_CHECK(parse_priv->ToNormalizedString(keys_priv, norm1, true));
132-
BOOST_CHECK(EqualDescriptor(norm1, norm_prv));
133-
BOOST_CHECK(parse_pub->ToNormalizedString(keys_priv, norm1, true));
134-
BOOST_CHECK(EqualDescriptor(norm1, norm_prv));
135131

136132
// Check whether IsRange on both returns the expected result
137133
BOOST_CHECK_EQUAL(parse_pub->IsRange(), (flags & RANGE) != 0);

src/wallet/rpcdump.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,8 +1787,6 @@ RPCHelpMan listdescriptors()
17871787
throw JSONRPCError(RPC_WALLET_ERROR, "listdescriptors is not available for non-descriptor wallets");
17881788
}
17891789

1790-
EnsureWalletIsUnlocked(*wallet);
1791-
17921790
LOCK(wallet->cs_wallet);
17931791

17941792
UniValue descriptors(UniValue::VARR);
@@ -1802,7 +1800,7 @@ RPCHelpMan listdescriptors()
18021800
LOCK(desc_spk_man->cs_desc_man);
18031801
const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor();
18041802
std::string descriptor;
1805-
if (!desc_spk_man->GetDescriptorString(descriptor, false)) {
1803+
if (!desc_spk_man->GetDescriptorString(descriptor)) {
18061804
throw JSONRPCError(RPC_WALLET_ERROR, "Can't get normalized descriptor string.");
18071805
}
18081806
spk.pushKV("desc", descriptor);

src/wallet/rpcwallet.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3872,7 +3872,7 @@ RPCHelpMan getaddressinfo()
38723872
DescriptorScriptPubKeyMan* desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(pwallet->GetScriptPubKeyMan(scriptPubKey));
38733873
if (desc_spk_man) {
38743874
std::string desc_str;
3875-
if (desc_spk_man->GetDescriptorString(desc_str, false)) {
3875+
if (desc_spk_man->GetDescriptorString(desc_str)) {
38763876
ret.pushKV("parent_desc", desc_str);
38773877
}
38783878
}

src/wallet/scriptpubkeyman.cpp

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1805,34 +1805,10 @@ bool DescriptorScriptPubKeyMan::TopUp(unsigned int size)
18051805
}
18061806
m_map_pubkeys[pubkey] = i;
18071807
}
1808-
// Write the cache
1809-
for (const auto& parent_xpub_pair : temp_cache.GetCachedParentExtPubKeys()) {
1810-
CExtPubKey xpub;
1811-
if (m_wallet_descriptor.cache.GetCachedParentExtPubKey(parent_xpub_pair.first, xpub)) {
1812-
if (xpub != parent_xpub_pair.second) {
1813-
throw std::runtime_error(std::string(__func__) + ": New cached parent xpub does not match already cached parent xpub");
1814-
}
1815-
continue;
1816-
}
1817-
if (!batch.WriteDescriptorParentCache(parent_xpub_pair.second, id, parent_xpub_pair.first)) {
1818-
throw std::runtime_error(std::string(__func__) + ": writing cache item failed");
1819-
}
1820-
m_wallet_descriptor.cache.CacheParentExtPubKey(parent_xpub_pair.first, parent_xpub_pair.second);
1821-
}
1822-
for (const auto& derived_xpub_map_pair : temp_cache.GetCachedDerivedExtPubKeys()) {
1823-
for (const auto& derived_xpub_pair : derived_xpub_map_pair.second) {
1824-
CExtPubKey xpub;
1825-
if (m_wallet_descriptor.cache.GetCachedDerivedExtPubKey(derived_xpub_map_pair.first, derived_xpub_pair.first, xpub)) {
1826-
if (xpub != derived_xpub_pair.second) {
1827-
throw std::runtime_error(std::string(__func__) + ": New cached derived xpub does not match already cached derived xpub");
1828-
}
1829-
continue;
1830-
}
1831-
if (!batch.WriteDescriptorDerivedCache(derived_xpub_pair.second, id, derived_xpub_map_pair.first, derived_xpub_pair.first)) {
1832-
throw std::runtime_error(std::string(__func__) + ": writing cache item failed");
1833-
}
1834-
m_wallet_descriptor.cache.CacheDerivedExtPubKey(derived_xpub_map_pair.first, derived_xpub_pair.first, derived_xpub_pair.second);
1835-
}
1808+
// Merge and write the cache
1809+
DescriptorCache new_items = m_wallet_descriptor.cache.MergeAndDiff(temp_cache);
1810+
if (!batch.WriteDescriptorCacheItems(id, new_items)) {
1811+
throw std::runtime_error(std::string(__func__) + ": writing cache items failed");
18361812
}
18371813
m_max_cached_index++;
18381814
}
@@ -2290,15 +2266,41 @@ const std::vector<CScript> DescriptorScriptPubKeyMan::GetScriptPubKeys() const
22902266
return script_pub_keys;
22912267
}
22922268

2293-
bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out, bool priv) const
2269+
bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out) const
22942270
{
22952271
LOCK(cs_desc_man);
2296-
if (m_storage.IsLocked()) {
2297-
return false;
2272+
2273+
FlatSigningProvider provider;
2274+
provider.keys = GetKeys();
2275+
2276+
return m_wallet_descriptor.descriptor->ToNormalizedString(provider, out, &m_wallet_descriptor.cache);
2277+
}
2278+
2279+
void DescriptorScriptPubKeyMan::UpgradeDescriptorCache()
2280+
{
2281+
LOCK(cs_desc_man);
2282+
if (m_storage.IsLocked() || m_storage.IsWalletFlagSet(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED)) {
2283+
return;
22982284
}
22992285

2286+
// Skip if we have the last hardened xpub cache
2287+
if (m_wallet_descriptor.cache.GetCachedLastHardenedExtPubKeys().size() > 0) {
2288+
return;
2289+
}
2290+
2291+
// Expand the descriptor
23002292
FlatSigningProvider provider;
23012293
provider.keys = GetKeys();
2294+
FlatSigningProvider out_keys;
2295+
std::vector<CScript> scripts_temp;
2296+
DescriptorCache temp_cache;
2297+
if (!m_wallet_descriptor.descriptor->Expand(0, provider, scripts_temp, out_keys, &temp_cache)){
2298+
throw std::runtime_error("Unable to expand descriptor");
2299+
}
23022300

2303-
return m_wallet_descriptor.descriptor->ToNormalizedString(provider, out, priv);
2301+
// Cache the last hardened xpubs
2302+
DescriptorCache diff = m_wallet_descriptor.cache.MergeAndDiff(temp_cache);
2303+
if (!WalletBatch(m_storage.GetDatabase()).WriteDescriptorCacheItems(GetID(), diff)) {
2304+
throw std::runtime_error(std::string(__func__) + ": writing cache items failed");
2305+
}
23042306
}

src/wallet/scriptpubkeyman.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,9 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
630630
const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
631631
const std::vector<CScript> GetScriptPubKeys() const;
632632

633-
bool GetDescriptorString(std::string& out, bool priv) const;
633+
bool GetDescriptorString(std::string& out) const;
634+
635+
void UpgradeDescriptorCache();
634636
};
635637

636638
#endif // BITCOIN_WALLET_SCRIPTPUBKEYMAN_H

src/wallet/wallet.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,19 @@ void CWallet::UpgradeKeyMetadata()
374374
SetWalletFlag(WALLET_FLAG_KEY_ORIGIN_METADATA);
375375
}
376376

377+
void CWallet::UpgradeDescriptorCache()
378+
{
379+
if (!IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS) || IsLocked() || IsWalletFlagSet(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED)) {
380+
return;
381+
}
382+
383+
for (ScriptPubKeyMan* spkm : GetAllScriptPubKeyMans()) {
384+
DescriptorScriptPubKeyMan* desc_spkm = dynamic_cast<DescriptorScriptPubKeyMan*>(spkm);
385+
desc_spkm->UpgradeDescriptorCache();
386+
}
387+
SetWalletFlag(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED);
388+
}
389+
377390
bool CWallet::Unlock(const SecureString& strWalletPassphrase, bool accept_no_keys)
378391
{
379392
CCrypter crypter;
@@ -390,6 +403,8 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase, bool accept_no_key
390403
if (Unlock(_vMasterKey, accept_no_keys)) {
391404
// Now that we've unlocked, upgrade the key metadata
392405
UpgradeKeyMetadata();
406+
// Now that we've unlocked, upgrade the descriptor cache
407+
UpgradeDescriptorCache();
393408
return true;
394409
}
395410
}

src/wallet/wallet.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ static constexpr uint64_t KNOWN_WALLET_FLAGS =
117117
WALLET_FLAG_AVOID_REUSE
118118
| WALLET_FLAG_BLANK_WALLET
119119
| WALLET_FLAG_KEY_ORIGIN_METADATA
120+
| WALLET_FLAG_LAST_HARDENED_XPUB_CACHED
120121
| WALLET_FLAG_DISABLE_PRIVATE_KEYS
121122
| WALLET_FLAG_DESCRIPTORS
122123
| WALLET_FLAG_EXTERNAL_SIGNER;
@@ -128,6 +129,7 @@ static const std::map<std::string,WalletFlags> WALLET_FLAG_MAP{
128129
{"avoid_reuse", WALLET_FLAG_AVOID_REUSE},
129130
{"blank", WALLET_FLAG_BLANK_WALLET},
130131
{"key_origin_metadata", WALLET_FLAG_KEY_ORIGIN_METADATA},
132+
{"last_hardened_xpub_cached", WALLET_FLAG_LAST_HARDENED_XPUB_CACHED},
131133
{"disable_private_keys", WALLET_FLAG_DISABLE_PRIVATE_KEYS},
132134
{"descriptor_wallet", WALLET_FLAG_DESCRIPTORS},
133135
{"external_signer", WALLET_FLAG_EXTERNAL_SIGNER}
@@ -476,6 +478,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
476478
//! Upgrade stored CKeyMetadata objects to store key origin info as KeyOriginInfo
477479
void UpgradeKeyMetadata() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
478480

481+
//! Upgrade DescriptorCaches
482+
void UpgradeDescriptorCache() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
483+
479484
bool LoadMinVersion(int nVersion) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; return true; }
480485

481486
//! Adds a destination data tuple to the store, without saving it to disk

src/wallet/walletdb.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const std::string TX{"tx"};
5252
const std::string VERSION{"version"};
5353
const std::string WALLETDESCRIPTOR{"walletdescriptor"};
5454
const std::string WALLETDESCRIPTORCACHE{"walletdescriptorcache"};
55+
const std::string WALLETDESCRIPTORLHCACHE{"walletdescriptorlhcache"};
5556
const std::string WALLETDESCRIPTORCKEY{"walletdescriptorckey"};
5657
const std::string WALLETDESCRIPTORKEY{"walletdescriptorkey"};
5758
const std::string WATCHMETA{"watchmeta"};
@@ -248,6 +249,35 @@ bool WalletBatch::WriteDescriptorParentCache(const CExtPubKey& xpub, const uint2
248249
return WriteIC(std::make_pair(std::make_pair(DBKeys::WALLETDESCRIPTORCACHE, desc_id), key_exp_index), ser_xpub);
249250
}
250251

252+
bool WalletBatch::WriteDescriptorLastHardenedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index)
253+
{
254+
std::vector<unsigned char> ser_xpub(BIP32_EXTKEY_SIZE);
255+
xpub.Encode(ser_xpub.data());
256+
return WriteIC(std::make_pair(std::make_pair(DBKeys::WALLETDESCRIPTORLHCACHE, desc_id), key_exp_index), ser_xpub);
257+
}
258+
259+
bool WalletBatch::WriteDescriptorCacheItems(const uint256& desc_id, const DescriptorCache& cache)
260+
{
261+
for (const auto& parent_xpub_pair : cache.GetCachedParentExtPubKeys()) {
262+
if (!WriteDescriptorParentCache(parent_xpub_pair.second, desc_id, parent_xpub_pair.first)) {
263+
return false;
264+
}
265+
}
266+
for (const auto& derived_xpub_map_pair : cache.GetCachedDerivedExtPubKeys()) {
267+
for (const auto& derived_xpub_pair : derived_xpub_map_pair.second) {
268+
if (!WriteDescriptorDerivedCache(derived_xpub_pair.second, desc_id, derived_xpub_map_pair.first, derived_xpub_pair.first)) {
269+
return false;
270+
}
271+
}
272+
}
273+
for (const auto& lh_xpub_pair : cache.GetCachedLastHardenedExtPubKeys()) {
274+
if (!WriteDescriptorLastHardenedCache(lh_xpub_pair.second, desc_id, lh_xpub_pair.first)) {
275+
return false;
276+
}
277+
}
278+
return true;
279+
}
280+
251281
class CWalletScanState {
252282
public:
253283
unsigned int nKeys{0};
@@ -602,6 +632,17 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
602632
} else {
603633
wss.m_descriptor_caches[desc_id].CacheDerivedExtPubKey(key_exp_index, der_index, xpub);
604634
}
635+
} else if (strType == DBKeys::WALLETDESCRIPTORLHCACHE) {
636+
uint256 desc_id;
637+
uint32_t key_exp_index;
638+
ssKey >> desc_id;
639+
ssKey >> key_exp_index;
640+
641+
std::vector<unsigned char> ser_xpub(BIP32_EXTKEY_SIZE);
642+
ssValue >> ser_xpub;
643+
CExtPubKey xpub;
644+
xpub.Decode(ser_xpub.data());
645+
wss.m_descriptor_caches[desc_id].CacheLastHardenedExtPubKey(key_exp_index, xpub);
605646
} else if (strType == DBKeys::WALLETDESCRIPTORKEY) {
606647
uint256 desc_id;
607648
CPubKey pubkey;
@@ -843,6 +884,14 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
843884
result = DBErrors::CORRUPT;
844885
}
845886

887+
// Upgrade all of the descriptor caches to cache the last hardened xpub
888+
// This operation is not atomic, but if it fails, only new entries are added so it is backwards compatible
889+
try {
890+
pwallet->UpgradeDescriptorCache();
891+
} catch (...) {
892+
result = DBErrors::CORRUPT;
893+
}
894+
846895
// Set the inactive chain
847896
if (wss.m_hd_chains.size() > 0) {
848897
LegacyScriptPubKeyMan* legacy_spkm = pwallet->GetLegacyScriptPubKeyMan();

0 commit comments

Comments
 (0)