@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
JavaScript
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);
}