Skip to content

Commit ade1761

Browse files
sgeislerdarosior
andcommitted
descriptor: Add an abstracted DescriptorPublicKey
A DescriptorPublicKey abstracts out the different public keys that can possibly be used in a descriptor, and their different forms (with / without source, wildcard in deriv. path, etc..). Co-Authored-By: Antoine Poinsot <[email protected]> Signed-off-by: Antoine Poinsot <[email protected]>
1 parent 0718263 commit ade1761

File tree

1 file changed

+259
-0
lines changed

1 file changed

+259
-0
lines changed

src/descriptor/mod.rs

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,13 @@ use std::fmt;
2727
use std::str::{self, FromStr};
2828

2929
use bitcoin::blockdata::{opcodes, script};
30+
use bitcoin::hashes::hex::FromHex;
31+
use bitcoin::util::bip32;
3032
use bitcoin::{self, Script};
3133

34+
#[cfg(feature = "serde")]
35+
use serde::{de, ser};
36+
3237
use expression;
3338
use miniscript;
3439
use miniscript::context::ScriptContextError;
@@ -68,6 +73,126 @@ pub enum Descriptor<Pk: MiniscriptKey> {
6873
ShWsh(Miniscript<Pk, Segwitv0>),
6974
}
7075

76+
#[derive(Debug, Eq, PartialEq)]
77+
pub enum DescriptorPublicKey {
78+
PukKey(bitcoin::PublicKey),
79+
XPub(DescriptorXPub),
80+
}
81+
82+
#[derive(Debug, Eq, PartialEq)]
83+
pub struct DescriptorXPub {
84+
origin: Option<(bip32::Fingerprint, bip32::DerivationPath)>,
85+
xpub: bip32::ExtendedPubKey,
86+
derivation_path: bip32::DerivationPath,
87+
is_wildcard: bool,
88+
}
89+
90+
#[derive(Debug, PartialEq)]
91+
pub struct DescriptorKeyParseError(&'static str);
92+
93+
impl FromStr for DescriptorPublicKey {
94+
type Err = DescriptorKeyParseError;
95+
96+
fn from_str(s: &str) -> Result<Self, Self::Err> {
97+
if s.len() < 66 {
98+
Err(DescriptorKeyParseError(
99+
"Key too short (<66 char), doesn't match any format",
100+
))
101+
} else if s.chars().next().unwrap() == '[' {
102+
let mut parts = s[1..].split(']');
103+
let mut raw_origin = parts
104+
.next()
105+
.ok_or(DescriptorKeyParseError("Unclosed '['"))?
106+
.split('/');
107+
108+
let origin_id_hex = raw_origin.next().ok_or(DescriptorKeyParseError(
109+
"No master fingerprint found after '['",
110+
))?;
111+
112+
if origin_id_hex.len() != 8 {
113+
return Err(DescriptorKeyParseError(
114+
"Master fingerprint should be 8 characters long",
115+
));
116+
}
117+
let parent_fingerprint = bip32::Fingerprint::from_hex(origin_id_hex).map_err(|_| {
118+
DescriptorKeyParseError("Malformed master fingerprint, expected 8 hex chars")
119+
})?;
120+
121+
let origin_path = raw_origin
122+
.map(|p| bip32::ChildNumber::from_str(p))
123+
.collect::<Result<bip32::DerivationPath, bip32::Error>>()
124+
.map_err(|_| {
125+
DescriptorKeyParseError("Error while parsing master derivation path")
126+
})?;
127+
128+
let key_deriv = parts
129+
.next()
130+
.ok_or(DescriptorKeyParseError("No key after origin."))?;
131+
132+
let (xpub, derivation_path, is_wildcard) = Self::parse_xpub_deriv(key_deriv)?;
133+
134+
Ok(DescriptorPublicKey::XPub(DescriptorXPub {
135+
origin: Some((parent_fingerprint, origin_path)),
136+
xpub,
137+
derivation_path,
138+
is_wildcard,
139+
}))
140+
} else if s.starts_with("02") || s.starts_with("03") || s.starts_with("04") {
141+
let pk = bitcoin::PublicKey::from_str(s)
142+
.map_err(|_| DescriptorKeyParseError("Error while parsing simple public key"))?;
143+
Ok(DescriptorPublicKey::PukKey(pk))
144+
} else {
145+
let (xpub, derivation_path, is_wildcard) = Self::parse_xpub_deriv(s)?;
146+
Ok(DescriptorPublicKey::XPub(DescriptorXPub {
147+
origin: None,
148+
xpub,
149+
derivation_path,
150+
is_wildcard,
151+
}))
152+
}
153+
}
154+
}
155+
156+
impl DescriptorPublicKey {
157+
/// Parse an extended public key concatenated to a derivation path.
158+
fn parse_xpub_deriv(
159+
key_deriv: &str,
160+
) -> Result<(bip32::ExtendedPubKey, bip32::DerivationPath, bool), DescriptorKeyParseError> {
161+
let mut key_deriv = key_deriv.split('/');
162+
let xpub_str = key_deriv.next().ok_or(DescriptorKeyParseError(
163+
"No key found after origin description",
164+
))?;
165+
let xpub = bip32::ExtendedPubKey::from_str(xpub_str)
166+
.map_err(|_| DescriptorKeyParseError("Error while parsing xpub."))?;
167+
168+
let mut is_wildcard = false;
169+
let derivation_path = key_deriv
170+
.filter_map(|p| {
171+
if !is_wildcard && p == "*" {
172+
is_wildcard = true;
173+
None
174+
} else if is_wildcard {
175+
Some(Err(DescriptorKeyParseError(
176+
"'*' may only appear as last element in a derivation path.",
177+
)))
178+
} else {
179+
Some(bip32::ChildNumber::from_str(p).map_err(|_| {
180+
DescriptorKeyParseError("Error while parsing key derivation path")
181+
}))
182+
}
183+
})
184+
.collect::<Result<bip32::DerivationPath, _>>()?;
185+
186+
if (&derivation_path).into_iter().all(|c| c.is_normal()) {
187+
Ok((xpub, derivation_path, is_wildcard))
188+
} else {
189+
Err(DescriptorKeyParseError(
190+
"Hardened derivation is currently not supported.",
191+
))
192+
}
193+
}
194+
}
195+
71196
impl<Pk: MiniscriptKey> Descriptor<Pk> {
72197
/// Convert a descriptor using abstract keys to one using specific keys
73198
/// This will panic if translatefpk returns an uncompressed key when
@@ -553,13 +678,18 @@ serde_string_impl_pk!(Descriptor, "a script descriptor");
553678

554679
#[cfg(test)]
555680
mod tests {
681+
use super::DescriptorKeyParseError;
682+
556683
use bitcoin::blockdata::opcodes::all::{OP_CLTV, OP_CSV};
557684
use bitcoin::blockdata::script::Instruction;
558685
use bitcoin::blockdata::{opcodes, script};
559686
use bitcoin::hashes::hex::FromHex;
560687
use bitcoin::hashes::{hash160, sha256};
688+
use bitcoin::util::bip32;
561689
use bitcoin::{self, secp256k1, PublicKey};
690+
use descriptor::{DescriptorPublicKey, DescriptorXPub};
562691
use miniscript::satisfy::BitcoinSig;
692+
use policy;
563693
use std::collections::HashMap;
564694
use std::str::FromStr;
565695
use {Descriptor, DummyKey, Miniscript, Satisfier};
@@ -1075,4 +1205,133 @@ mod tests {
10751205
.unwrap()[..]
10761206
);
10771207
}
1208+
1209+
#[test]
1210+
fn parse_descriptor_key() {
1211+
// With a wildcard
1212+
let key = "[78412e3a/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*";
1213+
let expected = DescriptorPublicKey::XPub(DescriptorXPub {
1214+
origin: Some((
1215+
bip32::Fingerprint::from(&[0x78, 0x41, 0x2e, 0x3a][..]),
1216+
(&[
1217+
bip32::ChildNumber::from_hardened_idx(44).unwrap(),
1218+
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
1219+
bip32::ChildNumber::from_hardened_idx(0).unwrap(),
1220+
][..])
1221+
.into(),
1222+
)),
1223+
xpub: bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap(),
1224+
derivation_path: (&[bip32::ChildNumber::from_normal_idx(1).unwrap()][..]).into(),
1225+
is_wildcard: true,
1226+
});
1227+
assert_eq!(expected, key.parse().unwrap());
1228+
1229+
// Without origin
1230+
let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1";
1231+
let expected = DescriptorPublicKey::XPub(DescriptorXPub {
1232+
origin: None,
1233+
xpub: bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap(),
1234+
derivation_path: (&[bip32::ChildNumber::from_normal_idx(1).unwrap()][..]).into(),
1235+
is_wildcard: false,
1236+
});
1237+
assert_eq!(expected, key.parse().unwrap());
1238+
1239+
// Without derivation path
1240+
let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL";
1241+
let expected = DescriptorPublicKey::XPub(DescriptorXPub {
1242+
origin: None,
1243+
xpub: bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap(),
1244+
derivation_path: bip32::DerivationPath::from(&[][..]),
1245+
is_wildcard: false,
1246+
});
1247+
assert_eq!(expected, key.parse().unwrap());
1248+
1249+
// Raw (compressed) pubkey
1250+
let key = "03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8";
1251+
let expected = DescriptorPublicKey::PukKey(
1252+
bitcoin::PublicKey::from_str(
1253+
"03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8",
1254+
)
1255+
.unwrap(),
1256+
);
1257+
assert_eq!(expected, key.parse().unwrap());
1258+
1259+
// Raw (uncompressed) pubkey
1260+
let key = "04f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446a";
1261+
let expected = DescriptorPublicKey::PukKey(
1262+
bitcoin::PublicKey::from_str(
1263+
"04f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446a",
1264+
)
1265+
.unwrap(),
1266+
);
1267+
assert_eq!(expected, key.parse().unwrap());
1268+
}
1269+
1270+
#[test]
1271+
fn parse_descriptor_key_errors() {
1272+
// origin is only supported for xpubs
1273+
let desc =
1274+
"[78412e3a/0'/0'/0']0231c7d3fc85c148717848033ce276ae2b464a4e2c367ed33886cc428b8af48ff8";
1275+
assert_eq!(
1276+
DescriptorPublicKey::from_str(desc),
1277+
Err(DescriptorKeyParseError("Error while parsing xpub."))
1278+
);
1279+
1280+
// We refuse creating descriptors which claim to be able to derive hardened childs
1281+
let desc = "[78412e3a/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/42'/*";
1282+
assert_eq!(
1283+
DescriptorPublicKey::from_str(desc),
1284+
Err(DescriptorKeyParseError(
1285+
"Hardened derivation is currently not supported."
1286+
))
1287+
);
1288+
1289+
// And ones with misplaced wildcard
1290+
let desc = "[78412e3a/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*/44";
1291+
assert_eq!(
1292+
DescriptorPublicKey::from_str(desc),
1293+
Err(DescriptorKeyParseError(
1294+
"\'*\' may only appear as last element in a derivation path."
1295+
))
1296+
);
1297+
1298+
// And ones with invalid fingerprints
1299+
let desc = "[NonHexor]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*";
1300+
assert_eq!(
1301+
DescriptorPublicKey::from_str(desc),
1302+
Err(DescriptorKeyParseError(
1303+
"Malformed master fingerprint, expected 8 hex chars"
1304+
))
1305+
);
1306+
1307+
// And ones with invalid xpubs
1308+
let desc = "[78412e3a]xpub1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaLcgJvLJuZZvRcEL/1/*";
1309+
assert_eq!(
1310+
DescriptorPublicKey::from_str(desc),
1311+
Err(DescriptorKeyParseError("Error while parsing xpub."))
1312+
);
1313+
}
1314+
1315+
#[test]
1316+
#[cfg(feature = "compiler")]
1317+
fn parse_and_derive() {
1318+
let descriptor_str = "thresh(2,\
1319+
pk([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*),\
1320+
pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1),\
1321+
pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
1322+
let policy: policy::concrete::Policy<DescriptorPublicKey> = descriptor_str.parse().unwrap();
1323+
let descriptor = Descriptor::Sh(policy.compile().unwrap());
1324+
let derived_descriptor =
1325+
descriptor.derive(&[bip32::ChildNumber::from_normal_idx(42).unwrap()]);
1326+
1327+
let res_descriptor_str = "thresh(2,\
1328+
pk([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/42),\
1329+
pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1),\
1330+
pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
1331+
let res_policy: policy::concrete::Policy<DescriptorPublicKey> =
1332+
res_descriptor_str.parse().unwrap();
1333+
let res_descriptor = Descriptor::Sh(res_policy.compile().unwrap());
1334+
1335+
assert_eq!(res_descriptor, derived_descriptor);
1336+
}
10781337
}

0 commit comments

Comments
 (0)