From 2f86af4a252bc579d44a24d8b83817971d5bd845 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 9 Mar 2022 18:38:11 +0100 Subject: [PATCH 1/9] [wallet] Add more getters --- src/wallet/mod.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 6986cf01a..cd4d4133b 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -428,6 +428,21 @@ where self.database.borrow().iter_txs(include_raw) } + /// Return a transaction given its txid + /// + /// Optionally fill the [`TransactionDetails::transaction`] field with the raw transaction if + /// `include_raw` is `true`. + /// + /// Note that this methods only operate on the internal database, which first needs to be + /// [`Wallet::sync`] manually. + pub fn get_transaction( + &self, + txid: &Txid, + include_raw: bool, + ) -> Result, Error> { + self.database.borrow().get_tx(txid, include_raw) + } + /// Return the balance, meaning the sum of this wallet's unspent outputs' values /// /// Note that this methods only operate on the internal database, which first needs to be @@ -456,6 +471,29 @@ where signers.add_external(signer.id(&self.secp), ordering, signer); } + /// Get the signers + /// + /// ## Example + /// + /// ``` + /// # use bdk::{Wallet, KeychainKind}; + /// # use bdk::bitcoin::Network; + /// # use bdk::database::MemoryDatabase; + /// let wallet = Wallet::new_offline("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet, MemoryDatabase::new())?; + /// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) { + /// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/* + /// println!("secret_key: {}", secret_key); + /// } + /// + /// Ok::<(), Box>(()) + /// ``` + pub fn get_signers(&self, keychain: KeychainKind) -> Arc { + match keychain { + KeychainKind::External => Arc::clone(&self.signers), + KeychainKind::Internal => Arc::clone(&self.change_signers), + } + } + /// Add an address validator /// /// See [the `address_validator` module](address_validator) for an example. @@ -463,6 +501,11 @@ where self.address_validators.push(validator); } + /// Get the address validators + pub fn get_address_validators(&self) -> &[Arc] { + &self.address_validators + } + /// Start building a transaction. /// /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction. @@ -1556,6 +1599,18 @@ where Ok(()) } + + /// Return the checksum of the public descriptor associated to `keychain` + /// + /// Internally calls [`Self::get_descriptor_for_keychain`] to fetch the right descriptor + pub fn descriptor_checksum(&self, keychain: KeychainKind) -> String { + self.get_descriptor_for_keychain(keychain) + .to_string() + .splitn(2, "#") + .next() + .unwrap() + .to_string() + } } /// Return a fake wallet that appears to be funded for testing. From c84790fd71844e67c78c0bdbfa20b85ad94f3470 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 9 Mar 2022 18:39:28 +0100 Subject: [PATCH 2/9] [export] Use the new getters on `Wallet` to generate export JSONs --- src/wallet/export.rs | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/wallet/export.rs b/src/wallet/export.rs index f531989e0..69b8c63fb 100644 --- a/src/wallet/export.rs +++ b/src/wallet/export.rs @@ -64,9 +64,10 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use miniscript::descriptor::{ShInner, WshInner}; -use miniscript::{Descriptor, DescriptorPublicKey, ScriptContext, Terminal}; +use miniscript::{Descriptor, ScriptContext, Terminal}; use crate::database::BatchDatabase; +use crate::types::KeychainKind; use crate::wallet::Wallet; /// Structure that contains the export of a wallet @@ -117,8 +118,12 @@ impl WalletExport { include_blockheight: bool, ) -> Result { let descriptor = wallet - .descriptor - .to_string_with_secret(&wallet.signers.as_key_map(wallet.secp_ctx())); + .get_descriptor_for_keychain(KeychainKind::External) + .to_string_with_secret( + &wallet + .get_signers(KeychainKind::External) + .as_key_map(wallet.secp_ctx()), + ); let descriptor = remove_checksum(descriptor); Self::is_compatible_with_core(&descriptor)?; @@ -142,12 +147,24 @@ impl WalletExport { blockheight, }; - let desc_to_string = |d: &Descriptor| { - let descriptor = - d.to_string_with_secret(&wallet.change_signers.as_key_map(wallet.secp_ctx())); - remove_checksum(descriptor) + let change_descriptor = match wallet + .public_descriptor(KeychainKind::Internal) + .map_err(|_| "Invalid change descriptor")? + .is_some() + { + false => None, + true => { + let descriptor = wallet + .get_descriptor_for_keychain(KeychainKind::Internal) + .to_string_with_secret( + &wallet + .get_signers(KeychainKind::Internal) + .as_key_map(wallet.secp_ctx()), + ); + Some(remove_checksum(descriptor)) + } }; - if export.change_descriptor() != wallet.change_descriptor.as_ref().map(desc_to_string) { + if export.change_descriptor() != change_descriptor { return Err("Incompatible change descriptor"); } From b170a01028ac767e1daf2ee84d80593c7e80a88c Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 9 Mar 2022 18:39:58 +0100 Subject: [PATCH 3/9] [descriptor] Expose utilities to deal with derived descriptors --- src/descriptor/derived.rs | 55 ++++++++++++++++++++++++++++++++------- src/descriptor/mod.rs | 12 ++++----- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/descriptor/derived.rs b/src/descriptor/derived.rs index d503b66fc..b6556a0b6 100644 --- a/src/descriptor/derived.rs +++ b/src/descriptor/derived.rs @@ -10,6 +10,41 @@ // licenses. //! Derived descriptor keys +//! +//! The [`DerivedDescriptorKey`] type is a wrapper over the standard [`DescriptorPublicKey`] which +//! guarantees that all the extended keys have a fixed derivation path, i.e. all the wildcards have +//! been replaced by actual derivation indexes. +//! +//! The [`AsDerived`] trait provides a quick way to derive descriptors to obtain a +//! `Descriptor` type. This, in turn, can be used to derive public +//! keys for arbitrary derivation indexes. +//! +//! Combining this with [`Wallet::get_signers`], secret keys can also be derived. +//! +//! # Example +//! +//! ``` +//! # use std::str::FromStr; +//! # use bitcoin::secp256k1::Secp256k1; +//! use bdk::descriptor::{AsDerived, DescriptorPublicKey}; +//! use bdk::miniscript::{ToPublicKey, TranslatePk, MiniscriptKey}; +//! +//! let secp = Secp256k1::gen_new(); +//! +//! let key = DescriptorPublicKey::from_str("[aa600a45/84'/0'/0']tpubDCbDXFKoLTQp44wQuC12JgSn5g9CWGjZdpBHeTqyypZ4VvgYjTJmK9CkyR5bFvG9f4PutvwmvpYCLkFx2rpx25hiMs4sUgxJveW8ZzSAVAc/0/*")?; +//! let (descriptor, _, _) = bdk::descriptor!(wpkh(key))?; +//! +//! // derived: wpkh([aa600a45/84'/0'/0']tpubDCbDXFKoLTQp44wQuC12JgSn5g9CWGjZdpBHeTqyypZ4VvgYjTJmK9CkyR5bFvG9f4PutvwmvpYCLkFx2rpx25hiMs4sUgxJveW8ZzSAVAc/0/42)#3ladd0t2 +//! let derived = descriptor.as_derived(42, &secp); +//! println!("derived: {}", derived); +//! +//! // with_pks: wpkh(02373ecb54c5e83bd7e0d40adf78b65efaf12fafb13571f0261fc90364eee22e1e)#p4jjgvll +//! let with_pks = derived.translate_pk_infallible(|pk| pk.to_public_key(), |pkh| pkh.to_public_key().to_pubkeyhash()); +//! println!("with_pks: {}", with_pks); +//! # Ok::<(), Box>(()) +//! ``` +//! +//! [`Wallet::get_signers`]: crate::wallet::Wallet::get_signers use std::cmp::Ordering; use std::fmt; @@ -19,10 +54,7 @@ use std::ops::Deref; use bitcoin::hashes::hash160; use bitcoin::PublicKey; -pub use miniscript::{ - descriptor::KeyMap, descriptor::Wildcard, Descriptor, DescriptorPublicKey, Legacy, Miniscript, - ScriptContext, Segwitv0, -}; +use miniscript::{descriptor::Wildcard, Descriptor, DescriptorPublicKey}; use miniscript::{MiniscriptKey, ToPublicKey, TranslatePk}; use crate::wallet::utils::SecpCtx; @@ -119,14 +151,19 @@ impl<'s> ToPublicKey for DerivedDescriptorKey<'s> { } } -pub(crate) trait AsDerived { - // Derive a descriptor and transform all of its keys to `DerivedDescriptorKey` +/// Utilities to derive descriptors +/// +/// Check out the [module level] documentation for more. +/// +/// [module level]: crate::descriptor::derived +pub trait AsDerived { + /// Derive a descriptor and transform all of its keys to `DerivedDescriptorKey` fn as_derived<'s>(&self, index: u32, secp: &'s SecpCtx) -> Descriptor>; - // Transform the keys into `DerivedDescriptorKey`. - // - // Panics if the descriptor is not "fixed", i.e. if it's derivable + /// Transform the keys into `DerivedDescriptorKey`. + /// + /// Panics if the descriptor is not "fixed", i.e. if it's derivable fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor>; } diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 14045f42f..5b8a3babc 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -21,16 +21,17 @@ use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerpr use bitcoin::util::psbt; use bitcoin::{Network, PublicKey, Script, TxOut}; -use miniscript::descriptor::{ - DescriptorPublicKey, DescriptorType, DescriptorXKey, InnerXKey, Wildcard, +use miniscript::descriptor::{DescriptorType, InnerXKey}; +pub use miniscript::{ + descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor, + DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0, }; -pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0}; use miniscript::{DescriptorTrait, ForEachKey, TranslatePk}; use crate::descriptor::policy::BuildSatisfaction; pub mod checksum; -pub(crate) mod derived; +pub mod derived; #[doc(hidden)] pub mod dsl; pub mod error; @@ -38,8 +39,7 @@ pub mod policy; pub mod template; pub use self::checksum::get_checksum; -use self::derived::AsDerived; -pub use self::derived::DerivedDescriptorKey; +pub use self::derived::{AsDerived, DerivedDescriptorKey}; pub use self::error::Error as DescriptorError; pub use self::policy::Policy; use self::template::DescriptorTemplateOut; From 80f7e0d0324e3f7f99ab5c162c83adf168f537b4 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 9 Mar 2022 18:43:42 +0100 Subject: [PATCH 4/9] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44e6ed4ae..3a040114f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `get_internal_address` to allow you to get internal addresses just as you get external addresses. - added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database - Add `is_spent` field to `LocalUtxo`; when we notice that a utxo has been spent we set `is_spent` field to true instead of deleting it from the db. +- Added `Wallet::get_signers()`, `Wallet::get_transaction()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait. ### Sync API change From 057e52d29df60c3783ab8f7f4abdc7c3c6dadffa Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 23 Mar 2022 12:00:24 +0100 Subject: [PATCH 5/9] Move testutils macro module before the others This allows using the `testuitils` macro in their tests as well --- src/lib.rs | 15 ++++++++------- src/testutils/mod.rs | 2 ++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0779eec4e..2c585718b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -251,6 +251,14 @@ pub extern crate sled; #[cfg(feature = "sqlite")] pub extern crate rusqlite; +// We should consider putting this under a feature flag but we need the macro in doctests so we need +// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed. +// +// Stuff in here is too rough to document atm +#[doc(hidden)] +#[macro_use] +pub mod testutils; + #[allow(unused_imports)] #[macro_use] pub(crate) mod error; @@ -279,10 +287,3 @@ pub use wallet::Wallet; pub fn version() -> &'static str { env!("CARGO_PKG_VERSION", "unknown") } - -// We should consider putting this under a feature flag but we need the macro in doctests so we need -// to wait until https://github.com/rust-lang/rust/issues/67295 is fixed. -// -// Stuff in here is too rough to document atm -#[doc(hidden)] -pub mod testutils; diff --git a/src/testutils/mod.rs b/src/testutils/mod.rs index f05c9df48..b10f1a3ba 100644 --- a/src/testutils/mod.rs +++ b/src/testutils/mod.rs @@ -267,3 +267,5 @@ macro_rules! testutils { (external, internal) }) } + +pub use testutils; From 4696ee7122933d595f1a7bb1b7a43f5e1eea6702 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Tue, 15 Mar 2022 10:48:00 +0100 Subject: [PATCH 6/9] [blockchain] Add traits to reuse `Blockchain`s across multiple wallets Add two new traits: - `StatelessBlockchain` is used to tag `Blockchain`s that don't have any wallet-specic state, i.e. they can be used as-is to sync multiple wallets. - `BlockchainFactory` is a trait for objects that can build multiple blockchains for different descriptors. It's implemented automatically for every `Arc` where `T` is a `StatelessBlockchain`. This allows a piece of code that deals with multiple sub-wallets to just get a `&B: BlockchainFactory` to sync all of them. These new traits have been implemented for Electrum, Esplora and RPC (the first two being stateless and the latter having a dedicated `RpcBlockchainFactory` struct). It hasn't been implemented on the CBF blockchain, because I don't think it would work in its current form (it throws away old block filters, so it's hard to go back and rescan). This is the first step for #549, as BIP47 needs to sync many different descriptors internally. It's also very useful for #486. --- CHANGELOG.md | 3 +- src/blockchain/electrum.rs | 2 + src/blockchain/esplora/reqwest.rs | 2 + src/blockchain/esplora/ureq.rs | 2 + src/blockchain/mod.rs | 55 ++++++++++++++++++ src/blockchain/rpc.rs | 97 +++++++++++++++++++++++++++++++ 6 files changed, 160 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a040114f..121f9fe8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database - Add `is_spent` field to `LocalUtxo`; when we notice that a utxo has been spent we set `is_spent` field to true instead of deleting it from the db. - Added `Wallet::get_signers()`, `Wallet::get_transaction()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait. +- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`). ### Sync API change @@ -438,4 +439,4 @@ final transaction is created by calling `finish` on the builder. [v0.16.0]: https://github.com/bitcoindevkit/bdk/compare/v0.15.0...v0.16.0 [v0.16.1]: https://github.com/bitcoindevkit/bdk/compare/v0.16.0...v0.16.1 [v0.17.0]: https://github.com/bitcoindevkit/bdk/compare/v0.16.1...v0.17.0 -[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...HEAD \ No newline at end of file +[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...HEAD diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index 0b8691bc1..7de60cc12 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -79,6 +79,8 @@ impl Blockchain for ElectrumBlockchain { } } +impl StatelessBlockchain for ElectrumBlockchain {} + impl GetHeight for ElectrumBlockchain { fn get_height(&self) -> Result { // TODO: unsubscribe when added to the client, or is there a better call to use here? diff --git a/src/blockchain/esplora/reqwest.rs b/src/blockchain/esplora/reqwest.rs index 2141b8e67..f68bdd8a1 100644 --- a/src/blockchain/esplora/reqwest.rs +++ b/src/blockchain/esplora/reqwest.rs @@ -101,6 +101,8 @@ impl Blockchain for EsploraBlockchain { } } +impl StatelessBlockchain for EsploraBlockchain {} + #[maybe_async] impl GetHeight for EsploraBlockchain { fn get_height(&self) -> Result { diff --git a/src/blockchain/esplora/ureq.rs b/src/blockchain/esplora/ureq.rs index 9bfa378fc..e15fb0e5b 100644 --- a/src/blockchain/esplora/ureq.rs +++ b/src/blockchain/esplora/ureq.rs @@ -98,6 +98,8 @@ impl Blockchain for EsploraBlockchain { } } +impl StatelessBlockchain for EsploraBlockchain {} + impl GetHeight for EsploraBlockchain { fn get_height(&self) -> Result { Ok(self.url_client._get_height()?) diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index 714fdf6a9..72a7ddc82 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -164,6 +164,61 @@ pub trait ConfigurableBlockchain: Blockchain + Sized { fn from_config(config: &Self::Config) -> Result; } +/// Trait for blockchains that don't contain any state +/// +/// Statless blockchains can be used to sync multiple wallets with different descriptors. +/// +/// [`BlockchainFactory`] is automatically implemented for `Arc` where `T` is a stateless +/// blockchain. +pub trait StatelessBlockchain: Blockchain {} + +/// Trait for a factory of blockchains that share the underlying connection or configuration +/// +/// ## Example +/// +/// This example shows how to sync multiple walles and return the sum of their balances +/// +/// ```no_run +/// # use bdk::Error; +/// # use bdk::blockchain::*; +/// # use bdk::database::*; +/// # use bdk::wallet::*; +/// fn sum_of_balances(blockchain_factory: B, wallets: &[Wallet]) -> Result { +/// Ok(wallets +/// .iter() +/// .map(|w| -> Result<_, Error> { +/// w.sync(&blockchain_factory.build("wallet_1", None)?, SyncOptions::default())?; +/// w.get_balance() +/// }) +/// .collect::, _>>()? +/// .into_iter() +/// .sum()) +/// } +/// ``` +pub trait BlockchainFactory { + /// The type returned when building a blockchain from this factory + type Inner: Blockchain; + + /// Build a new blockchain for the given descriptor checksum + /// + /// If `override_skip_blocks` is `None`, the returned blockchain will inherit the number of blocks + /// from the factory. Since it's not possible to override the value to `None`, set it to + /// `Some(0)` to rescan from the genesis. + fn build( + &self, + checksum: &str, + override_skip_blocks: Option, + ) -> Result; +} + +impl BlockchainFactory for Arc { + type Inner = Self; + + fn build(&self, _checksum: &str, _override_skip_blocks: Option) -> Result { + Ok(Arc::clone(self)) + } +} + /// Data sent with a progress update over a [`channel`] pub type ProgressData = (f32, Option); diff --git a/src/blockchain/rpc.rs b/src/blockchain/rpc.rs index e8da3451b..a594a48dd 100644 --- a/src/blockchain/rpc.rs +++ b/src/blockchain/rpc.rs @@ -441,6 +441,57 @@ fn list_wallet_dir(client: &Client) -> Result, Error> { Ok(result.wallets.into_iter().map(|n| n.name).collect()) } +/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`] +/// +/// Internally caches the node url and authentication params and allows getting many different [`RpcBlockchain`] +/// objects for different wallet names and with different rescan heights. +/// +/// ## Example +/// +/// ```no_run +/// # fn main() -> Result<(), Box> { +/// let factory = RpcBlockchainFactory { +/// url: "http://127.0.0.1:18332", +/// auth: Auth::Cookie { file: "/home/user/.bitcoin/.cookie".to_string() }, +/// network: Network::Testnet, +/// wallet_name_prefix: "prefix-".to_string(), +/// default_skip_blocks: 100_000, +/// }; +/// let main_wallet_blockchain = factory.build("main_wallet", Some(200_000))?; +/// # Ok(()) +/// ``` +#[derive(Debug, Clone)] +pub struct RpcBlockchainFactory { + /// The bitcoin node url + pub url: String, + /// The bitcoin node authentication mechanism + pub auth: Auth, + /// The network we are using (it will be checked the bitcoin node network matches this) + pub network: Network, + /// The prefix used to build the full wallet name for blockchains + pub wallet_name_prefix: String, + /// Default number of blocks to skip which will be inherited by blockchain unless overridden + pub default_skip_blocks: u32, +} + +impl BlockchainFactory for RpcBlockchainFactory { + type Inner = RpcBlockchain; + + fn build( + &self, + checksum: &str, + override_skip_blocks: Option, + ) -> Result { + Ok(RpcBlockchain::from_config(&RpcConfig { + url: self.url.clone(), + auth: self.auth.clone(), + network: self.network, + wallet_name: format!("{}{}", self.wallet_name_prefix, checksum), + skip_blocks: Some(override_skip_blocks.unwrap_or(self.default_skip_blocks)), + })?) + } +} + #[cfg(test)] #[cfg(feature = "test-rpc")] crate::bdk_blockchain_tests! { @@ -456,3 +507,49 @@ crate::bdk_blockchain_tests! { RpcBlockchain::from_config(&config).unwrap() } } + +#[cfg(test)] +#[cfg(feature = "test-rpc")] +mod test { + use super::*; + use crate::blockchain::*; + use crate::testutils::blockchain_tests::TestClient; + + use bitcoin::Network; + use bitcoincore_rpc::RpcApi; + + #[test] + fn test_rpc_blockchain_factory() { + let test_client = TestClient::default(); + + let factory = RpcBlockchainFactory { + url: test_client.bitcoind.rpc_url(), + auth: Auth::Cookie { + file: test_client.bitcoind.params.cookie_file.clone(), + }, + network: Network::Regtest, + wallet_name_prefix: "prefix-".into(), + default_skip_blocks: 0, + }; + + let a = factory.build("aaaaaa", None).unwrap(); + assert_eq!(a.skip_blocks, Some(0)); + assert_eq!( + a.client + .get_wallet_info() + .expect("Node connection is working") + .wallet_name, + "prefix-aaaaaa" + ); + + let b = factory.build("bbbbbb", Some(100)).unwrap(); + assert_eq!(b.skip_blocks, Some(100)); + assert_eq!( + a.client + .get_wallet_info() + .expect("Node connection is working") + .wallet_name, + "prefix-bbbbbb" + ); + } +} From 7c21ebf6e16e7fbabf19fbfafbe322c8451b6b3e Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 23 Mar 2022 12:03:03 +0100 Subject: [PATCH 7/9] Expose `DescriptorKey::as_public()` --- src/keys/mod.rs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/keys/mod.rs b/src/keys/mod.rs index a21a0c970..779f5e19f 100644 --- a/src/keys/mod.rs +++ b/src/keys/mod.rs @@ -22,10 +22,10 @@ use bitcoin::secp256k1::{self, Secp256k1, Signing}; use bitcoin::util::bip32; use bitcoin::{Network, PrivateKey, PublicKey}; -use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard}; +use miniscript::descriptor::{Descriptor, Wildcard}; pub use miniscript::descriptor::{ - DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, KeyMap, - SortedMultiVec, + DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, + DescriptorXKey, KeyMap, SortedMultiVec, }; pub use miniscript::ScriptContext; use miniscript::{Miniscript, Terminal}; @@ -94,6 +94,13 @@ impl DescriptorKey { } } + pub fn as_public(&self, secp: &SecpCtx) -> Result { + match self { + DescriptorKey::Public(pk, _, _) => Ok(pk.clone()), + DescriptorKey::Secret(secret, _, _) => Ok(secret.as_public(secp)?), + } + } + // 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. @@ -102,18 +109,15 @@ impl DescriptorKey { self, secp: &SecpCtx, ) -> Result<(DescriptorPublicKey, KeyMap, ValidNetworks), KeyError> { + let public = self.as_public(secp)?; + match self { - DescriptorKey::Public(public, valid_networks, _) => { + DescriptorKey::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 - .as_public(secp) - .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?; key_map.insert(public.clone(), secret); - Ok((public, key_map, valid_networks)) } } @@ -891,9 +895,15 @@ pub enum KeyError { Bip32(bitcoin::util::bip32::Error), /// Miniscript error Miniscript(miniscript::Error), + KeyParseError(miniscript::descriptor::DescriptorKeyParseError), } impl_error!(miniscript::Error, Miniscript, KeyError); +impl_error!( + miniscript::descriptor::DescriptorKeyParseError, + KeyParseError, + KeyError +); impl_error!(bitcoin::util::bip32::Error, Bip32, KeyError); impl std::fmt::Display for KeyError { From 17be15af656c3098e816e9ed43c6a335b938f3d0 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 23 Mar 2022 15:02:33 +0100 Subject: [PATCH 8/9] Allow cloning a `TxBuilder` using `BranchAndBoundCoinSelection` --- src/wallet/coin_selection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 2b549b45d..78bff0c4f 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -267,7 +267,7 @@ impl OutputGroup { /// Branch and bound coin selection /// /// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct BranchAndBoundCoinSelection { size_of_change: u64, } From 51b969d76e8566034df0f0da7dedc030cd04d9a2 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Wed, 23 Mar 2022 12:04:11 +0100 Subject: [PATCH 9/9] Add support for BIP47 (reusable payment codes) --- src/lib.rs | 1 + src/util/bip47.rs | 819 ++++++++++++++++++++++++++++++++++++++++++++++ src/util/mod.rs | 12 + 3 files changed, 832 insertions(+) create mode 100644 src/util/bip47.rs create mode 100644 src/util/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 2c585718b..4ef4fdcd4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -270,6 +270,7 @@ mod doctest; pub mod keys; pub(crate) mod psbt; pub(crate) mod types; +pub mod util; pub mod wallet; pub use descriptor::template; diff --git a/src/util/bip47.rs b/src/util/bip47.rs new file mode 100644 index 000000000..947a22c3b --- /dev/null +++ b/src/util/bip47.rs @@ -0,0 +1,819 @@ +// Bitcoin Dev Kit +// Written in 2022 by Alekos Filini +// +// Copyright (c) 2020-2022 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use std::collections::{BTreeMap, HashMap}; +use std::fmt; +use std::ops::Deref; +use std::str::FromStr; + +use bitcoin::blockdata::script::Instruction; +use bitcoin::consensus::encode::serialize; +use bitcoin::hashes::{sha256, sha512, Hmac, HmacEngine}; +use bitcoin::secp256k1::ecdh::SharedSecret; +use bitcoin::secp256k1::key::{PublicKey, SecretKey}; +use bitcoin::util::base58; +use bitcoin::util::bip32; +use bitcoin::util::psbt; +use bitcoin::{Address, Network, OutPoint, Script, Transaction, TxIn, Txid}; + +use crate::blockchain::BlockchainFactory; +use crate::database::{BatchDatabase, MemoryDatabase}; +use crate::descriptor::template::{DescriptorTemplate, DescriptorTemplateOut, P2Pkh}; +use crate::descriptor::{DescriptorError, Legacy}; +use crate::keys::{DerivableKey, DescriptorSecretKey, DescriptorSinglePriv, ExtendedKey}; +use crate::wallet::coin_selection::DefaultCoinSelectionAlgorithm; +use crate::wallet::tx_builder::{CreateTx, TxBuilder, TxOrdering}; +use crate::wallet::utils::SecpCtx; +use crate::wallet::{AddressIndex, SyncOptions, Wallet}; +use crate::{Error as WalletError, KeychainKind, LocalUtxo, TransactionDetails}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord, Hash)] +pub struct PaymentCode { + pub version: u8, + pub features: u8, + pub public_key: PublicKey, + pub chain_code: bip32::ChainCode, +} + +impl PaymentCode { + pub fn decode(data: &[u8]) -> Result { + if data.len() != 80 { + return Err(Error::WrongDataLength(data.len())); + } + + let version = data[0]; + if version != 0x01 { + return Err(Error::UnknownVersion(version)); + } + let features = data[1]; + let sign = data[2]; + if sign != 0x02 && sign != 0x03 { + return Err(Error::InvalidPublicKeySign(sign)); + } + + Ok(PaymentCode { + version, + features, + public_key: PublicKey::from_slice(&data[2..35])?, + chain_code: bip32::ChainCode::from(&data[35..67]), + }) + } + + pub fn decode_blinded( + data: &[u8], + blinding_factor: BlindingFactor, + ) -> Result { + let mut data = data.to_vec(); + + for (a, b) in data[3..68].iter_mut().zip(&blinding_factor[..]) { + *a ^= b; + } + + Self::decode(&data) + } + + pub fn encode(&self) -> [u8; 80] { + let mut ret = [0; 80]; + ret[0] = self.version; + ret[1] = self.features; + ret[2..35].copy_from_slice(&self.public_key.serialize()[..]); + ret[35..67].copy_from_slice(&self.chain_code[..]); + ret[67..80].copy_from_slice(&[0; 13]); + ret + } + + pub fn encode_blinded(&self, blinding_factor: BlindingFactor) -> [u8; 80] { + let mut encoded = self.encode(); + + for (a, b) in encoded[3..68].iter_mut().zip(&blinding_factor[..]) { + *a ^= b; + } + + encoded + } + + pub fn notification_address(&self, secp: &SecpCtx, network: Network) -> Address { + Address::p2pkh( + &bitcoin::PublicKey { + compressed: true, + key: self.derive(secp, 0), + }, + network, + ) + } + + pub fn derive(&self, secp: &SecpCtx, index: u32) -> PublicKey { + self.to_xpub() + .derive_pub(secp, &vec![bip32::ChildNumber::Normal { index }]) + .expect("Normal derivation should work") + .public_key + .key + } + + fn to_xpub(&self) -> bip32::ExtendedPubKey { + bip32::ExtendedPubKey { + network: Network::Bitcoin, + depth: 0, + parent_fingerprint: bip32::Fingerprint::default(), + child_number: bip32::ChildNumber::Normal { index: 0 }, + public_key: bitcoin::PublicKey { + compressed: true, + key: self.public_key, + }, + chain_code: self.chain_code, + } + } +} + +#[derive(Debug, Clone)] +pub struct BlindingFactor([u8; 64]); + +impl BlindingFactor { + pub fn new(shared_secret: SharedSecret, outpoint: &OutPoint) -> Self { + use bitcoin::hashes::{Hash, HashEngine}; + + let mut hmac = HmacEngine::::new(&serialize(outpoint)); + hmac.input(&shared_secret); + + BlindingFactor(Hmac::::from_engine(hmac).into_inner()) + } +} + +impl Deref for BlindingFactor { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for PaymentCode { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let mut prefixed = [0; 81]; + prefixed[0] = 0x47; + prefixed[1..].copy_from_slice(&self.encode()[..]); + base58::check_encode_slice_to_fmt(fmt, &prefixed[..]) + } +} + +impl FromStr for PaymentCode { + type Err = Error; + + fn from_str(inp: &str) -> Result { + let data = base58::from_check(inp)?; + + if data.len() != 81 { + return Err(base58::Error::InvalidLength(data.len()).into()); + } + if data[0] != 0x47 { + return Err(Error::InvalidPrefix(data[0])); + } + + Ok(PaymentCode::decode(&data[1..])?) + } +} + +pub struct Bip47Notification>(pub K); + +impl> DescriptorTemplate for Bip47Notification { + fn build(self) -> Result { + P2Pkh(( + self.0, + bip32::DerivationPath::from_str("m/47'/0'/0'").unwrap(), + )) + .build() + } +} + +pub struct Bip47Wallet<'w, D> { + seed: bip32::ExtendedPrivKey, + main_wallet: &'w Wallet, + notification_wallet: Wallet, + inbound_wallets: HashMap>>>, + outbound_wallets: HashMap>>>, + outbound_txs: HashMap, +} + +impl<'w, D: BatchDatabase> Bip47Wallet<'w, D> { + pub fn new>( + seed: K, + wallet: &'w Wallet, + ) -> Result { + let bip47_seed = match seed.clone().into_extended_key()? { + ExtendedKey::Private((xprv, _)) => xprv + .derive_priv( + wallet.secp_ctx(), + &bip32::DerivationPath::from_str("m/47'/0'/0'").unwrap(), + ) + .map_err(WalletError::from)?, + _ => panic!("The key must be derivable"), + }; + + Ok(Bip47Wallet { + seed: bip47_seed, + notification_wallet: Wallet::new( + Bip47Notification(seed), + None, + wallet.network(), + MemoryDatabase::new(), + )?, + main_wallet: wallet, + inbound_wallets: HashMap::new(), + outbound_wallets: HashMap::new(), + outbound_txs: HashMap::new(), + }) + } + + pub fn sync(&mut self, blockchain: &B) -> Result<(), Error> { + fn sync_wallet( + wallet: &Wallet, + blockchain: &BF, + ) -> Result<(), Error> { + wallet.sync( + &blockchain.build(&wallet.descriptor_checksum(KeychainKind::External), None)?, + SyncOptions::default(), + )?; + + Ok(()) + } + + sync_wallet(&self.notification_wallet, blockchain)?; + for tx in self.notification_wallet.list_transactions(true)? { + // let conf_height = match tx.confirmation_time { + // None => continue, + // Some(bt) => bt.height, + // }; + let tx = tx.transaction.as_ref().expect("Missing rawtx"); + + if let Some(payment_code) = self.handshake_inbound(&tx) { + println!("received notification from {}", payment_code.to_string()); + + // remove the wallets to avoid a mutable borrow from `self`, which would + // conflict with `self.derive_inbound_wallet()`. + let mut wallets = self + .inbound_wallets + .remove(&payment_code) + .unwrap_or_else(|| BTreeMap::new()); + for i in 0u32.. { + println!("\tcheck {}", i); + + match wallets + .entry(i) + .or_insert(self.derive_inbound_wallet(&payment_code, i)?) + { + Some(w) => { + sync_wallet(&w, blockchain)?; + println!( + "\tbalance ({}): {}", + w.get_address(AddressIndex::New)?, + w.get_balance()? + ); + if w.get_balance()? == 0 { + break; + } + } + None => continue, + }; + } + self.inbound_wallets.insert(payment_code, wallets); + } + } + + sync_wallet(&self.main_wallet, blockchain)?; + for tx in self.main_wallet.list_transactions(true)? { + let tx = tx.transaction.as_ref().expect("Missing rawtx"); + if let Some((scripts, txid)) = self.handshake_outbound(&tx)? { + println!( + "handshake outbound found potential notification tx: {}", + txid + ); + + for s in scripts { + self.outbound_txs.insert(s, txid); + } + } + } + + // remove the wallets to avoid a mutable borrow from `self`, which would + // conflict with `self.derive_outbound_wallet()`. + let mut outbound_wallets = self.outbound_wallets.drain().collect::>(); + for (payment_code, wallets) in outbound_wallets.iter_mut() { + for i in 0u32.. { + println!("\tcheck {} (out)", i); + + match wallets + .entry(i) + .or_insert(self.derive_outbound_wallet(&payment_code, i)?) + { + Some(w) => { + sync_wallet(&w, blockchain)?; + println!("\tbalance: {}", w.get_balance()?); + if w.get_balance()? == 0 { + break; + } + } + None => continue, + }; + } + } + self.outbound_wallets.extend(outbound_wallets.into_iter()); + + Ok(()) + } + + fn handshake_inbound(&self, tx: &Transaction) -> Option { + let pk = match get_designated_pubkey(&tx.input[0]) { + Some(pk) => pk, + None => return None, + }; + + let secret = self.secret(&bip32::DerivationPath::default()); + let shared_secret = SharedSecret::new(&pk, &secret); + let blinding_factor = BlindingFactor::new(shared_secret, &tx.input[0].previous_output); + + get_op_return_data(tx) + .and_then(|data| PaymentCode::decode_blinded(&data, blinding_factor).ok()) + } + + fn handshake_outbound(&self, tx: &Transaction) -> Result, Txid)>, Error> { + if self + .main_wallet + .get_utxo(tx.input[0].previous_output)? + .is_some() + { + if let Some(data) = get_op_return_data(tx) { + if data.len() != 80 { + return Ok(None); + } + + // Potential notification addresses + let scripts = tx + .output + .iter() + .map(|out| &out.script_pubkey) + .filter(|script| script.is_p2pkh()) + .cloned() + .collect::>(); + return Ok(Some((scripts, tx.txid()))); + } + } + + Ok(None) + } + + fn derive_inbound_wallet( + &self, + payment_code: &PaymentCode, + index: u32, + ) -> Result>, Error> { + use bitcoin::hashes::Hash; + + let secp = self.main_wallet.secp_ctx(); + let network = self.main_wallet.network(); + + let mut pk = payment_code.derive(secp, 0); + let mut sk = self.secret(&vec![bip32::ChildNumber::Normal { index }]); + + pk.mul_assign(secp, sk.as_ref())?; + let shared_secret = sha256::Hash::hash(&pk.serialize()[1..]); + if let Err(_) = SecretKey::from_slice(&shared_secret) { + return Ok(None); + } + sk.add_assign(&shared_secret)?; + + let wallet = Wallet::new( + P2Pkh(bitcoin::PrivateKey { + key: sk, + compressed: true, + network, + }), + None, + network, + MemoryDatabase::new(), + )?; + + Ok(Some(wallet)) + } + + fn derive_outbound_wallet( + &self, + payment_code: &PaymentCode, + index: u32, + ) -> Result>, Error> { + use bitcoin::hashes::Hash; + + let secp = self.main_wallet.secp_ctx(); + let network = self.main_wallet.network(); + + let pk = payment_code.derive(secp, index); + let sk = self.secret(&vec![bip32::ChildNumber::Normal { index: 0 }]); + + let mut s = pk.clone(); + s.mul_assign(secp, sk.as_ref())?; + let shared_secret = sha256::Hash::hash(&s.serialize()[1..]); + let pk = match SecretKey::from_slice(&shared_secret) { + Ok(sk) => pk.combine(&PublicKey::from_secret_key(secp, &sk))?, + Err(_) => return Ok(None), + }; + + let wallet = Wallet::new( + P2Pkh(bitcoin::PublicKey { + key: pk, + compressed: true, + }), + None, + network, + MemoryDatabase::new(), + )?; + + Ok(Some(wallet)) + } + + fn secret>(&self, derivation: &P) -> SecretKey { + let derived = self + .seed + .derive_priv(self.main_wallet.secp_ctx(), derivation) + .map_err(WalletError::from) + .expect("Derivation should work"); + + derived.private_key.key + } + + pub fn payment_code(&self) -> PaymentCode { + let xpub = + bip32::ExtendedPubKey::from_private(self.notification_wallet.secp_ctx(), &self.seed); + + PaymentCode { + version: 0x01, + features: 0x00, + chain_code: xpub.chain_code, + public_key: xpub.public_key.key, + } + } + + pub fn notification_address(&self) -> Address { + self.payment_code() + .notification_address(self.main_wallet.secp_ctx(), self.main_wallet.network()) + } + + pub fn build_notification_tx( + &mut self, + payment_code: &PaymentCode, + initial_builder: Option>, + amount: Option, + ) -> Result, Error> { + let secp = self.main_wallet.secp_ctx(); + let network = self.main_wallet.network(); + + // We already know about this payment code + if self.outbound_wallets.contains_key(payment_code) { + return Ok(None); + } + + // We might have sent a notification transaction to this code in the past, confirm it here + if let Some(txid) = self.outbound_txs.get( + &payment_code + .notification_address(secp, network) + .script_pubkey(), + ) { + if self.reconstruct_outbound_notification(txid, payment_code)? { + self.record_notification_tx(payment_code); + return Ok(None); + } + } + + let build_tx = |data| { + let mut builder = initial_builder + .clone() + .unwrap_or(self.main_wallet.build_tx()); + builder + .ordering(TxOrdering::Untouched) + .add_data(data) + .add_recipient( + payment_code + .notification_address(secp, network) + .script_pubkey(), + amount.unwrap_or(546), + ); + + builder + }; + + let utxos = { + // Build a tx with a dummy payment code, to perform coin selection and fee estimation + let (psbt, _) = build_tx(&[0u8; 80]).finish()?; + // Then reuse the inputs + psbt.global.unsigned_tx.input + }; + + let local_utxo = self + .main_wallet + .get_utxo(utxos[0].previous_output)? + .ok_or_else(|| Error::InvalidUTXO(utxos[0].previous_output))?; + + let blinding_factor = self.generate_blinding_factor(local_utxo, &payment_code)?; + let mut builder = build_tx(&self.payment_code().encode_blinded(blinding_factor)); + builder + .add_utxos(&utxos.iter().map(|x| x.previous_output).collect::>())? + .manually_selected_only(); + + Ok(Some(builder.finish()?)) + } + + pub fn record_notification_tx(&mut self, payment_code: &PaymentCode) { + self.outbound_wallets.insert(*payment_code, BTreeMap::new()); + } + + pub fn get_payment_address(&self, payment_code: &PaymentCode) -> Result { + match self.outbound_wallets.get(payment_code) { + Some(wallets) => match wallets.values().last() { + Some(w) => Ok(w + .as_ref() + .expect("Last wallet is valid") + .get_address(AddressIndex::New)? + .address), + _ => Err(Error::UnsyncedWallet), + }, + _ => Err(Error::UnknownReceipient), + } + } + + fn reconstruct_outbound_notification( + &self, + txid: &Txid, + payment_code: &PaymentCode, + ) -> Result { + let tx = match self.main_wallet.get_tx(txid, true)? { + Some(details) => details.transaction.expect("Raw tx requested"), + None => return Ok(false), + }; + + if let Some(utxo) = self.main_wallet.get_utxo(tx.input[0].previous_output)? { + if let Some(data) = get_op_return_data(&tx) { + let blinding_factor = self.generate_blinding_factor(utxo, payment_code)?; + + return match PaymentCode::decode_blinded(data, blinding_factor) { + Ok(pc) if &pc == payment_code => Ok(true), + _ => Ok(false), + }; + } + } + + Ok(false) + } + + fn generate_blinding_factor( + &self, + local_utxo: LocalUtxo, + payment_code: &PaymentCode, + ) -> Result { + let secp = self.main_wallet.secp_ctx(); + + let outpoint = local_utxo.outpoint.clone(); + + let keychain = local_utxo.keychain; + let psbt_input = self.main_wallet.get_psbt_input(local_utxo, None, true)?; + let keys_map = self + .main_wallet + .get_signers(keychain) + .signers() + .iter() + .filter_map(|signer| match signer.descriptor_secret_key() { + Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv { key, .. })) => { + Some((key.public_key(secp), key)) + } + Some(DescriptorSecretKey::XPrv(xkey)) => { + for (_, keysource) in &psbt_input.bip32_derivation { + if xkey.matches(keysource, secp).is_some() { + let deriv_path = &keysource + .1 + .into_iter() + .cloned() + .collect::>() + [xkey.origin.map(|o| o.1.len()).unwrap_or(0)..]; + let key = xkey + .xkey + .derive_priv(secp, &deriv_path) + .expect("Derivation shouldn't fail") + .private_key; + + return Some((key.public_key(secp), key)); + } + } + + None + } + _ => None, + }) + .collect::>(); + if keys_map.len() != 1 { + return Err(Error::UnsupportedWallet); + } + + let shared_secret = SharedSecret::new( + &payment_code.public_key, + &keys_map.values().next().expect("Key is present").key, + ); + Ok(BlindingFactor::new(shared_secret, &outpoint)) + } +} + +fn get_designated_pubkey(txin: &TxIn) -> Option { + // From the BIP: + // + // > Alice SHOULD use an input script in one of the following standard forms to expose a public key, and compliant applications SHOULD recognize all of these forms. + // > - P2PK (pay to pubkey) + // > - P2PKH (pay to pubkey hash) + // > - Multisig (bare multisig, without P2SH) + // > - a script which spends any of the above script forms via P2SH (pay to script hash) + // + // TODO: Unfortunately to check the script type we need to know the previous transaction. For now, + // assume it's a P2PKH and fail otherwise. + + match txin.script_sig.instructions().nth(1) { + Some(Ok(Instruction::PushBytes(pk))) => PublicKey::from_slice(pk).ok(), + _ => None, + } +} + +fn get_op_return_data(tx: &Transaction) -> Option<&[u8]> { + if let Some(txout) = tx.output.iter().find(|o| o.script_pubkey.is_op_return()) { + return match txout.script_pubkey.instructions().nth(1) { + Some(Ok(Instruction::PushBytes(data))) => Some(data), + _ => None, + }; + } + + None +} + +#[derive(Debug)] +pub enum Error { + WrongDataLength(usize), + UnknownVersion(u8), + InvalidPrefix(u8), + InvalidPublicKeySign(u8), + InvalidUTXO(OutPoint), + UnsupportedWallet, + UnknownReceipient, + UnsyncedWallet, + Base58(base58::Error), + SecpKey(bitcoin::secp256k1::Error), + Key(crate::keys::KeyError), + Wallet(WalletError), +} + +// TODO: impl display, std::err + +impl From for Error { + fn from(e: base58::Error) -> Error { + Error::Base58(e) + } +} +impl From for Error { + fn from(e: bitcoin::secp256k1::Error) -> Error { + Error::SecpKey(e) + } +} +impl From for Error { + fn from(e: crate::keys::KeyError) -> Error { + Error::Key(e) + } +} +impl From for Error { + fn from(e: WalletError) -> Error { + Error::Wallet(e) + } +} + +#[cfg(test)] +mod test { + use std::sync::Arc; + + use bitcoin::*; + use electrum_client::*; + + use super::*; + use crate::blockchain::*; + use crate::database::*; + use crate::descriptor::template::*; + use crate::wallet::*; + + #[test] + fn t() { + use crate::testutils::blockchain_tests::TestClient; + + let mut tc = TestClient::default(); + let blockchain = Arc::new(ElectrumBlockchain::from( + electrum_client::Client::new(&tc.electrsd.electrum_url).unwrap(), + )); + + // let client = electrum_client::Client::new("ssl://electrum.blockstream.info:60002").unwrap(); + // let blockchain = Arc::new(ElectrumBlockchain::from(client)); + + // let key = PrivateKey::from_wif("cU1zSPAAHNGE8quJZkBsFJELTxfJRsS82Z4M4WPb95VcdpBM9gBv").unwrap(); + // let key = + // PrivateKey::from_wif("L3esrLZfpd5B2GGSjVSUiHGZvDtDpAAFRLGKP9UiMC46pbfYAJJk").unwrap(); + let m = crate::keys::bip39::Mnemonic::parse( + "response seminar brave tip suit recall often sound stick owner lottery motion", + ) + .unwrap(); + let alice_main_wallet = Wallet::new( + Bip44(m.clone(), KeychainKind::External), + None, + Network::Regtest, + MemoryDatabase::new(), + ) + .unwrap(); + let tx = crate::testutils! { + @tx ( (@addr alice_main_wallet.get_address(AddressIndex::Peek(5)).unwrap().address) => 50_000 ) + }; + tc.receive(tx); + + alice_main_wallet + .sync(&blockchain, SyncOptions::default()) + .unwrap(); + println!("balance: {}", alice_main_wallet.get_balance().unwrap()); + println!( + "{}", + alice_main_wallet.get_address(AddressIndex::New).unwrap() + ); + + let mut alice = Bip47Wallet::new(m, &alice_main_wallet).unwrap(); + println!("{} {}", alice.payment_code(), alice.notification_address()); + assert_eq!(alice.payment_code().to_string(), "PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA"); + // assert_eq!(alice.notification_address().to_string(), "1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW"); + + // let sharedsecret = Vec::::from_hex("736a25d9250238ad64ed5da03450c6a3f4f8f4dcdf0b58d1ed69029d76ead48d").unwrap(); + // let sharedsecret: [u8; 32] = sharedsecret.try_into().unwrap(); + // let sharedsecret = bitcoin::secp256k1::ecdh::SharedSecret::from(sharedsecret); + // dbg!(&sharedsecret); + + // let outpoint = OutPoint::from_str("9c6000d597c5008f7bfc2618aed5e4a6ae57677aab95078aae708e1cab11f486:1").unwrap(); + // dbg!(&outpoint); + // let bf = BlindingFactor::new(sharedsecret, &outpoint); + + // use bitcoin::hashes::hex::ToHex; + // println!("blinded code: {}", alice.payment_code().encode_blinded(bf).to_hex()); + + let m = crate::keys::bip39::Mnemonic::parse( + "reward upper indicate eight swift arch injury crystal super wrestle already dentist", + ) + .unwrap(); + let bob_main_wallet = Wallet::new( + Bip44(m.clone(), KeychainKind::External), + None, + Network::Regtest, + MemoryDatabase::new(), + ) + .unwrap(); + let mut bob = Bip47Wallet::new(m, &bob_main_wallet).unwrap(); + println!("{} {}", bob.payment_code(), bob.notification_address()); + assert_eq!(bob.payment_code().to_string(), "PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97"); + // assert_eq!(bob.notification_address().unwrap().to_string(), "1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV"); + // bob.sync(&blockchain).unwrap(); + // bob.sync(&blockchain).unwrap(); + // bob.sync(&blockchain).unwrap(); + // bob.sync(&blockchain).unwrap(); + + let (mut psbt, _) = alice + .build_notification_tx(&bob.payment_code(), None, None) + .unwrap() + .unwrap(); + alice_main_wallet + .sign(&mut psbt, Default::default()) + .unwrap(); + blockchain.broadcast(&psbt.extract_tx()).unwrap(); + alice.record_notification_tx(&bob.payment_code()); + + alice.sync(&blockchain).unwrap(); + + let bob_addr = alice.get_payment_address(&bob.payment_code()).unwrap(); + let (mut psbt, _) = { + let mut builder = alice_main_wallet.build_tx(); + builder.add_recipient(bob_addr.script_pubkey(), 10_000); + builder.finish().unwrap() + }; + alice_main_wallet + .sign(&mut psbt, Default::default()) + .unwrap(); + blockchain.broadcast(&psbt.extract_tx()).unwrap(); + + alice.sync(&blockchain).unwrap(); + + println!("bob sync"); + bob.sync(&blockchain).unwrap(); + + // dbg!(&tx); + } + + // TODO: test main wallet with single key and xprv +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 000000000..048292bb5 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,12 @@ +// Bitcoin Dev Kit +// Written in 2022 by Alekos Filini +// +// Copyright (c) 2020-2022 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +pub mod bip47;