Skip to content

Commit b81f07a

Browse files
OttoAllmendingerllm-git
andcommitted
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>
1 parent 1e6aed6 commit b81f07a

60 files changed

Lines changed: 1196 additions & 7520 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: 78 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { BitGoPsbt, type SignerKey } from "../fixedScriptWallet/BitGoPsbt.js";
1+
import { BitGoPsbt, type NetworkName, type SignerKey } from "../fixedScriptWallet/BitGoPsbt.js";
22
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";
66
import {
7-
assertChainCode,
87
ChainCode,
98
createOpReturnScript,
109
inputScriptTypes,
@@ -16,7 +15,7 @@ import {
1615
type ScriptId,
1716
} from "../fixedScriptWallet/index.js";
1817
import type { CoinName } from "../coinName.js";
19-
import { coinNames, isMainnet } from "../coinName.js";
18+
import { coinNames, isMainnet, toCoinName } from "../coinName.js";
2019
import { getDefaultWalletKeys, getWalletKeysForSeed, getKeyTriple } from "./keys.js";
2120
import type { Triple } from "../triple.js";
2221

@@ -91,6 +90,22 @@ type SuiteConfig = {
9190
// Re-export for convenience
9291
export { inputScriptTypes, outputScriptTypes };
9392

93+
/** Map InputScriptType to the OutputScriptType used for chain code derivation */
94+
function inputScriptTypeToOutputScriptType(scriptType: InputScriptType): OutputScriptType {
95+
switch (scriptType) {
96+
case "p2sh":
97+
case "p2shP2wsh":
98+
case "p2wsh":
99+
case "p2trLegacy":
100+
return scriptType;
101+
case "p2shP2pk":
102+
return "p2sh";
103+
case "p2trMusig2ScriptPath":
104+
case "p2trMusig2KeyPath":
105+
return "p2trMusig2";
106+
}
107+
}
108+
94109
/**
95110
* Creates a valid PSBT with as many features as possible (kitchen sink).
96111
*
@@ -113,7 +128,7 @@ export { inputScriptTypes, outputScriptTypes };
113128
* - psbt-lite: Only witness_utxo (no non_witness_utxo)
114129
*/
115130
export class AcidTest {
116-
public readonly network: CoinName;
131+
public readonly network: CoinName | NetworkName;
117132
public readonly signStage: SignStage;
118133
public readonly txFormat: TxFormat;
119134
public readonly rootWalletKeys: RootWalletKeys;
@@ -126,7 +141,7 @@ export class AcidTest {
126141
private readonly bitgoXprv: BIP32;
127142

128143
constructor(
129-
network: CoinName,
144+
network: CoinName | NetworkName,
130145
signStage: SignStage,
131146
txFormat: TxFormat,
132147
rootWalletKeys: RootWalletKeys,
@@ -151,13 +166,14 @@ export class AcidTest {
151166
* Create an AcidTest with specific configuration
152167
*/
153168
static withConfig(
154-
network: CoinName,
169+
network: CoinName | NetworkName,
155170
signStage: SignStage,
156171
txFormat: TxFormat,
157172
suiteConfig: SuiteConfig = {},
158173
): AcidTest {
159174
const rootWalletKeys = getDefaultWalletKeys();
160175
const otherWalletKeys = getWalletKeysForSeed("too many secrets");
176+
const coin = toCoinName(network);
161177

162178
// Filter inputs based on network support
163179
const inputs: Input[] = inputScriptTypes
@@ -167,9 +183,9 @@ export class AcidTest {
167183

168184
// Map input script types to output script types for support check
169185
if (scriptType === "p2trMusig2KeyPath" || scriptType === "p2trMusig2ScriptPath") {
170-
return supportsScriptType(network, "p2trMusig2");
186+
return supportsScriptType(coin, "p2trMusig2");
171187
}
172-
return supportsScriptType(network, scriptType);
188+
return supportsScriptType(coin, scriptType);
173189
})
174190
.filter(
175191
(scriptType) =>
@@ -183,7 +199,7 @@ export class AcidTest {
183199

184200
// Filter outputs based on network support
185201
const outputs: Output[] = outputScriptTypes
186-
.filter((scriptType) => supportsScriptType(network, scriptType))
202+
.filter((scriptType) => supportsScriptType(coin, scriptType))
187203
.map((scriptType, index) => ({
188204
scriptType,
189205
value: BigInt(900 + index * 100), // Deterministic amounts
@@ -232,12 +248,16 @@ export class AcidTest {
232248
*/
233249
createPsbt(): BitGoPsbt {
234250
// Use ZcashBitGoPsbt for Zcash networks
235-
const isZcash = this.network === "zec" || this.network === "tzec";
251+
const isZcash =
252+
this.network === "zec" ||
253+
this.network === "tzec" ||
254+
this.network === "zcash" ||
255+
this.network === "zcashTest";
236256
const psbt = isZcash
237-
? ZcashBitGoPsbt.createEmptyWithConsensusBranchId(this.network, this.rootWalletKeys, {
238-
version: 2,
239-
lockTime: 0,
240-
consensusBranchId: 0xc2d6d0b4, // NU5
257+
? ZcashBitGoPsbt.createEmpty(this.network, this.rootWalletKeys, {
258+
// Sapling activation height: mainnet=419200, testnet=280000
259+
blockHeight:
260+
this.network === "zec" || this.network === "zcash" ? 419200 : 280000,
241261
})
242262
: BitGoPsbt.createEmpty(this.network, this.rootWalletKeys, {
243263
version: 2,
@@ -246,54 +266,36 @@ export class AcidTest {
246266

247267
// Add inputs with deterministic outpoints
248268
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-
};
254269
const walletKeys = input.walletKeys ?? this.rootWalletKeys;
270+
const outpoint = { txid: "0".repeat(64), vout: index, value: input.value };
255271

256-
// Get scriptType: either explicit or derive from scriptId chain
257-
const scriptType = input.scriptType ?? ChainCode.scriptType(assertChainCode(scriptId.chain));
272+
// scriptId variant: caller provides explicit chain + index
273+
if (input.scriptId) {
274+
psbt.addWalletInput(outpoint, walletKeys, {
275+
scriptId: input.scriptId,
276+
signPath: { signer: "user", cosigner: "bitgo" },
277+
});
278+
return;
279+
}
280+
281+
const scriptType = input.scriptType ?? "p2sh";
258282

259283
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);
264-
psbt.addReplayProtectionInput(
265-
{
266-
txid: "0".repeat(64),
267-
vout: index,
268-
value: input.value,
269-
},
270-
ecpair,
271-
);
272-
} else {
273-
// Determine signing path based on input type
274-
let signPath: { signer: SignerKey; cosigner: SignerKey };
284+
const ecpair = ECPair.fromPublicKey(this.getReplayProtectionKey().publicKey);
285+
psbt.addReplayProtectionInput(outpoint, ecpair);
286+
return;
287+
}
275288

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-
}
289+
const scriptId: ScriptId = {
290+
chain: ChainCode.value(inputScriptTypeToOutputScriptType(scriptType), "external"),
291+
index: input.index ?? index,
292+
};
293+
const signPath: { signer: SignerKey; cosigner: SignerKey } =
294+
scriptType === "p2trMusig2ScriptPath"
295+
? { signer: "user", cosigner: "backup" }
296+
: { signer: "user", cosigner: "bitgo" };
283297

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-
);
296-
}
298+
psbt.addWalletInput(outpoint, walletKeys, { scriptId, signPath });
297299
});
298300

299301
// Add outputs
@@ -366,40 +368,32 @@ export class AcidTest {
366368
);
367369

368370
if (hasMusig2Inputs) {
369-
const isZcash = this.network === "zec" || this.network === "tzec";
371+
const isZcash =
372+
this.network === "zec" ||
373+
this.network === "tzec" ||
374+
this.network === "zcash" ||
375+
this.network === "zcashTest";
370376
if (isZcash) {
371377
throw new Error("Zcash does not support MuSig2/Taproot inputs");
372378
}
373379

374-
// Generate nonces with user key
380+
// MuSig2 requires ALL participant nonces before ANY signing.
381+
// Generate nonces directly on the same PSBT for each participant key.
375382
psbt.generateMusig2Nonces(userKey);
376383

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-
);
387-
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-
}
384+
const hasKeyPath = this.inputs.some((input) => input.scriptType === "p2trMusig2KeyPath");
385+
const hasScriptPath = this.inputs.some(
386+
(input) => input.scriptType === "p2trMusig2ScriptPath",
387+
);
388+
389+
// Key path uses user+bitgo, script path uses user+backup.
390+
// generateMusig2Nonces fails if the key isn't a participant in any musig2 input,
391+
// so we only call it for keys that match.
392+
if (hasKeyPath) {
393+
psbt.generateMusig2Nonces(bitgoKey);
394+
}
395+
if (hasScriptPath) {
396+
psbt.generateMusig2Nonces(backupKey);
403397
}
404398
}
405399

0 commit comments

Comments
 (0)