Skip to content

Commit a2f4497

Browse files
committed
test(key_map): improve Xprv key origin tests
- introduce table-driven cases for `get_key_xpriv_with_key_origin` with named fields - drop redundant key parsing, and build requests from explicit key sources Assisted-by: Claude Opus 4.8
1 parent 20ae466 commit a2f4497

1 file changed

Lines changed: 89 additions & 65 deletions

File tree

src/descriptor/key_map.rs

Lines changed: 89 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ impl GetKey for DescriptorSecretKey {
174174
mod tests {
175175
use core::str::FromStr;
176176

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

179179
use super::*;
180180
use crate::Descriptor;
@@ -311,70 +311,6 @@ mod tests {
311311
assert_eq!(got_sk, want_sk)
312312
}
313313

314-
#[test]
315-
fn get_key_xpriv_with_key_origin_and_wildcard() {
316-
let secp = Secp256k1::new();
317-
318-
let descriptor_str = "wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)";
319-
let (_descriptor_pk, keymap) = Descriptor::parse_descriptor(&secp, descriptor_str).unwrap();
320-
321-
let descriptor_sk = DescriptorSecretKey::from_str("[d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*").unwrap();
322-
let xpriv = match descriptor_sk {
323-
DescriptorSecretKey::XPrv(descriptor_xkey) => descriptor_xkey,
324-
_ => unreachable!(),
325-
};
326-
327-
let expected_deriv_path: DerivationPath = (&[ChildNumber::Normal { index: 0 }][..]).into();
328-
let expected_pk = xpriv
329-
.xkey
330-
.derive_priv(&secp, &expected_deriv_path)
331-
.unwrap()
332-
.to_priv();
333-
334-
let derivation_path = DerivationPath::from_str("84'/1'/0'/0").unwrap();
335-
let (fp, _) = xpriv.origin.unwrap();
336-
let key_request = KeyRequest::Bip32((fp, derivation_path));
337-
338-
let pk = keymap
339-
.get_key(key_request, &secp)
340-
.expect("get_key should not fail")
341-
.expect("get_key should return a `PrivateKey`");
342-
343-
assert_eq!(pk, expected_pk);
344-
}
345-
346-
#[test]
347-
fn get_key_xpriv_with_key_origin_and_no_wildcard() {
348-
let secp = Secp256k1::new();
349-
350-
let descriptor_str = "wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/0)";
351-
let (_descriptor_pk, keymap) = Descriptor::parse_descriptor(&secp, descriptor_str).unwrap();
352-
353-
let descriptor_sk = DescriptorSecretKey::from_str("[d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/0").unwrap();
354-
let xpriv = match descriptor_sk {
355-
DescriptorSecretKey::XPrv(descriptor_xkey) => descriptor_xkey,
356-
_ => unreachable!(),
357-
};
358-
359-
let expected_deriv_path: DerivationPath = (&[ChildNumber::Normal { index: 0 }][..]).into();
360-
let expected_pk = xpriv
361-
.xkey
362-
.derive_priv(&secp, &expected_deriv_path)
363-
.unwrap()
364-
.to_priv();
365-
366-
let derivation_path = DerivationPath::from_str("84'/1'/0'/0").unwrap();
367-
let (fp, _) = xpriv.origin.unwrap();
368-
let key_request = KeyRequest::Bip32((fp, derivation_path));
369-
370-
let pk = keymap
371-
.get_key(key_request, &secp)
372-
.expect("get_key should not fail")
373-
.expect("get_key should return a `PrivateKey`");
374-
375-
assert_eq!(pk, expected_pk);
376-
}
377-
378314
#[test]
379315
fn get_key_keymap_no_match() {
380316
let secp = Secp256k1::new();
@@ -455,4 +391,92 @@ mod tests {
455391
let result = keymap.get_key(request_x, &secp).unwrap();
456392
assert!(result.is_none(), "Should return None even on error");
457393
}
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+
}
458482
}

0 commit comments

Comments
 (0)