init added

This commit is contained in:
erhant
2023-06-03 16:08:09 +03:00
parent d2098431c3
commit e553efed7e
9 changed files with 342 additions and 91 deletions

10
.npmignore Normal file
View File

@@ -0,0 +1,10 @@
ptau
node_modules
scripts
circuits
build
.github
.vscode
inputs
src
tests

View File

@@ -1,5 +1,3 @@
> Work in progress...
<p align="center">
<h1 align="center">
Circomkit
@@ -10,6 +8,9 @@
<p align="center">
<a href="https://opensource.org/licenses/MIT" target="_blank">
<img src="https://img.shields.io/badge/license-MIT-yellow.svg">
</a>
<a href="https://www.npmjs.com/package/circomkit" target="_blank">
<img alt="NPM" src="https://img.shields.io/npm/v/circomkit?logo=npm&color=CB3837">
</a>
<a href="./.github/workflows/styles.yml" target="_blank">
<img alt="Workflow: Styles" src="https://github.com/erhant/circomkit/actions/workflows/styles.yml/badge.svg?branch=main">

View File

@@ -13,7 +13,7 @@
"bin": "dist/bin/index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"homepage": "https://github.com/erhant/circomkit",
"homepage": "https://github.com/erhant/circomkit#readme",
"repository": {
"type": "git",
"url": "https://github.com/erhant/circomkit.git"
@@ -66,7 +66,18 @@
"ejs": "^3.1.9"
},
"peerDependencies": {
"chai": "^4.3.7",
"snarkjs": "^0.6.0"
}
"snarkjs": "^0.6.0",
"chai": "^4.3.7"
},
"keywords": [
"circom",
"zk",
"zero knowledge",
"snarkjs",
"typescript",
"cli",
"tooling",
"blockchain",
"ethereum"
]
}

View File

