UNPKG

@arklabs/wallet-sdk

Version:

Bitcoin wallet SDK with Taproot and Ark integration

185 lines (184 loc) 8.46 kB
"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; } } }