Repository: wizardsardine/liana
Pull request #389 (2023-03-28 12:11:10)

fix installer pick_list warning
![2023-03-28T14:10:07,557792143+02:00](https://user-images.githubusercontent.com/6933020/228231883-2da81e02-edb5-4271-9cd4-fc4b81ed5097.png)



Patch:
diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs
index 2721fe18..ba6ab336 100644
--- a/gui/src/installer/view.rs
+++ b/gui/src/installer/view.rs
@@ -157,15 +157,20 @@ pub fn define_descriptor<'a>(
             pick_list(&NETWORKS[..], Some(Network::from(network)), |net| {
                 Message::Network(net.into())
             })
-            .style(theme::PickList::Simple)
+            .style(if network_valid {
+                theme::PickList::Simple
+            } else {
+                theme::PickList::Invalid
+            })
             .padding(10),
         ))
         .push_maybe(if network_valid {
             None
         } else {
-            Some(card::warning(
-                "A data directory already exists for this network".to_string(),
-            ))
+            Some(
+                text("A data directory already exists for this network")
+                    .style(color::legacy::ALERT),
+            )
         })
         .padding(50);
 
@@ -363,14 +368,20 @@ pub fn import_descriptor<'a>(
             pick_list(&NETWORKS[..], Some(Network::from(network)), |net| {
                 Message::Network(net.into())
             })
+            .style(if network_valid {
+                theme::PickList::Simple
+            } else {
+                theme::PickList::Invalid
+            })
             .padding(10),
         ))
         .push_maybe(if network_valid {
             None
         } else {
-            Some(card::warning(
-                "A data directory already exists for this network".to_string(),
-            ))
+            Some(
+                text("A data directory already exists for this network")
+                    .style(color::legacy::ALERT),
+            )
         });
     let col_descriptor = Column::new()
         .push(text("Descriptor:").bold())
@@ -619,14 +630,20 @@ pub fn participate_xpub<'a>(
             pick_list(&NETWORKS[..], Some(Network::from(network)), |net| {
                 Message::Network(net.into())
             })
+            .style(if network_valid {
+                theme::PickList::Simple
+            } else {
+                theme::PickList::Invalid
+            })
             .padding(10),
         ))
         .push_maybe(if network_valid {
             None
         } else {
-            Some(card::warning(
-                "A data directory already exists for this network".to_string(),
-            ))
+            Some(
+                text("A data directory already exists for this network")
+                    .style(color::legacy::ALERT),
+            )
         });
 
     layout(
diff --git a/gui/ui/src/component/card.rs b/gui/ui/src/component/card.rs
index 44c9d1e4..965bed73 100644
--- a/gui/ui/src/component/card.rs
+++ b/gui/ui/src/component/card.rs
@@ -18,8 +18,8 @@ pub fn warning<'a, T: 'a>(message: String) -> Container<'a, T> {
         Row::new()
             .spacing(20)
             .align_items(iced::Alignment::Center)
-            .push(icon::warning_octagon_icon().style(color::legacy::WARNING))
-            .push(text(message).style(color::legacy::WARNING)),
+            .push(icon::warning_octagon_icon())
+            .push(text(message)),
     )
     .padding(15)
     .style(theme::Container::Card(theme::Card::Warning))
diff --git a/gui/ui/src/theme.rs b/gui/ui/src/theme.rs
index 927bd0ae..fbeabace 100644
--- a/gui/ui/src/theme.rs
+++ b/gui/ui/src/theme.rs
@@ -50,7 +50,7 @@ impl iced::overlay::menu::StyleSheet for Theme {
             background: color::LIGHT_GREY.into(),
             border_width: 1.0,
             border_radius: 0.0,
-            border_color: color::GREEN,
+            border_color: color::LIGHT_GREY,
             selected_text_color: color::BLACK,
             selected_background: color::GREEN.into(),
         }
