Skip to content

Commit 55b3e1d

Browse files
committed
descriptors: BIP389 multipath descriptors support
This makes it possible to get multiple descriptors out of a multipath descriptor, and test the parsing and detection of multipath descriptors.
1 parent ba110b9 commit 55b3e1d

File tree

2 files changed

+96
-1
lines changed

2 files changed

+96
-1
lines changed

src/descriptor/mod.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,68 @@ impl Descriptor<DescriptorPublicKey> {
727727

728728
Ok(None)
729729
}
730+
731+
/// Whether this descriptor contains a key that has multiple derivation paths.
732+
pub fn is_multipath(&self) -> bool {
733+
self.for_any_key(DescriptorPublicKey::is_multipath)
734+
}
735+
736+
/// Get as many descriptors as different paths in this descriptor.
737+
///
738+
/// For multipath descriptors it will return as many descriptors as there is
739+
/// "parallel" paths. For regular descriptors it will just return itself.
740+
pub fn into_single_descriptors(self) -> Result<Vec<Descriptor<DescriptorPublicKey>>, Error> {
741+
// All single-path descriptors contained in this descriptor.
742+
let mut descriptors = Vec::new();
743+
// We (ab)use `for_any_key` to gather the number of separate descriptors.
744+
if !self.for_any_key(|key| {
745+
// All multipath keys must have the same number of indexes at the "multi-index"
746+
// step. So we can return early if we already populated the vector.
747+
if !descriptors.is_empty() {
748+
return true;
749+
}
750+
751+
match key {
752+
DescriptorPublicKey::Single(..) | DescriptorPublicKey::XPub(..) => false,
753+
DescriptorPublicKey::MultiXPub(xpub) => {
754+
for _ in 0..xpub.derivation_paths.len() {
755+
descriptors.push(self.clone());
756+
}
757+
true
758+
}
759+
}
760+
}) {
761+
// If there is no multipath key, return early.
762+
return Ok(vec![self]);
763+
}
764+
assert!(!descriptors.is_empty());
765+
766+
// Now, transform the multipath key of each descriptor into a single-key using each index.
767+
struct IndexChoser(usize);
768+
impl Translator<DescriptorPublicKey, DescriptorPublicKey, Error> for IndexChoser {
769+
fn pk(&mut self, pk: &DescriptorPublicKey) -> Result<DescriptorPublicKey, Error> {
770+
match pk {
771+
DescriptorPublicKey::Single(..) | DescriptorPublicKey::XPub(..) => {
772+
Ok(pk.clone())
773+
}
774+
DescriptorPublicKey::MultiXPub(_) => pk
775+
.clone()
776+
.into_single_keys()
777+
.get(self.0)
778+
.cloned()
779+
.ok_or(Error::MultipathDescLenMismatch),
780+
}
781+
}
782+
translate_hash_clone!(DescriptorPublicKey, DescriptorPublicKey, Error);
783+
}
784+
785+
for (i, desc) in descriptors.iter_mut().enumerate() {
786+
let mut index_choser = IndexChoser(i);
787+
*desc = desc.translate_pk(&mut index_choser)?;
788+
}
789+
790+
Ok(descriptors)
791+
}
730792
}
731793

732794
impl Descriptor<DefiniteDescriptorKey> {
@@ -1763,4 +1825,32 @@ pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
17631825
Ok(Some((1, expected_concrete)))
17641826
);
17651827
}
1828+
1829+
#[test]
1830+
fn multipath_descriptors() {
1831+
// We can parse a multipath descriptors, and make it into separate single-path descriptors.
1832+
let desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<7';8h;20>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/<0;1;987>/*)))").unwrap();
1833+
assert!(desc.is_multipath());
1834+
assert_eq!(desc.into_single_descriptors().unwrap(), vec![
1835+
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/7'/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/0/*)))").unwrap(),
1836+
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/8h/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/1/*)))").unwrap(),
1837+
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/20/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/987/*)))").unwrap()
1838+
]);
1839+
1840+
// Even if only one of the keys is multipath.
1841+
let desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap();
1842+
assert!(desc.is_multipath());
1843+
assert_eq!(desc.into_single_descriptors().unwrap(), vec![
1844+
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/0/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap(),
1845+
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/1/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap(),
1846+
]);
1847+
1848+
// We can detect regular single-path descriptors.
1849+
let notmulti_desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap();
1850+
assert!(!notmulti_desc.is_multipath());
1851+
assert_eq!(
1852+
notmulti_desc.clone().into_single_descriptors().unwrap(),
1853+
vec![notmulti_desc]
1854+
);
1855+
}
17661856
}

src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,9 @@ pub enum Error {
676676
TrNoScriptCode,
677677
/// No explicit script for Tr descriptors
678678
TrNoExplicitScript,
679+
/// At least two BIP389 key expressions in the descriptor contain tuples of
680+
/// derivation indexes of different lengths.
681+
MultipathDescLenMismatch,
679682
}
680683

681684
// https://github.com/sipa/miniscript/pull/5 for discussion on this number
@@ -749,6 +752,7 @@ impl fmt::Display for Error {
749752
Error::TaprootSpendInfoUnavialable => write!(f, "Taproot Spend Info not computed."),
750753
Error::TrNoScriptCode => write!(f, "No script code for Tr descriptors"),
751754
Error::TrNoExplicitScript => write!(f, "No script code for Tr descriptors"),
755+
Error::MultipathDescLenMismatch => write!(f, "At least two BIP389 key expressions in the descriptor contain tuples of derivation indexes of different lengths"),
752756
}
753757
}
754758
}
@@ -789,7 +793,8 @@ impl error::Error for Error {
789793
| BareDescriptorAddr
790794
| TaprootSpendInfoUnavialable
791795
| TrNoScriptCode
792-
| TrNoExplicitScript => None,
796+
| TrNoExplicitScript
797+
| MultipathDescLenMismatch => None,
793798
Script(e) => Some(e),
794799
AddrError(e) => Some(e),
795800
BadPubkey(e) => Some(e),

0 commit comments

Comments
 (0)