Skip to content

Commit

Permalink
Merge bitcoin#21329: descriptor wallet: Cache last hardened xpub and …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
meshcollider authored and knst committed May 8, 2024
1 parent edeafde commit dc92bb1
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 94 deletions.
179 changes: 129 additions & 50 deletions src/script/descriptor.cpp

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion src/script/descriptor.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class DescriptorCache {
std::unordered_map<uint32_t, ExtPubKeyMap> m_derived_xpubs;
/** Map key expression index -> parent xpub */
ExtPubKeyMap m_parent_xpubs;
/** Map key expression index -> last hardened xpub */
ExtPubKeyMap m_last_hardened_xpubs;

public:
/** Cache a parent xpub
Expand Down Expand Up @@ -50,11 +52,30 @@ class DescriptorCache {
* @param[in] xpub The CExtPubKey to get from cache
*/
bool GetCachedDerivedExtPubKey(uint32_t key_exp_pos, uint32_t der_index, CExtPubKey& xpub) const;
/** Cache a last hardened xpub
*
* @param[in] key_exp_pos Position of the key expression within the descriptor
* @param[in] xpub The CExtPubKey to cache
*/
void CacheLastHardenedExtPubKey(uint32_t key_exp_pos, const CExtPubKey& xpub);
/** Retrieve a cached last hardened xpub
*
* @param[in] key_exp_pos Position of the key expression within the descriptor
* @param[in] xpub The CExtPubKey to get from cache
*/
bool GetCachedLastHardenedExtPubKey(uint32_t key_exp_pos, CExtPubKey& xpub) const;

/** Retrieve all cached parent xpubs */
const ExtPubKeyMap GetCachedParentExtPubKeys() const;
/** Retrieve all cached derived xpubs */
const std::unordered_map<uint32_t, ExtPubKeyMap> GetCachedDerivedExtPubKeys() const;
/** Retrieve all cached last hardened xpubs */
const ExtPubKeyMap GetCachedLastHardenedExtPubKeys() const;

/** Combine another DescriptorCache into this one.
* Returns a cache containing the items from the other cache unknown to current cache
*/
DescriptorCache MergeAndDiff(const DescriptorCache& other);
};

