]> Untitled Git - bdk/commitdiff
[wallet] Add a flag to fill-in `PSBT_GLOBAL_XPUB`
authorAlekos Filini <alekos.filini@gmail.com>
Mon, 30 Nov 2020 14:13:33 +0000 (15:13 +0100)
committerAlekos Filini <alekos.filini@gmail.com>
Tue, 1 Dec 2020 15:43:38 +0000 (16:43 +0100)
src/cli.rs
src/descriptor/mod.rs
src/error.rs
src/wallet/mod.rs
src/wallet/tx_builder.rs

index 87a5ff682df964bde8ac7516c5cd497ad090b3f9..882f6da72396c637b799a9a768bd441c65fe4479 100644 (file)
@@ -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();
         }
index aa8caf34fb4c6f319d6f06da46656ab5f81e92a6..ccd995691a79d4ff5219ac9ac6615a0c139a7b1e 100644 (file)
@@ -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<K: InnerXKey> XKeyUtils for DescriptorXKey<K> {
 pub(crate) trait DescriptorMeta: Sized {
     fn is_witness(&self) -> bool;
     fn get_hd_keypaths(&self, index: u32, secp: &SecpCtx) -> Result<HDKeyPaths, Error>;
+    fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, Error>;
     fn is_fixed(&self) -> bool;
     fn derive_from_hd_keypaths(&self, hd_keypaths: &HDKeyPaths, secp: &SecpCtx) -> Option<Self>;
     fn derive_from_psbt_input(
@@ -339,6 +340,30 @@ impl DescriptorMeta for Descriptor<DescriptorPublicKey> {
         Ok(answer_pk)
     }
 
+    fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, Error> {
+        let get_key = |key: &DescriptorPublicKey,
+                       keys: &mut Vec<DescriptorXKey<ExtendedPubKey>>|
+         -> Result<DummyKey, Error> {
+            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<DummyKey, Error> {
             match key {
index 925217e9e010f5f896e893543bbc7bcfd49b2fcc..bc35e1f1d217b93e4503ae6d600260d9b387406d 100644 (file)
@@ -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),
 
index 8a88cd432cf28e38e90a8e3f902d5cb62e03538c..20c189669bca0819bf1e877751531927e626c34e 100644 (file)
@@ -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<UTXO>,
         builder: TxBuilder<D, Cs, Ctx>,
     ) -> Result<PSBT, Error> {
+        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))
index 393797e5c0ffcd0b4f3cfd97e5b6ddc7bd01c820..f278acdf558a92bf0f6037a8ceab3f71c569b722 100644 (file)
@@ -89,6 +89,7 @@ pub struct TxBuilder<D: Database, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderC
     pub(crate) version: Option<Version>,
     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<D: Database, Cs: CoinSelectionAlgorithm<D>, 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<D: Database, Cs: CoinSelectionAlgorithm<D>, 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`