Skip to content

Commit 2e8ece7

Browse files
committed
keys: Implement derived() & xderived() to apply BIP32 derivation steps
1 parent af4c6b9 commit 2e8ece7

File tree

3 files changed

+166
-20
lines changed

3 files changed

+166
-20
lines changed

src/stdlib/keys.rs

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ pub fn attach_stdlib(scope: &ScopeRef<Mutable>) {
2727
.set_fn("xpriv::from_seed", fns::xpriv_from_seed)
2828
.unwrap();
2929

30-
scope.set_fn("xonly", fns::xonly).unwrap();
31-
3230
scope.set_fn("singles", fns::singles).unwrap();
31+
32+
scope.set_fn("xonly", fns::xonly).unwrap(); // xonly is always derived single
33+
scope.set_fn("derived", fns::derived).unwrap();
34+
scope.set_fn("xderived", fns::xderived).unwrap();
3335
}
3436

3537
/// BIP32 key derivation using the Slash operator
@@ -116,37 +118,115 @@ pub mod fns {
116118
Ok(Xpriv::new_master(network, &seed)?.into())
117119
}
118120

121+
/// Convert a multi-path PubKey/SecKey/Descriptor into an array of singles
122+
///
123+
/// singles(PubKey<Multi>|SecKey<Multi>|Descriptor<Multi>) -> Array<PubKey|SecKey|Descriptor>
124+
pub fn singles(args: Array, _: &ScopeRef) -> Result<Value> {
125+
Ok(match args.arg_into()? {
126+
Value::PubKey(pk) => pk.into_single_keys().into(),
127+
Value::SecKey(sk) => sk.into_single_keys().into(),
128+
Value::Descriptor(desc) => desc.into_single_descriptors()?.into(),
129+
other => bail!(Error::InvalidValue(other.into())),
130+
})
131+
}
132+
119133
/// Convert the pubkey into an x-only pubkey.
120-
/// Always returned as a single (non-xpub) pubkey (x-only xpubs cannot be represented as an Xpub/DescriptorPublicKey).
134+
/// Always returned as a single (non-Xpub) pubkey (x-only keys cannot be represented as a DescriptorPublicKey::XPub).
121135
///
122-
/// xonly(PubKey) -> PubKey
136+
/// xonly(PubKey<Xpub|Single>) -> PubKey<Single>
123137
pub fn xonly(args: Array, _: &ScopeRef) -> Result<Value> {
124138
let pk: DescriptorPublicKey = args.arg_into()?;
125139

126140
Ok(if pk.is_x_only_key() {
127141
pk
128142
} else {
129-
// Convert into an x-only single pubkey with BIP32 origin information
130-
let pk = pk.definite()?;
131-
let derived_single_pk = pk.derive_public_key(&EC)?;
132-
let derived_path = pk.full_derivation_path().ok_or(Error::InvalidMultiXpub)?;
143+
let full_path = pk.full_derivation_path().ok_or(Error::InvalidMultiXpub)?;
144+
let master_fp = pk.master_fingerprint();
145+
let derived_pk = pk.derive_definite()?;
133146

134147
DescriptorPublicKey::Single(SinglePub {
135-
key: SinglePubKey::XOnly(derived_single_pk.into()),
136-
origin: Some((pk.master_fingerprint(), derived_path)),
148+
key: SinglePubKey::XOnly(derived_pk.into()),
149+
origin: Some((master_fp, full_path)),
137150
})
138151
}
139152
.into())
140153
}
141154

