From 71b447002cf1a1de745351d77545bd12182da3b4 Mon Sep 17 00:00:00 2001 From: Sambhav Date: Fri, 23 Aug 2024 12:30:19 +0530 Subject: [PATCH] feat: substring matching (#17) * add optimised substring search * resolve random input bug using poseidon hash * import circomlib and remove circuits * add array and hash circuits * add search * add js poseidon * update tests * add search tests * refactor tests * change docstyle * add wrong key test * add comment and change input file name * separate utils circuits and tests --- README.md | 8 +- circomkit.json | 5 +- circuits.json | 8 + circuits/extract.circom | 6 +- circuits/parser.circom | 34 +- circuits/search.circom | 242 ++++++++ circuits/test/common/poseidon.ts | 74 +++ circuits/test/search.test.ts | 149 +++++ circuits/test/utils/array.test.ts | 185 ++++++ circuits/test/utils/bytes.test.ts | 25 + circuits/test/utils/hash.test.ts | 50 ++ circuits/test/utils/operators.test.ts | 131 +++++ circuits/test/utils/utils.test.ts | 311 ---------- circuits/utils.circom | 342 ----------- circuits/utils/array.circom | 203 +++++++ circuits/utils/bytes.circom | 22 + circuits/utils/hash.circom | 54 ++ circuits/utils/operators.circom | 151 +++++ inputs/search/witness.json | 803 ++++++++++++++++++++++++++ package-lock.json | 8 + package.json | 1 + tsconfig.json | 8 + 22 files changed, 2144 insertions(+), 676 deletions(-) create mode 100644 circuits/search.circom create mode 100644 circuits/test/common/poseidon.ts create mode 100644 circuits/test/search.test.ts create mode 100644 circuits/test/utils/array.test.ts create mode 100644 circuits/test/utils/bytes.test.ts create mode 100644 circuits/test/utils/hash.test.ts create mode 100644 circuits/test/utils/operators.test.ts delete mode 100644 circuits/test/utils/utils.test.ts delete mode 100644 circuits/utils.circom create mode 100644 circuits/utils/array.circom create mode 100644 circuits/utils/bytes.circom create mode 100644 circuits/utils/hash.circom create mode 100644 circuits/utils/operators.circom create mode 100644 inputs/search/witness.json create mode 100644 tsconfig.json diff --git a/README.md b/README.md index 723bf33..35382e0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # SPARK -> Succinct Parser Attestation for Reconciliation of Knowledge +> Succinct Parser Attestation for Reconciliation of Knowledge ## Repo Structure The repository is currently new and being organized as follows: @@ -26,7 +26,7 @@ npm install -g snarkjs ``` ### Circomkit -You will need `yarn` on your system (brew, or apt-get or something). +You will need `yarn` on your system (brew, or apt-get or something). Then run: `npm install` to get everything else. #### Commands @@ -41,7 +41,7 @@ For example, to compile the extractor, you can: ``` npx circomkit compile extract ``` -Then you can do +Then you can do ``` npx circomkit witness extract witness ``` @@ -69,6 +69,8 @@ To run specific tests, use the `-g` flag for `mocha`, e.g., to run any proof des npx mocha -g State ``` +> [!NOTE] +> Currently [search](./circuits/search.circom) circuit isn't working with circomkit, so you might have to compile using circom: `circom circuits/main/search.circom --r1cs --wasm -l node_modules/ -o build/search/` ## (MOSTLY DEPRECATED DUE TO CIRCOMKIT) Running an example ``` diff --git a/circomkit.json b/circomkit.json index 919f040..85615f8 100644 --- a/circomkit.json +++ b/circomkit.json @@ -1,5 +1,8 @@ { "version": "2.1.9", "proofSystem": "groth16", - "curve": "bn128" + "curve": "bn128", + "includes": [ + "node_modules" + ] } \ No newline at end of file diff --git a/circuits.json b/circuits.json index c931162..59c7f4f 100644 --- a/circuits.json +++ b/circuits.json @@ -62,5 +62,13 @@ 21, 3 ] + }, + "search": { + "file": "search", + "template": "SubstringMatch", + "params": [ + 787, + 10 + ] } } \ No newline at end of file diff --git a/circuits/extract.circom b/circuits/extract.circom index cffeeee..8fa4bdb 100644 --- a/circuits/extract.circom +++ b/circuits/extract.circom @@ -1,6 +1,6 @@ pragma circom 2.1.9; -include "utils.circom"; +include "./utils/bytes.circom"; include "parser.circom"; template Extract(DATA_BYTES, MAX_STACK_HEIGHT) { @@ -10,7 +10,7 @@ template Extract(DATA_BYTES, MAX_STACK_HEIGHT) { //--------------------------------------------------------------------------------------------// //-CONSTRAINTS--------------------------------------------------------------------------------// - //--------------------------------------------------------------------------------------------// + //--------------------------------------------------------------------------------------------// component dataASCII = ASCII(DATA_BYTES); dataASCII.in <== data; //--------------------------------------------------------------------------------------------// @@ -50,4 +50,4 @@ template Extract(DATA_BYTES, MAX_STACK_HEIGHT) { log("State[", DATA_BYTES, "].parsing_string", "= ", State[DATA_BYTES-1].next_parsing_string); log("State[", DATA_BYTES, "].parsing_number", "= ", State[DATA_BYTES-1].next_parsing_number); log("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); -} \ No newline at end of file +} diff --git a/circuits/parser.circom b/circuits/parser.circom index b7dcac6..0f63120 100644 --- a/circuits/parser.circom +++ b/circuits/parser.circom @@ -23,7 +23,9 @@ Tests for this module are located in the files: `circuits/test/parser/*.test.ts pragma circom 2.1.9; -include "utils.circom"; +include "./utils/array.circom"; +include "./utils/bytes.circom"; +include "./utils/operators.circom"; include "language.circom"; /* @@ -46,16 +48,16 @@ This template is for updating the state of the parser from a current state to a template StateUpdate(MAX_STACK_HEIGHT) { signal input byte; // TODO: Does this need to be constrained within here? - signal input stack[MAX_STACK_HEIGHT][2]; + signal input stack[MAX_STACK_HEIGHT][2]; signal input parsing_string; signal input parsing_number; signal output next_stack[MAX_STACK_HEIGHT][2]; signal output next_parsing_string; signal output next_parsing_number; - + component Syntax = Syntax(); - component Command = Command(); + component Command = Command(); //--------------------------------------------------------------------------------------------// // Break down what was read @@ -117,9 +119,9 @@ template StateUpdate(MAX_STACK_HEIGHT) { readQuoteInstruction.array <== Command.QUOTE; component Instruction = GenericArrayAdd(3,8); - Instruction.arrays <== [readStartBraceInstruction.out, readEndBraceInstruction.out, + Instruction.arrays <== [readStartBraceInstruction.out, readEndBraceInstruction.out, readStartBracketInstruction.out, readEndBracketInstruction.out, - readColonInstruction.out, readCommaInstruction.out, + readColonInstruction.out, readCommaInstruction.out, readNumberInstruction.out, readQuoteInstruction.out]; //--------------------------------------------------------------------------------------------// // Apply state changing data @@ -143,7 +145,7 @@ template StateUpdate(MAX_STACK_HEIGHT) { newStack.readEndBracket <== readEndBracket.out; newStack.readColon <== readColon.out; newStack.readComma <== readComma.out; - // * set all the next state of the parser * + // * set all the next state of the parser * next_stack <== newStack.next_stack; next_parsing_string <== parsing_string + mulMaskAndOut.out[1]; next_parsing_number <== parsing_number + mulMaskAndOut.out[2]; @@ -175,9 +177,9 @@ template StateToMask(n) { signal input parsing_string; signal input parsing_number; signal output out[3]; - - // `read_write_value`can change: IF NOT `parsing_string` + + // `read_write_value`can change: IF NOT `parsing_string` out[0] <== (1 - parsing_string); // `parsing_string` can change: @@ -192,12 +194,12 @@ template StateToMask(n) { Above is the binary value for each if is individually enabled This is a total of 2^4 states [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; - [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]; - and the above is what we want to set `next_parsing_number` to given those + [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]; + and the above is what we want to set `next_parsing_number` to given those possible. Below is an optimized version that could instead be done with a `Switch` */ - signal parsingNumberReadDelimeter <== parsing_number * (readDelimeter); + signal parsingNumberReadDelimeter <== parsing_number * (readDelimeter); signal readNumberNotParsingNumber <== (1 - parsing_number) * readNumber; signal notParsingStringAndParsingNumberReadDelimeterOrReadNumberNotParsingNumber <== (1 - parsing_string) * (parsingNumberReadDelimeter + readNumberNotParsingNumber); // 10 above ^^^^^^^^^^^^^^^^^ 4 above ^^^^^^^^^^^^^^^^^^ @@ -219,7 +221,7 @@ This template is for getting the values at the top of the stack as well as the p # Outputs: - `value[2]`: the value at the top of the stack - - `pointer` : the pointer for the top of stack index + - `pointer` : the pointer for the top of stack index */ template GetTopOfStack(n) { signal input stack[n][2]; @@ -259,7 +261,7 @@ This template is for updating the stack given the current stack and the byte we - `readComma` : a bool flag that indicates whether the byte value read was a comma `,`. # Outputs: - - `next_stack[n][2]`: the next stack of the parser. + - `next_stack[n][2]`: the next stack of the parser. */ template RewriteStack(n) { assert(n < 2**8); @@ -273,7 +275,7 @@ template RewriteStack(n) { signal input readComma; signal output next_stack[n][2]; - + //--------------------------------------------------------------------------------------------// // * scan value on top of stack * component topOfStack = GetTopOfStack(n); @@ -302,7 +304,7 @@ template RewriteStack(n) { isPush.in <== [readStartBrace + readStartBracket, 1]; component isPop = IsEqual(); isPop.in <== [readEndBrace + readEndBracket, 1]; - // * set an indicator array for where we are pushing to or popping from* + // * set an indicator array for where we are pushing to or popping from* component indicator[n]; for(var i = 0; i < n; i++) { // Points diff --git a/circuits/search.circom b/circuits/search.circom new file mode 100644 index 0000000..cfd8c12 --- /dev/null +++ b/circuits/search.circom @@ -0,0 +1,242 @@ +pragma circom 2.1.9; + +include "circomlib/circuits/mux1.circom"; +include "./utils/hash.circom"; +include "./utils/operators.circom"; +include "./utils/array.circom"; + +/* +SubstringSearch + +Calculates the index of a substring within a larger string. Uses a probabilistic algorithm to find a substring that is equal to random linear combination of difference between each element of `data` and `key`. + +# NOTE +- Is underconstrained and not suitable for standalone usage, i.e. `position` returned can be spoofed by an adversary. Must be verified with a similar template like `SubstringMatch` +- `r` should be equal to Hash(key + data), otherwise this algorithm yields false positives + +# Parameters +- `dataLen`: The maximum length of the input string +- `keyLen`: The maximum length of the substring to be matched + +# Inputs +- `data` Array of ASCII characters as input string +- `key` Array of ASCII characters as substring to be searched in `data` +- `random_num`: randomiser used to perform random linear summation for string comparison + +# Output +- `position`: index of matched `key` in `data` +*/ +template SubstringSearch(dataLen, keyLen) { + signal input data[dataLen]; + signal input key[keyLen]; + signal input random_num; + signal output position; + + assert(dataLen > 0); + assert(keyLen > 0); + assert(dataLen >= keyLen); + + // position accumulator + signal pos[dataLen-keyLen+2]; + pos[0] <== 0; + + // total matches found so far + signal num_matches[dataLen-keyLen+2]; + num_matches[0] <== 0; + + // calculate powers of r + signal r_powers[dataLen]; + r_powers[0] <== random_num; + for (var i=1 ; i): bigint { + let chunks = Math.ceil(input.length / 16); + let result: bigint = BigInt(0); + + for (var i = 0; i < chunks; i++) { + let chunk_hash: bigint = BigInt(0); + if (i == chunks - 1) { + switch (input.length % 16) { + case 0: + chunk_hash = poseidon16(input.slice(i * 16, (i + 1) * 16)); + break; + case 1: + chunk_hash = poseidon1(input.slice(i * 16, (i + 1) * 16)); + break; + case 2: + chunk_hash = poseidon2(input.slice(i * 16, (i + 1) * 16)); + break; + case 3: + chunk_hash = poseidon3(input.slice(i * 16, (i + 1) * 16)); + break; + case 4: + chunk_hash = poseidon4(input.slice(i * 16, (i + 1) * 16)); + break; + case 5: + chunk_hash = poseidon5(input.slice(i * 16, (i + 1) * 16)); + break; + case 6: + chunk_hash = poseidon6(input.slice(i * 16, (i + 1) * 16)); + break; + case 7: + chunk_hash = poseidon7(input.slice(i * 16, (i + 1) * 16)); + break; + case 8: + chunk_hash = poseidon8(input.slice(i * 16, (i + 1) * 16)); + break; + case 9: + chunk_hash = poseidon9(input.slice(i * 16, (i + 1) * 16)); + break; + case 10: + chunk_hash = poseidon10(input.slice(i * 16, (i + 1) * 16)); + break; + case 11: + chunk_hash = poseidon11(input.slice(i * 16, (i + 1) * 16)); + break; + case 12: + chunk_hash = poseidon12(input.slice(i * 16, (i + 1) * 16)); + break; + case 13: + chunk_hash = poseidon13(input.slice(i * 16, (i + 1) * 16)); + break; + case 14: + chunk_hash = poseidon14(input.slice(i * 16, (i + 1) * 16)); + break; + case 15: + chunk_hash = poseidon15(input.slice(i * 16, (i + 1) * 16)); + break; + + default: + break; + } + } else { + chunk_hash = poseidon16(input.slice(i * 16, (i + 1) * 16)); + } + if (i == 0) { + result = chunk_hash; + } else { + result = poseidon2([result, chunk_hash]); + } + } + + return result; +} \ No newline at end of file diff --git a/circuits/test/search.test.ts b/circuits/test/search.test.ts new file mode 100644 index 0000000..59692db --- /dev/null +++ b/circuits/test/search.test.ts @@ -0,0 +1,149 @@ +import { circomkit, WitnessTester } from "./common"; + +import witness from "../../inputs/search/witness.json"; +import { PoseidonModular } from "./common/poseidon"; + +describe("search", () => { + describe("SubstringSearch", () => { + let circuit: WitnessTester<["data", "key", "random_num"], ["position"]>; + + it("key at first position", async () => { + const data = [10, 8, 9, 4, 11, 9, 1, 2]; + const key = [10, 8, 9, 4]; + const concatenatedInput = key.concat(data); + const hashResult = PoseidonModular(concatenatedInput); + + circuit = await circomkit.WitnessTester(`SubstringSearch`, { + file: "circuits/search", + template: "SubstringSearch", + params: [data.length, key.length], + }); + + await circuit.expectPass( + { data: data, key: key, random_num: hashResult }, + { position: 0 }, + ); + }); + + it("key at last position", async () => { + const data = [11, 9, 1, 2, 10, 8, 9, 4]; + const key = [10, 8, 9, 4]; + const concatenatedInput = key.concat(data); + const hashResult = PoseidonModular(concatenatedInput); + + circuit = await circomkit.WitnessTester(`SubstringSearch`, { + file: "circuits/search", + template: "SubstringSearch", + params: [data.length, key.length], + }); + + await circuit.expectPass( + { data: data, key: key, random_num: hashResult }, + { position: 4 }, + ); + }); + + /// highlights the importance of appropriate calculation of random number for linear matching. + /// `1` as used here leads to passing constraints because [1, 0] matches with [0, 1] + /// because both have equal linear combination sum. + it("(INVALID `r=1` value) random_num input passes for different position, correct key position: 2", async () => { + const data = [0, 0, 1, 0, 0]; + const key = [1, 0]; + + circuit = await circomkit.WitnessTester(`SubstringSearch`, { + file: "circuits/search", + template: "SubstringSearch", + params: [data.length, key.length], + }); + + await circuit.expectPass( + { data: data, key: key, random_num: 1 }, + { position: 1 }, + ); + }); + + it("data = witness.json:data, key = witness.json:key, r = hash(data+key)", async () => { + const concatenatedInput = witness["key"].concat(witness["data"]); + const hashResult = PoseidonModular(concatenatedInput); + + circuit = await circomkit.WitnessTester(`SubstringSearch`, { + file: "circuits/search", + template: "SubstringSearch", + params: [witness["data"].length, witness["key"].length], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass( + { data: witness["data"], key: witness["key"], random_num: hashResult }, + { position: 6 } + ); + }); + }); + + describe("SubstringMatchWithIndex", () => { + let circuit: WitnessTester<["data", "key", "r", "start"]>; + + before(async () => { + circuit = await circomkit.WitnessTester(`SubstringSearch`, { + file: "circuits/search", + template: "SubstringMatchWithIndex", + params: [787, 10], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("data = witness.json:data, key = witness.json:key, r = hash(key+data)", async () => { + await circuit.expectPass( + { + data: witness["data"], + key: witness["key"], + r: PoseidonModular(witness["key"].concat(witness["data"])), + start: 6 + }, + ); + }); + + it("data = witness.json:data, key = witness.json:key, r = hash(key+data), incorrect position", async () => { + await circuit.expectFail( + { + data: witness["data"], + key: witness["key"], + r: PoseidonModular(witness["key"].concat(witness["data"])), + start: 98 + }, + ); + }); + }); + + describe("SubstringMatch", () => { + let circuit: WitnessTester<["data", "key"], ["position"]>; + + before(async () => { + circuit = await circomkit.WitnessTester(`SubstringSearch`, { + file: "circuits/search", + template: "SubstringMatch", + params: [787, 10], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("data = witness.json:data, key = witness.json:key", async () => { + await circuit.expectPass( + { data: witness["data"], key: witness["key"] }, + { position: 6 }, + ); + }); + + it("data = witness.json:data, key = invalid key byte", async () => { + await circuit.expectFail( + { data: witness["data"], key: witness["key"].concat(257) }, + ); + }); + + it("data = witness.json:data, key = wrong key", async () => { + await circuit.expectFail( + { data: witness["data"], key: witness["key"].concat(0) }, + ); + }); + }); +}); \ No newline at end of file diff --git a/circuits/test/utils/array.test.ts b/circuits/test/utils/array.test.ts new file mode 100644 index 0000000..ae4a42e --- /dev/null +++ b/circuits/test/utils/array.test.ts @@ -0,0 +1,185 @@ +import { circomkit, WitnessTester } from "../common"; + +describe("array", () => { + describe("Slice", () => { + let circuit: WitnessTester<["in"], ["out"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`Slice`, { + file: "circuits/utils/array", + template: "Slice", + params: [10, 2, 4], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("witness: [random*10], start: 2, end: 4", async () => { + const input = Array.from({ length: 10 }, () => Math.floor(Math.random() * 256)); + await circuit.expectPass( + { in: input }, + { out: input.slice(2, 4) } + ); + }); + + it("witness: [random*9], start: 2, end: 4", async () => { + const input = Array.from({ length: 9 }, () => Math.floor(Math.random() * 256)); + await circuit.expectFail( + { in: input }, + ); + }); + }); +}); + +describe("IsEqualArray", () => { + let circuit: WitnessTester<["in"], ["out"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`IsEqualArray`, { + file: "circuits/utils/array", + template: "IsEqualArray", + params: [3], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("witness: [[0,0,0],[0,0,0]]", async () => { + await circuit.expectPass( + { in: [[0, 0, 0], [0, 0, 0]] }, + { out: 1 } + ); + }); + + it("witness: [[1,420,69],[1,420,69]]", async () => { + await circuit.expectPass( + { in: [[1, 420, 69], [1, 420, 69]] }, + { out: 1 }, + ); + }); + + it("witness: [[0,0,0],[1,420,69]]", async () => { + await circuit.expectPass( + { in: [[0, 0, 0], [1, 420, 69]] }, + { out: 0 }, + ); + }); + + it("witness: [[1,420,0],[1,420,69]]", async () => { + await circuit.expectPass( + { in: [[1, 420, 0], [1, 420, 69]] }, + { out: 0 }, + ); + }); + + it("witness: [[1,0,69],[1,420,69]]", async () => { + await circuit.expectPass( + { in: [[1, 0, 69], [1, 420, 69]] }, + { out: 0 }, + ); + }); + + it("witness: [[0,420,69],[1,420,69]]", async () => { + await circuit.expectPass( + { in: [[0, 420, 69], [1, 420, 69]] }, + { out: 0 }, + ); + }); +}); + +describe("Contains", () => { + let circuit: WitnessTester<["in", "array"], ["out"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`Contains`, { + file: "circuits/utils/array", + template: "Contains", + params: [3], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("witness: in = 0, array = [0,1,2]", async () => { + await circuit.expectPass( + { in: 0, array: [0, 1, 2] }, + { out: 1 } + ); + }); + + it("witness: in = 1, array = [0,1,2]", async () => { + await circuit.expectPass( + { in: 1, array: [0, 1, 2] }, + { out: 1 } + ); + }); + + it("witness: in = 2, array = [0,1,2]", async () => { + await circuit.expectPass( + { in: 2, array: [0, 1, 2] }, + { out: 1 } + ); + }); + + it("witness: in = 42069, array = [0,1,2]", async () => { + await circuit.expectPass( + { in: 42069, array: [0, 1, 2] }, + { out: 0 } + ); + }); + +}); + +describe("ArrayAdd", () => { + let circuit: WitnessTester<["lhs", "rhs"], ["out"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`ArrayAdd`, { + file: "circuits/utils/array", + template: "ArrayAdd", + params: [3], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("witness: lhs = [0,1,2], rhs = [3,5,7]", async () => { + await circuit.expectPass( + { lhs: [0, 1, 2], rhs: [3, 5, 7] }, + { out: [3, 6, 9] } + ); + }); + +}); + +describe("ArrayMul", () => { + let circuit: WitnessTester<["lhs", "rhs"], ["out"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`ArrayMul`, { + file: "circuits/utils/array", + template: "ArrayMul", + params: [3], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("witness: lhs = [0,1,2], rhs = [3,5,7]", async () => { + await circuit.expectPass( + { lhs: [0, 1, 2], rhs: [3, 5, 7] }, + { out: [0, 5, 14] } + ); + }); + +}); + +describe("GenericArrayAdd", () => { + let circuit: WitnessTester<["arrays"], ["out"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`ArrayAdd`, { + file: "circuits/utils/array", + template: "GenericArrayAdd", + params: [3, 2], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("witness: arrays = [[0,1,2],[3,5,7]]", async () => { + await circuit.expectPass( + { arrays: [[0, 1, 2], [3, 5, 7]] }, + { out: [3, 6, 9] } + ); + }); + +}); \ No newline at end of file diff --git a/circuits/test/utils/bytes.test.ts b/circuits/test/utils/bytes.test.ts new file mode 100644 index 0000000..5ee55ef --- /dev/null +++ b/circuits/test/utils/bytes.test.ts @@ -0,0 +1,25 @@ +import { circomkit, WitnessTester } from "../common"; + +describe("ASCII", () => { + let circuit: WitnessTester<["in"], ["out"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`ASCII`, { + file: "circuits/utils/bytes", + template: "ASCII", + params: [13], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("(valid) witness: in = b\"Hello, world!\"", async () => { + await circuit.expectPass( + { in: [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33] }, + ); + }); + + it("(invalid) witness: in = [256, ...]", async () => { + await circuit.expectFail( + { in: [256, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33] } + ); + }); +}); \ No newline at end of file diff --git a/circuits/test/utils/hash.test.ts b/circuits/test/utils/hash.test.ts new file mode 100644 index 0000000..e834cc7 --- /dev/null +++ b/circuits/test/utils/hash.test.ts @@ -0,0 +1,50 @@ +import { circomkit, WitnessTester } from "../common"; +import { PoseidonModular } from "../common/poseidon"; + +describe("hash", () => { + describe("PoseidonModular_16", () => { + let circuit: WitnessTester<["in"], ["out"]>; + + before(async () => { + circuit = await circomkit.WitnessTester(`PoseidonModular`, { + file: "circuits/utils/hash", + template: "PoseidonModular", + params: [16], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("witness: in = [16*random]", async () => { + const input = Array.from({ length: 16 }, () => Math.floor(Math.random() * 256)); + const hash = PoseidonModular(input); + + await circuit.expectPass( + { in: input }, + { out: hash } + ); + }); + }); + + describe("PoseidonModular_379", () => { + let circuit: WitnessTester<["in"], ["out"]>; + + before(async () => { + circuit = await circomkit.WitnessTester(`PoseidonModular`, { + file: "circuits/utils/hash", + template: "PoseidonModular", + params: [379], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("witness: in = [379*random]", async () => { + const input = Array.from({ length: 379 }, () => Math.floor(Math.random() * 256)); + const hash = PoseidonModular(input); + + await circuit.expectPass( + { in: input }, + { out: hash } + ); + }); + }); +}); \ No newline at end of file diff --git a/circuits/test/utils/operators.test.ts b/circuits/test/utils/operators.test.ts new file mode 100644 index 0000000..7c82c61 --- /dev/null +++ b/circuits/test/utils/operators.test.ts @@ -0,0 +1,131 @@ +import { circomkit, WitnessTester } from "../common"; + +describe("SwitchArray", () => { + let circuit: WitnessTester<["case", "branches", "vals"], ["match", "out"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`SwitchArray`, { + file: "circuits/utils/operators", + template: "SwitchArray", + params: [3, 2], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("witness: case = 0, branches = [0, 1, 2], vals = [[69,0], [420,1], [1337,2]]", async () => { + await circuit.expectPass( + { case: 0, branches: [0, 1, 2], vals: [[69, 0], [420, 1], [1337, 2]] }, + { match: 1, out: [69, 0] }, + ); + }); + + it("witness: case = 1, branches = [0, 1, 2], vals = [[69,0], [420,1], [1337,2]]", async () => { + await circuit.expectPass( + { case: 1, branches: [0, 1, 2], vals: [[69, 0], [420, 1], [1337, 2]] }, + { match: 1, out: [420, 1] }, + ); + }); + + it("witness: case = 2, branches = [0, 1, 2], vals = [[69,0], [420,1], [1337,2]]", async () => { + await circuit.expectPass( + { case: 2, branches: [0, 1, 2], vals: [[69, 0], [420, 1], [1337, 2]] }, + { match: 1, out: [1337, 2] }, + ); + }); + + it("witness: case = 3, branches = [0, 1, 2], vals = [[69,0], [420,1], [1337,2]]", async () => { + await circuit.expectPass( + { case: 3, branches: [0, 1, 2], vals: [[69, 0], [420, 1], [1337, 2]] }, + { match: 0, out: [0, 0] } + ); + }); + + it("witness: case = 420, branches = [69, 420, 1337], vals = [[10,3], [20,5], [30,7]]", async () => { + await circuit.expectPass( + { case: 420, branches: [69, 420, 1337], vals: [[10, 3], [20, 5], [30, 7]] }, + { match: 1, out: [20, 5] } + ); + }); + + it("witness: case = 0, branches = [69, 420, 1337], vals = [[10,3], [20,5], [30,7]]", async () => { + await circuit.expectPass( + { case: 0, branches: [69, 420, 1337], vals: [[10, 3], [20, 5], [30, 7]] }, + { match: 0, out: [0, 0] } + ); + }); + +}); + +describe("Switch", () => { + let circuit: WitnessTester<["case", "branches", "vals"], ["match", "out"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`Switch`, { + file: "circuits/utils/operators", + template: "Switch", + params: [3], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("witness: case = 0, branches = [0, 1, 2], vals = [69, 420, 1337]", async () => { + await circuit.expectPass( + { case: 0, branches: [0, 1, 2], vals: [69, 420, 1337] }, + { match: 1, out: 69 }, + ); + }); + + it("witness: case = 1, branches = [0, 1, 2], vals = [69, 420, 1337]", async () => { + await circuit.expectPass( + { case: 1, branches: [0, 1, 2], vals: [69, 420, 1337] }, + { match: 1, out: 420 }, + ); + }); + + it("witness: case = 2, branches = [0, 1, 2], vals = [69, 420, 1337]", async () => { + await circuit.expectPass( + { case: 2, branches: [0, 1, 2], vals: [69, 420, 1337] }, + { match: 1, out: 1337 }, + ); + }); + + it("witness: case = 3, branches = [0, 1, 2], vals = [69, 420, 1337]", async () => { + await circuit.expectPass( + { case: 3, branches: [0, 1, 2], vals: [69, 420, 1337] }, + { match: 0, out: 0 }, + ); + }); + + +}); + +describe("InRange", () => { + let circuit: WitnessTester<["in", "range"], ["out"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`InRange`, { + file: "circuits/utils/operators", + template: "InRange", + params: [8], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + it("witness: in = 1, range = [0,2]", async () => { + await circuit.expectPass( + { in: 1, range: [0, 2] }, + { out: 1 } + ); + }); + + it("witness: in = 69, range = [128,255]", async () => { + await circuit.expectPass( + { in: 69, range: [128, 255] }, + { out: 0 } + ); + }); + + it("witness: in = 200, range = [128,255]", async () => { + await circuit.expectPass( + { in: 1, range: [0, 2] }, + { out: 1 } + ); + }); +}); \ No newline at end of file diff --git a/circuits/test/utils/utils.test.ts b/circuits/test/utils/utils.test.ts deleted file mode 100644 index 3721566..0000000 --- a/circuits/test/utils/utils.test.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { circomkit, WitnessTester } from "../common"; - -describe("ASCII", () => { - let circuit: WitnessTester<["in"], ["out"]>; - before(async () => { - circuit = await circomkit.WitnessTester(`ASCII`, { - file: "circuits/utils", - template: "ASCII", - params: [13], - }); - console.log("#constraints:", await circuit.getConstraintCount()); - }); - - it("(valid) witness: in = b\"Hello, world!\"", async () => { - await circuit.expectPass( - { in: [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33] }, - ); - }); - - it("(invalid) witness: in = [256, ...]", async () => { - await circuit.expectFail( - { in: [256, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33] } - ); - }); -}); - -describe("IsEqualArray", () => { - let circuit: WitnessTester<["in"], ["out"]>; - before(async () => { - circuit = await circomkit.WitnessTester(`IsEqualArray`, { - file: "circuits/utils", - template: "IsEqualArray", - params: [3], - }); - console.log("#constraints:", await circuit.getConstraintCount()); - }); - - it("witness: [[0,0,0],[0,0,0]]", async () => { - await circuit.expectPass( - { in: [[0, 0, 0], [0, 0, 0]] }, - { out: 1 } - ); - }); - - it("witness: [[1,420,69],[1,420,69]]", async () => { - await circuit.expectPass( - { in: [[1, 420, 69], [1, 420, 69]] }, - { out: 1 }, - ); - }); - - it("witness: [[0,0,0],[1,420,69]]", async () => { - await circuit.expectPass( - { in: [[0, 0, 0], [1, 420, 69]] }, - { out: 0 }, - ); - }); - - it("witness: [[1,420,0],[1,420,69]]", async () => { - await circuit.expectPass( - { in: [[1, 420, 0], [1, 420, 69]] }, - { out: 0 }, - ); - }); - - it("witness: [[1,0,69],[1,420,69]]", async () => { - await circuit.expectPass( - { in: [[1, 0, 69], [1, 420, 69]] }, - { out: 0 }, - ); - }); - - it("witness: [[0,420,69],[1,420,69]]", async () => { - await circuit.expectPass( - { in: [[0, 420, 69], [1, 420, 69]] }, - { out: 0 }, - ); - }); -}); - -describe("Contains", () => { - let circuit: WitnessTester<["in", "array"], ["out"]>; - before(async () => { - circuit = await circomkit.WitnessTester(`Contains`, { - file: "circuits/utils", - template: "Contains", - params: [3], - }); - console.log("#constraints:", await circuit.getConstraintCount()); - }); - - it("witness: in = 0, array = [0,1,2]", async () => { - await circuit.expectPass( - { in: 0, array: [0, 1, 2] }, - { out: 1 } - ); - }); - - it("witness: in = 1, array = [0,1,2]", async () => { - await circuit.expectPass( - { in: 1, array: [0, 1, 2] }, - { out: 1 } - ); - }); - - it("witness: in = 2, array = [0,1,2]", async () => { - await circuit.expectPass( - { in: 2, array: [0, 1, 2] }, - { out: 1 } - ); - }); - - it("witness: in = 42069, array = [0,1,2]", async () => { - await circuit.expectPass( - { in: 42069, array: [0, 1, 2] }, - { out: 0 } - ); - }); - -}); - -describe("ArrayAdd", () => { - let circuit: WitnessTester<["lhs", "rhs"], ["out"]>; - before(async () => { - circuit = await circomkit.WitnessTester(`ArrayAdd`, { - file: "circuits/utils", - template: "ArrayAdd", - params: [3], - }); - console.log("#constraints:", await circuit.getConstraintCount()); - }); - - it("witness: lhs = [0,1,2], rhs = [3,5,7]", async () => { - await circuit.expectPass( - { lhs: [0, 1, 2], rhs: [3, 5, 7] }, - { out: [3, 6, 9] } - ); - }); - -}); - -describe("ArrayMul", () => { - let circuit: WitnessTester<["lhs", "rhs"], ["out"]>; - before(async () => { - circuit = await circomkit.WitnessTester(`ArrayMul`, { - file: "circuits/utils", - template: "ArrayMul", - params: [3], - }); - console.log("#constraints:", await circuit.getConstraintCount()); - }); - - it("witness: lhs = [0,1,2], rhs = [3,5,7]", async () => { - await circuit.expectPass( - { lhs: [0, 1, 2], rhs: [3, 5, 7] }, - { out: [0, 5, 14] } - ); - }); - -}); - -describe("GenericArrayAdd", () => { - let circuit: WitnessTester<["arrays"], ["out"]>; - before(async () => { - circuit = await circomkit.WitnessTester(`ArrayAdd`, { - file: "circuits/utils", - template: "GenericArrayAdd", - params: [3, 2], - }); - console.log("#constraints:", await circuit.getConstraintCount()); - }); - - it("witness: arrays = [[0,1,2],[3,5,7]]", async () => { - await circuit.expectPass( - { arrays: [[0, 1, 2], [3, 5, 7]] }, - { out: [3, 6, 9] } - ); - }); - -}); - -describe("InRange", () => { - let circuit: WitnessTester<["in", "range"], ["out"]>; - before(async () => { - circuit = await circomkit.WitnessTester(`InRange`, { - file: "circuits/utils", - template: "InRange", - params: [8], - }); - console.log("#constraints:", await circuit.getConstraintCount()); - }); - - it("witness: in = 1, range = [0,2]", async () => { - await circuit.expectPass( - { in: 1, range: [0, 2] }, - { out: 1 } - ); - }); - - it("witness: in = 69, range = [128,255]", async () => { - await circuit.expectPass( - { in: 69, range: [128, 255] }, - { out: 0 } - ); - }); - - it("witness: in = 200, range = [128,255]", async () => { - await circuit.expectPass( - { in: 1, range: [0, 2] }, - { out: 1 } - ); - }); -}); - -describe("Switch", () => { - let circuit: WitnessTester<["case", "branches", "vals"], ["match", "out"]>; - before(async () => { - circuit = await circomkit.WitnessTester(`Switch`, { - file: "circuits/utils", - template: "Switch", - params: [3], - }); - console.log("#constraints:", await circuit.getConstraintCount()); - }); - - it("witness: case = 0, branches = [0, 1, 2], vals = [69, 420, 1337]", async () => { - await circuit.expectPass( - { case: 0, branches: [0, 1, 2], vals: [69, 420, 1337] }, - { match: 1, out: 69 }, - ); - }); - - it("witness: case = 1, branches = [0, 1, 2], vals = [69, 420, 1337]", async () => { - await circuit.expectPass( - { case: 1, branches: [0, 1, 2], vals: [69, 420, 1337] }, - { match: 1, out: 420 }, - ); - }); - - it("witness: case = 2, branches = [0, 1, 2], vals = [69, 420, 1337]", async () => { - await circuit.expectPass( - { case: 2, branches: [0, 1, 2], vals: [69, 420, 1337] }, - { match: 1, out: 1337 }, - ); - }); - - it("witness: case = 3, branches = [0, 1, 2], vals = [69, 420, 1337]", async () => { - await circuit.expectPass( - { case: 3, branches: [0, 1, 2], vals: [69, 420, 1337] }, - { match: 0, out: 0 }, - ); - }); - - -}); - -describe("SwitchArray", () => { - let circuit: WitnessTester<["case", "branches", "vals"], ["match", "out"]>; - before(async () => { - circuit = await circomkit.WitnessTester(`SwitchArray`, { - file: "circuits/utils", - template: "SwitchArray", - params: [3, 2], - }); - console.log("#constraints:", await circuit.getConstraintCount()); - }); - - it("witness: case = 0, branches = [0, 1, 2], vals = [[69,0], [420,1], [1337,2]]", async () => { - await circuit.expectPass( - { case: 0, branches: [0, 1, 2], vals: [[69, 0], [420, 1], [1337, 2]] }, - { match: 1, out: [69, 0] }, - ); - }); - - it("witness: case = 1, branches = [0, 1, 2], vals = [[69,0], [420,1], [1337,2]]", async () => { - await circuit.expectPass( - { case: 1, branches: [0, 1, 2], vals: [[69, 0], [420, 1], [1337, 2]] }, - { match: 1, out: [420, 1] }, - ); - }); - - it("witness: case = 2, branches = [0, 1, 2], vals = [[69,0], [420,1], [1337,2]]", async () => { - await circuit.expectPass( - { case: 2, branches: [0, 1, 2], vals: [[69, 0], [420, 1], [1337, 2]] }, - { match: 1, out: [1337, 2] }, - ); - }); - - it("witness: case = 3, branches = [0, 1, 2], vals = [[69,0], [420,1], [1337,2]]", async () => { - await circuit.expectPass( - { case: 3, branches: [0, 1, 2], vals: [[69, 0], [420, 1], [1337, 2]] }, - { match: 0, out: [0, 0] } - ); - }); - - it("witness: case = 420, branches = [69, 420, 1337], vals = [[10,3], [20,5], [30,7]]", async () => { - await circuit.expectPass( - { case: 420, branches: [69, 420, 1337], vals: [[10, 3], [20, 5], [30, 7]] }, - { match: 1, out: [20, 5] } - ); - }); - - it("witness: case = 0, branches = [69, 420, 1337], vals = [[10,3], [20,5], [30,7]]", async () => { - await circuit.expectPass( - { case: 0, branches: [69, 420, 1337], vals: [[10, 3], [20, 5], [30, 7]] }, - { match: 0, out: [0, 0] } - ); - }); - -}); - diff --git a/circuits/utils.circom b/circuits/utils.circom deleted file mode 100644 index ed0f485..0000000 --- a/circuits/utils.circom +++ /dev/null @@ -1,342 +0,0 @@ -/* -# `utils` -This module consists of helper templates for convencience. -It mostly extends the `bitify` and `comparators` modules from Circomlib. - -## Layout -The key ingredients of `utils` are: - - `ASCII`: Verify if a an input array contains valid ASCII values (e.g., u8 vals). - - `IsEqualArray`: Check if two arrays are equal component by component. - - `Contains`: Check if an element is contained in a given array. - - `ArrayAdd`: Add two arrays together component by component. - - `ArrayMul`: Multiply two arrays together component by component. - - `GenericArrayAdd`: Add together an arbitrary amount of arrays. - - `ScalarArrayMul`: Multiply each array element by a scalar value. - - `InRange`: Check if a given number is in a given range. - - `Switch`: Return a scalar value given a specific case. - - `SwitchArray`: Return an array given a specific case. - - -## Testing -Tests for this module are located in the file: `./test/utils/utils.test.ts` -*/ - -pragma circom 2.1.9; - -include "circomlib/circuits/bitify.circom"; -include "circomlib/circuits/comparators.circom"; - - - -/* -This template passes if a given array contains only valid ASCII values (e.g., u8 vals). - -# Params: - - `n`: the length of the array - -# Inputs: - - `in[n]`: array to check -*/ -template ASCII(n) { - signal input in[n]; - - component Byte[n]; - for(var i = 0; i < n; i++) { - Byte[i] = Num2Bits(8); - Byte[i].in <== in[i]; - } -} - -/* -This template is an indicator for two equal array inputs. - -# Params: - - `n`: the length of arrays to compare - -# Inputs: - - `in[2][n]`: two arrays of `n` numbers - -# Outputs: - - `out`: either `0` or `1` - - `1` if `in[0]` is equal to `in[1]` as arrays (i.e., component by component) - - `0` otherwise -*/ -template IsEqualArray(n) { - signal input in[2][n]; - signal output out; - - var accum = 0; - component equalComponent[n]; - - for(var i = 0; i < n; i++) { - equalComponent[i] = IsEqual(); - equalComponent[i].in[0] <== in[0][i]; - equalComponent[i].in[1] <== in[1][i]; - accum += equalComponent[i].out; - } - - component totalEqual = IsEqual(); - totalEqual.in[0] <== n; - totalEqual.in[1] <== accum; - out <== totalEqual.out; -} - - -// TODO: There should be a way to have the below assertion come from the field itself. -/* -This template is an indicator for if an array contains an element. - -# Params: - - `n`: the size of the array to search through - -# Inputs: - - `in`: a number - - `array[n]`: the array we want to search through - -# Outputs: - - `out`: either `0` or `1` - - `1` if `in` is found inside `array` - - `0` otherwise -*/ -template Contains(n) { - assert(n > 0); - /* - If `n = p` for this large `p`, then it could be that this template - returns the wrong value if every element in `array` was equal to `in`. - This is EXTREMELY unlikely and iterating this high is impossible anyway. - But it is better to check than miss something, so we bound it by `2**254` for now. - */ - assert(n < 2**254); - signal input in; - signal input array[n]; - signal output out; - - var accum = 0; - component equalComponent[n]; - for(var i = 0; i < n; i++) { - equalComponent[i] = IsEqual(); - equalComponent[i].in[0] <== in; - equalComponent[i].in[1] <== array[i]; - accum = accum + equalComponent[i].out; - } - - component someEqual = IsZero(); - someEqual.in <== accum; - - // Apply `not` to this by 1-x - out <== 1 - someEqual.out; -} - -/* -This template adds two arrays component by component. - -# Params: - - `n`: the length of arrays to compare - -# Inputs: - - `in[2][n]`: two arrays of `n` numbers - -# Outputs: - - `out[n]`: the array sum value -*/ -template ArrayAdd(n) { - signal input lhs[n]; - signal input rhs[n]; - signal output out[n]; - - for(var i = 0; i < n; i++) { - out[i] <== lhs[i] + rhs[i]; - } -} - -/* -This template multiplies two arrays component by component. - -# Params: - - `n`: the length of arrays to compare - -# Inputs: - - `in[2][n]`: two arrays of `n` numbers - -# Outputs: - - `out[n]`: the array multiplication value -*/ -template ArrayMul(n) { - signal input lhs[n]; - signal input rhs[n]; - signal output out[n]; - - for(var i = 0; i < n; i++) { - out[i] <== lhs[i] * rhs[i]; - } -} - -/* -This template multiplies two arrays component by component. - -# Params: - - `m`: the length of the arrays to add - - `n`: the number of arrays to add - -# Inputs: - - `arrays[m][n]`: `n` arrays of `m` numbers - -# Outputs: - - `out[m]`: the sum of all the arrays -*/ -template GenericArrayAdd(m,n) { - signal input arrays[n][m]; - signal output out[m]; - - var accum[m]; - for(var i = 0; i < m; i++) { - for(var j = 0; j < n; j++) { - accum[i] += arrays[j][i]; - } - } - out <== accum; -} - -/* -This template multiplies each component of an array by a scalar value. - -# Params: - - `n`: the length of the array - -# Inputs: - - `array[n]`: an array of `n` numbers - -# Outputs: - - `out[n]`: the scalar multiplied array -*/ -template ScalarArrayMul(n) { - signal input array[n]; - signal input scalar; - signal output out[n]; - - for(var i = 0; i < n; i++) { - out[i] <== scalar * array[i]; - } -} - -/* -This template checks if a given `n`-bit value is contained in a range of `n`-bit values - -# Params: - - `n`: the number of bits to use - -# Inputs: - - `range[2]`: the lower and upper bound of the array, respectively - -# Outputs: - - `out`: either `0` or `1` - - `1` if `in` is within the range - - `0` otherwise -*/ -template InRange(n) { - signal input in; - signal input range[2]; - signal output out; - - component gte = GreaterEqThan(n); - gte.in <== [in, range[0]]; - - component lte = LessEqThan(n); - lte.in <== [in, range[1]]; - - out <== gte.out * lte.out; -} - -/* -This template is creates an exhaustive switch statement from a list of branch values. -# Params: - - `n`: the number of switch cases - -# Inputs: - - `case`: which case of the switch to select - - `branches[n]`: the values that enable taking different branches in the switch - (e.g., if `branch[i] == 10` then if `case == 10` we set `out == `vals[i]`) - - `vals[n]`: the value that is emitted for a given switch case - (e.g., `val[i]` array is emitted on `case == `branch[i]`) - -# Outputs - - `match`: is set to `0` if `case` does not match on any of `branches` - - `out[n]`: the selected output value if one of `branches` is selected (will be `0` otherwise) - ^^^^^^ BEWARE OF THIS FACT ABOVE! -*/ -template Switch(n) { - assert(n > 0); - signal input case; - signal input branches[n]; - signal input vals[n]; - signal output match; - signal output out; - - - // Verify that the `case` is in the possible set of branches - component indicator[n]; - component matchChecker = Contains(n); - signal temp_val[n]; - var sum; - for(var i = 0; i < n; i++) { - indicator[i] = IsZero(); - indicator[i].in <== case - branches[i]; - matchChecker.array[i] <== 1 - indicator[i].out; - temp_val[i] <== indicator[i].out * vals[i]; - sum += temp_val[i]; - } - matchChecker.in <== 0; - match <== matchChecker.out; - - out <== sum; -} - -/* -This template is creates an exhaustive switch statement from a list of branch values. -# Params: - - `m`: the number of switch cases - - `n`: the output array length - -# Inputs: - - - `case`: which case of the switch to select - - `branches[m]`: the values that enable taking different branches in the switch - (e.g., if `branch[i] == 10` then if `case == 10` we set `out == `vals[i]`) - - `vals[m][n]`: the value that is emitted for a given switch case - (e.g., `val[i]` array is emitted on `case == `branch[i]`) - -# Outputs - - `match`: is set to `0` if `case` does not match on any of `branches` - - `out[n]`: the selected output value if one of `branches` is selected (will be `[0,0,...]` otherwise) - ^^^^^^ BEWARE OF THIS FACT ABOVE! -*/ -template SwitchArray(m, n) { - assert(m > 0); - assert(n > 0); - signal input case; - signal input branches[m]; - signal input vals[m][n]; - signal output match; - signal output out[n]; - - - // Verify that the `case` is in the possible set of branches - component indicator[m]; - component matchChecker = Contains(m); - signal component_out[m][n]; - var sum[n]; - for(var i = 0; i < m; i++) { - indicator[i] = IsZero(); - indicator[i].in <== case - branches[i]; - matchChecker.array[i] <== 1 - indicator[i].out; - for(var j = 0; j < n; j++) { - component_out[i][j] <== indicator[i].out * vals[i][j]; - sum[j] += component_out[i][j]; - } - } - matchChecker.in <== 0; - match <== matchChecker.out; - - out <== sum; -} - diff --git a/circuits/utils/array.circom b/circuits/utils/array.circom new file mode 100644 index 0000000..dc65b28 --- /dev/null +++ b/circuits/utils/array.circom @@ -0,0 +1,203 @@ +pragma circom 2.1.9; + +include "circomlib/circuits/comparators.circom"; + +/// Extract a fixed portion of an array +/// +/// # Note +/// Unlike SelectSubArray, Slice uses compile-time known indices and doesn't pad the output. +/// Slice is more efficient for fixed ranges, while SelectSubArray offers runtime flexibility +/// +/// # Parameters +/// - `n`: The length of the input array +/// - `start`: The starting index of the slice (inclusive) +/// - `end`: The ending index of the slice (exclusive) +/// +/// # Inputs +/// - `in`: The input array of length n +/// +/// # Output +/// - `out`: The sliced array of length (end - start) +template Slice(n, start, end) { + assert(n >= end); + assert(start >= 0); + assert(end >= start); + + signal input in[n]; + signal output out[end - start]; + + for (var i = start; i < end; i++) { + out[i - start] <== in[i]; + } +} + +/* +This template is an indicator for two equal array inputs. + +# Params: + - `n`: the length of arrays to compare + +# Inputs: + - `in[2][n]`: two arrays of `n` numbers + +# Outputs: + - `out`: either `0` or `1` + - `1` if `in[0]` is equal to `in[1]` as arrays (i.e., component by component) + - `0` otherwise +*/ +template IsEqualArray(n) { + signal input in[2][n]; + signal output out; + + var accum = 0; + component equalComponent[n]; + + for(var i = 0; i < n; i++) { + equalComponent[i] = IsEqual(); + equalComponent[i].in[0] <== in[0][i]; + equalComponent[i].in[1] <== in[1][i]; + accum += equalComponent[i].out; + } + + component totalEqual = IsEqual(); + totalEqual.in[0] <== n; + totalEqual.in[1] <== accum; + out <== totalEqual.out; +} + +// TODO: There should be a way to have the below assertion come from the field itself. +/* +This template is an indicator for if an array contains an element. + +# Params: + - `n`: the size of the array to search through + +# Inputs: + - `in`: a number + - `array[n]`: the array we want to search through + +# Outputs: + - `out`: either `0` or `1` + - `1` if `in` is found inside `array` + - `0` otherwise +*/ +template Contains(n) { + assert(n > 0); + /* + If `n = p` for this large `p`, then it could be that this template + returns the wrong value if every element in `array` was equal to `in`. + This is EXTREMELY unlikely and iterating this high is impossible anyway. + But it is better to check than miss something, so we bound it by `2**254` for now. + */ + assert(n < 2**254); + signal input in; + signal input array[n]; + signal output out; + + var accum = 0; + component equalComponent[n]; + for(var i = 0; i < n; i++) { + equalComponent[i] = IsEqual(); + equalComponent[i].in[0] <== in; + equalComponent[i].in[1] <== array[i]; + accum = accum + equalComponent[i].out; + } + + component someEqual = IsZero(); + someEqual.in <== accum; + + // Apply `not` to this by 1-x + out <== 1 - someEqual.out; +} + +/* +This template adds two arrays component by component. + +# Params: + - `n`: the length of arrays to compare + +# Inputs: + - `in[2][n]`: two arrays of `n` numbers + +# Outputs: + - `out[n]`: the array sum value +*/ +template ArrayAdd(n) { + signal input lhs[n]; + signal input rhs[n]; + signal output out[n]; + + for(var i = 0; i < n; i++) { + out[i] <== lhs[i] + rhs[i]; + } +} + +/* +This template multiplies two arrays component by component. + +# Params: + - `n`: the length of arrays to compare + +# Inputs: + - `in[2][n]`: two arrays of `n` numbers + +# Outputs: + - `out[n]`: the array multiplication value +*/ +template ArrayMul(n) { + signal input lhs[n]; + signal input rhs[n]; + signal output out[n]; + + for(var i = 0; i < n; i++) { + out[i] <== lhs[i] * rhs[i]; + } +} + +/* +This template multiplies two arrays component by component. + +# Params: + - `m`: the length of the arrays to add + - `n`: the number of arrays to add + +# Inputs: + - `arrays[m][n]`: `n` arrays of `m` numbers + +# Outputs: + - `out[m]`: the sum of all the arrays +*/ +template GenericArrayAdd(m,n) { + signal input arrays[n][m]; + signal output out[m]; + + var accum[m]; + for(var i = 0; i < m; i++) { + for(var j = 0; j < n; j++) { + accum[i] += arrays[j][i]; + } + } + out <== accum; +} + +/* +This template multiplies each component of an array by a scalar value. + +# Params: + - `n`: the length of the array + +# Inputs: + - `array[n]`: an array of `n` numbers + +# Outputs: + - `out[n]`: the scalar multiplied array +*/ +template ScalarArrayMul(n) { + signal input array[n]; + signal input scalar; + signal output out[n]; + + for(var i = 0; i < n; i++) { + out[i] <== scalar * array[i]; + } +} \ No newline at end of file diff --git a/circuits/utils/bytes.circom b/circuits/utils/bytes.circom new file mode 100644 index 0000000..65f8f80 --- /dev/null +++ b/circuits/utils/bytes.circom @@ -0,0 +1,22 @@ +pragma circom 2.1.9; + +include "circomlib/circuits/bitify.circom"; + +/* +This template passes if a given array contains only valid ASCII values (e.g., u8 vals). + +# Params: + - `n`: the length of the array + +# Inputs: + - `in[n]`: array to check +*/ +template ASCII(n) { + signal input in[n]; + + component Byte[n]; + for(var i = 0; i < n; i++) { + Byte[i] = Num2Bits(8); + Byte[i].in <== in[i]; + } +} \ No newline at end of file diff --git a/circuits/utils/hash.circom b/circuits/utils/hash.circom new file mode 100644 index 0000000..3d0ef40 --- /dev/null +++ b/circuits/utils/hash.circom @@ -0,0 +1,54 @@ +pragma circom 2.1.9; + +include "circomlib/circuits/poseidon.circom"; +include "./array.circom"; + +/// Circuit to calculate Poseidon hash of an arbitrary number of inputs. +/// Splits input into chunks of 16 elements (or less for the last chunk) and hashes them separately +/// Then combines the chunk hashes using a binary tree structure. +/// +/// NOTE: from +/// +/// # Parameters +/// - `numElements`: Number of elements in the input array +/// +/// # Inputs +/// - `in`: Array of numElements to be hashed +/// +/// # Output +/// - `out`: Poseidon hash of the input array +template PoseidonModular(numElements) { + signal input in[numElements]; + signal output out; + + var chunks = numElements \ 16; + var last_chunk_size = numElements % 16; + if (last_chunk_size != 0) { + chunks += 1; + } + + var _out; + + for (var i = 0; i < chunks; i++) { + var start = i * 16; + var end = start + 16; + var chunk_hash; + + if (end > numElements) { // last chunk + end = numElements; + var last_chunk[last_chunk_size] = Slice(numElements, start, end)(in); + chunk_hash = Poseidon(last_chunk_size)(last_chunk); + } else { + var chunk[16] = Slice(numElements, start, end)(in); + chunk_hash = Poseidon(16)(chunk); + } + + if (i == 0) { + _out = chunk_hash; + } else { + _out = Poseidon(2)([_out, chunk_hash]); + } + } + + out <== _out; +} \ No newline at end of file diff --git a/circuits/utils/operators.circom b/circuits/utils/operators.circom new file mode 100644 index 0000000..925005e --- /dev/null +++ b/circuits/utils/operators.circom @@ -0,0 +1,151 @@ +/* +# `utils` +This module consists of helper templates for convencience. +It mostly extends the `bitify` and `comparators` modules from Circomlib. + +## Layout +The key ingredients of `utils` are: + - `ASCII`: Verify if a an input array contains valid ASCII values (e.g., u8 vals). + - `IsEqualArray`: Check if two arrays are equal component by component. + - `Contains`: Check if an element is contained in a given array. + - `ArrayAdd`: Add two arrays together component by component. + - `ArrayMul`: Multiply two arrays together component by component. + - `GenericArrayAdd`: Add together an arbitrary amount of arrays. + - `ScalarArrayMul`: Multiply each array element by a scalar value. + - `InRange`: Check if a given number is in a given range. + - `Switch`: Return a scalar value given a specific case. + - `SwitchArray`: Return an array given a specific case. + + +## Testing +Tests for this module are located in the file: `./test/utils/utils.test.ts` +*/ + +pragma circom 2.1.9; + +include "circomlib/circuits/bitify.circom"; +include "circomlib/circuits/comparators.circom"; +include "array.circom"; + + +/* +This template checks if a given `n`-bit value is contained in a range of `n`-bit values + +# Params: + - `n`: the number of bits to use + +# Inputs: + - `range[2]`: the lower and upper bound of the array, respectively + +# Outputs: + - `out`: either `0` or `1` + - `1` if `in` is within the range + - `0` otherwise +*/ +template InRange(n) { + signal input in; + signal input range[2]; + signal output out; + + component gte = GreaterEqThan(n); + gte.in <== [in, range[0]]; + + component lte = LessEqThan(n); + lte.in <== [in, range[1]]; + + out <== gte.out * lte.out; +} + +/* +This template is creates an exhaustive switch statement from a list of branch values. +# Params: + - `n`: the number of switch cases + +# Inputs: + - `case`: which case of the switch to select + - `branches[n]`: the values that enable taking different branches in the switch + (e.g., if `branch[i] == 10` then if `case == 10` we set `out == `vals[i]`) + - `vals[n]`: the value that is emitted for a given switch case + (e.g., `val[i]` array is emitted on `case == `branch[i]`) + +# Outputs + - `match`: is set to `0` if `case` does not match on any of `branches` + - `out[n]`: the selected output value if one of `branches` is selected (will be `0` otherwise) + ^^^^^^ BEWARE OF THIS FACT ABOVE! +*/ +template Switch(n) { + assert(n > 0); + signal input case; + signal input branches[n]; + signal input vals[n]; + signal output match; + signal output out; + + + // Verify that the `case` is in the possible set of branches + component indicator[n]; + component matchChecker = Contains(n); + signal temp_val[n]; + var sum; + for(var i = 0; i < n; i++) { + indicator[i] = IsZero(); + indicator[i].in <== case - branches[i]; + matchChecker.array[i] <== 1 - indicator[i].out; + temp_val[i] <== indicator[i].out * vals[i]; + sum += temp_val[i]; + } + matchChecker.in <== 0; + match <== matchChecker.out; + + out <== sum; +} + +/* +This template is creates an exhaustive switch statement from a list of branch values. +# Params: + - `m`: the number of switch cases + - `n`: the output array length + +# Inputs: + + - `case`: which case of the switch to select + - `branches[m]`: the values that enable taking different branches in the switch + (e.g., if `branch[i] == 10` then if `case == 10` we set `out == `vals[i]`) + - `vals[m][n]`: the value that is emitted for a given switch case + (e.g., `val[i]` array is emitted on `case == `branch[i]`) + +# Outputs + - `match`: is set to `0` if `case` does not match on any of `branches` + - `out[n]`: the selected output value if one of `branches` is selected (will be `[0,0,...]` otherwise) + ^^^^^^ BEWARE OF THIS FACT ABOVE! +*/ +template SwitchArray(m, n) { + assert(m > 0); + assert(n > 0); + signal input case; + signal input branches[m]; + signal input vals[m][n]; + signal output match; + signal output out[n]; + + + // Verify that the `case` is in the possible set of branches + component indicator[m]; + component matchChecker = Contains(m); + signal component_out[m][n]; + var sum[n]; + for(var i = 0; i < m; i++) { + indicator[i] = IsZero(); + indicator[i].in <== case - branches[i]; + matchChecker.array[i] <== 1 - indicator[i].out; + for(var j = 0; j < n; j++) { + component_out[i][j] <== indicator[i].out * vals[i][j]; + sum[j] += component_out[i][j]; + } + } + matchChecker.in <== 0; + match <== matchChecker.out; + + out <== sum; +} + diff --git a/inputs/search/witness.json b/inputs/search/witness.json new file mode 100644 index 0000000..c69c0a8 --- /dev/null +++ b/inputs/search/witness.json @@ -0,0 +1,803 @@ +{ + "key": [ + 34, + 103, + 108, + 111, + 115, + 115, + 97, + 114, + 121, + 34 + ], + "data": [ + 123, + 10, + 32, + 32, + 32, + 32, + 34, + 103, + 108, + 111, + 115, + 115, + 97, + 114, + 121, + 34, + 58, + 32, + 123, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 116, + 105, + 116, + 108, + 101, + 34, + 58, + 32, + 34, + 101, + 120, + 97, + 109, + 112, + 108, + 101, + 32, + 103, + 108, + 111, + 115, + 115, + 97, + 114, + 121, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 71, + 108, + 111, + 115, + 115, + 68, + 105, + 118, + 34, + 58, + 32, + 123, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 116, + 105, + 116, + 108, + 101, + 34, + 58, + 32, + 34, + 83, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 71, + 108, + 111, + 115, + 115, + 76, + 105, + 115, + 116, + 34, + 58, + 32, + 123, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 71, + 108, + 111, + 115, + 115, + 69, + 110, + 116, + 114, + 121, + 34, + 58, + 32, + 123, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 73, + 68, + 34, + 58, + 32, + 34, + 83, + 71, + 77, + 76, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 83, + 111, + 114, + 116, + 65, + 115, + 34, + 58, + 32, + 34, + 83, + 71, + 77, + 76, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 71, + 108, + 111, + 115, + 115, + 84, + 101, + 114, + 109, + 34, + 58, + 32, + 34, + 83, + 116, + 97, + 110, + 100, + 97, + 114, + 100, + 32, + 71, + 101, + 110, + 101, + 114, + 97, + 108, + 105, + 122, + 101, + 100, + 32, + 77, + 97, + 114, + 107, + 117, + 112, + 32, + 76, + 97, + 110, + 103, + 117, + 97, + 103, + 101, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 65, + 99, + 114, + 111, + 110, + 121, + 109, + 34, + 58, + 32, + 34, + 83, + 71, + 77, + 76, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 65, + 98, + 98, + 114, + 101, + 118, + 34, + 58, + 32, + 34, + 73, + 83, + 79, + 32, + 56, + 56, + 55, + 57, + 58, + 49, + 57, + 56, + 54, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 71, + 108, + 111, + 115, + 115, + 68, + 101, + 102, + 34, + 58, + 32, + 123, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 112, + 97, + 114, + 97, + 34, + 58, + 32, + 34, + 65, + 32, + 109, + 101, + 116, + 97, + 45, + 109, + 97, + 114, + 107, + 117, + 112, + 32, + 108, + 97, + 110, + 103, + 117, + 97, + 103, + 101, + 44, + 32, + 117, + 115, + 101, + 100, + 32, + 116, + 111, + 32, + 99, + 114, + 101, + 97, + 116, + 101, + 32, + 109, + 97, + 114, + 107, + 117, + 112, + 32, + 108, + 97, + 110, + 103, + 117, + 97, + 103, + 101, + 115, + 32, + 115, + 117, + 99, + 104, + 32, + 97, + 115, + 32, + 68, + 111, + 99, + 66, + 111, + 111, + 107, + 46, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 71, + 108, + 111, + 115, + 115, + 83, + 101, + 101, + 65, + 108, + 115, + 111, + 34, + 58, + 32, + 91, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 71, + 77, + 76, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 88, + 77, + 76, + 34, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 93, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 125, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 71, + 108, + 111, + 115, + 115, + 83, + 101, + 101, + 34, + 58, + 32, + 34, + 109, + 97, + 114, + 107, + 117, + 112, + 34, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 125, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 125, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 125, + 10, + 32, + 32, + 32, + 32, + 125, + 10, + 125 + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3b3ca90..28ce12c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/mocha": "^10.0.1", "@types/node": "^22.1.0", "mocha": "^10.2.0", + "poseidon-lite": "^0.2.0", "ts-node": "^10.9.1", "typescript": "^5.1.3" } @@ -1656,6 +1657,13 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/poseidon-lite": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/poseidon-lite/-/poseidon-lite-0.2.0.tgz", + "integrity": "sha512-vivDZnGmz8W4G/GzVA72PXkfYStjilu83rjjUfpL4PueKcC8nfX6hCPh2XhoC5FBgC6y0TA3YuUeUo5YCcNoig==", + "dev": true, + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", diff --git a/package.json b/package.json index d427f13..762e91a 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@types/mocha": "^10.0.1", "@types/node": "^22.1.0", "mocha": "^10.2.0", + "poseidon-lite": "^0.2.0", "ts-node": "^10.9.1", "typescript": "^5.1.3" } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..41c6bec --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "strict": true + } +} \ No newline at end of file