From: Alekos Filini Date: Mon, 21 Sep 2020 13:44:07 +0000 (+0200) Subject: [keys] Add a way to restrict the networks in which keys are valid X-Git-Tag: v0.2.0~123 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/database/enum.DecodeError.html?a=commitdiff_plain;h=c51ba4a99f7f38332e059543215ad8c22b6f1413;p=bdk [keys] Add a way to restrict the networks in which keys are valid Thanks to the `ToWalletDescriptor` trait we can also very easily validate the checksum for descriptors that are loaded from strings, if they contain one. Fixes #20. --- diff --git a/src/descriptor/dsl.rs b/src/descriptor/dsl.rs index 0b9853b3..7fb28007 100644 --- a/src/descriptor/dsl.rs +++ b/src/descriptor/dsl.rs @@ -29,7 +29,7 @@ macro_rules! impl_top_level_sh { ( $descriptor_variant:ident, $( $minisc:tt )* ) => { $crate::fragment!($( $minisc )*) - .map(|(minisc, keymap)|($crate::miniscript::Descriptor::<$crate::miniscript::descriptor::DescriptorPublicKey>::$descriptor_variant(minisc), keymap)) + .map(|(minisc, keymap, networks)|($crate::miniscript::Descriptor::<$crate::miniscript::descriptor::DescriptorPublicKey>::$descriptor_variant(minisc), keymap, networks)) }; } @@ -40,13 +40,14 @@ macro_rules! impl_top_level_pk { use $crate::keys::{DescriptorKey, ToDescriptorKey}; $key.to_descriptor_key() - .and_then(|key: DescriptorKey<$ctx>| key.into_key_and_secret()) - .map(|(pk, key_map)| { + .and_then(|key: DescriptorKey<$ctx>| key.extract()) + .map(|(pk, key_map, valid_networks)| { ( $crate::miniscript::Descriptor::< $crate::miniscript::descriptor::DescriptorPublicKey, >::$descriptor_variant(pk), key_map, + valid_networks, ) }) }}; @@ -57,7 +58,8 @@ macro_rules! impl_top_level_pk { macro_rules! impl_modifier { ( $terminal_variant:ident, $( $inner:tt )* ) => { $crate::fragment!($( $inner )*) - .and_then(|(minisc, keymap)| Ok(($crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(std::sync::Arc::new(minisc)))?, keymap))) + .map_err(|e| -> $crate::Error { e.into() }) + .and_then(|(minisc, keymap, networks)| Ok(($crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(std::sync::Arc::new(minisc)))?, keymap, networks))) }; } @@ -69,7 +71,13 @@ macro_rules! impl_leaf_opcode { $crate::miniscript::miniscript::decode::Terminal::$terminal_variant, ) .map_err($crate::Error::Miniscript) - .map(|minisc| (minisc, $crate::miniscript::descriptor::KeyMap::default())) + .map(|minisc| { + ( + minisc, + $crate::miniscript::descriptor::KeyMap::default(), + $crate::keys::any_network(), + ) + }) }; } @@ -81,7 +89,13 @@ macro_rules! impl_leaf_opcode_value { $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($value), ) .map_err($crate::Error::Miniscript) - .map(|minisc| (minisc, $crate::miniscript::descriptor::KeyMap::default())) + .map(|minisc| { + ( + minisc, + $crate::miniscript::descriptor::KeyMap::default(), + $crate::keys::any_network(), + ) + }) }; } @@ -93,7 +107,13 @@ macro_rules! impl_leaf_opcode_value_two { $crate::miniscript::miniscript::decode::Terminal::$terminal_variant($one, $two), ) .map_err($crate::Error::Miniscript) - .map(|minisc| (minisc, $crate::miniscript::descriptor::KeyMap::default())) + .map(|minisc| { + ( + minisc, + $crate::miniscript::descriptor::KeyMap::default(), + $crate::keys::any_network(), + ) + }) }; } @@ -103,14 +123,14 @@ macro_rules! impl_node_opcode_two { ( $terminal_variant:ident, ( $( $a:tt )* ), ( $( $b:tt )* ) ) => { $crate::fragment!($( $a )*) .and_then(|a| Ok((a, $crate::fragment!($( $b )*)?))) - .and_then(|((a_minisc, mut a_keymap), (b_minisc, b_keymap))| { + .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks))| { // join key_maps a_keymap.extend(b_keymap.into_iter()); Ok(($crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant( std::sync::Arc::new(a_minisc), std::sync::Arc::new(b_minisc), - ))?, a_keymap)) + ))?, a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks))) }) }; } @@ -121,23 +141,26 @@ macro_rules! impl_node_opcode_three { ( $terminal_variant:ident, ( $( $a:tt )* ), ( $( $b:tt )* ), ( $( $c:tt )* ) ) => { $crate::fragment!($( $a )*) .and_then(|a| Ok((a, $crate::fragment!($( $b )*)?, $crate::fragment!($( $c )*)?))) - .and_then(|((a_minisc, mut a_keymap), (b_minisc, b_keymap), (c_minisc, c_keymap))| { + .and_then(|((a_minisc, mut a_keymap, a_networks), (b_minisc, b_keymap, b_networks), (c_minisc, c_keymap, c_networks))| { // join key_maps a_keymap.extend(b_keymap.into_iter()); a_keymap.extend(c_keymap.into_iter()); + let networks = $crate::keys::merge_networks(&a_networks, &b_networks); + let networks = $crate::keys::merge_networks(&networks, &c_networks); + Ok(($crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant( std::sync::Arc::new(a_minisc), std::sync::Arc::new(b_minisc), std::sync::Arc::new(c_minisc), - ))?, a_keymap)) + ))?, a_keymap, networks)) }) }; } /// Macro to write full descriptors with code /// -/// This macro expands to an object of type `Result<(Descriptor, KeyMap), Error>`. +/// This macro expands to an object of type `Result<(Descriptor, KeyMap, ValidNetworks), Error>`. /// /// ## Example /// @@ -147,7 +170,7 @@ macro_rules! impl_node_opcode_three { /// # use std::str::FromStr; /// let my_key = bitcoin::PublicKey::from_str("02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c")?; /// let my_timelock = 50; -/// let (my_descriptor, my_keys_map) = bdk::descriptor!(sh ( wsh ( and_v (+v pk my_key), ( older my_timelock ))))?; +/// let (my_descriptor, my_keys_map, networks) = bdk::descriptor!(sh ( wsh ( and_v (+v pk my_key), ( older my_timelock ))))?; /// # Ok::<(), Box>(()) /// ``` /// @@ -165,7 +188,7 @@ macro_rules! impl_node_opcode_three { /// let my_key_2 = bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; /// let my_timelock = 50; /// -/// let (descriptor_a, key_map_a) = bdk::descriptor! { +/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! { /// wsh ( /// thresh 2, (pk my_key_1), (+s pk my_key_2), (+s+d+v older my_timelock) /// ) @@ -176,7 +199,7 @@ macro_rules! impl_node_opcode_three { /// bdk::fragment!(+s pk my_key_2)?, /// bdk::fragment!(+s+d+v older my_timelock)?, /// ]; -/// let (descriptor_b, mut key_map_b) = bdk::descriptor!( wsh ( thresh_vec 2, b_items ) )?; +/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!( wsh ( thresh_vec 2, b_items ) )?; /// /// assert_eq!(descriptor_a, descriptor_b); /// assert_eq!(key_map_a.len(), key_map_b.len()); @@ -192,7 +215,7 @@ macro_rules! impl_node_opcode_three { /// let my_key_1 = bitcoin::PublicKey::from_str("02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c")?; /// let my_key_2 = bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; /// -/// let (descriptor, key_map) = bdk::descriptor! { +/// let (descriptor, key_map, networks) = bdk::descriptor! { /// wsh ( /// multi 2, my_key_1, my_key_2 /// ) @@ -205,10 +228,9 @@ macro_rules! impl_node_opcode_three { /// Native-Segwit single-sig, equivalent to: `wpkh(...)` /// /// ``` -/// # use std::str::FromStr; /// let my_key = bitcoin::PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy")?; /// -/// let (descriptor, key_map) = bdk::descriptor!(wpkh ( my_key ) )?; +/// let (descriptor, key_map, networks) = bdk::descriptor!(wpkh ( my_key ) )?; /// # Ok::<(), Box>(()) /// ``` #[macro_export] @@ -247,7 +269,7 @@ macro_rules! descriptor { /// Macro to write descriptor fragments with code /// -/// This macro will be expanded to an object of type `Result<(Miniscript, KeyMap), Error>`. It allows writing +/// This macro will be expanded to an object of type `Result<(Miniscript, KeyMap, ValidNetworks), Error>`. It allows writing /// fragments of larger descriptors that can be pieced together using `fragment!(thresh_vec ...)`. #[macro_export] macro_rules! fragment { @@ -291,8 +313,7 @@ macro_rules! fragment { $crate::impl_leaf_opcode!(False) }); ( pk_k $key:expr ) => ({ - use $crate::keys::ToDescriptorKey; - $key.into_miniscript_and_secret() + $crate::keys::make_pk($key) }); ( pk $key:expr ) => ({ $crate::fragment!(+c pk_k $key) @@ -342,15 +363,18 @@ macro_rules! fragment { ( thresh_vec $thresh:expr, $items:expr ) => ({ use $crate::miniscript::descriptor::KeyMap; - let (items, key_maps): (Vec<_>, Vec<_>) = $items.into_iter().unzip(); + let (items, key_maps_networks): (Vec<_>, Vec<_>) = $items.into_iter().map(|(a, b, c)| (a, (b, c))).unzip(); let items = items.into_iter().map(std::sync::Arc::new).collect(); - let key_maps = key_maps.into_iter().fold(KeyMap::default(), |mut acc, map| { - acc.extend(map.into_iter()); - acc + + let (key_maps, valid_networks) = key_maps_networks.into_iter().fold((KeyMap::default(), $crate::keys::any_network()), |(mut keys_acc, net_acc), (key, net)| { + keys_acc.extend(key.into_iter()); + let net_acc = $crate::keys::merge_networks(&net_acc, &net); + + (keys_acc, net_acc) }); $crate::impl_leaf_opcode_value_two!(Thresh, $thresh, items) - .map(|(minisc, _)| (minisc, key_maps)) + .map(|(minisc, _, _)| (minisc, key_maps, valid_networks)) }); ( thresh $thresh:expr $(, ( $( $item:tt )* ) )+ ) => ({ let mut items = vec![]; diff --git a/src/descriptor/error.rs b/src/descriptor/error.rs index 47a2473d..2bd0629f 100644 --- a/src/descriptor/error.rs +++ b/src/descriptor/error.rs @@ -31,7 +31,9 @@ pub enum Error { InvalidPrefix(Vec), HardenedDerivationOnXpub, MalformedInput, + KeyParsingError(String), + Key(crate::keys::KeyError), Policy(crate::descriptor::policy::PolicyError), @@ -50,6 +52,16 @@ pub enum Error { Hex(bitcoin::hashes::hex::Error), } +impl From for Error { + fn from(key_error: crate::keys::KeyError) -> Error { + match key_error { + crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner), + crate::keys::KeyError::BIP32(inner) => Error::BIP32(inner), + e @ _ => Error::Key(e), + } + } +} + impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 10283703..cd76aa38 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -35,7 +35,7 @@ use bitcoin::hashes::hash160; use bitcoin::secp256k1::Secp256k1; use bitcoin::util::bip32::{ChildNumber, DerivationPath, Fingerprint}; use bitcoin::util::psbt; -use bitcoin::{PublicKey, Script, TxOut}; +use bitcoin::{Network, PublicKey, Script, TxOut}; use miniscript::descriptor::{DescriptorPublicKey, DescriptorXKey, InnerXKey, KeyMap}; pub use miniscript::{ @@ -50,6 +50,7 @@ pub mod policy; pub use self::checksum::get_checksum; use self::error::Error; pub use self::policy::Policy; +use crate::keys::{KeyError, ToDescriptorKey, ValidNetworks}; use crate::wallet::signer::SignersContainer; /// Alias for a [`Descriptor`] that can contain extended keys using [`DescriptorPublicKey`] @@ -62,32 +63,124 @@ pub type ExtendedDescriptor = Descriptor; /// [`psbt::Output`]: bitcoin::util::psbt::Output pub type HDKeyPaths = BTreeMap; -/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet +/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`] pub trait ToWalletDescriptor { - fn to_wallet_descriptor(self) -> Result<(ExtendedDescriptor, KeyMap), Error>; + fn to_wallet_descriptor( + self, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), KeyError>; } impl ToWalletDescriptor for &str { - fn to_wallet_descriptor(self) -> Result<(ExtendedDescriptor, KeyMap), Error> { - Ok(ExtendedDescriptor::parse_secret(self)?) + fn to_wallet_descriptor( + self, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), KeyError> { + let descriptor = if self.contains("#") { + let parts: Vec<&str> = self.splitn(2, "#").collect(); + if !get_checksum(parts[0]) + .ok() + .map(|computed| computed == parts[1]) + .unwrap_or(false) + { + return Err(KeyError::InvalidChecksum); + } + + parts[0] + } else { + self + }; + + ExtendedDescriptor::parse_secret(descriptor)?.to_wallet_descriptor(network) } } impl ToWalletDescriptor for &String { - fn to_wallet_descriptor(self) -> Result<(ExtendedDescriptor, KeyMap), Error> { - self.as_str().to_wallet_descriptor() + fn to_wallet_descriptor( + self, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), KeyError> { + self.as_str().to_wallet_descriptor(network) + } +} + +impl ToWalletDescriptor for ExtendedDescriptor { + fn to_wallet_descriptor( + self, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), KeyError> { + (self, KeyMap::default()).to_wallet_descriptor(network) } } impl ToWalletDescriptor for (ExtendedDescriptor, KeyMap) { - fn to_wallet_descriptor(self) -> Result<(ExtendedDescriptor, KeyMap), Error> { - Ok(self) + fn to_wallet_descriptor( + self, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), KeyError> { + use crate::keys::DescriptorKey; + + // check the network for the keys + let translated = self.0.translate_pk( + |pk| { + let (pk, _, networks) = if self.0.is_witness() { + let desciptor_key: DescriptorKey = + pk.clone().to_descriptor_key()?; + desciptor_key.extract()? + } else { + let desciptor_key: DescriptorKey = + pk.clone().to_descriptor_key()?; + desciptor_key.extract()? + }; + + if networks.contains(&network) { + Ok(pk) + } else { + Err(KeyError::InvalidNetwork) + } + }, + |pkh| Ok::<_, KeyError>(*pkh), + )?; + + Ok((translated, self.1)) } } -impl ToWalletDescriptor for ExtendedDescriptor { - fn to_wallet_descriptor(self) -> Result<(ExtendedDescriptor, KeyMap), Error> { - (self, KeyMap::default()).to_wallet_descriptor() +impl ToWalletDescriptor for (ExtendedDescriptor, KeyMap, ValidNetworks) { + fn to_wallet_descriptor( + self, + network: Network, + ) -> Result<(ExtendedDescriptor, KeyMap), KeyError> { + let valid_networks = &self.2; + + // fixup the network for keys that need it + let translated = self.0.translate_pk( + |pk| { + if valid_networks.contains(&network) { + // workaround for xpubs generated by other key types, like bip39: since when the + // conversion is made one network has to be chosen, what we generally choose + // "mainnet", but then override the set of valid networks to specify that all of + // them are valid. here we reset the network to make sure the wallet struct gets a + // descriptor with the right network everywhere. + let pk = match pk { + DescriptorPublicKey::XPub(ref xpub) => { + let mut xpub = xpub.clone(); + xpub.xkey.network = network; + + DescriptorPublicKey::XPub(xpub) + } + other @ _ => other.clone(), + }; + + Ok(pk) + } else { + Err(KeyError::InvalidNetwork) + } + }, + |pkh| Ok::<_, KeyError>(*pkh), + )?; + + Ok((translated, self.1)) } } @@ -355,7 +448,7 @@ mod test { use bitcoin::consensus::encode::deserialize; use bitcoin::hashes::hex::FromHex; - use bitcoin::util::psbt; + use bitcoin::util::{bip32, psbt}; use super::*; use crate::psbt::PSBTUtils; @@ -467,4 +560,25 @@ mod test { .derive_from_psbt_input(&psbt.inputs[0], psbt.get_utxo_for(0)) .is_some()); } + + #[test] + fn test_to_wallet_descriptor_fixup_networks() { + use crate::keys::{any_network, ToDescriptorKey}; + + let xpub = bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap(); + let path = bip32::DerivationPath::from_str("m/0").unwrap(); + + // here `to_descriptor_key` will set the valid networks for the key to only mainnet, since + // we are using an "xpub" + let key = (xpub, path).to_descriptor_key().unwrap(); + // override it with any. this happens in some key conversions, like bip39 + let key = key.override_valid_networks(any_network()); + + // make a descriptor out of it + let desc = crate::descriptor!(wpkh(key)).unwrap(); + // this should conver the key that supports "any_network" to the right network (testnet) + let (wallet_desc, _) = desc.to_wallet_descriptor(Network::Testnet).unwrap(); + + assert_eq!(wallet_desc.to_string(), "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); + } } diff --git a/src/error.rs b/src/error.rs index b773f5f9..40f5820b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -48,6 +48,8 @@ pub enum Error { required: crate::types::FeeRate, }, + Key(crate::keys::KeyError), + ChecksumMismatch, DifferentDescriptorStructure, @@ -114,6 +116,17 @@ impl_error!( ); impl_error!(crate::wallet::signer::SignerError, Signer); +impl From for Error { + fn from(key_error: crate::keys::KeyError) -> Error { + match key_error { + crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner), + crate::keys::KeyError::BIP32(inner) => Error::BIP32(inner), + crate::keys::KeyError::InvalidChecksum => Error::ChecksumMismatch, + e @ _ => Error::Key(e), + } + } +} + impl_error!(bitcoin::consensus::encode::Error, Encode); impl_error!(miniscript::Error, Miniscript); impl_error!(bitcoin::util::bip32::Error, BIP32); diff --git a/src/keys/bip39.rs b/src/keys/bip39.rs index 005d0f99..d6e78767 100644 --- a/src/keys/bip39.rs +++ b/src/keys/bip39.rs @@ -30,30 +30,36 @@ use bitcoin::util::bip32; use bitcoin::Network; +use miniscript::ScriptContext; + use bip39::{Mnemonic, Seed}; -use super::{DescriptorKey, ToDescriptorKey}; -use crate::Error; +use super::{any_network, DescriptorKey, KeyError, ToDescriptorKey}; pub type MnemonicWithPassphrase = (Mnemonic, Option); -impl ToDescriptorKey for (Seed, bip32::DerivationPath) { - fn to_descriptor_key(self) -> Result { +impl ToDescriptorKey for (Seed, bip32::DerivationPath) { + fn to_descriptor_key(self) -> Result, KeyError> { let xprv = bip32::ExtendedPrivKey::new_master(Network::Bitcoin, &self.0.as_bytes())?; - (xprv, self.1).to_descriptor_key() + let descriptor_key = (xprv, self.1).to_descriptor_key()?; + + // here we must choose one network to build the xpub, but since the bip39 standard doesn't + // encode the network the xpub we create is actually valid everywhere. so we override the + // valid networks with `any_network()`. + Ok(descriptor_key.override_valid_networks(any_network())) } } -impl ToDescriptorKey for (MnemonicWithPassphrase, bip32::DerivationPath) { - fn to_descriptor_key(self) -> Result { +impl ToDescriptorKey for (MnemonicWithPassphrase, bip32::DerivationPath) { + fn to_descriptor_key(self) -> Result, KeyError> { let (mnemonic, passphrase) = self.0; let seed = Seed::new(&mnemonic, passphrase.as_deref().unwrap_or("")); (seed, self.1).to_descriptor_key() } } -impl ToDescriptorKey for (Mnemonic, bip32::DerivationPath) { - fn to_descriptor_key(self) -> Result { +impl ToDescriptorKey for (Mnemonic, bip32::DerivationPath) { + fn to_descriptor_key(self) -> Result, KeyError> { ((self.0, None), self.1).to_descriptor_key() } } @@ -74,9 +80,10 @@ mod test { let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap(); let key = (mnemonic, path); - let (desc, keys) = crate::descriptor!(wpkh(key)).unwrap(); + let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap(); assert_eq!(desc.to_string(), "wpkh([be83839f/44'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)"); assert_eq!(keys.len(), 1); + assert_eq!(networks.len(), 3); } #[test] @@ -87,8 +94,9 @@ mod test { let path = bip32::DerivationPath::from_str("m/44'/0'/0'/0").unwrap(); let key = ((mnemonic, Some("passphrase".into())), path); - let (desc, keys) = crate::descriptor!(wpkh(key)).unwrap(); + let (desc, keys, networks) = crate::descriptor!(wpkh(key)).unwrap(); assert_eq!(desc.to_string(), "wpkh([8f6cb80c/44'/0'/0']xpub6DWYS8bbihFevy29M4cbw4ZR3P5E12jB8R88gBDWCTCNpYiDHhYWNywrCF9VZQYagzPmsZpxXpytzSoxynyeFr4ZyzheVjnpLKuse4fiwZw/0/*)"); assert_eq!(keys.len(), 1); + assert_eq!(networks.len(), 3); } } diff --git a/src/keys/mod.rs b/src/keys/mod.rs index a8148aaf..397822e8 100644 --- a/src/keys/mod.rs +++ b/src/keys/mod.rs @@ -25,36 +25,81 @@ //! Key formats use std::any::TypeId; +use std::collections::HashSet; use std::marker::PhantomData; use bitcoin::util::bip32; -use bitcoin::{PrivateKey, PublicKey}; +use bitcoin::{Network, PrivateKey, PublicKey}; use miniscript::descriptor::{DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, KeyMap}; pub use miniscript::ScriptContext; use miniscript::{Miniscript, Terminal}; -use crate::Error; - #[cfg(feature = "keys-bip39")] #[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))] pub mod bip39; +/// Set of valid networks for a key +pub type ValidNetworks = HashSet; + +/// Create a set containing mainnet, testnet and regtest +pub fn any_network() -> ValidNetworks { + vec![Network::Bitcoin, Network::Testnet, Network::Regtest] + .into_iter() + .collect() +} +/// Create a set only containing mainnet +pub fn mainnet_network() -> ValidNetworks { + vec![Network::Bitcoin].into_iter().collect() +} +/// Create a set containing testnet and regtest +pub fn test_networks() -> ValidNetworks { + vec![Network::Testnet, Network::Regtest] + .into_iter() + .collect() +} +/// Compute the intersection of two sets +pub fn merge_networks(a: &ValidNetworks, b: &ValidNetworks) -> ValidNetworks { + a.intersection(b).cloned().collect() +} + /// Container for public or secret keys pub enum DescriptorKey { - Public(DescriptorPublicKey, PhantomData), - Secret(DescriptorSecretKey, PhantomData), + #[doc(hidden)] + Public(DescriptorPublicKey, ValidNetworks, PhantomData), + #[doc(hidden)] + Secret(DescriptorSecretKey, ValidNetworks, PhantomData), } impl DescriptorKey { + /// Create an instance given a public key and a set of valid networks + pub fn from_public(public: DescriptorPublicKey, networks: ValidNetworks) -> Self { + DescriptorKey::Public(public, networks, PhantomData) + } + + /// Create an instance given a secret key and a set of valid networks + pub fn from_secret(secret: DescriptorSecretKey, networks: ValidNetworks) -> Self { + DescriptorKey::Secret(secret, networks, PhantomData) + } + + /// Override the computed set of valid networks + pub fn override_valid_networks(self, networks: ValidNetworks) -> Self { + match self { + DescriptorKey::Public(key, _, _) => DescriptorKey::Public(key, networks, PhantomData), + DescriptorKey::Secret(key, _, _) => DescriptorKey::Secret(key, networks, PhantomData), + } + } + // This method is used internally by `bdk::fragment!` and `bdk::descriptor!`. It has to be // public because it is effectively called by external crates, once the macros are expanded, // but since it is not meant to be part of the public api we hide it from the docs. #[doc(hidden)] - pub fn into_key_and_secret(self) -> Result<(DescriptorPublicKey, KeyMap), Error> { + pub fn extract(self) -> Result<(DescriptorPublicKey, KeyMap, ValidNetworks), KeyError> { match self { - DescriptorKey::Public(public, _) => Ok((public, KeyMap::default())), - DescriptorKey::Secret(secret, _) => { + DescriptorKey::Public(public, valid_networks, _) => { + Ok((public, KeyMap::default(), valid_networks)) + } + DescriptorKey::Secret(secret, valid_networks, _) => { let mut key_map = KeyMap::with_capacity(1); let public = secret @@ -62,12 +107,13 @@ impl DescriptorKey { .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?; key_map.insert(public.clone(), secret); - Ok((public, key_map)) + Ok((public, key_map, valid_networks)) } } } } +/// Enum representation of the known valid [`ScriptContext`]s #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum ScriptContextEnum { Legacy, @@ -84,6 +130,7 @@ impl ScriptContextEnum { } } +/// Trait that adds extra useful methods to [`ScriptContext`]s pub trait ExtScriptContext: ScriptContext { fn as_enum() -> ScriptContextEnum; @@ -116,7 +163,7 @@ impl ExtScriptContext for Ctx { /// For key types that do care about this, the [`ExtScriptContext`] trait provides some useful /// methods that can be used to check at runtime which `Ctx` is being used. /// -/// For key types that that do not need to check this at runtime (because they can only work within a +/// For key types that can do this check statically (because they can only work within a /// single `Ctx`), the "specialized" trait can be implemented to make the compiler handle the type /// checking. /// @@ -127,15 +174,14 @@ impl ExtScriptContext for Ctx { /// ``` /// use bdk::bitcoin::PublicKey; /// -/// use bdk::keys::{ScriptContext, ToDescriptorKey, DescriptorKey}; -/// use bdk::Error; +/// use bdk::keys::{ScriptContext, ToDescriptorKey, DescriptorKey, KeyError}; /// /// pub struct MyKeyType { /// pubkey: PublicKey, /// } /// /// impl ToDescriptorKey for MyKeyType { -/// fn to_descriptor_key(self) -> Result, Error> { +/// fn to_descriptor_key(self) -> Result, KeyError> { /// self.pubkey.to_descriptor_key() /// } /// } @@ -146,8 +192,7 @@ impl ExtScriptContext for Ctx { /// ``` /// use bdk::bitcoin::PublicKey; /// -/// use bdk::keys::{ExtScriptContext, ScriptContext, ToDescriptorKey, DescriptorKey}; -/// use bdk::Error; +/// use bdk::keys::{ExtScriptContext, ScriptContext, ToDescriptorKey, DescriptorKey, KeyError}; /// /// pub struct MyKeyType { /// is_legacy: bool, @@ -155,11 +200,11 @@ impl ExtScriptContext for Ctx { /// } /// /// impl ToDescriptorKey for MyKeyType { -/// fn to_descriptor_key(self) -> Result, Error> { +/// fn to_descriptor_key(self) -> Result, KeyError> { /// if Ctx::is_legacy() == self.is_legacy { /// self.pubkey.to_descriptor_key() /// } else { -/// Err(Error::Generic("Invalid key context".into())) +/// Err(KeyError::InvalidScriptContext) /// } /// } /// } @@ -176,15 +221,14 @@ impl ExtScriptContext for Ctx { /// use std::str::FromStr; /// use bdk::bitcoin::PublicKey; /// -/// use bdk::keys::{ToDescriptorKey, DescriptorKey}; -/// use bdk::Error; +/// use bdk::keys::{ToDescriptorKey, DescriptorKey, KeyError}; /// /// pub struct MySegwitOnlyKeyType { /// pubkey: PublicKey, /// } /// /// impl ToDescriptorKey for MySegwitOnlyKeyType { -/// fn to_descriptor_key(self) -> Result, Error> { +/// fn to_descriptor_key(self) -> Result, KeyError> { /// self.pubkey.to_descriptor_key() /// } /// } @@ -192,25 +236,28 @@ impl ExtScriptContext for Ctx { /// let key = MySegwitOnlyKeyType { /// pubkey: PublicKey::from_str("...")?, /// }; -/// let (descriptor, _) = bdk::descriptor!(pkh ( key ) )?; -/// // ^^^^^ changing this to `wpkh` would make it compile +/// let (descriptor, _, _) = bdk::descriptor!(pkh ( key ) )?; +/// // ^^^^^ changing this to `wpkh` would make it compile /// /// # Ok::<_, Box>(()) /// ``` pub trait ToDescriptorKey: Sized { /// Turn the key into a [`DescriptorKey`] within the requested [`ScriptContext`] - fn to_descriptor_key(self) -> Result, Error>; + fn to_descriptor_key(self) -> Result, KeyError>; +} - // Used internally by `bdk::fragment!` to build `pk_k()` fragments - #[doc(hidden)] - fn into_miniscript_and_secret( - self, - ) -> Result<(Miniscript, KeyMap), Error> { - let descriptor_key = self.to_descriptor_key()?; - let (key, key_map) = descriptor_key.into_key_and_secret()?; +// Used internally by `bdk::fragment!` to build `pk_k()` fragments +#[doc(hidden)] +pub fn make_pk, Ctx: ScriptContext>( + descriptor_key: Pk, +) -> Result<(Miniscript, KeyMap, ValidNetworks), KeyError> { + let (key, key_map, valid_networks) = descriptor_key.to_descriptor_key()?.extract()?; - Ok((Miniscript::from_ast(Terminal::PkK(key))?, key_map)) - } + Ok(( + Miniscript::from_ast(Terminal::PkK(key))?, + key_map, + valid_networks, + )) } // Used internally by `bdk::fragment!` to build `multi()` fragments @@ -218,90 +265,136 @@ pub trait ToDescriptorKey: Sized { pub fn make_multi, Ctx: ScriptContext>( thresh: usize, pks: Vec, -) -> Result<(Miniscript, KeyMap), Error> { - let (pks, key_maps): (Vec<_>, Vec<_>) = pks +) -> Result<(Miniscript, KeyMap, ValidNetworks), KeyError> { + let (pks, key_maps_networks): (Vec<_>, Vec<_>) = pks .into_iter() - .map(|key| { - key.to_descriptor_key() - .and_then(DescriptorKey::into_key_and_secret) - }) + .map(|key| Ok::<_, KeyError>(key.to_descriptor_key()?.extract()?)) .collect::, _>>()? .into_iter() + .map(|(a, b, c)| (a, (b, c))) .unzip(); - let key_map = key_maps - .into_iter() - .fold(KeyMap::default(), |mut acc, map| { - acc.extend(map.into_iter()); - acc - }); + let (key_map, valid_networks) = key_maps_networks.into_iter().fold( + (KeyMap::default(), any_network()), + |(mut keys_acc, net_acc), (key, net)| { + keys_acc.extend(key.into_iter()); + let net_acc = merge_networks(&net_acc, &net); - Ok((Miniscript::from_ast(Terminal::Multi(thresh, pks))?, key_map)) + (keys_acc, net_acc) + }, + ); + + Ok(( + Miniscript::from_ast(Terminal::Multi(thresh, pks))?, + key_map, + valid_networks, + )) } /// The "identity" conversion is used internally by some `bdk::fragment`s impl ToDescriptorKey for DescriptorKey { - fn to_descriptor_key(self) -> Result, Error> { + fn to_descriptor_key(self) -> Result, KeyError> { Ok(self) } } impl ToDescriptorKey for DescriptorPublicKey { - fn to_descriptor_key(self) -> Result, Error> { - Ok(DescriptorKey::Public(self, PhantomData)) + fn to_descriptor_key(self) -> Result, KeyError> { + let networks = match self { + DescriptorPublicKey::PubKey(_) => any_network(), + DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. }) + if xkey.network == Network::Bitcoin => + { + mainnet_network() + } + _ => test_networks(), + }; + + Ok(DescriptorKey::from_public(self, networks)) } } impl ToDescriptorKey for PublicKey { - fn to_descriptor_key(self) -> Result, Error> { - Ok(DescriptorKey::Public( - DescriptorPublicKey::PubKey(self), - PhantomData, - )) + fn to_descriptor_key(self) -> Result, KeyError> { + DescriptorPublicKey::PubKey(self).to_descriptor_key() } } /// This assumes that "is_wildcard" is true, since this is generally the way extended keys are used impl ToDescriptorKey for (bip32::ExtendedPubKey, bip32::DerivationPath) { - fn to_descriptor_key(self) -> Result, Error> { - Ok(DescriptorKey::Public( - DescriptorPublicKey::XPub(DescriptorXKey { - source: None, - xkey: self.0, - derivation_path: self.1, - is_wildcard: true, - }), - PhantomData, - )) + fn to_descriptor_key(self) -> Result, KeyError> { + DescriptorPublicKey::XPub(DescriptorXKey { + source: None, + xkey: self.0, + derivation_path: self.1, + is_wildcard: true, + }) + .to_descriptor_key() } } impl ToDescriptorKey for DescriptorSecretKey { - fn to_descriptor_key(self) -> Result, Error> { - Ok(DescriptorKey::Secret(self, PhantomData)) + fn to_descriptor_key(self) -> Result, KeyError> { + let networks = match self { + DescriptorSecretKey::PrivKey(sk) if sk.network == Network::Bitcoin => mainnet_network(), + DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. }) + if xkey.network == Network::Bitcoin => + { + mainnet_network() + } + _ => test_networks(), + }; + + Ok(DescriptorKey::from_secret(self, networks)) } } impl ToDescriptorKey for PrivateKey { - fn to_descriptor_key(self) -> Result, Error> { - Ok(DescriptorKey::Secret( - DescriptorSecretKey::PrivKey(self), - PhantomData, - )) + fn to_descriptor_key(self) -> Result, KeyError> { + DescriptorSecretKey::PrivKey(self).to_descriptor_key() } } /// This assumes that "is_wildcard" is true, since this is generally the way extended keys are used impl ToDescriptorKey for (bip32::ExtendedPrivKey, bip32::DerivationPath) { - fn to_descriptor_key(self) -> Result, Error> { - Ok(DescriptorKey::Secret( - DescriptorSecretKey::XPrv(DescriptorXKey { - source: None, - xkey: self.0, - derivation_path: self.1, - is_wildcard: true, - }), - PhantomData, - )) + fn to_descriptor_key(self) -> Result, KeyError> { + DescriptorSecretKey::XPrv(DescriptorXKey { + source: None, + xkey: self.0, + derivation_path: self.1, + is_wildcard: true, + }) + .to_descriptor_key() } } + +#[derive(Debug)] +pub enum KeyError { + InvalidScriptContext, + InvalidNetwork, + InvalidChecksum, + Message(String), + + BIP32(bitcoin::util::bip32::Error), + Miniscript(miniscript::Error), +} + +impl From for KeyError { + fn from(inner: miniscript::Error) -> Self { + KeyError::Miniscript(inner) + } +} + +impl From for KeyError { + fn from(inner: bitcoin::util::bip32::Error) -> Self { + KeyError::BIP32(inner) + } +} + +impl std::fmt::Display for KeyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for KeyError {} diff --git a/src/wallet/export.rs b/src/wallet/export.rs index 9f4c3f97..ecb05af3 100644 --- a/src/wallet/export.rs +++ b/src/wallet/export.rs @@ -234,7 +234,7 @@ mod test { let wallet: OfflineWallet<_> = Wallet::new_offline( descriptor, Some(change_descriptor), - Network::Testnet, + Network::Bitcoin, get_test_db(), ) .unwrap(); @@ -256,7 +256,7 @@ mod test { let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)"; let wallet: OfflineWallet<_> = - Wallet::new_offline(descriptor, None, Network::Testnet, get_test_db()).unwrap(); + Wallet::new_offline(descriptor, None, Network::Bitcoin, get_test_db()).unwrap(); WalletExport::export_wallet(&wallet, "Test Label", true).unwrap(); } @@ -272,7 +272,7 @@ mod test { let wallet: OfflineWallet<_> = Wallet::new_offline( descriptor, Some(change_descriptor), - Network::Testnet, + Network::Bitcoin, get_test_db(), ) .unwrap(); @@ -315,7 +315,7 @@ mod test { let wallet: OfflineWallet<_> = Wallet::new_offline( descriptor, Some(change_descriptor), - Network::Testnet, + Network::Bitcoin, get_test_db(), ) .unwrap(); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 91f8ad8a..55d77642 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -113,7 +113,7 @@ where network: Network, mut database: D, ) -> Result { - let (descriptor, keymap) = descriptor.to_wallet_descriptor()?; + let (descriptor, keymap) = descriptor.to_wallet_descriptor(network)?; database.check_descriptor_checksum( ScriptType::External, get_checksum(&descriptor.to_string())?.as_bytes(), @@ -121,7 +121,7 @@ where let signers = Arc::new(SignersContainer::from(keymap)); let (change_descriptor, change_signers) = match change_descriptor { Some(desc) => { - let (change_descriptor, change_keymap) = desc.to_wallet_descriptor()?; + let (change_descriptor, change_keymap) = desc.to_wallet_descriptor(network)?; database.check_descriptor_checksum( ScriptType::Internal, get_checksum(&change_descriptor.to_string())?.as_bytes(), @@ -1737,7 +1737,7 @@ mod test { use bitcoin::util::bip32::{DerivationPath, Fingerprint}; use std::str::FromStr; - let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)"); + let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) @@ -1758,7 +1758,7 @@ mod test { use bitcoin::util::bip32::{DerivationPath, Fingerprint}; use std::str::FromStr; - let (wallet, descriptors, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)"); + let (wallet, descriptors, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); // cache some addresses wallet.get_new_address().unwrap();