Skip to content
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

Add key origin support to descriptors #14150

Merged
merged 3 commits into from
Oct 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions doc/descriptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Output descriptors currently support:
- `sh(wsh(multi(1,03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8,03499fdf9e895e719cfd64e67f07d38e3226aa7b63678949e6e49b241a60e823e4,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)))` describes a P2SH-P2WSH *1-of-3* multisig output with keys in the specified order.
- `pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)` describes a P2PK output with the public key of the specified xpub.
- `pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1'/2)` describes a P2PKH output with child key *1'/2* of the specified xpub.
- `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'`.
- `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).

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

`KEY` expressions:
- Hex encoded public keys (66 characters starting with `02` or `03`, or 130 characters starting with `04`).
- Inside `wpkh` and `wsh`, only compressed public keys are permitted.
- [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.
-`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)).
- Followed by zero or more `/NUM` unhardened and `/NUM'` hardened BIP32 derivation steps.
- Optionally followed by a single `/*` or `/*'` final step to denote all (direct) unhardened or hardened children.
- The usage of hardened derivation steps requires providing the private key.
- Instead of a `'`, the suffix `h` can be used to denote hardened derivation.
- Optionally, key origin information, consisting of:
- An open bracket `[`
- Exactly 8 hex characters for the fingerprint of the key where the derivation starts (see BIP32 for details)
- 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
- A closing bracket `]`
- Followed by the actual key, which is either:
- Hex encoded public keys (66 characters starting with `02` or `03`, or 130 characters starting with `04`).
- Inside `wpkh` and `wsh`, only compressed public keys are permitted.
- [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.
-`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)).
- Followed by zero or more `/NUM` unhardened and `/NUM'` hardened BIP32 derivation steps.
- Optionally followed by a single `/*` or `/*'` final step to denote all (direct) unhardened or hardened children.
- The usage of hardened derivation steps requires providing the private key.
- Anywhere a `'` suffix is permitted to denote hardened derivation, the suffix `h` can be used instead.

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

### Key origin identification

In order to describe scripts whose signing keys reside on another device,
it may be necessary to identify the master key and derivation path an
xpub was derived with.

For example, when following BIP44, it would be useful to describe a
change chain directly as `xpub.../44'/0'/0'/1/*` where `xpub...`
corresponds with the master key `m`. Unfortunately, since there are
hardened derivation steps that follow the xpub, this descriptor does not
let you compute scripts without access to the corresponding private keys.
Instead, it should be written as `xpub.../1/*`, where xpub corresponds to
sipa marked this conversation as resolved.
Show resolved Hide resolved
`m/44'/0'/0'`.

When interacting with a hardware device, it may be necessary to include
the entire path from the master down. BIP174 standardizes this by
providing the master key *fingerprint* (first 32 bit of the Hash160 of
the master pubkey), plus all derivation steps. To support constructing
these, we permit providing this key origin information inside the
descriptor language, even though it does not affect the actual
scriptPubKeys it refers to.

Every public key can be prefixed by an 8-character hexadecimal
fingerprint plus optional derivation steps (hardened and unhardened)
surrounded by brackets, identifying the master and derivation path the key or xpub
that follows was derived with.

### Including private keys

Often it is useful to communicate a description of scripts along with the
Expand All @@ -130,4 +164,4 @@ In order to easily represent the sets of scripts currently supported by
existing Bitcoin Core wallets, a convenience function `combo` is
provided, which takes as input a public key, and describes a set of P2PK,
P2PKH, P2WPKH, and P2SH-P2WPH scripts for that key. In case the key is
uncompressed, the set only includes P2PK and P2PKH scripts.
uncompressed, the set only includes P2PK and P2PKH scripts.
122 changes: 96 additions & 26 deletions src/script/descriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ struct PubkeyProvider
virtual ~PubkeyProvider() = default;

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

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

class OriginPubkeyProvider final : public PubkeyProvider
{
KeyOriginInfo m_origin;
std::unique_ptr<PubkeyProvider> m_provider;

std::string OriginString() const
{
return HexStr(std::begin(m_origin.fingerprint), std::end(m_origin.fingerprint)) + FormatKeyPath(m_origin.path);
}

public:
OriginPubkeyProvider(KeyOriginInfo info, std::unique_ptr<PubkeyProvider> provider) : m_origin(std::move(info)), m_provider(std::move(provider)) {}
bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info) const override
{
if (!m_provider->GetPubKey(pos, arg, key, info)) return false;
std::copy(std::begin(m_origin.fingerprint), std::end(m_origin.fingerprint), info.fingerprint);
info.path.insert(info.path.begin(), m_origin.path.begin(), m_origin.path.end());
return true;
}
bool IsRange() const override { return m_provider->IsRange(); }
size_t GetSize() const override { return m_provider->GetSize(); }
std::string ToString() const override { return "[" + OriginString() + "]" + m_provider->ToString(); }
bool ToPrivateString(const SigningProvider& arg, std::string& ret) const override
{
std::string sub;
if (!m_provider->ToPrivateString(arg, sub)) return false;
ret = "[" + OriginString() + "]" + std::move(sub);
return true;
}
};

