descriptors: multi-recovery-path Liana descriptor

This makes it possible to have more than one recovery path in a Liana
descriptor. The descriptor and partial spend analysis are adapted to
report information about all recovery paths.
This commit is contained in:
Antoine Poinsot 2023-03-25 16:16:28 +01:00
parent ec0009113a
commit cfbb02c7c8
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
5 changed files with 416 additions and 171 deletions

View File

@ -704,7 +704,7 @@ impl DaemonControl {
// Query the coins that we can spend through the recovery path from the database. // Query the coins that we can spend through the recovery path from the database.
let current_height = self.bitcoin.chain_tip().height; let current_height = self.bitcoin.chain_tip().height;
let desc_timelock = self.config.main_descriptor.timelock_value(); let desc_timelock = self.config.main_descriptor.first_timelock_value();
let timelock: i32 = desc_timelock let timelock: i32 = desc_timelock
.try_into() .try_into()
.expect("Must fit, it's effectively a u16"); .expect("Must fit, it's effectively a u16");

View File

@ -6,13 +6,14 @@ use miniscript::{
}; };
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
convert::TryFrom, convert::TryFrom,
error, fmt, error, fmt,
}; };
#[derive(Debug)] #[derive(Debug)]
pub enum LianaPolicyError { pub enum LianaPolicyError {
MissingRecoveryPath,
InsaneTimelock(u32), InsaneTimelock(u32),
InvalidKey(Box<descriptor::DescriptorPublicKey>), InvalidKey(Box<descriptor::DescriptorPublicKey>),
DuplicateKey(Box<descriptor::DescriptorPublicKey>), DuplicateKey(Box<descriptor::DescriptorPublicKey>),
@ -27,6 +28,7 @@ pub enum LianaPolicyError {
impl std::fmt::Display for LianaPolicyError { impl std::fmt::Display for LianaPolicyError {
fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
match self { match self {
Self::MissingRecoveryPath => write!(f, "A Liana policy requires at least one recovery path."),
Self::InsaneTimelock(tl) => { Self::InsaneTimelock(tl) => {
write!(f, "Timelock value '{}' isn't valid or safe to use", tl) write!(f, "Timelock value '{}' isn't valid or safe to use", tl)
} }
@ -64,17 +66,31 @@ fn is_single_key_or_multisig(policy: &SemanticPolicy<descriptor::DescriptorPubli
} }
} }
// We require the descriptor key to: struct DescKeyChecker {
// - Be deriveable (to contain a wildcard) keys_set: HashSet<bip32::ExtendedPubKey>,
// - 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) impl DescKeyChecker {
fn is_valid_desc_key(key: &descriptor::DescriptorPublicKey) -> bool { pub fn new() -> DescKeyChecker {
match *key { DescKeyChecker {
descriptor::DescriptorPublicKey::Single(..) | descriptor::DescriptorPublicKey::XPub(..) => { keys_set: HashSet::new(),
false
} }
descriptor::DescriptorPublicKey::MultiXPub(ref xpub) => { }
/// 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)
/// - Have an xpub that is not a duplicate.
pub fn check(&mut self, key: &descriptor::DescriptorPublicKey) -> Result<(), LianaPolicyError> {
if let descriptor::DescriptorPublicKey::MultiXPub(ref xpub) = *key {
// First make sure it's not a duplicate and record seeing it.
if self.keys_set.contains(&xpub.xkey) {
return Err(LianaPolicyError::DuplicateKey(key.clone().into()));
}
self.keys_set.insert(xpub.xkey);
// Then perform the contextless checks.
let der_paths = xpub.derivation_paths.paths(); let der_paths = xpub.derivation_paths.paths();
// Rust-miniscript enforces BIP389 which states that all paths must have the same len. // 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(); let len = der_paths.get(0).expect("Cannot be empty").len();
@ -82,12 +98,16 @@ fn is_valid_desc_key(key: &descriptor::DescriptorPublicKey) -> bool {
// no unlikely (and easily fixable) while users shooting themselves in the foot by // 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 // forgetting to provide the origin is so likely that it's worth ruling out xpubs
// without origin entirely. // without origin entirely.
xpub.origin.is_some() let valid = xpub.origin.is_some()
&& xpub.wildcard == descriptor::Wildcard::Unhardened && xpub.wildcard == descriptor::Wildcard::Unhardened
&& der_paths.len() == 2 && der_paths.len() == 2
&& der_paths[0][len - 1] == 0.into() && der_paths[0][len - 1] == 0.into()
&& der_paths[1][len - 1] == 1.into() && der_paths[1][len - 1] == 1.into();
if valid {
return Ok(());
}
} }
Err(LianaPolicyError::InvalidKey(key.clone().into()))
} }
} }
@ -283,15 +303,6 @@ impl PathInfo {
} }
} }
// 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(),
}
}
/// Get a Miniscript Policy for this path. /// Get a Miniscript Policy for this path.
pub fn into_ms_policy(self) -> ConcretePolicy<descriptor::DescriptorPublicKey> { pub fn into_ms_policy(self) -> ConcretePolicy<descriptor::DescriptorPublicKey> {
match self { match self {
@ -304,24 +315,31 @@ impl PathInfo {
} }
} }
/// A Liana spending policy. Can be created from some settings (the primary and recovery keys, the /// A Liana spending policy is one composed of at least two spending paths:
/// - A directly available path with any number of keys checks; or
/// - One or more recovery paths with any number of keys checks, behind increasing relative
/// timelocks. No two recovery paths may have the same timelock.
/// A Liana 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 /// timelock(s)) and be used to derive a descriptor. It can also be inferred from a descriptor and
/// be used to retrieve the settings. /// be used to retrieve the settings.
/// Do note however that the descriptor generation process is not deterministic, therefore you /// Do note however that the descriptor generation process is not deterministic, therefore you
/// **cannot roundtrip** a descriptor through a `LianaPolicy`. /// **cannot roundtrip** a descriptor through a `LianaPolicy`.
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)] #[derive(Debug, Eq, PartialEq, Clone)]
pub struct LianaPolicy { pub struct LianaPolicy {
pub(super) primary_path: PathInfo, pub(super) primary_path: PathInfo,
pub(super) recovery_path: (u16, PathInfo), pub(super) recovery_paths: BTreeMap<u16, PathInfo>,
} }
impl LianaPolicy { impl LianaPolicy {
/// Create a new Liana policy from a given configuration. /// Create a new Liana policy from a given configuration.
pub fn new( pub fn new(
primary_path: PathInfo, primary_path: PathInfo,
recovery_path: PathInfo, recovery_paths: BTreeMap<u16, PathInfo>,
recovery_timelock: u16,
) -> Result<LianaPolicy, LianaPolicyError> { ) -> Result<LianaPolicy, LianaPolicyError> {
if recovery_paths.is_empty() {
return Err(LianaPolicyError::MissingRecoveryPath);
}
// We require the locktime to: // We require the locktime to:
// - not be disabled // - not be disabled
// - be in number of blocks // - be in number of blocks
@ -329,36 +347,33 @@ impl LianaPolicy {
// - be positive (Miniscript requires it not to be 0) // - be positive (Miniscript requires it not to be 0)
// //
// All this is achieved through asking for a 16-bit integer. // All this is achieved through asking for a 16-bit integer.
if recovery_timelock == 0 { if recovery_paths.contains_key(&0) {
return Err(LianaPolicyError::InsaneTimelock(recovery_timelock as u32)); return Err(LianaPolicyError::InsaneTimelock(0));
} }
// Check all keys are valid according to our standard (this checks all are multipath keys). // 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()); // Note while the Miniscript compiler does check for duplicate, it does so at the
let all_keys = prim_keys.iter().chain(rec_keys.iter()); // "descriptor key expression" level. We don't want duplicate xpubs at all so we do it
if let Some(key) = all_keys.clone().find(|k| !is_valid_desc_key(k)) { // ourselves here.
return Err(LianaPolicyError::InvalidKey((*key).clone().into())); let spending_paths = recovery_paths
} .values()
.chain(std::iter::once(&primary_path));
// Check for key duplicates. They are invalid in (nonmalleable) miniscripts. This is let mut key_checker = DescKeyChecker::new();
// checked by the Miniscript policy compiler too but not at the raw xpub level. for path in spending_paths {
let mut key_set = HashSet::new(); match path {
for key in all_keys { PathInfo::Single(ref key) => key_checker.check(key)?,
let xpub = match key { PathInfo::Multi(_, ref keys) => {
descriptor::DescriptorPublicKey::MultiXPub(ref multi_xpub) => multi_xpub.xkey, for key in keys {
_ => unreachable!("Just checked it was a multixpub above"), key_checker.check(key)?
}; }
if key_set.contains(&xpub) { }
return Err(LianaPolicyError::DuplicateKey(key.clone().into()));
} }
key_set.insert(xpub);
} }
assert!(!key_set.is_empty());
// Make sure it is a valid Miniscript policy by (ab)using the compiler. // Make sure it is a valid Miniscript policy by (ab)using the compiler.
let policy = LianaPolicy { let policy = LianaPolicy {
primary_path, primary_path,
recovery_path: (recovery_timelock, recovery_path), recovery_paths,
}; };
policy.clone().into_miniscript()?; policy.clone().into_miniscript()?;
Ok(policy) Ok(policy)
@ -375,25 +390,12 @@ impl LianaPolicy {
_ => return Err(LianaPolicyError::IncompatibleDesc), _ => return Err(LianaPolicyError::IncompatibleDesc),
}; };
// Get the Miniscript from the descriptor and make sure it only contains valid multipath // Lift a semantic policy out of this Miniscript and normalize it to make sure we compare
// descriptor keys. // apples to apples below.
let ms = match wsh_desc.as_inner() { let ms = match wsh_desc.as_inner() {
descriptor::WshInner::Ms(ms) => ms, descriptor::WshInner::Ms(ms) => ms,
_ => return Err(LianaPolicyError::IncompatibleDesc), _ => 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 let policy = ms
.lift() .lift()
.expect("Lifting can't fail on a Miniscript") .expect("Lifting can't fail on a Miniscript")
@ -410,7 +412,7 @@ impl LianaPolicy {
// Fetch the two spending paths' semantic policies. The primary path is identified as the // Fetch the two spending paths' semantic policies. The primary path is identified as the
// only one that isn't timelocked. // only one that isn't timelocked.
let (mut primary_path, mut recovery_path) = (None::<PathInfo>, None); let (mut primary_path, mut recovery_paths) = (None::<PathInfo>, BTreeMap::new());
for sub in subs { for sub in subs {
// This is a (multi)key check. It must be the primary path. // This is a (multi)key check. It must be the primary path.
if is_single_key_or_multisig(&sub) { if is_single_key_or_multisig(&sub) {
@ -429,27 +431,29 @@ impl LianaPolicy {
} }
} else { } else {
// If it's not a simple (multi)key check, it must be the timelocked recovery path. // 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. let (timelock, path_info) = PathInfo::from_recovery_path(sub)?;
if recovery_path.is_some() { if recovery_paths.contains_key(&timelock) {
return Err(LianaPolicyError::IncompatibleDesc); return Err(LianaPolicyError::IncompatibleDesc);
} }
recovery_path = Some(PathInfo::from_recovery_path(sub)?); recovery_paths.insert(timelock, path_info);
} }
} }
// Use the constructor for the sanity checks (especially around the Miniscript policy). // Use the constructor for sanity checking the keys and the Miniscript policy. Note this
// makes sure the recovery paths mapping isn't empty, too.
let prim_path = primary_path.ok_or(LianaPolicyError::IncompatibleDesc)?; let prim_path = primary_path.ok_or(LianaPolicyError::IncompatibleDesc)?;
let (timelock, reco_path) = recovery_path.ok_or(LianaPolicyError::IncompatibleDesc)?; LianaPolicy::new(prim_path, recovery_paths)
LianaPolicy::new(prim_path, reco_path, timelock)
} }
pub fn primary_path(&self) -> &PathInfo { pub fn primary_path(&self) -> &PathInfo {
&self.primary_path &self.primary_path
} }
/// Timelock and path info for the recovery path. /// Timelocks and path info of the recovery paths. Note we guarantee this mapping is never
pub fn recovery_path(&self) -> (u16, &PathInfo) { /// empty, as there is always at least one recovery path.
(self.recovery_path.0, &self.recovery_path.1) pub fn recovery_paths(&self) -> &BTreeMap<u16, PathInfo> {
assert!(!self.recovery_paths.is_empty());
&self.recovery_paths
} }
fn into_miniscript( fn into_miniscript(
@ -460,18 +464,24 @@ impl LianaPolicy {
> { > {
let LianaPolicy { let LianaPolicy {
primary_path, primary_path,
recovery_path: (timelock, recovery_path), recovery_paths,
} = self; } = self;
// Create the timelocked recovery spending path. // Start with the primary spending path. We'll then or() all the recovery paths to it.
let recovery_timelock = ConcretePolicy::Older(Sequence::from_height(timelock));
let recovery_keys = recovery_path.into_ms_policy();
let recovery_branch = ConcretePolicy::And(vec![recovery_keys, recovery_timelock]);
// Create the primary spending path and combine both, assuming the recovery path will
// seldom be used.
let primary_keys = primary_path.into_ms_policy(); let primary_keys = primary_path.into_ms_policy();
let tl_policy = ConcretePolicy::Or(vec![(99, primary_keys), (1, recovery_branch)]);
// Incrementally create the top-level policy using all recovery paths.
assert!(!recovery_paths.is_empty());
let tl_policy =
recovery_paths
.into_iter()
.fold(primary_keys, |tl_policy, (timelock, path_info)| {
let timelock = ConcretePolicy::Older(Sequence::from_height(timelock));
let keys = path_info.into_ms_policy();
let recovery_branch = ConcretePolicy::And(vec![keys, timelock]);
// We assume the larger the timelock the less likely a branch would be used.
ConcretePolicy::Or(vec![(99, tl_policy), (1, recovery_branch)])
});
tl_policy tl_policy
.compile::<miniscript::Segwitv0>() .compile::<miniscript::Segwitv0>()
@ -510,9 +520,9 @@ pub struct PathSpendInfo {
pub struct PartialSpendInfo { pub struct PartialSpendInfo {
/// Number of signatures present for the primary path /// Number of signatures present for the primary path
pub(super) primary_path: PathSpendInfo, pub(super) primary_path: PathSpendInfo,
/// Number of signatures present for the recovery path, only present if the path is available /// Number of signatures present for the recovery path, only present for the recovery paths
/// in the first place. /// that are available.
pub(super) recovery_path: Option<PathSpendInfo>, pub(super) recovery_paths: BTreeMap<u16, PathSpendInfo>,
} }
impl PartialSpendInfo { impl PartialSpendInfo {
@ -521,9 +531,9 @@ impl PartialSpendInfo {
&self.primary_path &self.primary_path
} }
/// Get the number of signatures present for the recovery path. Only present if the path is /// Get the number of signatures present for each recovery path. Only present for available
/// available in the first place. /// paths.
pub fn recovery_path(&self) -> &Option<PathSpendInfo> { pub fn recovery_paths(&self) -> &BTreeMap<u16, PathSpendInfo> {
&self.recovery_path &self.recovery_paths
} }
} }

View File

@ -178,10 +178,15 @@ impl LianaDescriptor {
.expect("We never create a Liana descriptor with an invalid Liana policy.") .expect("We never create a Liana descriptor with an invalid Liana policy.")
} }
/// Get the value (in blocks) of the relative timelock for the heir's spending path. /// Get the value (in blocks) of the smallest relative timelock of the recovery paths.
pub fn timelock_value(&self) -> u32 { pub fn first_timelock_value(&self) -> u16 {
// TODO: make it return a u16 *self
self.policy().recovery_path.0 as u32 .policy()
.recovery_paths
.iter()
.next()
.expect("There is always at least one recovery path")
.0
} }
/// Get the maximum size in WU of a satisfaction for this descriptor. /// Get the maximum size in WU of a satisfaction for this descriptor.
@ -229,17 +234,21 @@ impl LianaDescriptor {
// (ie if the nSequence is >= to the chosen CSV value). // (ie if the nSequence is >= to the chosen CSV value).
let desc_info = self.policy(); let desc_info = self.policy();
let primary_path = desc_info.primary_path.spend_info(pubkeys_signed.clone()); let primary_path = desc_info.primary_path.spend_info(pubkeys_signed.clone());
let recovery_path = if txin.sequence.is_height_locked() let recovery_paths = desc_info
&& txin.sequence.0 >= desc_info.recovery_path.0 as u32 .recovery_paths
{ .iter()
Some(desc_info.recovery_path.1.spend_info(pubkeys_signed)) .filter_map(|(timelock, path_info)| {
} else { if txin.sequence.is_height_locked() && txin.sequence.0 >= *timelock as u32 {
None Some((*timelock, path_info.spend_info(pubkeys_signed.clone())))
}; } else {
None
}
})
.collect();
PartialSpendInfo { PartialSpendInfo {
primary_path, primary_path,
recovery_path, recovery_paths,
} }
} }
@ -394,12 +403,28 @@ mod tests {
use crate::signer::HotSigner; use crate::signer::HotSigner;
fn random_desc_key(
secp: &secp256k1::Secp256k1<impl secp256k1::Signing>,
) -> descriptor::DescriptorPublicKey {
let signer = HotSigner::generate(bitcoin::Network::Bitcoin).unwrap();
let xpub_str = format!(
"[{}]{}/<0;1>/*",
signer.fingerprint(secp),
signer.xpub_at(&bip32::DerivationPath::from_str("m").unwrap(), secp)
);
descriptor::DescriptorPublicKey::from_str(&xpub_str).unwrap()
}
#[test] #[test]
fn descriptor_creation() { fn descriptor_creation() {
let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap()); let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap());
let timelock = 52560; let timelock = 52560;
let policy = LianaPolicy::new(owner_key.clone(), heir_key.clone(), timelock).unwrap(); let policy = LianaPolicy::new(
owner_key.clone(),
[(timelock, heir_key.clone())].iter().cloned().collect(),
)
.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!(LianaDescriptor::new(policy).to_string(), "wsh(or_d(pk([abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh([abcdef01]xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#g7vk9r5l");
// A decaying multisig after 6 months. Note we can't duplicate the keys, so different ones // A decaying multisig after 6 months. Note we can't duplicate the keys, so different ones
@ -420,7 +445,11 @@ mod tests {
descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*").unwrap(),
], ],
); );
let policy = LianaPolicy::new(primary_keys, recovery_keys, 26352).unwrap(); let policy = LianaPolicy::new(
primary_keys,
[(26352, recovery_keys)].iter().cloned().collect(),
)
.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:thresh(2,pkh([abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*),a:pkh([abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*),a:pkh([aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*)),older(26352))))#hmsqemgr"); 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:thresh(2,pkh([abcdef01]xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*),a:pkh([abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*),a:pkh([aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*)),older(26352))))#hmsqemgr");
// We prevent footguns with timelocks by requiring a u16. Note how the following wouldn't // We prevent footguns with timelocks by requiring a u16. Note how the following wouldn't
@ -430,32 +459,52 @@ mod tests {
//LianaPolicy::new(owner_key, heir_key, (1 << 22) + 1).unwrap_err(); //LianaPolicy::new(owner_key, heir_key, (1 << 22) + 1).unwrap_err();
// You can't use a null timelock in Miniscript. // You can't use a null timelock in Miniscript.
LianaPolicy::new(owner_key, heir_key, 0).unwrap_err(); LianaPolicy::new(owner_key, [(0, heir_key)].iter().cloned().collect()).unwrap_err();
let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*").unwrap()); let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*").unwrap());
let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*").unwrap()); let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*").unwrap());
let timelock = 57600; let timelock = 57600;
let policy = LianaPolicy::new(owner_key.clone(), heir_key, timelock).unwrap(); let policy = LianaPolicy::new(
owner_key.clone(),
[(timelock, heir_key)].iter().cloned().collect(),
)
.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!(LianaDescriptor::new(policy).to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*),and_v(v:pkh([abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*),older(57600))))#ak4cm093");
// We can't pass a raw key, an xpub that is not deriveable, only hardened derivable, // 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 // without both the change and receive derivation paths, or with more than 2 different
// derivation paths. // derivation paths.
let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap()); 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(); LianaPolicy::new(
owner_key.clone(),
[(timelock, heir_key)].iter().cloned().collect(),
)
.unwrap_err();
let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/*'").unwrap()); 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(); LianaPolicy::new(
owner_key.clone(),
[(timelock, heir_key)].iter().cloned().collect(),
)
.unwrap_err();
let heir_key = PathInfo::Single( let heir_key = PathInfo::Single(
descriptor::DescriptorPublicKey::from_str( descriptor::DescriptorPublicKey::from_str(
"[abcdef01]02e24913be26dbcfdf8e8e94870b28725cdae09b448b6c127767bf0154e3a3c8e5", "[abcdef01]02e24913be26dbcfdf8e8e94870b28725cdae09b448b6c127767bf0154e3a3c8e5",
) )
.unwrap(), .unwrap(),
); );
LianaPolicy::new(owner_key.clone(), heir_key, timelock).unwrap_err(); LianaPolicy::new(
owner_key.clone(),
[(timelock, heir_key)].iter().cloned().collect(),
)
.unwrap_err();
let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap()); let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap());
LianaPolicy::new(owner_key.clone(), heir_key, timelock).unwrap_err(); LianaPolicy::new(
owner_key.clone(),
[(timelock, heir_key)].iter().cloned().collect(),
)
.unwrap_err();
let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/<0;1;2>/*'").unwrap()); let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/<0;1;2>/*'").unwrap());
LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err(); LianaPolicy::new(owner_key, [(timelock, heir_key)].iter().cloned().collect()).unwrap_err();
// And it's checked even in a multisig. For instance: // And it's checked even in a multisig. For instance:
let primary_keys = PathInfo::Multi( let primary_keys = PathInfo::Multi(
@ -472,18 +521,22 @@ mod tests {
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
], ],
); );
LianaPolicy::new(primary_keys, recovery_keys, 26352).unwrap_err(); LianaPolicy::new(
primary_keys,
[(26352, recovery_keys)].iter().cloned().collect(),
)
.unwrap_err();
// You can't pass duplicate keys, even if they are encoded differently. // 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 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()); let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err(); LianaPolicy::new(owner_key, [(timelock, heir_key)].iter().cloned().collect()).unwrap_err();
let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[00aabb44]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); 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()); let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err(); LianaPolicy::new(owner_key, [(timelock, heir_key)].iter().cloned().collect()).unwrap_err();
let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[00aabb44]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); 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()); 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(); LianaPolicy::new(owner_key, [(timelock, heir_key)].iter().cloned().collect()).unwrap_err();
// You can't pass duplicate keys, even across multisigs. // You can't pass duplicate keys, even across multisigs.
let primary_keys = PathInfo::Multi( let primary_keys = PathInfo::Multi(
@ -502,13 +555,17 @@ mod tests {
descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
], ],
); );
LianaPolicy::new(primary_keys, recovery_keys, 26352).unwrap_err(); LianaPolicy::new(
primary_keys,
[(26352, recovery_keys)].iter().cloned().collect(),
)
.unwrap_err();
// No origin in one of the keys // No origin in one of the keys
let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap()); let owner_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[abcdef01]xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap()); let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap());
let timelock = 52560; let timelock = 52560;
LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err(); LianaPolicy::new(owner_key, [(timelock, heir_key)].iter().cloned().collect()).unwrap_err();
// A 1-of-N multisig as primary path. // 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(); 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();
@ -536,13 +593,13 @@ mod tests {
LianaDescriptor::from_str("wsh(or_i(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap_err(); LianaDescriptor::from_str("wsh(or_i(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap_err();
let desc = LianaDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap(); let desc = LianaDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 1); assert_eq!(desc.first_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 = LianaDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(42000),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 42000); assert_eq!(desc.first_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 = LianaDescriptor::from_str("wsh(andor(pk([abcdef01]tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(65535),pk([abcdef01]tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 0xffff); assert_eq!(desc.first_timelock_value(), 0xffff);
} }
#[test] #[test]
@ -574,59 +631,88 @@ mod tests {
#[test] #[test]
fn liana_desc_keys() { fn liana_desc_keys() {
let secp = secp256k1::Secp256k1::signing_only(); let secp = secp256k1::Secp256k1::signing_only();
let random_desc_key = || { let prim_path = PathInfo::Single(random_desc_key(&secp));
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_eight_keys: Vec<descriptor::DescriptorPublicKey> = let twenty_eight_keys: Vec<descriptor::DescriptorPublicKey> =
(0..28).map(|_| random_desc_key()).collect(); (0..28).map(|_| random_desc_key(&secp)).collect();
let mut twenty_nine_keys = twenty_eight_keys.clone(); let mut twenty_nine_keys = twenty_eight_keys.clone();
twenty_nine_keys.push(random_desc_key()); twenty_nine_keys.push(random_desc_key(&secp));
LianaPolicy::new( LianaPolicy::new(
prim_path.clone(), prim_path.clone(),
PathInfo::Multi(2, vec![random_desc_key()]), [(1, PathInfo::Multi(2, vec![random_desc_key(&secp)]))]
1, .iter()
.cloned()
.collect(),
) )
.unwrap_err(); .unwrap_err();
LianaPolicy::new( LianaPolicy::new(
prim_path.clone(), prim_path.clone(),
PathInfo::Multi(1, vec![random_desc_key(), random_desc_key()]), [(
1, 1,
PathInfo::Multi(1, vec![random_desc_key(&secp), random_desc_key(&secp)]),
)]
.iter()
.cloned()
.collect(),
) )
.unwrap(); .unwrap();
LianaPolicy::new( LianaPolicy::new(
prim_path.clone(), prim_path.clone(),
PathInfo::Multi(0, vec![random_desc_key(), random_desc_key()]), [(
1, 1,
PathInfo::Multi(0, vec![random_desc_key(&secp), random_desc_key(&secp)]),
)]
.iter()
.cloned()
.collect(),
) )
.unwrap_err(); .unwrap_err();
LianaPolicy::new( LianaPolicy::new(
prim_path.clone(), prim_path.clone(),
PathInfo::Multi(2, vec![random_desc_key(), random_desc_key()]), [(
1, 1,
PathInfo::Multi(2, vec![random_desc_key(&secp), random_desc_key(&secp)]),
)]
.iter()
.cloned()
.collect(),
) )
.unwrap(); .unwrap();
LianaPolicy::new( LianaPolicy::new(
prim_path.clone(), prim_path.clone(),
PathInfo::Multi(3, vec![random_desc_key(), random_desc_key()]), [(
1, 1,
PathInfo::Multi(3, vec![random_desc_key(&secp), random_desc_key(&secp)]),
)]
.iter()
.cloned()
.collect(),
) )
.unwrap_err(); .unwrap_err();
LianaPolicy::new( LianaPolicy::new(
prim_path.clone(), prim_path.clone(),
PathInfo::Multi(3, twenty_eight_keys.clone()), [(1, PathInfo::Multi(3, twenty_eight_keys.clone()))]
1, .iter()
.cloned()
.collect(),
) )
.unwrap(); .unwrap();
LianaPolicy::new(prim_path.clone(), PathInfo::Multi(20, twenty_eight_keys), 1).unwrap(); LianaPolicy::new(
LianaPolicy::new(prim_path, PathInfo::Multi(20, twenty_nine_keys), 1).unwrap_err(); prim_path.clone(),
[(1, PathInfo::Multi(20, twenty_eight_keys))]
.iter()
.cloned()
.collect(),
)
.unwrap();
LianaPolicy::new(
prim_path,
[(1, PathInfo::Multi(20, twenty_nine_keys))]
.iter()
.cloned()
.collect(),
)
.unwrap_err();
} }
fn roundtrip(desc_str: &str) { fn roundtrip(desc_str: &str) {
@ -652,6 +738,8 @@ mod tests {
#[test] #[test]
fn partial_spend_info() { fn partial_spend_info() {
let secp = secp256k1::Secp256k1::signing_only();
// A simple descriptor with 1 keys as primary path and 1 recovery key. // 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 = 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_info = desc.policy();
@ -671,25 +759,24 @@ mod tests {
assert_eq!(info.primary_path.threshold, 1); assert_eq!(info.primary_path.threshold, 1);
assert_eq!(info.primary_path.sigs_count, 0); assert_eq!(info.primary_path.sigs_count, 0);
assert!(info.primary_path.signed_pubkeys.is_empty()); assert!(info.primary_path.signed_pubkeys.is_empty());
assert!(info.recovery_path.is_none()); assert!(info.recovery_paths.is_empty());
// If we set the sequence too low we still won't have the recovery path info. // If we set the sequence too low we still won't have the recovery path info.
unsigned_single_psbt.unsigned_tx.input[0].sequence = unsigned_single_psbt.unsigned_tx.input[0].sequence =
Sequence::from_height(desc_info.recovery_path.0 - 1); Sequence::from_height(desc_info.recovery_paths.keys().next().unwrap() - 1);
let info = desc.partial_spend_info(&unsigned_single_psbt).unwrap(); let info = desc.partial_spend_info(&unsigned_single_psbt).unwrap();
assert!(info.recovery_path.is_none()); assert!(info.recovery_paths.is_empty());
// Now if we set the sequence at the right value we'll have it. // Now if we set the sequence at the right value we'll have it.
unsigned_single_psbt.unsigned_tx.input[0].sequence = let timelock = *desc_info.recovery_paths.keys().next().unwrap();
Sequence::from_height(desc_info.recovery_path.0); unsigned_single_psbt.unsigned_tx.input[0].sequence = Sequence::from_height(timelock);
let info = desc.partial_spend_info(&unsigned_single_psbt).unwrap(); let info = desc.partial_spend_info(&unsigned_single_psbt).unwrap();
assert!(info.recovery_path.is_some()); assert!(info.recovery_paths.contains_key(&timelock));
// Even if it's a bit too high (as long as it's still a block height and activated) // Even if it's a bit too high (as long as it's still a block height and activated)
unsigned_single_psbt.unsigned_tx.input[0].sequence = unsigned_single_psbt.unsigned_tx.input[0].sequence = Sequence::from_height(timelock + 42);
Sequence::from_height(desc_info.recovery_path.0 + 42);
let info = desc.partial_spend_info(&unsigned_single_psbt).unwrap(); let info = desc.partial_spend_info(&unsigned_single_psbt).unwrap();
let recov_info = info.recovery_path.unwrap(); let recov_info = info.recovery_paths.get(&timelock).unwrap();
assert_eq!(recov_info.threshold, 1); assert_eq!(recov_info.threshold, 1);
assert_eq!(recov_info.sigs_count, 0); assert_eq!(recov_info.sigs_count, 0);
assert!(recov_info.signed_pubkeys.is_empty()); assert!(recov_info.signed_pubkeys.is_empty());
@ -707,11 +794,10 @@ mod tests {
.signed_pubkeys .signed_pubkeys
.contains_key(&prim_key_origin) .contains_key(&prim_key_origin)
); );
assert!(info.recovery_path.is_none()); assert!(info.recovery_paths.is_empty());
// Now enable the recovery path and add a signature for the recovery key. // Now enable the recovery path and add a signature for the recovery key.
signed_single_psbt.unsigned_tx.input[0].sequence = signed_single_psbt.unsigned_tx.input[0].sequence = Sequence::from_height(timelock);
Sequence::from_height(desc_info.recovery_path.0);
let recov_pubkey = bitcoin::PublicKey { let recov_pubkey = bitcoin::PublicKey {
compressed: true, compressed: true,
inner: *signed_single_psbt.inputs[0] inner: *signed_single_psbt.inputs[0]
@ -742,7 +828,7 @@ mod tests {
assert_eq!(info.primary_path.threshold, 1); assert_eq!(info.primary_path.threshold, 1);
assert_eq!(info.primary_path.sigs_count, 0); assert_eq!(info.primary_path.sigs_count, 0);
assert!(info.primary_path.signed_pubkeys.is_empty()); assert!(info.primary_path.signed_pubkeys.is_empty());
let recov_info = info.recovery_path.unwrap(); let recov_info = info.recovery_paths.get(&timelock).unwrap();
assert_eq!(recov_info.threshold, 1); assert_eq!(recov_info.threshold, 1);
assert_eq!(recov_info.sigs_count, 1); assert_eq!(recov_info.sigs_count, 1);
assert!( assert!(
@ -766,12 +852,12 @@ mod tests {
.signed_pubkeys .signed_pubkeys
.contains_key(&prim_key_origin) .contains_key(&prim_key_origin)
); );
assert!(info.recovery_path.is_none()); assert!(info.recovery_paths.is_empty());
// Enable the recovery path, it should show no recovery sig. // Enable the recovery path, it should show no recovery sig.
let mut rec_psbt = psbt.clone(); let mut rec_psbt = psbt.clone();
for txin in rec_psbt.unsigned_tx.input.iter_mut() { for txin in rec_psbt.unsigned_tx.input.iter_mut() {
txin.sequence = Sequence::from_height(desc_info.recovery_path.0); txin.sequence = Sequence::from_height(timelock);
} }
let info = desc.partial_spend_info(&rec_psbt).unwrap(); let info = desc.partial_spend_info(&rec_psbt).unwrap();
assert!(rec_psbt assert!(rec_psbt
@ -787,7 +873,7 @@ mod tests {
.signed_pubkeys .signed_pubkeys
.contains_key(&prim_key_origin) .contains_key(&prim_key_origin)
); );
let recov_info = info.recovery_path.unwrap(); let recov_info = info.recovery_paths.get(&timelock).unwrap();
assert_eq!(recov_info.threshold, 1); assert_eq!(recov_info.threshold, 1);
assert_eq!(recov_info.sigs_count, 0); assert_eq!(recov_info.sigs_count, 0);
assert!(recov_info.signed_pubkeys.is_empty()); assert!(recov_info.signed_pubkeys.is_empty());
@ -795,8 +881,7 @@ mod tests {
// If the sequence of one of the input is different from the other ones, it'll return // If the sequence of one of the input is different from the other ones, it'll return
// an error since the analysis is on the whole transaction. // an error since the analysis is on the whole transaction.
let mut inconsistent_psbt = psbt.clone(); let mut inconsistent_psbt = psbt.clone();
inconsistent_psbt.unsigned_tx.input[0].sequence = inconsistent_psbt.unsigned_tx.input[0].sequence = Sequence::from_height(timelock + 1);
Sequence::from_height(desc_info.recovery_path.0 + 1);
assert!(desc assert!(desc
.partial_spend_info(&inconsistent_psbt) .partial_spend_info(&inconsistent_psbt)
.unwrap_err() .unwrap_err()
@ -828,7 +913,7 @@ mod tests {
.signed_pubkeys .signed_pubkeys
.contains_key(&prim_key_origin) .contains_key(&prim_key_origin)
); );
assert!(info.recovery_path.is_none()); assert!(info.recovery_paths.is_empty());
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 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 info = desc.policy();
@ -839,19 +924,163 @@ mod tests {
descriptor::DescriptorPublicKey::from_str("[ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/<0;1>/*").unwrap(),
], ],
)); ));
assert_eq!(info.recovery_path, (2, PathInfo::Multi( assert_eq!(info.recovery_paths, [(2, PathInfo::Multi(
2, 2,
vec![ vec![
descriptor::DescriptorPublicKey::from_str("[636adf3f/48'/1'/1'/2']tpubDDvF2khuoBBj8vcSjQfa7iKaxsQZE7YjJ7cJL8A8eaneadMPKbHSpoSr4JD1F5LUvWD82HCxdtSppGfrMUmiNbFxrA2EHEVLnrdCFNFe75D/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[636adf3f/48'/1'/1'/2']tpubDDvF2khuoBBj8vcSjQfa7iKaxsQZE7YjJ7cJL8A8eaneadMPKbHSpoSr4JD1F5LUvWD82HCxdtSppGfrMUmiNbFxrA2EHEVLnrdCFNFe75D/<0;1>/*").unwrap(),
descriptor::DescriptorPublicKey::from_str("[ffd63c8d/48'/1'/1'/2']tpubDFMs44FD4kFt3M7Z317cFh5tdKEGN8tyQRY6Q5gcSha4NtxZfGmTVRMbsD1bWN469LstXU4aVSARDxrvxFCUjHeegfEY2cLSazMBkNCmDPD/<0;1>/*").unwrap(), descriptor::DescriptorPublicKey::from_str("[ffd63c8d/48'/1'/1'/2']tpubDFMs44FD4kFt3M7Z317cFh5tdKEGN8tyQRY6Q5gcSha4NtxZfGmTVRMbsD1bWN469LstXU4aVSARDxrvxFCUjHeegfEY2cLSazMBkNCmDPD/<0;1>/*").unwrap(),
], ],
))); ))].iter().cloned().collect());
let psbt = psbt_from_str("cHNidP8BAIkCAAAAAWi3OFgkj1CqCDT3Swm8kbxZS9lxz4L3i4W2v9KGC7nqAQAAAAD9////AkANAwAAAAAAIgAg27lNc1rog+dOq80ohRuds4Hgg/RcpxVun2XwgpuLSrFYMwwAAAAAACIAIDyWveqaElWmFGkTbFojg1zXWHODtiipSNjfgi2DqBy9AAAAAAABAOoCAAAAAAEBsRWl70USoAFFozxc86pC7Dovttdg4kvja//3WMEJskEBAAAAAP7///8CWKmCIk4GAAAWABRKBWYWkCNS46jgF0r69Ehdnq+7T0BCDwAAAAAAIgAgTt5fs+CiB+FRzNC8lHcgWLH205sNjz1pT59ghXlG5tQCRzBEAiBXK9MF8z3bX/VnY2aefgBBmiAHPL4tyDbUOe7+KpYA4AIgL5kU0DFG8szKd+szRzz/OTUWJ0tZqij41h2eU9rSe1IBIQNBB1hy+jKsg1TihMT0dXw7etpu9TkO3NuvhBDFJlBj1cP2AQABAStAQg8AAAAAACIAIE7eX7PgogfhUczQvJR3IFix9tObDY89aU+fYIV5RubUIgICSKJsNs0zFJN58yd2aYQ+C3vhMbi0x7k0FV3wBhR4THlIMEUCIQCPWWWOhs2lThxOq/G8X2fYBRvM9MXSm7qPH+dRVYQZEwIgfut2vx3RvwZWcgEj4ohQJD5lNJlwOkA4PAiN1fjx6dABIgID3mvj1zerZKohOVhKCiskYk+3qrCum6PIwDhQ16ePACpHMEQCICZNR+0/1hPkrDQwPFmg5VjUHkh6aK9cXUu3kPbM8hirAiAyE/5NUXKfmFKij30isuyysJbq8HrURjivd+S9vdRGKQEBBZNSIQJIomw2zTMUk3nzJ3ZphD4Le+ExuLTHuTQVXfAGFHhMeSEC9OfCXl+sJOrxUFLBuMV4ZUlJYjuzNGZSld5ioY14y8FSrnNkUSED3mvj1zerZKohOVhKCiskYk+3qrCum6PIwDhQ16ePACohA+ECH+HlR+8Sf3pumaXH3IwSsoqSLCH7H1THiBP93z3ZUq9SsmgiBgJIomw2zTMUk3nzJ3ZphD4Le+ExuLTHuTQVXfAGFHhMeRxjat8/MAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgYC9OfCXl+sJOrxUFLBuMV4ZUlJYjuzNGZSld5ioY14y8Ec/9Y8jTAAAIABAACAAAAAgAIAAIAAAAAAAQAAACIGA95r49c3q2SqITlYSgorJGJPt6qwrpujyMA4UNenjwAqHGNq3z8wAACAAQAAgAEAAIACAACAAAAAAAEAAAAiBgPhAh/h5UfvEn96bpmlx9yMErKKkiwh+x9Ux4gT/d892Rz/1jyNMAAAgAEAAIABAACAAgAAgAAAAAABAAAAACICAlBQ7gGocg7eF3sXrCio+zusAC9+xfoyIV95AeR69DWvHGNq3z8wAACAAQAAgAEAAIACAACAAAAAAAMAAAAiAgMvVy984eg8Kgvj058PBHetFayWbRGb7L0DMnS9KHSJzBxjat8/MAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgIDSRIG1dn6njdjsDXenHa2lUvQHWGPLKBVrSzbQOhiIxgc/9Y8jTAAAIABAACAAAAAgAIAAIAAAAAAAwAAACICA0/epE59sVEj7Et0I4R9qJQNuX23RNvDZKCRL7eUps9FHP/WPI0wAACAAQAAgAEAAIACAACAAAAAAAMAAAAAIgICgldCOK6iHscv//2NipgaMABLV5TICU/zlP7HlQmlg08cY2rfPzAAAIABAACAAQAAgAIAAIABAAAAAQAAACICApb0p9rfpJshB3J186PGWrvzQdixcwQZWmebOUMdkquZHP/WPI0wAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgLY5q+unoDxC/HI5BaNiPq12ei1REZIcUAN304JfKXUwxz/1jyNMAAAgAEAAIABAACAAgAAgAEAAAABAAAAIgIDg6cUVCJB79cMcofiURHojxFARWyS4YEhJNRixuOZZRgcY2rfPzAAAIABAACAAAAAgAIAAIABAAAAAQAAAAA="); let mut psbt = psbt_from_str("cHNidP8BAIkCAAAAAWi3OFgkj1CqCDT3Swm8kbxZS9lxz4L3i4W2v9KGC7nqAQAAAAD9////AkANAwAAAAAAIgAg27lNc1rog+dOq80ohRuds4Hgg/RcpxVun2XwgpuLSrFYMwwAAAAAACIAIDyWveqaElWmFGkTbFojg1zXWHODtiipSNjfgi2DqBy9AAAAAAABAOoCAAAAAAEBsRWl70USoAFFozxc86pC7Dovttdg4kvja//3WMEJskEBAAAAAP7///8CWKmCIk4GAAAWABRKBWYWkCNS46jgF0r69Ehdnq+7T0BCDwAAAAAAIgAgTt5fs+CiB+FRzNC8lHcgWLH205sNjz1pT59ghXlG5tQCRzBEAiBXK9MF8z3bX/VnY2aefgBBmiAHPL4tyDbUOe7+KpYA4AIgL5kU0DFG8szKd+szRzz/OTUWJ0tZqij41h2eU9rSe1IBIQNBB1hy+jKsg1TihMT0dXw7etpu9TkO3NuvhBDFJlBj1cP2AQABAStAQg8AAAAAACIAIE7eX7PgogfhUczQvJR3IFix9tObDY89aU+fYIV5RubUIgICSKJsNs0zFJN58yd2aYQ+C3vhMbi0x7k0FV3wBhR4THlIMEUCIQCPWWWOhs2lThxOq/G8X2fYBRvM9MXSm7qPH+dRVYQZEwIgfut2vx3RvwZWcgEj4ohQJD5lNJlwOkA4PAiN1fjx6dABIgID3mvj1zerZKohOVhKCiskYk+3qrCum6PIwDhQ16ePACpHMEQCICZNR+0/1hPkrDQwPFmg5VjUHkh6aK9cXUu3kPbM8hirAiAyE/5NUXKfmFKij30isuyysJbq8HrURjivd+S9vdRGKQEBBZNSIQJIomw2zTMUk3nzJ3ZphD4Le+ExuLTHuTQVXfAGFHhMeSEC9OfCXl+sJOrxUFLBuMV4ZUlJYjuzNGZSld5ioY14y8FSrnNkUSED3mvj1zerZKohOVhKCiskYk+3qrCum6PIwDhQ16ePACohA+ECH+HlR+8Sf3pumaXH3IwSsoqSLCH7H1THiBP93z3ZUq9SsmgiBgJIomw2zTMUk3nzJ3ZphD4Le+ExuLTHuTQVXfAGFHhMeRxjat8/MAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgYC9OfCXl+sJOrxUFLBuMV4ZUlJYjuzNGZSld5ioY14y8Ec/9Y8jTAAAIABAACAAAAAgAIAAIAAAAAAAQAAACIGA95r49c3q2SqITlYSgorJGJPt6qwrpujyMA4UNenjwAqHGNq3z8wAACAAQAAgAEAAIACAACAAAAAAAEAAAAiBgPhAh/h5UfvEn96bpmlx9yMErKKkiwh+x9Ux4gT/d892Rz/1jyNMAAAgAEAAIABAACAAgAAgAAAAAABAAAAACICAlBQ7gGocg7eF3sXrCio+zusAC9+xfoyIV95AeR69DWvHGNq3z8wAACAAQAAgAEAAIACAACAAAAAAAMAAAAiAgMvVy984eg8Kgvj058PBHetFayWbRGb7L0DMnS9KHSJzBxjat8/MAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgIDSRIG1dn6njdjsDXenHa2lUvQHWGPLKBVrSzbQOhiIxgc/9Y8jTAAAIABAACAAAAAgAIAAIAAAAAAAwAAACICA0/epE59sVEj7Et0I4R9qJQNuX23RNvDZKCRL7eUps9FHP/WPI0wAACAAQAAgAEAAIACAACAAAAAAAMAAAAAIgICgldCOK6iHscv//2NipgaMABLV5TICU/zlP7HlQmlg08cY2rfPzAAAIABAACAAQAAgAIAAIABAAAAAQAAACICApb0p9rfpJshB3J186PGWrvzQdixcwQZWmebOUMdkquZHP/WPI0wAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgLY5q+unoDxC/HI5BaNiPq12ei1REZIcUAN304JfKXUwxz/1jyNMAAAgAEAAIABAACAAgAAgAEAAAABAAAAIgIDg6cUVCJB79cMcofiURHojxFARWyS4YEhJNRixuOZZRgcY2rfPzAAAIABAACAAAAAgAIAAIABAAAAAQAAAAA=");
let partial_info = desc.partial_spend_info(&psbt).unwrap(); let partial_info = desc.partial_spend_info(&psbt).unwrap();
assert_eq!(partial_info.primary_path.threshold, 2); assert_eq!(partial_info.primary_path.threshold, 2);
assert_eq!(partial_info.primary_path.sigs_count, 1); assert_eq!(partial_info.primary_path.sigs_count, 1);
assert_eq!(partial_info.primary_path.signed_pubkeys.len(), 1); assert_eq!(partial_info.primary_path.signed_pubkeys.len(), 1);
assert!(partial_info.recovery_path.is_none()); assert!(partial_info.recovery_paths.is_empty());
// A not very well thought-out decaying multisig.
let prim_path = PathInfo::Multi(3, (0..3).map(|_| random_desc_key(&secp)).collect());
let first_reco_path = PathInfo::Multi(3, (0..5).map(|_| random_desc_key(&secp)).collect());
let sec_reco_path = PathInfo::Multi(2, (0..5).map(|_| random_desc_key(&secp)).collect());
let third_reco_path = PathInfo::Multi(1, (0..5).map(|_| random_desc_key(&secp)).collect());
let liana_policy = LianaPolicy::new(
prim_path.clone(),
[
(26784, first_reco_path.clone()),
(53568, sec_reco_path.clone()),
(62496, third_reco_path.clone()),
]
.iter()
.cloned()
.collect(),
)
.unwrap();
let desc = LianaDescriptor::new(liana_policy.clone());
let policy = desc.policy();
assert_eq!(policy, liana_policy);
let empty_partial_info = desc.partial_spend_info(&psbt).unwrap();
assert_eq!(empty_partial_info.primary_path.threshold, 3);
assert_eq!(empty_partial_info.primary_path.sigs_count, 0);
assert_eq!(
empty_partial_info.primary_path.sigs_count,
empty_partial_info.primary_path.signed_pubkeys.len()
);
assert!(empty_partial_info.recovery_paths.is_empty());
// Now set a signature for the primary path. All recovery paths still empty, a signature is
// present for the primary path.
let dummy_pubkey = bitcoin::PublicKey::from_str(
"0282574238aea21ec72ffffd8d8a981a30004b5794c8094ff394fec79509a5834f",
)
.unwrap();
let dummy_sig = bitcoin::EcdsaSig::from_str ("30440220264d47ed3fd613e4ac34303c59a0e558d41e487a68af5c5d4bb790f6ccf218ab02203213fe4d51729f9852a28f7d22b2ecb2b096eaf07ad44638af77e4bdbdd4462901").unwrap();
let dummy_der_path = bip32::DerivationPath::from_str("m/0/1").unwrap();
let fingerprint = prim_path.thresh_origins().1.into_iter().next().unwrap().0;
psbt.inputs[0]
.bip32_derivation
.insert(dummy_pubkey.inner, (fingerprint, dummy_der_path));
psbt.inputs[0].partial_sigs.insert(dummy_pubkey, dummy_sig);
let partial_info = desc.partial_spend_info(&psbt).unwrap();
assert_eq!(partial_info.primary_path.threshold, 3);
assert_eq!(partial_info.primary_path.sigs_count, 1);
assert_eq!(
partial_info.primary_path.sigs_count,
partial_info.primary_path.signed_pubkeys.len()
);
assert!(partial_info.recovery_paths.is_empty());
// Now enable the first recovery path and make the signature be for this path.
let fingerprint = first_reco_path
.thresh_origins()
.1
.into_iter()
.next()
.unwrap()
.0;
psbt.inputs[0]
.bip32_derivation
.get_mut(&dummy_pubkey.inner)
.unwrap()
.0 = fingerprint;
let partial_info = desc.partial_spend_info(&psbt).unwrap();
assert_eq!(partial_info.primary_path.threshold, 3);
assert_eq!(partial_info.primary_path.sigs_count, 0);
assert_eq!(
partial_info.primary_path.sigs_count,
partial_info.primary_path.signed_pubkeys.len()
);
assert!(partial_info.recovery_paths.is_empty());
psbt.unsigned_tx.input[0].sequence = bitcoin::Sequence::from_height(26784);
let partial_info = desc.partial_spend_info(&psbt).unwrap();
assert_eq!(partial_info.recovery_paths.len(), 1);
assert_eq!(partial_info.recovery_paths[&26784].threshold, 3);
assert_eq!(partial_info.recovery_paths[&26784].sigs_count, 1);
assert_eq!(
partial_info.recovery_paths[&26784].signed_pubkeys.len(),
partial_info.recovery_paths[&26784].sigs_count
);
// Now enable the second recovery path and make the signature be for this path.
let fingerprint = sec_reco_path
.thresh_origins()
.1
.into_iter()
.next()
.unwrap()
.0;
psbt.inputs[0]
.bip32_derivation
.get_mut(&dummy_pubkey.inner)
.unwrap()
.0 = fingerprint;
psbt.unsigned_tx.input[0].sequence = bitcoin::Sequence::from_height(53568);
let partial_info = desc.partial_spend_info(&psbt).unwrap();
assert_eq!(partial_info.primary_path.threshold, 3);
assert_eq!(partial_info.primary_path.sigs_count, 0);
assert_eq!(
partial_info.primary_path.sigs_count,
partial_info.primary_path.signed_pubkeys.len()
);
assert_eq!(partial_info.recovery_paths.len(), 2);
assert_eq!(partial_info.recovery_paths[&26784].threshold, 3);
assert_eq!(partial_info.recovery_paths[&26784].sigs_count, 0);
assert_eq!(partial_info.recovery_paths[&53568].threshold, 2);
assert_eq!(partial_info.recovery_paths[&53568].sigs_count, 1);
for rec_path in partial_info.recovery_paths.values() {
assert_eq!(rec_path.sigs_count, rec_path.signed_pubkeys.len());
}
// Finally do the same for the third recovery path.
let fingerprint = third_reco_path
.thresh_origins()
.1
.into_iter()
.next()
.unwrap()
.0;
psbt.inputs[0]
.bip32_derivation
.get_mut(&dummy_pubkey.inner)
.unwrap()
.0 = fingerprint;
psbt.unsigned_tx.input[0].sequence = bitcoin::Sequence::from_height(62496);
let partial_info = desc.partial_spend_info(&psbt).unwrap();
assert_eq!(partial_info.primary_path.threshold, 3);
assert_eq!(partial_info.primary_path.sigs_count, 0);
assert_eq!(
partial_info.primary_path.sigs_count,
partial_info.primary_path.signed_pubkeys.len()
);
assert_eq!(partial_info.recovery_paths.len(), 3);
assert_eq!(partial_info.recovery_paths[&26784].threshold, 3);
assert_eq!(partial_info.recovery_paths[&26784].sigs_count, 0);
assert_eq!(partial_info.recovery_paths[&53568].threshold, 2);
assert_eq!(partial_info.recovery_paths[&53568].sigs_count, 0);
assert_eq!(partial_info.recovery_paths[&62496].threshold, 1);
assert_eq!(partial_info.recovery_paths[&62496].sigs_count, 1);
for rec_path in partial_info.recovery_paths.values() {
assert_eq!(rec_path.sigs_count, rec_path.signed_pubkeys.len());
}
} }
// TODO: test error conditions of deserialization. // TODO: test error conditions of deserialization.

View File

@ -423,7 +423,9 @@ mod tests {
wildcard: Wildcard::Unhardened, wildcard: Wildcard::Unhardened,
}); });
let recov_keys = descriptors::PathInfo::Single(recov_key); let recov_keys = descriptors::PathInfo::Single(recov_key);
let policy = descriptors::LianaPolicy::new(prim_keys, recov_keys, 42).unwrap(); let policy =
descriptors::LianaPolicy::new(prim_keys, [(46, recov_keys)].iter().cloned().collect())
.unwrap();
let desc = descriptors::LianaDescriptor::new(policy); let desc = descriptors::LianaDescriptor::new(policy);
// Create a dummy PSBT spending a coin from this descriptor with a single input and single // Create a dummy PSBT spending a coin from this descriptor with a single input and single

View File

@ -402,7 +402,11 @@ impl DummyLiana {
let owner_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap()); let owner_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap());
let heir_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap()); let 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 policy = descriptors::LianaPolicy::new(
owner_key,
[(10_000, heir_key)].iter().cloned().collect(),
)
.unwrap();
let desc = descriptors::LianaDescriptor::new(policy); let desc = descriptors::LianaDescriptor::new(policy);
let config = Config { let config = Config {
bitcoin_config, bitcoin_config,