Skip to content

Commit f19e2de

Browse files
feat(miniscript): allow plain EC key singing for taproot descriptors
Adds SingleKeySigner implementation to sign with plain EC keys for taproot descriptors. Issue: beta
1 parent 72be5ac commit f19e2de

File tree

4 files changed

+226
-48
lines changed

4 files changed

+226
-48
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 = { git = "https://github.com/BitGo/rust-miniscript", branch = "opdrop" }
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/psbt.rs

Lines changed: 131 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,77 @@
1-
use std::collections::HashMap;
21
use crate::descriptor::WrapDescriptorEnum;
32
use crate::try_into_js_value::TryIntoJsValue;
43
use crate::WrapDescriptor;
5-
use miniscript::bitcoin::secp256k1::Secp256k1;
6-
use miniscript::bitcoin::Psbt;
4+
use miniscript::bitcoin::secp256k1::{Context, Secp256k1, Signing};
5+
use miniscript::bitcoin::{bip32, psbt, secp256k1, PublicKey, XOnlyPublicKey};
6+
use miniscript::bitcoin::{PrivateKey, Psbt};
7+
use miniscript::descriptor::{SinglePub, SinglePubKey};
78
use miniscript::psbt::PsbtExt;
9+
use miniscript::{DescriptorPublicKey, ToPublicKey};
810
use std::str::FromStr;
9-
use miniscript::ToPublicKey;
11+
use miniscript::bitcoin::bip32::Fingerprint;
1012
use wasm_bindgen::prelude::wasm_bindgen;
1113
use wasm_bindgen::{JsError, JsValue};
1214

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

