UNPKG

@solarity/zkit

Version:
267 lines (228 loc) 9.57 kB
import fs from "fs"; import path from "path"; import * as snarkjs from "snarkjs"; import { createHash } from "crypto"; import { ArtifactsFileType, CircuitZKitConfig, VerifierLanguageType, Signals, CalldataByProtocol, IProtocolImplementer, ProofStructByProtocol, ProvingSystemType, NumberLike, } from "../types"; import { MAX_FILE_NAME_LENGTH } from "../constants"; import { getTmpDir, modifyWitnessArray, checkWitnessOverrides, writeWitnessFile } from "../utils"; /** * `CircuitZKit` represents a single circuit and provides a high-level API to work with it. */ export class CircuitZKit<Type extends ProvingSystemType> { constructor( private readonly _config: CircuitZKitConfig, private readonly _implementer: IProtocolImplementer<Type>, ) {} /** * Creates a verifier contract for the specified contract language with optional name suffix. * For more details regarding the structure of the contract verifier name, see {@link getVerifierName} description. * * In case the length of the verifier filename exceeds the {@link MAX_FILE_NAME_LENGTH}, * the `verifierNameSuffix` will be replaced by the first four bytes of its `sha1` hash. * * If no suffix was passed, but the verifier's filename still exceeds {@link MAX_FILE_NAME_LENGTH}, an error will be thrown. * * @param {VerifierLanguageType} languageExtension - The verifier contract language extension. * @param {string} verifierNameSuffix - The optional verifier name suffix. */ public async createVerifier(languageExtension: VerifierLanguageType, verifierNameSuffix?: string): Promise<void> { const vKeyFilePath: string = this.mustGetArtifactsFilePath("vkey"); let verifierFileName: string = `${this.getVerifierName(verifierNameSuffix)}.${languageExtension}`; if (verifierFileName.length >= MAX_FILE_NAME_LENGTH) { const modifiedSuffix: string = verifierNameSuffix ? `_0x${createHash("sha1").update(verifierNameSuffix).digest("hex").slice(0, 8)}_` : ""; verifierFileName = `${this.getVerifierName(modifiedSuffix)}.${languageExtension}`; if (verifierFileName.length >= MAX_FILE_NAME_LENGTH) { throw new Error(`Verifier file name "${verifierFileName}" exceeds the maximum file name length`); } } const verifierFilePath = path.join(this._config.verifierDirPath, verifierFileName); await this._implementer.createVerifier(vKeyFilePath, verifierFilePath, languageExtension); } /** * Calculates a witness for the given inputs. * * If `witnessOverrides` are provided, the corresponding witness values will be substituted in the result. * * Signal names in `witnessOverrides` must be provided in their full form as represented in the `.sym` file, e.g., * `main.signal`, `main.component.signal`, or `main.component.signal[n][m]`. * * @param {Signals} inputs - The inputs for the circuit. * @param {Record<string, bigint>} [witnessOverrides] - Optional map of signal names to override their witness values. * @returns {Promise<bigint[]>} The generated witness. */ public async calculateWitness(inputs: Signals, witnessOverrides?: Record<string, bigint>): Promise<bigint[]> { const wtnsFile = this.getTemporaryWitnessPath(); const wasmFile = this.mustGetArtifactsFilePath("wasm"); let signalIndexes: Record<string, NumberLike> = {}; if (witnessOverrides) { const symFile = this.mustGetArtifactsFilePath("sym"); signalIndexes = await checkWitnessOverrides(symFile, witnessOverrides); } await snarkjs.wtns.calculate(inputs, wasmFile, wtnsFile); const wtnsJson = (await snarkjs.wtns.exportJson(wtnsFile)) as bigint[]; return witnessOverrides ? modifyWitnessArray(wtnsJson, signalIndexes, witnessOverrides) : wtnsJson; } /** * Generates a proof for the given inputs. * * @dev The `inputs` should be in the same order as the circuit expects them. * * If `witnessOverrides` are provided, the witness will be calculated from the inputs and overridden accordingly. * Otherwise, a standard witness will be calculated and used. * * Signal names in `witnessOverrides` must be provided in their full form as represented in the `.sym` file, e.g., * `main.signal`, `main.component.signal`, or `main.component.signal[n][m]`. * * @param {Signals} inputs - The inputs for the circuit. * @param {Record<string, bigint>} [witnessOverrides] - Optional map of signal names to override their witness values. * @returns {Promise<ProofStructByProtocol<Type>>} The generated proof. */ public async generateProof( inputs: Signals, witnessOverrides?: Record<string, bigint>, ): Promise<ProofStructByProtocol<Type>> { const zKeyFile = this.mustGetArtifactsFilePath("zkey"); const witnessFile = this.getTemporaryWitnessPath(); let proof: ProofStructByProtocol<Type>; try { const witness = await this.calculateWitness(inputs, witnessOverrides); if (witnessOverrides) { await writeWitnessFile(witnessFile, witness); } proof = await this._implementer.generateProof(zKeyFile, witnessFile); } finally { if (fs.existsSync(witnessFile)) { fs.rmSync(witnessFile); } } return proof; } /** * Verifies the given proof. * * @dev The `proof` can be generated using the `generateProof` method. * @dev The `proof.publicSignals` should be in the same order as the circuit expects them. * * @param {ProofStructByProtocol<Type>} proof - The proof to verify. * @returns {Promise<boolean>} Whether the proof is valid. */ public async verifyProof(proof: ProofStructByProtocol<Type>): Promise<boolean> { const vKeyFile = this.mustGetArtifactsFilePath("vkey"); return this._implementer.verifyProof(proof, vKeyFile); } /** * Generates the calldata for the given proof. The calldata can be used to verify the proof on-chain. * * @param {ProofStructByProtocol<Type>} proof - The proof to generate calldata for. * @returns {Promise<CalldataByProtocol<Type>>} - The generated calldata. */ public async generateCalldata(proof: ProofStructByProtocol<Type>): Promise<CalldataByProtocol<Type>> { return await this._implementer.generateCalldata(proof); } /** * Returns the circuit name. The circuit name is the name of the circuit file without the extension. * * @returns {string} The circuit name. */ public getCircuitName(): string { return this._config.circuitName; } /** * Returns the verifier name. The verifier name has the next structure: * `<template name><suffix><proving system>Verifier.<extension>`. * * @param {string} verifierNameSuffix - The optional verifier name suffix. * * @returns {string} The verifier name. */ public getVerifierName(verifierNameSuffix?: string): string { return this._implementer.getVerifierName(this._config.circuitName, verifierNameSuffix); } /** * Returns the type of the proving protocol * * @returns {ProvingSystemType} The protocol proving system type. */ public getProvingSystemType(): ProvingSystemType { return this._implementer.getProvingSystemType(); } /** * Returns the Solidity verifier template. * * @returns {string} The Solidity verifier template. */ public getVerifierTemplate(languageExtension: VerifierLanguageType): string { return this._implementer.getTemplate(languageExtension); } /** * Returns the path to the temporary witness file. * * The file is stored in the system temporary directory and is named after the circuit. * This file is used for intermediate witness generation and may be deleted after usage. * * @returns {string} The full path to the temporary `.wtns` file. */ public getTemporaryWitnessPath(): string { return path.join(getTmpDir(), `${this.getCircuitName()}.wtns`); } /** * Returns the path to the file of the given type inside artifacts directory. Throws an error if the file doesn't exist. * * @param {ArtifactsFileType} fileType - The type of the file. * @returns {string} The path to the file. */ public mustGetArtifactsFilePath(fileType: ArtifactsFileType): string { const file = this.getArtifactsFilePath(fileType); if (!fs.existsSync(file)) { throw new Error(`Expected the file "${file}" to exist`); } return file; } /** * Returns the path to the file of the given type inside artifacts directory. * * @param {ArtifactsFileType} fileType - The type of the file. * @returns {string} The path to the file. */ public getArtifactsFilePath(fileType: ArtifactsFileType): string { const circuitName = this.getCircuitName(); let fileName: string; let fileDir: string = this._config.circuitArtifactsPath; switch (fileType) { case "r1cs": fileName = `${circuitName}.r1cs`; break; case "zkey": fileName = `${this._implementer.getZKeyFileName(circuitName)}`; break; case "vkey": fileName = `${this._implementer.getVKeyFileName(circuitName)}`; break; case "sym": fileName = `${circuitName}.sym`; break; case "json": fileName = `${circuitName}_constraints.json`; break; case "wasm": fileName = `${circuitName}.wasm`; fileDir = path.join(fileDir, `${circuitName}_js`); break; default: throw new Error(`Ambiguous file type: ${fileType}.`); } return path.join(fileDir, fileName); } }