From 869d370daa354c76292517448646d0b7f76facba Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 18 Aug 2022 10:37:28 +0200 Subject: [PATCH] descriptors: introduce an InheritanceDescriptor type We check it when creating it either from material or from string. --- src/descriptors.rs | 195 +++++++++++++++++++++++++++++++++------------ src/testutils.rs | 2 +- 2 files changed, 147 insertions(+), 50 deletions(-) diff --git a/src/descriptors.rs b/src/descriptors.rs index d0dc1973..c224dfb0 100644 --- a/src/descriptors.rs +++ b/src/descriptors.rs @@ -2,23 +2,27 @@ use miniscript::{ descriptor, miniscript::{ decode::Terminal, + iter::PkPkh, limits::{SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_TYPE_FLAG}, Miniscript, }, + policy::{Liftable, Semantic as SemanticPolicy}, ScriptContext, }; -use std::{error, fmt, sync}; +use std::{error, fmt, str, sync}; // Flag applied to the nSequence and CSV value before comparing them. // // pub const SEQUENCE_LOCKTIME_MASK: u32 = 0x00_00_ff_ff; -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum DescCreationError { InsaneTimelock(u32), InvalidKey(descriptor::DescriptorPublicKey), + Miniscript(miniscript::Error), + IncompatibleDesc, } impl std::fmt::Display for DescCreationError { @@ -28,6 +32,8 @@ impl std::fmt::Display for DescCreationError { Self::InvalidKey(key) => { write!(f, "Invalid key '{}'. Need a wildcard ('ranged') xpub", key) } + Self::Miniscript(e) => write!(f, "Miniscript error: '{}'.", e), + Self::IncompatibleDesc => write!(f, "Descriptor is not compatible."), } } } @@ -38,10 +44,15 @@ impl error::Error for DescCreationError {} // - not be disabled // - be in number of blocks // - be 'clean' / minimal, ie all bits without consensus meaning should be 0 -fn csv_check(csv: u32) -> bool { - (csv & SEQUENCE_LOCKTIME_DISABLE_FLAG) == 0 +fn csv_check(csv: u32) -> Result<(), DescCreationError> { + if (csv & SEQUENCE_LOCKTIME_DISABLE_FLAG) == 0 && (csv & SEQUENCE_LOCKTIME_TYPE_FLAG) == 0 && (csv & SEQUENCE_LOCKTIME_MASK) == csv + { + Ok(()) + } else { + Err(DescCreationError::InsaneTimelock(csv)) + } } fn is_unhardened_deriv(key: &descriptor::DescriptorPublicKey) -> bool { @@ -53,52 +64,138 @@ fn is_unhardened_deriv(key: &descriptor::DescriptorPublicKey) -> bool { } } -/// Create a Miniscript descriptor with a main, unencombered, branch (the main owner of the coins) +/// A Miniscript descriptor with a main, unencombered, branch (the main owner of the coins) /// and a timelocked branch (the heir). -pub fn inheritance_descriptor( - owner_key: descriptor::DescriptorPublicKey, - heir_key: descriptor::DescriptorPublicKey, - timelock: u32, -) -> Result, DescCreationError> { - if !csv_check(timelock) { - return Err(DescCreationError::InsaneTimelock(timelock)); +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InheritanceDescriptor(descriptor::Descriptor); + +impl fmt::Display for InheritanceDescriptor { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) } +} - if let Some(key) = vec![&owner_key, &heir_key] - .iter() - .find(|k| !is_unhardened_deriv(k)) - { - return Err(DescCreationError::InvalidKey((**key).clone())); +impl str::FromStr for InheritanceDescriptor { + type Err = DescCreationError; + + fn from_str(s: &str) -> Result { + let wsh_desc = descriptor::Wsh::::from_str(s) + .map_err(DescCreationError::Miniscript)?; + let ms = match wsh_desc.as_inner() { + descriptor::WshInner::Ms(ms) => ms, + _ => return Err(DescCreationError::IncompatibleDesc), + }; + let invalid_key = ms.iter_pk_pkh().find_map(|pk_pkh| { + let pk = match pk_pkh { + PkPkh::PlainPubkey(pk) => pk, + PkPkh::HashedPubkey(pk) => pk, + }; + if is_unhardened_deriv(&pk) { + None + } else { + Some(pk) + } + }); + if let Some(key) = invalid_key { + return Err(DescCreationError::InvalidKey(key)); + } + + // 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(DescCreationError::IncompatibleDesc)?; + if subs.len() != 2 { + return Err(DescCreationError::IncompatibleDesc); + } + + // Owner branch + subs.iter() + .find(|s| matches!(s, SemanticPolicy::KeyHash(_))) + .ok_or(DescCreationError::IncompatibleDesc)?; + + // Heir branch + let heir_subs = subs + .iter() + .find_map(|s| match s { + SemanticPolicy::Threshold(2, subs) => Some(subs), + _ => None, + }) + .ok_or(DescCreationError::IncompatibleDesc)?; + if heir_subs.len() != 2 { + return Err(DescCreationError::IncompatibleDesc); + } + // Must be timelocked + let csv = heir_subs + .iter() + .find_map(|s| match s { + SemanticPolicy::Older(csv) => Some(csv), + _ => None, + }) + .ok_or(DescCreationError::IncompatibleDesc)?; + csv_check(*csv)?; + // And key locked + heir_subs + .iter() + .find(|s| matches!(s, SemanticPolicy::KeyHash(_))) + .ok_or(DescCreationError::IncompatibleDesc)?; + + Ok(InheritanceDescriptor(descriptor::Descriptor::Wsh(wsh_desc))) } +} - let owner_pk = Miniscript::from_ast(Terminal::Check(sync::Arc::from( - Miniscript::from_ast(Terminal::PkK(owner_key)).expect("TODO"), - ))) - .expect("Well typed"); +impl InheritanceDescriptor { + pub fn new( + owner_key: descriptor::DescriptorPublicKey, + heir_key: descriptor::DescriptorPublicKey, + timelock: u32, + ) -> Result, DescCreationError> { + csv_check(timelock)?; - let heir_pkh = Miniscript::from_ast(Terminal::Check(sync::Arc::from( - Miniscript::from_ast(Terminal::PkH(heir_key)).expect("TODO"), - ))) - .expect("Well typed"); + if let Some(key) = vec![&owner_key, &heir_key] + .iter() + .find(|k| !is_unhardened_deriv(k)) + { + return Err(DescCreationError::InvalidKey((**key).clone())); + } - let heir_timelock = Terminal::Older(timelock); - let heir_branch = Miniscript::from_ast(Terminal::AndV( - Miniscript::from_ast(Terminal::Verify(heir_pkh.into())) - .expect("Well typed") - .into(), - Miniscript::from_ast(heir_timelock) - .expect("Well typed") - .into(), - )) - .expect("Well typed"); - - let tl_miniscript = Miniscript::from_ast(Terminal::OrD(owner_pk.into(), heir_branch.into())) + let owner_pk = Miniscript::from_ast(Terminal::Check(sync::Arc::from( + Miniscript::from_ast(Terminal::PkK(owner_key)).expect("TODO"), + ))) .expect("Well typed"); - miniscript::Segwitv0::check_local_validity(&tl_miniscript).expect("Miniscript must be sane"); - Ok(descriptor::Descriptor::Wsh( - descriptor::Wsh::new(tl_miniscript).expect("Must pass sanity checks"), - )) + let heir_pkh = Miniscript::from_ast(Terminal::Check(sync::Arc::from( + Miniscript::from_ast(Terminal::PkH(heir_key)).expect("TODO"), + ))) + .expect("Well typed"); + + let heir_timelock = Terminal::Older(timelock); + let heir_branch = Miniscript::from_ast(Terminal::AndV( + Miniscript::from_ast(Terminal::Verify(heir_pkh.into())) + .expect("Well typed") + .into(), + Miniscript::from_ast(heir_timelock) + .expect("Well typed") + .into(), + )) + .expect("Well typed"); + + let tl_miniscript = + Miniscript::from_ast(Terminal::OrD(owner_pk.into(), heir_branch.into())) + .expect("Well typed"); + miniscript::Segwitv0::check_local_validity(&tl_miniscript) + .expect("Miniscript must be sane"); + + Ok(descriptor::Descriptor::Wsh( + descriptor::Wsh::new(tl_miniscript).expect("Must pass sanity checks"), + )) + } } #[cfg(test)] @@ -112,27 +209,27 @@ mod tests { let owner_key = descriptor::DescriptorPublicKey::from_str(&"xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/*").unwrap(); let heir_key = descriptor::DescriptorPublicKey::from_str(&"xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/*").unwrap(); let timelock = 52560; - assert_eq!(inheritance_descriptor(owner_key.clone(), heir_key.clone(), timelock).unwrap().to_string(), "wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/*),older(52560))))#eeyujkt7"); + assert_eq!(InheritanceDescriptor::new(owner_key.clone(), heir_key.clone(), timelock).unwrap().to_string(), "wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/*),older(52560))))#eeyujkt7"); // We prevent footguns with timelocks - inheritance_descriptor(owner_key.clone(), heir_key.clone(), 0x00_01_0f_00).unwrap_err(); - inheritance_descriptor(owner_key.clone(), heir_key.clone(), (1 << 31) + 1).unwrap_err(); - inheritance_descriptor(owner_key.clone(), heir_key.clone(), (1 << 22) + 1).unwrap_err(); + InheritanceDescriptor::new(owner_key.clone(), heir_key.clone(), 0x00_01_0f_00).unwrap_err(); + InheritanceDescriptor::new(owner_key.clone(), heir_key.clone(), (1 << 31) + 1).unwrap_err(); + InheritanceDescriptor::new(owner_key.clone(), heir_key.clone(), (1 << 22) + 1).unwrap_err(); let owner_key = descriptor::DescriptorPublicKey::from_str(&"[aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/*").unwrap(); let heir_key = descriptor::DescriptorPublicKey::from_str(&"xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/*").unwrap(); let timelock = 57600; - assert_eq!(inheritance_descriptor(owner_key.clone(), heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/*),and_v(v:pkh(xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/*),older(57600))))#8kamh6y8"); + assert_eq!(InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/*),and_v(v:pkh(xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/*),older(57600))))#8kamh6y8"); // We can't pass a raw key, an xpub that is not deriveable, or only hardened derivable let heir_key = descriptor::DescriptorPublicKey::from_str(&"xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/354").unwrap(); - inheritance_descriptor(owner_key.clone(), heir_key, timelock).unwrap_err(); + InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err(); let heir_key = descriptor::DescriptorPublicKey::from_str(&"xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap(); - inheritance_descriptor(owner_key.clone(), heir_key, timelock).unwrap_err(); + InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err(); let heir_key = descriptor::DescriptorPublicKey::from_str( &"02e24913be26dbcfdf8e8e94870b28725cdae09b448b6c127767bf0154e3a3c8e5", ) .unwrap(); - inheritance_descriptor(owner_key.clone(), heir_key, timelock).unwrap_err(); + InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err(); } } diff --git a/src/testutils.rs b/src/testutils.rs index 9f4781ba..215a8524 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -177,7 +177,7 @@ impl DummyMinisafe { let owner_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/*").unwrap(); let heir_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/*").unwrap(); - let desc = crate::descriptors::inheritance_descriptor(owner_key, heir_key, 10_000).unwrap(); + let desc = crate::descriptors::InheritanceDescriptor::new(owner_key, heir_key, 10_000).unwrap(); let config = Config { bitcoin_config, bitcoind_config: None,