UNPKG

@bitgo-forks/bitcoinjs-lib

Version:

Client-side Bitcoin JavaScript library

405 lines (404 loc) 15.9 kB
'use strict'; // Taproot-specific key aggregation and taptree logic as defined in: // https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki Object.defineProperty(exports, '__esModule', { value: true }); exports.getTaptreeRoot = exports.getTapleafHash = exports.parseControlBlock = exports.parseTaprootWitness = exports.getControlBlock = exports.getHuffmanTaptree = exports.getDepthFirstTaptree = exports.tapTweakPubkey = exports.tapTweakPrivkey = exports.hashTapBranch = exports.hashTapLeaf = exports.serializeScriptSize = exports.aggregateMuSigPubkeys = exports.INITIAL_TAPSCRIPT_VERSION = exports.EVEN_Y_COORD_PREFIX = void 0; const assert = require('assert'); const FastPriorityQueue = require('fastpriorityqueue'); const bcrypto = require('./crypto'); const bscript = require('./script'); const varuint = require('varuint-bitcoin'); /** * The 0x02 prefix indicating an even Y coordinate which is implicitly assumed * on all 32 byte x-only pub keys as defined in BIP340. */ exports.EVEN_Y_COORD_PREFIX = Buffer.of(0x02); exports.INITIAL_TAPSCRIPT_VERSION = 0xc0; /** * Aggregates a list of public keys into a single MuSig2* public key * according to the MuSig2 paper. * @param ecc Elliptic curve implementation * @param pubkeys The list of pub keys to aggregate * @returns a 32 byte Buffer representing the aggregate key */ function aggregateMuSigPubkeys(ecc, pubkeys) { // TODO: Consider enforcing key uniqueness. assert( pubkeys.length > 1, 'at least two pubkeys are required for musig key aggregation', ); // Sort the keys in ascending order pubkeys.sort(Buffer.compare); // 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 || P_i) const L = bcrypto.taggedHash('KeyAgg list', Buffer.concat(pubkeys)); const secondUniquePubkey = pubkeys.find(pubkey => !pubkeys[0].equals(pubkey)); const tweakedPubkeys = pubkeys.map(pubkey => { const xyPubkey = Buffer.concat([exports.EVEN_Y_COORD_PREFIX, pubkey]); if (secondUniquePubkey !== undefined && secondUniquePubkey.equals(pubkey)) { // The second unique key in the pubkey list given to ''KeyAgg'' (as well // as any keys identical to this key) gets the constant KeyAgg // coefficient 1 which saves an exponentiation (see the MuSig2* appendix // in the MuSig2 paper). return xyPubkey; } const c = bcrypto.taggedHash( 'KeyAgg coefficient', Buffer.concat([L, pubkey]), ); const tweakedPubkey = ecc.pointMultiply(xyPubkey, c); if (!tweakedPubkey) throw new Error('Failed to multiply pubkey by coefficient'); return tweakedPubkey; }); const aggregatePubkey = tweakedPubkeys.reduce((prev, curr) => { const next = ecc.pointAdd(prev, curr); if (!next) throw new Error('Failed to sum pubkeys'); return next; }); return aggregatePubkey.slice(1); } exports.aggregateMuSigPubkeys = aggregateMuSigPubkeys; /** * Encodes the length of a script as a bitcoin variable length integer. * @param script * @returns */ function serializeScriptSize(script) { return varuint.encode(script.length); } exports.serializeScriptSize = serializeScriptSize; /** * Gets a tapleaf tagged hash from a script. * @param script * @returns */ function hashTapLeaf(script, leafVersion = exports.INITIAL_TAPSCRIPT_VERSION) { const size = serializeScriptSize(script); return bcrypto.taggedHash( 'TapLeaf', Buffer.concat([Buffer.of(leafVersion), 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) { // sort the children lexicographically const sortedChildren = [child1, child2].sort(Buffer.compare); return bcrypto.taggedHash('TapBranch', Buffer.concat(sortedChildren)); } exports.hashTapBranch = hashTapBranch; function calculateTapTweak(pubkey, taptreeRoot) { if (taptreeRoot) { return bcrypto.taggedHash('TapTweak', Buffer.concat([pubkey, taptreeRoot])); } // 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. // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-22 return bcrypto.taggedHash('TapTweak', Buffer.from(pubkey)); } /** * Tweaks a privkey, using the tagged hash of its pubkey, and (optionally) a taptree root * @param ecc Elliptic curve implementation * @param pubkey public key, used to calculate the tweak * @param privkey the privkey to tweak * @param taptreeRoot the taptree root tagged hash * @returns {Buffer} the tweaked privkey */ function tapTweakPrivkey(ecc, pubkey, privkey, taptreeRoot) { const tapTweak = calculateTapTweak(pubkey, taptreeRoot); const point = ecc.pointFromScalar(privkey); if (!point) throw new Error('Invalid private key'); if (point[0] % 2 === 1) privkey = ecc.privateNegate(privkey); const result = ecc.privateAdd(privkey, tapTweak); if (!result) throw new Error('Invalid private key'); return result; } exports.tapTweakPrivkey = tapTweakPrivkey; /** * Tweaks an internal pubkey, using the tagged hash of itself, and (optionally) a taptree root * @param ecc Elliptic curve implementation * @param pubkey the internal pubkey to tweak * @param taptreeRoot the taptree root tagged hash * @returns {TweakedPubkey} the tweaked pubkey */ function tapTweakPubkey(ecc, pubkey, taptreeRoot) { const tapTweak = calculateTapTweak(pubkey, taptreeRoot); const result = ecc.xOnlyPointAddTweak(pubkey, tapTweak); if (!result) throw new Error('Invalid pubkey'); return result; } exports.tapTweakPubkey = tapTweakPubkey; function recurseTaptree(leaves, targetDepth = 0) { const { value, done } = leaves.next(); assert(!done, 'insufficient leaves to reconstruct tap tree'); const [index, leaf] = value; const tree = { root: hashTapLeaf(leaf.script, leaf.leafVersion), paths: [], }; tree.paths[index] = []; for (let depth = leaf.depth; depth > targetDepth; depth--) { const sibling = recurseTaptree(leaves, depth); tree.paths.forEach(path => path.push(sibling.root)); sibling.paths.forEach(path => path.push(tree.root)); tree.root = hashTapBranch(tree.root, sibling.root); // Merge disjoint sparse arrays of paths into tree.paths Object.assign(tree.paths, sibling.paths); } return tree; } /** * Gets the root hash and hash-paths of a taptree from the depth-first * construction used in BIP-0371 PSBTs * @param tree * @returns {Taptree} the tree, represented by its root hash, and the paths to * that root from each of the input scripts */ function getDepthFirstTaptree(tree) { const iter = tree.leaves.entries(); const ret = recurseTaptree(iter); assert(iter.next().done, 'invalid tap tree, no path to some leaves'); return ret; } exports.getDepthFirstTaptree = getDepthFirstTaptree; /** * 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 {Taptree} the tree, represented by its root hash, and the paths to that root from each of the input scripts */ function getHuffmanTaptree(scripts, weights) { assert( scripts.length > 0, 'at least one script is required to construct a tap tree', ); // Create a queue/heap of the provided scripts prioritized according to their // corresponding weights. const queue = new FastPriorityQueue((a, b) => { return a.weight < b.weight; }); scripts.forEach((script, index) => { const weight = weights[index] || 1; assert(weight > 0, 'script weight must be a positive value'); queue.add({ weight, taggedHash: hashTapLeaf(script), paths: { [index]: [] }, }); }); // Now that we have a queue of weighted scripts, we begin a loop whereby we // remove the two lowest weighted items from the queue. We create a tap branch // node from the two items, and add the branch back to the queue with the // combined weight of both its children. Each loop reduces the number of items // in the queue by one, and we repeat until we are left with only one item - // this becomes the tap tree root. // // For example, if we begin with scripts A, B, C, D with weights 6, 3, 1, 1 // After first loop: A(6), B(3), CD(1 + 1) // After second loop: A(6), B[CD](3 + 2) // Final loop: A[B[CD]](6+5) // The final tree will look like: // // A[B[CD]] // / \ // A B[CD] // / \ // B [CD] // / \ // C D // // This ensures that the spending conditions we believe to have the highest // probability of being used are further up the tree than less likely scripts, // thereby reducing the size of the merkle proofs for the more likely scripts. while (queue.size > 1) { // We can safely expect two polls to return non-null elements since we've // checked that the queue has at least two elements before looping. const child1 = queue.poll(); const child2 = queue.poll(); Object.values(child1.paths).forEach(path => path.push(child2.taggedHash)); Object.values(child2.paths).forEach(path => path.push(child1.taggedHash)); queue.add({ taggedHash: hashTapBranch(child1.taggedHash, child2.taggedHash), weight: child1.weight + child2.weight, paths: { ...child1.paths, ...child2.paths }, }); } // After the while loop above completes we should have exactly one element // remaining in the queue, which we can safely extract below. const rootNode = queue.poll(); const paths = Object.entries(rootNode.paths).reduce((acc, [index, path]) => { acc[Number(index)] = path; // TODO: Why doesn't TS know it's a number? return acc; }, Array(scripts.length)); return { root: rootNode.taggedHash, paths }; } exports.getHuffmanTaptree = getHuffmanTaptree; function getControlBlock( parity, pubkey, path, leafVersion = exports.INITIAL_TAPSCRIPT_VERSION, ) { const parityVersion = leafVersion + parity; return Buffer.concat([Buffer.of(parityVersion), pubkey, ...path]); } exports.getControlBlock = getControlBlock; /** * Parses a taproot witness stack and extracts key data elements. * @param witnessStack * @returns {ScriptPathWitness|KeyPathWitness} an object representing the * parsed witness for a script path or key path spend. * @throws {Error} if the witness stack does not conform to the BIP 341 script validation rules */ function parseTaprootWitness(witnessStack) { let annex; if ( witnessStack.length >= 2 && witnessStack[witnessStack.length - 1][0] === 0x50 ) { // If there are at least two witness elements, and the first byte of the last element is // 0x50, this last element is called annex a and is removed from the witness stack annex = witnessStack[witnessStack.length - 1]; witnessStack = witnessStack.slice(0, -1); } if (witnessStack.length < 1) { throw new Error('witness stack must have at least one element'); } else if (witnessStack.length === 1) { // key path spend const signature = witnessStack[0]; if (!bscript.isCanonicalSchnorrSignature(signature)) { throw new Error('invalid signature'); } return { spendType: 'Key', signature, annex }; } // script path spend // second to last element is the tapscript const tapscript = witnessStack[witnessStack.length - 2]; const tapscriptChunks = bscript.decompile(tapscript); if (!tapscriptChunks || tapscriptChunks.length === 0) { throw new Error('tapscript is not a valid script'); } // The last stack element is called the control block c, and must have length 33 + 32m, // for a value of m that is an integer between 0 and 128, inclusive const controlBlock = witnessStack[witnessStack.length - 1]; if ( controlBlock.length < 33 || controlBlock.length > 33 + 32 * 128 || controlBlock.length % 32 !== 1 ) { throw new Error('invalid control block length'); } return { spendType: 'Script', scriptSig: witnessStack.slice(0, -2), tapscript, controlBlock, annex, }; } exports.parseTaprootWitness = parseTaprootWitness; /** * Parses a taproot control block. * @param ecc Elliptic curve implementation * @param controlBlock the control block to parse * @returns {ControlBlock} the parsed control block * @throws {Error} if the witness stack does not conform to the BIP 341 script validation rules */ function parseControlBlock(ecc, controlBlock) { if ((controlBlock.length - 1) % 32 !== 0) throw new TypeError('Invalid control block length'); const parity = controlBlock[0] & 0x01; // Let p = c[1:33] and let P = lift_x(int(p)) where lift_x and [:] are defined as in BIP340. // Fail if this point is not on the curve const internalPubkey = controlBlock.slice(1, 33); if (!ecc.isXOnlyPoint(internalPubkey)) { throw new Error('internal pubkey is not an EC point'); } // The leaf version cannot be 0x50 as that would result in ambiguity with the annex. const leafVersion = controlBlock[0] & 0xfe; if (leafVersion === 0x50) { throw new Error('invalid leaf version'); } const path = []; for (let j = 33; j < controlBlock.length; j += 32) { path.push(controlBlock.slice(j, j + 32)); } return { parity, internalPubkey, leafVersion, path, }; } exports.parseControlBlock = parseControlBlock; /** * Calculates the tapleaf hash from a control block and script. * @param ecc Elliptic curve implementation * @param controlBlock the control block, either raw or parsed * @param tapscript the leaf script corresdponding to the control block * @returns {Buffer} the tapleaf hash */ function getTapleafHash(ecc, controlBlock, tapscript) { if (Buffer.isBuffer(controlBlock)) { controlBlock = parseControlBlock(ecc, controlBlock); } const { leafVersion } = controlBlock; return bcrypto.taggedHash( 'TapLeaf', Buffer.concat([ Buffer.of(leafVersion), serializeScriptSize(tapscript), tapscript, ]), ); } exports.getTapleafHash = getTapleafHash; /** * Calculates the taptree root hash from a control block and script. * @param ecc Elliptic curve implementation * @param controlBlock the control block, either raw or parsed * @param tapscript the leaf script corresdponding to the control block * @param tapleafHash the leaf hash if already calculated * @returns {Buffer} the taptree root hash */ function getTaptreeRoot(ecc, controlBlock, tapscript, tapleafHash) { if (Buffer.isBuffer(controlBlock)) { controlBlock = parseControlBlock(ecc, controlBlock); } const { path } = controlBlock; tapleafHash = tapleafHash || getTapleafHash(ecc, controlBlock, tapscript); // `taptreeMerkleHash` begins as our tapscript tapleaf hash and its value iterates // through its parent tapbranch hashes until it ends up as the taptree root hash let taptreeMerkleHash = tapleafHash; for (const taptreeSiblingHash of path) { taptreeMerkleHash = Buffer.compare(taptreeMerkleHash, taptreeSiblingHash) === -1 ? bcrypto.taggedHash( 'TapBranch', Buffer.concat([taptreeMerkleHash, taptreeSiblingHash]), ) : bcrypto.taggedHash( 'TapBranch', Buffer.concat([taptreeSiblingHash, taptreeMerkleHash]), ); } return taptreeMerkleHash; } exports.getTaptreeRoot = getTaptreeRoot;