Skip to content

Commit 6cc4369

Browse files
committed
Merge #872: fix(key_map): derivation_path for Xprv with key origin info
a2f4497 test(key_map): improve `Xprv` key origin tests (Leonardo Lima) 20ae466 fix(key_map): derivation_path for `Xprv` with key origin info (Leonardo Lima) Pull request description: ### Description The `GetKey` for `DescriptorSecretKey` (and thus `KeyMap`) returned the wrong key when an extended private key with key origin info was queried via `KeyRequest::Bip32`. The key request path is master-relative, but the `xkey` is anchored at its origin (e.g. `[d34db33f/84h/1h/0h]`). The old code stripped the path returned by `matches()`; the fix strips the origin prefix instead (and uses the full path when there is no origin) before deriving from the `xkey`. -- Also, this PR introduces the test `get_key_xpriv_with_key_origin` covering the scenarios: bare wildcard; single fixed step; fixed step then wildcard, and the matching/non-matching cases build requests from explicit `(fingerprint, path)` key sources. ### Notes If you'd like an overview on the issue, see: #863 (comment) ### Changelog notice ```​ ### Fixed - keymap: fix `GetKey` derivation path for `Xprv` with key origin info [#872](#872) ``` ### Checklists #### Bugfixes: * [ ] This pull request breaks the existing API * [x] I've added tests to reproduce the issue which are now passing * [ ] I'm linking the issue being fixed by this PR ACKs for top commit: notmandatory: ACK a2f4497 noahjoeris: tACK a2f4497 apoelstra: ACK a2f4497; successfully ran local tests Tree-SHA512: 7980e4cfccc8b0b53a054350dd7870122192407c4e17c36f75350093d7be63d4a5e75be40f4e4583c1a1cf0641a469d55842b7108e8bce6e8aa44f519e51b963
2 parents 6131095 + a2f4497 commit 6cc4369

1 file changed

Lines changed: 110 additions & 48 deletions

File tree

src/descriptor/key_map.rs

Lines changed: 110 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -133,21 +133,27 @@ impl GetKey for DescriptorSecretKey {
133133
return Ok(Some(key));
134134
}
135135

