Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions wallet/src/descriptor/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ pub enum Error {
InvalidDescriptorChecksum,
/// The descriptor contains hardened derivation steps on public extended keys
HardenedDerivationXpub,
/// The descriptor contains multipath keys
/// The descriptor contains multipath keys with an invalid number of paths (must have exactly 2
/// paths for receive and change)
MultiPath,
/// Error thrown while working with [`keys`](crate::keys)
Key(crate::keys::KeyError),
Expand Down Expand Up @@ -68,7 +69,7 @@ impl fmt::Display for Error {
),
Self::MultiPath => write!(
f,
"The descriptor contains multipath keys, which are not supported yet"
"The descriptor contains multipath keys with invalid number of paths (must have exactly 2 paths for receive and change)"
),
Self::Key(err) => write!(f, "Key error: {}", err),
Self::Policy(err) => write!(f, "Policy error: {}", err),
Expand Down
15 changes: 13 additions & 2 deletions wallet/src/descriptor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
//! This module contains generic utilities to work with descriptors, plus some re-exported types
//! from [`miniscript`].

use crate::alloc::string::ToString;
use crate::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
Expand Down Expand Up @@ -312,7 +313,11 @@ pub(crate) fn check_wallet_descriptor(
}

if descriptor.is_multipath() {
return Err(DescriptorError::MultiPath);
return Err(DescriptorError::Miniscript(
miniscript::Error::BadDescriptor(
"`check_wallet_descriptor` must not contain multipath keys".to_string(),
),
));
}

// Run miniscript's sanity check, which will look for duplicated keys and other potential
Expand Down Expand Up @@ -875,13 +880,19 @@ mod test {

assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub));

