Skip to content

Commit fafde11

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 36b89f4 commit fafde11

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

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)