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",