diff --git a/README.md b/README.md index 5536175..b291259 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,14 @@ If you clone or link this repo as `opencircom` in your project root, compile wit circom your.circom --r1cs --wasm -o build -l opencircom/circuits ``` +## Compatibility + +Poseidon compatibility with circomlib is covered by `test/poseidon_compat_test.js`. +The suite compares opencircom and circomlib wrapper circuits for `nInputs = 1, 2, 4, 16` +against fixed witness vectors. `circomlib` is a dev-only test dependency; the published +opencircom circuits remain dependency-free. No intentional Poseidon output differences +are documented. + ## Include in Hardhat or Foundry ### Install diff --git a/package-lock.json b/package-lock.json index 686bf58..4799a6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,18 @@ "name": "opencircom", "version": "0.7.0", "license": "MIT", + "bin": { + "opencircom": "bin/opencircom.js" + }, "devDependencies": { "chai": "^4.3.10", "circom_tester": "^0.0.21", + "circomlib": "^2.0.5", "mocha": "^10.7.3", "snarkjs": "^0.7.5" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@iden3/bigarray": { @@ -396,6 +403,13 @@ "util": "^0.12.5" } }, + "node_modules/circomlib": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/circomlib/-/circomlib-2.0.5.tgz", + "integrity": "sha512-O7NQ8OS+J4eshBuoy36z/TwQU0YHw8W3zxZcs4hVwpEll3e4hDm3mgkIPqItN8FDeLEKZFK3YeT/+k8TiLF3/A==", + "dev": true, + "license": "GPL-3.0" + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", diff --git a/package.json b/package.json index cf3b897..a46363a 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "devDependencies": { "chai": "^4.3.10", "circom_tester": "^0.0.21", + "circomlib": "^2.0.5", "mocha": "^10.7.3", "snarkjs": "^0.7.5" } diff --git a/test/circuits/poseidon16_test.circom b/test/circuits/poseidon16_test.circom new file mode 100644 index 0000000..2def806 --- /dev/null +++ b/test/circuits/poseidon16_test.circom @@ -0,0 +1,15 @@ +pragma circom 2.0.0; + +include "../../circuits/hashing/poseidon.circom"; + +template Poseidon16Test() { + signal input in[16]; + signal output out; + component p = Poseidon(16); + for (var i = 0; i < 16; i++) { + p.inputs[i] <== in[i]; + } + out <== p.out; +} + +component main = Poseidon16Test(); diff --git a/test/circuits/poseidon1_test.circom b/test/circuits/poseidon1_test.circom new file mode 100644 index 0000000..2b5d5e9 --- /dev/null +++ b/test/circuits/poseidon1_test.circom @@ -0,0 +1,13 @@ +pragma circom 2.0.0; + +include "../../circuits/hashing/poseidon.circom"; + +template Poseidon1Test() { + signal input in[1]; + signal output out; + component p = Poseidon(1); + p.inputs[0] <== in[0]; + out <== p.out; +} + +component main = Poseidon1Test(); diff --git a/test/circuits/poseidon_circomlib_16_test.circom b/test/circuits/poseidon_circomlib_16_test.circom new file mode 100644 index 0000000..0fa8189 --- /dev/null +++ b/test/circuits/poseidon_circomlib_16_test.circom @@ -0,0 +1,15 @@ +pragma circom 2.0.0; + +include "../../node_modules/circomlib/circuits/poseidon.circom"; + +template CircomlibPoseidon16Test() { + signal input in[16]; + signal output out; + component p = Poseidon(16); + for (var i = 0; i < 16; i++) { + p.inputs[i] <== in[i]; + } + out <== p.out; +} + +component main = CircomlibPoseidon16Test(); diff --git a/test/circuits/poseidon_circomlib_1_test.circom b/test/circuits/poseidon_circomlib_1_test.circom new file mode 100644 index 0000000..6c1ae40 --- /dev/null +++ b/test/circuits/poseidon_circomlib_1_test.circom @@ -0,0 +1,13 @@ +pragma circom 2.0.0; + +include "../../node_modules/circomlib/circuits/poseidon.circom"; + +template CircomlibPoseidon1Test() { + signal input in[1]; + signal output out; + component p = Poseidon(1); + p.inputs[0] <== in[0]; + out <== p.out; +} + +component main = CircomlibPoseidon1Test(); diff --git a/test/circuits/poseidon_circomlib_2_test.circom b/test/circuits/poseidon_circomlib_2_test.circom new file mode 100644 index 0000000..4b9e27b --- /dev/null +++ b/test/circuits/poseidon_circomlib_2_test.circom @@ -0,0 +1,15 @@ +pragma circom 2.0.0; + +include "../../node_modules/circomlib/circuits/poseidon.circom"; + +template CircomlibPoseidon2Test() { + signal input in[2]; + signal output out; + component p = Poseidon(2); + for (var i = 0; i < 2; i++) { + p.inputs[i] <== in[i]; + } + out <== p.out; +} + +component main = CircomlibPoseidon2Test(); diff --git a/test/circuits/poseidon_circomlib_4_test.circom b/test/circuits/poseidon_circomlib_4_test.circom new file mode 100644 index 0000000..88fe2ca --- /dev/null +++ b/test/circuits/poseidon_circomlib_4_test.circom @@ -0,0 +1,15 @@ +pragma circom 2.0.0; + +include "../../node_modules/circomlib/circuits/poseidon.circom"; + +template CircomlibPoseidon4Test() { + signal input in[4]; + signal output out; + component p = Poseidon(4); + for (var i = 0; i < 4; i++) { + p.inputs[i] <== in[i]; + } + out <== p.out; +} + +component main = CircomlibPoseidon4Test(); diff --git a/test/poseidon_compat_test.js b/test/poseidon_compat_test.js new file mode 100644 index 0000000..a4da437 --- /dev/null +++ b/test/poseidon_compat_test.js @@ -0,0 +1,65 @@ +const chai = require("chai"); +const path = require("path"); +const wasm_tester = require("circom_tester").wasm; + +const assert = chai.assert; + +const OPEN_CIRCUITS = { + 1: "poseidon1_test.circom", + 2: "poseidon2_test.circom", + 4: "poseidon4_test.circom", + 16: "poseidon16_test.circom", +}; + +const CIRCOMLIB_CIRCUITS = { + 1: "poseidon_circomlib_1_test.circom", + 2: "poseidon_circomlib_2_test.circom", + 4: "poseidon_circomlib_4_test.circom", + 16: "poseidon_circomlib_16_test.circom", +}; + +function vectorSet(width) { + return [ + Array(width).fill(0), + Array.from({ length: width }, (_, i) => i + 1), + Array.from({ length: width }, (_, i) => String((i + 1) * 17)), + ]; +} + +describe("Poseidon compatibility with circomlib", function () { + const circuits = {}; + this.timeout(120000); + + before(async () => { + for (const width of Object.keys(OPEN_CIRCUITS)) { + circuits[width] = { + opencircom: await wasm_tester(path.join(__dirname, "circuits", OPEN_CIRCUITS[width]), { + output: path.join(__dirname, "..", "build"), + recompile: false, + }), + circomlib: await wasm_tester(path.join(__dirname, "circuits", CIRCOMLIB_CIRCUITS[width]), { + output: path.join(__dirname, "..", "build"), + recompile: false, + }), + }; + } + }); + + for (const width of [1, 2, 4, 16]) { + it(`matches circomlib Poseidon(${width}) reference outputs`, async () => { + for (const inputs of vectorSet(width)) { + const opencircomWitness = await circuits[width].opencircom.calculateWitness({ in: inputs }, true); + const circomlibWitness = await circuits[width].circomlib.calculateWitness({ in: inputs }, true); + + await circuits[width].opencircom.checkConstraints(opencircomWitness); + await circuits[width].circomlib.checkConstraints(circomlibWitness); + + assert.equal( + opencircomWitness[1].toString(), + circomlibWitness[1].toString(), + `Poseidon(${width}) mismatch for ${JSON.stringify(inputs)}` + ); + } + }); + } +});