-
Notifications
You must be signed in to change notification settings - Fork 42
Open
Description
import * as bitcoin from "bitcoinjs-lib";
import * as ecc from "tiny-secp256k1";
import { Taptree } from "bitcoinjs-lib/src/types";
import { tapleafHash } from "bitcoinjs-lib/src/payments/bip341";
function makeUnspendableInternalKey(provableNonce?: Buffer): Buffer {
// This is the generator point of secp256k1. Private key is known (equal to 1)
const G = Buffer.from(
"0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8",
"hex"
);
// This is the hash of the uncompressed generator point.
// It is also a valid X value on the curve, but we don't know what the private key is.
// Since we know this X value (a fake "public key") is made from a hash of a well known value,
// We can prove that the internalKey is unspendable.
const Hx = bitcoin.crypto.sha256(G);
if (provableNonce) {
if (provableNonce.length !== 32) {
throw new Error(
"provableNonce must be a 32 byte random value shared between script holders"
);
}
// Using a shared random value, we create an unspendable internalKey
// P = H + int(hash_taptweak(provableNonce))*G
// Since we don't know H's private key (see explanation above), we can't know P's private key
const tapHash = bitcoin.crypto.taggedHash("TapTweak", provableNonce);
const ret = ecc.xOnlyPointAddTweak(Hx, tapHash);
if (!ret) {
throw new Error(
"provableNonce produced an invalid key when tweaking the G hash"
);
}
return Buffer.from(ret.xOnlyPubkey);
} else {
// The downside to using no shared provable nonce is that anyone viewing a spend
// on the blockchain can KNOW that you CAN'T use key spend.
// Most people would be ok with this being public, but some wallets (exchanges etc)
// might not want ANY details about how their wallet works public.
return Hx;
}
}
export class TaprootMultisigWallet {
private leafScriptCache: Buffer | null = null;
private internalPubkeyCache: Buffer | null = null;
private paymentCache: bitcoin.Payment | null = null;
private readonly publicKeyCache: Buffer;
network: bitcoin.Network;
constructor(
/**
* A list of all the (x-only) pubkeys in the multisig
*/
private readonly pubkeys: Buffer[],
/**
* The number of required signatures
*/
private readonly requiredSigs: number,
/**
* The private key you hold.
*/
private readonly privateKey: Buffer,
/**
* leaf version (0xc0 currently)
*/
readonly leafVersion: number,
/**
* Optional shared nonce. This should be used in wallets where
* the fact that key-spend is unspendable should not be public,
* BUT each signer must verify that it is unspendable to be safe.
*/
private readonly sharedNonce?: Buffer
) {
this.network = bitcoin.networks.bitcoin;
const pubkey = ecc.pointFromScalar(privateKey);
if (!pubkey) throw "Invalid Keys";
this.publicKeyCache = Buffer.from(pubkey);
// IMPORTANT: Make sure the pubkeys are sorted (To prevent ordering issues between wallet signers)
this.pubkeys.sort((a, b) => a.compare(b));
}
setNetwork(network: bitcoin.Network): this {
this.network = network;
return this;
}
// Required for Signer interface.
// Prevent setting by using a getter.
get publicKey(): Buffer {
return this.publicKeyCache;
}
/**
* Lazily build the leafScript. A 2 of 3 would look like:
* key1 OP_CHECKSIG key2 OP_CHECKSIGADD key3 OP_CHECKSIGADD OP_2 OP_GREATERTHANOREQUAL
*/
get leafScript(): Buffer {
if (this.leafScriptCache) {
return this.leafScriptCache;
}
const ops = [];
this.pubkeys.forEach((pubkey) => {
if (ops.length === 0) {
ops.push(pubkey);
ops.push(bitcoin.opcodes.OP_CHECKSIG);
} else {
ops.push(pubkey);
ops.push(bitcoin.opcodes.OP_CHECKSIGADD);
}
});
if (this.requiredSigs > 16) {
ops.push(bitcoin.script.number.encode(this.requiredSigs));
} else {
ops.push(bitcoin.opcodes.OP_1 - 1 + this.requiredSigs);
}
ops.push(bitcoin.opcodes.OP_GREATERTHANOREQUAL);
this.leafScriptCache = bitcoin.script.compile(ops);
return this.leafScriptCache;
}
get internalPubkey(): Buffer {
if (this.internalPubkeyCache) {
return this.internalPubkeyCache;
}
// See the helper function for explanation
this.internalPubkeyCache = makeUnspendableInternalKey(this.sharedNonce);
return this.internalPubkeyCache;
}
get scriptTree(): Taptree {
// If more complicated, maybe it should be cached.
// (ie. if other scripts are created only to create the tree
// and will only be stored in the tree.)
return {
output: this.leafScript,
};
}
get redeem(): {
output: Buffer;
redeemVersion: number;
} {
return {
output: this.leafScript,
redeemVersion: this.leafVersion,
};
}
private get payment(): bitcoin.Payment {
if (this.paymentCache) {
return this.paymentCache;
}
this.paymentCache = bitcoin.payments.p2tr({
internalPubkey: this.internalPubkey,
scriptTree: this.scriptTree,
redeem: this.redeem,
network: this.network,
});
return this.paymentCache;
}
get output(): Buffer {
return this.payment.output!;
}
get address(): string {
return this.payment.address!;
}
get controlBlock(): Buffer {
const witness = this.payment.witness!;
return witness[witness.length - 1];
}
verifyInputScript(psbt: bitcoin.Psbt, index: number) {
if (index >= psbt.data.inputs.length)
throw new Error("Invalid input index");
const input = psbt.data.inputs[index];
if (!input.tapLeafScript) throw new Error("Input has no tapLeafScripts");
const hasMatch =
input.tapLeafScript.length === 1 &&
input.tapLeafScript[0].leafVersion === this.leafVersion &&
input.tapLeafScript[0].script.equals(this.leafScript) &&
input.tapLeafScript[0].controlBlock.equals(this.controlBlock);
if (!hasMatch)
throw new Error(
"No matching leafScript, or extra leaf script. Refusing to sign."
);
}
addInput(
psbt: bitcoin.Psbt,
hash: string | Buffer,
index: number,
value: number
) {
psbt.addInput({
hash,
index,
witnessUtxo: { value, script: this.output },
});
psbt.updateInput(psbt.inputCount - 1, {
tapLeafScript: [
{
leafVersion: this.leafVersion,
script: this.leafScript,
controlBlock: this.controlBlock,
},
],
});
}
addDummySigs(psbt: bitcoin.Psbt) {
const leafHash = tapleafHash({
output: this.leafScript,
version: this.leafVersion,
});
for (const input of psbt.data.inputs) {
if (!input.tapScriptSig) continue;
const signedPubkeys = input.tapScriptSig
.filter((ts) => ts.leafHash.equals(leafHash))
.map((ts) => ts.pubkey);
for (const pubkey of this.pubkeys) {
if (signedPubkeys.some((sPub) => sPub.equals(pubkey))) continue;
// Before finalizing, every key that did not sign must have an empty signature
// in place where their signature would be.
// In order to do this currently we need to construct a dummy signature manually.
input.tapScriptSig.push({
// This can be reused for each dummy signature
leafHash,
// This is the pubkey that didn't sign
pubkey,
// This must be an empty Buffer.
signature: Buffer.from([]),
});
}
}
}
// required for Signer interface
sign(hash: Buffer, _lowR?: boolean): Buffer {
return Buffer.from(ecc.sign(hash, this.privateKey));
}
// required for Signer interface
signSchnorr(hash: Buffer): Buffer {
return Buffer.from(ecc.signSchnorr(hash, this.privateKey));
}
}
const testTaproot = async () => {
const sigWalletPubkey1 =
"026272fe4cf7746c9c3de3d48afc5f27fe4ba052fc8f72913a6020fa970f7f5823";
const sigWalletAddress1 =
"tb1pgda5khhwqlc7jmdzn4plca3pa4m7jg38zcspj0mmuyk8hnj5pphskers72";
const sigWalletPubkey2 =
"02c032c56d3af8899a15915d52c706c4659bc5ddb88ddf965bb334641ff9498d58";
const sigWalletAddress2 =
"tb1p2vsa0qxsn96sulauasfgyyccfjdwp2rzg8h2ejpxcdauulltczuqw02jmj";
const sigWalletPubkey3 =
"02df4a77af5c93acb96b9749a84f4e61e1e259304adec248891d66247faddd8c3e";
const sigWalletAddress3 =
"tb1pa8h7v54gqy9dww7y248h4j48sltcqjkasz0mk28steqf4ln02geq8s2u42";
const leafPubkeys: Buffer[] = [
toXOnly(Buffer.from(sigWalletPubkey1, "hex")),
toXOnly(Buffer.from(sigWalletPubkey2, "hex")),
toXOnly(Buffer.from(sigWalletPubkey3, "hex")),
];
const leafKey = bip32.fromSeed(rng(64), bitcoin.networks.testnet);
const multiSigWallet = new TaprootMultisigWallet(
leafPubkeys,
2,
leafKey.privateKey!,
LEAF_VERSION_TAPSCRIPT
).setNetwork(bitcoin.networks.testnet);
console.log(multiSigWallet.address);
const psbt = new bitcoin.Psbt({ network: bitcoin.networks.testnet });
multiSigWallet.addInput(
psbt,
"a878fc53bd543212f1dde7618a4fa05e9c35d71ef88633b4c2c2cc7a5eb14233",
0,
100000
);
psbt.addOutput({
value: 50000,
address: "tb1psh9f9dk4fuhclvlvu929gqrftzpwpxlrcl208k0k3geltnrgh4dscpzwng",
});
console.log("psbt", psbt.toHex());
};
I used above code for generate MultiSig Wallet
But after get PSBT, I can not sign use Unisat Wallet.
I tried to sign PSBT like below
unisat.signPsbt("70736274ff01005e02000000017372b04d478556c29beb88e1eeee43cced8318bc95f664c3dbe5daa61067b2bc0000000000ffffffff0150c300000000000022512085ca92b6d54f2f8fb3ece1545400695882e09be3c7d4f3d9f68a33f5cc68bd5b000000000001012ba086010000000000225120b34b172f4ceba5ed36608acfe7e5921e6ff394810fcfabdefe527dac18ba389d6215c0d902f35f560e0470c63313c7369168d9d7df2d49bf295fd9fb7cb109ccee04941a529c9fb3cd7e776d61b6225b6c610e8906fb8faa6c59ac5c3e95b5f82d29d61a529c9fb3cd7e776d61b6225b6c610e8906fb8faa6c59ac5c3e95b5f82d29d66920df4a77af5c93acb96b9749a84f4e61e1e259304adec248891d66247faddd8c3eac20c032c56d3af8899a15915d52c706c4659bc5ddb88ddf965bb334641ff9498d58ba206272fe4cf7746c9c3de3d48afc5f27fe4ba052fc8f72913a6020fa970f7f5823ba539cc00000", {
autoFinalized:false,
toSignInputs:[
{
index: 0,
address: "tb1p2vsa0qxsn96sulauasfgyyccfjdwp2rzg8h2ejpxcdauulltczuqw02jmj",
}
]
})
When I try to use above command, I get this error
"Can not sign for input #0 with the key 025321d780d099750e7fbcec128213184c9ae0a86241eeacc826c37bce7febc0b8"
Hope to get help about taproot multi-sig wallet
Metadata
Metadata
Assignees
Labels
No labels