Skip to content

Commit 57ea243

Browse files
feat(core): add PSBT signing with private key buffers
Implement PSBT signing with raw private keys alongside xprv-based signing. Add tests to verify both signing methods. Issue: BTC-1845
1 parent 4fbdd32 commit 57ea243

File tree

3 files changed

+74
-2
lines changed

3 files changed

+74
-2
lines changed

packages/wasm-miniscript/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ declare module "./wasm/wasm_miniscript" {
3434

3535
interface WrapPsbt {
3636
signWithXprv(this: WrapPsbt, xprv: string): SignPsbtResult;
37+
signWithPrv(this: WrapPsbt, prv: Buffer): SignPsbtResult;
3738
}
3839
}
3940

packages/wasm-miniscript/src/psbt.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use crate::WrapDescriptor;
44
use miniscript::bitcoin::secp256k1::Secp256k1;
55
use miniscript::bitcoin::Psbt;
66
use miniscript::psbt::PsbtExt;
7+
use miniscript::ToPublicKey;
8+
use std::collections::HashMap;
79
use std::str::FromStr;
810
use wasm_bindgen::prelude::wasm_bindgen;
911
use wasm_bindgen::{JsError, JsValue};
@@ -75,6 +77,29 @@ impl WrapPsbt {
7577
.and_then(|r| r.try_to_js_value())
7678
}
7779

80+
#[wasm_bindgen(js_name = signWithPrv)]
81+
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+
);
97+
self.0
98+
.sign(&map, &Secp256k1::new())
99+
.map_err(|(_, errors)| JsError::new(&format!("{} errors: {:?}", errors.len(), errors)))
100+
.and_then(|r| r.try_to_js_value())
101+
}
102+
78103
#[wasm_bindgen(js_name = finalize)]
79104
pub fn finalize_mut(&mut self) -> Result<(), JsError> {
80105
self.0

packages/wasm-miniscript/test/psbtFromDescriptor.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import assert from "node:assert";
12
import { BIP32Interface } from "@bitgo/utxo-lib";
23
import { getKey } from "@bitgo/utxo-lib/dist/src/testutil";
34

@@ -10,6 +11,10 @@ function toKeyWithPath(k: BIP32Interface, path = "*"): string {
1011
return k.toBase58() + "/" + path;
1112
}
1213

14+
function toKeyPlain(k: Buffer): string {
15+
return k.toString("hex");
16+
}
17+
1318
const external = getKey("external");
1419
const a = getKey("a");
1520
const b = getKey("b");
@@ -27,6 +32,7 @@ function describeSignDescriptor(
2732
signSeqs: BIP32Interface[][],
2833
) {
2934
describe(`psbt with descriptor ${name}`, function () {
35+
const isTaproot = Object.keys(descriptor)[0] === "tr";
3036
const psbt = mockPsbtDefault({
3137
descriptorSelf: Descriptor.fromString(formatNode(descriptor), "derivable"),
3238
descriptorOther: Descriptor.fromString(
@@ -35,14 +41,39 @@ function describeSignDescriptor(
3541
),
3642
});
3743

44+
function getSigResult(keys: BIP32Interface[]) {
45+
return {
46+
[isTaproot ? "Schnorr" : "Ecdsa"]: keys.map((key) =>
47+
key.publicKey.subarray(isTaproot ? 1 : 0).toString("hex"),
48+
),
49+
};
50+
}
51+
3852
signSeqs.forEach((signSeq, i) => {
39-
it(`should sign ${signSeq.map((k) => getKeyName(k))}`, function () {
53+
it(`should sign ${signSeq.map((k) => getKeyName(k))} xprv`, function () {
4054
const wrappedPsbt = toWrappedPsbt(psbt);
4155
signSeq.forEach((key) => {
42-
wrappedPsbt.signWithXprv(key.toBase58());
56+
assert.deepStrictEqual(wrappedPsbt.signWithXprv(key.toBase58()), {
57+
0: getSigResult([key.derive(0)]),
58+
1: getSigResult([key.derive(1)]),
59+
});
4360
});
4461
wrappedPsbt.finalize();
4562
});
63+
64+
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+
}
69+
const wrappedPsbt = toWrappedPsbt(psbt);
70+
signSeq.forEach((key) => {
71+
assert.deepStrictEqual(wrappedPsbt.signWithPrv(key.derive(0).privateKey), {
72+
0: getSigResult([key.derive(0)]),
73+
1: getSigResult([]),
74+
});
75+
});
76+
});
4677
});
4778
});
4879
}
@@ -65,3 +96,18 @@ describeSignDescriptor(
6596
},
6697
[[a], [b], [c]],
6798
);
99+
100+
// while we cannot sign with a derived plain xonly key, we can sign with an xprv
101+
describeSignDescriptor(
102+
"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) }] },
109+
],
110+
],
111+
},
112+
[[a]],
113+
);

0 commit comments

Comments
 (0)