Skip to content

Commit dd3b543

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 d6d81a0 commit dd3b543

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
@@ -728,6 +728,68 @@ impl Descriptor<DescriptorPublicKey> {
728728

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

733795
impl Descriptor<DefiniteDescriptorKey> {
@@ -1844,4 +1906,32 @@ pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
18441906
"tr(020000000000000000000000000000000000000000000000000000000000000002)",
18451907
);
18461908
}
1909+
1910+
#[test]
1911+
fn multipath_descriptors() {
1912+
// We can parse a multipath descriptors, and make it into separate single-path descriptors.
1913+
let desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<7';8h;20>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/<0;1;987>/*)))").unwrap();
1914+
assert!(desc.is_multipath());
1915+
assert_eq!(desc.into_single_descriptors().unwrap(), vec![
1916+
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/7'/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/0/*)))").unwrap(),
1917+
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/8h/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/1/*)))").unwrap(),
1918+
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/20/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/987/*)))").unwrap()
1919+
]);
1920+
1921+
// Even if only one of the keys is multipath.
1922+
let desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap();
1923+
assert!(desc.is_multipath());
1924+
assert_eq!(desc.into_single_descriptors().unwrap(), vec![
1925+
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/0/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap(),
1926+
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/1/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap(),
1927+
]);
1928+
1929+
// We can detect regular single-path descriptors.
1930+
let notmulti_desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap();
1931+
assert!(!notmulti_desc.is_multipath());
1932+
assert_eq!(
1933+
notmulti_desc.clone().into_single_descriptors().unwrap(),
1934+
vec![notmulti_desc]
1935+
);
1936+
}
18471937
}

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)