Skip to content

Commit 63c7108

Browse files
test(wasm-utxo): remove utxo-lib dependencies from fixed-script tests
Remove utxo-lib dependencies from fixed-script PSBT tests and implement fixture auto-generation. Tests now generate fixtures on-demand when missing, ensuring consistency across all signature states (unsigned, halfsigned, fullsigned). Changes: - Implement fixture generation in generateFixture.ts - Update fixtureUtil to auto-generate missing fixtures - Remove utxo-lib network references, use NetworkName type - Remove txid validation against utxo-lib - Remove OP_RETURN output construction via utxo-lib - Update all test files to use async fixture loading - Switch to format validation for txids instead of cross-library checks Issue: BTC-3047 Co-authored-by: llm-git <llm-git@ttll.de> feat(wasm-utxo): attach non_witness_utxo to PSBT inputs in AcidTest Use the new Transaction builder to construct a fake previous transaction for each input when txFormat is "psbt", populating non_witness_utxo. refactor(wasm-utxo): make txFormat a parameter in fixture generation Accept txFormat in generateAllStates() and loadPsbtFixture() instead of hardcoding "psbt-lite", enabling generation of both psbt and psbt-lite fixtures from the same infrastructure. test(wasm-utxo): run fixed-script tests across both psbt and psbt-lite formats Iterate over txFormats in parseTransactionWithWalletKeys and signAndVerifySignature, skipping psbt for zcash which doesn't support non_witness_utxo. update fixtures test(wasm-utxo): add psbt-format fixtures for all 7 coins Generate unsigned/halfsigned/fullsigned PSBT fixtures (with non_witness_utxo) for bitcoin, bitcoincash, bitcoingold, dash, dogecoin, ecash, and litecoin.
1 parent cfcddc0 commit 63c7108

116 files changed

