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..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" @@ -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>; @@ -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); @@ -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. @@ -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) } @@ -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..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"); @@ -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..379141f8 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; @@ -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 { @@ -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/analysis.rs b/src/descriptors/analysis.rs new file mode 100644 index 00000000..c34ea2e6 --- /dev/null +++ b/src/descriptors/analysis.rs @@ -0,0 +1,553 @@ +use miniscript::{ + bitcoin::{util::bip32, Sequence}, + descriptor, + policy::{Liftable, Semantic as SemanticPolicy}, + Miniscript, ScriptContext, Terminal, +}; + +use std::{ + collections::{HashMap, HashSet}, + convert::TryFrom, + error, fmt, sync, +}; + +#[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 { + match policy { + SemanticPolicy::Key(..) => true, + SemanticPolicy::Threshold(_, subs) => { + subs.iter().all(|sub| matches!(sub, SemanticPolicy::Key(_))) + } + _ => false, + } +} + +// 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() + } + } +} + +// 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(|_| LianaPolicyError::InsaneTimelock(csv_value)) + } else { + Err(LianaPolicyError::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<_, LianaPolicyError> = subs + .into_iter() + .map(|sub| match sub { + SemanticPolicy::Key(key) => Ok(key), + _ => Err(LianaPolicyError::IncompatibleDesc), + }) + .collect(); + Ok(PathInfo::Multi(k, keys?)) + } + _ => Err(LianaPolicyError::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), 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(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 + // 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(LianaPolicyError::IncompatibleDesc)??; + let keys_sub = subs + .into_iter() + .find(is_single_key_or_multisig) + .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() + // 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(LianaPolicyError::IncompatibleDesc); + } + tl_value = Some(csv_check(val.0)?); + } + _ => return Err(LianaPolicyError::IncompatibleDesc), + } + } + assert!(keys.len() > 1); // At least 3 subs, only one of which may be older(). + Ok(( + 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(LianaPolicyError::IncompatibleDesc) + } + } + + /// 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)>) { + 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, + } + } + + // 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 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, + pub(super) recovery_path: (u16, PathInfo), +} + +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(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(LianaPolicyError::InvalidMultiKeys(keys.len())); + } + if thresh == &0 || thresh > &keys.len() { + return Err(LianaPolicyError::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(LianaPolicyError::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(LianaPolicyError::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( + desc: &descriptor::Descriptor, + ) -> Result { + // For now we only allow P2WSH descriptors. + let wsh_desc = match &desc { + descriptor::Descriptor::Wsh(desc) => desc, + _ => 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(LianaPolicyError::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(LianaPolicyError::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(); + + // 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)?; + + // Fetch the two spending paths' semantic policies. The primary path is identified as the + // only one that isn't timelocked. + 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 { + return Err(LianaPolicyError::IncompatibleDesc); + } + } 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: primary_path.ok_or(LianaPolicyError::IncompatibleDesc)?, + recovery_path: recovery_path.ok_or(LianaPolicyError::IncompatibleDesc)?, + }) + } + + 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) + } + + /// 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. +#[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/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.rs b/src/descriptors/mod.rs similarity index 50% rename from src/descriptors.rs rename to src/descriptors/mod.rs index c0d534fa..b8d169ea 100644 --- a/src/descriptors.rs +++ b/src/descriptors/mod.rs @@ -1,29 +1,24 @@ use miniscript::{ bitcoin::{ - self, - blockdata::transaction::Sequence, - hashes::{hash160, ripemd160, sha256}, - secp256k1, + self, secp256k1, util::{ bip32, psbt::{Input as PsbtIn, Psbt}, }, }, - descriptor, hash256, - miniscript::{decode::Terminal, Miniscript}, - policy::{Liftable, Semantic as SemanticPolicy}, - translate_hash_clone, ForEachKey, MiniscriptKey, ScriptContext, ToPublicKey, TranslatePk, - Translator, + descriptor, translate_hash_clone, ForEachKey, TranslatePk, Translator, }; -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - convert::TryFrom, - error, fmt, str, sync, -}; +use std::{collections::BTreeMap, error, fmt, str}; 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. @@ -35,14 +30,9 @@ fn wu_to_vb(vb: usize) -> usize { #[derive(Debug)] pub enum LianaDescError { - InsaneTimelock(u32), - InvalidKey(Box), - DuplicateKey(Box), Miniscript(miniscript::Error), - IncompatibleDesc, - DerivedKeyParsing, - InvalidMultiThresh(usize), - InvalidMultiKeys(usize), + 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, .. @@ -52,655 +42,94 @@ 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::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), + 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.") + Self::InconsistentPsbt => write!(f, "Analyzed PSBT is inconsistent across inputs."), } } } 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 From for LianaDescError { + fn from(e: LianaPolicyError) -> LianaDescError { + LianaDescError::Policy(e) } } -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 -// - 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)) - } -} - -// 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( - key: &descriptor::DescriptorPublicKey, -) -> Option<&(bip32::Fingerprint, bip32::DerivationPath)> { - match key { - descriptor::DescriptorPublicKey::MultiXPub(ref xpub) => xpub.origin.as_ref(), - _ => None, - } -} - -/// 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 +/// 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 MultipathDescriptor { +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 MultipathDescriptor { +impl fmt::Display for LianaDescriptor { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.multi_desc) } } -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 { +impl str::FromStr for LianaDescriptor { type Err = LianaDescError; - fn from_str(s: &str) -> Result { - let wsh_desc = descriptor::Wsh::::from_str(s) + 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) .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. - for sub in subs { - if is_single_key_or_multisig(&sub) { - PathInfo::from_primary_path(sub)?; - } 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") .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(MultipathDescriptor { - multi_desc, + Ok(LianaDescriptor { + multi_desc: desc, receive_desc, change_desc, }) } } -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) } } -/// 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, - 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"), - ); +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(); // 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. @@ -712,14 +141,14 @@ impl MultipathDescriptor { .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(MultipathDescriptor { + LianaDescriptor { multi_desc, receive_desc, change_desc, - }) + } } /// Whether all xpubs contained in this descriptor are for the passed expected network. @@ -734,69 +163,25 @@ impl MultipathDescriptor { } /// 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 } - /// Parse information about this descriptor - pub fn info(&self) -> LianaDescInfo { - // 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."); - - LianaDescInfo::new(primary_path, reco_path) + /// Get the spending policy of this descriptor. + pub fn policy(&self) -> LianaPolicy { + 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. 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. @@ -842,7 +227,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 @@ -904,7 +289,7 @@ impl MultipathDescriptor { } } -impl InheritanceDescriptor { +impl SinglePathLianaDesc { /// Derive this descriptor at a given index for a receiving address. /// /// # Panics @@ -913,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()` @@ -953,7 +338,7 @@ impl InheritanceDescriptor { ); } - DerivedInheritanceDescriptor( + DerivedSinglePathLianaDesc( self.0 .translate_pk(&mut Derivator(index.into(), secp)) .expect( @@ -966,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) @@ -1003,132 +388,136 @@ 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!(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. - 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!(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: - //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!(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 // 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(); + + // 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] 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", @@ -1143,19 +532,22 @@ mod tests { #[test] fn inheritance_descriptor_tl_value() { - let desc = MultipathDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap(); + // Must always contain at least one timelocked path. + LianaDescriptor::from_str("wsh(or_i(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap_err(); + + 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). @@ -1181,23 +573,70 @@ 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) { - let desc = MultipathDescriptor::from_str(desc_str).unwrap(); + let desc = LianaDescriptor::from_str(desc_str).unwrap(); assert_eq!(desc.to_string(), desc_str); } @@ -1220,8 +659,8 @@ 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_info = desc.info(); + 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(), Vec::new().into(), @@ -1380,7 +819,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 @@ -1397,8 +836,8 @@ 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 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, vec![ diff --git a/src/lib.rs b/src/lib.rs index 19f78541..401d2606 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 6d415392..646eabdd 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::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 400141a6..54d2ae6e 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() } @@ -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!() } @@ -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::LianaDescriptor::new(policy); let config = Config { bitcoin_config, bitcoind_config: None,