@bitgo/wasm-utxo 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/js/fixedScriptWallet/BitGoPsbt.d.ts +99 -0
- package/dist/cjs/js/fixedScriptWallet/BitGoPsbt.js +122 -0
- package/dist/cjs/js/wasm/wasm_utxo.d.ts +86 -0
- package/dist/cjs/js/wasm/wasm_utxo.js +277 -25
- package/dist/cjs/js/wasm/wasm_utxo_bg.wasm +0 -0
- package/dist/cjs/js/wasm/wasm_utxo_bg.wasm.d.ts +21 -17
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/js/fixedScriptWallet/BitGoPsbt.d.ts +99 -0
- package/dist/esm/js/fixedScriptWallet/BitGoPsbt.js +122 -0
- package/dist/esm/js/wasm/wasm_utxo.d.ts +86 -0
- package/dist/esm/js/wasm/wasm_utxo_bg.js +277 -25
- package/dist/esm/js/wasm/wasm_utxo_bg.wasm +0 -0
- package/dist/esm/js/wasm/wasm_utxo_bg.wasm.d.ts +21 -17
- package/dist/esm/test/fixedScript/fixtureUtil.d.ts +8 -0
- package/dist/esm/test/fixedScript/fixtureUtil.js +10 -0
- package/dist/esm/test/fixedScript/musig2Nonces.js +77 -0
- package/dist/esm/test/fixedScript/signAndVerifySignature.d.ts +1 -0
- package/dist/esm/test/fixedScript/{verifySignature.js → signAndVerifySignature.js} +105 -24
- package/dist/esm/test/fixedScript/walletKeys.util.d.ts +12 -0
- package/dist/esm/test/fixedScript/walletKeys.util.js +17 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- /package/dist/esm/test/fixedScript/{verifySignature.d.ts → musig2Nonces.d.ts} +0 -0
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import * as utxolib from "@bitgo/utxo-lib";
|
|
3
|
-
import {
|
|
4
|
-
import { loadPsbtFixture, loadWalletKeysFromFixture,
|
|
3
|
+
import { BIP32 } from "../../js/index.js";
|
|
4
|
+
import { loadPsbtFixture, loadWalletKeysFromFixture, getBitGoPsbt, loadReplayProtectionKeyFromFixture, } from "./fixtureUtil.js";
|
|
5
|
+
/**
|
|
6
|
+
* Load xprivs from a fixture
|
|
7
|
+
* @param fixture - The test fixture
|
|
8
|
+
* @returns The xprivs for user, backup, and bitgo keys
|
|
9
|
+
*/
|
|
10
|
+
function loadXprivsFromFixture(fixture) {
|
|
11
|
+
const [userXpriv, backupXpriv, bitgoXpriv] = fixture.walletKeys.map((xprv) => BIP32.fromBase58(xprv));
|
|
12
|
+
return {
|
|
13
|
+
user: userXpriv,
|
|
14
|
+
backup: backupXpriv,
|
|
15
|
+
bitgo: bitgoXpriv,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
5
18
|
/**
|
|
6
19
|
* Get expected signature state for an input based on type and signing stage
|
|
7
|
-
* @param inputType - The type of input (e.g., "p2shP2pk", "p2trMusig2")
|
|
20
|
+
* @param inputType - The type of input (e.g., "p2shP2pk", "p2trMusig2", "taprootKeyPathSpend")
|
|
8
21
|
* @param signatureStage - The signing stage (unsigned, halfsigned, fullsigned)
|
|
9
22
|
* @returns Expected signature state for replay protection OR multi-key signatures
|
|
10
23
|
*/
|
|
@@ -59,9 +72,10 @@ function verifyInputSignatures(bitgoPsbt, parsed, rootWalletKeys, replayProtecti
|
|
|
59
72
|
const hasUserSig = bitgoPsbt.verifySignature(inputIndex, rootWalletKeys.userKey());
|
|
60
73
|
const hasBackupSig = bitgoPsbt.verifySignature(inputIndex, rootWalletKeys.backupKey());
|
|
61
74
|
const hasBitGoSig = bitgoPsbt.verifySignature(inputIndex, rootWalletKeys.bitgoKey());
|
|
62
|
-
|
|
63
|
-
assert.strictEqual(
|
|
64
|
-
assert.strictEqual(
|
|
75
|
+
const scriptType = parsed.inputs[inputIndex].scriptType;
|
|
76
|
+
assert.strictEqual(hasUserSig, expectedSignatures.user, `Input ${inputIndex} user key signature mismatch type=${scriptType}`);
|
|
77
|
+
assert.strictEqual(hasBackupSig, expectedSignatures.backup, `Input ${inputIndex} backup key signature mismatch type=${scriptType}`);
|
|
78
|
+
assert.strictEqual(hasBitGoSig, expectedSignatures.bitgo, `Input ${inputIndex} BitGo key signature mismatch type=${scriptType}`);
|
|
65
79
|
}
|
|
66
80
|
/**
|
|
67
81
|
* Helper to verify signatures for all inputs in a PSBT
|
|
@@ -79,6 +93,71 @@ function verifyAllInputSignatures(bitgoPsbt, fixture, rootWalletKeys, replayProt
|
|
|
79
93
|
verifyInputSignatures(bitgoPsbt, parsed, rootWalletKeys, replayProtectionKey, index, getExpectedSignatures(input.type, signatureStage));
|
|
80
94
|
});
|
|
81
95
|
}
|
|
96
|
+
function signInputAndVerify(bitgoPsbt, index, key, keyName, inputType) {
|
|
97
|
+
bitgoPsbt.sign(index, key);
|
|
98
|
+
assert.strictEqual(bitgoPsbt.verifySignature(index, key), true, `Input ${index} signature mismatch key=${keyName} type=${inputType}`);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Sign all inputs in a PSBT according to the signature stage
|
|
102
|
+
* @param bitgoPsbt - The PSBT to sign
|
|
103
|
+
* @param rootWalletKeys - Wallet keys for parsing the transaction
|
|
104
|
+
* @param xprivs - The xprivs to use for signing
|
|
105
|
+
* @param replayProtectionKey - The ECPair for signing replay protection (p2shP2pk) inputs
|
|
106
|
+
*/
|
|
107
|
+
function signAllInputs(bitgoPsbt, rootWalletKeys, xprivs, replayProtectionKey) {
|
|
108
|
+
// Parse transaction to get input types
|
|
109
|
+
const parsed = bitgoPsbt.parseTransactionWithWalletKeys(rootWalletKeys, {
|
|
110
|
+
publicKeys: [replayProtectionKey],
|
|
111
|
+
});
|
|
112
|
+
// Generate MuSig2 nonces for user and backup keys (MuSig2 uses 2-of-2 with user+backup)
|
|
113
|
+
bitgoPsbt.generateMusig2Nonces(xprivs.user);
|
|
114
|
+
bitgoPsbt.generateMusig2Nonces(xprivs.bitgo);
|
|
115
|
+
// First pass: sign with user key (skip p2shP2pk inputs)
|
|
116
|
+
parsed.inputs.forEach((input, index) => {
|
|
117
|
+
switch (input.scriptType) {
|
|
118
|
+
case "p2shP2pk":
|
|
119
|
+
break;
|
|
120
|
+
default:
|
|
121
|
+
signInputAndVerify(bitgoPsbt, index, xprivs.user, "user", input.scriptType);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// Second pass: sign with appropriate second key
|
|
126
|
+
parsed.inputs.forEach((input, index) => {
|
|
127
|
+
switch (input.scriptType) {
|
|
128
|
+
case "p2shP2pk":
|
|
129
|
+
signInputAndVerify(bitgoPsbt, index, replayProtectionKey, "replayProtection", input.scriptType);
|
|
130
|
+
break;
|
|
131
|
+
case "p2trMusig2ScriptPath":
|
|
132
|
+
// MuSig2 script path inputs use backup key for second signature
|
|
133
|
+
signInputAndVerify(bitgoPsbt, index, xprivs.backup, "backup", input.scriptType);
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
// Regular multisig uses bitgo key
|
|
137
|
+
signInputAndVerify(bitgoPsbt, index, xprivs.bitgo, "bitgo", input.scriptType);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Run tests for a fixture: load PSBT, verify, sign, and verify again
|
|
144
|
+
* @param fixture - The test fixture
|
|
145
|
+
* @param networkName - The network name for deserializing the PSBT
|
|
146
|
+
* @param rootWalletKeys - Wallet keys for verification
|
|
147
|
+
* @param replayProtectionKey - Key for replay protection inputs
|
|
148
|
+
* @param xprivs - The xprivs to use for signing
|
|
149
|
+
* @param signatureStage - The current signing stage
|
|
150
|
+
*/
|
|
151
|
+
function runTestsForFixture(fixture, networkName, rootWalletKeys, replayProtectionKey, xprivs, signatureStage) {
|
|
152
|
+
// Load PSBT from fixture
|
|
153
|
+
const bitgoPsbt = getBitGoPsbt(fixture, networkName);
|
|
154
|
+
// Verify current state
|
|
155
|
+
verifyAllInputSignatures(bitgoPsbt, fixture, rootWalletKeys, replayProtectionKey, signatureStage);
|
|
156
|
+
// Sign inputs (if not already fully signed)
|
|
157
|
+
if (signatureStage !== "unsigned") {
|
|
158
|
+
signAllInputs(bitgoPsbt, rootWalletKeys, xprivs, replayProtectionKey);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
82
161
|
describe("verifySignature", function () {
|
|
83
162
|
const supportedNetworks = utxolib.getNetworkList().filter((network) => {
|
|
84
163
|
return (utxolib.isMainnet(network) &&
|
|
@@ -93,65 +172,65 @@ describe("verifySignature", function () {
|
|
|
93
172
|
describe(`network: ${networkName}`, function () {
|
|
94
173
|
let rootWalletKeys;
|
|
95
174
|
let replayProtectionKey;
|
|
175
|
+
let xprivs;
|
|
96
176
|
let unsignedFixture;
|
|
97
177
|
let halfsignedFixture;
|
|
98
178
|
let fullsignedFixture;
|
|
99
|
-
let unsignedBitgoPsbt;
|
|
100
|
-
let halfsignedBitgoPsbt;
|
|
101
|
-
let fullsignedBitgoPsbt;
|
|
102
179
|
before(function () {
|
|
103
180
|
unsignedFixture = loadPsbtFixture(networkName, "unsigned");
|
|
104
181
|
halfsignedFixture = loadPsbtFixture(networkName, "halfsigned");
|
|
105
182
|
fullsignedFixture = loadPsbtFixture(networkName, "fullsigned");
|
|
106
183
|
rootWalletKeys = loadWalletKeysFromFixture(fullsignedFixture);
|
|
107
184
|
replayProtectionKey = loadReplayProtectionKeyFromFixture(fullsignedFixture);
|
|
108
|
-
|
|
109
|
-
halfsignedBitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(getPsbtBuffer(halfsignedFixture), networkName);
|
|
110
|
-
fullsignedBitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(getPsbtBuffer(fullsignedFixture), networkName);
|
|
185
|
+
xprivs = loadXprivsFromFixture(fullsignedFixture);
|
|
111
186
|
});
|
|
112
187
|
describe("unsigned PSBT", function () {
|
|
113
|
-
it("should return false for unsigned inputs", function () {
|
|
114
|
-
|
|
188
|
+
it("should return false for unsigned inputs, then sign and verify", function () {
|
|
189
|
+
runTestsForFixture(unsignedFixture, networkName, rootWalletKeys, replayProtectionKey, xprivs, "unsigned");
|
|
115
190
|
});
|
|
116
191
|
});
|
|
117
192
|
describe("half-signed PSBT", function () {
|
|
118
|
-
it("should return true for signed xpubs and false for unsigned", function () {
|
|
119
|
-
|
|
193
|
+
it("should return true for signed xpubs and false for unsigned, then sign and verify", function () {
|
|
194
|
+
runTestsForFixture(halfsignedFixture, networkName, rootWalletKeys, replayProtectionKey, xprivs, "halfsigned");
|
|
120
195
|
});
|
|
121
196
|
});
|
|
122
197
|
describe("fully signed PSBT", function () {
|
|
123
198
|
it("should have 2 signatures (2-of-3 multisig)", function () {
|
|
124
|
-
|
|
199
|
+
runTestsForFixture(fullsignedFixture, networkName, rootWalletKeys, replayProtectionKey, xprivs, "fullsigned");
|
|
125
200
|
});
|
|
126
201
|
});
|
|
127
202
|
describe("error handling", function () {
|
|
128
203
|
it("should throw error for out of bounds input index", function () {
|
|
204
|
+
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
129
205
|
assert.throws(() => {
|
|
130
|
-
|
|
206
|
+
psbt.verifySignature(999, rootWalletKeys.userKey());
|
|
131
207
|
}, (error) => {
|
|
132
208
|
return error.message.includes("Input index 999 out of bounds");
|
|
133
209
|
}, "Should throw error for out of bounds input index");
|
|
134
210
|
});
|
|
135
211
|
it("should throw error for invalid xpub", function () {
|
|
212
|
+
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
136
213
|
assert.throws(() => {
|
|
137
|
-
|
|
214
|
+
psbt.verifySignature(0, "invalid-xpub");
|
|
138
215
|
}, (error) => {
|
|
139
216
|
return error.message.includes("Invalid");
|
|
140
217
|
}, "Should throw error for invalid xpub");
|
|
141
218
|
});
|
|
142
219
|
it("should return false for xpub not in derivation path", function () {
|
|
220
|
+
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
143
221
|
// Create a different xpub that's not in the wallet
|
|
144
222
|
// Use a proper 32-byte seed (256 bits)
|
|
145
223
|
const differentSeed = Buffer.alloc(32, 0xaa); // 32 bytes filled with 0xaa
|
|
146
224
|
const differentKey = BIP32.fromSeed(differentSeed);
|
|
147
225
|
const differentXpub = differentKey.neutered();
|
|
148
|
-
const result =
|
|
226
|
+
const result = psbt.verifySignature(0, differentXpub);
|
|
149
227
|
assert.strictEqual(result, false, "Should return false for xpub not in PSBT derivation paths");
|
|
150
228
|
});
|
|
151
229
|
it("should verify signature with raw public key (Uint8Array)", function () {
|
|
230
|
+
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
152
231
|
// Verify that xpub-based verification works
|
|
153
232
|
const userKey = rootWalletKeys.userKey();
|
|
154
|
-
const hasXpubSig =
|
|
233
|
+
const hasXpubSig = psbt.verifySignature(0, userKey);
|
|
155
234
|
// This test specifically checks that raw public key verification works
|
|
156
235
|
// We test the underlying WASM API by ensuring both xpub and raw pubkey
|
|
157
236
|
// calls reach the correct methods
|
|
@@ -160,23 +239,25 @@ describe("verifySignature", function () {
|
|
|
160
239
|
const randomKey = BIP32.fromSeed(randomSeed);
|
|
161
240
|
const randomPubkey = randomKey.publicKey;
|
|
162
241
|
// This should return false (no signature for this key)
|
|
163
|
-
const result =
|
|
242
|
+
const result = psbt.verifySignature(0, randomPubkey);
|
|
164
243
|
assert.strictEqual(result, false, "Should return false for public key not in PSBT");
|
|
165
244
|
// Verify the xpub check still works (regression test)
|
|
166
245
|
assert.strictEqual(hasXpubSig, true, "Should still verify with xpub");
|
|
167
246
|
});
|
|
168
247
|
it("should return false for raw public key with no signature", function () {
|
|
248
|
+
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
169
249
|
// Create a random public key that's not in the PSBT
|
|
170
250
|
const randomSeed = Buffer.alloc(32, 0xbb);
|
|
171
251
|
const randomKey = BIP32.fromSeed(randomSeed);
|
|
172
252
|
const randomPubkey = randomKey.publicKey;
|
|
173
|
-
const result =
|
|
253
|
+
const result = psbt.verifySignature(0, randomPubkey);
|
|
174
254
|
assert.strictEqual(result, false, "Should return false for public key not in PSBT signatures");
|
|
175
255
|
});
|
|
176
256
|
it("should throw error for invalid key length", function () {
|
|
257
|
+
const psbt = getBitGoPsbt(fullsignedFixture, networkName);
|
|
177
258
|
const invalidKey = Buffer.alloc(31); // Invalid length (should be 32 for private key or 33 for public key)
|
|
178
259
|
assert.throws(() => {
|
|
179
|
-
|
|
260
|
+
psbt.verifySignature(0, invalidKey);
|
|
180
261
|
}, (error) => {
|
|
181
262
|
return error.message.includes("Invalid key length");
|
|
182
263
|
}, "Should throw error for invalid key length");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as utxolib from "@bitgo/utxo-lib";
|
|
2
|
+
import type { Triple } from "../../js/triple.js";
|
|
3
|
+
/**
|
|
4
|
+
* Convert utxolib BIP32 keys to WASM wallet keys format (Triple<string>)
|
|
5
|
+
*/
|
|
6
|
+
export declare function toWasmWalletKeys(keys: [utxolib.BIP32Interface, utxolib.BIP32Interface, utxolib.BIP32Interface]): Triple<string>;
|
|
7
|
+
/**
|
|
8
|
+
* Get standard replay protection configuration
|
|
9
|
+
*/
|
|
10
|
+
export declare function getStandardReplayProtection(): {
|
|
11
|
+
outputScripts: Uint8Array[];
|
|
12
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert utxolib BIP32 keys to WASM wallet keys format (Triple<string>)
|
|
3
|
+
*/
|
|
4
|
+
export function toWasmWalletKeys(keys) {
|
|
5
|
+
return [
|
|
6
|
+
keys[0].neutered().toBase58(),
|
|
7
|
+
keys[1].neutered().toBase58(),
|
|
8
|
+
keys[2].neutered().toBase58(),
|
|
9
|
+
];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get standard replay protection configuration
|
|
13
|
+
*/
|
|
14
|
+
export function getStandardReplayProtection() {
|
|
15
|
+
const replayProtectionScript = Buffer.from("a91420b37094d82a513451ff0ccd9db23aba05bc5ef387", "hex");
|
|
16
|
+
return { outputScripts: [replayProtectionScript] };
|
|
17
|
+
}
|