@@ -7,7 +7,7 @@ import { PsbtInput, TapLeaf, TapLeafScript } from 'bip174/src/lib/interfaces';
77import { regtestUtils } from './_regtest' ;
88import * as bitcoin from '../..' ;
99import { Taptree } from '../../src/types' ;
10- import { LEAF_VERSION_TAPSCRIPT } from '../../src/payments/bip341' ;
10+ import { LEAF_VERSION_TAPSCRIPT , tapleafHash } from '../../src/payments/bip341' ;
1111import { toXOnly , tapTreeToList , tapTreeFromList } from '../../src/psbt/bip371' ;
1212import { witnessStackToScriptWitness } from '../../src/psbt/psbtutils' ;
1313
@@ -528,6 +528,70 @@ describe('bitcoinjs-lib (transaction with taproot)', () => {
528528 } ) ;
529529 } ) ;
530530
531+ it ( 'can create (and broadcast via 3PBP) a taproot script-path spend Transaction - OP_CHECKSIGADD (2-of-3) and verify unspendable internalKey' , async ( ) => {
532+ const leafKeys = [ ] ;
533+ const leafPubkeys : Buffer [ ] = [ ] ;
534+ for ( let i = 0 ; i < 3 ; i ++ ) {
535+ const leafKey = bip32 . fromSeed ( rng ( 64 ) , regtest ) ;
536+ leafKeys . push ( leafKey ) ;
537+ leafPubkeys . push ( toXOnly ( leafKey . publicKey ) ) ;
538+ }
539+
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+ ) ;
550+
551+ // amount from faucet
552+ const amount = 42e4 ;
553+ // amount to send
554+ const sendAmount = amount - 1e4 ;
555+ // get faucet
556+ const unspent = await regtestUtils . faucetComplex ( wallet . output , amount ) ;
557+
558+ const psbt = new bitcoin . Psbt ( { network : regtest } ) ;
559+
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 ) ;
564+
565+ psbt . addOutput ( { value : sendAmount , address : wallet . address } ) ;
566+
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 ) ;
575+
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 ) ;
579+
580+ psbt . finalizeAllInputs ( ) ;
581+ const tx = psbt . extractTransaction ( ) ;
582+ const rawTx = tx . toBuffer ( ) ;
583+ const hex = rawTx . toString ( 'hex' ) ;
584+
585+ await regtestUtils . broadcast ( hex ) ;
586+ await regtestUtils . verify ( {
587+ txId : tx . getId ( ) ,
588+ // Any wallet can do this, wallet2 or wallet3 could be used.
589+ address : wallet . address ,
590+ vout : 0 ,
591+ value : sendAmount ,
592+ } ) ;
593+ } ) ;
594+
531595 it ( 'can create (and broadcast via 3PBP) a taproot script-path spend Transaction - custom finalizer' , async ( ) => {
532596 const leafCount = 8 ;
533597 const leaves = Array . from ( { length : leafCount } ) . map (
@@ -693,3 +757,283 @@ function buildLeafIndexFinalizer(
693757 }
694758 } ;
695759}
760+
761+ function makeUnspendableInternalKey ( provableNonce ?: Buffer ) : Buffer {
762+ // This is the generator point of secp256k1. Private key is known (equal to 1)
763+ const G = Buffer . from (
764+ '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8' ,
765+ 'hex' ,
766+ ) ;
767+ // This is the hash of the uncompressed generator point.
768+ // It is also a valid X value on the curve, but we don't know what the private key is.
769+ // Since we know this X value (a fake "public key") is made from a hash of a well known value,
770+ // We can prove that the internalKey is unspendable.
771+ const Hx = bitcoin . crypto . sha256 ( G ) ;
772+
773+ // This "Nothing Up My Sleeve" value is mentioned in BIP341 so we verify it here:
774+ assert . strictEqual (
775+ Hx . toString ( 'hex' ) ,
776+ '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0' ,
777+ ) ;
778+
779+ if ( provableNonce ) {
780+ // Using a shared random value, we create an unspendable internalKey
781+ // P = H + int(hash_taptweak(provableNonce))*G
782+ // Since we don't know H's private key (see explanation above), we can't know P's private key
783+ if ( provableNonce . length !== 32 ) {
784+ throw new Error (
785+ 'provableNonce must be a 32 byte random value shared between script holders' ,
786+ ) ;
787+ }
788+ const ret = ecc . xOnlyPointAddTweak ( Hx , provableNonce ) ;
789+ if ( ! ret ) {
790+ throw new Error (
791+ 'provableNonce produced an invalid key when tweaking the G hash' ,
792+ ) ;
793+ }
794+ return Buffer . from ( ret . xOnlyPubkey ) ;
795+ } else {
796+ // The downside to using no shared provable nonce is that anyone viewing a spend
797+ // on the blockchain can KNOW that you CAN'T use key spend.
798+ // Most people would be ok with this being public, but some wallets (exchanges etc)
799+ // might not want ANY details about how their wallet works public.
800+ return Hx ;
801+ }
802+ }
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