Skip to content

Commit 33b417b

Browse files
committed
Merge #757: Support conversion of multi-Xprivs into multi-Xpubs
ae64ce6 Some minor formatting changes (Nadav Ivgi) 7d3e1e8 Support conversion of multi-Xprivs into multi-Xpubs (Nadav Ivgi) Pull request description: Possible when all hardened derivation steps are shared among all paths (or if there are none). Errors otherwise. ACKs for top commit: sanket1729: utACK ae64ce6 apoelstra: ACK ae64ce6; successfully ran local tests; the stringly-typed errors are OK since this module is full of them Tree-SHA512: 47d90831409b538d6b521b354696a5ed459eb45a036938d1f6fd59dca66254115abe65bca861de3428b285c0cbf7ae3591779cfdd6be0a2dc12b3b967e221b2d
2 parents 7ae7596 + ae64ce6 commit 33b417b

File tree

1 file changed

+103
-20
lines changed

1 file changed

+103
-20
lines changed

src/descriptor/key.rs

+103-20
Original file line numberDiff line numberDiff line change
@@ -227,20 +227,13 @@ impl DescriptorXKey<bip32::Xpriv> {
227227
let xpub = bip32::Xpub::from_priv(secp, &xprv);
228228

229229
let origin = match &self.origin {
230-
Some((fingerprint, path)) => Some((
231-
*fingerprint,
232-
path.into_iter()
233-
.chain(hardened_path.iter())
234-
.cloned()
235-
.collect(),
236-
)),
237-
None => {
238-
if hardened_path.is_empty() {
239-
None
240-
} else {
241-
Some((self.xkey.fingerprint(secp), hardened_path.into()))
242-
}
230+
Some((fingerprint, path)) => {
231+
Some((*fingerprint, path.into_iter().chain(hardened_path).copied().collect()))
243232
}
233+
None if !hardened_path.is_empty() => {
234+
Some((self.xkey.fingerprint(secp), hardened_path.into()))
235+
}
236+
None => None,
244237
};
245238

246239
Ok(DescriptorXKey {
@@ -252,6 +245,85 @@ impl DescriptorXKey<bip32::Xpriv> {
252245
}
253246
}
254247

248+
impl DescriptorMultiXKey<bip32::Xpriv> {
249+
/// Returns the public version of this multi-key, applying all the hardened derivation steps that
250+
/// are shared among all derivation paths before turning it into a public key.
251+
///
252+
/// Errors if there are hardened derivation steps that are not shared among all paths.
253+
fn to_public<C: Signing>(
254+
&self,
255+
secp: &Secp256k1<C>,
256+
) -> Result<DescriptorMultiXKey<bip32::Xpub>, DescriptorKeyParseError> {
257+
let deriv_paths = self.derivation_paths.paths();
258+
259+
let shared_prefix: Vec<_> = deriv_paths[0]
260+
.into_iter()
261+
.enumerate()
262+
.take_while(|(index, child_num)| {
263+
deriv_paths[1..].iter().all(|other_path| {
264+
other_path.len() > *index && other_path[*index] == **child_num
265+
})
266+
})
267+
.map(|(_, child_num)| *child_num)
268+
.collect();
269+
270+
let suffixes: Vec<Vec<_>> = deriv_paths
271+
.iter()
272+
.map(|path| {
273+
path.into_iter()
274+
.skip(shared_prefix.len())
275+
.map(|child_num| {
276+
if child_num.is_normal() {
277+
Ok(*child_num)
278+
} else {
279+
Err(DescriptorKeyParseError("Can't make a multi-xpriv with hardened derivation steps that are not shared among all paths into a public key."))
280+
}
281+
})
282+
.collect()
283+
})
284+
.collect::<Result<_, _>>()?;
285+
286+
let unhardened = shared_prefix
287+
.iter()
288+
.rev()
289+
.take_while(|c| c.is_normal())
290+
.count();
291+
let last_hardened_idx = shared_prefix.len() - unhardened;
292+
let hardened_path = &shared_prefix[..last_hardened_idx];
293+
let unhardened_path = &shared_prefix[last_hardened_idx..];
294+
295+
let xprv = self
296+
.xkey
297+
.derive_priv(secp, &hardened_path)
298+
.map_err(|_| DescriptorKeyParseError("Unable to derive the hardened steps"))?;
299+
let xpub = bip32::Xpub::from_priv(secp, &xprv);
300+
301+
let origin = match &self.origin {
302+
Some((fingerprint, path)) => {
303+
Some((*fingerprint, path.into_iter().chain(hardened_path).copied().collect()))
304+
}
305+
None if !hardened_path.is_empty() => {
306+
Some((self.xkey.fingerprint(secp), hardened_path.into()))
307+
}
308+
None => None,
309+
};
310+
let new_deriv_paths = suffixes
311+
.into_iter()
312+
.map(|suffix| {
313+
let path = unhardened_path.iter().copied().chain(suffix);
314+
path.collect::<Vec<_>>().into()
315+
})
316+
.collect();
317+
318+
Ok(DescriptorMultiXKey {
319+
origin,
320+
xkey: xpub,
321+
derivation_paths: DerivPaths::new(new_deriv_paths).expect("not empty"),
322+
wildcard: self.wildcard,
323+
})
324+
}
325+
}
326+
255327
/// Descriptor Key parsing errors
256328
// FIXME: replace with error enums
257329
#[derive(Debug, PartialEq, Clone, Copy)]
@@ -309,20 +381,17 @@ impl DescriptorSecretKey {
309381
/// If the key is an "XPrv", the hardened derivation steps will be applied
310382
/// before converting it to a public key.
311383
///
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.
384+
/// It will return an error if the key is a "multi-xpriv" that includes
385+
/// hardened derivation steps not shared for all paths.
315386
pub fn to_public<C: Signing>(
316387
&self,
317388
secp: &Secp256k1<C>,
318389
) -> Result<DescriptorPublicKey, DescriptorKeyParseError> {
319390
let pk = match self {
320391
DescriptorSecretKey::Single(prv) => DescriptorPublicKey::Single(prv.to_public(secp)),
321392
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-
))
393+
DescriptorSecretKey::MultiXPrv(xprv) => {
394+
DescriptorPublicKey::MultiXPub(xprv.to_public(secp)?)
326395
}
327396
};
328397

@@ -1489,6 +1558,20 @@ mod test {
14891558
DescriptorPublicKey::from_str("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/4/<0;1;>").unwrap_err();
14901559
}
14911560

1561+
#[test]
1562+
fn test_multixprv_to_public() {
1563+
let secp = secp256k1::Secp256k1::signing_only();
1564+
1565+
// Works if all hardended derivation steps are part of the shared path
1566+
let xprv = get_multipath_xprv("[01020304/5]tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1'/2'/3/<4;5>/6");
1567+
let xpub = DescriptorPublicKey::MultiXPub(xprv.to_public(&secp).unwrap()); // wrap in a DescriptorPublicKey to have Display
1568+
assert_eq!(xpub.to_string(), "[01020304/5/1'/2']tpubDBTRkEMEFkUbk3WTz6CFSULyswkTPpPr38AWibf5TVkB5GxuBxbSbmdFGr3jmswwemknyYxAGoX7BJnKfyPy4WXaHmcrxZhfzFwoUFvFtm5/3/<4;5>/6");
1569+
1570+
// Fails if they're part of the multi-path specifier or following it
1571+
get_multipath_xprv("tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1/2/<3';4'>/5").to_public(&secp).unwrap_err();
1572+
get_multipath_xprv("tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1/2/<3;4>/5/6'").to_public(&secp).unwrap_err();
1573+
}
1574+
14921575
#[test]
14931576
fn test_parse_wif() {
14941577
let secret_key = "[0dd03d09/0'/1/2']5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ"

0 commit comments

Comments
 (0)