UNPKG

@polymerdao/mcp-polymer

Version:

A Model Context Protocol (MCP) server for blockchain event verification using Polymer's Prove API.

249 lines (248 loc) 11.2 kB
import { ethers } from 'ethers'; import { createHash } from 'crypto'; export class ProofVerifier { constructor(validationKey, chainId, clientType = "proof_api") { // Validate the validation key format if (!validationKey || !validationKey.match(/^0x[0-9a-fA-F]{40}$/)) { throw new Error('Invalid validation key format'); } this.validationKey = validationKey.toLowerCase(); this.chainId = chainId; this.clientType = clientType; } verifyPolymerProof(proofBase64) { try { // Validate input if (!proofBase64 || typeof proofBase64 !== 'string') { throw new Error('Invalid proof format'); } // Remove whitespace and add padding if necessary let cleanProof = proofBase64.replace(/\s/g, ''); while (cleanProof.length % 4 !== 0) { cleanProof += '='; } // Decode the base64 proof const proofBytes = Buffer.from(cleanProof, 'base64'); if (proofBytes.length < 128) { throw new Error('Proof data too short'); } // Parse the binary proof structure (matching the Rust implementation) const appHash = proofBytes.subarray(0, 32); const sigR = proofBytes.subarray(32, 64); const sigS = proofBytes.subarray(64, 96); const sigV = proofBytes[96]; const chainIdBytes = proofBytes.subarray(97, 101); const peptideHeightBytes = proofBytes.subarray(101, 109); const blockNumberBytes = proofBytes.subarray(109, 117); const txIndexBytes = proofBytes.subarray(117, 121); const logIndexBytes = proofBytes.subarray(121, 125); const eventEndBytes = proofBytes.subarray(126, 128); // Parse values const chainIdU32 = readUInt32BE(chainIdBytes); const peptideHeight = readUInt64BE(peptideHeightBytes); const blockNumber = readUInt64BE(blockNumberBytes); const txIndex = readUInt32BE(txIndexBytes); const logIndex = readUInt32BE(logIndexBytes); const eventEnd = readUInt16BE(eventEndBytes); // Validate recovery ID if (sigV < 27 || sigV > 28) { throw new Error(`Invalid recovery ID: ${sigV}`); } // Validate event end offset if (eventEnd > proofBytes.length) { throw new Error('Invalid event end offset'); } // Verify signature const signatureValid = this.verifySignature(appHash, peptideHeight, sigR, sigS, sigV); // Extract raw event data and generate key/value for the parsed result const rawEvent = proofBytes.subarray(128, eventEnd); const eventRootKey = this.generateEventRootKey(chainIdU32, this.clientType, blockNumber, txIndex, logIndex); const eventValueHash = Buffer.from(ethers.keccak256(rawEvent).slice(2), 'hex'); const merkleProofBytes = proofBytes.subarray(eventEnd); // Verify merkle proof using complete IAVL verification const merkleValid = this.verifyCompleteProof(proofBytes, eventEnd, chainIdU32, blockNumber, txIndex, logIndex); const valid = signatureValid && merkleValid; let error; if (!valid) { const errors = []; if (!signatureValid) errors.push("Signature validation failed"); if (!merkleValid) errors.push("Merkle proof validation failed"); error = errors.join('; '); } // Create parsed proof data const parsedProof = { appHash: appHash.toString('hex'), signature: { r: sigR.toString('hex'), s: sigS.toString('hex'), v: sigV }, chainId: chainIdU32, peptideHeight: peptideHeight.toString(), blockNumber: blockNumber.toString(), txIndex: txIndex, logIndex: logIndex, eventEnd: eventEnd, rawEvent: rawEvent.toString('hex'), merkleProof: merkleProofBytes.toString('hex'), eventRootKey: eventRootKey.toString('hex'), eventValueHash: eventValueHash.toString('hex') }; return { valid, signatureValid, merkleValid, error, parsedProof }; } catch (e) { const error = e; return { valid: false, signatureValid: false, merkleValid: false, error: `Proof verification failed: ${error.message}`, }; } } verifySignature(appHash, peptideHeight, sigR, sigS, sigV) { try { // Construct message hash using the same algorithm as Rust // keccak256(abi.encodePacked(appHash, peptideHeight)) const peptideHeightBuffer = Buffer.alloc(8); peptideHeightBuffer.writeBigUInt64BE(peptideHeight, 0); const innerHashData = Buffer.concat([ appHash, peptideHeightBuffer ]); const innerHash = ethers.keccak256(innerHashData); // keccak256(bytes.concat(bytes32(0), CHAIN_ID, innerHash)) const chainIdBytes32 = Buffer.alloc(32); chainIdBytes32.writeUInt32BE(this.chainId, 28); // Write as big-endian uint32 in the last 4 bytes const messageHashData = Buffer.concat([ Buffer.alloc(32), // bytes32(0) chainIdBytes32, // CHAIN_ID as bytes32 Buffer.from(innerHash.slice(2), 'hex') // Remove 0x prefix ]); const messageHash = ethers.keccak256(messageHashData); // Recover the public key const signature = { r: '0x' + sigR.toString('hex'), s: '0x' + sigS.toString('hex'), v: sigV }; const recoveredAddress = ethers.recoverAddress(messageHash, signature); return recoveredAddress.toLowerCase() === this.validationKey; } catch (e) { return false; } } /** * Verifies IAVL merkle tree membership proof * Based on the Solidity verifyMembership function from CrossL2ProverV2 */ verifyIAVLMembership(root, key, value, proof) { try { if (proof.length < 2) { return false; } const numPaths = proof[0]; const path0start = proof[1]; if (path0start > proof.length) { return false; } // Initial hash computation: sha256(prefix + key + 0x20 + sha256(value)) const valueHash = createHash('sha256').update(value).digest(); const prefix = proof.subarray(2, path0start); const hex20 = Buffer.from([0x20]); // hex"20" let prehash = createHash('sha256') .update(Buffer.concat([prefix, key, hex20, valueHash])) .digest(); let offset = path0start; // Iterate through each path in the proof for (let i = 0; i < numPaths; i++) { if (offset + 2 > proof.length) { return false; } const suffixStart = proof[offset]; const suffixEnd = proof[offset + 1]; if (offset + suffixEnd > proof.length) { return false; } // sha256(pathPrefix + prehash + pathSuffix) const pathPrefix = proof.subarray(offset + 2, offset + suffixStart); const pathSuffix = proof.subarray(offset + suffixStart, offset + suffixEnd); prehash = createHash('sha256') .update(Buffer.concat([pathPrefix, prehash, pathSuffix])) .digest(); // CRITICAL: This must match the Solidity logic exactly offset = offset + suffixEnd; } // Verify final hash matches root const matches = prehash.equals(root); return matches; } catch (e) { return false; } } /** * Complete proof verification that extracts key, value, and performs IAVL verification * Based on the validateEvent function from CrossL2ProverV2 */ verifyCompleteProof(proofBytes, eventEnd, chainId, blockNumber, txIndex, logIndex) { try { // Extract root from first 32 bytes (appHash from the proof structure) const root = proofBytes.subarray(0, 32); // Extract raw event data (from eventEnd backwards to find the event) const rawEvent = proofBytes.subarray(128, eventEnd); // Event data after fixed header // Generate the key using the event root key algorithm // This should match ReceiptParser.eventRootKey(chainId, clientType, blockNumber, txIndex, logIndex) const key = this.generateEventRootKey(chainId, this.clientType, blockNumber, txIndex, logIndex); // Generate the value by hashing the raw event data with keccak256 (matches Solidity) const value = Buffer.from(ethers.keccak256(rawEvent).slice(2), 'hex'); // Extract the IAVL proof data after eventEnd const iavlProof = proofBytes.subarray(eventEnd); if (iavlProof.length === 0) { return false; } // Perform full IAVL membership verification // The appHash (root) should match the computed root from the IAVL proof return this.verifyIAVLMembership(root, key, value, iavlProof); } catch (e) { return false; } } /** * Generate event root key based on chain ID, client type, block number, tx index, and log index * This implements the exact key generation logic from ReceiptParser.eventRootKey */ generateEventRootKey(chainId, clientType, blockNumber, txIndex, logIndex) { // Exact implementation from ReceiptParser.eventRootKey: // abi.encodePacked( // "chain/", // Strings.toString(uint256(chainId)), // "/storedLogs/", // clientType, // "/", // Strings.toString(height), // "/", // Strings.toString(receiptIndex), // "/", // Strings.toString(logIndex) // ); const key = `chain/${chainId}/storedLogs/${clientType}/${blockNumber}/${txIndex}/${logIndex}`; return Buffer.from(key, 'utf8'); } } // Helper functions for reading big-endian integers function readUInt16BE(buffer) { return buffer.readUInt16BE(0); } function readUInt32BE(buffer) { return buffer.readUInt32BE(0); } function readUInt64BE(buffer) { const high = buffer.readUInt32BE(0); const low = buffer.readUInt32BE(4); return (BigInt(high) << 32n) | BigInt(low); }