Lines changed: 15627 additions & 1161 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/wasm-utxo/js/coinName.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,54 @@ export function isTestnet(name: CoinName): boolean {
6464
export function isCoinName(v: string): v is CoinName {
6565
return (coinNames as readonly string[]).includes(v);
6666
}
67+
68+
import type { UtxolibName } from "./utxolibCompat.js";
69+
70+
/** Convert a CoinName or UtxolibName to CoinName */
71+
export function toCoinName(name: CoinName | UtxolibName): CoinName {
72+
switch (name) {
73+
case "bitcoin":
74+
return "btc";
75+
case "testnet":
76+
return "tbtc";
77+
case "bitcoinTestnet4":
78+
return "tbtc4";
79+
case "bitcoinPublicSignet":
80+
return "tbtcsig";
81+
case "bitcoinBitGoSignet":
82+
return "tbtcbgsig";
83+
case "bitcoincash":
84+
return "bch";
85+
case "bitcoincashTestnet":
86+
return "tbch";
87+
case "ecash":
88+
return "bcha";
89+
case "ecashTest":
90+
return "tbcha";
91+
case "bitcoingold":
92+
return "btg";
93+
case "bitcoingoldTestnet":
94+
return "tbtg";
95+
case "bitcoinsv":
96+
return "bsv";
97+
case "bitcoinsvTestnet":
98+
return "tbsv";
99+
case "dashTest":
100+
return "tdash";
101+
case "dogecoin":
102+
return "doge";
103+
case "dogecoinTest":
104+
return "tdoge";
105+
case "litecoin":
106+
return "ltc";
107+
case "litecoinTest":
108+
return "tltc";
109+
case "zcash":
110+
return "zec";
111+
case "zcashTest":
112+
return "tzec";
113+
default:
114+
// CoinName values pass through (including "dash" which is both CoinName and UtxolibName)
115+
return name;
116+
}
117+
}

packages/wasm-utxo/js/testutils/AcidTest.ts

Lines changed: 97 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { ZcashBitGoPsbt } from "../fixedScriptWallet/ZcashBitGoPsbt.js";
33
import { RootWalletKeys } from "../fixedScriptWallet/RootWalletKeys.js";
44
import { BIP32 } from "../bip32.js";
55
import { ECPair } from "../ecpair.js";
6+
import { Transaction } from "../transaction.js";
67
import {
7-
assertChainCode,
88
ChainCode,
99
createOpReturnScript,
1010
inputScriptTypes,
1111
outputScript,
1212
outputScriptTypes,
13+
p2shP2pkOutputScript,
1314
supportsScriptType,
1415
type InputScriptType,
1516
type OutputScriptType,
@@ -91,6 +92,22 @@ type SuiteConfig = {
9192
// Re-export for convenience
9293
export { inputScriptTypes, outputScriptTypes };
9394

95+
/** Map InputScriptType to the OutputScriptType used for chain code derivation */
96+
function inputScriptTypeToOutputScriptType(scriptType: InputScriptType): OutputScriptType {
97+
switch (scriptType) {
98+
case "p2sh":
99+
case "p2shP2wsh":
100+
case "p2wsh":
101+
case "p2trLegacy":
102+
return scriptType;
103+
case "p2shP2pk":
104+
return "p2sh";
105+
case "p2trMusig2ScriptPath":
106+
case "p2trMusig2KeyPath":
107+
return "p2trMusig2";
108+
}
109+
}
110+
94111
/**
95112
* Creates a valid PSBT with as many features as possible (kitchen sink).
96113
*
@@ -158,6 +175,7 @@ export class AcidTest {
158175
): AcidTest {
159176
const rootWalletKeys = getDefaultWalletKeys();
160177
const otherWalletKeys = getWalletKeysForSeed("too many secrets");
178+
const coin = network;
161179

162180
// Filter inputs based on network support
163181
const inputs: Input[] = inputScriptTypes
@@ -167,9 +185,9 @@ export class AcidTest {
167185

168186
// Map input script types to output script types for support check
169187
if (scriptType === "p2trMusig2KeyPath" || scriptType === "p2trMusig2ScriptPath") {
170-
return supportsScriptType(network, "p2trMusig2");
188+
return supportsScriptType(coin, "p2trMusig2");
171189
}
172-
return supportsScriptType(network, scriptType);
190+
return supportsScriptType(coin, scriptType);
173191
})
174192
.filter(
175193
(scriptType) =>
@@ -183,7 +201,7 @@ export class AcidTest {
183201

184202
// Filter outputs based on network support
185203
const outputs: Output[] = outputScriptTypes
186-
.filter((scriptType) => supportsScriptType(network, scriptType))
204+
.filter((scriptType) => supportsScriptType(coin, scriptType))
187205
.map((scriptType, index) => ({
188206
scriptType,
189207
value: BigInt(900 + index * 100), // Deterministic amounts
@@ -234,66 +252,80 @@ export class AcidTest {
234252
// Use ZcashBitGoPsbt for Zcash networks
235253
const isZcash = this.network === "zec" || this.network === "tzec";
236254
const psbt = isZcash
237-
? ZcashBitGoPsbt.createEmptyWithConsensusBranchId(this.network, this.rootWalletKeys, {
238-
version: 2,
239-
lockTime: 0,
240-
consensusBranchId: 0xc2d6d0b4, // NU5
255+
? ZcashBitGoPsbt.createEmpty(this.network, this.rootWalletKeys, {
256+
// Sapling activation height: mainnet=419200, testnet=280000
257+
blockHeight: this.network === "zec" ? 419200 : 280000,
241258
})
242259
: BitGoPsbt.createEmpty(this.network, this.rootWalletKeys, {
243260
version: 2,
244261
lockTime: 0,
245262
});
246263

264+
// Build a fake previous transaction for non_witness_utxo (psbt format)
265+
const usePrevTx = this.txFormat === "psbt" && !isZcash;
266+
const buildPrevTx = (
267+
vout: number,
268+
script: Uint8Array,
269+
value: bigint,
270+
): Uint8Array | undefined => {
271+
if (!usePrevTx) return undefined;
272+
const tx = Transaction.create();
273+
tx.addInput("0".repeat(64), 0xffffffff);
274+
for (let i = 0; i < vout; i++) {
275+
tx.addOutput(new Uint8Array(0), 0n);
276+
}
277+
tx.addOutput(script, value);
278+
return tx.toBytes();
279+
};
280+
247281
// Add inputs with deterministic outpoints
248282
this.inputs.forEach((input, index) => {
249-
// Resolve scriptId: either from explicit scriptId or from scriptType + index
250-
const scriptId: ScriptId = input.scriptId ?? {
251-
chain: ChainCode.value("p2sh", "external"),
252-
index: input.index ?? index,
253-
};
254283
const walletKeys = input.walletKeys ?? this.rootWalletKeys;
284+
const outpoint = { txid: "0".repeat(64), vout: index, value: input.value };
255285

256-
// Get scriptType: either explicit or derive from scriptId chain
257-
const scriptType = input.scriptType ?? ChainCode.scriptType(assertChainCode(scriptId.chain));
286+
// scriptId variant: caller provides explicit chain + index
287+
if (input.scriptId) {
288+
const script = outputScript(
289+
walletKeys,
290+
input.scriptId.chain,
291+
input.scriptId.index,
292+
this.network,
293+
);
294+
psbt.addWalletInput(
295+
{ ...outpoint, prevTx: buildPrevTx(index, script, input.value) },
296+
walletKeys,
297+
{ scriptId: input.scriptId, signPath: { signer: "user", cosigner: "bitgo" } },
298+
);
299+
return;
300+
}
301+
302+
const scriptType = input.scriptType ?? "p2sh";
258303

259304
if (scriptType === "p2shP2pk") {
260-
// Add replay protection input
261-
const replayKey = this.getReplayProtectionKey();
262-
// Convert BIP32 to ECPair using public key
263-
const ecpair = ECPair.fromPublicKey(replayKey.publicKey);
305+
const ecpair = ECPair.fromPublicKey(this.getReplayProtectionKey().publicKey);
306+
const script = p2shP2pkOutputScript(ecpair.publicKey);
264307
psbt.addReplayProtectionInput(
265-
{
266-
txid: "0".repeat(64),
267-
vout: index,
268-
value: input.value,
269-
},
308+
{ ...outpoint, prevTx: buildPrevTx(index, script, input.value) },
270309
ecpair,
271310
);
272-
} else {
273-
// Determine signing path based on input type
274-
let signPath: { signer: SignerKey; cosigner: SignerKey };
275-
276-
if (scriptType === "p2trMusig2ScriptPath") {
277-
// Script path uses user + backup
278-
signPath = { signer: "user", cosigner: "backup" };
279-
} else {
280-
// Default: user + bitgo
281-
signPath = { signer: "user", cosigner: "bitgo" };
282-
}
283-
284-
psbt.addWalletInput(
285-
{
286-
txid: "0".repeat(64),
287-
vout: index,
288-
value: input.value,
289-
},
290-
walletKeys,
291-
{
292-
scriptId,
293-
signPath,
294-
},
295-
);
311+
return;
296312
}
313+
314+
const scriptId: ScriptId = {
315+
chain: ChainCode.value(inputScriptTypeToOutputScriptType(scriptType), "external"),
316+
index: input.index ?? index,
317+
};
318+
const signPath: { signer: SignerKey; cosigner: SignerKey } =
319+
scriptType === "p2trMusig2ScriptPath"
320+
? { signer: "user", cosigner: "backup" }
321+
: { signer: "user", cosigner: "bitgo" };
322+
const script = outputScript(walletKeys, scriptId.chain, scriptId.index, this.network);
323+
324+
psbt.addWalletInput(
325+
{ ...outpoint, prevTx: buildPrevTx(index, script, input.value) },
326+
walletKeys,
327+
{ scriptId, signPath },
328+
);
297329
});
298330

299331
// Add outputs
@@ -366,40 +398,27 @@ export class AcidTest {
366398
);
367399

368400
if (hasMusig2Inputs) {
369-
const isZcash = this.network === "zec" || this.network === "tzec";
370-
if (isZcash) {
401+
if (this.network === "zec" || this.network === "tzec") {
371402
throw new Error("Zcash does not support MuSig2/Taproot inputs");
372403
}
373404

374-
// Generate nonces with user key
405+
// MuSig2 requires ALL participant nonces before ANY signing.
406+
// Generate nonces directly on the same PSBT for each participant key.
375407
psbt.generateMusig2Nonces(userKey);
376408

377-
if (this.signStage === "fullsigned") {
378-
// Create a second PSBT with cosigner nonces for combination
379-
// For p2trMusig2ScriptPath use backup, for p2trMusig2KeyPath use bitgo
380-
// Since we might have both types, we need to generate nonces separately
381-
const bytes = psbt.serialize();
382-
383-
const hasKeyPath = this.inputs.some((input) => input.scriptType === "p2trMusig2KeyPath");
384-
const hasScriptPath = this.inputs.some(
385-
(input) => input.scriptType === "p2trMusig2ScriptPath",
386-
);
409+
const hasKeyPath = this.inputs.some((input) => input.scriptType === "p2trMusig2KeyPath");
410+
const hasScriptPath = this.inputs.some(
411+
(input) => input.scriptType === "p2trMusig2ScriptPath",
412+
);
387413

388-
if (hasKeyPath && !hasScriptPath) {
389-
// Only key path inputs - generate bitgo nonces for all
390-
const psbt2 = BitGoPsbt.fromBytes(bytes, this.network);
391-
psbt2.generateMusig2Nonces(bitgoKey);
392-
psbt.combineMusig2Nonces(psbt2);
393-
} else if (hasScriptPath && !hasKeyPath) {
394-
// Only script path inputs - generate backup nonces for all
395-
const psbt2 = BitGoPsbt.fromBytes(bytes, this.network);
396-
psbt2.generateMusig2Nonces(backupKey);
397-
psbt.combineMusig2Nonces(psbt2);
398-
} else {
399-
const psbt2 = BitGoPsbt.fromBytes(bytes, this.network);
400-
psbt2.generateMusig2Nonces(bitgoKey);
401-
psbt.combineMusig2Nonces(psbt2);
402-
}
414+
// Key path uses user+bitgo, script path uses user+backup.
415+
// generateMusig2Nonces fails if the key isn't a participant in any musig2 input,
416+
// so we only call it for keys that match.
417+
if (hasKeyPath) {
418+
psbt.generateMusig2Nonces(bitgoKey);
419+
}
420+
if (hasScriptPath) {
421+
psbt.generateMusig2Nonces(backupKey);
403422
}
404423
}
405424

@@ -446,8 +465,8 @@ export class AcidTest {
446465
* Generate test suite for all networks, sign stages, and tx formats
447466
*/
448467
static forAllNetworksSignStagesTxFormats(suiteConfig: SuiteConfig = {}): AcidTest[] {
449-
return (coinNames as readonly CoinName[])
450-
.filter((network) => isMainnet(network) && network !== "bsv") // Exclude bitcoinsv
468+
return coinNames
469+
.filter((network): network is CoinName => isMainnet(network) && network !== "bsv")
451470
.flatMap((network) =>
452471
signStages.flatMap((signStage) =>
453472
txFormats.map((txFormat) =>

0 commit comments

Comments
 (0)