UNPKG

lotus-sdk

Version:

Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem

289 lines (288 loc) 11.2 kB
import { Hash } from './crypto/hash.js'; import { PublicKey } from './publickey.js'; import { PrivateKey } from './privatekey.js'; import { Script } from './script.js'; import { Opcode } from './opcode.js'; import { BN } from './crypto/bn.js'; import { BufferWriter } from './encoding/bufferwriter.js'; import { Signature } from './crypto/signature.js'; export const TAPROOT_LEAF_MASK = 0xfe; export const TAPROOT_LEAF_TAPSCRIPT = 0xc0; export const TAPROOT_CONTROL_BASE_SIZE = 33; export const TAPROOT_CONTROL_NODE_SIZE = 32; export const TAPROOT_CONTROL_MAX_NODE_COUNT = 128; export const TAPROOT_CONTROL_MAX_SIZE = TAPROOT_CONTROL_BASE_SIZE + TAPROOT_CONTROL_NODE_SIZE * TAPROOT_CONTROL_MAX_NODE_COUNT; export const TAPROOT_SCRIPTTYPE = Opcode.OP_1; export const TAPROOT_INTRO_SIZE = 3; export const TAPROOT_SIZE_WITHOUT_STATE = TAPROOT_INTRO_SIZE + 33; export const TAPROOT_SIZE_WITH_STATE = TAPROOT_INTRO_SIZE + 33 + 33; export const TAPROOT_SIGHASH_TYPE = Signature.SIGHASH_ALL | Signature.SIGHASH_LOTUS; export const TAPROOT_ANNEX_TAG = 0x50; export function taggedHash(tag, data) { const tagHash = Hash.sha256(Buffer.from(tag, 'utf8')); const combined = Buffer.concat([tagHash, tagHash, data]); return Hash.sha256(combined); } export function calculateTapTweak(internalPubKey, merkleRoot = Buffer.alloc(32)) { const pubKeyBytes = internalPubKey.toBuffer(); const data = Buffer.concat([pubKeyBytes, merkleRoot]); return taggedHash('TapTweak', data); } export function calculateTapLeaf(script, leafVersion = TAPROOT_LEAF_TAPSCRIPT) { const scriptBuf = Buffer.isBuffer(script) ? script : script.toBuffer(); const writer = new BufferWriter(); writer.writeUInt8(leafVersion); writer.writeVarintNum(scriptBuf.length); writer.write(scriptBuf); return taggedHash('TapLeaf', writer.toBuffer()); } export function calculateTapBranch(left, right) { const ordered = Buffer.compare(left, right) < 0 ? Buffer.concat([left, right]) : Buffer.concat([right, left]); return taggedHash('TapBranch', ordered); } export function tweakPublicKey(internalPubKey, merkleRoot = Buffer.alloc(32)) { const tweak = calculateTapTweak(internalPubKey, merkleRoot); return internalPubKey.addScalar(tweak); } export function tweakPrivateKey(internalPrivKey, merkleRoot = Buffer.alloc(32)) { const internalPubKey = internalPrivKey.publicKey; const tweak = calculateTapTweak(internalPubKey, merkleRoot); const tweakBN = new BN(tweak); const privKeyBN = internalPrivKey.bn; const tweakedBN = privKeyBN.add(tweakBN).umod(PublicKey.getN()); return new PrivateKey(tweakedBN); } export function isTapLeafNode(node) { return 'script' in node; } export function isTapBranchNode(node) { return 'left' in node && 'right' in node; } export function buildTapTree(tree) { if (isTapLeafNode(tree)) { const leafNode = tree; const leafVersion = leafNode.leafVersion || TAPROOT_LEAF_TAPSCRIPT; const scriptBuf = Buffer.isBuffer(leafNode.script) ? leafNode.script : leafNode.script.toBuffer(); const leafHash = calculateTapLeaf(scriptBuf, leafVersion); return { merkleRoot: leafHash, leaves: [ { script: Script.fromBuffer(scriptBuf), leafVersion, leafHash, merklePath: [], }, ], }; } const leftResult = buildTapTree(tree.left); const rightResult = buildTapTree(tree.right); const branchHash = calculateTapBranch(leftResult.merkleRoot, rightResult.merkleRoot); const leftLeaves = leftResult.leaves.map(leaf => ({ ...leaf, merklePath: [...leaf.merklePath, rightResult.merkleRoot], })); const rightLeaves = rightResult.leaves.map(leaf => ({ ...leaf, merklePath: [...leaf.merklePath, leftResult.merkleRoot], })); return { merkleRoot: branchHash, leaves: [...leftLeaves, ...rightLeaves], }; } export function createControlBlock(internalPubKey, leafIndex, tree) { const treeResult = buildTapTree(tree); if (leafIndex < 0 || leafIndex >= treeResult.leaves.length) { throw new Error(`Invalid leaf index: ${leafIndex}`); } const leaf = treeResult.leaves[leafIndex]; const pubKeyBytes = internalPubKey.toBuffer(); const parity = pubKeyBytes[0] === 0x03 ? 1 : 0; const controlByte = (leaf.leafVersion & TAPROOT_LEAF_MASK) | parity; const writer = new BufferWriter(); writer.writeUInt8(controlByte); writer.write(pubKeyBytes.slice(1, 33)); for (const node of leaf.merklePath) { writer.write(node); } return writer.toBuffer(); } export function verifyTaprootCommitment(commitmentPubKey, internalPubKey, merkleRoot) { const expectedCommitment = tweakPublicKey(internalPubKey, merkleRoot); return commitmentPubKey.toString() === expectedCommitment.toString(); } export function isPayToTaproot(script) { const buf = script.toBuffer(); if (buf.length < TAPROOT_SIZE_WITHOUT_STATE) { return false; } if (buf[0] !== Opcode.OP_SCRIPTTYPE || buf[1] !== TAPROOT_SCRIPTTYPE) { return false; } if (buf[2] !== 33) { return false; } if (buf.length === TAPROOT_SIZE_WITHOUT_STATE) { return true; } return (buf.length === TAPROOT_SIZE_WITH_STATE && buf[TAPROOT_SIZE_WITHOUT_STATE] === 32); } export function extractTaprootCommitment(script) { if (!isPayToTaproot(script)) { throw new Error('Not a valid Pay-To-Taproot script'); } const buf = script.toBuffer(); const commitmentBytes = buf.subarray(3, 3 + 33); return PublicKey.fromBuffer(commitmentBytes); } export function extractTaprootState(script) { const buf = script.toBuffer(); if (buf.length !== TAPROOT_SIZE_WITH_STATE) { return null; } return buf.subarray(TAPROOT_SIZE_WITHOUT_STATE + 1, TAPROOT_SIZE_WITH_STATE); } export function buildPayToTaproot(commitment, state) { if (state && state.length !== 32) { throw new Error('Taproot state must be exactly 32 bytes'); } const commitmentBytes = commitment.toBuffer(); if (commitmentBytes.length !== 33) { throw new Error('Commitment must be 33-byte compressed public key'); } if (state) { return new Script() .add(Opcode.OP_SCRIPTTYPE) .add(TAPROOT_SCRIPTTYPE) .add(commitmentBytes) .add(state); } else { return new Script() .add(Opcode.OP_SCRIPTTYPE) .add(TAPROOT_SCRIPTTYPE) .add(commitmentBytes); } } export function buildKeyPathTaproot(internalPubKey, state) { const merkleRoot = Buffer.alloc(32); const commitment = tweakPublicKey(internalPubKey, merkleRoot); return buildPayToTaproot(commitment, state); } export function buildScriptPathTaproot(internalPubKey, tree, state) { const treeInfo = buildTapTree(tree); const commitment = tweakPublicKey(internalPubKey, treeInfo.merkleRoot); const script = buildPayToTaproot(commitment, state); return { script, commitment, merkleRoot: treeInfo.merkleRoot, leaves: treeInfo.leaves, }; } export function verifyTaprootScriptPath(internalPubKey, script, commitmentPubKey, leafVersion, merklePath, parity) { try { const pubkeyPrefix = parity === 0 ? 0x02 : 0x03; const fullPubkey = Buffer.concat([ Buffer.from([pubkeyPrefix]), internalPubKey, ]); let leafHash = calculateTapLeaf(script, leafVersion); for (const pathNode of merklePath) { if (Buffer.compare(leafHash, pathNode) < 0) { leafHash = calculateTapBranch(leafHash, pathNode); } else { leafHash = calculateTapBranch(pathNode, leafHash); } } const internalKey = new PublicKey(fullPubkey); const expectedCommitment = tweakPublicKey(internalKey, leafHash); const actualCommitment = new PublicKey(commitmentPubKey); if (expectedCommitment.toString() !== actualCommitment.toString()) { return false; } return true; } catch (e) { return false; } } export function verifyTaprootSpend(scriptPubkey, stack, flags) { const SCRIPT_DISABLE_TAPROOT_SIGHASH_LOTUS = 1 << 22; const SCRIPT_TAPROOT_KEY_SPEND_PATH = 1 << 23; const TAPROOT_ANNEX_TAG = 0x50; if (flags & SCRIPT_DISABLE_TAPROOT_SIGHASH_LOTUS) { return { success: false, error: 'SCRIPT_ERR_TAPROOT_PHASEOUT' }; } if (!isPayToTaproot(scriptPubkey)) { return { success: false, error: 'SCRIPT_ERR_SCRIPTTYPE_MALFORMED_SCRIPT' }; } const scriptBuf = scriptPubkey.toBuffer(); const vchPubkey = scriptBuf.slice(TAPROOT_INTRO_SIZE, TAPROOT_SIZE_WITHOUT_STATE); if (stack.length === 0) { return { success: false, error: 'SCRIPT_ERR_INVALID_STACK_OPERATION' }; } if (stack.length >= 2 && stack[stack.length - 1].length > 0 && stack[stack.length - 1][0] === TAPROOT_ANNEX_TAG) { return { success: false, error: 'SCRIPT_ERR_TAPROOT_ANNEX_NOT_SUPPORTED' }; } if (stack.length === 1) { return { success: true, stack, }; } const controlBlock = stack[stack.length - 1]; const scriptBytes = stack[stack.length - 2]; const execScript = new Script(scriptBytes); const newStack = stack.slice(0, stack.length - 2); const sizeRemainder = (controlBlock.length - TAPROOT_CONTROL_BASE_SIZE) % TAPROOT_CONTROL_NODE_SIZE; if (controlBlock.length < TAPROOT_CONTROL_BASE_SIZE || controlBlock.length > TAPROOT_CONTROL_MAX_SIZE || sizeRemainder !== 0) { return { success: false, error: 'SCRIPT_ERR_TAPROOT_WRONG_CONTROL_SIZE' }; } if ((controlBlock[0] & TAPROOT_LEAF_MASK) !== TAPROOT_LEAF_TAPSCRIPT) { return { success: false, error: 'SCRIPT_ERR_TAPROOT_LEAF_VERSION_NOT_SUPPORTED', }; } const internalPubkey = controlBlock.slice(1, TAPROOT_CONTROL_BASE_SIZE); const merklePath = []; for (let i = TAPROOT_CONTROL_BASE_SIZE; i < controlBlock.length; i += TAPROOT_CONTROL_NODE_SIZE) { merklePath.push(controlBlock.slice(i, i + TAPROOT_CONTROL_NODE_SIZE)); } const leafVersion = controlBlock[0] & TAPROOT_LEAF_MASK; const parity = controlBlock[0] & 0x01; const isValid = verifyTaprootScriptPath(internalPubkey, execScript, vchPubkey, leafVersion, merklePath, parity); if (!isValid) { return { success: false, error: 'SCRIPT_ERR_TAPROOT_CONTROL_BLOCK_VERIFICATION_FAILED', }; } const scriptPubkeyBuf = scriptPubkey.toBuffer(); if (scriptPubkeyBuf.length === TAPROOT_SIZE_WITH_STATE) { const state = extractTaprootState(scriptPubkey); if (state) { newStack.push(state); } } return { success: true, stack: newStack, scriptToExecute: execScript, }; }