Skip to content

Commit

Permalink
Switch hardened derivation marker to h in descriptors
Browse files Browse the repository at this point in the history
This makes it easier to handle descriptor strings manually. E.g. an RPC call that takes an array of descriptors can now use '["desc": ".../0h/..."]'.

Both markers can still be parsed. The default for new descriptors is changed to h. In normalized form h is also used. For private keys the chosen marker is preserved in a round trip.

The hdkeypath field in getaddressinfo is also impacted by this change.
  • Loading branch information
Sjors committed Apr 4, 2023
1 parent fe1b325 commit bd13dc2
Show file tree
Hide file tree
Showing 20 changed files with 161 additions and 142 deletions.
12 changes: 12 additions & 0 deletions doc/release-notes-26076.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
RPC
---

- The `listdescriptors` RPC now shows `h` rather than apostrophe (`'`) to indicate
hardened derivation. This does not apply when using the `private` parameter, which
matches the marker used when descriptor was generated or imported. Newly created
wallets use `h`. This change makes it easier to handle descriptor strings manually.
E.g. an RPC call that takes an array of descriptors can now use '["desc": ".../0h/..."]'.
Either `h` or `'` can still be used when providing a descriptor to e.g. `getdescriptorinfo`
or `importdescriptors`. Note that this choice changes the descriptor checksum.
For legacy wallets the `hdkeypath` field in `getaddressinfo` is unchanged,
nor is the serialization format of wallet dumps. (#26076)
83 changes: 55 additions & 28 deletions src/script/descriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ struct PubkeyProvider
/** Get the descriptor string form including private data (if available in arg). */
virtual bool ToPrivateString(const SigningProvider& arg, std::string& out) const = 0;

/** Get the descriptor string form with the xpub at the last hardened derivation */
/** Get the descriptor string form with the xpub at the last hardened derivation,
* and always use h for hardened derivation.
*/
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. */
Expand All @@ -208,14 +210,15 @@ class OriginPubkeyProvider final : public PubkeyProvider
{
KeyOriginInfo m_origin;
std::unique_ptr<PubkeyProvider> m_provider;
bool m_apostrophe;

std::string OriginString() const
std::string OriginString(bool normalized=false) const
{
return HexStr(m_origin.fingerprint) + FormatHDKeypath(m_origin.path);
return HexStr(m_origin.fingerprint) + FormatHDKeypath(m_origin.path, /*apostrophe=*/!normalized && m_apostrophe);
}

public:
OriginPubkeyProvider(uint32_t exp_index, KeyOriginInfo info, std::unique_ptr<PubkeyProvider> provider) : PubkeyProvider(exp_index), m_origin(std::move(info)), m_provider(std::move(provider)) {}
OriginPubkeyProvider(uint32_t exp_index, KeyOriginInfo info, std::unique_ptr<PubkeyProvider> provider, bool apostrophe) : PubkeyProvider(exp_index), m_origin(std::move(info)), m_provider(std::move(provider)), m_apostrophe(apostrophe) {}
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;
Expand All @@ -242,9 +245,9 @@ class OriginPubkeyProvider final : public PubkeyProvider
// and append that to our own origin string.
if (sub[0] == '[') {
sub = sub.substr(9);
ret = "[" + OriginString() + std::move(sub);
ret = "[" + OriginString(/*normalized=*/true) + std::move(sub);
} else {
ret = "[" + OriginString() + "]" + std::move(sub);
ret = "[" + OriginString(/*normalized=*/true) + "]" + std::move(sub);
}
return true;
}
Expand Down Expand Up @@ -312,6 +315,8 @@ class BIP32PubkeyProvider final : public PubkeyProvider
CExtPubKey m_root_extkey;
KeyPath m_path;
DeriveType m_derive;
// Whether ' or h is used in harded derivation
bool m_apostrophe;

bool GetExtKey(const SigningProvider& arg, CExtKey& ret) const
{
Expand Down Expand Up @@ -348,7 +353,7 @@ class BIP32PubkeyProvider final : public PubkeyProvider
}

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) {}
BIP32PubkeyProvider(uint32_t exp_index, const CExtPubKey& extkey, KeyPath path, DeriveType derive, bool apostrophe) : PubkeyProvider(exp_index), m_root_extkey(extkey), m_path(std::move(path)), m_derive(derive), m_apostrophe(apostrophe) {}
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) const override
Expand Down Expand Up @@ -416,31 +421,36 @@ class BIP32PubkeyProvider final : public PubkeyProvider