@@ -69,8 +131,7 @@ impl WrapPsbt {
69131

70132
#[wasm_bindgen(js_name = signWithXprv)]
71133
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"))?;
134+
let key = bip32::Xpriv::from_str(&xprv).map_err(|_| JsError::new("Invalid xprv"))?;
74135
self.0
75136
.sign(&key, &Secp256k1::new())
76137
.map_err(|(_, errors)| JsError::new(&format!("{} errors: {:?}", errors.len(), errors)))
@@ -79,14 +140,12 @@ impl WrapPsbt {
79140

80141
#[wasm_bindgen(js_name = signWithPrv)]
81142
pub fn sign_with_prv(&mut self, prv: Vec<u8>) -> Result<JsValue, JsError> {
82-
let privkey = miniscript::bitcoin::PrivateKey::from_slice(&prv, miniscript::bitcoin::network::Network::Bitcoin)
143+
let privkey = PrivateKey::from_slice(&prv, miniscript::bitcoin::network::Network::Bitcoin)
83144
.map_err(|_| JsError::new("Invalid private key"))?;
84-
let mut map: HashMap<miniscript::bitcoin::PublicKey, miniscript::bitcoin::PrivateKey> = std::collections::HashMap::new();
85-
map.insert(privkey.public_key(&Secp256k1::new()), privkey);
86-
map.insert(privkey.public_key(&Secp256k1::new()).to_x_only_pubkey().to_public_key(), privkey);
145+
let secp = Secp256k1::new();
87146
self.0
88-
.sign(&map, &Secp256k1::new())
89-
.map_err(|(_, errors)| JsError::new(&format!("{} errors: {:?}", errors.len(), errors)))
147+
.sign(&SingleKeySigner::from_privkey(privkey, &secp), &secp)
148+
.map_err(|(r, errors)| JsError::new(&format!("{} errors: {:?}", errors.len(), errors)))
90149
.and_then(|r| r.try_to_js_value())
91150
}
92151

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

55
import { DescriptorNode, formatNode } from "../js/ast";
@@ -8,48 +8,66 @@ import { Descriptor } from "../js";
88
import { toWrappedPsbt } from "./psbt.util";
99

1010
function toKeyWithPath(k: BIP32Interface, path = "*"): string {
11-
return k.toBase58() + "/" + path;
11+
return k.neutered().toBase58() + "/" + path;
1212
}
1313

1414
function toKeyPlain(k: Buffer): string {
1515
return k.toString("hex");
1616
}
1717

18+
function toECPair(k: BIP32Interface): ECPairInterface {
19+
assert(k.privateKey);
20+
return ECPair.fromPrivateKey(k.privateKey);
21+
}
22+
23+
function toKeyPlainXOnly(k: Buffer): string {
24+
return k.subarray(1).toString("hex");
25+
}
26+
1827
const external = getKey("external");
1928
const a = getKey("a");
2029
const b = getKey("b");
2130
const c = getKey("c");
2231
const keys = { external, a, b, c };
23-
function getKeyName(bipKey: BIP32Interface) {
24-
return Object.keys(keys).find(
25-
(k) => keys[k as keyof typeof keys] === bipKey,
26-
) as keyof typeof keys;
32+
function getKeyName(k: BIP32Interface | ECPairInterface) {
33+
const objKeys = Object.keys(keys) as (keyof typeof keys)[];
34+
return objKeys.find(
35+
(key) => keys[key] === k || toECPair(keys[key]).publicKey.equals(k.publicKey),
36+
);
2737
}
2838

39+
type SigningKey = BIP32Interface | ECPairInterface;
40+
2941
function describeSignDescriptor(
3042
name: string,
31-
descriptor: DescriptorNode,
32-
signSeqs: BIP32Interface[][],
43+
descriptor: Descriptor,
44+
{
45+
signBip32 = [],
46+
signECPair = [],
47+
}: {
48+
signBip32?: BIP32Interface[][];
49+
signECPair?: ECPairInterface[][];
50+
},
3351
) {
3452
describe(`psbt with descriptor ${name}`, function () {
35-
const isTaproot = Object.keys(descriptor)[0] === "tr";
53+
const isTaproot = Object.keys(descriptor.node())[0] === "Tr";
3654
const psbt = mockPsbtDefault({
37-
descriptorSelf: Descriptor.fromString(formatNode(descriptor), "derivable"),
55+
descriptorSelf: descriptor,
3856
descriptorOther: Descriptor.fromString(
3957
formatNode({ wpkh: toKeyWithPath(external) }),
4058
"derivable",
4159
),
4260
});
4361

44-
function getSigResult(keys: BIP32Interface[]) {
62+
function getSigResult(keys: (BIP32Interface | ECPairInterface)[]) {
4563
return {
4664
[isTaproot ? "Schnorr" : "Ecdsa"]: keys.map((key) =>
4765
key.publicKey.subarray(isTaproot ? 1 : 0).toString("hex"),
4866
),
4967
};
5068
}
5169

52-
signSeqs.forEach((signSeq, i) => {
70+
signBip32.forEach((signSeq, i) => {
5371
it(`should sign ${signSeq.map((k) => getKeyName(k))} xprv`, function () {
5472
const wrappedPsbt = toWrappedPsbt(psbt);
5573
signSeq.forEach((key) => {
@@ -62,52 +80,83 @@ function describeSignDescriptor(
6280
});
6381

6482
it(`should sign ${signSeq.map((k) => getKeyName(k))} prv buffer`, function () {
65-
if (isTaproot) {
66-
// signing with non-bip32 taproot keys is not supported apparently
67-
this.skip();
68-
}
6983
const wrappedPsbt = toWrappedPsbt(psbt);
7084
signSeq.forEach((key) => {
7185
assert.deepStrictEqual(wrappedPsbt.signWithPrv(key.derive(0).privateKey), {
72-
0: getSigResult([key.derive(0)]),
86+
// NOTE: signing with a plain derived key does not work for taproot
87+
// see SingleKeySigner implementation in psbt.rs for details
88+
0: getSigResult(isTaproot ? [] : [key.derive(0)]),
7389
1: getSigResult([]),
7490
});
7591
});
7692
});
7793
});
94+
95+
signECPair.forEach((signSeq, i) => {
96+
it(`should sign ${signSeq.map((k) => getKeyName(k))} ec pair`, function () {
97+
const wrappedPsbt = toWrappedPsbt(psbt);
98+
signSeq.forEach((key) => {
99+
assert(key.privateKey);
100+
assert.deepStrictEqual(wrappedPsbt.signWithPrv(key.privateKey), {
101+
0: getSigResult([key]),
102+
1: getSigResult([key]),
103+
});
104+
});
105+
wrappedPsbt.finalize();
106+
});
107+
});
78108
});
79109
}
80110

111+
function fromNodes(node: DescriptorNode, type: "definite" | "derivable") {
112+
return Descriptor.fromString(formatNode(node), type);
113+
}
114+
81115
describeSignDescriptor(
82116
"Wsh2Of3",
117+
fromNodes(
118+
{
119+
wsh: { multi: [2, toKeyWithPath(a), toKeyWithPath(b), toKeyWithPath(c)] },
120+
},
121+
"derivable",
122+
),
83123
{
84-
wsh: { multi: [2, toKeyWithPath(a), toKeyWithPath(b), toKeyWithPath(c)] },
124+
signBip32: [
125+
[a, b],
126+
[b, a],
127+
],
85128
},
86-
[
87-
[a, b],
88-
[b, a],
89-
],
90129
);
91130

92131
describeSignDescriptor(
93132
"Tr1Of3",
94-
{
95-
tr: [toKeyWithPath(a), [{ pk: toKeyWithPath(b) }, { pk: toKeyWithPath(c) }]],
96-
},
97-
[[a], [b], [c]],
133+
fromNodes(
134+
{
135+
tr: [toKeyWithPath(a), [{ pk: toKeyWithPath(b) }, { pk: toKeyWithPath(c) }]],
136+
},
137+
"derivable",
138+
),
139+
{ signBip32: [[a], [b], [c]] },
98140
);
99141

100-
// while we cannot sign with a derived plain xonly key, we can sign with an xprv
101142
describeSignDescriptor(
102143
"TrWithExternalPlain",
103-
{
104-
tr: [
105-
toKeyPlain(external.publicKey),
106-
[
107-
{ pk: toKeyPlain(external.publicKey) },
108-
{ or_b: [{ pk: toKeyPlain(external.publicKey) }, { "s:pk": toKeyWithPath(a) }] },
144+
fromNodes(
145+
{
146+
tr: [
147+
toKeyPlainXOnly(external.publicKey),
148+
[
149+
{ pk: toKeyPlainXOnly(external.publicKey) },
150+
{
151+
or_b: [
152+
{ pk: toKeyPlainXOnly(external.publicKey) },
153+
{ "s:pk": toKeyPlainXOnly(a.publicKey) },
154+
],
155+
},
156+
],
109157
],
110-
],
111-
},
112-
[[a]],
158+
},
159+
"definite",
160+
),
161+
{ signECPair: [[toECPair(a)]] },
113162
);

0 commit comments

Comments
 (0)