descriptors: Taproot support

We introduce support for tr() descriptors alongside wsh() descriptors in
creating (compiling from policy, parsing from string) and working with
(analyizing its policy, getting spend information) a descriptor.

When compiling a Taproot descriptor, if no key from the policy could be
used as single internal key we deterministically generate an unspendable
internal key as per
https://delvingbitcoin.org/t/unspendable-keys-in-descriptors/304/21.
Similarly when lifting the policy of a Taproot descriptor, if the
internal key matches the deterministic unspendable key for this
descriptor we discard it from the analysis.

To fill information about an output for signers, we re-use
rust-miniscript PSBT input updated instead of re-inventing the wheel. It
does necessitate a hack however to use a type they would accept.

We don't change the "max size of a spending input" for now, even though
it means we would significantly overpay fees for descriptors with a
spendable internal key.
This commit is contained in:
Antoine Poinsot 2024-01-18 09:59:51 +01:00
parent c897d41965
commit 04f4b8a744
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
4 changed files with 888 additions and 142 deletions

View File

@ -1,7 +1,11 @@
use miniscript::{
bitcoin::{bip32, Sequence},
bitcoin::{
self, bip32,
hashes::{sha256, Hash},
secp256k1, Sequence,
},
descriptor,
policy::{compiler, Concrete as ConcretePolicy, Liftable, Semantic as SemanticPolicy},
policy::{Concrete as ConcretePolicy, Liftable, Semantic as SemanticPolicy},
ScriptContext,
};
@ -9,6 +13,7 @@ use std::{
collections::{BTreeMap, HashMap, HashSet},
convert::TryFrom,
error, fmt,
str::FromStr,
};
#[derive(Debug)]
@ -22,10 +27,10 @@ pub enum LianaPolicyError {
InvalidMultiThresh(usize),
InvalidMultiKeys(usize),
IncompatibleDesc,
PolicyAnalysis(miniscript::Error),
/// The spending policy is not a valid Miniscript policy: it may for instance be malleable, or
/// overflow some limit.
InvalidPolicy(compiler::CompilerError),
PolicyAnalysis(miniscript::Error),
InvalidPolicy(miniscript::Error),
}
impl std::fmt::Display for LianaPolicyError {
@ -357,6 +362,91 @@ impl PathInfo {
}
}
// See
// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs:
// > One example of such a point is H =
// > lift_x(0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0) which is constructed
// > by taking the hash of the standard uncompressed encoding of the secp256k1 base point G as X
// > coordinate.
fn bip341_nums() -> secp256k1::PublicKey {
secp256k1::PublicKey::from_str(
"0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0",
)
.expect("Valid pubkey: NUMS from BIP341")
}
// Given a descpubkey, extract its xpub assuming it is a multixpub. Returns None otherwise.
fn get_multi_xkey(desc_key: &descriptor::DescriptorPublicKey) -> Option<&bip32::Xpub> {
if let descriptor::DescriptorPublicKey::MultiXPub(descriptor::DescriptorMultiXKey {
xkey,
..
}) = desc_key
{
Some(xkey)
} else {
None
}
}
// Construct an unspendable xpub to be used as internal key in a Taproot descriptor, in a way which
// could eventually be standardized into wallet policies for a signer to display to the user
// "UNSPENDABLE" upon registration (instead of a meaningless key).
// See https://delvingbitcoin.org/t/unspendable-keys-in-descriptors/304/21.
//
// Returns `None` if:
// - The given descriptor does not contain a Taptree with at least a key in each leaf.
// - The keys contained in the descriptor aren't all MultiXPub's.
fn unspendable_internal_xpub(
desc: &descriptor::Tr<descriptor::DescriptorPublicKey>,
) -> Option<bip32::Xpub> {
let tap_tree = desc.tap_tree().as_ref()?;
// Fetch the network to use for the unspendable key from the first key in the descriptor.
let first_key = tap_tree.iter().flat_map(|(_, ms)| ms.iter_pk()).next()?;
let network = get_multi_xkey(&first_key)?.network;
// Compute the chaincode to use for the xpub. This is the sha256() of the concatenation of all
// the xpubs' pubkey part in the Taptree.
let concat =
tap_tree
.iter()
.flat_map(|(_, ms)| ms.iter_pk())
.try_fold(Vec::new(), |mut acc, pk| {
let xkey = get_multi_xkey(&pk)?;
acc.extend_from_slice(&xkey.public_key.serialize());
Some(acc)
})?;
let chain_code = bip32::ChainCode::from(sha256::Hash::hash(&concat).as_ref());
// Construct the unspendable key. The pubkey part is always BIP341's NUMS.
let public_key = bip341_nums();
Some(bip32::Xpub {
public_key,
chain_code,
depth: 0,
parent_fingerprint: [0; 4].into(),
child_number: 0.into(),
network,
})
}
fn unspendable_internal_key(
desc: &descriptor::Tr<descriptor::DescriptorPublicKey>,
) -> Option<descriptor::DescriptorPublicKey> {
Some(descriptor::DescriptorPublicKey::MultiXPub(
descriptor::DescriptorMultiXKey {
origin: None,
xkey: unspendable_internal_xpub(desc)?,
derivation_paths: descriptor::DerivPaths::new(vec![
[0.into()][..].into(),
[1.into()][..].into(),
])
.expect("Non empty vec"),
wildcard: descriptor::Wildcard::Unhardened,
},
))
}
/// A Liana spending policy is one composed of at least two spending paths:
/// - A directly available path with any number of keys checks; or
/// - One or more recovery paths with any number of keys checks, behind increasing relative
@ -370,13 +460,15 @@ impl PathInfo {
pub struct LianaPolicy {
pub(super) primary_path: PathInfo,
pub(super) recovery_paths: BTreeMap<u16, PathInfo>,
is_taproot: bool,
}
impl LianaPolicy {
/// Create a new Liana policy from a given configuration.
pub fn new(
fn _new(
primary_path: PathInfo,
recovery_paths: BTreeMap<u16, PathInfo>,
is_taproot: bool,
) -> Result<LianaPolicy, LianaPolicyError> {
if recovery_paths.is_empty() {
return Err(LianaPolicyError::MissingRecoveryPath);
@ -430,32 +522,73 @@ impl LianaPolicy {
let policy = LianaPolicy {
primary_path,
recovery_paths,
is_taproot,
};
policy.clone().into_miniscript()?;
policy.clone().into_multipath_descriptor_fallible()?;
Ok(policy)
}
/// Create a new Liana policy for use under a Taproot context.
pub fn new(
primary_path: PathInfo,
recovery_paths: BTreeMap<u16, PathInfo>,
) -> Result<LianaPolicy, LianaPolicyError> {
Self::_new(primary_path, recovery_paths, /* is_taproot = */ true)
}
/// Create a new Liana policy for use under a P2WSH context.
pub fn new_legacy(
primary_path: PathInfo,
recovery_paths: BTreeMap<u16, PathInfo>,
) -> Result<LianaPolicy, LianaPolicyError> {
Self::_new(primary_path, recovery_paths, /* is_taproot = */ false)
}
/// Create a Liana policy from a descriptor. This will check the descriptor is correctly formed
/// (P2WSH, multipath, ..) and has a valid Liana semantic.
pub fn from_multipath_descriptor(
desc: &descriptor::Descriptor<descriptor::DescriptorPublicKey>,
) -> Result<LianaPolicy, LianaPolicyError> {
// For now we only allow P2WSH descriptors.
let wsh_desc = match &desc {
descriptor::Descriptor::Wsh(desc) => desc,
_ => return Err(LianaPolicyError::IncompatibleDesc),
};
// Lift a semantic policy out of this Miniscript and normalize it to make sure we compare
// apples to apples below.
let ms = match wsh_desc.as_inner() {
descriptor::WshInner::Ms(ms) => ms,
let policy = match desc {
descriptor::Descriptor::Wsh(wsh_desc) => {
let ms = match wsh_desc.as_inner() {
descriptor::WshInner::Ms(ms) => ms,
_ => return Err(LianaPolicyError::IncompatibleDesc),
};
ms.lift().map_err(LianaPolicyError::PolicyAnalysis)?
}
descriptor::Descriptor::Tr(desc) => {
// For Taproot, make sure to not take the internal key into account in the semantic
// policy if it's unspendable.
if let Some(tree) = desc.tap_tree() {
let tree_policy = tree.lift().map_err(LianaPolicyError::PolicyAnalysis)?;
let unspend_int_xpub = unspendable_internal_xpub(desc)
.ok_or(LianaPolicyError::IncompatibleDesc)?;
let desc_int_xpub = get_multi_xkey(desc.internal_key())
.ok_or(LianaPolicyError::IncompatibleDesc)?;
if *desc_int_xpub == unspend_int_xpub {
tree_policy
} else {
SemanticPolicy::Threshold(
1,
vec![
SemanticPolicy::Key(desc.internal_key().clone()),
tree_policy,
],
)
}
} else {
// A Liana descriptor must contain a timelocked path.
return Err(LianaPolicyError::IncompatibleDesc);
}
}
// We only allow P2WSH and Taproot descriptors.
_ => return Err(LianaPolicyError::IncompatibleDesc),
};
let policy = ms
.lift()
.map_err(LianaPolicyError::PolicyAnalysis)?
.normalized();
}
.normalized();
let is_taproot = matches!(desc, descriptor::Descriptor::Tr(..));
// The policy must always be "1 of N spending paths" with at least an always-available
// primary path with at least one key, and at least one timelocked recovery path with at
@ -499,7 +632,7 @@ impl LianaPolicy {
// Use the constructor for sanity checking the keys and the Miniscript policy. Note this
// makes sure the recovery paths mapping isn't empty, too.
let prim_path = primary_path.ok_or(LianaPolicyError::IncompatibleDesc)?;
LianaPolicy::new(prim_path, recovery_paths)
LianaPolicy::_new(prim_path, recovery_paths, is_taproot)
}
pub fn primary_path(&self) -> &PathInfo {
@ -513,15 +646,11 @@ impl LianaPolicy {
&self.recovery_paths
}
fn into_miniscript(
self,
) -> Result<
miniscript::Miniscript<descriptor::DescriptorPublicKey, miniscript::Segwitv0>,
LianaPolicyError,
> {
fn into_policy(self) -> miniscript::policy::Concrete<descriptor::DescriptorPublicKey> {
let LianaPolicy {
primary_path,
recovery_paths,
..
} = self;
// Start with the primary spending path. We'll then or() all the recovery paths to it.
@ -529,20 +658,76 @@ impl LianaPolicy {
// Incrementally create the top-level policy using all recovery paths.
assert!(!recovery_paths.is_empty());
let tl_policy =
recovery_paths
.into_iter()
.fold(primary_keys, |tl_policy, (timelock, path_info)| {
let timelock = ConcretePolicy::Older(Sequence::from_height(timelock));
let keys = path_info.into_ms_policy();
let recovery_branch = ConcretePolicy::And(vec![keys.into(), timelock.into()]);
// We assume the larger the timelock the less likely a branch would be used.
ConcretePolicy::Or(vec![(99, tl_policy.into()), (1, recovery_branch.into())])
});
recovery_paths
.into_iter()
.fold(primary_keys, |tl_policy, (timelock, path_info)| {
let timelock = ConcretePolicy::Older(Sequence::from_height(timelock));
let keys = path_info.into_ms_policy();
let recovery_branch = ConcretePolicy::And(vec![keys.into(), timelock.into()]);
// We assume the larger the timelock the less likely a branch would be used.
ConcretePolicy::Or(vec![(99, tl_policy.into()), (1, recovery_branch.into())])
})
}
tl_policy
.compile::<miniscript::Segwitv0>()
.map_err(LianaPolicyError::InvalidPolicy)
fn into_multipath_descriptor_fallible(
self,
) -> Result<descriptor::Descriptor<descriptor::DescriptorPublicKey>, LianaPolicyError> {
if self.is_taproot {
// If compiling to a Taproot descriptor and we can't have an internal key, we want to
// compute a deterministic unspendable key to use as internal key. We compute it from
// the xpubs in the Taptree as per
// https://delvingbitcoin.org/t/unspendable-keys-in-descriptors/304/21. However, there
// is clearly an inter-dependency here: we need an internal key to get the Taptree, and
// vice-versa. So we use a dummy internal key. If it ends up as the internal key in the
// compiled descriptor, we replace it with a deterministically computed unspendable
// internal key.
let dummy_internal_key =
descriptor::DescriptorPublicKey::XPub(descriptor::DescriptorXKey::<bip32::Xpub> {
origin: None,
xkey: bip32::Xpub {
public_key: bip341_nums(),
chain_code: [0; 32].into(),
depth: 0,
parent_fingerprint: [0; 4].into(),
child_number: 0.into(),
network: bitcoin::Network::Regtest,
},
derivation_path: vec![].into(),
wildcard: descriptor::Wildcard::None,
});
let policy = self.into_policy();
let desc = policy
.clone()
.compile_tr(Some(dummy_internal_key.clone()))
.map_err(LianaPolicyError::InvalidPolicy)?;
let inner_desc = if let descriptor::Descriptor::Tr(ref d) = desc {
d
} else {
unreachable!("compile_tr() always gives a tr() descriptor.");
};
if inner_desc.internal_key() == &dummy_internal_key {
// Unfortunately to replace the dummy internal key with the correct one we need to
// perform the computation again.
let actual_internal_key = unspendable_internal_key(inner_desc)
.expect("Desc has a Taptree and only multixpubs.");
policy
.compile_tr(Some(actual_internal_key))
.map_err(LianaPolicyError::InvalidPolicy)
} else {
// A key from the policy could be used as internal key. No need for a deterministic
// internal key.
Ok(desc)
}
} else {
let ms = self
.into_policy()
.compile::<miniscript::Segwitv0>()
.map_err(|e| LianaPolicyError::InvalidPolicy(e.into()))?;
miniscript::Segwitv0::check_local_validity(&ms).expect("Miniscript must be sane");
Ok(descriptor::Descriptor::Wsh(
descriptor::Wsh::new(ms).expect("Must pass sanity checks"),
))
}
}
/// Create a descriptor from this spending policy with multipath key expressions. Note this
@ -552,11 +737,8 @@ impl LianaPolicy {
pub fn into_multipath_descriptor(
self,
) -> descriptor::Descriptor<descriptor::DescriptorPublicKey> {
let ms = self
.into_miniscript()
.expect("This is always checked when creating a LianaPolicy.");
miniscript::Segwitv0::check_local_validity(&ms).expect("Miniscript must be sane");
descriptor::Descriptor::Wsh(descriptor::Wsh::new(ms).expect("Must pass sanity checks"))
self.into_multipath_descriptor_fallible()
.expect("This is always checked when creating a LianaPolicy.")
}
}

File diff suppressed because one or more lines are too long

View File

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

View File

@ -481,7 +481,7 @@ impl DummyLiana {
let owner_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap());
let heir_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap());
let policy = descriptors::LianaPolicy::new(
let policy = descriptors::LianaPolicy::new_legacy(
owner_key,
[(10_000, heir_key)].iter().cloned().collect(),
)