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
JavaScript
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,
};
}