@@ -1,44 +1,134 @@
#!/usr/bin/env node
import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'fs';
import {Circomkit} from '../circomkit';
import {initFiles} from '../utils/init';
const CONFIG_PATH = './circomkit.json';
const DEFAULT_INPUT = 'default';
const DEFAULT_PTAU = './ptau/powersOfTau28_hez_final_12.ptau';
const USAGE = `Usage:
Compile the circuit.
compile circuit
Create main component.
instantiate circuit
Clean build artifacts & main.
clean circuit
Export Solidity verifier.
contract circuit
Generate a proof.
prove circuit input
Verify a proof.
verify circuit input
Generate a witness.
witness circuit input
Circuit-specific setup.
setup circuit input
`;
async function cli(): Promise<number> {
const circomkit = new Circomkit();
switch (process.argv[2] as unknown as keyof Circomkit) {
case 'compile':
await circomkit.compile(process.argv[3]);
// read user configs
let config = {};
if (existsSync(CONFIG_PATH)) {
config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
}
const circomkit = new Circomkit(config);
// execute command
switch (process.argv[2] as unknown as keyof Circomkit | 'init') {
case 'compile': {
circomkit.log('\n=== Compiling the circuit ===', 'title');
const path = await circomkit.compile(process.argv[3]);
circomkit.log('Built at: ' + path);
break;
case 'instantiate':
circomkit.instantiate(process.argv[3]);
}
case 'instantiate': {
circomkit.log('\n=== Creating main component ===', 'title');
const path = circomkit.instantiate(process.argv[3]);
circomkit.log('Created at: ' + path);
break;
case 'clean':
}
case 'clean': {
circomkit.log('\n=== Cleaning artifacts ===', 'title');
await circomkit.clean(process.argv[3]);
circomkit.log('Cleaned.');
break;
// case 'type':
}
// case 'type': // TODO
// await circomkit.type(process.argv[3]);
// break;
case 'contract':
await circomkit.contract(process.argv[3]);
case 'contract': {
circomkit.log('=== Generating verifier contract ===', 'title');
const path = await circomkit.contract(process.argv[3]);
circomkit.log('Created at: ' + path);
break;
// case 'calldata':
}
// case 'calldata': // // TODO
// await circomkit.calldata(process.argv[3]);
// break;
case 'prove':
await circomkit.prove(process.argv[3], process.argv[4] || DEFAULT_INPUT);
case 'prove': {
circomkit.log('=== Generating proof ===', 'title');
const path = await circomkit.prove(process.argv[3], process.argv[4] || DEFAULT_INPUT);
circomkit.log('Generated under: ' + path);
break;
case 'verify':
await circomkit.verify(process.argv[3], process.argv[4] || DEFAULT_INPUT);
}
case 'verify': {
circomkit.log('=== Verifying proof ===', 'title');
const result = await circomkit.verify(process.argv[3], process.argv[4] || DEFAULT_INPUT);
if (result) {
circomkit.log('Verification successful.', 'log');
} else {
circomkit.log('Verification failed!', 'error');
}
break;
case 'witness':
await circomkit.witness(process.argv[3], process.argv[4] || DEFAULT_INPUT);
}
case 'witness': {
circomkit.log('=== Calculating witness ===', 'title');
const path = await circomkit.witness(process.argv[3], process.argv[4] || DEFAULT_INPUT);
circomkit.log('Witness created: ' + path);
break;
case 'setup':
await circomkit.setup(process.argv[3], process.argv[4] || DEFAULT_PTAU);
}
case 'setup': {
circomkit.log('=== Circuit-specific setup ===', 'title');
const path = await circomkit.setup(process.argv[3], process.argv[4] || DEFAULT_PTAU);
circomkit.log('Prover key created: ' + path);
break;
}
case 'init': {
await Promise.all(
[initFiles.circuit, initFiles.circuits, initFiles.input, initFiles.tests].map(item => {
const path = `${item.dir}/${item.name}`;
if (!existsSync(path)) {
mkdirSync(item.dir, {recursive: true});
writeFileSync(path, item.content);
} else {
circomkit.log(path + ' exists, skipping.', 'error');
}
})
);
break;
}
// setup project structure
default:
console.error('Invalid command.');
console.log(USAGE);
return 1;
}

View File

@@ -1,11 +1,31 @@
const snarkjs = require('snarkjs');
const wasm_tester = require('circom_tester').wasm;
import {type CircomkitConfig, defaultConfig} from './config';
import {writeFileSync, readFileSync, existsSync} from 'fs';
import {mkdirSync} from 'fs';
import {readFile, rm} from 'fs/promises';
import instantiate from './utils/instantiate';
import type {CircuitConfig} from './types/circuit';
import type {CircomkitConfig, CircuitInputPathBuilders, CircuitPathBuilders} from './types/circomkit';
/** Default configurations */
const defaultConfig: Readonly<CircomkitConfig> = {
proofSystem: 'plonk',
curve: 'bn128',
version: '2.1.0',
silent: false,
ptauDir: './ptau',
compiler: {
optimization: 0,
verbose: false,
json: false,
include: ['./node_modules'],
},
colors: {
title: '\x1b[0;34m', // blue
log: '\x1b[2;37m', // gray
error: '\x1b[0;31m', // red
},
};
/**
* Circomkit is an opinionated wrapper around a few
@@ -18,20 +38,22 @@ import type {CircuitConfig} from './types/circuit';
*/
export class Circomkit {
readonly config: CircomkitConfig;
private readonly resetColor = '\x1b[0m';
constructor(overrides: Partial<CircomkitConfig> = {}) {
// override default options, if any
this.config = {
const config: CircomkitConfig = {
...defaultConfig,
...overrides,
};
// sanitize
this.config = config;
}
/** Colorful logging based on config. */
private log(message: string, type: keyof CircomkitConfig['colors'] = 'log') {
/** Colorful logging based on config, used by CLI. */
log(message: string, type: keyof CircomkitConfig['colors'] = 'log') {
if (!this.config.silent) {
console.log(`${this.config.colors[type]}${message}${this.resetColor}`);
console.log(`${this.config.colors[type]}${message}'\x1b[0m'`);
}
}
@@ -40,13 +62,22 @@ export class Circomkit {
return JSON.stringify(obj, undefined, 2);
}
/** Parse circuit config from `circuits.json` */
private readCircuitConfig(circuit: string): CircuitConfig {
const circuits = JSON.parse(readFileSync('./circuits.json', 'utf-8'));
if (!(circuit in circuits)) {
throw new Error('No such circuit in circuits.json');
}
return circuits[circuit] as CircuitConfig;
}
/**
* Computes a path that requires a circuit name.
* @param circuit circuit name
* @param type path type
* @returns path
*/
private path(circuit: string, type: 'target' | 'sym' | 'pkey' | 'vkey' | 'wasm' | 'sol' | 'dir' | 'r1cs'): string {
private path(circuit: string, type: CircuitPathBuilders): string {
const dir = `./build/${circuit}`;
switch (type) {
case 'dir':
@@ -62,7 +93,7 @@ export class Circomkit {
case 'sym':
return `${dir}/${circuit}.sym`;
case 'sol':
return `${dir}/Verifier.sol`;
return `${dir}/Verifier_${this.config.proofSystem}.sol`;
case 'wasm':
return `${dir}/${circuit}_js/${circuit}.wasm`;
default:
@@ -77,7 +108,7 @@ export class Circomkit {
* @param type path type
* @returns path
*/
private path2(circuit: string, input: string, type: 'pubs' | 'proof' | 'wtns' | 'in' | 'dir'): string {
private path2(circuit: string, input: string, type: CircuitInputPathBuilders): string {
const dir = `./build/${circuit}/${input}`;
switch (type) {
case 'dir':
@@ -97,28 +128,28 @@ export class Circomkit {
/** Clean build files and the main component. */
async clean(circuit: string) {
this.log('\n=== Cleaning artifacts ===', 'title');
await Promise.all([
rm(this.path(circuit, 'dir'), {recursive: true, force: true}),
rm(this.path(circuit, 'target'), {force: true}),
]);
this.log('Cleaned.');
}
/** Compile the circuit.
* This function uses [wasm tester](../../node_modules/circom_tester/wasm/tester.js)
* in the background.
*
* @returns path to build files
*/
async compile(circuit: string) {
this.log('\n=== Compiling the circuit ===', 'title');
const outDir = this.path(circuit, 'dir');
const targetPath = this.path(circuit, 'target');
if (!existsSync(this.path(circuit, 'target'))) {
if (!existsSync(targetPath)) {
this.log('Main component does not exist, creating it now.');
this.instantiate(circuit);
}
const outDir = this.path(circuit, 'dir');
await wasm_tester(this.path(circuit, 'target'), {
await wasm_tester(targetPath, {
output: outDir,
prime: this.config.curve,
verbose: this.config.compiler.verbose,
@@ -129,22 +160,26 @@ export class Circomkit {
sym: true,
recompile: true,
});
this.log('Built at: ' + outDir);
// TODO: add C output
return outDir;
}
/** Exports a solidity contract for the verifier. */
async contract(circuit: string) {
this.log('=== Generating verifier contract ===', 'title');
const pkey = this.path(circuit, 'pkey');
const verifierCode = await snarkjs.zKey.exportSolidityVerifier(pkey, {
groth16: readFileSync('./node_modules/snarkjs/templates/verifier_groth16.sol.ejs', 'utf-8'),
plonk: readFileSync('./node_modules/snarkjs/templates/verifier_plonk.sol.ejs', 'utf-8'),
const template = readFileSync(
`./node_modules/snarkjs/templates/verifier_${this.config.proofSystem}.sol.ejs`,
'utf-8'
);
const contractCode = await snarkjs.zKey.exportSolidityVerifier(pkey, {
[this.config.proofSystem]: template,
});
const sol = this.path(circuit, 'sol');
writeFileSync(sol, verifierCode);
this.log('Contract created at: ' + sol);
const contractPath = this.path(circuit, 'sol');
writeFileSync(contractPath, contractCode);
return contractPath;
}
/** Export calldata to console. */
@@ -154,23 +189,17 @@ export class Circomkit {
/** Instantiate the `main` component. */
instantiate(circuit: string) {
this.log('\n=== Creating main component ===', 'title');
const circuits = JSON.parse(readFileSync('./circuits.json', 'utf-8'));
if (!(circuit in circuits)) {
throw new Error('No such circuit in circuits.json');
}
const circuitConfig = circuits[circuit] as CircuitConfig;
instantiate(circuit, {
const circuitConfig = this.readCircuitConfig(circuit);
const target = instantiate(circuit, {
...circuitConfig,
dir: 'main',
version: this.config.version,
});
this.log('Done!');
return target;
}
/** Generate a proof. */
async prove(circuit: string, input: string) {
this.log('=== Generating proof ===', 'title');
const jsonInput = JSON.parse(readFileSync(this.path2(circuit, input, 'in'), 'utf-8'));
const fullProof = await snarkjs[this.config.proofSystem].fullProve(
jsonInput,
@@ -182,13 +211,11 @@ export class Circomkit {
mkdirSync(dir, {recursive: true});
writeFileSync(this.path2(circuit, input, 'pubs'), this.prettyStringify(fullProof.publicSignals));
writeFileSync(this.path2(circuit, input, 'proof'), this.prettyStringify(fullProof.proof));
this.log('Generated under: ' + dir);
return dir;
}
/** Commence a circuit-specific setup. */
async setup(circuit: string, ptau: string) {
this.log('=== Circuit-specific setup ===', 'title');
const r1csPath = this.path(circuit, 'r1cs');
const pkeyPath = this.path(circuit, 'pkey');
const vkeyPath = this.path(circuit, 'vkey');
@@ -209,7 +236,7 @@ export class Circomkit {
// export verification key
const vkey = await snarkjs.zKey.exportVerificationKey(pkeyPath);
writeFileSync(vkeyPath, this.prettyStringify(vkey));
this.log('Prover key created: ' + vkeyPath);
return vkeyPath;
}
/**
@@ -222,8 +249,6 @@ export class Circomkit {
/** Verify a proof for some public signals. */
async verify(circuit: string, input: string) {
this.log('=== Verifying proof ===', 'title');
const [vkey, pubs, proof] = (
await Promise.all(
[this.path(circuit, 'vkey'), this.path2(circuit, input, 'pubs'), this.path2(circuit, input, 'proof')].map(
@@ -232,17 +257,11 @@ export class Circomkit {
)
).map(content => JSON.parse(content));
const result = await snarkjs[this.config.proofSystem].verify(vkey, pubs, proof);
if (result) {
this.log('Verification successful.', 'log');
} else {
this.log('Verification failed!', 'error');
}
return await snarkjs[this.config.proofSystem].verify(vkey, pubs, proof);
}
/** Calculates the witness for the given circuit and input. */
async witness(circuit: string, input: string) {
this.log('=== Calculating witness ===', 'title');
const wasmPath = this.path(circuit, 'wasm');
const wtnsPath = this.path2(circuit, input, 'wtns');
const outDir = this.path2(circuit, input, 'dir');
@@ -250,6 +269,6 @@ export class Circomkit {
mkdirSync(outDir, {recursive: true});
await snarkjs.wtns.calculate(jsonInput, wasmPath, wtnsPath);
this.log('Created under: ' + wtnsPath);
return wtnsPath;
}
}

View File

@@ -1,6 +1,7 @@
import ProofTester from './testers/proofTester';
import WasmTester from './testers/wasmTester';
import instantiate from './utils/instantiate';
import {Circomkit} from './circomkit';
export * from './types/circuit';
export {ProofTester, WasmTester, instantiate};
export {ProofTester, WasmTester, instantiate, Circomkit};

View File

@@ -6,6 +6,8 @@ export type CircomkitConfig = {
proofSystem: 'groth16' | 'plonk';
/** Curve to be used, which defines the underlying prime field. */
curve: 'bn128' | 'bls12381' | 'goldilocks';
/** Directory to download PTAU files. */
ptauDir: string;
/** Version number for main components. */
version: VersionType;
/** Hide Circomkit logs */
@@ -33,20 +35,5 @@ export type CircomkitConfig = {
};
};
export const defaultConfig: Readonly<CircomkitConfig> = {
proofSystem: 'plonk',
curve: 'bn128',
version: '2.1.0',
silent: false,
colors: {
title: '\x1b[0;34m', // blue
log: '\x1b[2;37m', // gray
error: '\x1b[0;31m', // red
},
compiler: {
optimization: 0,
verbose: false,
json: false,
include: ['./node_modules'],
},
};
export type CircuitPathBuilders = 'target' | 'sym' | 'pkey' | 'vkey' | 'wasm' | 'sol' | 'dir' | 'r1cs';
export type CircuitInputPathBuilders = 'pubs' | 'proof' | 'wtns' | 'in' | 'dir';

83
src/utils/init.ts Normal file
View File

@@ -0,0 +1,83 @@
/**
* Initializes a development environment. This function is
* most likely to be called from the CLI.
*
* It creates the following:
*
* - `circuits/multiplier.circom`
* - `inputs/multiplier/default.json`
* - `circuits.json`
*/
import {existsSync, mkdirSync, writeFileSync, writeSync} from 'fs';
export const initFiles = {
circuit: {
dir: './circuits',
name: 'multiplier.circom',
content: `pragma circom 2.0.0;
template Multiplier(N) {
assert(N > 1);
signal input in[N];
signal output out;
signal inner[N-1];
inner[0] <== in[0] * in[1];
for(var i = 2; i < N; i++) {
inner[i-1] <== inner[i-2] * in[i];
}
out <== inner[N-2];
}`,
},
input: {
dir: './inputs/multiplier_3',
name: '80.json',
content: `{
"in": [2, 4, 10]
}
`,
},
circuits: {
dir: '.',
name: 'circuits.json',
content: `{
"multiplier_3": {
"file": "multiplier",
"template": "Multiplier",
"params": [3]
},
}
`,
},
tests: {
dir: './tests',
name: 'multipier.test.ts',
content: `import { WasmTester } from "circomkit";
// exercise: make this test work for all numbers, not just 3
describe("multiplier", () => {
let circuit: WasmTester<["in"], ["out"]>;
before(async () => {
circuit = await WasmTester.new('multiplier_3', {
file: "multiplier",
template: "Multiplier",
params: [3],
});
await circuit.checkConstraintCount(2);
});
it("should multiply correctly", async () => {
await circuit.expectCorrectAssert({ in: [3, 8, 20] }, { out: 480 });
});
});
`,
},
} satisfies {
[key: string]: {
dir: string;
name: string;
content: string;
};
};

49
src/utils/ptau.ts Normal file
View File

@@ -0,0 +1,49 @@
import {createWriteStream} from 'fs';
import {get} from 'https';
const PTAU_DIR = './ptau';
const PTAU_URL_BASE = 'https://hermez.s3-eu-west-1.amazonaws.com';
/**
* Returns the URL of PTAU file for a given power.
*
* - If power is larger than 27,
* @see {@link https://github.com/iden3/snarkjs#7-prepare-phase-2}
* @param p a number such that numConstraints <= 2^p
* @returns
*/
function getPtauName(p: number): string {
let id = ''; // default for large values
if (p < 8) {
id = '_08';
} else if (p < 10) {
id = `_0${p}`;
} else if (p < 28) {
id = `_${p}`;
} else if (p === 28) {
id = '';
} else {
throw new Error('No PTAU for power level ' + p);
}
return `powersOfTau28_hez_final${id}.ptau`;
}
/**
* Downloads phase-1 powers of tau from Polygon Hermez.
* @param numConstraints number of constraints in the circuit
* @returns path to ptau
*/
export function downloadPtau(numConstraints: number): Promise<string> {
const ptauName = getPtauName(Math.floor(Math.log2(numConstraints)));
const ptauPath = `${PTAU_DIR}/${ptauName}`;
const file = createWriteStream(ptauPath);
return new Promise<string>(resolve => {
get(`${PTAU_URL_BASE}/${ptauName}`, response => {
response.pipe(file);
file.on('finish', () => {
file.close();
resolve(ptauPath);
});
});
});
}