Skip to content

Commit ae0f3d9

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 131bb5b commit ae0f3d9

File tree

4 files changed

+106
-13
lines changed

4 files changed

+106
-13
lines changed

src/descriptor/key.rs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,14 @@ impl MiniscriptKey for DescriptorPublicKey {
10041004
_ => false,
10051005
}
10061006
}
1007+
1008+
fn num_der_paths(&self) -> usize {
1009+
match self {
1010+
DescriptorPublicKey::Single(_) => 0,
1011+
DescriptorPublicKey::XPub(_) => 1,
1012+
DescriptorPublicKey::MultiXPub(xpub) => xpub.derivation_paths.paths().len(),
1013+
}
1014+
}
10071015
}
10081016

10091017
impl DefiniteDescriptorKey {
@@ -1097,6 +1105,10 @@ impl MiniscriptKey for DefiniteDescriptorKey {
10971105
fn is_x_only_key(&self) -> bool {
10981106
self.0.is_x_only_key()
10991107
}
1108+
1109+
fn num_der_paths(&self) -> usize {
1110+
self.0.num_der_paths()
1111+
}
11001112
}
11011113

11021114
impl ToPublicKey for DefiniteDescriptorKey {
@@ -1137,7 +1149,7 @@ mod test {
11371149

11381150
use super::{
11391151
DescriptorKeyParseError, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey,
1140-
Wildcard,
1152+
MiniscriptKey, Wildcard,
11411153
};
11421154
use crate::prelude::*;
11431155

@@ -1324,8 +1336,12 @@ mod test {
13241336
);
13251337
}
13261338

1327-
fn get_multipath_xpub(key_str: &str) -> DescriptorMultiXKey<bip32::ExtendedPubKey> {
1339+
fn get_multipath_xpub(
1340+
key_str: &str,
1341+
num_paths: usize,
1342+
) -> DescriptorMultiXKey<bip32::ExtendedPubKey> {
13281343
let desc_key = DescriptorPublicKey::from_str(key_str).unwrap();
1344+
assert_eq!(desc_key.num_der_paths(), num_paths);
13291345
match desc_key {
13301346
DescriptorPublicKey::MultiXPub(xpub) => xpub,
13311347
_ => unreachable!(),
@@ -1345,7 +1361,7 @@ mod test {
13451361
let secp = secp256k1::Secp256k1::signing_only();
13461362

13471363
// We can have a key in a descriptor that has multiple paths
1348-
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;42;9854>");
1364+
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;42;9854>", 4);
13491365
assert_eq!(
13501366
xpub.derivation_paths.paths(),
13511367
&vec![
@@ -1357,10 +1373,10 @@ mod test {
13571373
);
13581374
assert_eq!(
13591375
xpub,
1360-
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string())
1376+
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string(), 4)
13611377
);
13621378
// Even if it's in the middle of the derivation path.
1363-
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/0/5/10");
1379+
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/0/5/10", 3);
13641380
assert_eq!(
13651381
xpub.derivation_paths.paths(),
13661382
&vec![
@@ -1371,10 +1387,10 @@ mod test {
13711387
);
13721388
assert_eq!(
13731389
xpub,
1374-
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string())
1390+
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string(), 3)
13751391
);
13761392
// Even if it is a wildcard extended key.
1377-
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/3456/9876/*");
1393+
let xpub = get_multipath_xpub("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/<0;1;9854>/3456/9876/*", 3);
13781394
assert_eq!(xpub.wildcard, Wildcard::Unhardened);
13791395
assert_eq!(
13801396
xpub.derivation_paths.paths(),
@@ -1386,10 +1402,10 @@ mod test {
13861402
);
13871403
assert_eq!(
13881404
xpub,
1389-
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string())
1405+
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string(), 3)
13901406
);
13911407
// Also even if it has an origin.
1392-
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/<0;1>/*");
1408+
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/<0;1>/*", 2);
13931409
assert_eq!(xpub.wildcard, Wildcard::Unhardened);
13941410
assert_eq!(
13951411
xpub.derivation_paths.paths(),
@@ -1400,11 +1416,11 @@ mod test {
14001416
);
14011417
assert_eq!(
14021418
xpub,
1403-
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string())
1419+
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string(), 2)
14041420
);
14051421
// Also if it has hardened steps in the derivation path. In fact, it can also have hardened
14061422
// indexes even at the step with multiple indexes!
1407-
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/9478'/<0';1h>/8h/*'");
1423+
let xpub = get_multipath_xpub("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/9478'/<0';1h>/8h/*'", 2);
14081424
assert_eq!(xpub.wildcard, Wildcard::Hardened);
14091425
assert_eq!(
14101426
xpub.derivation_paths.paths(),
@@ -1415,7 +1431,7 @@ mod test {
14151431
);
14161432
assert_eq!(
14171433
xpub,
1418-
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string())
1434+
get_multipath_xpub(&DescriptorPublicKey::MultiXPub(xpub.clone()).to_string(), 2)
14191435
);
14201436
// You can't get the "full derivation path" for a multipath extended public key.
14211437
let desc_key = DescriptorPublicKey::from_str("[abcdef00/0'/1']tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/9478'/<0';1>/8h/*'").unwrap();

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)