liana/src/descriptors.rs

1338 lines
68 KiB
Rust

use miniscript::{
bitcoin::{
self,
blockdata::transaction::Sequence,
hashes::{hash160, ripemd160, sha256},
secp256k1,
util::{
bip32,
psbt::{Input as PsbtIn, Psbt},
},
},
descriptor, hash256,
miniscript::{decode::Terminal, Miniscript},
policy::{Liftable, Semantic as SemanticPolicy},
translate_hash_clone, ForEachKey, MiniscriptKey, ScriptContext, ToPublicKey, TranslatePk,
Translator,
};
use std::{
collections::{BTreeMap, HashMap, HashSet},
convert::TryFrom,
error, fmt, str, sync,
};
use serde::{Deserialize, Serialize};
const WITNESS_FACTOR: usize = 4;
// Convert a size in weight units to a size in virtual bytes, rounding up.
fn wu_to_vb(vb: usize) -> usize {
(vb + WITNESS_FACTOR - 1)
.checked_div(WITNESS_FACTOR)
.expect("Non 0")
}
#[derive(Debug)]
pub enum LianaDescError {
InsaneTimelock(u32),
InvalidKey(Box<descriptor::DescriptorPublicKey>),
DuplicateKey(Box<descriptor::DescriptorPublicKey>),
Miniscript(miniscript::Error),
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, ..
InconsistentPsbt,
}
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 a multipath for (and only for) deriving change addresses. That is, an xpub of the form 'xpub.../<0;1>/*'.",
key
)
}
Self::DuplicateKey(key) => {
write!(f, "Duplicate key '{}'.", key)
}
Self::Miniscript(e) => write!(f, "Miniscript error: '{}'.", e),
Self::IncompatibleDesc => write!(f, "Descriptor is not compatible."),
Self::DerivedKeyParsing => write!(f, "Parsing derived key,"),
Self::InvalidMultiThresh(thresh) => write!(f, "Invalid threshold value '{}'. The threshold must be > to 0 and <= to the number of keys.", thresh),
Self::InvalidMultiKeys(n_keys) => write!(f, "Invalid number of keys '{}'. Between 2 and 20 keys must be given to use multiple keys in a specific path.", n_keys),
Self::InsanePsbt => write!(f, "Analyzed PSBT is empty or malformed."),
Self::InconsistentPsbt => write!(f, "Analyzed PSBT is inconsistent across inputs.")
}
}
}
impl error::Error for LianaDescError {}
/// A public key used in derived descriptors
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
pub struct DerivedPublicKey {
/// Fingerprint of the master xpub and the derivation index used. We don't use a path
/// since we never derive at more than one depth.
pub origin: (bip32::Fingerprint, bip32::DerivationPath),
/// The actual key
pub key: bitcoin::PublicKey,
}
impl fmt::Display for DerivedPublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (fingerprint, deriv_path) = &self.origin;
write!(f, "[")?;
for byte in fingerprint.as_bytes().iter() {
write!(f, "{:02x}", byte)?;
}
write!(f, "/{}", deriv_path)?;
write!(f, "]{}", self.key)
}
}
impl 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<(), LianaDescError> {
if csv_value > 0 && u16::try_from(csv_value).is_ok() {
return Ok(());
}
Err(LianaDescError::InsaneTimelock(csv_value))
}
// We require the descriptor key to:
// - Be deriveable (to contain a wildcard)
// - Be multipath (to contain a step in the derivation path with multiple indexes)
// - The multipath step to only contain two indexes, 0 and 1.
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();
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 fingerprint for the key in a multipath descriptors.
// Returns None if the given key isn't a multixpub.
fn key_fingerprint(key: &descriptor::DescriptorPublicKey) -> Option<bip32::Fingerprint> {
match key {
descriptor::DescriptorPublicKey::MultiXPub(ref xpub) => Some(
xpub.origin
.as_ref()
.map(|o| o.0)
.unwrap_or_else(|| xpub.xkey.fingerprint()),
),
_ => 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 MultipathDescriptor {
multi_desc: descriptor::Descriptor<descriptor::DescriptorPublicKey>,
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 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 DerivedInheritanceDescriptor(descriptor::Descriptor<DerivedPublicKey>);
impl fmt::Display for MultipathDescriptor {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.multi_desc)
}
}
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<MultipathDescriptor, Self::Err> {
let wsh_desc = descriptor::Wsh::<descriptor::DescriptorPublicKey>::from_str(s)
.map_err(LianaDescError::Miniscript)?;
let ms = match wsh_desc.as_inner() {
descriptor::WshInner::Ms(ms) => ms,
_ => return Err(LianaDescError::IncompatibleDesc),
};
let invalid_key = ms.iter_pk().find_map(|pk| {
if is_valid_desc_key(&pk) {
None
} else {
Some(pk)
}
});
if let Some(key) = invalid_key {
return Err(LianaDescError::InvalidKey(key.into()));
}
// Semantic of the Miniscript must be either the owner now, or the heir after
// a timelock.
let policy = ms
.lift()
.expect("Lifting can't fail on a Miniscript")
.normalized();
let subs = match policy {
SemanticPolicy::Threshold(1, subs) => Some(subs),
_ => None,
}
.ok_or(LianaDescError::IncompatibleDesc)?;
if subs.len() != 2 {
return Err(LianaDescError::IncompatibleDesc);
}
// Non-timelocked spending path may be either a single key check or a multisig.
subs.iter()
.find(is_single_key_or_multisig)
.ok_or(LianaDescError::IncompatibleDesc)?;
// The recovery spending path is always a 2-of-2 between a timelock and a set of
// keys.
let recov_subs = subs
.iter()
.find_map(|s| match s {
SemanticPolicy::Threshold(2, subs) => {
if subs.len() == 2
&& subs
.iter()
.any(|sub| matches!(sub, SemanticPolicy::Older(_)))
{
Some(subs)
} else {
None
}
}
_ => None,
})
.ok_or(LianaDescError::IncompatibleDesc)?;
if recov_subs.len() != 2 {
return Err(LianaDescError::IncompatibleDesc);
}
// Must be timelocked
let csv_value = recov_subs
.iter()
.find_map(|s| match s {
SemanticPolicy::Older(csv) => Some(csv),
_ => None,
})
.ok_or(LianaDescError::IncompatibleDesc)?;
csv_check(csv_value.to_consensus_u32())?;
// The timelocked spending path may have a single key check or a multisig.
recov_subs
.iter()
.find(is_single_key_or_multisig)
.ok_or(LianaDescError::IncompatibleDesc)?;
// 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 = 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 = InheritanceDescriptor(singlepath_descs.next().expect("First of 2"));
let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2"));
Ok(MultipathDescriptor {
multi_desc,
receive_desc,
change_desc,
})
}
}
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 InheritanceDescriptor {
fn eq(&self, other: &descriptor::Descriptor<descriptor::DescriptorPublicKey>) -> bool {
self.0.eq(other)
}
}
/// Information about a single spending path in the descriptor.
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
pub enum PathInfo {
Single(descriptor::DescriptorPublicKey),
Multi(usize, Vec<descriptor::DescriptorPublicKey>),
}
impl PathInfo {
/// Get the spending path info from a policy describing a single key or a multisig.
/// Will return None if the policy isn't a key or a multisig.
pub fn from_single_key_or_multisig(
policy: SemanticPolicy<descriptor::DescriptorPublicKey>,
) -> Option<PathInfo> {
match policy {
SemanticPolicy::Key(key) => Some(PathInfo::Single(key)),
SemanticPolicy::Threshold(k, subs) => {
let keys: Option<Vec<descriptor::DescriptorPublicKey>> = subs
.into_iter()
.map(|sub| match sub {
SemanticPolicy::Key(key) => Some(key),
_ => None,
})
.collect();
Some(PathInfo::Multi(k, keys?))
}
_ => None,
}
}
/// 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_fingerprints(&self) -> (usize, HashSet<bip32::Fingerprint>) {
match self {
PathInfo::Single(key) => {
let mut fingerprints = HashSet::with_capacity(1);
fingerprints.insert(key_fingerprint(key).expect("Must be a multixpub."));
(1, fingerprints)
}
PathInfo::Multi(k, keys) => (
*k,
keys.iter()
.map(|key| key_fingerprint(key).expect("Must be a multixpub."))
.collect(),
),
}
}
/// Get the spend information for this descriptor based from the list of all pubkeys that
/// signed the transaction.
pub fn spend_info(
&self,
all_pubkeys_signed: impl Iterator<Item = bip32::Fingerprint>,
) -> PathSpendInfo {
let mut signed_pubkeys = HashMap::new();
let mut sigs_count = 0;
let (threshold, fingerprints) = self.thresh_fingerprints();
// For all existing signatures, pick those that are from one of our pubkeys.
for fingerprint in all_pubkeys_signed {
if fingerprints.contains(&fingerprint) {
sigs_count += 1;
if let Some(count) = signed_pubkeys.get_mut(&fingerprint) {
*count += 1;
} else {
signed_pubkeys.insert(fingerprint, 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, 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.
// We use /0/* for receiving and /1/* for change.
// FIXME: don't rely on into_single_descs()'s ordering.
let mut singlepath_descs = multi_desc
.clone()
.into_single_descriptors()
.expect("Can't error, all paths have the same length")
.into_iter();
assert_eq!(singlepath_descs.len(), 2);
let receive_desc = InheritanceDescriptor(singlepath_descs.next().expect("First of 2"));
let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2"));
Ok(MultipathDescriptor {
multi_desc,
receive_desc,
change_desc,
})
}
/// Whether all xpubs contained in this descriptor are for the passed expected network.
pub fn all_xpubs_net_is(&self, expected_net: bitcoin::Network) -> bool {
self.multi_desc.for_each_key(|xpub| {
if let descriptor::DescriptorPublicKey::MultiXPub(xpub) = xpub {
xpub.xkey.network == expected_net
} else {
false
}
})
}
/// Get the descriptor for receiving addresses.
pub fn receive_descriptor(&self) -> &InheritanceDescriptor {
&self.receive_desc
}
/// Get the descriptor for change addresses.
pub fn change_descriptor(&self) -> &InheritanceDescriptor {
&self.change_desc
}
/// Parse information about this descriptor
pub fn info(&self) -> LianaDescInfo {
// Get the Miniscript
let wsh_desc = match &self.multi_desc {
descriptor::Descriptor::Wsh(desc) => desc,
_ => unreachable!(),
};
let ms = match wsh_desc.as_inner() {
descriptor::WshInner::Ms(ms) => ms,
_ => unreachable!(),
};
// Lift the semantic policy from the Miniscript
let policy = ms
.lift()
.expect("Lifting can't fail on a Miniscript")
.normalized();
let subs = match policy {
SemanticPolicy::Threshold(1, subs) => subs,
_ => unreachable!("The policy is always 'one of the primary or the recovery path'"),
};
// For now we only ever allow a single recovery path.
assert_eq!(subs.len(), 2);
// Fetch the primary, non-timelocked path from the two sub-policies. Then parse information
// about it.
let prim_path_pos = subs
.iter()
.position(|sub| is_single_key_or_multisig(&sub))
.expect(
"One of the two available paths must always be a set of keys without timelock.",
);
let primary_path = PathInfo::from_single_key_or_multisig(subs[prim_path_pos].clone())
.expect("Must always be a set of keys without timelock");
// Since there is only two subs, the timelocked recovery path must be the other sub. From
// the recovery sub policy fetch the timelock policy on the one hand, and the set of keys
// on the other one.
let reco_path_pos = prim_path_pos ^ 1;
let reco_subs = match subs[reco_path_pos] {
SemanticPolicy::Threshold(2, ref subs) => subs,
_ => unreachable!(
"The recovery path policy must be two subs: a timelock + a set of keys."
),
};
let (csv_pos, csv) = reco_subs
.iter()
.enumerate()
.find_map(|(i, s)| match s {
SemanticPolicy::Older(csv) => Some((
i,
u16::try_from(csv.0).expect("Must always be a 'clean' block height"),
)),
_ => None,
})
.expect("A relative timelock policy is always present in the recovery path.");
let recovery_keys = PathInfo::from_single_key_or_multisig(reco_subs[csv_pos ^ 1].clone())
.expect("Must always be a set of keys alongside the timelock");
let recovery_path = (csv, recovery_keys);
LianaDescInfo::new(primary_path, recovery_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.info().recovery_path.0 as u32
}
/// Get the maximum size in WU of a satisfaction for this descriptor.
pub fn max_sat_weight(&self) -> usize {
self.multi_desc
.max_satisfaction_weight()
.expect("Cannot fail for P2WSH")
}
/// Get the maximum size in vbytes (rounded up) of a satisfaction for this descriptor.
pub fn max_sat_vbytes(&self) -> usize {
self.multi_desc
.max_satisfaction_weight()
.expect("Cannot fail for P2WSH")
.checked_add(WITNESS_FACTOR - 1)
.unwrap()
.checked_div(WITNESS_FACTOR)
.unwrap()
}
/// Get the maximum size in virtual bytes of the whole input in a transaction spending
/// a coin with this Script.
pub fn spender_input_size(&self) -> usize {
// txid + vout + nSequence + empty scriptSig + witness
32 + 4 + 4 + 1 + wu_to_vb(self.max_sat_weight())
}
/// Get some information about a PSBT input spending Liana coins.
/// This analysis assumes that:
/// - The PSBT input actually spend a Liana coin for this descriptor. Otherwise the analysis will be off.
/// - The signatures contained in the PSBT input are valid for this script.
pub fn partial_spend_info_txin(
&self,
psbt_in: &PsbtIn,
txin: &bitcoin::TxIn,
) -> PartialSpendInfo {
// Get the identifier of all the keys that signed this transaction.
let pubkeys_signed = psbt_in
.partial_sigs
.iter()
.filter_map(|(pk, _)| psbt_in.bip32_derivation.get(&pk.inner).map(|(fg, _)| *fg));
// Determine the structure of the descriptor. Then compute the spend info for the primary
// and recovery paths. Only provide the spend info for the recovery path if it is available
// (ie if the nSequence is >= to the chosen CSV value).
let desc_info = self.info();
let 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
{
Some(desc_info.recovery_path.1.spend_info(pubkeys_signed))
} else {
None
};
PartialSpendInfo {
primary_path,
recovery_path,
}
}
// TODO: decide whether we should check the signatures too. To be useful it should check pubkeys
// correspond to those in the script. And we could be checking the witness scripts are all for
// our descriptor too..
/// Get some information about a PSBT spending Liana coins.
/// This analysis assumes that:
/// - The PSBT only contains input that spends Liana coins. Otherwise the analysis will be off.
/// - The PSBT is consistent across inputs (the sequence is the same across inputs, the
/// signatures are either absent or present for all inputs, ..)
/// - The provided signatures are valid for this script.
pub fn partial_spend_info(&self, psbt: &Psbt) -> Result<PartialSpendInfo, LianaDescError> {
// Check the PSBT isn't empty or malformed.
if psbt.inputs.len() != psbt.unsigned_tx.input.len()
|| psbt.outputs.len() != psbt.unsigned_tx.output.len()
|| psbt.inputs.is_empty()
|| psbt.outputs.is_empty()
{
return Err(LianaDescError::InsanePsbt);
}
// We are doing this analysis at a transaction level. We assume that if an input
// is set to use the recovery path, all are. If one input is signed with a key, all
// must be.
// This gets the information needed to analyze the number of signatures from the
// first input, and checks that this info matches on all inputs.
let (mut psbt_ins, mut txins) = (psbt.inputs.iter(), psbt.unsigned_tx.input.iter());
let (first_psbt_in, first_txin) = (
psbt_ins
.next()
.expect("We checked at least one is present."),
txins.next().expect("We checked at least one is present."),
);
let spend_info = self.partial_spend_info_txin(first_psbt_in, first_txin);
for (psbt_in, txin) in psbt_ins.zip(txins) {
// TODO: maybe it's better to not error if one of the input has more, or different
// signatures? Instead of erroring we could ignore the superfluous data?
if txin.sequence != first_txin.sequence
|| spend_info != self.partial_spend_info_txin(psbt_in, txin)
{
return Err(LianaDescError::InconsistentPsbt);
}
}
Ok(spend_info)
}
}
impl InheritanceDescriptor {
/// Derive this descriptor at a given index for a receiving address.
///
/// # Panics
/// - If the given index is hardened.
pub fn derive(
&self,
index: bip32::ChildNumber,
secp: &secp256k1::Secp256k1<impl secp256k1::Verification>,
) -> DerivedInheritanceDescriptor {
assert!(index.is_normal());
// Unfortunately we can't just use `self.0.at_derivation_index().derived_descriptor()`
// since it would return a raw public key, but we need the origin too.
// TODO: upstream our DerivedPublicKey stuff to rust-miniscript.
//
// So we roll our own translation.
struct Derivator<'a, C: secp256k1::Verification>(u32, &'a secp256k1::Secp256k1<C>);
impl<'a, C: secp256k1::Verification>
Translator<
descriptor::DescriptorPublicKey,
DerivedPublicKey,
descriptor::ConversionError,
> for Derivator<'a, C>
{
fn pk(
&mut self,
pk: &descriptor::DescriptorPublicKey,
) -> Result<DerivedPublicKey, descriptor::ConversionError> {
let definite_key = pk
.clone()
.at_derivation_index(self.0)
.expect("We disallow multipath keys.");
let origin = (
definite_key.master_fingerprint(),
definite_key
.full_derivation_path()
.expect("We disallow multipath keys."),
);
let key = definite_key.derive_public_key(self.1)?;
Ok(DerivedPublicKey { origin, key })
}
translate_hash_clone!(
descriptor::DescriptorPublicKey,
DerivedPublicKey,
descriptor::ConversionError
);
}
DerivedInheritanceDescriptor(
self.0
.translate_pk(&mut Derivator(index.into(), secp))
.expect(
"May only fail on hardened derivation indexes, but we ruled out this case.",
),
)
}
}
/// 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 DerivedInheritanceDescriptor {
pub fn address(&self, network: bitcoin::Network) -> bitcoin::Address {
self.0
.address(network)
.expect("A P2WSH always has an address")
}
pub fn script_pubkey(&self) -> bitcoin::Script {
self.0.script_pubkey()
}
pub fn witness_script(&self) -> bitcoin::Script {
self.0.explicit_script().expect("Not a Taproot descriptor")
}
pub fn bip32_derivations(&self) -> Bip32Deriv {
let ms = match self.0 {
descriptor::Descriptor::Wsh(ref wsh) => match wsh.as_inner() {
descriptor::WshInner::Ms(ms) => ms,
descriptor::WshInner::SortedMulti(_) => {
unreachable!("None of our descriptors is a sorted multi")
}
},
_ => unreachable!("All our descriptors are always P2WSH"),
};
// For DerivedPublicKey, Pk::Hash == Self.
ms.iter_pk()
.map(|k| (k.key.inner, (k.origin.0, k.origin.1)))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn descriptor_creation() {
let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap());
let timelock = 52560;
assert_eq!(MultipathDescriptor::new(owner_key.clone(), heir_key.clone(), timelock).unwrap().to_string(), "wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#8n2ydpkt");
// A decaying multisig after 6 months. Note we can't duplicate the keys, so different ones
// are used. In practice they would both be controlled by the same entity.
let primary_keys = LianaDescKeys::from_multi(
3,
vec![
descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
descriptor::DescriptorPublicKey::from_str("xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*").unwrap()
]
)
.unwrap();
let recovery_keys = LianaDescKeys::from_multi(
2,
vec![
descriptor::DescriptorPublicKey::from_str("xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
descriptor::DescriptorPublicKey::from_str("xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*").unwrap(),
],
)
.unwrap();
assert_eq!(MultipathDescriptor::new(primary_keys, recovery_keys, 26352).unwrap().to_string(), "wsh(or_d(multi(3,xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#slaa6mlr");
// We prevent footguns with timelocks by requiring a u16. Note how the following wouldn't
// compile:
//MultipathDescriptor::new(owner_key.clone(), heir_key.clone(), 0x00_01_0f_00).unwrap_err();
//MultipathDescriptor::new(owner_key.clone(), heir_key.clone(), (1 << 31) + 1).unwrap_err();
//MultipathDescriptor::new(owner_key, heir_key, (1 << 22) + 1).unwrap_err();
// You can't use a null timelock in Miniscript.
MultipathDescriptor::new(owner_key, heir_key, 0).unwrap_err();
let owner_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*").unwrap());
let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*").unwrap());
let timelock = 57600;
assert_eq!(MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*),and_v(v:pkh(xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*),older(57600))))#l6dlpc2l");
// We can't pass a raw key, an xpub that is not deriveable, only hardened derivable,
// without both the change and receive derivation paths, or with more than 2 different
// derivation paths.
let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("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("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(
"02e24913be26dbcfdf8e8e94870b28725cdae09b448b6c127767bf0154e3a3c8e5",
)
.unwrap(),
);
MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap());
MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("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 = LianaDescKeys::from_multi(
1,
vec![
descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap(),
]
)
.unwrap();
let recovery_keys = LianaDescKeys::from_multi(
1,
vec![
descriptor::DescriptorPublicKey::from_str("xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
descriptor::DescriptorPublicKey::from_str("xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
],
)
.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 = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap());
let heir_key = LianaDescKeys::from_single(descriptor::DescriptorPublicKey::from_str("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("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 = LianaDescKeys::from_multi(
3,
vec![
descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap(),
descriptor::DescriptorPublicKey::from_str("xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
descriptor::DescriptorPublicKey::from_str("xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*").unwrap()
]
)
.unwrap();
let recovery_keys = LianaDescKeys::from_multi(
2,
vec![
descriptor::DescriptorPublicKey::from_str("xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*").unwrap(),
descriptor::DescriptorPublicKey::from_str("xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*").unwrap(),
descriptor::DescriptorPublicKey::from_str("xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*").unwrap(),
],
)
.unwrap();
MultipathDescriptor::new(primary_keys, recovery_keys, 26352).unwrap_err();
}
#[test]
fn inheritance_descriptor_derivation() {
let secp = secp256k1::Secp256k1::verification_only();
let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9").unwrap();
let der_desc = desc.receive_descriptor().derive(11.into(), &secp);
assert_eq!(
"bc1q26gtczlz03u6juf5cxppapk4sr4fyz53s3g4zs2cgactcahqv6yqc2t8e6",
der_desc.address(bitcoin::Network::Bitcoin).to_string()
);
// Sanity check we can call the methods on the derived desc
der_desc.script_pubkey();
der_desc.witness_script();
assert!(!der_desc.bip32_derivations().is_empty());
}
#[test]
fn inheritance_descriptor_tl_value() {
let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 1);
let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(42000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 42000);
let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(65535),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 0xffff);
}
#[test]
fn inheritance_descriptor_sat_size() {
let desc = MultipathDescriptor::from_str("wsh(or_d(pk([92162c45]tpubD6NzVbkrYhZ4WzTf9SsD6h7AH7oQEippXK2KP8qvhMMqFoNeN5YFVi7vRyeRSDGtgd2bPyMxUNmHui8t5yCgszxPPxMafu1VVzDpg9aruYW/<0;1>/*),and_v(v:pkh(tpubD6NzVbkrYhZ4Wdgu2yfdmrce5g4fiH1ZLmKhewsnNKupbi4sxjH1ZVAorkBLWSkhsjhg8kiq8C4BrBjMy3SjAKDyDdbuvUa1ToAHbiR98js/<0;1>/*),older(2))))#uact7s3g").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).
// Where max_sat is:
// - Push the witness stack size
// - Push the script
// - Push an empty vector for using the recovery path
// - Push the recovery key
// - Push a signature for the recovery key
// NOTE: The specific value is asserted because this was tested against a regtest
// transaction.
let stack = vec![vec![0; 68], vec![0; 0], vec![0; 33], vec![0; 72]];
let witness_size = bitcoin::VarInt(stack.len() as u64).len()
+ stack
.iter()
.map(|item| bitcoin::VarInt(stack.len() as u64).len() + item.len())
.sum::<usize>();
assert_eq!(
desc.spender_input_size(),
32 + 4 + 1 + 4 + wu_to_vb(witness_size),
);
}
#[test]
fn liana_desc_keys() {
let desc_key_a = descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap();
let desc_key_b = descriptor::DescriptorPublicKey::from_str("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 = MultipathDescriptor::from_str(desc_str).unwrap();
assert_eq!(desc.to_string(), desc_str);
}
#[test]
fn roundtrip_descriptor() {
// A descriptor with single keys in both primary and recovery paths
roundtrip("wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#8n2ydpkt");
// One with a multisig in both paths
roundtrip("wsh(or_d(multi(3,xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:multi(2,xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#slaa6mlr");
// A single key as primary path, a multisig as recovery
roundtrip("wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:multi(2,xpub69cP4Y7S9TWcbSNxmk6CEDBsoaqr3ZEdjHuZcHxEFFKGh569RsJNr2V27XGhsbH9FXgWUEmKXRN7c5wQfq2VPjt31xP9VsYnVUyU8HcVevm/<0;1>/*,xpub6AA2N8RALRYgLD6jT1iXYCEDkndTeZndMtWPbtNX6sY5dPiLtf2T88ahdxrGXMUPoNadgR86sFhBXWQVgifPzDYbY9ZtwK4gqzx4y5Da1DW/<0;1>/*,[aabb0011/10/4893]xpub6AyxexvxizZJffF153evmfqHcE9MV88fCNCAtP3jQjXJHwrAKri71Tq9jWUkPxj9pja4u6AkCPHY7atgxzSEa2HtDwJfrRWKK4fsfQg4o77/<0;1>/*),older(26352))))#f5m0vfpf");
// The other way around
roundtrip("wsh(or_d(multi(3,xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*,[aabb0011/10/4893]xpub6Bw79HbNSeS2xXw1sngPE3ehnk1U3iSPCgLYzC9LpN8m9nDuaKLZvkg8QXxL5pDmEmQtYscmUD8B9MkAAZbh6vxPzNXMaLfGQ9Sb3z85qhR/<0;1>/*,xpub67zuTXF9Ln4731avKTBSawoVVNRuMfmRvkL7kLUaLBRqma9ZqdHBJg9qx8cPUm3oNQMiXT4TmGovXNoQPuwg17RFcVJ8YrnbcooN7pxVJqC/<0;1>/*),and_v(v:pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),older(26352))))#3f4xttt3");
}
fn psbt_from_str(psbt_str: &str) -> Psbt {
bitcoin::consensus::deserialize(&base64::decode(psbt_str).unwrap()).unwrap()
}
#[test]
fn repro() {
// A simple descriptor with 1 keys as primary path and 1 recovery key.
let desc = MultipathDescriptor::from_str("wsh(or_d(pk([f5acc2fd]tpubD6NzVbkrYhZ4YgUx2ZLNt2rLYAMTdYysCRzKoLu2BeSHKvzqPaBDvf17GeBPnExUVPkuBpx4kniP964e2MxyzzazcXLptxLXModSVCVEV1T/<0;1>/*),and_v(v:pkh([8a64f2a9]tpubD6NzVbkrYhZ4WmzFjvQrp7sDa4ECUxTi9oby8K4FZkd3XCBtEdKwUiQyYJaxiJo5y42gyDWEczrFpozEjeLxMPxjf2WtkfcbpUdfvNnozWF/<0;1>/*),older(10))))#d72le4dr").unwrap();
let desc_info = desc.info();
let prim_key_fg = bip32::Fingerprint::from_str("f5acc2fd").unwrap();
let recov_key_fg = bip32::Fingerprint::from_str("8a64f2a9").unwrap();
// A PSBT with a single input and output, no signature. nSequence is not set to use the
// recovery path.
let mut unsigned_single_psbt: Psbt = psbt_from_str("cHNidP8BAHECAAAAAUSHuliRtuCX1S6JxRuDRqDCKkWfKmWL5sV9ukZ/wzvfAAAAAAD9////AogTAAAAAAAAFgAUIxe7UY6LJ6y5mFBoWTOoVispDmdwFwAAAAAAABYAFKqO83TK+t/KdpAt21z2HGC7/Z2FAAAAAAABASsQJwAAAAAAACIAIIIySQjGCTeyx/rKUQx8qobjhJeNCiVCliBJPdyRX6XKAQVBIQI2cqWpc9UAW2gZt2WkKjvi8KoMCui00pRlL6wG32uKDKxzZHapFNYASzIYkEdH9bJz6nnqUG3uBB8kiK1asmgiBgI2cqWpc9UAW2gZt2WkKjvi8KoMCui00pRlL6wG32uKDAz1rML9AAAAAG8AAAAiBgMLcbOxsfLe6+3r1UcjQo77HY0As8OKE4l37yj0/qhIyQyKZPKpAAAAAG8AAAAAAAA=");
let info = desc.partial_spend_info(&unsigned_single_psbt).unwrap();
assert_eq!(info.primary_path.threshold, 1);
assert_eq!(info.primary_path.sigs_count, 0);
assert!(info.primary_path.signed_pubkeys.is_empty());
assert!(info.recovery_path.is_none());
// If we set the sequence too low we still won't have the recovery path info.
unsigned_single_psbt.unsigned_tx.input[0].sequence =
Sequence::from_height(desc_info.recovery_path.0 - 1);
let info = desc.partial_spend_info(&unsigned_single_psbt).unwrap();
assert!(info.recovery_path.is_none());
// Now if we set the sequence at the right value we'll have it.
unsigned_single_psbt.unsigned_tx.input[0].sequence =
Sequence::from_height(desc_info.recovery_path.0);
let info = desc.partial_spend_info(&unsigned_single_psbt).unwrap();
assert!(info.recovery_path.is_some());
// 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 =
Sequence::from_height(desc_info.recovery_path.0 + 42);
let info = desc.partial_spend_info(&unsigned_single_psbt).unwrap();
let recov_info = info.recovery_path.unwrap();
assert_eq!(recov_info.threshold, 1);
assert_eq!(recov_info.sigs_count, 0);
assert!(recov_info.signed_pubkeys.is_empty());
// The same PSBT but with an (invalid) signature for the primary key.
let mut signed_single_psbt = psbt_from_str("cHNidP8BAHECAAAAAUSHuliRtuCX1S6JxRuDRqDCKkWfKmWL5sV9ukZ/wzvfAAAAAAD9////AogTAAAAAAAAFgAUIxe7UY6LJ6y5mFBoWTOoVispDmdwFwAAAAAAABYAFKqO83TK+t/KdpAt21z2HGC7/Z2FAAAAAAABASsQJwAAAAAAACIAIIIySQjGCTeyx/rKUQx8qobjhJeNCiVCliBJPdyRX6XKIgICNnKlqXPVAFtoGbdlpCo74vCqDArotNKUZS+sBt9rigxIMEUCIQCYZusUL8bdi2PnjWao4bIDDgMQ9Dj2Lcup3/VmkGbYJAIgX/wF5HsqugC5JzvU2cGOmUWtHr2Pg0N4912qogYgDH4BAQVBIQI2cqWpc9UAW2gZt2WkKjvi8KoMCui00pRlL6wG32uKDKxzZHapFNYASzIYkEdH9bJz6nnqUG3uBB8kiK1asmgiBgI2cqWpc9UAW2gZt2WkKjvi8KoMCui00pRlL6wG32uKDAz1rML9AAAAAG8AAAAiBgMLcbOxsfLe6+3r1UcjQo77HY0As8OKE4l37yj0/qhIyQyKZPKpAAAAAG8AAAAAAAA=");
let info = desc.partial_spend_info(&signed_single_psbt).unwrap();
assert_eq!(signed_single_psbt.inputs[0].partial_sigs.len(), 1);
assert_eq!(info.primary_path.threshold, 1);
assert_eq!(info.primary_path.sigs_count, 1);
assert!(
info.primary_path.signed_pubkeys.len() == 1
&& info.primary_path.signed_pubkeys.contains_key(&prim_key_fg)
);
assert!(info.recovery_path.is_none());
// Now enable the recovery path and add a signature for the recovery key.
signed_single_psbt.unsigned_tx.input[0].sequence =
Sequence::from_height(desc_info.recovery_path.0);
let recov_pubkey = bitcoin::PublicKey {
compressed: true,
inner: *signed_single_psbt.inputs[0]
.bip32_derivation
.iter()
.find(|(_, (fg, _))| fg == &recov_key_fg)
.unwrap()
.0,
};
let prim_key = *signed_single_psbt.inputs[0]
.partial_sigs
.iter()
.next()
.unwrap()
.0;
let sig = signed_single_psbt.inputs[0]
.partial_sigs
.remove(&prim_key)
.unwrap();
signed_single_psbt.inputs[0]
.partial_sigs
.insert(recov_pubkey, sig);
let info = desc.partial_spend_info(&signed_single_psbt).unwrap();
assert_eq!(signed_single_psbt.inputs[0].partial_sigs.len(), 1);
assert_eq!(info.primary_path.threshold, 1);
assert_eq!(info.primary_path.sigs_count, 0);
assert!(info.primary_path.signed_pubkeys.is_empty());
let recov_info = info.recovery_path.unwrap();
assert_eq!(recov_info.threshold, 1);
assert_eq!(recov_info.sigs_count, 1);
assert!(
recov_info.signed_pubkeys.len() == 1
&& recov_info.signed_pubkeys.contains_key(&recov_key_fg)
);
// A PSBT with multiple inputs, all signed for the primary path.
let psbt: Psbt = psbt_from_str("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");
let info = desc.partial_spend_info(&psbt).unwrap();
assert!(psbt
.inputs
.iter()
.all(|psbt_in| psbt_in.partial_sigs.len() == 1));
assert_eq!(info.primary_path.threshold, 1);
assert_eq!(info.primary_path.sigs_count, 1);
assert!(
info.primary_path.signed_pubkeys.len() == 1
&& info.primary_path.signed_pubkeys.contains_key(&prim_key_fg)
);
assert!(info.recovery_path.is_none());
// Enable the recovery path, it should show no recovery sig.
let mut rec_psbt = psbt.clone();
for txin in rec_psbt.unsigned_tx.input.iter_mut() {
txin.sequence = Sequence::from_height(desc_info.recovery_path.0);
}
let info = desc.partial_spend_info(&rec_psbt).unwrap();
assert!(rec_psbt
.inputs
.iter()
.all(|psbt_in| psbt_in.partial_sigs.len() == 1));
assert_eq!(info.primary_path.threshold, 1);
assert_eq!(info.primary_path.sigs_count, 1);
assert!(
info.primary_path.signed_pubkeys.len() == 1
&& info.primary_path.signed_pubkeys.contains_key(&prim_key_fg)
);
let recov_info = info.recovery_path.unwrap();
assert_eq!(recov_info.threshold, 1);
assert_eq!(recov_info.sigs_count, 0);
assert!(recov_info.signed_pubkeys.is_empty());
// 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.
let mut inconsistent_psbt = psbt.clone();
inconsistent_psbt.unsigned_tx.input[0].sequence =
Sequence::from_height(desc_info.recovery_path.0 + 1);
assert!(desc
.partial_spend_info(&inconsistent_psbt)
.unwrap_err()
.to_string()
.contains("Analyzed PSBT is inconsistent across inputs."));
// Same if all inputs don't have the same number of signatures.
let mut inconsistent_psbt = psbt.clone();
inconsistent_psbt.inputs[0].partial_sigs.clear();
assert!(desc
.partial_spend_info(&inconsistent_psbt)
.unwrap_err()
.to_string()
.contains("Analyzed PSBT is inconsistent across inputs."));
// If we analyze a descriptor with a multisig we'll get the right threshold.
let desc = MultipathDescriptor::from_str("wsh(or_d(multi(2,[f5acc2fd]tpubD6NzVbkrYhZ4YgUx2ZLNt2rLYAMTdYysCRzKoLu2BeSHKvzqPaBDvf17GeBPnExUVPkuBpx4kniP964e2MxyzzazcXLptxLXModSVCVEV1T/<0;1>/*,[00112233]xpub6FC8vmQGGfSuQGfKG5L73fZ7WjXit8TzfJYDKwTtHkhrbAhU5Kma41oenVq6aMnpgULJRXpQuxnVysyfdpRhVgD6vYe7XLbFDhmvYmDrAVq/<0;1>/*,[aabbccdd]xpub68XtbpvDM19d39wEKdvadHkZ4FGKf4tnryKzAacttp8BLX3uHj7eK8shRnFBhZ2UL83S9dwXe42Qm6eG6BkR1jy8XwUSNBcHKtET7j4V5FB/<0;1>/*),and_v(v:pkh([8a64f2a9]tpubD6NzVbkrYhZ4WmzFjvQrp7sDa4ECUxTi9oby8K4FZkd3XCBtEdKwUiQyYJaxiJo5y42gyDWEczrFpozEjeLxMPxjf2WtkfcbpUdfvNnozWF/<0;1>/*),older(10))))#2kgxuax5").unwrap();
let info = desc.partial_spend_info(&psbt).unwrap();
assert!(psbt
.inputs
.iter()
.all(|psbt_in| psbt_in.partial_sigs.len() == 1));
assert_eq!(info.primary_path.threshold, 2);
assert_eq!(info.primary_path.sigs_count, 1);
assert!(
info.primary_path.signed_pubkeys.len() == 1
&& info.primary_path.signed_pubkeys.contains_key(&prim_key_fg)
);
assert!(info.recovery_path.is_none());
}
// TODO: test error conditions of deserialization.
}