@arkade-os/sdk
Version:
Bitcoin wallet SDK with Taproot and Ark integration
102 lines (101 loc) • 4.55 kB
JavaScript
import { hex } from "@scure/base";
import { Transaction } from "@scure/btc-signer/transaction.js";
import { base64 } from "@scure/base";
import { aggregateKeys } from '../musig2/index.js';
import { CosignerPublicKey, getArkPsbtFields } from '../utils/unknownFields.js';
export const ErrInvalidSettlementTx = (tx) => new Error(`invalid settlement transaction: ${tx}`);
export const ErrInvalidSettlementTxOutputs = new Error("invalid settlement transaction outputs");
export const ErrEmptyTree = new Error("empty tree");
export const ErrNumberOfInputs = new Error("invalid number of inputs");
export const ErrWrongSettlementTxid = new Error("wrong settlement txid");
export const ErrInvalidAmount = new Error("invalid amount");
export const ErrNoLeaves = new Error("no leaves");
export const ErrInvalidTaprootScript = new Error("invalid taproot script");
export const ErrInvalidRoundTxOutputs = new Error("invalid round transaction outputs");
export const ErrWrongCommitmentTxid = new Error("wrong commitment txid");
export const ErrMissingCosignersPublicKeys = new Error("missing cosigners public keys");
const BATCH_OUTPUT_VTXO_INDEX = 0;
const BATCH_OUTPUT_CONNECTORS_INDEX = 1;
export function validateConnectorsTxGraph(settlementTxB64, connectorsGraph) {
connectorsGraph.validate();
if (connectorsGraph.root.inputsLength !== 1)
throw ErrNumberOfInputs;
const rootInput = connectorsGraph.root.getInput(0);
const settlementTx = Transaction.fromPSBT(base64.decode(settlementTxB64));
if (settlementTx.outputsLength <= BATCH_OUTPUT_CONNECTORS_INDEX)
throw ErrInvalidSettlementTxOutputs;
const expectedRootTxid = settlementTx.id;
if (!rootInput.txid)
throw ErrWrongSettlementTxid;
if (hex.encode(rootInput.txid) !== expectedRootTxid)
throw ErrWrongSettlementTxid;
if (rootInput.index !== BATCH_OUTPUT_CONNECTORS_INDEX)
throw 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.
export function validateVtxoTxGraph(graph, roundTransaction, sweepTapTreeRoot) {
if (roundTransaction.outputsLength < BATCH_OUTPUT_VTXO_INDEX + 1) {
throw ErrInvalidRoundTxOutputs;
}
const batchOutputAmount = roundTransaction.getOutput(BATCH_OUTPUT_VTXO_INDEX)?.amount;
if (!batchOutputAmount) {
throw ErrInvalidRoundTxOutputs;
}
if (!graph.root) {
throw ErrEmptyTree;
}
const rootInput = graph.root.getInput(0);
const commitmentTxid = roundTransaction.id;
if (!rootInput.txid ||
hex.encode(rootInput.txid) !== commitmentTxid ||
rootInput.index !== BATCH_OUTPUT_VTXO_INDEX) {
throw 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 ErrInvalidAmount;
}
const leaves = graph.leaves();
if (leaves.length === 0) {
throw 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.iterator()) {
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 = getArkPsbtFields(child.root, 0, CosignerPublicKey);
if (cosigners.length === 0) {
throw ErrMissingCosignersPublicKeys;
}
const cosignerKeys = cosigners.map((c) => c.key);
const { finalKey } = aggregateKeys(cosignerKeys, true, {
taprootTweak: sweepTapTreeRoot,
});
if (!finalKey ||
hex.encode(finalKey.slice(1)) !== hex.encode(previousScriptKey)) {
throw ErrInvalidTaprootScript;
}
}
}
}