@@ -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,107 @@ 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 = [ ] ;
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 ) . toString ( 'hex' ) ) ;
538+ }
539+
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+ } ) ;
569+
570+ // amount from faucet
571+ const amount = 42e4 ;
572+ // amount to send
573+ const sendAmount = amount - 1e4 ;
574+ // get faucet
575+ const unspent = await regtestUtils . faucetComplex ( output ! , amount ) ;
576+
577+ 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+ } ) ;
592+
593+ psbt . addOutput ( { value : sendAmount , address : address ! } ) ;
594+
595+ // random order for signers
596+ psbt . signInput ( 0 , leafKeys [ 2 ] ) ;
597+ psbt . signInput ( 0 , leafKeys [ 0 ] ) ;
598+
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+ } ;
613+
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 ) ;
617+
618+ psbt . finalizeInput ( 0 ) ;
619+ const tx = psbt . extractTransaction ( ) ;
620+ const rawTx = tx . toBuffer ( ) ;
621+ const hex = rawTx . toString ( 'hex' ) ;
622+
623+ await regtestUtils . broadcast ( hex ) ;
624+ await regtestUtils . verify ( {
625+ txId : tx . getId ( ) ,
626+ address : address ! ,
627+ vout : 0 ,
628+ value : sendAmount ,
629+ } ) ;
630+ } ) ;
631+
531632 it ( 'can create (and broadcast via 3PBP) a taproot script-path spend Transaction - custom finalizer' , async ( ) => {
532633 const leafCount = 8 ;
533634 const leaves = Array . from ( { length : leafCount } ) . map (
@@ -693,3 +794,46 @@ function buildLeafIndexFinalizer(
693794 }
694795 } ;
695796}
797+
798+ function makeUnspendableInternalKey ( provableNonce ?: Buffer ) : Buffer {
799+ // This is the generator point of secp256k1. Private key is known (equal to 1)
800+ const G = Buffer . from (
801+ '0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8' ,
802+ 'hex' ,
803+ ) ;
804+ // This is the hash of the uncompressed generator point.
805+ // It is also a valid X value on the curve, but we don't know what the private key is.
806+ // Since we know this X value (a fake "public key") is made from a hash of a well known value,
807+ // We can prove that the internalKey is unspendable.
808+ const Hx = bitcoin . crypto . sha256 ( G ) ;
809+
810+ // This "Nothing Up My Sleeve" value is mentioned in BIP341 so we verify it here:
811+ assert . strictEqual (
812+ Hx . toString ( 'hex' ) ,
813+ '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0' ,
814+ ) ;
815+
816+ if ( provableNonce ) {
817+ // Using a shared random value, we create an unspendable internalKey
818+ // P = H + int(hash_taptweak(provableNonce))*G
819+ // Since we don't know H's private key (see explanation above), we can't know P's private key
820+ if ( provableNonce . length !== 32 ) {
821+ throw new Error (
822+ 'provableNonce must be a 32 byte random value shared between script holders' ,
823+ ) ;
824+ }
825+ const ret = ecc . xOnlyPointAddTweak ( Hx , provableNonce ) ;
826+ if ( ! ret ) {
827+ throw new Error (
828+ 'provableNonce produced an invalid key when tweaking the G hash' ,
829+ ) ;
830+ }
831+ return Buffer . from ( ret . xOnlyPubkey ) ;
832+ } else {
833+ // The downside to using no shared provable nonce is that anyone viewing a spend
834+ // on the blockchain can KNOW that you CAN'T use key spend.
835+ // Most people would be ok with this being public, but some wallets (exchanges etc)
836+ // might not want ANY details about how their wallet works public.
837+ return Hx ;
838+ }
839+ }
0 commit comments