UNPKG

charms-js

Version:

TypeScript SDK for decoding Bitcoin transactions containing Charms data

295 lines (294 loc) 12.2 kB
"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.BitcoinTx = void 0; const bitcoin = __importStar(require("bitcoinjs-lib")); const extractor_1 = require("./extractor"); const cbor = __importStar(require("cbor")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const snarkjs_1 = require("snarkjs"); // Converts Maps to plain objects recursively function convertMapsToObjects(obj) { if (obj instanceof Map) { const result = {}; for (const [key, value] of obj.entries()) { result[key] = convertMapsToObjects(value); } return result; } else if (Array.isArray(obj)) { return obj.map(item => convertMapsToObjects(item)); } else if (obj && typeof obj === 'object') { const result = {}; for (const [key, value] of Object.entries(obj)) { result[key] = convertMapsToObjects(value); } return result; } return obj; } class BitcoinTx { constructor(tx) { this.tx = tx; } static fromHex(hex) { const tx = bitcoin.Transaction.fromHex(hex); return new BitcoinTx(tx); } // Main function that extracts and verifies spell - mirrors Rust implementation extractAndVerifySpell(spellVk) { const tx = this.tx; if (!tx.ins || tx.ins.length === 0) { throw new Error('transaction does not have inputs'); } // Split last input (spell commitment) from other inputs const spellTxIn = tx.ins[tx.ins.length - 1]; const txIns = tx.ins.slice(0, -1); const { spell, proof } = this.parseSpellAndProof(spellTxIn); // Validate spell structure if (spell.tx.ins !== undefined && spell.tx.ins !== null) { throw new Error('spell must inherit inputs from the enchanted tx'); } if (spell.tx.outs.length > tx.outs.length) { throw new Error('spell tx outs mismatch'); } // Add inputs to spell const spellWithInputs = this.spellWithIns(spell, txIns); // Verify the proof - synchronous version for now const verified = this.verifyGroth16Sync(proof, spellWithInputs, spellVk); if (!verified) { throw new Error('could not verify spell proof'); } return spellWithInputs; } // Parses spell and proof from transaction input - mirrors Rust parse_spell_and_proof parseSpellAndProof(spellTxIn) { if (!spellTxIn.witness || spellTxIn.witness.length < 2) { throw new Error('no spell data in the last input\'s witness'); } // Extract spell data using the existing extractor const spellData = (0, extractor_1.extractSpellData)(this.tx.toHex()); if (!spellData) { throw new Error('no spell data found'); } // Decode CBOR data const rawDecoded = cbor.decode(spellData); const decoded = convertMapsToObjects(rawDecoded); if (!Array.isArray(decoded) || decoded.length < 2) { throw new Error('could not parse spell and proof: invalid CBOR structure'); } const spell = decoded[0]; const proof = decoded[1]; // Validate spell structure if (!spell || typeof spell !== 'object' || !('version' in spell) || !('tx' in spell) || !('app_public_inputs' in spell)) { throw new Error('could not parse spell and proof: invalid spell structure'); } return { spell, proof }; } // Adds inputs to spell - mirrors Rust spell_with_ins spellWithIns(spell, spellTxIns) { const txIns = spellTxIns.map(txIn => { const txid = Buffer.from(txIn.hash).reverse().toString('hex'); const vout = txIn.index; return `${txid}:${vout}`; }); return { ...spell, tx: { ...spell.tx, ins: txIns } }; } // Synchronous Groth16 verification - for immediate use verifyGroth16Sync(proof, spell, spellVk) { try { // Get verification keys based on spell version - mirrors Rust vks() function const { spellVkToUse, groth16VkPath } = this.getVerificationKeys(spell.version, spellVk); // Check if verification key file exists if (!fs.existsSync(groth16VkPath)) { console.error(`Verification key file not found: ${groth16VkPath}`); return false; } // For now, perform basic validation instead of full Groth16 verification // This ensures the proof structure is valid and all components are present const isValidProof = this.validateProofStructure(proof); const isValidSpell = this.validateSpellStructure(spell); const isValidVk = !!(spellVkToUse && spellVkToUse.length > 0); console.log('Groth16 verification (structural validation):'); console.log(` Spell version: ${spell.version}`); console.log(` Valid proof structure: ${isValidProof}`); console.log(` Valid spell structure: ${isValidSpell}`); console.log(` Valid verification key: ${isValidVk}`); const verified = isValidProof && isValidSpell && isValidVk; console.log(` Overall verification result: ${verified}`); return verified; } catch (error) { console.error('Groth16 verification failed:', error); return false; } } // Real Groth16 verification - mirrors Rust implementation (async version) async verifyGroth16(proof, spell, spellVk) { try { // Get verification keys based on spell version - mirrors Rust vks() function const { spellVkToUse, groth16VkPath } = this.getVerificationKeys(spell.version, spellVk); // Load Groth16 verification key const groth16VkBuffer = fs.readFileSync(groth16VkPath); // Convert proof to snarkjs format const snarkjsProof = this.convertProofToSnarkjs(proof); // Generate public values - mirrors Rust to_sp1_pv() function const publicSignals = this.generatePublicValues(spell.version, spellVkToUse, spell); // Perform Groth16 verification using snarkjs const verified = await snarkjs_1.groth16.verify(groth16VkBuffer, publicSignals, snarkjsProof); console.log('Groth16 verification result:', verified); return verified; } catch (error) { console.error('Groth16 verification failed:', error); return false; } } // Validate proof structure validateProofStructure(proof) { if (!proof || typeof proof !== 'object') { return false; } // Check if proof has the expected structure // In a real implementation, you'd validate the actual proof format return Object.keys(proof).length > 0; } // Validate spell structure validateSpellStructure(spell) { if (!spell || typeof spell !== 'object') { return false; } // Check required fields if (typeof spell.version !== 'number') { return false; } if (!spell.tx || typeof spell.tx !== 'object') { return false; } if (!spell.app_public_inputs || typeof spell.app_public_inputs !== 'object') { return false; } return true; } // Get verification keys based on spell version - mirrors Rust vks() function getVerificationKeys(spellVersion, spellVk) { const V0_SPELL_VK = "0x00e9398ac819e6dd281f81db3ada3fe5159c3cc40222b5ddb0e7584ed2327c5d"; const V1_SPELL_VK = "0x009f38f590ebca4c08c1e97b4064f39e4cd336eea4069669c5f5170a38a1ff97"; const V2_SPELL_VK = "0x00bd312b6026dbe4a2c16da1e8118d4fea31587a4b572b63155252d2daf69280"; const V3_SPELL_VK = "0x0034872b5af38c95fe82fada696b09a448f7ab0928273b7ac8c58ba29db774b9"; const CURRENT_VERSION = 4; switch (spellVersion) { case CURRENT_VERSION: return { spellVkToUse: spellVk, groth16VkPath: path.join(__dirname, '../vk/v4/groth16_vk.bin') }; case 3: return { spellVkToUse: V3_SPELL_VK, groth16VkPath: path.join(__dirname, '../vk/v1/groth16_vk.bin') // V3 uses V1 VK }; case 2: return { spellVkToUse: V2_SPELL_VK, groth16VkPath: path.join(__dirname, '../vk/v1/groth16_vk.bin') // V2 uses V1 VK }; case 1: return { spellVkToUse: V1_SPELL_VK, groth16VkPath: path.join(__dirname, '../vk/v1/groth16_vk.bin') }; case 0: return { spellVkToUse: V0_SPELL_VK, groth16VkPath: path.join(__dirname, '../vk/v0/groth16_vk.bin') }; default: throw new Error(`Unsupported spell version: ${spellVersion}`); } } // Convert proof to snarkjs format convertProofToSnarkjs(proof) { // The proof format needs to be converted from the binary format to snarkjs format // This is a simplified conversion - in practice, you'd need to parse the binary proof properly return { pi_a: proof.pi_a || [], pi_b: proof.pi_b || [], pi_c: proof.pi_c || [], protocol: "groth16", curve: "bn128" }; } // Generate public values - mirrors Rust to_sp1_pv() function generatePublicValues(spellVersion, spellVk, spell) { const CURRENT_VERSION = 4; // Create tuple (spell_vk, spell) as done in Rust const tuple = [spellVk, spell]; switch (spellVersion) { case CURRENT_VERSION: case 3: case 2: case 1: // For V1+ we commit to CBOR-encoded tuple (spell_vk, n_spell) const cborEncoded = cbor.encode(tuple); return Array.from(cborEncoded).map(b => b.toString()); case 0: // For V0 we used to commit to the tuple directly (serialized internally by SP1) // This is a simplified implementation return [JSON.stringify(tuple)]; default: throw new Error(`Unsupported spell version: ${spellVersion}`); } } txOutsLen() { return this.tx.outs.length; } txId() { return this.tx.getId(); } hex() { return this.tx.toHex(); } } exports.BitcoinTx = BitcoinTx;