136-
if let Some(matched_path) = descriptor_xkey.matches(key_source, secp) {
137-
let (_, full_path) = key_source;
138-
139-
let derivation_path = &full_path[matched_path.len()..];
140-
141-
return Ok(Some(
142-
descriptor_xkey
143-
.xkey
144-
.derive_priv(secp, &derivation_path)
145-
.map_err(GetKeyError::Bip32)?
146-
.to_priv(),
147-
));
148-
}
149-
150-
Ok(None)
136+
// A successful `matches()` already guarantees the requested key source's fingerprint equals our origin
137+
// (or, when there is no origin, the xkey's own) fingerprint.
138+
//
139+
// `xkey` is anchored at the origin, but the request path is master-relative, either:
140+
// - origin: strip the origin prefix and use the remaining suffix.
141+
// - no origin: use the full request path.
142+
let (_, full_path) = key_source;
143+
let derivation_path = match descriptor_xkey.matches(key_source, secp) {
144+
Some(_) => match &descriptor_xkey.origin {
145+
Some((_, origin_path)) => &full_path[origin_path.len()..],
146+
None => full_path.as_ref(),
147+
},
148+
None => return Ok(None),
149+
};
150+
151+
Ok(Some(
152+
descriptor_xkey
153+
.xkey
154+
.derive_priv(secp, &derivation_path)?
155+
.to_priv(),
156+
))
151157
}
152158
(Self::XPrv(_), KeyRequest::XOnlyPubkey(_)) => Err(GetKeyError::NotSupported),
153159
(desc_multi_sk @ Self::MultiXPrv(_descriptor_multi_xkey), key_request) => {
@@ -168,7 +174,7 @@ impl GetKey for DescriptorSecretKey {
168174
mod tests {
169175
use core::str::FromStr;
170176

171-
use bitcoin::bip32::{ChildNumber, DerivationPath, IntoDerivationPath, Xpriv};
177+
use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, IntoDerivationPath, Xpriv};
172178

173179
use super::*;
174180
use crate::Descriptor;
@@ -305,38 +311,6 @@ mod tests {
305311
assert_eq!(got_sk, want_sk)
306312
}
307313

308-
#[test]
309-
fn get_key_xpriv_with_key_origin() {
310-
let secp = Secp256k1::new();
311-
312-
let descriptor_str = "wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)";
313-
let (_descriptor_pk, keymap) = Descriptor::parse_descriptor(&secp, descriptor_str).unwrap();
314-
315-
let descriptor_sk = DescriptorSecretKey::from_str("[d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*").unwrap();
316-
let xpriv = match descriptor_sk {
317-
DescriptorSecretKey::XPrv(descriptor_xkey) => descriptor_xkey,
318-
_ => unreachable!(),
319-
};
320-
321-
let expected_deriv_path: DerivationPath = (&[ChildNumber::Normal { index: 0 }][..]).into();
322-
let expected_pk = xpriv
323-
.xkey
324-
.derive_priv(&secp, &expected_deriv_path)
325-
.unwrap()
326-
.to_priv();
327-
328-
let derivation_path = DerivationPath::from_str("84'/1'/0'/0").unwrap();
329-
let (fp, _) = xpriv.origin.unwrap();
330-
let key_request = KeyRequest::Bip32((fp, derivation_path));
331-
332-
let pk = keymap
333-
.get_key(key_request, &secp)
334-
.expect("get_key should not fail")
335-
.expect("get_key should return a `PrivateKey`");
336-
337-
assert_eq!(pk, expected_pk);
338-
}
339-
340314
#[test]
341315
fn get_key_keymap_no_match() {
342316
let secp = Secp256k1::new();
@@ -417,4 +391,92 @@ mod tests {
417391
let result = keymap.get_key(request_x, &secp).unwrap();
418392
assert!(result.is_none(), "Should return None even on error");
419393
}
394+
395+
#[test]
396+
fn get_key_xpriv_with_key_origin() {
397+
// `get_key` should match the request against the key origin, strip the origin prefix
398+
// from the requested path, and derive the rest from the extended key.
399+
struct TestCase {
400+
/// Scenario description.
401+
name: &'static str,
402+
/// The descriptor under test.
403+
descriptor: &'static str,
404+
/// Requested key source: `(master fingerprint, path from the master)`.
405+
key_request: (&'static str, &'static str),
406+
/// Expected steps from the extended key (requested path minus the key origin).
407+
exp_derivation_path: &'static str,
408+
}
409+
410+
let cases = [
411+
TestCase {
412+
name: "bare wildcard",
413+
descriptor: "wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)",
414+
key_request: ("d34db33f", "84'/1'/0'/0"),
415+
exp_derivation_path: "0",
416+
},
417+
TestCase {
418+
name: "single fixed step",
419+
descriptor: "wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/0)",
420+
key_request: ("d34db33f", "84'/1'/0'/0"),
421+
exp_derivation_path: "0",
422+
},
423+
TestCase {
424+
name: "fixed step then wildcard",
425+
descriptor: "wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/0/*)",
426+
key_request: ("d34db33f", "84'/1'/0'/0/5"),
427+
exp_derivation_path: "0/5",
428+
},
429+
];
430+
431+
let secp = Secp256k1::new();
432+
let xpriv = Xpriv::from_str("tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS").unwrap();
433+
for test_case in &cases {
434+
let exp_derivation_path =
435+
DerivationPath::from_str(test_case.exp_derivation_path).unwrap();
436+
let exp_private_key = xpriv
437+
.derive_priv(&secp, &exp_derivation_path)
438+
.unwrap()
439+
.to_priv();
440+
441+
let (_, keymap) = Descriptor::parse_descriptor(&secp, test_case.descriptor).unwrap();
442+
443+
let (fingerprint, derivation_path) = test_case.key_request;
444+
let key_request = KeyRequest::Bip32((
445+
Fingerprint::from_str(fingerprint).unwrap(),
446+
DerivationPath::from_str(derivation_path).unwrap(),
447+
));
448+
449+
let private_key = keymap
450+
.get_key(key_request, &secp)
451+
.expect("get_key SHOULD NOT fail")
452+
.expect("get_key SHOULD get a `PrivateKey`");
453+
454+
assert_eq!(private_key, exp_private_key, "{}", test_case.name);
455+
}
456+
}
457+
458+
#[test]
459+
fn get_key_xpriv_with_key_origin_and_non_matching_path() {
460+
let secp = Secp256k1::new();
461+
462+
// descriptor with a fixed derivation index of `0`.
463+
let descriptor = "wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/0)";
464+
let (_, keymap) = Descriptor::parse_descriptor(&secp, descriptor).unwrap();
465+
466+
// the key_request has the correct `key_origin`, but the requested derivation index (`5`) does not match the
467+
// descriptor's fixed derivation index (`0`), so the descriptor does not own this key.
468+
let key_request = KeyRequest::Bip32((
469+
Fingerprint::from_str("d34db33f").unwrap(),
470+
DerivationPath::from_str("84'/1'/0'/5").unwrap(),
471+
));
472+
473+
let private_key = keymap
474+
.get_key(key_request, &secp)
475+
.expect("get_key SHOULD NOT fail!");
476+
477+
assert!(
478+
private_key.is_none(),
479+
"SHOULD get NO private key when the requested path does not match the descriptor"
480+
);
481+
}
420482
}

0 commit comments

Comments
 (0)