From 906598ad9254643a204df7711693dcc0a73a0332 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Tue, 26 Apr 2022 16:54:10 +0200 Subject: [PATCH 01/18] Refactor signer traits, add support for taproot signatures --- Cargo.toml | 2 +- src/wallet/mod.rs | 16 +- src/wallet/signer.rs | 420 ++++++++++++++++++++++++++++++------------- 3 files changed, 306 insertions(+), 132 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 14d21c3e..e7e1fd70 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", features = ["use-serde", "base64", "rand"] } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } rand = "^0.7" diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 55ef3ba4..b04921bd 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -50,7 +50,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 +79,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, @@ -457,7 +457,7 @@ where &mut self, keychain: KeychainKind, ordering: SignerOrdering, - signer: Arc, + signer: Arc, ) { let signers = match keychain { KeychainKind::External => Arc::make_mut(&mut self.signers), @@ -1036,13 +1036,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 diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index ebb9217b..b6371035 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,25 +47,22 @@ //! } //! } //! -//! impl Signer for CustomSigner { -//! fn sign( -//! &self, -//! psbt: &mut psbt::PartiallySignedTransaction, -//! input_index: Option, -//! _secp: &Secp256k1, -//! ) -> Result<(), SignerError> { -//! let input_index = input_index.ok_or(SignerError::InputIndexOutOfRange)?; -//! self.device.sign_input(psbt, input_index)?; -//! -//! Ok(()) -//! } -//! +//! impl SignerCommon for CustomSigner { //! fn id(&self, _secp: &Secp256k1) -> SignerId { //! self.device.get_id() //! } +//! } //! -//! fn sign_whole_tx(&self) -> bool { -//! false +//! impl InputSigner for CustomSigner { +//! fn sign_input( +//! &self, +//! psbt: &mut psbt::PartiallySignedTransaction, +//! input_index: usize, +//! _secp: &Secp256k1, +//! ) -> Result<(), SignerError> { +//! self.device.hsm_sign_input(psbt, input_index)?; +//! +//! Ok(()) //! } //! } //! @@ -91,14 +88,14 @@ 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::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::{Legacy, MiniscriptKey, Segwitv0, Tap}; use super::utils::SecpCtx; use crate::descriptor::XKeyUtils; @@ -174,27 +171,8 @@ impl fmt::Display for SignerError { impl std::error::Error for SignerError {} -/// Trait for signers -/// -/// 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>; - - /// Return whether or not the signer signs the whole transaction in one go instead of every - /// input individually - fn sign_whole_tx(&self) -> bool; - +/// 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 +189,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 DescriptorXKey { + fn id(&self, secp: &SecpCtx) -> SignerId { + SignerId::from(self.root_fingerprint(secp)) + } + + fn descriptor_secret_key(&self) -> Option { + Some(DescriptorSecretKey::XPrv(self.clone())) + } +} + +impl InputSigner for DescriptorXKey { + 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); } @@ -265,31 +294,31 @@ impl Signer for DescriptorXKey { network: self.xkey.network, inner: derived_key.private_key, } - .sign(psbt, Some(input_index), secp) + .sign_input(psbt, input_index, secp) } } - - fn sign_whole_tx(&self) -> bool { - false - } - - fn id(&self, secp: &SecpCtx) -> SignerId { - SignerId::from(self.root_fingerprint(secp)) - } - - fn descriptor_secret_key(&self) -> Option { - Some(DescriptorSecretKey::XPrv(self.clone())) - } } -impl Signer for PrivateKey { - fn sign( +impl SignerCommon for PrivateKey { + fn id(&self, secp: &SecpCtx) -> SignerId { + SignerId::from(self.public_key(secp).to_pubkeyhash()) + } + + fn descriptor_secret_key(&self) -> Option { + Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv { + key: *self, + origin: None, + })) + } +} + +impl InputSigner for PrivateKey { + 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 +330,127 @@ impl Signer for PrivateKey { } let pubkey = PublicKey::from_private_key(secp, self); - if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) { - return Ok(()); + let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner); + let is_taproot = psbt.inputs[input_index].tap_internal_key.is_some() + || psbt.inputs[input_index].tap_merkle_root.is_some(); + + match psbt.inputs[input_index].tap_internal_key { + Some(k) if k == x_only_pubkey && 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, + ); + } } - // 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)?, - }; + if !is_taproot { + if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) { + return Ok(()); + } - let sig = secp.sign_ecdsa( - &Message::from_slice(&hash.into_inner()[..]).unwrap(), - &self.inner, - ); - - let final_signature = ecdsa::EcdsaSig { - sig, - hash_ty: sighash.ecdsa_hash_ty().unwrap(), // FIXME - }; - psbt.inputs[input_index] - .partial_sigs - .insert(pubkey, final_signature); + // 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, hash_ty) = match psbt.inputs[input_index].witness_utxo { + Some(_) => Segwitv0::sighash(psbt, input_index, ())?, + None => Legacy::sighash(psbt, input_index, ())?, + }; + sign_psbt_ecdsa( + &self.inner, + pubkey, + &mut psbt.inputs[input_index], + hash, + hash_ty, + secp, + ); + } Ok(()) } +} - fn sign_whole_tx(&self) -> bool { - false - } +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, + ); - fn id(&self, secp: &SecpCtx) -> SignerId { - SignerId::from(self.public_key(secp).to_pubkeyhash()) - } + 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 +485,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 @@ -426,13 +534,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 +557,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 +620,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 +650,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 +691,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 +760,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 +850,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) } @@ -752,22 +935,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(()) } } From 1312184ed7a9c5c7a6f99cf8bb08d43775ba5083 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Thu, 12 May 2022 17:28:41 +0200 Subject: [PATCH 02/18] Attach a context to our software signers This allows the signer to know the signing context precisely without relying on heuristics on the psbt fields. Due to the context being static, we still have to look at the PSBT when producing taproot signatures to determine the set of leaf hashes that the key can sign for. --- src/descriptor/policy.rs | 39 ++++---- src/wallet/mod.rs | 8 +- src/wallet/signer.rs | 197 ++++++++++++++++++++++++--------------- 3 files changed, 150 insertions(+), 94 deletions(-) diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index 98b26573..c4b94a52 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>(()) @@ -1109,7 +1110,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() @@ -1124,7 +1125,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() @@ -1148,7 +1149,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() @@ -1180,7 +1181,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() @@ -1212,7 +1213,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() @@ -1244,7 +1245,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() @@ -1277,7 +1278,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() @@ -1293,7 +1294,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() @@ -1320,7 +1321,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() @@ -1363,7 +1364,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 +1403,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 +1428,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 +1446,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 +1468,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 +1534,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 +1595,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,7 +1683,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); assert!(policy.is_ok()); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index b04921bd..ebbf5d4f 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -197,7 +197,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 +207,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); // } diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index b6371035..cf6c882e 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -82,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::{Message, Secp256k1}; +use bitcoin::secp256k1::Message; use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint}; 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::descriptor::{ + Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey, + KeyMap, +}; 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 @@ -171,6 +174,44 @@ impl fmt::Display for SignerError { impl std::error::Error for SignerError {} +/// Signing context +/// +/// 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; + + fn deref(&self) -> &Self::Target { + &self.signer + } +} + /// Common signer methods pub trait SignerCommon: fmt::Debug + Send + Sync { /// Return the [`SignerId`] for this signer @@ -231,17 +272,17 @@ impl TransactionSigner for T { } } -impl SignerCommon for DescriptorXKey { +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.clone())) + Some(DescriptorSecretKey::XPrv(self.signer.clone())) } } -impl InputSigner for DescriptorXKey { +impl InputSigner for SignerWrapper> { fn sign_input( &self, psbt: &mut psbt::PartiallySignedTransaction, @@ -289,30 +330,31 @@ impl InputSigner for DescriptorXKey { 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_input(psbt, input_index, secp) + }; + + SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, secp) } } } -impl SignerCommon for PrivateKey { +impl SignerCommon for SignerWrapper { fn id(&self, secp: &SecpCtx) -> SignerId { SignerId::from(self.public_key(secp).to_pubkeyhash()) } fn descriptor_secret_key(&self) -> Option { Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv { - key: *self, + key: self.signer, origin: None, })) } } -impl InputSigner for PrivateKey { +impl InputSigner for SignerWrapper { fn sign_input( &self, psbt: &mut psbt::PartiallySignedTransaction, @@ -331,11 +373,9 @@ impl InputSigner for PrivateKey { let pubkey = PublicKey::from_private_key(secp, self); let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner); - let is_taproot = psbt.inputs[input_index].tap_internal_key.is_some() - || psbt.inputs[input_index].tap_merkle_root.is_some(); - match psbt.inputs[input_index].tap_internal_key { - Some(k) if k == x_only_pubkey && psbt.inputs[input_index].tap_key_sig.is_none() => { + 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, @@ -347,56 +387,54 @@ impl InputSigner for PrivateKey { 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, - ); - } - } - if !is_taproot { - if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) { - return Ok(()); + 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, + ); + } } - // 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, hash_ty) = match psbt.inputs[input_index].witness_utxo { - Some(_) => Segwitv0::sighash(psbt, input_index, ())?, - None => Legacy::sighash(psbt, input_index, ())?, - }; - sign_psbt_ecdsa( - &self.inner, - pubkey, - &mut psbt.inputs[input_index], - hash, - hash_ty, - secp, - ); + return Ok(()); } + if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) { + return Ok(()); + } + + 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 + }; + sign_psbt_ecdsa( + &self.inner, + pubkey, + &mut psbt.inputs[input_index], + hash, + hash_ty, + secp, + ); + Ok(()) } } @@ -496,24 +534,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)), ), }; } @@ -869,11 +920,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(); From 8a5a87b0752e0bdaa003ff7c3f1d153a447dbe85 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 27 Apr 2022 16:29:02 +0200 Subject: [PATCH 03/18] Populate `tap_key_origin` in PSBT inputs and outputs --- src/descriptor/mod.rs | 73 +++++++++++++++++++++++++++++++++-- src/wallet/mod.rs | 90 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 149 insertions(+), 14 deletions(-) diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 9f993c0c..62b9a809 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -19,7 +19,7 @@ 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::{Network, Script, TxOut}; use miniscript::descriptor::{DescriptorType, InnerXKey}; @@ -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 { @@ -497,7 +505,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 +523,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/wallet/mod.rs b/src/wallet/mod.rs index ebbf5d4f..98ed8607 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1225,7 +1225,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 { @@ -1436,6 +1436,7 @@ where psbt_input: foreign_psbt_input, outpoint, } => { + // TODO: do not require non_witness_utxo for taproot utxos if !params.only_witness_utxo && foreign_psbt_input.non_witness_utxo.is_none() { return Err(Error::Generic(format!( "Missing non_witness_utxo on foreign utxo {}", @@ -1461,7 +1462,15 @@ 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 desc.is_taproot() { + psbt_output + .tap_key_origins + .append(&mut derived_descriptor.get_tap_key_origins(&self.secp)); + } 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(); @@ -1494,17 +1503,21 @@ 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 desc.is_taproot() { + psbt_input.tap_key_origins = derived_descriptor.get_tap_key_origins(&self.secp); + } 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); } } @@ -1530,12 +1543,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); + } } } } @@ -1790,6 +1810,10 @@ pub(crate) mod test { "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))" } + pub(crate) fn get_test_tr_single_sig() -> &'static str { + "tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)" + } + 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(); @@ -1819,6 +1843,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() { @@ -4095,4 +4130,39 @@ 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()); + 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" + ); + } } From 855382113380612ca4f4c7da0a4a93c7a4f9aab9 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Thu, 28 Apr 2022 15:39:31 +0200 Subject: [PATCH 04/18] Populate more taproot fields in PSBTs --- src/wallet/mod.rs | 201 ++++++++++++++++++++++++++++++++++++++- src/wallet/tx_builder.rs | 5 +- 2 files changed, 199 insertions(+), 7 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 98ed8607..495f9cef 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -24,13 +24,14 @@ 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, }; use miniscript::descriptor::DescriptorTrait; use miniscript::psbt::PsbtInputSatisfier; +use miniscript::ToPublicKey; #[allow(unused_imports)] use log::{debug, error, info, trace}; @@ -1436,8 +1437,15 @@ where psbt_input: foreign_psbt_input, outpoint, } => { - // TODO: do not require non_witness_utxo for taproot utxos - 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 @@ -1462,10 +1470,27 @@ where let (desc, _) = self._get_descriptor_for_keychain(keychain); let derived_descriptor = desc.as_derived(child, &self.secp); - if desc.is_taproot() { + 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 @@ -1503,8 +1528,22 @@ where let desc = self.get_descriptor_for_keychain(keychain); let derived_descriptor = desc.as_derived(child, &self.secp); - if desc.is_taproot() { + + 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); } @@ -1814,6 +1853,14 @@ pub(crate) mod test { "tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)" } + pub(crate) fn get_test_tr_with_taptree() -> &'static str { + "tr(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh,{pk(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55),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))})" + } + 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(); @@ -4165,4 +4212,148 @@ pub(crate) mod test { "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 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( + "d9ca24475ed2c4081ae181d8faa7461649961237bee7bc692f1de448d2d62031" + ) + .unwrap() + ), + ); + assert_eq!( + psbt.inputs[0].tap_scripts.clone().into_iter().collect::>(), + vec![ + (taproot::ControlBlock::from_slice(&Vec::::from_hex("c151494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef3452012958b0afded0ee3149dbf6710d349dc30e55ae30c319c30efbc79efe19cf70f46a8").unwrap()).unwrap(), (from_str!("208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac"), taproot::LeafVersion::TapScript)), + (taproot::ControlBlock::from_slice(&Vec::::from_hex("c151494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295b9a515f7be31a70186e3c5937ee4a70cc4b4e1efe876c1d38e408222ffc64834").unwrap()).unwrap(), (from_str!("20b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55ac"), 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 + ); + } + + #[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" + ); + } } diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 9866117c..59dedcf8 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 From c121dd0252f392ac4bd5e787c9cb570dedcec478 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Fri, 29 Apr 2022 12:59:09 +0200 Subject: [PATCH 05/18] Use `tap_key_origins` in PSBTs to derive descriptors --- src/descriptor/mod.rs | 126 +++++++++++++++++++++++++++++++++--------- src/wallet/mod.rs | 53 +++++++++++++++--- src/wallet/signer.rs | 31 +++++++---- 3 files changed, 166 insertions(+), 44 deletions(-) diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 62b9a809..89dd3f40 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, 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, @@ -322,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, @@ -401,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, @@ -465,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; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 495f9cef..c778d750 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -26,7 +26,8 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::consensus::encode::serialize; 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; @@ -1013,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)); @@ -1854,7 +1859,7 @@ pub(crate) mod test { } pub(crate) fn get_test_tr_with_taptree() -> &'static str { - "tr(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh,{pk(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" + "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})" } pub(crate) fn get_test_tr_repeated_key() -> &'static str { @@ -4218,7 +4223,7 @@ pub(crate) mod test { 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])] + let path = vec![("u6ugnnck".to_string(), vec![0])] .into_iter() .collect(); @@ -4290,7 +4295,7 @@ pub(crate) mod test { psbt.inputs[0].tap_merkle_root, Some( FromHex::from_hex( - "d9ca24475ed2c4081ae181d8faa7461649961237bee7bc692f1de448d2d62031" + "61f81509635053e52d9d1217545916167394490da2287aca4693606e43851986" ) .unwrap() ), @@ -4298,8 +4303,8 @@ pub(crate) mod test { assert_eq!( psbt.inputs[0].tap_scripts.clone().into_iter().collect::>(), vec![ - (taproot::ControlBlock::from_slice(&Vec::::from_hex("c151494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef3452012958b0afded0ee3149dbf6710d349dc30e55ae30c319c30efbc79efe19cf70f46a8").unwrap()).unwrap(), (from_str!("208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac"), taproot::LeafVersion::TapScript)), - (taproot::ControlBlock::from_slice(&Vec::::from_hex("c151494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295b9a515f7be31a70186e3c5937ee4a70cc4b4e1efe876c1d38e408222ffc64834").unwrap()).unwrap(), (from_str!("20b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55ac"), taproot::LeafVersion::TapScript)), + (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!( @@ -4356,4 +4361,34 @@ pub(crate) mod test { "foreign_utxo should be in there" ); } + + #[test] + fn test_taproot_key_spend() { + let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig()); + 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 taproot key spend" + ); + } + + #[test] + fn test_taproot_script_spend() { + let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree()); + 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 taproot script spend" + ); + } } diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index cf6c882e..19041ba4 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -96,7 +96,7 @@ use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Scrip use miniscript::descriptor::{ Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey, - KeyMap, + KeyMap, SinglePubKey, }; use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap}; @@ -299,19 +299,24 @@ impl InputSigner for SignerWrapper> { return Ok(()); } + let tap_key_origins = psbt.inputs[input_index] + .tap_key_origins + .iter() + .map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource)) + .collect::>(); 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(()), }; @@ -326,7 +331,13 @@ impl InputSigner for SignerWrapper> { 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 @@ -488,7 +499,7 @@ fn sign_psbt_schnorr( .tap_script_sigs .insert((pubkey, lh), final_signature); } else { - psbt_input.tap_key_sig = Some(final_signature) + psbt_input.tap_key_sig = Some(final_signature); } } From cdc7057813a4a2acba5224c966da2c3473efa669 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Sat, 13 Nov 2021 15:39:34 +0100 Subject: [PATCH 06/18] Add `tr()` descriptors to the `descriptor!()` macro --- src/descriptor/dsl.rs | 97 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/descriptor/dsl.rs b/src/descriptor/dsl.rs index caee94bd..4296b4d9 100644 --- a/src/descriptor/dsl.rs +++ b/src/descriptor/dsl.rs @@ -73,6 +73,38 @@ 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, Tr}; + use $crate::miniscript::Tap; + + #[allow(unused_imports)] + use $crate::keys::{DescriptorKey, IntoDescriptorKey}; + 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)| { + 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 +260,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 +529,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)] From fe1877fb185af7a235ceb1a9696d1edd66f957d8 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Fri, 29 Apr 2022 15:38:45 +0200 Subject: [PATCH 07/18] Support `tr()` descriptors in dsl --- src/descriptor/dsl.rs | 42 ++++++++++++++++++++++++++++++------------ src/keys/mod.rs | 9 +++++++-- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/descriptor/dsl.rs b/src/descriptor/dsl.rs index 4296b4d9..34081ab1 100644 --- a/src/descriptor/dsl.rs +++ b/src/descriptor/dsl.rs @@ -577,6 +577,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 { @@ -737,21 +754,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) - }); - ( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({ - use $crate::keys::IntoDescriptorKey; let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); - let keys = vec![ - $( - $key.into_descriptor_key(), - )* - ]; + $crate::keys::make_multi($thresh, $crate::miniscript::Terminal::Multi, $keys, &secp) + }); + ( multi ( $thresh:expr $(, $key:expr )+ ) ) => ({ + $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(); - 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 diff --git a/src/keys/mod.rs b/src/keys/mod.rs index 320cca1f..20ff5818 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()?; From 308708952b598f8f0f1f6cb5f3743e443485cb1e Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Fri, 29 Apr 2022 16:08:03 +0200 Subject: [PATCH 08/18] Fix type inference for the `tr()` descriptor, add basic tests --- src/descriptor/dsl.rs | 55 +++++++++++++++++++++++++++++++++++++------ src/wallet/signer.rs | 3 +-- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/descriptor/dsl.rs b/src/descriptor/dsl.rs index 34081ab1..ccbe2b2b 100644 --- a/src/descriptor/dsl.rs +++ b/src/descriptor/dsl.rs @@ -77,11 +77,14 @@ macro_rules! impl_top_level_pk { #[macro_export] macro_rules! impl_top_level_tr { ( $internal_key:expr, $tap_tree:expr ) => {{ - use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, Tr}; + use $crate::miniscript::descriptor::{ + Descriptor, DescriptorPublicKey, KeyMap, TapTree, Tr, + }; use $crate::miniscript::Tap; #[allow(unused_imports)] - use $crate::keys::{DescriptorKey, IntoDescriptorKey}; + use $crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks}; + let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); $internal_key @@ -89,12 +92,19 @@ macro_rules! impl_top_level_tr { .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)| { - key_map.extend(tree_keymap.into_iter()); - valid_networks = $crate::keys::merge_networks(&valid_networks, &tree_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 - }); + tap_tree + }, + ); Ok(( Descriptor::::Tr(Tr::new(pk, tap_tree)?), @@ -1179,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/wallet/signer.rs b/src/wallet/signer.rs index 19041ba4..96e3034e 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -302,8 +302,7 @@ impl InputSigner for SignerWrapper> { let tap_key_origins = psbt.inputs[input_index] .tap_key_origins .iter() - .map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource)) - .collect::>(); + .map(|(pk, (_, keysource))| (SinglePubKey::XOnly(*pk), keysource)); let (public_key, full_path) = match psbt.inputs[input_index] .bip32_derivation .iter() From ff1abc63e03e5c24d375eb2c4f20302c9187d24d Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Tue, 24 May 2022 10:39:17 +0200 Subject: [PATCH 09/18] policy: Refactor `PkOrF` into an enum For whatever reason we were using a struct as an enum, so we might as well fix it in this PR since we are already breaking the API quite badly. --- CHANGELOG.md | 1 + src/descriptor/policy.rs | 78 ++++++++++++++++------------------------ 2 files changed, 31 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7348f185..1416756c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ 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 ## [v0.18.0] - [v0.17.0] diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index c4b94a52..987cda97 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -67,17 +67,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 { @@ -86,27 +85,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 @@ -260,7 +250,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 @@ -434,7 +424,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, @@ -1116,9 +1106,7 @@ mod test { .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(); @@ -1131,9 +1119,7 @@ mod test { .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) ); @@ -1157,8 +1143,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? @@ -1188,8 +1174,8 @@ mod test { .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!( @@ -1221,8 +1207,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 @@ -1253,8 +1239,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!( @@ -1284,9 +1270,7 @@ mod test { .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(); @@ -1300,9 +1284,7 @@ mod test { .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) ); @@ -1329,8 +1311,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 From 572c3ee70d6d335af0e2e541d2497f8bdcfb63c9 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Tue, 24 May 2022 11:24:48 +0200 Subject: [PATCH 10/18] policy: Build `SatisfiableItem::*Signature` based on the context Also refactor our code to lookup signatures in PSBTs to use the context --- src/descriptor/policy.rs | 187 +++++++++++++++++++++++++-------------- src/wallet/mod.rs | 2 +- 2 files changed, 123 insertions(+), 66 deletions(-) diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index 987cda97..d7fde810 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -572,13 +572,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 { @@ -607,9 +606,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(), @@ -737,13 +734,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 { @@ -754,7 +753,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(), } @@ -794,39 +793,78 @@ fn generic_sig_in_psbt< }) } -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, - }; +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), + ) + } + } - // 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), - } - }, - ) + fn find_signature(psbt: &Psbt, key: &DescriptorPublicKey, secp: &SecpCtx) -> bool { + if T::as_enum().is_taproot() { + generic_sig_in_psbt( + psbt, + key, + secp, + |pk| SinglePubKey::XOnly((*pk).into()), + |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) + } + }, + ) + } else { + 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, + }, + ) + } + } } impl ExtractPolicy for Miniscript { @@ -839,8 +877,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 { @@ -901,15 +941,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) @@ -999,28 +1033,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() { @@ -1036,17 +1084,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::, _>>()?, - ); + // 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); - Ok(Policy::make_thresh(items, 1)?) + 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)?) + } } } } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index c778d750..872d5919 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -4223,7 +4223,7 @@ pub(crate) mod test { let (wallet, _, _) = get_funded_wallet(get_test_tr_repeated_key()); let addr = wallet.get_address(AddressIndex::New).unwrap(); - let path = vec![("u6ugnnck".to_string(), vec![0])] + let path = vec![("rn4nre9c".to_string(), vec![0])] .into_iter() .collect(); From c67116fb55de525a9045527eefcb5cdb6f4d34a9 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Tue, 24 May 2022 15:50:49 +0200 Subject: [PATCH 11/18] policy: Consider `tap_key_origins` when looking for sigs in PSBTs We used to only look at `bip32_derivations` which is only used for ECDSA keys. --- src/descriptor/policy.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index d7fde810..90c84385 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -44,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}; @@ -766,27 +765,25 @@ fn make_generic_signature SatisfiableItem, F: Fn(&Psbt) -> bool>( } 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, } } @@ -838,7 +835,6 @@ impl SigExt for T { psbt, key, secp, - |pk| SinglePubKey::XOnly((*pk).into()), |input, pk| { let pk = match pk { SinglePubKey::XOnly(pk) => pk, @@ -851,17 +847,30 @@ impl SigExt for T { 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, - |pk| SinglePubKey::FullKey(PublicKey::new(*pk)), |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))) + }, ) } } From 461397e590471a8d10590b7d754e206310bceef5 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 18 May 2022 11:36:15 +0200 Subject: [PATCH 12/18] taproot-tests: Test taproot key and script spend in the blockchain tests This is to ensure a Bitcoin node accepts our transactions --- src/testutils/blockchain_tests.rs | 72 +++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs index 556d98f6..87a77630 100644 --- a/src/testutils/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -387,11 +387,25 @@ 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, + } + + 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/*") ) ) + } }; let test_client = TestClient::default(); @@ -405,6 +419,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 +1221,54 @@ 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(); + } } }; From 89cb425e69303fe62fe7332d8528011b7a98b4df Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 18 May 2022 19:55:21 +0200 Subject: [PATCH 13/18] taproot-tests: Add test coverage for tx signing --- src/wallet/mod.rs | 189 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 181 insertions(+), 8 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 872d5919..13c7fa2a 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1855,7 +1855,7 @@ pub(crate) mod test { } pub(crate) fn get_test_tr_single_sig() -> &'static str { - "tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)" + "tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)" } pub(crate) fn get_test_tr_with_taptree() -> &'static str { @@ -1866,6 +1866,14 @@ pub(crate) mod test { "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(); @@ -4185,7 +4193,7 @@ pub(crate) mod test { #[test] fn test_taproot_psbt_populate_tap_key_origins() { - let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig()); + 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(); @@ -4322,6 +4330,52 @@ pub(crate) mod test { ); } + #[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()); @@ -4362,9 +4416,7 @@ pub(crate) mod test { ); } - #[test] - fn test_taproot_key_spend() { - let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig()); + fn test_spend_from_wallet(wallet: Wallet) { let addr = wallet.get_address(AddressIndex::New).unwrap(); let mut builder = wallet.build_tx(); @@ -4373,22 +4425,143 @@ pub(crate) mod test { assert!( wallet.sign(&mut psbt, Default::default()).unwrap(), - "Unable to finalize taproot key spend" + "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.sign(&mut psbt, Default::default()).unwrap(), - "Unable to finalize taproot script spend" + 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" ); } } From 0643f76c1fac5ee25f8a9de4c3a2cd57a3974b5e Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Tue, 24 May 2022 11:49:31 +0200 Subject: [PATCH 14/18] taproot-tests: Add tests for the policy module --- src/descriptor/policy.rs | 176 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index 90c84385..215078b6 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -1736,4 +1736,180 @@ mod test { 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() + } + ); + } } From c1bfaf9b1ebd92ac8952afd0d5867897672acfb4 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 24 May 2022 19:26:41 -0700 Subject: [PATCH 15/18] Add blockchain tests for parsing, signing, finalizing taproot core psbts --- src/testutils/blockchain_tests.rs | 86 ++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/src/testutils/blockchain_tests.rs b/src/testutils/blockchain_tests.rs index 87a77630..297f2677 100644 --- a/src/testutils/blockchain_tests.rs +++ b/src/testutils/blockchain_tests.rs @@ -391,6 +391,8 @@ macro_rules! bdk_blockchain_tests { WpkhSingleSig, TaprootKeySpend, TaprootScriptSpend, + TaprootScriptSpend2, + TaprootScriptSpend3, } fn init_wallet(ty: WalletType) -> (Wallet, $blockchain, (String, Option), TestClient) { @@ -405,7 +407,13 @@ macro_rules! bdk_blockchain_tests { }, 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(); @@ -1269,6 +1277,82 @@ macro_rules! bdk_blockchain_tests { }; 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); + } } }; From 5320c8353e05adecf0d18336d48934567257e64d Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Tue, 31 May 2022 17:47:26 +0200 Subject: [PATCH 16/18] taproot-tests: validate `tap_tree` in psbt outputs Co-authored-by: Daniela Brozzoni --- src/wallet/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 13c7fa2a..6d2ff385 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -4289,6 +4289,8 @@ pub(crate) mod test { #[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; @@ -4328,6 +4330,11 @@ pub(crate) mod test { 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] From ef08fbd3c7f424cb77e231c2daaed2039b28b938 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 1 Jun 2022 14:50:08 +0200 Subject: [PATCH 17/18] Update to the newest release of rust-bitcoin --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e7e1fd70..ddbc4612 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", "rand"] } +bitcoin = { version = "0.28.1", features = ["use-serde", "base64", "rand"] } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } rand = "^0.7" From 20d36c71d470ea5ece98dd218f4dbdb0ba872d11 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 1 Jun 2022 14:55:43 +0200 Subject: [PATCH 18/18] Update CHANGELOG.md for Taproot --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1416756c..18a5ae06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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]