Skip to content

Commit 230138f

Browse files
committed
Add taproot multisig with verified unspendable internalPubkey example
1 parent c6105c4 commit 230138f

File tree

1 file changed

+145
-1
lines changed

1 file changed

+145
-1
lines changed

test/integration/taproot.spec.ts

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { PsbtInput, TapLeaf, TapLeafScript } from 'bip174/src/lib/interfaces';
77
import { regtestUtils } from './_regtest';
88
import * as bitcoin from '../..';
99
import { Taptree } from '../../src/types';
10-
import { LEAF_VERSION_TAPSCRIPT } from '../../src/payments/bip341';
10+
import { LEAF_VERSION_TAPSCRIPT, tapleafHash } from '../../src/payments/bip341';
1111
import { toXOnly, tapTreeToList, tapTreeFromList } from '../../src/psbt/bip371';
1212
import { 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

Comments
 (0)