1338 lines
68 KiB
Rust
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.
|
|
}
|