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

descriptor wallet: Cache last hardened xpub and use in normalized descriptors #21329

Closed
wants to merge 10 commits into from

Conversation

achow101
Copy link
Member

@achow101 achow101 commented Mar 1, 2021

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

src/script/descriptor.cpp Outdated Show resolved Hide resolved
@DrahtBot
Copy link
Contributor

DrahtBot commented Mar 2, 2021

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Conflicts

Reviewers, 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.

@S3RK
Copy link
Contributor

S3RK commented Mar 9, 2021

Started reviewing this

Copy link
Contributor

@S3RK S3RK left a 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) {
Copy link
Contributor

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

Copy link
Member Author

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.

Copy link
Contributor

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.

@@ -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;
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

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)) {
Copy link
Contributor

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.

Copy link
Member Author

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;

Copy link
Contributor

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

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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)){
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

@S3RK S3RK left a 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.

}
continue;
}
if (!batch.WriteDescriptorLastHardenedCache(lh_xpub_pair.second, id, lh_xpub_pair.first)) {
Copy link
Contributor

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?

Copy link
Member Author

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 ScriptPubKeyMans, 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)){
Copy link
Contributor

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) {
Copy link
Contributor

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.

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)) {
Copy link
Contributor

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.

@S3RK
Copy link
Contributor

S3RK commented Apr 21, 2021

reACK ec22064. Just rebased on top of latest master

My comment above is still relevant though.

@achow101
Copy link
Member Author

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.

What direction do you think it should be going in?

@S3RK
Copy link
Contributor

S3RK commented Apr 22, 2021

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:

  1. for ToStringHelper we can a) avoid multiple flags controlling the control flow b) shift non-general code to child classes (addr, raw, multisig).

To be more specific for this PR: I think it's better to have ToNormalizedString reimplement some logic and not call ToStringHelper at all. Both functions would be easier to reason about. We also don't need to support normalized private descriptors case, no code actually uses this.

  1. for DescriptorScriptPubKeyMan::TopUp we can keep the code on the same/constant level of abstraction by better encapsulating cache structure. One option is to introduce something like DescriptorCache::CopyFrom(DescriptorCache) and WalletBatch::WriteCache(DescriptorCache).

@achow101
Copy link
Member Author

To be more specific for this PR: I think it's better to have ToNormalizedString reimplement some logic and not call ToStringHelper at all. Both functions would be easier to reason about. We also don't need to support normalized private descriptors case, no code actually uses this.

I kind of disagree. All of the To*String functions do roughly the same thing. The construction of all of the non-key related parts are all the same and it doesn't make sense to me to be duplicating that. The only parts that are different are the key things, which is why ToStringHelper has options in order to determine which key function to use. I think that instead of using bools, it would be better to use an enum, so I have implemented it that way.

for DescriptorScriptPubKeyMan::TopUp we can keep the code on the same/constant level of abstraction by better encapsulating cache structure. One option is to introduce something like DescriptorCache::CopyFrom(DescriptorCache) and WalletBatch::WriteCache(DescriptorCache).

I've added DescriptorCache::MergeAndDiff which merges another DescriptorCache with the current DescriptorCache and returns another DescriptorCache containing a copy of the new items. I've also added WalletBatch::WriteDescriptorCache which takes a DescriptorCache and writes all of the items. This cleans up TopUp and deduplicates some code.

@S3RK
Copy link
Contributor

S3RK commented Apr 26, 2021

utACK e668b9e. Thanks for incorporating my suggestions.

@meshcollider meshcollider added this to the 22.0 milestone Jun 3, 2021
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.
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.
Copy link
Contributor

@jonatack jonatack left a 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;
Copy link
Contributor

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)) {
Copy link
Contributor

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

Suggested change
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);
Copy link
Contributor

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"};
Copy link
Contributor

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"

Suggested change
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) {
Copy link
Contributor

@jonatack jonatack Jun 26, 2021

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)) {
Copy link
Contributor

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)) {
Copy link
Contributor

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;
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

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

3280704

Suggested change
// Cache miss, or nor cache, or need privkey
// Cache miss, or no cache, or need privkey

@fjahr
Copy link
Contributor

fjahr commented Jun 27, 2021

tACK e6cf0ed

Reviewed code, light manual testing with restarts to ensure persistence works (could have been covered by a test case as well).

nit: in d87b544 there is a typo in commit description: "hardenex"

@achow101
Copy link
Member Author

I will implement the suggestions if I have to retouch.

@S3RK
Copy link
Contributor

S3RK commented Jun 30, 2021

reACK e6cf0ed

Changes from last review: 1) PubkeyProvider::GetPubKey became const 2) necessary changes for taproot descriptors

Happy to reACK again if you want to incorporate jonatack's readability suggestions

@fanquake
Copy link
Member

@meshcollider

Copy link
Contributor

@meshcollider meshcollider left a 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

meshcollider added a commit to bitcoin-core/gui that referenced this pull request Jun 30, 2021
…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
@meshcollider
Copy link
Contributor

Nits can be left for follow-up if needed, lets just get this in

@DrahtBot
Copy link
Contributor

🐙 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".

@achow101
Copy link
Member Author

This was merged, but github isn't detecting it.

@jonatack
Copy link
Contributor

There have been several of these, I think this is the third one in as many days.

@achow101 achow101 closed this Jun 30, 2021
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Jul 1, 2021
…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
gwillen pushed a commit to ElementsProject/elements that referenced this pull request Jun 1, 2022
@bitcoin bitcoin locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants