Skip to content

Commit 0a8f519

Browse files
committed
Merge #14150: Add key origin support to descriptors
8afb166 Update documentation to incude origin information (Pieter Wuille) ff37459 Add tests for key origin support (Pieter Wuille) 2c6281f Add key origin support to descriptors (Pieter Wuille) Pull request description: This adds support for [key origin](https://gist.github.com/sipa/e3d23d498c430bb601c5bca83523fa82#key-origin-identification) information to the descriptor parser, and exposes the resulting key path information through `FlatSigningProvider`. There is no observable functionality from this right now, except having the `scantxoutset` RPC accept descriptors that include key origin information. Longer term this feature helps with a potential descriptors-based walletless PSBT updater, or for importing hardware wallet xpubs (once the wallet can import descriptors). Tree-SHA512: 399828127b2e90a2f32d81ecc30a8a9261d08f4182d5d1744f05e46b25fde1bd383c54835b0820ca668e7d17353fa92c0fb2987e211ce269e0824c9395d210c2
2 parents 5c25409 + 8afb166 commit 0a8f519

File tree

6 files changed

+174
-49
lines changed

6 files changed

+174
-49
lines changed

doc/descriptors.md

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Output descriptors currently support:
3434
- `sh(wsh(multi(1,03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8,03499fdf9e895e719cfd64e67f07d38e3226aa7b63678949e6e49b241a60e823e4,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)))` describes a P2SH-P2WSH *1-of-3* multisig output with keys in the specified order.
3535
- `pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)` describes a P2PK output with the public key of the specified xpub.
3636
- `pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1'/2)` describes a P2PKH output with child key *1'/2* of the specified xpub.
37+
- `pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)` describes a set of P2PKH outputs, but additionally specifies that the specified xpub is a child of a master with fingerprint `d34db33f`, and derived using path `44'/0'/0'`.
3738
- `wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))` describes a set of *1-of-2* P2WSH multisig outputs where the first multisig key is the *1/0/`i`* child of the first specified xpub and the second multisig key is the *0/0/`i`* child of the second specified xpub, and `i` is any number in a configurable range (`0-1000` by default).
3839

3940
## Reference
@@ -52,14 +53,20 @@ Descriptors consist of several types of expressions. The top level expression is
5253
- `raw(HEX)` (top level only): the script whose hex encoding is HEX.
5354

5455
`KEY` expressions:
55-
- Hex encoded public keys (66 characters starting with `02` or `03`, or 130 characters starting with `04`).
56-
- Inside `wpkh` and `wsh`, only compressed public keys are permitted.
57-
- [WIF](https://en.bitcoin.it/wiki/Wallet_import_format) encoded private keys may be specified instead of the corresponding public key, with the same meaning.
58-
-`xpub` encoded extended public key or `xprv` encoded private key (as defined in [BIP 32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)).
59-
- Followed by zero or more `/NUM` unhardened and `/NUM'` hardened BIP32 derivation steps.
60-
- Optionally followed by a single `/*` or `/*'` final step to denote all (direct) unhardened or hardened children.
61-
- The usage of hardened derivation steps requires providing the private key.
62-
- Instead of a `'`, the suffix `h` can be used to denote hardened derivation.
56+
- Optionally, key origin information, consisting of:
57+
- An open bracket `[`
58+
- Exactly 8 hex characters for the fingerprint of the key where the derivation starts (see BIP32 for details)
59+
- Followed by zero or more `/NUM` or `/NUM'` path elements to indicate unhardened or hardened derivation steps between the fingerprint and the key or xpub/xprv root that follows
60+
- A closing bracket `]`
61+
- Followed by the actual key, which is either:
62+
- Hex encoded public keys (66 characters starting with `02` or `03`, or 130 characters starting with `04`).
63+
- Inside `wpkh` and `wsh`, only compressed public keys are permitted.
64+
- [WIF](https://en.bitcoin.it/wiki/Wallet_import_format) encoded private keys may be specified instead of the corresponding public key, with the same meaning.
65+
-`xpub` encoded extended public key or `xprv` encoded private key (as defined in [BIP 32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)).
66+
- Followed by zero or more `/NUM` unhardened and `/NUM'` hardened BIP32 derivation steps.
67+
- Optionally followed by a single `/*` or `/*'` final step to denote all (direct) unhardened or hardened children.
68+
- The usage of hardened derivation steps requires providing the private key.
69+
- Anywhere a `'` suffix is permitted to denote hardened derivation, the suffix `h` can be used instead.
6370

6471
`ADDR` expressions are any type of supported address:
6572
- P2PKH addresses (base58, of the form `1...`). Note that P2PKH addresses in descriptors cannot be used for P2PK outputs (use the `pk` function instead).
@@ -116,6 +123,33 @@ Whenever a public key is described using a hardened derivation step, the
116123
script cannot be computed without access to the corresponding private
117124
key.
118125

126+
### Key origin identification
127+
128+
In order to describe scripts whose signing keys reside on another device,
129+
it may be necessary to identify the master key and derivation path an
130+
xpub was derived with.
131+
132+
For example, when following BIP44, it would be useful to describe a
133+
change chain directly as `xpub.../44'/0'/0'/1/*` where `xpub...`
134+
corresponds with the master key `m`. Unfortunately, since there are
135+
hardened derivation steps that follow the xpub, this descriptor does not
136+
let you compute scripts without access to the corresponding private keys.
137+
Instead, it should be written as `xpub.../1/*`, where xpub corresponds to
138+
`m/44'/0'/0'`.
139+
140+
When interacting with a hardware device, it may be necessary to include
141+
the entire path from the master down. BIP174 standardizes this by
142+
providing the master key *fingerprint* (first 32 bit of the Hash160 of
143+
the master pubkey), plus all derivation steps. To support constructing
144+
these, we permit providing this key origin information inside the
145+
descriptor language, even though it does not affect the actual
146+
scriptPubKeys it refers to.
147+
148+
Every public key can be prefixed by an 8-character hexadecimal
149+
fingerprint plus optional derivation steps (hardened and unhardened)
150+
surrounded by brackets, identifying the master and derivation path the key or xpub
151+
that follows was derived with.
152+
119153
### Including private keys
120154

121155
Often it is useful to communicate a description of scripts along with the
@@ -130,4 +164,4 @@ In order to easily represent the sets of scripts currently supported by
130164
existing Bitcoin Core wallets, a convenience function `combo` is
131165
provided, which takes as input a public key, and describes a set of P2PK,
132166
P2PKH, P2WPKH, and P2SH-P2WPH scripts for that key. In case the key is
133-
uncompressed, the set only includes P2PK and P2PKH scripts.
167+
uncompressed, the set only includes P2PK and P2PKH scripts.

src/script/descriptor.cpp

Lines changed: 96 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ struct PubkeyProvider
4141
virtual ~PubkeyProvider() = default;
4242

4343
/** Derive a public key. */
44-
virtual bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& out) const = 0;
44+
virtual bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info) const = 0;
4545

4646
/** Whether this represent multiple public keys at different positions. */
4747
virtual bool IsRange() const = 0;
@@ -56,16 +56,50 @@ struct PubkeyProvider
5656
virtual bool ToPrivateString(const SigningProvider& arg, std::string& out) const = 0;
5757
};
5858

