Merge pull request #11 from erhant/setup-groth16

Setup phase for Groth16, better testers
This commit is contained in:
erhant.eth
2023-06-12 18:46:04 +03:00
committed by GitHub
28 changed files with 315 additions and 1811 deletions

313
README.md
View File

@@ -26,265 +26,134 @@
</a>
</p>
- [x] **Programmable Circuits**: The `main` component is created & compiled programmatically.
- [x] **Simple CLI**: A straightforward CLI is provided as a wrapper around SnarkJS commands, exposed via NPM scripts!
- [x] **Easily Configurable**: A single `circomkit.env` file stores the general configuration settings.
- [x] **Constraint Testing**: You can test computations & assertions for every template in a circuit, with minimal code-repetition.
- [x] **Proof Testing**: With prover & verification keys and the WASM circuit, you can test proof generation & verification.
- [x] **Witness Manipulation**: You can parse the output from a witness, and furthermore create fake witnesses to try and fool the verifier.
- [x] **Type-safe**: Witness & proof testers, as well as circuit signal inputs & outputs are all type-safe via generics.
- [x] **Solidity Exports**: Export a verifier contract in Solidity, or export a calldata for your proofs & public signals.
## Installation
Circomkit is an NPM package, which you can install via:
```sh
yarn add circomkit # yarn
npm install circomkit # NPM
```
You will also need Circom, which can be installed following the instructions [here](https://docs.circom.io/getting-started/installation/).
## Usage
Using Circomkit is easy:
Create an empty project, and install Circomkit. Then, you can setup the environment by simply executing:
1. Install [Circom](https://docs.circom.io/getting-started/installation/).
2. Clone this repo (or use it as a template) and install packages (`yarn` or `npm install`).
3. Write your circuit templates under the [circuits](./circuits/) folder. Your circuit code itself should be templates only; Circomkit programmatically generates the `main` component
4. Write your tests under the [tests](./tests/) folder.
5. Once you are ready, write the circuit configurations at [circuits.json](./circuits.json).
6. Use NPM scripts (`yarn <script>` or `npm run <script>`) to compile your circuit, build keys, generate & verify proofs and much more!
```sh
npx circomkit init
```
A circuit config looks like this:
This command creates the following:
- An example Multiplier circuit, under `circuits` folder.
- A circuit configuration file called `circuits.json`, with an example Multiplier circuit configuration.
- An example input JSON file for Multiplier, under `inputs/multiplier` folder.
- A test using Mocha, under `tests` folder.
- A Mocha configuration file.
Although Circomkit initializes with a Mocha test, uses Chai in the background so you could use anything that supports Chai.
### Configuration
A circuit config within `circuits.json` looks like below, where the `key` is the circuit name to be used in commands, and the value is an object that describes the filename, template name, public signals and template parameters:
```js
// the key is <circuit-name>
sudoku_4x4: {
file: 'sudoku', // file name (circuits/sudoku.circom)
template: 'Sudoku', // template name
pubs: ['puzzle'], // public signals
params: [Math.sqrt(4)], // template parameters
sudoku_9x9: {
file: 'sudoku',
template: 'Sudoku',
pubs: ['puzzle'],
params: [3], // sqrt(9)
},
```
You can omit `pubs` and `params` options, they default to `[]`. Afterwards, you can use the following commands:
You can omit `pubs` and `params` options, they default to `[]`.
### Command Line Interface
Actions that require a circuit name can be called as follows:
```bash
# Compile the circuit (generates the main component & compiles it)
npx circomkit compile circuit-name
# Compile the circuit
npx circomkit compile circuit
# Circuit setup
npx circomkit setup circuit-name -p phase1-ptau-path
# Create the main component
npx circomkit instantiate circuit
# Create a Solidity verifier contract
npx circomkit contract circuit-name
npx circomkit contract circuit
# Clean circuit artifacts
npx circomkit clean circuit-name
npx circomkit clean circuit
# Generate the `main` component without compiling it afterwards
npx circomkit instantiate circuit-name
# Circuit-specific setup
npx circomkit setup circuit [ptau-path]
```
You can change some general settings such as the configured proof system or the prime field under [circomkit.env](./circomkit.env).
Circuit-specific setup optionally takes the path to a PTAU file as argument. If not provided, it will automatically decide the PTAU to use with respect to constraint count, and download it for you! This feature only works for `bn128` curve.
### Working with Input Signals
Some actions such as generating a witness, generating a proof and verifying a proof require JSON inputs to provide the signal values. For that, we specifically create our input files under the `inputs` folder, and under the target circuit name there. For example, an input named `foobar` for some circuit named `circ` would be at `inputs/circ/foobar.json`.
Some actions such as generating a witness, generating a proof and verifying a proof require JSON inputs to provide the signal values. For that, we specifically create our input files under the `inputs` folder, and under the target circuit name there. For example, an input named `foo` for some circuit named `bar` would be at `inputs/bar/foo.json`.
```bash
# Generate a witness for some input
npx circomkit witness circuit-name [-i input-name (default: "default")]
# Generate a witness
npx circomkit witness circuit input
# Generate a proof for some input
npx circomkit prove circuit-name [-i input-name (default: "default")]
# Generate a proof
npx circomkit prove circuit input
# Verify a proof for some input (public signals only)
npx circomkit verify circuit-name [-i input-name (default: "default")]
# Verify a proof with public signals
npx circomkit verify circuit input
# Debug a witness of some input
npx circomkit debug circuit-name input-name
# Export calldata to call your Solidity verifier contract
npx circomkit calldata circuit-name [-i input-name (default: "default")]
# Export Solidity calldata to console
npx circomkit calldata circuit input
```
### Example Circuits
We have several example circuits that you can check out. With them, you can prove the following statements:
- **Multiplier**: "I know `n` factors that make up some number".
- **Fibonacci**: "I know the `n`'th Fibonacci number".
- **SHA256**: "I know the `n`-byte preimage of some SHA256 digest".
- **Sudoku**: "I know the solution to some `(n^2)x(n^2)` Sudoku puzzle".
- **Floating-Point Addition**: "I know two floating-point numbers that make up some number with `e` exponent and `m` mantissa bits." (adapted from [Berkeley ZKP MOOC 2023 - Lab 1](https://github.com/rdi-berkeley/zkp-mooc-lab)).
### Witness Calculation
Witness calculation tests check whether your circuit computes the correct result based on your inputs, and makes sure that assertions are correct. We provide very useful utility functions to help write these tests.
```ts
import WasmTester from '../utils/wasmTester';
const N = 3;
describe('multiplier', () => {
// type-safe signal names ✔
let circuit: WasmTester<['in'], ['out']>;
before(async () => {
circuit = await WasmTester.new(`multiplier_${N}`, {
file: 'multiplier',
template: 'Multiplier',
params: [N], // template parameters ✔
pubs: [], // public signals ✔
});
// constraint count checks ✔
await circuit.checkConstraintCount(N - 1);
});
it('should compute correctly', async () => {
const randomNumbers = Array.from({length: N}, () => Math.floor(Math.random() * 100 * N));
await circuit.expectPass({in: randomNumbers}, {out: randomNumbers.reduce((prev, acc) => acc * prev)});
});
});
```
With the circuit object, we can do the following:
- `circuit.expectPass(input, output)` to test whether we get the expected output for some given input.
- `circuit.expectPass(input)` to test whether the circuit assertions pass for some given input
- `circuit.expectFail(input)` to test whether the circuit assertions pass for some given input
#### Witness
What if we would just like to see what the output is, instead of comparing it to some witness? Well, that would be a trouble because we would have to parse the witness array (which is huge for some circuits) with respect to which signals the output signals correspond to. Thankfully, Circomkit has a function for that:
```ts
const output = await circuit.compute(INPUT, ['foo', 'bar']);
/* {
foo: [[1n, 2n], [3n, 4n]]
bar: 42n
} */
```
Note that this operation requires parsing the symbols file (`.sym`) and reading the witness array, which may be costly for large circuits. Most of the time, you won't need this for testing; instead, you will likely use it to see what the circuit actually does for debugging.
On top of these, you can create a fake witness by overriding symbols in the witness. This is useful in case you think there is a soundness error and would like to try and generate an adversarial witness.
```ts
// correct witness
const witness = await circuit.calculateWitness(INPUT);
// faked witness
const fakeWitness = await circuit.fakeWitness(witness, {
'symbol-names-here': 42n,
});
```
#### Multiple templates
You will often have multiple templates in your circuit code, and you might want to test them in the same test file of your main circuit too. Well, you can!
```ts
describe('multiplier utilities', () => {
describe('multiplication gate', () => {
let circuit: WasmTester<['in'], ['out']>;
before(async () => {
circuit = await WasmTester.new(circuitName, {
file: 'multiplier',
template: 'MultiplicationGate',
dir: 'test/multiplier', // nested paths ✔
});
});
it('should pass for in range', async () => {
await circuit.expectPass({in: [7, 5]}, {out: 7 * 5});
});
});
});
```
### Proof Verification
If you have created the prover key, verification key & the circuit WASM file (which is simply `yarn keygen <circuit-name> -p <pptau-path>`), you can also test proof generation & verification.
```ts
describe('multiplier proofs', () => {
let fullProof: FullProof;
let circuit: ProofTester<['in']>;
before(async () => {
circuit = new ProofTester(`multiplier_${N}`);
fullProof = await circuit.prove({
in: Array.from({length: N}, () => Math.floor(Math.random() * 100 * N)),
});
});
it('should verify', async () => {
await circuit.expectPass(fullProof.proof, fullProof.publicSignals);
});
it('should NOT verify', async () => {
await circuit.expectFail(fullProof.proof, ['13']);
});
});
```
The two utility functions provided here are:
- `circuit.expectPass(proof, publicSignals)` that makes sure that the given proof is **accepted** by the verifier for the given public signals.
- `circuit.expectFail(proof, publicSignals)` that makes sure that the given proof is **rejected** by the verifier for the given public signals.
## File Structure
The repository follows an _opinionated file structure_ shown below, abstracting away the pathing and orientation behind the scenes. Circomkit handles most of the work with respect to this structure.
Circomkit follows an _opinionated file structure_, abstracting away the pathing and orientation behind the scenes. All of these can be customized by overriding the respective settings in `circomkit.json`.
Here is an example structure, where we have a generic Sudoku proof-of-solution circuit, and we instantiate it for a 9x9 board:
```sh
circomkit
├── circuits.json # configs for circuit main components
├── circomkit.env # environment variables for cli
├── circuits # where you write templates
│ ├── main # auto-generated main components
│ │── sudoku_9x9.circom # e.g. a 9x9 sudoku board
│ │ └── ...
── test # auto-generated test components
│ └── ...
│ │── sudoku.circom # a generic sudoku circuit template
│ └── ...
├── inputs # where you write JSON inputs per circuit
├── sudoku_9x9 # each main template has its own folder
│ │ ├── example-input.json # e.g. a solution & its puzzle
│ └── ...
└── ...
── ptau # universal phase-1 setups
── powersOfTau28_hez_final_12.ptau
└── ...
└── build # build artifacts, these are .gitignore'd
│── sudoku_9x9 # each main template has its own folder
│── sudoku_9x9_js # artifacts of compilation
│ │── generate_witness.js
│ │ │── witness_calculator.js
── sudoku_9x9.wasm
│ │── example-input # artifacts of an input
── proof.json # generated proof object
│── public.json # public signals
│ └── witness.wtns # witness file
│── ... # folders for other inputs
│── sudoku_9x9.r1cs
── sudoku_9x9.sym
│ │── prover_key.zkey
│ └── verification_key.json
└── ...
├── circuits.json # circuit configs
├── circomkit.json # customizations
├── circuits
├── main
│ │ └── sudoku_9x9.circom
── sudoku.circom
├── inputs
│ └── sudoku_9x9
│ └── my_solution.json
├── ptau
│ └── powersOfTau28_hez_final_12.ptau
── build
── sudoku_9x9
│── sudoku_9x9_js
│ │── generate_witness.js
│ │── witness_calculator.js
── sudoku_9x9.wasm
│── my_solution
── proof.json
│ │── public.json
── witness.wtns
│── sudoku_9x9.r1cs
│── sudoku_9x9.sym
│── prover_key.zkey
── verifier_key.json
```
## Testing
To run tests do the following:
```bash
# test a specific circuit
yarn test <circuit-name>
# test all circuits
yarn test:all
```
You can test both witness calculations and proof generation & verification. We describe both in their respective sections, going over an example of "Multiplication" circuit.
## Styling
We use [Google TypeScript Style Guide](https://google.github.io/styleguide/tsguide.html) for the TypeScript codes.
Circomkit uses [Google TypeScript Style Guide](https://google.github.io/styleguide/tsguide.html).
```bash
# check the formatting

View File

@@ -3,32 +3,5 @@
"file": "multiplier",
"template": "Multiplier",
"params": [3]
},
"sudoku_9x9": {
"file": "sudoku",
"template": "Sudoku",
"pubs": ["puzzle"],
"params": [9]
},
"sudoku_4x4": {
"file": "sudoku",
"template": "Sudoku",
"pubs": ["puzzle"],
"params": [4]
},
"fp64": {
"file": "float_add",
"template": "FloatAdd",
"params": [11, 52]
},
"fp32": {
"file": "float_add",
"template": "FloatAdd",
"params": [8, 23]
},
"fibonacci_11": {
"file": "fibonacci",
"template": "Fibonacci",
"params": [11]
}
}

View File

@@ -1,33 +0,0 @@
pragma circom 2.0.0;
// Fibonacci with custom starting numbers
template Fibonacci(n) {
assert(n >= 2);
signal input in[2];
signal output out;
signal fib[n+1];
fib[0] <== in[0];
fib[1] <== in[1];
for (var i = 2; i <= n; i++) {
fib[i] <== fib[i-2] + fib[i-1];
}
out <== fib[n];
}
// Fibonacci with custom starting numbers, recursive & inefficient
template FibonacciRecursive(n) {
signal input in[2];
signal output out;
component f1, f2;
if (n <= 1) {
out <== in[n];
} else {
f1 = FibonacciRecursive(n-1);
f1.in <== in;
f2 = FibonacciRecursive(n-2);
f2.in <== in;
out <== f1.out + f2.out;
}
}

View File

@@ -1,417 +0,0 @@
pragma circom 2.0.0;
// circuits adapted from https://github.com/rdi-berkeley/zkp-mooc-lab
include "circomlib/circuits/comparators.circom";
include "circomlib/circuits/switcher.circom";
include "circomlib/circuits/gates.circom";
include "circomlib/circuits/bitify.circom";
/*
* Finds Math.floor(log2(n))
*/
function log2(n) {
var tmp = 1, ans = 1;
while (tmp < n) {
ans++;
tmp *= 2;
}
return ans;
}
/*
* Basically `out = cond ? ifTrue : ifFalse`
*/
template IfElse() {
signal input cond;
signal input ifTrue;
signal input ifFalse;
signal output out;
// cond * T - cond * F + F
// 0 * T - 0 * F + F = 0 - 0 + F = F
// 1 * T - 1 * F + F = T - F + F = T
out <== cond * (ifTrue - ifFalse) + ifFalse;
}
/*
* Outputs `out` = 1 if `in` is at most `b` bits long, and 0 otherwise.
*/
template CheckBitLength(b) {
assert(b < 254);
signal input in;
signal output out;
// compute b-bit representation of the number
signal bits[b];
var sum_of_bits = 0;
for (var i = 0; i < b; i++) {
bits[i] <-- (in >> i) & 1;
bits[i] * (1 - bits[i]) === 0;
sum_of_bits += (2 ** i) * bits[i];
}
// check if sum is equal to number itself
component eq = IsEqual();
eq.in[0] <== sum_of_bits;
eq.in[1] <== in;
out <== eq.out;
}
/*
* Enforces the well-formedness of an exponent-mantissa pair (e, m), which is defined as follows:
* if `e` is zero, then `m` must be zero
* else, `e` must be at most `k` bits long, and `m` must be in the range [2^p, 2^p+1)
*/
template CheckWellFormedness(k, p) {
signal input e;
signal input m;
// check if `e` is zero
component is_e_zero = IsZero();
is_e_zero.in <== e;
// Case I: `e` is zero
//// `m` must be zero
component is_m_zero = IsZero();
is_m_zero.in <== m;
// Case II: `e` is nonzero
//// `e` is `k` bits
component check_e_bits = CheckBitLength(k);
check_e_bits.in <== e;
//// `m` is `p`+1 bits with the MSB equal to 1
//// equivalent to check `m` - 2^`p` is in `p` bits
component check_m_bits = CheckBitLength(p);
check_m_bits.in <== m - (1 << p);
// choose the right checks based on `is_e_zero`
component if_else = IfElse();
if_else.cond <== is_e_zero.out;
if_else.ifTrue <== is_m_zero.out;
//// check_m_bits.out * check_e_bits.out is equivalent to check_m_bits.out AND check_e_bits.out
if_else.ifFalse <== check_m_bits.out * check_e_bits.out;
// assert that those checks passed
if_else.out === 1;
}
/*
* Right-shifts `x` by `shift` bits to output `y`, where `shift` is a public circuit parameter.
*/
template RightShift(b, shift) {
assert(shift < b);
signal input x;
signal output y;
// convert number to bits
component x_bits = Num2Bits(b);
x_bits.in <== x;
// do the shifting
signal y_bits[b-shift];
for (var i = 0; i < b-shift; i++) {
y_bits[i] <== x_bits.out[shift+i];
}
// convert shifted bits to number
component y_num = Bits2Num(b-shift);
y_num.in <== y_bits;
y <== y_num.out;
}
/*
* Rounds the input floating-point number and checks to ensure that rounding does not make the mantissa unnormalized.
* Rounding is necessary to prevent the bitlength of the mantissa from growing with each successive operation.
* The input is a normalized floating-point number (e, m) with precision `P`, where `e` is a `k`-bit exponent and `m` is a `P`+1-bit mantissa.
* The output is a normalized floating-point number (e_out, m_out) representing the same value with a lower precision `p`.
*/
template RoundAndCheck(k, p, P) {
signal input e;
signal input m;
signal output e_out;
signal output m_out;
assert(P > p);
// check if no overflow occurs
component if_no_overflow = LessThan(P+1);
if_no_overflow.in[0] <== m;
if_no_overflow.in[1] <== (1 << (P+1)) - (1 << (P-p-1));
signal no_overflow <== if_no_overflow.out;
var round_amt = P-p;
// Case I: no overflow
// compute (m + 2^{round_amt-1}) >> round_amt
var m_prime = m + (1 << (round_amt-1));
component right_shift = RightShift(P+2, round_amt);
right_shift.x <== m_prime;
var m_out_1 = right_shift.y;
var e_out_1 = e;
// Case II: overflow
var e_out_2 = e + 1;
var m_out_2 = (1 << p);
// select right output based on no_overflow
component if_else[2];
for (var i = 0; i < 2; i++) {
if_else[i] = IfElse();
if_else[i].cond <== no_overflow;
}
if_else[0].ifTrue <== e_out_1;
if_else[0].ifFalse <== e_out_2;
if_else[1].ifTrue <== m_out_1;
if_else[1].ifFalse <== m_out_2;
e_out <== if_else[0].out;
m_out <== if_else[1].out;
}
template Num2BitsWithSkipChecks(b) {
signal input in;
signal input skip_checks;
signal output out[b];
for (var i = 0; i < b; i++) {
out[i] <-- (in >> i) & 1;
out[i] * (1 - out[i]) === 0;
}
var sum_of_bits = 0;
for (var i = 0; i < b; i++) {
sum_of_bits += (2 ** i) * out[i];
}
// is always true if skip_checks is 1
(sum_of_bits - in) * (1 - skip_checks) === 0;
}
template LessThanWithSkipChecks(n) {
assert(n <= 252);
signal input in[2];
signal input skip_checks;
signal output out;
component n2b = Num2BitsWithSkipChecks(n+1);
n2b.in <== in[0] + (1<<n) - in[1];
n2b.skip_checks <== skip_checks;
out <== 1-n2b.out[n];
}
/*
* Left-shifts `x` by `shift` bits to output `y`.
* Enforces 0 <= `shift` < `shift_bound`.
* If `skip_checks` = 1, then we don't care about the output
* and the `shift_bound` constraint is not enforced.
*/
template LeftShift(shift_bound) {
signal input x;
signal input shift;
signal input skip_checks;
signal output y;
// find number of bits in shift_bound
var n = log2(shift_bound) + 1;
// convert "shift" to bits
component shift_bits = Num2BitsWithSkipChecks(n);
shift_bits.in <== shift;
shift_bits.skip_checks <== skip_checks;
// check "shift" < "shift_bound"
component less_than = LessThanWithSkipChecks(n);
less_than.in[0] <== shift;
less_than.in[1] <== shift_bound;
less_than.skip_checks <== skip_checks;
(less_than.out - 1) * (1 - skip_checks) === 0;
// compute pow2_shift from bits
// represents the shift amount
var pow2_shift = 1;
component muxes[n];
for (var i = 0; i < n; i++) {
muxes[i] = IfElse();
muxes[i].cond <== shift_bits.out[i];
muxes[i].ifTrue <== pow2_shift * (2 ** (2 ** i));
muxes[i].ifFalse <== pow2_shift;
pow2_shift = muxes[i].out;
}
// if skip checks, set pow2_shift to 0
component if_else = IfElse();
if_else.cond <== skip_checks;
if_else.ifTrue <== 0;
if_else.ifFalse <== pow2_shift;
pow2_shift = if_else.out; // not <== because it's a variable
// do the shift
y <== x * pow2_shift;
}
/*
* Find the Most-Significant Non-Zero Bit (MSNZB) of `in`, where `in` is assumed to be non-zero value of `b` bits.
* Outputs the MSNZB as a one-hot vector `one_hot` of `b` bits, where `one_hot`[i] = 1 if MSNZB(`in`) = i and 0 otherwise.
* The MSNZB is output as a one-hot vector to reduce the number of constraints in the subsequent `Normalize` template.
* Enforces that `in` is non-zero as MSNZB(0) is undefined.
* If `skip_checks` = 1, then we don't care about the output and the non-zero constraint is not enforced.
*/
template MSNZB(b) {
signal input in;
signal input skip_checks;
signal output one_hot[b];
// compute ell, ensuring that it is made of bits too
for (var i = 0; i < b; i++) {
var temp;
if (((1 << i) <= in) && (in < (1 << (i + 1)))) {
temp = 1;
} else {
temp = 0;
}
one_hot[i] <-- temp;
}
// verify that one_hot only has bits, and has only one set bit
var sum_of_bits = 0;
for (var i = 0; i < b; i++) {
sum_of_bits += one_hot[i];
one_hot[i] * (1 - one_hot[i]) === 0; // is bit
}
(1 - sum_of_bits) * (1 - skip_checks) === 0;
// verify that the set bit is at correct place
var pow2_ell = 0;
var pow2_ell_plus1 = 0;
for (var i = 0; i < b; i++) {
pow2_ell += one_hot[i] * (1 << i);
pow2_ell_plus1 += one_hot[i] * (1 << (i + 1));
}
component lt1 = LessThan(b+1);
lt1.in[0] <== in;
lt1.in[1] <== pow2_ell_plus1;
(lt1.out - 1) * (1 - skip_checks) === 0;
component lt2 = LessThan(b);
lt2.in[0] <== pow2_ell - 1;
lt2.in[1] <== in;
(lt2.out - 1) * (1 - skip_checks) === 0;
}
/*
* Normalizes the input floating-point number.
* The input is a floating-point number with a `k`-bit exponent `e` and a `P`+1-bit *unnormalized* mantissa `m` with precision `p`, where `m` is assumed to be non-zero.
* The output is a floating-point number representing the same value with exponent `e_out` and a *normalized* mantissa `m_out` of `P`+1-bits and precision `P`.
* Enforces that `m` is non-zero as a zero-value can not be normalized.
* If `skip_checks` = 1, then we don't care about the output and the non-zero constraint is not enforced.
*/
template Normalize(k, p, P) {
signal input e;
signal input m;
signal input skip_checks;
signal output e_out;
signal output m_out;
assert(P > p);
// compute ell = MSNZB
component msnzb = MSNZB(P+1);
msnzb.in <== m;
msnzb.skip_checks <== skip_checks;
// compute ell and L = 2 ** (P - ell)
var ell, L;
for (var i = 0; i < P+1; i++) {
ell += msnzb.one_hot[i] * i;
L += msnzb.one_hot[i] * (1 << (P - i));
}
// return
e_out <== e + ell - p;
m_out <== m * L;
}
/*
* Adds two floating-point numbers.
* The inputs are normalized floating-point numbers with `k`-bit exponents `e` and `p`+1-bit mantissas `m` with scale `p`.
* Does not assume that the inputs are well-formed and makes appropriate checks for the same.
* The output is a normalized floating-point number with exponent `e_out` and mantissa `m_out` of `p`+1-bits and scale `p`.
* Enforces that inputs are well-formed.
*/
template FloatAdd(k, p) {
signal input e[2];
signal input m[2];
signal output e_out;
signal output m_out;
// check well formedness
component well_form[2];
for (var i = 0; i < 2; i++) {
well_form[i] = CheckWellFormedness(k, p);
well_form[i].e <== e[i];
well_form[i].m <== m[i];
}
// find the larger magnitude
var magn[2];
component larger_magn = LessThan(k+p+1);
for (var i = 0; i < 2; i++) {
magn[i] = (e[i] * (1 << (p+1))) + m[i];
larger_magn.in[i] <== magn[i];
}
signal is_input2_larger <== larger_magn.out;
// arrange by magnitude
var input_1[2] = [e[0], m[0]];
var input_2[2] = [e[1], m[1]];
component switcher[2];
for (var i = 0; i < 2; i++) {
switcher[i] = Switcher();
switcher[i].sel <== is_input2_larger;
switcher[i].L <== input_1[i];
switcher[i].R <== input_2[i];
}
var alpha_e = switcher[0].outL;
var alpha_m = switcher[1].outL;
var beta_e = switcher[0].outR;
var beta_m = switcher[1].outR;
// if-else part
var diff_e = alpha_e - beta_e;
component compare_diff_e = LessThan(k);
compare_diff_e.in[0] <== p+1;
compare_diff_e.in[1] <== diff_e;
component is_alpha_e_zero = IsZero();
is_alpha_e_zero.in <== alpha_e;
//// case 1
component or = OR();
or.a <== compare_diff_e.out;
or.b <== is_alpha_e_zero.out;
signal is_case_1 <== or.out; // true branch
var case_1_output[2] = [alpha_e, alpha_m];
//// case 2
component shl = LeftShift(p+2);
shl.x <== alpha_m;
shl.shift <== diff_e;
shl.skip_checks <== is_case_1; // skip if this isnt the case
alpha_m = shl.y;
var mantissa = alpha_m + beta_m;
var exponent = beta_e;
component normalize = Normalize(k, p, 2*p + 1);
normalize.m <== mantissa;
normalize.e <== exponent;
normalize.skip_checks <== is_case_1;
component rnc = RoundAndCheck(k, p, 2*p + 1);
rnc.m <== normalize.m_out;
rnc.e <== normalize.e_out;
var case_2_output[2] = [rnc.e_out, rnc.m_out];
// return
component if_else[2];
for (var i = 0; i < 2; i++) {
if_else[i] = IfElse();
if_else[i].cond <== is_case_1;
if_else[i].ifTrue <== case_1_output[i];
if_else[i].ifFalse <== case_2_output[i];
}
e_out <== if_else[0].out;
m_out <== if_else[1].out;
}

View File

@@ -1,6 +0,0 @@
// auto-generated by instantiate.js
pragma circom 2.0.0;
include "../fibonacci.circom";
component main = Fibonacci(11);

View File

@@ -1,6 +0,0 @@
// auto-generated by instantiate.js
pragma circom 2.0.0;
include "../float_add.circom";
component main = FloatAdd(8, 23);

View File

@@ -1,6 +0,0 @@
// auto-generated by instantiate.js
pragma circom 2.0.0;
include "../float_add.circom";
component main = FloatAdd(11, 52);

View File

@@ -1,6 +0,0 @@
// auto-generated by circomkit
pragma circom 2.1.0;
include "../multiplier.circom";
component main = Multiplier(3);

View File

@@ -1,6 +0,0 @@
// auto-generated by instantiate.js
pragma circom 2.0.0;
include "../sudoku.circom";
component main {public[puzzle]} = Sudoku(2);

View File

@@ -1,6 +0,0 @@
// auto-generated by instantiate.js
pragma circom 2.0.0;
include "../sudoku.circom";
component main {public[puzzle]} = Sudoku(3);

View File

@@ -1,53 +1,16 @@
pragma circom 2.0.0;
template MultiplicationGate() {
signal input in[2];
signal output out <== in[0] * in[1];
}
template Multiplier(N) {
assert(N > 1);
signal input in[N];
signal output out;
component gate[N-1];
// instantiate gates
for (var i = 0; i < N-1; i++) {
gate[i] = MultiplicationGate();
}
// multiply
gate[0].in <== [in[0], in[1]];
for (var i = 0; i < N-2; i++) {
gate[i+1].in <== [gate[i].out, in[i+2]];
}
out <== gate[N-2].out;
}
// Alternative way using anonymous components
template MultiplierAnonymous(N) {
assert(N > 1);
signal input in[N];
signal output out;
signal inner[N-1];
inner[0] <== MultiplicationGate()([in[0], in[1]]);
for(var i = 0; i < N-2; i++) {
inner[i+1] <== MultiplicationGate()([inner[i], in[i+2]]);
}
out <== inner[N-2];
}
// Alternative way without the gate component
template MultiplierSimple(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];
}

View File

@@ -1,42 +0,0 @@
pragma circom 2.0.0;
include "circomlib/circuits/sha256/sha256.circom";
include "circomlib/circuits/bitify.circom";
/**
* Wrapper around SHA256 to support bytes as input instead of bits
* @param N The number of input bytes
* @input in The input bytes
* @output out The SHA256 output of the n input bytes, in bytes
*
* SOURCE: https://github.com/celer-network/zk-benchmark/blob/main/circom/circuits/sha256/sha256_bytes.circom
*/
template Sha256Bytes(N) {
signal input in[N];
signal output out[32];
// convert input bytes to bits
component byte_to_bits[N];
for (var i = 0; i < N; i++) {
byte_to_bits[i] = Num2Bits(8);
byte_to_bits[i].in <== in[i];
}
// sha256 over bits
component sha256 = Sha256(N*8);
for (var i = 0; i < N; i++) {
for (var j = 0; j < 8; j++) {
sha256.in[i*8+j] <== byte_to_bits[i].out[7-j];
}
}
// convert output bytes to bits
component bits_to_bytes[32];
for (var i = 0; i < 32; i++) {
bits_to_bytes[i] = Bits2Num(8);
for (var j = 0; j < 8; j++) {
bits_to_bytes[i].in[7-j] <== sha256.out[i*8+j];
}
out[i] <== bits_to_bytes[i].out;
}
}

View File

@@ -1,136 +0,0 @@
pragma circom 2.0.0;
include "circomlib/circuits/bitify.circom";
// Finds Math.floor(log2(n))
function log2(n) {
var tmp = 1, ans = 1;
while (tmp < n) {
ans++;
tmp *= 2;
}
return ans;
}
// Assert that two elements are not equal
template NonEqual() {
signal input in[2];
signal inv;
// we check if (in[0] - in[1] != 0)
// because 1/0 results in 0, so the constraint won't hold
inv <-- 1 / (in[1] - in[0]);
inv * (in[1] - in[0]) === 1;
}
// Assert that number is representable by b-bits
template AssertBitLength(b) {
assert(b < 254);
signal input in;
// compute b-bit representation of the number
signal bits[b];
var sum_of_bits = 0;
for (var i = 0; i < b; i++) {
bits[i] <-- (in >> i) & 1;
bits[i] * (1 - bits[i]) === 0;
sum_of_bits += (2 ** i) * bits[i];
}
// check if sum is equal to number itself
in === sum_of_bits;
}
// Checks that `in` is in range [MIN, MAX]
template InRange(MIN, MAX) {
assert(MIN < MAX);
signal input in;
// number of bits to represent MAX
var b = log2(MAX) + 1;
component lowerBound = AssertBitLength(b);
component upperBound = AssertBitLength(b);
lowerBound.in <== in - MIN; // e.g. 1 - 1 = 0 (for 0 <= in)
upperBound.in <== in + (2 ** b) - MAX - 1; // e.g. 9 + (15 - 9) = 15 (for in <= 15)
}
// Assert that all given values are unique
template Distinct(n) {
signal input in[n];
component nonEqual[n][n]; // TODO; has extra comps here
for(var i = 0; i < n; i++){
for(var j = 0; j < i; j++){
nonEqual[i][j] = NonEqual();
nonEqual[i][j].in <== [in[i], in[j]];
}
}
}
template Sudoku(n_sqrt) {
var n = n_sqrt * n_sqrt;
signal input solution[n][n]; // solution is a 2D array of numbers
signal input puzzle[n][n]; // puzzle is the same, but a zero indicates a blank
// ensure that solution & puzzle agrees
for (var row_i = 0; row_i < n; row_i++) {
for (var col_i = 0; col_i < n; col_i++) {
// puzzle is either empty (0), or the same as solution
puzzle[row_i][col_i] * (puzzle[row_i][col_i] - solution[row_i][col_i]) === 0;
}
}
// ensure all values in the solution are in range
component inRange[n][n];
for (var row_i = 0; row_i < n; row_i++) {
for (var col_i = 0; col_i < n; col_i++) {
inRange[row_i][col_i] = InRange(1, n);
inRange[row_i][col_i].in <== solution[row_i][col_i];
}
}
// ensure all values in the solution are distinct
component distinctRows[n];
for (var row_i = 0; row_i < n; row_i++) {
for (var col_i = 0; col_i < n; col_i++) {
if (row_i == 0) {
distinctRows[col_i] = Distinct(n);
}
distinctRows[col_i].in[row_i] <== solution[row_i][col_i];
}
}
component distinctCols[n];
for (var col_i = 0; col_i < n; col_i++) {
for (var row_i = 0; row_i < n; row_i++) {
if (col_i == 0) {
distinctCols[row_i] = Distinct(n);
}
distinctCols[row_i].in[col_i] <== solution[row_i][col_i];
}
}
// ensure that all values in squares are distinct
component distinctSquares[n];
var s_i = 0;
for (var sr_i = 0; sr_i < n_sqrt; sr_i++) {
for (var sc_i = 0; sc_i < n_sqrt; sc_i++) {
// square index
distinctSquares[s_i] = Distinct(n);
// (r, c) now marks the start of this square
var r = sr_i * n_sqrt;
var c = sc_i * n_sqrt;
var i = 0;
for (var row_i = r; row_i < r + n_sqrt; row_i++) {
for (var col_i = c; col_i < c + n_sqrt; col_i++) {
distinctSquares[s_i].in[i] <== solution[row_i][col_i];
i++;
}
}
s_i++;
}
}
}

View File

@@ -1,3 +0,0 @@
{
"in": [1, 1]
}

View File

@@ -1,4 +0,0 @@
{
"e": ["1122", "1024"],
"m": ["7807742059002284", "7045130465601185"]
}

View File

@@ -1,24 +0,0 @@
{
"solution": [
[1, 9, 4, 8, 6, 5, 2, 3, 7],
[7, 3, 5, 4, 1, 2, 9, 6, 8],
[8, 6, 2, 3, 9, 7, 1, 4, 5],
[9, 2, 1, 7, 4, 8, 3, 5, 6],
[6, 7, 8, 5, 3, 1, 4, 2, 9],
[4, 5, 3, 9, 2, 6, 8, 7, 1],
[3, 8, 9, 6, 5, 4, 7, 1, 2],
[2, 4, 6, 1, 7, 9, 5, 8, 3],
[5, 1, 7, 2, 8, 3, 6, 9, 4]
],
"puzzle": [
[0, 0, 0, 8, 6, 0, 2, 3, 0],
[7, 0, 5, 0, 0, 0, 9, 0, 8],
[0, 6, 0, 3, 0, 7, 0, 4, 0],
[0, 2, 0, 7, 0, 8, 0, 5, 0],
[0, 7, 8, 5, 0, 0, 0, 0, 0],
[4, 0, 0, 9, 0, 6, 0, 7, 0],
[3, 0, 9, 0, 5, 0, 7, 0, 2],
[0, 4, 0, 1, 0, 9, 0, 8, 0],
[5, 0, 7, 0, 8, 0, 0, 9, 4]
]
}

View File

@@ -20,7 +20,7 @@ const USAGE = `Usage:
contract circuit
Commence circuit-specific setup.
setup circuit
setup circuit [ptau-path]
Download the PTAU file needed for the circuit.
ptau circuit

View File

@@ -1,15 +1,24 @@
const snarkjs = require('snarkjs');
const wasm_tester = require('circom_tester').wasm;
import {writeFileSync, readFileSync, existsSync, mkdirSync} from 'fs';
import {writeFileSync, readFileSync, existsSync, mkdirSync, rmSync, renameSync} from 'fs';
import {readFile, rm, writeFile} from 'fs/promises';
import {instantiate} from './utils/instantiate';
import {downloadPtau, getPtauName} from './utils/ptau';
import type {CircuitConfig, R1CSInfoType} from './types/circuit';
import type {CircomkitConfig, CircuitInputPathBuilders, CircuitPathBuilders} from './types/circomkit';
import type {
CircomkitConfig,
CircomkitConfigOverrides,
CircuitInputPathBuilders,
CircuitPathBuilders,
} from './types/circomkit';
import {randomBytes} from 'crypto';
import {CircomWasmTester} from './types/wasmTester';
import WasmTester from './testers/wasmTester';
import ProofTester from './testers/proofTester';
/** Default configurations */
const defaultConfig: Readonly<CircomkitConfig> = {
proofSystem: 'plonk',
proofSystem: 'groth16',
curve: 'bn128',
version: '2.1.0',
silent: false,
@@ -26,14 +35,24 @@ const defaultConfig: Readonly<CircomkitConfig> = {
json: false,
include: ['./node_modules'],
},
groth16: {
numContributions: 1,
askForEntropy: false,
},
colors: {
title: '\u001b[0;34m', // blue
log: '\u001b[2;37m', // gray
error: '\u001b[0;31m', // red
success: '\u001b[0;32m', // green
warn: '\u001b[0;33m', // yellow
},
};
// TODO: maybe pass a logger using logplease to snarkjs?
// TODO: write a "check dependencies" module to decide which
// components to create if missing, such as WASM / R1CS or PKEY
/**
* Circomkit is an opinionated wrapper around a few
* [Snarkjs](../../node_modules/snarkjs/main.js) functions.
@@ -45,7 +64,7 @@ const defaultConfig: Readonly<CircomkitConfig> = {
export class Circomkit {
readonly config: CircomkitConfig;
constructor(overrides: Partial<CircomkitConfig> = {}) {
constructor(overrides: CircomkitConfigOverrides = {}) {
// override default options with the user-provided ones
const config: CircomkitConfig = Object.assign({}, overrides, defaultConfig);
@@ -111,7 +130,7 @@ export class Circomkit {
* @param type path type
* @returns path
*/
private path2(circuit: string, input: string, type: CircuitInputPathBuilders): string {
private pathWithInput(circuit: string, input: string, type: CircuitInputPathBuilders): string {
const dir = `${this.config.dirs.build}/${circuit}/${input}`;
switch (type) {
case 'dir':
@@ -129,6 +148,18 @@ export class Circomkit {
}
}
/** Given a PTAU name, returns the relative path. */
private pathPtau(ptauName: string): string {
return `${this.config.dirs.ptau}/${ptauName}`;
}
/** Given a circuit & id name, returns the relative path to phase-2 PTAU.
* This is used in particular by Groth16's circuit-specific setup phase.
*/
private pathZkey(circuit: string, id: number): string {
return `${this.config.dirs.build}/${circuit}/${circuit}_${id}.zkey`;
}
/** Clean build files and the main component. */
async clean(circuit: string) {
await Promise.all([
@@ -156,10 +187,14 @@ export class Circomkit {
const ptauName = getPtauName(constraints);
// return if ptau exists already
const ptauPath = `${this.config.dirs.ptau}/${ptauName}`;
const ptauPath = this.pathPtau(ptauName);
if (existsSync(ptauPath)) {
return ptauPath;
} else {
if (this.config.curve !== 'bn128') {
throw new Error('Auto-downloading PTAU only allowed for bn128 at the moment.');
}
mkdirSync(this.config.dirs.ptau);
this.log('Downloading ' + ptauName + '...');
return await downloadPtau(ptauName, this.config.dirs.ptau);
}
@@ -176,7 +211,7 @@ export class Circomkit {
const targetPath = this.path(circuit, 'target');
if (!existsSync(targetPath)) {
this.log('Main component does not exist, creating it now.');
this.log('Main component does not exist, creating it now.', 'warn');
const path = this.instantiate(circuit);
this.log('Main component created at: ' + path);
}
@@ -220,7 +255,9 @@ export class Circomkit {
async calldata(circuit: string, input: string) {
const [pubs, proof] = (
await Promise.all(
[this.path2(circuit, input, 'pubs'), this.path2(circuit, input, 'proof')].map(path => readFile(path, 'utf-8'))
(['pubs', 'proof'] as const)
.map(type => this.pathWithInput(circuit, input, type))
.map(path => readFile(path, 'utf-8'))
)
).map(content => JSON.parse(content));
return await snarkjs[this.config.proofSystem].exportSolidityCallData(proof, pubs);
@@ -229,40 +266,59 @@ export class Circomkit {
/** Instantiate the `main` component.
* @returns path to created main component
*/
instantiate(circuit: string) {
const circuitConfig = this.readCircuitConfig(circuit);
const target = instantiate(circuit, {
...circuitConfig,
dir: this.config.dirs.main,
version: this.config.version,
});
return target;
instantiate(circuit: string, config?: CircuitConfig) {
if (config) {
return instantiate(circuit, config);
} else {
const circuitConfig = this.readCircuitConfig(circuit);
return instantiate(circuit, {
...circuitConfig,
dir: this.config.dirs.main,
version: this.config.version,
});
}
}
/** Generate a proof.
* @returns path to directory where public signals and proof are created
*/
async prove(circuit: string, input: string) {
const jsonInput = JSON.parse(readFileSync(this.path2(circuit, input, 'in'), 'utf-8'));
const fullProof = await snarkjs[this.config.proofSystem].fullProve(
jsonInput,
this.path(circuit, 'wasm'),
this.path(circuit, 'pkey')
);
// create WASM if needed
const wasmPath = this.path(circuit, 'wasm');
if (!existsSync(wasmPath)) {
this.log('WASM file does not exist, creating it now...', 'warn');
await this.compile(circuit);
}
const dir = this.path2(circuit, input, 'dir');
// create PKEY if needed
const pkeyPath = this.path(circuit, 'pkey');
if (!existsSync(pkeyPath)) {
this.log('Prover key does not exist, creating it now...', 'warn');
await this.setup(circuit);
}
// check input path
const inputPath = this.pathWithInput(circuit, input, 'in');
if (!existsSync(inputPath)) {
throw new Error('Input does not exist at: ' + inputPath);
}
const jsonInput = JSON.parse(readFileSync(inputPath, 'utf-8'));
const fullProof = await snarkjs[this.config.proofSystem].fullProve(jsonInput, wasmPath, pkeyPath);
const dir = this.pathWithInput(circuit, input, 'dir');
mkdirSync(dir, {recursive: true});
await Promise.all([
writeFile(this.path2(circuit, input, 'pubs'), this.prettyStringify(fullProof.publicSignals)),
writeFile(this.path2(circuit, input, 'proof'), this.prettyStringify(fullProof.proof)),
writeFile(this.pathWithInput(circuit, input, 'pubs'), this.prettyStringify(fullProof.publicSignals)),
writeFile(this.pathWithInput(circuit, input, 'proof'), this.prettyStringify(fullProof.proof)),
]);
return dir;
}
/** Commence a circuit-specific setup.
* @returns path to prover key
* @returns path to verifier key
*/
async setup(circuit: string) {
async setup(circuit: string, ptauPath?: string) {
const r1csPath = this.path(circuit, 'r1cs');
const pkeyPath = this.path(circuit, 'pkey');
const vkeyPath = this.path(circuit, 'vkey');
@@ -274,14 +330,49 @@ export class Circomkit {
}
// get ptau path
this.log('Checking for PTAU file...');
const ptau = await this.ptau(circuit);
if (ptauPath) {
this.log('Using provided PTAU: ' + ptauPath);
} else {
this.log('Checking for PTAU file...');
ptauPath = await this.ptau(circuit);
}
// circuit specific setup
if (this.config.proofSystem === 'plonk') {
await snarkjs.plonk.setup(this.path(circuit, 'r1cs'), ptau, pkeyPath);
this.log('Beginning setup phase!');
if (this.config.proofSystem === 'plonk' || this.config.proofSystem === 'fflonk') {
// PLONK or FFLONK don't need specific setup
await snarkjs[this.config.proofSystem].setup(r1csPath, ptauPath, pkeyPath);
} else {
throw new Error('Not implemented.');
// Groth16 needs a specific setup with its own PTAU ceremony
// initialize phase 2
const ptau2Init = this.pathPtau(`${circuit}_init.zkey`);
await snarkjs.powersOfTau.preparePhase2(ptauPath, ptau2Init);
// start PTAU generation
let curZkey = this.pathZkey(circuit, 0);
await snarkjs.zKey.newZKey(r1csPath, ptau2Init, curZkey);
rmSync(ptau2Init);
// make contributions
for (let contrib = 1; contrib <= this.config.groth16.numContributions; contrib++) {
const nextZkey = this.pathZkey(circuit, contrib);
// entropy, if user wants to prompt give undefined
this.log(`Making contribution: ${contrib}`);
await snarkjs.zKey.contribute(
curZkey,
nextZkey,
`${circuit}_${contrib}`,
this.config.groth16.askForEntropy ? undefined : randomBytes(32) // entropy
);
// remove current key, and move on to next one
rmSync(curZkey);
curZkey = nextZkey;
}
// finally, rename the resulting key to pkey
renameSync(curZkey, pkeyPath);
}
// export verification key
@@ -294,9 +385,11 @@ export class Circomkit {
async verify(circuit: string, input: string) {
const [vkey, pubs, proof] = (
await Promise.all(
[this.path(circuit, 'vkey'), this.path2(circuit, input, 'pubs'), this.path2(circuit, input, 'proof')].map(
path => readFile(path, 'utf-8')
)
[
this.path(circuit, 'vkey'),
this.pathWithInput(circuit, input, 'pubs'),
this.pathWithInput(circuit, input, 'proof'),
].map(path => readFile(path, 'utf-8'))
)
).map(content => JSON.parse(content));
@@ -306,12 +399,52 @@ export class Circomkit {
/** Calculates the witness for the given circuit and input. */
async witness(circuit: string, input: string) {
const wasmPath = this.path(circuit, 'wasm');
const wtnsPath = this.path2(circuit, input, 'wtns');
const outDir = this.path2(circuit, input, 'dir');
const jsonInput = JSON.parse(readFileSync(this.path2(circuit, input, 'in'), 'utf-8'));
const wtnsPath = this.pathWithInput(circuit, input, 'wtns');
const outDir = this.pathWithInput(circuit, input, 'dir');
const jsonInput = JSON.parse(readFileSync(this.pathWithInput(circuit, input, 'in'), 'utf-8'));
mkdirSync(outDir, {recursive: true});
await snarkjs.wtns.calculate(jsonInput, wasmPath, wtnsPath);
return wtnsPath;
}
/**
* Compiles and reutrns a circuit tester class instance.
* @param circuit name of circuit
* @param config circuit configuration
* @returns a `WasmTester` instance
*/
async WasmTester<IN extends string[] = [], OUT extends string[] = []>(circuit: string, config: CircuitConfig) {
config.dir ||= 'test';
// create circuit
const targetPath = this.instantiate(circuit, config);
const circomWasmTester: CircomWasmTester = await wasm_tester(targetPath, {
output: undefined, // todo: check if this is ok
prime: this.config.curve,
verbose: this.config.compiler.verbose,
O: this.config.compiler.optimization,
json: this.config.compiler.json,
include: this.config.compiler.include,
wasm: true,
sym: true,
recompile: true,
});
return new WasmTester<IN, OUT>(circomWasmTester);
}
async ProofTester<IN extends string[] = []>(circuit: string, ptauPath?: string) {
const wasmPath = this.path(circuit, 'wasm');
const pkeyPath = this.path(circuit, 'pkey');
const vkeyPath = this.path(circuit, 'vkey');
// create keys if required
if (!existsSync(vkeyPath)) {
this.log('Verifier key does not exist, creating it now...');
await this.setup(circuit, ptauPath);
}
return new ProofTester<IN>(wasmPath, pkeyPath, vkeyPath);
}
}

View File

@@ -1,7 +1,6 @@
import {Circomkit} from './circomkit';
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, Circomkit};
export {Circomkit, ProofTester, WasmTester};

View File

@@ -1,49 +1,20 @@
import {readFileSync, existsSync} from 'fs';
const snarkjs = require('snarkjs');
import {expect} from 'chai';
import {readFileSync} from 'fs';
import type {CircuitSignals, FullProof} from '../types/circuit';
import type {CircomkitConfig} from '../types/circomkit';
/** Allowed proof systems as defined in SnarkJS. */
const PROOF_SYSTEMS = ['groth16', 'plonk'] as const;
/**
* A more extensive Circuit class, able to generate proofs & verify them.
* Assumes that prover key and verifier key have been computed.
*/
/** A tester that is able to generate proofs & verify them.
* Use `expectFail` and `expectPass` to test out evaluations. */
export default class ProofTester<IN extends string[] = []> {
public readonly protocol: 'groth16' | 'plonk';
private readonly wasmPath: string;
private readonly proverKeyPath: string;
private readonly verificationKeyPath: string;
public readonly protocol: CircomkitConfig['proofSystem'];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly verificationKey: any;
public readonly verificationKey: any;
/**
* Sets the paths & loads the verification key. The underlying proof system is checked by looking
* at `verificationKey.protocol`.
* @param circuit a proof tester
*/
constructor(circuit: string) {
// find paths (computed w.r.t circuit name)
this.wasmPath = `./build/${circuit}/${circuit}_js/${circuit}.wasm`;
this.proverKeyPath = `./build/${circuit}/prover_key.zkey`;
this.verificationKeyPath = `./build/${circuit}/verification_key.json`;
constructor(readonly wasmPath: string, readonly pkeyPath: string, readonly vkeyPath: string) {
this.verificationKey = JSON.parse(readFileSync(vkeyPath).toString()) as typeof this.verificationKey;
// ensure that paths exist
const missing = [this.wasmPath, this.proverKeyPath, this.verificationKeyPath].filter(p => !existsSync(p));
if (missing.length !== 0) {
throw new Error('Missing files for' + circuit + ':\n' + missing.join('\n'));
}
// load verification key
this.verificationKey = JSON.parse(readFileSync(this.verificationKeyPath).toString()) as typeof this.verificationKey;
// check proof system
const protocol = this.verificationKey.protocol;
if (!PROOF_SYSTEMS.includes(protocol)) {
throw new Error('Unknown protocol in verification key: ' + protocol);
}
this.protocol = protocol;
this.protocol = this.verificationKey.protocol;
}
/**
@@ -53,7 +24,7 @@ export default class ProofTester<IN extends string[] = []> {
* @returns a proof and public signals
*/
async prove(input: CircuitSignals<IN>): Promise<FullProof> {
return await snarkjs[this.protocol].fullProve(input, this.wasmPath, this.proverKeyPath);
return await snarkjs[this.protocol].fullProve(input, this.wasmPath, this.pkeyPath);
}
/**

View File

@@ -1,8 +1,6 @@
const wasm_tester = require('circom_tester').wasm;
import type {WitnessType, CircuitSignals, SymbolsType, SignalValueType, CircuitConfig} from '../types/circuit';
import type {WitnessType, CircuitSignals, SymbolsType, SignalValueType} from '../types/circuit';
import type {CircomWasmTester} from '../types/wasmTester';
import {assert, expect} from 'chai';
import {instantiate} from '../utils/instantiate';
/** A utility class to test your circuits. Use `expectFail` and `expectPass` to test out evaluations. */
export default class WasmTester<IN extends readonly string[] = [], OUT extends readonly string[] = []> {
@@ -12,23 +10,6 @@ export default class WasmTester<IN extends readonly string[] = [], OUT extends r
symbols: SymbolsType | undefined;
/** List of constraints, see {@link loadConstraints} */
constraints: unknown[] | undefined;
/**
* Compiles and reutrns a circuit tester class instance.
* @param circuit name of circuit
* @param dir directory to read the circuit from, defaults to `test`
* @returns a `WasmTester` instance
*/
static async new<IN extends string[] = [], OUT extends string[] = []>(
circuitName: string,
circuitConfig: CircuitConfig
): Promise<WasmTester<IN, OUT>> {
const dir = circuitConfig.dir || 'test';
instantiate(circuitName, circuitConfig);
const circomWasmTester: CircomWasmTester = await wasm_tester(`./circuits/${dir}/${circuitName}.circom`, {
include: 'node_modules', // will link circomlib circuits
});
return new WasmTester<IN, OUT>(circomWasmTester);
}
constructor(circomWasmTester: CircomWasmTester) {
this.circomWasmTester = circomWasmTester;
@@ -60,9 +41,7 @@ export default class WasmTester<IN extends readonly string[] = [], OUT extends r
return this.circomWasmTester.calculateWitness(input, sanityCheck);
}
/**
* Loads the list of R1CS constraints to `this.constraints`
*/
/** Loads the list of R1CS constraints to `this.constraints`. */
async loadConstraints(): Promise<void> {
await this.circomWasmTester.loadConstraints();
this.constraints = this.circomWasmTester.constraints;

View File

@@ -3,7 +3,7 @@ type VersionType = `${number}.${number}.${number}`;
export type CircomkitConfig = {
/** Proof system to be used. */
proofSystem: 'groth16' | 'plonk';
proofSystem: 'groth16' | 'plonk' | 'fflonk';
/** Curve to be used, which defines the underlying prime field. */
curve: 'bn128' | 'bls12381' | 'goldilocks';
/** Directory overrides, it is best you leave this as is. */
@@ -19,6 +19,13 @@ export type CircomkitConfig = {
/** Directory to output circuit build files. */
build: string;
};
/** Groth16-specific configurations */
groth16: {
/** Number of contributions */
numContributions: number;
/** Ask user input to create entropy */
askForEntropy: boolean;
};
/** Version number for main components. */
version: VersionType;
/** Hide Circomkit logs */
@@ -44,6 +51,7 @@ export type CircomkitConfig = {
success: ColorType;
log: ColorType;
error: ColorType;
warn: ColorType;
};
};
@@ -52,3 +60,12 @@ export type CircuitPathBuilders = 'target' | 'sym' | 'pkey' | 'vkey' | 'wasm' |
/** Shorthand notations for which path to build in Circomkit. These paths require a circuit name and input name. */
export type CircuitInputPathBuilders = 'pubs' | 'proof' | 'wtns' | 'in' | 'dir';
type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? RecursivePartial<U>[]
: T[P] extends object | undefined
? RecursivePartial<T[P]>
: T[P];
};
export type CircomkitConfigOverrides = RecursivePartial<CircomkitConfig>;

View File

@@ -1,5 +1,5 @@
/** An integer value is a numerical string, a number, or a bigint. */
type IntegerValueType = `${number}` | number | bigint;
export type IntegerValueType = `${number}` | number | bigint;
/** A signal value is a number, or an array of numbers (recursively). */
export type SignalValueType = IntegerValueType | SignalValueType[];

View File

@@ -1,53 +0,0 @@
import {WasmTester} from '../src';
describe('fibonacci', () => {
let circuit: WasmTester<['in'], ['out']>;
const N = 19;
before(async () => {
circuit = await WasmTester.new(`fibonacci_${N}`, {
file: 'fibonacci',
template: 'Fibonacci',
params: [N],
});
await circuit.checkConstraintCount();
});
it('should compute correctly', async () => {
await circuit.expectPass({in: [1, 1]}, {out: fibonacci([1, 1], N)});
});
});
// skipping because this takes a bit longer
describe.skip('fibonacci recursive', () => {
let circuit: WasmTester<['in'], ['out']>;
const N = 19;
before(async () => {
circuit = await WasmTester.new(`fibonacci_${N}_recursive`, {
file: 'fibonacci',
template: 'FibonacciRecursive',
params: [N],
});
await circuit.checkConstraintCount();
});
it('should compute correctly', async () => {
await circuit.expectPass({in: [1, 1]}, {out: fibonacci([1, 1], N)});
});
});
// simple fibonacci with 2 variables
function fibonacci(init: [number, number], n: number): number {
if (n < 0) {
throw new Error('N must be positive');
}
let [a, b] = init;
for (let i = 2; i <= n; i++) {
b = a + b;
a = b - a;
}
return n === 0 ? a : b;
}

View File

@@ -1,405 +0,0 @@
import {WasmTester} from '../src';
const expectedConstraints = {
fp32: 401,
fp64: 819,
checkBitLength: (bits: number) => bits + 2,
leftShift: (shiftBound: number) => shiftBound + 2,
right: (bits: number) => bits + 2,
normalize: (P: number) => 3 * P + 1 + 1, // MSNZB + 1
msnzb: (bits: number) => 3 * bits + 1,
};
// tests adapted from https://github.com/rdi-berkeley/zkp-mooc-lab
describe('float_add 32-bit', () => {
let circuit: WasmTester<['e', 'm'], ['e_out', 'm_out']>;
const k = 8;
const p = 23;
before(async () => {
circuit = await WasmTester.new('fp32', {
file: 'float_add',
template: 'FloatAdd',
params: [k, p],
});
await circuit.checkConstraintCount(401);
});
it('case I test', async () => {
await circuit.expectPass(
{
e: ['43', '5'],
m: ['11672136', '10566265'],
},
{e_out: '43', m_out: '11672136'}
);
});
it('case II test 1', async () => {
await circuit.expectPass(
{
e: ['104', '106'],
m: ['12444445', '14159003'],
},
{e_out: '107', m_out: '8635057'}
);
});
it('case II test 2', async () => {
await circuit.expectPass(
{
e: ['176', '152'],
m: ['16777215', '16777215'],
},
{e_out: '177', m_out: '8388608'}
);
});
it('case II test 3', async () => {
await circuit.expectPass(
{
e: ['142', '142'],
m: ['13291872', '13291872'],
},
{e_out: '143', m_out: '13291872'}
);
});
it('one input zero test', async () => {
await circuit.expectPass(
{
e: ['0', '43'],
m: ['0', '10566265'],
},
{e_out: '43', m_out: '10566265'}
);
});
it('both inputs zero test', async () => {
await circuit.expectPass(
{
e: ['0', '0'],
m: ['0', '0'],
},
{e_out: '0', m_out: '0'}
);
});
it('should fail - exponent zero but mantissa non-zero', async () => {
await circuit.expectFail({
e: ['0', '0'],
m: ['0', '10566265'],
});
});
it('should fail - mantissa ≥ 2^(p+1)', async () => {
await circuit.expectFail({
e: ['0', '43'],
m: ['0', '16777216'],
});
});
it('should fail - mantissa < 2^p', async () => {
await circuit.expectFail({
e: ['0', '43'],
m: ['0', '6777216'],
});
});
it('should fail - exponent ≥ 2^k', async () => {
await circuit.expectFail({
e: ['0', '256'],
m: ['0', '10566265'],
});
});
});
describe('float_add 64-bit', () => {
const k = 11;
const p = 52;
let circuit: WasmTester<['e', 'm'], ['e_out', 'm_out']>;
before(async () => {
circuit = await WasmTester.new('fp64', {
file: 'float_add',
template: 'FloatAdd',
params: [k, p],
});
await circuit.checkConstraintCount(819);
});
it('case I test', async () => {
await circuit.expectPass(
{
e: ['1122', '1024'],
m: ['7807742059002284', '7045130465601185'],
},
{e_out: '1122', m_out: '7807742059002284'}
);
});
it('case II test 1', async () => {
await circuit.expectPass(
{
e: ['1056', '1053'],
m: ['8879495032259305', '5030141535601637'],
},
{e_out: '1057', m_out: '4754131362104755'}
);
});
it('case II test 2', async () => {
await circuit.expectPass(
{
e: ['1035', '982'],
m: ['4804509148660890', '8505192799372177'],
},
{e_out: '1035', m_out: '4804509148660891'}
);
});
it('case II test 3', async () => {
await circuit.expectPass(
{
e: ['982', '982'],
m: ['8505192799372177', '8505192799372177'],
},
{e_out: '983', m_out: '8505192799372177'}
);
});
it('one input zero test', async () => {
await circuit.expectPass(
{
e: ['0', '982'],
m: ['0', '8505192799372177'],
},
{e_out: '982', m_out: '8505192799372177'}
);
});
it('both inputs zero test', async () => {
await circuit.expectPass(
{
e: ['0', '0'],
m: ['0', '0'],
},
{e_out: '0', m_out: '0'}
);
});
it('should fail - exponent zero but mantissa non-zero', async () => {
await circuit.expectFail({
e: ['0', '0'],
m: ['0', '8505192799372177'],
});
});
it('should fail - mantissa < 2^p', async () => {
await circuit.expectFail({
e: ['0', '43'],
m: ['0', '16777216'],
});
});
});
describe('float_add utilities', () => {
describe('check bit length', () => {
const b = 23; // bit count
let circuit: WasmTester<['in'], ['out']>;
before(async () => {
circuit = await WasmTester.new(`cbl_${b}`, {
file: 'float_add',
template: 'CheckBitLength',
params: [b],
dir: 'test/float_add',
});
await circuit.checkConstraintCount(expectedConstraints.checkBitLength(b));
});
it('should give 1 for in ≤ b', async () => {
await circuit.expectPass({in: '4903265'}, {out: '1'});
});
it('should give 0 for in > b', async () => {
await circuit.expectPass({in: '13291873'}, {out: '0'});
});
});
describe('left shift', () => {
const shift_bound = 25;
let circuit: WasmTester<['x', 'shift', 'skip_checks'], ['y']>;
before(async () => {
circuit = await WasmTester.new(`shl_${shift_bound}`, {
file: 'float_add',
template: 'LeftShift',
dir: 'test/float_add',
params: [shift_bound],
});
await circuit.checkConstraintCount(expectedConstraints.leftShift(shift_bound));
});
it("should pass test 1 - don't skip checks", async () => {
await circuit.expectPass(
{
x: '65',
shift: '24',
skip_checks: '0',
},
{y: '1090519040'}
);
});
it("should pass test 2 - don't skip checks", async () => {
await circuit.expectPass(
{
x: '65',
shift: '0',
skip_checks: '0',
},
{y: '65'}
);
});
it("should fail - don't skip checks", async () => {
await circuit.expectFail({
x: '65',
shift: '25',
skip_checks: '0',
});
});
it('should pass when skip_checks = 1 and shift is ≥ shift_bound', async () => {
await circuit.expectPass({
x: '65',
shift: '25',
skip_checks: '1',
});
});
});
describe('right shift', () => {
const b = 49;
const shift = 24;
let circuit: WasmTester<['x'], ['y']>;
before(async () => {
circuit = await WasmTester.new(`shr_${b}`, {
file: 'float_add',
template: 'RightShift',
dir: 'test/float_add',
params: [b, shift],
});
await circuit.checkConstraintCount(b);
});
it('should pass - small bitwidth', async () => {
await circuit.expectPass({x: '82263136010365'}, {y: '4903265'});
});
it('should fail - large bitwidth', async () => {
await circuit.expectFail({x: '15087340228765024367'});
});
});
describe('normalize', () => {
const k = 8;
const p = 23;
const P = 47;
let circuit: WasmTester<['e', 'm', 'skip_checks'], ['e_out', 'm_out']>;
before(async () => {
circuit = await WasmTester.new(`normalize_${k}_${p}_${P}`, {
file: 'float_add',
template: 'Normalize',
params: [k, p, P],
dir: 'test/float_add',
});
await circuit.checkConstraintCount(expectedConstraints.normalize(P));
});
it("should pass - don't skip checks", async () => {
await circuit.expectPass(
{
e: '100',
m: '20565784002591',
skip_checks: '0',
},
{e_out: '121', m_out: '164526272020728'}
);
});
it("should pass - already normalized and don't skip checks", async () => {
await circuit.expectPass(
{
e: '100',
m: '164526272020728',
skip_checks: '0',
},
{e_out: '124', m_out: '164526272020728'}
);
});
it("should fail when m = 0 - don't skip checks", async () => {
await circuit.expectFail({
e: '100',
m: '0',
skip_checks: '0',
});
});
it('should pass when skip_checks = 1 and m is 0', async () => {
await circuit.expectPass({
e: '100',
m: '0',
skip_checks: '1',
});
});
});
describe('msnzb', () => {
const b = 48;
let circuit: WasmTester<['in', 'skip_checks'], ['one_hot']>;
before(async () => {
circuit = await WasmTester.new(`msnzb_${b}`, {
file: 'float_add',
template: 'MSNZB',
dir: 'test/float_add',
params: [b],
});
await circuit.checkConstraintCount(expectedConstraints.msnzb(b));
});
it("should pass test 1 - don't skip checks", async () => {
await circuit.expectPass(
{in: '1', skip_checks: '0'},
{
// prettier-ignore
one_hot: ['1', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'],
}
);
});
it("should pass test 2 - don't skip checks", async () => {
await circuit.expectPass(
{in: '281474976710655', skip_checks: '0'},
{
// prettier-ignore
one_hot: ['0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1'],
}
);
});
it("should fail when in = 0 - don't skip checks", async () => {
await circuit.expectFail({in: '0', skip_checks: '0'});
});
it('should pass when skip_checks = 1 and in is 0', async () => {
await circuit.expectPass({in: '0', skip_checks: '1'});
});
});
});

View File

@@ -1,19 +1,23 @@
import {FullProof, ProofTester, WasmTester} from '../src';
import {Circomkit, CircuitConfig, FullProof, ProofTester, WasmTester} from '../src';
function createConfig(n: number): CircuitConfig {
return {
file: 'multiplier',
template: 'Multiplier',
params: [n],
};
}
const N = 3;
const circuitName = `multiplier_${N}`;
const circomkit = new Circomkit();
describe('multiplier', () => {
const N = 3;
let circuit: WasmTester<['in'], ['out']>;
before(async () => {
circuit = await WasmTester.new(`multiplier_${N}`, {
file: 'multiplier',
template: 'Multiplier',
params: [N],
});
// constraint count checks!
await circuit.checkConstraintCount(N - 1);
circuit = await circomkit.WasmTester(circuitName, createConfig(N));
await circuit.checkConstraintCount(N);
});
it('should multiply correctly', async () => {
@@ -22,31 +26,12 @@ describe('multiplier', () => {
});
});
describe('multiplier utilities', () => {
describe('multiplication gate', () => {
let circuit: WasmTester<['in'], ['out']>;
before(async () => {
circuit = await WasmTester.new('mulgate', {
file: 'multiplier',
template: 'MultiplicationGate',
dir: 'test/multiplier',
});
});
it('should multiply correctly', async () => {
await circuit.expectPass({in: [7, 5]}, {out: 7 * 5});
});
});
});
describe.skip('multiplier proofs', () => {
const N = 3;
describe('multiplier proofs', () => {
let fullProof: FullProof;
let circuit: ProofTester<['in']>;
before(async () => {
const circuitName = 'multiplier_' + N;
circuit = new ProofTester(circuitName);
circuit = await circomkit.ProofTester(circuitName);
fullProof = await circuit.prove({
in: Array.from({length: N}, () => Math.floor(Math.random() * 100 * N)),
});

View File

@@ -1,30 +0,0 @@
import {expect} from 'chai';
import {WasmTester} from '../src';
import {randomBytes, createHash} from 'crypto';
describe('sha256', () => {
let circuit: WasmTester<['in'], ['out']>;
const NUM_BYTES = 36;
const BYTES = randomBytes(NUM_BYTES);
const LOCAL_HASH = createHash('sha256').update(BYTES).digest('hex');
const INPUT = {
in: BYTES.toJSON().data,
};
before(async () => {
circuit = await WasmTester.new('sha256', {
file: 'sha256',
template: 'Sha256Bytes',
params: [NUM_BYTES],
});
await circuit.checkConstraintCount();
});
it('should compute hash correctly', async () => {
const {out} = await circuit.compute(INPUT, ['out']);
const outputBytes = (out as bigint[]).map(b => parseInt(b.toString()));
const outputHex = Buffer.from(outputBytes).toString('hex');
expect(outputHex).to.eq(LOCAL_HASH);
});
});

View File

@@ -1,202 +0,0 @@
import {WasmTester, CircuitSignals} from '../src';
type BoardSizes = 4 | 9;
const INPUTS: {[N in BoardSizes]: CircuitSignals<['solution', 'puzzle']>} = {
9: {
solution: [
[1, 9, 4, 8, 6, 5, 2, 3, 7],
[7, 3, 5, 4, 1, 2, 9, 6, 8],
[8, 6, 2, 3, 9, 7, 1, 4, 5],
[9, 2, 1, 7, 4, 8, 3, 5, 6],
[6, 7, 8, 5, 3, 1, 4, 2, 9],
[4, 5, 3, 9, 2, 6, 8, 7, 1],
[3, 8, 9, 6, 5, 4, 7, 1, 2],
[2, 4, 6, 1, 7, 9, 5, 8, 3],
[5, 1, 7, 2, 8, 3, 6, 9, 4],
],
puzzle: [
[0, 0, 0, 8, 6, 0, 2, 3, 0],
[7, 0, 5, 0, 0, 0, 9, 0, 8],
[0, 6, 0, 3, 0, 7, 0, 4, 0],
[0, 2, 0, 7, 0, 8, 0, 5, 0],
[0, 7, 8, 5, 0, 0, 0, 0, 0],
[4, 0, 0, 9, 0, 6, 0, 7, 0],
[3, 0, 9, 0, 5, 0, 7, 0, 2],
[0, 4, 0, 1, 0, 9, 0, 8, 0],
[5, 0, 7, 0, 8, 0, 0, 9, 4],
],
},
4: {
solution: [
[4, 1, 3, 2],
[3, 2, 4, 1],
[2, 4, 1, 3],
[1, 3, 2, 4],
],
puzzle: [
[0, 1, 0, 2],
[3, 2, 0, 0],
[0, 0, 1, 0],
[1, 0, 0, 0],
],
},
};
([4, 9] as BoardSizes[]).map(N =>
describe(`sudoku (${N} by ${N})`, () => {
const INPUT = INPUTS[N];
let circuit: WasmTester<['solution', 'puzzle']>;
before(async () => {
circuit = await WasmTester.new(`sudoku_${N}x${N}`, {
file: 'sudoku',
template: 'Sudoku',
pubs: ['puzzle'],
params: [Math.sqrt(N)],
});
await circuit.checkConstraintCount();
});
it('should compute correctly', async () => {
await circuit.expectPass(INPUT);
});
it('should NOT accept non-distinct rows', async () => {
const badInput = JSON.parse(JSON.stringify(INPUT));
badInput.solution[0][0] = badInput.solution[0][1];
await circuit.expectFail(badInput);
});
it('should NOT accept non-distinct columns', async () => {
const badInput = JSON.parse(JSON.stringify(INPUT));
badInput.solution[0][0] = badInput.solution[1][0];
await circuit.expectFail(badInput);
});
it('should NOT accept non-distinct square', async () => {
const badInput = JSON.parse(JSON.stringify(INPUT));
badInput.solution[0][0] = badInput.solution[1][1];
await circuit.expectFail(badInput);
});
it('should NOT accept empty value in solution', async () => {
const badInput = JSON.parse(JSON.stringify(INPUT));
badInput.solution[0][0] = 0;
await circuit.expectFail(badInput);
});
it('should NOT accept out-of-range values', async () => {
const badInput = JSON.parse(JSON.stringify(INPUT));
badInput.solution[0][0] = 99999;
await circuit.expectFail(badInput);
});
})
);
describe('sudoku utilities', () => {
describe('assert bit length', () => {
const b = 3; // bit count
let circuit: WasmTester<['in'], []>;
before(async () => {
circuit = await WasmTester.new(`bitlen_${b}`, {
file: 'sudoku',
template: 'AssertBitLength',
dir: 'test/sudoku',
params: [b],
});
});
it('should pass for input < 2^b', async () => {
await circuit.expectPass({
in: 2 ** b - 1,
});
});
it('should fail for input ≥ 2^b ', async () => {
await circuit.expectFail({
in: 2 ** b,
});
await circuit.expectFail({
in: 2 ** b + 1,
});
});
});
describe('distinct', () => {
const n = 3;
let circuit: WasmTester<['in'], []>;
before(async () => {
circuit = await WasmTester.new(`distinct_${n}`, {
file: 'sudoku',
template: 'Distinct',
dir: 'test/sudoku',
params: [n],
});
});
it('should pass if all inputs are unique', async () => {
await circuit.expectPass({
in: Array(n)
.fill(0)
.map((v, i) => v + i),
});
});
it('should fail if there is a duplicate', async () => {
const arr = Array(n)
.fill(0)
.map((v, i) => v + i);
// make a duplicate
arr[0] = arr[arr.length - 1];
await circuit.expectFail({
in: arr,
});
});
});
describe('in range', () => {
const MIN = 1;
const MAX = 9;
let circuit: WasmTester<['in'], []>;
before(async () => {
circuit = await WasmTester.new(`inRange_${MIN}_${MAX}`, {
file: 'sudoku',
template: 'InRange',
dir: 'test/sudoku',
params: [MIN, MAX],
});
});
it('should pass for in range', async () => {
await circuit.expectPass({
in: MAX,
});
await circuit.expectPass({
in: MIN,
});
await circuit.expectPass({
in: Math.floor((MIN + MAX) / 2),
});
});
it('should FAIL for out of range (upper bound)', async () => {
await circuit.expectFail({
in: MAX + 1,
});
});
it('should FAIL for out of range (lower bound)', async () => {
if (MIN > 0) {
await circuit.expectFail({
in: MIN - 1,
});
}
});
});
});