Skip to content

Commit 8edc5e8

Browse files
authored
Merge pull request #2112 from bitcoinjs/example/multisigtaproot
Add taproot multisig with verified unspendable internalPubkey example
2 parents 2013f19 + 097a505 commit 8edc5e8

File tree

1 file changed

+345
-1
lines changed

1 file changed

+345
-1
lines changed

test/integration/taproot.spec.ts

Lines changed: 345 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,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

Comments
 (0)