Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for BIP47 (reusable payment codes) #574

Closed
wants to merge 9 commits into from
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ 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.
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).

### Sync API change

Expand Down Expand Up @@ -437,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
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...HEAD
2 changes: 2 additions & 0 deletions src/blockchain/electrum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ impl Blockchain for ElectrumBlockchain {
}
}

impl StatelessBlockchain for ElectrumBlockchain {}

impl GetHeight for ElectrumBlockchain {
fn get_height(&self) -> Result<u32, Error> {
// TODO: unsubscribe when added to the client, or is there a better call to use here?
Expand Down
2 changes: 2 additions & 0 deletions src/blockchain/esplora/reqwest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ impl Blockchain for EsploraBlockchain {
}
}

impl StatelessBlockchain for EsploraBlockchain {}

#[maybe_async]
impl GetHeight for EsploraBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Expand Down
2 changes: 2 additions & 0 deletions src/blockchain/esplora/ureq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ impl Blockchain for EsploraBlockchain {
}
}

impl StatelessBlockchain for EsploraBlockchain {}

impl GetHeight for EsploraBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(self.url_client._get_height()?)
Expand Down
55 changes: 55 additions & 0 deletions src/blockchain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,61 @@ pub trait ConfigurableBlockchain: Blockchain + Sized {
fn from_config(config: &Self::Config) -> Result<Self, Error>;
}

/// 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<T>` 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<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
/// Ok(wallets
/// .iter()
/// .map(|w| -> Result<_, Error> {
/// w.sync(&blockchain_factory.build("wallet_1", None)?, SyncOptions::default())?;
/// w.get_balance()
/// })
/// .collect::<Result<Vec<_>, _>>()?
/// .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<u32>,
) -> Result<Self::Inner, Error>;
}

impl<T: StatelessBlockchain> BlockchainFactory for Arc<T> {
type Inner = Self;

fn build(&self, _checksum: &str, _override_skip_blocks: Option<u32>) -> Result<Self, Error> {
Ok(Arc::clone(self))
}
}

/// Data sent with a progress update over a [`channel`]
pub type ProgressData = (f32, Option<String>);

Expand Down
97 changes: 97 additions & 0 deletions src/blockchain/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,57 @@ fn list_wallet_dir(client: &Client) -> Result<Vec<String>, 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<dyn std::error::Error>> {
/// 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<u32>,
) -> Result<Self::Inner, Error> {
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! {
Expand All @@ -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"
);
}
}
55 changes: 46 additions & 9 deletions src/descriptor/derived.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DerivedDescriptorKey>` 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<dyn std::error::Error>>(())
//! ```
//!
//! [`Wallet::get_signers`]: crate::wallet::Wallet::get_signers

use std::cmp::Ordering;
use std::fmt;
Expand All @@ -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;
Expand Down Expand Up @@ -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<DerivedDescriptorKey<'s>>;

// 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<DerivedDescriptorKey<'s>>;
}

Expand Down
12 changes: 6 additions & 6 deletions src/descriptor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,25 @@ 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;
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;
Expand Down
28 changes: 19 additions & 9 deletions src/keys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -94,6 +94,13 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
}
}

pub fn as_public(&self, secp: &SecpCtx) -> Result<DescriptorPublicKey, KeyError> {
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.
Expand All @@ -102,18 +109,15 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
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))
}
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading