diff --git a/src/pubkey.h b/src/pubkey.h --- a/src/pubkey.h +++ b/src/pubkey.h @@ -202,6 +202,10 @@ a.pubkey == b.pubkey; } + friend bool operator!=(const CExtPubKey &a, const CExtPubKey &b) { + return !(a == b); + } + void Encode(uint8_t code[BIP32_EXTKEY_SIZE]) const; void Decode(const uint8_t code[BIP32_EXTKEY_SIZE]); bool Derive(CExtPubKey &out, unsigned int nChild) const; diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -630,6 +630,7 @@ using KeyMap = std::map; ScriptPubKeyMap m_map_script_pub_keys GUARDED_BY(cs_desc_man); + int32_t m_max_cached_index = -1; OutputType m_address_type; bool m_internal; @@ -645,6 +646,8 @@ bool AddDescriptorKeyWithDB(WalletBatch &batch, const CKey &key, const CPubKey &pubkey); + KeyMap GetKeys() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man); + public: DescriptorScriptPubKeyMan(WalletStorage &storage, WalletDescriptor &descriptor) @@ -671,6 +674,11 @@ void ReturnDestination(int64_t index, bool internal, const CTxDestination &addr) override; + // Tops up the descriptor cache and m_map_script_pub_keys. The cache is + // stored in the wallet file and is used to expand the descriptor in + // GetNewDestination. DescriptorScriptPubKeyMan relies more on ephemeral + // data than LegacyScriptPubKeyMan. For wallets using unhardened derivation + // (with or without private keys), the "keypool" is a single xpub. bool TopUp(unsigned int size = 0) override; void MarkUnusedAddresses(const CScript &script) override; diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -1554,8 +1554,125 @@ void DescriptorScriptPubKeyMan::ReturnDestination(int64_t index, bool internal, const CTxDestination &addr) {} +std::map DescriptorScriptPubKeyMan::GetKeys() const { + AssertLockHeld(cs_desc_man); + if (m_storage.HasEncryptionKeys() && !m_storage.IsLocked()) { + KeyMap keys; + for (auto key_pair : m_map_crypted_keys) { + const CPubKey &pubkey = key_pair.second.first; + const std::vector &crypted_secret = key_pair.second.second; + CKey key; + DecryptKey(m_storage.GetEncryptionKey(), crypted_secret, pubkey, + key); + keys[pubkey.GetID()] = key; + } + return keys; + } + return m_map_keys; +} + bool DescriptorScriptPubKeyMan::TopUp(unsigned int size) { - return false; + LOCK(cs_desc_man); + unsigned int target_size; + if (size > 0) { + target_size = size; + } else { + target_size = std::max(gArgs.GetArg("-keypool", DEFAULT_KEYPOOL_SIZE), + int64_t(1)); + } + + // Calculate the new range_end + int32_t new_range_end = + std::max(m_wallet_descriptor.next_index + int32_t(target_size), + m_wallet_descriptor.range_end); + + // If the descriptor is not ranged, we actually just want to fill the first + // cache item + if (!m_wallet_descriptor.descriptor->IsRange()) { + new_range_end = 1; + m_wallet_descriptor.range_end = 1; + m_wallet_descriptor.range_start = 0; + } + + FlatSigningProvider provider; + provider.keys = GetKeys(); + + WalletBatch batch(m_storage.GetDatabase()); + uint256 id = GetID(); + for (int32_t i = m_max_cached_index + 1; i < new_range_end; ++i) { + FlatSigningProvider out_keys; + std::vector scripts_temp; + DescriptorCache temp_cache; + // Maybe we have a cached xpub and we can expand from the cache first + if (!m_wallet_descriptor.descriptor->ExpandFromCache( + i, m_wallet_descriptor.cache, scripts_temp, out_keys)) { + if (!m_wallet_descriptor.descriptor->Expand( + i, provider, scripts_temp, out_keys, &temp_cache)) { + return false; + } + } + // Add all of the scriptPubKeys to the scriptPubKey set + for (const CScript &script : scripts_temp) { + m_map_script_pub_keys[script] = 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); + } + } + m_max_cached_index++; + } + m_wallet_descriptor.range_end = new_range_end; + batch.WriteDescriptor(GetID(), m_wallet_descriptor); + + // By this point, the cache size should be the size of the entire range + assert(m_wallet_descriptor.range_end - 1 == m_max_cached_index); + + NotifyCanGetAddressesChanged(); + return true; } void DescriptorScriptPubKeyMan::MarkUnusedAddresses(const CScript &script) { @@ -1778,6 +1895,7 @@ } m_map_script_pub_keys[script] = i; } + m_max_cached_index++; } } diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -222,6 +222,13 @@ const std::vector &secret); 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); /// Write destination data key,value tuple to database. bool WriteDestData(const CTxDestination &address, const std::string &key, diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -238,6 +238,29 @@ return WriteIC(make_pair(DBKeys::WALLETDESCRIPTOR, desc_id), descriptor); } +bool WalletBatch::WriteDescriptorDerivedCache(const CExtPubKey &xpub, + const uint256 &desc_id, + uint32_t key_exp_index, + uint32_t der_index) { + std::vector ser_xpub(BIP32_EXTKEY_SIZE); + xpub.Encode(ser_xpub.data()); + return WriteIC( + std::make_pair(std::make_pair(DBKeys::WALLETDESCRIPTORCACHE, desc_id), + std::make_pair(key_exp_index, der_index)), + ser_xpub); +} + +bool WalletBatch::WriteDescriptorParentCache(const CExtPubKey &xpub, + const uint256 &desc_id, + uint32_t key_exp_index) { + std::vector ser_xpub(BIP32_EXTKEY_SIZE); + xpub.Encode(ser_xpub.data()); + return WriteIC( + std::make_pair(std::make_pair(DBKeys::WALLETDESCRIPTORCACHE, desc_id), + key_exp_index), + ser_xpub); +} + class CWalletScanState { public: unsigned int nKeys{0};