Skip to content

Commit fa53611

Browse files
committed
Merge bitcoin/bitcoin#26076: Switch hardened derivation marker to h
fe49f06 doc: clarify PR 26076 release note (Sjors Provoost) bd13dc2 Switch hardened derivation marker to h in descriptors (Sjors Provoost) Pull request description: This makes it easier to handle descriptor strings manually, especially when importing from another Bitcoin Core wallet. For example the `importdescriptors` RPC call is easiest to use `h` as the marker: `'["desc": ".../0h/..."]'`, avoiding the need for escape characters. With this change `listdescriptors` will use `h`, so you can copy-paste the result, without having to add escape characters or switch `'` to 'h' manually. Both markers can still be parsed. The `hdkeypath` field in `getaddressinfo` is also impacted by this change, except for legacy wallets. The latter is to prevent accidentally breaking ancient software that uses our legacy wallet. See discussion in #15740 ACKs for top commit: achow101: ACK fe49f06 darosior: re-ACK fe49f06 Tree-SHA512: f78bc873b24a6f7a2bf38f5dd58f2b723e35e6b10e4d65c36ec300e2d362d475eeca6e5afa04b3037ab4bee0bf8ebc93ea5fc18102a2111d3d88fc873c08dc89
2 parents 26cb32c + fe49f06 commit fa53611

20 files changed

+162
-142
lines changed

doc/release-notes-26076.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
RPC
2+
---
3+
4+
- The `listdescriptors`, `decodepsbt` and similar RPC methods now show `h` rather than apostrophe (`'`) to indicate
5+
hardened derivation. This does not apply when using the `private` parameter, which
6+
matches the marker used when descriptor was generated or imported. Newly created
7+
wallets use `h`. This change makes it easier to handle descriptor strings manually.
8+
E.g. the `importdescriptors` RPC call is easiest to use `h` as the marker: `'["desc": ".../0h/..."]'`.
9+
With this change `listdescriptors` will use `h`, so you can copy-paste the result,
10+
without having to add escape characters or switch `'` to 'h' manually.
11+
Note that this changes the descriptor checksum.
12+
For legacy wallets the `hdkeypath` field in `getaddressinfo` is unchanged,
13+
nor is the serialization format of wallet dumps. (#26076)

src/script/descriptor.cpp

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@ struct PubkeyProvider
197197
/** Get the descriptor string form including private data (if available in arg). */
198198
virtual bool ToPrivateString(const SigningProvider& arg, std::string& out) const = 0;
199199

200-
/** Get the descriptor string form with the xpub at the last hardened derivation */
200+
/** Get the descriptor string form with the xpub at the last hardened derivation,
201+
* and always use h for hardened derivation.
202+
*/
201203
virtual bool ToNormalizedString(const SigningProvider& arg, std::string& out, const DescriptorCache* cache = nullptr) const = 0;
202204

203205
/** Derive a private key, if private data is available in arg. */
@@ -208,14 +210,15 @@ class OriginPubkeyProvider final : public PubkeyProvider
208210
{
209211
KeyOriginInfo m_origin;
210212
std::unique_ptr<PubkeyProvider> m_provider;
213+
bool m_apostrophe;
211214

212-
std::string OriginString() const
215+
std::string OriginString(bool normalized=false) const
213216
{
214-
return HexStr(m_origin.fingerprint) + FormatHDKeypath(m_origin.path);
217+
return HexStr(m_origin.fingerprint) + FormatHDKeypath(m_origin.path, /*apostrophe=*/!normalized && m_apostrophe);
215218
}
216219

217220
public:
218-
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)) {}
221+
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) {}
219222
bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info, const DescriptorCache* read_cache = nullptr, DescriptorCache* write_cache = nullptr) const override
220223
{
221224
if (!m_provider->GetPubKey(pos, arg, key, info, read_cache, write_cache)) return false;
@@ -242,9 +245,9 @@ class OriginPubkeyProvider final : public PubkeyProvider
242245
// and append that to our own origin string.
243246
if (sub[0] == '[') {
244247
sub = sub.substr(9);
245-
ret = "[" + OriginString() + std::move(sub);
248+
ret = "[" + OriginString(/*normalized=*/true) + std::move(sub);
246249
} else {
247-
ret = "[" + OriginString() + "]" + std::move(sub);
250+
ret = "[" + OriginString(/*normalized=*/true) + "]" + std::move(sub);
248251
}
249252
return true;
250253
}
@@ -312,6 +315,8 @@ class BIP32PubkeyProvider final : public PubkeyProvider
312315
CExtPubKey m_root_extkey;
313316
KeyPath m_path;
314317
DeriveType m_derive;
318+
// Whether ' or h is used in harded derivation
319+
bool m_apostrophe;
315320

