@arklabs/wallet-sdk
Version:
Bitcoin wallet SDK with Taproot and Ark integration
239 lines (238 loc) • 9.28 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.TreeSignerSession = exports.ErrMissingAggregateKey = exports.ErrMissingVtxoTree = void 0;
exports.validateTreeSigs = validateTreeSigs;
const musig2 = __importStar(require("../musig2"));
const vtxoTree_1 = require("./vtxoTree");
const btc_signer_1 = require("@scure/btc-signer");
const base_1 = require("@scure/base");
const secp256k1_1 = require("@noble/curves/secp256k1");
const utils_1 = require("@scure/btc-signer/utils");
exports.ErrMissingVtxoTree = new Error("missing vtxo tree");
exports.ErrMissingAggregateKey = new Error("missing aggregate key");
class TreeSignerSession {
constructor(secretKey) {
this.secretKey = secretKey;
this.myNonces = null;
this.aggregateNonces = null;
this.tree = null;
this.scriptRoot = null;
this.rootSharedOutputAmount = null;
}
static random() {
const secretKey = (0, utils_1.randomPrivateKeyBytes)();
return new TreeSignerSession(secretKey);
}
init(tree, scriptRoot, rootInputAmount) {
this.tree = tree;
this.scriptRoot = scriptRoot;
this.rootSharedOutputAmount = rootInputAmount;
}
getPublicKey() {
return secp256k1_1.secp256k1.getPublicKey(this.secretKey);
}
getNonces() {
if (!this.tree)
throw exports.ErrMissingVtxoTree;
if (!this.myNonces) {
this.myNonces = this.generateNonces();
}
const nonces = [];
for (const levelNonces of this.myNonces) {
const levelPubNonces = [];
for (const nonce of levelNonces) {
if (!nonce) {
levelPubNonces.push(null);
continue;
}
levelPubNonces.push({ pubNonce: nonce.pubNonce });
}
nonces.push(levelPubNonces);
}
return nonces;
}
setAggregatedNonces(nonces) {
if (this.aggregateNonces)
throw new Error("nonces already set");
this.aggregateNonces = nonces;
}
sign() {
if (!this.tree)
throw exports.ErrMissingVtxoTree;
if (!this.aggregateNonces)
throw new Error("nonces not set");
if (!this.myNonces)
throw new Error("nonces not generated");
const sigs = [];
for (let levelIndex = 0; levelIndex < this.tree.levels.length; levelIndex++) {
const levelSigs = [];
const level = this.tree.levels[levelIndex];
for (let nodeIndex = 0; nodeIndex < level.length; nodeIndex++) {
const node = level[nodeIndex];
const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(node.tx));
const sig = this.signPartial(tx, levelIndex, nodeIndex);
if (sig) {
levelSigs.push(sig);
}
else {
levelSigs.push(null);
}
}
sigs.push(levelSigs);
}
return sigs;
}
generateNonces() {
if (!this.tree)
throw exports.ErrMissingVtxoTree;
const myNonces = [];
const publicKey = secp256k1_1.secp256k1.getPublicKey(this.secretKey);
for (const level of this.tree.levels) {
const levelNonces = [];
for (let i = 0; i < level.length; i++) {
const nonces = musig2.generateNonces(publicKey);
levelNonces.push(nonces);
}
myNonces.push(levelNonces);
}
return myNonces;
}
signPartial(tx, levelIndex, nodeIndex) {
if (!this.tree || !this.scriptRoot || !this.rootSharedOutputAmount) {
throw TreeSignerSession.NOT_INITIALIZED;
}
if (!this.myNonces || !this.aggregateNonces) {
throw new Error("session not properly initialized");
}
const myNonce = this.myNonces[levelIndex][nodeIndex];
if (!myNonce)
return null;
const aggNonce = this.aggregateNonces[levelIndex][nodeIndex];
if (!aggNonce)
throw new Error("missing aggregate nonce");
const prevoutAmounts = [];
const prevoutScripts = [];
const cosigners = (0, vtxoTree_1.getCosignerKeys)(tx);
const { finalKey } = musig2.aggregateKeys(cosigners, true, {
taprootTweak: this.scriptRoot,
});
for (let inputIndex = 0; inputIndex < tx.inputsLength; inputIndex++) {
const prevout = getPrevOutput(finalKey, this.tree, this.rootSharedOutputAmount, tx);
prevoutAmounts.push(prevout.amount);
prevoutScripts.push(prevout.script);
}
const message = tx.preimageWitnessV1(0, // always first input
prevoutScripts, btc_signer_1.SigHash.DEFAULT, prevoutAmounts);
return musig2.sign(myNonce.secNonce, this.secretKey, aggNonce.pubNonce, cosigners, message, {
taprootTweak: this.scriptRoot,
sortKeys: true,
});
}
}
exports.TreeSignerSession = TreeSignerSession;
TreeSignerSession.NOT_INITIALIZED = new Error("session not initialized, call init method");
// Helper function to validate tree signatures
async function validateTreeSigs(finalAggregatedKey, sharedOutputAmount, vtxoTree) {
// Iterate through each level of the tree
for (const level of vtxoTree.levels) {
for (const node of level) {
// Parse the transaction
const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(node.tx));
const input = tx.getInput(0);
// Check if input has signature
if (!input.tapKeySig) {
throw new Error("unsigned tree input");
}
// Get the previous output information
const prevout = getPrevOutput(finalAggregatedKey, vtxoTree, sharedOutputAmount, tx);
// Calculate the message that was signed
const message = tx.preimageWitnessV1(0, // always first input
[prevout.script], btc_signer_1.SigHash.DEFAULT, [prevout.amount]);
// Verify the signature
const isValid = secp256k1_1.schnorr.verify(input.tapKeySig, message, finalAggregatedKey);
if (!isValid) {
throw new Error("invalid signature");
}
}
}
}
function getPrevOutput(finalKey, vtxoTree, sharedOutputAmount, partial) {
// Generate P2TR script
const pkScript = btc_signer_1.Script.encode(["OP_1", finalKey.slice(1)]);
// Get root node
const rootNode = vtxoTree.levels[0][0];
if (!rootNode)
throw new Error("empty vtxo tree");
const input = partial.getInput(0);
if (!input.txid)
throw new Error("missing input txid");
const parentTxID = base_1.hex.encode(input.txid);
// Check if parent is root
if (rootNode.parentTxid === parentTxID) {
return {
amount: sharedOutputAmount,
script: pkScript,
};
}
// Search for parent in tree
let parent = null;
for (const level of vtxoTree.levels) {
for (const node of level) {
if (node.txid === parentTxID) {
parent = node;
break;
}
}
if (parent)
break;
}
if (!parent) {
throw new Error("parent tx not found");
}
// Parse parent tx
const parentTx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(parent.tx));
if (!input.index)
throw new Error("missing input index");
const parentOutput = parentTx.getOutput(input.index);
if (!parentOutput)
throw new Error("parent output not found");
if (!parentOutput.amount)
throw new Error("parent output amount not found");
return {
amount: parentOutput.amount,
script: pkScript,
};
}