// Any multipath descriptor should fail
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/<0;1>/*)";
let (descriptor, _) = descriptor
.into_wallet_descriptor(&secp, Network::Testnet)
.expect("must parse");
let result = check_wallet_descriptor(&descriptor);

assert_matches!(result, Err(DescriptorError::MultiPath));
assert_matches!(
result,
Err(DescriptorError::Miniscript(
miniscript::Error::BadDescriptor(_)
))
);

// repeated pubkeys
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
Expand Down
94 changes: 94 additions & 0 deletions wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,45 @@ impl Wallet {
CreateParams::new(descriptor, change_descriptor)
}

/// Build a new [`Wallet`] from a two-path descriptor.
///
/// This function parses a multipath descriptor with exactly 2 paths and creates a wallet
/// using the existing receive and change wallet creation logic.
///
/// Multipath descriptors follow [BIP 389] and allow defining both receive and change
/// derivation paths in a single descriptor using the `<0;1>` syntax.
///
/// If you have previously created a wallet, use [`load`](Self::load) instead.
///
/// # Errors
/// Returns an error if the descriptor is invalid or not a 2-path multipath descriptor.
///
/// # Synopsis
///
/// ```rust
/// # use bdk_wallet::Wallet;
/// # use bitcoin::Network;
/// # use bdk_wallet::KeychainKind;
/// # const TWO_PATH_DESC: &str = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)";
/// let wallet = Wallet::create_from_two_path_descriptor(TWO_PATH_DESC)
/// .network(Network::Testnet)
/// .create_wallet_no_persist()
/// .unwrap();
///
/// // The multipath descriptor automatically creates separate receive and change descriptors
/// let receive_addr = wallet.peek_address(KeychainKind::External, 0); // Uses path /0/*
/// let change_addr = wallet.peek_address(KeychainKind::Internal, 0); // Uses path /1/*
/// assert_ne!(receive_addr.address, change_addr.address);
/// ```
///
/// [BIP 389]: https://github.com/bitcoin/bips/blob/master/bip-0389.mediawiki
pub fn create_from_two_path_descriptor<D>(two_path_descriptor: D) -> CreateParams
where
D: IntoWalletDescriptor + Send + Clone + 'static,
{
CreateParams::new_two_path(two_path_descriptor)
}

/// Create a new [`Wallet`] with given `params`.
///
/// Refer to [`Wallet::create`] for more.
Expand Down Expand Up @@ -2765,4 +2804,59 @@ mod test {

assert_eq!(expected, received);
}

#[test]
fn test_create_two_path_wallet() {
let two_path_descriptor = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)";

// Test successful creation of a two-path wallet
let params = Wallet::create_from_two_path_descriptor(two_path_descriptor);
let wallet = params.network(Network::Testnet).create_wallet_no_persist();
assert!(wallet.is_ok());

let wallet = wallet.unwrap();

// Verify that the wallet has both external and internal keychains
let keychains: Vec<_> = wallet.keychains().collect();
assert_eq!(keychains.len(), 2);

// Verify that the descriptors are different (receive vs change)
let external_desc = keychains
.iter()
.find(|(k, _)| *k == KeychainKind::External)
.unwrap()
.1;
let internal_desc = keychains
.iter()
.find(|(k, _)| *k == KeychainKind::Internal)
.unwrap()
.1;
assert_ne!(external_desc.to_string(), internal_desc.to_string());

// Verify that addresses can be generated
let external_addr = wallet.peek_address(KeychainKind::External, 0);
let internal_addr = wallet.peek_address(KeychainKind::Internal, 0);
assert_ne!(external_addr.address, internal_addr.address);
}

#[test]
fn test_create_two_path_wallet_invalid_descriptor() {
// Test with invalid single-path descriptor
let single_path_descriptor = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/0/*)";
let params = Wallet::create_from_two_path_descriptor(single_path_descriptor);
let wallet = params.network(Network::Testnet).create_wallet_no_persist();
assert!(matches!(wallet, Err(DescriptorError::MultiPath)));

// Test with invalid 3-path multipath descriptor
let three_path_descriptor = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1;2>/*)";
let params = Wallet::create_from_two_path_descriptor(three_path_descriptor);
let wallet = params.network(Network::Testnet).create_wallet_no_persist();
assert!(matches!(wallet, Err(DescriptorError::MultiPath)));

// Test with completely invalid descriptor
let invalid_descriptor = "invalid_descriptor";
let params = Wallet::create_from_two_path_descriptor(invalid_descriptor);
let wallet = params.network(Network::Testnet).create_wallet_no_persist();
assert!(wallet.is_err());
}
}
51 changes: 51 additions & 0 deletions wallet/src/wallet/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,32 @@ use crate::{

use super::{ChangeSet, LoadError, PersistedWallet};

fn make_two_path_descriptor_to_extract<D>(
two_path_descriptor: D,
index: usize,
) -> DescriptorToExtract
where
D: IntoWalletDescriptor + Send + 'static,
{
Box::new(move |secp, network| {
let (desc, keymap) = two_path_descriptor.into_wallet_descriptor(secp, network)?;

if !desc.is_multipath() {
return Err(DescriptorError::MultiPath);
}

let descriptors = desc
.into_single_descriptors()
.map_err(|_| DescriptorError::MultiPath)?;

if descriptors.len() != 2 {
return Err(DescriptorError::MultiPath);
}

Ok((descriptors[index].clone(), keymap))
})
}

/// This atrocity is to avoid having type parameters on [`CreateParams`] and [`LoadParams`].
///
/// The better option would be to do `Box<dyn IntoWalletDescriptor>`, but we cannot due to Rust's
Expand Down Expand Up @@ -88,6 +114,31 @@ impl CreateParams {
}
}

/// Construct parameters with a two-path descriptor that will be parsed into receive and change
/// descriptors.
///
/// This function parses a two-path descriptor (receive and change) and creates parameters
/// using the existing receive and change wallet creation logic.
///
/// Default values:
/// * `network` = [`Network::Bitcoin`]
/// * `genesis_hash` = `None`
/// * `lookahead` = [`DEFAULT_LOOKAHEAD`]
pub fn new_two_path<D: IntoWalletDescriptor + Send + Clone + 'static>(
two_path_descriptor: D,
) -> Self {
Self {
descriptor: make_two_path_descriptor_to_extract(two_path_descriptor.clone(), 0),
descriptor_keymap: KeyMap::default(),
change_descriptor: Some(make_two_path_descriptor_to_extract(two_path_descriptor, 1)),
change_descriptor_keymap: KeyMap::default(),
network: Network::Bitcoin,
genesis_hash: None,
lookahead: DEFAULT_LOOKAHEAD,
use_spk_cache: false,
}
}

/// Extend the given `keychain`'s `keymap`.
pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
match keychain {
Expand Down
Loading