-
Notifications
You must be signed in to change notification settings - Fork 35.6k
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
descriptor wallet: Cache last hardened xpub and use in normalized descriptors #21329
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
c096de4
to
ce42387
Compare
Started reviewing this |
ce42387
to
1cb5017
Compare
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.
Wow, it took more than expected to grasp what's going on. I think I still need more time, but I have a few questions/suggestions now already.
// Derive the xpub at the last hardened step | ||
CExtKey xprv; | ||
if (!GetExtKey(arg, xprv)) return false; | ||
// Get the path to the last hardened stup | ||
KeyOriginInfo origin; | ||
int k = 0; | ||
for (; k <= i; ++k) { |
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.
nit: I think it would be nice to simply this function by extracting the code for splitting KeyPath. We can introduce a member function like:
std::pair<KeyPath, KeyPath> KeyPath::split_unhardened() const;
or
KeyPath KeyPath::split_unhardened();
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.
I don't think that's really necessary since we only do the split in this one spot.
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.
Feel free to leave it as it. But just to clarify my point. It's not about code deduplication, it's about readability and comprehension. There are 3 loops dedicated to splitting derivation path and I think they distract from the main purpose of this function.
src/script/descriptor.h
Outdated
@@ -110,7 +110,7 @@ struct Descriptor { | |||
virtual bool ToPrivateString(const SigningProvider& provider, std::string& out) const = 0; | |||
|
|||
/** 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. */ | |||
virtual bool ToNormalizedString(const SigningProvider& provider, std::string& out, bool priv) const = 0; | |||
virtual bool ToNormalizedString(const SigningProvider& provider, std::string& out, bool priv, const DescriptorCache* cache = nullptr) const = 0; |
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.
Why not pass it as const reference? It can help to remove some of the checks and we can always relax the requirements and make it optional later if needed.
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.
Not every caller of this will necessarily have a cache to pass.
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.
Ok. I don't have strong feelings about it, but right now the only caller besides the unit tests is DescriptorScriptPubKeyMan
. Though I can imagine other possibilities in the future I'd still go with more restrictive signature as it's easier to relax the requirements rather than the other way around.
src/wallet/scriptpubkeyman.cpp
Outdated
uint256 id = GetID(); | ||
for (const auto& lh_xpub_pair : temp_cache.GetCachedLastHardenedExtPubKeys()) { | ||
CExtPubKey xpub; | ||
if (m_wallet_descriptor.cache.GetCachedLastHardenedExtPubKey(lh_xpub_pair.first, xpub)) { |
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.
I can't see how this ever evaluates to true
since we exit early if the cache exists.
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.
I don't follow.
This is checking to see if elements in the temp cache are in the stored cache;
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.
But the stored cache is always empty, no? I'm looking at the line 2303
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.
Ah, I see. There shouldn't be anything in the stored cache, however I am doing this as a belt-and-suspenders check.
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.
I find it confusing when we check same conditions multiple times in different places because it's not clear what is the responsibility/assumptions of each class/function. It also makes it harder to modify the code. DescriptorScriptPubKeyMan
and CWallet
are strongly coupled anyway.
Maybe we can just remove the check on the top of the function? Even in the worst case nothing will go wrong 1) If wallet is locked then expand will fail 2) if the cache exists already we will just regenerate it.
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.
This is largely just code duplicated from TopUp. The intention was to refactor them to avoid the duplication, but I never got around to doing that.
The check at the top of the function is important to avoid unnecessary extra computation which slows down wallet loading and unlocking.
FlatSigningProvider out_keys; | ||
std::vector<CScript> scripts_temp; | ||
DescriptorCache temp_cache; | ||
if (!m_wallet_descriptor.descriptor->Expand(0, provider, scripts_temp, out_keys, &temp_cache)){ |
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.
Why do we need a temp cache?
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.
To avoid modifying the wallet state if something goes wrong during expansion. It is possible that the cache could be modified, but something else causes expansion to fail.
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.
I don't understand the real life scenario. From what I see Expand
fails if we can't get a pub key. For example we tried to derive hardened derivation from the cache or the cache itself was inconsistent. This seems to indicate some logic error in the code itself or a corrupted state. Is my understanding correct or do I miss something?
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.
Consider multi(2,xpub.../0/*,xpub.../0h/*)
. The first xpub is entirely unhardened derivation. So we will have entries added to the cache when it is expanded. When ExpandHelper
moves onto the second xpub, it finds it is entirely hardened derivation. If the keys are unavailable (e.g. they're encrypted), expand now fails here. However the cache now has entries for the first xpub but no entries for the second. While this is not a dangerous state to be in, the in memory state will not be accurately reflected on disk and that is generally something that I think we should avoid. Thus the temp cache is used.
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.
Code review ACK 1cb5017
I don't see any logical errors in the code, it does what it supposed to do.
With that said, I don't like the direction in which DescriptorImpl::ToStringHelper
and DescriptorScriptPubKeyMan::TopUp
is going. The code becomes more and more complicated and there are untapped opportunities for simplification.
I believe refactoring should go together with feature implementation because of the following reasons:
- just a pure refactoring PR by itself will get much less attention and therefore lower chances to get merged
- this is not an urgent feature and it can wait for a refactoring to be implemented
I'm happy to propose, discuss and implement some refactoring ideas if you think it will benefit the project.
src/wallet/scriptpubkeyman.cpp
Outdated
} | ||
continue; | ||
} | ||
if (!batch.WriteDescriptorLastHardenedCache(lh_xpub_pair.second, id, lh_xpub_pair.first)) { |
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.
If this is a new wallet, at what time do we set the WALLET_FLAG_LAST_HARDENED_XPUB_CACHED
flag?
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.
LoadWallet
, which is called as part of wallet creation, will always call CWallet::UpgradeDescriptorCache
. Since a newly created wallet is never encrypted at that stage, and has no ScriptPubKeyMan
s, UpgradeDescriptorCache
will just set the flag.
FlatSigningProvider out_keys; | ||
std::vector<CScript> scripts_temp; | ||
DescriptorCache temp_cache; | ||
if (!m_wallet_descriptor.descriptor->Expand(0, provider, scripts_temp, out_keys, &temp_cache)){ |
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.
I don't understand the real life scenario. From what I see Expand
fails if we can't get a pub key. For example we tried to derive hardened derivation from the cache or the cache itself was inconsistent. This seems to indicate some logic error in the code itself or a corrupted state. Is my understanding correct or do I miss something?
// Derive the xpub at the last hardened step | ||
CExtKey xprv; | ||
if (!GetExtKey(arg, xprv)) return false; | ||
// Get the path to the last hardened stup | ||
KeyOriginInfo origin; | ||
int k = 0; | ||
for (; k <= i; ++k) { |
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.
Feel free to leave it as it. But just to clarify my point. It's not about code deduplication, it's about readability and comprehension. There are 3 loops dedicated to splitting derivation path and I think they distract from the main purpose of this function.
src/wallet/scriptpubkeyman.cpp
Outdated
uint256 id = GetID(); | ||
for (const auto& lh_xpub_pair : temp_cache.GetCachedLastHardenedExtPubKeys()) { | ||
CExtPubKey xpub; | ||
if (m_wallet_descriptor.cache.GetCachedLastHardenedExtPubKey(lh_xpub_pair.first, xpub)) { |
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.
I find it confusing when we check same conditions multiple times in different places because it's not clear what is the responsibility/assumptions of each class/function. It also makes it harder to modify the code. DescriptorScriptPubKeyMan
and CWallet
are strongly coupled anyway.
Maybe we can just remove the check on the top of the function? Even in the worst case nothing will go wrong 1) If wallet is locked then expand will fail 2) if the cache exists already we will just regenerate it.
1cb5017
to
ec22064
Compare
reACK ec22064. Just rebased on top of latest master My comment above is still relevant though. |
What direction do you think it should be going in? |
Thanks for asking. I think we need to try to simplify the code for readability and comprehension. Some rough ideas:
To be more specific for this PR: I think it's better to have
|
I kind of disagree. All of the
I've added |
ec22064
to
e668b9e
Compare
utACK e668b9e. Thanks for incorporating my suggestions. |
e668b9e
to
58dbf65
Compare
This reverts commit 09e2507. The changes made in this commit have turned out to be unnecessary and confusing, so it is being reverted.
Instead of having a large blob of cache merging code in TopUp, refactor this into DescriptorCache so that it can merge and provide a diff (another DescriptorCache containing just the items that were added). Then TopUp can just write everything that was in the diff.
Instead of adhoc writing of the items in DescriptorCache, move it all into WalletBatch.
Cache the last hardenex xpub in the DescriptorCache
Add functions to upgrade existing descriptor caches to support the use of last hardened xpub caching.
58dbf65
to
725fdc2
Compare
Instead of having multiple, possibly conflicting, bools controlling the flow of ToStringHelper, use an enum.
Use the descriptor xpub cache in ToNormalizedString so that the wallet does not need to be unlocked in order to get the normalized descriptor.
With the last hardened xpub cache, we don't neeed to have the wallet be unlocked for listdescriptors.
725fdc2
to
e6cf0ed
Compare
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.
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.
} else if (IsHardened()) { | ||
CExtKey xprv; | ||
if (!GetDerivedExtKey(arg, xprv)) return false; | ||
CExtKey lh_xprv; |
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.
d87b544 consider naming s/lh_xprv/last_hardened_xprv/
as "lh" is a common abbreviation for left-hand and last_hardened_extkey
is a localvar too
} | ||
for (const auto& lh_xpub_pair : other.GetCachedLastHardenedExtPubKeys()) { | ||
CExtPubKey xpub; | ||
if (GetCachedLastHardenedExtPubKey(lh_xpub_pair.first, xpub)) { |
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.
d87b544 could make this easier to quickly understand
if (GetCachedLastHardenedExtPubKey(lh_xpub_pair.first, xpub)) { | |
if (GetCachedLastHardenedExtPubKey(/* key_exp_pos */ lh_xpub_pair.first, xpub)) { |
} | ||
continue; | ||
} | ||
CacheLastHardenedExtPubKey(lh_xpub_pair.first, lh_xpub_pair.second); |
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.
d87b544 It would be nice to make the code easier to quickly understand (using a struct instead of pairs would make the code more self-documenting but is outside the scope here)
- CacheLastHardenedExtPubKey(lh_xpub_pair.first, lh_xpub_pair.second);
- diff.CacheLastHardenedExtPubKey(lh_xpub_pair.first, lh_xpub_pair.second);
+ CacheLastHardenedExtPubKey(
+ /* key_exp_pos */ last_hardened_xpub_pair.first,
+ /* xpub */ last_hardened_xpub_pair.second);
+ diff.CacheLastHardenedExtPubKey(
+ /* key_exp_pos */ last_hardened_xpub_pair.first,
+ /* xpub */ last_hardened_xpub_pair.second);
@@ -52,6 +52,7 @@ const std::string TX{"tx"}; | |||
const std::string VERSION{"version"}; | |||
const std::string WALLETDESCRIPTOR{"walletdescriptor"}; | |||
const std::string WALLETDESCRIPTORCACHE{"walletdescriptorcache"}; | |||
const std::string WALLETDESCRIPTORLHCACHE{"walletdescriptorlhcache"}; |
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.
432ba9e this naming isn't very readable; s/LH/LASTHARDENED/
would be nice and per developer-notes.md, "constant names are all uppercase, and use _
to separate words"
const std::string WALLETDESCRIPTORLHCACHE{"walletdescriptorlhcache"}; | |
const std::string WALLET_DESCRIPTOR_LAST_HARDENED_CACHE{"walletdescriptorlhcache"}; |
{ | ||
if (!GetExtKey(arg, xprv)) return false; | ||
for (auto entry : m_path) { | ||
xprv.Derive(xprv, entry); | ||
if (entry >> 31) { |
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.
d87b544 it would be nice to hoist these 31
values to a well-named constant
} | ||
} | ||
for (const auto& lh_xpub_pair : cache.GetCachedLastHardenedExtPubKeys()) { | ||
if (!WriteDescriptorLastHardenedCache(lh_xpub_pair.second, desc_id, lh_xpub_pair.first)) { |
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.
432ba9e readability suggestion
- for (const auto& lh_xpub_pair : cache.GetCachedLastHardenedExtPubKeys()) {
- if (!WriteDescriptorLastHardenedCache(lh_xpub_pair.second, desc_id, lh_xpub_pair.first)) {
+ for (const auto& last_hardened_xpub_pair : cache.GetCachedLastHardenedExtPubKeys()) {
+ if (!WriteDescriptorLastHardenedCache(/* xpub */ last_hardened_xpub_pair.second, desc_id, /* key_exp_index */ lh_xpub_pair.first)) {
void DescriptorScriptPubKeyMan::UpgradeDescriptorCache() | ||
{ | ||
LOCK(cs_desc_man); | ||
if (m_storage.IsLocked() || m_storage.IsWalletFlagSet(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED)) { |
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.
74fede3 It seems like IsLocked()
is already checked by the callers CWallet::UpgradeDescriptorCache
and CWallet::Unlock
IIUC, but belt-and-suspenders, I suppose.
std::copy(id.begin(), id.begin() + 4, origin.fingerprint); | ||
|
||
CExtPubKey xpub; | ||
CExtKey lh_xprv; |
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.
3280704 naming style, suggest last_hardened_xprv
, as this looks like "left-hand xprv"
cache->GetCachedLastHardenedExtPubKey(m_expr_index, xpub); | ||
} | ||
if (!xpub.pubkey.IsValid()) { | ||
// Cache miss, or nor cache, or need privkey |
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.
// Cache miss, or nor cache, or need privkey | |
// Cache miss, or no cache, or need privkey |
I will implement the suggestions if I have to retouch. |
reACK e6cf0ed Changes from last review: 1) Happy to reACK again if you want to incorporate jonatack's readability suggestions |
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.
Code review + functional test run ACK e6cf0ed
…pub 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
Nits can be left for follow-up if needed, lets just get this in |
🐙 This pull request conflicts with the target branch and needs rebase. Want to unsubscribe from rebase notifications on this pull request? Just convert this pull request to a "draft". |
This was merged, but github isn't detecting it. |
There have been several of these, I think this is the third one in as many days. |
…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
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 ism/84'/0'/0'/0
, and the child keys ofm/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 atm/84'/0'/0'
. So this PR adds another field toDescriptorCache
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 thatUpgradeKeyMetadata
operates. It will use a new wallet flagWALLET_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 andgetaddressinfo
'sparent_desc
will always be output (assuming the upgrade has occurred).