From ebfe5db0c34b57f0a46e2cdf5be2a3b2aabd2584 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Mon, 30 Nov 2020 15:13:33 +0100 Subject: [PATCH] [wallet] Add a flag to fill-in `PSBT_GLOBAL_XPUB` --- src/cli.rs | 2 ++ src/descriptor/mod.rs | 27 ++++++++++++++++++++++++++- src/error.rs | 6 ++++++ src/wallet/mod.rs | 33 ++++++++++++++++++++++++++++++++- src/wallet/tx_builder.rs | 33 +++++++++++++++++++++++---------- 5 files changed, 89 insertions(+), 12 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 87a5ff68..882f6da7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -458,6 +458,7 @@ where if sub_matches.is_present("offline_signer") { tx_builder = tx_builder + .add_global_xpubs() .force_non_witness_utxo() .include_output_redeem_witness_script(); } @@ -515,6 +516,7 @@ where if sub_matches.is_present("offline_signer") { tx_builder = tx_builder + .add_global_xpubs() .force_non_witness_utxo() .include_output_redeem_witness_script(); } diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index aa8caf34..ccd99569 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -31,7 +31,7 @@ use std::collections::{BTreeMap, HashMap}; use std::fmt; use bitcoin::secp256k1::Secp256k1; -use bitcoin::util::bip32::{ChildNumber, DerivationPath, Fingerprint}; +use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint}; use bitcoin::util::psbt; use bitcoin::{Network, PublicKey, Script, TxOut}; @@ -232,6 +232,7 @@ impl XKeyUtils for DescriptorXKey { pub(crate) trait DescriptorMeta: Sized { fn is_witness(&self) -> bool; fn get_hd_keypaths(&self, index: u32, secp: &SecpCtx) -> Result; + fn get_extended_keys(&self) -> Result>, Error>; fn is_fixed(&self) -> bool; fn derive_from_hd_keypaths(&self, hd_keypaths: &HDKeyPaths, secp: &SecpCtx) -> Option; fn derive_from_psbt_input( @@ -339,6 +340,30 @@ impl DescriptorMeta for Descriptor { Ok(answer_pk) } + fn get_extended_keys(&self) -> Result>, Error> { + let get_key = |key: &DescriptorPublicKey, + keys: &mut Vec>| + -> Result { + if let DescriptorPublicKey::XPub(xpub) = key { + keys.push(xpub.clone()) + } + + Ok(DummyKey::default()) + }; + + let mut answer_pk = Vec::new(); + let mut answer_pkh = Vec::new(); + + self.translate_pk( + |pk| get_key(pk, &mut answer_pk), + |pkh| get_key(pkh, &mut answer_pkh), + )?; + + answer_pk.append(&mut answer_pkh); + + Ok(answer_pk) + } + fn is_fixed(&self) -> bool { fn check_key(key: &DescriptorPublicKey, flag: &mut bool) -> Result { match key { diff --git a/src/error.rs b/src/error.rs index 925217e9..bc35e1f1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -54,6 +54,12 @@ pub enum Error { FeeTooLow { required: u64, }, + /// In order to use the [`TxBuilder::add_global_xpubs`] option every extended + /// key in the descriptor must either be a master key itself (having depth = 0) or have an + /// explicit origin provided + /// + /// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs + MissingKeyOrigin(String), Key(crate::keys::KeyError), diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 8a88cd43..20c18966 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -35,7 +35,9 @@ use std::sync::Arc; use bitcoin::secp256k1::Secp256k1; use bitcoin::consensus::encode::serialize; +use bitcoin::util::base58; use bitcoin::util::bip32::ChildNumber; +use bitcoin::util::psbt::raw::Key as PSBTKey; use bitcoin::util::psbt::PartiallySignedTransaction as PSBT; use bitcoin::{Address, Network, OutPoint, Script, Transaction, TxOut, Txid}; @@ -63,7 +65,7 @@ use crate::blockchain::{Blockchain, BlockchainMarker, OfflineBlockchain, Progres use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; use crate::descriptor::{ get_checksum, DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, Policy, - ToWalletDescriptor, + ToWalletDescriptor, XKeyUtils, }; use crate::error::Error; use crate::psbt::PSBTUtils; @@ -1157,7 +1159,36 @@ where selected: Vec, builder: TxBuilder, ) -> Result { + use bitcoin::util::psbt::serialize::Serialize; + let mut psbt = PSBT::from_unsigned_tx(tx)?; + + if builder.add_global_xpubs { + let mut all_xpubs = self.descriptor.get_extended_keys()?; + if let Some(change_descriptor) = &self.change_descriptor { + all_xpubs.extend(change_descriptor.get_extended_keys()?); + } + + for xpub in all_xpubs { + let serialized_xpub = base58::from_check(&xpub.xkey.to_string()) + .expect("Internal serialization error"); + let key = PSBTKey { + type_value: 0x01, + key: serialized_xpub, + }; + + let origin = match xpub.origin { + Some(origin) => origin, + None if xpub.xkey.depth == 0 => { + (xpub.root_fingerprint(&self.secp), vec![].into()) + } + _ => return Err(Error::MissingKeyOrigin(xpub.xkey.to_string())), + }; + + psbt.global.unknown.insert(key, origin.serialize()); + } + } + let lookup_output = selected .into_iter() .map(|utxo| (utxo.outpoint, utxo)) diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 393797e5..f278acdf 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -89,6 +89,7 @@ pub struct TxBuilder, Ctx: TxBuilderC pub(crate) version: Option, pub(crate) change_policy: ChangeSpendPolicy, pub(crate) force_non_witness_utxo: bool, + pub(crate) add_global_xpubs: bool, pub(crate) coin_selection: Cs, pub(crate) include_output_redeem_witness_script: bool, @@ -131,6 +132,7 @@ where version: Default::default(), change_policy: Default::default(), force_non_witness_utxo: Default::default(), + add_global_xpubs: Default::default(), coin_selection: Default::default(), include_output_redeem_witness_script: Default::default(), @@ -345,6 +347,25 @@ impl, Ctx: TxBuilderContext> TxBuilde self } + /// Fill-in the [`psbt::Output::redeem_script`](bitcoin::util::psbt::Output::redeem_script) and + /// [`psbt::Output::witness_script`](bitcoin::util::psbt::Output::witness_script) fields. + /// + /// This is useful for signers which always require it, like ColdCard hardware wallets. + pub fn include_output_redeem_witness_script(mut self) -> Self { + self.include_output_redeem_witness_script = true; + self + } + + /// Fill-in the `PSBT_GLOBAL_XPUB` field with the extended keys contained in both the external + /// and internal descriptors + /// + /// This is useful for offline signers that take part to a multisig. Some hardware wallets like + /// BitBox and ColdCard are known to require this. + pub fn add_global_xpubs(mut self) -> Self { + self.add_global_xpubs = true; + self + } + /// Spend all the available inputs. This respects filters like [`unspendable`] and the change policy. pub fn drain_wallet(mut self) -> Self { self.drain_wallet = true; @@ -375,21 +396,13 @@ impl, Ctx: TxBuilderContext> TxBuilde version: self.version, change_policy: self.change_policy, force_non_witness_utxo: self.force_non_witness_utxo, - coin_selection, + add_global_xpubs: self.add_global_xpubs, include_output_redeem_witness_script: self.include_output_redeem_witness_script, + coin_selection, phantom: PhantomData, } } - - /// Fill-in the [`psbt::Output::redeem_script`](bitcoin::util::psbt::Output::redeem_script) and - /// [`psbt::Output::witness_script`](bitcoin::util::psbt::Output::witness_script) fields. - /// - /// This is useful for signers which always require it, like ColdCard hardware wallets. - pub fn include_output_redeem_witness_script(mut self) -> Self { - self.include_output_redeem_witness_script = true; - self - } } // methods supported only by create_tx, and only for `DefaultCoinSelectionAlgorithm`