Skip to content

Commit 29d0f6d

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 29d0f6d

File tree

15 files changed

+876
-643
lines changed

15 files changed

+876
-643
lines changed

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: 109 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
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";
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,
1617
type ScriptId,
1718
} from "../fixedScriptWallet/index.js";
1819
import type { CoinName } from "../coinName.js";
19-
import { coinNames, isMainnet } from "../coinName.js";
20+
import { coinNames, isMainnet, toCoinName } from "../coinName.js";
2021
import { getDefaultWalletKeys, getWalletKeysForSeed, getKeyTriple } from "./keys.js";
2122
import type { Triple } from "../triple.js";
2223

@@ -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
*
@@ -113,7 +130,7 @@ export { inputScriptTypes, outputScriptTypes };
113130
* - psbt-lite: Only witness_utxo (no non_witness_utxo)
114131
*/
115132
export class AcidTest {
116-
public readonly network: CoinName;
133+
public readonly network: CoinName | NetworkName;
117134
public readonly signStage: SignStage;
118135
public readonly txFormat: TxFormat;
119136
public readonly rootWalletKeys: RootWalletKeys;
@@ -126,7 +143,7 @@ export class AcidTest {
126143
private readonly bitgoXprv: BIP32;
127144

128145
constructor(
129-
network: CoinName,
146+
network: CoinName | NetworkName,
130147
signStage: SignStage,
131148
txFormat: TxFormat,
132149
rootWalletKeys: RootWalletKeys,
@@ -151,13 +168,14 @@ export class AcidTest {
151168
* Create an AcidTest with specific configuration
152169
*/
153170
static withConfig(
154-
network: CoinName,
171+
network: CoinName | NetworkName,
155172
signStage: SignStage,
156173
txFormat: TxFormat,
157174
suiteConfig: SuiteConfig = {},
158175
): AcidTest {
159176
const rootWalletKeys = getDefaultWalletKeys();
160177
const otherWalletKeys = getWalletKeysForSeed("too many secrets");
178+
const coin = toCoinName(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
@@ -232,68 +250,86 @@ export class AcidTest {
232250
*/
233251
createPsbt(): BitGoPsbt {
234252
// Use ZcashBitGoPsbt for Zcash networks
235-
const isZcash = this.network === "zec" || this.network === "tzec";
253+
const isZcash =
254+
this.network === "zec" ||
255+
this.network === "tzec" ||
256+
this.network === "zcash" ||
257+
this.network === "zcashTest";
236258
const psbt = isZcash
237-
? ZcashBitGoPsbt.createEmptyWithConsensusBranchId(this.network, this.rootWalletKeys, {
238-
version: 2,
239-
lockTime: 0,
240-
consensusBranchId: 0xc2d6d0b4, // NU5
259+
? ZcashBitGoPsbt.createEmpty(this.network, this.rootWalletKeys, {
260+
// Sapling activation height: mainnet=419200, testnet=280000
261+
blockHeight: this.network === "zec" || this.network === "zcash" ? 419200 : 280000,
241262
})
242263
: BitGoPsbt.createEmpty(this.network, this.rootWalletKeys, {
243264
version: 2,
244265
lockTime: 0,
245266
});
246267

268+
// Build a fake previous transaction for non_witness_utxo (psbt format)
269+
const usePrevTx = this.txFormat === "psbt" && !isZcash;
270+
const buildPrevTx = (
271+
vout: number,
272+
script: Uint8Array,
273+
value: bigint,
274+
): Uint8Array | undefined => {
275+
if (!usePrevTx) return undefined;
276+
const tx = Transaction.create();
277+
tx.addInput("0".repeat(64), 0xffffffff);
278+
for (let i = 0; i < vout; i++) {
279+
tx.addOutput(new Uint8Array(0), 0n);
280+
}
281+
tx.addOutput(script, value);
282+
return tx.toBytes();
283+
};
284+
247285
// Add inputs with deterministic outpoints
248286
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-
};
254287
const walletKeys = input.walletKeys ?? this.rootWalletKeys;
288+
const outpoint = { txid: "0".repeat(64), vout: index, value: input.value };
255289

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

259308
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);
309+
const ecpair = ECPair.fromPublicKey(this.getReplayProtectionKey().publicKey);
310+
const script = p2shP2pkOutputScript(ecpair.publicKey);
264311
psbt.addReplayProtectionInput(
265-
{
266-
txid: "0".repeat(64),
267-
vout: index,
268-
value: input.value,
269-
},
312+
{ ...outpoint, prevTx: buildPrevTx(index, script, input.value) },
270313
ecpair,
271314
);
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-
);
315+
return;
296316
}
317+
318+
const scriptId: ScriptId = {
319+
chain: ChainCode.value(inputScriptTypeToOutputScriptType(scriptType), "external"),
320+
index: input.index ?? index,
321+
};
322+
const signPath: { signer: SignerKey; cosigner: SignerKey } =
323+
scriptType === "p2trMusig2ScriptPath"
324+
? { signer: "user", cosigner: "backup" }
325+
: { signer: "user", cosigner: "bitgo" };
326+
const script = outputScript(walletKeys, scriptId.chain, scriptId.index, this.network);
327+
328+
psbt.addWalletInput(
329+
{ ...outpoint, prevTx: buildPrevTx(index, script, input.value) },
330+
walletKeys,
331+
{ scriptId, signPath },
332+
);
297333
});
298334

299335
// Add outputs
@@ -366,40 +402,32 @@ export class AcidTest {
366402
);
367403

368404
if (hasMusig2Inputs) {
369-
const isZcash = this.network === "zec" || this.network === "tzec";
405+
const isZcash =
406+
this.network === "zec" ||
407+
this.network === "tzec" ||
408+
this.network === "zcash" ||
409+
this.network === "zcashTest";
370410
if (isZcash) {
371411
throw new Error("Zcash does not support MuSig2/Taproot inputs");
372412
}
373413

374-
// Generate nonces with user key
414+
// MuSig2 requires ALL participant nonces before ANY signing.
415+
// Generate nonces directly on the same PSBT for each participant key.
375416
psbt.generateMusig2Nonces(userKey);
376417

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-
);
418+
const hasKeyPath = this.inputs.some((input) => input.scriptType === "p2trMusig2KeyPath");
419+
const hasScriptPath = this.inputs.some(
420+
(input) => input.scriptType === "p2trMusig2ScriptPath",
421+
);
387422

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-
}
423+
// Key path uses user+bitgo, script path uses user+backup.
424+
// generateMusig2Nonces fails if the key isn't a participant in any musig2 input,
425+
// so we only call it for keys that match.
426+
if (hasKeyPath) {
427+
psbt.generateMusig2Nonces(bitgoKey);
428+
}
429+
if (hasScriptPath) {
430+
psbt.generateMusig2Nonces(backupKey);
403431
}
404432
}
405433

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/p2tr_musig2_input.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,18 +1388,13 @@ mod tests {
13881388
assert_eq!(fixture_witness_bytes[1], 0x40, "Expected 64-byte signature");
13891389
let fixture_signature = &fixture_witness_bytes[2..66];
13901390

1391-
// Get tap merkle root from fixture
1391+
// Get tap merkle root from PSBT input (more reliable than fixture JSON)
13921392
use crate::bitcoin::sighash::SighashCache;
13931393
use crate::bitcoin::taproot::TapNodeHash;
13941394

1395-
let tap_tree_root_bytes =
1396-
<Vec<u8> as hex::FromHex>::from_hex(&fixture_keypath_input.tap_merkle_root)
1397-
.expect("Failed to decode tap merkle root");
1398-
let tap_tree_root_array: [u8; 32] = tap_tree_root_bytes
1399-
.as_slice()
1400-
.try_into()
1401-
.expect("Invalid tap merkle root length");
1402-
let tap_tree_root = TapNodeHash::from_byte_array(tap_tree_root_array);
1395+
let tap_tree_root = psbt.inputs[*musig2_input_index]
1396+
.tap_merkle_root
1397+
.expect("Expected tap_merkle_root in PSBT input");
14031398

14041399
// Collect all prevouts for sighash computation
14051400
let prevouts = collect_prevouts(&psbt).expect("Failed to collect prevouts");

0 commit comments

Comments
 (0)