-
Notifications
You must be signed in to change notification settings - Fork 37.7k
Switch hardened derivation marker to h #26076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
doc/release-notes-26076.md
Outdated
|
||
- The `deriveaddresses`, `getdescriptorinfo` and `listdescriptors` RPC's now | ||
use `h` rather than apostrophe (`'`) to indicate hardened derivation. This | ||
makes it easier to handle descriptor strings manually. E.g. an RPC call that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The enclosing single quotes are shell escaping, not part of the actual RPC call (which has no issue at present).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "manually" I mean using bitcoin-cli
from the shell. Afaik the GUI console - probably used more often - has the same problem.
concept ack |
ACK fc5594d |
I'm in favor of using Would it be unreasonable to make the descriptor classes remember which of the two was used when parsing, and reuse the same one when serializing? That guarantees roundtripping, but defaults could still prefer Not a NACK, but I'd be more comfortable with just honoring whatever was parsed. |
My apologies for the accidental close. That was unintentional. |
008df84
to
9df0fd2
Compare
Made a slight documentation change to the first commit and then pushed three more which change the following behaviour:
I could squash the new behavior into the original commit, but I'll wait for concept ACK. |
Concept ACK Please squash |
@achow101 squashed |
9df0fd2
to
80f2031
Compare
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this consider normalized
as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I intentionally didn't normalize the private-key versions. Though I could.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What was the rationale for only normalizing the public-key version with h
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't normalize the private key version of descriptors in general. I'm not sure if it matters a lot, but would prefer to keep such a change to followup.
src/script/descriptor.cpp
Outdated
@@ -1047,14 +1056,18 @@ enum class ParseScriptContext { | |||
}; | |||
|
|||
/** 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) | |||
[[nodiscard]] bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath& out, std::optional<bool>& apostrophe, std::string& error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does apostrophe
need to be optional? Either the path uses an apostrophe, or it doesn't. And it doesn't matter if the path is not hardened at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In one of the call sites it first checks the origin and then the rest of the path. The optional is necessary to combine those results, especially in the case where the origin doesn't have any hardened derivation (one of the test cases).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not seeing how it is necessary. It could initialized to false
in ParsePubKey
and I think that would achieve the same result. Nothing is checking whether the optional has a value, except at the very end with value_or(false)
, and that's equivalent to initializing as false
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I initialise to false
, it would be set to true
when checking the origin and then back to false
when checking the path.
(at minimum this confusion suggests I should add more comments)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to work:
diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp
index e639e494582..877d2538b2c 100644
--- a/src/script/descriptor.cpp
+++ b/src/script/descriptor.cpp
@@ -1056,7 +1056,7 @@ enum class ParseScriptContext {
};
/** 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::optional<bool>& apostrophe, std::string& error)
+[[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];
@@ -1083,7 +1083,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::optional<bool>& apostrophe, 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;
@@ -1149,7 +1149,7 @@ std::unique_ptr<PubkeyProvider> ParsePubkeyInner(uint32_t key_exp_index, const S
extpubkey = extkey.Neuter();
out.keys.emplace(extpubkey.pubkey.GetID(), extkey.key);
}
- return std::make_unique<BIP32PubkeyProvider>(key_exp_index, extpubkey, std::move(path), type, apostrophe.value_or(false));
+ return std::make_unique<BIP32PubkeyProvider>(key_exp_index, extpubkey, std::move(path), type, apostrophe);
}
/** Parse a public key including origin information (if enabled). */
@@ -1163,7 +1163,7 @@ std::unique_ptr<PubkeyProvider> ParsePubkey(uint32_t key_exp_index, const Span<c
return nullptr;
}
// This is set if either the origin or path suffix contains a hardened deriviation.
- std::optional<bool> apostrophe;
+ bool apostrophe = false;
if (origin_split.size() == 1) {
return ParsePubkeyInner(key_exp_index, origin_split[0], ctx, out, apostrophe, error);
}
@@ -1190,7 +1190,7 @@ std::unique_ptr<PubkeyProvider> ParsePubkey(uint32_t key_exp_index, const Span<c
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), apostrophe.value_or(false));
+ 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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, because ParseKeyPath
leaves &apostrophe
alone if it doesn't find hardened derivation. Fixed and added some documentation.
parse_priv = Parse(UseHInsteadOfApostrophe(prv), keys_priv, error); | ||
} else { | ||
parse_priv = Parse(prv, keys_priv, error); | ||
prv = UseHInsteadOfApostrophe(prv); | ||
} | ||
parse_priv = Parse(prv, keys_priv, error); | ||
BOOST_CHECK_MESSAGE(parse_priv, error); | ||
if (replace_apostrophe_with_h_in_pub) { | ||
parse_pub = Parse(UseHInsteadOfApostrophe(pub), keys_pub, error); | ||
} else { | ||
parse_pub = Parse(pub, keys_pub, error); | ||
pub = UseHInsteadOfApostrophe(pub); | ||
} | ||
parse_pub = Parse(pub, keys_pub, error); | ||
BOOST_CHECK_MESSAGE(parse_pub, error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear to me why this and the change in Check
are necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be a left-over from the squash. Will look into this once we decide whether or not to normalise private keys, because that might change things again: #26076 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a left-over; the test fails without it. The behaviour is quite different now that we echo the original descriptor rather than always put '
in it.
it might make sense to have new wallets only generate |
@JeremyRubin is there software that can't parse BIP32 paths with Rebased. |
80f2031
to
5df4d99
Compare
ah nope i misremembered that rust-bitcoin didn't support |
Thanks for the review! The reason for not breaking legacy wallet RPC calls is that I'm assuming any code that uses legacy wallets is really old and nobody wants to touch it. On our side eventually we want to get rid of the legacy wallet - or at least freeze it - so there's no need to modernise it. I updated the title, description and release note. Pushed the latter as a new commit to not lose ACKs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re-ACK fe49f06
ACK fe49f06 |
For legacy wallets the `hdkeypath` field in `getaddressinfo` is unchanged, | ||
nor is the serialization format of wallet dumps. (#26076) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The “nor” here seems misplaced:
For legacy wallets the `hdkeypath` field in `getaddressinfo` is unchanged, | |
nor is the serialization format of wallet dumps. (#26076) | |
For legacy wallets the `hdkeypath` field in `getaddressinfo` | |
and the serialization format of wallet dumps remain unchanged. (#26076) |
Consistent with bitcoin#26076
Consistent with bitcoin#26076
Consistent with bitcoin#26076
Consistent with bitcoin#26076
Consistent with bitcoin#26076
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 useh
as the marker:'["desc": ".../0h/..."]'
, avoiding the need for escape characters. With this changelistdescriptors
will useh
, 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 ingetaddressinfo
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