diff --git a/README.md b/README.md index 9480b8a..9515744 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,143 @@ -# @solarity/zkit +[![npm](https://img.shields.io/npm/v/@solarity/zkit.svg)](https://www.npmjs.com/package/@solarity/zkit) -To install dependencies: +# ZKit + +A zero knowledge kit that helps you develop circuits using Circom. + +## Installation + +To install the package, run: ```bash -npm install +npm install --save-dev @solarity/zkit ``` -To run tests: +## Usage -```bash -npm run test +ZKit is an S-tier Circom assistant: + +- Compile and interact with circuits without snarkjs hassle +- Generate and verify ZK proofs with a single line of code +- Render optimized Solidity verifiers +- Forget about native dependencies - everything is in TypeScript + +### CircomZKit + +ZKit is a configless package, which means you don't need to provide any configuration to use it. + +Suppose you have the following circuit: + +```circom +pragma circom 2.1.6; + +template Multiplier() { + signal input a; + signal input b; + signal output out; + out <== a * b; +} + +component main = Multiplier(); +``` + +You can start work with it as follows: + +```typescript +import { CircomZKit } from "@solarity/zkit"; + +async function main() { + const zkit = new CircomZKit(); + + const multiplier = zkit.getCircuit("Multiplier"); + + /// Generates artifacts in the "./zkit-artifacts" directory + await multiplier.compile(); +} + +main() + .catch((err) => { + process.exit(1); + }); +``` + +By default, ZKit will look for the circuit file in the `./circuits` directory. However, you can change this by providing a custom one: + +```typescript +new CircomZKit({ circuitsDir: "./my-circuits" }); +``` + +To generate zkey, the power-of-tau file is required. ZKit automatically downloads those files from [Hermes](https://hermez.s3-eu-west-1.amazonaws.com/) to the `${HOME}/.zkit/.ptau` directory, so you don't need to re-download them every time you start a new project. + +You can also provide a custom path to the directory where the power-of-tau files are stored: + +```typescript +new CircomZKit({ ptauDir: "./my-ptau" }); +``` + +> [!NOTE] +> Note that all the files in the `ptauDir` directory must have the `powers-of-tau-{x}.ptau` name format, where `{x}` is a maximum degree (2x) of constraints a `ptau` supports. + +ZKit may also ask you for the permission to download the power-of-tau files. You can enable this by toggling off the `allowDownload` option: + +```typescript +new CircomZKit({ allowDownload: false }); ``` + +### CircuitZKit + +Once you created a `CircuitZKit` instance using the `getCircuit` method, you can manage the underlying circuit using the following methods: + +#### compile() + +Compiles the circuit and generates the artifacts in the `./zkit-artifacts` or in the provided `artifactsDir` directory. The default output is `r1cs`, `zkey` and `vkey` files. + +```typescript +await multiplier.compile(); +``` + +#### createVerifier() + +Creates Solidity verifier contract in the `./contracts/verifiers` or in the provided `verifiersDir` directory. + +> [!NOTE] +> You should first compile the circuit before creating the verifier. + +```typescript +await multiplier.createVerifier(); +``` + +#### generateProof() + +Generates a proof for the given inputs. + +> [!NOTE] +> You should first compile the circuit before generating the proof. + +```typescript +/// { proof: { pi_a, pi_b, pi_c, protocol, curve }, publicSignals: [6] } +const proof = await multiplier.createVerifier({ a: 2, b: 3}); +``` + +#### verifyProof() + +Verifies the proof. + +```typescript +/// true +const isValidProof = await multiplier.verifyProof(proof); +``` + +#### generateCalldata() + +Generates calldata by proof for the Solidity verifier's `verifyProof` method. + +```typescript +/// You can use this calldata to call the verifier contract +const calldata = await multiplier.verifyProof(proof); +``` + +## Known limitations + +- Currently, ZKit supports only the Groth16 proving system. +- Zkey generation doesn't allow additional contributions. +- The `compile` method may cause [issues](https://github.com/iden3/snarkjs/issues/494). diff --git a/package.json b/package.json index 6b09c95..06bd7c7 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "build": "tsc", "test": "jest --forceExit", "lint-fix": "prettier --write 'src/**/*.ts'", - "publish-to-npm": "npm run build && rm -rf dist/templates && cp -rf src/templates dist/templates && npm publish ./ --access public" + "publish-to-npm": "npm run build && rm -rf dist/core/templates && cp -rf src/core/templates dist/core/templates && npm publish ./ --access public" }, "dependencies": { "@distributedlab/circom2": "0.2.18-rc.2", diff --git a/src/ManagerZKit.ts b/src/ManagerZKit.ts deleted file mode 100644 index d6b7c9d..0000000 --- a/src/ManagerZKit.ts +++ /dev/null @@ -1,197 +0,0 @@ -import fs from "fs"; -import https from "https"; -import os from "os"; -import path from "path"; -import process from "process"; -import * as readline from "readline"; -import { v4 as uuid } from "uuid"; - -import { ManagerZKitConfig, ManagerZKitPrivateConfig, TemplateType } from "./types"; -import { defaultManagerOptions } from "./defaults"; - -export class ManagerZKit { - private _config: ManagerZKitPrivateConfig; - - constructor(config: Partial = defaultManagerOptions) { - const overriddenConfig = { - ...defaultManagerOptions, - ...config, - } as ManagerZKitConfig; - - const projectRoot = process.cwd(); - - const isGlobalPtau = !overriddenConfig.ptauFile; - - if (!isGlobalPtau && path.extname(overriddenConfig.ptauFile!) != ".ptau") { - throw new Error('Ptau file must have ".ptau" extension.'); - } - - const tempDir = path.join(os.tmpdir(), ".zkit"); - const ptauPath = isGlobalPtau - ? path.join(os.homedir(), ".zkit", ".ptau") - : path.join(projectRoot, overriddenConfig.ptauFile!); - - this._config = { - circuitsDir: path.join(projectRoot, overriddenConfig.circuitsDir), - artifactsDir: path.join(projectRoot, overriddenConfig.artifactsDir), - verifiersDir: path.join(projectRoot, overriddenConfig.verifiersDir), - tempDir, - ptau: { - isGlobal: isGlobalPtau, - path: ptauPath, - }, - compiler: fs.readFileSync(require.resolve("@distributedlab/circom2/circom.wasm")), - templates: { - groth16: fs.readFileSync(path.join(__dirname, "templates", "verifier_groth16.sol.ejs"), "utf8"), - }, - }; - } - - public async fetchPtauFile(minConstraints: number): Promise { - if (this._config.ptau.isGlobal) { - return await this._fetchGlobalPtau(minConstraints); - } - - return this._fetchLocalPtau(); - } - - public getArtifactsDir(): string { - return this._config.artifactsDir; - } - - public getCircuitsDir(): string { - return this._config.circuitsDir; - } - - public getVerifiersDir(): string { - return this._config.verifiersDir; - } - - public getPtauPath(): string { - return this._config.ptau.path; - } - - public getCompiler(): string { - return this._config.compiler; - } - - public getTempDir(): string { - return path.join(this._config.tempDir, uuid()); - } - - public getTemplate(templateType: TemplateType): string { - switch (templateType) { - case "groth16": - return this._config.templates.groth16; - default: - throw new Error(`Ambiguous template type: ${templateType}.`); - } - } - - private async _fetchGlobalPtau(minConstraints: number): Promise { - const ptauId = Math.max(Math.ceil(Math.log2(minConstraints)), 8); - - if (ptauId > 20) { - throw new Error( - 'Circuit has too many constraints. The maximum number of constraints is 2^20. Consider passing "ptau=PATH_TO_FILE".', - ); - } - - const ptauInfo = this._searchGlobalPtau(ptauId); - - if (ptauInfo.url) { - if (!(await this._askForDownloadAllowance(ptauInfo.url))) { - throw new Error('Download is cancelled. Allow download or consider passing "ptauFile=PATH_TO_FILE"'); - } - - fs.mkdirSync(this._config.ptau.path, { recursive: true }); - - await this._downloadPtau(ptauInfo.file, ptauInfo.url); - } - - return ptauInfo.file; - } - - private _fetchLocalPtau(): string { - if (!fs.existsSync(this._config.ptau.path)) { - throw new Error(`Ptau file "${this._config.ptau.path}" doesn't exist.`); - } - - return this._config.ptau.path; - } - - private _searchGlobalPtau(ptauId: number): { file: string; url: string | null } { - let entries = [] as fs.Dirent[]; - - if (fs.existsSync(this._config.ptau.path)) { - entries = fs.readdirSync(this._config.ptau.path, { withFileTypes: true }); - } - - const entry = entries.find((entry) => { - if (!entry.isFile()) { - return false; - } - - const match = entry.name.match(/^powers-of-tau-(\d+)\.ptau$/); - - if (!match) { - return false; - } - - const entryPtauId = parseInt(match[1]); - - return ptauId <= entryPtauId; - }); - - const file = path.join(this._config.ptau.path, entry ? entry.name : `powers-of-tau-${ptauId}.ptau`); - const url = (() => { - if (entry) { - return null; - } - - if (ptauId < 10) { - return `https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_0${ptauId}.ptau`; - } - - return `https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_${ptauId}.ptau`; - })(); - - return { file, url }; - } - - private async _downloadPtau(file: string, url: string): Promise { - const ptauFileStream = fs.createWriteStream(file); - - return new Promise((resolve, reject) => { - const request = https.get(url, (response) => { - response.pipe(ptauFileStream); - }); - - ptauFileStream.on("finish", () => resolve(true)); - - request.on("error", (err) => { - fs.unlink(file, () => reject(err)); - }); - - ptauFileStream.on("error", (err) => { - fs.unlink(file, () => reject(err)); - }); - - request.end(); - }); - } - - private _askForDownloadAllowance(url: string): Promise { - return new Promise((resolve) => { - const readLine = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - readLine.question(`No ptau found. Press [Y] to download it from "${url}": `, (response) => { - readLine.close(); - resolve(response.toUpperCase() == "Y"); - }); - }); - } -} diff --git a/src/config/config.ts b/src/config/config.ts new file mode 100644 index 0000000..34b618b --- /dev/null +++ b/src/config/config.ts @@ -0,0 +1,37 @@ +const { Context } = require("@distributedlab/circom2"); + +export type ManagerZKitConfig = { + circuitsDir: string; + artifactsDir: string; + verifiersDir: string; + ptauDir: string; + allowDownload: boolean; +}; + +export const defaultManagerOptions: Partial = { + circuitsDir: "circuits", + artifactsDir: "zkit-artifacts", + verifiersDir: "contracts/verifiers", + allowDownload: true, +}; + +export type CompileOptions = { + sym: boolean; + json: boolean; + c: boolean; + quiet: boolean; +}; + +export const defaultCompileOptions: CompileOptions = { + sym: false, + json: false, + c: false, + quiet: false, +}; + +export type ManagerZKitPrivateConfig = ManagerZKitConfig & { + compiler: typeof Context; + templates: { + groth16: string; + }; +}; diff --git a/src/CircomZKit.ts b/src/core/CircomZKit.ts similarity index 54% rename from src/CircomZKit.ts rename to src/core/CircomZKit.ts index 5896417..41bcac7 100644 --- a/src/CircomZKit.ts +++ b/src/core/CircomZKit.ts @@ -3,16 +3,33 @@ import path from "path"; import { CircuitZKit } from "./CircuitZKit"; import { ManagerZKit } from "./ManagerZKit"; -import { CircuitInfo } from "./types"; -import { readDirRecursively } from "./utils"; +import { CircuitInfo } from "../types/types"; +import { readDirRecursively } from "../utils/utils"; +import { defaultManagerOptions, ManagerZKitConfig } from "../config/config"; +/** + * `CircomZKit` acts as a factory for `CircuitZKit` instances. + */ export class CircomZKit { private readonly _manager: ManagerZKit; - constructor(manager?: ManagerZKit) { - this._manager = manager ?? new ManagerZKit(); + /** + * Creates a new `CircomZKit` instance. + * + * @param {Partial} [options=defaultManagerOptions] - The configuration options to use. + */ + constructor(options: Partial = defaultManagerOptions) { + this._manager = new ManagerZKit({ ...defaultManagerOptions, ...options }); } + /** + * Returns a `CircuitZKit` instance for the specified circuit. + * + * @dev If the circuit id is not unique, the path to the circuit file must be provided. + * + * @param {string} circuit - The path to the circuit file or the circuit id (filename without extension). + * @returns {CircomZKit} The `CircuitZKit` instance. + */ public getCircuit(circuit: string): CircuitZKit { const circuits = this._getAllCircuits(); @@ -37,9 +54,16 @@ export class CircomZKit { ); } - return new CircuitZKit(this._manager, path.join(this._manager.getCircuitsDir(), candidates[0])); + return new CircuitZKit(path.join(this._manager.getCircuitsDir(), candidates[0]), this._manager); } + /** + * Returns an array of all circuits available in the circuits directory. + * + * @dev If a circuit id is not unique, the id will be set to `null`. + * + * @returns {CircuitInfo[]} An array of circuit information objects. + */ public getCircuits(): CircuitInfo[] { const circuits = this._getAllCircuits(); @@ -65,12 +89,17 @@ export class CircomZKit { return result; } + /** + * Returns an array of all circuits paths available in the circuits directory. + * + * @returns {string[]} An array of circuit paths. + */ private _getAllCircuits(): string[] { const circuitsDir = this._manager.getCircuitsDir(); let circuits = [] as string[]; - readDirRecursively(circuitsDir, (dir: string, file: string) => { + readDirRecursively(circuitsDir, (_dir: string, file: string) => { if (path.extname(file) == ".circom") { circuits.push(path.relative(circuitsDir, file)); } diff --git a/src/CircuitZKit.ts b/src/core/CircuitZKit.ts similarity index 63% rename from src/CircuitZKit.ts rename to src/core/CircuitZKit.ts index 3abd738..ecae079 100644 --- a/src/CircuitZKit.ts +++ b/src/core/CircuitZKit.ts @@ -3,19 +3,38 @@ import fs from "fs"; import path from "path"; import * as snarkjs from "snarkjs"; -import { defaultCompileOptions } from "./defaults"; +import { defaultCompileOptions, CompileOptions } from "../config/config"; import { ManagerZKit } from "./ManagerZKit"; -import { Calldata, CompileOptions, DirType, FileType, Inputs, ProofStruct } from "./types"; -import { readDirRecursively } from "./utils"; +import { Calldata, DirType, FileType, Inputs, ProofStruct } from "../types/types"; +import { readDirRecursively } from "../utils/utils"; const { CircomRunner, bindings } = require("@distributedlab/circom2"); +/** + * `CircuitZKit` represents a single circuit and provides a high-level API to work with it. + * + * @dev This class is not meant to be used directly. Use the `CircomZKit` to create its instance. + */ export class CircuitZKit { + /** + * Creates a new instance of `CircuitZKit`. + * + * @param {string} _circuit - The path to the circuit. + * @param {ManagerZKit} _manager - The manager that maintains the global state. + */ constructor( - private readonly _manager: ManagerZKit, private readonly _circuit: string, + private readonly _manager: ManagerZKit, ) {} + /** + * Compiles the circuit and generates the artifacts. + * + * @dev If compilation fails, the latest valid artifacts will be preserved. + * @dev Doesn't show the compilation error if `quiet` is set to `true`. + * + * @param {Partial} [options=defaultCompileOptions] - Compilation options. + */ public async compile(options: Partial = defaultCompileOptions): Promise { const tempDir = this._manager.getTempDir(); @@ -24,10 +43,7 @@ export class CircuitZKit { fs.mkdirSync(tempDir, { recursive: true }); - const overriddenOptions: CompileOptions = { - ...defaultCompileOptions, - ...options, - }; + const overriddenOptions: CompileOptions = { ...defaultCompileOptions, ...options }; await this._compile(overriddenOptions, tempDir); @@ -40,6 +56,9 @@ export class CircuitZKit { } } + /** + * Creates a verifier contract. + */ public async createVerifier(): Promise { const tempDir = this._manager.getTempDir(); @@ -66,6 +85,15 @@ export class CircuitZKit { } } + /** + * Generates a proof for the given inputs. + * + * @dev The `inputs` should be in the same order as the circuit expects them. + * + * @param {Inputs} inputs - The inputs for the circuit. + * @returns {Promise} The generated proof. + * @todo Add support for other proving systems. + */ public async generateProof(inputs: Inputs): Promise { const zKeyFile = this._mustGetFile("zkey"); const wasmFile = this._mustGetFile("wasm"); @@ -73,6 +101,15 @@ export class CircuitZKit { return (await snarkjs.groth16.fullProve(inputs, wasmFile, zKeyFile)) as ProofStruct; } + /** + * 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 {ProofStruct} proof - The proof to verify. + * @returns {Promise} Whether the proof is valid. + */ public async verifyProof(proof: ProofStruct): Promise { const vKeyFile = this._mustGetFile("vkey"); @@ -81,20 +118,43 @@ export class CircuitZKit { return await snarkjs.groth16.verify(verifier, proof.publicSignals, proof.proof); } + /** + * Generates the calldata for the given proof. The calldata can be used to verify the proof on-chain. + * + * @param {ProofStruct} proof - The proof to generate calldata for. + * @returns {Promise} - The generated calldata. + * @todo Add other types of calldata. + */ public async generateCalldata(proof: ProofStruct): Promise { const calldata = await snarkjs.groth16.exportSolidityCallData(proof.proof, proof.publicSignals); return JSON.parse(`[${calldata}]`) as Calldata; } + /** + * Returns the circuit ID. The circuit ID is the name of the circuit file without the extension. + * + * @returns {string} The circuit ID. + */ public getCircuitId(): string { return path.parse(this._circuit).name; } + /** + * Returns the verifier ID. The verifier ID is the name of the circuit file without the extension, suffixed with "Verifier". + * + * @returns {string} The verifier ID. + */ public getVerifierId(): string { return `${path.parse(this._circuit).name}Verifier`; } + /** + * Generates zero-knowledge key for the circuit. + * + * @param {string} outDir - The directory to save the generated key. + * @todo This method may cause issues https://github.com/iden3/snarkjs/issues/494 + */ private async _generateZKey(outDir: string): Promise { const r1csFile = this._getFile("r1cs", outDir); const zKeyFile = this._getFile("zkey", outDir); @@ -105,6 +165,11 @@ export class CircuitZKit { await snarkjs.zKey.newZKey(r1csFile, ptauFile, zKeyFile); } + /** + * Generates verification key for the circuit. + * + * @param {string} outDir - The directory to save the generated key. + */ private async _generateVKey(outDir: string): Promise { const zKeyFile = this._getFile("zkey", outDir); const vKeyFile = this._getFile("vkey", outDir); @@ -114,6 +179,13 @@ export class CircuitZKit { fs.writeFileSync(vKeyFile, JSON.stringify(vKeyData)); } + /** + * Returns the arguments to compile the circuit. + * + * @param {CompileOptions} options - Compilation options. + * @param {string} outDir - The directory to save the compiled artifacts. + * @returns {string[]} The arguments to compile the circuit. + */ private _getCompileArgs(options: CompileOptions, outDir: string): string[] { let args = [this._circuit, "--r1cs", "--wasm"]; @@ -126,6 +198,12 @@ export class CircuitZKit { return args; } + /** + * Compiles the circuit. + * + * @param {CompileOptions} options - Compilation options. + * @param {string} outDir - The directory to save the compiled artifacts. + */ private async _compile(options: CompileOptions, outDir: string): Promise { const args = this._getCompileArgs(options, outDir); @@ -143,6 +221,12 @@ export class CircuitZKit { } } + /** + * Returns the number of constraints in the circuit. This value is used to fetch the correct `ptau` file. + * + * @param {string} outDir - The directory where the compiled artifacts are saved. + * @returns {Promise} The number of constraints in the circuit. + */ async _getConstraints(outDir: string): Promise { const r1csFile = this._getFile("r1cs", outDir); @@ -177,6 +261,13 @@ export class CircuitZKit { throw new Error("Header section is not found."); } + /** + * Returns the path to the file of the given type. + * + * @param {FileType} fileType - The type of the file. + * @param {string | undefined} temp - The temporary directory to use. + * @returns {string} The path to the file. + */ private _getFile(fileType: FileType, temp?: string): string { const circuitId = this.getCircuitId(); @@ -200,6 +291,12 @@ export class CircuitZKit { } } + /** + * Returns the path to the directory of the given type. + * + * @param {DirType} dirType - The type of the directory. + * @returns {string} The path to the directory. + */ private _getDir(dirType: DirType): string { const circuitRelativePath = path.relative(this._manager.getCircuitsDir(), this._circuit); @@ -215,6 +312,13 @@ export class CircuitZKit { } } + /** + * Returns the path to the file of the given type. Throws an error if the file doesn't exist. + * + * @param {FileType} fileType - The type of the file. + * @param {string | undefined} temp - The temporary directory to use. + * @returns {string} The path to the file. + */ private _mustGetFile(fileType: FileType, temp?: string): string { const file = this._getFile(fileType, temp); @@ -225,7 +329,13 @@ export class CircuitZKit { return file; } - private _moveFromTempDirToOutDir(tempDir: string, outDir: string) { + /** + * Moves the files from the temporary directory to the output directory. + * + * @param {string} tempDir - The temporary directory. + * @param {string} outDir - The output directory. + */ + private _moveFromTempDirToOutDir(tempDir: string, outDir: string): void { fs.rmSync(outDir, { recursive: true, force: true }); fs.mkdirSync(outDir, { recursive: true }); @@ -241,6 +351,13 @@ export class CircuitZKit { }); } + /** + * Returns a new instance of `CircomRunner`. The `CircomRunner` is used to compile the circuit. + * + * @param {string[]} args - The arguments to run the `circom` compiler. + * @param {boolean} quiet - Whether to suppress the compilation error. + * @returns {typeof CircomRunner} The `CircomRunner` instance. + */ private _getCircomRunner(args: string[], quiet: boolean): typeof CircomRunner { return new CircomRunner({ args, diff --git a/src/core/ManagerZKit.ts b/src/core/ManagerZKit.ts new file mode 100644 index 0000000..8b1c705 --- /dev/null +++ b/src/core/ManagerZKit.ts @@ -0,0 +1,231 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import process from "process"; +import * as readline from "readline"; +import { v4 as uuid } from "uuid"; + +import { ManagerZKitConfig, ManagerZKitPrivateConfig, defaultManagerOptions } from "../config/config"; +import { PtauInfo, TemplateType } from "../types/types"; +import { downloadFile } from "../utils/utils"; + +/** + * `ManagerZKit` provides configuration options and utility methods used by the `CircomZKit` and `CircuitZKit` classes. + */ +export class ManagerZKit { + private _config: ManagerZKitPrivateConfig; + + /** + * Creates a new `ManagerZKit` instance. + * + * @param {Partial} [config=defaultManagerOptions] - The configuration options to use. + */ + constructor(config: Partial = defaultManagerOptions) { + const overriddenConfig = { ...defaultManagerOptions, ...config } as ManagerZKitConfig; + + overriddenConfig.circuitsDir = path.join(process.cwd(), overriddenConfig.circuitsDir); + overriddenConfig.artifactsDir = path.join(process.cwd(), overriddenConfig.artifactsDir); + overriddenConfig.verifiersDir = path.join(process.cwd(), overriddenConfig.verifiersDir); + + if (overriddenConfig.ptauDir) { + overriddenConfig.ptauDir = path.join(process.cwd(), overriddenConfig.ptauDir); + } else { + overriddenConfig.ptauDir = path.join(os.homedir(), ".zkit", ".ptau"); + } + + this._config = { + ...overriddenConfig, + compiler: fs.readFileSync(require.resolve("@distributedlab/circom2/circom.wasm")), + templates: { + groth16: fs.readFileSync(path.join(__dirname, "templates", "verifier_groth16.sol.ejs"), "utf8"), + }, + }; + } + + /** + * Fetches the `ptau` file. + * + * @dev If `ptau` file is not found, this method will try to download it. Use `allowDownload=false` to disable this behavior. + * + * @param {number} minConstraints - The minimum number of constraints the `ptau` file must support. + * @returns {Promise} The path to the `ptau` file. + */ + public async fetchPtauFile(minConstraints: number): Promise { + const ptauId = Math.max(Math.ceil(Math.log2(minConstraints)), 8); + + if (ptauId > 20) { + throw new Error( + 'Circuit has too many constraints. The maximum number of constraints is 2^20. Consider passing "ptauDir=PATH_TO_LOCAL_DIR".', + ); + } + + const ptauInfo = this._searchPtau(ptauId); + + if (ptauInfo.url) { + await this._downloadPtau(ptauInfo); + } + + return ptauInfo.file; + } + + /** + * Returns the path to the artifacts' directory. + * + * @returns {string} The path to the artifacts' directory. + */ + public getArtifactsDir(): string { + return this._config.artifactsDir; + } + + /** + * Returns the path to the circuits' directory. + * + * @returns {string} The path to the circuits' directory. + */ + public getCircuitsDir(): string { + return this._config.circuitsDir; + } + + /** + * Returns the path to the verifiers' directory. + * + * @returns {string} The path to the verifiers' directory. + */ + public getVerifiersDir(): string { + return this._config.verifiersDir; + } + + /** + * Returns the path to the `ptau` directory. + * + * @dev The default `ptau` directory is located at `${HOME}/.zkit/.ptau`. + * + * @returns {string} The path to the `ptau` directory. + */ + public getPtauDir(): string { + return this._config.ptauDir; + } + + /** + * Returns a temporary directory path. + * + * @dev Temporary files are stored in the OS's temporary directory. + * + * @returns {string} A temporary directory path. + */ + public getTempDir(): string { + return path.join(os.tmpdir(), ".zkit", uuid()); + } + + /** + * Returns the circom compiler's wasm binary. + * + * @returns {string} The circom compiler's wasm binary. + */ + public getCompiler(): string { + return this._config.compiler; + } + + /** + * Returns the Solidity verifier template for the specified proving system. + * + * @param {TemplateType} templateType - The template type. + * @returns {string} The Solidity verifier template. + */ + public getTemplate(templateType: TemplateType): string { + switch (templateType) { + case "groth16": + return this._config.templates.groth16; + default: + throw new Error(`Ambiguous template type: ${templateType}.`); + } + } + + /** + * Returns whether the download of the `ptau` file is allowed. + * + * @returns {boolean} Whether the download of the `ptau` file is allowed. + */ + public getAllowDownload(): boolean { + return this._config.allowDownload; + } + + /** + * Downloads the `ptau` file. The download is allowed only if the user confirms it. + * + * @param {PtauInfo} ptauInfo - The `ptau` file and download url. + */ + private async _downloadPtau(ptauInfo: PtauInfo): Promise { + if (!this.getAllowDownload() && !(await this._askForDownloadAllowance(ptauInfo))) { + throw new Error( + 'Download is cancelled. Allow download or consider passing "ptauDir=PATH_TO_LOCAL_DIR" to the existing ptau files', + ); + } + + fs.mkdirSync(this.getPtauDir(), { recursive: true }); + + if (!(await downloadFile(ptauInfo.file, ptauInfo.url!))) { + throw new Error("Something went wrong while downloading the ptau file."); + } + } + + /** + * Searches for the `ptau` file that supports the specified number of constraints. + * + * @param {number} ptauId - The `ptau` file id. + * @returns {PtauInfo} The `ptau` file path and download url if the file doesn't exist. + */ + private _searchPtau(ptauId: number): PtauInfo { + let entries = [] as fs.Dirent[]; + + if (fs.existsSync(this.getPtauDir())) { + entries = fs.readdirSync(this.getPtauDir(), { withFileTypes: true }); + } + + const entry = entries.find((entry) => { + if (!entry.isFile()) { + return false; + } + + const match = entry.name.match(/^powers-of-tau-(\d+)\.ptau$/); + + if (!match) { + return false; + } + + const entryPtauId = parseInt(match[1]); + + return ptauId <= entryPtauId; + }); + + const file = path.join(this.getPtauDir(), entry ? entry.name : `powers-of-tau-${ptauId}.ptau`); + const url = entry + ? null + : `https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_${ptauId.toString().padStart(2, "0")}.ptau`; + + return { file, url }; + } + + /** + * Prompts the user to allow the download of the `ptau` file. + * + * @param {PtauInfo} ptauInfo - The `ptau` file and download url. + * @returns {Promise} Whether the download is allowed. + */ + private _askForDownloadAllowance(ptauInfo: PtauInfo): Promise { + return new Promise((resolve) => { + const readLine = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + readLine.question( + `No ptau found. Press [Y] to download it from "${ptauInfo.url!}" to ${ptauInfo.file}: `, + (response) => { + readLine.close(); + resolve(response.toUpperCase() == "Y"); + }, + ); + }); + } +} diff --git a/src/templates/verifier_groth16.sol.ejs b/src/core/templates/verifier_groth16.sol.ejs similarity index 100% rename from src/templates/verifier_groth16.sol.ejs rename to src/core/templates/verifier_groth16.sol.ejs diff --git a/src/defaults.ts b/src/defaults.ts deleted file mode 100644 index 7d02d28..0000000 --- a/src/defaults.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CompileOptions, ManagerZKitConfig } from "./types"; - -export const defaultManagerOptions: Partial = { - circuitsDir: "circuits", - artifactsDir: "zkit-artifacts", - verifiersDir: "contracts/verifiers", -}; - -export const defaultCompileOptions: CompileOptions = { - sym: false, - json: false, - c: false, - quiet: false, -}; diff --git a/src/index.ts b/src/index.ts index 2402bb0..a3197aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,7 @@ -export * from "./CircomZKit"; -export * from "./CircuitZKit"; -export * from "./ManagerZKit"; +export * from "./core/CircomZKit"; +export * from "./core/CircuitZKit"; +export * from "./core/ManagerZKit"; -export { - NumericString, - PublicSignals, - Groth16Proof, - Calldata, - ProofStruct, - Inputs, - ManagerZKitConfig, - CompileOptions, - CircuitInfo, -} from "./types"; +export { NumericString, PublicSignals, Groth16Proof, Calldata, ProofStruct, Inputs, CircuitInfo } from "./types/types"; -export * from "./defaults"; +export { CompileOptions, ManagerZKitConfig, defaultCompileOptions, defaultManagerOptions } from "./config/config"; diff --git a/src/types.ts b/src/types/types.ts similarity index 65% rename from src/types.ts rename to src/types/types.ts index 312b323..4a3264a 100644 --- a/src/types.ts +++ b/src/types/types.ts @@ -1,5 +1,3 @@ -const { Context } = require("@distributedlab/circom2"); - export type NumericString = `${number}` | string; export type PublicSignals = NumericString[]; @@ -30,35 +28,6 @@ export type InputLike = NumberLike | ArrayLike; export type Inputs = Record; -export type ManagerZKitConfig = { - circuitsDir: string; - artifactsDir: string; - verifiersDir: string; - ptauFile: string; -}; - -export type ManagerZKitPrivateConfig = { - circuitsDir: string; - artifactsDir: string; - verifiersDir: string; - tempDir: string; - ptau: { - isGlobal: boolean; - path: string; - }; - compiler: typeof Context; - templates: { - groth16: string; - }; -}; - -export type CompileOptions = { - sym: boolean; - json: boolean; - c: boolean; - quiet: boolean; -}; - export type CircuitInfo = { path: string; id: string | null; @@ -67,3 +36,8 @@ export type CircuitInfo = { export type FileType = "r1cs" | "zkey" | "vkey" | "sym" | "json" | "wasm" | "sol"; export type DirType = "circuit" | "artifact" | "verifier"; export type TemplateType = "groth16"; + +export type PtauInfo = { + file: string; + url: string | null; +}; diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index cbc1e4d..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import fs from "fs"; -import path from "path"; - -/// @dev After Node.js v20 `recursive` flag can be passed to `fs.readdir` -export function readDirRecursively(dir: string, callback: (dir: string, file: string) => void): void { - if (!fs.existsSync(dir)) { - return; - } - - const entries = fs.readdirSync(dir, { withFileTypes: true }); - - for (const entry of entries) { - const entryPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - readDirRecursively(entryPath, callback); - } - - if (entry.isFile()) { - callback(dir, entryPath); - } - } -} diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..0925296 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,60 @@ +import fs from "fs"; +import path from "path"; +import https from "https"; + +/** + * Reads a directory recursively and calls the callback for each file. + * + * @dev After Node.js 20.0.0 the `recursive` option is available. + * + * @param {string} dir - The directory to read. + * @param {(dir: string, file: string) => void} callback - The callback function. + */ +export function readDirRecursively(dir: string, callback: (dir: string, file: string) => void): void { + if (!fs.existsSync(dir)) { + return; + } + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const entryPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + readDirRecursively(entryPath, callback); + } + + if (entry.isFile()) { + callback(dir, entryPath); + } + } +} + +/** + * Downloads a file from the specified URL. + * + * @param {string} file - The path to save the file to. + * @param {string} url - The URL to download the file from. + * @returns {Promise} Whether the file was downloaded successfully. + */ +export async function downloadFile(file: string, url: string): Promise { + const fileStream = fs.createWriteStream(file); + + return new Promise((resolve, reject) => { + const request = https.get(url, (response) => { + response.pipe(fileStream); + }); + + fileStream.on("finish", () => resolve(true)); + + request.on("error", (err) => { + fs.unlink(file, () => reject(err)); + }); + + fileStream.on("error", (err) => { + fs.unlink(file, () => reject(err)); + }); + + request.end(); + }); +} diff --git a/test/CircomZKit.test.ts b/test/CircomZKit.test.ts index 80b1af7..c91302c 100644 --- a/test/CircomZKit.test.ts +++ b/test/CircomZKit.test.ts @@ -1,4 +1,4 @@ -import { CircomZKit, ManagerZKit } from "../src"; +import { CircomZKit } from "../src"; jest.mock("readline", () => ({ createInterface: jest.fn().mockReturnValue({ @@ -13,14 +13,14 @@ jest.mock("readline", () => ({ describe("happy flow", function () { test("happy flow", async () => { - const manager = new ManagerZKit({ + const circom = new CircomZKit({ circuitsDir: "test/circuits", artifactsDir: "test/zkit-artifacts", - verifiersDir: "test/verifiers" + verifiersDir: "test/verifiers", + ptauDir: "test/ptau", + allowDownload: false, }); - const circom = new CircomZKit(manager); - console.log(circom.getCircuits()); const circuit = circom.getCircuit("Addition"); diff --git a/tsconfig.json b/tsconfig.json index bc6b971..c16c188 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "skipDefaultLibCheck": true, "skipLibCheck": true, "outDir": "dist", - "experimentalDecorators": true + "experimentalDecorators": true, }, "exclude": [ "dist",