Skip to content

Commit 40beb2b

Browse files
Merge pull request #60 from BitGo/BTC-1826.signWithPrvTaprootPlain
feat: add plain EC key signing for taproot descriptors
2 parents ea8a018 + f19587c commit 40beb2b

File tree

6 files changed

+231
-74
lines changed

6 files changed

+231
-74
lines changed

packages/wasm-miniscript/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/wasm-miniscript/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ wasm-bindgen = "0.2"
1111
js-sys = "0.3"
1212
miniscript = { version = "12.3.0" }
1313

14+
[dev-dependencies]
15+
base64 = "0.22.1"
16+
1417
[profile.release]
1518
# this is required to make webpack happy
1619
# https://github.com/webpack/webpack/issues/15566#issuecomment-2558347645

packages/wasm-miniscript/src/miniscript.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,3 @@ impl From<Miniscript<PublicKey, Legacy>> for WrapMiniscript {
103103
WrapMiniscript(WrapMiniscriptEnum::Legacy(miniscript))
104104
}
105105
}
106-
107-
#[test]
108-
pub fn panic_xprv() {
109-
use miniscript::bitcoin::secp256k1::Secp256k1;
110-
use miniscript::Descriptor;
111-
let (d,m) = Descriptor::parse_descriptor(
112-
&Secp256k1::new(),
113-
"wsh(multi(2,xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0,xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*'))",
114-
).unwrap();
115-
116-
let dd = d.at_derivation_index(0).unwrap();
117-
118-
let _ = dd.explicit_script().unwrap();
119-
}

packages/wasm-miniscript/src/psbt.rs

Lines changed: 130 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,78 @@
11
use crate::descriptor::WrapDescriptorEnum;
22
use crate::try_into_js_value::TryIntoJsValue;
33
use crate::WrapDescriptor;
4-
use miniscript::bitcoin::secp256k1::Secp256k1;
5-
use miniscript::bitcoin::Psbt;
4+
use miniscript::bitcoin::bip32::Fingerprint;
5+
use miniscript::bitcoin::secp256k1::{Context, Secp256k1, Signing};
6+
use miniscript::bitcoin::{bip32, psbt, secp256k1, PublicKey, XOnlyPublicKey};
7+
use miniscript::bitcoin::{PrivateKey, Psbt};
8+
use miniscript::descriptor::{SinglePub, SinglePubKey};
69
use miniscript::psbt::PsbtExt;
7-
use miniscript::ToPublicKey;
10+
use miniscript::{DescriptorPublicKey, ToPublicKey};
811
use std::collections::HashMap;
912
use std::str::FromStr;
1013
use wasm_bindgen::prelude::wasm_bindgen;
1114
use wasm_bindgen::{JsError, JsValue};
1215

16+
#[derive(Debug)]
17+
struct SingleKeySigner {
18+
privkey: PrivateKey,
19+
pubkey: PublicKey,
20+
pubkey_xonly: XOnlyPublicKey,
21+
fingerprint: Fingerprint,
22+
fingerprint_xonly: Fingerprint,
23+
}
24+
25+
impl SingleKeySigner {
26+
fn fingerprint(key: SinglePubKey) -> Fingerprint {
27+
DescriptorPublicKey::Single(SinglePub { origin: None, key }).master_fingerprint()
28+
}
29+
30+
fn from_privkey<C: Signing>(privkey: PrivateKey, secp: &Secp256k1<C>) -> SingleKeySigner {
31+
let pubkey = privkey.public_key(secp);
32+
let pubkey_xonly = pubkey.to_x_only_pubkey();
33+
SingleKeySigner {
34+
privkey,
35+
pubkey,
36+
pubkey_xonly,
37+
fingerprint: SingleKeySigner::fingerprint(SinglePubKey::FullKey(pubkey)),
38+
fingerprint_xonly: SingleKeySigner::fingerprint(SinglePubKey::XOnly(pubkey_xonly)),
39+
}
40+
}
41+
}
42+
43+
impl psbt::GetKey for SingleKeySigner {
44+
type Error = String;
45+
46+
fn get_key<C: Signing>(
47+
&self,
48+
key_request: psbt::KeyRequest,
49+
secp: &Secp256k1<C>,
50+
) -> Result<Option<PrivateKey>, Self::Error> {
51+
match key_request {
52+
// NOTE: this KeyRequest does not occur for taproot signatures
53+
// even if the descriptor keys are definite, we will receive a bip32 request
54+
// instead based on `DescriptorPublicKey::Single(SinglePub { origin: None, key, })`
55+
psbt::KeyRequest::Pubkey(req_pubkey) => {
56+
if req_pubkey == self.pubkey {
57+
Ok(Some(self.privkey.clone()))
58+
} else {
59+
Ok(None)
60+
}
61+
}
62+
63+
psbt::KeyRequest::Bip32((fingerprint, path)) => {
64+
if fingerprint.eq(&self.fingerprint) || fingerprint.eq(&self.fingerprint_xonly) {
65+
Ok(Some(self.privkey.clone()))
66+
} else {
67+
Ok(None)
68+
}
69+
}
70+
71+
_ => Ok(None),
72+
}
73+
}
74+
}
75+
1376
#[wasm_bindgen]
1477
pub struct WrapPsbt(Psbt);
1578

