Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions packages/wasm-utxo/js/coinName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,54 @@ export function isTestnet(name: CoinName): boolean {
export function isCoinName(v: string): v is CoinName {
return (coinNames as readonly string[]).includes(v);
}

import type { UtxolibName } from "./utxolibCompat.js";

/** Convert a CoinName or UtxolibName to CoinName */
export function toCoinName(name: CoinName | UtxolibName): CoinName {
switch (name) {
case "bitcoin":
return "btc";
case "testnet":
return "tbtc";
case "bitcoinTestnet4":
return "tbtc4";
case "bitcoinPublicSignet":
return "tbtcsig";
case "bitcoinBitGoSignet":
return "tbtcbgsig";
case "bitcoincash":
return "bch";
case "bitcoincashTestnet":
return "tbch";
case "ecash":
return "bcha";
case "ecashTest":
return "tbcha";
case "bitcoingold":
return "btg";
case "bitcoingoldTestnet":
return "tbtg";
case "bitcoinsv":
return "bsv";
case "bitcoinsvTestnet":
return "tbsv";
case "dashTest":
return "tdash";
case "dogecoin":
return "doge";
case "dogecoinTest":
return "tdoge";
case "litecoin":
return "ltc";
case "litecoinTest":
return "tltc";
case "zcash":
return "zec";
case "zcashTest":
return "tzec";
default:
// CoinName values pass through (including "dash" which is both CoinName and UtxolibName)
return name;
}
}
162 changes: 78 additions & 84 deletions packages/wasm-utxo/js/testutils/AcidTest.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { BitGoPsbt, type SignerKey } from "../fixedScriptWallet/BitGoPsbt.js";
import { BitGoPsbt, type NetworkName, type SignerKey } from "../fixedScriptWallet/BitGoPsbt.js";
import { ZcashBitGoPsbt } from "../fixedScriptWallet/ZcashBitGoPsbt.js";
import { RootWalletKeys } from "../fixedScriptWallet/RootWalletKeys.js";
import { BIP32 } from "../bip32.js";
import { ECPair } from "../ecpair.js";
import {
assertChainCode,
ChainCode,
createOpReturnScript,
inputScriptTypes,
Expand All @@ -16,7 +15,7 @@ import {
type ScriptId,
} from "../fixedScriptWallet/index.js";
import type { CoinName } from "../coinName.js";
import { coinNames, isMainnet } from "../coinName.js";
import { coinNames, isMainnet, toCoinName } from "../coinName.js";
import { getDefaultWalletKeys, getWalletKeysForSeed, getKeyTriple } from "./keys.js";
import type { Triple } from "../triple.js";

Expand Down Expand Up @@ -91,6 +90,22 @@ type SuiteConfig = {
// Re-export for convenience
export { inputScriptTypes, outputScriptTypes };

/** Map InputScriptType to the OutputScriptType used for chain code derivation */
function inputScriptTypeToOutputScriptType(scriptType: InputScriptType): OutputScriptType {
switch (scriptType) {
case "p2sh":
case "p2shP2wsh":
case "p2wsh":
case "p2trLegacy":
return scriptType;
case "p2shP2pk":
return "p2sh";
case "p2trMusig2ScriptPath":
case "p2trMusig2KeyPath":
return "p2trMusig2";
}
}

/**
* Creates a valid PSBT with as many features as possible (kitchen sink).
*
Expand All @@ -113,7 +128,7 @@ export { inputScriptTypes, outputScriptTypes };
* - psbt-lite: Only witness_utxo (no non_witness_utxo)
*/
export class AcidTest {
public readonly network: CoinName;
public readonly network: CoinName | NetworkName;
public readonly signStage: SignStage;
public readonly txFormat: TxFormat;
public readonly rootWalletKeys: RootWalletKeys;
Expand All @@ -126,7 +141,7 @@ export class AcidTest {
private readonly bitgoXprv: BIP32;

constructor(
network: CoinName,
network: CoinName | NetworkName,
signStage: SignStage,
txFormat: TxFormat,
rootWalletKeys: RootWalletKeys,
Expand All @@ -151,13 +166,14 @@ export class AcidTest {
* Create an AcidTest with specific configuration
*/
static withConfig(
network: CoinName,
network: CoinName | NetworkName,
signStage: SignStage,
txFormat: TxFormat,
suiteConfig: SuiteConfig = {},
): AcidTest {
const rootWalletKeys = getDefaultWalletKeys();
const otherWalletKeys = getWalletKeysForSeed("too many secrets");
const coin = toCoinName(network);

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

// Map input script types to output script types for support check
if (scriptType === "p2trMusig2KeyPath" || scriptType === "p2trMusig2ScriptPath") {
return supportsScriptType(network, "p2trMusig2");
return supportsScriptType(coin, "p2trMusig2");
}
return supportsScriptType(network, scriptType);
return supportsScriptType(coin, scriptType);
})
.filter(
(scriptType) =>
Expand All @@ -183,7 +199,7 @@ export class AcidTest {

// Filter outputs based on network support
const outputs: Output[] = outputScriptTypes
.filter((scriptType) => supportsScriptType(network, scriptType))
.filter((scriptType) => supportsScriptType(coin, scriptType))
.map((scriptType, index) => ({
scriptType,
value: BigInt(900 + index * 100), // Deterministic amounts
Expand Down Expand Up @@ -232,12 +248,16 @@ export class AcidTest {
*/
createPsbt(): BitGoPsbt {
// Use ZcashBitGoPsbt for Zcash networks
const isZcash = this.network === "zec" || this.network === "tzec";
const isZcash =
this.network === "zec" ||
this.network === "tzec" ||
this.network === "zcash" ||
this.network === "zcashTest";
const psbt = isZcash
? ZcashBitGoPsbt.createEmptyWithConsensusBranchId(this.network, this.rootWalletKeys, {
version: 2,
lockTime: 0,
consensusBranchId: 0xc2d6d0b4, // NU5
? ZcashBitGoPsbt.createEmpty(this.network, this.rootWalletKeys, {
// Sapling activation height: mainnet=419200, testnet=280000
blockHeight:
this.network === "zec" || this.network === "zcash" ? 419200 : 280000,
})
: BitGoPsbt.createEmpty(this.network, this.rootWalletKeys, {
version: 2,
Expand All @@ -246,54 +266,36 @@ export class AcidTest {

// Add inputs with deterministic outpoints
this.inputs.forEach((input, index) => {
// Resolve scriptId: either from explicit scriptId or from scriptType + index
const scriptId: ScriptId = input.scriptId ?? {
chain: ChainCode.value("p2sh", "external"),
index: input.index ?? index,
};
const walletKeys = input.walletKeys ?? this.rootWalletKeys;
const outpoint = { txid: "0".repeat(64), vout: index, value: input.value };

// Get scriptType: either explicit or derive from scriptId chain
const scriptType = input.scriptType ?? ChainCode.scriptType(assertChainCode(scriptId.chain));
// scriptId variant: caller provides explicit chain + index
if (input.scriptId) {
psbt.addWalletInput(outpoint, walletKeys, {
scriptId: input.scriptId,
signPath: { signer: "user", cosigner: "bitgo" },
});
return;
}

const scriptType = input.scriptType ?? "p2sh";

if (scriptType === "p2shP2pk") {
// Add replay protection input
const replayKey = this.getReplayProtectionKey();
// Convert BIP32 to ECPair using public key
const ecpair = ECPair.fromPublicKey(replayKey.publicKey);
psbt.addReplayProtectionInput(
{
txid: "0".repeat(64),
vout: index,
value: input.value,
},
ecpair,
);
} else {
// Determine signing path based on input type
let signPath: { signer: SignerKey; cosigner: SignerKey };
const ecpair = ECPair.fromPublicKey(this.getReplayProtectionKey().publicKey);
psbt.addReplayProtectionInput(outpoint, ecpair);
return;
}

if (scriptType === "p2trMusig2ScriptPath") {
// Script path uses user + backup
signPath = { signer: "user", cosigner: "backup" };
} else {
// Default: user + bitgo
signPath = { signer: "user", cosigner: "bitgo" };
}
const scriptId: ScriptId = {
chain: ChainCode.value(inputScriptTypeToOutputScriptType(scriptType), "external"),
index: input.index ?? index,
};
const signPath: { signer: SignerKey; cosigner: SignerKey } =
scriptType === "p2trMusig2ScriptPath"
? { signer: "user", cosigner: "backup" }
: { signer: "user", cosigner: "bitgo" };

psbt.addWalletInput(
{
txid: "0".repeat(64),
vout: index,
value: input.value,
},
walletKeys,
{
scriptId,
signPath,
},
);
}
psbt.addWalletInput(outpoint, walletKeys, { scriptId, signPath });
});

// Add outputs
Expand Down Expand Up @@ -366,40 +368,32 @@ export class AcidTest {
);

if (hasMusig2Inputs) {
const isZcash = this.network === "zec" || this.network === "tzec";
const isZcash =
this.network === "zec" ||
this.network === "tzec" ||
this.network === "zcash" ||
this.network === "zcashTest";
if (isZcash) {
throw new Error("Zcash does not support MuSig2/Taproot inputs");
}

// Generate nonces with user key
// MuSig2 requires ALL participant nonces before ANY signing.
// Generate nonces directly on the same PSBT for each participant key.
psbt.generateMusig2Nonces(userKey);

if (this.signStage === "fullsigned") {
// Create a second PSBT with cosigner nonces for combination
// For p2trMusig2ScriptPath use backup, for p2trMusig2KeyPath use bitgo
// Since we might have both types, we need to generate nonces separately
const bytes = psbt.serialize();

const hasKeyPath = this.inputs.some((input) => input.scriptType === "p2trMusig2KeyPath");
const hasScriptPath = this.inputs.some(
(input) => input.scriptType === "p2trMusig2ScriptPath",
);

if (hasKeyPath && !hasScriptPath) {
// Only key path inputs - generate bitgo nonces for all
const psbt2 = BitGoPsbt.fromBytes(bytes, this.network);
psbt2.generateMusig2Nonces(bitgoKey);
psbt.combineMusig2Nonces(psbt2);
} else if (hasScriptPath && !hasKeyPath) {
// Only script path inputs - generate backup nonces for all
const psbt2 = BitGoPsbt.fromBytes(bytes, this.network);
psbt2.generateMusig2Nonces(backupKey);
psbt.combineMusig2Nonces(psbt2);
} else {
const psbt2 = BitGoPsbt.fromBytes(bytes, this.network);
psbt2.generateMusig2Nonces(bitgoKey);
psbt.combineMusig2Nonces(psbt2);
}
const hasKeyPath = this.inputs.some((input) => input.scriptType === "p2trMusig2KeyPath");
const hasScriptPath = this.inputs.some(
(input) => input.scriptType === "p2trMusig2ScriptPath",
);

// Key path uses user+bitgo, script path uses user+backup.
// generateMusig2Nonces fails if the key isn't a participant in any musig2 input,
// so we only call it for keys that match.
if (hasKeyPath) {
psbt.generateMusig2Nonces(bitgoKey);
}
if (hasScriptPath) {
psbt.generateMusig2Nonces(backupKey);
}
}

Expand Down
Loading
Loading