diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 3b0839023..190bae292 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -154,7 +154,7 @@ jobs: - name: Update toolchain run: rustup update - name: Check - run: cargo check --target wasm32-unknown-unknown --features use-esplora-async --no-default-features + run: cargo check --target wasm32-unknown-unknown --features use-esplora-async,dev-getrandom-wasm --no-default-features fmt: name: Rust fmt diff --git a/Cargo.toml b/Cargo.toml index b931976f1..6dcfead1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,16 +14,16 @@ license = "MIT OR Apache-2.0" [dependencies] bdk-macros = "^0.6" log = "^0.4" -miniscript = { version = "7.0", features = ["use-serde"] } -bitcoin = { version = "0.28.1", features = ["use-serde", "base64", "rand"] } +miniscript = { version = "8.0", features = ["serde"] } +bitcoin = { version = "0.29.1", features = ["serde", "base64", "rand"] } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } -rand = "^0.7" +rand = "^0.8" # Optional dependencies sled = { version = "0.34", optional = true } -electrum-client = { version = "0.11", optional = true } -esplora-client = { version = "0.1.1", default-features = false, optional = true } +electrum-client = { version = "0.12", optional = true } +esplora-client = { version = "0.2", default-features = false, optional = true } rusqlite = { version = "0.27.0", optional = true } ahash = { version = "0.7.6", optional = true } futures = { version = "0.3", optional = true } @@ -31,22 +31,22 @@ async-trait = { version = "0.1", optional = true } rocksdb = { version = "0.14", default-features = false, features = ["snappy"], optional = true } cc = { version = ">=1.0.64", optional = true } socks = { version = "0.3", optional = true } -hwi = { version = "0.2.3", optional = true } +hwi = { version = "0.3.0", optional = true } bip39 = { version = "1.0.1", optional = true } bitcoinconsensus = { version = "0.19.0-3", optional = true } # Needed by bdk_blockchain_tests macro and the `rpc` feature -bitcoincore-rpc = { version = "0.15", optional = true } +bitcoincore-rpc = { version = "0.16", optional = true } # Platform-specific dependencies [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "1", features = ["rt"] } [target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = "0.2" async-trait = "0.1" js-sys = "0.3" -rand = { version = "^0.7", features = ["wasm-bindgen"] } [features] minimal = [] @@ -98,13 +98,18 @@ test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoi test-md-docs = ["electrum"] test-hardware-signer = ["hardware-signer"] +# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended +# for libraries to explicitly include the "getrandom/js" feature, so we only do it when +# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support +dev-getrandom-wasm = ["getrandom/js"] + [dev-dependencies] lazy_static = "1.4" env_logger = "0.7" -electrsd = "0.20" +electrsd = "0.21" +# Move back to importing from rust-bitcoin once https://github.com/rust-bitcoin/rust-bitcoin/pull/1342 is released +base64 = "^0.13" -[[example]] -name = "address_validator" [[example]] name = "compact_filters_balance" required-features = ["compact_filters"] diff --git a/README.md b/README.md index 02a55604d..d3ff3dc9e 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ use bdk::blockchain::ElectrumBlockchain; use bdk::electrum_client::Client; use bdk::wallet::AddressIndex::New; -use bitcoin::base64; +use base64; use bitcoin::consensus::serialize; fn main() -> Result<(), bdk::Error> { @@ -132,7 +132,7 @@ fn main() -> Result<(), bdk::Error> { ```rust,no_run use bdk::{Wallet, SignOptions, database::MemoryDatabase}; -use bitcoin::base64; +use base64; use bitcoin::consensus::deserialize; fn main() -> Result<(), bdk::Error> { @@ -171,6 +171,17 @@ cargo test --features test-electrum The other options are `test-esplora`, `test-rpc` or `test-rpc-legacy` which runs against an older version of Bitcoin Core. Note that `electrs` and `bitcoind` binaries are automatically downloaded (on mac and linux), to specify you already have installed binaries you must use `--no-default-features` and provide `BITCOIND_EXE` and `ELECTRS_EXE` as environment variables. +## Running under WASM + +If you want to run this library under WASM you will probably have to add the following lines to you `Cargo.toml`: + +```toml +[dependencies] +getrandom = { version = "0.2", features = ["js"] } +``` + +This enables the `rand` crate to work in environments where JavaScript is available. See [this link](https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support) to learn more. + ## License Licensed under either of diff --git a/examples/address_validator.rs b/examples/address_validator.rs deleted file mode 100644 index 26c36dfe3..000000000 --- a/examples/address_validator.rs +++ /dev/null @@ -1,63 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 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::sync::Arc; - -use bdk::bitcoin; -use bdk::database::MemoryDatabase; -use bdk::descriptor::HdKeyPaths; -#[allow(deprecated)] -use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError}; -use bdk::KeychainKind; -use bdk::Wallet; - -use bdk::wallet::AddressIndex::New; -use bitcoin::hashes::hex::FromHex; -use bitcoin::util::bip32::Fingerprint; -use bitcoin::{Network, Script}; - -#[derive(Debug)] -struct DummyValidator; -#[allow(deprecated)] -impl AddressValidator for DummyValidator { - fn validate( - &self, - keychain: KeychainKind, - hd_keypaths: &HdKeyPaths, - script: &Script, - ) -> Result<(), AddressValidatorError> { - let (_, path) = hd_keypaths - .values() - .find(|(fing, _)| fing == &Fingerprint::from_hex("bc123c3e").unwrap()) - .ok_or(AddressValidatorError::InvalidScript)?; - - println!( - "Validating `{:?}` {} address, script: {}", - keychain, path, script - ); - - Ok(()) - } -} - -fn main() -> Result<(), bdk::Error> { - let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))"; - let mut wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new())?; - - #[allow(deprecated)] - wallet.add_address_validator(Arc::new(DummyValidator)); - - wallet.get_address(New)?; - wallet.get_address(New)?; - wallet.get_address(New)?; - - Ok(()) -} diff --git a/examples/psbt_signer.rs b/examples/psbt_signer.rs index 8d6e96fdf..35c539dad 100644 --- a/examples/psbt_signer.rs +++ b/examples/psbt_signer.rs @@ -33,8 +33,8 @@ fn main() -> Result<(), Box> { let internal_secret_xkey = DescriptorSecretKey::from_str("[e9824965/84'/1'/0']tprv8fvem7qWxY3SGCQczQpRpqTKg455wf1zgixn6MZ4ze8gRfHjov5gXBQTadNfDgqs9ERbZZ3Bi1PNYrCCusFLucT39K525MWLpeURjHwUsfX/1/*").unwrap(); let secp = Secp256k1::new(); - let external_public_xkey = external_secret_xkey.as_public(&secp).unwrap(); - let internal_public_xkey = internal_secret_xkey.as_public(&secp).unwrap(); + let external_public_xkey = external_secret_xkey.to_public(&secp).unwrap(); + let internal_public_xkey = internal_secret_xkey.to_public(&secp).unwrap(); let signing_external_descriptor = descriptor!(wpkh(external_secret_xkey)).unwrap(); let signing_internal_descriptor = descriptor!(wpkh(internal_secret_xkey)).unwrap(); diff --git a/src/blockchain/any.rs b/src/blockchain/any.rs index 5ef1a3385..3138d0253 100644 --- a/src/blockchain/any.rs +++ b/src/blockchain/any.rs @@ -194,7 +194,7 @@ impl_from!(boxed rpc::RpcBlockchain, AnyBlockchain, Rpc, #[cfg(feature = "rpc")] /// ); /// # } /// ``` -#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum AnyBlockchainConfig { #[cfg(feature = "electrum")] diff --git a/src/blockchain/compact_filters/mod.rs b/src/blockchain/compact_filters/mod.rs index 7ca78a2c3..9b47df9cf 100644 --- a/src/blockchain/compact_filters/mod.rs +++ b/src/blockchain/compact_filters/mod.rs @@ -479,7 +479,7 @@ impl WalletSync for CompactFiltersBlockchain { } /// Data to connect to a Bitcoin P2P peer -#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)] pub struct BitcoinPeerConfig { /// Peer address such as 127.0.0.1:18333 pub address: String, @@ -490,7 +490,7 @@ pub struct BitcoinPeerConfig { } /// Configuration for a [`CompactFiltersBlockchain`] -#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)] pub struct CompactFiltersBlockchainConfig { /// List of peers to try to connect to for asking headers and filters pub peers: Vec, diff --git a/src/blockchain/compact_filters/peer.rs b/src/blockchain/compact_filters/peer.rs index 413ea1697..665a033d4 100644 --- a/src/blockchain/compact_filters/peer.rs +++ b/src/blockchain/compact_filters/peer.rs @@ -75,7 +75,10 @@ impl Mempool { /// Look-up a transaction in the mempool given an [`Inventory`] request pub fn get_tx(&self, inventory: &Inventory) -> Option { let identifer = match inventory { - Inventory::Error | Inventory::Block(_) | Inventory::WitnessBlock(_) => return None, + Inventory::Error + | Inventory::Block(_) + | Inventory::WitnessBlock(_) + | Inventory::CompactBlock(_) => return None, Inventory::Transaction(txid) => TxIdentifier::Txid(*txid), Inventory::WitnessTransaction(txid) => TxIdentifier::Txid(*txid), Inventory::WTx(wtxid) => TxIdentifier::Wtxid(*wtxid), diff --git a/src/blockchain/compact_filters/store.rs b/src/blockchain/compact_filters/store.rs index bb42a9c07..9d5731009 100644 --- a/src/blockchain/compact_filters/store.rs +++ b/src/blockchain/compact_filters/store.rs @@ -103,42 +103,42 @@ where } impl Encodable for BundleStatus { - fn consensus_encode(&self, mut e: W) -> Result { + fn consensus_encode(&self, e: &mut W) -> Result { let mut written = 0; match self { BundleStatus::Init => { - written += 0x00u8.consensus_encode(&mut e)?; + written += 0x00u8.consensus_encode(e)?; } BundleStatus::CfHeaders { cf_headers } => { - written += 0x01u8.consensus_encode(&mut e)?; - written += VarInt(cf_headers.len() as u64).consensus_encode(&mut e)?; + written += 0x01u8.consensus_encode(e)?; + written += VarInt(cf_headers.len() as u64).consensus_encode(e)?; for header in cf_headers { - written += header.consensus_encode(&mut e)?; + written += header.consensus_encode(e)?; } } BundleStatus::CFilters { cf_filters } => { - written += 0x02u8.consensus_encode(&mut e)?; - written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?; + written += 0x02u8.consensus_encode(e)?; + written += VarInt(cf_filters.len() as u64).consensus_encode(e)?; for filter in cf_filters { - written += filter.consensus_encode(&mut e)?; + written += filter.consensus_encode(e)?; } } BundleStatus::Processed { cf_filters } => { - written += 0x03u8.consensus_encode(&mut e)?; - written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?; + written += 0x03u8.consensus_encode(e)?; + written += VarInt(cf_filters.len() as u64).consensus_encode(e)?; for filter in cf_filters { - written += filter.consensus_encode(&mut e)?; + written += filter.consensus_encode(e)?; } } BundleStatus::Pruned => { - written += 0x04u8.consensus_encode(&mut e)?; + written += 0x04u8.consensus_encode(e)?; } BundleStatus::Tip { cf_filters } => { - written += 0x05u8.consensus_encode(&mut e)?; - written += VarInt(cf_filters.len() as u64).consensus_encode(&mut e)?; + written += 0x05u8.consensus_encode(e)?; + written += VarInt(cf_filters.len() as u64).consensus_encode(e)?; for filter in cf_filters { - written += filter.consensus_encode(&mut e)?; + written += filter.consensus_encode(e)?; } } } @@ -148,51 +148,53 @@ impl Encodable for BundleStatus { } impl Decodable for BundleStatus { - fn consensus_decode(mut d: D) -> Result { - let byte_type = u8::consensus_decode(&mut d)?; + fn consensus_decode( + d: &mut D, + ) -> Result { + let byte_type = u8::consensus_decode(d)?; match byte_type { 0x00 => Ok(BundleStatus::Init), 0x01 => { - let num = VarInt::consensus_decode(&mut d)?; + let num = VarInt::consensus_decode(d)?; let num = num.0 as usize; let mut cf_headers = Vec::with_capacity(num); for _ in 0..num { - cf_headers.push(FilterHeader::consensus_decode(&mut d)?); + cf_headers.push(FilterHeader::consensus_decode(d)?); } Ok(BundleStatus::CfHeaders { cf_headers }) } 0x02 => { - let num = VarInt::consensus_decode(&mut d)?; + let num = VarInt::consensus_decode(d)?; let num = num.0 as usize; let mut cf_filters = Vec::with_capacity(num); for _ in 0..num { - cf_filters.push(Vec::::consensus_decode(&mut d)?); + cf_filters.push(Vec::::consensus_decode(d)?); } Ok(BundleStatus::CFilters { cf_filters }) } 0x03 => { - let num = VarInt::consensus_decode(&mut d)?; + let num = VarInt::consensus_decode(d)?; let num = num.0 as usize; let mut cf_filters = Vec::with_capacity(num); for _ in 0..num { - cf_filters.push(Vec::::consensus_decode(&mut d)?); + cf_filters.push(Vec::::consensus_decode(d)?); } Ok(BundleStatus::Processed { cf_filters }) } 0x04 => Ok(BundleStatus::Pruned), 0x05 => { - let num = VarInt::consensus_decode(&mut d)?; + let num = VarInt::consensus_decode(d)?; let num = num.0 as usize; let mut cf_filters = Vec::with_capacity(num); for _ in 0..num { - cf_filters.push(Vec::::consensus_decode(&mut d)?); + cf_filters.push(Vec::::consensus_decode(d)?); } Ok(BundleStatus::Tip { cf_filters }) @@ -276,7 +278,11 @@ impl ChainStore { } pub fn start_snapshot(&self, from: usize) -> Result, CompactFiltersError> { - let new_cf_name: String = thread_rng().sample_iter(&Alphanumeric).take(16).collect(); + let new_cf_name: String = thread_rng() + .sample_iter(&Alphanumeric) + .map(|byte| byte as char) + .take(16) + .collect(); let new_cf_name = format!("_headers:{}", new_cf_name); let mut write_store = self.store.write().unwrap(); @@ -647,7 +653,7 @@ impl CfStore { &first_key, ( BundleStatus::Init, - filter.filter_header(&FilterHeader::from_hash(Default::default())), + filter.filter_header(&FilterHeader::from_hash(Hash::all_zeros())), ) .serialize(), )?; diff --git a/src/blockchain/compact_filters/sync.rs b/src/blockchain/compact_filters/sync.rs index b12268dd2..a67b5705f 100644 --- a/src/blockchain/compact_filters/sync.rs +++ b/src/blockchain/compact_filters/sync.rs @@ -14,6 +14,7 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use bitcoin::hash_types::{BlockHash, FilterHeader}; +use bitcoin::hashes::Hash; use bitcoin::network::message::NetworkMessage; use bitcoin::network::message_blockdata::GetHeadersMessage; use bitcoin::util::bip158::BlockFilter; @@ -254,7 +255,7 @@ where peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new( locators_vec, - Default::default(), + Hash::all_zeros(), )))?; let (mut snapshot, mut last_hash) = if let NetworkMessage::Headers(headers) = peer .recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))? @@ -276,7 +277,7 @@ where while sync_height < peer.get_version().start_height as usize { peer.send(NetworkMessage::GetHeaders(GetHeadersMessage::new( vec![last_hash], - Default::default(), + Hash::all_zeros(), )))?; if let NetworkMessage::Headers(headers) = peer .recv("headers", Some(Duration::from_secs(TIMEOUT_SECS)))? diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index fdb10b470..54381241a 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -296,7 +296,7 @@ impl<'a, 'b, D: Database> TxCache<'a, 'b, D> { } /// Configuration for an [`ElectrumBlockchain`] -#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)] pub struct ElectrumBlockchainConfig { /// URL of the Electrum server (such as ElectrumX, Esplora, BWT) may start with `ssl://` or `tcp://` and include a port /// diff --git a/src/blockchain/esplora/async.rs b/src/blockchain/esplora/async.rs index 5ddbdeb41..900d95376 100644 --- a/src/blockchain/esplora/async.rs +++ b/src/blockchain/esplora/async.rs @@ -125,8 +125,9 @@ impl GetTx for EsploraBlockchain { #[maybe_async] impl GetBlockHash for EsploraBlockchain { fn get_block_hash(&self, height: u64) -> Result { - let block_header = await_or_block!(self.url_client.get_header(height as u32))?; - Ok(block_header.block_hash()) + Ok(await_or_block!(self + .url_client + .get_block_hash(height as u32))?) } } diff --git a/src/blockchain/esplora/blocking.rs b/src/blockchain/esplora/blocking.rs index 1e9d1cfcd..768573c3f 100644 --- a/src/blockchain/esplora/blocking.rs +++ b/src/blockchain/esplora/blocking.rs @@ -110,8 +110,7 @@ impl GetTx for EsploraBlockchain { impl GetBlockHash for EsploraBlockchain { fn get_block_hash(&self, height: u64) -> Result { - let block_header = self.url_client.get_header(height as u32)?; - Ok(block_header.block_hash()) + Ok(self.url_client.get_block_hash(height as u32)?) } } diff --git a/src/blockchain/esplora/mod.rs b/src/blockchain/esplora/mod.rs index 57032e49d..c4308406b 100644 --- a/src/blockchain/esplora/mod.rs +++ b/src/blockchain/esplora/mod.rs @@ -33,7 +33,7 @@ mod blocking; pub use self::blocking::*; /// Configuration for an [`EsploraBlockchain`] -#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)] pub struct EsploraBlockchainConfig { /// Base URL of the esplora service /// diff --git a/src/blockchain/rpc.rs b/src/blockchain/rpc.rs index b2c64ba5a..d6a74d9c3 100644 --- a/src/blockchain/rpc.rs +++ b/src/blockchain/rpc.rs @@ -77,7 +77,7 @@ impl Deref for RpcBlockchain { } /// RpcBlockchain configuration options -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct RpcConfig { /// The bitcoin node url pub url: String, @@ -96,7 +96,7 @@ pub struct RpcConfig { /// In general, BDK tries to sync `scriptPubKey`s cached in [`crate::database::Database`] with /// `scriptPubKey`s imported in the Bitcoin Core Wallet. These parameters are used for determining /// how the `importdescriptors` RPC calls are to be made. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct RpcSyncParams { /// The minimum number of scripts to scan for on initial sync. pub start_script_count: usize, @@ -167,7 +167,7 @@ impl Blockchain for RpcBlockchain { .estimate_smart_fee(target as u16, None)? .fee_rate .ok_or(Error::FeeRateUnavailable)? - .as_sat() as f64; + .to_sat() as f64; Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32)) } @@ -410,7 +410,12 @@ impl<'a, D: BatchDatabase> DbState<'a, D> { updated = true; TransactionDetails { txid: tx_res.info.txid, - ..Default::default() + transaction: None, + + received: 0, + sent: 0, + fee: None, + confirmation_time: None, } }); @@ -430,7 +435,7 @@ impl<'a, D: BatchDatabase> DbState<'a, D> { // update fee (if needed) if let (None, Some(new_fee)) = (db_tx.fee, tx_res.detail.fee) { updated = true; - db_tx.fee = Some(new_fee.as_sat().unsigned_abs()); + db_tx.fee = Some(new_fee.to_sat().unsigned_abs()); } // update confirmation time (if needed) @@ -603,7 +608,7 @@ impl<'a, D: BatchDatabase> DbState<'a, D> { LocalUtxo { outpoint: OutPoint::new(entry.txid, entry.vout), txout: TxOut { - value: entry.amount.as_sat(), + value: entry.amount.to_sat(), script_pubkey: entry.script_pub_key, }, keychain, @@ -873,15 +878,13 @@ impl BlockchainFactory for RpcBlockchainFactory { mod test { use super::*; use crate::{ - descriptor::{into_wallet_descriptor_checked, AsDerived}, - testutils::blockchain_tests::TestClient, + descriptor::into_wallet_descriptor_checked, testutils::blockchain_tests::TestClient, wallet::utils::SecpCtx, }; use bitcoin::{Address, Network}; use bitcoincore_rpc::RpcApi; use log::LevelFilter; - use miniscript::DescriptorTrait; crate::bdk_blockchain_tests! { fn test_instance(test_client: &TestClient) -> RpcBlockchain { @@ -958,7 +961,7 @@ mod test { // generate scripts (1 tx per script) let scripts = (0..TX_COUNT) - .map(|index| desc.as_derived(index, &secp).script_pubkey()) + .map(|index| desc.at_derivation_index(index).script_pubkey()) .collect::>(); // import scripts and wait diff --git a/src/database/memory.rs b/src/database/memory.rs index 691e7eb16..6cfca6fc9 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -497,7 +497,7 @@ macro_rules! populate_test_db { } let tx = $crate::bitcoin::Transaction { version: 1, - lock_time: 0, + lock_time: bitcoin::PackedLockTime(0), input, output: tx_meta .output diff --git a/src/descriptor/derived.rs b/src/descriptor/derived.rs deleted file mode 100644 index 585c39749..000000000 --- a/src/descriptor/derived.rs +++ /dev/null @@ -1,210 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 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. - -//! 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; -use std::hash::{Hash, Hasher}; -use std::ops::Deref; - -use bitcoin::hashes::hash160; -use bitcoin::{PublicKey, XOnlyPublicKey}; - -use miniscript::descriptor::{DescriptorSinglePub, SinglePubKey, Wildcard}; -use miniscript::{Descriptor, DescriptorPublicKey, MiniscriptKey, ToPublicKey, TranslatePk}; - -use crate::wallet::utils::SecpCtx; - -/// Extended [`DescriptorPublicKey`] that has been derived -/// -/// Derived keys are guaranteed to never contain wildcards of any kind -#[derive(Debug, Clone)] -pub struct DerivedDescriptorKey<'s>(DescriptorPublicKey, &'s SecpCtx); - -impl<'s> DerivedDescriptorKey<'s> { - /// Construct a new derived key - /// - /// Panics if the key is wildcard - pub fn new(key: DescriptorPublicKey, secp: &'s SecpCtx) -> DerivedDescriptorKey<'s> { - if let DescriptorPublicKey::XPub(xpub) = &key { - assert!(xpub.wildcard == Wildcard::None) - } - - DerivedDescriptorKey(key, secp) - } -} - -impl<'s> Deref for DerivedDescriptorKey<'s> { - type Target = DescriptorPublicKey; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'s> PartialEq for DerivedDescriptorKey<'s> { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl<'s> Eq for DerivedDescriptorKey<'s> {} - -impl<'s> PartialOrd for DerivedDescriptorKey<'s> { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.partial_cmp(&other.0) - } -} - -impl<'s> Ord for DerivedDescriptorKey<'s> { - fn cmp(&self, other: &Self) -> Ordering { - self.0.cmp(&other.0) - } -} - -impl<'s> fmt::Display for DerivedDescriptorKey<'s> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl<'s> Hash for DerivedDescriptorKey<'s> { - fn hash(&self, state: &mut H) { - self.0.hash(state); - } -} - -impl<'s> MiniscriptKey for DerivedDescriptorKey<'s> { - type Hash = Self; - - fn to_pubkeyhash(&self) -> Self::Hash { - DerivedDescriptorKey(self.0.to_pubkeyhash(), self.1) - } - - fn is_uncompressed(&self) -> bool { - self.0.is_uncompressed() - } -} - -impl<'s> ToPublicKey for DerivedDescriptorKey<'s> { - fn to_public_key(&self) -> PublicKey { - match &self.0 { - DescriptorPublicKey::SinglePub(DescriptorSinglePub { - key: SinglePubKey::XOnly(_), - .. - }) => panic!("Found x-only public key in non-tr descriptor"), - DescriptorPublicKey::SinglePub(DescriptorSinglePub { - key: SinglePubKey::FullKey(ref pk), - .. - }) => *pk, - DescriptorPublicKey::XPub(ref xpub) => PublicKey::new( - xpub.xkey - .derive_pub(self.1, &xpub.derivation_path) - .expect("Shouldn't fail, only normal derivations") - .public_key, - ), - } - } - - fn to_x_only_pubkey(&self) -> XOnlyPublicKey { - match &self.0 { - DescriptorPublicKey::SinglePub(DescriptorSinglePub { - key: SinglePubKey::XOnly(ref pk), - .. - }) => *pk, - DescriptorPublicKey::SinglePub(DescriptorSinglePub { - key: SinglePubKey::FullKey(ref pk), - .. - }) => XOnlyPublicKey::from(pk.inner), - DescriptorPublicKey::XPub(ref xpub) => XOnlyPublicKey::from( - xpub.xkey - .derive_pub(self.1, &xpub.derivation_path) - .expect("Shouldn't fail, only normal derivations") - .public_key, - ), - } - } - - fn hash_to_hash160(hash: &Self::Hash) -> hash160::Hash { - hash.to_public_key().to_pubkeyhash() - } -} - -/// 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 - fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor>; -} - -impl AsDerived for Descriptor { - fn as_derived<'s>( - &self, - index: u32, - secp: &'s SecpCtx, - ) -> Descriptor> { - self.derive(index).translate_pk_infallible( - |key| DerivedDescriptorKey::new(key.clone(), secp), - |key| DerivedDescriptorKey::new(key.clone(), secp), - ) - } - - fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor> { - assert!(!self.is_deriveable()); - - self.as_derived(0, secp) - } -} diff --git a/src/descriptor/dsl.rs b/src/descriptor/dsl.rs index 2d0d9422d..67ef67057 100644 --- a/src/descriptor/dsl.rs +++ b/src/descriptor/dsl.rs @@ -700,10 +700,10 @@ macro_rules! fragment { $crate::keys::make_pkh($key, &secp) }); ( after ( $value:expr ) ) => ({ - $crate::impl_leaf_opcode_value!(After, $value) + $crate::impl_leaf_opcode_value!(After, $crate::bitcoin::PackedLockTime($value)) // TODO!! https://github.com/rust-bitcoin/rust-bitcoin/issues/1302 }); ( older ( $value:expr ) ) => ({ - $crate::impl_leaf_opcode_value!(Older, $value) + $crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!! }); ( sha256 ( $hash:expr ) ) => ({ $crate::impl_leaf_opcode_value!(Sha256, $hash) @@ -795,7 +795,7 @@ macro_rules! fragment { mod test { use bitcoin::hashes::hex::ToHex; use bitcoin::secp256k1::Secp256k1; - use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap}; + use miniscript::descriptor::{DescriptorPublicKey, KeyMap}; use miniscript::{Descriptor, Legacy, Segwitv0}; use std::str::FromStr; @@ -806,8 +806,6 @@ mod test { use bitcoin::util::bip32; use bitcoin::PrivateKey; - use crate::descriptor::derived::AsDerived; - // test the descriptor!() macro // verify descriptor generates expected script(s) (if bare or pk) or address(es) @@ -817,17 +815,15 @@ mod test { is_fixed: bool, expected: &[&str], ) { - let secp = Secp256k1::new(); - let (desc, _key_map, _networks) = desc.unwrap(); assert_eq!(desc.is_witness(), is_witness); - assert_eq!(!desc.is_deriveable(), is_fixed); + assert_eq!(!desc.has_wildcard(), is_fixed); for i in 0..expected.len() { let index = i as u32; - let child_desc = if !desc.is_deriveable() { - desc.as_derived_fixed(&secp) + let child_desc = if !desc.has_wildcard() { + desc.at_derivation_index(0) } else { - desc.as_derived(index, &secp) + desc.at_derivation_index(index) }; let address = child_desc.address(Regtest); if let Ok(address) = address { diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 802ccd19c..c8f4d29dc 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -15,24 +15,22 @@ //! from [`miniscript`]. use std::collections::BTreeMap; -use std::ops::Deref; use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource}; use bitcoin::util::{psbt, taproot}; use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey}; -use bitcoin::{Network, Script, TxOut}; +use bitcoin::{Network, TxOut}; -use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey}; +use miniscript::descriptor::{DefiniteDescriptorKey, DescriptorType, InnerXKey, SinglePubKey}; pub use miniscript::{ descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor, DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0, }; -use miniscript::{DescriptorTrait, ForEachKey, TranslatePk}; +use miniscript::{ForEachKey, MiniscriptKey, TranslatePk}; use crate::descriptor::policy::BuildSatisfaction; pub mod checksum; -pub mod derived; #[doc(hidden)] pub mod dsl; pub mod error; @@ -40,7 +38,6 @@ pub mod policy; pub mod template; pub use self::checksum::get_checksum; -pub use self::derived::{AsDerived, DerivedDescriptorKey}; pub use self::error::Error as DescriptorError; pub use self::policy::Policy; use self::template::DescriptorTemplateOut; @@ -52,7 +49,7 @@ use crate::wallet::utils::SecpCtx; pub type ExtendedDescriptor = Descriptor; /// Alias for a [`Descriptor`] that contains extended **derived** keys -pub type DerivedDescriptor<'s> = Descriptor>; +pub type DerivedDescriptor = Descriptor; /// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or /// [`psbt::Output`] @@ -132,28 +129,76 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) { ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { use crate::keys::DescriptorKey; - let check_key = |pk: &DescriptorPublicKey| { - let (pk, _, networks) = if self.0.is_witness() { - let descriptor_key: DescriptorKey = - pk.clone().into_descriptor_key()?; - descriptor_key.extract(secp)? - } else { - let descriptor_key: DescriptorKey = - pk.clone().into_descriptor_key()?; - descriptor_key.extract(secp)? - }; - - if networks.contains(&network) { - Ok(pk) - } else { - Err(DescriptorError::Key(KeyError::InvalidNetwork)) + struct Translator<'s, 'd> { + secp: &'s SecpCtx, + descriptor: &'d ExtendedDescriptor, + network: Network, + } + + impl<'s, 'd> + miniscript::Translator + for Translator<'s, 'd> + { + fn pk( + &mut self, + pk: &DescriptorPublicKey, + ) -> Result { + let secp = &self.secp; + + let (_, _, networks) = if self.descriptor.is_taproot() { + let descriptor_key: DescriptorKey = + pk.clone().into_descriptor_key()?; + descriptor_key.extract(secp)? + } else if self.descriptor.is_witness() { + let descriptor_key: DescriptorKey = + pk.clone().into_descriptor_key()?; + descriptor_key.extract(secp)? + } else { + let descriptor_key: DescriptorKey = + pk.clone().into_descriptor_key()?; + descriptor_key.extract(secp)? + }; + + if networks.contains(&self.network) { + Ok(miniscript::DummyKey) + } else { + Err(DescriptorError::Key(KeyError::InvalidNetwork)) + } } - }; + fn sha256( + &mut self, + _sha256: &::Sha256, + ) -> Result { + Ok(Default::default()) + } + fn hash256( + &mut self, + _hash256: &::Hash256, + ) -> Result { + Ok(Default::default()) + } + fn ripemd160( + &mut self, + _ripemd160: &::Ripemd160, + ) -> Result { + Ok(Default::default()) + } + fn hash160( + &mut self, + _hash160: &::Hash160, + ) -> Result { + Ok(Default::default()) + } + } // check the network for the keys - let translated = self.0.translate_pk(check_key, check_key)?; + self.0.translate_pk(&mut Translator { + secp, + network, + descriptor: &self.0, + })?; - Ok((translated, self.1)) + Ok(self) } } @@ -163,10 +208,17 @@ impl IntoWalletDescriptor for DescriptorTemplateOut { _secp: &SecpCtx, network: Network, ) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> { - let valid_networks = &self.2; + struct Translator { + network: Network, + } - let fix_key = |pk: &DescriptorPublicKey| { - if valid_networks.contains(&network) { + impl miniscript::Translator + for Translator + { + fn pk( + &mut self, + pk: &DescriptorPublicKey, + ) -> Result { // 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 @@ -175,7 +227,7 @@ impl IntoWalletDescriptor for DescriptorTemplateOut { let pk = match pk { DescriptorPublicKey::XPub(ref xpub) => { let mut xpub = xpub.clone(); - xpub.xkey.network = network; + xpub.xkey.network = self.network; DescriptorPublicKey::XPub(xpub) } @@ -183,13 +235,20 @@ impl IntoWalletDescriptor for DescriptorTemplateOut { }; Ok(pk) - } else { - Err(DescriptorError::Key(KeyError::InvalidNetwork)) } - }; + miniscript::translate_hash_clone!( + DescriptorPublicKey, + DescriptorPublicKey, + DescriptorError + ); + } + + if !self.2.contains(&network) { + return Err(DescriptorError::Key(KeyError::InvalidNetwork)); + } // fixup the network for keys that need it - let translated = self.0.translate_pk(fix_key, fix_key)?; + let translated = self.0.translate_pk(&mut Translator { network })?; Ok((translated, self.1)) } @@ -210,7 +269,7 @@ pub(crate) fn into_wallet_descriptor_checked( derivation_path, wildcard, .. - }) = k.as_key() + }) = k { return *wildcard == Wildcard::Hardened || derivation_path.into_iter().any(ChildNumber::is_hardened); @@ -257,7 +316,6 @@ pub trait ExtractPolicy { } pub(crate) trait XKeyUtils { - fn full_path(&self, append: &[ChildNumber]) -> DerivationPath; fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint; } @@ -265,27 +323,6 @@ impl XKeyUtils for DescriptorXKey where T: InnerXKey, { - fn full_path(&self, append: &[ChildNumber]) -> DerivationPath { - let full_path = match self.origin { - Some((_, ref path)) => path - .into_iter() - .chain(self.derivation_path.into_iter()) - .cloned() - .collect(), - None => self.derivation_path.clone(), - }; - - if self.wildcard != Wildcard::None { - full_path - .into_iter() - .chain(append.iter()) - .cloned() - .collect() - } else { - full_path - } - } - fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint { match self.origin { Some((fingerprint, _)) => fingerprint, @@ -294,11 +331,6 @@ where } } -pub(crate) trait DerivedDescriptorMeta { - fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths; - fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins; -} - pub(crate) trait DescriptorMeta { fn is_witness(&self) -> bool; fn is_taproot(&self) -> bool; @@ -307,63 +339,23 @@ pub(crate) trait DescriptorMeta { &self, hd_keypaths: &HdKeyPaths, secp: &'s SecpCtx, - ) -> Option>; + ) -> Option; fn derive_from_tap_key_origins<'s>( &self, tap_key_origins: &TapKeyOrigins, secp: &'s SecpCtx, - ) -> Option>; + ) -> Option; fn derive_from_psbt_key_origins<'s>( &self, key_origins: BTreeMap, secp: &'s SecpCtx, - ) -> Option>; + ) -> Option; fn derive_from_psbt_input<'s>( &self, psbt_input: &psbt::Input, utxo: Option, secp: &'s SecpCtx, - ) -> Option>; -} - -pub(crate) trait DescriptorScripts { - fn psbt_redeem_script(&self) -> Option