1- import { BitGoPsbt , type SignerKey } from "../fixedScriptWallet/BitGoPsbt.js" ;
1+ import { BitGoPsbt , type NetworkName , type SignerKey } from "../fixedScriptWallet/BitGoPsbt.js" ;
22import { ZcashBitGoPsbt } from "../fixedScriptWallet/ZcashBitGoPsbt.js" ;
33import { RootWalletKeys } from "../fixedScriptWallet/RootWalletKeys.js" ;
44import { BIP32 } from "../bip32.js" ;
55import { ECPair } from "../ecpair.js" ;
6+ import { Transaction } from "../transaction.js" ;
67import {
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" ;
1819import type { CoinName } from "../coinName.js" ;
19- import { coinNames , isMainnet } from "../coinName.js" ;
20+ import { coinNames , isMainnet , toCoinName } from "../coinName.js" ;
2021import { getDefaultWalletKeys , getWalletKeysForSeed , getKeyTriple } from "./keys.js" ;
2122import type { Triple } from "../triple.js" ;
2223
@@ -91,6 +92,22 @@ type SuiteConfig = {
9192// Re-export for convenience
9293export { 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 */
115132export 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
0 commit comments