Skip to content

Commit

Permalink
Merge branch 'master' into typos
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonandjay authored Jul 10, 2024
2 parents dba1d9f + 8edc5e8 commit a249b87
Showing 1 changed file with 345 additions and 1 deletion.
346 changes: 345 additions & 1 deletion test/integration/taproot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { PsbtInput, TapLeaf, TapLeafScript } from 'bip174/src/lib/interfaces';
import { regtestUtils } from './_regtest';
import * as bitcoin from '../..';
import { Taptree } from '../../src/types';
import { LEAF_VERSION_TAPSCRIPT } from '../../src/payments/bip341';
import { LEAF_VERSION_TAPSCRIPT, tapleafHash } from '../../src/payments/bip341';
import { toXOnly, tapTreeToList, tapTreeFromList } from '../../src/psbt/bip371';
import { witnessStackToScriptWitness } from '../../src/psbt/psbtutils';

Expand Down Expand Up @@ -528,6 +528,70 @@ describe('bitcoinjs-lib (transaction with taproot)', () => {
});
});

it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction - OP_CHECKSIGADD (2-of-3) and verify unspendable internalKey', async () => {
const leafKeys = [];
const leafPubkeys: Buffer[] = [];
for (let i = 0; i < 3; i++) {
const leafKey = bip32.fromSeed(rng(64), regtest);
leafKeys.push(leafKey);
leafPubkeys.push(toXOnly(leafKey.publicKey));
}

// The only thing that differs between the wallets is the private key.
// So we will use the first wallet for all the Psbt stuff.
const [wallet, wallet2, wallet3] = leafKeys.map(key =>
new TaprootMultisigWallet(
leafPubkeys,
2, // Number of required signatures
key.privateKey!,
LEAF_VERSION_TAPSCRIPT,
).setNetwork(regtest),
);

// amount from faucet
const amount = 42e4;
// amount to send
const sendAmount = amount - 1e4;
// get faucet
const unspent = await regtestUtils.faucetComplex(wallet.output, amount);

const psbt = new bitcoin.Psbt({ network: regtest });

// Adding an input is a bit special in this case,
// So we contain it in the wallet class
// Any wallet can do this, wallet2 or wallet3 could be used.
wallet.addInput(psbt, unspent.txId, unspent.vout, unspent.value);

psbt.addOutput({ value: sendAmount, address: wallet.address });

// Sign with at least 2 of the 3 wallets.
// Verify that there is a matching leaf script
// (which includes the unspendable internalPubkey,
// so we verify that no one can key-spend it)
wallet3.verifyInputScript(psbt, 0);
wallet2.verifyInputScript(psbt, 0);
psbt.signInput(0, wallet3);
psbt.signInput(0, wallet2);

// Before finalizing, we need to add dummy signatures for all that did not sign.
// Any wallet can do this, wallet2 or wallet3 could be used.
wallet.addDummySigs(psbt);

psbt.finalizeAllInputs();
const tx = psbt.extractTransaction();
const rawTx = tx.toBuffer();
const hex = rawTx.toString('hex');

await regtestUtils.broadcast(hex);
await regtestUtils.verify({
txId: tx.getId(),
// Any wallet can do this, wallet2 or wallet3 could be used.
address: wallet.address,
vout: 0,
value: sendAmount,
});
});

it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction - custom finalizer', async () => {
const leafCount = 8;
const leaves = Array.from({ length: leafCount }).map(
Expand Down Expand Up @@ -693,3 +757,283 @@ function buildLeafIndexFinalizer(
}
};
}

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);

// This "Nothing Up My Sleeve" value is mentioned in BIP341 so we verify it here:
assert.strictEqual(
Hx.toString('hex'),
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0',
);

if (provableNonce) {
// 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
if (provableNonce.length !== 32) {
throw new Error(
'provableNonce must be a 32 byte random value shared between script holders',
);
}
const ret = ecc.xOnlyPointAddTweak(Hx, provableNonce);
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;
}
}

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;
assert(pubkeys.length > 0, 'Need pubkeys');
assert(
pubkeys.every(p => p.length === 32),
'Pubkeys must be 32 bytes (x-only)',
);
assert(
requiredSigs > 0 && requiredSigs <= pubkeys.length,
'Invalid requiredSigs',
);

assert(
leafVersion <= 0xff && (leafVersion & 1) === 0,
'Invalid leafVersion',
);

if (sharedNonce) {
assert(
sharedNonce.length === 32 && ecc.isPrivate(sharedNonce),
'Invalid sharedNonce',
);
}

const pubkey = ecc.pointFromScalar(privateKey);
assert(pubkey, 'Invalid pubkey');

this.publicKeyCache = Buffer.from(pubkey);
assert(
pubkeys.some(p => p.equals(toXOnly(this.publicKeyCache))),
'At least one pubkey must match your private key',
);

// 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));
}
}

0 comments on commit a249b87

Please sign in to comment.