142-
/// Convert a multi-path PubKey/SecKey/Descriptor into an array of singles
155+
/// Apply Xpub/Xpriv derivation steps to arrive at the final child as a single key
143156
///
144-
/// singles(PubKey<Multi>|SecKey<Multi>|Descriptor<Multi>) -> Array<PubKey|SecKey|Descriptor>
145-
pub fn singles(args: Array, _: &ScopeRef) -> Result<Value> {
157+
/// derived(PubKey<Xpub>) -> PubKey<Single>
158+
/// derived(SecKey<Xpriv>) -> SecKey<Single>
159+
pub fn derived(args: Array, _: &ScopeRef) -> Result<Value> {
146160
Ok(match args.arg_into()? {
147-
Value::PubKey(pk) => pk.into_single_keys().into(),
148-
Value::SecKey(sk) => sk.into_single_keys().into(),
149-
Value::Descriptor(desc) => desc.into_single_descriptors()?.into(),
161+
// Derive Xpubs
162+
Value::PubKey(ref pk @ DescriptorPublicKey::XPub(ref xpub)) => {
163+
let derived_pk = xpub.xkey.derive_pub(&EC, &xpub.derivation_path)?.public_key;
164+
let full_path = pk
165+
.full_derivation_path()
166+
.expect("must exists for DPK::Xpub");
167+
DescriptorPublicKey::Single(SinglePub {
168+
key: SinglePubKey::FullKey(derived_pk.into()),
169+
origin: Some((pk.master_fingerprint(), full_path)),
170+
})
171+
.into()
172+
}
173+
// Derive Xprivs
174+
Value::SecKey(ref sk @ DescriptorSecretKey::XPrv(ref xpriv)) => {
175+
let derived_sk = xpriv
176+
.xkey
177+
.derive_priv(&EC, &xpriv.derivation_path)?
178+
.private_key;
179+
let full_path = sk
180+
.full_derivation_path()
181+
.expect("must exists for DPK::Xprv");
182+
DescriptorSecretKey::Single(SinglePriv {
183+
key: bitcoin::PrivateKey::new(derived_sk, Network::Testnet), // XXX always uses Testnet
184+
origin: Some((sk.master_fingerprint(), full_path)),
185+
})
186+
.into()
187+
}
188+
// Return Single keys as-is
189+
single @ Value::PubKey(DescriptorPublicKey::Single(_))
190+
| single @ Value::SecKey(DescriptorSecretKey::Single(_)) => single,
191+
192+
other => bail!(Error::InvalidValue(other.into())),
193+
})
194+
}
195+
196+
/// Apply Xpub/Xpriv derivation steps to arrive at the final child Xpub/Xpriv
197+
///
198+
/// xderived(PubKey<Xpub>) -> PubKey<Xpub>
199+
/// xderived(SecKey<Xpriv>) -> SecKey<Xpriv>
200+
pub fn xderived(args: Array, _: &ScopeRef) -> Result<Value> {
201+
Ok(match args.arg_into()? {
202+
// Derive Xpubs
203+
Value::PubKey(ref pk @ DescriptorPublicKey::XPub(ref xpub)) => {
204+
let derived_xpub = xpub.xkey.derive_pub(&EC, &xpub.derivation_path)?;
205+
let full_path = pk
206+
.full_derivation_path()
207+
.expect("must exists for DPK::Xpub");
208+
DescriptorPublicKey::XPub(DescriptorXKey {
209+
xkey: derived_xpub,
210+
derivation_path: DerivationPath::master(),
211+
wildcard: xpub.wildcard,
212+
origin: Some((pk.master_fingerprint(), full_path)),
213+
})
214+
.into()
215+
}
216+
// Derive Xprivs
217+
Value::SecKey(ref sk @ DescriptorSecretKey::XPrv(ref xprv)) => {
218+
let derived_xpriv = xprv.xkey.derive_priv(&EC, &xprv.derivation_path)?;
219+
let full_path = sk
220+
.full_derivation_path()
221+
.expect("must exists for DPK::Xprv");
222+
DescriptorSecretKey::XPrv(DescriptorXKey {
223+
xkey: derived_xpriv,
224+
derivation_path: DerivationPath::master(),
225+
wildcard: xprv.wildcard,
226+
origin: Some((sk.master_fingerprint(), full_path)),
227+
})
228+
.into()
229+
}
150230
other => bail!(Error::InvalidValue(other.into())),
151231
})
152232
}

src/util.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,12 +313,52 @@ impl DescriptorPubKeyExt for DescriptorPublicKey {
313313
}
314314

315315
pub trait DescriptorSecretKeyExt {
316-
/// Like `DescriptorPublicKey::full_derivation_paths()`, which isn't available for secret keys
316+
// Mimicking the methods available on `DescriptorPublicKey`, which are not natively available for secret keys
317+
fn full_derivation_path(&self) -> Option<DerivationPath>;
317318
fn full_derivation_paths(&self) -> Vec<DerivationPath>;
319+
fn master_fingerprint(&self) -> bip32::Fingerprint;
318320

321+
// Pending https://github.com/rust-bitcoin/rust-miniscript/pull/757
319322
fn to_public_(&self) -> Result<DescriptorPublicKey>;
320323
}
321324
impl DescriptorSecretKeyExt for DescriptorSecretKey {
325+
fn master_fingerprint(&self) -> bip32::Fingerprint {
326+
match *self {
327+
DescriptorSecretKey::XPrv(ref xpub) => match xpub.origin {
328+
Some((fingerprint, _)) => fingerprint,
329+
None => xpub.xkey.fingerprint(&EC),
330+
},
331+
DescriptorSecretKey::MultiXPrv(ref xpub) => match xpub.origin {
332+
Some((fingerprint, _)) => fingerprint,
333+
None => xpub.xkey.fingerprint(&EC),
334+
},
335+
DescriptorSecretKey::Single(_) => self
336+
.to_public(&EC)
337+
.expect("cannot fail")
338+
.master_fingerprint(),
339+
}
340+
}
341+
fn full_derivation_path(&self) -> Option<DerivationPath> {
342+
match self {
343+
DescriptorSecretKey::XPrv(ref xpub) => {
344+
let origin_path = if let Some((_, ref path)) = xpub.origin {
345+
path.clone()
346+
} else {
347+
DerivationPath::from(vec![])
348+
};
349+
Some(origin_path.extend(&xpub.derivation_path))
350+
}
351+
DescriptorSecretKey::Single(ref single) => {
352+
Some(if let Some((_, ref path)) = single.origin {
353+
path.clone()
354+
} else {
355+
DerivationPath::from(vec![])
356+
})
357+
}
358+
DescriptorSecretKey::MultiXPrv(_) => None,
359+
}
360+
}
361+
322362
fn full_derivation_paths(&self) -> Vec<DerivationPath> {
323363
match self {
324364
DescriptorSecretKey::MultiXPrv(xprv) => {
@@ -351,7 +391,6 @@ impl DescriptorSecretKeyExt for DescriptorSecretKey {
351391
}
352392
}
353393

354-
// Pending https://github.com/rust-bitcoin/rust-miniscript/pull/757
355394
fn to_public_(&self) -> Result<DescriptorPublicKey> {
356395
Ok(match self {
357396
DescriptorSecretKey::Single(_) | DescriptorSecretKey::XPrv(_) => self.to_public(&EC)?,

tests/keys.minsc

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ test("Keys", |T| {
2323
it("Is deterministic (RFC 6979)", |T|
2424
t::eq(ecdsa::sign($sk, $H1), ecdsa::sign($sk, $H1)));
2525

26-
it("Uses DER by default", len(ecdsa::sign($sk, $H1)) >= 70); // 70-73
26+
it("Uses DER by default", |T| {
27+
$sig = ecdsa::sign($sk, $H1);
28+
t::assert(len(ecdsa::sign($sk, $H1)) >= 67)
29+
}); // typically 70-73, sometimes shorter by chance
2730

2831
it("Supports compact encoding", |T|
2932
t::eq(len(ecdsa::sign($sk, $H1, true)), 64));
@@ -63,4 +66,28 @@ test("Keys", |T| {
6366
t::eq($pk, xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB);
6467
});
6568
});
66-
})
69+
});
70+
71+
test("Key Derivation", |T| {
72+
test("with Xprivx", |T| {
73+
$sk = [00112233/4'/5]xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi;
74+
$sk_child = $sk/10'/90;
75+
assert::eq($sk_child, [00112233/4'/5]xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10'/90);
76+
assert::eq(xderived($sk_child), [00112233/4'/5/10'/90]xprv9wGStYmEDBnRnqJyYwkMktb9JZTJTh7EvfbcvY6HPDuSMZtcpMRXfU43JYqK7dCkZKEtacimBiVkrvhEXeR6i1iFuwPTdMWk3go7gebX9ir);
77+
assert::eq(str(derived($sk_child)), "seckey([00112233/4'/5/10'/90]cTsvns7tYeKnrzgJxzV4ea16GisYHtCBvA3rcdnRVSYYeAU4GCVc)");
78+
// Must assert by stringifying $sk_child due to https://github.com/rust-bitcoin/rust-miniscript/pull/753
79+
// assert::eq(derived($sk_child), [00112233/4'/5/10'/90]cTsvns7tYeKnrzgJxzV4ea16GisYHtCBvA3rcdnRVSYYeAU4GCVc);
80+
});
81+
test("with Xpubs", |T| {
82+
$pk = [66778899/0'/1]xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8;
83+
$pk_child = $pk/20/70;
84+
assert::eq($pk_child, [66778899/0'/1]xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/20/70);
85+
assert::eq(xderived($pk_child), [66778899/0'/1/20/70]xpub6ANvqejmnBqUV1bNqXmLYNAAGtPCRSVGkYTysAVCzSmXxY1oSn211VYxEqvM2QJ48RV4LdJPBXh2gvBGR6sdAtEmbPXWKQoXkcp7h1mn9Xi);
86+
assert::eq(derived($pk_child), [66778899/0'/1/20/70]03224c5284fb722a254a5f0119dfa6b0b2dc1214e7a8c9244428be73e824edb283);
87+
});
88+
test("Xpriv matches Xpub", |T| {
89+
$sk = [00112233/4'/5]xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi;
90+
$pk = pubkey($sk);
91+
assert::eq(pubkey($sk/1/2), $pk/1/2);
92+
});
93+
});

0 commit comments

Comments
 (0)