316321
bool GetExtKey(const SigningProvider& arg, CExtKey& ret) const
317322
{
@@ -348,7 +353,7 @@ class BIP32PubkeyProvider final : public PubkeyProvider
348353
}
349354

350355
public:
351-
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) {}
356+
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) {}
352357
bool IsRange() const override { return m_derive != DeriveType::NO; }
353358
size_t GetSize() const override { return 33; }
354359
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
@@ -416,31 +421,36 @@ class BIP32PubkeyProvider final : public PubkeyProvider
416421

417422
return true;
418423
}
419-
std::string ToString() const override
424+
std::string ToString(bool normalized) const
420425
{
421-
std::string ret = EncodeExtPubKey(m_root_extkey) + FormatHDKeypath(m_path);
426+
const bool use_apostrophe = !normalized && m_apostrophe;
427+
std::string ret = EncodeExtPubKey(m_root_extkey) + FormatHDKeypath(m_path, /*apostrophe=*/use_apostrophe);
422428
if (IsRange()) {
423429
ret += "/*";
424-
if (m_derive == DeriveType::HARDENED) ret += '\'';
430+
if (m_derive == DeriveType::HARDENED) ret += use_apostrophe ? '\'' : 'h';
425431
}
426432
return ret;
427433
}
434+
std::string ToString() const override
435+
{
436+
return ToString(/*normalized=*/false);
437+
}
428438
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
429439
{
430440
CExtKey key;
431441
if (!GetExtKey(arg, key)) return false;
432-
out = EncodeExtKey(key) + FormatHDKeypath(m_path);
442+
out = EncodeExtKey(key) + FormatHDKeypath(m_path, /*apostrophe=*/m_apostrophe);
433443
if (IsRange()) {
434444
out += "/*";
435-
if (m_derive == DeriveType::HARDENED) out += '\'';
445+
if (m_derive == DeriveType::HARDENED) out += m_apostrophe ? '\'' : 'h';
436446
}
437447
return true;
438448
}
439449
bool ToNormalizedString(const SigningProvider& arg, std::string& out, const DescriptorCache* cache) const override
440450
{
441-
// For hardened derivation type, just return the typical string, nothing to normalize
442451
if (m_derive == DeriveType::HARDENED) {
443-
out = ToString();
452+
out = ToString(/*normalized=*/true);
453+
444454
return true;
445455
}
446456
// Step backwards to find the last hardened step in the path
@@ -1049,15 +1059,27 @@ enum class ParseScriptContext {
10491059
P2TR, //!< Inside tr() (either internal key, or BIP342 script leaf)
10501060
};
10511061

1052-
/** Parse a key path, being passed a split list of elements (the first element is ignored). */
1053-
[[nodiscard]] bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath& out, std::string& error)
1062+
/**
1063+
* Parse a key path, being passed a split list of elements (the first element is ignored).
1064+
*
1065+
* @param[in] split BIP32 path string, using either ' or h for hardened derivation
1066+
* @param[out] out the key path
1067+
* @param[out] apostrophe only updated if hardened derivation is found
1068+
* @param[out] error parsing error message
1069+
* @returns false if parsing failed
1070+
**/
1071+
[[nodiscard]] bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath& out, bool& apostrophe, std::string& error)
10541072
{
10551073
for (size_t i = 1; i < split.size(); ++i) {
10561074
Span<const char> elem = split[i];
10571075
bool hardened = false;
1058-
if (elem.size() > 0 && (elem[elem.size() - 1] == '\'' || elem[elem.size() - 1] == 'h')) {
1059-
elem = elem.first(elem.size() - 1);
1060-
hardened = true;
1076+
if (elem.size() > 0) {
1077+
const char last = elem[elem.size() - 1];
1078+
if (last == '\'' || last == 'h') {
1079+
elem = elem.first(elem.size() - 1);
1080+
hardened = true;
1081+
apostrophe = last == '\'';
1082+
}
10611083
}
10621084
uint32_t p;
10631085
if (!ParseUInt32(std::string(elem.begin(), elem.end()), &p)) {
@@ -1073,7 +1095,7 @@ enum class ParseScriptContext {
10731095
}
10741096

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

@@ -1130,15 +1152,16 @@ std::unique_ptr<PubkeyProvider> ParsePubkeyInner(uint32_t key_exp_index, const S
11301152
split.pop_back();
11311153
type = DeriveType::UNHARDENED;
11321154
} else if (split.back() == Span{"*'"}.first(2) || split.back() == Span{"*h"}.first(2)) {
1155+
apostrophe = split.back() == Span{"*'"}.first(2);
11331156
split.pop_back();
11341157
type = DeriveType::HARDENED;
11351158
}
1136-
if (!ParseKeyPath(split, path, error)) return nullptr;
1159+
if (!ParseKeyPath(split, path, apostrophe, error)) return nullptr;
11371160
if (extkey.key.IsValid()) {
11381161
extpubkey = extkey.Neuter();
11391162
out.keys.emplace(extpubkey.pubkey.GetID(), extkey.key);
11401163
}
1141-
return std::make_unique<BIP32PubkeyProvider>(key_exp_index, extpubkey, std::move(path), type);
1164+
return std::make_unique<BIP32PubkeyProvider>(key_exp_index, extpubkey, std::move(path), type, apostrophe);
11421165
}
11431166

11441167
/** Parse a public key including origin information (if enabled). */
@@ -1151,7 +1174,11 @@ std::unique_ptr<PubkeyProvider> ParsePubkey(uint32_t key_exp_index, const Span<c
11511174
error = "Multiple ']' characters found for a single pubkey";
11521175
return nullptr;
11531176
}
1154-
if (origin_split.size() == 1) return ParsePubkeyInner(key_exp_index, origin_split[0], ctx, out, error);
1177+
// This is set if either the origin or path suffix contains a hardened derivation.
1178+
bool apostrophe = false;
1179+
if (origin_split.size() == 1) {
1180+
return ParsePubkeyInner(key_exp_index, origin_split[0], ctx, out, apostrophe, error);
1181+
}
11551182
if (origin_split[0].empty() || origin_split[0][0] != '[') {
11561183
error = strprintf("Key origin start '[ character expected but not found, got '%c' instead",
11571184
origin_split[0].empty() ? /** empty, implies split char */ ']' : origin_split[0][0]);
@@ -1172,18 +1199,18 @@ std::unique_ptr<PubkeyProvider> ParsePubkey(uint32_t key_exp_index, const Span<c
11721199
static_assert(sizeof(info.fingerprint) == 4, "Fingerprint must be 4 bytes");
11731200
assert(fpr_bytes.size() == 4);
11741201
std::copy(fpr_bytes.begin(), fpr_bytes.end(), info.fingerprint);
1175-
if (!ParseKeyPath(slash_split, info.path, error)) return nullptr;
1176-
auto provider = ParsePubkeyInner(key_exp_index, origin_split[1], ctx, out, error);
1202+
if (!ParseKeyPath(slash_split, info.path, apostrophe, error)) return nullptr;
1203+
auto provider = ParsePubkeyInner(key_exp_index, origin_split[1], ctx, out, apostrophe, error);
11771204
if (!provider) return nullptr;
1178-
return std::make_unique<OriginPubkeyProvider>(key_exp_index, std::move(info), std::move(provider));
1205+
return std::make_unique<OriginPubkeyProvider>(key_exp_index, std::move(info), std::move(provider), apostrophe);
11791206
}
11801207

11811208
std::unique_ptr<PubkeyProvider> InferPubkey(const CPubKey& pubkey, ParseScriptContext, const SigningProvider& provider)
11821209
{
11831210
std::unique_ptr<PubkeyProvider> key_provider = std::make_unique<ConstPubkeyProvider>(0, pubkey, false);
11841211
KeyOriginInfo info;
11851212
if (provider.GetKeyOrigin(pubkey.GetID(), info)) {
1186-
return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider));
1213+
return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider), /*apostrophe=*/false);
11871214
}
11881215
return key_provider;
11891216
}
@@ -1196,7 +1223,7 @@ std::unique_ptr<PubkeyProvider> InferXOnlyPubkey(const XOnlyPubKey& xkey, ParseS
11961223
std::unique_ptr<PubkeyProvider> key_provider = std::make_unique<ConstPubkeyProvider>(0, pubkey, true);
11971224
KeyOriginInfo info;
11981225
if (provider.GetKeyOriginByXOnly(xkey, info)) {
1199-
return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider));
1226+
return std::make_unique<OriginPubkeyProvider>(0, std::move(info), std::move(key_provider), /*apostrophe=*/false);
12001227
}
12011228
return key_provider;
12021229
}

0 commit comments

Comments
 (0)