Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add initial p2tr output script support #1726

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"bs58check": "^2.0.0",
"create-hash": "^1.1.0",
"create-hmac": "^1.1.3",
"fastpriorityqueue": "^0.7.1",
"merkle-lib": "^2.0.10",
"pushdata-bitcoin": "^1.0.1",
"randombytes": "^2.0.1",
Expand Down
2 changes: 2 additions & 0 deletions src/payments/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const p2pkh_1 = require('./p2pkh');
exports.p2pkh = p2pkh_1.p2pkh;
const p2sh_1 = require('./p2sh');
exports.p2sh = p2sh_1.p2sh;
const p2tr_1 = require('./p2tr');
exports.p2tr = p2tr_1.p2tr;
const p2wpkh_1 = require('./p2wpkh');
exports.p2wpkh = p2wpkh_1.p2wpkh;
const p2wsh_1 = require('./p2wsh');
Expand Down
72 changes: 72 additions & 0 deletions src/payments/p2tr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const networks_1 = require('../networks');
const bscript = require('../script');
const taproot = require('../taproot');
const lazy = require('./lazy');
const typef = require('typeforce');
const OPS = bscript.OPS;
const ecc = require('tiny-secp256k1');
const { bech32m } = require('bech32');
/** Internal key with unknown discrete logarithm for eliminating keypath spends */
const H = Buffer.from(
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0',
'hex',
);
// output: OP_1 {witnessProgram}
function p2tr(a, opts) {
if (!a.address && !a.pubkey && !a.pubkeys && !a.scripts && !a.output)
throw new TypeError('Not enough data');
opts = Object.assign({ validate: true }, opts || {});
typef(
{
network: typef.maybe(typef.Object),
address: typef.maybe(typef.String),
output: typef.maybe(typef.BufferN(34)),
// a single pubkey
pubkey: typef.maybe(ecc.isPoint),
// the pub keys used for aggregate musig signing
pubkeys: typef.maybe(typef.arrayOf(ecc.isPoint)),
scripts: typef.maybe(typef.arrayOf(typef.Buffer)),
weights: typef.maybe(typef.arrayOf(typef.Number)),
},
a,
);
const network = a.network || networks_1.bitcoin;
const o = { network };
lazy.prop(o, 'address', () => {
if (!o.output) return;
const words = bech32m.toWords(o.output.slice(2));
words.unshift(0x01);
return bech32m.encode(network.bech32, words);
});
lazy.prop(o, 'output', () => {
let internalPubkey;
if (a.pubkey) {
// single pubkey
// internalPubkey = taproot.trimFirstByte(a.pubkey);
internalPubkey = a.pubkey;
} else if (a.pubkeys && a.pubkeys.length) {
// multiple pubkeys
internalPubkey = taproot.aggregateMuSigPubkeys(a.pubkeys);
} else {
// no key path spends
if (!a.scripts) return; // must have either scripts or pubkey(s)
// use internal key with unknown secret key
internalPubkey = H;
}
let tapTreeRoot;
if (a.scripts) {
tapTreeRoot = taproot.getHuffmanTaptreeRoot(a.scripts, a.weights);
}
const taprootPubkey = taproot.tapTweakPubkey(internalPubkey, tapTreeRoot);
// OP_1 indicates segwit version 1
return bscript.compile([OPS.OP_1, taprootPubkey]);
});
lazy.prop(o, 'name', () => {
const nameParts = ['p2tr'];
return nameParts.join('-');
});
return Object.assign(o, a);
}
exports.p2tr = p2tr;
173 changes: 173 additions & 0 deletions src/taproot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const assert = require('assert');
const FastPriorityQueue = require('fastpriorityqueue');
const bcrypto = require('./crypto');
const ecc = require('tiny-secp256k1');
// const SECP256K1_ORDER = Buffer.from('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', 'hex')
const INITIAL_TAPSCRIPT_VERSION = Buffer.from('c0', 'hex');
const TAPLEAF_TAGGED_HASH = bcrypto.sha256(Buffer.from('TapLeaf'));
const TAPBRANCH_TAGGED_HASH = bcrypto.sha256(Buffer.from('TapBranch'));
const TAPTWEAK_TAGGED_HASH = bcrypto.sha256(Buffer.from('TapTweak'));
/**
* Trims the leading 02/03 byte from an ECDSA pub key to get a 32 byte schnorr
* pub key with x-only coordinates.
* @param pubkey A 33 byte pubkey representing an EC point
* @returns a 32 byte x-only coordinate
*/
function trimFirstByte(pubkey) {
assert(pubkey.length === 33);
return pubkey.slice(1, 33);
}
exports.trimFirstByte = trimFirstByte;
/**
* Aggregates a list of public keys into a single MuSig public key
* according to the MuSig paper.
* @param pubkeys The list of pub keys to aggregate
* @returns a 32 byte Buffer representing the aggregate key
*/
function aggregateMuSigPubkeys(pubkeys) {
// sort keys in ascending order
pubkeys.sort();
const trimmedPubkeys = [];
pubkeys.forEach(pubkey => {
const trimmedPubkey = trimFirstByte(pubkey);
trimmedPubkeys.push(trimmedPubkey);
});
// In MuSig all signers contribute key material to a single signing key,
// using the equation
//
// P = sum_i µ_i * P_i
//
// where `P_i` is the public key of the `i`th signer and `µ_i` is a so-called
// _MuSig coefficient_ computed according to the following equation
//
// L = H(P_1 || P_2 || ... || P_n)
// µ_i = H(L || i)
const L = bcrypto.sha256(Buffer.concat(trimmedPubkeys));
let aggregatePubkey;
pubkeys.forEach(pubkey => {
const trimmedPubkey = trimFirstByte(pubkey);
const c = bcrypto.sha256(Buffer.concat([L, trimmedPubkey]));
const tweakedPubkey = ecc.pointMultiply(pubkey, c);
if (aggregatePubkey === undefined) {
aggregatePubkey = tweakedPubkey;
} else {
aggregatePubkey = ecc.pointAdd(aggregatePubkey, tweakedPubkey);
}
});
return aggregatePubkey;
}
exports.aggregateMuSigPubkeys = aggregateMuSigPubkeys;
/**
* Gets a tapleaf tagged hash from a script.
* @param script
* @returns
*/
function hashTapLeaf(script) {
// TODO: use multiple byte `size` when script length is >= 253 bytes
const size = new Uint8Array([script.length]);
return bcrypto.sha256(
Buffer.concat([
TAPLEAF_TAGGED_HASH,
TAPLEAF_TAGGED_HASH,
INITIAL_TAPSCRIPT_VERSION,
size,
script,
]),
);
}
exports.hashTapLeaf = hashTapLeaf;
/**
* Creates a lexicographically sorted tapbranch from two child taptree nodes
* and returns its tagged hash.
* @param child1
* @param child2
* @returns the tagged tapbranch hash
*/
function hashTapBranch(child1, child2) {
let leftChild;
let rightChild;
// sort the children lexicographically
if (child1 < child2) {
leftChild = child1;
rightChild = child2;
} else {
leftChild = child2;
rightChild = child1;
}
return bcrypto.sha256(
Buffer.concat([
TAPBRANCH_TAGGED_HASH,
TAPBRANCH_TAGGED_HASH,
leftChild,
rightChild,
]),
);
}
exports.hashTapBranch = hashTapBranch;
/**
* Tweaks an internal pubkey using the tagged hash of a taptree root.
* @param pubkey the internal pubkey to tweak
* @param tapTreeRoot the taptree root tagged hash
* @returns the tweaked pubkey
*/
function tapTweakPubkey(pubkey, tapTreeRoot) {
let tweakedPubkey;
if (tapTreeRoot) {
const trimmedPubkey = trimFirstByte(pubkey);
const tapTweak = bcrypto.sha256(
Buffer.concat([
TAPTWEAK_TAGGED_HASH,
TAPTWEAK_TAGGED_HASH,
trimmedPubkey,
tapTreeRoot,
]),
);
tweakedPubkey = ecc.pointAddScalar(pubkey, tapTweak);
} else {
// If the spending conditions do not require a script path, the output key should commit to an
// unspendable script path instead of having no script path.
const unspendableScriptPathRoot = bcrypto.sha256(pubkey);
tweakedPubkey = ecc.pointAddScalar(pubkey, unspendableScriptPathRoot);
}
return trimFirstByte(tweakedPubkey);
}
exports.tapTweakPubkey = tapTweakPubkey;
/**
* Gets the root hash of a taptree using a weighted Huffman construction from a
* list of scripts and corresponding weights,
* @param scripts
* @param weights
* @returns the tagged hash of the taptree root
*/
function getHuffmanTaptreeRoot(scripts, weights) {
const weightedScripts = [];
scripts.forEach((script, index) => {
const weight = weights ? weights[index] || 1 : 1;
assert(weight > 0);
assert(Number.isInteger(weight));
weightedScripts.push({
weight,
taggedHash: hashTapLeaf(script),
});
});
const queue = new FastPriorityQueue((a, b) => {
return a.weight < b.weight;
});
weightedScripts.forEach(weightedScript => {
queue.add(weightedScript);
});
while (queue.size > 1) {
const child1 = queue.poll();
const child2 = queue.poll();
const branchHash = hashTapBranch(child1.taggedHash, child2.taggedHash);
queue.add({
taggedHash: branchHash,
weight: child1.weight + child2.weight,
});
}
const tapTreeHash = queue.poll().taggedHash;
return tapTreeHash;
}
exports.getHuffmanTaptreeRoot = getHuffmanTaptreeRoot;
70 changes: 70 additions & 0 deletions test/fixtures/p2tr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"valid": [
{
"description": "p2tr, out (from scripts & key)",
"arguments": {
"pubkey": "03af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3",
"scripts": [
"208f5173bc367914e1574aceb3c7232a178a764fb6f14730b6b20bd36394c6c717ac",
"2007c7c32d159a27ba1824798b3b1d11e1b85f4dbc9e9fe63d95440a30737496deac",
"204d4b27ab455a6e2b03af29a141ef47fc579c8435f563c065bf0dd12e6180ccd4ac"
],
"weights": [1, 1, 2],
"network": "regtest"
},
"options": {},
"expected": {
"name": "p2tr",
"output": "OP_1 a64b94fdd14d11d268ae3aee9669e5489984ec326bc5c593fc1ae28ec9057cab",
"address": "bcrt1p5e9eflw3f5gay69w8thfv609fzvcfmpjd0zutylurt3gajg90j4stfe7x0"
}
},
{
"description": "p2tr, out (from scripts & aggregate key)",
"arguments": {
"pubkeys": [
"020e5bf9b7123091a300382fb400565a1867e839b24654c4138c9093a82fd58459",
"02dbec3eee44f474af6751e0a453b427ff610b9cc7f79b80f60337aeb06761c394"
],
"scripts": [
"200e5bf9b7123091a300382fb400565a1867e839b24654c4138c9093a82fd58459ad20dbec3eee44f474af6751e0a453b427ff610b9cc7f79b80f60337aeb06761c394ad",
"20dbec3eee44f474af6751e0a453b427ff610b9cc7f79b80f60337aeb06761c394ad200dcd7e6035f7ff5c860b78cfdd2bd80b4b160ca99a71654796afde11457e11e7ad",
"200dcd7e6035f7ff5c860b78cfdd2bd80b4b160ca99a71654796afde11457e11e7ad200e5bf9b7123091a300382fb400565a1867e839b24654c4138c9093a82fd58459ad"
],
"weights": [1, 1, 2],
"network": "regtest"
},
"options": {},
"expected": {
"name": "p2tr",
"output": "OP_1 d3793a0e3a819a9eea4ab03691cc57157a1a7190fa2edbe077fe509cc7b499cb",
"address": "bcrt1p6dun5r36sxdfa6j2kqmfrnzhz4ap5uvslghdhcrhlegfe3a5n89s3ulntt"
}
},
{
"description": "p2tr, address from output",
"arguments": {
"output": "OP_1 618d4140bbf980976a0f4d2ff9bb05a6772866840770452ff405148b872f0dc8",
"network": "regtest"
},
"options": {},
"expected": {
"name": "p2tr",
"address": "bcrt1pvxx5zs9mlxqfw6s0f5hlnwc95emjse5yqacy2tl5q52ghpe0phyqzwzvwu"
}
},
{
"description": "p2tr, testnet address from output",
"arguments": {
"output": "OP_1 d5e89e0b73605abba690ba5e00484e279d006283bed0055a0530fb6a8c9adac7",
"network": "testnet"
},
"options": {},
"expected": {
"name": "p2tr",
"address": "tb1p6h5fuzmnvpdthf5shf0qqjzwy7wsqc5rhmgq2ks9xrak4ry6mtrscsqvzp"
}
}
],
"invalid": []
}
Loading