From 9e78ac7e8dd8bfb7170f64aa314aea30921aca4b Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 14:24:37 +0100 Subject: [PATCH 01/12] descriptors: make the module a folder. --- src/{descriptors.rs => descriptors/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{descriptors.rs => descriptors/mod.rs} (100%) diff --git a/src/descriptors.rs b/src/descriptors/mod.rs similarity index 100% rename from src/descriptors.rs rename to src/descriptors/mod.rs From 7772ae8d8a74dc0f52be0381a77e68e4d2e8478f Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 14:35:15 +0100 Subject: [PATCH 02/12] descriptors: move derived keys into their own submodule --- src/descriptors/keys.rs | 137 ++++++++++++++++++++++++++++++++++++++++ src/descriptors/mod.rs | 125 ++---------------------------------- 2 files changed, 144 insertions(+), 118 deletions(-) create mode 100644 src/descriptors/keys.rs diff --git a/src/descriptors/keys.rs b/src/descriptors/keys.rs new file mode 100644 index 00000000..29e1d13e --- /dev/null +++ b/src/descriptors/keys.rs @@ -0,0 +1,137 @@ +use miniscript::{ + bitcoin::{ + self, + hashes::{hash160, ripemd160, sha256}, + util::bip32, + }, + hash256, MiniscriptKey, ToPublicKey, +}; + +use std::{error, fmt, str}; + +#[derive(Debug)] +pub enum DescKeyError { + DerivedKeyParsing, +} + +impl std::fmt::Display for DescKeyError { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + match self { + DescKeyError::DerivedKeyParsing => write!(f, "Parsing derived key"), + } + } +} + +impl error::Error for DescKeyError {} + +/// A public key used in derived descriptors +#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] +pub struct DerivedPublicKey { + /// Fingerprint of the master xpub and the derivation index used. We don't use a path + /// since we never derive at more than one depth. + pub origin: (bip32::Fingerprint, bip32::DerivationPath), + /// The actual key + pub key: bitcoin::PublicKey, +} + +impl fmt::Display for DerivedPublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (fingerprint, deriv_path) = &self.origin; + + write!(f, "[")?; + for byte in fingerprint.as_bytes().iter() { + write!(f, "{:02x}", byte)?; + } + write!(f, "/{}", deriv_path)?; + write!(f, "]{}", self.key) + } +} + +impl str::FromStr for DerivedPublicKey { + type Err = DescKeyError; + + fn from_str(s: &str) -> Result { + // The key is always of the form: + // [ fingerprint / index ] + + // 1 + 8 + 1 + 1 + 1 + 66 minimum + if s.len() < 78 { + return Err(DescKeyError::DerivedKeyParsing); + } + + // Non-ASCII? + for ch in s.as_bytes() { + if *ch < 20 || *ch > 127 { + return Err(DescKeyError::DerivedKeyParsing); + } + } + + if s.chars().next().expect("Size checked above") != '[' { + return Err(DescKeyError::DerivedKeyParsing); + } + + let mut parts = s[1..].split(']'); + let fg_deriv = parts.next().ok_or(DescKeyError::DerivedKeyParsing)?; + let key_str = parts.next().ok_or(DescKeyError::DerivedKeyParsing)?; + + if fg_deriv.len() < 10 { + return Err(DescKeyError::DerivedKeyParsing); + } + let fingerprint = bip32::Fingerprint::from_str(&fg_deriv[..8]) + .map_err(|_| DescKeyError::DerivedKeyParsing)?; + let deriv_path = bip32::DerivationPath::from_str(&fg_deriv[9..]) + .map_err(|_| DescKeyError::DerivedKeyParsing)?; + if deriv_path.into_iter().any(bip32::ChildNumber::is_hardened) { + return Err(DescKeyError::DerivedKeyParsing); + } + + let key = + bitcoin::PublicKey::from_str(key_str).map_err(|_| DescKeyError::DerivedKeyParsing)?; + + Ok(DerivedPublicKey { + key, + origin: (fingerprint, deriv_path), + }) + } +} + +impl MiniscriptKey for DerivedPublicKey { + type Sha256 = sha256::Hash; + type Hash256 = hash256::Hash; + type Ripemd160 = ripemd160::Hash; + type Hash160 = hash160::Hash; + + fn is_uncompressed(&self) -> bool { + self.key.is_uncompressed() + } + + fn is_x_only_key(&self) -> bool { + false + } + + fn num_der_paths(&self) -> usize { + 0 + } +} + +impl ToPublicKey for DerivedPublicKey { + fn to_public_key(&self) -> bitcoin::PublicKey { + self.key + } + + fn to_sha256(hash: &sha256::Hash) -> sha256::Hash { + *hash + } + + fn to_hash256(hash: &hash256::Hash) -> hash256::Hash { + *hash + } + + fn to_ripemd160(hash: &ripemd160::Hash) -> ripemd160::Hash { + *hash + } + + fn to_hash160(hash: &hash160::Hash) -> hash160::Hash { + *hash + } +} diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index c0d534fa..0981b2a6 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -2,18 +2,16 @@ use miniscript::{ bitcoin::{ self, blockdata::transaction::Sequence, - hashes::{hash160, ripemd160, sha256}, secp256k1, util::{ bip32, psbt::{Input as PsbtIn, Psbt}, }, }, - descriptor, hash256, + descriptor, miniscript::{decode::Terminal, Miniscript}, policy::{Liftable, Semantic as SemanticPolicy}, - translate_hash_clone, ForEachKey, MiniscriptKey, ScriptContext, ToPublicKey, TranslatePk, - Translator, + translate_hash_clone, ForEachKey, ScriptContext, TranslatePk, Translator, }; use std::{ @@ -24,6 +22,9 @@ use std::{ use serde::{Deserialize, Serialize}; +pub mod keys; +pub use keys::*; + const WITNESS_FACTOR: usize = 4; // Convert a size in weight units to a size in virtual bytes, rounding up. @@ -40,7 +41,7 @@ pub enum LianaDescError { DuplicateKey(Box), Miniscript(miniscript::Error), IncompatibleDesc, - DerivedKeyParsing, + DescKey(DescKeyError), InvalidMultiThresh(usize), InvalidMultiKeys(usize), /// Different number of PSBT vs tx inputs, etc.. @@ -67,7 +68,7 @@ impl std::fmt::Display for LianaDescError { } Self::Miniscript(e) => write!(f, "Miniscript error: '{}'.", e), Self::IncompatibleDesc => write!(f, "Descriptor is not compatible."), - Self::DerivedKeyParsing => write!(f, "Parsing derived key,"), + Self::DescKey(e) => write!(f, "{}", e), Self::InvalidMultiThresh(thresh) => write!(f, "Invalid threshold value '{}'. The threshold must be > to 0 and <= to the number of keys.", thresh), Self::InvalidMultiKeys(n_keys) => write!(f, "Invalid number of keys '{}'. Between 2 and 20 keys must be given to use multiple keys in a specific path.", n_keys), Self::InsanePsbt => write!(f, "Analyzed PSBT is empty or malformed."), @@ -78,118 +79,6 @@ impl std::fmt::Display for LianaDescError { impl error::Error for LianaDescError {} -/// A public key used in derived descriptors -#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] -pub struct DerivedPublicKey { - /// Fingerprint of the master xpub and the derivation index used. We don't use a path - /// since we never derive at more than one depth. - pub origin: (bip32::Fingerprint, bip32::DerivationPath), - /// The actual key - pub key: bitcoin::PublicKey, -} - -impl fmt::Display for DerivedPublicKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let (fingerprint, deriv_path) = &self.origin; - - write!(f, "[")?; - for byte in fingerprint.as_bytes().iter() { - write!(f, "{:02x}", byte)?; - } - write!(f, "/{}", deriv_path)?; - write!(f, "]{}", self.key) - } -} - -impl str::FromStr for DerivedPublicKey { - type Err = LianaDescError; - - fn from_str(s: &str) -> Result { - // The key is always of the form: - // [ fingerprint / index ] - - // 1 + 8 + 1 + 1 + 1 + 66 minimum - if s.len() < 78 { - return Err(LianaDescError::DerivedKeyParsing); - } - - // Non-ASCII? - for ch in s.as_bytes() { - if *ch < 20 || *ch > 127 { - return Err(LianaDescError::DerivedKeyParsing); - } - } - - if s.chars().next().expect("Size checked above") != '[' { - return Err(LianaDescError::DerivedKeyParsing); - } - - let mut parts = s[1..].split(']'); - let fg_deriv = parts.next().ok_or(LianaDescError::DerivedKeyParsing)?; - let key_str = parts.next().ok_or(LianaDescError::DerivedKeyParsing)?; - - if fg_deriv.len() < 10 { - return Err(LianaDescError::DerivedKeyParsing); - } - let fingerprint = bip32::Fingerprint::from_str(&fg_deriv[..8]) - .map_err(|_| LianaDescError::DerivedKeyParsing)?; - let deriv_path = bip32::DerivationPath::from_str(&fg_deriv[9..]) - .map_err(|_| LianaDescError::DerivedKeyParsing)?; - if deriv_path.into_iter().any(bip32::ChildNumber::is_hardened) { - return Err(LianaDescError::DerivedKeyParsing); - } - - let key = - bitcoin::PublicKey::from_str(key_str).map_err(|_| LianaDescError::DerivedKeyParsing)?; - - Ok(DerivedPublicKey { - key, - origin: (fingerprint, deriv_path), - }) - } -} - -impl MiniscriptKey for DerivedPublicKey { - type Sha256 = sha256::Hash; - type Hash256 = hash256::Hash; - type Ripemd160 = ripemd160::Hash; - type Hash160 = hash160::Hash; - - fn is_uncompressed(&self) -> bool { - self.key.is_uncompressed() - } - - fn is_x_only_key(&self) -> bool { - false - } - - fn num_der_paths(&self) -> usize { - 0 - } -} - -impl ToPublicKey for DerivedPublicKey { - fn to_public_key(&self) -> bitcoin::PublicKey { - self.key - } - - fn to_sha256(hash: &sha256::Hash) -> sha256::Hash { - *hash - } - - fn to_hash256(hash: &hash256::Hash) -> hash256::Hash { - *hash - } - - fn to_ripemd160(hash: &ripemd160::Hash) -> ripemd160::Hash { - *hash - } - - fn to_hash160(hash: &hash160::Hash) -> hash160::Hash { - *hash - } -} - // We require the locktime to: // - not be disabled // - be in number of blocks From c0dd63dfb2b6831666fb260581dce36e6f7601fa Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 14:40:45 +0100 Subject: [PATCH 03/12] descriptors: move the LianaDescKey to the keys submodules --- src/descriptors/keys.rs | 96 ++++++++++++++++++++++++++++++++++++++++- src/descriptors/mod.rs | 96 +---------------------------------------- 2 files changed, 96 insertions(+), 96 deletions(-) diff --git a/src/descriptors/keys.rs b/src/descriptors/keys.rs index 29e1d13e..0cdc8f1f 100644 --- a/src/descriptors/keys.rs +++ b/src/descriptors/keys.rs @@ -4,20 +4,24 @@ use miniscript::{ hashes::{hash160, ripemd160, sha256}, util::bip32, }, - hash256, MiniscriptKey, ToPublicKey, + descriptor, hash256, Miniscript, MiniscriptKey, Terminal, ToPublicKey, }; -use std::{error, fmt, str}; +use std::{error, fmt, str, sync}; #[derive(Debug)] pub enum DescKeyError { DerivedKeyParsing, + InvalidMultiThresh(usize), + InvalidMultiKeys(usize), } impl std::fmt::Display for DescKeyError { fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { match self { DescKeyError::DerivedKeyParsing => write!(f, "Parsing derived key"), + Self::InvalidMultiThresh(thresh) => write!(f, "Invalid threshold value '{}'. The threshold must be > to 0 and <= to the number of keys.", thresh), + Self::InvalidMultiKeys(n_keys) => write!(f, "Invalid number of keys '{}'. Between 2 and 20 keys must be given to use multiple keys in a specific path.", n_keys), } } } @@ -135,3 +139,91 @@ impl ToPublicKey for DerivedPublicKey { *hash } } + +/// 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). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LianaDescKeys { + thresh: Option, + keys: Vec, +} + +impl LianaDescKeys { + pub fn from_single(key: descriptor::DescriptorPublicKey) -> LianaDescKeys { + LianaDescKeys { + thresh: None, + keys: vec![key], + } + } + + pub fn from_multi( + thresh: usize, + keys: Vec, + ) -> Result { + if keys.len() < 2 || keys.len() > 20 { + return Err(DescKeyError::InvalidMultiKeys(keys.len())); + } + if thresh == 0 || thresh > keys.len() { + return Err(DescKeyError::InvalidMultiThresh(thresh)); + } + Ok(LianaDescKeys { + thresh: Some(thresh), + keys, + }) + } + + pub fn keys(&self) -> &Vec { + &self.keys + } + + pub fn into_miniscript( + mut self, + as_hash: bool, + ) -> Miniscript { + if let Some(thresh) = self.thresh { + assert!(self.keys.len() >= 2 && self.keys.len() <= 20); + Miniscript::from_ast(Terminal::Multi(thresh, self.keys)) + .expect("multi is a valid Miniscript") + } else { + assert_eq!(self.keys.len(), 1); + let key = self.keys.pop().expect("Length was just asserted"); + Miniscript::from_ast(Terminal::Check(sync::Arc::from( + Miniscript::from_ast(if as_hash { + Terminal::PkH(key) + } else { + Terminal::PkK(key) + }) + .expect("pk_k is a valid Miniscript"), + ))) + .expect("Well typed") + } + } +} diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index 0981b2a6..d01ae580 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -17,7 +17,7 @@ use miniscript::{ use std::{ collections::{BTreeMap, HashMap, HashSet}, convert::TryFrom, - error, fmt, str, sync, + error, fmt, str, }; use serde::{Deserialize, Serialize}; @@ -42,8 +42,6 @@ pub enum LianaDescError { Miniscript(miniscript::Error), IncompatibleDesc, DescKey(DescKeyError), - InvalidMultiThresh(usize), - InvalidMultiKeys(usize), /// Different number of PSBT vs tx inputs, etc.. InsanePsbt, /// Not all inputs' sequence the same, not all inputs signed with the same key, .. @@ -69,10 +67,8 @@ impl std::fmt::Display for LianaDescError { Self::Miniscript(e) => write!(f, "Miniscript error: '{}'.", e), Self::IncompatibleDesc => write!(f, "Descriptor is not compatible."), Self::DescKey(e) => write!(f, "{}", e), - Self::InvalidMultiThresh(thresh) => write!(f, "Invalid threshold value '{}'. The threshold must be > to 0 and <= to the number of keys.", thresh), - Self::InvalidMultiKeys(n_keys) => write!(f, "Invalid number of keys '{}'. Between 2 and 20 keys must be given to use multiple keys in a specific path.", n_keys), Self::InsanePsbt => write!(f, "Analyzed PSBT is empty or malformed."), - Self::InconsistentPsbt => write!(f, "Analyzed PSBT is inconsistent across inputs.") + Self::InconsistentPsbt => write!(f, "Analyzed PSBT is inconsistent across inputs."), } } } @@ -94,33 +90,6 @@ fn csv_check(csv_value: u32) -> Result { } } -// 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) -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() - } - } -} - // Get the origin of a key in a multipath descriptors. // Returns None if the given key isn't a multixpub. fn key_origin( @@ -132,67 +101,6 @@ fn key_origin( } } -/// 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). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LianaDescKeys { - thresh: Option, - keys: Vec, -} - -impl LianaDescKeys { - pub fn from_single(key: descriptor::DescriptorPublicKey) -> LianaDescKeys { - LianaDescKeys { - thresh: None, - keys: vec![key], - } - } - - pub fn from_multi( - thresh: usize, - keys: Vec, - ) -> Result { - if keys.len() < 2 || keys.len() > 20 { - return Err(LianaDescError::InvalidMultiKeys(keys.len())); - } - if thresh == 0 || thresh > keys.len() { - return Err(LianaDescError::InvalidMultiThresh(thresh)); - } - Ok(LianaDescKeys { - thresh: Some(thresh), - keys, - }) - } - - pub fn keys(&self) -> &Vec { - &self.keys - } - - pub fn into_miniscript( - mut self, - as_hash: bool, - ) -> Miniscript { - if let Some(thresh) = self.thresh { - assert!(self.keys.len() >= 2 && self.keys.len() <= 20); - Miniscript::from_ast(Terminal::Multi(thresh, self.keys)) - .expect("multi is a valid Miniscript") - } else { - assert_eq!(self.keys.len(), 1); - let key = self.keys.pop().expect("Length was just asserted"); - Miniscript::from_ast(Terminal::Check(sync::Arc::from( - Miniscript::from_ast(if as_hash { - Terminal::PkH(key) - } else { - Terminal::PkK(key) - }) - .expect("pk_k is a valid Miniscript"), - ))) - .expect("Well typed") - } - } -} - /// An [InheritanceDescriptor] that contains multipath keys for (and only for) the receive keychain /// and the change keychain. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] From eebfa4755944f14aaa23f8b1a293a1b6a0f0f30f Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 16:09:55 +0100 Subject: [PATCH 04/12] descriptors: move descriptor policy analysis into its own submodule --- src/descriptors/analysis.rs | 262 ++++++++++++++++++++++++++++++++++++ src/descriptors/mod.rs | 259 +---------------------------------- 2 files changed, 266 insertions(+), 255 deletions(-) create mode 100644 src/descriptors/analysis.rs diff --git a/src/descriptors/analysis.rs b/src/descriptors/analysis.rs new file mode 100644 index 00000000..749e6937 --- /dev/null +++ b/src/descriptors/analysis.rs @@ -0,0 +1,262 @@ +use miniscript::{bitcoin::util::bip32, descriptor, policy::Semantic as SemanticPolicy}; + +use std::{ + collections::{HashMap, HashSet}, + convert::TryFrom, +}; + +use crate::descriptors::LianaDescError; + +/// Whether a Miniscript policy node represents a key check (or several of them). +pub fn is_single_key_or_multisig(policy: &SemanticPolicy) -> bool { + match policy { + SemanticPolicy::Key(..) => true, + SemanticPolicy::Threshold(_, subs) => { + subs.iter().all(|sub| matches!(sub, SemanticPolicy::Key(_))) + } + _ => false, + } +} + +// We require the locktime to: +// - not be disabled +// - be in number of blocks +// - be 'clean' / minimal, ie all bits without consensus meaning should be 0 +// +// All this is achieved simply through asking for a 16-bit integer, since all the +// above are signaled in leftmost bits. +fn csv_check(csv_value: u32) -> Result { + if csv_value > 0 { + u16::try_from(csv_value).map_err(|_| LianaDescError::InsaneTimelock(csv_value)) + } else { + Err(LianaDescError::InsaneTimelock(csv_value)) + } +} + +// Get the origin of a key in a multipath descriptors. +// Returns None if the given key isn't a multixpub. +fn key_origin( + key: &descriptor::DescriptorPublicKey, +) -> Option<&(bip32::Fingerprint, bip32::DerivationPath)> { + match key { + descriptor::DescriptorPublicKey::MultiXPub(ref xpub) => xpub.origin.as_ref(), + _ => None, + } +} + +/// Information about a single spending path in the descriptor. +#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] +pub enum PathInfo { + Single(descriptor::DescriptorPublicKey), + Multi(usize, Vec), +} + +impl PathInfo { + /// Get the information about the primary spending path. + /// Returns None if the policy does not describe the primary spending path of a Liana + /// descriptor (that is, a set of keys). + pub fn from_primary_path( + policy: SemanticPolicy, + ) -> Result { + match policy { + SemanticPolicy::Key(key) => Ok(PathInfo::Single(key)), + SemanticPolicy::Threshold(k, subs) => { + let keys: Result<_, LianaDescError> = subs + .into_iter() + .map(|sub| match sub { + SemanticPolicy::Key(key) => Ok(key), + _ => Err(LianaDescError::IncompatibleDesc), + }) + .collect(); + Ok(PathInfo::Multi(k, keys?)) + } + _ => Err(LianaDescError::IncompatibleDesc), + } + } + + /// Get the information about the recovery spending path. + /// Returns None if the policy does not describe the recovery spending path of a Liana + /// descriptor (that is, a set of keys after a timelock). + pub fn from_recovery_path( + policy: SemanticPolicy, + ) -> Result<(u16, PathInfo), LianaDescError> { + // The recovery spending path must always be a policy of type `thresh(2, older(x), thresh(n, key1, + // key2, ..))`. In the special case n == 1, it is only `thresh(2, older(x), key)`. In the + // special case n == len(keys) (i.e. it's an N-of-N multisig), it is normalized as + // `thresh(n+1, older(x), key1, key2, ...)`. + let (k, subs) = match policy { + SemanticPolicy::Threshold(k, subs) => (k, subs), + _ => return Err(LianaDescError::IncompatibleDesc), + }; + if k == 2 && subs.len() == 2 { + // The general case (as well as the n == 1 case). The sub that is not the timelock is + // of the same form as a primary path. + let tl_value = subs + .iter() + .find_map(|s| match s { + SemanticPolicy::Older(val) => Some(csv_check(val.0)), + _ => None, + }) + .ok_or(LianaDescError::IncompatibleDesc)??; + let keys_sub = subs + .into_iter() + .find(is_single_key_or_multisig) + .ok_or(LianaDescError::IncompatibleDesc)?; + PathInfo::from_primary_path(keys_sub).map(|info| (tl_value, info)) + } else if k == subs.len() && subs.len() > 2 { + // The N-of-N case. All subs but the threshold must be keys (if one had been thresh() + // of keys it would have been normalized). + let mut tl_value = None; + let mut keys = Vec::with_capacity(subs.len()); + for sub in subs { + match sub { + SemanticPolicy::Key(key) => keys.push(key), + SemanticPolicy::Older(val) => { + if tl_value.is_some() { + return Err(LianaDescError::IncompatibleDesc); + } + tl_value = Some(csv_check(val.0)?); + } + _ => return Err(LianaDescError::IncompatibleDesc), + } + } + assert!(keys.len() > 1); // At least 3 subs, only one of which may be older(). + Ok(( + tl_value.ok_or(LianaDescError::IncompatibleDesc)?, + PathInfo::Multi(k - 1, keys), + )) + } else { + // If there is less than 2 subs, there can't be both a timelock and keys. If the + // threshold is not equal to the number of subs, the timelock can't be mandatory. + Err(LianaDescError::IncompatibleDesc) + } + } + + /// Get the required number of keys for spending through this path, and the set of keys + /// that can be used to provide a signature for this path. + pub fn thresh_origins(&self) -> (usize, HashSet<(bip32::Fingerprint, bip32::DerivationPath)>) { + match self { + PathInfo::Single(key) => { + let mut origins = HashSet::with_capacity(1); + origins.insert( + key_origin(key) + .expect("Must be a multixpub with an origin.") + .clone(), + ); + (1, origins) + } + PathInfo::Multi(k, keys) => ( + *k, + keys.iter() + .map(|key| { + key_origin(key) + .expect("Must be a multixpub with an origin.") + .clone() + }) + .collect(), + ), + } + } + + /// Get the spend information for this descriptor based from the list of all pubkeys that + /// signed the transaction. + pub fn spend_info<'a>( + &self, + all_pubkeys_signed: impl Iterator, + ) -> PathSpendInfo { + let mut signed_pubkeys = HashMap::new(); + let mut sigs_count = 0; + let (threshold, origins) = self.thresh_origins(); + + // For all existing signatures, pick those that are from one of our pubkeys. + for (fg, der_path) in all_pubkeys_signed { + // For all xpubs in the descriptor, we derive at /0/* or /1/*, so the xpub's origin's + // derivation path is the key's one without the last two derivation indexes. + if der_path.len() < 2 { + continue; + } + let parent_der_path: bip32::DerivationPath = der_path[..der_path.len() - 2].into(); + let parent_origin = (*fg, parent_der_path); + + // Now if the origin of this key without the two final derivation indexes is part of + // the set of our keys, count it as a signature for it. Note it won't mixup keys + // between spending paths, since we can't have duplicate xpubs in the descriptor and + // the (fingerprint, der_path) tuple is a UID for an xpub. + if origins.contains(&parent_origin) { + sigs_count += 1; + if let Some(count) = signed_pubkeys.get_mut(&parent_origin) { + *count += 1; + } else { + signed_pubkeys.insert(parent_origin, 1); + } + } + } + + PathSpendInfo { + threshold, + sigs_count, + signed_pubkeys, + } + } +} + +/// Information about the descriptor: how many keys are present in each path, what's the timelock +/// of the recovery path, what's the threshold if there are multiple keys, etc.. +#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] +pub struct LianaDescInfo { + pub(super) primary_path: PathInfo, + pub(super) recovery_path: (u16, PathInfo), +} + +impl LianaDescInfo { + pub(super) fn new(primary_path: PathInfo, recovery_path: (u16, PathInfo)) -> LianaDescInfo { + LianaDescInfo { + primary_path, + recovery_path, + } + } + + pub fn primary_path(&self) -> &PathInfo { + &self.primary_path + } + + /// Timelock and path info for the recovery path. + pub fn recovery_path(&self) -> (u16, &PathInfo) { + (self.recovery_path.0, &self.recovery_path.1) + } +} + +/// Partial spend information for a specific spending path within a descriptor. +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct PathSpendInfo { + /// The required number of signatures to provide to spend through this path. + pub threshold: usize, + /// The number of signatures provided. + pub sigs_count: usize, + /// The keys for which a signature was provided and the number (always >=1) of + /// signatures provided for this key. + pub signed_pubkeys: HashMap<(bip32::Fingerprint, bip32::DerivationPath), usize>, +} + +/// Information about a partial spend of Liana coins +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct PartialSpendInfo { + /// Number of signatures present for the primary path + pub(super) primary_path: PathSpendInfo, + /// Number of signatures present for the recovery path, only present if the path is available + /// in the first place. + pub(super) recovery_path: Option, +} + +impl PartialSpendInfo { + /// Get the number of signatures present for the primary path + pub fn primary_path(&self) -> &PathSpendInfo { + &self.primary_path + } + + /// Get the number of signatures present for the recovery path. Only present if the path is + /// available in the first place. + pub fn recovery_path(&self) -> &Option { + &self.recovery_path + } +} diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index d01ae580..0f7a0a2c 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -15,8 +15,7 @@ use miniscript::{ }; use std::{ - collections::{BTreeMap, HashMap, HashSet}, - convert::TryFrom, + collections::{BTreeMap, HashSet}, error, fmt, str, }; @@ -25,6 +24,9 @@ use serde::{Deserialize, Serialize}; pub mod keys; pub use keys::*; +pub mod analysis; +pub use analysis::*; + const WITNESS_FACTOR: usize = 4; // Convert a size in weight units to a size in virtual bytes, rounding up. @@ -75,32 +77,6 @@ impl std::fmt::Display for LianaDescError { impl error::Error for LianaDescError {} -// We require the locktime to: -// - not be disabled -// - be in number of blocks -// - be 'clean' / minimal, ie all bits without consensus meaning should be 0 -// -// All this is achieved simply through asking for a 16-bit integer, since all the -// above are signaled in leftmost bits. -fn csv_check(csv_value: u32) -> Result { - if csv_value > 0 { - u16::try_from(csv_value).map_err(|_| LianaDescError::InsaneTimelock(csv_value)) - } else { - Err(LianaDescError::InsaneTimelock(csv_value)) - } -} - -// Get the origin of a key in a multipath descriptors. -// Returns None if the given key isn't a multixpub. -fn key_origin( - key: &descriptor::DescriptorPublicKey, -) -> Option<&(bip32::Fingerprint, bip32::DerivationPath)> { - match key { - descriptor::DescriptorPublicKey::MultiXPub(ref xpub) => xpub.origin.as_ref(), - _ => None, - } -} - /// An [InheritanceDescriptor] that contains multipath keys for (and only for) the receive keychain /// and the change keychain. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -125,16 +101,6 @@ impl fmt::Display for MultipathDescriptor { } } -fn is_single_key_or_multisig(policy: &SemanticPolicy) -> bool { - match policy { - SemanticPolicy::Key(..) => true, - SemanticPolicy::Threshold(_, subs) => { - subs.iter().all(|sub| matches!(sub, SemanticPolicy::Key(_))) - } - _ => false, - } -} - impl str::FromStr for MultipathDescriptor { type Err = LianaDescError; @@ -217,223 +183,6 @@ impl PartialEq> for Inhe } } -/// Information about a single spending path in the descriptor. -#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] -pub enum PathInfo { - Single(descriptor::DescriptorPublicKey), - Multi(usize, Vec), -} - -impl PathInfo { - /// Get the information about the primary spending path. - /// Returns None if the policy does not describe the primary spending path of a Liana - /// descriptor (that is, a set of keys). - pub fn from_primary_path( - policy: SemanticPolicy, - ) -> Result { - match policy { - SemanticPolicy::Key(key) => Ok(PathInfo::Single(key)), - SemanticPolicy::Threshold(k, subs) => { - let keys: Result<_, LianaDescError> = subs - .into_iter() - .map(|sub| match sub { - SemanticPolicy::Key(key) => Ok(key), - _ => Err(LianaDescError::IncompatibleDesc), - }) - .collect(); - Ok(PathInfo::Multi(k, keys?)) - } - _ => Err(LianaDescError::IncompatibleDesc), - } - } - - /// Get the information about the recovery spending path. - /// Returns None if the policy does not describe the recovery spending path of a Liana - /// descriptor (that is, a set of keys after a timelock). - pub fn from_recovery_path( - policy: SemanticPolicy, - ) -> Result<(u16, PathInfo), LianaDescError> { - // The recovery spending path must always be a policy of type `thresh(2, older(x), thresh(n, key1, - // key2, ..))`. In the special case n == 1, it is only `thresh(2, older(x), key)`. In the - // special case n == len(keys) (i.e. it's an N-of-N multisig), it is normalized as - // `thresh(n+1, older(x), key1, key2, ...)`. - let (k, subs) = match policy { - SemanticPolicy::Threshold(k, subs) => (k, subs), - _ => return Err(LianaDescError::IncompatibleDesc), - }; - if k == 2 && subs.len() == 2 { - // The general case (as well as the n == 1 case). The sub that is not the timelock is - // of the same form as a primary path. - let tl_value = subs - .iter() - .find_map(|s| match s { - SemanticPolicy::Older(val) => Some(csv_check(val.0)), - _ => None, - }) - .ok_or(LianaDescError::IncompatibleDesc)??; - let keys_sub = subs - .into_iter() - .find(is_single_key_or_multisig) - .ok_or(LianaDescError::IncompatibleDesc)?; - PathInfo::from_primary_path(keys_sub).map(|info| (tl_value, info)) - } else if k == subs.len() && subs.len() > 2 { - // The N-of-N case. All subs but the threshold must be keys (if one had been thresh() - // of keys it would have been normalized). - let mut tl_value = None; - let mut keys = Vec::with_capacity(subs.len()); - for sub in subs { - match sub { - SemanticPolicy::Key(key) => keys.push(key), - SemanticPolicy::Older(val) => { - if tl_value.is_some() { - return Err(LianaDescError::IncompatibleDesc); - } - tl_value = Some(csv_check(val.0)?); - } - _ => return Err(LianaDescError::IncompatibleDesc), - } - } - assert!(keys.len() > 1); // At least 3 subs, only one of which may be older(). - Ok(( - tl_value.ok_or(LianaDescError::IncompatibleDesc)?, - PathInfo::Multi(k - 1, keys), - )) - } else { - // If there is less than 2 subs, there can't be both a timelock and keys. If the - // threshold is not equal to the number of subs, the timelock can't be mandatory. - Err(LianaDescError::IncompatibleDesc) - } - } - - /// Get the required number of keys for spending through this path, and the set of keys - /// that can be used to provide a signature for this path. - pub fn thresh_origins(&self) -> (usize, HashSet<(bip32::Fingerprint, bip32::DerivationPath)>) { - match self { - PathInfo::Single(key) => { - let mut origins = HashSet::with_capacity(1); - origins.insert( - key_origin(key) - .expect("Must be a multixpub with an origin.") - .clone(), - ); - (1, origins) - } - PathInfo::Multi(k, keys) => ( - *k, - keys.iter() - .map(|key| { - key_origin(key) - .expect("Must be a multixpub with an origin.") - .clone() - }) - .collect(), - ), - } - } - - /// Get the spend information for this descriptor based from the list of all pubkeys that - /// signed the transaction. - pub fn spend_info<'a>( - &self, - all_pubkeys_signed: impl Iterator, - ) -> PathSpendInfo { - let mut signed_pubkeys = HashMap::new(); - let mut sigs_count = 0; - let (threshold, origins) = self.thresh_origins(); - - // For all existing signatures, pick those that are from one of our pubkeys. - for (fg, der_path) in all_pubkeys_signed { - // For all xpubs in the descriptor, we derive at /0/* or /1/*, so the xpub's origin's - // derivation path is the key's one without the last two derivation indexes. - if der_path.len() < 2 { - continue; - } - let parent_der_path: bip32::DerivationPath = der_path[..der_path.len() - 2].into(); - let parent_origin = (*fg, parent_der_path); - - // Now if the origin of this key without the two final derivation indexes is part of - // the set of our keys, count it as a signature for it. Note it won't mixup keys - // between spending paths, since we can't have duplicate xpubs in the descriptor and - // the (fingerprint, der_path) tuple is a UID for an xpub. - if origins.contains(&parent_origin) { - sigs_count += 1; - if let Some(count) = signed_pubkeys.get_mut(&parent_origin) { - *count += 1; - } else { - signed_pubkeys.insert(parent_origin, 1); - } - } - } - - PathSpendInfo { - threshold, - sigs_count, - signed_pubkeys, - } - } -} - -/// Information about the descriptor: how many keys are present in each path, what's the timelock -/// of the recovery path, what's the threshold if there are multiple keys, etc.. -#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] -pub struct LianaDescInfo { - primary_path: PathInfo, - recovery_path: (u16, PathInfo), -} - -impl LianaDescInfo { - fn new(primary_path: PathInfo, recovery_path: (u16, PathInfo)) -> LianaDescInfo { - LianaDescInfo { - primary_path, - recovery_path, - } - } - - pub fn primary_path(&self) -> &PathInfo { - &self.primary_path - } - - /// Timelock and path info for the recovery path. - pub fn recovery_path(&self) -> (u16, &PathInfo) { - (self.recovery_path.0, &self.recovery_path.1) - } -} - -/// Partial spend information for a specific spending path within a descriptor. -#[derive(Debug, Eq, PartialEq, Clone)] -pub struct PathSpendInfo { - /// The required number of signatures to provide to spend through this path. - pub threshold: usize, - /// The number of signatures provided. - pub sigs_count: usize, - /// The keys for which a signature was provided and the number (always >=1) of - /// signatures provided for this key. - pub signed_pubkeys: HashMap<(bip32::Fingerprint, bip32::DerivationPath), usize>, -} - -/// Information about a partial spend of Liana coins -#[derive(Debug, Eq, PartialEq, Clone)] -pub struct PartialSpendInfo { - /// Number of signatures present for the primary path - primary_path: PathSpendInfo, - /// Number of signatures present for the recovery path, only present if the path is available - /// in the first place. - recovery_path: Option, -} - -impl PartialSpendInfo { - /// Get the number of signatures present for the primary path - pub fn primary_path(&self) -> &PathSpendInfo { - &self.primary_path - } - - /// Get the number of signatures present for the recovery path. Only present if the path is - /// available in the first place. - pub fn recovery_path(&self) -> &Option { - &self.recovery_path - } -} - impl MultipathDescriptor { pub fn new( owner_keys: LianaDescKeys, From 757009536b489b333ddb6a2d6bf237196e930e4e Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 16:10:21 +0100 Subject: [PATCH 05/12] descriptors: make sure there is at least one timelocked path when parsing --- src/descriptors/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index 0f7a0a2c..5037b6ca 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -139,9 +139,11 @@ impl str::FromStr for MultipathDescriptor { // 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 is_single_key_or_multisig(&sub) { + 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)?; } @@ -689,6 +691,9 @@ mod tests { #[test] fn inheritance_descriptor_tl_value() { + // Must always contain at least one timelocked path. + MultipathDescriptor::from_str("wsh(or_i(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap_err(); + let desc = MultipathDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap(); assert_eq!(desc.timelock_value(), 1); From cd566b91af07a53f9651c034ef4da95a8a033c56 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 16:23:31 +0100 Subject: [PATCH 06/12] descriptors: rename LianaDescInfo into LianaPolicy What this really represents is a spending policy. We'll extend it to be able to infer it from a descriptor and to create a descriptor from it. --- src/descriptors/analysis.rs | 11 +++++------ src/descriptors/mod.rs | 14 +++++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/descriptors/analysis.rs b/src/descriptors/analysis.rs index 749e6937..5a725f69 100644 --- a/src/descriptors/analysis.rs +++ b/src/descriptors/analysis.rs @@ -200,17 +200,16 @@ impl PathInfo { } } -/// Information about the descriptor: how many keys are present in each path, what's the timelock -/// of the recovery path, what's the threshold if there are multiple keys, etc.. +/// A Liana spending policy. #[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] -pub struct LianaDescInfo { +pub struct LianaPolicy { pub(super) primary_path: PathInfo, pub(super) recovery_path: (u16, PathInfo), } -impl LianaDescInfo { - pub(super) fn new(primary_path: PathInfo, recovery_path: (u16, PathInfo)) -> LianaDescInfo { - LianaDescInfo { +impl LianaPolicy { + pub(super) fn new(primary_path: PathInfo, recovery_path: (u16, PathInfo)) -> LianaPolicy { + LianaPolicy { primary_path, recovery_path, } diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index 5037b6ca..e14494be 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -291,8 +291,8 @@ impl MultipathDescriptor { &self.change_desc } - /// Parse information about this descriptor - pub fn info(&self) -> LianaDescInfo { + /// 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, @@ -338,13 +338,13 @@ impl MultipathDescriptor { 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."); - LianaDescInfo::new(primary_path, reco_path) + LianaPolicy::new(primary_path, reco_path) } /// Get the value (in blocks) of the relative timelock for the heir's spending path. pub fn timelock_value(&self) -> u32 { // TODO: make it return a u16 - self.info().recovery_path.0 as u32 + self.policy().recovery_path.0 as u32 } /// Get the maximum size in WU of a satisfaction for this descriptor. @@ -390,7 +390,7 @@ impl MultipathDescriptor { // Determine the structure of the descriptor. Then compute the spend info for the primary // and recovery paths. Only provide the spend info for the recovery path if it is available // (ie if the nSequence is >= to the chosen CSV value). - let desc_info = self.info(); + let desc_info = self.policy(); let primary_path = desc_info.primary_path.spend_info(pubkeys_signed.clone()); let recovery_path = if txin.sequence.is_height_locked() && txin.sequence.0 >= desc_info.recovery_path.0 as u32 @@ -772,7 +772,7 @@ mod tests { fn partial_spend_info() { // A simple descriptor with 1 keys as primary path and 1 recovery key. let desc = MultipathDescriptor::from_str("wsh(or_d(pk([f5acc2fd]tpubD6NzVbkrYhZ4YgUx2ZLNt2rLYAMTdYysCRzKoLu2BeSHKvzqPaBDvf17GeBPnExUVPkuBpx4kniP964e2MxyzzazcXLptxLXModSVCVEV1T/<0;1>/*),and_v(v:pkh([8a64f2a9]tpubD6NzVbkrYhZ4WmzFjvQrp7sDa4ECUxTi9oby8K4FZkd3XCBtEdKwUiQyYJaxiJo5y42gyDWEczrFpozEjeLxMPxjf2WtkfcbpUdfvNnozWF/<0;1>/*),older(10))))#d72le4dr").unwrap(); - let desc_info = desc.info(); + let desc_info = desc.policy(); let prim_key_origin = ( bip32::Fingerprint::from_str("f5acc2fd").unwrap(), Vec::new().into(), @@ -949,7 +949,7 @@ mod tests { assert!(info.recovery_path.is_none()); let desc = MultipathDescriptor::from_str("wsh(or_d(multi(2,[636adf3f/48'/1'/0'/2']tpubDEE9FvWbG4kg4gxDNrALgrWLiHwNMXNs8hk6nXNPw4VHKot16xd2251vwi2M6nsyQTkak5FJNHVHkCcuzmvpSbWHdumX3DxpDm89iTfSBaL/<0;1>/*,[ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/<0;1>/*),and_v(v:multi(2,[636adf3f/48'/1'/1'/2']tpubDDvF2khuoBBj8vcSjQfa7iKaxsQZE7YjJ7cJL8A8eaneadMPKbHSpoSr4JD1F5LUvWD82HCxdtSppGfrMUmiNbFxrA2EHEVLnrdCFNFe75D/<0;1>/*,[ffd63c8d/48'/1'/1'/2']tpubDFMs44FD4kFt3M7Z317cFh5tdKEGN8tyQRY6Q5gcSha4NtxZfGmTVRMbsD1bWN469LstXU4aVSARDxrvxFCUjHeegfEY2cLSazMBkNCmDPD/<0;1>/*),older(2))))#xcf6jr2r").unwrap(); - let info = desc.info(); + let info = desc.policy(); assert_eq!(info.primary_path, PathInfo::Multi( 2, vec![ From 9b866300be53be8a4e49e7913be25ff8887eac63 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 16:58:50 +0100 Subject: [PATCH 07/12] 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. From 647d65fe045a71158041cfb4bf5b98e5200db2e8 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 18:40:36 +0100 Subject: [PATCH 08/12] descriptors: create Liana descriptors through the policy This makes it possible for a LianaPolicy to be created from a user configuration. This in turn centralizes the descriptor creation inside it as well and make `MultipathDescriptor` take a `LianaPolicy` directly. This is useful to centralize all the Miniscript and Miniscript policy handling under in a single place as we'll soon be managing much more complex policies (and make use of the Minsicript policy compiler). Unfortunately this is an invasive API change. But at least the API now makes a lot more sense: you can create a spending policy from a configuration and create a descriptor from it. And vice-versa you can infer a spending policy from a descriptor and inspect the configuration from it. --- src/descriptors/analysis.rs | 168 +++++++++++++++++++++-- src/descriptors/keys.rs | 65 +-------- src/descriptors/mod.rs | 261 +++++++++++++++++------------------- src/signer.rs | 9 +- src/testutils.rs | 7 +- 5 files changed, 288 insertions(+), 222 deletions(-) diff --git a/src/descriptors/analysis.rs b/src/descriptors/analysis.rs index a4a47c49..9d1b3bbe 100644 --- a/src/descriptors/analysis.rs +++ b/src/descriptors/analysis.rs @@ -1,18 +1,20 @@ use miniscript::{ - bitcoin::util::bip32, + bitcoin::{util::bip32, Sequence}, descriptor, policy::{Liftable, Semantic as SemanticPolicy}, + Miniscript, ScriptContext, Terminal, }; use std::{ collections::{HashMap, HashSet}, convert::TryFrom, + sync, }; -use crate::descriptors::LianaDescError; +use crate::descriptors::{keys::DescKeyError, LianaDescError}; -/// Whether a Miniscript policy node represents a key check (or several of them). -pub fn is_single_key_or_multisig(policy: &SemanticPolicy) -> bool { +// Whether a Miniscript policy node represents a key check (or several of them). +fn is_single_key_or_multisig(policy: &SemanticPolicy) -> bool { match policy { SemanticPolicy::Key(..) => true, SemanticPolicy::Threshold(_, subs) => { @@ -22,12 +24,12 @@ pub fn is_single_key_or_multisig(policy: &SemanticPolicy bool { +// 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) +fn is_valid_desc_key(key: &descriptor::DescriptorPublicKey) -> bool { match *key { descriptor::DescriptorPublicKey::Single(..) | descriptor::DescriptorPublicKey::XPub(..) => { false @@ -229,9 +231,50 @@ impl PathInfo { signed_pubkeys, } } + + // TODO: avoid using a vec... + /// Get the keys contained in this spending path. + pub fn keys(&self) -> Vec { + match self { + PathInfo::Single(ref key) => vec![key.clone()], + PathInfo::Multi(_, keys) => keys.clone(), + } + } + + /// Returns `None` if it is a multisig that does not fit inside a CHECKMULTISIG. + pub fn into_miniscript( + self, + as_hash: bool, + ) -> Option> { + match self { + PathInfo::Single(key) => Some( + Miniscript::from_ast(Terminal::Check(sync::Arc::from( + Miniscript::from_ast(if as_hash { + Terminal::PkH(key) + } else { + Terminal::PkK(key) + }) + .expect("pk_k is a valid Miniscript"), + ))) + .expect("Well typed"), + ), + PathInfo::Multi(thresh, keys) => { + if thresh < 1 || keys.len() > 20 || thresh > keys.len() { + None + } else { + Some( + Miniscript::from_ast(Terminal::Multi(thresh, keys)) + .expect("multi is a valid Miniscript"), + ) + } + } + } + } } -/// A Liana spending policy. Can be inferred from a Miniscript semantic policy. +/// A Liana spending policy. Can be created from some settings (the primary and recovery keys, the +/// timelock(s)) and be used to derive a descriptor. It can also be inferred from a descriptor and +/// be used to retrieve the settings. #[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] pub struct LianaPolicy { pub(super) primary_path: PathInfo, @@ -239,6 +282,66 @@ pub struct LianaPolicy { } impl LianaPolicy { + /// Create a new Liana policy from a given configuration. + pub fn new( + primary_path: PathInfo, + recovery_path: PathInfo, + recovery_timelock: u16, + ) -> Result { + // We require the locktime to: + // - not be disabled + // - be in number of blocks + // - be 'clean' / minimal, ie all bits without consensus meaning should be 0 + // - be positive (Miniscript requires it not to be 0) + // + // All this is achieved through asking for a 16-bit integer. + if recovery_timelock == 0 { + return Err(LianaDescError::InsaneTimelock(recovery_timelock as u32)); + } + + // If any of the paths is a multisig, make sure they are within the CHECKMULTISIG bounds. + for path_info in &[&primary_path, &recovery_path] { + if let PathInfo::Multi(thresh, keys) = path_info { + if keys.len() < 2 || keys.len() > 20 { + return Err(LianaDescError::DescKey(DescKeyError::InvalidMultiKeys( + keys.len(), + ))); + } + if thresh == &0 || thresh > &keys.len() { + return Err(LianaDescError::DescKey(DescKeyError::InvalidMultiThresh( + *thresh, + ))); + } + } + } + + // Check all keys are valid according to our standard (this checks all are multipath keys). + let (prim_keys, rec_keys) = (primary_path.keys(), recovery_path.keys()); + let all_keys = prim_keys.iter().chain(rec_keys.iter()); + if let Some(key) = all_keys.clone().find(|k| !is_valid_desc_key(k)) { + return Err(LianaDescError::InvalidKey((*key).clone().into())); + } + + // Check for key duplicates. They are invalid in (nonmalleable) miniscripts. + let mut key_set = HashSet::new(); + for key in all_keys { + let xpub = match key { + descriptor::DescriptorPublicKey::MultiXPub(ref multi_xpub) => multi_xpub.xkey, + _ => unreachable!("Just checked it was a multixpub above"), + }; + if key_set.contains(&xpub) { + return Err(LianaDescError::DuplicateKey(key.clone().into())); + } + key_set.insert(xpub); + } + assert!(!key_set.is_empty()); + + Ok(LianaPolicy { + primary_path, + recovery_path: (recovery_timelock, recovery_path), + }) + } + /// 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( @@ -319,6 +422,49 @@ impl LianaPolicy { pub fn recovery_path(&self) -> (u16, &PathInfo) { (self.recovery_path.0, &self.recovery_path.1) } + + /// Create a descriptor from this spending policy with multipath key expressions. + /// + /// Although for now this function is deterministic, it **will not** be in the future. + pub fn into_multipath_descriptor( + self, + ) -> descriptor::Descriptor { + let LianaPolicy { + primary_path, + recovery_path: (timelock, recovery_path), + } = self; + + // Create the timelocked spending path. If there is a single key we make it a pk_h() in + // order to save on the script size (since we assume the timelocked recovery path will + // seldom be used). + let recovery_timelock = Terminal::Older(Sequence::from_height(timelock)); + let recovery_keys = recovery_path + .into_miniscript(true) + .expect("We check the multisig never overflows in our constructors."); + let recovery_branch = Miniscript::from_ast(Terminal::AndV( + Miniscript::from_ast(Terminal::Verify(recovery_keys.into())) + .expect("Well typed") + .into(), + Miniscript::from_ast(recovery_timelock) + .expect("Well typed") + .into(), + )) + .expect("Well typed"); + + // Combine the timelocked spending path with the simple "primary" path. For the primary key + // we don't use a pkh since it's the one that will likely always be used. + let primary_keys = primary_path + .into_miniscript(false) + .expect("We check the multisig never overflows in our constructors."); + let tl_miniscript = + Miniscript::from_ast(Terminal::OrD(primary_keys.into(), recovery_branch.into())) + .expect("Well typed"); + miniscript::Segwitv0::check_local_validity(&tl_miniscript) + .expect("Miniscript must be sane"); + descriptor::Descriptor::Wsh( + descriptor::Wsh::new(tl_miniscript).expect("Must pass sanity checks"), + ) + } } /// Partial spend information for a specific spending path within a descriptor. diff --git a/src/descriptors/keys.rs b/src/descriptors/keys.rs index cf5c5538..422ef59b 100644 --- a/src/descriptors/keys.rs +++ b/src/descriptors/keys.rs @@ -4,10 +4,10 @@ use miniscript::{ hashes::{hash160, ripemd160, sha256}, util::bip32, }, - descriptor, hash256, Miniscript, MiniscriptKey, Terminal, ToPublicKey, + hash256, MiniscriptKey, ToPublicKey, }; -use std::{error, fmt, str, sync}; +use std::{error, fmt, str}; #[derive(Debug)] pub enum DescKeyError { @@ -139,64 +139,3 @@ impl ToPublicKey for DerivedPublicKey { *hash } } - -/// 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). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LianaDescKeys { - thresh: Option, - keys: Vec, -} - -impl LianaDescKeys { - pub fn from_single(key: descriptor::DescriptorPublicKey) -> LianaDescKeys { - LianaDescKeys { - thresh: None, - keys: vec![key], - } - } - - pub fn from_multi( - thresh: usize, - keys: Vec, - ) -> Result { - if keys.len() < 2 || keys.len() > 20 { - return Err(DescKeyError::InvalidMultiKeys(keys.len())); - } - if thresh == 0 || thresh > keys.len() { - return Err(DescKeyError::InvalidMultiThresh(thresh)); - } - Ok(LianaDescKeys { - thresh: Some(thresh), - keys, - }) - } - - pub fn keys(&self) -> &Vec { - &self.keys - } - - pub fn into_miniscript( - mut self, - as_hash: bool, - ) -> Miniscript { - if let Some(thresh) = self.thresh { - assert!(self.keys.len() >= 2 && self.keys.len() <= 20); - Miniscript::from_ast(Terminal::Multi(thresh, self.keys)) - .expect("multi is a valid Miniscript") - } else { - assert_eq!(self.keys.len(), 1); - let key = self.keys.pop().expect("Length was just asserted"); - Miniscript::from_ast(Terminal::Check(sync::Arc::from( - Miniscript::from_ast(if as_hash { - Terminal::PkH(key) - } else { - Terminal::PkK(key) - }) - .expect("pk_k is a valid Miniscript"), - ))) - .expect("Well typed") - } - } -} diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index e9f6c3ca..142787e5 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -1,22 +1,15 @@ use miniscript::{ bitcoin::{ - self, - blockdata::transaction::Sequence, - secp256k1, + self, secp256k1, util::{ bip32, psbt::{Input as PsbtIn, Psbt}, }, }, - descriptor, - miniscript::{decode::Terminal, Miniscript}, - translate_hash_clone, ForEachKey, ScriptContext, TranslatePk, Translator, + descriptor, translate_hash_clone, ForEachKey, TranslatePk, Translator, }; -use std::{ - collections::{BTreeMap, HashSet}, - error, fmt, str, -}; +use std::{collections::BTreeMap, error, fmt, str}; use serde::{Deserialize, Serialize}; @@ -144,69 +137,9 @@ impl PartialEq> for Inhe } impl MultipathDescriptor { - pub fn new( - owner_keys: LianaDescKeys, - heir_keys: LianaDescKeys, - timelock: u16, - ) -> Result { - // We require the locktime to: - // - not be disabled - // - be in number of blocks - // - be 'clean' / minimal, ie all bits without consensus meaning should be 0 - // - be positive (Miniscript requires it not to be 0) - // - // All this is achieved through asking for a 16-bit integer. - if timelock == 0 { - return Err(LianaDescError::InsaneTimelock(timelock as u32)); - } - let timelock = Sequence::from_height(timelock); - - // Check all keys are valid according to our standard (this checks all are multipath keys). - let all_keys = owner_keys.keys().iter().chain(heir_keys.keys().iter()); - if let Some(key) = all_keys.clone().find(|k| !is_valid_desc_key(k)) { - return Err(LianaDescError::InvalidKey((*key).clone().into())); - } - - // Check for key duplicates. They are invalid in (nonmalleable) miniscripts. - let mut key_set = HashSet::new(); - for key in all_keys { - let xpub = match key { - descriptor::DescriptorPublicKey::MultiXPub(ref multi_xpub) => multi_xpub.xkey, - _ => unreachable!("Just checked it was a multixpub above"), - }; - if key_set.contains(&xpub) { - return Err(LianaDescError::DuplicateKey(key.clone().into())); - } - key_set.insert(xpub); - } - assert!(!key_set.is_empty()); - - // Create the timelocked spending path. If there is a single key we make it a pk_h() in - // order to save on the script size (since we assume the timelocked recovery path will - // seldom be used). - let heir_timelock = Terminal::Older(timelock); - let heir_branch = Miniscript::from_ast(Terminal::AndV( - Miniscript::from_ast(Terminal::Verify(heir_keys.into_miniscript(true).into())) - .expect("Well typed") - .into(), - Miniscript::from_ast(heir_timelock) - .expect("Well typed") - .into(), - )) - .expect("Well typed"); - - // Combine the timelocked spending path with the simple "primary" path. For the primary key - // we don't use a pkh since it's the one that will likely always be used. - let tl_miniscript = Miniscript::from_ast(Terminal::OrD( - owner_keys.into_miniscript(false).into(), - heir_branch.into(), - )) - .expect("Well typed"); - miniscript::Segwitv0::check_local_validity(&tl_miniscript) - .expect("Miniscript must be sane"); - let multi_desc = descriptor::Descriptor::Wsh( - descriptor::Wsh::new(tl_miniscript).expect("Must pass sanity checks"), - ); + pub fn new(spending_policy: LianaPolicy) -> MultipathDescriptor { + // Get the descriptor from the chosen spending policy. + let multi_desc = spending_policy.into_multipath_descriptor(); // 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. @@ -221,11 +154,11 @@ impl MultipathDescriptor { let receive_desc = InheritanceDescriptor(singlepath_descs.next().expect("First of 2")); let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2")); - Ok(MultipathDescriptor { + MultipathDescriptor { multi_desc, receive_desc, change_desc, - }) + } } /// Whether all xpubs contained in this descriptor are for the passed expected network. @@ -465,126 +398,127 @@ impl DerivedInheritanceDescriptor { mod tests { use super::*; + use bitcoin::Sequence; + use std::str::FromStr; + use crate::signer::HotSigner; + #[test] fn descriptor_creation() { - let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); - let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap()); + let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); + let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap()); let timelock = 52560; - assert_eq!(MultipathDescriptor::new(owner_key.clone(), heir_key.clone(), timelock).unwrap().to_string(), "wsh(or_d(pk([abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh([abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#g7vk9r5l"); + let policy = LianaPolicy::new(owner_key.clone(), heir_key.clone(), timelock).unwrap(); + assert_eq!(MultipathDescriptor::new(policy).to_string(), "wsh(or_d(pk([abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh([abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#g7vk9r5l"); // A decaying multisig after 6 months. Note we can't duplicate the keys, so different ones // are used. In practice they would both be controlled by the same entity. - let primary_keys = LianaDescKeys::from_multi( + let primary_keys = PathInfo::Multi( 3, vec![ descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*").unwrap() ] - ) - .unwrap(); - let recovery_keys = LianaDescKeys::from_multi( + ); + let recovery_keys = PathInfo::Multi( 2, vec![ descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*").unwrap(), ], - ) - .unwrap(); - assert_eq!(MultipathDescriptor::new(primary_keys, recovery_keys, 26352).unwrap().to_string(), "wsh(or_d(multi(3,[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,[abcdef01]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#s0zsa6uc"); + ); + let policy = LianaPolicy::new(primary_keys, recovery_keys, 26352).unwrap(); + assert_eq!(MultipathDescriptor::new(policy).to_string(), "wsh(or_d(multi(3,[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,[abcdef01]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#s0zsa6uc"); // We prevent footguns with timelocks by requiring a u16. Note how the following wouldn't // compile: - //MultipathDescriptor::new(owner_key.clone(), heir_key.clone(), 0x00_01_0f_00).unwrap_err(); - //MultipathDescriptor::new(owner_key.clone(), heir_key.clone(), (1 << 31) + 1).unwrap_err(); - //MultipathDescriptor::new(owner_key, heir_key, (1 << 22) + 1).unwrap_err(); + //LianaPolicy::new(owner_key.clone(), heir_key.clone(), 0x00_01_0f_00).unwrap_err(); + //LianaPolicy::new(owner_key.clone(), heir_key.clone(), (1 << 31) + 1).unwrap_err(); + //LianaPolicy::new(owner_key, heir_key, (1 << 22) + 1).unwrap_err(); // You can't use a null timelock in Miniscript. - MultipathDescriptor::new(owner_key, heir_key, 0).unwrap_err(); + LianaPolicy::new(owner_key, heir_key, 0).unwrap_err(); - let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*").unwrap()); - let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*").unwrap()); + let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*").unwrap()); + let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*").unwrap()); let timelock = 57600; - assert_eq!(MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*),and_v(v:pkh([abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*),older(57600))))#ak4cm093"); + let policy = LianaPolicy::new(owner_key.clone(), heir_key, timelock).unwrap(); + assert_eq!(MultipathDescriptor::new(policy).to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*),and_v(v:pkh([abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*),older(57600))))#ak4cm093"); // We can't pass a raw key, an xpub that is not deriveable, only hardened derivable, // without both the change and receive derivation paths, or with more than 2 different // derivation paths. - let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap()); - MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err(); - let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/*'").unwrap()); - MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err(); - let heir_key = LianaDescKeys::from_single( + let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap()); + LianaPolicy::new(owner_key.clone(), heir_key, timelock).unwrap_err(); + let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/*'").unwrap()); + LianaPolicy::new(owner_key.clone(), heir_key, timelock).unwrap_err(); + let heir_key = PathInfo::Single( descriptor::DescriptorPublicKey::from_str( "[abcdef01]02e24913be26dbcfdf8e8e94870b28725cdae09b448b6c127767bf0154e3a3c8e5", ) .unwrap(), ); - MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err(); - let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap()); - MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err(); - let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/<0;1;2>/*'").unwrap()); - MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap_err(); + LianaPolicy::new(owner_key.clone(), heir_key, timelock).unwrap_err(); + let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap()); + LianaPolicy::new(owner_key.clone(), heir_key, timelock).unwrap_err(); + let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/<0;1;2>/*'").unwrap()); + LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err(); // And it's checked even in a multisig. For instance: - let primary_keys = LianaDescKeys::from_multi( + let primary_keys = PathInfo::Multi( 1, vec![ descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap(), ] - ) - .unwrap(); - let recovery_keys = LianaDescKeys::from_multi( + ); + let recovery_keys = PathInfo::Multi( 1, vec![ descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(), ], - ) - .unwrap(); - MultipathDescriptor::new(primary_keys, recovery_keys, 26352).unwrap_err(); + ); + LianaPolicy::new(primary_keys, recovery_keys, 26352).unwrap_err(); // You can't pass duplicate keys, even if they are encoded differently. - let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); - let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); - MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap_err(); - let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[00aabb44]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); - let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); - MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap_err(); - let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[00aabb44]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); - let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[11223344/2/98]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); - MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap_err(); + let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); + let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); + LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err(); + let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[00aabb44]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); + let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); + LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err(); + let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[00aabb44]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); + let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[11223344/2/98]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); + LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err(); // You can't pass duplicate keys, even across multisigs. - let primary_keys = LianaDescKeys::from_multi( + let primary_keys = PathInfo::Multi( 3, vec![ descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*").unwrap() ] - ) - .unwrap(); - let recovery_keys = LianaDescKeys::from_multi( + ); + let recovery_keys = PathInfo::Multi( 2, vec![ descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(), ], - ) - .unwrap(); - MultipathDescriptor::new(primary_keys, recovery_keys, 26352).unwrap_err(); + ); + LianaPolicy::new(primary_keys, recovery_keys, 26352).unwrap_err(); // No origin in one of the keys - let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); - let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap()); + let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); + let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap()); let timelock = 52560; - MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap_err(); + LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err(); } #[test] @@ -646,19 +580,66 @@ mod tests { #[test] fn liana_desc_keys() { - let desc_key_a = descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(); - let desc_key_b = descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap(); - LianaDescKeys::from_single(desc_key_a.clone()); + let secp = secp256k1::Secp256k1::signing_only(); + let random_desc_key = || { + let xpub_str = format!( + "[aabbccdd]{}/<0;1>/*", + HotSigner::generate(bitcoin::Network::Bitcoin) + .unwrap() + .xpub_at(&bip32::DerivationPath::from_str("m").unwrap(), &secp) + ); + descriptor::DescriptorPublicKey::from_str(&xpub_str).unwrap() + }; + let prim_path = PathInfo::Single(random_desc_key()); + let twenty_keys: Vec = + (0..20).map(|_| random_desc_key()).collect(); + let mut twenty_one_keys = twenty_keys.clone(); + twenty_one_keys.push(random_desc_key()); - LianaDescKeys::from_multi(1, vec![desc_key_a.clone()]).unwrap_err(); - LianaDescKeys::from_multi(2, vec![desc_key_a.clone()]).unwrap_err(); - LianaDescKeys::from_multi(1, vec![desc_key_a.clone(), desc_key_b.clone()]).unwrap(); - LianaDescKeys::from_multi(0, vec![desc_key_a.clone(), desc_key_b.clone()]).unwrap_err(); - LianaDescKeys::from_multi(2, vec![desc_key_a.clone(), desc_key_b.clone()]).unwrap(); - LianaDescKeys::from_multi(3, vec![desc_key_a.clone(), desc_key_b]).unwrap_err(); - LianaDescKeys::from_multi(3, (0..20).map(|_| desc_key_a.clone()).collect()).unwrap(); - LianaDescKeys::from_multi(20, (0..20).map(|_| desc_key_a.clone()).collect()).unwrap(); - LianaDescKeys::from_multi(20, (0..21).map(|_| desc_key_a.clone()).collect()).unwrap_err(); + LianaPolicy::new( + prim_path.clone(), + PathInfo::Multi(1, vec![random_desc_key()]), + 1, + ) + .unwrap_err(); + LianaPolicy::new( + prim_path.clone(), + PathInfo::Multi(2, vec![random_desc_key()]), + 1, + ) + .unwrap_err(); + LianaPolicy::new( + prim_path.clone(), + PathInfo::Multi(1, vec![random_desc_key(), random_desc_key()]), + 1, + ) + .unwrap(); + LianaPolicy::new( + prim_path.clone(), + PathInfo::Multi(0, vec![random_desc_key(), random_desc_key()]), + 1, + ) + .unwrap_err(); + LianaPolicy::new( + prim_path.clone(), + PathInfo::Multi(2, vec![random_desc_key(), random_desc_key()]), + 1, + ) + .unwrap(); + LianaPolicy::new( + prim_path.clone(), + PathInfo::Multi(3, vec![random_desc_key(), random_desc_key()]), + 1, + ) + .unwrap_err(); + LianaPolicy::new( + prim_path.clone(), + PathInfo::Multi(3, twenty_keys.clone()), + 1, + ) + .unwrap(); + LianaPolicy::new(prim_path.clone(), PathInfo::Multi(20, twenty_keys), 1).unwrap(); + LianaPolicy::new(prim_path, PathInfo::Multi(20, twenty_one_keys), 1).unwrap_err(); } fn roundtrip(desc_str: &str) { diff --git a/src/signer.rs b/src/signer.rs index 6d415392..d2fe8cf6 100644 --- a/src/signer.rs +++ b/src/signer.rs @@ -409,9 +409,7 @@ mod tests { .unwrap(), wildcard: Wildcard::Unhardened, }); - let prim_keys = - descriptors::LianaDescKeys::from_multi(2, vec![prim_key_a, prim_key_b, prim_key_c]) - .unwrap(); + let prim_keys = descriptors::PathInfo::Multi(2, vec![prim_key_a, prim_key_b, prim_key_c]); let origin_der = bip32::DerivationPath::from_str("m/1/2'/3/4'").unwrap(); let xkey = recov_signer.xpub_at(&origin_der, &secp); let recov_key = DescriptorPublicKey::MultiXPub(DescriptorMultiXKey { @@ -424,8 +422,9 @@ mod tests { .unwrap(), wildcard: Wildcard::Unhardened, }); - let recov_keys = descriptors::LianaDescKeys::from_single(recov_key); - let desc = descriptors::MultipathDescriptor::new(prim_keys, recov_keys, 42).unwrap(); + let recov_keys = descriptors::PathInfo::Single(recov_key); + let policy = descriptors::LianaPolicy::new(prim_keys, recov_keys, 42).unwrap(); + let desc = descriptors::MultipathDescriptor::new(policy); // Create a dummy PSBT spending a coin from this descriptor with a single input and single // (external) output. We'll be modifying it as we go. diff --git a/src/testutils.rs b/src/testutils.rs index dcdb1018..4d4300cd 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -399,9 +399,10 @@ impl DummyLiana { poll_interval_secs: time::Duration::from_secs(2), }; - let owner_key = descriptors::LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap()); - let heir_key = descriptors::LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap()); - let desc = descriptors::MultipathDescriptor::new(owner_key, heir_key, 10_000).unwrap(); + let owner_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap()); + let heir_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap()); + let policy = descriptors::LianaPolicy::new(owner_key, heir_key, 10_000).unwrap(); + let desc = descriptors::MultipathDescriptor::new(policy); let config = Config { bitcoin_config, bitcoind_config: None, From f6885e358bfe78a790226e6246dad4922cf82d02 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 19:07:43 +0100 Subject: [PATCH 09/12] descriptors: cleanup error types This removes circular dependencies and apply the appropriate variants to the appropriate enums. --- src/descriptors/analysis.rs | 102 +++++++++++++++++++++++------------- src/descriptors/keys.rs | 4 -- src/descriptors/mod.rs | 26 +++------ 3 files changed, 75 insertions(+), 57 deletions(-) diff --git a/src/descriptors/analysis.rs b/src/descriptors/analysis.rs index 9d1b3bbe..07e34a1c 100644 --- a/src/descriptors/analysis.rs +++ b/src/descriptors/analysis.rs @@ -8,10 +8,46 @@ use miniscript::{ use std::{ collections::{HashMap, HashSet}, convert::TryFrom, - sync, + error, fmt, sync, }; -use crate::descriptors::{keys::DescKeyError, LianaDescError}; +#[derive(Debug)] +pub enum LianaPolicyError { + InsaneTimelock(u32), + InvalidKey(Box), + DuplicateKey(Box), + InvalidMultiThresh(usize), + InvalidMultiKeys(usize), + IncompatibleDesc, +} + +impl std::fmt::Display for LianaPolicyError { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + match self { + Self::InsaneTimelock(tl) => { + write!(f, "Timelock value '{}' isn't valid or safe to use", tl) + } + Self::InvalidKey(key) => { + write!( + f, + "Invalid key '{}'. Need a wildcard ('ranged') xpub with an origin and a multipath for (and only for) deriving change addresses. That is, an xpub of the form '[aaff0099]xpub.../<0;1>/*'.", + key + ) + } + Self::InvalidMultiThresh(thresh) => write!(f, "Invalid multisig threshold value '{}'. The threshold must be > to 0 and <= to the number of keys.", thresh), + Self::InvalidMultiKeys(n_keys) => write!(f, "Invalid number of keys '{}'. Between 2 and 20 keys must be given to use multiple keys in a specific path.", n_keys), + Self::DuplicateKey(key) => { + write!(f, "Duplicate key '{}'.", key) + } + Self::IncompatibleDesc => write!( + f, + "Descriptor is not compatible with a Liana spending policy." + ), + } + } +} + +impl error::Error for LianaPolicyError {} // Whether a Miniscript policy node represents a key check (or several of them). fn is_single_key_or_multisig(policy: &SemanticPolicy) -> bool { @@ -58,11 +94,11 @@ fn is_valid_desc_key(key: &descriptor::DescriptorPublicKey) -> bool { // // All this is achieved simply through asking for a 16-bit integer, since all the // above are signaled in leftmost bits. -fn csv_check(csv_value: u32) -> Result { +fn csv_check(csv_value: u32) -> Result { if csv_value > 0 { - u16::try_from(csv_value).map_err(|_| LianaDescError::InsaneTimelock(csv_value)) + u16::try_from(csv_value).map_err(|_| LianaPolicyError::InsaneTimelock(csv_value)) } else { - Err(LianaDescError::InsaneTimelock(csv_value)) + Err(LianaPolicyError::InsaneTimelock(csv_value)) } } @@ -90,20 +126,20 @@ impl PathInfo { /// descriptor (that is, a set of keys). pub fn from_primary_path( policy: SemanticPolicy, - ) -> Result { + ) -> Result { match policy { SemanticPolicy::Key(key) => Ok(PathInfo::Single(key)), SemanticPolicy::Threshold(k, subs) => { - let keys: Result<_, LianaDescError> = subs + let keys: Result<_, LianaPolicyError> = subs .into_iter() .map(|sub| match sub { SemanticPolicy::Key(key) => Ok(key), - _ => Err(LianaDescError::IncompatibleDesc), + _ => Err(LianaPolicyError::IncompatibleDesc), }) .collect(); Ok(PathInfo::Multi(k, keys?)) } - _ => Err(LianaDescError::IncompatibleDesc), + _ => Err(LianaPolicyError::IncompatibleDesc), } } @@ -112,14 +148,14 @@ impl PathInfo { /// descriptor (that is, a set of keys after a timelock). pub fn from_recovery_path( policy: SemanticPolicy, - ) -> Result<(u16, PathInfo), LianaDescError> { + ) -> Result<(u16, PathInfo), LianaPolicyError> { // The recovery spending path must always be a policy of type `thresh(2, older(x), thresh(n, key1, // key2, ..))`. In the special case n == 1, it is only `thresh(2, older(x), key)`. In the // special case n == len(keys) (i.e. it's an N-of-N multisig), it is normalized as // `thresh(n+1, older(x), key1, key2, ...)`. let (k, subs) = match policy { SemanticPolicy::Threshold(k, subs) => (k, subs), - _ => return Err(LianaDescError::IncompatibleDesc), + _ => return Err(LianaPolicyError::IncompatibleDesc), }; if k == 2 && subs.len() == 2 { // The general case (as well as the n == 1 case). The sub that is not the timelock is @@ -130,11 +166,11 @@ impl PathInfo { SemanticPolicy::Older(val) => Some(csv_check(val.0)), _ => None, }) - .ok_or(LianaDescError::IncompatibleDesc)??; + .ok_or(LianaPolicyError::IncompatibleDesc)??; let keys_sub = subs .into_iter() .find(is_single_key_or_multisig) - .ok_or(LianaDescError::IncompatibleDesc)?; + .ok_or(LianaPolicyError::IncompatibleDesc)?; PathInfo::from_primary_path(keys_sub).map(|info| (tl_value, info)) } else if k == subs.len() && subs.len() > 2 { // The N-of-N case. All subs but the threshold must be keys (if one had been thresh() @@ -146,22 +182,22 @@ impl PathInfo { SemanticPolicy::Key(key) => keys.push(key), SemanticPolicy::Older(val) => { if tl_value.is_some() { - return Err(LianaDescError::IncompatibleDesc); + return Err(LianaPolicyError::IncompatibleDesc); } tl_value = Some(csv_check(val.0)?); } - _ => return Err(LianaDescError::IncompatibleDesc), + _ => return Err(LianaPolicyError::IncompatibleDesc), } } assert!(keys.len() > 1); // At least 3 subs, only one of which may be older(). Ok(( - tl_value.ok_or(LianaDescError::IncompatibleDesc)?, + tl_value.ok_or(LianaPolicyError::IncompatibleDesc)?, PathInfo::Multi(k - 1, keys), )) } else { // If there is less than 2 subs, there can't be both a timelock and keys. If the // threshold is not equal to the number of subs, the timelock can't be mandatory. - Err(LianaDescError::IncompatibleDesc) + Err(LianaPolicyError::IncompatibleDesc) } } @@ -287,7 +323,7 @@ impl LianaPolicy { primary_path: PathInfo, recovery_path: PathInfo, recovery_timelock: u16, - ) -> Result { + ) -> Result { // We require the locktime to: // - not be disabled // - be in number of blocks @@ -296,21 +332,17 @@ impl LianaPolicy { // // All this is achieved through asking for a 16-bit integer. if recovery_timelock == 0 { - return Err(LianaDescError::InsaneTimelock(recovery_timelock as u32)); + return Err(LianaPolicyError::InsaneTimelock(recovery_timelock as u32)); } // If any of the paths is a multisig, make sure they are within the CHECKMULTISIG bounds. for path_info in &[&primary_path, &recovery_path] { if let PathInfo::Multi(thresh, keys) = path_info { if keys.len() < 2 || keys.len() > 20 { - return Err(LianaDescError::DescKey(DescKeyError::InvalidMultiKeys( - keys.len(), - ))); + return Err(LianaPolicyError::InvalidMultiKeys(keys.len())); } if thresh == &0 || thresh > &keys.len() { - return Err(LianaDescError::DescKey(DescKeyError::InvalidMultiThresh( - *thresh, - ))); + return Err(LianaPolicyError::InvalidMultiThresh(*thresh)); } } } @@ -319,7 +351,7 @@ impl LianaPolicy { let (prim_keys, rec_keys) = (primary_path.keys(), recovery_path.keys()); let all_keys = prim_keys.iter().chain(rec_keys.iter()); if let Some(key) = all_keys.clone().find(|k| !is_valid_desc_key(k)) { - return Err(LianaDescError::InvalidKey((*key).clone().into())); + return Err(LianaPolicyError::InvalidKey((*key).clone().into())); } // Check for key duplicates. They are invalid in (nonmalleable) miniscripts. @@ -330,7 +362,7 @@ impl LianaPolicy { _ => unreachable!("Just checked it was a multixpub above"), }; if key_set.contains(&xpub) { - return Err(LianaDescError::DuplicateKey(key.clone().into())); + return Err(LianaPolicyError::DuplicateKey(key.clone().into())); } key_set.insert(xpub); } @@ -346,18 +378,18 @@ impl LianaPolicy { /// (P2WSH, multipath, ..) and has a valid Liana semantic. pub fn from_multipath_descriptor( desc: &descriptor::Descriptor, - ) -> Result { + ) -> Result { // For now we only allow P2WSH descriptors. let wsh_desc = match &desc { descriptor::Descriptor::Wsh(desc) => desc, - _ => return Err(LianaDescError::IncompatibleDesc), + _ => return Err(LianaPolicyError::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), + _ => return Err(LianaPolicyError::IncompatibleDesc), }; let invalid_key = ms.iter_pk().find_map(|pk| { if is_valid_desc_key(&pk) { @@ -367,7 +399,7 @@ impl LianaPolicy { } }); if let Some(key) = invalid_key { - return Err(LianaDescError::InvalidKey(key.into())); + return Err(LianaPolicyError::InvalidKey(key.into())); } // Now lift a semantic policy out of this Miniscript and normalize it to make sure we @@ -382,9 +414,9 @@ impl LianaPolicy { SemanticPolicy::Threshold(1, subs) => Some(subs), _ => None, } - .ok_or(LianaDescError::IncompatibleDesc)?; + .ok_or(LianaPolicyError::IncompatibleDesc)?; if subs.len() != 2 { - return Err(LianaDescError::IncompatibleDesc); + return Err(LianaPolicyError::IncompatibleDesc); } // Fetch the two spending paths' semantic policies. The primary path is identified as the @@ -400,8 +432,8 @@ impl LianaPolicy { (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)?, + prim_path_sub.ok_or(LianaPolicyError::IncompatibleDesc)?, + reco_path_sub.ok_or(LianaPolicyError::IncompatibleDesc)?, ); // Now parse information about each spending path. diff --git a/src/descriptors/keys.rs b/src/descriptors/keys.rs index 422ef59b..29e1d13e 100644 --- a/src/descriptors/keys.rs +++ b/src/descriptors/keys.rs @@ -12,16 +12,12 @@ use std::{error, fmt, str}; #[derive(Debug)] pub enum DescKeyError { DerivedKeyParsing, - InvalidMultiThresh(usize), - InvalidMultiKeys(usize), } impl std::fmt::Display for DescKeyError { fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { match self { DescKeyError::DerivedKeyParsing => write!(f, "Parsing derived key"), - Self::InvalidMultiThresh(thresh) => write!(f, "Invalid threshold value '{}'. The threshold must be > to 0 and <= to the number of keys.", thresh), - Self::InvalidMultiKeys(n_keys) => write!(f, "Invalid number of keys '{}'. Between 2 and 20 keys must be given to use multiple keys in a specific path.", n_keys), } } } diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index 142787e5..e9ecee40 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -30,12 +30,9 @@ fn wu_to_vb(vb: usize) -> usize { #[derive(Debug)] pub enum LianaDescError { - InsaneTimelock(u32), - InvalidKey(Box), - DuplicateKey(Box), Miniscript(miniscript::Error), - IncompatibleDesc, DescKey(DescKeyError), + Policy(LianaPolicyError), /// Different number of PSBT vs tx inputs, etc.. InsanePsbt, /// Not all inputs' sequence the same, not all inputs signed with the same key, .. @@ -45,22 +42,9 @@ pub enum LianaDescError { impl std::fmt::Display for LianaDescError { fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { match self { - Self::InsaneTimelock(tl) => { - write!(f, "Timelock value '{}' isn't valid or safe to use", tl) - } - Self::InvalidKey(key) => { - write!( - f, - "Invalid key '{}'. Need a wildcard ('ranged') xpub with an origin and a multipath for (and only for) deriving change addresses. That is, an xpub of the form '[aaff0099]xpub.../<0;1>/*'.", - key - ) - } - Self::DuplicateKey(key) => { - write!(f, "Duplicate key '{}'.", key) - } Self::Miniscript(e) => write!(f, "Miniscript error: '{}'.", e), - Self::IncompatibleDesc => write!(f, "Descriptor is not compatible."), Self::DescKey(e) => write!(f, "{}", e), + Self::Policy(e) => write!(f, "{}", e), Self::InsanePsbt => write!(f, "Analyzed PSBT is empty or malformed."), Self::InconsistentPsbt => write!(f, "Analyzed PSBT is inconsistent across inputs."), } @@ -69,6 +53,12 @@ impl std::fmt::Display for LianaDescError { impl error::Error for LianaDescError {} +impl From for LianaDescError { + fn from(e: LianaPolicyError) -> LianaDescError { + LianaDescError::Policy(e) + } +} + /// An [InheritanceDescriptor] that contains multipath keys for (and only for) the receive keychain /// and the change keychain. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] From 8d1c6de5dde85583a6fb3a03458774d113ccb7b9 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 19:10:11 +0100 Subject: [PATCH 10/12] descriptors: rename MultipathDescriptor into LianaDescriptor --- src/bitcoin/d/mod.rs | 10 ++++----- src/bitcoin/mod.rs | 6 ++--- src/bitcoin/poller/looper.rs | 2 +- src/bitcoin/poller/mod.rs | 2 +- src/commands/mod.rs | 4 ++-- src/config.rs | 6 ++--- src/database/sqlite/mod.rs | 12 +++++----- src/database/sqlite/schema.rs | 6 ++--- src/descriptors/mod.rs | 42 +++++++++++++++++------------------ src/lib.rs | 4 ++-- src/signer.rs | 2 +- src/testutils.rs | 4 ++-- 12 files changed, 50 insertions(+), 50 deletions(-) diff --git a/src/bitcoin/d/mod.rs b/src/bitcoin/d/mod.rs index 27494502..32f4680c 100644 --- a/src/bitcoin/d/mod.rs +++ b/src/bitcoin/d/mod.rs @@ -5,7 +5,7 @@ mod utils; use crate::{ bitcoin::{Block, BlockChainTip}, config, - descriptors::MultipathDescriptor, + descriptors::LianaDescriptor, }; use utils::{block_before_date, roundup_progress}; @@ -485,7 +485,7 @@ impl BitcoinD { } // Import the receive and change descriptors from the multipath descriptor to bitcoind. - fn import_descriptor(&self, desc: &MultipathDescriptor) -> Option { + fn import_descriptor(&self, desc: &LianaDescriptor) -> Option { let descriptors = [desc.receive_descriptor(), desc.change_descriptor()] .iter() .map(|desc| { @@ -553,7 +553,7 @@ impl BitcoinD { /// Create the watchonly wallet on bitcoind, and import it the main descriptor. pub fn create_watchonly_wallet( &self, - main_descriptor: &MultipathDescriptor, + main_descriptor: &LianaDescriptor, ) -> Result<(), BitcoindError> { // Remove any leftover. This can happen if we delete the watchonly wallet but don't restart // bitcoind. @@ -627,7 +627,7 @@ impl BitcoinD { /// Perform various sanity checks of our watchonly wallet. pub fn wallet_sanity_checks( &self, - main_descriptor: &MultipathDescriptor, + main_descriptor: &LianaDescriptor, ) -> Result<(), BitcoindError> { // Check our watchonly wallet is loaded if self @@ -919,7 +919,7 @@ impl BitcoinD { pub fn start_rescan( &self, - desc: &MultipathDescriptor, + desc: &LianaDescriptor, timestamp: u32, ) -> Result<(), BitcoindError> { // Re-import the receive and change descriptors to the watchonly wallet for the purpose of diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index fe4b4608..20c22b0c 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -87,7 +87,7 @@ pub trait BitcoinInterface: Send { /// the given date. fn start_rescan( &self, - desc: &descriptors::MultipathDescriptor, + desc: &descriptors::LianaDescriptor, timestamp: u32, ) -> Result<(), String>; @@ -288,7 +288,7 @@ impl BitcoinInterface for d::BitcoinD { fn start_rescan( &self, - desc: &descriptors::MultipathDescriptor, + desc: &descriptors::LianaDescriptor, timestamp: u32, ) -> Result<(), String> { // FIXME: in theory i think this could potentially fail to actually start the rescan. @@ -374,7 +374,7 @@ impl BitcoinInterface for sync::Arc> fn start_rescan( &self, - desc: &descriptors::MultipathDescriptor, + desc: &descriptors::LianaDescriptor, timestamp: u32, ) -> Result<(), String> { self.lock().unwrap().start_rescan(desc, timestamp) diff --git a/src/bitcoin/poller/looper.rs b/src/bitcoin/poller/looper.rs index 2718be48..d94bc07c 100644 --- a/src/bitcoin/poller/looper.rs +++ b/src/bitcoin/poller/looper.rs @@ -300,7 +300,7 @@ pub fn looper( db: sync::Arc>, shutdown: sync::Arc, poll_interval: time::Duration, - desc: descriptors::MultipathDescriptor, + desc: descriptors::LianaDescriptor, ) { let mut last_poll = None; let mut synced = false; diff --git a/src/bitcoin/poller/mod.rs b/src/bitcoin/poller/mod.rs index 46cbb60d..95a20535 100644 --- a/src/bitcoin/poller/mod.rs +++ b/src/bitcoin/poller/mod.rs @@ -22,7 +22,7 @@ impl Poller { bit: sync::Arc>, db: sync::Arc>, poll_interval: time::Duration, - desc: descriptors::MultipathDescriptor, + desc: descriptors::LianaDescriptor, ) -> Poller { let shutdown = sync::Arc::from(atomic::AtomicBool::from(false)); let handle = thread::Builder::new() diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 07d5ed04..7449b8fe 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -155,7 +155,7 @@ pub enum InsaneFeeInfo { // Apply some sanity checks on a created transaction's PSBT. // TODO: add more sanity checks from revault_tx fn sanity_check_psbt( - spent_desc: &descriptors::MultipathDescriptor, + spent_desc: &descriptors::LianaDescriptor, psbt: &Psbt, ) -> Result<(), CommandError> { let tx = &psbt.unsigned_tx; @@ -783,7 +783,7 @@ impl DaemonControl { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GetInfoDescriptors { - pub main: descriptors::MultipathDescriptor, + pub main: descriptors::LianaDescriptor, } /// Information about the daemon diff --git a/src/config.rs b/src/config.rs index 335401f0..46152b88 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use crate::descriptors::MultipathDescriptor; +use crate::descriptors::LianaDescriptor; use std::{net::SocketAddr, path::PathBuf, str::FromStr, time::Duration}; @@ -91,7 +91,7 @@ pub struct Config { deserialize_with = "deserialize_fromstr", serialize_with = "serialize_to_string" )] - pub main_descriptor: MultipathDescriptor, + pub main_descriptor: LianaDescriptor, /// Settings for the Bitcoin interface pub bitcoin_config: BitcoinConfig, /// Settings specific to bitcoind as the Bitcoin interface @@ -112,7 +112,7 @@ pub enum ConfigError { DatadirNotFound, FileNotFound, ReadingFile(String), - UnexpectedDescriptor(Box), + UnexpectedDescriptor(Box), Unexpected(String), } diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index 17663a13..dedd79e9 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -18,7 +18,7 @@ use crate::{ }, Coin, CoinType, }, - descriptors::MultipathDescriptor, + descriptors::LianaDescriptor, }; use std::{cmp, convert::TryInto, fmt, io, path}; @@ -39,7 +39,7 @@ pub enum SqliteDbError { FileNotFound(path::PathBuf), UnsupportedVersion(i64), InvalidNetwork(bitcoin::Network), - DescriptorMismatch(Box), + DescriptorMismatch(Box), Rusqlite(rusqlite::Error), } @@ -83,7 +83,7 @@ impl From for SqliteDbError { #[derive(Debug, Clone)] pub struct FreshDbOptions { pub bitcoind_network: bitcoin::Network, - pub main_descriptor: MultipathDescriptor, + pub main_descriptor: LianaDescriptor, } #[derive(Debug, Clone)] @@ -122,7 +122,7 @@ impl SqliteDb { pub fn sanity_check( &self, bitcoind_network: bitcoin::Network, - main_descriptor: &MultipathDescriptor, + main_descriptor: &LianaDescriptor, ) -> Result<(), SqliteDbError> { let mut conn = self.connection()?; @@ -597,7 +597,7 @@ mod tests { fn dummy_options() -> FreshDbOptions { let desc_str = "wsh(andor(pk([aabbccdd]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#dw4ulnrs"; - let main_descriptor = MultipathDescriptor::from_str(desc_str).unwrap(); + let main_descriptor = LianaDescriptor::from_str(desc_str).unwrap(); FreshDbOptions { bitcoind_network: bitcoin::Network::Bitcoin, main_descriptor, @@ -646,7 +646,7 @@ mod tests { .contains("Database was created for network"); fs::remove_file(&db_path).unwrap(); let other_desc_str = "wsh(andor(pk([aabbccdd]tpubDExU4YLJkyQ9RRbVScQq2brFxWWha7WmAUByPWyaWYwmcTv3Shx8aHp6mVwuE5n4TeM4z5DTWGf2YhNPmXtfvyr8cUDVvA3txdrFnFgNdF7/<0;1>/*),older(10000),pk([aabbccdd]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))"; - let other_desc = MultipathDescriptor::from_str(other_desc_str).unwrap(); + let other_desc = LianaDescriptor::from_str(other_desc_str).unwrap(); let db = SqliteDb::new(db_path.clone(), Some(options.clone()), &secp).unwrap(); db.sanity_check(bitcoin::Network::Bitcoin, &other_desc) .unwrap_err() diff --git a/src/database/sqlite/schema.rs b/src/database/sqlite/schema.rs index a4a51feb..65ed1670 100644 --- a/src/database/sqlite/schema.rs +++ b/src/database/sqlite/schema.rs @@ -1,4 +1,4 @@ -use crate::descriptors::MultipathDescriptor; +use crate::descriptors::LianaDescriptor; use std::{convert::TryFrom, str::FromStr}; @@ -112,7 +112,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbTip { pub struct DbWallet { pub id: i64, pub timestamp: u32, - pub main_descriptor: MultipathDescriptor, + pub main_descriptor: LianaDescriptor, pub deposit_derivation_index: bip32::ChildNumber, pub change_derivation_index: bip32::ChildNumber, pub rescan_timestamp: Option, @@ -126,7 +126,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbWallet { let timestamp = row.get(1)?; let desc_str: String = row.get(2)?; - let main_descriptor = MultipathDescriptor::from_str(&desc_str) + let main_descriptor = LianaDescriptor::from_str(&desc_str) .expect("Insane database: can't parse deposit descriptor"); let der_idx: u32 = row.get(3)?; diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index e9ecee40..3744b87f 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -62,7 +62,7 @@ impl From for LianaDescError { /// An [InheritanceDescriptor] that contains multipath keys for (and only for) the receive keychain /// and the change keychain. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct MultipathDescriptor { +pub struct LianaDescriptor { multi_desc: descriptor::Descriptor, receive_desc: InheritanceDescriptor, change_desc: InheritanceDescriptor, @@ -77,16 +77,16 @@ pub struct InheritanceDescriptor(descriptor::Descriptor); -impl fmt::Display for MultipathDescriptor { +impl fmt::Display for LianaDescriptor { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.multi_desc) } } -impl str::FromStr for MultipathDescriptor { +impl str::FromStr for LianaDescriptor { type Err = LianaDescError; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { // Parse a descriptor and check it is a multipath descriptor corresponding to a valid Liana // spending policy. let desc = descriptor::Descriptor::::from_str(s) @@ -106,7 +106,7 @@ impl str::FromStr for MultipathDescriptor { let receive_desc = InheritanceDescriptor(singlepath_descs.next().expect("First of 2")); let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2")); - Ok(MultipathDescriptor { + Ok(LianaDescriptor { multi_desc: desc, receive_desc, change_desc, @@ -126,8 +126,8 @@ impl PartialEq> for Inhe } } -impl MultipathDescriptor { - pub fn new(spending_policy: LianaPolicy) -> MultipathDescriptor { +impl LianaDescriptor { + pub fn new(spending_policy: LianaPolicy) -> LianaDescriptor { // Get the descriptor from the chosen spending policy. let multi_desc = spending_policy.into_multipath_descriptor(); @@ -144,7 +144,7 @@ impl MultipathDescriptor { let receive_desc = InheritanceDescriptor(singlepath_descs.next().expect("First of 2")); let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2")); - MultipathDescriptor { + LianaDescriptor { multi_desc, receive_desc, change_desc, @@ -400,7 +400,7 @@ mod tests { let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap()); let timelock = 52560; let policy = LianaPolicy::new(owner_key.clone(), heir_key.clone(), timelock).unwrap(); - assert_eq!(MultipathDescriptor::new(policy).to_string(), "wsh(or_d(pk([abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh([abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#g7vk9r5l"); + assert_eq!(LianaDescriptor::new(policy).to_string(), "wsh(or_d(pk([abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh([abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#g7vk9r5l"); // A decaying multisig after 6 months. Note we can't duplicate the keys, so different ones // are used. In practice they would both be controlled by the same entity. @@ -421,7 +421,7 @@ mod tests { ], ); let policy = LianaPolicy::new(primary_keys, recovery_keys, 26352).unwrap(); - assert_eq!(MultipathDescriptor::new(policy).to_string(), "wsh(or_d(multi(3,[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,[abcdef01]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#s0zsa6uc"); + assert_eq!(LianaDescriptor::new(policy).to_string(), "wsh(or_d(multi(3,[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,[abcdef01]xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#s0zsa6uc"); // We prevent footguns with timelocks by requiring a u16. Note how the following wouldn't // compile: @@ -436,7 +436,7 @@ mod tests { let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*").unwrap()); let timelock = 57600; let policy = LianaPolicy::new(owner_key.clone(), heir_key, timelock).unwrap(); - assert_eq!(MultipathDescriptor::new(policy).to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*),and_v(v:pkh([abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*),older(57600))))#ak4cm093"); + assert_eq!(LianaDescriptor::new(policy).to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*),and_v(v:pkh([abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*),older(57600))))#ak4cm093"); // We can't pass a raw key, an xpub that is not deriveable, only hardened derivable, // without both the change and receive derivation paths, or with more than 2 different @@ -514,7 +514,7 @@ mod tests { #[test] fn inheritance_descriptor_derivation() { let secp = secp256k1::Secp256k1::verification_only(); - let desc = MultipathDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#2qj59a9y").unwrap(); + let desc = LianaDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#2qj59a9y").unwrap(); let der_desc = desc.receive_descriptor().derive(11.into(), &secp); assert_eq!( "bc1q26gtczlz03u6juf5cxppapk4sr4fyz53s3g4zs2cgactcahqv6yqc2t8e6", @@ -530,21 +530,21 @@ mod tests { #[test] fn inheritance_descriptor_tl_value() { // Must always contain at least one timelocked path. - MultipathDescriptor::from_str("wsh(or_i(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap_err(); + LianaDescriptor::from_str("wsh(or_i(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap_err(); - let desc = MultipathDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap(); + let desc = LianaDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap(); assert_eq!(desc.timelock_value(), 1); - let desc = MultipathDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(42000),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap(); + let desc = LianaDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(42000),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap(); assert_eq!(desc.timelock_value(), 42000); - let desc = MultipathDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(65535),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap(); + let desc = LianaDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(65535),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap(); assert_eq!(desc.timelock_value(), 0xffff); } #[test] fn inheritance_descriptor_sat_size() { - let desc = MultipathDescriptor::from_str("wsh(or_d(pk([92162c45]tpubD6NzVbkrYhZ4WzTf9SsD6h7AH7oQEippXK2KP8qvhMMqFoNeN5YFVi7vRyeRSDGtgd2bPyMxUNmHui8t5yCgszxPPxMafu1VVzDpg9aruYW/<0;1>/*),and_v(v:pkh([abcdef01]tpubD6NzVbkrYhZ4Wdgu2yfdmrce5g4fiH1ZLmKhewsnNKupbi4sxjH1ZVAorkBLWSkhsjhg8kiq8C4BrBjMy3SjAKDyDdbuvUa1ToAHbiR98js/<0;1>/*),older(2))))#ravw7jw5").unwrap(); + let desc = LianaDescriptor::from_str("wsh(or_d(pk([92162c45]tpubD6NzVbkrYhZ4WzTf9SsD6h7AH7oQEippXK2KP8qvhMMqFoNeN5YFVi7vRyeRSDGtgd2bPyMxUNmHui8t5yCgszxPPxMafu1VVzDpg9aruYW/<0;1>/*),and_v(v:pkh([abcdef01]tpubD6NzVbkrYhZ4Wdgu2yfdmrce5g4fiH1ZLmKhewsnNKupbi4sxjH1ZVAorkBLWSkhsjhg8kiq8C4BrBjMy3SjAKDyDdbuvUa1ToAHbiR98js/<0;1>/*),older(2))))#ravw7jw5").unwrap(); assert_eq!(desc.max_sat_vbytes(), (1 + 69 + 1 + 34 + 73 + 3) / 4); // See the stack details below. // Maximum input size is (txid + vout + scriptsig + nSequence + max_sat). @@ -633,7 +633,7 @@ mod tests { } fn roundtrip(desc_str: &str) { - let desc = MultipathDescriptor::from_str(desc_str).unwrap(); + let desc = LianaDescriptor::from_str(desc_str).unwrap(); assert_eq!(desc.to_string(), desc_str); } @@ -656,7 +656,7 @@ mod tests { #[test] fn partial_spend_info() { // A simple descriptor with 1 keys as primary path and 1 recovery key. - let desc = MultipathDescriptor::from_str("wsh(or_d(pk([f5acc2fd]tpubD6NzVbkrYhZ4YgUx2ZLNt2rLYAMTdYysCRzKoLu2BeSHKvzqPaBDvf17GeBPnExUVPkuBpx4kniP964e2MxyzzazcXLptxLXModSVCVEV1T/<0;1>/*),and_v(v:pkh([8a64f2a9]tpubD6NzVbkrYhZ4WmzFjvQrp7sDa4ECUxTi9oby8K4FZkd3XCBtEdKwUiQyYJaxiJo5y42gyDWEczrFpozEjeLxMPxjf2WtkfcbpUdfvNnozWF/<0;1>/*),older(10))))#d72le4dr").unwrap(); + let desc = LianaDescriptor::from_str("wsh(or_d(pk([f5acc2fd]tpubD6NzVbkrYhZ4YgUx2ZLNt2rLYAMTdYysCRzKoLu2BeSHKvzqPaBDvf17GeBPnExUVPkuBpx4kniP964e2MxyzzazcXLptxLXModSVCVEV1T/<0;1>/*),and_v(v:pkh([8a64f2a9]tpubD6NzVbkrYhZ4WmzFjvQrp7sDa4ECUxTi9oby8K4FZkd3XCBtEdKwUiQyYJaxiJo5y42gyDWEczrFpozEjeLxMPxjf2WtkfcbpUdfvNnozWF/<0;1>/*),older(10))))#d72le4dr").unwrap(); let desc_info = desc.policy(); let prim_key_origin = ( bip32::Fingerprint::from_str("f5acc2fd").unwrap(), @@ -816,7 +816,7 @@ mod tests { .contains("Analyzed PSBT is inconsistent across inputs.")); // If we analyze a descriptor with a multisig we'll get the right threshold. - let desc = MultipathDescriptor::from_str("wsh(or_d(multi(2,[f5acc2fd]tpubD6NzVbkrYhZ4YgUx2ZLNt2rLYAMTdYysCRzKoLu2BeSHKvzqPaBDvf17GeBPnExUVPkuBpx4kniP964e2MxyzzazcXLptxLXModSVCVEV1T/<0;1>/*,[00112233]xpub6FC8vmQGGfSuQGfKG5L73fZ7WjXit8TzfJYDKwTtHkhrbAhU5Kma41oenVq6aMnpgULJRXpQuxnVysyfdpRhVgD6vYe7XLbFDhmvYmDrAVq/<0;1>/*,[aabbccdd]xpub68XtbpvDM19d39wEKdvadHkZ4FGKf4tnryKzAacttp8BLX3uHj7eK8shRnFBhZ2UL83S9dwXe42Qm6eG6BkR1jy8XwUSNBcHKtET7j4V5FB/<0;1>/*),and_v(v:pkh([8a64f2a9]tpubD6NzVbkrYhZ4WmzFjvQrp7sDa4ECUxTi9oby8K4FZkd3XCBtEdKwUiQyYJaxiJo5y42gyDWEczrFpozEjeLxMPxjf2WtkfcbpUdfvNnozWF/<0;1>/*),older(10))))#2kgxuax5").unwrap(); + let desc = LianaDescriptor::from_str("wsh(or_d(multi(2,[f5acc2fd]tpubD6NzVbkrYhZ4YgUx2ZLNt2rLYAMTdYysCRzKoLu2BeSHKvzqPaBDvf17GeBPnExUVPkuBpx4kniP964e2MxyzzazcXLptxLXModSVCVEV1T/<0;1>/*,[00112233]xpub6FC8vmQGGfSuQGfKG5L73fZ7WjXit8TzfJYDKwTtHkhrbAhU5Kma41oenVq6aMnpgULJRXpQuxnVysyfdpRhVgD6vYe7XLbFDhmvYmDrAVq/<0;1>/*,[aabbccdd]xpub68XtbpvDM19d39wEKdvadHkZ4FGKf4tnryKzAacttp8BLX3uHj7eK8shRnFBhZ2UL83S9dwXe42Qm6eG6BkR1jy8XwUSNBcHKtET7j4V5FB/<0;1>/*),and_v(v:pkh([8a64f2a9]tpubD6NzVbkrYhZ4WmzFjvQrp7sDa4ECUxTi9oby8K4FZkd3XCBtEdKwUiQyYJaxiJo5y42gyDWEczrFpozEjeLxMPxjf2WtkfcbpUdfvNnozWF/<0;1>/*),older(10))))#2kgxuax5").unwrap(); let info = desc.partial_spend_info(&psbt).unwrap(); assert!(psbt .inputs @@ -833,7 +833,7 @@ mod tests { ); assert!(info.recovery_path.is_none()); - let desc = MultipathDescriptor::from_str("wsh(or_d(multi(2,[636adf3f/48'/1'/0'/2']tpubDEE9FvWbG4kg4gxDNrALgrWLiHwNMXNs8hk6nXNPw4VHKot16xd2251vwi2M6nsyQTkak5FJNHVHkCcuzmvpSbWHdumX3DxpDm89iTfSBaL/<0;1>/*,[ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/<0;1>/*),and_v(v:multi(2,[636adf3f/48'/1'/1'/2']tpubDDvF2khuoBBj8vcSjQfa7iKaxsQZE7YjJ7cJL8A8eaneadMPKbHSpoSr4JD1F5LUvWD82HCxdtSppGfrMUmiNbFxrA2EHEVLnrdCFNFe75D/<0;1>/*,[ffd63c8d/48'/1'/1'/2']tpubDFMs44FD4kFt3M7Z317cFh5tdKEGN8tyQRY6Q5gcSha4NtxZfGmTVRMbsD1bWN469LstXU4aVSARDxrvxFCUjHeegfEY2cLSazMBkNCmDPD/<0;1>/*),older(2))))#xcf6jr2r").unwrap(); + let desc = LianaDescriptor::from_str("wsh(or_d(multi(2,[636adf3f/48'/1'/0'/2']tpubDEE9FvWbG4kg4gxDNrALgrWLiHwNMXNs8hk6nXNPw4VHKot16xd2251vwi2M6nsyQTkak5FJNHVHkCcuzmvpSbWHdumX3DxpDm89iTfSBaL/<0;1>/*,[ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/<0;1>/*),and_v(v:multi(2,[636adf3f/48'/1'/1'/2']tpubDDvF2khuoBBj8vcSjQfa7iKaxsQZE7YjJ7cJL8A8eaneadMPKbHSpoSr4JD1F5LUvWD82HCxdtSppGfrMUmiNbFxrA2EHEVLnrdCFNFe75D/<0;1>/*,[ffd63c8d/48'/1'/1'/2']tpubDFMs44FD4kFt3M7Z317cFh5tdKEGN8tyQRY6Q5gcSha4NtxZfGmTVRMbsD1bWN469LstXU4aVSARDxrvxFCUjHeegfEY2cLSazMBkNCmDPD/<0;1>/*),older(2))))#xcf6jr2r").unwrap(); let info = desc.policy(); assert_eq!(info.primary_path, PathInfo::Multi( 2, diff --git a/src/lib.rs b/src/lib.rs index bee7934d..a2906372 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -464,7 +464,7 @@ mod tests { use super::*; use crate::{ config::{BitcoinConfig, BitcoindConfig}, - descriptors::MultipathDescriptor, + descriptors::LianaDescriptor, testutils::*, }; @@ -686,7 +686,7 @@ mod tests { // Create a dummy config with this bitcoind let desc_str = "wsh(andor(pk([aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*),older(10000),pk([aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*)))#3xh8xmhn"; - let desc = MultipathDescriptor::from_str(desc_str).unwrap(); + let desc = LianaDescriptor::from_str(desc_str).unwrap(); let receive_desc = desc.receive_descriptor().clone(); let change_desc = desc.change_descriptor().clone(); let config = Config { diff --git a/src/signer.rs b/src/signer.rs index d2fe8cf6..646eabdd 100644 --- a/src/signer.rs +++ b/src/signer.rs @@ -424,7 +424,7 @@ mod tests { }); let recov_keys = descriptors::PathInfo::Single(recov_key); let policy = descriptors::LianaPolicy::new(prim_keys, recov_keys, 42).unwrap(); - let desc = descriptors::MultipathDescriptor::new(policy); + let desc = descriptors::LianaDescriptor::new(policy); // Create a dummy PSBT spending a coin from this descriptor with a single input and single // (external) output. We'll be modifying it as we go. diff --git a/src/testutils.rs b/src/testutils.rs index 4d4300cd..33d5e8dd 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -91,7 +91,7 @@ impl BitcoinInterface for DummyBitcoind { todo!() } - fn start_rescan(&self, _: &descriptors::MultipathDescriptor, _: u32) -> Result<(), String> { + fn start_rescan(&self, _: &descriptors::LianaDescriptor, _: u32) -> Result<(), String> { todo!() } @@ -402,7 +402,7 @@ impl DummyLiana { let owner_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap()); let heir_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap()); let policy = descriptors::LianaPolicy::new(owner_key, heir_key, 10_000).unwrap(); - let desc = descriptors::MultipathDescriptor::new(policy); + let desc = descriptors::LianaDescriptor::new(policy); let config = Config { bitcoin_config, bitcoind_config: None, From 1a13b7a6f820e92ff436198bffc78b8ad785a758 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 23 Mar 2023 19:11:39 +0100 Subject: [PATCH 11/12] descriptors: rename InheritanceDescriptor into SinglePathLianaDesc It was named at a time where there was an over emphasis on inheritance as a Liana usecase. In addition, "SinglePath" reflects better it is only one part of the main, multipath, Liana descriptor. --- src/bitcoin/mod.rs | 6 +++--- src/bitcoin/poller/looper.rs | 6 +++--- src/commands/mod.rs | 2 +- src/descriptors/mod.rs | 34 +++++++++++++++++----------------- src/testutils.rs | 2 +- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index 20c22b0c..9aa2732e 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -55,7 +55,7 @@ pub trait BitcoinInterface: Send { fn received_coins( &self, tip: &BlockChainTip, - descs: &[descriptors::InheritanceDescriptor], + descs: &[descriptors::SinglePathLianaDesc], ) -> Vec; /// Get all coins that were confirmed, and at what height and time. Along with "expired" @@ -131,7 +131,7 @@ impl BitcoinInterface for d::BitcoinD { fn received_coins( &self, tip: &BlockChainTip, - descs: &[descriptors::InheritanceDescriptor], + descs: &[descriptors::SinglePathLianaDesc], ) -> Vec { let lsb_res = self.list_since_block(&tip.hash); @@ -338,7 +338,7 @@ impl BitcoinInterface for sync::Arc> fn received_coins( &self, tip: &BlockChainTip, - descs: &[descriptors::InheritanceDescriptor], + descs: &[descriptors::SinglePathLianaDesc], ) -> Vec { self.lock().unwrap().received_coins(tip, descs) } diff --git a/src/bitcoin/poller/looper.rs b/src/bitcoin/poller/looper.rs index d94bc07c..99ec8a3d 100644 --- a/src/bitcoin/poller/looper.rs +++ b/src/bitcoin/poller/looper.rs @@ -28,7 +28,7 @@ fn update_coins( bit: &impl BitcoinInterface, db_conn: &mut Box, previous_tip: &BlockChainTip, - descs: &[descriptors::InheritanceDescriptor], + descs: &[descriptors::SinglePathLianaDesc], secp: &secp256k1::Secp256k1, ) -> UpdatedCoins { let curr_coins = db_conn.coins(CoinType::All); @@ -189,7 +189,7 @@ fn new_tip(bit: &impl BitcoinInterface, current_tip: &BlockChainTip) -> TipUpdat fn updates( bit: &impl BitcoinInterface, db: &impl DatabaseInterface, - descs: &[descriptors::InheritanceDescriptor], + descs: &[descriptors::SinglePathLianaDesc], secp: &secp256k1::Secp256k1, ) { let mut db_conn = db.connection(); @@ -238,7 +238,7 @@ fn updates( fn rescan_check( bit: &impl BitcoinInterface, db: &impl DatabaseInterface, - descs: &[descriptors::InheritanceDescriptor], + descs: &[descriptors::SinglePathLianaDesc], secp: &secp256k1::Secp256k1, ) { log::debug!("Checking the state of an ongoing rescan if there is any"); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 7449b8fe..379141f8 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -216,7 +216,7 @@ fn serializable_size(t: &T) -> u64 { impl DaemonControl { // Get the derived descriptor for this coin - fn derived_desc(&self, coin: &Coin) -> descriptors::DerivedInheritanceDescriptor { + fn derived_desc(&self, coin: &Coin) -> descriptors::DerivedSinglePathLianaDesc { let desc = if coin.is_change { self.config.main_descriptor.change_descriptor() } else { diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index 3744b87f..05b55bab 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -59,23 +59,23 @@ impl From for LianaDescError { } } -/// An [InheritanceDescriptor] that contains multipath keys for (and only for) the receive keychain +/// An [SinglePathLianaDesc] that contains multipath keys for (and only for) the receive keychain /// and the change keychain. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct LianaDescriptor { multi_desc: descriptor::Descriptor, - receive_desc: InheritanceDescriptor, - change_desc: InheritanceDescriptor, + receive_desc: SinglePathLianaDesc, + change_desc: SinglePathLianaDesc, } /// A Miniscript descriptor with a main, unencombered, branch (the main owner of the coins) /// and a timelocked branch (the heir). All keys in this descriptor are singlepath. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct InheritanceDescriptor(descriptor::Descriptor); +pub struct SinglePathLianaDesc(descriptor::Descriptor); /// Derived (containing only raw Bitcoin public keys) version of the inheritance descriptor. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct DerivedInheritanceDescriptor(descriptor::Descriptor); +pub struct DerivedSinglePathLianaDesc(descriptor::Descriptor); impl fmt::Display for LianaDescriptor { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -103,8 +103,8 @@ impl str::FromStr for LianaDescriptor { .expect("Can't error, all paths have the same length") .into_iter(); assert_eq!(singlepath_descs.len(), 2); - let receive_desc = InheritanceDescriptor(singlepath_descs.next().expect("First of 2")); - let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2")); + let receive_desc = SinglePathLianaDesc(singlepath_descs.next().expect("First of 2")); + let change_desc = SinglePathLianaDesc(singlepath_descs.next().expect("Second of 2")); Ok(LianaDescriptor { multi_desc: desc, @@ -114,13 +114,13 @@ impl str::FromStr for LianaDescriptor { } } -impl fmt::Display for InheritanceDescriptor { +impl fmt::Display for SinglePathLianaDesc { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } -impl PartialEq> for InheritanceDescriptor { +impl PartialEq> for SinglePathLianaDesc { fn eq(&self, other: &descriptor::Descriptor) -> bool { self.0.eq(other) } @@ -141,8 +141,8 @@ impl LianaDescriptor { .expect("Can't error, all paths have the same length") .into_iter(); assert_eq!(singlepath_descs.len(), 2); - let receive_desc = InheritanceDescriptor(singlepath_descs.next().expect("First of 2")); - let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2")); + let receive_desc = SinglePathLianaDesc(singlepath_descs.next().expect("First of 2")); + let change_desc = SinglePathLianaDesc(singlepath_descs.next().expect("Second of 2")); LianaDescriptor { multi_desc, @@ -163,12 +163,12 @@ impl LianaDescriptor { } /// Get the descriptor for receiving addresses. - pub fn receive_descriptor(&self) -> &InheritanceDescriptor { + pub fn receive_descriptor(&self) -> &SinglePathLianaDesc { &self.receive_desc } /// Get the descriptor for change addresses. - pub fn change_descriptor(&self) -> &InheritanceDescriptor { + pub fn change_descriptor(&self) -> &SinglePathLianaDesc { &self.change_desc } @@ -289,7 +289,7 @@ impl LianaDescriptor { } } -impl InheritanceDescriptor { +impl SinglePathLianaDesc { /// Derive this descriptor at a given index for a receiving address. /// /// # Panics @@ -298,7 +298,7 @@ impl InheritanceDescriptor { &self, index: bip32::ChildNumber, secp: &secp256k1::Secp256k1, - ) -> DerivedInheritanceDescriptor { + ) -> DerivedSinglePathLianaDesc { assert!(index.is_normal()); // Unfortunately we can't just use `self.0.at_derivation_index().derived_descriptor()` @@ -338,7 +338,7 @@ impl InheritanceDescriptor { ); } - DerivedInheritanceDescriptor( + DerivedSinglePathLianaDesc( self.0 .translate_pk(&mut Derivator(index.into(), secp)) .expect( @@ -351,7 +351,7 @@ impl InheritanceDescriptor { /// Map of a raw public key to the xpub used to derive it and its derivation path pub type Bip32Deriv = BTreeMap; -impl DerivedInheritanceDescriptor { +impl DerivedSinglePathLianaDesc { pub fn address(&self, network: bitcoin::Network) -> bitcoin::Address { self.0 .address(network) diff --git a/src/testutils.rs b/src/testutils.rs index 33d5e8dd..482fdf67 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -60,7 +60,7 @@ impl BitcoinInterface for DummyBitcoind { fn received_coins( &self, _: &BlockChainTip, - _: &[descriptors::InheritanceDescriptor], + _: &[descriptors::SinglePathLianaDesc], ) -> Vec { Vec::new() } From 9394be645c698591da9c477dd77363010cb3298e Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Mon, 27 Mar 2023 16:56:12 +0200 Subject: [PATCH 12/12] [bugfix] descriptors: fix parsing of descriptor with 1-of-N multisig --- src/descriptors/analysis.rs | 62 ++++++++++++++++++++++++------------- src/descriptors/mod.rs | 3 ++ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/descriptors/analysis.rs b/src/descriptors/analysis.rs index 07e34a1c..c34ea2e6 100644 --- a/src/descriptors/analysis.rs +++ b/src/descriptors/analysis.rs @@ -201,6 +201,17 @@ impl PathInfo { } } + /// Add another available key to this `PathInfo`. Note this doesn't change the threshold. + pub fn with_added_key(mut self, key: descriptor::DescriptorPublicKey) -> Self { + match self { + Self::Single(curr_key) => Self::Multi(1, vec![curr_key, key]), + Self::Multi(_, ref mut keys) => { + keys.push(key); + self + } + } + } + /// Get the required number of keys for spending through this path, and the set of keys /// that can be used to provide a signature for this path. pub fn thresh_origins(&self) -> (usize, HashSet<(bip32::Fingerprint, bip32::DerivationPath)>) { @@ -409,40 +420,47 @@ impl LianaPolicy { .expect("Lifting can't fail on a Miniscript") .normalized(); - // For now we only accept a single timelocked recovery path. + // The policy must always be "1 of N spending paths" with at least an always-available + // primary path with at least one key, and at least one timelocked recovery path with at + // least one key. let subs = match policy { SemanticPolicy::Threshold(1, subs) => Some(subs), _ => None, } .ok_or(LianaPolicyError::IncompatibleDesc)?; - if subs.len() != 2 { - return Err(LianaPolicyError::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); + let (mut primary_path, mut recovery_path) = (None::, None); + for sub in subs { + // This is a (multi)key check. It must be the primary path. + if is_single_key_or_multisig(&sub) { + // We only support a single primary path. But it may be that the primary path is a + // 1-of-N multisig. In this case the policy is normalized from `thresh(1, thresh(1, + // pk(A), pk(B)), thresh(2, older(42), pk(C)))` to `thresh(1, pk(A), pk(B), + // thresh(2, older(42), pk(C)))`. + if let Some(prim_path) = primary_path { + if let SemanticPolicy::Key(key) = sub { + primary_path = Some(prim_path.with_added_key(key)); } else { - reco_sub = Some(sub); + return Err(LianaPolicyError::IncompatibleDesc); } - (prim_sub, reco_sub) - }); - let (prim_path_sub, reco_path_sub) = ( - prim_path_sub.ok_or(LianaPolicyError::IncompatibleDesc)?, - reco_path_sub.ok_or(LianaPolicyError::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)?; + } else { + primary_path = Some(PathInfo::from_primary_path(sub)?); + } + } else { + // If it's not a simple (multi)key check, it must be the timelocked recovery path. + // For now, we only support a single recovery path. + if recovery_path.is_some() { + return Err(LianaPolicyError::IncompatibleDesc); + } + recovery_path = Some(PathInfo::from_recovery_path(sub)?); + } + } Ok(LianaPolicy { - primary_path, - recovery_path, + primary_path: primary_path.ok_or(LianaPolicyError::IncompatibleDesc)?, + recovery_path: recovery_path.ok_or(LianaPolicyError::IncompatibleDesc)?, }) } diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index 05b55bab..b8d169ea 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -509,6 +509,9 @@ mod tests { let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap()); let timelock = 52560; LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err(); + + // A 1-of-N multisig as primary path. + LianaDescriptor::from_str("wsh(or_d(multi(1,[573fb35b/48'/1'/0'/2']tpubDFKp9T7WAYDcENSjoifkrpq1gMDF47KGJcJrpxzX23Qor8wuGbrEVs9utNq1MDS8E2WXJSBk1qoPQLpwyokW7DiUNPwFuxQkL7owNkLAb9W/<0;1>/*,[573fb35b/48'/1'/1'/2']tpubDFGezyzuHJPhdP3jHGW7v7Hwes4Hihqv5W2yyCmRY9VZJCRchETvxrMC8uECeJZdxQ14V4iD4DecoArkUSDwj8ogYE9WEv4MNZr12thNHCs/<0;1>/*),and_v(v:multi(2,[573fb35b/48'/1'/2'/2']tpubDDwxQauiaU964vPzt5Vd7jnDHEUtp2Vc34PaWpEXg5TQ3bRccxnc1MKKh88Hi7xiMeZo9Tm6fBcq4UGXqnDtGUniJLjqAD8SjQ8Eci3aSR7/<0;1>/*,[573fb35b/48'/1'/3'/2']tpubDE37XAVB5CQ1x85md3BQ5uHCoMwT5fgT8X13zzCUQ3x5o2jskYxKjj7Qcxt1Jpj4QB8tqspn2dooPCekRuQDYrDHov7J1ueUNu2wcvgRDxr/<0;1>/*),older(1000))))#qjx6ycpc").unwrap(); } #[test]