UNPKG

@arkade-os/sdk

Version:

Bitcoin wallet SDK with Taproot and Ark integration

109 lines (108 loc) 5.41 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ErrMissingCosignersPublicKeys = exports.ErrWrongCommitmentTxid = exports.ErrInvalidRoundTxOutputs = exports.ErrInvalidTaprootScript = exports.ErrNoLeaves = exports.ErrInvalidAmount = exports.ErrWrongSettlementTxid = exports.ErrNumberOfInputs = exports.ErrEmptyTree = exports.ErrInvalidSettlementTxOutputs = exports.ErrInvalidSettlementTx = void 0; exports.validateConnectorsTxGraph = validateConnectorsTxGraph; exports.validateVtxoTxGraph = validateVtxoTxGraph; 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 unknownFields_1 = require("../utils/unknownFields"); const ErrInvalidSettlementTx = (tx) => new Error(`invalid settlement transaction: ${tx}`); exports.ErrInvalidSettlementTx = ErrInvalidSettlementTx; exports.ErrInvalidSettlementTxOutputs = new Error("invalid settlement transaction outputs"); exports.ErrEmptyTree = new Error("empty tree"); exports.ErrNumberOfInputs = new Error("invalid number of inputs"); exports.ErrWrongSettlementTxid = new Error("wrong settlement txid"); exports.ErrInvalidAmount = new Error("invalid amount"); exports.ErrNoLeaves = new Error("no leaves"); exports.ErrInvalidTaprootScript = new Error("invalid taproot script"); exports.ErrInvalidRoundTxOutputs = new Error("invalid round transaction outputs"); exports.ErrWrongCommitmentTxid = new Error("wrong commitment txid"); exports.ErrMissingCosignersPublicKeys = new Error("missing cosigners public keys"); const BATCH_OUTPUT_VTXO_INDEX = 0; const BATCH_OUTPUT_CONNECTORS_INDEX = 1; function validateConnectorsTxGraph(settlementTxB64, connectorsGraph) { connectorsGraph.validate(); if (connectorsGraph.root.inputsLength !== 1) throw exports.ErrNumberOfInputs; const rootInput = connectorsGraph.root.getInput(0); const settlementTx = btc_signer_1.Transaction.fromPSBT(base_2.base64.decode(settlementTxB64)); if (settlementTx.outputsLength <= BATCH_OUTPUT_CONNECTORS_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 !== BATCH_OUTPUT_CONNECTORS_INDEX) throw exports.ErrWrongSettlementTxid; } // ValidateVtxoTxGraph checks if the given vtxo graph is valid. // The function validates: // - the number of nodes // - the number of leaves // - children coherence with parent. // - every control block and taproot output scripts. // - input and output amounts. function validateVtxoTxGraph(graph, roundTransaction, sweepTapTreeRoot) { if (roundTransaction.outputsLength < BATCH_OUTPUT_VTXO_INDEX + 1) { throw exports.ErrInvalidRoundTxOutputs; } const batchOutputAmount = roundTransaction.getOutput(BATCH_OUTPUT_VTXO_INDEX)?.amount; if (!batchOutputAmount) { throw exports.ErrInvalidRoundTxOutputs; } if (!graph.root) { throw exports.ErrEmptyTree; } const rootInput = graph.root.getInput(0); const commitmentTxid = base_1.hex.encode((0, utils_1.sha256x2)(roundTransaction.toBytes(true)).reverse()); if (!rootInput.txid || base_1.hex.encode(rootInput.txid) !== commitmentTxid || rootInput.index !== BATCH_OUTPUT_VTXO_INDEX) { throw exports.ErrWrongCommitmentTxid; } let sumRootValue = 0n; for (let i = 0; i < graph.root.outputsLength; i++) { const output = graph.root.getOutput(i); if (output?.amount) { sumRootValue += output.amount; } } if (sumRootValue !== batchOutputAmount) { throw exports.ErrInvalidAmount; } const leaves = graph.leaves(); if (leaves.length === 0) { throw exports.ErrNoLeaves; } // validate the graph structure graph.validate(); // iterates over all the nodes of the graph to verify that cosigners public keys are corresponding to the parent output for (const g of graph) { for (const [childIndex, child] of g.children) { const parentOutput = g.root.getOutput(childIndex); if (!parentOutput?.script) { throw new Error(`parent output ${childIndex} not found`); } const previousScriptKey = parentOutput.script.slice(2); if (previousScriptKey.length !== 32) { throw new Error(`parent output ${childIndex} has invalid script`); } const cosigners = (0, unknownFields_1.getArkPsbtFields)(child.root, 0, unknownFields_1.CosignerPublicKey); if (cosigners.length === 0) { throw exports.ErrMissingCosignersPublicKeys; } const cosignerKeys = cosigners.map((c) => c.key); const { finalKey } = (0, musig2_1.aggregateKeys)(cosignerKeys, true, { taprootTweak: sweepTapTreeRoot, }); if (!finalKey || base_1.hex.encode(finalKey.slice(1)) !== base_1.hex.encode(previousScriptKey)) { throw exports.ErrInvalidTaprootScript; } } } }