@@ -530,100 +530,63 @@ describe('bitcoinjs-lib (transaction with taproot)', () => {
530530
531531 it ( 'can create (and broadcast via 3PBP) a taproot script-path spend Transaction - OP_CHECKSIGADD (2-of-3) and verify unspendable internalKey' , async ( ) => {
532532 const leafKeys = [ ] ;
533- const leafPubkeys = [ ] ;
533+ const leafPubkeys : Buffer [ ] = [ ] ;
534534 for ( let i = 0 ; i < 3 ; i ++ ) {
535535 const leafKey = bip32 . fromSeed ( rng ( 64 ) , regtest ) ;
536536 leafKeys . push ( leafKey ) ;
537- leafPubkeys . push ( toXOnly ( leafKey . publicKey ) . toString ( 'hex' ) ) ;
537+ leafPubkeys . push ( toXOnly ( leafKey . publicKey ) ) ;
538538 }
539539
540- // This is just a visual way of creating the script for educational purposes.
541- // In a production application there's no need to bother with this step, creating the binary
542- // directly from buffers is fine.
543- const leafScriptAsm = `${ leafPubkeys [ 2 ] } OP_CHECKSIG ${ leafPubkeys [ 1 ] } OP_CHECKSIGADD ${ leafPubkeys [ 0 ] } OP_CHECKSIGADD OP_2 OP_GREATERTHANOREQUAL` ;
544- const leafScript = bitcoin . script . fromASM ( leafScriptAsm ) ;
545-
546- // Taptree can also be a single TapLeaf
547- // Since we only have one script, it's all we need.
548- const scriptTree : Taptree = {
549- output : leafScript ,
550- } ;
551- const redeem = {
552- output : leafScript ,
553- redeemVersion : LEAF_VERSION_TAPSCRIPT ,
554- } ;
555-
556- // We don't pass in a shared nonce because our wallet doesn't care.
557- // See the helper function's comments to understand why you might want to use a shared nonce.
558- // All signers should verify that the internalPubkey of the script they're signing is unspendable.
559- // Otherwise the person who made the script could be hiding a secret master key (for one-key-only spending).
560- // If a nonce is used, that nonce should be shared among all signers.
561- const internalPubkey = makeUnspendableInternalKey ( ) ;
562-
563- const { output, address, witness } = bitcoin . payments . p2tr ( {
564- internalPubkey,
565- scriptTree,
566- redeem,
567- network : regtest ,
568- } ) ;
540+ // The only thing that differs between the wallets is the private key.
541+ // So we will use the first wallet for all the Psbt stuff.
542+ const [ wallet , wallet2 , wallet3 ] = leafKeys . map ( key =>
543+ new TaprootMultisigWallet (
544+ leafPubkeys ,
545+ 2 , // Number of required signatures
546+ key . privateKey ! ,
547+ LEAF_VERSION_TAPSCRIPT ,
548+ ) . setNetwork ( regtest ) ,
549+ ) ;
569550
570551 // amount from faucet
571552 const amount = 42e4 ;
572553 // amount to send
573554 const sendAmount = amount - 1e4 ;
574555 // get faucet
575- const unspent = await regtestUtils . faucetComplex ( output ! , amount ) ;
556+ const unspent = await regtestUtils . faucetComplex ( wallet . output , amount ) ;
576557
577558 const psbt = new bitcoin . Psbt ( { network : regtest } ) ;
578- psbt . addInput ( {
579- hash : unspent . txId ,
580- index : 0 ,
581- witnessUtxo : { value : amount , script : output ! } ,
582- } ) ;
583- psbt . updateInput ( 0 , {
584- tapLeafScript : [
585- {
586- leafVersion : redeem . redeemVersion ,
587- script : redeem . output ,
588- controlBlock : witness ! [ witness ! . length - 1 ] ,
589- } ,
590- ] ,
591- } ) ;
592559
593- psbt . addOutput ( { value : sendAmount , address : address ! } ) ;
560+ // Adding an input is a bit special in this case,
561+ // So we contain it in the wallet class
562+ // Any wallet can do this, wallet2 or wallet3 could be used.
563+ wallet . addInput ( psbt , unspent . txId , unspent . vout , unspent . value ) ;
594564
595- // random order for signers
596- psbt . signInput ( 0 , leafKeys [ 2 ] ) ;
597- psbt . signInput ( 0 , leafKeys [ 0 ] ) ;
565+ psbt . addOutput ( { value : sendAmount , address : wallet . address } ) ;
598566
599- // Before finalizing, every key that did not sign must have an empty signature
600- // in place where their signature would be.
601- // In order to do this currently we need to construct a dummy signature manually.
602- const noSignatureKeyDummySig = {
603- // This can be reused for each dummy signature
604- leafHash : tapleafHash ( {
605- output : leafScript ,
606- version : LEAF_VERSION_TAPSCRIPT ,
607- } ) ,
608- // This is the pubkey that didn't sign
609- pubkey : toXOnly ( leafKeys [ 1 ] . publicKey ) ,
610- // This must be an empty Buffer.
611- signature : Buffer . from ( [ ] ) ,
612- } ;
567+ // Sign with at least 2 of the 3 wallets.
568+ // Verify that there is a matching leaf script
569+ // (which includes the unspendable internalPubkey,
570+ // so we verify that no one can key-spend it)
571+ wallet3 . verifyInputScript ( psbt , 0 ) ;
572+ wallet2 . verifyInputScript ( psbt , 0 ) ;
573+ psbt . signInput ( 0 , wallet3 ) ;
574+ psbt . signInput ( 0 , wallet2 ) ;
613575
614- // We know that the first input exists and we have added tapScriptSigs
615- // so the tapScriptSig must exist .
616- psbt . data . inputs [ 0 ] . tapScriptSig ! . push ( noSignatureKeyDummySig ) ;
576+ // Before finalizing, we need to add dummy signatures for all that did not sign.
577+ // Any wallet can do this, wallet2 or wallet3 could be used .
578+ wallet . addDummySigs ( psbt ) ;
617579
618- psbt . finalizeInput ( 0 ) ;
580+ psbt . finalizeAllInputs ( ) ;
619581 const tx = psbt . extractTransaction ( ) ;
620582 const rawTx = tx . toBuffer ( ) ;
621583 const hex = rawTx . toString ( 'hex' ) ;
622584
623585 await regtestUtils . broadcast ( hex ) ;
624586 await regtestUtils . verify ( {
625587 txId : tx . getId ( ) ,
626- address : address ! ,
588+ // Any wallet can do this, wallet2 or wallet3 could be used.
589+ address : wallet . address ,
627590 vout : 0 ,
628591 value : sendAmount ,
629592 } ) ;
@@ -837,3 +800,240 @@ function makeUnspendableInternalKey(provableNonce?: Buffer): Buffer {
837800 return Hx ;
838801 }
839802}
803+
804+ class TaprootMultisigWallet {
805+ private leafScriptCache : Buffer | null = null ;
806+ private internalPubkeyCache : Buffer | null = null ;
807+ private paymentCache : bitcoin . Payment | null = null ;
808+ private readonly publicKeyCache : Buffer ;
809+ network : bitcoin . Network ;
810+
811+ constructor (
812+ /**
813+ * A list of all the (x-only) pubkeys in the multisig
814+ */
815+ private readonly pubkeys : Buffer [ ] ,
816+ /**
817+ * The number of required signatures
818+ */
819+ private readonly requiredSigs : number ,
820+ /**
821+ * The private key you hold.
822+ */
823+ private readonly privateKey : Buffer ,
824+ /**
825+ * leaf version (0xc0 currently)
826+ */
827+ readonly leafVersion : number ,
828+ /**
829+ * Optional shared nonce. This should be used in wallets where
830+ * the fact that key-spend is unspendable should not be public,
831+ * BUT each signer must verify that it is unspendable to be safe.
832+ */
833+ private readonly sharedNonce ?: Buffer ,
834+ ) {
835+ this . network = bitcoin . networks . bitcoin ;
836+ assert ( pubkeys . length > 0 , 'Need pubkeys' ) ;
837+ assert (
838+ pubkeys . every ( p => p . length === 32 ) ,
839+ 'Pubkeys must be 32 bytes (x-only)' ,
840+ ) ;
841+ assert (
842+ requiredSigs > 0 && requiredSigs <= pubkeys . length ,
843+ 'Invalid requiredSigs' ,
844+ ) ;
845+
846+ assert (
847+ leafVersion <= 0xff && ( leafVersion & 1 ) === 0 ,
848+ 'Invalid leafVersion' ,
849+ ) ;
850+
851+ if ( sharedNonce ) {
852+ assert (
853+ sharedNonce . length === 32 && ecc . isPrivate ( sharedNonce ) ,
854+ 'Invalid sharedNonce' ,
855+ ) ;
856+ }
857+
858+ const pubkey = ecc . pointFromScalar ( privateKey ) ;
859+ assert ( pubkey , 'Invalid pubkey' ) ;
860+
861+ this . publicKeyCache = Buffer . from ( pubkey ) ;
862+ assert (
863+ pubkeys . some ( p => p . equals ( toXOnly ( this . publicKeyCache ) ) ) ,
864+ 'At least one pubkey must match your private key' ,
865+ ) ;
866+
867+ // IMPORTANT: Make sure the pubkeys are sorted (To prevent ordering issues between wallet signers)
868+ this . pubkeys . sort ( ( a , b ) => a . compare ( b ) ) ;
869+ }
870+
871+ setNetwork ( network : bitcoin . Network ) : this {
872+ this . network = network ;
873+ return this ;
874+ }
875+
876+ // Required for Signer interface.
877+ // Prevent setting by using a getter.
878+ get publicKey ( ) : Buffer {
879+ return this . publicKeyCache ;
880+ }
881+
882+ /**
883+ * Lazily build the leafScript. A 2 of 3 would look like:
884+ * key1 OP_CHECKSIG key2 OP_CHECKSIGADD key3 OP_CHECKSIGADD OP_2 OP_GREATERTHANOREQUAL
885+ */
886+ get leafScript ( ) : Buffer {
887+ if ( this . leafScriptCache ) {
888+ return this . leafScriptCache ;
889+ }
890+ const ops = [ ] ;
891+ this . pubkeys . forEach ( pubkey => {
892+ if ( ops . length === 0 ) {
893+ ops . push ( pubkey ) ;
894+ ops . push ( bitcoin . opcodes . OP_CHECKSIG ) ;
895+ } else {
896+ ops . push ( pubkey ) ;
897+ ops . push ( bitcoin . opcodes . OP_CHECKSIGADD ) ;
898+ }
899+ } ) ;
900+ if ( this . requiredSigs > 16 ) {
901+ ops . push ( bitcoin . script . number . encode ( this . requiredSigs ) ) ;
902+ } else {
903+ ops . push ( bitcoin . opcodes . OP_1 - 1 + this . requiredSigs ) ;
904+ }
905+ ops . push ( bitcoin . opcodes . OP_GREATERTHANOREQUAL ) ;
906+
907+ this . leafScriptCache = bitcoin . script . compile ( ops ) ;
908+ return this . leafScriptCache ;
909+ }
910+
911+ get internalPubkey ( ) : Buffer {
912+ if ( this . internalPubkeyCache ) {
913+ return this . internalPubkeyCache ;
914+ }
915+ // See the helper function for explanation
916+ this . internalPubkeyCache = makeUnspendableInternalKey ( this . sharedNonce ) ;
917+ return this . internalPubkeyCache ;
918+ }
919+
920+ get scriptTree ( ) : Taptree {
921+ // If more complicated, maybe it should be cached.
922+ // (ie. if other scripts are created only to create the tree
923+ // and will only be stored in the tree.)
924+ return {
925+ output : this . leafScript ,
926+ } ;
927+ }
928+
929+ get redeem ( ) : {
930+ output : Buffer ;
931+ redeemVersion : number ;
932+ } {
933+ return {
934+ output : this . leafScript ,
935+ redeemVersion : this . leafVersion ,
936+ } ;
937+ }
938+
939+ private get payment ( ) : bitcoin . Payment {
940+ if ( this . paymentCache ) {
941+ return this . paymentCache ;
942+ }
943+ this . paymentCache = bitcoin . payments . p2tr ( {
944+ internalPubkey : this . internalPubkey ,
945+ scriptTree : this . scriptTree ,
946+ redeem : this . redeem ,
947+ network : this . network ,
948+ } ) ;
949+ return this . paymentCache ;
950+ }
951+
952+ get output ( ) : Buffer {
953+ return this . payment . output ! ;
954+ }
955+
956+ get address ( ) : string {
957+ return this . payment . address ! ;
958+ }
959+
960+ get controlBlock ( ) : Buffer {
961+ const witness = this . payment . witness ! ;
962+ return witness [ witness . length - 1 ] ;
963+ }
964+
965+ verifyInputScript ( psbt : bitcoin . Psbt , index : number ) {
966+ if ( index >= psbt . data . inputs . length )
967+ throw new Error ( 'Invalid input index' ) ;
968+ const input = psbt . data . inputs [ index ] ;
969+ if ( ! input . tapLeafScript ) throw new Error ( 'Input has no tapLeafScripts' ) ;
970+ const hasMatch =
971+ input . tapLeafScript . length === 1 &&
972+ input . tapLeafScript [ 0 ] . leafVersion === this . leafVersion &&
973+ input . tapLeafScript [ 0 ] . script . equals ( this . leafScript ) &&
974+ input . tapLeafScript [ 0 ] . controlBlock . equals ( this . controlBlock ) ;
975+ if ( ! hasMatch )
976+ throw new Error (
977+ 'No matching leafScript, or extra leaf script. Refusing to sign.' ,
978+ ) ;
979+ }
980+
981+ addInput (
982+ psbt : bitcoin . Psbt ,
983+ hash : string | Buffer ,
984+ index : number ,
985+ value : number ,
986+ ) {
987+ psbt . addInput ( {
988+ hash,
989+ index,
990+ witnessUtxo : { value, script : this . output } ,
991+ } ) ;
992+ psbt . updateInput ( psbt . inputCount - 1 , {
993+ tapLeafScript : [
994+ {
995+ leafVersion : this . leafVersion ,
996+ script : this . leafScript ,
997+ controlBlock : this . controlBlock ,
998+ } ,
999+ ] ,
1000+ } ) ;
1001+ }
1002+
1003+ addDummySigs ( psbt : bitcoin . Psbt ) {
1004+ const leafHash = tapleafHash ( {
1005+ output : this . leafScript ,
1006+ version : this . leafVersion ,
1007+ } ) ;
1008+ for ( const input of psbt . data . inputs ) {
1009+ if ( ! input . tapScriptSig ) continue ;
1010+ const signedPubkeys = input . tapScriptSig
1011+ . filter ( ts => ts . leafHash . equals ( leafHash ) )
1012+ . map ( ts => ts . pubkey ) ;
1013+ for ( const pubkey of this . pubkeys ) {
1014+ if ( signedPubkeys . some ( sPub => sPub . equals ( pubkey ) ) ) continue ;
1015+ // Before finalizing, every key that did not sign must have an empty signature
1016+ // in place where their signature would be.
1017+ // In order to do this currently we need to construct a dummy signature manually.
1018+ input . tapScriptSig . push ( {
1019+ // This can be reused for each dummy signature
1020+ leafHash,
1021+ // This is the pubkey that didn't sign
1022+ pubkey,
1023+ // This must be an empty Buffer.
1024+ signature : Buffer . from ( [ ] ) ,
1025+ } ) ;
1026+ }
1027+ }
1028+ }
1029+
1030+ // required for Signer interface
1031+ sign ( hash : Buffer , _lowR ?: boolean ) : Buffer {
1032+ return Buffer . from ( ecc . sign ( hash , this . privateKey ) ) ;
1033+ }
1034+
1035+ // required for Signer interface
1036+ signSchnorr ( hash : Buffer ) : Buffer {
1037+ return Buffer . from ( ecc . signSchnorr ( hash , this . privateKey ) ) ;
1038+ }
1039+ }
0 commit comments