59+
class OriginPubkeyProvider final : public PubkeyProvider
60+
{
61+
KeyOriginInfo m_origin;
62+
std::unique_ptr<PubkeyProvider> m_provider;
63+
64+
std::string OriginString() const
65+
{
66+
return HexStr(std::begin(m_origin.fingerprint), std::end(m_origin.fingerprint)) + FormatKeyPath(m_origin.path);
67+
}
68+
69+
public:
70+
OriginPubkeyProvider(KeyOriginInfo info, std::unique_ptr<PubkeyProvider> provider) : m_origin(std::move(info)), m_provider(std::move(provider)) {}
71+
bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info) const override
72+
{
73+
if (!m_provider->GetPubKey(pos, arg, key, info)) return false;
74+
std::copy(std::begin(m_origin.fingerprint), std::end(m_origin.fingerprint), info.fingerprint);
75+
info.path.insert(info.path.begin(), m_origin.path.begin(), m_origin.path.end());
76+
return true;
77+
}
78+
bool IsRange() const override { return m_provider->IsRange(); }
79+
size_t GetSize() const override { return m_provider->GetSize(); }
80+
std::string ToString() const override { return "[" + OriginString() + "]" + m_provider->ToString(); }
81+
bool ToPrivateString(const SigningProvider& arg, std::string& ret) const override
82+
{
83+
std::string sub;
84+
if (!m_provider->ToPrivateString(arg, sub)) return false;
85+
ret = "[" + OriginString() + "]" + std::move(sub);
86+
return true;
87+
}
88+
};
89+
5990
/** An object representing a parsed constant public key in a descriptor. */
6091
class ConstPubkeyProvider final : public PubkeyProvider
6192
{
6293
CPubKey m_pubkey;
6394

6495
public:
6596
ConstPubkeyProvider(const CPubKey& pubkey) : m_pubkey(pubkey) {}
66-
bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& out) const override
97+
bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info) const override
6798
{
68-
out = m_pubkey;
99+
key = m_pubkey;
100+
info.path.clear();
101+
CKeyID keyid = m_pubkey.GetID();
102+
std::copy(keyid.begin(), keyid.begin() + sizeof(info.fingerprint), info.fingerprint);
69103
return true;
70104
}
71105
bool IsRange() const override { return false; }
@@ -98,7 +132,7 @@ class BIP32PubkeyProvider final : public PubkeyProvider
98132
CKey key;
99133
if (!arg.GetKey(m_extkey.pubkey.GetID(), key)) return false;
100134
ret.nDepth = m_extkey.nDepth;
101-
std::copy(m_extkey.vchFingerprint, m_extkey.vchFingerprint + 4, ret.vchFingerprint);
135+
std::copy(m_extkey.vchFingerprint, m_extkey.vchFingerprint + sizeof(ret.vchFingerprint), ret.vchFingerprint);
102136
ret.nChild = m_extkey.nChild;
103137
ret.chaincode = m_extkey.chaincode;
104138
ret.key = key;
@@ -118,27 +152,32 @@ class BIP32PubkeyProvider final : public PubkeyProvider
118152
BIP32PubkeyProvider(const CExtPubKey& extkey, KeyPath path, DeriveType derive) : m_extkey(extkey), m_path(std::move(path)), m_derive(derive) {}
119153
bool IsRange() const override { return m_derive != DeriveType::NO; }
120154
size_t GetSize() const override { return 33; }
121-
bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& out) const override
155+
bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info) const override
122156
{
123157
if (IsHardened()) {
124-
CExtKey key;
125-
if (!GetExtKey(arg, key)) return false;
158+
CExtKey extkey;
159+
if (!GetExtKey(arg, extkey)) return false;
126160
for (auto entry : m_path) {
127-
key.Derive(key, entry);
161+
extkey.Derive(extkey, entry);
128162
}
129-
if (m_derive == DeriveType::UNHARDENED) key.Derive(key, pos);
130-
if (m_derive == DeriveType::HARDENED) key.Derive(key, pos | 0x80000000UL);
131-
out = key.Neuter().pubkey;
163+
if (m_derive == DeriveType::UNHARDENED) extkey.Derive(extkey, pos);
164+
if (m_derive == DeriveType::HARDENED) extkey.Derive(extkey, pos | 0x80000000UL);
165+
key = extkey.Neuter().pubkey;
132166
} else {
133167
// TODO: optimize by caching
134-
CExtPubKey key = m_extkey;
168+
CExtPubKey extkey = m_extkey;
135169
for (auto entry : m_path) {
136-
key.Derive(key, entry);
170+
extkey.Derive(extkey, entry);
137171
}
138-
if (m_derive == DeriveType::UNHARDENED) key.Derive(key, pos);
172+
if (m_derive == DeriveType::UNHARDENED) extkey.Derive(extkey, pos);
139173
assert(m_derive != DeriveType::HARDENED);
140-
out = key.pubkey;
174+
key = extkey.pubkey;
141175
}
176+
CKeyID keyid = m_extkey.pubkey.GetID();
177+
std::copy(keyid.begin(), keyid.begin() + sizeof(info.fingerprint), info.fingerprint);
178+
info.path = m_path;
179+
if (m_derive == DeriveType::UNHARDENED) info.path.push_back((uint32_t)pos);
180+
if (m_derive == DeriveType::HARDENED) info.path.push_back(((uint32_t)pos) | 0x80000000L);
142181
return true;
143182
}
144183
std::string ToString() const override
@@ -221,9 +260,11 @@ class SingleKeyDescriptor final : public Descriptor
221260
bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
222261
{
223262
CPubKey key;
224-
if (!m_provider->GetPubKey(pos, arg, key)) return false;
263+
KeyOriginInfo info;
264+
if (!m_provider->GetPubKey(pos, arg, key, info)) return false;
225265
output_scripts = std::vector<CScript>{m_script_fn(key)};
226-
out.pubkeys.emplace(key.GetID(), std::move(key));
266+
out.origins.emplace(key.GetID(), std::move(info));
267+
out.pubkeys.emplace(key.GetID(), key);
227268
return true;
228269
}
229270
};
@@ -272,15 +313,19 @@ class MultisigDescriptor : public Descriptor
272313

