Skip to content

Commit 7d3e1e8

Browse files
committed
Support conversion of multi-Xprivs into multi-Xpubs
Possible when all hardened derivation steps are shared among all paths (or if there are none). Errors otherwise.
1 parent 1e40a8d commit 7d3e1e8

File tree

1 file changed

+101
-7
lines changed

1 file changed

+101
-7
lines changed

src/descriptor/key.rs

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,89 @@ impl DescriptorXKey<bip32::Xpriv> {
252252
}
253253
}
254254

255+
impl DescriptorMultiXKey<bip32::Xpriv> {
256+
/// Returns the public version of this multi-key, applying all the hardened derivation steps that
257+
/// are shared among all derivation paths before turning it into a public key.
258+
///
259+
/// Errors if there are hardened derivation steps that are not shared among all paths.
260+
fn to_public<C: Signing>(
261+
&self,
262+
secp: &Secp256k1<C>,
263+
) -> Result<DescriptorMultiXKey<bip32::Xpub>, DescriptorKeyParseError> {
264+
let deriv_paths = self.derivation_paths.paths();
265+
266+
let shared_prefix: Vec<_> = deriv_paths[0]
267+
.into_iter()
268+
.enumerate()
269+
.take_while(|(index, child_num)| {
270+
deriv_paths[1..].iter().all(|other_path| {
271+
other_path.len() > *index && other_path[*index] == **child_num
272+
})
273+
})
274+
.map(|(_, child_num)| *child_num)
275+
.collect();
276+
277+
let suffixes: Vec<Vec<_>> = deriv_paths
278+
.iter()
279+
.map(|path| {
280+
path.into_iter()
281+
.skip(shared_prefix.len())
282+
.map(|child_num| {
283+
if child_num.is_normal() {
284+
Ok(*child_num)
285+
} else {
286+
Err(DescriptorKeyParseError("Can't make a multi-xpriv with hardened derivation steps that are not shared among all paths into a public key."))
287+
}
288+
})
289+
.collect()
290+
})
291+
.collect::<Result<_, _>>()?;
292+
293+
let unhardened = shared_prefix
294+
.iter()
295+
.rev()
296+
.take_while(|c| c.is_normal())
297+
.count();
298+
let last_hardened_idx = shared_prefix.len() - unhardened;
299+
let hardened_path = &shared_prefix[..last_hardened_idx];
300+
let unhardened_path = &shared_prefix[last_hardened_idx..];
301+
302+
let xprv = self
303+
.xkey
304+
.derive_priv(secp, &hardened_path)
305+
.map_err(|_| DescriptorKeyParseError("Unable to derive the hardened steps"))?;
306+
let xpub = bip32::Xpub::from_priv(secp, &xprv);
307+
308+
let origin = match &self.origin {
309+
Some((fingerprint, path)) => Some((
310+
*fingerprint,
311+
path.into_iter()
312+
.chain(hardened_path.iter())
313+
.copied()
314+
.collect(),
315+
)),
316+
None if !hardened_path.is_empty() => {
317+
Some((self.xkey.fingerprint(secp), hardened_path.into()))
318+
}
319+
None => None,
320+
};
321+
let new_deriv_paths = suffixes
322+
.into_iter()
323+
.map(|suffix| {
324+
let path = unhardened_path.iter().copied().chain(suffix);
325+
path.collect::<Vec<_>>().into()
326+
})
327+
.collect();
328+
329+
Ok(DescriptorMultiXKey {
330+
origin,
331+
xkey: xpub,
332+
derivation_paths: DerivPaths::new(new_deriv_paths).expect("not empty"),
333+
wildcard: self.wildcard,
334+
})
335+
}
336+
}
337+
255338
/// Descriptor Key parsing errors
256339
// FIXME: replace with error enums
257340
#[derive(Debug, PartialEq, Clone, Copy)]
@@ -309,20 +392,17 @@ impl DescriptorSecretKey {
309392
/// If the key is an "XPrv", the hardened derivation steps will be applied
310393
/// before converting it to a public key.
311394
///
312-
/// It will return an error if the key is a "multi-xpriv", as we wouldn't
313-
/// always be able to apply hardened derivation steps if there are multiple
314-
/// paths.
395+
/// It will return an error if the key is a "multi-xpriv" that includes
396+
/// hardened derivation steps not shared for all paths.
315397
pub fn to_public<C: Signing>(
316398
&self,
317399
secp: &Secp256k1<C>,
318400
) -> Result<DescriptorPublicKey, DescriptorKeyParseError> {
319401
let pk = match self {
320402
DescriptorSecretKey::Single(prv) => DescriptorPublicKey::Single(prv.to_public(secp)),
321403
DescriptorSecretKey::XPrv(xprv) => DescriptorPublicKey::XPub(xprv.to_public(secp)?),
322-
DescriptorSecretKey::MultiXPrv(_) => {
323-
return Err(DescriptorKeyParseError(
324-
"Can't make an extended private key with multiple paths into a public key.",
325-
))
404+
DescriptorSecretKey::MultiXPrv(xprv) => {
405+
DescriptorPublicKey::MultiXPub(xprv.to_public(secp)?)
326406
}
327407
};
328408

@@ -1489,6 +1569,20 @@ mod test {
14891569
DescriptorPublicKey::from_str("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/4/<0;1;>").unwrap_err();
14901570
}
14911571

1572+
#[test]
1573+
fn test_multixprv_to_public() {
1574+
let secp = secp256k1::Secp256k1::signing_only();
1575+
1576+
// Works if all hardended derivation steps are part of the shared path
1577+
let xprv = get_multipath_xprv("[01020304/5]tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1'/2'/3/<4;5>/6");
1578+
let xpub = DescriptorPublicKey::MultiXPub(xprv.to_public(&secp).unwrap()); // wrap in a DescriptorPublicKey to have Display
1579+
assert_eq!(xpub.to_string(), "[01020304/5/1'/2']tpubDBTRkEMEFkUbk3WTz6CFSULyswkTPpPr38AWibf5TVkB5GxuBxbSbmdFGr3jmswwemknyYxAGoX7BJnKfyPy4WXaHmcrxZhfzFwoUFvFtm5/3/<4;5>/6");
1580+
1581+
// Fails if they're part of the multi-path specifier or following it
1582+
get_multipath_xprv("tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1/2/<3';4'>/5").to_public(&secp).unwrap_err();
1583+
get_multipath_xprv("tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1/2/<3;4>/5/6'").to_public(&secp).unwrap_err();
1584+
}
1585+
14921586
#[test]
14931587
fn test_parse_wif() {
14941588
let secret_key = "[0dd03d09/0'/1/2']5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ"

0 commit comments

Comments
 (0)