@arklabs/wallet-sdk
Version:
Bitcoin wallet SDK with Taproot and Ark integration
185 lines (184 loc) • 8.46 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrInvalidNodeTransaction = exports.ErrInvalidRootTransaction = exports.ErrInvalidControlBlock = exports.ErrInternalKey = exports.ErrInvalidTaprootScript = exports.ErrLeafChildren = exports.ErrParentTxidInput = exports.ErrNodeTxidDifferent = exports.ErrNodeParentTxidEmpty = exports.ErrNodeTxidEmpty = exports.ErrNodeTxEmpty = exports.ErrNoLeaves = exports.ErrInvalidAmount = exports.ErrWrongSettlementTxid = exports.ErrNumberOfInputs = exports.ErrInvalidRootLevel = exports.ErrEmptyTree = exports.ErrInvalidSettlementTxOutputs = exports.ErrInvalidSettlementTx = void 0;
exports.validateConnectorsTree = validateConnectorsTree;
exports.validateVtxoTree = validateVtxoTree;
const base_1 = require("@scure/base");
const btc_signer_1 = require("@scure/btc-signer");
const base_2 = require("@scure/base");
const utils_1 = require("@scure/btc-signer/utils");
const musig2_1 = require("../musig2");
const vtxoTree_1 = require("./vtxoTree");
exports.ErrInvalidSettlementTx = new vtxoTree_1.TxTreeError("invalid settlement transaction");
exports.ErrInvalidSettlementTxOutputs = new vtxoTree_1.TxTreeError("invalid settlement transaction outputs");
exports.ErrEmptyTree = new vtxoTree_1.TxTreeError("empty tree");
exports.ErrInvalidRootLevel = new vtxoTree_1.TxTreeError("invalid root level");
exports.ErrNumberOfInputs = new vtxoTree_1.TxTreeError("invalid number of inputs");
exports.ErrWrongSettlementTxid = new vtxoTree_1.TxTreeError("wrong settlement txid");
exports.ErrInvalidAmount = new vtxoTree_1.TxTreeError("invalid amount");
exports.ErrNoLeaves = new vtxoTree_1.TxTreeError("no leaves");
exports.ErrNodeTxEmpty = new vtxoTree_1.TxTreeError("node transaction empty");
exports.ErrNodeTxidEmpty = new vtxoTree_1.TxTreeError("node txid empty");
exports.ErrNodeParentTxidEmpty = new vtxoTree_1.TxTreeError("node parent txid empty");
exports.ErrNodeTxidDifferent = new vtxoTree_1.TxTreeError("node txid different");
exports.ErrParentTxidInput = new vtxoTree_1.TxTreeError("parent txid input mismatch");
exports.ErrLeafChildren = new vtxoTree_1.TxTreeError("leaf node has children");
exports.ErrInvalidTaprootScript = new vtxoTree_1.TxTreeError("invalid taproot script");
exports.ErrInternalKey = new vtxoTree_1.TxTreeError("invalid internal key");
exports.ErrInvalidControlBlock = new vtxoTree_1.TxTreeError("invalid control block");
exports.ErrInvalidRootTransaction = new vtxoTree_1.TxTreeError("invalid root transaction");
exports.ErrInvalidNodeTransaction = new vtxoTree_1.TxTreeError("invalid node transaction");
const SHARED_OUTPUT_INDEX = 0;
const CONNECTORS_OUTPUT_INDEX = 1;
function validateConnectorsTree(settlementTxB64, connectorsTree) {
connectorsTree.validate();
const rootNode = connectorsTree.root();
if (!rootNode)
throw exports.ErrEmptyTree;
const rootTx = btc_signer_1.Transaction.fromPSBT(base_2.base64.decode(rootNode.tx));
if (rootTx.inputsLength !== 1)
throw exports.ErrNumberOfInputs;
const rootInput = rootTx.getInput(0);
const settlementTx = btc_signer_1.Transaction.fromPSBT(base_2.base64.decode(settlementTxB64));
if (settlementTx.outputsLength <= CONNECTORS_OUTPUT_INDEX)
throw exports.ErrInvalidSettlementTxOutputs;
const expectedRootTxid = base_1.hex.encode((0, utils_1.sha256x2)(settlementTx.toBytes(true)).reverse());
if (!rootInput.txid)
throw exports.ErrWrongSettlementTxid;
if (base_1.hex.encode(rootInput.txid) !== expectedRootTxid)
throw exports.ErrWrongSettlementTxid;
if (rootInput.index !== CONNECTORS_OUTPUT_INDEX)
throw exports.ErrWrongSettlementTxid;
}
function validateVtxoTree(settlementTx, vtxoTree, sweepTapTreeRoot) {
vtxoTree.validate();
// Parse settlement transaction
let settlementTransaction;
try {
settlementTransaction = btc_signer_1.Transaction.fromPSBT(base_2.base64.decode(settlementTx));
}
catch {
throw exports.ErrInvalidSettlementTx;
}
if (settlementTransaction.outputsLength <= SHARED_OUTPUT_INDEX) {
throw exports.ErrInvalidSettlementTxOutputs;
}
const sharedOutput = settlementTransaction.getOutput(SHARED_OUTPUT_INDEX);
if (!sharedOutput?.amount)
throw exports.ErrInvalidSettlementTxOutputs;
const sharedOutputAmount = sharedOutput.amount;
const nbNodes = vtxoTree.numberOfNodes();
if (nbNodes === 0) {
throw exports.ErrEmptyTree;
}
if (vtxoTree.levels[0].length !== 1) {
throw exports.ErrInvalidRootLevel;
}
// Check root input is connected to settlement tx
const rootNode = vtxoTree.levels[0][0];
let rootTx;
try {
rootTx = btc_signer_1.Transaction.fromPSBT(base_2.base64.decode(rootNode.tx));
}
catch {
throw exports.ErrInvalidRootTransaction;
}
if (rootTx.inputsLength !== 1) {
throw exports.ErrNumberOfInputs;
}
const rootInput = rootTx.getInput(0);
if (!rootInput.txid || rootInput.index === undefined)
throw exports.ErrWrongSettlementTxid;
const settlementTxid = base_1.hex.encode((0, utils_1.sha256x2)(settlementTransaction.toBytes(true)).reverse());
if (base_1.hex.encode(rootInput.txid) !== settlementTxid ||
rootInput.index !== SHARED_OUTPUT_INDEX) {
throw exports.ErrWrongSettlementTxid;
}
// Check root output amounts
let sumRootValue = 0n;
for (let i = 0; i < rootTx.outputsLength; i++) {
const output = rootTx.getOutput(i);
if (!output?.amount)
continue;
sumRootValue += output.amount;
}
if (sumRootValue >= sharedOutputAmount) {
throw exports.ErrInvalidAmount;
}
if (vtxoTree.leaves().length === 0) {
throw exports.ErrNoLeaves;
}
// Validate each node in the tree
for (const level of vtxoTree.levels) {
for (const node of level) {
validateNode(vtxoTree, node, sweepTapTreeRoot);
}
}
}
function validateNode(vtxoTree, node, tapTreeRoot) {
if (!node.tx)
throw exports.ErrNodeTxEmpty;
if (!node.txid)
throw exports.ErrNodeTxidEmpty;
if (!node.parentTxid)
throw exports.ErrNodeParentTxidEmpty;
// Parse node transaction
let tx;
try {
tx = btc_signer_1.Transaction.fromPSBT(base_2.base64.decode(node.tx));
}
catch {
throw exports.ErrInvalidNodeTransaction;
}
const txid = base_1.hex.encode((0, utils_1.sha256x2)(tx.toBytes(true)).reverse());
if (txid !== node.txid) {
throw exports.ErrNodeTxidDifferent;
}
if (tx.inputsLength !== 1) {
throw exports.ErrNumberOfInputs;
}
const input = tx.getInput(0);
if (!input.txid)
throw exports.ErrParentTxidInput;
if (base_1.hex.encode(input.txid) !== node.parentTxid) {
throw exports.ErrParentTxidInput;
}
const children = vtxoTree.children(node.txid);
if (node.leaf && children.length >= 1) {
throw exports.ErrLeafChildren;
}
// Validate each child
for (let childIndex = 0; childIndex < children.length; childIndex++) {
const child = children[childIndex];
const childTx = btc_signer_1.Transaction.fromPSBT(base_2.base64.decode(child.tx));
const parentOutput = tx.getOutput(childIndex);
if (!parentOutput?.script)
throw exports.ErrInvalidTaprootScript;
const previousScriptKey = parentOutput.script.slice(2);
if (previousScriptKey.length !== 32) {
throw exports.ErrInvalidTaprootScript;
}
// Get cosigner keys from input
const cosignerKeys = (0, vtxoTree_1.getCosignerKeys)(childTx);
// Aggregate keys
const { finalKey } = (0, musig2_1.aggregateKeys)(cosignerKeys, true, {
taprootTweak: tapTreeRoot,
});
if (base_1.hex.encode(finalKey) !== base_1.hex.encode(previousScriptKey.slice(2))) {
throw exports.ErrInternalKey;
}
// Check amounts
let sumChildAmount = 0n;
for (let i = 0; i < childTx.outputsLength; i++) {
const output = childTx.getOutput(i);
if (!output?.amount)
continue;
sumChildAmount += output.amount;
}
if (!parentOutput.amount)
throw exports.ErrInvalidAmount;
if (sumChildAmount >= parentOutput.amount) {
throw exports.ErrInvalidAmount;
}
}
}