mirror of
https://github.com/Rate-Limiting-Nullifier/circom-rln.git
synced 2026-01-08 23:17:59 -05:00
Merge pull request #5 from Rate-Limiting-Nullifier/test/add-tests
Tests & CI
This commit is contained in:
35
.github/workflows/test.yml
vendored
Normal file
35
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# This workflow test if circuits can be built and the tests pass.
|
||||
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
- name: Cache circom
|
||||
id: cache-circom
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cargo/bin/circom
|
||||
# Since the version of circom is specified in `scripts/install-circom.sh`,
|
||||
# as long as the file doesn't change we can reuse the circom binary.
|
||||
key: ${{ runner.os }}-circom-${{ hashFiles('./scripts/install-circom.sh') }}
|
||||
- name: Install circom if not cached
|
||||
run: ./scripts/install-circom.sh
|
||||
- run: npm ci
|
||||
- name: Build all circuits
|
||||
run: npm run build
|
||||
- name: Run the tests
|
||||
run: npm test
|
||||
2602
package-lock.json
generated
2602
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -1,9 +1,20 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "bash scripts/build-circuits.sh"
|
||||
"build": "bash scripts/build-circuits.sh same && bash scripts/build-circuits.sh diff && bash scripts/build-circuits.sh withdraw",
|
||||
"test": "ts-mocha --exit test/**/*.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"circomlib": "^2.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"circomlib": "^2.0.5",
|
||||
"snarkjs": "^0.4.22"
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/node": "^18.14.6",
|
||||
"@zk-kit/incremental-merkle-tree": "^1.0.0",
|
||||
"circom_tester": "^0.0.19",
|
||||
"mocha": "^10.2.0",
|
||||
"poseidon-lite": "^0.0.2",
|
||||
"snarkjs": "^0.6.7",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
zkeypath="../zkeyFiles"
|
||||
mkdir -p ../build/contracts
|
||||
mkdir -p ../build/setup
|
||||
mkdir -p $zkeypath
|
||||
|
||||
# Build context
|
||||
cd ../build
|
||||
@@ -19,21 +17,26 @@ else
|
||||
wget https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_14.ptau
|
||||
fi
|
||||
|
||||
circuit_dir="../circuits"
|
||||
circuit_path=""
|
||||
circuit_type=""
|
||||
zkeydir="../zkeyFiles"
|
||||
|
||||
if [ "$1" = "diff" ]; then
|
||||
echo -e "\033[32mUsing Diff circuit\033[0m"
|
||||
circuit_type="diff"
|
||||
circuit_path="../circuits/rln-diff.circom"
|
||||
circuit_name="rln-diff"
|
||||
elif [ "$1" = "same" ]; then
|
||||
echo -e "\033[32mUsing Same circuit\033[0m"
|
||||
circuit_type="same"
|
||||
circuit_path="../circuits/rln-same.circom"
|
||||
circuit_name="rln-same"
|
||||
elif [ "$1" = "withdraw" ]; then
|
||||
echo -e "\033[32mUsing Withdraw circuit\033[0m"
|
||||
circuit_name="withdraw"
|
||||
else
|
||||
circuit_type="same"
|
||||
circuit_path="../circuits/rln-same.circom"
|
||||
echo -e "\033[33mUnrecognized argument, using 'same' as default.\033[0m"
|
||||
circuit_name="rln-same"
|
||||
fi
|
||||
circuit_path="$circuit_dir/$circuit_name.circom"
|
||||
zkeypath="$zkeydir/v2/$circuit_name"
|
||||
|
||||
if ! [ -x "$(command -v circom)" ]; then
|
||||
echo -e '\033[31mError: circom is not installed.\033[0m' >&2
|
||||
@@ -51,11 +54,11 @@ echo -e "\033[36mBuild Path: $PWD\033[0m"
|
||||
circom --version
|
||||
circom $circuit_path --r1cs --wasm --sym
|
||||
|
||||
snarkjs r1cs export json rln-same.r1cs rln-same.r1cs.json
|
||||
snarkjs r1cs export json $circuit_name.r1cs $circuit_name.r1cs.json
|
||||
|
||||
echo -e "\033[36mRunning groth16 trusted setup\033[0m"
|
||||
|
||||
snarkjs groth16 setup rln-same.r1cs powersOfTau28_hez_final_14.ptau setup/rln_0000.zkey
|
||||
snarkjs groth16 setup $circuit_name.r1cs powersOfTau28_hez_final_14.ptau setup/rln_0000.zkey
|
||||
|
||||
snarkjs zkey contribute setup/rln_0000.zkey setup/rln_0001.zkey --name="First contribution" -v -e="Random entropy"
|
||||
snarkjs zkey contribute setup/rln_0001.zkey setup/rln_0002.zkey --name="Second contribution" -v -e="Another random entropy"
|
||||
@@ -63,10 +66,11 @@ snarkjs zkey beacon setup/rln_0002.zkey setup/rln_final.zkey 0102030405060708090
|
||||
|
||||
echo -e "Exporting artifacts to zkeyFiles and contracts directory"
|
||||
|
||||
mkdir -p $zkeypath
|
||||
snarkjs zkey export verificationkey setup/rln_final.zkey $zkeypath/verification_key.json
|
||||
snarkjs zkey export solidityverifier setup/rln_final.zkey contracts/verifier.sol
|
||||
|
||||
cp rln-$circuit_type\_js/rln-$circuit_type.wasm $zkeypath/rln.wasm
|
||||
cp $circuit_name\_js/$circuit_name.wasm $zkeypath/rln.wasm
|
||||
cp setup/rln_final.zkey $zkeypath/rln_final.zkey
|
||||
|
||||
shasumcmd="shasum -a 256"
|
||||
@@ -74,7 +78,7 @@ shasumcmd="shasum -a 256"
|
||||
config_path="$zkeypath/circuit.config.toml"
|
||||
echo -e "[Circuit_Version]" > $config_path
|
||||
echo -e "RLN_Version = 2" >> $config_path
|
||||
echo -e "RLN_Type = \"$circuit_type\"" >> $config_path
|
||||
echo -e "RLN_Type = \"$circuit_name\"" >> $config_path
|
||||
|
||||
echo -e "" >> $config_path
|
||||
|
||||
|
||||
11
scripts/install-circom.sh
Executable file
11
scripts/install-circom.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
circom_version=v2.1.5
|
||||
|
||||
if ! [ -x "$(command -v circom)" ]; then
|
||||
git clone https://github.com/iden3/circom.git
|
||||
cd circom
|
||||
git checkout $circom_version
|
||||
cargo build --release
|
||||
cargo install --path circom
|
||||
fi
|
||||
5
test/configs.ts
Normal file
5
test/configs.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as path from "path";
|
||||
|
||||
// MERKLE TREE
|
||||
export const MERKLE_TREE_DEPTH = 20;
|
||||
export const MERKLE_TREE_ZERO_VALUE = BigInt(0);
|
||||
99
test/rln-diff.test.ts
Normal file
99
test/rln-diff.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import * as path from "path";
|
||||
import assert from "assert";
|
||||
const tester = require("circom_tester").wasm;
|
||||
import poseidon from "poseidon-lite";
|
||||
import { calculateOutput, genFieldElement, genMerkleProof, getSignal } from "./utils"
|
||||
|
||||
|
||||
const circuitPath = path.join(__dirname, "..", "circuits", "rln-diff.circom");
|
||||
|
||||
// ffjavascript has no types so leave circuit with untyped
|
||||
type CircuitT = any;
|
||||
|
||||
|
||||
function calculateLeaf(identitySecret: bigint, userMessageLimit: bigint) {
|
||||
const identityCommitment = poseidon([identitySecret])
|
||||
const rateCommitment = poseidon([identityCommitment, userMessageLimit])
|
||||
return rateCommitment
|
||||
}
|
||||
|
||||
|
||||
describe("Test rln-diff.circom", function () {
|
||||
let circuit: CircuitT;
|
||||
|
||||
this.timeout(30000);
|
||||
|
||||
before(async function () {
|
||||
circuit = await tester(circuitPath);
|
||||
});
|
||||
|
||||
it("Should generate witness with correct outputs", async () => {
|
||||
// Public inputs
|
||||
const x = genFieldElement();
|
||||
const externalNullifier = genFieldElement();
|
||||
// Private inputs
|
||||
const identitySecret = genFieldElement();
|
||||
const userMessageLimit = BigInt(10)
|
||||
const leaf = calculateLeaf(identitySecret, userMessageLimit)
|
||||
const merkleProof = genMerkleProof([leaf], 0)
|
||||
const merkleRoot = merkleProof.root
|
||||
const messageId = userMessageLimit - BigInt(1)
|
||||
|
||||
const inputs = {
|
||||
// Private inputs
|
||||
identitySecret,
|
||||
userMessageLimit,
|
||||
messageId,
|
||||
pathElements: merkleProof.siblings,
|
||||
identityPathIndex: merkleProof.pathIndices,
|
||||
// Public inputs
|
||||
x,
|
||||
externalNullifier,
|
||||
}
|
||||
|
||||
// Test: should generate proof if inputs are correct
|
||||
const witness: bigint[] = await circuit.calculateWitness(inputs, true);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
const {y, nullifier} = calculateOutput(identitySecret, x, externalNullifier, messageId)
|
||||
|
||||
const outputRoot = await getSignal(circuit, witness, "root")
|
||||
const outputY = await getSignal(circuit, witness, "y")
|
||||
const outputNullifier = await getSignal(circuit, witness, "nullifier")
|
||||
|
||||
assert.equal(outputY, y)
|
||||
assert.equal(outputRoot, merkleRoot)
|
||||
assert.equal(outputNullifier, nullifier)
|
||||
});
|
||||
|
||||
it("should fail to generate witness if messageId is not in range [1, userMessageLimit]", async function () {
|
||||
// Public inputs
|
||||
const x = genFieldElement();
|
||||
const externalNullifier = genFieldElement();
|
||||
// Private inputs
|
||||
const identitySecret = genFieldElement();
|
||||
const identitySecretCommitment = poseidon([identitySecret]);
|
||||
const merkleProof = genMerkleProof([identitySecretCommitment], 0)
|
||||
const userMessageLimit = BigInt(10)
|
||||
// valid message id is in the range [1, userMessageLimit]
|
||||
const invalidMessageIds = [BigInt(0), userMessageLimit + BigInt(1)]
|
||||
|
||||
for (const invalidMessageId of invalidMessageIds) {
|
||||
const inputs = {
|
||||
// Private inputs
|
||||
identitySecret,
|
||||
userMessageLimit,
|
||||
messageId: invalidMessageId,
|
||||
pathElements: merkleProof.siblings,
|
||||
identityPathIndex: merkleProof.pathIndices,
|
||||
// Public inputs
|
||||
x,
|
||||
externalNullifier,
|
||||
}
|
||||
await assert.rejects(async () => {
|
||||
await circuit.calculateWitness(inputs, true);
|
||||
}, /Error: Assert Failed/);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
91
test/rln-same.test.ts
Normal file
91
test/rln-same.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import * as path from "path";
|
||||
import assert from "assert";
|
||||
const tester = require("circom_tester").wasm;
|
||||
import poseidon from "poseidon-lite";
|
||||
import { calculateOutput, genFieldElement, genMerkleProof, getSignal } from "./utils";
|
||||
|
||||
const circuitPath = path.join(__dirname, "..", "circuits", "rln-same.circom");
|
||||
|
||||
// ffjavascript has no types so leave circuit with untyped
|
||||
type CircuitT = any;
|
||||
|
||||
|
||||
describe("Test rln-diff.circom", function () {
|
||||
let circuit: CircuitT;
|
||||
|
||||
this.timeout(30000);
|
||||
|
||||
before(async function () {
|
||||
circuit = await tester(circuitPath);
|
||||
});
|
||||
|
||||
it("Should generate witness with correct outputs", async () => {
|
||||
// Public inputs
|
||||
const x = genFieldElement();
|
||||
const externalNullifier = genFieldElement();
|
||||
// Private inputs
|
||||
const identitySecret = genFieldElement();
|
||||
const identitySecretCommitment = poseidon([identitySecret]);
|
||||
const merkleProof = genMerkleProof([identitySecretCommitment], 0)
|
||||
const merkleRoot = merkleProof.root
|
||||
const messageLimit = BigInt(10)
|
||||
const messageId = BigInt(1)
|
||||
|
||||
const inputs = {
|
||||
// Private inputs
|
||||
identitySecret,
|
||||
messageId,
|
||||
pathElements: merkleProof.siblings,
|
||||
identityPathIndex: merkleProof.pathIndices,
|
||||
// Public inputs
|
||||
x,
|
||||
externalNullifier,
|
||||
messageLimit,
|
||||
}
|
||||
|
||||
// Test: should generate proof if inputs are correct
|
||||
const witness: bigint[] = await circuit.calculateWitness(inputs, true);
|
||||
await circuit.checkConstraints(witness);
|
||||
|
||||
const {y, nullifier} = calculateOutput(identitySecret, x, externalNullifier, messageId)
|
||||
|
||||
const outputRoot = await getSignal(circuit, witness, "root")
|
||||
const outputY = await getSignal(circuit, witness, "y")
|
||||
const outputNullifier = await getSignal(circuit, witness, "nullifier")
|
||||
|
||||
assert.equal(outputY, y)
|
||||
assert.equal(outputRoot, merkleRoot)
|
||||
assert.equal(outputNullifier, nullifier)
|
||||
});
|
||||
|
||||
it("should fail to generate witness if messageId is not in range [1, messageLimit]", async function () {
|
||||
// Public inputs
|
||||
const x = genFieldElement();
|
||||
const externalNullifier = genFieldElement();
|
||||
// Private inputs
|
||||
const identitySecret = genFieldElement();
|
||||
const identitySecretCommitment = poseidon([identitySecret]);
|
||||
const merkleProof = genMerkleProof([identitySecretCommitment], 0)
|
||||
const messageLimit = BigInt(10)
|
||||
// valid message id is in the range [1, messageLimit]
|
||||
const invalidMessageIds = [BigInt(0), messageLimit + BigInt(1)]
|
||||
|
||||
for (const invalidMessageId of invalidMessageIds) {
|
||||
const inputs = {
|
||||
// Private inputs
|
||||
identitySecret,
|
||||
messageId: invalidMessageId,
|
||||
pathElements: merkleProof.siblings,
|
||||
identityPathIndex: merkleProof.pathIndices,
|
||||
// Public inputs
|
||||
x,
|
||||
externalNullifier,
|
||||
messageLimit,
|
||||
}
|
||||
await assert.rejects(async () => {
|
||||
await circuit.calculateWitness(inputs, true);
|
||||
}, /Error: Assert Failed/);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
50
test/utils.ts
Normal file
50
test/utils.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { IncrementalMerkleTree } from "@zk-kit/incremental-merkle-tree";
|
||||
import poseidon from "poseidon-lite";
|
||||
const ffjavascript = require("ffjavascript");
|
||||
|
||||
import { MERKLE_TREE_DEPTH, MERKLE_TREE_ZERO_VALUE } from "./configs";
|
||||
|
||||
|
||||
// ffjavascript has no types so leave circuit with untyped
|
||||
type CircuitT = any;
|
||||
|
||||
const SNARK_FIELD_SIZE = BigInt('21888242871839275222246405745257275088548364400416034343698204186575808495617')
|
||||
const F = new ffjavascript.ZqField(SNARK_FIELD_SIZE);
|
||||
|
||||
export function genFieldElement() {
|
||||
return F.random()
|
||||
}
|
||||
|
||||
export function genMerkleProof(elements: BigInt[], leafIndex: number) {
|
||||
const tree = new IncrementalMerkleTree(poseidon, MERKLE_TREE_DEPTH, MERKLE_TREE_ZERO_VALUE, 2);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
tree.insert(elements[i]);
|
||||
}
|
||||
const merkleProof = tree.createProof(leafIndex)
|
||||
merkleProof.siblings = merkleProof.siblings.map((s) => s[0])
|
||||
return merkleProof;
|
||||
}
|
||||
|
||||
export function calculateOutput(identitySecret: bigint, x: bigint, externalNullifier: bigint, messageId: bigint) {
|
||||
// signal a1 <== Poseidon(3)([identitySecret, externalNullifier, messageId]);
|
||||
const a1 = poseidon([identitySecret, externalNullifier, messageId]);
|
||||
// y <== identitySecret + a1 * x;
|
||||
const y = F.normalize(identitySecret + a1 * x);
|
||||
const nullifier = poseidon([a1]);
|
||||
return {y, nullifier}
|
||||
}
|
||||
|
||||
|
||||
export async function getSignal(circuit: CircuitT, witness: bigint[], name: string) {
|
||||
const prefix = "main"
|
||||
// E.g. the full name of the signal "root" is "main.root"
|
||||
// You can look up the signal names using `circuit.getDecoratedOutput(witness))`
|
||||
const signalFullName = `${prefix}.${name}`
|
||||
await circuit.loadSymbols()
|
||||
// symbols[n] = { labelIdx: 1, varIdx: 1, componentIdx: 142 },
|
||||
const signalMeta = circuit.symbols[signalFullName]
|
||||
// Assigned value of the signal is located in the `varIdx`th position
|
||||
// of the witness array
|
||||
const indexInWitness = signalMeta.varIdx
|
||||
return BigInt(witness[indexInWitness]);
|
||||
}
|
||||
34
test/withdraw.test.ts
Normal file
34
test/withdraw.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as path from "path";
|
||||
import assert from "assert";
|
||||
const tester = require("circom_tester").wasm;
|
||||
import poseidon from "poseidon-lite";
|
||||
import { genFieldElement, getSignal } from "./utils";
|
||||
|
||||
const circuitPath = path.join(__dirname, "..", "circuits", "withdraw.circom");
|
||||
|
||||
// ffjavascript has no types so leave circuit with untyped
|
||||
type CircuitT = any;
|
||||
|
||||
|
||||
describe("Test withdraw.circom", function () {
|
||||
let circuit: CircuitT;
|
||||
|
||||
this.timeout(30000);
|
||||
|
||||
before(async function () {
|
||||
circuit = await tester(circuitPath);
|
||||
});
|
||||
|
||||
it("Should generate witness with correct outputs", async () => {
|
||||
// Private inputs
|
||||
const identitySecret = genFieldElement();
|
||||
// Public inputs
|
||||
const addressHash = genFieldElement();
|
||||
// Test: should generate proof if inputs are correct
|
||||
const witness: bigint[] = await circuit.calculateWitness({identitySecret, addressHash}, true);
|
||||
await circuit.checkConstraints(witness);
|
||||
const expectedIdentityCommitment = poseidon([identitySecret])
|
||||
const outputIdentityCommitment = await getSignal(circuit, witness, "identityCommitment")
|
||||
assert.equal(outputIdentityCommitment, expectedIdentityCommitment)
|
||||
});
|
||||
});
|
||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"types": ["mocha", "node"]
|
||||
},
|
||||
"include": ["test/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user