Merge pull request #5 from Rate-Limiting-Nullifier/test/add-tests

Tests & CI
This commit is contained in:
Magamedrasul Ibragimov
2023-04-02 13:07:15 +04:00
committed by GitHub
11 changed files with 2882 additions and 105 deletions

35
.github/workflows/test.yml vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"]
}