@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.
@@ -1,10 +1,23 @@
1
1
  import assert from "node:assert";
2
2
  import * as utxolib from "@bitgo/utxo-lib";
3
- import { fixedScriptWallet, BIP32 } from "../../js/index.js";
4
- import { loadPsbtFixture, loadWalletKeysFromFixture, getPsbtBuffer, loadReplayProtectionKeyFromFixture, } from "./fixtureUtil.js";
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
- assert.strictEqual(hasUserSig, expectedSignatures.user, `Input ${inputIndex} user key signature mismatch`);
63
- assert.strictEqual(hasBackupSig, expectedSignatures.backup, `Input ${inputIndex} backup key signature mismatch`);
64
- assert.strictEqual(hasBitGoSig, expectedSignatures.bitgo, `Input ${inputIndex} BitGo key signature mismatch`);
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
- unsignedBitgoPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(getPsbtBuffer(unsignedFixture), networkName);
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
- verifyAllInputSignatures(unsignedBitgoPsbt, unsignedFixture, rootWalletKeys, replayProtectionKey, "unsigned");
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
- verifyAllInputSignatures(halfsignedBitgoPsbt, halfsignedFixture, rootWalletKeys, replayProtectionKey, "halfsigned");
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
- verifyAllInputSignatures(fullsignedBitgoPsbt, fullsignedFixture, rootWalletKeys, replayProtectionKey, "fullsigned");
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
- fullsignedBitgoPsbt.verifySignature(999, rootWalletKeys.userKey());
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
- fullsignedBitgoPsbt.verifySignature(0, "invalid-xpub");
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 = fullsignedBitgoPsbt.verifySignature(0, differentXpub);
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 = fullsignedBitgoPsbt.verifySignature(0, userKey);
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 = fullsignedBitgoPsbt.verifySignature(0, randomPubkey);
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 = fullsignedBitgoPsbt.verifySignature(0, randomPubkey);
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
- fullsignedBitgoPsbt.verifySignature(0, invalidKey);
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
+ }