From 71add82e2bb5e7376dcfffe8f65073faab3e098c Mon Sep 17 00:00:00 2001 From: Sebastian Geisler Date: Sun, 31 May 2020 23:03:16 +0200 Subject: [PATCH 1/6] 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 Signed-off-by: Antoine Poinsot --- src/descriptor/mod.rs | 261 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 9fa9b5020..3b17324d3 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -27,8 +27,13 @@ use std::fmt; use std::str::{self, FromStr}; use bitcoin::blockdata::{opcodes, script}; +use bitcoin::hashes::hex::FromHex; +use bitcoin::util::bip32; use bitcoin::{self, Script}; +#[cfg(feature = "serde")] +use serde::{de, ser}; + use expression; use miniscript; use miniscript::context::ScriptContextError; @@ -68,6 +73,140 @@ pub enum Descriptor { ShWsh(Miniscript), } +#[derive(Debug, Eq, PartialEq)] +pub enum DescriptorPublicKey { + PubKey(bitcoin::PublicKey), + XPub(DescriptorXPub), +} + +#[derive(Debug, Eq, PartialEq)] +pub struct DescriptorXPub { + pub origin: Option<(bip32::Fingerprint, bip32::DerivationPath)>, + pub xpub: bip32::ExtendedPubKey, + pub derivation_path: bip32::DerivationPath, + pub is_wildcard: bool, +} + +#[derive(Debug, PartialEq)] +pub struct DescriptorKeyParseError(&'static str); + +impl FromStr for DescriptorPublicKey { + type Err = DescriptorKeyParseError; + + fn from_str(s: &str) -> Result { + if s.len() < 66 { + return Err(DescriptorKeyParseError( + "Key too short (<66 char), doesn't match any format", + )); + } + + for ch in s.as_bytes() { + if *ch < 20 || *ch > 127 { + return Err(DescriptorKeyParseError( + "Encountered an unprintable character", + )); + } + } + + if s.chars().next().unwrap() == '[' { + let mut parts = s[1..].split(']'); + let mut raw_origin = parts + .next() + .ok_or(DescriptorKeyParseError("Unclosed '['"))? + .split('/'); + + let origin_id_hex = raw_origin.next().ok_or(DescriptorKeyParseError( + "No master fingerprint found after '['", + ))?; + + if origin_id_hex.len() != 8 { + return Err(DescriptorKeyParseError( + "Master fingerprint should be 8 characters long", + )); + } + let parent_fingerprint = bip32::Fingerprint::from_hex(origin_id_hex).map_err(|_| { + DescriptorKeyParseError("Malformed master fingerprint, expected 8 hex chars") + })?; + + let origin_path = raw_origin + .map(|p| bip32::ChildNumber::from_str(p)) + .collect::>() + .map_err(|_| { + DescriptorKeyParseError("Error while parsing master derivation path") + })?; + + let key_deriv = parts + .next() + .ok_or(DescriptorKeyParseError("No key after origin."))?; + + let (xpub, derivation_path, is_wildcard) = Self::parse_xpub_deriv(key_deriv)?; + + Ok(DescriptorPublicKey::XPub(DescriptorXPub { + origin: Some((parent_fingerprint, origin_path)), + xpub, + derivation_path, + is_wildcard, + })) + } else if s.starts_with("02") || s.starts_with("03") || s.starts_with("04") { + let pk = bitcoin::PublicKey::from_str(s) + .map_err(|_| DescriptorKeyParseError("Error while parsing simple public key"))?; + Ok(DescriptorPublicKey::PubKey(pk)) + } else { + let (xpub, derivation_path, is_wildcard) = Self::parse_xpub_deriv(s)?; + Ok(DescriptorPublicKey::XPub(DescriptorXPub { + origin: None, + xpub, + derivation_path, + is_wildcard, + })) + } + } +} + +impl DescriptorPublicKey { + /// Parse an extended public key concatenated to a derivation path. + fn parse_xpub_deriv( + key_deriv: &str, + ) -> Result<(bip32::ExtendedPubKey, bip32::DerivationPath, bool), DescriptorKeyParseError> { + let mut key_deriv = key_deriv.split('/'); + let xpub_str = key_deriv.next().ok_or(DescriptorKeyParseError( + "No key found after origin description", + ))?; + let xpub = bip32::ExtendedPubKey::from_str(xpub_str) + .map_err(|_| DescriptorKeyParseError("Error while parsing xpub."))?; + + let mut is_wildcard = false; + let derivation_path = key_deriv + .filter_map(|p| { + if !is_wildcard && p == "*" { + is_wildcard = true; + None + } else if !is_wildcard && p == "*'" { + Some(Err(DescriptorKeyParseError( + "Hardened derivation is currently not supported.", + ))) + } else if is_wildcard { + Some(Err(DescriptorKeyParseError( + "'*' may only appear as last element in a derivation path.", + ))) + } else { + Some(bip32::ChildNumber::from_str(p).map_err(|_| { + DescriptorKeyParseError("Error while parsing key derivation path") + })) + } + }) + .collect::>()?; + + if (&derivation_path).into_iter().all(|c| c.is_normal()) { + Ok((xpub, derivation_path, is_wildcard)) + } else { + Err(DescriptorKeyParseError( + "Hardened derivation is currently not supported.", + )) + } + } +} + impl Descriptor { /// Convert a descriptor using abstract keys to one using specific keys /// This will panic if translatefpk returns an uncompressed key when @@ -551,17 +690,24 @@ serde_string_impl_pk!(Descriptor, "a script descriptor"); #[cfg(test)] mod tests { + use super::DescriptorKeyParseError; + use bitcoin::blockdata::opcodes::all::{OP_CLTV, OP_CSV}; use bitcoin::blockdata::script::Instruction; use bitcoin::blockdata::{opcodes, script}; use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::{hash160, sha256}; + use bitcoin::util::bip32; use bitcoin::{self, secp256k1, PublicKey}; + use descriptor::{DescriptorPublicKey, DescriptorXPub}; use miniscript::satisfy::BitcoinSig; use std::collections::HashMap; use std::str::FromStr; use {Descriptor, DummyKey, Miniscript, Satisfier}; + #[cfg(feature = "compiler")] + use policy; + type StdDescriptor = Descriptor; const TEST_PK: &'static str = "pk(020000000000000000000000000000000000000000000000000000000000000002)"; @@ -1065,4 +1211,119 @@ mod tests { .unwrap()[..] ); } + + #[test] + fn parse_descriptor_key() { + // With a wildcard + let key = "[78412e3a/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*"; + let expected = DescriptorPublicKey::XPub(DescriptorXPub { + origin: Some(( + bip32::Fingerprint::from(&[0x78, 0x41, 0x2e, 0x3a][..]), + (&[ + bip32::ChildNumber::from_hardened_idx(44).unwrap(), + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + ][..]) + .into(), + )), + xpub: bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap(), + derivation_path: (&[bip32::ChildNumber::from_normal_idx(1).unwrap()][..]).into(), + is_wildcard: true, + }); + assert_eq!(expected, key.parse().unwrap()); + + // Without origin + let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1"; + let expected = DescriptorPublicKey::XPub(DescriptorXPub { + origin: None, + xpub: bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap(), + derivation_path: (&[bip32::ChildNumber::from_normal_idx(1).unwrap()][..]).into(), + is_wildcard: false, + }); + assert_eq!(expected, key.parse().unwrap()); + + // Without derivation path + let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"; + let expected = DescriptorPublicKey::XPub(DescriptorXPub { + origin: None, + xpub: bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap(), + derivation_path: bip32::DerivationPath::from(&[][..]), + is_wildcard: false, + }); + assert_eq!(expected, key.parse().unwrap()); + + // Raw (compressed) pubkey + let key = "03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8"; + let expected = DescriptorPublicKey::PubKey( + bitcoin::PublicKey::from_str( + "03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8", + ) + .unwrap(), + ); + assert_eq!(expected, key.parse().unwrap()); + + // Raw (uncompressed) pubkey + let key = "04f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446a"; + let expected = DescriptorPublicKey::PubKey( + bitcoin::PublicKey::from_str( + "04f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446a", + ) + .unwrap(), + ); + assert_eq!(expected, key.parse().unwrap()); + } + + #[test] + fn parse_descriptor_key_errors() { + // origin is only supported for xpubs + let desc = + "[78412e3a/0'/0'/0']0231c7d3fc85c148717848033ce276ae2b464a4e2c367ed33886cc428b8af48ff8"; + assert_eq!( + DescriptorPublicKey::from_str(desc), + Err(DescriptorKeyParseError("Error while parsing xpub.")) + ); + + // We refuse creating descriptors which claim to be able to derive hardened childs + let desc = "[78412e3a/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/42'/*"; + assert_eq!( + DescriptorPublicKey::from_str(desc), + Err(DescriptorKeyParseError( + "Hardened derivation is currently not supported." + )) + ); + + // And even if they they claim it for the wildcard! + let desc = "[78412e3a/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/42/*'"; + assert_eq!( + DescriptorPublicKey::from_str(desc), + Err(DescriptorKeyParseError( + "Hardened derivation is currently not supported." + )) + ); + + // And ones with misplaced wildcard + let desc = "[78412e3a/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*/44"; + assert_eq!( + DescriptorPublicKey::from_str(desc), + Err(DescriptorKeyParseError( + "\'*\' may only appear as last element in a derivation path." + )) + ); + + // And ones with invalid fingerprints + let desc = "[NonHexor]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*"; + assert_eq!( + DescriptorPublicKey::from_str(desc), + Err(DescriptorKeyParseError( + "Malformed master fingerprint, expected 8 hex chars" + )) + ); + + // And ones with invalid xpubs + let desc = "[78412e3a]xpub1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaLcgJvLJuZZvRcEL/1/*"; + assert_eq!( + DescriptorPublicKey::from_str(desc), + Err(DescriptorKeyParseError("Error while parsing xpub.")) + ); + } } From 35d2ab724167eb5f347290b87500f3899e104a35 Mon Sep 17 00:00:00 2001 From: Sebastian Geisler Date: Wed, 3 Jun 2020 18:10:48 +0200 Subject: [PATCH 2/6] Implement MiniscriptKey for DescriptorKey Signed-off-by: Antoine Poinsot --- src/descriptor/mod.rs | 63 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 3b17324d3..bc9e78999 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -27,7 +27,9 @@ use std::fmt; use std::str::{self, FromStr}; use bitcoin::blockdata::{opcodes, script}; +use bitcoin::hashes::hash160; use bitcoin::hashes::hex::FromHex; +use bitcoin::secp256k1; use bitcoin::util::bip32; use bitcoin::{self, Script}; @@ -73,13 +75,13 @@ pub enum Descriptor { ShWsh(Miniscript), } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] pub enum DescriptorPublicKey { PubKey(bitcoin::PublicKey), XPub(DescriptorXPub), } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] pub struct DescriptorXPub { pub origin: Option<(bip32::Fingerprint, bip32::DerivationPath)>, pub xpub: bip32::ExtendedPubKey, @@ -87,9 +89,41 @@ pub struct DescriptorXPub { pub is_wildcard: bool, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub struct DescriptorKeyParseError(&'static str); +impl fmt::Display for DescriptorPublicKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + DescriptorPublicKey::PubKey(ref pk) => pk.fmt(f), + DescriptorPublicKey::XPub(ref xpub) => { + if let Some((ref master_id, ref master_deriv)) = xpub.origin { + fmt::Formatter::write_str(f, "[")?; + for byte in master_id.into_bytes().iter() { + write!(f, "{:02x}", byte)?; + } + fmt_derivation_path(f, master_deriv)?; + fmt::Formatter::write_str(f, "]")?; + } + xpub.xpub.fmt(f)?; + fmt_derivation_path(f, &xpub.derivation_path)?; + if xpub.is_wildcard { + write!(f, "/*")?; + } + Ok(()) + } + } + } +} + +/// Writes a derivation path to the formatter, no leading 'm' +fn fmt_derivation_path(f: &mut fmt::Formatter, path: &bip32::DerivationPath) -> fmt::Result { + for child in path { + write!(f, "/{}", child)?; + } + Ok(()) +} + impl FromStr for DescriptorPublicKey { type Err = DescriptorKeyParseError; @@ -207,6 +241,24 @@ impl DescriptorPublicKey { } } +impl MiniscriptKey for DescriptorPublicKey { + type Hash = hash160::Hash; + + fn to_pubkeyhash(&self) -> Self::Hash { + match *self { + DescriptorPublicKey::PubKey(ref pk) => pk.to_pubkeyhash(), + DescriptorPublicKey::XPub(ref xpub) => { + let ctx = secp256k1::Secp256k1::verification_only(); + xpub.xpub + .derive_pub(&ctx, &xpub.derivation_path) + .expect("Won't fail, only normal derivations") + .public_key + .to_pubkeyhash() + } + } + } +} + impl Descriptor { /// Convert a descriptor using abstract keys to one using specific keys /// This will panic if translatefpk returns an uncompressed key when @@ -1231,6 +1283,7 @@ mod tests { is_wildcard: true, }); assert_eq!(expected, key.parse().unwrap()); + assert_eq!(format!("{}", expected), key); // Without origin let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1"; @@ -1241,6 +1294,7 @@ mod tests { is_wildcard: false, }); assert_eq!(expected, key.parse().unwrap()); + assert_eq!(format!("{}", expected), key); // Without derivation path let key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"; @@ -1251,6 +1305,7 @@ mod tests { is_wildcard: false, }); assert_eq!(expected, key.parse().unwrap()); + assert_eq!(format!("{}", expected), key); // Raw (compressed) pubkey let key = "03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8"; @@ -1261,6 +1316,7 @@ mod tests { .unwrap(), ); assert_eq!(expected, key.parse().unwrap()); + assert_eq!(format!("{}", expected), key); // Raw (uncompressed) pubkey let key = "04f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446a"; @@ -1271,6 +1327,7 @@ mod tests { .unwrap(), ); assert_eq!(expected, key.parse().unwrap()); + assert_eq!(format!("{}", expected), key); } #[test] From 021880e11fbf5387310bc1aea4302eb8b961b8ad Mon Sep 17 00:00:00 2001 From: Sebastian Geisler Date: Wed, 3 Jun 2020 18:41:50 +0200 Subject: [PATCH 3/6] Implement ToPublicKey for DescriptorKey --- src/descriptor/mod.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index bc9e78999..cd1e574cc 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -245,18 +245,27 @@ impl MiniscriptKey for DescriptorPublicKey { type Hash = hash160::Hash; fn to_pubkeyhash(&self) -> Self::Hash { + self.to_public_key().to_pubkeyhash() + } +} + +impl ToPublicKey for DescriptorPublicKey { + fn to_public_key(&self) -> bitcoin::PublicKey { match *self { - DescriptorPublicKey::PubKey(ref pk) => pk.to_pubkeyhash(), + DescriptorPublicKey::PubKey(ref pk) => *pk, DescriptorPublicKey::XPub(ref xpub) => { let ctx = secp256k1::Secp256k1::verification_only(); xpub.xpub .derive_pub(&ctx, &xpub.derivation_path) - .expect("Won't fail, only normal derivations") + .expect("Shouldn't fail, only normal derivations") .public_key - .to_pubkeyhash() } } } + + fn hash_to_hash160(hash: &Self::Hash) -> hash160::Hash { + *hash + } } impl Descriptor { From 367eda0d07a485b2d52f00d3b08bb8f45e74fe70 Mon Sep 17 00:00:00 2001 From: Sebastian Geisler Date: Thu, 4 Jun 2020 00:05:01 +0200 Subject: [PATCH 4/6] Make Descriptors derivable Co-Authored-by: Antoine Poinsot --- src/descriptor/mod.rs | 72 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index cd1e574cc..8a8a1527f 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -92,6 +92,12 @@ pub struct DescriptorXPub { #[derive(Debug, PartialEq, Clone, Copy)] pub struct DescriptorKeyParseError(&'static str); +impl fmt::Display for DescriptorKeyParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.0) + } +} + impl fmt::Display for DescriptorPublicKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -239,13 +245,37 @@ impl DescriptorPublicKey { )) } } + + /// Derives the specified child key if self is a wildcard xpub. Otherwise returns self. + /// + /// Panics if given a hardened child number + pub fn derive(self, child_number: bip32::ChildNumber) -> DescriptorPublicKey { + debug_assert!(child_number.is_normal()); + + match self { + DescriptorPublicKey::PubKey(_) => self, + DescriptorPublicKey::XPub(xpub) => { + if xpub.is_wildcard { + DescriptorPublicKey::XPub(DescriptorXPub { + origin: xpub.origin, + xpub: xpub.xpub, + derivation_path: xpub.derivation_path.into_child(child_number), + is_wildcard: false, + }) + } else { + DescriptorPublicKey::XPub(xpub) + } + } + } + } } impl MiniscriptKey for DescriptorPublicKey { - type Hash = hash160::Hash; + // This allows us to be able to derive public keys even for PkH s + type Hash = Self; - fn to_pubkeyhash(&self) -> Self::Hash { - self.to_public_key().to_pubkeyhash() + fn to_pubkeyhash(&self) -> Self { + self.clone() } } @@ -264,7 +294,7 @@ impl ToPublicKey for DescriptorPublicKey { } fn hash_to_hash160(hash: &Self::Hash) -> hash160::Hash { - *hash + hash.to_public_key().to_pubkeyhash() } } @@ -625,6 +655,17 @@ impl Descriptor { } } +impl Descriptor { + /// Derives all wildcard keys in the descriptor using the supplied `child_number` + pub fn derive(&self, child_number: bip32::ChildNumber) -> Descriptor { + self.translate_pk( + |pk| Result::Ok::(pk.clone().derive(child_number)), + |pk| Result::Ok::(pk.clone().derive(child_number)), + ) + .expect("Translation fn can't fail.") + } +} + impl expression::FromTree for Descriptor where Pk: MiniscriptKey, @@ -1392,4 +1433,27 @@ mod tests { Err(DescriptorKeyParseError("Error while parsing xpub.")) ); } + + #[test] + #[cfg(feature = "compiler")] + fn parse_and_derive() { + let descriptor_str = "thresh(2,\ +pk([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*),\ +pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1),\ +pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))"; + let policy: policy::concrete::Policy = descriptor_str.parse().unwrap(); + let descriptor = Descriptor::Sh(policy.compile().unwrap()); + let derived_descriptor = + descriptor.derive(bip32::ChildNumber::from_normal_idx(42).unwrap()); + + let res_descriptor_str = "thresh(2,\ +pk([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/42),\ +pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1),\ +pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))"; + let res_policy: policy::concrete::Policy = + res_descriptor_str.parse().unwrap(); + let res_descriptor = Descriptor::Sh(res_policy.compile().unwrap()); + + assert_eq!(res_descriptor, derived_descriptor); + } } From 5ac41125b392b682c1ebe3afed782144a2828b9b Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Fri, 11 Sep 2020 16:37:01 +0200 Subject: [PATCH 5/6] descriptor: allow "raw" pubkeys to have origin Signed-off-by: Antoine Poinsot --- src/descriptor/mod.rs | 146 +++++++++++++++++++++++++++++------------- 1 file changed, 103 insertions(+), 43 deletions(-) diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 8a8a1527f..fbf20fcac 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -77,10 +77,16 @@ pub enum Descriptor { #[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] pub enum DescriptorPublicKey { - PubKey(bitcoin::PublicKey), + SinglePub(DescriptorSinglePub), XPub(DescriptorXPub), } +#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] +pub struct DescriptorSinglePub { + pub origin: Option<(bip32::Fingerprint, bip32::DerivationPath)>, + pub key: bitcoin::PublicKey, +} + #[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] pub struct DescriptorXPub { pub origin: Option<(bip32::Fingerprint, bip32::DerivationPath)>, @@ -101,16 +107,13 @@ impl fmt::Display for DescriptorKeyParseError { impl fmt::Display for DescriptorPublicKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - DescriptorPublicKey::PubKey(ref pk) => pk.fmt(f), + DescriptorPublicKey::SinglePub(ref pk) => { + maybe_fmt_master_id(f, &pk.origin)?; + pk.key.fmt(f)?; + Ok(()) + } DescriptorPublicKey::XPub(ref xpub) => { - if let Some((ref master_id, ref master_deriv)) = xpub.origin { - fmt::Formatter::write_str(f, "[")?; - for byte in master_id.into_bytes().iter() { - write!(f, "{:02x}", byte)?; - } - fmt_derivation_path(f, master_deriv)?; - fmt::Formatter::write_str(f, "]")?; - } + maybe_fmt_master_id(f, &xpub.origin)?; xpub.xpub.fmt(f)?; fmt_derivation_path(f, &xpub.derivation_path)?; if xpub.is_wildcard { @@ -122,6 +125,23 @@ impl fmt::Display for DescriptorPublicKey { } } +/// Writes the fingerprint of the origin, if there is one. +fn maybe_fmt_master_id( + f: &mut fmt::Formatter, + origin: &Option<(bip32::Fingerprint, bip32::DerivationPath)>, +) -> fmt::Result { + if let Some((ref master_id, ref master_deriv)) = *origin { + fmt::Formatter::write_str(f, "[")?; + for byte in master_id.into_bytes().iter() { + write!(f, "{:02x}", byte)?; + } + fmt_derivation_path(f, master_deriv)?; + fmt::Formatter::write_str(f, "]")?; + } + + Ok(()) +} + /// Writes a derivation path to the formatter, no leading 'm' fn fmt_derivation_path(f: &mut fmt::Formatter, path: &bip32::DerivationPath) -> fmt::Result { for child in path { @@ -134,6 +154,7 @@ impl FromStr for DescriptorPublicKey { type Err = DescriptorKeyParseError; fn from_str(s: &str) -> Result { + // A "raw" public key without any origin is the least we accept. if s.len() < 66 { return Err(DescriptorKeyParseError( "Key too short (<66 char), doesn't match any format", @@ -148,8 +169,11 @@ impl FromStr for DescriptorPublicKey { } } + let mut parts = s[1..].split(']'); + + // They may specify an origin + let mut origin = None; if s.chars().next().unwrap() == '[' { - let mut parts = s[1..].split(']'); let mut raw_origin = parts .next() .ok_or(DescriptorKeyParseError("Unclosed '['"))? @@ -174,30 +198,33 @@ impl FromStr for DescriptorPublicKey { .map_err(|_| { DescriptorKeyParseError("Error while parsing master derivation path") })?; + origin = Some((parent_fingerprint, origin_path)); + } - let key_deriv = parts + let key_part = if origin == None { + Ok(s) + } else { + parts .next() - .ok_or(DescriptorKeyParseError("No key after origin."))?; + .ok_or(DescriptorKeyParseError("No key after origin.")) + }?; - let (xpub, derivation_path, is_wildcard) = Self::parse_xpub_deriv(key_deriv)?; + // To support testnet as well + if key_part.contains("pub") { + let (xpub, derivation_path, is_wildcard) = Self::parse_xpub_deriv(key_part)?; Ok(DescriptorPublicKey::XPub(DescriptorXPub { - origin: Some((parent_fingerprint, origin_path)), + origin, xpub, derivation_path, is_wildcard, })) - } else if s.starts_with("02") || s.starts_with("03") || s.starts_with("04") { - let pk = bitcoin::PublicKey::from_str(s) - .map_err(|_| DescriptorKeyParseError("Error while parsing simple public key"))?; - Ok(DescriptorPublicKey::PubKey(pk)) } else { - let (xpub, derivation_path, is_wildcard) = Self::parse_xpub_deriv(s)?; - Ok(DescriptorPublicKey::XPub(DescriptorXPub { - origin: None, - xpub, - derivation_path, - is_wildcard, + let key = bitcoin::PublicKey::from_str(key_part) + .map_err(|_| DescriptorKeyParseError("Error while parsing simple public key"))?; + Ok(DescriptorPublicKey::SinglePub(DescriptorSinglePub { + key, + origin, })) } } @@ -253,7 +280,7 @@ impl DescriptorPublicKey { debug_assert!(child_number.is_normal()); match self { - DescriptorPublicKey::PubKey(_) => self, + DescriptorPublicKey::SinglePub(_) => self, DescriptorPublicKey::XPub(xpub) => { if xpub.is_wildcard { DescriptorPublicKey::XPub(DescriptorXPub { @@ -282,7 +309,7 @@ impl MiniscriptKey for DescriptorPublicKey { impl ToPublicKey for DescriptorPublicKey { fn to_public_key(&self) -> bitcoin::PublicKey { match *self { - DescriptorPublicKey::PubKey(ref pk) => *pk, + DescriptorPublicKey::SinglePub(ref spub) => spub.key.to_public_key(), DescriptorPublicKey::XPub(ref xpub) => { let ctx = secp256k1::Secp256k1::verification_only(); xpub.xpub @@ -801,7 +828,7 @@ mod tests { use bitcoin::hashes::{hash160, sha256}; use bitcoin::util::bip32; use bitcoin::{self, secp256k1, PublicKey}; - use descriptor::{DescriptorPublicKey, DescriptorXPub}; + use descriptor::{DescriptorPublicKey, DescriptorSinglePub, DescriptorXPub}; use miniscript::satisfy::BitcoinSig; use std::collections::HashMap; use std::str::FromStr; @@ -1359,37 +1386,52 @@ mod tests { // Raw (compressed) pubkey let key = "03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8"; - let expected = DescriptorPublicKey::PubKey( - bitcoin::PublicKey::from_str( + let expected = DescriptorPublicKey::SinglePub(DescriptorSinglePub { + key: bitcoin::PublicKey::from_str( "03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8", ) .unwrap(), - ); + origin: None, + }); assert_eq!(expected, key.parse().unwrap()); assert_eq!(format!("{}", expected), key); // Raw (uncompressed) pubkey let key = "04f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446a"; - let expected = DescriptorPublicKey::PubKey( - bitcoin::PublicKey::from_str( + let expected = DescriptorPublicKey::SinglePub(DescriptorSinglePub { + key: bitcoin::PublicKey::from_str( "04f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446a", ) .unwrap(), - ); + origin: None, + }); assert_eq!(expected, key.parse().unwrap()); assert_eq!(format!("{}", expected), key); + + // Raw pubkey with origin + let desc = + "[78412e3a/0'/42/0']0231c7d3fc85c148717848033ce276ae2b464a4e2c367ed33886cc428b8af48ff8"; + let expected = DescriptorPublicKey::SinglePub(DescriptorSinglePub { + key: bitcoin::PublicKey::from_str( + "0231c7d3fc85c148717848033ce276ae2b464a4e2c367ed33886cc428b8af48ff8", + ) + .unwrap(), + origin: Some(( + bip32::Fingerprint::from(&[0x78, 0x41, 0x2e, 0x3a][..]), + (&[ + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + bip32::ChildNumber::from_normal_idx(42).unwrap(), + bip32::ChildNumber::from_hardened_idx(0).unwrap(), + ][..]) + .into(), + )), + }); + assert_eq!(expected, desc.parse().expect("Parsing desc")); + assert_eq!(format!("{}", expected), desc); } #[test] fn parse_descriptor_key_errors() { - // origin is only supported for xpubs - let desc = - "[78412e3a/0'/0'/0']0231c7d3fc85c148717848033ce276ae2b464a4e2c367ed33886cc428b8af48ff8"; - assert_eq!( - DescriptorPublicKey::from_str(desc), - Err(DescriptorKeyParseError("Error while parsing xpub.")) - ); - // We refuse creating descriptors which claim to be able to derive hardened childs let desc = "[78412e3a/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/42'/*"; assert_eq!( @@ -1426,12 +1468,30 @@ mod tests { )) ); - // And ones with invalid xpubs + // And ones with invalid xpubs.. let desc = "[78412e3a]xpub1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaLcgJvLJuZZvRcEL/1/*"; assert_eq!( DescriptorPublicKey::from_str(desc), Err(DescriptorKeyParseError("Error while parsing xpub.")) ); + + // ..or invalid raw keys + let desc = "[78412e3a]0208a117f3897c3a13c9384b8695eed98dc31bc2500feb19a1af424cd47a5d83/1/*"; + assert_eq!( + DescriptorPublicKey::from_str(desc), + Err(DescriptorKeyParseError( + "Error while parsing simple public key" + )) + ); + + // ..or invalid separators + let desc = "[78412e3a]]03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8"; + assert_eq!( + DescriptorPublicKey::from_str(desc), + Err(DescriptorKeyParseError( + "Error while parsing simple public key" + )) + ); } #[test] From 28158a399978dc0c69ab1258ed0b4025d305c615 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Fri, 25 Sep 2020 19:26:58 +0200 Subject: [PATCH 6/6] fuzz: add a fuzz target for descriptor parsing Co-Authored-By: Andrew Poelstra Signed-off-by: Antoine Poinsot --- fuzz/Cargo.toml | 6 ++++- fuzz/fuzz_targets/parse_descriptor.rs | 33 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 fuzz/fuzz_targets/parse_descriptor.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 311afd911..f4b585562 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -43,4 +43,8 @@ path = "fuzz_targets/roundtrip_semantic.rs" [[bin]] name = "compile_descriptor" -path = "fuzz_targets/compile_descriptor.rs" \ No newline at end of file +path = "fuzz_targets/compile_descriptor.rs" + +[[bin]] +name = "parse_descriptor" +path = "fuzz_targets/parse_descriptor.rs" diff --git a/fuzz/fuzz_targets/parse_descriptor.rs b/fuzz/fuzz_targets/parse_descriptor.rs new file mode 100644 index 000000000..38d60797f --- /dev/null +++ b/fuzz/fuzz_targets/parse_descriptor.rs @@ -0,0 +1,33 @@ +extern crate miniscript; + +use miniscript::descriptor::DescriptorPublicKey; +use std::str::FromStr; + +fn do_test(data: &[u8]) { + let data_str = String::from_utf8_lossy(data); + if let Ok(dpk) = DescriptorPublicKey::from_str(&data_str) { + let output = dpk.to_string(); + assert_eq!(data_str.to_lowercase(), output.to_lowercase()); + } +} + +#[cfg(feature = "afl")] +extern crate afl; +#[cfg(feature = "afl")] +fn main() { + afl::read_stdio_bytes(|data| { + do_test(&data); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] +extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + do_test(data); + }); + } +}