UNPKG

@arklabs/wallet-sdk

Version:

Bitcoin wallet SDK with Taproot and Ark integration

180 lines (179 loc) 7.14 kB
import { hex } from "@scure/base"; import { Transaction } from "@scure/btc-signer"; import { base64 } from "@scure/base"; import { sha256x2 } from "@scure/btc-signer/utils"; import { aggregateKeys } from '../musig2/index.js'; import { getCosignerKeys, TxTreeError } from './vtxoTree.js'; export const ErrInvalidSettlementTx = new TxTreeError("invalid settlement transaction"); export const ErrInvalidSettlementTxOutputs = new TxTreeError("invalid settlement transaction outputs"); export const ErrEmptyTree = new TxTreeError("empty tree"); export const ErrInvalidRootLevel = new TxTreeError("invalid root level"); export const ErrNumberOfInputs = new TxTreeError("invalid number of inputs"); export const ErrWrongSettlementTxid = new TxTreeError("wrong settlement txid"); export const ErrInvalidAmount = new TxTreeError("invalid amount"); export const ErrNoLeaves = new TxTreeError("no leaves"); export const ErrNodeTxEmpty = new TxTreeError("node transaction empty"); export const ErrNodeTxidEmpty = new TxTreeError("node txid empty"); export const ErrNodeParentTxidEmpty = new TxTreeError("node parent txid empty"); export const ErrNodeTxidDifferent = new TxTreeError("node txid different"); export const ErrParentTxidInput = new TxTreeError("parent txid input mismatch"); export const ErrLeafChildren = new TxTreeError("leaf node has children"); export const ErrInvalidTaprootScript = new TxTreeError("invalid taproot script"); export const ErrInternalKey = new TxTreeError("invalid internal key"); export const ErrInvalidControlBlock = new TxTreeError("invalid control block"); export const ErrInvalidRootTransaction = new TxTreeError("invalid root transaction"); export const ErrInvalidNodeTransaction = new TxTreeError("invalid node transaction"); const SHARED_OUTPUT_INDEX = 0; const CONNECTORS_OUTPUT_INDEX = 1; export function validateConnectorsTree(settlementTxB64, connectorsTree) { connectorsTree.validate(); const rootNode = connectorsTree.root(); if (!rootNode) throw ErrEmptyTree; const rootTx = Transaction.fromPSBT(base64.decode(rootNode.tx)); if (rootTx.inputsLength !== 1) throw ErrNumberOfInputs; const rootInput = rootTx.getInput(0); const settlementTx = Transaction.fromPSBT(base64.decode(settlementTxB64)); if (settlementTx.outputsLength <= CONNECTORS_OUTPUT_INDEX) throw ErrInvalidSettlementTxOutputs; const expectedRootTxid = hex.encode(sha256x2(settlementTx.toBytes(true)).reverse()); if (!rootInput.txid) throw ErrWrongSettlementTxid; if (hex.encode(rootInput.txid) !== expectedRootTxid) throw ErrWrongSettlementTxid; if (rootInput.index !== CONNECTORS_OUTPUT_INDEX) throw ErrWrongSettlementTxid; } export function validateVtxoTree(settlementTx, vtxoTree, sweepTapTreeRoot) { vtxoTree.validate(); // Parse settlement transaction let settlementTransaction; try { settlementTransaction = Transaction.fromPSBT(base64.decode(settlementTx)); } catch { throw ErrInvalidSettlementTx; } if (settlementTransaction.outputsLength <= SHARED_OUTPUT_INDEX) { throw ErrInvalidSettlementTxOutputs; } const sharedOutput = settlementTransaction.getOutput(SHARED_OUTPUT_INDEX); if (!sharedOutput?.amount) throw ErrInvalidSettlementTxOutputs; const sharedOutputAmount = sharedOutput.amount; const nbNodes = vtxoTree.numberOfNodes(); if (nbNodes === 0) { throw ErrEmptyTree; } if (vtxoTree.levels[0].length !== 1) { throw ErrInvalidRootLevel; } // Check root input is connected to settlement tx const rootNode = vtxoTree.levels[0][0]; let rootTx; try { rootTx = Transaction.fromPSBT(base64.decode(rootNode.tx)); } catch { throw ErrInvalidRootTransaction; } if (rootTx.inputsLength !== 1) { throw ErrNumberOfInputs; } const rootInput = rootTx.getInput(0); if (!rootInput.txid || rootInput.index === undefined) throw ErrWrongSettlementTxid; const settlementTxid = hex.encode(sha256x2(settlementTransaction.toBytes(true)).reverse()); if (hex.encode(rootInput.txid) !== settlementTxid || rootInput.index !== SHARED_OUTPUT_INDEX) { throw 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 ErrInvalidAmount; } if (vtxoTree.leaves().length === 0) { throw 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 ErrNodeTxEmpty; if (!node.txid) throw ErrNodeTxidEmpty; if (!node.parentTxid) throw ErrNodeParentTxidEmpty; // Parse node transaction let tx; try { tx = Transaction.fromPSBT(base64.decode(node.tx)); } catch { throw ErrInvalidNodeTransaction; } const txid = hex.encode(sha256x2(tx.toBytes(true)).reverse()); if (txid !== node.txid) { throw ErrNodeTxidDifferent; } if (tx.inputsLength !== 1) { throw ErrNumberOfInputs; } const input = tx.getInput(0); if (!input.txid) throw ErrParentTxidInput; if (hex.encode(input.txid) !== node.parentTxid) { throw ErrParentTxidInput; } const children = vtxoTree.children(node.txid); if (node.leaf && children.length >= 1) { throw ErrLeafChildren; } // Validate each child for (let childIndex = 0; childIndex < children.length; childIndex++) { const child = children[childIndex]; const childTx = Transaction.fromPSBT(base64.decode(child.tx)); const parentOutput = tx.getOutput(childIndex); if (!parentOutput?.script) throw ErrInvalidTaprootScript; const previousScriptKey = parentOutput.script.slice(2); if (previousScriptKey.length !== 32) { throw ErrInvalidTaprootScript; } // Get cosigner keys from input const cosignerKeys = getCosignerKeys(childTx); // Aggregate keys const { finalKey } = aggregateKeys(cosignerKeys, true, { taprootTweak: tapTreeRoot, }); if (hex.encode(finalKey) !== hex.encode(previousScriptKey.slice(2))) { throw 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 ErrInvalidAmount; if (sumChildAmount >= parentOutput.amount) { throw ErrInvalidAmount; } } }