123 lines
3.8 KiB
TypeScript
123 lines
3.8 KiB
TypeScript
|
import BIP32Factory from 'bip32';
|
||
|
import * as ecc from 'tiny-secp256k1';
|
||
|
import { describe, it } from 'mocha';
|
||
|
import * as bitcoin from '../..';
|
||
|
import { regtestUtils } from './_regtest';
|
||
|
const rng = require('randombytes');
|
||
|
const regtest = regtestUtils.network;
|
||
|
const bip32 = BIP32Factory(ecc);
|
||
|
|
||
|
describe('bitcoinjs-lib (transaction with taproot)', () => {
|
||
|
it('can create (and broadcast via 3PBP) a taproot keyspend Transaction', async () => {
|
||
|
const myKey = bip32.fromSeed(rng(64), regtest);
|
||
|
|
||
|
const output = createKeySpendOutput(myKey.publicKey);
|
||
|
const address = bitcoin.address.fromOutputScript(output, regtest);
|
||
|
// amount from faucet
|
||
|
const amount = 42e4;
|
||
|
// amount to send
|
||
|
const sendAmount = amount - 1e4;
|
||
|
// get faucet
|
||
|
const unspent = await regtestUtils.faucetComplex(output, amount);
|
||
|
|
||
|
const tx = createSigned(
|
||
|
myKey,
|
||
|
unspent.txId,
|
||
|
unspent.vout,
|
||
|
sendAmount,
|
||
|
[output],
|
||
|
[amount],
|
||
|
);
|
||
|
|
||
|
const hex = tx.toHex();
|
||
|
// console.log('Valid tx sent from:');
|
||
|
// console.log(address);
|
||
|
// console.log('tx hex:');
|
||
|
// console.log(hex);
|
||
|
await regtestUtils.broadcast(hex);
|
||
|
await regtestUtils.verify({
|
||
|
txId: tx.getId(),
|
||
|
address,
|
||
|
vout: 0,
|
||
|
value: sendAmount,
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Order of the curve (N) - 1
|
||
|
const N_LESS_1 = Buffer.from(
|
||
|
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140',
|
||
|
'hex',
|
||
|
);
|
||
|
// 1 represented as 32 bytes BE
|
||
|
const ONE = Buffer.from(
|
||
|
'0000000000000000000000000000000000000000000000000000000000000001',
|
||
|
'hex',
|
||
|
);
|
||
|
|
||
|
// Function for creating a tweaked p2tr key-spend only address
|
||
|
// (This is recommended by BIP341)
|
||
|
function createKeySpendOutput(publicKey: Buffer): Buffer {
|
||
|
// x-only pubkey (remove 1 byte y parity)
|
||
|
const myXOnlyPubkey = publicKey.slice(1, 33);
|
||
|
const commitHash = bitcoin.crypto.taggedHash('TapTweak', myXOnlyPubkey);
|
||
|
const tweakResult = ecc.xOnlyPointAddTweak(myXOnlyPubkey, commitHash);
|
||
|
if (tweakResult === null) throw new Error('Invalid Tweak');
|
||
|
const { xOnlyPubkey: tweaked } = tweakResult;
|
||
|
// scriptPubkey
|
||
|
return Buffer.concat([
|
||
|
// witness v1, PUSH_DATA 32 bytes
|
||
|
Buffer.from([0x51, 0x20]),
|
||
|
// x-only tweaked pubkey
|
||
|
tweaked,
|
||
|
]);
|
||
|
}
|
||
|
|
||
|
// Function for signing for a tweaked p2tr key-spend only address
|
||
|
// (Required for the above address)
|
||
|
interface KeyPair {
|
||
|
publicKey: Buffer;
|
||
|
privateKey?: Buffer;
|
||
|
}
|
||
|
function signTweaked(messageHash: Buffer, key: KeyPair): Uint8Array {
|
||
|
const privateKey =
|
||
|
key.publicKey[0] === 2
|
||
|
? key.privateKey
|
||
|
: ecc.privateAdd(ecc.privateSub(N_LESS_1, key.privateKey!)!, ONE)!;
|
||
|
const tweakHash = bitcoin.crypto.taggedHash(
|
||
|
'TapTweak',
|
||
|
key.publicKey.slice(1, 33),
|
||
|
);
|
||
|
const newPrivateKey = ecc.privateAdd(privateKey!, tweakHash);
|
||
|
if (newPrivateKey === null) throw new Error('Invalid Tweak');
|
||
|
return ecc.signSchnorr(messageHash, newPrivateKey, Buffer.alloc(32));
|
||
|
}
|
||
|
|
||
|
// Function for creating signed tx
|
||
|
function createSigned(
|
||
|
key: KeyPair,
|
||
|
txid: string,
|
||
|
vout: number,
|
||
|
amountToSend: number,
|
||
|
scriptPubkeys: Buffer[],
|
||
|
values: number[],
|
||
|
): bitcoin.Transaction {
|
||
|
const tx = new bitcoin.Transaction();
|
||
|
tx.version = 2;
|
||
|
// Add input
|
||
|
tx.addInput(Buffer.from(txid, 'hex').reverse(), vout);
|
||
|
// Add output
|
||
|
tx.addOutput(scriptPubkeys[0], amountToSend);
|
||
|
const sighash = tx.hashForWitnessV1(
|
||
|
0, // which input
|
||
|
scriptPubkeys, // All previous outputs of all inputs
|
||
|
values, // All previous values of all inputs
|
||
|
bitcoin.Transaction.SIGHASH_DEFAULT, // sighash flag, DEFAULT is schnorr-only (DEFAULT == ALL)
|
||
|
);
|
||
|
const signature = Buffer.from(signTweaked(sighash, key));
|
||
|
// witness stack for keypath spend is just the signature.
|
||
|
// If sighash is not SIGHASH_DEFAULT (ALL) then you must add 1 byte with sighash value
|
||
|
tx.ins[0].witness = [signature];
|
||
|
return tx;
|
||
|
}
|