diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 241f931ee..5bbb92c79 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -12,6 +12,7 @@ use bitcoin::key::{PublicKey, XOnlyPublicKey}; use bitcoin::secp256k1::{Secp256k1, Signing, Verification}; use bitcoin::NetworkKind; +use super::WalletPolicyError; use crate::prelude::*; #[cfg(feature = "serde")] use crate::serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -201,6 +202,16 @@ pub enum Wildcard { Hardened, } +impl fmt::Display for Wildcard { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Wildcard::None => write!(f, ""), + Wildcard::Unhardened => write!(f, "/*"), + Wildcard::Hardened => write!(f, "/*h"), + } + } +} + impl SinglePriv { /// Returns the public key of this key. fn to_public(&self, secp: &Secp256k1) -> SinglePub { @@ -412,16 +423,12 @@ impl fmt::Display for MalformedKeyDataKind { #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub enum DescriptorKeyParseError { - /// Error while parsing a BIP32 extended private key - Bip32Xpriv(bip32::Error), - /// Error while parsing a BIP32 extended public key - Bip32Xpub(bip32::Error), /// Error while parsing a derivation index DerivationIndexError { /// The invalid index index: String, /// The underlying parse error - err: bitcoin::bip32::Error, + err: bip32::Error, }, /// Error deriving the hardened private key. DeriveHardenedKey(bip32::Error), @@ -444,13 +451,13 @@ pub enum DescriptorKeyParseError { WifPrivateKey(bitcoin::key::FromWifError), /// Error while parsing an X-only public key (Secp256k1 error). XonlyPublicKey(bitcoin::secp256k1::Error), + /// XKey parsing error + XKeyParseError(XKeyParseError), } impl fmt::Display for DescriptorKeyParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::Bip32Xpriv(err) => err.fmt(f), - Self::Bip32Xpub(err) => err.fmt(f), Self::DerivationIndexError { index, err } => { write!(f, "at derivation index '{index}': {err}") } @@ -464,17 +471,16 @@ impl fmt::Display for DescriptorKeyParseError { Self::FullPublicKey(err) => err.fmt(f), Self::WifPrivateKey(err) => err.fmt(f), Self::XonlyPublicKey(err) => err.fmt(f), + Self::XKeyParseError(err) => err.fmt(f), } } } #[cfg(feature = "std")] impl error::Error for DescriptorKeyParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { - Self::Bip32Xpriv(err) - | Self::Bip32Xpub(err) - | Self::DerivationIndexError { err, .. } + Self::DerivationIndexError { err, .. } | Self::DeriveHardenedKey(err) | Self::MasterDerivationPath(err) => Some(err), Self::MasterFingerprint { err, .. } => Some(err), @@ -482,11 +488,42 @@ impl error::Error for DescriptorKeyParseError { Self::FullPublicKey(err) => Some(err), Self::WifPrivateKey(err) => Some(err), Self::XonlyPublicKey(err) => Some(err), + Self::XKeyParseError(err) => Some(err), Self::MalformedKeyData(_) => None, } } } +/// An error when parsing an extended key. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum XKeyParseError { + Bip32(bip32::Error), + Bip388(WalletPolicyError), +} + +#[cfg(feature = "std")] +impl error::Error for XKeyParseError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Self::Bip32(err) => Some(err), + Self::Bip388(err) => Some(err), + } + } +} + +impl fmt::Display for XKeyParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Bip32(err) => err.fmt(f), + Self::Bip388(err) => err.fmt(f), + } + } +} + +impl From for XKeyParseError { + fn from(err: bip32::Error) -> Self { Self::Bip32(err) } +} + impl fmt::Display for DescriptorPublicKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -502,22 +539,14 @@ impl fmt::Display for DescriptorPublicKey { maybe_fmt_master_id(f, &xpub.origin)?; xpub.xkey.fmt(f)?; fmt_derivation_path(f, &xpub.derivation_path)?; - match xpub.wildcard { - Wildcard::None => {} - Wildcard::Unhardened => write!(f, "/*")?, - Wildcard::Hardened => write!(f, "/*h")?, - } + xpub.wildcard.fmt(f)?; Ok(()) } Self::MultiXPub(ref xpub) => { maybe_fmt_master_id(f, &xpub.origin)?; xpub.xkey.fmt(f)?; fmt_derivation_paths(f, xpub.derivation_paths.paths())?; - match xpub.wildcard { - Wildcard::None => {} - Wildcard::Unhardened => write!(f, "/*")?, - Wildcard::Hardened => write!(f, "/*h")?, - } + xpub.wildcard.fmt(f)?; Ok(()) } } @@ -610,7 +639,10 @@ fn fmt_derivation_path(f: &mut fmt::Formatter, path: &bip32::DerivationPath) -> /// Writes multiple derivation paths to the formatter, no leading 'm'. /// NOTE: we assume paths only differ at a single index, as prescribed by BIP389. /// Will panic if the list of paths is empty. -fn fmt_derivation_paths(f: &mut fmt::Formatter, paths: &[bip32::DerivationPath]) -> fmt::Result { +pub(crate) fn fmt_derivation_paths( + f: &mut W, + paths: &[bip32::DerivationPath], +) -> fmt::Result { for (i, child) in paths[0].into_iter().enumerate() { if paths.len() > 1 && child != &paths[1][i] { write!(f, "/<")?; @@ -642,7 +674,7 @@ impl FromStr for DescriptorPublicKey { let (key_part, origin) = parse_key_origin(s)?; if key_part.contains("pub") { - let (xpub, derivation_paths, wildcard) = parse_xkey_deriv(parse_bip32_xpub, key_part)?; + let (xpub, derivation_paths, wildcard) = parse_xkey_deriv(key_part)?; if derivation_paths.len() > 1 { Ok(DescriptorPublicKey::MultiXPub(DescriptorMultiXKey { origin, @@ -795,6 +827,38 @@ impl DescriptorPublicKey { } } + /// Derivation path without the origin prefix. + /// + /// For wildcard keys this will return the path up to the wildcard, so you + /// can get full paths by appending one additional derivation step, according + /// to the wildcard type (hardened or normal). + /// + /// For multipath extended keys, this returns `None`. + pub fn derivation_path(&self) -> Option { + match *self { + DescriptorPublicKey::XPub(ref xpub) => Some(xpub.derivation_path.clone()), + DescriptorPublicKey::Single(_) => Some(bip32::DerivationPath::from(vec![])), + DescriptorPublicKey::MultiXPub(_) => None, + } + } + + /// Returns a vector of derivation paths without the origin prefix. + /// + /// For wildcard keys this will return the path up to the wildcard, so you + /// can get full paths by appending one additional derivation step, according + /// to the wildcard type (hardened or normal). + pub fn derivation_paths(&self) -> Vec { + match &self { + DescriptorPublicKey::XPub(xpub) => { + vec![xpub.derivation_path.clone()] + } + DescriptorPublicKey::Single(_) => { + vec![bip32::DerivationPath::from(vec![])] + } + DescriptorPublicKey::MultiXPub(xpub) => xpub.derivation_paths.paths().clone(), + } + } + /// Whether or not the key has a wildcard pub fn has_wildcard(&self) -> bool { match *self { @@ -804,7 +868,16 @@ impl DescriptorPublicKey { } } - /// Whether or not the key has a wildcard + /// Return a Wildcard if key is a XKey + pub fn wildcard(&self) -> Option { + match *self { + DescriptorPublicKey::Single(..) => None, + DescriptorPublicKey::XPub(ref xpub) => Some(xpub.wildcard), + DescriptorPublicKey::MultiXPub(ref xpub) => Some(xpub.wildcard), + } + } + + /// Whether or not the key has a hardened step in path pub fn has_hardened_step(&self) -> bool { let paths = match self { DescriptorPublicKey::Single(..) => &[], @@ -925,8 +998,7 @@ impl FromStr for DescriptorSecretKey { .map_err(DescriptorKeyParseError::WifPrivateKey)?; Ok(DescriptorSecretKey::Single(SinglePriv { key: sk, origin })) } else { - let (xpriv, derivation_paths, wildcard) = - parse_xkey_deriv(parse_bip32_xpriv, key_part)?; + let (xpriv, derivation_paths, wildcard) = parse_xkey_deriv(key_part)?; if derivation_paths.len() > 1 { Ok(DescriptorSecretKey::MultiXPrv(DescriptorMultiXKey { origin, @@ -1009,18 +1081,13 @@ fn parse_key_origin(s: &str) -> Result<(&str, Option), Descrip } } -fn parse_bip32_xpub(xkey_str: &str) -> Result { - bip32::Xpub::from_str(xkey_str).map_err(DescriptorKeyParseError::Bip32Xpub) -} - -fn parse_bip32_xpriv(xkey_str: &str) -> Result { - bip32::Xpriv::from_str(xkey_str).map_err(DescriptorKeyParseError::Bip32Xpriv) -} - -fn parse_xkey_deriv( - parse_xkey_fn: impl Fn(&str) -> Result, +pub(crate) fn parse_xkey_deriv( key_deriv: &str, -) -> Result<(Key, Vec, Wildcard), DescriptorKeyParseError> { +) -> Result<(Key, Vec, Wildcard), DescriptorKeyParseError> +where + Key: FromStr, + E: Into, +{ let mut key_deriv = key_deriv.split('/'); let xkey_str = key_deriv .next() @@ -1028,7 +1095,8 @@ fn parse_xkey_deriv( MalformedKeyDataKind::NoKeyAfterOrigin, ))?; - let xkey = parse_xkey_fn(xkey_str)?; + let xkey = + Key::from_str(xkey_str).map_err(|e| DescriptorKeyParseError::XKeyParseError(e.into()))?; let mut wildcard = Wildcard::None; let mut multipath = false; @@ -1097,7 +1165,7 @@ fn parse_xkey_deriv( // step all the vectors of indexes contain a single element. If it did though, one of the // vectors contains more than one element. // Now transform this list of vectors of steps into distinct derivation paths. - .try_fold(Vec::new(), |mut paths, index_list| { + .try_fold(Vec::new(), |mut paths, index_list| -> Result<_, DescriptorKeyParseError> { let mut index_list = index_list?.into_iter(); let first_index = index_list .next() diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 68fd5e2dc..d0f367b1c 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -53,6 +53,7 @@ pub use self::tr::{ pub mod checksum; mod key; mod key_map; +mod wallet_policy; pub use self::key::{ DefiniteDescriptorKey, DerivPaths, DescriptorKeyParseError, DescriptorMultiXKey, @@ -60,6 +61,7 @@ pub use self::key::{ NonDefiniteKeyError, SinglePriv, SinglePub, SinglePubKey, Wildcard, XKeyNetwork, }; pub use self::key_map::KeyMap; +pub use self::wallet_policy::{WalletPolicy, WalletPolicyError}; /// Script descriptor #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/src/descriptor/wallet_policy/key_expression.rs b/src/descriptor/wallet_policy/key_expression.rs new file mode 100644 index 000000000..eac27480b --- /dev/null +++ b/src/descriptor/wallet_policy/key_expression.rs @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: CC0-1.0 + +use core::fmt::{self, Display, Write}; +use core::str::FromStr; + +use super::{DerivPaths, DescriptorKeyParseError, Wildcard}; +use crate::descriptor::key::{fmt_derivation_paths, parse_xkey_deriv}; +use crate::descriptor::WalletPolicyError; +use crate::{BTreeSet, MiniscriptKey, String}; + +const RECEIVE_CHANGE_SHORTHAND: &str = "**"; +const RECEIVE_CHANGE_PATH: &str = "<0;1>/*"; + +/// A key expression type based off of the description of KEY and KP in BIP-388. +/// Used as a `Pk` in `Descriptor` +#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct KeyExpression { + /// The numeric part of key index (KI) + pub index: KeyIndex, + /// The derivation paths of this key + pub derivation_paths: DerivPaths, + /// The wildcard value + pub wildcard: Wildcard, +} + +#[derive(Debug, Clone, Copy, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub struct KeyIndex(pub u32); + +impl KeyExpression { + pub fn is_disjoint(&self, other: &KeyExpression) -> bool { + let lhs: BTreeSet<_> = self + .derivation_paths + .paths() + .iter() + .flat_map(|p| p.into_iter().copied()) + .collect(); + + !other + .derivation_paths + .paths() + .iter() + .flat_map(|p| p.into_iter()) + .any(|cn| lhs.contains(cn)) + } +} + +impl TryFrom<&str> for KeyExpression { + type Error = DescriptorKeyParseError; + + fn try_from(s: &str) -> Result { + let path = match s.split_once('/') { + Some((_placeholder, path)) => path, + None => return Err(WalletPolicyError::KeyExpressionParseMustHaveDerivPath.into()), + }; + if path != RECEIVE_CHANGE_SHORTHAND && !valid_unhardened_derivation_path(path) { + return Err(WalletPolicyError::KeyExpressionParseInvalidDerivPath.into()); + } + let (ki, derivation_paths, wildcard) = + parse_xkey_deriv(&s.replace(RECEIVE_CHANGE_SHORTHAND, RECEIVE_CHANGE_PATH))?; + Ok(KeyExpression { + index: ki, + derivation_paths: DerivPaths::new(derivation_paths) + .ok_or(WalletPolicyError::KeyExpressionParseMustHaveDerivPath)?, + wildcard, + }) + } +} + +// Returns true if `path` is a string of the form //*, for two distinct +// decimal numbers NUM representing unhardened derivations +// NOTE: the prefix '/' should be stripped in the caller +fn valid_unhardened_derivation_path(path: &str) -> bool { + let (left, right) = match path.split_once(';') { + Some(pair) => pair, + None => return false, + }; + let left_num = match left.strip_prefix("<") { + Some(num) => num, + None => return false, + }; + let right_num = match right.strip_suffix(">/*") { + Some(num) => num, + None => return false, + }; + matches!( + (left_num.parse::(), right_num.parse::()), + (Ok(a), Ok(b)) if a < b + ) +} + +impl FromStr for KeyExpression { + type Err = DescriptorKeyParseError; + + fn from_str(s: &str) -> Result { s.try_into() } +} + +impl Display for KeyExpression { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.index.fmt(f)?; + let mut path = String::new(); + fmt_derivation_paths(&mut path, self.derivation_paths.paths())?; + write!(&mut path, "{}", self.wildcard)?; + write!(f, "{}", path.replace(RECEIVE_CHANGE_PATH, RECEIVE_CHANGE_SHORTHAND)) + } +} + +impl MiniscriptKey for KeyExpression { + type Sha256 = String; + type Hash256 = String; + type Ripemd160 = String; + type Hash160 = String; + + fn is_x_only_key(&self) -> bool { false } + fn num_der_paths(&self) -> usize { self.derivation_paths.paths().len() } +} + +impl FromStr for KeyIndex { + type Err = WalletPolicyError; + + fn from_str(s: &str) -> Result { + let mut chars = s.chars(); + match chars.next() { + Some('@') => { + let index_str = chars.take_while(char::is_ascii_digit).collect::(); + let index = index_str + .parse() + .map_err(|_| WalletPolicyError::KeyIndexParseInvalidIndex(index_str))?; + Ok(KeyIndex(index)) + } + Some(ch) => Err(WalletPolicyError::KeyIndexParseExpectedAtSign(ch)), + None => Err(WalletPolicyError::KeyIndexParseInvalidIndex(s.into())), + } + } +} + +impl Display for KeyIndex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "@{}", self.0) } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_test_disjoin_deriv_paths() { + assert!(!KeyExpression::from_str("@0/<0;1>/*") + .unwrap() + .is_disjoint(&KeyExpression::from_str("@0/<1;2>/*").unwrap())); + } +} diff --git a/src/descriptor/wallet_policy/mod.rs b/src/descriptor/wallet_policy/mod.rs new file mode 100644 index 000000000..7828b6014 --- /dev/null +++ b/src/descriptor/wallet_policy/mod.rs @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: CC0-1.0 + +use core::fmt::{self, Display}; +use core::str::FromStr; + +use super::key::XKeyParseError; +use super::{DerivPaths, DescriptorKeyParseError, Wildcard}; +use crate::{Descriptor, DescriptorPublicKey, String, Translator, Vec}; + +mod key_expression; + +use key_expression::{KeyExpression, KeyIndex}; + +/// A wallet policy as described in BIP-388 +/// +///```rust +/// use std::str::FromStr; +/// use miniscript::{Descriptor, DescriptorPublicKey}; +/// use miniscript::descriptor::WalletPolicy; +/// +/// // Convert from a `Descriptor`: +/// let desc_str = "pkh([6738736c/44'/0'/0']xpub6Br37sWxruYfT8ASpCjVHKGwgdnYFEn98DwiN76i2oyY6fgH1LAPmmDcF46xjxJr22gw4jmVjTE2E3URMnRPEPYyo1zoPSUba563ESMXCeb/<0;1>/*)"; +/// let descriptor = Descriptor::::from_str(desc_str).unwrap(); +/// let policy1: WalletPolicy = (&descriptor).try_into().unwrap(); +/// +/// // Convert from a Descriptor string: +/// let policy2 = WalletPolicy::from_str(desc_str).unwrap(); +/// assert_eq!(policy1, policy2); +/// +/// // Convert from/to a wallet policy template string: +/// let from_template = WalletPolicy::from_str("pkh(@0/**)").unwrap(); +/// assert_eq!(from_template.to_string(), "pkh(@0/**)"); +/// +/// // Cannot go back into descriptor if you created from template: +/// assert!(from_template.into_descriptor().is_err()); +/// +/// // Convert into a full descriptor: +/// assert_eq!(policy1.into_descriptor().unwrap(), descriptor); +///``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WalletPolicy { + /// Wallet descriptor template + template: Descriptor, + /// Vector of key information items + key_info: Vec, +} + +struct WalletPolicyTranslator { + key_info: Vec, +} + +impl Translator for WalletPolicyTranslator { + type TargetPk = DescriptorPublicKey; + type Error = WalletPolicyError; + + fn pk(&mut self, pk: &KeyExpression) -> Result { + let idx = pk.index.0 as usize; + self.key_info + .get(idx) + .cloned() + .ok_or(WalletPolicyError::KeyInfoInvalidKeyIndex(idx)) + } + + translate_hash_fail!(KeyExpression, DescriptorPublicKey, Self::Error); +} + +impl Translator for WalletPolicyTranslator { + type TargetPk = KeyExpression; + type Error = WalletPolicyError; + + fn pk(&mut self, pk: &DescriptorPublicKey) -> Result { + let ke = KeyExpression { + // FIXME: use a BTreeSet here? maybe doesn't really matter + index: KeyIndex(self.key_info.iter().position(|p| p == pk).unwrap() as u32), + derivation_paths: DerivPaths::new(pk.derivation_paths()) + .ok_or(WalletPolicyError::TranslatorEmptyDerivationPaths)?, + wildcard: pk + .wildcard() + .ok_or(WalletPolicyError::TranslatorMissingWildcard)?, + }; + Ok(ke) + } + + translate_hash_fail!(DescriptorPublicKey, KeyExpression, Self::Error); +} + +impl WalletPolicy { + /// Create a new `WalletPolicy` from a + /// `Descriptor`. Does not validate the underlying + /// template. + pub fn from_descriptor_unchecked( + descriptor: &Descriptor, + ) -> Result { + let mut translator = WalletPolicyTranslator { key_info: descriptor.iter_pk().collect() }; + Ok(WalletPolicy { + template: descriptor.translate_pk(&mut translator).map_err(|e| { + e.expect_translator_err("converting descriptor to wallet policy template") + })?, + key_info: translator.key_info, + }) + } + + /// Create a new `WalletPolicy` from a `Descriptor` and + /// validates the underyling template. + pub fn from_descriptor( + descriptor: &Descriptor, + ) -> Result { + WalletPolicy::from_descriptor_unchecked(descriptor).and_then(WalletPolicy::validate) + } + + /// Convert a `WalletPolicy` into a `Descriptor` using + /// the underlying template and key information. + pub fn into_descriptor(self) -> Result, WalletPolicyError> { + self.template + .translate_pk(&mut WalletPolicyTranslator { key_info: self.key_info }) + .map_err(|e| e.expect_translator_err("converting to full descriptor")) + } + + /// Sets the key information so that `WalletPolicy::into_descriptor` can be + /// called successfully. Errors when there are not enough keys for the template. + pub fn set_key_info(&mut self, keys: &[DescriptorPublicKey]) -> Result<(), WalletPolicyError> { + if keys.len() != self.template.iter_pk().count() { + return Err(WalletPolicyError::WalletPolicyInvalidKeyInfo); + } + self.key_info = keys.to_vec(); + Ok(()) + } + + /// Validates the wallet policy template. + #[must_use = "Wallet policy won't be considered valid until this is called"] + fn validate(self) -> Result { + // HACK: don't know how else to prevent the following invalid cases from + // the test vectors while still using the current Descriptor parsing: + // skipped or out of order placeholders, repeated placeholds, + // non-disjoin multipath expressions + let mut prev: Option = None; + for key in self.template.iter_pk() { + if let (Some(prev), curr) = (&prev, &key) { + if prev.index.0 > curr.index.0 || prev.index.0 != curr.index.0.saturating_sub(1) { + return Err(WalletPolicyError::TemplateValidationKeyIndexOutOfOrder); + } else if prev.index.0 == curr.index.0 && !prev.is_disjoint(curr) { + return Err(WalletPolicyError::TemplateValidationNonDisjointPaths); + } + } + prev = Some(key); + } + Ok(self) + } +} + +impl Display for WalletPolicy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:#}", self.template) } +} + +impl TryFrom<&Descriptor> for WalletPolicy { + type Error = WalletPolicyError; + + fn try_from(desc: &Descriptor) -> Result { + WalletPolicy::from_descriptor(desc) + } +} + +impl TryFrom<&str> for WalletPolicy { + type Error = WalletPolicyError; + + fn try_from(desc: &str) -> Result { + match Descriptor::::from_str(desc) { + Ok(template) => Ok(WalletPolicy { template, key_info: vec![] }.validate()?), + Err(err1) => match Descriptor::::from_str(desc) { + Ok(desc) => Ok(WalletPolicy::from_descriptor(&desc)?), + Err(err2) => Err(WalletPolicyError::WalletPolicyParseFromString(format!( + "Couldn't parse from descriptor [{err1}], or wallet policy template: [{err2}]" + ))), + }, + } + } +} + +impl FromStr for WalletPolicy { + type Err = WalletPolicyError; + fn from_str(s: &str) -> Result { s.try_into() } +} + +/// WalletPolicy errors +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum WalletPolicyError { + /// A derivation path must be present when parsing a KeyExpression + KeyExpressionParseMustHaveDerivPath, + /// The derivation path for a KeyExpression is invalid + KeyExpressionParseInvalidDerivPath, + /// The KeyIndex is missing an '@' sign + KeyIndexParseExpectedAtSign(char), + /// The KeyIndex is not a valid unsigned integer + KeyIndexParseInvalidIndex(String), + /// The key info is not found for the given index + KeyInfoInvalidKeyIndex(usize), + /// The key indexes in the template are out of order + TemplateValidationKeyIndexOutOfOrder, + /// The key indexes in the template are the same but the paths are non-disjoint + TemplateValidationNonDisjointPaths, + /// There must be at least one derivation path for a xpub + TranslatorEmptyDerivationPaths, + /// Missing wildcard on xpub + TranslatorMissingWildcard, + /// Couldn't parse wallet policy from string + WalletPolicyParseFromString(String), + /// Couldn't set key info on WalletPolicy + WalletPolicyInvalidKeyInfo, +} + +impl From for DescriptorKeyParseError { + fn from(err: WalletPolicyError) -> Self { + DescriptorKeyParseError::XKeyParseError(XKeyParseError::Bip388(err)) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for WalletPolicyError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } +} + +impl Display for WalletPolicyError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + WalletPolicyError::KeyExpressionParseMustHaveDerivPath => { + write!(f, "Key expression placeholder must have a derivation path after it") + } + WalletPolicyError::KeyExpressionParseInvalidDerivPath => { + write!( + f, + "Key expression placeholder must be of the format \"/**\" or \"//*\"" + ) + } + WalletPolicyError::KeyIndexParseInvalidIndex(index_str) => { + write!(f, "Couldn't parse index, got {index_str}") + } + WalletPolicyError::KeyIndexParseExpectedAtSign(ch) => { + write!(f, "Expected KeyIndex '@' sign, got {ch}") + } + WalletPolicyError::KeyInfoInvalidKeyIndex(idx) => { + write!(f, "Invalid index [{idx}] into key info for wallet policy") + } + WalletPolicyError::TemplateValidationKeyIndexOutOfOrder => { + write!(f, "The template has indexes that are out of order") + } + WalletPolicyError::TemplateValidationNonDisjointPaths => { + write!(f, "The template has identical indexes but the paths are non-disjoint") + } + WalletPolicyError::TranslatorEmptyDerivationPaths => { + write!(f, "Expected derivation paths when translating into KeyExpression") + } + WalletPolicyError::TranslatorMissingWildcard => { + write!(f, "Missing wildcard. Not an xpub?") + } + WalletPolicyError::WalletPolicyParseFromString(msg) => msg.fmt(f), + WalletPolicyError::WalletPolicyInvalidKeyInfo => { + write!(f, "Invalid key information for WalletPolicy template") + } + } + } +} + +impl From for XKeyParseError { + fn from(err: WalletPolicyError) -> Self { XKeyParseError::Bip388(err) } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + use crate::Descriptor; + + const VALID_TEMPLATES: &[(&str, &str)] = &[ + ( + "pkh(@0/**)", + "pkh([6738736c/44'/0'/0']xpub6Br37sWxruYfT8ASpCjVHKGwgdnYFEn98DwiN76i2oyY6fgH1LAPmmDcF46xjxJr22gw4jmVjTE2E3URMnRPEPYyo1zoPSUba563ESMXCeb/<0;1>/*)" + ), + ( + "sh(wpkh(@0/**))", + "sh(wpkh([6738736c/49'/0'/1']xpub6Bex1CHWGXNNwGVKHLqNC7kcV348FxkCxpZXyCWp1k27kin8sRPayjZUKDjyQeZzGUdyeAj2emoW5zStFFUAHRgd5w8iVVbLgZ7PmjAKAm9/<0;1>/*))" + ), + ( + "wpkh(@0/**)", + "wpkh([6738736c/84'/0'/2']xpub6CRQzb8u9dmMcq5XAwwRn9gcoYCjndJkhKgD11WKzbVGd932UmrExWFxCAvRnDN3ez6ZujLmMvmLBaSWdfWVn75L83Qxu1qSX4fJNrJg2Gt/<0;1>/*)" + ), + ( + "tr(@0/**)", + "tr([6738736c/86'/0'/0']xpub6CryUDWPS28eR2cDyojB8G354izmx294BdjeSvH469Ty3o2E6Tq5VjBJCn8rWBgesvTJnyXNAJ3QpLFGuNwqFXNt3gn612raffLWfdHNkYL/<0;1>/*)" + ), + ( + "wsh(sortedmulti(2,@0/**,@1/**))", + "wsh(sortedmulti(2,[6738736c/48'/0'/0'/2']xpub6FC1fXFP1GXLX5TKtcjHGT4q89SDRehkQLtbKJ2PzWcvbBHtyDsJPLtpLtkGqYNYZdVVAjRQ5kug9CsapegmmeRutpP7PW4u4wVF9JfkDhw/<0;1>/*,[b2b1f0cf/48'/0'/0'/2']xpub6EWhjpPa6FqrcaPBuGBZRJVjzGJ1ZsMygRF26RwN932Vfkn1gyCiTbECVitBjRCkexEvetLdiqzTcYimmzYxyR1BZ79KNevgt61PDcukmC7/<0;1>/*))" + ), + ( + "wsh(thresh(3,pk(@0/**),s:pk(@1/**),s:pk(@2/**),sln:older(12960)))", + "wsh(thresh(3,pk([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa/<0;1>/*),s:pk([b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js/<0;1>/*),s:pk([a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2/<0;1>/*),sln:older(12960)))" + ), + ( + "wsh(or_d(pk(@0/**),and_v(v:multi(2,@1/**,@2/**,@3/**),older(65535))))", + "wsh(or_d(pk([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa/<0;1>/*),and_v(v:multi(2,[b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js/<0;1>/*,[a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2/<0;1>/*,[bb641298/44'/0'/0'/100']xpub6Dz8PHFmXkYkykQ83ySkruky567XtJb9N69uXScJZqweYiQn6FyieajdiyjCvWzRZ2GoLHMRE1cwDfuJZ6461YvNRGVBJNnLA35cZrQKSRJ/<0;1>/*),older(65535))))" + ), + ( + "sh(multi(1,@0/**,@0/<2;3>/*))", + "sh(multi(1,xpub6Bex1CHWGXNNwGVKHLqNC7kcV348FxkCxpZXyCWp1k27kin8sRPayjZUKDjyQeZzGUdyeAj2emoW5zStFFUAHRgd5w8iVVbLgZ7PmjAKAm9/<0;1>/*,xpub6Bex1CHWGXNNwGVKHLqNC7kcV348FxkCxpZXyCWp1k27kin8sRPayjZUKDjyQeZzGUdyeAj2emoW5zStFFUAHRgd5w8iVVbLgZ7PmjAKAm9/<2;3>/*))" + ), + // TODO: uncomment if BIP-390 is ever supported + // ( + // "tr(@0/**,{sortedmulti_a(1,@0/<2;3>/*,@1/**),or_b(pk(@2/**),s:pk(@3/**))})", + // "tr([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa/<0;1>/*,{sortedmulti_a(1,[6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa/<2;3>/*,xpub6Fc2TRaCWNgfT49nRGG2G78d1dPnjhW66gEXi7oYZML7qEFN8e21b2DLDipTZZnfV6V7ivrMkvh4VbnHY2ChHTS9qM3XVLJiAgcfagYQk6K/<0;1>/*),or_b(pk(xpub6GxHB9kRdFfTqYka8tgtX9Gh3Td3A9XS8uakUGVcJ9NGZ1uLrGZrRVr67DjpMNCHprZmVmceFTY4X4wWfksy8nVwPiNvzJ5pjLxzPtpnfEM/<0;1>/*),s:pk(xpub6GjFUVVYewLj5no5uoNKCWuyWhQ1rKGvV8DgXBG9Uc6DvAKxt2dhrj1EZFrTNB5qxAoBkVW3wF8uCS3q1ri9fueAa6y7heFTcf27Q4gyeh6/<0;1>/*))})" + // ), + // ( + // "tr(musig(@0,@1,@2)/**,{and_v(v:pk(musig(@0,@1)/**),older(12960)),{and_v(v:pk(musig(@0,@2)/**),older(12960)),and_v(v:pk(musig(@1,@2)/**),older(12960))}})", + // "tr(musig([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa,[b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js,[a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2)/<0;1>/*,{and_v(v:pk(musig([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa,[b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js)/<0;1>/*),older(12960)),{and_v(v:pk(musig([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa,[a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2)/<0;1>/*),older(12960)),and_v(v:pk(musig([b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js,[a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2)/<0;1>/*),older(12960))}})" + // ), + ]; + + const INVALID_TEMPLATES: &[&str] = &[ + // Key placeholder with no path following it + "pkh(@0)", + + // Key placeholder with an explicit path present + "pkh(@0/0/**)", + + // Key placeholders out of order + "sh(multi(1,@1/**,@0/**))", + + // Skipped key placeholder @1 + "sh(multi(1,@0/**,@2/**))", + + // Repeated keys with the same path expression + "sh(multi(1,@0/**,@0/**))", + + // Non-disjoint multipath expressions (@0/1/* appears twice) + "sh(multi(1,@0/<0;1>/*,@0/<1;2>/*))", + + // Expression with a non-KP key present + "sh(multi(1,@0/**,xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/<0;1>/*))", + + // Allowed cardinality > 2 + "pkh(@0/<0;1;2>/*)", + + // Derivation before aggregation is not allowed in wallet policies (despite + // being allowed in BIP-390) + // TODO: uncomment if BIP-390 is ever supported + // "tr(musig(@0/**,@1/**))", +]; + + #[test] + fn can_parse_valid_wallet_policy_templates() { + for (t, desc) in VALID_TEMPLATES { + let descriptor = Descriptor::::from_str(desc).unwrap(); + let policy = WalletPolicy::from_str(desc).expect("invalid descriptor"); + let template = WalletPolicy::from_str(t).expect("invalid template"); + assert_eq!(format!("{:#}", template.template), *t); + assert_eq!(policy.into_descriptor().unwrap(), descriptor); + } + } + + #[test] + fn can_error_on_invalid_wallet_policy_templates() { + for t in INVALID_TEMPLATES { + assert!(WalletPolicy::from_str(t).is_err()); + } + } + + #[test] + fn can_set_key_info() { + let mut template_only = + WalletPolicy::from_str("wsh(sortedmulti(2,@0/**,@1/**))").expect("invalid template"); + assert!(template_only.clone().into_descriptor().is_err()); + let keys = ["[6738736c/48'/0'/0'/2']xpub6FC1fXFP1GXLX5TKtcjHGT4q89SDRehkQLtbKJ2PzWcvbBHtyDsJPLtpLtkGqYNYZdVVAjRQ5kug9CsapegmmeRutpP7PW4u4wVF9JfkDhw", "[b2b1f0cf/48'/0'/0'/2']xpub6EWhjpPa6FqrcaPBuGBZRJVjzGJ1ZsMygRF26RwN932Vfkn1gyCiTbECVitBjRCkexEvetLdiqzTcYimmzYxyR1BZ79KNevgt61PDcukmC7"] + .into_iter() + .map(FromStr::from_str) + .collect::, _>>() + .unwrap(); + template_only.set_key_info(&keys).unwrap(); + assert!(template_only.clone().into_descriptor().is_ok()); + } +}