Skip to content

Commit a63d5a2

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

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
@@ -977,6 +977,14 @@ impl MiniscriptKey for DescriptorPublicKey {
977977
_ => false,
978978
}
979979
}
980+
981+
fn num_der_paths(&self) -> usize {
982+
match self {
983+
DescriptorPublicKey::Single(_) => 0,
984+
DescriptorPublicKey::XPub(_) => 1,
985+
DescriptorPublicKey::MultiXPub(xpub) => xpub.derivation_paths.len(),
986+
}
987+
}
980988
}
981989

982990
impl DefiniteDescriptorKey {
@@ -1070,6 +1078,10 @@ impl MiniscriptKey for DefiniteDescriptorKey {
10701078
fn is_x_only_key(&self) -> bool {
10711079
self.0.is_x_only_key()
10721080
}
1081+
1082+
fn num_der_paths(&self) -> usize {
1083+
self.0.num_der_paths()
1084+
}
10731085
}
10741086

10751087
impl ToPublicKey for DefiniteDescriptorKey {
@@ -1110,7 +1122,7 @@ mod test {
11101122

11111123
use super::{
11121124
DescriptorKeyParseError, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey,
1113-
Wildcard,
1125+
MiniscriptKey, Wildcard,
11141126
};
11151127
use crate::prelude::*;
11161128

@@ -1297,8 +1309,12 @@ mod test {
12971309
);
12981310
}
12991311

1300-
fn get_multipath_xpub(key_str: &str) -> DescriptorMultiXKey<bip32::ExtendedPubKey> {
1312+
fn get_multipath_xpub(
1313+
key_str: &str,
1314+
num_paths: usize,
1315+
) -> DescriptorMultiXKey<bip32::ExtendedPubKey> {
13011316
let desc_key = DescriptorPublicKey::from_str(key_str).unwrap();
1317+
assert_eq!(desc_key.num_der_paths(), num_paths);
13021318
match desc_key {
13031319
DescriptorPublicKey::MultiXPub(xpub) => xpub,
13041320
_ => unreachable!(),
@@ -1318,7 +1334,7 @@ mod test {
13181334
let secp = secp256k1::Secp256k1::signing_only();
13191335

13201336
// We can have a key in a descriptor that has multiple paths
1321-
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;42;9854>");
1337+
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;42;9854>", 4);
13221338
assert_eq!(
13231339
xpub.derivation_paths,
13241340
vec![
@@ -1333,7 +1349,7 @@ mod test {
13331349
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string(), 4)
13341350
);
13351351
// Even if it's in the middle of the derivation path.
1336-
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/0/5/10");
1352+
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/0/5/10", 3);
13371353
assert_eq!(
13381354
xpub.derivation_paths,
13391355
vec![
@@ -1347,7 +1363,7 @@ mod test {
13471363
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string(), 3)
13481364
);
13491365
// Even if it is a wildcard extended key.
1350-
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/3456/9876/*");
1366+
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/3456/9876/*", 3);
13511367
assert_eq!(xpub.wildcard, Wildcard::Unhardened);
13521368
assert_eq!(
13531369
xpub.derivation_paths,
@@ -1362,7 +1378,7 @@ mod test {
13621378
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string(), 3)
13631379
);
13641380
// Also even if it has an origin.
1365-
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/<0;1>/*");
1381+
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/<0;1>/*", 2);
13661382
assert_eq!(xpub.wildcard, Wildcard::Unhardened);
13671383
assert_eq!(
13681384
xpub.derivation_paths,
@@ -1377,7 +1393,7 @@ mod test {
13771393
);
13781394
// Also if it has hardened steps in the derivation path. In fact, it can also have hardened
13791395
// indexes even at the step with multiple indexes!
1380-
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/9478'/<0';1h>/8h/*'");
1396+
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/9478'/<0';1h>/8h/*'", 2);
13811397
assert_eq!(xpub.wildcard, Wildcard::Hardened);
13821398
assert_eq!(
13831399
xpub.derivation_paths,

src/descriptor/mod.rs

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

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

@@ -1912,6 +1954,7 @@ pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
19121954
// We can parse a multipath descriptors, and make it into separate single-path descriptors.
19131955
let desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<7';8h;20>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/<0;1;987>/*)))").unwrap();
19141956
assert!(desc.is_multipath());
1957+
assert!(!desc.multipath_length_mismatch());
19151958
assert_eq!(desc.into_single_descriptors().unwrap(), vec![
19161959
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/7'/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/0/*)))").unwrap(),
19171960
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/8h/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/1/*)))").unwrap(),
@@ -1921,6 +1964,7 @@ pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
19211964
// Even if only one of the keys is multipath.
19221965
let desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap();
19231966
assert!(desc.is_multipath());
1967+
assert!(!desc.multipath_length_mismatch());
19241968
assert_eq!(desc.into_single_descriptors().unwrap(), vec![
19251969
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/0/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap(),
19261970
Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/1/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap(),
@@ -1929,9 +1973,14 @@ pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
19291973
// We can detect regular single-path descriptors.
19301974
let notmulti_desc = Descriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/4567/*)))").unwrap();
19311975
assert!(!notmulti_desc.is_multipath());
1976+
assert!(!notmulti_desc.multipath_length_mismatch());
19321977
assert_eq!(
19331978
notmulti_desc.clone().into_single_descriptors().unwrap(),
19341979
vec![notmulti_desc]
19351980
);
1981+
1982+
// We refuse to parse multipath descriptors with a mismatch in the number of derivation paths between keys.
1983+
Descriptor::<DescriptorPublicKey>::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/<0;1;2;3;4>/*)))").unwrap_err();
1984+
Descriptor::<DescriptorPublicKey>::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/0'/<0;1;2;3>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/8/<0;1;2>/*)))").unwrap_err();
19361985
}
19371986
}

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)