return true;
}
std::string ToString() const override
std::string ToString(bool normalized) const
{
std::string ret = EncodeExtPubKey(m_root_extkey) + FormatHDKeypath(m_path);
const bool use_apostrophe = !normalized && m_apostrophe;
std::string ret = EncodeExtPubKey(m_root_extkey) + FormatHDKeypath(m_path, /*apostrophe=*/use_apostrophe);
if (IsRange()) {
ret += "/*";
if (m_derive == DeriveType::HARDENED) ret += '\'';
if (m_derive == DeriveType::HARDENED) ret += use_apostrophe ? '\'' : 'h';
}
return ret;
}
std::string ToString() const override
{
return ToString(/*normalized=*/false);
}
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
{
CExtKey key;
if (!GetExtKey(arg, key)) return false;
out = EncodeExtKey(key) + FormatHDKeypath(m_path);
out = EncodeExtKey(key) + FormatHDKeypath(m_path, /*apostrophe=*/m_apostrophe);
if (IsRange()) {
out += "/*";
if (m_derive == DeriveType::HARDENED) out += '\'';
if (m_derive == DeriveType::HARDENED) out += m_apostrophe ? '\'' : 'h';
}
return true;
}
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) {
out = ToString();
out = ToString(/*normalized=*/true);

return true;
}
// Step backwards to find the last hardened step in the path
Expand Down Expand Up @@ -1048,15 +1058,27 @@ enum class ParseScriptContext {
P2TR, //!< Inside tr() (either internal key, or BIP342 script leaf)
};

