diff --git a/src/descriptors/analysis.rs b/src/descriptors/analysis.rs index 07e34a1c..c34ea2e6 100644 --- a/src/descriptors/analysis.rs +++ b/src/descriptors/analysis.rs @@ -201,6 +201,17 @@ impl PathInfo { } } + /// Add another available key to this `PathInfo`. Note this doesn't change the threshold. + pub fn with_added_key(mut self, key: descriptor::DescriptorPublicKey) -> Self { + match self { + Self::Single(curr_key) => Self::Multi(1, vec![curr_key, key]), + Self::Multi(_, ref mut keys) => { + keys.push(key); + self + } + } + } + /// Get the required number of keys for spending through this path, and the set of keys /// that can be used to provide a signature for this path. pub fn thresh_origins(&self) -> (usize, HashSet<(bip32::Fingerprint, bip32::DerivationPath)>) { @@ -409,40 +420,47 @@ impl LianaPolicy { .expect("Lifting can't fail on a Miniscript") .normalized(); - // For now we only accept a single timelocked recovery path. + // The policy must always be "1 of N spending paths" with at least an always-available + // primary path with at least one key, and at least one timelocked recovery path with at + // least one key. let subs = match policy { SemanticPolicy::Threshold(1, subs) => Some(subs), _ => None, } .ok_or(LianaPolicyError::IncompatibleDesc)?; - if subs.len() != 2 { - return Err(LianaPolicyError::IncompatibleDesc); - } // Fetch the two spending paths' semantic policies. The primary path is identified as the // only one that isn't timelocked. - let (prim_path_sub, reco_path_sub) = - subs.into_iter() - .fold((None, None), |(mut prim_sub, mut reco_sub), sub| { - if is_single_key_or_multisig(&sub) { - prim_sub = Some(sub); + let (mut primary_path, mut recovery_path) = (None::, None); + for sub in subs { + // This is a (multi)key check. It must be the primary path. + if is_single_key_or_multisig(&sub) { + // We only support a single primary path. But it may be that the primary path is a + // 1-of-N multisig. In this case the policy is normalized from `thresh(1, thresh(1, + // pk(A), pk(B)), thresh(2, older(42), pk(C)))` to `thresh(1, pk(A), pk(B), + // thresh(2, older(42), pk(C)))`. + if let Some(prim_path) = primary_path { + if let SemanticPolicy::Key(key) = sub { + primary_path = Some(prim_path.with_added_key(key)); } else { - reco_sub = Some(sub); + return Err(LianaPolicyError::IncompatibleDesc); } - (prim_sub, reco_sub) - }); - let (prim_path_sub, reco_path_sub) = ( - prim_path_sub.ok_or(LianaPolicyError::IncompatibleDesc)?, - reco_path_sub.ok_or(LianaPolicyError::IncompatibleDesc)?, - ); - - // Now parse information about each spending path. - let primary_path = PathInfo::from_primary_path(prim_path_sub)?; - let recovery_path = PathInfo::from_recovery_path(reco_path_sub)?; + } else { + primary_path = Some(PathInfo::from_primary_path(sub)?); + } + } else { + // If it's not a simple (multi)key check, it must be the timelocked recovery path. + // For now, we only support a single recovery path. + if recovery_path.is_some() { + return Err(LianaPolicyError::IncompatibleDesc); + } + recovery_path = Some(PathInfo::from_recovery_path(sub)?); + } + } Ok(LianaPolicy { - primary_path, - recovery_path, + primary_path: primary_path.ok_or(LianaPolicyError::IncompatibleDesc)?, + recovery_path: recovery_path.ok_or(LianaPolicyError::IncompatibleDesc)?, }) } diff --git a/src/descriptors/mod.rs b/src/descriptors/mod.rs index 05b55bab..b8d169ea 100644 --- a/src/descriptors/mod.rs +++ b/src/descriptors/mod.rs @@ -509,6 +509,9 @@ mod tests { let heir_key = PathInfo::Single(descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap()); let timelock = 52560; LianaPolicy::new(owner_key, heir_key, timelock).unwrap_err(); + + // A 1-of-N multisig as primary path. + LianaDescriptor::from_str("wsh(or_d(multi(1,[573fb35b/48'/1'/0'/2']tpubDFKp9T7WAYDcENSjoifkrpq1gMDF47KGJcJrpxzX23Qor8wuGbrEVs9utNq1MDS8E2WXJSBk1qoPQLpwyokW7DiUNPwFuxQkL7owNkLAb9W/<0;1>/*,[573fb35b/48'/1'/1'/2']tpubDFGezyzuHJPhdP3jHGW7v7Hwes4Hihqv5W2yyCmRY9VZJCRchETvxrMC8uECeJZdxQ14V4iD4DecoArkUSDwj8ogYE9WEv4MNZr12thNHCs/<0;1>/*),and_v(v:multi(2,[573fb35b/48'/1'/2'/2']tpubDDwxQauiaU964vPzt5Vd7jnDHEUtp2Vc34PaWpEXg5TQ3bRccxnc1MKKh88Hi7xiMeZo9Tm6fBcq4UGXqnDtGUniJLjqAD8SjQ8Eci3aSR7/<0;1>/*,[573fb35b/48'/1'/3'/2']tpubDE37XAVB5CQ1x85md3BQ5uHCoMwT5fgT8X13zzCUQ3x5o2jskYxKjj7Qcxt1Jpj4QB8tqspn2dooPCekRuQDYrDHov7J1ueUNu2wcvgRDxr/<0;1>/*),older(1000))))#qjx6ycpc").unwrap(); } #[test]