@@ -69,8 +132,7 @@ impl WrapPsbt {
69132

70133
#[wasm_bindgen(js_name = signWithXprv)]
71134
pub fn sign_with_xprv(&mut self, xprv: String) -> Result<JsValue, JsError> {
72-
let key = miniscript::bitcoin::bip32::Xpriv::from_str(&xprv)
73-
.map_err(|_| JsError::new("Invalid xprv"))?;
135+
let key = bip32::Xpriv::from_str(&xprv).map_err(|_| JsError::new("Invalid xprv"))?;
74136
self.0
75137
.sign(&key, &Secp256k1::new())
76138
.map_err(|(_, errors)| JsError::new(&format!("{} errors: {:?}", errors.len(), errors)))
@@ -79,24 +141,12 @@ impl WrapPsbt {
79141

80142
#[wasm_bindgen(js_name = signWithPrv)]
81143
pub fn sign_with_prv(&mut self, prv: Vec<u8>) -> Result<JsValue, JsError> {
82-
let privkey = miniscript::bitcoin::PrivateKey::from_slice(
83-
&prv,
84-
miniscript::bitcoin::network::Network::Bitcoin,
85-
)
86-
.map_err(|_| JsError::new("Invalid private key"))?;
87-
let mut map: HashMap<miniscript::bitcoin::PublicKey, miniscript::bitcoin::PrivateKey> =
88-
std::collections::HashMap::new();
89-
map.insert(privkey.public_key(&Secp256k1::new()), privkey);
90-
map.insert(
91-
privkey
92-
.public_key(&Secp256k1::new())
93-
.to_x_only_pubkey()
94-
.to_public_key(),
95-
privkey,
96-
);
144+
let privkey = PrivateKey::from_slice(&prv, miniscript::bitcoin::network::Network::Bitcoin)
145+
.map_err(|_| JsError::new("Invalid private key"))?;
146+
let secp = Secp256k1::new();
97147
self.0
98-
.sign(&map, &Secp256k1::new())
99-
.map_err(|(_, errors)| JsError::new(&format!("{} errors: {:?}", errors.len(), errors)))
148+
.sign(&SingleKeySigner::from_privkey(privkey, &secp), &secp)
149+
.map_err(|(r, errors)| JsError::new(&format!("{} errors: {:?}", errors.len(), errors)))
100150
.and_then(|r| r.try_to_js_value())
101151
}
102152

@@ -107,3 +157,61 @@ impl WrapPsbt {
107157
.map_err(|vec_err| JsError::new(&format!("{} errors: {:?}", vec_err.len(), vec_err)))
108158
}
109159
}
160+
161+
#[cfg(test)]
162+
mod tests {
163+
use crate::psbt::SingleKeySigner;
164+
use base64::prelude::*;
165+
use miniscript::bitcoin::bip32::{DerivationPath, Fingerprint, KeySource};
166+
use miniscript::bitcoin::psbt::{SigningKeys, SigningKeysMap};
167+
use miniscript::bitcoin::secp256k1::Secp256k1;
168+
use miniscript::bitcoin::{PrivateKey, Psbt};
169+
use miniscript::psbt::PsbtExt;
170+
use miniscript::{DefiniteDescriptorKey, Descriptor, DescriptorPublicKey, ToPublicKey};
171+
use std::str::FromStr;
172+
173+
fn psbt_from_base64(s: &str) -> Psbt {
174+
let psbt = BASE64_STANDARD.decode(s.as_bytes()).unwrap();
175+
Psbt::deserialize(&psbt).unwrap()
176+
}
177+
178+
#[test]
179+
pub fn test_wrap_privkey() {
180+
let desc = "tr(039ab0771c5f88913208a26f81ab8223e98d25176e4648a5a2bb8ff79cf1c5198b,pk(039ab0771c5f88913208a26f81ab8223e98d25176e4648a5a2bb8ff79cf1c5198b))";
181+
let desc = Descriptor::<DefiniteDescriptorKey>::from_str(desc).unwrap();
182+
let psbt = "cHNidP8BAKYCAAAAAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAD9////AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAP3///8CgBoGAAAAAAAWABRTtvjcap+5t7odMosMnHl97YJClYAaBgAAAAAAIlEg1S2GuUvFU+Ve4XFLV65ffhuYsGeDkpaER6lQFjONAmEAAAAAAAEBK0BCDwAAAAAAIlEg1S2GuUvFU+Ve4XFLV65ffhuYsGeDkpaER6lQFjONAmEAAQErQEIPAAAAAAAiUSDVLYa5S8VT5V7hcUtXrl9+G5iwZ4OSloRHqVAWM40CYQAAAA==";
183+
let mut psbt = psbt_from_base64(psbt);
184+
psbt.update_input_with_descriptor(0, &desc).unwrap();
185+
println!("{:?}", psbt.inputs[0].tap_key_origins);
186+
let prv =
187+
PrivateKey::from_str("KzEGYtKcbhYwUWcZygbsqmF31f3iV7HC3iUQug7MBecwCz9hm1Tv").unwrap();
188+
let pk = prv.public_key(&Secp256k1::new()).to_x_only_pubkey();
189+
let secp = Secp256k1::new();
190+
let sks = SingleKeySigner::from_privkey(prv, &secp);
191+
psbt.inputs[0]
192+
.tap_key_origins
193+
.values()
194+
.for_each(|key_source| {
195+
let key_source_ref: KeySource = (
196+
Fingerprint::from_hex(&"aeee1e6a").unwrap(),
197+
DerivationPath::from(vec![]),
198+
);
199+
assert_eq!(key_source.1, key_source_ref);
200+
assert_eq!(sks.fingerprint, key_source.1 .0,);
201+
});
202+
let mut expected_keys = SigningKeysMap::new();
203+
expected_keys.insert(0, SigningKeys::Schnorr(vec![pk]));
204+
expected_keys.insert(1, SigningKeys::Schnorr(vec![]));
205+
assert_eq!(psbt.sign(&sks, &secp).unwrap(), expected_keys);
206+
}
207+
208+
#[test]
209+
fn test_tr_xpub() {
210+
let d = "tr(xpub661MyMwAqRbcEv1i36otFUwWZRcQBJHjdCoQvqykteW4sMHP3m4h9TzvPhK9q7rtkkWMMTJB4jFxCgVki9GwB9GvfHf366dpXDAaHHHdad2/*,{pk(xpub661MyMwAqRbcFod8uqcC3G2jub4McRVKZsZrvWZXAUFBjeuyMT2UqDFkw3TAUebQRAE7XQKFFhvLRW2mWvmKC2KzNuCkzVkFucWapGqnkXj/*),pk(xpub661MyMwAqRbcFVAMsxk7PkfGh66U9K9qWh2dvS5s4kL4JaDHdZdBbb4CbzQxZMC2MAUcKZudSk86RxeaTQctKa6tpSCPEkKGYfMEFDKWJu9/*)})";
211+
let desc = Descriptor::<DescriptorPublicKey>::from_str(d).unwrap();
212+
let psbt = "cHNidP8BAKYCAAAAAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAD9////AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAP3///8CgBoGAAAAAAAWABRTtvjcap+5t7odMosMnHl97YJClYAaBgAAAAAAIlEgBBlsh6bt3RStSy0egEjFHML8bVhqFYO8knG5OLcA/zcAAAAAAAEBK0BCDwAAAAAAIlEgBBlsh6bt3RStSy0egEjFHML8bVhqFYO8knG5OLcA/zcAAQErQEIPAAAAAAAiUSDFpFC16pT0pXIHKzV7teFiXul3DtlyYj9DdCpF1CHVQAAAAA==";
213+
let mut psbt = psbt_from_base64(psbt);
214+
psbt.update_input_with_descriptor(0, &desc.at_derivation_index(0).unwrap())
215+
.unwrap();
216+
}
217+
}

0 commit comments

Comments
 (0)