/** \brief Interface for parsed descriptor objects.
Expand Down Expand Up @@ -94,7 +115,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, const DescriptorCache* cache = nullptr) const = 0;

/** Expand a descriptor at a specified position.
*
Expand Down
8 changes: 2 additions & 6 deletions src/test/descriptor_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,10 @@ void DoCheck(const std::string& prv, const std::string& pub, const std::string&

// Check that private can produce the normalized descriptors
std::string norm1;
BOOST_CHECK(parse_priv->ToNormalizedString(keys_priv, norm1, false));
BOOST_CHECK(parse_priv->ToNormalizedString(keys_priv, norm1));
BOOST_CHECK(EqualDescriptor(norm1, norm_pub));
BOOST_CHECK(parse_pub->ToNormalizedString(keys_priv, norm1, false));
BOOST_CHECK(parse_pub->ToNormalizedString(keys_priv, norm1));
BOOST_CHECK(EqualDescriptor(norm1, norm_pub));
BOOST_CHECK(parse_priv->ToNormalizedString(keys_priv, norm1, true));
BOOST_CHECK(EqualDescriptor(norm1, norm_prv));
BOOST_CHECK(parse_pub->ToNormalizedString(keys_priv, norm1, true));
BOOST_CHECK(EqualDescriptor(norm1, norm_prv));

// Check whether IsRange on both returns the expected result
BOOST_CHECK_EQUAL(parse_pub->IsRange(), (flags & RANGE) != 0);
Expand Down
4 changes: 1 addition & 3 deletions src/wallet/rpcdump.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1972,8 +1972,6 @@ RPCHelpMan listdescriptors()
throw JSONRPCError(RPC_WALLET_ERROR, "listdescriptors is not available for non-descriptor wallets");
}

EnsureWalletIsUnlocked(wallet.get());

LOCK(wallet->cs_wallet);

UniValue descriptors(UniValue::VARR);
Expand All @@ -1987,7 +1985,7 @@ RPCHelpMan listdescriptors()
LOCK(desc_spk_man->cs_desc_man);
const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor();
std::string descriptor;
if (!desc_spk_man->GetDescriptorString(descriptor, false)) {
if (!desc_spk_man->GetDescriptorString(descriptor)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Can't get normalized descriptor string.");
}
spk.pushKV("desc", descriptor);
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/rpcwallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3936,7 +3936,7 @@ RPCHelpMan getaddressinfo()
DescriptorScriptPubKeyMan* desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(pwallet->GetScriptPubKeyMan(scriptPubKey));
if (desc_spk_man) {
std::string desc_str;
if (desc_spk_man->GetDescriptorString(desc_str, false)) {
if (desc_spk_man->GetDescriptorString(desc_str)) {
ret.pushKV("parent_desc", desc_str);
}
}
Expand Down
66 changes: 34 additions & 32 deletions src/wallet/scriptpubkeyman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1950,34 +1950,10 @@ bool DescriptorScriptPubKeyMan::TopUp(unsigned int size)
}
m_map_pubkeys[pubkey] = i;
}
// Write the cache
for (const auto& parent_xpub_pair : temp_cache.GetCachedParentExtPubKeys()) {
CExtPubKey xpub;
if (m_wallet_descriptor.cache.GetCachedParentExtPubKey(parent_xpub_pair.first, xpub)) {
if (xpub != parent_xpub_pair.second) {
throw std::runtime_error(std::string(__func__) + ": New cached parent xpub does not match already cached parent xpub");
}
continue;
}
if (!batch.WriteDescriptorParentCache(parent_xpub_pair.second, id, parent_xpub_pair.first)) {
throw std::runtime_error(std::string(__func__) + ": writing cache item failed");
}
m_wallet_descriptor.cache.CacheParentExtPubKey(parent_xpub_pair.first, parent_xpub_pair.second);
}
for (const auto& derived_xpub_map_pair : temp_cache.GetCachedDerivedExtPubKeys()) {
for (const auto& derived_xpub_pair : derived_xpub_map_pair.second) {
CExtPubKey xpub;
if (m_wallet_descriptor.cache.GetCachedDerivedExtPubKey(derived_xpub_map_pair.first, derived_xpub_pair.first, xpub)) {
if (xpub != derived_xpub_pair.second) {
throw std::runtime_error(std::string(__func__) + ": New cached derived xpub does not match already cached derived xpub");
}
continue;
}
if (!batch.WriteDescriptorDerivedCache(derived_xpub_pair.second, id, derived_xpub_map_pair.first, derived_xpub_pair.first)) {
throw std::runtime_error(std::string(__func__) + ": writing cache item failed");
}
m_wallet_descriptor.cache.CacheDerivedExtPubKey(derived_xpub_map_pair.first, derived_xpub_pair.first, derived_xpub_pair.second);
}
// Merge and write the cache
DescriptorCache new_items = m_wallet_descriptor.cache.MergeAndDiff(temp_cache);
if (!batch.WriteDescriptorCacheItems(id, new_items)) {
throw std::runtime_error(std::string(__func__) + ": writing cache items failed");
}
m_max_cached_index++;
}
Expand Down Expand Up @@ -2402,15 +2378,41 @@ const std::vector<CScript> DescriptorScriptPubKeyMan::GetScriptPubKeys() const
return script_pub_keys;
}

bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out, bool priv) const
bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out) const
{
LOCK(cs_desc_man);
if (m_storage.IsLocked()) {
return false;

FlatSigningProvider provider;
provider.keys = GetKeys();

return m_wallet_descriptor.descriptor->ToNormalizedString(provider, out, &m_wallet_descriptor.cache);
}

void DescriptorScriptPubKeyMan::UpgradeDescriptorCache()
{
LOCK(cs_desc_man);
if (m_storage.IsLocked() || m_storage.IsWalletFlagSet(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED)) {
return;
}

// Skip if we have the last hardened xpub cache
if (m_wallet_descriptor.cache.GetCachedLastHardenedExtPubKeys().size() > 0) {
return;
}

// Expand the descriptor
FlatSigningProvider provider;
provider.keys = GetKeys();
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)){
throw std::runtime_error("Unable to expand descriptor");
}

return m_wallet_descriptor.descriptor->ToNormalizedString(provider, out, priv);
// Cache the last hardened xpubs
DescriptorCache diff = m_wallet_descriptor.cache.MergeAndDiff(temp_cache);
if (!WalletBatch(m_storage.GetDatabase()).WriteDescriptorCacheItems(GetID(), diff)) {
throw std::runtime_error(std::string(__func__) + ": writing cache items failed");
}
}
4 changes: 3 additions & 1 deletion src/wallet/scriptpubkeyman.h
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,9 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
const std::vector<CScript> GetScriptPubKeys() const;

bool GetDescriptorString(std::string& out, bool priv) const;
bool GetDescriptorString(std::string& out) const;

void UpgradeDescriptorCache();
};

#endif // BITCOIN_WALLET_SCRIPTPUBKEYMAN_H
13 changes: 13 additions & 0 deletions src/wallet/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,19 @@ void CWallet::UpgradeKeyMetadata()
SetWalletFlag(WALLET_FLAG_KEY_ORIGIN_METADATA);
}

void CWallet::UpgradeDescriptorCache()
{
if (!IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS) || IsLocked() || IsWalletFlagSet(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED)) {
return;
}

for (ScriptPubKeyMan* spkm : GetAllScriptPubKeyMans()) {
DescriptorScriptPubKeyMan* desc_spkm = dynamic_cast<DescriptorScriptPubKeyMan*>(spkm);
desc_spkm->UpgradeDescriptorCache();
}
SetWalletFlag(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED);
}

bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, const SecureString& strNewWalletPassphrase)
{
bool fWasLocked = IsLocked(true);
Expand Down
5 changes: 5 additions & 0 deletions src/wallet/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ static constexpr uint64_t KNOWN_WALLET_FLAGS =
WALLET_FLAG_AVOID_REUSE
| WALLET_FLAG_BLANK_WALLET
| WALLET_FLAG_KEY_ORIGIN_METADATA
| WALLET_FLAG_LAST_HARDENED_XPUB_CACHED
| WALLET_FLAG_DISABLE_PRIVATE_KEYS
| WALLET_FLAG_DESCRIPTORS;

Expand All @@ -138,6 +139,7 @@ static const std::map<std::string,WalletFlags> WALLET_FLAG_MAP{
{"avoid_reuse", WALLET_FLAG_AVOID_REUSE},
{"blank", WALLET_FLAG_BLANK_WALLET},
{"key_origin_metadata", WALLET_FLAG_KEY_ORIGIN_METADATA},
{"last_hardened_xpub_cached", WALLET_FLAG_LAST_HARDENED_XPUB_CACHED},
{"disable_private_keys", WALLET_FLAG_DISABLE_PRIVATE_KEYS},
{"descriptor_wallet", WALLET_FLAG_DESCRIPTORS},
};
Expand Down Expand Up @@ -978,6 +980,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
//! Upgrade stored CKeyMetadata objects to store key origin info as KeyOriginInfo
void UpgradeKeyMetadata() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

//! Upgrade DescriptorCaches
void UpgradeDescriptorCache() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

bool LoadMinVersion(int nVersion) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; return true; }

//! Adds a destination data tuple to the store, without saving it to disk
Expand Down
49 changes: 49 additions & 0 deletions src/wallet/walletdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,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"};
const std::string WALLETDESCRIPTORCKEY{"walletdescriptorckey"};
const std::string WALLETDESCRIPTORKEY{"walletdescriptorkey"};
const std::string WATCHMETA{"watchmeta"};
Expand Down Expand Up @@ -272,6 +273,35 @@ bool WalletBatch::WriteDescriptorParentCache(const CExtPubKey& xpub, const uint2
return WriteIC(std::make_pair(std::make_pair(DBKeys::WALLETDESCRIPTORCACHE, desc_id), key_exp_index), ser_xpub);
}

bool WalletBatch::WriteDescriptorLastHardenedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index)
{
std::vector<unsigned char> ser_xpub(BIP32_EXTKEY_SIZE);
xpub.Encode(ser_xpub.data());
return WriteIC(std::make_pair(std::make_pair(DBKeys::WALLETDESCRIPTORLHCACHE, desc_id), key_exp_index), ser_xpub);
}

bool WalletBatch::WriteDescriptorCacheItems(const uint256& desc_id, const DescriptorCache& cache)
{
for (const auto& parent_xpub_pair : cache.GetCachedParentExtPubKeys()) {
if (!WriteDescriptorParentCache(parent_xpub_pair.second, desc_id, parent_xpub_pair.first)) {
return false;
}
}
for (const auto& derived_xpub_map_pair : cache.GetCachedDerivedExtPubKeys()) {
for (const auto& derived_xpub_pair : derived_xpub_map_pair.second) {
if (!WriteDescriptorDerivedCache(derived_xpub_pair.second, desc_id, derived_xpub_map_pair.first, derived_xpub_pair.first)) {
return false;
}
}
}
for (const auto& lh_xpub_pair : cache.GetCachedLastHardenedExtPubKeys()) {
if (!WriteDescriptorLastHardenedCache(lh_xpub_pair.second, desc_id, lh_xpub_pair.first)) {
return false;
}
}
return true;
}

class CWalletScanState {
public:
unsigned int nKeys{0};
Expand Down Expand Up @@ -610,6 +640,17 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
} else {
wss.m_descriptor_caches[desc_id].CacheDerivedExtPubKey(key_exp_index, der_index, xpub);
}
} else if (strType == DBKeys::WALLETDESCRIPTORLHCACHE) {
uint256 desc_id;
uint32_t key_exp_index;
ssKey >> desc_id;
ssKey >> key_exp_index;

std::vector<unsigned char> ser_xpub(BIP32_EXTKEY_SIZE);
ssValue >> ser_xpub;
CExtPubKey xpub;
xpub.Decode(ser_xpub.data());
wss.m_descriptor_caches[desc_id].CacheLastHardenedExtPubKey(key_exp_index, xpub);
} else if (strType == DBKeys::WALLETDESCRIPTORKEY) {
uint256 desc_id;
CPubKey pubkey;
Expand Down Expand Up @@ -851,6 +892,14 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
result = DBErrors::CORRUPT;
}

// Upgrade all of the descriptor caches to cache the last hardened xpub
// This operation is not atomic, but if it fails, only new entries are added so it is backwards compatible
try {
pwallet->UpgradeDescriptorCache();
} catch (...) {
result = DBErrors::CORRUPT;
}

return result;
}

Expand Down
2 changes: 2 additions & 0 deletions src/wallet/walletdb.h
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ class WalletBatch
bool WriteDescriptor(const uint256& desc_id, const WalletDescriptor& descriptor);
bool WriteDescriptorDerivedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index, uint32_t der_index);
bool WriteDescriptorParentCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index);
bool WriteDescriptorLastHardenedCache(const CExtPubKey& xpub, const uint256& desc_id, uint32_t key_exp_index);
bool WriteDescriptorCacheItems(const uint256& desc_id, const DescriptorCache& cache);

/// Write destination data key,value tuple to database
bool WriteDestData(const std::string &address, const std::string &key, const std::string &value);
Expand Down
3 changes: 3 additions & 0 deletions src/wallet/walletutil.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ enum WalletFlags : uint64_t {
// Indicates that the metadata has already been upgraded to contain key origins
WALLET_FLAG_KEY_ORIGIN_METADATA = (1ULL << 1),

// Indicates that the descriptor cache has been upgraded to cache last hardened xpubs
WALLET_FLAG_LAST_HARDENED_XPUB_CACHED = (1ULL << 2),

// will enforce the rule that the wallet can't contain any private keys (only watch-only/pubkeys)
WALLET_FLAG_DISABLE_PRIVATE_KEYS = (1ULL << 32),

Expand Down
4 changes: 4 additions & 0 deletions test/functional/wallet_listdescriptors.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ def run_test(self):
}
assert_equal(expected, wallet.listdescriptors())

self.log.info("Test listdescriptors with encrypted wallet")
wallet.encryptwallet("pass")
assert_equal(expected, wallet.listdescriptors())

self.log.info('Test non-active non-range combo descriptor')
node.createwallet(wallet_name='w4', blank=True, descriptors=True)
wallet = node.get_wallet_rpc('w4')
Expand Down

0 comments on commit dc92bb1

Please sign in to comment.