diff --git a/doc/release-notes-6017.md b/doc/release-notes-6017.md new file mode 100644 index 0000000000..03d05a42aa --- /dev/null +++ b/doc/release-notes-6017.md @@ -0,0 +1,3 @@ +RPC changes +----------- +- A new `sethdseed` RPC allows users to initialize their blank HD wallets with an HD seed. **A new backup must be made when a new HD seed is set.** This command cannot replace an existing HD seed if one is already set. `sethdseed` uses WIF private key as a seed. If you have a mnemonic, use the `upgradetohd` RPC. diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index 25d06547ec..26d2acb1e0 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -70,6 +70,7 @@ namespace { const QStringList historyFilter = QStringList() << "importprivkey" << "importmulti" + << "sethdseed" << "signmessagewithprivkey" << "signrawtransactionwithkey" << "upgradetohd" diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 4645854ba6..510ce0443e 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -45,6 +45,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "sendtoaddress", 7, "conf_target" }, { "sendtoaddress", 9, "avoid_reuse" }, { "settxfee", 0, "amount" }, + { "sethdseed", 0, "newkeypool" }, { "getreceivedbyaddress", 1, "minconf" }, { "getreceivedbyaddress", 2, "addlocked" }, { "getreceivedbylabel", 1, "minconf" }, diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index 3664a90db2..cc17ea947a 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -166,7 +166,7 @@ public: * write_cache is the cache to write keys to (if not nullptr) * Caches are not exclusive but this is not tested. Currently we use them exclusively */ - virtual bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info, const DescriptorCache* read_cache = nullptr, DescriptorCache* write_cache = nullptr) = 0; + virtual bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info, const DescriptorCache* read_cache = nullptr, DescriptorCache* write_cache = nullptr) const = 0; /** Whether this represent multiple public keys at different positions. */ virtual bool IsRange() const = 0; @@ -181,7 +181,7 @@ public: virtual bool ToPrivateString(const SigningProvider& arg, std::string& out) const = 0; /** Get the descriptor string form with the xpub at the last hardened derivation */ - virtual bool ToNormalizedString(const SigningProvider& arg, std::string& out, bool priv) const = 0; + virtual bool ToNormalizedString(const SigningProvider& arg, std::string& out, const DescriptorCache* cache = nullptr) const = 0; /** Derive a private key, if private data is available in arg. */ virtual bool GetPrivKey(int pos, const SigningProvider& arg, CKey& key) const = 0; @@ -199,7 +199,7 @@ class OriginPubkeyProvider final : public PubkeyProvider public: OriginPubkeyProvider(uint32_t exp_index, KeyOriginInfo info, std::unique_ptr provider) : PubkeyProvider(exp_index), m_origin(std::move(info)), m_provider(std::move(provider)) {} - bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info, const DescriptorCache* read_cache = nullptr, DescriptorCache* write_cache = nullptr) override + bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info, const DescriptorCache* read_cache = nullptr, DescriptorCache* write_cache = nullptr) const override { if (!m_provider->GetPubKey(pos, arg, key, info, read_cache, write_cache)) return false; std::copy(std::begin(m_origin.fingerprint), std::end(m_origin.fingerprint), info.fingerprint); @@ -216,10 +216,10 @@ public: ret = "[" + OriginString() + "]" + std::move(sub); return true; } - bool ToNormalizedString(const SigningProvider& arg, std::string& ret, bool priv) const override + bool ToNormalizedString(const SigningProvider& arg, std::string& ret, const DescriptorCache* cache) const override { std::string sub; - if (!m_provider->ToNormalizedString(arg, sub, priv)) return false; + if (!m_provider->ToNormalizedString(arg, sub, cache)) return false; // If m_provider is a BIP32PubkeyProvider, we may get a string formatted like a OriginPubkeyProvider // In that case, we need to strip out the leading square bracket and fingerprint from the substring, // and append that to our own origin string. @@ -244,7 +244,7 @@ class ConstPubkeyProvider final : public PubkeyProvider public: ConstPubkeyProvider(uint32_t exp_index, const CPubKey& pubkey) : PubkeyProvider(exp_index), m_pubkey(pubkey) {} - bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info, const DescriptorCache* read_cache = nullptr, DescriptorCache* write_cache = nullptr) override + bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info, const DescriptorCache* read_cache = nullptr, DescriptorCache* write_cache = nullptr) const override { key = m_pubkey; info.path.clear(); @@ -262,9 +262,8 @@ public: ret = EncodeSecret(key); return true; } - bool ToNormalizedString(const SigningProvider& arg, std::string& ret, bool priv) const override + bool ToNormalizedString(const SigningProvider& arg, std::string& ret, const DescriptorCache* cache) const override { - if (priv) return ToPrivateString(arg, ret); ret = ToString(); return true; } @@ -287,9 +286,6 @@ class BIP32PubkeyProvider final : public PubkeyProvider CExtPubKey m_root_extkey; KeyPath m_path; DeriveType m_derive; - // Cache of the parent of the final derived pubkeys. - // Primarily useful for situations when no read_cache is provided - CExtPubKey m_cached_xpub; bool GetExtKey(const SigningProvider& arg, CExtKey& ret) const { @@ -304,11 +300,14 @@ class BIP32PubkeyProvider final : public PubkeyProvider } // Derives the last xprv - bool GetDerivedExtKey(const SigningProvider& arg, CExtKey& xprv) const + bool GetDerivedExtKey(const SigningProvider& arg, CExtKey& xprv, CExtKey& last_hardened) const { if (!GetExtKey(arg, xprv)) return false; for (auto entry : m_path) { xprv.Derive(xprv, entry); + if (entry >> 31) { + last_hardened = xprv; + } } return true; } @@ -326,7 +325,7 @@ public: BIP32PubkeyProvider(uint32_t exp_index, const CExtPubKey& extkey, KeyPath path, DeriveType derive) : PubkeyProvider(exp_index), m_root_extkey(extkey), m_path(std::move(path)), m_derive(derive) {} bool IsRange() const override { return m_derive != DeriveType::NO; } size_t GetSize() const override { return 33; } - bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key_out, KeyOriginInfo& final_info_out, const DescriptorCache* read_cache = nullptr, DescriptorCache* write_cache = nullptr) override + bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key_out, KeyOriginInfo& final_info_out, const DescriptorCache* read_cache = nullptr, DescriptorCache* write_cache = nullptr) const override { // Info of parent of the to be derived pubkey KeyOriginInfo parent_info; @@ -342,6 +341,7 @@ public: // Derive keys or fetch them from cache CExtPubKey final_extkey = m_root_extkey; CExtPubKey parent_extkey = m_root_extkey; + CExtPubKey last_hardened_extkey; bool der = true; if (read_cache) { if (!read_cache->GetCachedDerivedExtPubKey(m_expr_index, pos, final_extkey)) { @@ -351,16 +351,17 @@ public: final_extkey = parent_extkey; if (m_derive == DeriveType::UNHARDENED) der = parent_extkey.Derive(final_extkey, pos); } - } else if (m_cached_xpub.pubkey.IsValid() && m_derive != DeriveType::HARDENED) { - parent_extkey = final_extkey = m_cached_xpub; - if (m_derive == DeriveType::UNHARDENED) der = parent_extkey.Derive(final_extkey, pos); } else if (IsHardened()) { CExtKey xprv; - if (!GetDerivedExtKey(arg, xprv)) return false; + CExtKey lh_xprv; + if (!GetDerivedExtKey(arg, xprv, lh_xprv)) return false; parent_extkey = xprv.Neuter(); if (m_derive == DeriveType::UNHARDENED) der = xprv.Derive(xprv, pos); if (m_derive == DeriveType::HARDENED) der = xprv.Derive(xprv, pos | 0x80000000UL); final_extkey = xprv.Neuter(); + if (lh_xprv.key.IsValid()) { + last_hardened_extkey = lh_xprv.Neuter(); + } } else { for (auto entry : m_path) { der = parent_extkey.Derive(parent_extkey, entry); @@ -375,15 +376,14 @@ public: final_info_out = final_info_out_tmp; key_out = final_extkey.pubkey; - // We rely on the consumer to check that m_derive isn't HARDENED as above - // But we can't have already cached something in case we read something from the cache - // and parent_extkey isn't actually the parent. - if (!m_cached_xpub.pubkey.IsValid()) m_cached_xpub = parent_extkey; - if (write_cache) { // Only cache parent if there is any unhardened derivation if (m_derive != DeriveType::HARDENED) { write_cache->CacheParentExtPubKey(m_expr_index, parent_extkey); + // Cache last hardened xpub if we have it + if (last_hardened_extkey.pubkey.IsValid()) { + write_cache->CacheLastHardenedExtPubKey(m_expr_index, last_hardened_extkey); + } } else if (final_info_out.path.size() > 0) { write_cache->CacheDerivedExtPubKey(m_expr_index, pos, final_extkey); } @@ -411,11 +411,10 @@ public: } return true; } - bool ToNormalizedString(const SigningProvider& arg, std::string& out, bool priv) const override + bool ToNormalizedString(const SigningProvider& arg, std::string& out, const DescriptorCache* cache) const override { // For hardened derivation type, just return the typical string, nothing to normalize if (m_derive == DeriveType::HARDENED) { - if (priv) return ToPrivateString(arg, out); out = ToString(); return true; } @@ -428,33 +427,42 @@ public: } // Either no derivation or all unhardened derivation if (i == -1) { - if (priv) return ToPrivateString(arg, out); out = ToString(); return true; } - // 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) { - // Derive - xprv.Derive(xprv, m_path.at(k)); // Add to the path origin.path.push_back(m_path.at(k)); - // First derivation element, get the fingerprint for origin - if (k == 0) { - std::copy(xprv.vchFingerprint, xprv.vchFingerprint + 4, origin.fingerprint); - } } // Build the remaining path KeyPath end_path; for (; k < (int)m_path.size(); ++k) { end_path.push_back(m_path.at(k)); } + // Get the fingerprint + CKeyID id = m_root_extkey.pubkey.GetID(); + std::copy(id.begin(), id.begin() + 4, origin.fingerprint); + + CExtPubKey xpub; + CExtKey lh_xprv; + // If we have the cache, just get the parent xpub + if (cache != nullptr) { + cache->GetCachedLastHardenedExtPubKey(m_expr_index, xpub); + } + if (!xpub.pubkey.IsValid()) { + // Cache miss, or nor cache, or need privkey + CExtKey xprv; + if (!GetDerivedExtKey(arg, xprv, lh_xprv)) return false; + xpub = lh_xprv.Neuter(); + } + assert(xpub.pubkey.IsValid()); + // Build the string std::string origin_str = HexStr(origin.fingerprint) + FormatHDKeypath(origin.path); - out = "[" + origin_str + "]" + (priv ? EncodeExtKey(xprv) : EncodeExtPubKey(xprv.Neuter())) + FormatHDKeypath(end_path); + out = "[" + origin_str + "]" + EncodeExtPubKey(xpub) + FormatHDKeypath(end_path); if (IsRange()) { out += "/*"; assert(m_derive == DeriveType::UNHARDENED); @@ -464,7 +472,8 @@ public: bool GetPrivKey(int pos, const SigningProvider& arg, CKey& key) const override { CExtKey extkey; - if (!GetDerivedExtKey(arg, extkey)) return false; + CExtKey dummy; + if (!GetDerivedExtKey(arg, extkey, dummy)) return false; if (m_derive == DeriveType::UNHARDENED) extkey.Derive(extkey, pos); if (m_derive == DeriveType::HARDENED) extkey.Derive(extkey, pos | 0x80000000UL); key = extkey.key; @@ -481,34 +490,42 @@ class DescriptorImpl : public Descriptor const std::string m_name; protected: - //! The sub-descriptor argument (nullptr for everything but SH and WSH). + //! The sub-descriptor arguments (empty for everything but SH and WSH). //! In doc/descriptors.m this is referred to as SCRIPT expressions sh(SCRIPT) //! and wsh(SCRIPT), and distinct from KEY expressions and ADDR expressions. - const std::unique_ptr m_subdescriptor_arg; + //! Subdescriptors can only ever generate a single script. + const std::vector> m_subdescriptor_args; //! Return a serialization of anything except pubkey and script arguments, to be prepended to those. virtual std::string ToStringExtra() const { return ""; } /** A helper function to construct the scripts for this descriptor. * - * This function is invoked once for every CScript produced by evaluating - * m_subdescriptor_arg, or just once in case m_subdescriptor_arg is nullptr. - + * This function is invoked once by ExpandHelper. + * * @param pubkeys The evaluations of the m_pubkey_args field. - * @param scripts The evaluation of m_subdescriptor_arg (or nullptr when m_subdescriptor_arg is nullptr). + * @param scripts The evaluations of m_subdescriptor_args (one for each m_subdescriptor_args element). * @param out A FlatSigningProvider to put scripts or public keys in that are necessary to the solver. - * The script arguments to this function are automatically added, as is the origin info of the provided pubkeys. + * The origin info of the provided pubkeys is automatically added. * @return A vector with scriptPubKeys for this descriptor. */ - virtual std::vector MakeScripts(const std::vector& pubkeys, const CScript* script, FlatSigningProvider& out) const = 0; + virtual std::vector MakeScripts(const std::vector& pubkeys, Span scripts, FlatSigningProvider& out) const = 0; public: - DescriptorImpl(std::vector> pubkeys, std::unique_ptr script, const std::string& name) : m_pubkey_args(std::move(pubkeys)), m_name(name), m_subdescriptor_arg(std::move(script)) {} + DescriptorImpl(std::vector> pubkeys, const std::string& name) : m_pubkey_args(std::move(pubkeys)), m_name(name), m_subdescriptor_args() {} + DescriptorImpl(std::vector> pubkeys, std::unique_ptr script, const std::string& name) : m_pubkey_args(std::move(pubkeys)), m_name(name), m_subdescriptor_args(Vector(std::move(script))) {} + + enum class StringType + { + PUBLIC, + PRIVATE, + NORMALIZED, + }; bool IsSolvable() const override { - if (m_subdescriptor_arg) { - if (!m_subdescriptor_arg->IsSolvable()) return false; + for (const auto& arg : m_subdescriptor_args) { + if (!arg->IsSolvable()) return false; } return true; } @@ -518,13 +535,25 @@ public: for (const auto& pubkey : m_pubkey_args) { if (pubkey->IsRange()) return true; } - if (m_subdescriptor_arg) { - if (m_subdescriptor_arg->IsRange()) return true; + for (const auto& arg : m_subdescriptor_args) { + if (arg->IsRange()) return true; } return false; } - bool ToStringHelper(const SigningProvider* arg, std::string& out, bool priv, bool normalized) const + virtual bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, const DescriptorCache* cache = nullptr) const + { + size_t pos = 0; + for (const auto& scriptarg : m_subdescriptor_args) { + if (pos++) ret += ","; + std::string tmp; + if (!scriptarg->ToStringHelper(arg, tmp, type, cache)) return false; + ret += std::move(tmp); + } + return true; + } + + bool ToStringHelper(const SigningProvider* arg, std::string& out, const StringType type, const DescriptorCache* cache = nullptr) const { std::string extra = ToStringExtra(); size_t pos = extra.size() > 0 ? 1 : 0; @@ -532,42 +561,43 @@ public: for (const auto& pubkey : m_pubkey_args) { if (pos++) ret += ","; std::string tmp; - if (normalized) { - if (!pubkey->ToNormalizedString(*arg, tmp, priv)) return false; - } else if (priv) { - if (!pubkey->ToPrivateString(*arg, tmp)) return false; - } else { - tmp = pubkey->ToString(); + switch (type) { + case StringType::NORMALIZED: + if (!pubkey->ToNormalizedString(*arg, tmp, cache)) return false; + break; + case StringType::PRIVATE: + if (!pubkey->ToPrivateString(*arg, tmp)) return false; + break; + case StringType::PUBLIC: + tmp = pubkey->ToString(); + break; } ret += std::move(tmp); } - if (m_subdescriptor_arg) { - if (pos++) ret += ","; - std::string tmp; - if (!m_subdescriptor_arg->ToStringHelper(arg, tmp, priv, normalized)) return false; - ret += std::move(tmp); - } - out = std::move(ret) + ")"; + std::string subscript; + if (!ToStringSubScriptHelper(arg, subscript, type, cache)) return false; + if (pos && subscript.size()) ret += ','; + out = std::move(ret) + std::move(subscript) + ")"; return true; } std::string ToString() const final { std::string ret; - ToStringHelper(nullptr, ret, false, false); + ToStringHelper(nullptr, ret, StringType::PUBLIC); return AddChecksum(ret); } bool ToPrivateString(const SigningProvider& arg, std::string& out) const override final { - bool ret = ToStringHelper(&arg, out, true, false); + bool ret = ToStringHelper(&arg, out, StringType::PRIVATE); out = AddChecksum(out); return ret; } - bool ToNormalizedString(const SigningProvider& arg, std::string& out, bool priv) const override final + bool ToNormalizedString(const SigningProvider& arg, std::string& out, const DescriptorCache* cache) const override final { - bool ret = ToStringHelper(&arg, out, priv, true); + bool ret = ToStringHelper(&arg, out, StringType::NORMALIZED, cache); out = AddChecksum(out); return ret; } @@ -577,17 +607,20 @@ public: std::vector> entries; entries.reserve(m_pubkey_args.size()); - // Construct temporary data in `entries` and `subscripts`, to avoid producing output in case of failure. + // Construct temporary data in `entries`, `subscripts`, and `subprovider` to avoid producing output in case of failure. for (const auto& p : m_pubkey_args) { entries.emplace_back(); if (!p->GetPubKey(pos, arg, entries.back().first, entries.back().second, read_cache, write_cache)) return false; } std::vector subscripts; - if (m_subdescriptor_arg) { - FlatSigningProvider subprovider; - if (!m_subdescriptor_arg->ExpandHelper(pos, arg, read_cache, subscripts, subprovider, write_cache)) return false; - out = Merge(out, subprovider); + FlatSigningProvider subprovider; + for (const auto& subarg : m_subdescriptor_args) { + std::vector outscripts; + if (!subarg->ExpandHelper(pos, arg, read_cache, outscripts, subprovider, write_cache)) return false; + assert(outscripts.size() == 1); + subscripts.emplace_back(std::move(outscripts[0])); } + out = Merge(std::move(out), std::move(subprovider)); std::vector pubkeys; pubkeys.reserve(entries.size()); @@ -595,17 +628,8 @@ public: pubkeys.push_back(entry.first); out.origins.emplace(entry.first.GetID(), std::make_pair(CPubKey(entry.first), std::move(entry.second))); } - if (m_subdescriptor_arg) { - for (const auto& subscript : subscripts) { - out.scripts.emplace(CScriptID(subscript), subscript); - std::vector addscripts = MakeScripts(pubkeys, &subscript, out); - for (auto& addscript : addscripts) { - output_scripts.push_back(std::move(addscript)); - } - } - } else { - output_scripts = MakeScripts(pubkeys, nullptr, out); - } + + output_scripts = MakeScripts(pubkeys, Span{subscripts}, out); return true; } @@ -626,10 +650,8 @@ public: if (!p->GetPrivKey(pos, provider, key)) continue; out.keys.emplace(key.GetPubKey().GetID(), key); } - if (m_subdescriptor_arg) { - FlatSigningProvider subprovider; - m_subdescriptor_arg->ExpandPrivate(pos, provider, subprovider); - out = Merge(out, subprovider); + for (const auto& arg : m_subdescriptor_args) { + arg->ExpandPrivate(pos, provider, out); } } std::optional GetOutputType() const override { return std::nullopt; } @@ -641,9 +663,9 @@ class AddressDescriptor final : public DescriptorImpl const CTxDestination m_destination; protected: std::string ToStringExtra() const override { return EncodeDestination(m_destination); } - std::vector MakeScripts(const std::vector&, const CScript*, FlatSigningProvider&) const override { return Vector(GetScriptForDestination(m_destination)); } + std::vector MakeScripts(const std::vector&, Span, FlatSigningProvider&) const override { return Vector(GetScriptForDestination(m_destination)); } public: - AddressDescriptor(CTxDestination destination) : DescriptorImpl({}, {}, "addr"), m_destination(std::move(destination)) {} + AddressDescriptor(CTxDestination destination) : DescriptorImpl({}, "addr"), m_destination(std::move(destination)) {} bool IsSolvable() const final { return false; } std::optional GetOutputType() const override @@ -664,9 +686,9 @@ class RawDescriptor final : public DescriptorImpl const CScript m_script; protected: std::string ToStringExtra() const override { return HexStr(m_script); } - std::vector MakeScripts(const std::vector&, const CScript*, FlatSigningProvider&) const override { return Vector(m_script); } + std::vector MakeScripts(const std::vector&, Span, FlatSigningProvider&) const override { return Vector(m_script); } public: - RawDescriptor(CScript script) : DescriptorImpl({}, {}, "raw"), m_script(std::move(script)) {} + RawDescriptor(CScript script) : DescriptorImpl({}, "raw"), m_script(std::move(script)) {} bool IsSolvable() const final { return false; } std::optional GetOutputType() const override @@ -687,9 +709,9 @@ public: class PKDescriptor final : public DescriptorImpl { protected: - std::vector MakeScripts(const std::vector& keys, const CScript*, FlatSigningProvider&) const override { return Vector(GetScriptForRawPubKey(keys[0])); } + std::vector MakeScripts(const std::vector& keys, Span, FlatSigningProvider&) const override { return Vector(GetScriptForRawPubKey(keys[0])); } public: - PKDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), {}, "pk") {} + PKDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), "pk") {} std::optional GetOutputType() const override { return OutputType::LEGACY; } bool IsSingleType() const final { return true; } }; @@ -698,14 +720,14 @@ public: class PKHDescriptor final : public DescriptorImpl { protected: - std::vector MakeScripts(const std::vector& keys, const CScript*, FlatSigningProvider& out) const override + std::vector MakeScripts(const std::vector& keys, Span, FlatSigningProvider& out) const override { CKeyID id = keys[0].GetID(); out.pubkeys.emplace(id, keys[0]); return Vector(GetScriptForDestination(PKHash(id))); } public: - PKHDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), {}, "pkh") {} + PKHDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), "pkh") {} std::optional GetOutputType() const override { return OutputType::LEGACY; } bool IsSingleType() const final { return true; } }; @@ -717,7 +739,7 @@ class MultisigDescriptor final : public DescriptorImpl const bool m_sorted; protected: std::string ToStringExtra() const override { return strprintf("%i", m_threshold); } - std::vector MakeScripts(const std::vector& keys, const CScript*, FlatSigningProvider&) const override { + std::vector MakeScripts(const std::vector& keys, Span, FlatSigningProvider&) const override { if (m_sorted) { std::vector sorted_keys(keys); std::sort(sorted_keys.begin(), sorted_keys.end()); @@ -726,7 +748,7 @@ protected: return Vector(GetScriptForMultisig(m_threshold, keys)); } public: - MultisigDescriptor(int threshold, std::vector> providers, bool sorted = false) : DescriptorImpl(std::move(providers), {}, sorted ? "sortedmulti" : "multi"), m_threshold(threshold), m_sorted(sorted) {} + MultisigDescriptor(int threshold, std::vector> providers, bool sorted = false) : DescriptorImpl(std::move(providers), sorted ? "sortedmulti" : "multi"), m_threshold(threshold), m_sorted(sorted) {} bool IsSingleType() const final { return true; } }; @@ -734,13 +756,18 @@ public: class SHDescriptor final : public DescriptorImpl { protected: - std::vector MakeScripts(const std::vector&, const CScript* script, FlatSigningProvider&) const override { return Vector(GetScriptForDestination(ScriptHash(*script))); } + std::vector MakeScripts(const std::vector&, Span scripts, FlatSigningProvider& out) const override + { + auto ret = Vector(GetScriptForDestination(ScriptHash(scripts[0]))); + if (ret.size()) out.scripts.emplace(CScriptID(scripts[0]), scripts[0]); + return ret; + } public: SHDescriptor(std::unique_ptr desc) : DescriptorImpl({}, std::move(desc), "sh") {} std::optional GetOutputType() const override { - assert(m_subdescriptor_arg); + assert(m_subdescriptor_args.size() == 1); return OutputType::LEGACY; } bool IsSingleType() const final { return true; } @@ -750,7 +777,7 @@ public: class ComboDescriptor final : public DescriptorImpl { protected: - std::vector MakeScripts(const std::vector& keys, const CScript*, FlatSigningProvider& out) const override + std::vector MakeScripts(const std::vector& keys, Span scripts, FlatSigningProvider& out) const override { std::vector ret; CKeyID id = keys[0].GetID(); @@ -766,7 +793,7 @@ protected: } public: - ComboDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), {}, "combo") {} + ComboDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), "combo") {} std::optional GetOutputType() const override { return OutputType::LEGACY; } bool IsSingleType() const final { return false; } }; @@ -776,8 +803,8 @@ public: //////////////////////////////////////////////////////////////////////////// enum class ParseScriptContext { - TOP, - P2SH + TOP, //!< Top-level context (script goes directly in scriptPubKey) + P2SH, //!< Inside sh() (script becomes P2SH redeemScript) }; /** Parse a key path, being passed a split list of elements (the first element is ignored). */ @@ -804,10 +831,11 @@ enum class ParseScriptContext { } /** Parse a public key that excludes origin information. */ -std::unique_ptr ParsePubkeyInner(uint32_t key_exp_index, const Span& sp, bool permit_uncompressed, FlatSigningProvider& out, std::string& error) +std::unique_ptr ParsePubkeyInner(uint32_t key_exp_index, const Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error) { using namespace spanparsing; + bool permit_uncompressed = ctx == ParseScriptContext::TOP || ctx == ParseScriptContext::P2SH; auto split = Split(sp, '/'); std::string str(split[0].begin(), split[0].end()); if (str.size() == 0) { @@ -865,7 +893,7 @@ std::unique_ptr ParsePubkeyInner(uint32_t key_exp_index, const S } /** Parse a public key including origin information (if enabled). */ -std::unique_ptr ParsePubkey(uint32_t key_exp_index, const Span& sp, bool permit_uncompressed, FlatSigningProvider& out, std::string& error) +std::unique_ptr ParsePubkey(uint32_t key_exp_index, const Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error) { using namespace spanparsing; @@ -874,7 +902,7 @@ std::unique_ptr ParsePubkey(uint32_t key_exp_index, const Span ParsePubkey(uint32_t key_exp_index, const Span(key_exp_index, std::move(info), std::move(provider)); } /** Parse a script in a particular context. */ -std::unique_ptr ParseScript(uint32_t key_exp_index, Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error) +std::unique_ptr ParseScript(uint32_t& key_exp_index, Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error) { using namespace spanparsing; auto expr = Expr(sp); bool sorted_multi = false; if (Func("pk", expr)) { - auto pubkey = ParsePubkey(key_exp_index, expr, true, out, error); + auto pubkey = ParsePubkey(key_exp_index, expr, ctx, out, error); if (!pubkey) return nullptr; + ++key_exp_index; return std::make_unique(std::move(pubkey)); } if (Func("pkh", expr)) { - auto pubkey = ParsePubkey(key_exp_index, expr, true, out, error); + auto pubkey = ParsePubkey(key_exp_index, expr, ctx, out, error); if (!pubkey) return nullptr; + ++key_exp_index; return std::make_unique(std::move(pubkey)); } if (ctx == ParseScriptContext::TOP && Func("combo", expr)) { - auto pubkey = ParsePubkey(key_exp_index, expr, true, out, error); + auto pubkey = ParsePubkey(key_exp_index, expr, ctx, out, error); if (!pubkey) return nullptr; + ++key_exp_index; return std::make_unique(std::move(pubkey)); - } else if (ctx != ParseScriptContext::TOP && Func("combo", expr)) { - error = "Cannot have combo in non-top level"; + } else if (Func("combo", expr)) { + error = "Can only have combo() at top level"; return nullptr; } if ((sorted_multi = Func("sortedmulti", expr)) || Func("multi", expr)) { @@ -941,7 +972,7 @@ std::unique_ptr ParseScript(uint32_t key_exp_index, SpanGetSize() + 1; providers.emplace_back(std::move(pk)); @@ -975,8 +1006,8 @@ std::unique_ptr ParseScript(uint32_t key_exp_index, Span(std::move(desc)); - } else if (ctx != ParseScriptContext::TOP && Func("sh", expr)) { - error = "Cannot have sh in non-top level"; + } else if (Func("sh", expr)) { + error = "Can only have sh() at top level"; return nullptr; } if (ctx == ParseScriptContext::TOP && Func("addr", expr)) { @@ -986,6 +1017,9 @@ std::unique_ptr ParseScript(uint32_t key_exp_index, Span(std::move(dest)); + } else if (Func("addr", expr)) { + error = "Can only have addr() at top level"; + return nullptr; } if (ctx == ParseScriptContext::TOP && Func("raw", expr)) { std::string str(expr.begin(), expr.end()); @@ -995,6 +1029,9 @@ std::unique_ptr ParseScript(uint32_t key_exp_index, Span(CScript(bytes.begin(), bytes.end())); + } else if (Func("raw", expr)) { + error = "Can only have raw() at top level"; + return nullptr; } if (ctx == ParseScriptContext::P2SH) { error = "A function is needed within P2SH"; @@ -1104,7 +1141,8 @@ std::unique_ptr Parse(const std::string& descriptor, FlatSigningProv { Span sp{descriptor}; if (!CheckChecksum(sp, require_checksum, error)) return nullptr; - auto ret = ParseScript(0, sp, ParseScriptContext::TOP, out, error); + uint32_t key_exp_index = 0; + auto ret = ParseScript(key_exp_index, sp, ParseScriptContext::TOP, out, error); if (sp.size() == 0 && ret) return std::unique_ptr(std::move(ret)); return nullptr; } @@ -1134,6 +1172,11 @@ void DescriptorCache::CacheDerivedExtPubKey(uint32_t key_exp_pos, uint32_t der_i xpubs[der_index] = xpub; } +void DescriptorCache::CacheLastHardenedExtPubKey(uint32_t key_exp_pos, const CExtPubKey& xpub) +{ + m_last_hardened_xpubs[key_exp_pos] = xpub; +} + bool DescriptorCache::GetCachedParentExtPubKey(uint32_t key_exp_pos, CExtPubKey& xpub) const { const auto& it = m_parent_xpubs.find(key_exp_pos); @@ -1152,6 +1195,55 @@ bool DescriptorCache::GetCachedDerivedExtPubKey(uint32_t key_exp_pos, uint32_t d return true; } +bool DescriptorCache::GetCachedLastHardenedExtPubKey(uint32_t key_exp_pos, CExtPubKey& xpub) const +{ + const auto& it = m_last_hardened_xpubs.find(key_exp_pos); + if (it == m_last_hardened_xpubs.end()) return false; + xpub = it->second; + return true; +} + +DescriptorCache DescriptorCache::MergeAndDiff(const DescriptorCache& other) +{ + DescriptorCache diff; + for (const auto& parent_xpub_pair : other.GetCachedParentExtPubKeys()) { + CExtPubKey xpub; + if (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; + } + CacheParentExtPubKey(parent_xpub_pair.first, parent_xpub_pair.second); + diff.CacheParentExtPubKey(parent_xpub_pair.first, parent_xpub_pair.second); + } + for (const auto& derived_xpub_map_pair : other.GetCachedDerivedExtPubKeys()) { + for (const auto& derived_xpub_pair : derived_xpub_map_pair.second) { + CExtPubKey xpub; + if (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; + } + CacheDerivedExtPubKey(derived_xpub_map_pair.first, derived_xpub_pair.first, derived_xpub_pair.second); + diff.CacheDerivedExtPubKey(derived_xpub_map_pair.first, derived_xpub_pair.first, derived_xpub_pair.second); + } + } + for (const auto& lh_xpub_pair : other.GetCachedLastHardenedExtPubKeys()) { + CExtPubKey xpub; + if (GetCachedLastHardenedExtPubKey(lh_xpub_pair.first, xpub)) { + if (xpub != lh_xpub_pair.second) { + throw std::runtime_error(std::string(__func__) + ": New cached last hardened xpub does not match already cached last hardened xpub"); + } + continue; + } + CacheLastHardenedExtPubKey(lh_xpub_pair.first, lh_xpub_pair.second); + diff.CacheLastHardenedExtPubKey(lh_xpub_pair.first, lh_xpub_pair.second); + } + return diff; +} + const ExtPubKeyMap DescriptorCache::GetCachedParentExtPubKeys() const { return m_parent_xpubs; @@ -1161,3 +1253,8 @@ const std::unordered_map DescriptorCache::GetCachedDeriv { return m_derived_xpubs; } + +const ExtPubKeyMap DescriptorCache::GetCachedLastHardenedExtPubKeys() const +{ + return m_last_hardened_xpubs; +} diff --git a/src/script/descriptor.h b/src/script/descriptor.h index 28df1ed4df..f5bbf487c6 100644 --- a/src/script/descriptor.h +++ b/src/script/descriptor.h @@ -22,6 +22,8 @@ private: std::unordered_map 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 @@ -50,11 +52,30 @@ public: * @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 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. @@ -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. * diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index 2324610754..8d0c812b6f 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -24,7 +24,7 @@ void CheckUnparsable(const std::string& prv, const std::string& pub, const std:: auto parse_pub = Parse(pub, keys_pub, error); BOOST_CHECK_MESSAGE(!parse_priv, prv); BOOST_CHECK_MESSAGE(!parse_pub, pub); - BOOST_CHECK(error == expected_error); + BOOST_CHECK_EQUAL(error, expected_error); } constexpr int DEFAULT = 0; @@ -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); @@ -335,9 +331,8 @@ BOOST_AUTO_TEST_CASE(descriptor_test) // Check for invalid nesting of structures CheckUnparsable("sh(XJvEUEcFWCHCyruc8ZX5exPZaGe4UR7gC5FHrhwPnQGDs1uWCsT2)", "sh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", "A function is needed within P2SH"); // P2SH needs a script, not a key - CheckUnparsable("sh(sh(pk(XJvEUEcFWCHCyruc8ZX5exPZaGe4UR7gC5FHrhwPnQGDs1uWCsT2)))", "sh(sh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))", "Cannot have sh in non-top level"); // Cannot embed P2SH inside P2SH - CheckUnparsable("sh(combo(XJvEUEcFWCHCyruc8ZX5exPZaGe4UR7gC5FHrhwPnQGDs1uWCsT2))", "sh(combo(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", "Cannot have combo in non-top level"); // Old must be top level - + CheckUnparsable("sh(sh(pk(XJvEUEcFWCHCyruc8ZX5exPZaGe4UR7gC5FHrhwPnQGDs1uWCsT2)))", "sh(sh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))", "Can only have sh() at top level"); // Cannot embed P2SH inside P2SH + CheckUnparsable("sh(combo(XJvEUEcFWCHCyruc8ZX5exPZaGe4UR7gC5FHrhwPnQGDs1uWCsT2))", "sh(combo(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", "Can only have combo() at top level"); // Old must be top level // Checksums Check("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfy", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t", "sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxfy", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t", DEFAULT, {{"a91445a9a622a8b0a1269944be477640eedc447bbd8487"}}, OutputType::LEGACY, {{0x8000006FUL,222},{0}}); Check("sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))", "sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))", "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))", DEFAULT, {{"a91445a9a622a8b0a1269944be477640eedc447bbd8487"}}, OutputType::LEGACY, {{0x8000006FUL,222},{0}}); diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 230a6fb2c7..55488c7080 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -1766,7 +1766,7 @@ static UniValue ProcessDescriptorImport(CWallet * const pwallet, const UniValue& if (!w_desc.descriptor->GetOutputType()) { warnings.push_back("Unknown output type, cannot set descriptor to active."); } else { - pwallet->SetActiveScriptPubKeyMan(spk_manager->GetID(), internal); + pwallet->AddActiveScriptPubKeyMan(spk_manager->GetID(), internal); } } @@ -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); @@ -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); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index fec1bb8e4e..6b3986ef3f 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -83,14 +83,12 @@ static bool ParseIncludeWatchonly(const UniValue& include_watchonly, const CWall /** Checks if a CKey is in the given CWallet compressed or otherwise*/ -/* bool HaveKey(const SigningProvider& wallet, const CKey& key) { CKey key2; key2.Set(key.begin(), key.end(), !key.IsCompressed()); return wallet.HaveKey(key.GetPubKey().GetID()) || wallet.HaveKey(key2.GetPubKey().GetID()); } -*/ bool GetWalletNameFromJSONRPCRequest(const JSONRPCRequest& request, std::string& wallet_name) { @@ -2976,7 +2974,7 @@ static RPCHelpMan createwallet() { {"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name for the new wallet. If this is a path, the wallet will be created at the path location."}, {"disable_private_keys", RPCArg::Type::BOOL, /* default */ "false", "Disable the possibility of private keys (only watchonlys are possible in this mode)."}, - {"blank", RPCArg::Type::BOOL, /* default */ "false", "Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using upgradetohd."}, + {"blank", RPCArg::Type::BOOL, /* default */ "false", "Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using upgradetohd (by mnemonic) or sethdseed (WIF private key)."}, {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the wallet with this passphrase."}, {"avoid_reuse", RPCArg::Type::BOOL, /* default */ "false", "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."}, {"descriptors", RPCArg::Type::BOOL, /* default */ "false", "Create a native descriptor wallet. The wallet will use descriptors internally to handle address creation"}, @@ -3517,7 +3515,7 @@ static RPCHelpMan fundrawtransaction() CAmount fee; int change_position; CCoinControl coin_control; - // Automatically select (additional) coins. Can be overriden by options.add_inputs. + // Automatically select (additional) coins. Can be overridden by options.add_inputs. coin_control.m_add_inputs = true; FundTransaction(pwallet, tx, fee, change_position, request.params[1], coin_control); @@ -3938,7 +3936,7 @@ RPCHelpMan getaddressinfo() DescriptorScriptPubKeyMan* desc_spk_man = dynamic_cast(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); } } @@ -4272,6 +4270,85 @@ static RPCHelpMan send() }; } +static RPCHelpMan sethdseed() +{ + return RPCHelpMan{"sethdseed", + "\nSet or generate a new HD wallet seed. Non-HD wallets will not be upgraded to being a HD wallet. Wallets that are already\n" + "HD can not be updated to a new HD seed.\n" + "\nNote that you will need to MAKE A NEW BACKUP of your wallet after setting the HD wallet seed." + + HELP_REQUIRING_PASSPHRASE, + { + {"newkeypool", RPCArg::Type::BOOL, /* default */ "true", "Whether to flush old unused addresses, including change addresses, from the keypool and regenerate it.\n" + "If true, the next address from getnewaddress and change address from getrawchangeaddress will be from this new seed.\n" + "If false, addresses from the existing keypool will be used until it has been depleted."}, + {"seed", RPCArg::Type::STR, /* default */ "random seed", "The WIF private key to use as the new HD seed.\n" + "The seed value can be retrieved using the dumpwallet command. It is the private key marked hdseed=1"}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("sethdseed", "") + + HelpExampleCli("sethdseed", "false") + + HelpExampleCli("sethdseed", "true \"wifkey\"") + + HelpExampleRpc("sethdseed", "true, \"wifkey\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + // TODO: add mnemonic feature to sethdseed or remove it in favour of upgradetohd + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + CWallet* const pwallet = wallet.get(); + + LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true); + + if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a HD seed to a wallet with private keys disabled"); + } + + LOCK2(pwallet->cs_wallet, spk_man.cs_KeyStore); + + // Do not do anything to non-HD wallets + if (!pwallet->CanSupportFeature(FEATURE_HD)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a HD seed on a non-HD wallet. Use the upgradewallet RPC in order to upgrade a non-HD wallet to HD"); + } + if (pwallet->IsHDEnabled()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a HD seed. The wallet already has a seed"); + } + + EnsureWalletIsUnlocked(pwallet); + + bool flush_key_pool = true; + if (!request.params[0].isNull()) { + flush_key_pool = request.params[0].get_bool(); + } + + if (request.params[1].isNull()) { + spk_man.GenerateNewHDChain("", ""); + } else { + CKey key = DecodeSecret(request.params[1].get_str()); + if (!key.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key"); + } + if (HaveKey(spk_man, key)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Already have this key (either as an HD seed or as a loose private key)"); + } + CHDChain newHdChain; + if (!newHdChain.SetSeed(SecureVector(key.begin(), key.end()), true)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key: SetSeed failed"); + } + if (!spk_man.AddHDChainSingle(newHdChain)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key: AddHDChainSingle failed"); + } + // add default account + newHdChain.AddAccount(); + } + + if (flush_key_pool) spk_man.NewKeyPool(); + + return NullUniValue; +}, + }; +} + RPCHelpMan walletprocesspsbt() { return RPCHelpMan{"walletprocesspsbt", @@ -4432,7 +4509,7 @@ static RPCHelpMan walletcreatefundedpsbt() CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2]); CCoinControl coin_control; // Automatically select coins, unless at least one is manually selected. Can - // be overriden by options.add_inputs. + // be overridden by options.add_inputs. coin_control.m_add_inputs = rawTx.vin.size() == 0; FundTransaction(pwallet, rawTx, fee, change_position, request.params[3], coin_control); @@ -4463,14 +4540,18 @@ static RPCHelpMan walletcreatefundedpsbt() static RPCHelpMan upgradewallet() { return RPCHelpMan{"upgradewallet", - "\nUpgrade the wallet. Upgrades to the latest version if no version number is specified\n" + "\nUpgrade the wallet. Upgrades to the latest version if no version number is specified.\n" "New keys may be generated and a new wallet backup will need to be made.", { - {"version", RPCArg::Type::NUM, /* default */ strprintf("%d", FEATURE_LATEST), "The version number to upgrade to. Default is the latest wallet version"} + {"version", RPCArg::Type::NUM, /* default */ strprintf("%d", FEATURE_LATEST), "The version number to upgrade to. Default is the latest wallet version."} }, RPCResult{ RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::STR, "wallet_name", "Name of wallet this operation was performed on"}, + {RPCResult::Type::NUM, "previous_version", "Version of wallet before this operation"}, + {RPCResult::Type::NUM, "current_version", "Version of wallet after this operation"}, + {RPCResult::Type::STR, "result", /* optional */ true, "Description of result, if no error"}, {RPCResult::Type::STR, "error", /* optional */ true, "Error message (if there is one)"} }, }, @@ -4493,11 +4574,27 @@ static RPCHelpMan upgradewallet() version = request.params[0].get_int(); } bilingual_str error; - if (!pwallet->UpgradeWallet(version, error)) { - throw JSONRPCError(RPC_WALLET_ERROR, error.original); + const int previous_version{pwallet->GetVersion()}; + const bool wallet_upgraded{pwallet->UpgradeWallet(version, error)}; + const int current_version{pwallet->GetVersion()}; + std::string result; + + if (wallet_upgraded) { + if (previous_version == current_version) { + result = "Already at latest version. Wallet version unchanged."; + } else { + result = strprintf("Wallet upgraded successfully from version %i to version %i.", previous_version, current_version); + } } + UniValue obj(UniValue::VOBJ); - if (!error.empty()) { + obj.pushKV("wallet_name", pwallet->GetName()); + obj.pushKV("previous_version", previous_version); + obj.pushKV("current_version", current_version); + if (!result.empty()) { + obj.pushKV("result", result); + } else { + CHECK_NONFATAL(!error.empty()); obj.pushKV("error", error.original); } return obj; @@ -4576,6 +4673,7 @@ static const CRPCCommand commands[] = { "wallet", "send", &send, {"outputs","conf_target","estimate_mode","options"} }, { "wallet", "sendmany", &sendmany, {"dummy","amounts","minconf","addlocked","comment","subtractfeefrom","use_is","use_cj","conf_target","estimate_mode"} }, { "wallet", "sendtoaddress", &sendtoaddress, {"address","amount","comment","comment_to","subtractfeefromamount","use_is","use_cj","conf_target","estimate_mode", "avoid_reuse"} }, + { "wallet", "sethdseed", &sethdseed, {"newkeypool","seed"} }, { "wallet", "setcoinjoinrounds", &setcoinjoinrounds, {"rounds"} }, { "wallet", "setcoinjoinamount", &setcoinjoinamount, {"amount"} }, { "wallet", "setlabel", &setlabel, {"address","label"} }, diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index fdf019d183..45c03055f1 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -218,14 +218,14 @@ bool LegacyScriptPubKeyMan::CheckDecryptionKey(const CKeyingMaterial& master_key if (keyFail) { return false; } - if (!keyPass && !accept_no_keys && (hdChain.IsNull() || !hdChain.IsNull() && !hdChain.IsCrypted())) { + if (!keyPass && !accept_no_keys && (m_hd_chain.IsNull() || !m_hd_chain.IsNull() && !m_hd_chain.IsCrypted())) { return false; } - if(!hdChain.IsNull() && !hdChain.IsCrypted()) { + if(!m_hd_chain.IsNull() && !m_hd_chain.IsCrypted()) { // try to decrypt seed and make sure it matches CHDChain hdChainTmp; - if (!DecryptHDChain(master_key, hdChainTmp) || (hdChain.GetID() != hdChainTmp.GetSeedHash())) { + if (!DecryptHDChain(master_key, hdChainTmp) || (m_hd_chain.GetID() != hdChainTmp.GetSeedHash())) { return false; } } @@ -267,8 +267,8 @@ bool LegacyScriptPubKeyMan::Encrypt(const CKeyingMaterial& master_key, WalletBat } if (!hdChainCurrent.IsNull()) { - assert(EncryptHDChain(master_key, hdChain)); - assert(SetHDChain(hdChain)); + assert(EncryptHDChain(master_key, m_hd_chain)); + assert(LoadHDChain(m_hd_chain)); CHDChain hdChainCrypted; assert(GetHDChain(hdChainCrypted)); @@ -277,7 +277,7 @@ bool LegacyScriptPubKeyMan::Encrypt(const CKeyingMaterial& master_key, WalletBat assert(hdChainCurrent.GetID() == hdChainCrypted.GetID()); assert(hdChainCurrent.GetSeedHash() != hdChainCrypted.GetSeedHash()); - assert(SetHDChain(*encrypted_batch, hdChainCrypted, false)); + assert(AddHDChain(*encrypted_batch, hdChainCrypted)); } encrypted_batch = nullptr; @@ -396,7 +396,7 @@ void LegacyScriptPubKeyMan::GenerateNewCryptedHDChain(const SecureString& secure CHDChain hdChainPrev = hdChainTmp; bool res = EncryptHDChain(vMasterKey, hdChainTmp); assert(res); - res = SetHDChain(hdChainTmp); + res = LoadHDChain(hdChainTmp); assert(res); CHDChain hdChainCrypted; @@ -407,8 +407,8 @@ void LegacyScriptPubKeyMan::GenerateNewCryptedHDChain(const SecureString& secure assert(hdChainPrev.GetID() == hdChainCrypted.GetID()); assert(hdChainPrev.GetSeedHash() != hdChainCrypted.GetSeedHash()); - if (!SetHDChainSingle(hdChainCrypted, false)) { - throw std::runtime_error(std::string(__func__) + ": SetHDChainSingle failed"); + if (!AddHDChainSingle(hdChainCrypted)) { + throw std::runtime_error(std::string(__func__) + ": AddHDChainSingle failed"); } } @@ -426,8 +426,8 @@ void LegacyScriptPubKeyMan::GenerateNewHDChain(const SecureString& secureMnemoni // add default account newHdChain.AddAccount(); - if (!SetHDChainSingle(newHdChain, false)) { - throw std::runtime_error(std::string(__func__) + ": SetHDChainSingle failed"); + if (!AddHDChainSingle(newHdChain)) { + throw std::runtime_error(std::string(__func__) + ": AddHDChainSingle failed"); } if (!NewKeyPool()) { @@ -435,14 +435,24 @@ void LegacyScriptPubKeyMan::GenerateNewHDChain(const SecureString& secureMnemoni } } -bool LegacyScriptPubKeyMan::SetHDChain(WalletBatch &batch, const CHDChain& chain, bool memonly) +bool LegacyScriptPubKeyMan::LoadHDChain(const CHDChain& chain) { LOCK(cs_KeyStore); - if (!SetHDChain(chain)) + if (m_storage.HasEncryptionKeys() != chain.IsCrypted()) return false; + + m_hd_chain = chain; + return true; +} + +bool LegacyScriptPubKeyMan::AddHDChain(WalletBatch &batch, const CHDChain& chain) +{ + LOCK(cs_KeyStore); + + if (!LoadHDChain(chain)) return false; - if (!memonly) { + { if (chain.IsCrypted() && encrypted_batch) { if (!encrypted_batch->WriteHDChain(chain)) throw std::runtime_error(std::string(__func__) + ": WriteHDChain failed for encrypted batch"); @@ -458,10 +468,10 @@ bool LegacyScriptPubKeyMan::SetHDChain(WalletBatch &batch, const CHDChain& chain return true; } -bool LegacyScriptPubKeyMan::SetHDChainSingle(const CHDChain& chain, bool memonly) +bool LegacyScriptPubKeyMan::AddHDChainSingle(const CHDChain& chain) { WalletBatch batch(m_storage.GetDatabase()); - return SetHDChain(batch, chain, memonly); + return AddHDChain(batch, chain); } bool LegacyScriptPubKeyMan::GetDecryptedHDChain(CHDChain& hdChainRet) @@ -539,40 +549,40 @@ bool LegacyScriptPubKeyMan::DecryptHDChain(const CKeyingMaterial& vMasterKeyIn, if (!m_storage.HasEncryptionKeys()) return true; - if (hdChain.IsNull()) + if (m_hd_chain.IsNull()) return false; - if (!hdChain.IsCrypted()) + if (!m_hd_chain.IsCrypted()) return false; SecureVector vchSecureSeed; - SecureVector vchSecureCryptedSeed = hdChain.GetSeed(); + SecureVector vchSecureCryptedSeed = m_hd_chain.GetSeed(); std::vector vchCryptedSeed(vchSecureCryptedSeed.begin(), vchSecureCryptedSeed.end()); - if (!DecryptSecret(vMasterKeyIn, vchCryptedSeed, hdChain.GetID(), vchSecureSeed)) + if (!DecryptSecret(vMasterKeyIn, vchCryptedSeed, m_hd_chain.GetID(), vchSecureSeed)) return false; - hdChainRet = hdChain; + hdChainRet = m_hd_chain; if (!hdChainRet.SetSeed(vchSecureSeed, false)) return false; // hash of decrypted seed must match chain id - if (hdChainRet.GetSeedHash() != hdChain.GetID()) + if (hdChainRet.GetSeedHash() != m_hd_chain.GetID()) return false; SecureVector vchSecureCryptedMnemonic; SecureVector vchSecureCryptedMnemonicPassphrase; // it's ok to have no mnemonic if wallet was initialized via hdseed - if (hdChain.GetMnemonic(vchSecureCryptedMnemonic, vchSecureCryptedMnemonicPassphrase)) { + if (m_hd_chain.GetMnemonic(vchSecureCryptedMnemonic, vchSecureCryptedMnemonicPassphrase)) { SecureVector vchSecureMnemonic; SecureVector vchSecureMnemonicPassphrase; std::vector vchCryptedMnemonic(vchSecureCryptedMnemonic.begin(), vchSecureCryptedMnemonic.end()); std::vector vchCryptedMnemonicPassphrase(vchSecureCryptedMnemonicPassphrase.begin(), vchSecureCryptedMnemonicPassphrase.end()); - if (!vchCryptedMnemonic.empty() && !DecryptSecret(vMasterKeyIn, vchCryptedMnemonic, hdChain.GetID(), vchSecureMnemonic)) + if (!vchCryptedMnemonic.empty() && !DecryptSecret(vMasterKeyIn, vchCryptedMnemonic, m_hd_chain.GetID(), vchSecureMnemonic)) return false; - if (!vchCryptedMnemonicPassphrase.empty() && !DecryptSecret(vMasterKeyIn, vchCryptedMnemonicPassphrase, hdChain.GetID(), vchSecureMnemonicPassphrase)) + if (!vchCryptedMnemonicPassphrase.empty() && !DecryptSecret(vMasterKeyIn, vchCryptedMnemonicPassphrase, m_hd_chain.GetID(), vchSecureMnemonicPassphrase)) return false; if (!hdChainRet.SetMnemonic(vchSecureMnemonic, vchSecureMnemonicPassphrase, false)) @@ -1090,16 +1100,6 @@ bool LegacyScriptPubKeyMan::AddWatchOnly(const CScript& dest, int64_t nCreateTim return AddWatchOnly(dest); } -bool LegacyScriptPubKeyMan::SetHDChain(const CHDChain& chain) -{ - LOCK(cs_KeyStore); - - if (m_storage.HasEncryptionKeys() != chain.IsCrypted()) return false; - - hdChain = chain; - return true; -} - bool LegacyScriptPubKeyMan::HaveHDKey(const CKeyID &address, CHDChain& hdChainCurrent) const { LOCK(cs_KeyStore); @@ -1322,8 +1322,8 @@ void LegacyScriptPubKeyMan::DeriveNewChildKey(WalletBatch &batch, CKeyMetadata& if (!hdChainCurrent.SetAccount(nAccountIndex, acc)) throw std::runtime_error(std::string(__func__) + ": SetAccount failed"); - if (!SetHDChain(batch, hdChainCurrent, false)) { - throw std::runtime_error(std::string(__func__) + ": SetHDChain failed"); + if (!AddHDChain(batch, hdChainCurrent)) { + throw std::runtime_error(std::string(__func__) + ": AddHDChain failed"); } if (!AddHDPubKey(batch, childKey.Neuter(), fInternal)) @@ -1758,8 +1758,8 @@ std::set LegacyScriptPubKeyMan::GetKeys() const bool LegacyScriptPubKeyMan::GetHDChain(CHDChain& hdChainRet) const { LOCK(cs_KeyStore); - hdChainRet = hdChain; - return !hdChain.IsNull(); + hdChainRet = m_hd_chain; + return !m_hd_chain.IsNull(); } void LegacyScriptPubKeyMan::SetInternal(bool internal) {} @@ -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++; } @@ -2402,15 +2378,41 @@ const std::vector 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, priv); + 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 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"); + } + + // 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"); + } } diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h index adf19cc8e5..524deb21a2 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -37,7 +37,7 @@ public: virtual bool IsWalletFlagSet(uint64_t) const = 0; virtual void UnsetBlankWalletFlag(WalletBatch&) = 0; virtual bool CanSupportFeature(enum WalletFeature) const = 0; - virtual void SetMinVersion(enum WalletFeature, WalletBatch* = nullptr, bool = false) = 0; + virtual void SetMinVersion(enum WalletFeature, WalletBatch* = nullptr) = 0; virtual const CKeyingMaterial& GetEncryptionKey() const = 0; virtual bool HasEncryptionKeys() const = 0; virtual bool IsLocked(bool fForMixing = false) const = 0; @@ -278,15 +278,11 @@ private: /** Add a KeyOriginInfo to the wallet */ bool AddKeyOriginWithDB(WalletBatch& batch, const CPubKey& pubkey, const KeyOriginInfo& info); - /* Set the HD chain model (chain child index counters) */ - bool SetHDChain(WalletBatch &batch, const CHDChain& chain, bool memonly); - bool EncryptHDChain(const CKeyingMaterial& vMasterKeyIn, CHDChain& chain); bool DecryptHDChain(const CKeyingMaterial& vMasterKeyIn, CHDChain& hdChainRet) const; - bool SetHDChain(const CHDChain& chain); /* the HD chain data model (external chain counters) */ - CHDChain hdChain GUARDED_BY(cs_KeyStore); + CHDChain m_hd_chain GUARDED_BY(cs_KeyStore); /* HD derive new child key (on internal or external chain) */ void DeriveNewChildKey(WalletBatch& batch, CKeyMetadata& metadata, CKey& secretRet, uint32_t nAccountIndex, bool fInternal /*= false*/) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore); @@ -398,11 +394,15 @@ public: //! Generate a new key CPubKey GenerateNewKey(WalletBatch& batch, uint32_t nAccountIndex, bool fInternal /*= false*/) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore); + /* Set the HD chain model (chain child index counters) and writes it to the database */ + bool AddHDChain(WalletBatch &batch, const CHDChain& chain); + //! Load a HD chain model (used by LoadWallet) + bool LoadHDChain(const CHDChain& chain); /** * Set the HD chain model (chain child index counters) using temporary wallet db object * which causes db flush every time these methods are used */ - bool SetHDChainSingle(const CHDChain& chain, bool memonly); + bool AddHDChainSingle(const CHDChain& chain); //! Adds a watch-only address to the store, without saving it to disk (used by LoadWallet) bool LoadWatchOnly(const CScript &dest); @@ -606,7 +606,9 @@ public: const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man); const std::vector GetScriptPubKeys() const; - bool GetDescriptorString(std::string& out, bool priv) const; + bool GetDescriptorString(std::string& out) const; + + void UpgradeDescriptorCache(); }; #endif // BITCOIN_WALLET_SCRIPTPUBKEYMAN_H diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index e1f2537561..e2bcbcdabd 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -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(spkm); + desc_spkm->UpgradeDescriptorCache(); + } + SetWalletFlag(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED); +} + bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, const SecureString& strNewWalletPassphrase) { bool fWasLocked = IsLocked(true); @@ -442,21 +455,13 @@ void CWallet::chainStateFlushed(const CBlockLocator& loc) batch.WriteBestBlock(loc); } -void CWallet::SetMinVersion(enum WalletFeature nVersion, WalletBatch* batch_in, bool fExplicit) +void CWallet::SetMinVersion(enum WalletFeature nVersion, WalletBatch* batch_in) { LOCK(cs_wallet); if (nWalletVersion >= nVersion) return; - - // when doing an explicit upgrade, if we pass the max version permitted, upgrade all the way - if (fExplicit && nVersion > nWalletMaxVersion) - nVersion = FEATURE_LATEST; - nWalletVersion = nVersion; - if (nVersion > nWalletMaxVersion) - nWalletMaxVersion = nVersion; - { WalletBatch* batch = batch_in ? batch_in : new WalletBatch(GetDatabase()); if (nWalletVersion > 40000) @@ -466,18 +471,6 @@ void CWallet::SetMinVersion(enum WalletFeature nVersion, WalletBatch* batch_in, } } -bool CWallet::SetMaxVersion(int nVersion) -{ - LOCK(cs_wallet); - // cannot downgrade below current version - if (nWalletVersion > nVersion) - return false; - - nWalletMaxVersion = nVersion; - - return true; -} - std::set CWallet::GetConflicts(const uint256& txid) const { std::set result; @@ -665,7 +658,7 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) // Encryption was introduced in version 0.4.0 - SetMinVersion(FEATURE_WALLETCRYPT, encrypted_batch, true); + SetMinVersion(FEATURE_WALLETCRYPT, encrypted_batch); if (!encrypted_batch->TxnCommit()) { delete encrypted_batch; @@ -1684,19 +1677,28 @@ bool CWallet::IsWalletFlagSet(uint64_t flag) const return (m_wallet_flags & flag); } -bool CWallet::SetWalletFlags(uint64_t overwriteFlags, bool memonly) +bool CWallet::LoadWalletFlags(uint64_t flags) { LOCK(cs_wallet); - m_wallet_flags = overwriteFlags; - if (((overwriteFlags & KNOWN_WALLET_FLAGS) >> 32) ^ (overwriteFlags >> 32)) { + if (((flags & KNOWN_WALLET_FLAGS) >> 32) ^ (flags >> 32)) { // contains unknown non-tolerable wallet flags return false; } - if (!memonly && !WalletBatch(GetDatabase()).WriteWalletFlags(m_wallet_flags)) { + m_wallet_flags = flags; + + return true; +} + +bool CWallet::AddWalletFlags(uint64_t flags) +{ + LOCK(cs_wallet); + // We should never be writing unknown non-tolerable wallet flags + assert(((flags & KNOWN_WALLET_FLAGS) >> 32) == (flags >> 32)); + if (!WalletBatch(GetDatabase()).WriteWalletFlags(flags)) { throw std::runtime_error(std::string(__func__) + ": writing wallet flags failed"); } - return true; + return LoadWalletFlags(flags); } int64_t CWalletTx::GetTxTime() const @@ -4574,8 +4576,9 @@ std::shared_ptr CWallet::Create(interfaces::Chain& chain, interfaces::C if (fFirstRun) { - walletInstance->SetMaxVersion(FEATURE_LATEST); - walletInstance->SetWalletFlags(wallet_creation_flags, false); + walletInstance->SetMinVersion(FEATURE_LATEST); + + walletInstance->AddWalletFlags(wallet_creation_flags); // Only create LegacyScriptPubKeyMan when not descriptor wallet if (!walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { @@ -4600,8 +4603,8 @@ std::shared_ptr CWallet::Create(interfaces::Chain& chain, interfaces::C } LOCK(walletInstance->cs_wallet); if (auto spk_man = walletInstance->GetLegacyScriptPubKeyMan()) { - if (!spk_man->SetHDChainSingle(newHdChain, false)) { - error = strprintf(_("%s failed"), "SetHDChainSingle"); + if (!spk_man->AddHDChainSingle(newHdChain)) { + error = strprintf(_("%s failed"), "AddHDChainSingle"); return nullptr; } } @@ -4887,6 +4890,7 @@ std::shared_ptr CWallet::Create(interfaces::Chain& chain, interfaces::C bool CWallet::UpgradeWallet(int version, bilingual_str& error) { + int prev_version = GetVersion(); int nMaxVersion = version; auto nMinVersion = DEFAULT_USE_HD_WALLET ? FEATURE_LATEST : FEATURE_COMPRPUBKEY; if (nMaxVersion == 0) { @@ -4898,17 +4902,18 @@ bool CWallet::UpgradeWallet(int version, bilingual_str& error) } if (nMaxVersion < GetVersion()) { - error = Untranslated("Cannot downgrade wallet"); + error = strprintf(_("Cannot downgrade wallet from version %i to version %i. Wallet version unchanged."), prev_version, version); return false; } // TODO: consider discourage users to skip passphrase for HD wallets for v21 if (false && nMaxVersion >= FEATURE_HD && !IsHDEnabled()) { error = Untranslated("You should use upgradetohd RPC to upgrade non-HD wallet to HD"); + error = strprintf(_("Cannot upgrade a non HD wallet from version %i to version %i which is non-HD wallet. Use upgradetohd RPC"), prev_version, version); return false; } - SetMaxVersion(nMaxVersion); + SetMinVersion(GetClosestWalletFeature(version)); return true; } @@ -5649,12 +5654,21 @@ void CWallet::SetupDescriptorScriptPubKeyMans() spk_manager->SetupDescriptorGeneration(master_key); uint256 id = spk_manager->GetID(); m_spk_managers[id] = std::move(spk_manager); - SetActiveScriptPubKeyMan(id, internal); + AddActiveScriptPubKeyMan(id, internal); } } } -void CWallet::SetActiveScriptPubKeyMan(uint256 id, bool internal, bool memonly) +void CWallet::AddActiveScriptPubKeyMan(uint256 id, bool internal) +{ + WalletBatch batch(GetDatabase()); + if (!batch.WriteActiveScriptPubKeyMan(id, internal)) { + throw std::runtime_error(std::string(__func__) + ": writing active ScriptPubKeyMan id failed"); + } + LoadActiveScriptPubKeyMan(id, internal); +} + +void CWallet::LoadActiveScriptPubKeyMan(uint256 id, bool internal) { WalletLogPrintf("Setting spkMan to active: id = %s, type = %d, internal = %d\n", id.ToString(), static_cast(OutputType::LEGACY), static_cast(internal)); auto& spk_mans = internal ? m_internal_spk_managers : m_external_spk_managers; @@ -5662,12 +5676,6 @@ void CWallet::SetActiveScriptPubKeyMan(uint256 id, bool internal, bool memonly) spk_man->SetInternal(internal); spk_mans = spk_man; - if (!memonly) { - WalletBatch batch(GetDatabase()); - if (!batch.WriteActiveScriptPubKeyMan(id, internal)) { - throw std::runtime_error(std::string(__func__) + ": writing active ScriptPubKeyMan id failed"); - } - } NotifyCanGetAddressesChanged(); } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index a0c5618ac5..39dc9b96d9 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -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; @@ -138,6 +139,7 @@ static const std::map 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}, }; @@ -706,9 +708,6 @@ private: //! the current wallet version: clients below this version are not able to load the wallet int nWalletVersion GUARDED_BY(cs_wallet){FEATURE_BASE}; - //! the maximum wallet format version: memory-only variable that specifies to what version this wallet may be upgraded - int nWalletMaxVersion GUARDED_BY(cs_wallet) = FEATURE_BASE; - int64_t nNextResend = 0; bool fBroadcastTransactions = false; // Local time that the tip block was received. Used to schedule wallet rebroadcasts. @@ -911,8 +910,8 @@ public: const CWalletTx* GetWalletTx(const uint256& hash) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsTrusted(const CWalletTx& wtx, std::set& trusted_parents) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - //! check whether we are allowed to upgrade (or already support) to the named feature - bool CanSupportFeature(enum WalletFeature wf) const override EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return nWalletMaxVersion >= wf; } + //! check whether we support the named feature + bool CanSupportFeature(enum WalletFeature wf) const override EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); return IsFeatureSupported(nWalletVersion, wf); } /** * populate vCoins with vector of available COutputs. @@ -981,7 +980,10 @@ public: //! Upgrade stored CKeyMetadata objects to store key origin info as KeyOriginInfo void UpgradeKeyMetadata() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - bool LoadMinVersion(int nVersion) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; nWalletMaxVersion = std::max(nWalletMaxVersion, nVersion); return true; } + //! 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 void LoadDestData(const CTxDestination& dest, const std::string& key, const std::string& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); @@ -1198,11 +1200,8 @@ public: unsigned int GetKeyPoolSize() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - //! signify that a particular wallet feature is now used. this may change nWalletVersion and nWalletMaxVersion if those are lower - void SetMinVersion(enum WalletFeature, WalletBatch* batch_in = nullptr, bool fExplicit = false) override; - - //! change which version we're allowed to upgrade to (note that this does not immediately imply upgrading to that format) - bool SetMaxVersion(int nVersion); + //! signify that a particular wallet feature is now used. + void SetMinVersion(enum WalletFeature, WalletBatch* batch_in = nullptr) override; //! get the current wallet format (the oldest client version guaranteed to understand this wallet) int GetVersion() const { LOCK(cs_wallet); return nWalletVersion; } @@ -1334,7 +1333,9 @@ public: /** overwrite all flags by the given uint64_t returns false if unknown, non-tolerable flags are present */ - bool SetWalletFlags(uint64_t overwriteFlags, bool memOnly); + bool AddWalletFlags(uint64_t flags); + /** Loads the flags into the wallet. (used by LoadWallet) */ + bool LoadWalletFlags(uint64_t flags); /** Determine if we are a legacy wallet */ bool IsLegacy() const; @@ -1415,12 +1416,15 @@ public: //! Instantiate a descriptor ScriptPubKeyMan from the WalletDescriptor and load it void LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc); - //! Sets the active ScriptPubKeyMan for the specified type and internal + //! Adds the active ScriptPubKeyMan for the specified type and internal. Writes it to the wallet file //! @param[in] id The unique id for the ScriptPubKeyMan - //! @param[in] type The OutputType this ScriptPubKeyMan provides addresses for //! @param[in] internal Whether this ScriptPubKeyMan provides change addresses - //! @param[in] memonly Whether to record this update to the database. Set to true for wallet loading, normally false when actually updating the wallet. - void SetActiveScriptPubKeyMan(uint256 id, bool internal, bool memonly = false); + void AddActiveScriptPubKeyMan(uint256 id, bool internal); + + //! Loads an active ScriptPubKeyMan for the specified type and internal. (used by LoadWallet) + //! @param[in] id The unique id for the ScriptPubKeyMan + //! @param[in] internal Whether this ScriptPubKeyMan provides change addresses + void LoadActiveScriptPubKeyMan(uint256 id, bool internal); //! Create new DescriptorScriptPubKeyMans and add them to the wallet void SetupDescriptorScriptPubKeyMans(); diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 3c9d2394bd..a7b02934f3 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -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"}; @@ -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 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}; @@ -516,7 +546,7 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, CHDChain chain; ssValue >> chain; assert ((strType == DBKeys::CRYPTED_HDCHAIN) == chain.IsCrypted()); - if (!pwallet->GetOrCreateLegacyScriptPubKeyMan()->SetHDChainSingle(chain, true)) + if (!pwallet->GetOrCreateLegacyScriptPubKeyMan()->LoadHDChain(chain)) { strErr = "Error reading wallet database: SetHDChain failed"; return false; @@ -554,13 +584,6 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, strErr = "Invalid governance object: LoadGovernanceObject"; return false; } - } else if (strType == DBKeys::FLAGS) { - uint64_t flags; - ssValue >> flags; - if (!pwallet->SetWalletFlags(flags, true)) { - strErr = "Error reading wallet database: Unknown non-tolerable wallet flags found"; - return false; - } } else if (strType == DBKeys::OLD_KEY) { strErr = "Found unsupported 'wkey' record, try loading with version 0.17"; return false; @@ -610,6 +633,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 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; @@ -665,7 +699,8 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, } else if (strType != DBKeys::BESTBLOCK && strType != DBKeys::BESTBLOCK_NOMERKLE && strType != DBKeys::MINVERSION && strType != DBKeys::ACENTRY && strType != DBKeys::VERSION && strType != DBKeys::SETTINGS && - strType != DBKeys::PRIVATESEND_SALT && strType != DBKeys::COINJOIN_SALT) { + strType != DBKeys::PRIVATESEND_SALT && strType != DBKeys::COINJOIN_SALT && + strType != DBKeys::FLAGS) { wss.m_unknown_records++; } } catch (const std::exception& e) { @@ -711,6 +746,16 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet) pwallet->LoadMinVersion(nMinVersion); } + // Load wallet flags, so they are known when processing other records. + // The FLAGS key is absent during wallet creation. + uint64_t flags; + if (m_batch->Read(DBKeys::FLAGS, flags)) { + if (!pwallet->LoadWalletFlags(flags)) { + pwallet->WalletLogPrintf("Error reading wallet database: Unknown non-tolerable wallet flags found\n"); + return DBErrors::CORRUPT; + } + } + // Get cursor if (!m_batch->StartCursor()) { @@ -768,10 +813,10 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet) // Set the active ScriptPubKeyMans for (auto spk_man : wss.m_active_external_spks) { - pwallet->SetActiveScriptPubKeyMan(spk_man.second, /* internal */ false, /* memonly */ true); + pwallet->LoadActiveScriptPubKeyMan(spk_man.second, /* internal */ false); } for (auto spk_man : wss.m_active_internal_spks) { - pwallet->SetActiveScriptPubKeyMan(spk_man.second, /* internal */ true, /* memonly */ true); + pwallet->LoadActiveScriptPubKeyMan(spk_man.second, /* internal */ true); } // Set the descriptor caches @@ -840,6 +885,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; } diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 547fb1e351..916155d727 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -217,6 +217,8 @@ public: 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); diff --git a/src/wallet/walletutil.cpp b/src/wallet/walletutil.cpp index 2e98940e3b..68bae737a8 100644 --- a/src/wallet/walletutil.cpp +++ b/src/wallet/walletutil.cpp @@ -28,3 +28,17 @@ fs::path GetWalletDir() return path; } + +bool IsFeatureSupported(int wallet_version, int feature_version) +{ + return wallet_version >= feature_version; +} + +WalletFeature GetClosestWalletFeature(int version) +{ + const std::array wallet_features{{FEATURE_LATEST, FEATURE_HD, FEATURE_COMPRPUBKEY, FEATURE_WALLETCRYPT, FEATURE_BASE}}; + for (const WalletFeature& wf : wallet_features) { + if (version >= wf) return wf; + } + return static_cast(0); +} diff --git a/src/wallet/walletutil.h b/src/wallet/walletutil.h index 93547ce0d9..2e67db11d4 100644 --- a/src/wallet/walletutil.h +++ b/src/wallet/walletutil.h @@ -23,6 +23,8 @@ enum WalletFeature FEATURE_LATEST = FEATURE_HD }; +bool IsFeatureSupported(int wallet_version, int feature_version); +WalletFeature GetClosestWalletFeature(int version); enum WalletFlags : uint64_t { // wallet flags in the upper section (> 1 << 31) will lead to not opening the wallet if flag is unknown @@ -35,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), diff --git a/test/functional/data/wallets/high_minversion/.walletlock b/test/functional/data/wallets/high_minversion/.walletlock deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/functional/data/wallets/high_minversion/GENERATE.md b/test/functional/data/wallets/high_minversion/GENERATE.md deleted file mode 100644 index e55c4557ca..0000000000 --- a/test/functional/data/wallets/high_minversion/GENERATE.md +++ /dev/null @@ -1,8 +0,0 @@ -The wallet has been created by starting Bitcoin Core with the options -`-regtest -datadir=/tmp -nowallet -walletdir=$(pwd)/test/functional/data/wallets/`. - -In the source code, `WalletFeature::FEATURE_LATEST` has been modified to be large, so that the minversion is too high -for a current build of the wallet. - -The wallet has then been created with the RPC `createwallet high_minversion true true`, so that a blank wallet with -private keys disabled is created. diff --git a/test/functional/data/wallets/high_minversion/db.log b/test/functional/data/wallets/high_minversion/db.log deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/functional/data/wallets/high_minversion/wallet.dat b/test/functional/data/wallets/high_minversion/wallet.dat deleted file mode 100644 index 99ab809263..0000000000 Binary files a/test/functional/data/wallets/high_minversion/wallet.dat and /dev/null differ diff --git a/test/functional/test_framework/bdb.py b/test/functional/test_framework/bdb.py new file mode 100644 index 0000000000..9de358aa0a --- /dev/null +++ b/test/functional/test_framework/bdb.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Utilities for working directly with the wallet's BDB database file + +This is specific to the configuration of BDB used in this project: + - pagesize: 4096 bytes + - Outer database contains single subdatabase named 'main' + - btree + - btree leaf pages + +Each key-value pair is two entries in a btree leaf. The first is the key, the one that follows +is the value. And so on. Note that the entry data is itself not in the correct order. Instead +entry offsets are stored in the correct order and those offsets are needed to then retrieve +the data itself. + +Page format can be found in BDB source code dbinc/db_page.h +This only implements the deserialization of btree metadata pages and normal btree pages. Overflow +pages are not implemented but may be needed in the future if dealing with wallets with large +transactions. + +`db_dump -da wallet.dat` is useful to see the data in a wallet.dat BDB file +""" + +import binascii +import struct + +# Important constants +PAGESIZE = 4096 +OUTER_META_PAGE = 0 +INNER_META_PAGE = 2 + +# Page type values +BTREE_INTERNAL = 3 +BTREE_LEAF = 5 +BTREE_META = 9 + +# Some magic numbers for sanity checking +BTREE_MAGIC = 0x053162 +DB_VERSION = 9 + +# Deserializes a leaf page into a dict. +# Btree internal pages have the same header, for those, return None. +# For the btree leaf pages, deserialize them and put all the data into a dict +def dump_leaf_page(data): + page_info = {} + page_header = data[0:26] + _, pgno, prev_pgno, next_pgno, entries, hf_offset, level, pg_type = struct.unpack('QIIIHHBB', page_header) + page_info['pgno'] = pgno + page_info['prev_pgno'] = prev_pgno + page_info['next_pgno'] = next_pgno + page_info['entries'] = entries + page_info['hf_offset'] = hf_offset + page_info['level'] = level + page_info['pg_type'] = pg_type + page_info['entry_offsets'] = struct.unpack('{}H'.format(entries), data[26:26 + entries * 2]) + page_info['entries'] = [] + + if pg_type == BTREE_INTERNAL: + # Skip internal pages. These are the internal nodes of the btree and don't contain anything relevant to us + return None + + assert pg_type == BTREE_LEAF, 'A non-btree leaf page has been encountered while dumping leaves' + + for i in range(0, entries): + offset = page_info['entry_offsets'][i] + entry = {'offset': offset} + page_data_header = data[offset:offset + 3] + e_len, pg_type = struct.unpack('HB', page_data_header) + entry['len'] = e_len + entry['pg_type'] = pg_type + entry['data'] = data[offset + 3:offset + 3 + e_len] + page_info['entries'].append(entry) + + return page_info + +# Deserializes a btree metadata page into a dict. +# Does a simple sanity check on the magic value, type, and version +def dump_meta_page(page): + # metadata page + # general metadata + metadata = {} + meta_page = page[0:72] + _, pgno, magic, version, pagesize, encrypt_alg, pg_type, metaflags, _, free, last_pgno, nparts, key_count, record_count, flags, uid = struct.unpack('QIIIIBBBBIIIIII20s', meta_page) + metadata['pgno'] = pgno + metadata['magic'] = magic + metadata['version'] = version + metadata['pagesize'] = pagesize + metadata['encrypt_alg'] = encrypt_alg + metadata['pg_type'] = pg_type + metadata['metaflags'] = metaflags + metadata['free'] = free + metadata['last_pgno'] = last_pgno + metadata['nparts'] = nparts + metadata['key_count'] = key_count + metadata['record_count'] = record_count + metadata['flags'] = flags + metadata['uid'] = binascii.hexlify(uid) + + assert magic == BTREE_MAGIC, 'bdb magic does not match bdb btree magic' + assert pg_type == BTREE_META, 'Metadata page is not a btree metadata page' + assert version == DB_VERSION, 'Database too new' + + # btree metadata + btree_meta_page = page[72:512] + _, minkey, re_len, re_pad, root, _, crypto_magic, _, iv, chksum = struct.unpack('IIIII368sI12s16s20s', btree_meta_page) + metadata['minkey'] = minkey + metadata['re_len'] = re_len + metadata['re_pad'] = re_pad + metadata['root'] = root + metadata['crypto_magic'] = crypto_magic + metadata['iv'] = binascii.hexlify(iv) + metadata['chksum'] = binascii.hexlify(chksum) + return metadata + +# Given the dict from dump_leaf_page, get the key-value pairs and put them into a dict +def extract_kv_pairs(page_data): + out = {} + last_key = None + for i, entry in enumerate(page_data['entries']): + # By virtue of these all being pairs, even number entries are keys, and odd are values + if i % 2 == 0: + out[entry['data']] = b'' + last_key = entry['data'] + else: + out[last_key] = entry['data'] + return out + +# Extract the key-value pairs of the BDB file given in filename +def dump_bdb_kv(filename): + # Read in the BDB file and start deserializing it + pages = [] + with open(filename, 'rb') as f: + data = f.read(PAGESIZE) + while len(data) > 0: + pages.append(data) + data = f.read(PAGESIZE) + + # Sanity check the meta pages + dump_meta_page(pages[OUTER_META_PAGE]) + dump_meta_page(pages[INNER_META_PAGE]) + + # Fetch the kv pairs from the leaf pages + kv = {} + for i in range(3, len(pages)): + info = dump_leaf_page(pages[i]) + if info is not None: + info_kv = extract_kv_pairs(info) + kv = {**kv, **info_kv} + return kv diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index fd636ec136..0736669811 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -9,6 +9,7 @@ from base64 import b64encode from binascii import unhexlify from decimal import Decimal, ROUND_DOWN from subprocess import CalledProcessError +import hashlib import inspect import json import logging @@ -268,6 +269,14 @@ def wait_until_helper(predicate, *, attempts=float('inf'), timeout=float('inf'), else: return False +def sha256sum_file(filename): + h = hashlib.sha256() + with open(filename, 'rb') as f: + d = f.read(4096) + while len(d) > 0: + h.update(d) + d = f.read(4096) + return h.digest() # RPC/P2P connection constants and functions ############################################ diff --git a/test/functional/wallet_hd.py b/test/functional/wallet_hd.py index f07bf69625..eaa2c0e710 100755 --- a/test/functional/wallet_hd.py +++ b/test/functional/wallet_hd.py @@ -11,6 +11,7 @@ from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_raises_rpc_error, ) class WalletHDTest(BitcoinTestFramework): @@ -137,5 +138,148 @@ class WalletHDTest(BitcoinTestFramework): assert_equal(keypath[0:13], "m/44'/1'/0'/1") + if not self.options.descriptors: + # NOTE: sethdseed can't replace existing seed in Dash Core + # though bitcoin lets to do it. Therefore this functional test + # are not the same with bitcoin's + # Generate a new HD seed on node 1 and make sure it is set + + self.nodes[1].createwallet(wallet_name='wallet_new_seed', blank=True) + wallet_new_seed = self.nodes[1].get_wallet_rpc('wallet_new_seed') + assert 'hdchainid' not in wallet_new_seed.getwalletinfo() + wallet_new_seed.sethdseed() + new_masterkeyid = wallet_new_seed.getwalletinfo()['hdchainid'] + addr = wallet_new_seed.getnewaddress() + # Make sure the new address is the first from the keypool + assert_equal(wallet_new_seed.getaddressinfo(addr)['hdkeypath'], "m/44'/1'/0'/0/1") + wallet_new_seed.keypoolrefill(1) # Fill keypool with 1 key + + # Set a new HD seed on node 1 without flushing the keypool + new_seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress()) + assert_raises_rpc_error(-4, "Cannot set a HD seed. The wallet already has a seed", wallet_new_seed.sethdseed, False, new_seed) + self.nodes[1].createwallet(wallet_name='wallet_imported_seed', blank=True) + wallet_imported_seed = self.nodes[1].get_wallet_rpc('wallet_imported_seed') + wallet_imported_seed.sethdseed(False, new_seed) + + new_masterkeyid = wallet_imported_seed.getwalletinfo()['hdchainid'] + addr = wallet_imported_seed.getnewaddress() + assert_equal(new_masterkeyid, wallet_imported_seed.getaddressinfo(addr)['hdchainid']) + # Make sure the new address continues previous keypool + assert_equal(wallet_imported_seed.getaddressinfo(addr)['hdkeypath'], "m/44'/1'/0'/0/0") + + # Check that the next address is from the new seed + wallet_imported_seed.keypoolrefill(1) + next_addr = wallet_imported_seed.getnewaddress() + assert_equal(new_masterkeyid, wallet_imported_seed.getaddressinfo(next_addr)['hdchainid']) + # Make sure the new address is not from previous keypool + assert_equal(wallet_imported_seed.getaddressinfo(next_addr)['hdkeypath'], "m/44'/1'/0'/0/1") + assert next_addr != addr + + self.nodes[1].createwallet(wallet_name='wallet_no_seed', blank=True) + wallet_no_seed = self.nodes[1].get_wallet_rpc('wallet_no_seed') + wallet_no_seed.importprivkey(non_hd_key) + # Sethdseed parameter validity + assert_raises_rpc_error(-1, 'sethdseed', self.nodes[0].sethdseed, False, new_seed, 0) + assert_raises_rpc_error(-5, "Invalid private key", wallet_no_seed.sethdseed, False, "not_wif") + assert_raises_rpc_error(-1, "JSON value is not a boolean as expected", wallet_no_seed.sethdseed, "Not_bool") + assert_raises_rpc_error(-1, "JSON value is not a string as expected", wallet_no_seed.sethdseed, False, True) + assert_raises_rpc_error(-5, "Already have this key", wallet_no_seed.sethdseed, False, non_hd_key) + + self.log.info('Test sethdseed restoring with keys outside of the initial keypool') + self.nodes[0].generate(10) + # Restart node 1 with keypool of 3 and a different wallet + self.nodes[1].createwallet(wallet_name='origin', blank=True) + self.restart_node(1, extra_args=['-keypool=3', '-wallet=origin']) + self.connect_nodes(0, 1) + + # sethdseed restoring and seeing txs to addresses out of the keypool + origin_rpc = self.nodes[1].get_wallet_rpc('origin') + seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress()) + origin_rpc.sethdseed(True, seed) + + self.nodes[1].createwallet(wallet_name='restore', blank=True) + restore_rpc = self.nodes[1].get_wallet_rpc('restore') + restore_rpc.sethdseed(True, seed) # Set to be the same seed as origin_rpc + + self.nodes[1].createwallet(wallet_name='restore2', blank=True) + restore2_rpc = self.nodes[1].get_wallet_rpc('restore2') + restore2_rpc.sethdseed(True, seed) # Set to be the same seed as origin_rpc + + # Check persistence of inactive seed by reloading restore. restore2 is still loaded to test the case where the wallet is not reloaded + restore_rpc.unloadwallet() + self.nodes[1].loadwallet('restore') + restore_rpc = self.nodes[1].get_wallet_rpc('restore') + + # Empty origin keypool and get an address that is beyond the initial keypool + origin_rpc.getnewaddress() + origin_rpc.getnewaddress() + last_addr = origin_rpc.getnewaddress() # Last address of initial keypool + addr = origin_rpc.getnewaddress() # First address beyond initial keypool + + # Check that the restored seed has last_addr but does not have addr + info = restore_rpc.getaddressinfo(last_addr) + assert_equal(info['ismine'], True) + info = restore_rpc.getaddressinfo(addr) + assert_equal(info['ismine'], False) + info = restore2_rpc.getaddressinfo(last_addr) + assert_equal(info['ismine'], True) + info = restore2_rpc.getaddressinfo(addr) + assert_equal(info['ismine'], False) + # Check that the origin seed has addr + info = origin_rpc.getaddressinfo(addr) + assert_equal(info['ismine'], True) + + # Send a transaction to addr, which is out of the initial keypool. + # The wallet that has set a new seed (restore_rpc) should not detect this transaction. + txid = self.nodes[0].sendtoaddress(addr, 1) + origin_rpc.sendrawtransaction(self.nodes[0].gettransaction(txid)['hex']) + self.nodes[0].generate(1) + self.sync_blocks() + origin_rpc.gettransaction(txid) + assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, txid) + out_of_kp_txid = txid + + # Send a transaction to last_addr, which is in the initial keypool. + # The wallet that has set a new seed (restore_rpc) should detect this transaction and generate 3 new keys from the initial seed. + # The previous transaction (out_of_kp_txid) should still not be detected as a rescan is required. + txid = self.nodes[0].sendtoaddress(last_addr, 1) + origin_rpc.sendrawtransaction(self.nodes[0].gettransaction(txid)['hex']) + self.nodes[0].generate(1) + self.sync_blocks() + origin_rpc.gettransaction(txid) + restore_rpc.gettransaction(txid) + assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, out_of_kp_txid) + restore2_rpc.gettransaction(txid) + assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore2_rpc.gettransaction, out_of_kp_txid) + + # After rescanning, restore_rpc should now see out_of_kp_txid and generate an additional key. + # addr should now be part of restore_rpc and be ismine + restore_rpc.rescanblockchain() + restore_rpc.gettransaction(out_of_kp_txid) + info = restore_rpc.getaddressinfo(addr) + assert_equal(info['ismine'], True) + restore2_rpc.rescanblockchain() + restore2_rpc.gettransaction(out_of_kp_txid) + info = restore2_rpc.getaddressinfo(addr) + assert_equal(info['ismine'], True) + + # Check again that 3 keys were derived. + # Empty keypool and get an address that is beyond the initial keypool + origin_rpc.getnewaddress() + origin_rpc.getnewaddress() + last_addr = origin_rpc.getnewaddress() + addr = origin_rpc.getnewaddress() + + # Check that the restored seed has last_addr but does not have addr + info = restore_rpc.getaddressinfo(last_addr) + assert_equal(info['ismine'], True) + info = restore_rpc.getaddressinfo(addr) + assert_equal(info['ismine'], False) + info = restore2_rpc.getaddressinfo(last_addr) + assert_equal(info['ismine'], True) + info = restore2_rpc.getaddressinfo(addr) + assert_equal(info['ismine'], False) + + if __name__ == '__main__': WalletHDTest().main () diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py index e1ffd34ecd..8896a765f0 100755 --- a/test/functional/wallet_listdescriptors.py +++ b/test/functional/wallet_listdescriptors.py @@ -73,6 +73,10 @@ class ListDescriptorsTest(BitcoinTestFramework): } 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') diff --git a/test/functional/wallet_upgradewallet.py b/test/functional/wallet_upgradewallet.py index 4d81201c01..f7cf9ee8da 100755 --- a/test/functional/wallet_upgradewallet.py +++ b/test/functional/wallet_upgradewallet.py @@ -10,25 +10,29 @@ Only v0.15.2 and v0.16.3 are required by this test. The others are used in featu import os import shutil +import struct +from test_framework.bdb import dump_bdb_kv from test_framework.blocktools import COINBASE_MATURITY from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_greater_than, - assert_greater_than_or_equal, assert_is_hex_string, + sha256sum_file, ) +UPGRADED_KEYMETA_VERSION = 12 + class UpgradeWalletTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 self.extra_args = [ - [], # current wallet version - ["-usehd=1"], # v18.2.2 wallet - ["-usehd=0"] # v0.16.1.1 wallet + ["-keypool=2"], # current wallet version + ["-usehd=1", "-keypool=2"], # v18.2.2 wallet + ["-usehd=0", "-keypool=2"], # v0.16.1.1 wallet ] self.wallet_names = [self.default_wallet_name, None, None] @@ -65,6 +69,32 @@ class UpgradeWalletTest(BitcoinTestFramework): v18_2_node.submitblock(b) assert_equal(v18_2_node.getblockcount(), to_height) + def test_upgradewallet(self, wallet, previous_version, requested_version=None, expected_version=None): + unchanged = expected_version == previous_version + new_version = previous_version if unchanged else expected_version if expected_version else requested_version + assert_equal(wallet.getwalletinfo()["walletversion"], previous_version) + assert_equal(wallet.upgradewallet(requested_version), + { + "wallet_name": "", + "previous_version": previous_version, + "current_version": new_version, + "result": "Already at latest version. Wallet version unchanged." if unchanged else "Wallet upgraded successfully from version {} to version {}.".format(previous_version, new_version), + } + ) + assert_equal(wallet.getwalletinfo()["walletversion"], new_version) + + def test_upgradewallet_error(self, wallet, previous_version, requested_version, msg): + assert_equal(wallet.getwalletinfo()["walletversion"], previous_version) + assert_equal(wallet.upgradewallet(requested_version), + { + "wallet_name": "", + "previous_version": previous_version, + "current_version": previous_version, + "error": msg, + } + ) + assert_equal(wallet.getwalletinfo()["walletversion"], previous_version) + def run_test(self): self.nodes[0].generatetoaddress(COINBASE_MATURITY + 1, self.nodes[0].getnewaddress()) self.dumb_sync_blocks() @@ -84,12 +114,12 @@ class UpgradeWalletTest(BitcoinTestFramework): self.log.info("Test upgradewallet RPC...") # Prepare for copying of the older wallet - node_master_wallet_dir = os.path.join(node_master.datadir, "regtest/wallets") + node_master_wallet_dir = os.path.join(node_master.datadir, "regtest/wallets", self.default_wallet_name) + node_master_wallet = os.path.join(node_master_wallet_dir, self.default_wallet_name, self.wallet_data_filename) v18_2_wallet = os.path.join(v18_2_node.datadir, "regtest/wallets/wallet.dat") v16_1_wallet = os.path.join(v16_1_node.datadir, "regtest/wallets/wallet.dat") self.stop_nodes() - # Copy the 0.16.3 wallet to the last Dash Core version and open it: shutil.rmtree(node_master_wallet_dir) os.mkdir(node_master_wallet_dir) shutil.copy( @@ -99,39 +129,45 @@ class UpgradeWalletTest(BitcoinTestFramework): self.restart_node(0, ['-nowallet']) node_master.loadwallet('') - wallet = node_master.get_wallet_rpc('') - old_version = wallet.getwalletinfo()["walletversion"] + def copy_v16(): + node_master.get_wallet_rpc(self.default_wallet_name).unloadwallet() + # Copy the 0.16.3 wallet to the last Dash Core version and open it: + shutil.rmtree(node_master_wallet_dir) + os.mkdir(node_master_wallet_dir) + shutil.copy( + v18_2_wallet, + node_master_wallet_dir + ) + node_master.loadwallet(self.default_wallet_name) - # calling upgradewallet without version arguments - # should return nothing if successful - assert_equal(wallet.upgradewallet(), {}) - new_version = wallet.getwalletinfo()["walletversion"] - # upgraded wallet version should be greater than older one - assert_greater_than_or_equal(new_version, old_version) + def copy_non_hd(): + node_master.get_wallet_rpc(self.default_wallet_name).unloadwallet() + # Copy the 19.3.0 wallet to the last Dash Core version and open it: + shutil.rmtree(node_master_wallet_dir) + os.mkdir(node_master_wallet_dir) + shutil.copy( + v16_1_wallet, + node_master_wallet_dir + ) + node_master.loadwallet(self.default_wallet_name) + + + self.restart_node(0) + copy_v16() + wallet = node_master.get_wallet_rpc(self.default_wallet_name) + self.test_upgradewallet(wallet, previous_version=120200, expected_version=120200) # wallet should still contain the same balance assert_equal(wallet.getbalance(), v18_2_balance) - self.stop_node(0) - # Copy the 19.3.0 wallet to the last Dash Core version and open it: - shutil.rmtree(node_master_wallet_dir) - os.mkdir(node_master_wallet_dir) - shutil.copy( - v16_1_wallet, - node_master_wallet_dir - ) - self.restart_node(0, ['-nowallet']) - node_master.loadwallet('') - - wallet = node_master.get_wallet_rpc('') + copy_non_hd() + wallet = node_master.get_wallet_rpc(self.default_wallet_name) # should have no master key hash before conversion - assert_equal('hdseedid' in wallet.getwalletinfo(), False) + assert_equal('hdchainid' in wallet.getwalletinfo(), False) # calling upgradewallet with explicit version number # should return nothing if successful - assert_equal(wallet.upgradewallet(169900), {}) - new_version = wallet.getwalletinfo()["walletversion"] - # upgraded wallet would not have 120200 version until HD seed actually appeared - assert_greater_than(120200, new_version) + self.log.info("Test upgradewallet to HD will have version 120200 but no HD seed actually appeared") + self.test_upgradewallet(wallet, previous_version=61000, expected_version=120200, requested_version=169900) # after conversion master key hash should not be present yet assert 'hdchainid' not in wallet.getwalletinfo() assert_equal(wallet.upgradetohd(), True) @@ -139,5 +175,65 @@ class UpgradeWalletTest(BitcoinTestFramework): assert_equal(new_version, 120200) assert_is_hex_string(wallet.getwalletinfo()['hdchainid']) + self.log.info("Intermediary versions don't effect anything") + copy_non_hd() + # Wallet starts with 61000 (legacy "latest") + assert_equal(61000, wallet.getwalletinfo()['walletversion']) + wallet.unloadwallet() + before_checksum = sha256sum_file(node_master_wallet) + node_master.loadwallet('') + # Test an "upgrade" from 61000 to 120199 has no effect, as the next version is 120200 + self.test_upgradewallet(wallet, previous_version=61000, requested_version=120199, expected_version=61000) + wallet.unloadwallet() + assert_equal(before_checksum, sha256sum_file(node_master_wallet)) + node_master.loadwallet('') + + self.log.info('Wallets cannot be downgraded') + copy_non_hd() + self.test_upgradewallet_error(wallet, previous_version=61000, requested_version=40000, + msg="Cannot downgrade wallet from version 61000 to version 40000. Wallet version unchanged.") + wallet.unloadwallet() + assert_equal(before_checksum, sha256sum_file(node_master_wallet)) + node_master.loadwallet('') + + self.log.info('Can upgrade to HD') + # Inspect the old wallet and make sure there is no hdchain + orig_kvs = dump_bdb_kv(node_master_wallet) + assert b'\x07hdchain' not in orig_kvs + # Upgrade to HD + self.test_upgradewallet(wallet, previous_version=61000, requested_version=120200) + # Check that there is now a hd chain and it is version 1, no internal chain counter + new_kvs = dump_bdb_kv(node_master_wallet) + wallet.upgradetohd() + new_kvs = dump_bdb_kv(node_master_wallet) + assert b'\x07hdchain' in new_kvs + hd_chain = new_kvs[b'\x07hdchain'] + # hd_chain: + # obj.nVersion int + # obj.id uint256 + # obj.fCrypted bool + # obj.vchSeed SecureVector + # obj.vchMnemonic SecureVector + # obj.vchMnemonicPassphrase SecureVector + # obj.mapAccounts map<> accounts + assert_greater_than(220, len(hd_chain)) + assert_greater_than(len(hd_chain), 180) + hd_chain_version, seed_id, is_crypted = struct.unpack('