diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cf22f85..b2528d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v0.4.0] - [v0.3.0] + ### Keys #### Changed - Renamed `DerivableKey::add_metadata()` to `DerivableKey::into_descriptor_key()` @@ -16,13 +18,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Misc #### Removed -- Removed the `parse_descriptor` example, since it wasn't demostrating any bdk-specific API anymore. +- Removed the `parse_descriptor` example, since it wasn't demonstrating any bdk-specific API anymore. #### Changed - Updated `bitcoin` to `0.26`, `miniscript` to `5.1` and `electrum-client` to `0.6` #### Added - Added support for the `signet` network (issue #62) - -#### Added - Added a function to get the version of BDK at runtime ### Wallet @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Policy #### Changed - Removed unneeded `Result<(), PolicyError>` return type for `Satisfaction::finalize()` +- Removed the `TooManyItemsSelected` policy error (see commit message for more details) ## [v0.3.0] - [v0.2.0] @@ -269,7 +270,8 @@ final transaction is created by calling `finish` on the builder. - Use `MemoryDatabase` in the compiler example - Make the REPL return JSON -[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...HEAD +[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.4.0...HEAD [0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1 [v0.2.0]: https://github.com/bitcoindevkit/bdk/compare/0.1.0-beta.1...v0.2.0 [v0.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0 +[v0.4.0]: https://github.com/bitcoindevkit/bdk/compare/v0.3.0...v0.4.0 diff --git a/Cargo.toml b/Cargo.toml index 798aa871..1624da25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk" -version = "0.3.1-dev" +version = "0.4.1-dev" edition = "2018" authors = ["Alekos Filini ", "Riccardo Casatta "] homepage = "https://bitcoindevkit.org" @@ -12,7 +12,7 @@ readme = "README.md" license = "MIT" [dependencies] -bdk-macros = { path = "./macros" } +bdk-macros = "^0.3" log = "^0.4" miniscript = "5.1" bitcoin = { version = "^0.26", features = ["use-serde"] } @@ -59,8 +59,8 @@ test-electrum = ["electrum"] test-md-docs = ["electrum"] [dev-dependencies] -bdk-testutils = { path = "./testutils" } -bdk-testutils-macros = { path = "./testutils-macros" } +bdk-testutils = "^0.3" +bdk-testutils-macros = "^0.3" serial_test = "0.4" lazy_static = "1.4" env_logger = "0.7" diff --git a/macros/Cargo.toml b/macros/Cargo.toml index c8338cdf..9123bb1f 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk-macros" -version = "0.2.0" +version = "0.3.0" authors = ["Alekos Filini "] edition = "2018" homepage = "https://bitcoindevkit.org" diff --git a/src/descriptor/error.rs b/src/descriptor/error.rs index 495f999c..64605061 100644 --- a/src/descriptor/error.rs +++ b/src/descriptor/error.rs @@ -31,6 +31,8 @@ pub enum Error { InvalidHDKeyPath, /// The provided descriptor doesn't match its checksum InvalidDescriptorChecksum, + /// The descriptor contains hardened derivation steps on public extended keys + HardenedDerivationXpub, /// Error thrown while working with [`keys`](crate::keys) Key(crate::keys::KeyError), diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 10000786..ee4f0629 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -198,6 +198,36 @@ impl IntoWalletDescriptor for DescriptorTemplateOut { } } +/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the +/// descriptor +pub(crate) fn into_wallet_descriptor_checked( + inner: T, + secp: &SecpCtx, + network: Network, +) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { + let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?; + + // Ensure the keys don't contain any hardened derivation steps or hardened wildcards + let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| { + if let DescriptorPublicKey::XPub(DescriptorXKey { + derivation_path, + wildcard, + .. + }) = k.as_key() + { + return *wildcard == Wildcard::Hardened + || derivation_path.into_iter().any(ChildNumber::is_hardened); + } + + false + }); + if descriptor_contains_hardened_steps { + return Err(DescriptorError::HardenedDerivationXpub); + } + + Ok((descriptor, keymap)) +} + #[doc(hidden)] /// Used internally mainly by the `descriptor!()` and `fragment!()` macros pub trait CheckMiniscript { @@ -740,4 +770,18 @@ mod test { .unwrap(); assert_eq!(wallet_desc, wallet_desc2) } + + #[test] + fn test_into_wallet_descriptor_checked() { + let secp = Secp256k1::new(); + + let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)"; + let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet); + + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + DescriptorError::HardenedDerivationXpub + )); + } } diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index 54ba808b..8929e085 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -47,7 +47,7 @@ //! # Ok::<(), bdk::Error>(()) //! ``` -use std::cmp::{max, Ordering}; +use std::cmp::max; use std::collections::{BTreeMap, HashSet, VecDeque}; use std::fmt; @@ -506,13 +506,11 @@ impl Condition { } /// Errors that can happen while extracting and manipulating policies -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum PolicyError { - /// Not enough items are selected to satisfy a [`SatisfiableItem::Thresh`] + /// Not enough items are selected to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`] NotEnoughItemsSelected(String), - /// Too many items are selected to satisfy a [`SatisfiableItem::Thresh`] - TooManyItemsSelected(String), - /// Index out of range for an item to satisfy a [`SatisfiableItem::Thresh`] + /// Index out of range for an item to satisfy a [`SatisfiableItem::Thresh`] or a [`SatisfiableItem::Multisig`] IndexOutOfRange(usize), /// Can not add to an item that is [`Satisfaction::None`] or [`Satisfaction::Complete`] AddOnLeaf, @@ -644,10 +642,10 @@ impl Policy { SatisfiableItem::Thresh { items, threshold } if items.len() == *threshold => { (0..*threshold).collect() } + SatisfiableItem::Multisig { keys, .. } => (0..keys.len()).collect(), _ => vec![], }; let selected = match path.get(&self.id) { - _ if !default.is_empty() => &default, Some(arr) => arr, _ => &default, }; @@ -668,14 +666,8 @@ impl Policy { // if we have something, make sure we have enough items. note that the user can set // an empty value for this step in case of n-of-n, because `selected` is set to all // the elements above - match selected.len().cmp(threshold) { - Ordering::Less => { - return Err(PolicyError::NotEnoughItemsSelected(self.id.clone())) - } - Ordering::Greater => { - return Err(PolicyError::TooManyItemsSelected(self.id.clone())) - } - Ordering::Equal => (), + if selected.len() < *threshold { + return Err(PolicyError::NotEnoughItemsSelected(self.id.clone())); } // check the selected items, see if there are conflicting requirements @@ -690,7 +682,16 @@ impl Policy { Ok(requirements) } - _ if !selected.is_empty() => Err(PolicyError::TooManyItemsSelected(self.id.clone())), + SatisfiableItem::Multisig { keys, threshold } => { + if selected.len() < *threshold { + return Err(PolicyError::NotEnoughItemsSelected(self.id.clone())); + } + if let Some(item) = selected.iter().find(|i| **i >= keys.len()) { + return Err(PolicyError::IndexOutOfRange(*item)); + } + + Ok(Condition::default()) + } SatisfiableItem::AbsoluteTimelock { value } => Ok(Condition { csv: None, timelock: Some(*value), @@ -1257,4 +1258,50 @@ mod test { // // // TODO how should this merge timelocks? // } + + #[test] + fn test_get_condition_multisig() { + let secp = Secp256k1::gen_new(); + + let (_, pk0, _) = setup_keys(TPRV0_STR); + let (_, pk1, _) = setup_keys(TPRV1_STR); + + let desc = descriptor!(wsh(multi(1, pk0, pk1))).unwrap(); + let (wallet_desc, keymap) = desc + .into_wallet_descriptor(&secp, Network::Testnet) + .unwrap(); + let signers = keymap.into(); + + let policy = wallet_desc + .extract_policy(&signers, &secp) + .unwrap() + .unwrap(); + + // no args, choose the default + let no_args = policy.get_condition(&vec![].into_iter().collect()); + assert_eq!(no_args, Ok(Condition::default())); + + // enough args + let eq_thresh = + policy.get_condition(&vec![(policy.id.clone(), vec![0])].into_iter().collect()); + assert_eq!(eq_thresh, Ok(Condition::default())); + + // more args, it doesn't really change anything + let gt_thresh = + policy.get_condition(&vec![(policy.id.clone(), vec![0, 1])].into_iter().collect()); + assert_eq!(gt_thresh, Ok(Condition::default())); + + // not enough args, error + let lt_thresh = + policy.get_condition(&vec![(policy.id.clone(), vec![])].into_iter().collect()); + assert_eq!( + lt_thresh, + Err(PolicyError::NotEnoughItemsSelected(policy.id.clone())) + ); + + // index out of range + let out_of_range = + policy.get_condition(&vec![(policy.id.clone(), vec![5])].into_iter().collect()); + assert_eq!(out_of_range, Err(PolicyError::IndexOutOfRange(5))); + } } diff --git a/src/lib.rs b/src/lib.rs index 197613af..df6cf7c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,7 +56,7 @@ //! interact with the bitcoin P2P network. //! //! ```toml -//! bdk = "0.3.0" +//! bdk = "0.4.0" //! ``` //! //! ## Sync the balance of a descriptor diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index b2729ec7..8bb254c4 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -66,8 +66,9 @@ use crate::blockchain::{Blockchain, Progress}; use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; use crate::descriptor::derived::AsDerived; use crate::descriptor::{ - get_checksum, DerivedDescriptor, DerivedDescriptorMeta, DescriptorMeta, DescriptorScripts, - ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils, + get_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DerivedDescriptorMeta, + DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, + Policy, XKeyUtils, }; use crate::error::Error; use crate::psbt::PSBTUtils; @@ -134,7 +135,7 @@ where ) -> Result { let secp = Secp256k1::new(); - let (descriptor, keymap) = descriptor.into_wallet_descriptor(&secp, network)?; + let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network)?; database.check_descriptor_checksum( KeychainKind::External, get_checksum(&descriptor.to_string())?.as_bytes(), @@ -143,7 +144,7 @@ where let (change_descriptor, change_signers) = match change_descriptor { Some(desc) => { let (change_descriptor, change_keymap) = - desc.into_wallet_descriptor(&secp, network)?; + into_wallet_descriptor_checked(desc, &secp, network)?; database.check_descriptor_checksum( KeychainKind::Internal, get_checksum(&change_descriptor.to_string())?.as_bytes(), diff --git a/testutils-macros/Cargo.toml b/testutils-macros/Cargo.toml index 5bd8a3db..3766e5bd 100644 --- a/testutils-macros/Cargo.toml +++ b/testutils-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk-testutils-macros" -version = "0.2.0" +version = "0.3.0" authors = ["Alekos Filini "] edition = "2018" homepage = "https://bitcoindevkit.org" diff --git a/testutils/Cargo.toml b/testutils/Cargo.toml index 8ce0035d..ee6efe60 100644 --- a/testutils/Cargo.toml +++ b/testutils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk-testutils" -version = "0.2.0" +version = "0.3.0" authors = ["Alekos Filini "] edition = "2018" homepage = "https://bitcoindevkit.org"