/** An object representing a parsed constant public key in a descriptor. */
class ConstPubkeyProvider final : public PubkeyProvider
{
CPubKey m_pubkey;

public:
ConstPubkeyProvider(const CPubKey& pubkey) : m_pubkey(pubkey) {}
bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& out) const override
bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info) const override
{
out = m_pubkey;
key = m_pubkey;
info.path.clear();
CKeyID keyid = m_pubkey.GetID();
std::copy(keyid.begin(), keyid.begin() + sizeof(info.fingerprint), info.fingerprint);
return true;
}
bool IsRange() const override { return false; }
Expand Down Expand Up @@ -98,7 +132,7 @@ class BIP32PubkeyProvider final : public PubkeyProvider
CKey key;
if (!arg.GetKey(m_extkey.pubkey.GetID(), key)) return false;
ret.nDepth = m_extkey.nDepth;
std::copy(m_extkey.vchFingerprint, m_extkey.vchFingerprint + 4, ret.vchFingerprint);
std::copy(m_extkey.vchFingerprint, m_extkey.vchFingerprint + sizeof(ret.vchFingerprint), ret.vchFingerprint);
ret.nChild = m_extkey.nChild;
ret.chaincode = m_extkey.chaincode;
ret.key = key;
Expand All @@ -118,27 +152,32 @@ class BIP32PubkeyProvider final : public PubkeyProvider
BIP32PubkeyProvider(const CExtPubKey& extkey, KeyPath path, DeriveType derive) : m_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& out) const override
bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& key, KeyOriginInfo& info) const override
{
if (IsHardened()) {
CExtKey key;
if (!GetExtKey(arg, key)) return false;
CExtKey extkey;
if (!GetExtKey(arg, extkey)) return false;
for (auto entry : m_path) {
key.Derive(key, entry);
extkey.Derive(extkey, entry);
}
if (m_derive == DeriveType::UNHARDENED) key.Derive(key, pos);
if (m_derive == DeriveType::HARDENED) key.Derive(key, pos | 0x80000000UL);
out = key.Neuter().pubkey;
if (m_derive == DeriveType::UNHARDENED) extkey.Derive(extkey, pos);
if (m_derive == DeriveType::HARDENED) extkey.Derive(extkey, pos | 0x80000000UL);
key = extkey.Neuter().pubkey;
} else {
// TODO: optimize by caching
CExtPubKey key = m_extkey;
CExtPubKey extkey = m_extkey;
for (auto entry : m_path) {
key.Derive(key, entry);
extkey.Derive(extkey, entry);
}
if (m_derive == DeriveType::UNHARDENED) key.Derive(key, pos);
if (m_derive == DeriveType::UNHARDENED) extkey.Derive(extkey, pos);
assert(m_derive != DeriveType::HARDENED);
out = key.pubkey;
key = extkey.pubkey;
}
CKeyID keyid = m_extkey.pubkey.GetID();
std::copy(keyid.begin(), keyid.begin() + sizeof(info.fingerprint), info.fingerprint);
info.path = m_path;
if (m_derive == DeriveType::UNHARDENED) info.path.push_back((uint32_t)pos);
if (m_derive == DeriveType::HARDENED) info.path.push_back(((uint32_t)pos) | 0x80000000L);
return true;
}
std::string ToString() const override
Expand Down Expand Up @@ -221,9 +260,11 @@ class SingleKeyDescriptor final : public Descriptor
bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
{
CPubKey key;
if (!m_provider->GetPubKey(pos, arg, key)) return false;
KeyOriginInfo info;
if (!m_provider->GetPubKey(pos, arg, key, info)) return false;
output_scripts = std::vector<CScript>{m_script_fn(key)};
out.pubkeys.emplace(key.GetID(), std::move(key));
out.origins.emplace(key.GetID(), std::move(info));
out.pubkeys.emplace(key.GetID(), key);
return true;
}
};
Expand Down Expand Up @@ -272,15 +313,19 @@ class MultisigDescriptor : public Descriptor

bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
{
std::vector<CPubKey> pubkeys;
pubkeys.reserve(m_providers.size());
std::vector<std::pair<CPubKey, KeyOriginInfo>> entries;
entries.reserve(m_providers.size());
// Construct temporary data in `entries`, to avoid producing output in case of failure.
for (const auto& p : m_providers) {
CPubKey key;
if (!p->GetPubKey(pos, arg, key)) return false;
pubkeys.push_back(key);
entries.emplace_back();
if (!p->GetPubKey(pos, arg, entries.back().first, entries.back().second)) return false;
}
for (const CPubKey& key : pubkeys) {
out.pubkeys.emplace(key.GetID(), std::move(key));
std::vector<CPubKey> pubkeys;
pubkeys.reserve(entries.size());
for (auto& entry : entries) {
sipa marked this conversation as resolved.
Show resolved Hide resolved
pubkeys.push_back(entry.first);
out.origins.emplace(entry.first.GetID(), std::move(entry.second));
out.pubkeys.emplace(entry.first.GetID(), entry.first);
}
output_scripts = std::vector<CScript>{GetScriptForMultisig(m_threshold, pubkeys)};
return true;
Expand Down Expand Up @@ -343,13 +388,15 @@ class ComboDescriptor final : public Descriptor
bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
{
CPubKey key;
if (!m_provider->GetPubKey(pos, arg, key)) return false;
KeyOriginInfo info;
if (!m_provider->GetPubKey(pos, arg, key, info)) return false;
CKeyID keyid = key.GetID();
{
CScript p2pk = GetScriptForRawPubKey(key);
CScript p2pkh = GetScriptForDestination(keyid);
output_scripts = std::vector<CScript>{std::move(p2pk), std::move(p2pkh)};
out.pubkeys.emplace(keyid, key);
out.origins.emplace(keyid, std::move(info));
}
if (key.IsCompressed()) {
CScript p2wpkh = GetScriptForDestination(WitnessV0KeyHash(keyid));
Expand Down Expand Up @@ -447,7 +494,8 @@ bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath& out)
return true;
}

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

/** Parse a public key including origin information (if enabled). */
std::unique_ptr<PubkeyProvider> ParsePubkey(const Span<const char>& sp, bool permit_uncompressed, FlatSigningProvider& out)
{
auto origin_split = Split(sp, ']');
if (origin_split.size() > 2) return nullptr;
if (origin_split.size() == 1) return ParsePubkeyInner(origin_split[0], permit_uncompressed, out);
if (origin_split[0].size() < 1 || origin_split[0][0] != '[') return nullptr;
auto slash_split = Split(origin_split[0].subspan(1), '/');
if (slash_split[0].size() != 8) return nullptr;
sipa marked this conversation as resolved.
Show resolved Hide resolved
std::string fpr_hex = std::string(slash_split[0].begin(), slash_split[0].end());
if (!IsHex(fpr_hex)) return nullptr;
auto fpr_bytes = ParseHex(fpr_hex);
KeyOriginInfo info;
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);
sipa marked this conversation as resolved.
Show resolved Hide resolved
if (!ParseKeyPath(slash_split, info.path)) return nullptr;
auto provider = ParsePubkeyInner(origin_split[1], permit_uncompressed, out);
if (!provider) return nullptr;
return MakeUnique<OriginPubkeyProvider>(std::move(info), std::move(provider));
}

/** Parse a script in a particular context. */
std::unique_ptr<Descriptor> ParseScript(Span<const char>& sp, ParseScriptContext ctx, FlatSigningProvider& out)
{
Expand Down
1 change: 1 addition & 0 deletions src/script/sign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@ bool HidingSigningProvider::GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& inf

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

FlatSigningProvider Merge(const FlatSigningProvider& a, const FlatSigningProvider& b)
Expand Down
4 changes: 3 additions & 1 deletion src/script/sign.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class SigningProvider
virtual bool GetCScript(const CScriptID &scriptid, CScript& script) const { return false; }
virtual bool GetPubKey(const CKeyID &address, CPubKey& pubkey) const { return false; }
virtual bool GetKey(const CKeyID &address, CKey& key) const { return false; }
virtual bool GetKeyOrigin(const CKeyID& id, KeyOriginInfo& info) const { return false; }
virtual bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const { return false; }
};

extern const SigningProvider& DUMMY_SIGNING_PROVIDER;
Expand All @@ -58,10 +58,12 @@ struct FlatSigningProvider final : public SigningProvider
{
std::map<CScriptID, CScript> scripts;
std::map<CKeyID, CPubKey> pubkeys;
std::map<CKeyID, KeyOriginInfo> origins;
std::map<CKeyID, CKey> keys;

bool GetCScript(const CScriptID& scriptid, CScript& script) const override;
bool GetPubKey(const CKeyID& keyid, CPubKey& pubkey) const override;
bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const override;
bool GetKey(const CKeyID& keyid, CKey& key) const override;
};

Expand Down
Loading