Skip to content

Commit 99bc84a

Browse files
committed
desc: check for mismatch in the number of derivation paths between multipath keys
This is invalid by BIP389 (as combinations would blowup when getting single descriptors otherwise). We add a num_der_paths method to MiniscriptKey in order to be able to check for this at parsing time without specializing from_str for Descriptor<DescriptorPublicKey>.
1 parent 55b3e1d commit 99bc84a

File tree

4 files changed

+101
-8
lines changed

4 files changed

+101
-8
lines changed

src/descriptor/key.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,14 @@ impl MiniscriptKey for DescriptorPublicKey {
10101010
_ => false,
10111011
}
10121012
}
1013+
1014+
fn num_der_paths(&self) -> usize {
1015+
match self {
1016+
DescriptorPublicKey::Single(_) => 0,
1017+
DescriptorPublicKey::XPub(_) => 1,
1018+
DescriptorPublicKey::MultiXPub(xpub) => xpub.derivation_paths.len(),
1019+
}
1020+
}
10131021
}
10141022

10151023
impl DefiniteDescriptorKey {
@@ -1080,6 +1088,10 @@ impl MiniscriptKey for DefiniteDescriptorKey {
10801088
fn is_x_only_key(&self) -> bool {
10811089
self.0.is_x_only_key()
10821090
}
1091+
1092+
fn num_der_paths(&self) -> usize {
1093+
self.0.num_der_paths()
1094+
}
10831095
}
10841096

10851097
impl ToPublicKey for DefiniteDescriptorKey {
@@ -1120,7 +1132,7 @@ mod test {
11201132

11211133
use super::{
11221134
DescriptorKeyParseError, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey,
1123-
Wildcard,
1135+
MiniscriptKey, Wildcard,
11241136
};
11251137
use crate::prelude::*;
11261138

@@ -1307,8 +1319,12 @@ mod test {
13071319
);
13081320
}
13091321

1310-
fn get_multipath_xpub(key_str: &str) -> DescriptorMultiXKey<bip32::ExtendedPubKey> {
1322+
fn get_multipath_xpub(
1323+
key_str: &str,
1324+
num_paths: usize,
1325+
) -> DescriptorMultiXKey<bip32::ExtendedPubKey> {
13111326
let desc_key = DescriptorPublicKey::from_str(key_str).unwrap();
1327+
assert_eq!(desc_key.num_der_paths(), num_paths);
13121328
match desc_key {
13131329
DescriptorPublicKey::MultiXPub(xpub) => xpub,
13141330
_ => unreachable!(),
@@ -1328,7 +1344,7 @@ mod test {
13281344
let secp = secp256k1::Secp256k1::signing_only();
13291345

13301346
// We can have a key in a descriptor that has multiple paths
1331-
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;42;9854>");
1347+
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;42;9854>", 4);
13321348
assert_eq!(
13331349
xpub.derivation_paths,
13341350
vec![
@@ -1339,7 +1355,7 @@ mod test {
13391355
],
13401356
);
13411357
// Even if it's in the middle of the derivation path.
1342-
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/0/5/10");
1358+
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/0/5/10", 3);
13431359
assert_eq!(
13441360
xpub.derivation_paths,
13451361
vec![
@@ -1349,7 +1365,7 @@ mod test {
13491365
],
13501366
);
13511367
// Even if it is a wildcard extended key.
1352-
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/3456/9876/*");
1368+
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/3456/9876/*", 3);
13531369
assert_eq!(xpub.wildcard, Wildcard::Unhardened);
13541370
assert_eq!(
13551371
xpub.derivation_paths,
@@ -1360,7 +1376,7 @@ mod test {
13601376
],
13611377
);
13621378
// Also even if it has an origin.
1363-
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/<0;1>/*");
1379+
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/<0;1>/*", 2);
13641380
assert_eq!(xpub.wildcard, Wildcard::Unhardened);
13651381
assert_eq!(
13661382
xpub.derivation_paths,
@@ -1371,7 +1387,7 @@ mod test {
13711387
);
13721388
// Also if it has hardened steps in the derivation path. In fact, it can also have hardened
13731389
// indexes even at the step with multiple indexes!
1374-
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/9478'/<0';1h>/8h/*'");
1390+
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/9478'/<0';1h>/8h/*'", 2);
13751391
assert_eq!(xpub.wildcard, Wildcard::Hardened);
13761392
assert_eq!(
13771393
xpub.derivation_paths,

src/descriptor/mod.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,42 @@ impl Descriptor<DescriptorPublicKey> {
791791
}
792792
}
793793

794+
impl<Pk: MiniscriptKey> Descriptor<Pk> {
795+
/// Whether this descriptor is a multipath descriptor that contains any 2 multipath keys
796+
/// with a different number of derivation paths.
797+
/// Such a descriptor is invalid according to BIP389.
798+
pub fn multipath_length_mismatch(&self) -> bool {
799+
// (Ab)use `for_each_key` to record the number of derivation paths a multipath key has.
800+
#[derive(PartialEq)]
801+
enum MultipathLenChecker {
802+
SinglePath,
803+
MultipathLen(usize),
804+
LenMismatch,
805+
}
806+
807+
let mut checker = MultipathLenChecker::SinglePath;
808+
self.for_each_key(|key| {
809+
match key.num_der_paths() {
810+
0 | 1 => {}
811+
n => match checker {
812+
MultipathLenChecker::SinglePath => {
813+
checker = MultipathLenChecker::MultipathLen(n);
814+
}
815+
MultipathLenChecker::MultipathLen(len) => {
816+
if len != n {
817+
checker = MultipathLenChecker::LenMismatch;
818+
}
819+
}
820+
MultipathLenChecker::LenMismatch => {}
821+
},
822+
}
823+
true
824+
});
825+
826+
checker == MultipathLenChecker::LenMismatch
827+
}
828+
}
829+
794830
impl Descriptor<DefiniteDescriptorKey> {
795831
/// Convert all the public keys in the descriptor to [`bitcoin::PublicKey`] by deriving them or
796832
/// otherwise converting them. All [`bitcoin::XOnlyPublicKey`]s are converted to by adding a
@@ -861,13 +897,19 @@ impl_from_str!(
861897
// tr tree parsing has special code
862898
// Tr::from_str will check the checksum
863899
// match "tr(" to handle more extensibly
864-
if s.starts_with("tr(") {
900+
let desc = if s.starts_with("tr(") {
865901
Ok(Descriptor::Tr(Tr::from_str(s)?))
866902
} else {
867903
let desc_str = verify_checksum(s)?;
868904
let top = expression::Tree::from_str(desc_str)?;
869905
expression::FromTree::from_tree(&top)
906+
}?;
907+
908+
if desc.multipath_length_mismatch() {
909+
return Err(Error::MultipathDescLenMismatch);
870910
}
911+
912+
Ok(desc)
871913
}
872914
);
873915

@@ -1831,6 +1873,7 @@ pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
18311873
// We can parse a multipath descriptors, and make it into separate single-path descriptors.
18321874
let desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<7';8h;20>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/<0;1;987>/*)))").unwrap();
18331875
assert!(desc.is_multipath());
1876+
assert!(!desc.multipath_length_mismatch());
18341877
assert_eq!(desc.into_single_descriptors().unwrap(), vec![
18351878
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/7'/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/0/*)))").unwrap(),
18361879
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/8h/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/1/*)))").unwrap(),
@@ -1840,6 +1883,7 @@ pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
18401883
// Even if only one of the keys is multipath.
18411884
let desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap();
18421885
assert!(desc.is_multipath());
1886+
assert!(!desc.multipath_length_mismatch());
18431887
assert_eq!(desc.into_single_descriptors().unwrap(), vec![
18441888
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/0/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap(),
18451889
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/1/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap(),
@@ -1848,9 +1892,14 @@ pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
18481892
// We can detect regular single-path descriptors.
18491893
let notmulti_desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap();
18501894
assert!(!notmulti_desc.is_multipath());
1895+
assert!(!notmulti_desc.multipath_length_mismatch());
18511896
assert_eq!(
18521897
notmulti_desc.clone().into_single_descriptors().unwrap(),
18531898
vec![notmulti_desc]
18541899
);
1900+
1901+
// We refuse to parse multipath descriptors with a mismatch in the number of derivation paths between keys.
1902+
Descriptor::<DescriptorPublicKey>::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/<0;1;2;3;4>/*)))").unwrap_err();
1903+
Descriptor::<DescriptorPublicKey>::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1;2;3>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/<0;1;2>/*)))").unwrap_err();
18551904
}
18561905
}

src/interpreter/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ impl MiniscriptKey for BitcoinKey {
138138
type Hash256 = hash256::Hash;
139139
type Ripemd160 = ripemd160::Hash;
140140
type Hash160 = hash160::Hash;
141+
142+
fn num_der_paths(&self) -> usize {
143+
0
144+
}
141145
}
142146

143147
impl<'txin> Interpreter<'txin> {

src/lib.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ pub trait MiniscriptKey: Clone + Eq + Ord + fmt::Debug + fmt::Display + hash::Ha
162162
false
163163
}
164164

165+
/// Returns the number of different derivation paths in this key. Only >1 for keys
166+
/// in BIP389 multipath descriptors.
167+
fn num_der_paths(&self) -> usize;
168+
165169
/// The associated [`sha256::Hash`] for this [`MiniscriptKey`],
166170
/// used in the hash256 fragment.
167171
type Sha256: Clone + Eq + Ord + fmt::Display + fmt::Debug + hash::Hash;
@@ -183,6 +187,10 @@ impl MiniscriptKey for bitcoin::secp256k1::PublicKey {
183187
type Hash256 = hash256::Hash;
184188
type Ripemd160 = ripemd160::Hash;
185189
type Hash160 = hash160::Hash;
190+
191+
fn num_der_paths(&self) -> usize {
192+
0
193+
}
186194
}
187195

188196
impl MiniscriptKey for bitcoin::PublicKey {
@@ -191,6 +199,10 @@ impl MiniscriptKey for bitcoin::PublicKey {
191199
!self.compressed
192200
}
193201

202+
fn num_der_paths(&self) -> usize {
203+
0
204+
}
205+
194206
type Sha256 = sha256::Hash;
195207
type Hash256 = hash256::Hash;
196208
type Ripemd160 = ripemd160::Hash;
@@ -206,13 +218,21 @@ impl MiniscriptKey for bitcoin::secp256k1::XOnlyPublicKey {
206218
fn is_x_only_key(&self) -> bool {
207219
true
208220
}
221+
222+
fn num_der_paths(&self) -> usize {
223+
0
224+
}
209225
}
210226

211227
impl MiniscriptKey for String {
212228
type Sha256 = String; // specify hashes as string
213229
type Hash256 = String;
214230
type Ripemd160 = String;
215231
type Hash160 = String;
232+
233+
fn num_der_paths(&self) -> usize {
234+
0
235+
}
216236
}
217237

218238
/// Trait describing public key types which can be converted to bitcoin pubkeys
@@ -345,6 +365,10 @@ impl MiniscriptKey for DummyKey {
345365
type Hash256 = DummyHash256Hash;
346366
type Ripemd160 = DummyRipemd160Hash;
347367
type Hash160 = DummyHash160Hash;
368+
369+
fn num_der_paths(&self) -> usize {
370+
0
371+
}
348372
}
349373

350374
impl hash::Hash for DummyKey {

0 commit comments

Comments
 (0)