From 9b866300be53be8a4e49e7913be25ff8887eac63 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 16:58:50 +0100 Subject: [PATCH] descriptors: merge the semantic analysis in one place This merges the Miniscript policy semantic analysis we perform both when parsing a descriptor and when gathering information about a Liana descriptor in one, right, place: the analysis submodule. --- src/descriptors/analysis.rs | 106 ++++++++++++++++++++++++++++++++++-- src/descriptors/keys.rs | 27 --------- src/descriptors/mod.rs | 102 +++------------------------------- 3 files changed, 109 insertions(+), 126 deletions(-) diff --git a/src/descriptors/analysis.rs b/src/descriptors/analysis.rs index 5a725f69..a4a47c49 100644 --- a/src/descriptors/analysis.rs +++ b/src/descriptors/analysis.rs @@ -1,4 +1,8 @@ -use miniscript::{bitcoin::util::bip32, descriptor, policy::Semantic as SemanticPolicy}; +use miniscript::{ + bitcoin::util::bip32, + descriptor, + policy::{Liftable, Semantic as SemanticPolicy}, +}; use std::{ collections::{HashMap, HashSet}, @@ -18,6 +22,33 @@ pub fn is_single_key_or_multisig(policy: &SemanticPolicy bool { + match *key { + descriptor::DescriptorPublicKey::Single(..) | descriptor::DescriptorPublicKey::XPub(..) => { + false + } + descriptor::DescriptorPublicKey::MultiXPub(ref xpub) => { + let der_paths = xpub.derivation_paths.paths(); + // Rust-miniscript enforces BIP389 which states that all paths must have the same len. + let len = der_paths.get(0).expect("Cannot be empty").len(); + // Technically the xpub could be for the master xpub and not have an origin. But it's + // no unlikely (and easily fixable) while users shooting themselves in the foot by + // forgetting to provide the origin is so likely that it's worth ruling out xpubs + // without origin entirely. + xpub.origin.is_some() + && xpub.wildcard == descriptor::Wildcard::Unhardened + && der_paths.len() == 2 + && der_paths[0][len - 1] == 0.into() + && der_paths[1][len - 1] == 1.into() + } + } +} + // We require the locktime to: // - not be disabled // - be in number of blocks @@ -200,7 +231,7 @@ impl PathInfo { } } -/// A Liana spending policy. +/// A Liana spending policy. Can be inferred from a Miniscript semantic policy. #[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] pub struct LianaPolicy { pub(super) primary_path: PathInfo, @@ -208,11 +239,76 @@ pub struct LianaPolicy { } impl LianaPolicy { - pub(super) fn new(primary_path: PathInfo, recovery_path: (u16, PathInfo)) -> LianaPolicy { - LianaPolicy { + /// Create a Liana policy from a descriptor. This will check the descriptor is correctly formed + /// (P2WSH, multipath, ..) and has a valid Liana semantic. + pub fn from_multipath_descriptor( + desc: &descriptor::Descriptor, + ) -> Result { + // For now we only allow P2WSH descriptors. + let wsh_desc = match &desc { + descriptor::Descriptor::Wsh(desc) => desc, + _ => return Err(LianaDescError::IncompatibleDesc), + }; + + // Get the Miniscript from the descriptor and make sure it only contains valid multipath + // descriptor keys. + let ms = match wsh_desc.as_inner() { + descriptor::WshInner::Ms(ms) => ms, + _ => return Err(LianaDescError::IncompatibleDesc), + }; + let invalid_key = ms.iter_pk().find_map(|pk| { + if is_valid_desc_key(&pk) { + None + } else { + Some(pk) + } + }); + if let Some(key) = invalid_key { + return Err(LianaDescError::InvalidKey(key.into())); + } + + // Now lift a semantic policy out of this Miniscript and normalize it to make sure we + // compare apples to apples below. + let policy = ms + .lift() + .expect("Lifting can't fail on a Miniscript") + .normalized(); + + // For now we only accept a single timelocked recovery path. + let subs = match policy { + SemanticPolicy::Threshold(1, subs) => Some(subs), + _ => None, + } + .ok_or(LianaDescError::IncompatibleDesc)?; + if subs.len() != 2 { + return Err(LianaDescError::IncompatibleDesc); + } + + // Fetch the two spending paths' semantic policies. The primary path is identified as the + // only one that isn't timelocked. + let (prim_path_sub, reco_path_sub) = + subs.into_iter() + .fold((None, None), |(mut prim_sub, mut reco_sub), sub| { + if is_single_key_or_multisig(&sub) { + prim_sub = Some(sub); + } else { + reco_sub = Some(sub); + } + (prim_sub, reco_sub) + }); + let (prim_path_sub, reco_path_sub) = ( + prim_path_sub.ok_or(LianaDescError::IncompatibleDesc)?, + reco_path_sub.ok_or(LianaDescError::IncompatibleDesc)?, + ); + + // Now parse information about each spending path. + let primary_path = PathInfo::from_primary_path(prim_path_sub)?; + let recovery_path = PathInfo::from_recovery_path(reco_path_sub)?; + + Ok(LianaPolicy { primary_path, recovery_path, - } + }) } pub fn primary_path(&self) -> &PathInfo { diff --git a/src/descriptors/keys.rs b/src/descriptors/keys.rs index 0cdc8f1f..cf5c5538 100644 --- a/src/descriptors/keys.rs +++ b/src/descriptors/keys.rs @@ -140,33 +140,6 @@ impl ToPublicKey for DerivedPublicKey { } } -/// We require the descriptor key to: -/// - Be deriveable (to contain a wildcard) -/// - Be multipath (to contain a step in the derivation path with multiple indexes) -/// - The multipath step to only contain two indexes, 0 and 1. -/// - Be 'signable' by an external signer (to contain an origin) -pub fn is_valid_desc_key(key: &descriptor::DescriptorPublicKey) -> bool { - match *key { - descriptor::DescriptorPublicKey::Single(..) | descriptor::DescriptorPublicKey::XPub(..) => { - false - } - descriptor::DescriptorPublicKey::MultiXPub(ref xpub) => { - let der_paths = xpub.derivation_paths.paths(); - // Rust-miniscript enforces BIP389 which states that all paths must have the same len. - let len = der_paths.get(0).expect("Cannot be empty").len(); - // Technically the xpub could be for the master xpub and not have an origin. But it's - // no unlikely (and easily fixable) while users shooting themselves in the foot by - // forgetting to provide the origin is so likely that it's worth ruling out xpubs - // without origin entirely. - xpub.origin.is_some() - && xpub.wildcard == descriptor::Wildcard::Unhardened - && der_paths.len() == 2 - && der_paths[0][len - 1] == 0.into() - && der_paths[1][len - 1] == 1.into() - } - } -} - /// The keys in one of the two spending paths of a Liana descriptor. /// May either be a single key, or between 2 and 20 keys along with a threshold (between two and /// the number of keys). diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index e14494be..e9f6c3ca 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -10,7 +10,6 @@ use miniscript::{ }, descriptor, miniscript::{decode::Terminal, Miniscript}, - policy::{Liftable, Semantic as SemanticPolicy}, translate_hash_clone, ForEachKey, ScriptContext, TranslatePk, Translator, }; @@ -105,58 +104,17 @@ impl str::FromStr for MultipathDescriptor { type Err = LianaDescError; fn from_str(s: &str) -> Result { - let wsh_desc = descriptor::Wsh::::from_str(s) + // Parse a descriptor and check it is a multipath descriptor corresponding to a valid Liana + // spending policy. + let desc = descriptor::Descriptor::::from_str(s) .map_err(LianaDescError::Miniscript)?; - let ms = match wsh_desc.as_inner() { - descriptor::WshInner::Ms(ms) => ms, - _ => return Err(LianaDescError::IncompatibleDesc), - }; - let invalid_key = ms.iter_pk().find_map(|pk| { - if is_valid_desc_key(&pk) { - None - } else { - Some(pk) - } - }); - if let Some(key) = invalid_key { - return Err(LianaDescError::InvalidKey(key.into())); - } - - // Semantic of the Miniscript must be either the owner now, or the heir after - // a timelock. - let policy = ms - .lift() - .expect("Lifting can't fail on a Miniscript") - .normalized(); - let subs = match policy { - SemanticPolicy::Threshold(1, subs) => Some(subs), - _ => None, - } - .ok_or(LianaDescError::IncompatibleDesc)?; - if subs.len() != 2 { - return Err(LianaDescError::IncompatibleDesc); - } - - // Must always contain a non-timelocked primary spending path and a timelocked recovery - // path. The PathInfo constructors perform the checks that each path is well formed. - let mut primary_path_seen = false; - for sub in subs { - if !primary_path_seen && is_single_key_or_multisig(&sub) { - PathInfo::from_primary_path(sub)?; - primary_path_seen = true; - } else { - PathInfo::from_recovery_path(sub)?; - } - } - - // All good, construct the multipath descriptor. - let multi_desc = descriptor::Descriptor::Wsh(wsh_desc); + LianaPolicy::from_multipath_descriptor(&desc)?; // Compute the receive and change "sub" descriptors right away. According to our pubkey // check above, there must be only two of those, 0 and 1. // We use /0/* for receiving and /1/* for change. // FIXME: don't rely on into_single_descs()'s ordering. - let mut singlepath_descs = multi_desc + let mut singlepath_descs = desc .clone() .into_single_descriptors() .expect("Can't error, all paths have the same length") @@ -166,7 +124,7 @@ impl str::FromStr for MultipathDescriptor { let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2")); Ok(MultipathDescriptor { - multi_desc, + multi_desc: desc, receive_desc, change_desc, }) @@ -293,52 +251,8 @@ impl MultipathDescriptor { /// Get the spending policy of this descriptor. pub fn policy(&self) -> LianaPolicy { - // Get the Miniscript - let wsh_desc = match &self.multi_desc { - descriptor::Descriptor::Wsh(desc) => desc, - _ => unreachable!(), - }; - let ms = match wsh_desc.as_inner() { - descriptor::WshInner::Ms(ms) => ms, - _ => unreachable!(), - }; - - // Lift the semantic policy from the Miniscript - let policy = ms - .lift() - .expect("Lifting can't fail on a Miniscript") - .normalized(); - let subs = match policy { - SemanticPolicy::Threshold(1, subs) => subs, - _ => unreachable!("The policy is always 'one of the primary or the recovery path'"), - }; - // For now we only ever allow a single recovery path. - assert_eq!(subs.len(), 2); - - // Fetch the two spending paths' semantic policies. The primary path is identified as the - // only one that isn't timelocked. - let (prim_path_sub, reco_path_sub) = - subs.into_iter() - .fold((None, None), |(mut prim_sub, mut reco_sub), sub| { - if is_single_key_or_multisig(&sub) { - prim_sub = Some(sub); - } else { - reco_sub = Some(sub); - } - (prim_sub, reco_sub) - }); - let (prim_path_sub, reco_path_sub) = ( - prim_path_sub.expect("Must be present"), - reco_path_sub.expect("Must be present"), - ); - - // Now parse information about each spending path. - let primary_path = PathInfo::from_primary_path(prim_path_sub) - .expect("Must always be a set of keys without timelock"); - let reco_path = PathInfo::from_recovery_path(reco_path_sub) - .expect("The recovery path policy must always be a timelock along with a set of keys."); - - LianaPolicy::new(primary_path, reco_path) + LianaPolicy::from_multipath_descriptor(&self.multi_desc) + .expect("We never create a Liana descriptor with an invalid Liana policy.") } /// Get the value (in blocks) of the relative timelock for the heir's spending path.