diff --git a/CHANGELOG.md b/CHANGELOG.md index 7348f185e..18a5ae06c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`). - Upgrade to rust-bitcoin `0.28` - If using the `sqlite-db` feature all cached wallet data is deleted due to a possible UTXO inconsistency, a wallet.sync will recreate it +- Update `PkOrF` in the policy module to become an enum +- Add experimental support for Taproot, including: + - Support for `tr()` descriptors with complex tapscript trees + - Creation of Taproot PSBTs (BIP-371) + - Signing Taproot PSBTs (key spend and script spend) + - Support for `tr()` descriptors in the `descriptor!()` macro ## [v0.18.0] - [v0.17.0] diff --git a/Cargo.toml b/Cargo.toml index 14d21c3e1..ddbc46124 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ license = "MIT OR Apache-2.0" bdk-macros = "^0.6" log = "^0.4" miniscript = { version = "7.0", features = ["use-serde"] } -bitcoin = { version = "0.28", features = ["use-serde", "base64"] } +bitcoin = { version = "0.28.1", features = ["use-serde", "base64", "rand"] } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } rand = "^0.7" diff --git a/src/descriptor/dsl.rs b/src/descriptor/dsl.rs index caee94bd2..ccbe2b2bb 100644 --- a/src/descriptor/dsl.rs +++ b/src/descriptor/dsl.rs @@ -73,6 +73,48 @@ macro_rules! impl_top_level_pk { }}; } +#[doc(hidden)] +#[macro_export] +macro_rules! impl_top_level_tr { + ( $internal_key:expr, $tap_tree:expr ) => {{ + use $crate::miniscript::descriptor::{ + Descriptor, DescriptorPublicKey, KeyMap, TapTree, Tr, + }; + use $crate::miniscript::Tap; + + #[allow(unused_imports)] + use $crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks}; + + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + $internal_key + .into_descriptor_key() + .and_then(|key: DescriptorKey| key.extract(&secp)) + .map_err($crate::descriptor::DescriptorError::Key) + .and_then(|(pk, mut key_map, mut valid_networks)| { + let tap_tree = $tap_tree.map( + |(tap_tree, tree_keymap, tree_networks): ( + TapTree, + KeyMap, + ValidNetworks, + )| { + key_map.extend(tree_keymap.into_iter()); + valid_networks = + $crate::keys::merge_networks(&valid_networks, &tree_networks); + + tap_tree + }, + ); + + Ok(( + Descriptor::::Tr(Tr::new(pk, tap_tree)?), + key_map, + valid_networks, + )) + }) + }}; +} + #[doc(hidden)] #[macro_export] macro_rules! impl_leaf_opcode { @@ -228,6 +270,62 @@ macro_rules! impl_sortedmulti { } +#[doc(hidden)] +#[macro_export] +macro_rules! parse_tap_tree { + ( @merge $tree_a:expr, $tree_b:expr) => {{ + use std::sync::Arc; + use $crate::miniscript::descriptor::TapTree; + + $tree_a + .and_then(|tree_a| Ok((tree_a, $tree_b?))) + .and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| { + a_keymap.extend(b_keymap.into_iter()); + Ok((TapTree::Tree(Arc::new(a_tree), Arc::new(b_tree)), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks))) + }) + + }}; + + // Two sub-trees + ( { { $( $tree_a:tt )* }, { $( $tree_b:tt )* } } ) => {{ + let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } ); + let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + + // One leaf and a sub-tree + ( { $op_a:ident ( $( $minisc_a:tt )* ), { $( $tree_b:tt )* } } ) => {{ + let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) ); + let tree_b = $crate::parse_tap_tree!( { $( $tree_b )* } ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + ( { { $( $tree_a:tt )* }, $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{ + let tree_a = $crate::parse_tap_tree!( { $( $tree_a )* } ); + let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + + // Two leaves + ( { $op_a:ident ( $( $minisc_a:tt )* ), $op_b:ident ( $( $minisc_b:tt )* ) } ) => {{ + let tree_a = $crate::parse_tap_tree!( $op_a ( $( $minisc_a )* ) ); + let tree_b = $crate::parse_tap_tree!( $op_b ( $( $minisc_b )* ) ); + + $crate::parse_tap_tree!(@merge tree_a, tree_b) + }}; + + // Single leaf + ( $op:ident ( $( $minisc:tt )* ) ) => {{ + use std::sync::Arc; + use $crate::miniscript::descriptor::TapTree; + + $crate::fragment!( $op ( $( $minisc )* ) ) + .map(|(a_minisc, a_keymap, a_networks)| (TapTree::Leaf(Arc::new(a_minisc)), a_keymap, a_networks)) + }}; +} + #[doc(hidden)] #[macro_export] macro_rules! apply_modifier { @@ -441,6 +539,15 @@ macro_rules! descriptor { ( wsh ( $( $minisc:tt )* ) ) => ({ $crate::impl_top_level_sh!(Wsh, new, new_sortedmulti, Segwitv0, $( $minisc )*) }); + + ( tr ( $internal_key:expr ) ) => ({ + $crate::impl_top_level_tr!($internal_key, None) + }); + ( tr ( $internal_key:expr, $( $taptree:tt )* ) ) => ({ + let tap_tree = $crate::parse_tap_tree!( $( $taptree )* ); + tap_tree + .and_then(|tap_tree| $crate::impl_top_level_tr!($internal_key, Some(tap_tree))) + }); } #[doc(hidden)] @@ -480,6 +587,23 @@ impl From<(A, (B, (C, ())))> for TupleThree { } } +#[doc(hidden)] +#[macro_export] +macro_rules! group_multi_keys { + ( $( $key:expr ),+ ) => {{ + use $crate::keys::IntoDescriptorKey; + + let keys = vec![ + $( + $key.into_descriptor_key(), + )* + ]; + + keys.into_iter().collect::, _>>() + .map_err($crate::descriptor::DescriptorError::Key) + }}; +} + #[doc(hidden)] #[macro_export] macro_rules! fragment_internal { @@ -640,21 +764,22 @@ macro_rules! fragment { .and_then(|items| $crate::fragment!(thresh_vec($thresh, items))) }); ( multi_vec ( $thresh:expr, $keys:expr ) ) => ({ - $crate::keys::make_multi($thresh, $keys) + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); + + $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp) }); ( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({ - use $crate::keys::IntoDescriptorKey; + $crate::group_multi_keys!( $( $key ),* ) + .and_then(|keys| $crate::fragment!( multi_vec ( $thresh, keys ) )) + }); + ( multi_a_vec ( $thresh:expr, $keys:expr ) ) => ({ let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); - let keys = vec![ - $( - $key.into_descriptor_key(), - )* - ]; - - keys.into_iter().collect::, _>>() - .map_err($crate::descriptor::DescriptorError::Key) - .and_then(|keys| $crate::keys::make_multi($thresh, keys, &secp)) + $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::MultiA, $keys, &secp) + }); + ( multi_a ( $thresh:expr $(, $key:expr )+ ) ) => ({ + $crate::group_multi_keys!( $( $key ),* ) + .and_then(|keys| $crate::fragment!( multi_a_vec ( $thresh, keys ) )) }); // `sortedmulti()` is handled separately @@ -1064,4 +1189,35 @@ mod test { descriptor!(wsh(v: pk(uncompressed_pk))).unwrap(); } + + #[test] + fn test_dsl_tr_only_key() { + let private_key = + PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); + let (descriptor, _, _) = descriptor!(tr(private_key)).unwrap(); + + assert_eq!( + descriptor.to_string(), + "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)#heq9m95v" + ) + } + + #[test] + fn test_dsl_tr_simple_tree() { + let private_key = + PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); + let (descriptor, _, _) = + descriptor!(tr(private_key, { pk(private_key), pk(private_key) })).unwrap(); + + assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,{pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)})#xy5fjw6d") + } + + #[test] + fn test_dsl_tr_single_leaf() { + let private_key = + PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap(); + let (descriptor, _, _) = descriptor!(tr(private_key, pk(private_key))).unwrap(); + + assert_eq!(descriptor.to_string(), "tr(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c,pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c))#lzl2vmc7") + } } diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 9f993c0c4..89dd3f402 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -14,15 +14,15 @@ //! This module contains generic utilities to work with descriptors, plus some re-exported types //! from [`miniscript`]. -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, HashSet}; use std::ops::Deref; -use bitcoin::secp256k1; use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource}; -use bitcoin::util::psbt; +use bitcoin::util::{psbt, taproot}; +use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey}; use bitcoin::{Network, Script, TxOut}; -use miniscript::descriptor::{DescriptorType, InnerXKey}; +use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey}; pub use miniscript::{ descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor, DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0, @@ -61,6 +61,13 @@ pub type DerivedDescriptor<'s> = Descriptor>; /// [`psbt::Output`]: bitcoin::util::psbt::Output pub type HdKeyPaths = BTreeMap; +/// Alias for the type of maps that represent taproot key origins in a [`psbt::Input`] or +/// [`psbt::Output`] +/// +/// [`psbt::Input`]: bitcoin::util::psbt::Input +/// [`psbt::Output`]: bitcoin::util::psbt::Output +pub type TapKeyOrigins = BTreeMap, KeySource)>; + /// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`] pub trait IntoWalletDescriptor { /// Convert to wallet descriptor @@ -302,7 +309,8 @@ where } pub(crate) trait DerivedDescriptorMeta { - fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result; + fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths; + fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins; } pub(crate) trait DescriptorMeta { @@ -314,6 +322,16 @@ pub(crate) trait DescriptorMeta { hd_keypaths: &HdKeyPaths, secp: &'s SecpCtx, ) -> Option>; + fn derive_from_tap_key_origins<'s>( + &self, + tap_key_origins: &TapKeyOrigins, + secp: &'s SecpCtx, + ) -> Option>; + fn derive_from_psbt_key_origins<'s>( + &self, + key_origins: BTreeMap, + secp: &'s SecpCtx, + ) -> Option>; fn derive_from_psbt_input<'s>( &self, psbt_input: &psbt::Input, @@ -393,61 +411,124 @@ impl DescriptorMeta for ExtendedDescriptor { Ok(answer) } - fn derive_from_hd_keypaths<'s>( + fn derive_from_psbt_key_origins<'s>( &self, - hd_keypaths: &HdKeyPaths, + key_origins: BTreeMap, secp: &'s SecpCtx, ) -> Option> { - let index: HashMap<_, _> = hd_keypaths.values().map(|(a, b)| (a, b)).collect(); + // Ensure that deriving `xpub` with `path` yields `expected` + let verify_key = |xpub: &DescriptorXKey, + path: &DerivationPath, + expected: &SinglePubKey| { + let derived = xpub + .xkey + .derive_pub(secp, path) + .expect("The path should never contain hardened derivation steps") + .public_key; + + match expected { + SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true, + SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true, + _ => false, + } + }; let mut path_found = None; - self.for_each_key(|key| { - if path_found.is_some() { - // already found a matching path, we are done - return true; - } + // using `for_any_key` should make this stop as soon as we return `true` + self.for_any_key(|key| { if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() { - // Check if the key matches one entry in our `index`. If it does, `matches()` will + // Check if the key matches one entry in our `key_origins`. If it does, `matches()` will // return the "prefix" that matched, so we remove that prefix from the full path - // found in `index` and save it in `derive_path`. We expect this to be a derivation + // found in `key_origins` and save it in `derive_path`. We expect this to be a derivation // path of length 1 if the key is `wildcard` and an empty path otherwise. let root_fingerprint = xpub.root_fingerprint(secp); - let derivation_path: Option> = index + let derive_path = key_origins .get_key_value(&root_fingerprint) - .and_then(|(fingerprint, path)| { - xpub.matches(&(**fingerprint, (*path).clone()), secp) + .and_then(|(fingerprint, (path, expected))| { + xpub.matches(&(*fingerprint, (*path).clone()), secp) + .zip(Some((path, expected))) }) - .map(|prefix| { - index - .get(&xpub.root_fingerprint(secp)) - .unwrap() + .and_then(|(prefix, (full_path, expected))| { + let derive_path = full_path .into_iter() .skip(prefix.into_iter().count()) .cloned() - .collect() + .collect::(); + + // `derive_path` only contains the replacement index for the wildcard, if present, or + // an empty path for fixed descriptors. To verify the key we also need the normal steps + // that come before the wildcard, so we take them directly from `xpub` and then append + // the final index + if verify_key( + xpub, + &xpub.derivation_path.extend(derive_path.clone()), + expected, + ) { + Some(derive_path) + } else { + log::debug!( + "Key `{}` derived with {} yields an unexpected key", + root_fingerprint, + derive_path + ); + None + } }); - match derivation_path { + match derive_path { Some(path) if xpub.wildcard != Wildcard::None && path.len() == 1 => { // Ignore hardened wildcards if let ChildNumber::Normal { index } = path[0] { - path_found = Some(index) + path_found = Some(index); + return true; } } Some(path) if xpub.wildcard == Wildcard::None && path.is_empty() => { - path_found = Some(0) + path_found = Some(0); + return true; } _ => {} } } - true + false }); path_found.map(|path| self.as_derived(path, secp)) } + fn derive_from_hd_keypaths<'s>( + &self, + hd_keypaths: &HdKeyPaths, + secp: &'s SecpCtx, + ) -> Option> { + // "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins` + let key_origins = hd_keypaths + .iter() + .map(|(pk, (fingerprint, path))| { + ( + *fingerprint, + (path, SinglePubKey::FullKey(PublicKey::new(*pk))), + ) + }) + .collect(); + self.derive_from_psbt_key_origins(key_origins, secp) + } + + fn derive_from_tap_key_origins<'s>( + &self, + tap_key_origins: &TapKeyOrigins, + secp: &'s SecpCtx, + ) -> Option> { + // "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins` + let key_origins = tap_key_origins + .iter() + .map(|(pk, (_, (fingerprint, path)))| (*fingerprint, (path, SinglePubKey::XOnly(*pk)))) + .collect(); + self.derive_from_psbt_key_origins(key_origins, secp) + } + fn derive_from_psbt_input<'s>( &self, psbt_input: &psbt::Input, @@ -457,6 +538,9 @@ impl DescriptorMeta for ExtendedDescriptor { if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) { return Some(derived); } + if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) { + return Some(derived); + } if self.is_deriveable() { // We can't try to bruteforce the derivation index, exit here return None; @@ -497,7 +581,7 @@ impl DescriptorMeta for ExtendedDescriptor { } impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> { - fn get_hd_keypaths(&self, secp: &SecpCtx) -> Result { + fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths { let mut answer = BTreeMap::new(); self.for_each_key(|key| { if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() { @@ -515,7 +599,64 @@ impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> { true }); - Ok(answer) + answer + } + + fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins { + use miniscript::ToPublicKey; + + let mut answer = BTreeMap::new(); + let mut insert_path = |pk: &DerivedDescriptorKey<'_>, lh| { + let key_origin = match pk.deref() { + DescriptorPublicKey::XPub(xpub) => { + Some((xpub.root_fingerprint(secp), xpub.full_path(&[]))) + } + DescriptorPublicKey::SinglePub(_) => None, + }; + + // If this is the internal key, we only insert the key origin if it's not None. + // For keys found in the tap tree we always insert a key origin (because the signer + // looks for it to know which leaves to sign for), even though it may be None + match (lh, key_origin) { + (None, Some(ko)) => { + answer + .entry(pk.to_x_only_pubkey()) + .or_insert_with(|| (vec![], ko)); + } + (Some(lh), origin) => { + answer + .entry(pk.to_x_only_pubkey()) + .or_insert_with(|| (vec![], origin.unwrap_or_default())) + .0 + .push(lh); + } + _ => {} + } + }; + + if let Descriptor::Tr(tr) = &self { + // Internal key first, then iterate the scripts + insert_path(tr.internal_key(), None); + + for (_, ms) in tr.iter_scripts() { + // Assume always the same leaf version + let leaf_hash = taproot::TapLeafHash::from_script( + &ms.encode(), + taproot::LeafVersion::TapScript, + ); + + for key in ms.iter_pk_pkh() { + let key = match key { + miniscript::miniscript::iter::PkPkh::PlainPubkey(pk) => pk, + miniscript::miniscript::iter::PkPkh::HashedPubkey(pk) => pk, + }; + + insert_path(&key, Some(leaf_hash)); + } + } + } + + answer } } diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index 98b265734..215078b60 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -21,6 +21,7 @@ //! ``` //! # use std::sync::Arc; //! # use bdk::descriptor::*; +//! # use bdk::wallet::signer::*; //! # use bdk::bitcoin::secp256k1::Secp256k1; //! use bdk::descriptor::policy::BuildSatisfaction; //! let secp = Secp256k1::new(); @@ -29,7 +30,7 @@ //! let (extended_desc, key_map) = ExtendedDescriptor::parse_descriptor(&secp, desc)?; //! println!("{:?}", extended_desc); //! -//! let signers = Arc::new(key_map.into()); +//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp)); //! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?; //! println!("policy: {}", serde_json::to_string(&policy)?); //! # Ok::<(), bdk::Error>(()) @@ -43,7 +44,6 @@ use serde::ser::SerializeMap; use serde::{Serialize, Serializer}; use bitcoin::hashes::*; -use bitcoin::secp256k1; use bitcoin::util::bip32::Fingerprint; use bitcoin::{PublicKey, XOnlyPublicKey}; @@ -66,17 +66,16 @@ use super::XKeyUtils; use bitcoin::util::psbt::{Input as PsbtInput, PartiallySignedTransaction as Psbt}; use miniscript::psbt::PsbtInputSatisfier; -/// Raw public key or extended key fingerprint -#[derive(Debug, Clone, Default, Serialize)] -pub struct PkOrF { - #[serde(skip_serializing_if = "Option::is_none")] - pubkey: Option, - #[serde(skip_serializing_if = "Option::is_none")] - x_only_pubkey: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pubkey_hash: Option, - #[serde(skip_serializing_if = "Option::is_none")] - fingerprint: Option, +/// A unique identifier for a key +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum PkOrF { + /// A legacy public key + Pubkey(PublicKey), + /// A x-only public key + XOnlyPubkey(XOnlyPublicKey), + /// An extended key fingerprint + Fingerprint(Fingerprint), } impl PkOrF { @@ -85,27 +84,18 @@ impl PkOrF { DescriptorPublicKey::SinglePub(DescriptorSinglePub { key: SinglePubKey::FullKey(pk), .. - }) => PkOrF { - pubkey: Some(*pk), - ..Default::default() - }, + }) => PkOrF::Pubkey(*pk), DescriptorPublicKey::SinglePub(DescriptorSinglePub { key: SinglePubKey::XOnly(pk), .. - }) => PkOrF { - x_only_pubkey: Some(*pk), - ..Default::default() - }, - DescriptorPublicKey::XPub(xpub) => PkOrF { - fingerprint: Some(xpub.root_fingerprint(secp)), - ..Default::default() - }, + }) => PkOrF::XOnlyPubkey(*pk), + DescriptorPublicKey::XPub(xpub) => PkOrF::Fingerprint(xpub.root_fingerprint(secp)), } } } /// An item that needs to be satisfied -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(tag = "type", rename_all = "UPPERCASE")] pub enum SatisfiableItem { // Leaves @@ -259,7 +249,7 @@ where } /// Represent if and how much a policy item is satisfied by the wallet's descriptor -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(tag = "type", rename_all = "UPPERCASE")] pub enum Satisfaction { /// Only a partial satisfaction of some kind of threshold policy @@ -433,7 +423,7 @@ impl From for Satisfaction { } /// Descriptor spending policy -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Policy { /// Identifier for this policy node pub id: String, @@ -581,13 +571,12 @@ impl Policy { Ok(Some(policy)) } - fn make_multisig( + fn make_multisig( keys: &[DescriptorPublicKey], signers: &SignersContainer, build_sat: BuildSatisfaction, threshold: usize, sorted: bool, - is_ecdsa: bool, secp: &SecpCtx, ) -> Result, PolicyError> { if threshold == 0 { @@ -616,9 +605,7 @@ impl Policy { } if let Some(psbt) = build_sat.psbt() { - if is_ecdsa && ecdsa_signature_in_psbt(psbt, key, secp) - || !is_ecdsa && schnorr_signature_in_psbt(psbt, key, secp) - { + if Ctx::find_signature(psbt, key, secp) { satisfaction.add( &Satisfaction::Complete { condition: Default::default(), @@ -746,13 +733,15 @@ fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId { } } -fn signature( +fn make_generic_signature SatisfiableItem, F: Fn(&Psbt) -> bool>( key: &DescriptorPublicKey, signers: &SignersContainer, build_sat: BuildSatisfaction, secp: &SecpCtx, + make_policy: M, + find_sig: F, ) -> Policy { - let mut policy: Policy = SatisfiableItem::EcdsaSignature(PkOrF::from_key(key, secp)).into(); + let mut policy: Policy = make_policy().into(); policy.contribution = if signers.find(signer_id(key, secp)).is_some() { Satisfaction::Complete { @@ -763,7 +752,7 @@ fn signature( }; if let Some(psbt) = build_sat.psbt() { - policy.satisfaction = if ecdsa_signature_in_psbt(psbt, key, secp) { + policy.satisfaction = if find_sig(psbt) { Satisfaction::Complete { condition: Default::default(), } @@ -776,66 +765,115 @@ fn signature( } fn generic_sig_in_psbt< + // C is for "check", it's a closure we use to *check* if a psbt input contains the signature + // for a specific key C: Fn(&PsbtInput, &SinglePubKey) -> bool, - M: Fn(&secp256k1::PublicKey) -> SinglePubKey, + // E is for "extract", it extracts a key from the bip32 derivations found in the psbt input + E: Fn(&PsbtInput, Fingerprint) -> Option, >( psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx, - map: M, check: C, + extract: E, ) -> bool { //TODO check signature validity psbt.inputs.iter().all(|input| match key { DescriptorPublicKey::SinglePub(DescriptorSinglePub { key, .. }) => check(input, key), DescriptorPublicKey::XPub(xpub) => { - let pubkey = input - .bip32_derivation - .iter() - .find(|(_, (f, _))| *f == xpub.root_fingerprint(secp)) - .map(|(p, _)| p); //TODO check actual derivation matches - match pubkey { - Some(pubkey) => check(input, &map(pubkey)), + match extract(input, xpub.root_fingerprint(secp)) { + Some(pubkey) => check(input, &pubkey), None => false, } } }) } -fn ecdsa_signature_in_psbt(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool { - generic_sig_in_psbt( - psbt, - key, - secp, - |pk| SinglePubKey::FullKey(PublicKey::new(*pk)), - |input, pk| match pk { - SinglePubKey::FullKey(pk) => input.partial_sigs.contains_key(pk), - _ => false, - }, - ) +trait SigExt: ScriptContext { + fn make_signature( + key: &DescriptorPublicKey, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Policy; + + fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool; } -fn schnorr_signature_in_psbt(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool { - generic_sig_in_psbt( - psbt, - key, - secp, - |pk| SinglePubKey::XOnly((*pk).into()), - |input, pk| { - let pk = match pk { - SinglePubKey::XOnly(pk) => pk, - _ => return false, - }; - - // This assumes the internal key is never used in the script leaves, which I think is - // reasonable - match &input.tap_internal_key { - Some(ik) if ik == pk => input.tap_key_sig.is_some(), - _ => input.tap_script_sigs.keys().any(|(sk, _)| sk == pk), - } - }, - ) +impl SigExt for T { + fn make_signature( + key: &DescriptorPublicKey, + signers: &SignersContainer, + build_sat: BuildSatisfaction, + secp: &SecpCtx, + ) -> Policy { + if T::as_enum().is_taproot() { + make_generic_signature( + key, + signers, + build_sat, + secp, + || SatisfiableItem::SchnorrSignature(PkOrF::from_key(key, secp)), + |psbt| Self::find_signature(psbt, key, secp), + ) + } else { + make_generic_signature( + key, + signers, + build_sat, + secp, + || SatisfiableItem::EcdsaSignature(PkOrF::from_key(key, secp)), + |psbt| Self::find_signature(psbt, key, secp), + ) + } + } + + fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool { + if T::as_enum().is_taproot() { + generic_sig_in_psbt( + psbt, + key, + secp, + |input, pk| { + let pk = match pk { + SinglePubKey::XOnly(pk) => pk, + _ => return false, + }; + + if input.tap_internal_key == Some(*pk) && input.tap_key_sig.is_some() { + true + } else { + input.tap_script_sigs.keys().any(|(sk, _)| sk == pk) + } + }, + |input, fing| { + input + .tap_key_origins + .iter() + .find(|(_, (_, (f, _)))| f == &fing) + .map(|(pk, _)| SinglePubKey::XOnly(*pk)) + }, + ) + } else { + generic_sig_in_psbt( + psbt, + key, + secp, + |input, pk| match pk { + SinglePubKey::FullKey(pk) => input.partial_sigs.contains_key(pk), + _ => false, + }, + |input, fing| { + input + .bip32_derivation + .iter() + .find(|(_, (f, _))| f == &fing) + .map(|(pk, _)| SinglePubKey::FullKey(PublicKey::new(*pk))) + }, + ) + } + } } impl ExtractPolicy for Miniscript { @@ -848,8 +886,10 @@ impl ExtractPolicy for Miniscript None, - Terminal::PkK(pubkey) => Some(signature(pubkey, signers, build_sat, secp)), - Terminal::PkH(pubkey_hash) => Some(signature(pubkey_hash, signers, build_sat, secp)), + Terminal::PkK(pubkey) => Some(Ctx::make_signature(pubkey, signers, build_sat, secp)), + Terminal::PkH(pubkey_hash) => { + Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp)) + } Terminal::After(value) => { let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into(); policy.contribution = Satisfaction::Complete { @@ -910,15 +950,9 @@ impl ExtractPolicy for Miniscript { Some(SatisfiableItem::Hash160Preimage { hash: *hash }.into()) } - Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => Policy::make_multisig( - pks, - signers, - build_sat, - *k, - false, - !Ctx::as_enum().is_taproot(), - secp, - )?, + Terminal::Multi(k, pks) | Terminal::MultiA(k, pks) => { + Policy::make_multisig::(pks, signers, build_sat, *k, false, secp)? + } // Identities Terminal::Alt(inner) | Terminal::Swap(inner) @@ -1008,28 +1042,42 @@ impl ExtractPolicy for Descriptor { build_sat: BuildSatisfaction, secp: &SecpCtx, ) -> Result, Error> { - fn make_sortedmulti( + fn make_sortedmulti( keys: &SortedMultiVec, signers: &SignersContainer, build_sat: BuildSatisfaction, secp: &SecpCtx, ) -> Result, Error> { - Ok(Policy::make_multisig( + Ok(Policy::make_multisig::( keys.pks.as_ref(), signers, build_sat, keys.k, true, - true, secp, )?) } match self { - Descriptor::Pkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))), - Descriptor::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))), + Descriptor::Pkh(pk) => Ok(Some(miniscript::Legacy::make_signature( + pk.as_inner(), + signers, + build_sat, + secp, + ))), + Descriptor::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature( + pk.as_inner(), + signers, + build_sat, + secp, + ))), Descriptor::Sh(sh) => match sh.as_inner() { - ShInner::Wpkh(pk) => Ok(Some(signature(pk.as_inner(), signers, build_sat, secp))), + ShInner::Wpkh(pk) => Ok(Some(miniscript::Segwitv0::make_signature( + pk.as_inner(), + signers, + build_sat, + secp, + ))), ShInner::Ms(ms) => Ok(ms.extract_policy(signers, build_sat, secp)?), ShInner::SortedMulti(ref keys) => make_sortedmulti(keys, signers, build_sat, secp), ShInner::Wsh(wsh) => match wsh.as_inner() { @@ -1045,17 +1093,26 @@ impl ExtractPolicy for Descriptor { }, Descriptor::Bare(ms) => Ok(ms.as_inner().extract_policy(signers, build_sat, secp)?), Descriptor::Tr(tr) => { - let mut items = vec![signature(tr.internal_key(), signers, build_sat, secp)]; - items.append( - &mut tr - .iter_scripts() - .filter_map(|(_, ms)| { - ms.extract_policy(signers, build_sat, secp).transpose() - }) - .collect::, _>>()?, - ); - - Ok(Policy::make_thresh(items, 1)?) + // If there's no tap tree, treat this as a single sig, otherwise build a `Thresh` + // node with threshold = 1 and the key spend signature plus all the tree leaves + let key_spend_sig = + miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp); + + if tr.taptree().is_none() { + Ok(Some(key_spend_sig)) + } else { + let mut items = vec![key_spend_sig]; + items.append( + &mut tr + .iter_scripts() + .filter_map(|(_, ms)| { + ms.extract_policy(signers, build_sat, secp).transpose() + }) + .collect::, _>>()?, + ); + + Ok(Policy::make_thresh(items, 1)?) + } } } } @@ -1109,30 +1166,26 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() .unwrap(); - assert!( - matches!(&policy.item, EcdsaSignature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint) - ); + assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint)); assert!(matches!(&policy.contribution, Satisfaction::None)); let desc = descriptor!(wpkh(prvkey)).unwrap(); let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() .unwrap(); - assert!( - matches!(&policy.item, EcdsaSignature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint) - ); + assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint)); assert!( matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None) ); @@ -1148,7 +1201,7 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() @@ -1156,8 +1209,8 @@ mod test { assert!( matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize - && keys[0].fingerprint.unwrap() == fingerprint0 - && keys[1].fingerprint.unwrap() == fingerprint1) + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1)) ); // TODO should this be "Satisfaction::None" since we have no prv keys? // TODO should items and conditions not be empty? @@ -1180,15 +1233,15 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() .unwrap(); assert!( matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize - && keys[0].fingerprint.unwrap() == fingerprint0 - && keys[1].fingerprint.unwrap() == fingerprint1) + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1)) ); assert!( @@ -1212,7 +1265,7 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() @@ -1220,8 +1273,8 @@ mod test { assert!( matches!(&policy.item, Multisig { keys, threshold } if threshold == &1 - && keys[0].fingerprint.unwrap() == fingerprint0 - && keys[1].fingerprint.unwrap() == fingerprint1) + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1)) ); assert!( matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2 @@ -1244,7 +1297,7 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() @@ -1252,8 +1305,8 @@ mod test { assert!( matches!(&policy.item, Multisig { keys, threshold } if threshold == &2 - && keys[0].fingerprint.unwrap() == fingerprint0 - && keys[1].fingerprint.unwrap() == fingerprint1) + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1)) ); assert!( @@ -1277,15 +1330,13 @@ mod test { .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); let single_key = wallet_desc.derive(0); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = single_key .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() .unwrap(); - assert!( - matches!(&policy.item, EcdsaSignature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint) - ); + assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint)); assert!(matches!(&policy.contribution, Satisfaction::None)); let desc = descriptor!(wpkh(prvkey)).unwrap(); @@ -1293,15 +1344,13 @@ mod test { .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); let single_key = wallet_desc.derive(0); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = single_key .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() .unwrap(); - assert!( - matches!(&policy.item, EcdsaSignature(pk_or_f) if pk_or_f.fingerprint.unwrap() == fingerprint) - ); + assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint)); assert!( matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None) ); @@ -1320,7 +1369,7 @@ mod test { .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); let single_key = wallet_desc.derive(0); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = single_key .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() @@ -1328,8 +1377,8 @@ mod test { assert!( matches!(&policy.item, Multisig { keys, threshold } if threshold == &1 - && keys[0].fingerprint.unwrap() == fingerprint0 - && keys[1].fingerprint.unwrap() == fingerprint1) + && keys[0] == PkOrF::Fingerprint(fingerprint0) + && keys[1] == PkOrF::Fingerprint(fingerprint1)) ); assert!( matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2 @@ -1363,7 +1412,7 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() @@ -1402,7 +1451,7 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() @@ -1427,7 +1476,7 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() @@ -1445,7 +1494,7 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() @@ -1467,10 +1516,10 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers = keymap.into(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc - .extract_policy(&signers, BuildSatisfaction::None, &secp) + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) .unwrap() .unwrap(); @@ -1533,7 +1582,7 @@ mod test { addr.to_string() ); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let psbt = Psbt::from_str(ALICE_SIGNED_PSBT).unwrap(); @@ -1594,7 +1643,7 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let addr = wallet_desc .as_derived(0, &secp) @@ -1682,9 +1731,185 @@ mod test { let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers_container = Arc::new(SignersContainer::from(keymap)); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); let policy = wallet_desc.extract_policy(&signers_container, BuildSatisfaction::None, &secp); assert!(policy.is_ok()); } + + #[test] + fn test_extract_tr_key_spend() { + let secp = Secp256k1::new(); + + let (prvkey, _, fingerprint) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(prvkey)).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap(); + assert_eq!( + policy, + Some(Policy { + id: "48u0tz0n".to_string(), + item: SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(fingerprint)), + satisfaction: Satisfaction::None, + contribution: Satisfaction::Complete { + condition: Condition::default() + } + }) + ); + } + + #[test] + fn test_extract_tr_script_spend() { + let secp = Secp256k1::new(); + + let (alice_prv, _, alice_fing) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + let (_, bob_pub, bob_fing) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(bob_pub, pk(alice_prv))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp)); + + let policy = wallet_desc + .extract_policy(&signers_container, BuildSatisfaction::None, &secp) + .unwrap() + .unwrap(); + + assert!( + matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2) + ); + assert!( + matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1]) + ); + + let alice_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(alice_fing)); + let bob_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(bob_fing)); + + let thresh_items = match policy.item { + SatisfiableItem::Thresh { items, .. } => items, + _ => unreachable!(), + }; + + assert_eq!(thresh_items[0].item, bob_sig); + assert_eq!(thresh_items[1].item, alice_sig); + } + + #[test] + fn test_extract_tr_satisfaction_key_spend() { + const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSIRYnkGTDxwXMHP32fkDFoGJY28trxbkkVgR2z7jZa2pOJA0AyRF8LgAAAIADAAAAARcgJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQAAA=="; + const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAUKgMCqtGLSiGYhsTols2UJ/VQQgQi/SXO38uXs2SahdAQAAAAD/////ARyWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRIEiEBFjbZa1xdjLfFjrKzuC1F1LeRyI/gL6IuGKNmUuSARNAIsRvARpRxuyQosVA7guRQT9vXr+S25W2tnP2xOGBsSgq7A4RL8yrbvwDmNlWw9R0Nc/6t+IsyCyy7dD/lbUGgyEWJ5Bkw8cFzBz99n5AxaBiWNvLa8W5JFYEds+42WtqTiQNAMkRfC4AAACAAwAAAAEXICeQZMPHBcwc/fZ+QMWgYljby2vFuSRWBHbPuNlrak4kAAA="; + + let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap(); + let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap(); + + let secp = Secp256k1::new(); + + let (_, pubkey, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(pubkey)).unwrap(); + let (wallet_desc, _) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let policy_unsigned = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&unsigned_psbt), + &secp, + ) + .unwrap() + .unwrap(); + let policy_signed = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&signed_psbt), + &secp, + ) + .unwrap() + .unwrap(); + + assert_eq!(policy_unsigned.satisfaction, Satisfaction::None); + assert_eq!( + policy_signed.satisfaction, + Satisfaction::Complete { + condition: Default::default() + } + ); + } + + #[test] + fn test_extract_tr_satisfaction_script_spend() { + const UNSIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2IhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA=="; + const SIGNED_PSBT: &str = "cHNidP8BAFMBAAAAAWZalxaErOL7P3WPIUc8DsjgE68S+ww+uqiqEI2SAwlPAAAAAAD/////AQiWmAAAAAAAF6kU4R3W8CnGzZcSsaovTYu0X8vHt3WHAAAAAAABASuAlpgAAAAAACJRINa6bLPZwp3/CYWoxyI3mLYcSC5f9LInAMUng94nspa2AQcAAQhCAUALcP9w/+Ddly9DWdhHTnQ9uCDWLPZjR6vKbKePswW2Ee6W5KNfrklus/8z98n7BQ1U4vADHk0FbadeeL8rrbHlARNAC3D/cP/g3ZcvQ1nYR050Pbgg1iz2Y0erymynj7MFthHuluSjX65JbrP/M/fJ+wUNVOLwAx5NBW2nXni/K62x5UEUeEbK57HG1FUp69HHhjBZH9bSvss8e3qhLoMuXPK5hBr2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHUAXNmWieJ80Fs+PMa2C186YOBPZbYG/ieEUkagMwzJ788SoCucNdp5wnxfpuJVygFhglDrXGzujFtC82PrMohwuIhXBgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYjIHhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQarMAhFnhGyuexxtRVKevRx4YwWR/W0r7LPHt6oS6DLlzyuYQaLQH2onWFc3UR6I9ZhuHVeJCi5LNAf4APVd7mHn4BhdViHRwu7j4AAACAAgAAACEWgiPY+kcolS1Hp0niOK/+7VHz6F+nsz8JVxnzWzkgToYNAMkRfC4AAACAAgAAAAEXIIIj2PpHKJUtR6dJ4jiv/u1R8+hfp7M/CVcZ81s5IE6GARgg9qJ1hXN1EeiPWYbh1XiQouSzQH+AD1Xe5h5+AYXVYh0AAA=="; + + let unsigned_psbt = Psbt::from_str(UNSIGNED_PSBT).unwrap(); + let signed_psbt = Psbt::from_str(SIGNED_PSBT).unwrap(); + + let secp = Secp256k1::new(); + + let (_, alice_pub, _) = setup_keys(ALICE_TPRV_STR, ALICE_BOB_PATH, &secp); + let (_, bob_pub, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp); + + let desc = descriptor!(tr(bob_pub, pk(alice_pub))).unwrap(); + let (wallet_desc, _) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + + let policy_unsigned = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&unsigned_psbt), + &secp, + ) + .unwrap() + .unwrap(); + let policy_signed = wallet_desc + .extract_policy( + &SignersContainer::default(), + BuildSatisfaction::Psbt(&signed_psbt), + &secp, + ) + .unwrap() + .unwrap(); + + assert!( + matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2) + ); + assert!( + matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty()) + ); + + assert!( + matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2) + ); + assert!( + matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1]) + ); + + let satisfied_items = match policy_signed.item { + SatisfiableItem::Thresh { items, .. } => items, + _ => unreachable!(), + }; + + assert_eq!( + satisfied_items[0].satisfaction, + Satisfaction::Complete { + condition: Default::default() + } + ); + assert_eq!( + satisfied_items[1].satisfaction, + Satisfaction::Complete { + condition: Default::default() + } + ); + } } diff --git a/src/keys/mod.rs b/src/keys/mod.rs index 320cca1f5..20ff58184 100644 --- a/src/keys/mod.rs +++ b/src/keys/mod.rs @@ -792,13 +792,18 @@ pub fn make_pkh, Ctx: ScriptContext>( // Used internally by `bdk::fragment!` to build `multi()` fragments #[doc(hidden)] -pub fn make_multi, Ctx: ScriptContext>( +pub fn make_multi< + Pk: IntoDescriptorKey, + Ctx: ScriptContext, + V: Fn(usize, Vec) -> Terminal, +>( thresh: usize, + variant: V, pks: Vec, secp: &SecpCtx, ) -> Result<(Miniscript, KeyMap, ValidNetworks), DescriptorError> { let (pks, key_map, valid_networks) = expand_multi_keys(pks, secp)?; - let minisc = Miniscript::from_ast(Terminal::Multi(thresh, pks))?; + let minisc = Miniscript::from_ast(variant(thresh, pks))?; minisc.check_miniscript()?; diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs index 556d98f64..297f26772 100644 --- a/src/testutils/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -387,11 +387,33 @@ macro_rules! bdk_blockchain_tests { Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new()).unwrap() } - fn init_single_sig() -> (Wallet, $blockchain, (String, Option), TestClient) { + enum WalletType { + WpkhSingleSig, + TaprootKeySpend, + TaprootScriptSpend, + TaprootScriptSpend2, + TaprootScriptSpend3, + } + + fn init_wallet(ty: WalletType) -> (Wallet, $blockchain, (String, Option), TestClient) { let _ = env_logger::try_init(); - let descriptors = testutils! { - @descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) ) + let descriptors = match ty { + WalletType::WpkhSingleSig => testutils! { + @descriptors ( "wpkh(Alice)" ) ( "wpkh(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) ) + }, + WalletType::TaprootKeySpend => testutils! { + @descriptors ( "tr(Alice)" ) ( "tr(Alice)" ) ( @keys ( "Alice" => (@generate_xprv "/44'/0'/0'/0/*", "/44'/0'/0'/1/*") ) ) + }, + WalletType::TaprootScriptSpend => testutils! { + @descriptors ( "tr(Key,and_v(v:pk(Script),older(6)))" ) ( "tr(Key,and_v(v:pk(Script),older(6)))" ) ( @keys ( "Key" => (@literal "30e14486f993d5a2d222770e97286c56cec5af115e1fb2e0065f476a0fcf8788"), "Script" => (@generate_xprv "/0/*", "/1/*") ) ) + }, + WalletType::TaprootScriptSpend2 => testutils! { + @descriptors ( "tr(Alice,pk(Bob))" ) ( "tr(Alice,pk(Bob))" ) ( @keys ( "Alice" => (@literal "30e14486f993d5a2d222770e97286c56cec5af115e1fb2e0065f476a0fcf8788"), "Bob" => (@generate_xprv "/0/*", "/1/*") ) ) + }, + WalletType::TaprootScriptSpend3 => testutils! { + @descriptors ( "tr(Alice,{pk(Bob),pk(Carol)})" ) ( "tr(Alice,{pk(Bob),pk(Carol)})" ) ( @keys ( "Alice" => (@literal "30e14486f993d5a2d222770e97286c56cec5af115e1fb2e0065f476a0fcf8788"), "Bob" => (@generate_xprv "/0/*", "/1/*"), "Carol" => (@generate_xprv "/0/*", "/1/*") ) ) + }, }; let test_client = TestClient::default(); @@ -405,6 +427,10 @@ macro_rules! bdk_blockchain_tests { (wallet, blockchain, descriptors, test_client) } + fn init_single_sig() -> (Wallet, $blockchain, (String, Option), TestClient) { + init_wallet(WalletType::WpkhSingleSig) + } + #[test] fn test_sync_simple() { use std::ops::Deref; @@ -1203,6 +1229,130 @@ macro_rules! bdk_blockchain_tests { wallet.sync(&blockchain, SyncOptions::default()).unwrap(); } + + #[test] + fn test_taproot_key_spend() { + let (wallet, blockchain, descriptors, mut test_client) = init_wallet(WalletType::TaprootKeySpend); + + let _ = test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) + }); + wallet.sync(&blockchain, SyncOptions::default()).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000); + + let tx = { + let mut builder = wallet.build_tx(); + builder.add_recipient(test_client.get_node_address(None).script_pubkey(), 25_000); + let (mut psbt, _details) = builder.finish().unwrap(); + wallet.sign(&mut psbt, Default::default()).unwrap(); + psbt.extract_tx() + }; + blockchain.broadcast(&tx).unwrap(); + } + + #[test] + fn test_taproot_script_spend() { + let (wallet, blockchain, descriptors, mut test_client) = init_wallet(WalletType::TaprootScriptSpend); + + let _ = test_client.receive(testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 6 ) + }); + wallet.sync(&blockchain, SyncOptions::default()).unwrap(); + assert_eq!(wallet.get_balance().unwrap(), 50_000); + + let ext_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); + let int_policy = wallet.policies(KeychainKind::Internal).unwrap().unwrap(); + + let ext_path = vec![(ext_policy.id.clone(), vec![1])].into_iter().collect(); + let int_path = vec![(int_policy.id.clone(), vec![1])].into_iter().collect(); + + let tx = { + let mut builder = wallet.build_tx(); + builder.add_recipient(test_client.get_node_address(None).script_pubkey(), 25_000) + .policy_path(ext_path, KeychainKind::External) + .policy_path(int_path, KeychainKind::Internal); + let (mut psbt, _details) = builder.finish().unwrap(); + wallet.sign(&mut psbt, Default::default()).unwrap(); + psbt.extract_tx() + }; + blockchain.broadcast(&tx).unwrap(); + } + + #[test] + fn test_sign_taproot_core_keyspend_psbt() { + test_sign_taproot_core_psbt(WalletType::TaprootKeySpend); + } + + #[test] + fn test_sign_taproot_core_scriptspend2_psbt() { + test_sign_taproot_core_psbt(WalletType::TaprootScriptSpend2); + } + + #[test] + fn test_sign_taproot_core_scriptspend3_psbt() { + test_sign_taproot_core_psbt(WalletType::TaprootScriptSpend3); + } + + fn test_sign_taproot_core_psbt(wallet_type: WalletType) { + use std::str::FromStr; + use serde_json; + use bitcoincore_rpc::jsonrpc::serde_json::Value; + use bitcoincore_rpc::{Auth, Client, RpcApi}; + + let (wallet, _blockchain, _descriptors, test_client) = init_wallet(wallet_type); + + // TODO replace once rust-bitcoincore-rpc with PR 174 released + // https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/174 + let _createwallet_result: Value = test_client.bitcoind.client.call("createwallet", &["taproot_wallet".into(), true.into(), true.into(), serde_json::to_value("").unwrap(), false.into(), true.into(), true.into(), false.into()]).expect("created wallet"); + + let external_descriptor = wallet.get_descriptor_for_keychain(KeychainKind::External); + + // TODO replace once bitcoind released with support for rust-bitcoincore-rpc PR 174 + let taproot_wallet_client = Client::new(&test_client.bitcoind.rpc_url_with_wallet("taproot_wallet"), Auth::CookieFile(test_client.bitcoind.params.cookie_file.clone())).unwrap(); + + let descriptor_info = taproot_wallet_client.get_descriptor_info(external_descriptor.to_string().as_str()).expect("descriptor info"); + + let import_descriptor_args = json!([{ + "desc": descriptor_info.descriptor, + "active": true, + "timestamp": "now", + "label":"taproot key spend", + }]); + let _importdescriptors_result: Value = taproot_wallet_client.call("importdescriptors", &[import_descriptor_args]).expect("import wallet"); + let generate_to_address: bitcoin::Address = taproot_wallet_client.call("getnewaddress", &["test address".into(), "bech32m".into()]).expect("new address"); + let _generatetoaddress_result = taproot_wallet_client.generate_to_address(101, &generate_to_address).expect("generated to address"); + let send_to_address = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address.to_string(); + let change_address = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address.to_string(); + let send_addr_amounts = json!([{ + send_to_address: "0.4321" + }]); + let send_options = json!({ + "change_address": change_address, + "psbt": true, + }); + let send_result: Value = taproot_wallet_client.call("send", &[send_addr_amounts, Value::Null, "unset".into(), Value::Null, send_options]).expect("send psbt"); + let core_psbt = send_result["psbt"].as_str().expect("core psbt str"); + + use bitcoin::util::psbt::PartiallySignedTransaction; + + // Test parsing core created PSBT + let mut psbt = PartiallySignedTransaction::from_str(&core_psbt).expect("core taproot psbt"); + + // Test signing core created PSBT + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert_eq!(finalized, true); + + // Test with updated psbt + let update_result: Value = taproot_wallet_client.call("utxoupdatepsbt", &[core_psbt.into()]).expect("update psbt utxos"); + let core_updated_psbt = update_result.as_str().expect("core updated psbt"); + + // Test parsing core created and updated PSBT + let mut psbt = PartiallySignedTransaction::from_str(&core_updated_psbt).expect("core taproot psbt"); + + // Test signing core created and updated PSBT + let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); + assert_eq!(finalized, true); + } } }; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 55ef3ba42..6d2ff385e 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -24,13 +24,15 @@ use std::sync::Arc; use bitcoin::secp256k1::Secp256k1; use bitcoin::consensus::encode::serialize; -use bitcoin::util::psbt; +use bitcoin::util::{psbt, taproot}; use bitcoin::{ - Address, EcdsaSighashType, Network, OutPoint, Script, Transaction, TxOut, Txid, Witness, + Address, EcdsaSighashType, Network, OutPoint, SchnorrSighashType, Script, Transaction, TxOut, + Txid, Witness, }; use miniscript::descriptor::DescriptorTrait; use miniscript::psbt::PsbtInputSatisfier; +use miniscript::ToPublicKey; #[allow(unused_imports)] use log::{debug, error, info, trace}; @@ -50,7 +52,7 @@ pub use utils::IsDust; use address_validator::AddressValidator; use coin_selection::DefaultCoinSelectionAlgorithm; -use signer::{SignOptions, Signer, SignerOrdering, SignersContainer}; +use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner}; use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams}; use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx}; @@ -79,10 +81,10 @@ const CACHE_ADDR_BATCH_SIZE: u32 = 100; /// /// 1. output *descriptors* from which it can derive addresses. /// 2. A [`Database`] where it tracks transactions and utxos related to the descriptors. -/// 3. [`Signer`]s that can contribute signatures to addresses instantiated from the descriptors. +/// 3. [`signer`]s that can contribute signatures to addresses instantiated from the descriptors. /// /// [`Database`]: crate::database::Database -/// [`Signer`]: crate::signer::Signer +/// [`signer`]: crate::signer #[derive(Debug)] pub struct Wallet { descriptor: ExtendedDescriptor, @@ -197,7 +199,7 @@ where KeychainKind::External, get_checksum(&descriptor.to_string())?.as_bytes(), )?; - let signers = Arc::new(SignersContainer::from(keymap)); + let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp)); let (change_descriptor, change_signers) = match change_descriptor { Some(desc) => { let (change_descriptor, change_keymap) = @@ -207,7 +209,11 @@ where get_checksum(&change_descriptor.to_string())?.as_bytes(), )?; - let change_signers = Arc::new(SignersContainer::from(change_keymap)); + let change_signers = Arc::new(SignersContainer::build( + change_keymap, + &change_descriptor, + &secp, + )); // if !parsed.same_structure(descriptor.as_ref()) { // return Err(Error::DifferentDescriptorStructure); // } @@ -457,7 +463,7 @@ where &mut self, keychain: KeychainKind, ordering: SignerOrdering, - signer: Arc, + signer: Arc, ) { let signers = match keychain { KeychainKind::External => Arc::make_mut(&mut self.signers), @@ -1008,23 +1014,27 @@ where // this helps us doing our job later self.add_input_hd_keypaths(psbt)?; - // If we aren't allowed to use `witness_utxo`, ensure that every input but finalized one + // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones) // has the `non_witness_utxo` if !sign_options.trust_witness_utxo && psbt .inputs .iter() .filter(|i| i.final_script_witness.is_none() && i.final_script_sig.is_none()) + .filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none()) .any(|i| i.non_witness_utxo.is_none()) { return Err(Error::Signer(signer::SignerError::MissingNonWitnessUtxo)); } // If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input - // is using `SIGHASH_ALL` + // is using `SIGHASH_ALL` or `SIGHASH_DEFAULT` for taproot if !sign_options.allow_all_sighashes && !psbt.inputs.iter().all(|i| { - i.sighash_type.is_none() || i.sighash_type == Some(EcdsaSighashType::All.into()) + i.sighash_type.is_none() + || i.sighash_type == Some(EcdsaSighashType::All.into()) + || i.sighash_type == Some(SchnorrSighashType::All.into()) + || i.sighash_type == Some(SchnorrSighashType::Default.into()) }) { return Err(Error::Signer(signer::SignerError::NonStandardSighash)); @@ -1036,13 +1046,7 @@ where .iter() .chain(self.change_signers.signers().iter()) { - if signer.sign_whole_tx() { - signer.sign(psbt, None, &self.secp)?; - } else { - for index in 0..psbt.inputs.len() { - signer.sign(psbt, Some(index), &self.secp)?; - } - } + signer.sign_transaction(psbt, &self.secp)?; } // attempt to finalize @@ -1227,7 +1231,7 @@ where let derived_descriptor = descriptor.as_derived(index, &self.secp); - let hd_keypaths = derived_descriptor.get_hd_keypaths(&self.secp)?; + let hd_keypaths = derived_descriptor.get_hd_keypaths(&self.secp); let script = derived_descriptor.script_pubkey(); for validator in &self.address_validators { @@ -1438,7 +1442,15 @@ where psbt_input: foreign_psbt_input, outpoint, } => { - if !params.only_witness_utxo && foreign_psbt_input.non_witness_utxo.is_none() { + let is_taproot = foreign_psbt_input + .witness_utxo + .as_ref() + .map(|txout| txout.script_pubkey.is_v1_p2tr()) + .unwrap_or(false); + if !is_taproot + && !params.only_witness_utxo + && foreign_psbt_input.non_witness_utxo.is_none() + { return Err(Error::Generic(format!( "Missing non_witness_utxo on foreign utxo {}", outpoint @@ -1463,7 +1475,32 @@ where let (desc, _) = self._get_descriptor_for_keychain(keychain); let derived_descriptor = desc.as_derived(child, &self.secp); - psbt_output.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp)?; + if let miniscript::Descriptor::Tr(tr) = &derived_descriptor { + let tap_tree = if tr.taptree().is_some() { + let mut builder = taproot::TaprootBuilder::new(); + for (depth, ms) in tr.iter_scripts() { + let script = ms.encode(); + builder = builder.add_leaf(depth, script).expect( + "Computing spend data on a valid Tree should always succeed", + ); + } + Some( + psbt::TapTree::from_builder(builder) + .expect("The tree should always be valid"), + ) + } else { + None + }; + psbt_output.tap_tree = tap_tree; + psbt_output + .tap_key_origins + .append(&mut derived_descriptor.get_tap_key_origins(&self.secp)); + psbt_output.tap_internal_key = Some(tr.internal_key().to_x_only_pubkey()); + } else { + psbt_output + .bip32_derivation + .append(&mut derived_descriptor.get_hd_keypaths(&self.secp)); + } if params.include_output_redeem_witness_script { psbt_output.witness_script = derived_descriptor.psbt_witness_script(); psbt_output.redeem_script = derived_descriptor.psbt_redeem_script(); @@ -1496,17 +1533,35 @@ where let desc = self.get_descriptor_for_keychain(keychain); let derived_descriptor = desc.as_derived(child, &self.secp); - psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp)?; + + if let miniscript::Descriptor::Tr(tr) = &derived_descriptor { + psbt_input.tap_key_origins = derived_descriptor.get_tap_key_origins(&self.secp); + psbt_input.tap_internal_key = Some(tr.internal_key().to_x_only_pubkey()); + + let spend_info = tr.spend_info(); + psbt_input.tap_merkle_root = spend_info.merkle_root(); + psbt_input.tap_scripts = spend_info + .as_script_map() + .keys() + .filter_map(|script_ver| { + spend_info + .control_block(script_ver) + .map(|cb| (cb, script_ver.clone())) + }) + .collect(); + } else { + psbt_input.bip32_derivation = derived_descriptor.get_hd_keypaths(&self.secp); + } psbt_input.redeem_script = derived_descriptor.psbt_redeem_script(); psbt_input.witness_script = derived_descriptor.psbt_witness_script(); let prev_output = utxo.outpoint; if let Some(prev_tx) = self.database.borrow().get_raw_tx(&prev_output.txid)? { - if desc.is_witness() { + if desc.is_witness() || desc.is_taproot() { psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); } - if !desc.is_witness() || !only_witness_utxo { + if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) { psbt_input.non_witness_utxo = Some(prev_tx); } } @@ -1532,12 +1587,19 @@ where { debug!("Found descriptor {:?}/{}", keychain, child); - // merge hd_keypaths + // merge hd_keypaths or tap_key_origins let desc = self.get_descriptor_for_keychain(keychain); - let mut hd_keypaths = desc - .as_derived(child, &self.secp) - .get_hd_keypaths(&self.secp)?; - psbt_input.bip32_derivation.append(&mut hd_keypaths); + if desc.is_taproot() { + let mut tap_key_origins = desc + .as_derived(child, &self.secp) + .get_tap_key_origins(&self.secp); + psbt_input.tap_key_origins.append(&mut tap_key_origins); + } else { + let mut hd_keypaths = desc + .as_derived(child, &self.secp) + .get_hd_keypaths(&self.secp); + psbt_input.bip32_derivation.append(&mut hd_keypaths); + } } } } @@ -1792,6 +1854,26 @@ pub(crate) mod test { "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))" } + pub(crate) fn get_test_tr_single_sig() -> &'static str { + "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)" + } + + pub(crate) fn get_test_tr_with_taptree() -> &'static str { + "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" + } + + pub(crate) fn get_test_tr_repeated_key() -> &'static str { + "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})" + } + + pub(crate) fn get_test_tr_single_sig_xprv() -> &'static str { + "tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)" + } + + pub(crate) fn get_test_tr_with_taptree_xprv() -> &'static str { + "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" + } + macro_rules! assert_fee_rate { ($tx:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({ let mut tx = $tx.clone(); @@ -1821,6 +1903,17 @@ pub(crate) mod test { }); } + macro_rules! from_str { + ($e:expr, $t:ty) => {{ + use std::str::FromStr; + <$t>::from_str($e).unwrap() + }}; + + ($e:expr) => { + from_str!($e, _) + }; + } + #[test] #[should_panic(expected = "NoRecipients")] fn test_create_tx_empty_recipients() { @@ -4097,4 +4190,385 @@ pub(crate) mod test { "when there's no internal descriptor it should just use external" ); } + + #[test] + fn test_taproot_psbt_populate_tap_key_origins() { + let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); + let addr = wallet.get_address(AddressIndex::New).unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(), + vec![( + from_str!("b96d3a3dc76a4fc74e976511b23aecb78e0754c23c0ed7a6513e18cbbc7178e9"), + (vec![], (from_str!("f6a5cb8b"), from_str!("m/0"))) + )], + "Wrong input tap_key_origins" + ); + assert_eq!( + psbt.outputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(), + vec![( + from_str!("e9b03068cf4a2621d4f81e68f6c4216e6bd260fe6edf6acc55c8d8ae5aeff0a8"), + (vec![], (from_str!("f6a5cb8b"), from_str!("m/1"))) + )], + "Wrong output tap_key_origins" + ); + } + + #[test] + fn test_taproot_psbt_populate_tap_key_origins_repeated_key() { + let (wallet, _, _) = get_funded_wallet(get_test_tr_repeated_key()); + let addr = wallet.get_address(AddressIndex::New).unwrap(); + + let path = vec![("rn4nre9c".to_string(), vec![0])] + .into_iter() + .collect(); + + let mut builder = wallet.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 25_000) + .policy_path(path, KeychainKind::External); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(), + vec![( + from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"), + ( + vec![ + from_str!( + "858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e" + ), + from_str!( + "f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903" + ) + ], + (Default::default(), Default::default()) + ) + )], + "Wrong input tap_key_origins" + ); + assert_eq!( + psbt.outputs[0] + .tap_key_origins + .clone() + .into_iter() + .collect::>(), + vec![( + from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"), + ( + vec![ + from_str!( + "858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e" + ), + from_str!( + "f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903" + ) + ], + (Default::default(), Default::default()) + ) + )], + "Wrong output tap_key_origins" + ); + } + + #[test] + fn test_taproot_psbt_input_tap_tree() { + use crate::bitcoin::psbt::serialize::Deserialize; + use crate::bitcoin::psbt::TapTree; + use bitcoin::hashes::hex::FromHex; + use bitcoin::util::taproot; + + let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree()); + let addr = wallet.get_address(AddressIndex::Peek(0)).unwrap(); + + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (psbt, _) = builder.finish().unwrap(); + + assert_eq!( + psbt.inputs[0].tap_merkle_root, + Some( + FromHex::from_hex( + "61f81509635053e52d9d1217545916167394490da2287aca4693606e43851986" + ) + .unwrap() + ), + ); + assert_eq!( + psbt.inputs[0].tap_scripts.clone().into_iter().collect::>(), + vec![ + (taproot::ControlBlock::from_slice(&Vec::::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b7ef769a745e625ed4b9a4982a4dc08274c59187e73e6f07171108f455081cb2").unwrap()).unwrap(), (from_str!("208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac"), taproot::LeafVersion::TapScript)), + (taproot::ControlBlock::from_slice(&Vec::::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b9a515f7be31a70186e3c5937ee4a70cc4b4e1efe876c1d38e408222ffc64834").unwrap()).unwrap(), (from_str!("2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac"), taproot::LeafVersion::TapScript)), + ], + ); + assert_eq!( + psbt.inputs[0].tap_internal_key, + Some(from_str!( + "b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55" + )) + ); + + // Since we are creating an output to the same address as the input, assert that the + // internal_key is the same + assert_eq!( + psbt.inputs[0].tap_internal_key, + psbt.outputs[0].tap_internal_key + ); + + assert_eq!( + psbt.outputs[0].tap_tree, + Some(TapTree::deserialize(&Vec::::from_hex("01c022208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac01c0222051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac",).unwrap()).unwrap()) + ); + } + + #[test] + fn test_taproot_sign_missing_witness_utxo() { + let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig()); + let addr = wallet.get_address(New).unwrap(); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + let witness_utxo = psbt.inputs[0].witness_utxo.take(); + + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + assert!( + result.is_err(), + "Signing should have failed because the witness_utxo is missing" + ); + assert!( + matches!( + result.unwrap_err(), + Error::Signer(SignerError::MissingWitnessUtxo) + ), + "Signing failed with the wrong error type" + ); + + // restore the witness_utxo + psbt.inputs[0].witness_utxo = witness_utxo; + + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + + assert!(result.is_ok(), "Signing should have worked"); + assert!( + result.unwrap(), + "Should finalize the input since we can produce signatures" + ); + } + + #[test] + fn test_taproot_foreign_utxo() { + let (wallet1, _, _) = get_funded_wallet(get_test_wpkh()); + let (wallet2, _, _) = get_funded_wallet(get_test_tr_single_sig()); + + let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); + let utxo = wallet2.list_unspent().unwrap().remove(0); + let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); + let foreign_utxo_satisfaction = wallet2 + .get_descriptor_for_keychain(KeychainKind::External) + .max_satisfaction_weight() + .unwrap(); + + assert!( + psbt_input.non_witness_utxo.is_none(), + "`non_witness_utxo` should never be populated for taproot" + ); + + let mut builder = wallet1.build_tx(); + builder + .add_recipient(addr.script_pubkey(), 60_000) + .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) + .unwrap(); + let (psbt, details) = builder.finish().unwrap(); + + assert_eq!( + details.sent - details.received, + 10_000 + details.fee.unwrap_or(0), + "we should have only net spent ~10_000" + ); + + assert!( + psbt.unsigned_tx + .input + .iter() + .any(|input| input.previous_output == utxo.outpoint), + "foreign_utxo should be in there" + ); + } + + fn test_spend_from_wallet(wallet: Wallet) { + let addr = wallet.get_address(AddressIndex::New).unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (mut psbt, _) = builder.finish().unwrap(); + + assert!( + wallet.sign(&mut psbt, Default::default()).unwrap(), + "Unable to finalize tx" + ); + } + + #[test] + fn test_taproot_key_spend() { + let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig()); + test_spend_from_wallet(wallet); + + let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); + test_spend_from_wallet(wallet); + } + + #[test] + fn test_taproot_script_spend() { + let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree()); + test_spend_from_wallet(wallet); + + let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_xprv()); + test_spend_from_wallet(wallet); + } + + #[test] + fn test_taproot_sign_derive_index_from_psbt() { + let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); + + let addr = wallet.get_address(AddressIndex::New).unwrap(); + + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), 25_000); + let (mut psbt, _) = builder.finish().unwrap(); + + // re-create the wallet with an empty db + let wallet_empty = Wallet::new( + get_test_tr_single_sig_xprv(), + None, + Network::Regtest, + AnyDatabase::Memory(MemoryDatabase::new()), + ) + .unwrap(); + + // signing with an empty db means that we will only look at the psbt to infer the + // derivation index + assert!( + wallet_empty.sign(&mut psbt, Default::default()).unwrap(), + "Unable to finalize tx" + ); + } + + #[test] + fn test_taproot_sign_explicit_sighash_all() { + let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig()); + let addr = wallet.get_address(New).unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .sighash(SchnorrSighashType::All.into()) + .drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let result = wallet.sign(&mut psbt, Default::default()); + assert!( + result.is_ok(), + "Signing should work because SIGHASH_ALL is safe" + ) + } + + #[test] + fn test_taproot_sign_non_default_sighash() { + let sighash = SchnorrSighashType::NonePlusAnyoneCanPay; + + let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig()); + let addr = wallet.get_address(New).unwrap(); + let mut builder = wallet.build_tx(); + builder + .drain_to(addr.script_pubkey()) + .sighash(sighash.into()) + .drain_wallet(); + let (mut psbt, _) = builder.finish().unwrap(); + + let witness_utxo = psbt.inputs[0].witness_utxo.take(); + + let result = wallet.sign(&mut psbt, Default::default()); + assert!( + result.is_err(), + "Signing should have failed because the TX uses non-standard sighashes" + ); + assert!( + matches!( + result.unwrap_err(), + Error::Signer(SignerError::NonStandardSighash) + ), + "Signing failed with the wrong error type" + ); + + // try again after opting-in + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + assert!( + result.is_err(), + "Signing should have failed because the witness_utxo is missing" + ); + assert!( + matches!( + result.unwrap_err(), + Error::Signer(SignerError::MissingWitnessUtxo) + ), + "Signing failed with the wrong error type" + ); + + // restore the witness_utxo + psbt.inputs[0].witness_utxo = witness_utxo; + + let result = wallet.sign( + &mut psbt, + SignOptions { + allow_all_sighashes: true, + ..Default::default() + }, + ); + + assert!(result.is_ok(), "Signing should have worked"); + assert!( + result.unwrap(), + "Should finalize the input since we can produce signatures" + ); + + let extracted = psbt.extract_tx(); + assert_eq!( + *extracted.input[0].witness.to_vec()[0].last().unwrap(), + sighash as u8, + "The signature should have been made with the right sighash" + ); + } } diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index ebb9217b3..96e3034e0 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -26,7 +26,7 @@ //! # #[derive(Debug)] //! # struct CustomHSM; //! # impl CustomHSM { -//! # fn sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> { +//! # fn hsm_sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> { //! # Ok(()) //! # } //! # fn connect() -> Self { @@ -47,26 +47,23 @@ //! } //! } //! -//! impl Signer for CustomSigner { -//! fn sign( +//! impl SignerCommon for CustomSigner { +//! fn id(&self, _secp: &Secp256k1) -> SignerId { +//! self.device.get_id() +//! } +//! } +//! +//! impl InputSigner for CustomSigner { +//! fn sign_input( //! &self, //! psbt: &mut psbt::PartiallySignedTransaction, -//! input_index: Option, +//! input_index: usize, //! _secp: &Secp256k1, //! ) -> Result<(), SignerError> { -//! let input_index = input_index.ok_or(SignerError::InputIndexOutOfRange)?; -//! self.device.sign_input(psbt, input_index)?; +//! self.device.hsm_sign_input(psbt, input_index)?; //! //! Ok(()) //! } -//! -//! fn id(&self, _secp: &Secp256k1) -> SignerId { -//! self.device.get_id() -//! } -//! -//! fn sign_whole_tx(&self) -> bool { -//! false -//! } //! } //! //! let custom_signer = CustomSigner::connect(); @@ -85,23 +82,26 @@ use std::cmp::Ordering; use std::collections::BTreeMap; use std::fmt; -use std::ops::Bound::Included; +use std::ops::{Bound::Included, Deref}; use std::sync::Arc; use bitcoin::blockdata::opcodes; use bitcoin::blockdata::script::Builder as ScriptBuilder; use bitcoin::hashes::{hash160, Hash}; -use bitcoin::secp256k1; -use bitcoin::secp256k1::{Message, Secp256k1}; +use bitcoin::secp256k1::Message; use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint}; -use bitcoin::util::{ecdsa, psbt, sighash}; -use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, Script, Sighash}; +use bitcoin::util::{ecdsa, psbt, schnorr, sighash, taproot}; +use bitcoin::{secp256k1, XOnlyPublicKey}; +use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script}; -use miniscript::descriptor::{DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey, KeyMap}; -use miniscript::{Legacy, MiniscriptKey, Segwitv0}; +use miniscript::descriptor::{ + Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey, + KeyMap, SinglePubKey, +}; +use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap}; use super::utils::SecpCtx; -use crate::descriptor::XKeyUtils; +use crate::descriptor::{DescriptorMeta, XKeyUtils}; /// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among /// multiple of them @@ -174,27 +174,46 @@ impl fmt::Display for SignerError { impl std::error::Error for SignerError {} -/// Trait for signers +/// Signing context /// -/// This trait can be implemented to provide customized signers to the wallet. For an example see -/// [`this module`](crate::wallet::signer)'s documentation. -pub trait Signer: fmt::Debug + Send + Sync { - /// Sign a PSBT - /// - /// The `input_index` argument is only provided if the wallet doesn't declare to sign the whole - /// transaction in one go (see [`Signer::sign_whole_tx`]). Otherwise its value is `None` and - /// can be ignored. - fn sign( - &self, - psbt: &mut psbt::PartiallySignedTransaction, - input_index: Option, - secp: &SecpCtx, - ) -> Result<(), SignerError>; +/// Used by our software signers to determine the type of signatures to make +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignerContext { + /// Legacy context + Legacy, + /// Segwit v0 context (BIP 143) + Segwitv0, + /// Taproot context (BIP 340) + Tap { + /// Whether the signer can sign for the internal key or not + is_internal_key: bool, + }, +} + +/// Wrapper structure to pair a signer with its context +#[derive(Debug, Clone)] +pub struct SignerWrapper { + signer: S, + ctx: SignerContext, +} + +impl SignerWrapper { + /// Create a wrapped signer from a signer and a context + pub fn new(signer: S, ctx: SignerContext) -> Self { + SignerWrapper { signer, ctx } + } +} + +impl Deref for SignerWrapper { + type Target = S; - /// Return whether or not the signer signs the whole transaction in one go instead of every - /// input individually - fn sign_whole_tx(&self) -> bool; + fn deref(&self) -> &Self::Target { + &self.signer + } +} +/// Common signer methods +pub trait SignerCommon: fmt::Debug + Send + Sync { /// Return the [`SignerId`] for this signer /// /// The [`SignerId`] can be used to lookup a signer in the [`Wallet`](crate::Wallet)'s signers map or to @@ -211,14 +230,65 @@ pub trait Signer: fmt::Debug + Send + Sync { } } -impl Signer for DescriptorXKey { - fn sign( +/// PSBT Input signer +/// +/// This trait can be implemented to provide custom signers to the wallet. If the signer supports signing +/// individual inputs, this trait should be implemented and BDK will provide automatically an implementation +/// for [`TransactionSigner`]. +pub trait InputSigner: SignerCommon { + /// Sign a single psbt input + fn sign_input( + &self, + psbt: &mut psbt::PartiallySignedTransaction, + input_index: usize, + secp: &SecpCtx, + ) -> Result<(), SignerError>; +} + +/// PSBT signer +/// +/// This trait can be implemented when the signer can't sign inputs individually, but signs the whole transaction +/// at once. +pub trait TransactionSigner: SignerCommon { + /// Sign all the inputs of the psbt + fn sign_transaction( + &self, + psbt: &mut psbt::PartiallySignedTransaction, + secp: &SecpCtx, + ) -> Result<(), SignerError>; +} + +impl TransactionSigner for T { + fn sign_transaction( &self, psbt: &mut psbt::PartiallySignedTransaction, - input_index: Option, secp: &SecpCtx, ) -> Result<(), SignerError> { - let input_index = input_index.unwrap(); + for input_index in 0..psbt.inputs.len() { + self.sign_input(psbt, input_index, secp)?; + } + + Ok(()) + } +} + +impl SignerCommon for SignerWrapper> { + fn id(&self, secp: &SecpCtx) -> SignerId { + SignerId::from(self.root_fingerprint(secp)) + } + + fn descriptor_secret_key(&self) -> Option { + Some(DescriptorSecretKey::XPrv(self.signer.clone())) + } +} + +impl InputSigner for SignerWrapper> { + fn sign_input( + &self, + psbt: &mut psbt::PartiallySignedTransaction, + input_index: usize, + secp: &SecpCtx, + ) -> Result<(), SignerError> { if input_index >= psbt.inputs.len() { return Err(SignerError::InputIndexOutOfRange); } @@ -229,19 +299,23 @@ impl Signer for DescriptorXKey { return Ok(()); } + let tap_key_origins = psbt.inputs[input_index] + .tap_key_origins + .iter() + .map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource)); let (public_key, full_path) = match psbt.inputs[input_index] .bip32_derivation .iter() - .filter_map(|(pk, &(fingerprint, ref path))| { - if self.matches(&(fingerprint, path.clone()), secp).is_some() { - Some((pk, path)) + .map(|(pk, keysource)| (SinglePubKey::FullKey(PublicKey::new(*pk)), keysource)) + .chain(tap_key_origins) + .find_map(|(pk, keysource)| { + if self.matches(keysource, secp).is_some() { + Some((pk, keysource.1.clone())) } else { None } - }) - .next() - { - Some((pk, full_path)) => (pk, full_path.clone()), + }) { + Some((pk, full_path)) => (pk, full_path), None => return Ok(()), }; @@ -256,40 +330,47 @@ impl Signer for DescriptorXKey { None => self.xkey.derive_priv(secp, &full_path).unwrap(), }; - if &secp256k1::PublicKey::from_secret_key(secp, &derived_key.private_key) != public_key { + let computed_pk = secp256k1::PublicKey::from_secret_key(secp, &derived_key.private_key); + let valid_key = match public_key { + SinglePubKey::FullKey(pk) if pk.inner == computed_pk => true, + SinglePubKey::XOnly(x_only) if XOnlyPublicKey::from(computed_pk) == x_only => true, + _ => false, + }; + if !valid_key { Err(SignerError::InvalidKey) } else { // HD wallets imply compressed keys - PrivateKey { + let priv_key = PrivateKey { compressed: true, network: self.xkey.network, inner: derived_key.private_key, - } - .sign(psbt, Some(input_index), secp) - } - } + }; - fn sign_whole_tx(&self) -> bool { - false + SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, secp) + } } +} +impl SignerCommon for SignerWrapper { fn id(&self, secp: &SecpCtx) -> SignerId { - SignerId::from(self.root_fingerprint(secp)) + SignerId::from(self.public_key(secp).to_pubkeyhash()) } fn descriptor_secret_key(&self) -> Option { - Some(DescriptorSecretKey::XPrv(self.clone())) + Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv { + key: self.signer, + origin: None, + })) } } -impl Signer for PrivateKey { - fn sign( +impl InputSigner for SignerWrapper { + fn sign_input( &self, psbt: &mut psbt::PartiallySignedTransaction, - input_index: Option, + input_index: usize, secp: &SecpCtx, ) -> Result<(), SignerError> { - let input_index = input_index.unwrap(); if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { return Err(SignerError::InputIndexOutOfRange); } @@ -301,48 +382,123 @@ impl Signer for PrivateKey { } let pubkey = PublicKey::from_private_key(secp, self); + let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner); + + if let SignerContext::Tap { is_internal_key } = self.ctx { + if is_internal_key && psbt.inputs[input_index].tap_key_sig.is_none() { + let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?; + sign_psbt_schnorr( + &self.inner, + x_only_pubkey, + None, + &mut psbt.inputs[input_index], + hash, + hash_ty, + secp, + ); + } + + if let Some((leaf_hashes, _)) = + psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey) + { + let leaf_hashes = leaf_hashes + .iter() + .filter(|lh| { + !psbt.inputs[input_index] + .tap_script_sigs + .contains_key(&(x_only_pubkey, **lh)) + }) + .cloned() + .collect::>(); + for lh in leaf_hashes { + let (hash, hash_ty) = Tap::sighash(psbt, input_index, Some(lh))?; + sign_psbt_schnorr( + &self.inner, + x_only_pubkey, + Some(lh), + &mut psbt.inputs[input_index], + hash, + hash_ty, + secp, + ); + } + } + + return Ok(()); + } + if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) { return Ok(()); } - // FIXME: use the presence of `witness_utxo` as an indication that we should make a bip143 - // sig. Does this make sense? Should we add an extra argument to explicitly switch between - // these? The original idea was to declare sign() as sign() and use Ctx, - // but that violates the rules for trait-objects, so we can't do it. - let (hash, sighash) = match psbt.inputs[input_index].witness_utxo { - Some(_) => Segwitv0::sighash(psbt, input_index)?, - None => Legacy::sighash(psbt, input_index)?, + let (hash, hash_ty) = match self.ctx { + SignerContext::Segwitv0 => Segwitv0::sighash(psbt, input_index, ())?, + SignerContext::Legacy => Legacy::sighash(psbt, input_index, ())?, + _ => return Ok(()), // handled above }; - - let sig = secp.sign_ecdsa( - &Message::from_slice(&hash.into_inner()[..]).unwrap(), + sign_psbt_ecdsa( &self.inner, + pubkey, + &mut psbt.inputs[input_index], + hash, + hash_ty, + secp, ); - let final_signature = ecdsa::EcdsaSig { - sig, - hash_ty: sighash.ecdsa_hash_ty().unwrap(), // FIXME - }; - psbt.inputs[input_index] - .partial_sigs - .insert(pubkey, final_signature); - Ok(()) } +} - fn sign_whole_tx(&self) -> bool { - false - } - - fn id(&self, secp: &SecpCtx) -> SignerId { - SignerId::from(self.public_key(secp).to_pubkeyhash()) - } +fn sign_psbt_ecdsa( + secret_key: &secp256k1::SecretKey, + pubkey: PublicKey, + psbt_input: &mut psbt::Input, + hash: bitcoin::Sighash, + hash_ty: EcdsaSighashType, + secp: &SecpCtx, +) { + let sig = secp.sign_ecdsa( + &Message::from_slice(&hash.into_inner()[..]).unwrap(), + secret_key, + ); + + let final_signature = ecdsa::EcdsaSig { sig, hash_ty }; + psbt_input.partial_sigs.insert(pubkey, final_signature); +} - fn descriptor_secret_key(&self) -> Option { - Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv { - key: *self, - origin: None, - })) +// Calling this with `leaf_hash` = `None` will sign for key-spend +fn sign_psbt_schnorr( + secret_key: &secp256k1::SecretKey, + pubkey: XOnlyPublicKey, + leaf_hash: Option, + psbt_input: &mut psbt::Input, + hash: taproot::TapSighashHash, + hash_ty: SchnorrSighashType, + secp: &SecpCtx, +) { + use schnorr::TapTweak; + + let keypair = secp256k1::KeyPair::from_seckey_slice(secp, secret_key.as_ref()).unwrap(); + let keypair = match leaf_hash { + None => keypair + .tap_tweak(secp, psbt_input.tap_merkle_root) + .into_inner(), + Some(_) => keypair, // no tweak for script spend + }; + + let sig = secp.sign_schnorr( + &Message::from_slice(&hash.into_inner()[..]).unwrap(), + &keypair, + ); + + let final_signature = schnorr::SchnorrSig { sig, hash_ty }; + + if let Some(lh) = leaf_hash { + psbt_input + .tap_script_sigs + .insert((pubkey, lh), final_signature); + } else { + psbt_input.tap_key_sig = Some(final_signature); } } @@ -377,7 +533,7 @@ impl From<(SignerId, SignerOrdering)> for SignersContainerKey { /// Container for multiple signers #[derive(Debug, Default, Clone)] -pub struct SignersContainer(BTreeMap>); +pub struct SignersContainer(BTreeMap>); impl SignersContainer { /// Create a map of public keys to secret keys @@ -388,24 +544,37 @@ impl SignersContainer { .filter_map(|secret| secret.as_public(secp).ok().map(|public| (public, secret))) .collect() } -} -impl From for SignersContainer { - fn from(keymap: KeyMap) -> SignersContainer { - let secp = Secp256k1::new(); + /// Build a new signer container from a [`KeyMap`] + /// + /// Also looks at the corresponding descriptor to determine the [`SignerContext`] to attach to + /// the signers + pub fn build( + keymap: KeyMap, + descriptor: &Descriptor, + secp: &SecpCtx, + ) -> SignersContainer { let mut container = SignersContainer::new(); - for (_, secret) in keymap { + for (pubkey, secret) in keymap { + let ctx = match descriptor { + Descriptor::Tr(tr) => SignerContext::Tap { + is_internal_key: tr.internal_key() == &pubkey, + }, + _ if descriptor.is_witness() => SignerContext::Segwitv0, + _ => SignerContext::Legacy, + }; + match secret { DescriptorSecretKey::SinglePriv(private_key) => container.add_external( - SignerId::from(private_key.key.public_key(&secp).to_pubkeyhash()), + SignerId::from(private_key.key.public_key(secp).to_pubkeyhash()), SignerOrdering::default(), - Arc::new(private_key.key), + Arc::new(SignerWrapper::new(private_key.key, ctx)), ), DescriptorSecretKey::XPrv(xprv) => container.add_external( - SignerId::from(xprv.root_fingerprint(&secp)), + SignerId::from(xprv.root_fingerprint(secp)), SignerOrdering::default(), - Arc::new(xprv), + Arc::new(SignerWrapper::new(xprv, ctx)), ), }; } @@ -426,13 +595,17 @@ impl SignersContainer { &mut self, id: SignerId, ordering: SignerOrdering, - signer: Arc, - ) -> Option> { + signer: Arc, + ) -> Option> { self.0.insert((id, ordering).into(), signer) } /// Removes a signer from the container and returns it - pub fn remove(&mut self, id: SignerId, ordering: SignerOrdering) -> Option> { + pub fn remove( + &mut self, + id: SignerId, + ordering: SignerOrdering, + ) -> Option> { self.0.remove(&(id, ordering).into()) } @@ -445,12 +618,12 @@ impl SignersContainer { } /// Returns the list of signers in the container, sorted by lowest to highest `ordering` - pub fn signers(&self) -> Vec<&Arc> { + pub fn signers(&self) -> Vec<&Arc> { self.0.values().collect() } /// Finds the signer with lowest ordering for a given id in the container. - pub fn find(&self, id: SignerId) -> Option<&Arc> { + pub fn find(&self, id: SignerId) -> Option<&Arc> { self.0 .range(( Included(&(id.clone(), SignerOrdering(0)).into()), @@ -508,17 +681,27 @@ impl Default for SignOptions { } pub(crate) trait ComputeSighash { + type Extra; + type Sighash; + type SighashType; + fn sighash( psbt: &psbt::PartiallySignedTransaction, input_index: usize, - ) -> Result<(Sighash, psbt::PsbtSighashType), SignerError>; + extra: Self::Extra, + ) -> Result<(Self::Sighash, Self::SighashType), SignerError>; } impl ComputeSighash for Legacy { + type Extra = (); + type Sighash = bitcoin::Sighash; + type SighashType = EcdsaSighashType; + fn sighash( psbt: &psbt::PartiallySignedTransaction, input_index: usize, - ) -> Result<(Sighash, psbt::PsbtSighashType), SignerError> { + _extra: (), + ) -> Result<(Self::Sighash, Self::SighashType), SignerError> { if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { return Err(SignerError::InputIndexOutOfRange); } @@ -528,7 +711,9 @@ impl ComputeSighash for Legacy { let sighash = psbt_input .sighash_type - .unwrap_or_else(|| EcdsaSighashType::All.into()); + .unwrap_or_else(|| EcdsaSighashType::All.into()) + .ecdsa_hash_ty() + .map_err(|_| SignerError::InvalidSighash)?; let script = match psbt_input.redeem_script { Some(ref redeem_script) => redeem_script.clone(), None => { @@ -567,10 +752,15 @@ fn p2wpkh_script_code(script: &Script) -> Script { } impl ComputeSighash for Segwitv0 { + type Extra = (); + type Sighash = bitcoin::Sighash; + type SighashType = EcdsaSighashType; + fn sighash( psbt: &psbt::PartiallySignedTransaction, input_index: usize, - ) -> Result<(Sighash, psbt::PsbtSighashType), SignerError> { + _extra: (), + ) -> Result<(Self::Sighash, Self::SighashType), SignerError> { if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { return Err(SignerError::InputIndexOutOfRange); } @@ -631,7 +821,62 @@ impl ComputeSighash for Segwitv0 { value, sighash, )?, - sighash.into(), + sighash, + )) + } +} + +impl ComputeSighash for Tap { + type Extra = Option; + type Sighash = taproot::TapSighashHash; + type SighashType = SchnorrSighashType; + + fn sighash( + psbt: &psbt::PartiallySignedTransaction, + input_index: usize, + extra: Self::Extra, + ) -> Result<(Self::Sighash, SchnorrSighashType), SignerError> { + if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() { + return Err(SignerError::InputIndexOutOfRange); + } + + let psbt_input = &psbt.inputs[input_index]; + + let sighash_type = psbt_input + .sighash_type + .unwrap_or_else(|| SchnorrSighashType::Default.into()) + .schnorr_hash_ty() + .map_err(|_| SignerError::InvalidSighash)?; + let witness_utxos = psbt + .inputs + .iter() + .cloned() + .map(|i| i.witness_utxo) + .collect::>(); + let mut all_witness_utxos = vec![]; + + let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx); + let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0; + let prevouts = if is_anyone_can_pay { + sighash::Prevouts::One( + input_index, + witness_utxos[input_index] + .as_ref() + .ok_or(SignerError::MissingWitnessUtxo)?, + ) + } else if witness_utxos.iter().all(Option::is_some) { + all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref())); + sighash::Prevouts::All(&all_witness_utxos) + } else { + return Err(SignerError::MissingWitnessUtxo); + }; + + // Assume no OP_CODESEPARATOR + let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF)); + + Ok(( + cache.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)?, + sighash_type, )) } } @@ -666,12 +911,11 @@ mod signers_container_tests { use crate::keys::{DescriptorKey, IntoDescriptorKey}; use bitcoin::secp256k1::{All, Secp256k1}; use bitcoin::util::bip32; - use bitcoin::util::psbt::PartiallySignedTransaction; use bitcoin::Network; use miniscript::ScriptContext; use std::str::FromStr; - fn is_equal(this: &Arc, that: &Arc) -> bool { + fn is_equal(this: &Arc, that: &Arc) -> bool { let secp = Secp256k1::new(); this.id(&secp) == that.id(&secp) } @@ -686,11 +930,11 @@ mod signers_container_tests { let (prvkey1, _, _) = setup_keys(TPRV0_STR); let (prvkey2, _, _) = setup_keys(TPRV1_STR); let desc = descriptor!(sh(multi(2, prvkey1, prvkey2))).unwrap(); - let (_, keymap) = desc + let (wallet_desc, keymap) = desc .into_wallet_descriptor(&secp, Network::Testnet) .unwrap(); - let signers = SignersContainer::from(keymap); + let signers = SignersContainer::build(keymap, &wallet_desc, &secp); assert_eq!(signers.ids().len(), 2); let signers = signers.signers(); @@ -752,22 +996,19 @@ mod signers_container_tests { number: u64, } - impl Signer for DummySigner { - fn sign( - &self, - _psbt: &mut PartiallySignedTransaction, - _input_index: Option, - _secp: &SecpCtx, - ) -> Result<(), SignerError> { - Ok(()) - } - + impl SignerCommon for DummySigner { fn id(&self, _secp: &SecpCtx) -> SignerId { SignerId::Dummy(self.number) } + } - fn sign_whole_tx(&self) -> bool { - true + impl TransactionSigner for DummySigner { + fn sign_transaction( + &self, + _psbt: &mut psbt::PartiallySignedTransaction, + _secp: &SecpCtx, + ) -> Result<(), SignerError> { + Ok(()) } } diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 9866117c3..59dedcf8a 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -334,8 +334,9 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> /// 1. The `psbt_input` does not contain a `witness_utxo` or `non_witness_utxo`. /// 2. The data in `non_witness_utxo` does not match what is in `outpoint`. /// - /// Note unless you set [`only_witness_utxo`] any `psbt_input` you pass to this method must - /// have `non_witness_utxo` set otherwise you will get an error when [`finish`] is called. + /// Note unless you set [`only_witness_utxo`] any non-taproot `psbt_input` you pass to this + /// method must have `non_witness_utxo` set otherwise you will get an error when [`finish`] + /// is called. /// /// [`only_witness_utxo`]: Self::only_witness_utxo /// [`finish`]: Self::finish