/** Parse a key path, being passed a split list of elements (the first element is ignored). */
[[nodiscard]] bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath& out, std::string& error)
/**
* Parse a key path, being passed a split list of elements (the first element is ignored).
*
* @param[in] split BIP32 path string, using either ' or h for hardened derivation
* @param[out] out the key path
* @param[out] apostrophe only updated if hardened derivation is found
* @param[out] error parsing error message
* @returns false if parsing failed
**/
[[nodiscard]] bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath& out, bool& apostrophe, std::string& error)
{
for (size_t i = 1; i < split.size(); ++i) {
Span<const char> elem = split[i];
bool hardened = false;
if (elem.size() > 0 && (elem[elem.size() - 1] == '\'' || elem[elem.size() - 1] == 'h')) {
elem = elem.first(elem.size() - 1);
hardened = true;
if (elem.size() > 0) {
const char last = elem[elem.size() - 1];
if (last == '\'' || last == 'h') {
elem = elem.first(elem.size() - 1);
hardened = true;
apostrophe = last == '\'';
}
}
uint32_t p;
if (!ParseUInt32(std::string(elem.begin(), elem.end()), &p)) {
Expand All @@ -1072,7 +1094,7 @@ enum class ParseScriptContext {
}

/** Parse a public key that excludes origin information. */
std::unique_ptr<PubkeyProvider> ParsePubkeyInner(uint32_t key_exp_index, const Span<const char>& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error)
std::unique_ptr<PubkeyProvider> ParsePubkeyInner(uint32_t key_exp_index, const Span<const char>& sp, ParseScriptContext ctx, FlatSigningProvider& out, bool& apostrophe, std::string& error)
{
using namespace spanparsing;

Expand Down Expand Up @@ -1129,15 +1151,16 @@ std::unique_ptr<PubkeyProvider> ParsePubkeyInner(uint32_t key_exp_index, const S
split.pop_back();
type = DeriveType::UNHARDENED;
} else if (split.back() == Span{"*'"}.first(2) || split.back() == Span{"*h"}.first(2)) {
apostrophe = split.back() == Span{"*'"}.first(2);
split.pop_back();
type = DeriveType::HARDENED;
}
if (!ParseKeyPath(split, path, error)) return nullptr;
if (!ParseKeyPath(split, path, apostrophe, error)) return nullptr;
if (extkey.key.IsValid()) {
extpubkey = extkey.Neuter();
out.keys.emplace(extpubkey.pubkey.GetID(), extkey.key);
}
return std::make_unique<BIP32PubkeyProvider>(key_exp_index, extpubkey, std::move(path), type);
return std::make_unique<BIP32PubkeyProvider>(key_exp_index, extpubkey, std::move(path), type, apostrophe);
}

/** Parse a public key including origin information (if enabled). */
Expand All @@ -1150,7 +1173,11 @@ std::unique_ptr<PubkeyProvider> ParsePubkey(uint32_t key_exp_index, const Span<c
error = "Multiple ']' characters found for a single pubkey";
return nullptr;
}
if (origin_split.size() == 1) return ParsePubkeyInner(key_exp_index, origin_split[0], ctx, out, error);
// This is set if either the origin or path suffix contains a hardened derivation.
bool apostrophe = false;
if (origin_split.size() == 1) {
return ParsePubkeyInner(key_exp_index, origin_split[0], ctx, out, apostrophe, error);
}
if (origin_split[0].empty() || origin_split[0][0] != '[') {
error = strprintf("Key origin start '[ character expected but not found, got '%c' instead",
origin_split[0].empty() ? /** empty, implies split char */ ']' : origin_split[0][0]);
Expand All @@ -1171,18 +1198,18 @@ std::unique_ptr<PubkeyProvider> ParsePubkey(uint32_t key_exp_index, const Span<c
static_assert(sizeof(info.fingerprint) == 4, "Fingerprint must be 4 bytes");
assert(fpr_bytes.size() == 4);
std::copy(fpr_bytes.begin(), fpr_bytes.end(), info.fingerprint);
if (!ParseKeyPath(slash_split, info.path, error)) return nullptr;
auto provider = ParsePubkeyInner(key_exp_index, origin_split[1], ctx, out, error);
if (!ParseKeyPath(slash_split, info.path, apostrophe, error)) return nullptr;
auto provider = ParsePubkeyInner(key_exp_index, origin_split[1], ctx, out, apostrophe, error);
if (!provider) return nullptr;
return std::make_unique<OriginPubkeyProvider>(key_exp_index, std::move(info), std::move(provider));
return std::make_unique<OriginPubkeyProvider>(key_exp_index, std::move(info), std::move(provider), apostrophe);
}

std::unique_ptr<PubkeyProvider> InferPubkey(const CPubKey& pubkey, ParseScriptContext, const SigningProvider& provider)
{
std::unique_ptr<PubkeyProvider> key_provider = std::make_unique<ConstPubkeyProvider>(0, pubkey, false);
KeyOriginInfo info;
if (provider.GetKeyOrigin(pubkey.GetID(), info)) {
return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider));
return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider), /*apostrophe=*/false);
}
return key_provider;
}
Expand All @@ -1195,7 +1222,7 @@ std::unique_ptr<PubkeyProvider> InferXOnlyPubkey(const XOnlyPubKey& xkey, ParseS
std::unique_ptr<PubkeyProvider> key_provider = std::make_unique<ConstPubkeyProvider>(0, pubkey, true);
KeyOriginInfo info;
if (provider.GetKeyOriginByXOnly(xkey, info)) {
return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider));
return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider), /*apostrophe=*/false);
}
return key_provider;
}
Expand Down
Loading

0 comments on commit bd13dc2

Please sign in to comment.