@@ -422,19 +422,31 @@ impl scrollable::StyleSheet for Theme {
 pub enum PickList {
     #[default]
     Simple,
+    Invalid,
 }
 impl pick_list::StyleSheet for Theme {
     type Style = PickList;
 
-    fn active(&self, _style: &Self::Style) -> pick_list::Appearance {
-        pick_list::Appearance {
-            placeholder_color: color::legacy::FOREGROUND,
-            handle_color: color::legacy::FOREGROUND,
-            background: color::legacy::FOREGROUND.into(),
-            border_width: 1.0,
-            border_color: color::legacy::BORDER_GREY,
-            border_radius: 10.0,
-            text_color: iced::Color::BLACK,
+    fn active(&self, style: &Self::Style) -> pick_list::Appearance {
+        match style {
+            PickList::Simple => pick_list::Appearance {
+                placeholder_color: color::legacy::FOREGROUND,
+                handle_color: color::legacy::FOREGROUND,
+                background: color::legacy::FOREGROUND.into(),
+                border_width: 1.0,
+                border_color: color::legacy::BORDER_GREY,
+                border_radius: 10.0,
+                text_color: iced::Color::BLACK,
+            },
+            PickList::Invalid => pick_list::Appearance {
+                placeholder_color: color::legacy::FOREGROUND,
+                handle_color: color::legacy::FOREGROUND,
+                background: color::legacy::FOREGROUND.into(),
+                border_width: 1.0,
+                border_color: color::legacy::ALERT,
+                border_radius: 10.0,
+                text_color: iced::Color::BLACK,
+            },
         }
     }
 
diff --git a/src/bitcoin/d/mod.rs b/src/bitcoin/d/mod.rs
index 32f4680c..27494502 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::LianaDescriptor,
+    descriptors::MultipathDescriptor,
 };
 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: &LianaDescriptor) -> Option<String> {
+    fn import_descriptor(&self, desc: &MultipathDescriptor) -> Option<String> {
         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: &LianaDescriptor,
+        main_descriptor: &MultipathDescriptor,
     ) -> 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: &LianaDescriptor,
+        main_descriptor: &MultipathDescriptor,
     ) -> Result<(), BitcoindError> {
         // Check our watchonly wallet is loaded
         if self
@@ -919,7 +919,7 @@ impl BitcoinD {
 
     pub fn start_rescan(
         &self,
-        desc: &LianaDescriptor,
+        desc: &MultipathDescriptor,
         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 9aa2732e..fe4b4608 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::SinglePathLianaDesc],
+        descs: &[descriptors::InheritanceDescriptor],
     ) -> Vec<UTxO>;
 
     /// 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::LianaDescriptor,
+        desc: &descriptors::MultipathDescriptor,
         timestamp: u32,
     ) -> Result<(), String>;
 
@@ -131,7 +131,7 @@ impl BitcoinInterface for d::BitcoinD {
     fn received_coins(
         &self,
         tip: &BlockChainTip,
-        descs: &[descriptors::SinglePathLianaDesc],
+        descs: &[descriptors::InheritanceDescriptor],
     ) -> Vec<UTxO> {
         let lsb_res = self.list_since_block(&tip.hash);
 
@@ -288,7 +288,7 @@ impl BitcoinInterface for d::BitcoinD {
 
     fn start_rescan(
         &self,
-        desc: &descriptors::LianaDescriptor,
+        desc: &descriptors::MultipathDescriptor,
         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<sync::Mutex<dyn BitcoinInterface + 'static>>
     fn received_coins(
         &self,
         tip: &BlockChainTip,
-        descs: &[descriptors::SinglePathLianaDesc],
+        descs: &[descriptors::InheritanceDescriptor],
     ) -> Vec<UTxO> {
         self.lock().unwrap().received_coins(tip, descs)
     }
@@ -374,7 +374,7 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
 
     fn start_rescan(
         &self,
-        desc: &descriptors::LianaDescriptor,
+        desc: &descriptors::MultipathDescriptor,
         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 99ec8a3d..2718be48 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<dyn DatabaseConnection>,
     previous_tip: &BlockChainTip,
-    descs: &[descriptors::SinglePathLianaDesc],
+    descs: &[descriptors::InheritanceDescriptor],
     secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
 ) -> 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::SinglePathLianaDesc],
+    descs: &[descriptors::InheritanceDescriptor],
     secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
 ) {
     let mut db_conn = db.connection();
@@ -238,7 +238,7 @@ fn updates(
 fn rescan_check(
     bit: &impl BitcoinInterface,
     db: &impl DatabaseInterface,
-    descs: &[descriptors::SinglePathLianaDesc],
+    descs: &[descriptors::InheritanceDescriptor],
     secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
 ) {
     log::debug!("Checking the state of an ongoing rescan if there is any");
@@ -300,7 +300,7 @@ pub fn looper(
     db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
     shutdown: sync::Arc<atomic::AtomicBool>,
     poll_interval: time::Duration,
-    desc: descriptors::LianaDescriptor,
+    desc: descriptors::MultipathDescriptor,
 ) {
     let mut last_poll = None;
     let mut synced = false;
diff --git a/src/bitcoin/poller/mod.rs b/src/bitcoin/poller/mod.rs
index 95a20535..46cbb60d 100644
--- a/src/bitcoin/poller/mod.rs
+++ b/src/bitcoin/poller/mod.rs
@@ -22,7 +22,7 @@ impl Poller {
         bit: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
         db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
         poll_interval: time::Duration,
-        desc: descriptors::LianaDescriptor,
+        desc: descriptors::MultipathDescriptor,
     ) -> 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 379141f8..07d5ed04 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::LianaDescriptor,
+    spent_desc: &descriptors::MultipathDescriptor,
     psbt: &Psbt,
 ) -> Result<(), CommandError> {
     let tx = &psbt.unsigned_tx;
@@ -216,7 +216,7 @@ fn serializable_size<T: bitcoin::consensus::Encodable + ?Sized>(t: &T) -> u64 {
 
 impl DaemonControl {
     // Get the derived descriptor for this coin
-    fn derived_desc(&self, coin: &Coin) -> descriptors::DerivedSinglePathLianaDesc {
+    fn derived_desc(&self, coin: &Coin) -> descriptors::DerivedInheritanceDescriptor {
         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::LianaDescriptor,
+    pub main: descriptors::MultipathDescriptor,
 }
 
 /// Information about the daemon
diff --git a/src/config.rs b/src/config.rs
index 46152b88..335401f0 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,4 +1,4 @@
-use crate::descriptors::LianaDescriptor;
+use crate::descriptors::MultipathDescriptor;
 
 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: LianaDescriptor,
+    pub main_descriptor: MultipathDescriptor,
     /// 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<LianaDescriptor>),
+    UnexpectedDescriptor(Box<MultipathDescriptor>),
     Unexpected(String),
 }
 
diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs
index dedd79e9..17663a13 100644
--- a/src/database/sqlite/mod.rs
+++ b/src/database/sqlite/mod.rs
@@ -18,7 +18,7 @@ use crate::{
         },
         Coin, CoinType,
     },
-    descriptors::LianaDescriptor,
+    descriptors::MultipathDescriptor,
 };
 
 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<LianaDescriptor>),
+    DescriptorMismatch(Box<MultipathDescriptor>),
     Rusqlite(rusqlite::Error),
 }
 
@@ -83,7 +83,7 @@ impl From<rusqlite::Error> for SqliteDbError {
 #[derive(Debug, Clone)]
 pub struct FreshDbOptions {
     pub bitcoind_network: bitcoin::Network,
-    pub main_descriptor: LianaDescriptor,
+    pub main_descriptor: MultipathDescriptor,
 }
 
 #[derive(Debug, Clone)]
@@ -122,7 +122,7 @@ impl SqliteDb {
     pub fn sanity_check(
         &self,
         bitcoind_network: bitcoin::Network,
-        main_descriptor: &LianaDescriptor,
+        main_descriptor: &MultipathDescriptor,
     ) -> 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 = LianaDescriptor::from_str(desc_str).unwrap();
+        let main_descriptor = MultipathDescriptor::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 = LianaDescriptor::from_str(other_desc_str).unwrap();
+        let other_desc = MultipathDescriptor::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 65ed1670..a4a51feb 100644
--- a/src/database/sqlite/schema.rs
+++ b/src/database/sqlite/schema.rs
@@ -1,4 +1,4 @@
-use crate::descriptors::LianaDescriptor;
+use crate::descriptors::MultipathDescriptor;
 
 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: LianaDescriptor,
+    pub main_descriptor: MultipathDescriptor,
     pub deposit_derivation_index: bip32::ChildNumber,
     pub change_derivation_index: bip32::ChildNumber,
     pub rescan_timestamp: Option<u32>,
@@ -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 = LianaDescriptor::from_str(&desc_str)
+        let main_descriptor = MultipathDescriptor::from_str(&desc_str)
             .expect("Insane database: can't parse deposit descriptor");
 
         let der_idx: u32 = row.get(3)?;
diff --git a/src/descriptors/mod.rs b/src/descriptors.rs
similarity index 50%
rename from src/descriptors/mod.rs
rename to src/descriptors.rs
index b8d169ea..c0d534fa 100644
--- a/src/descriptors/mod.rs
+++ b/src/descriptors.rs
@@ -1,24 +1,29 @@
 use miniscript::{
     bitcoin::{
-        self, secp256k1,
+        self,
+        blockdata::transaction::Sequence,
+        hashes::{hash160, ripemd160, sha256},
+        secp256k1,
         util::{
             bip32,
             psbt::{Input as PsbtIn, Psbt},
         },
     },
-    descriptor, translate_hash_clone, ForEachKey, TranslatePk, Translator,
+    descriptor, hash256,
+    miniscript::{decode::Terminal, Miniscript},
+    policy::{Liftable, Semantic as SemanticPolicy},
+    translate_hash_clone, ForEachKey, MiniscriptKey, ScriptContext, ToPublicKey, TranslatePk,
+    Translator,
 };
 
-use std::{collections::BTreeMap, error, fmt, str};
+use std::{
+    collections::{BTreeMap, HashMap, HashSet},
+    convert::TryFrom,
+    error, fmt, str, sync,
+};
 
 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.
@@ -30,9 +35,14 @@ fn wu_to_vb(vb: usize) -> usize {
 
 #[derive(Debug)]
 pub enum LianaDescError {
+    InsaneTimelock(u32),
+    InvalidKey(Box<descriptor::DescriptorPublicKey>),
+    DuplicateKey(Box<descriptor::DescriptorPublicKey>),
     Miniscript(miniscript::Error),
-    DescKey(DescKeyError),
-    Policy(LianaPolicyError),
+    IncompatibleDesc,
+    DerivedKeyParsing,
+    InvalidMultiThresh(usize),
+    InvalidMultiKeys(usize),
     /// Different number of PSBT vs tx inputs, etc..
     InsanePsbt,
     /// Not all inputs' sequence the same, not all inputs signed with the same key, ..
@@ -42,94 +52,655 @@ 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::DescKey(e) => write!(f, "{}", e),
-            Self::Policy(e) => write!(f, "{}", 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::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 {}
 
-impl From<LianaPolicyError> for LianaDescError {
-    fn from(e: LianaPolicyError) -> LianaDescError {
-        LianaDescError::Policy(e)
+/// A public key used in derived descriptors
+#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
+pub struct DerivedPublicKey {
+    /// Fingerprint of the master xpub and the derivation index used. We don't use a path
+    /// since we never derive at more than one depth.
+    pub origin: (bip32::Fingerprint, bip32::DerivationPath),
+    /// The actual key
+    pub key: bitcoin::PublicKey,
+}
+
+impl fmt::Display for DerivedPublicKey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let (fingerprint, deriv_path) = &self.origin;
+
+        write!(f, "[")?;
+        for byte in fingerprint.as_bytes().iter() {
+            write!(f, "{:02x}", byte)?;
+        }
+        write!(f, "/{}", deriv_path)?;
+        write!(f, "]{}", self.key)
+    }
+}
+
+impl str::FromStr for DerivedPublicKey {
+    type Err = LianaDescError;
+
+    fn from_str(s: &str) -> Result<DerivedPublicKey, Self::Err> {
+        // The key is always of the form:
+        // [ fingerprint / index ]<key>
+
+        // 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<u16, LianaDescError> {
+    if csv_value > 0 {
+        u16::try_from(csv_value).map_err(|_| LianaDescError::InsaneTimelock(csv_value))
+    } else {
+        Err(LianaDescError::InsaneTimelock(csv_value))
     }
 }
 
-/// An [SinglePathLianaDesc] that contains multipath keys for (and only for) the receive keychain
+// 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<usize>,
+    keys: Vec<descriptor::DescriptorPublicKey>,
+}
+
+impl LianaDescKeys {
+    pub fn from_single(key: descriptor::DescriptorPublicKey) -> LianaDescKeys {
+        LianaDescKeys {
+            thresh: None,
+            keys: vec![key],
+        }
+    }
+
+    pub fn from_multi(
+        thresh: usize,
+        keys: Vec<descriptor::DescriptorPublicKey>,
+    ) -> Result<LianaDescKeys, LianaDescError> {
+        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<descriptor::DescriptorPublicKey> {
+        &self.keys
+    }
+
+    pub fn into_miniscript(
+        mut self,
+        as_hash: bool,
+    ) -> Miniscript<descriptor::DescriptorPublicKey, miniscript::Segwitv0> {
+        if let Some(thresh) = self.thresh {
+            assert!(self.keys.len() >= 2 && self.keys.len() <= 20);
+            Miniscript::from_ast(Terminal::Multi(thresh, self.keys))
+                .expect("multi is a valid Miniscript")
+        } else {
+            assert_eq!(self.keys.len(), 1);
+            let key = self.keys.pop().expect("Length was just asserted");
+            Miniscript::from_ast(Terminal::Check(sync::Arc::from(
+                Miniscript::from_ast(if as_hash {
+                    Terminal::PkH(key)
+                } else {
+                    Terminal::PkK(key)
+                })
+                .expect("pk_k is a valid Miniscript"),
+            )))
+            .expect("Well typed")
+        }
+    }
+}
+
+/// An [InheritanceDescriptor] that contains multipath keys for (and only for) the receive keychain
 /// and the change keychain.
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct LianaDescriptor {
+pub struct MultipathDescriptor {
     multi_desc: descriptor::Descriptor<descriptor::DescriptorPublicKey>,
-    receive_desc: SinglePathLianaDesc,
-    change_desc: SinglePathLianaDesc,
+    receive_desc: InheritanceDescriptor,
+    change_desc: InheritanceDescriptor,
 }
 
 /// 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 SinglePathLianaDesc(descriptor::Descriptor<descriptor::DescriptorPublicKey>);
+pub struct InheritanceDescriptor(descriptor::Descriptor<descriptor::DescriptorPublicKey>);
 
 /// Derived (containing only raw Bitcoin public keys) version of the inheritance descriptor.
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct DerivedSinglePathLianaDesc(descriptor::Descriptor<DerivedPublicKey>);
+pub struct DerivedInheritanceDescriptor(descriptor::Descriptor<DerivedPublicKey>);
 
-impl fmt::Display for LianaDescriptor {
+impl fmt::Display for MultipathDescriptor {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "{}", self.multi_desc)
     }
 }
 
-impl str::FromStr for LianaDescriptor {
+fn is_single_key_or_multisig(policy: &SemanticPolicy<descriptor::DescriptorPublicKey>) -> bool {
+    match policy {
+        SemanticPolicy::Key(..) => true,
+        SemanticPolicy::Threshold(_, subs) => {
+            subs.iter().all(|sub| matches!(sub, SemanticPolicy::Key(_)))
+        }
+        _ => false,
+    }
+}
+
+impl str::FromStr for MultipathDescriptor {
     type Err = LianaDescError;
 
-    fn from_str(s: &str) -> Result<LianaDescriptor, Self::Err> {
-        // Parse a descriptor and check it is a multipath descriptor corresponding to a valid Liana
-        // spending policy.
-        let desc = descriptor::Descriptor::<descriptor::DescriptorPublicKey>::from_str(s)
+    fn from_str(s: &str) -> Result<MultipathDescriptor, Self::Err> {
+        let wsh_desc = descriptor::Wsh::<descriptor::DescriptorPublicKey>::from_str(s)
             .map_err(LianaDescError::Miniscript)?;
-        LianaPolicy::from_multipath_descriptor(&desc)?;
+        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);
 
         // 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 = desc
+        let mut singlepath_descs = multi_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 = SinglePathLianaDesc(singlepath_descs.next().expect("First of 2"));
-        let change_desc = SinglePathLianaDesc(singlepath_descs.next().expect("Second of 2"));
+        let receive_desc = InheritanceDescriptor(singlepath_descs.next().expect("First of 2"));
+        let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2"));
 
-        Ok(LianaDescriptor {
-            multi_desc: desc,
+        Ok(MultipathDescriptor {
+            multi_desc,
             receive_desc,
             change_desc,
         })
     }
 }
 
-impl fmt::Display for SinglePathLianaDesc {
+impl fmt::Display for InheritanceDescriptor {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "{}", self.0)
     }
 }
 
-impl PartialEq<descriptor::Descriptor<descriptor::DescriptorPublicKey>> for SinglePathLianaDesc {
+impl PartialEq<descriptor::Descriptor<descriptor::DescriptorPublicKey>> for InheritanceDescriptor {
     fn eq(&self, other: &descriptor::Descriptor<descriptor::DescriptorPublicKey>) -> bool {
         self.0.eq(other)
     }
 }
 
-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();
+/// 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<descriptor::DescriptorPublicKey>),
+}
+
+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<descriptor::DescriptorPublicKey>,
+    ) -> Result<PathInfo, LianaDescError> {
+        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<descriptor::DescriptorPublicKey>,
+    ) -> 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<Item = &'a (bip32::Fingerprint, bip32::DerivationPath)>,
+    ) -> 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<PathSpendInfo>,
+}
+
+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<PathSpendInfo> {
+        &self.recovery_path
+    }
+}
+
+impl MultipathDescriptor {
+    pub fn new(
+        owner_keys: LianaDescKeys,
+        heir_keys: LianaDescKeys,
+        timelock: u16,
+    ) -> Result<MultipathDescriptor, LianaDescError> {
+        // We require the locktime to:
+        //  - not be disabled
+        //  - be in number of blocks
+        //  - be 'clean' / minimal, ie all bits without consensus meaning should be 0
+        //  - 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"),
+        );
 
         // 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.
@@ -141,14 +712,14 @@ impl LianaDescriptor {
             .expect("Can't error, all paths have the same length")
             .into_iter();
         assert_eq!(singlepath_descs.len(), 2);
-        let receive_desc = SinglePathLianaDesc(singlepath_descs.next().expect("First of 2"));
-        let change_desc = SinglePathLianaDesc(singlepath_descs.next().expect("Second of 2"));
+        let receive_desc = InheritanceDescriptor(singlepath_descs.next().expect("First of 2"));
+        let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2"));
 
-        LianaDescriptor {
+        Ok(MultipathDescriptor {
             multi_desc,
             receive_desc,
             change_desc,
-        }
+        })
     }
 
     /// Whether all xpubs contained in this descriptor are for the passed expected network.
@@ -163,25 +734,69 @@ impl LianaDescriptor {
     }
 
     /// Get the descriptor for receiving addresses.
-    pub fn receive_descriptor(&self) -> &SinglePathLianaDesc {
+    pub fn receive_descriptor(&self) -> &InheritanceDescriptor {
         &self.receive_desc
     }
 
     /// Get the descriptor for change addresses.
-    pub fn change_descriptor(&self) -> &SinglePathLianaDesc {
+    pub fn change_descriptor(&self) -> &InheritanceDescriptor {
         &self.change_desc
     }
 
-    /// 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.")
+    /// 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 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.policy().recovery_path.0 as u32
+        self.info().recovery_path.0 as u32
     }
 
     /// Get the maximum size in WU of a satisfaction for this descriptor.
@@ -227,7 +842,7 @@ impl LianaDescriptor {
         // 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.policy();
+        let desc_info = self.info();
         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
@@ -289,7 +904,7 @@ impl LianaDescriptor {
     }
 }
 
-impl SinglePathLianaDesc {
+impl InheritanceDescriptor {
     /// Derive this descriptor at a given index for a receiving address.
     ///
     /// # Panics
@@ -298,7 +913,7 @@ impl SinglePathLianaDesc {
         &self,
         index: bip32::ChildNumber,
         secp: &secp256k1::Secp256k1<impl secp256k1::Verification>,
-    ) -> DerivedSinglePathLianaDesc {
+    ) -> DerivedInheritanceDescriptor {
         assert!(index.is_normal());
 
         // Unfortunately we can't just use `self.0.at_derivation_index().derived_descriptor()`
@@ -338,7 +953,7 @@ impl SinglePathLianaDesc {
             );
         }
 
-        DerivedSinglePathLianaDesc(
+        DerivedInheritanceDescriptor(
             self.0
                 .translate_pk(&mut Derivator(index.into(), secp))
                 .expect(
@@ -351,7 +966,7 @@ impl SinglePathLianaDesc {
 /// Map of a raw public key to the xpub used to derive it and its derivation path
 pub type Bip32Deriv = BTreeMap<secp256k1::PublicKey, (bip32::Fingerprint, bip32::DerivationPath)>;
 
-impl DerivedSinglePathLianaDesc {
+impl DerivedInheritanceDescriptor {
     pub fn address(&self, network: bitcoin::Network) -> bitcoin::Address {
         self.0
             .address(network)
@@ -388,136 +1003,132 @@ impl DerivedSinglePathLianaDesc {
 mod tests {
     use super::*;
 
-    use bitcoin::Sequence;
-
     use std::str::FromStr;
 
-    use crate::signer::HotSigner;
-
     #[test]
     fn descriptor_creation() {
-        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 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 timelock = 52560;
-        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");
+        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");
 
         // 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 = PathInfo::Multi(
+        let primary_keys = LianaDescKeys::from_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()
             ]
-        );
-        let recovery_keys = PathInfo::Multi(
+        )
+        .unwrap();
+        let recovery_keys = LianaDescKeys::from_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(),
             ],
-        );
-        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");
+        )
+        .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");
 
         // We prevent footguns with timelocks by requiring a u16. Note how the following wouldn't
         // compile:
-        //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();
+        //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();
 
         // You can't use a null timelock in Miniscript.
-        LianaPolicy::new(owner_key, heir_key, 0).unwrap_err();
+        MultipathDescriptor::new(owner_key, heir_key, 0).unwrap_err();
 
-        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 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 timelock = 57600;
-        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");
+        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");
 
         // 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 = 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(
+        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(
             descriptor::DescriptorPublicKey::from_str(
                 "[abcdef01]02e24913be26dbcfdf8e8e94870b28725cdae09b448b6c127767bf0154e3a3c8e5",
             )
             .unwrap(),
         );
-        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();
+        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();
 
         // And it's checked even in a multisig. For instance:
-        let primary_keys = PathInfo::Multi(
+        let primary_keys = LianaDescKeys::from_multi(
             1,
             vec![
                 descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
                 descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap(),
             ]
-        );
-        let recovery_keys = PathInfo::Multi(
+        )
+        .unwrap();
+        let recovery_keys = LianaDescKeys::from_multi(
             1,
             vec![
                 descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
                 descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
             ],
-        );
-        LianaPolicy::new(primary_keys, recovery_keys, 26352).unwrap_err();
+        )
+        .unwrap();
+        MultipathDescriptor::new(primary_keys, recovery_keys, 26352).unwrap_err();
 
         // You can't pass duplicate keys, even if they are encoded differently.
-        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();
+        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();
 
         // You can't pass duplicate keys, even across multisigs.
-        let primary_keys = PathInfo::Multi(
+        let primary_keys = LianaDescKeys::from_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()
             ]
-        );
-        let recovery_keys = PathInfo::Multi(
+        )
+        .unwrap();
+        let recovery_keys = LianaDescKeys::from_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(),
             ],
-        );
-        LianaPolicy::new(primary_keys, recovery_keys, 26352).unwrap_err();
+        )
+        .unwrap();
+        MultipathDescriptor::new(primary_keys, recovery_keys, 26352).unwrap_err();
 
         // No origin in one of the keys
-        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 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 timelock = 52560;
-        LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err();
-
-        // A 1-of-N multisig as primary path.
-        LianaDescriptor::from_str("wsh(or_d(multi(1,[573fb35b/48'/1'/0'/2']tpubDFKp9T7WAYDcENSjoifkrpq1gMDF47KGJcJrpxzX23Qor8wuGbrEVs9utNq1MDS8E2WXJSBk1qoPQLpwyokW7DiUNPwFuxQkL7owNkLAb9W/<0;1>/*,[573fb35b/48'/1'/1'/2']tpubDFGezyzuHJPhdP3jHGW7v7Hwes4Hihqv5W2yyCmRY9VZJCRchETvxrMC8uECeJZdxQ14V4iD4DecoArkUSDwj8ogYE9WEv4MNZr12thNHCs/<0;1>/*),and_v(v:multi(2,[573fb35b/48'/1'/2'/2']tpubDDwxQauiaU964vPzt5Vd7jnDHEUtp2Vc34PaWpEXg5TQ3bRccxnc1MKKh88Hi7xiMeZo9Tm6fBcq4UGXqnDtGUniJLjqAD8SjQ8Eci3aSR7/<0;1>/*,[573fb35b/48'/1'/3'/2']tpubDE37XAVB5CQ1x85md3BQ5uHCoMwT5fgT8X13zzCUQ3x5o2jskYxKjj7Qcxt1Jpj4QB8tqspn2dooPCekRuQDYrDHov7J1ueUNu2wcvgRDxr/<0;1>/*),older(1000))))#qjx6ycpc").unwrap();
+        MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap_err();
     }
 
     #[test]
     fn inheritance_descriptor_derivation() {
         let secp = secp256k1::Secp256k1::verification_only();
-        let desc = LianaDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#2qj59a9y").unwrap();
+        let desc = MultipathDescriptor::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",
@@ -532,22 +1143,19 @@ mod tests {
 
     #[test]
     fn inheritance_descriptor_tl_value() {
-        // 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();
+        let desc = MultipathDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
         assert_eq!(desc.timelock_value(), 1);
 
-        let desc = LianaDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(42000),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
+        let desc = MultipathDescriptor::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 = LianaDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(65535),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
+        let desc = MultipathDescriptor::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 = LianaDescriptor::from_str("wsh(or_d(pk([92162c45]tpubD6NzVbkrYhZ4WzTf9SsD6h7AH7oQEippXK2KP8qvhMMqFoNeN5YFVi7vRyeRSDGtgd2bPyMxUNmHui8t5yCgszxPPxMafu1VVzDpg9aruYW/<0;1>/*),and_v(v:pkh([abcdef01]tpubD6NzVbkrYhZ4Wdgu2yfdmrce5g4fiH1ZLmKhewsnNKupbi4sxjH1ZVAorkBLWSkhsjhg8kiq8C4BrBjMy3SjAKDyDdbuvUa1ToAHbiR98js/<0;1>/*),older(2))))#ravw7jw5").unwrap();
+        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();
         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).
@@ -573,70 +1181,23 @@ mod tests {
 
     #[test]
     fn liana_desc_keys() {
-        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<descriptor::DescriptorPublicKey> =
-            (0..20).map(|_| random_desc_key()).collect();
-        let mut twenty_one_keys = twenty_keys.clone();
-        twenty_one_keys.push(random_desc_key());
-
-        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();
+        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());
+
+        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();
     }
 
     fn roundtrip(desc_str: &str) {
-        let desc = LianaDescriptor::from_str(desc_str).unwrap();
+        let desc = MultipathDescriptor::from_str(desc_str).unwrap();
         assert_eq!(desc.to_string(), desc_str);
     }
 
@@ -659,8 +1220,8 @@ mod tests {
     #[test]
     fn partial_spend_info() {
         // A simple descriptor with 1 keys as primary path and 1 recovery key.
-        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 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 prim_key_origin = (
             bip32::Fingerprint::from_str("f5acc2fd").unwrap(),
             Vec::new().into(),
@@ -819,7 +1380,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 = 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 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 info = desc.partial_spend_info(&psbt).unwrap();
         assert!(psbt
             .inputs
@@ -836,8 +1397,8 @@ mod tests {
         );
         assert!(info.recovery_path.is_none());
 
-        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();
+        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();
         assert_eq!(info.primary_path, PathInfo::Multi(
             2,
             vec![
diff --git a/src/descriptors/analysis.rs b/src/descriptors/analysis.rs
deleted file mode 100644
index c34ea2e6..00000000
--- a/src/descriptors/analysis.rs
+++ /dev/null
@@ -1,553 +0,0 @@
-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<descriptor::DescriptorPublicKey>),
-    DuplicateKey(Box<descriptor::DescriptorPublicKey>),
-    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<descriptor::DescriptorPublicKey>) -> 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<u16, LianaPolicyError> {
-    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<descriptor::DescriptorPublicKey>),
-}
-
-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<descriptor::DescriptorPublicKey>,
-    ) -> Result<PathInfo, LianaPolicyError> {
-        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<descriptor::DescriptorPublicKey>,
-    ) -> 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<Item = &'a (bip32::Fingerprint, bip32::DerivationPath)>,
-    ) -> 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<descriptor::DescriptorPublicKey> {
-        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<Miniscript<descriptor::DescriptorPublicKey, miniscript::Segwitv0>> {
-        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<LianaPolicy, LianaPolicyError> {
-        // 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<descriptor::DescriptorPublicKey>,
-    ) -> Result<LianaPolicy, LianaPolicyError> {
-        // 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::<PathInfo>, 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<descriptor::DescriptorPublicKey> {
-        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<PathSpendInfo>,
-}
-
-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<PathSpendInfo> {
-        &self.recovery_path
-    }
-}
diff --git a/src/descriptors/keys.rs b/src/descriptors/keys.rs
deleted file mode 100644
index 29e1d13e..00000000
--- a/src/descriptors/keys.rs
+++ /dev/null
@@ -1,137 +0,0 @@
-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<DerivedPublicKey, Self::Err> {
-        // The key is always of the form:
-        // [ fingerprint / index ]<key>
-
-        // 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/lib.rs b/src/lib.rs
index 401d2606..19f78541 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -464,7 +464,7 @@ mod tests {
     use super::*;
     use crate::{
         config::{BitcoinConfig, BitcoindConfig},
-        descriptors::LianaDescriptor,
+        descriptors::MultipathDescriptor,
         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 = LianaDescriptor::from_str(desc_str).unwrap();
+        let desc = MultipathDescriptor::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 646eabdd..6d415392 100644
--- a/src/signer.rs
+++ b/src/signer.rs
@@ -409,7 +409,9 @@ mod tests {
             .unwrap(),
             wildcard: Wildcard::Unhardened,
         });
-        let prim_keys = descriptors::PathInfo::Multi(2, vec![prim_key_a, prim_key_b, prim_key_c]);
+        let prim_keys =
+            descriptors::LianaDescKeys::from_multi(2, vec![prim_key_a, prim_key_b, prim_key_c])
+                .unwrap();
         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 {
@@ -422,9 +424,8 @@ mod tests {
             .unwrap(),
             wildcard: Wildcard::Unhardened,
         });
-        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);
+        let recov_keys = descriptors::LianaDescKeys::from_single(recov_key);
+        let desc = descriptors::MultipathDescriptor::new(prim_keys, recov_keys, 42).unwrap();
 
         // 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.


Test patch:
diff --git a/src/testutils.rs b/src/testutils.rs
index 54d2ae6e..400141a6 100644
--- a/src/testutils.rs
+++ b/src/testutils.rs
@@ -60,7 +60,7 @@ impl BitcoinInterface for DummyBitcoind {
     fn received_coins(
         &self,
         _: &BlockChainTip,
-        _: &[descriptors::SinglePathLianaDesc],
+        _: &[descriptors::InheritanceDescriptor],
     ) -> Vec<UTxO> {
         Vec::new()
     }
@@ -91,7 +91,7 @@ impl BitcoinInterface for DummyBitcoind {
         todo!()
     }
 
-    fn start_rescan(&self, _: &descriptors::LianaDescriptor, _: u32) -> Result<(), String> {
+    fn start_rescan(&self, _: &descriptors::MultipathDescriptor, _: u32) -> Result<(), String> {
         todo!()
     }
 
@@ -399,10 +399,9 @@ impl DummyLiana {
             poll_interval_secs: time::Duration::from_secs(2),
         };
 
-        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 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 config = Config {
             bitcoin_config,
             bitcoind_config: None,