273314
bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
274315
{
275-
std::vector<CPubKey> pubkeys;
276-
pubkeys.reserve(m_providers.size());
316+
std::vector<std::pair<CPubKey, KeyOriginInfo>> entries;
317+
entries.reserve(m_providers.size());
318+
// Construct temporary data in `entries`, to avoid producing output in case of failure.
277319
for (const auto& p : m_providers) {
278-
CPubKey key;
279-
if (!p->GetPubKey(pos, arg, key)) return false;
280-
pubkeys.push_back(key);
320+
entries.emplace_back();
321+
if (!p->GetPubKey(pos, arg, entries.back().first, entries.back().second)) return false;
281322
}
282-
for (const CPubKey& key : pubkeys) {
283-
out.pubkeys.emplace(key.GetID(), std::move(key));
323+
std::vector<CPubKey> pubkeys;
324+
pubkeys.reserve(entries.size());
325+
for (auto& entry : entries) {
326+
pubkeys.push_back(entry.first);
327+
out.origins.emplace(entry.first.GetID(), std::move(entry.second));
328+
out.pubkeys.emplace(entry.first.GetID(), entry.first);
284329
}
285330
output_scripts = std::vector<CScript>{GetScriptForMultisig(m_threshold, pubkeys)};
286331
return true;
@@ -343,13 +388,15 @@ class ComboDescriptor final : public Descriptor
343388
bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
344389
{
345390
CPubKey key;
346-
if (!m_provider->GetPubKey(pos, arg, key)) return false;
391+
KeyOriginInfo info;
392+
if (!m_provider->GetPubKey(pos, arg, key, info)) return false;
347393
CKeyID keyid = key.GetID();
348394
{
349395
CScript p2pk = GetScriptForRawPubKey(key);
350396
CScript p2pkh = GetScriptForDestination(keyid);
351397
output_scripts = std::vector<CScript>{std::move(p2pk), std::move(p2pkh)};
352398
out.pubkeys.emplace(keyid, key);
399+
out.origins.emplace(keyid, std::move(info));
353400
}
354401
if (key.IsCompressed()) {
355402
CScript p2wpkh = GetScriptForDestination(WitnessV0KeyHash(keyid));
@@ -447,7 +494,8 @@ bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath& out)
447494
return true;
448495
}
449496

450-
std::unique_ptr<PubkeyProvider> ParsePubkey(const Span<const char>& sp, bool permit_uncompressed, FlatSigningProvider& out)
497+
/** Parse a public key that excludes origin information. */
498+
std::unique_ptr<PubkeyProvider> ParsePubkeyInner(const Span<const char>& sp, bool permit_uncompressed, FlatSigningProvider& out)
451499
{
452500
auto split = Split(sp, '/');
453501
std::string str(split[0].begin(), split[0].end());
@@ -484,6 +532,28 @@ std::unique_ptr<PubkeyProvider> ParsePubkey(const Span<const char>& sp, bool per
484532
return MakeUnique<BIP32PubkeyProvider>(extpubkey, std::move(path), type);
485533
}
486534

535+
/** Parse a public key including origin information (if enabled). */
536+
std::unique_ptr<PubkeyProvider> ParsePubkey(const Span<const char>& sp, bool permit_uncompressed, FlatSigningProvider& out)
537+
{
538+
auto origin_split = Split(sp, ']');
539+
if (origin_split.size() > 2) return nullptr;
540+
if (origin_split.size() == 1) return ParsePubkeyInner(origin_split[0], permit_uncompressed, out);
541+
if (origin_split[0].size() < 1 || origin_split[0][0] != '[') return nullptr;
542+
auto slash_split = Split(origin_split[0].subspan(1), '/');
543+
if (slash_split[0].size() != 8) return nullptr;
544+
std::string fpr_hex = std::string(slash_split[0].begin(), slash_split[0].end());
545+
if (!IsHex(fpr_hex)) return nullptr;
546+
auto fpr_bytes = ParseHex(fpr_hex);
547+
KeyOriginInfo info;
548+
static_assert(sizeof(info.fingerprint) == 4, "Fingerprint must be 4 bytes");
549+
assert(fpr_bytes.size() == 4);
550+
std::copy(fpr_bytes.begin(), fpr_bytes.end(), info.fingerprint);
551+
if (!ParseKeyPath(slash_split, info.path)) return nullptr;
552+
auto provider = ParsePubkeyInner(origin_split[1], permit_uncompressed, out);
553+
if (!provider) return nullptr;
554+
return MakeUnique<OriginPubkeyProvider>(std::move(info), std::move(provider));
555+
}
556+
487557
/** Parse a script in a particular context. */
488558
std::unique_ptr<Descriptor> ParseScript(Span<const char>& sp, ParseScriptContext ctx, FlatSigningProvider& out)
489559
{

src/script/sign.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@ bool HidingSigningProvider::GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& inf
686686

687687
bool FlatSigningProvider::GetCScript(const CScriptID& scriptid, CScript& script) const { return LookupHelper(scripts, scriptid, script); }
688688
bool FlatSigningProvider::GetPubKey(const CKeyID& keyid, CPubKey& pubkey) const { return LookupHelper(pubkeys, keyid, pubkey); }
689+
bool FlatSigningProvider::GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const { return LookupHelper(origins, keyid, info); }
689690
bool FlatSigningProvider::GetKey(const CKeyID& keyid, CKey& key) const { return LookupHelper(keys, keyid, key); }
690691

691692
FlatSigningProvider Merge(const FlatSigningProvider& a, const FlatSigningProvider& b)

src/script/sign.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class SigningProvider
3434
virtual bool GetCScript(const CScriptID &scriptid, CScript& script) const { return false; }
3535
virtual bool GetPubKey(const CKeyID &address, CPubKey& pubkey) const { return false; }
3636
virtual bool GetKey(const CKeyID &address, CKey& key) const { return false; }
37-
virtual bool GetKeyOrigin(const CKeyID& id, KeyOriginInfo& info) const { return false; }
37+
virtual bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const { return false; }
3838
};
3939

4040
extern const SigningProvider& DUMMY_SIGNING_PROVIDER;
@@ -58,10 +58,12 @@ struct FlatSigningProvider final : public SigningProvider
5858
{
5959
std::map<CScriptID, CScript> scripts;
6060
std::map<CKeyID, CPubKey> pubkeys;
61+
std::map<CKeyID, KeyOriginInfo> origins;
6162
std::map<CKeyID, CKey> keys;
6263

6364
bool GetCScript(const CScriptID& scriptid, CScript& script) const override;
6465
bool GetPubKey(const CKeyID& keyid, CPubKey& pubkey) const override;
66+
bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const override;
6567
bool GetKey(const CKeyID& keyid, CKey& key) const override;
6668
};
6769

0 commit comments

Comments
 (0)