@@ -3,13 +3,14 @@ import { 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 ,
@@ -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 *
@@ -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