diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4ada78d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: circom + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '16' + + - name: Install dependencies + run: | + npm install + npm install -g snarkjs + + - name: Download and install Circom + run: | + CIRCOM_VERSION=2.1.9 + curl -L https://github.com/iden3/circom/releases/download/v$CIRCOM_VERSION/circom-linux-amd64 -o circom + chmod +x circom + sudo mv circom /usr/local/bin/ + circom --version + + - name: Run tests + run: npx mocha diff --git a/README.md b/README.md index 169b433..723bf33 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,11 @@ npx mocha ``` from the repository root. +To run specific tests, use the `-g` flag for `mocha`, e.g., to run any proof described with "State" we can pass: +``` +npx mocha -g State +``` + ## (MOSTLY DEPRECATED DUE TO CIRCOMKIT) Running an example ``` diff --git a/circuits.json b/circuits.json index 1046f1c..3308dae 100644 --- a/circuits.json +++ b/circuits.json @@ -1,4 +1,20 @@ { + "test_extract": { + "file": "extract", + "template": "Extract", + "params": [ + 4, + 21 + ] + }, + "test_extract_hard": { + "file": "extract", + "template": "Extract", + "params": [ + 4, + 48 + ] + }, "extract": { "file": "extract", "template": "Extract", diff --git a/circuits/bytes.circom b/circuits/bytes.circom index c81d431..6b5c38b 100644 --- a/circuits/bytes.circom +++ b/circuits/bytes.circom @@ -1,8 +1,25 @@ pragma circom 2.1.9; -// Converts a u8 number into a byte, -// verifying that this number does indeed fit into u8 (i.e., will fail if >256 is input) -// See: https://github.com/iden3/circomlib/blob/cff5ab6288b55ef23602221694a6a38a0239dcc0/circuits/bitify.circom +/* +All tests for this file are located in: `./test/bytes.test.ts` + +Some of the functions here were based off the circomlib: +https://github.com/iden3/circomlib/blob/cff5ab6288b55ef23602221694a6a38a0239dcc0/circuits/bitify.circom +*/ + +/* +This function reads in a unsigned 8-bit integer and converts it to an array of bits. + +# Inputs: +- `in`: a number +- `array[n]`: the array we want to search through +- `out`: either `0` or `1` + - `1` if `in` is found inside `array` + - `0` otherwise + +# Constraints: +- `in`: must be between `0` and `2**8 - 1` +*/ template U8ToBits() { signal input in; signal byte[8]; @@ -20,8 +37,16 @@ template U8ToBits() { lc1 === in; } -// If above passes, output can be constrained to input since they're -// valid bytes. +/* +This function reads in an array of unsigned numbers that will be constrained to be valid unsigned 8-bit integers. + +# Inputs: +- `n`: the length of the ASCII string (as integers) to verify +- `in[n]`: a list of numbers + +# Constraints: +- `in[n]`: each element of this array must be between `0` and `2**8-1` +*/ template ASCII(n) { signal input in[n]; diff --git a/circuits/extract.circom b/circuits/extract.circom index deb7010..2997cc7 100644 --- a/circuits/extract.circom +++ b/circuits/extract.circom @@ -2,6 +2,7 @@ pragma circom 2.1.9; include "bytes.circom"; include "operators.circom"; +include "parser.circom"; template Extract(KEY_BYTES, DATA_BYTES) { signal input key[KEY_BYTES]; @@ -20,14 +21,41 @@ template Extract(KEY_BYTES, DATA_BYTES) { component dataASCII = ASCII(DATA_BYTES); dataASCII.in <== data; //--------------------------------------------------------------------------------------------// - component Matches[DATA_BYTES]; - for(var data_pointer = 0; data_pointer < DATA_BYTES - KEY_BYTES; data_pointer++) { - Matches[data_pointer] = IsEqualArray(KEY_BYTES); - for(var key_pointer_offset = 0; key_pointer_offset < KEY_BYTES; key_pointer_offset++) { - Matches[data_pointer].in[0][key_pointer_offset] <== key[key_pointer_offset]; - Matches[data_pointer].in[1][key_pointer_offset] <== data[data_pointer + key_pointer_offset]; - } - log("Matches[", data_pointer, "] = ", Matches[data_pointer].out); - KeyMatches[data_pointer] <== Matches[data_pointer].out; + // Initialze the parser + component State[DATA_BYTES]; + State[0] = StateUpdate(); + State[0].byte <== data[0]; + State[0].tree_depth <== 0; + State[0].parsing_to_key <== 1; // Initialize by saying we are parsing to the first key + State[0].inside_key <== 0; + State[0].parsing_to_value <== 0; + State[0].inside_value <== 0; + State[0].escaping <== 0; + State[0].end_of_kv <== 0; + + for(var data_pointer = 1; data_pointer < DATA_BYTES; data_pointer++) { + State[data_pointer] = StateUpdate(); + State[data_pointer].byte <== data[data_pointer]; + State[data_pointer].tree_depth <== State[data_pointer - 1].next_tree_depth; + State[data_pointer].parsing_to_key <== State[data_pointer - 1].next_parsing_to_key; + State[data_pointer].inside_key <== State[data_pointer - 1].next_inside_key; + State[data_pointer].parsing_to_value <== State[data_pointer - 1].next_parsing_to_value; + State[data_pointer].inside_value <== State[data_pointer - 1].next_inside_value; + State[data_pointer].end_of_kv <== State[data_pointer - 1].next_end_of_kv; + // TODO: For the next state, we should use `next_`, this is only to make this compile for now. + State[data_pointer].escaping <== State[data_pointer - 1].escaping; + + + // Debugging + log("State[", data_pointer, "].tree_depth", "= ", State[data_pointer].tree_depth); + log("State[", data_pointer, "].parsing_to_key", "= ", State[data_pointer].parsing_to_key); + log("State[", data_pointer, "].inside_key", "= ", State[data_pointer].inside_key); + log("State[", data_pointer, "].parsing_to_value", "= ", State[data_pointer].parsing_to_value); + log("State[", data_pointer, "].inside_value", "= ", State[data_pointer].inside_value); + log("State[", data_pointer, "].end_of_kv", "= ", State[data_pointer].end_of_kv); + log("---"); } -} \ No newline at end of file + + // Constrain to have valid JSON (TODO: more is needed) + State[DATA_BYTES - 1].next_tree_depth === 0; +} \ No newline at end of file diff --git a/circuits/operators.circom b/circuits/operators.circom index 8b36796..27d6616 100644 --- a/circuits/operators.circom +++ b/circuits/operators.circom @@ -1,7 +1,25 @@ pragma circom 2.1.9; -// For both see: https://github.com/iden3/circomlib/blob/cff5ab6288b55ef23602221694a6a38a0239dcc0/circuits/comparators.circom +/* +All tests for this file are located in: `./test/operators.test.ts` +Some of the functions here were based off the circomlib: +https://github.com/iden3/circomlib/blob/cff5ab6288b55ef23602221694a6a38a0239dcc0/circuits/comparators.circom +*/ + + +/* +This function is an indicator for zero. + +# Inputs: +- `in`: some number +- `out`: either `0` or `1` + - `1` if `in` is equal to `0` + - `0` otherwise + +# Constraints +- `in`: must be either `0` or `1`. +*/ template IsZero() { signal input in; signal output out; @@ -14,7 +32,15 @@ template IsZero() { in * out === 0; } +/* +This function is an indicator for two equal inputs. +# Inputs: +- `in[2]`: two numbers +- `out`: either `0` or `1` + - `1` if `in[0]` is equal to `in[1]` + - `0` otherwise +*/ template IsEqual() { signal input in[2]; signal output out; @@ -26,6 +52,16 @@ template IsEqual() { isz.out ==> out; } +/* +This function is an indicator for two equal array inputs. + +# Inputs: +- `n`: the length of arrays to compare +- `in[2][n]`: two arrays of `n` numbers +- `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; @@ -44,4 +80,46 @@ template IsEqualArray(n) { totalEqual.in[0] <== n; totalEqual.in[1] <== accum; out <== totalEqual.out; -} \ No newline at end of file +} + + +// TODO: There should be a way to have the below assertion come from the field itself. +/* +This function is an indicator for if an array contains an element. + +# Inputs: +- `n`: the size of the array to search through +- `in`: a number +- `array[n]`: the array we want to search through +- `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 function + 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; +} diff --git a/circuits/parser.circom b/circuits/parser.circom new file mode 100644 index 0000000..3eb446d --- /dev/null +++ b/circuits/parser.circom @@ -0,0 +1,180 @@ +pragma circom 2.1.9; + +include "operators.circom"; +/* +Notes: for `test.json` +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + POINTER | Read In: | STATE +------------------------------------------------- +State[1] | { | PARSING TO KEY +------------------------------------------------- +State[7] | " | INSIDE KEY +------------------------------------------------- +State[12]| " | NOT INSIDE KEY +------------------------------------------------- +State[13]| : | PARSING TO VALUE +------------------------------------------------- +State[15]| " | INSIDE VALUE +------------------------------------------------- +State[19]| " | NOT INSIDE VALUE +------------------------------------------------- +State[20]| " | COMPLETE WITH KV PARSING +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +State[20].next_tree_depth == 0 | VALID JSON +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + + +Notes: +- If there is no comma after leaving a value, then we should not be parsing to key. If anything breaks here, JSON was bad. +*/ + +/* +TODO +*/ +template StateUpdate() { + signal input byte; + + signal input tree_depth; // STATUS_INDICATOR -- how deep in a JSON branch we are, e.g., `user.balance.value` key should be at depth `3`. + // Should always be greater than or equal to `0` (TODO: implement this constraint). + + signal input parsing_to_key; // BIT_FLAG -- whether we are currently parsing bytes until we find the next key (mutally exclusive with `inside_key` and both `*_value flags). + signal input inside_key; // BIT_FLAG -- whether we are currently inside a key (mutually exclusive with `parsing_to_key` and both `*_value` flags). + + signal input parsing_to_value; // BIT_FLAG -- whether we are currently parsing bytes until we find the next value (mutually exclusive with `inside_value` and both `*_key` flags). + signal input inside_value; // BIT_FLAG -- whether we are currently inside a value (mutually exclusive with `parsing_to_value` and both `*_key` flags). + + signal input escaping; // BIT_FLAG -- whether we have hit an escape ASCII symbol inside of a key or value. + + signal input end_of_kv; // BIT_FLAG -- reached end of key-value sequence, looking for comma delimiter or end of file signified by `tree_depth == 0`. + + signal output next_tree_depth; // BIT_FLAG -- next state for `tree_depth`. + signal output next_parsing_to_key; // BIT_FLAG -- next state for `parsing_to_key`. + signal output next_inside_key; // BIT_FLAG -- next state for `inside_key`. + signal output next_parsing_to_value; // BIT_FLAG -- next state for `parsing_to_value`. + signal output next_inside_value; // BIT_FLAG -- next state for `inside_value`. + signal output next_end_of_kv; // BIT_FLAG -- next state for `end_of_kv`. + + // signal output escaping; // TODO: Add this in! + + //--------------------------------------------------------------------------------------------// + //-Delimeters---------------------------------------------------------------------------------// + // - ASCII char: `{` + var start_brace = 123; + // - ASCII char: `}` + var end_brace = 125; + // - ASCII char `[` + var start_bracket = 91; + // - ASCII char `]` + var end_bracket = 93; + // - ASCII char `"` + var quote = 34; + // - ASCII char `:` + var colon = 58; + // - ASCII char `,` + var comma = 44; + //--------------------------------------------------------------------------------------------// + // White space + // - ASCII char: `\n` + var newline = 10; + // - ASCII char: ` ` + var space = 32; + //--------------------------------------------------------------------------------------------// + // Escape + // - ASCII char: `\` + var escape = 92; + //--------------------------------------------------------------------------------------------// + + //--------------------------------------------------------------------------------------------// + //-MACHINE INSTRUCTIONS-----------------------------------------------------------------------// + // TODO: ADD CASE FOR `is_number` for in range 48-57 https://www.ascii-code.com since a value may just be a number + // Output management + component matcher = Switch(8, 3); + var do_nothing[3] = [ 0, 0, 0]; // Command returned by switch if we want to do nothing, e.g. read a whitespace char while looking for a key + var increase_depth[3] = [ 1, 0, 0]; // Command returned by switch if we hit a start brace `{` + var decrease_depth[3] = [-1, 0, 0]; // Command returned by switch if we hit a end brace `}` + var hit_quote[3] = [ 0, 1, 0]; // Command returned by switch if we hit a quote `"` + var hit_colon[3] = [ 0, 0, 1]; // Command returned by switch if we hit a colon `:` + + matcher.branches <== [start_brace, end_brace, quote, colon, start_bracket, end_bracket, comma, escape ]; + matcher.vals <== [increase_depth, decrease_depth, hit_quote, hit_colon, do_nothing, do_nothing, do_nothing, do_nothing]; + matcher.case <== byte; + + + // TODO: These could likely go into a switch statement with the output of the `Switch` above. + // TODO: Also could probably clean up things with de Morgan's laws or whatever. + // An `IF ELSE` template would also be handy! + next_inside_key <== inside_key + (parsing_to_key - inside_key) * matcher.out[1]; // IF (`parsing_to_key` AND `hit_quote`) THEN `next_inside_key <== 1` ELSEIF (`inside_key` AND `hit_quote`) THEN `next_inside_key <== 0` + // - note: can rewrite as -> `inside_key * (1-matcher.out[1]) + parsing_to_key * matcher.out[1]`, but this will not be quadratic (according to circom) + next_parsing_to_key <== parsing_to_key * (1 - matcher.out[1]); // IF (`parsing_to_key` AND `hit_quote`) THEN `parsing_to_key <== 0` + + next_inside_value <== inside_value + (parsing_to_value - inside_value) * matcher.out[1]; // IF (`parsing_to_value` AND `hit_quote`) THEN `next_inside_value <== 1` ELSEIF (`inside_value` AND `hit_quote`) THEN `next_inside_value <==0` + // -note: can rewrite as -> `(1 - inside_value) * matcher_out[1] + parsing_to_value * matcher.out[1] + + signal NOT_PARSING_TO_KEY_AND_NOT_INSIDE_KEY <== (1 - parsing_to_key) * (1 - inside_key); // (NOT `parsing_to_key`) AND (NOT `inside_key`) + signal PARSING_TO_VALUE_AND_NOT_HIT_QUOTE <== parsing_to_value * (1 - matcher.out[1]); // `parsing_to_value` AND (NOT `hit_quote`) + next_parsing_to_value <== PARSING_TO_VALUE_AND_NOT_HIT_QUOTE + NOT_PARSING_TO_KEY_AND_NOT_INSIDE_KEY * matcher.out[2]; // IF (`parsing_to_value` AND (NOT `hit_quote`)) THEN `next_parsing_to_value <== 1 ELSEIF ((NOT `parsing_to_value` AND (NOT `inside_value)) AND `hit_colon`) THEN `next_parsing_to_value <== 1` + + signal NOT_PARSING_TO_VALUE_AND_NOT_INSIDE_VALUE <== (1 - parsing_to_value) * (1 - inside_value); // (NOT `parsing_to_value`) AND (NOT `inside_value`) + next_end_of_kv <== NOT_PARSING_TO_KEY_AND_NOT_INSIDE_KEY * NOT_PARSING_TO_VALUE_AND_NOT_INSIDE_VALUE; // IF ((NOT `parsing_to_key`) AND (NOT `inside_key`)) AND (NOT(`parsing_to_value`) AND NOT( `inside_value)) THEN `next_end_of_kv <== 1` + + + // TODO: Assert this never goes below zero (mod p) + next_tree_depth <== tree_depth + (parsing_to_key + next_end_of_kv) * matcher.out[0]; // IF ((`parsing_to_key` OR `next_end_of_kv`) AND `read_brace` THEN `increase/decrease_depth` + + // Constrain bit flags + next_parsing_to_key * (1 - next_parsing_to_key) === 0; // - constrain that `next_parsing_to_key` remain a bit flag + next_inside_key * (1 - next_inside_key) === 0; // - constrain that `next_inside_key` remain a bit flag + next_parsing_to_value * (1 - next_parsing_to_value) === 0; // - constrain that `next_parsing_to_value` remain a bit flag + next_inside_value * (1 - next_inside_value) === 0; // - constrain that `next_inside_value` remain a bit flag + next_end_of_kv * (1 - next_end_of_kv) === 0; // - constrain that `next_end_of_kv` remain a bit flag + + // TODO: Can hit comma and then be sent to next KV, so comma will engage `parsing_to_key` + //--------------------------------------------------------------------------------------------// +} + +/* +This function is creates an exhaustive switch statement from `0` up to `n`. + +# Inputs: +- `m`: the number of switch cases +- `n`: the output array length +- `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) +*/ +template Switch(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; +} \ No newline at end of file diff --git a/circuits/test/bytes.test.ts b/circuits/test/bytes.test.ts index 7521c2a..2d26bde 100644 --- a/circuits/test/bytes.test.ts +++ b/circuits/test/bytes.test.ts @@ -12,23 +12,23 @@ describe("bytes", () => { console.log("#constraints:", await circuit.getConstraintCount()); }); - it("valid witness 0", async () => { + it("(valid) witness: in = 0", async () => { await circuit.expectPass({ in: 0 }); }); - it("valid witness 15", async () => { + it("(valid) witness: in = 15", async () => { await circuit.expectPass({ in: 15 }); }); - it("valid witness 255", async () => { + it("(valid) witness: in = 255", async () => { await circuit.expectPass({ in: 255 }); }); - it("failing witness 256", async () => { + it("(invalid) witness: in = 256", async () => { await circuit.expectFail({ in: 256 }); }); - it("failing witness 42069", async () => { + it("(invalid) witness: in = 42069", async () => { await circuit.expectFail({ in: 42069 }); }); }); @@ -43,13 +43,13 @@ describe("bytes", () => { console.log("#constraints:", await circuit.getConstraintCount()); }); - it("valid ASCII input", async () => { + 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 ASCII input", async () => { + it("(invalid) witness: in = [256, ...]", async () => { await circuit.expectFail( { in: [256, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33] } ); diff --git a/circuits/test/operators.test.ts b/circuits/test/operators.test.ts index 6e438dc..378708b 100644 --- a/circuits/test/operators.test.ts +++ b/circuits/test/operators.test.ts @@ -1,9 +1,8 @@ import { circomkit, WitnessTester } from "./common"; describe("operators", () => { - let circuit: WitnessTester<["in"], ["out"]>; - describe("IsZero", () => { + let circuit: WitnessTester<["in"], ["out"]>; before(async () => { circuit = await circomkit.WitnessTester(`IsZero`, { file: "circuits/operators", @@ -12,20 +11,20 @@ describe("operators", () => { console.log("#constraints:", await circuit.getConstraintCount()); }); - it("witness 0", async () => { + it("witness: 0", async () => { await circuit.expectPass( { in: 0 }, { out: 1 }); }); - it("witness 1", async () => { + it("witness: 1", async () => { await circuit.expectPass( { in: 1 }, { out: 0 } ); }); - it("witness 42069", async () => { + it("witness: 42069", async () => { await circuit.expectPass( { in: 42069 }, { out: 0 } @@ -34,6 +33,7 @@ describe("operators", () => { }); describe("IsEqual", () => { + let circuit: WitnessTester<["in"], ["out"]>; before(async () => { circuit = await circomkit.WitnessTester(`IsEqual`, { file: "circuits/operators", @@ -42,21 +42,21 @@ describe("operators", () => { console.log("#constraints:", await circuit.getConstraintCount()); }); - it("witness [0,0]", async () => { + it("witness: [0,0]", async () => { await circuit.expectPass( { in: [0, 0] }, { out: 1 } ); }); - it("witness [42069, 42069]", async () => { + it("witness: [42069, 42069]", async () => { await circuit.expectPass( { in: [42069, 42069] }, { out: 1 }, ); }); - it("witness [42069, 0]", async () => { + it("witness: [42069, 0]", async () => { await circuit.expectPass( { in: [42069, 0] }, { out: 0 }, @@ -65,6 +65,7 @@ describe("operators", () => { }); describe("IsEqualArray", () => { + let circuit: WitnessTester<["in"], ["out"]>; before(async () => { circuit = await circomkit.WitnessTester(`IsEqualArray`, { file: "circuits/operators", @@ -74,46 +75,87 @@ describe("operators", () => { console.log("#constraints:", await circuit.getConstraintCount()); }); - it("witness [[0,0,0],[0,0,0]]", async () => { + 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 () => { + 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 () => { + 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 () => { + 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 () => { + 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 () => { + 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/operators", + 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 } + ); + }); + + }); }); diff --git a/circuits/test/parser.test.ts b/circuits/test/parser.test.ts new file mode 100644 index 0000000..5f3488d --- /dev/null +++ b/circuits/test/parser.test.ts @@ -0,0 +1,116 @@ +import { circomkit, WitnessTester } from "./common"; + +describe("parser", () => { + describe("Switch", () => { + let circuit: WitnessTester<["case", "branches", "vals"], ["match", "out"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`Switch`, { + file: "circuits/parser", + template: "Switch", + 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("StateUpdate", () => { + let circuit: WitnessTester< + ["byte", "tree_depth", "parsing_to_key", "inside_key", "parsing_to_value", "inside_value", "escaping", "end_of_kv"], + ["next_tree_depth", "next_parsing_to_key", "next_inside_key", "next_parsing_to_value", "next_inside_value", "next_end_of_kv"] + >; + + function generateTestCase(input: any, expected: any) { + const description = Object.entries(input) + .map(([key, value]) => `${key} = ${value}`) + .join(", "); + + it(`witness: ${description}`, async () => { + await circuit.expectPass(input, expected); + }); + } + + before(async () => { + circuit = await circomkit.WitnessTester(`StateUpdate`, { + file: "circuits/parser", + template: "StateUpdate", + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + let init = { + byte: 0, + tree_depth: 0, + parsing_to_key: 1, + inside_key: 0, + parsing_to_value: 0, + inside_value: 0, + escaping: 0, + end_of_kv: 0, + }; + + // Test 1: init setup -> `do_nothing` byte + let out = { + next_tree_depth: init.tree_depth, + next_parsing_to_key: init.parsing_to_key, + next_inside_key: init.inside_key, + next_parsing_to_value: init.parsing_to_value, + next_inside_value: init.inside_value, + next_end_of_kv: init.end_of_kv + }; + generateTestCase(init, out); + + // Test 2: init setup -> `{` is read + let read_start_brace = init; + read_start_brace.byte = 123; + let read_start_brace_out = out; + read_start_brace_out.next_tree_depth = 1; + generateTestCase(read_start_brace, read_start_brace_out); + + }); + +}); + + diff --git a/create_witness/src/main.rs b/create_witness/src/main.rs index a715711..469478a 100644 --- a/create_witness/src/main.rs +++ b/create_witness/src/main.rs @@ -1,13 +1,15 @@ use std::io::Write; -pub const KEY: &[u8] = b"\"glossary\"".as_slice(); +// pub const KEY: &[u8] = b"\"glossary\"".as_slice(); +pub const KEY: &[u8] = b"key1".as_slice(); pub const KEYS: &[&[u8]] = &[ b"\"glossary\"".as_slice(), b"\"GlossDiv\"".as_slice(), b"\"title\"".as_slice(), ]; -pub const DATA: &[u8] = include_bytes!("../../json_examples/example.json"); +// pub const DATA: &[u8] = include_bytes!("../../json_examples/example.json"); +pub const DATA: &[u8] = include_bytes!("../../json_examples/test.json"); #[derive(serde::Serialize)] pub struct Witness { @@ -57,7 +59,7 @@ pub fn main() { // num_data_bytes: DATA.len(), // For now we can set this to be the same data: DATA.to_vec(), }; - let mut file = std::fs::File::create("circuit/witness.json").unwrap(); + let mut file = std::fs::File::create("inputs/test_extract/input.json").unwrap(); file.write_all(serde_json::to_string_pretty(&witness).unwrap().as_bytes()) .unwrap(); } diff --git a/inputs/extract/witness.json b/inputs/extract/input.json similarity index 100% rename from inputs/extract/witness.json rename to inputs/extract/input.json diff --git a/inputs/test_extract/input.json b/inputs/test_extract/input.json new file mode 100644 index 0000000..d99f3f3 --- /dev/null +++ b/inputs/test_extract/input.json @@ -0,0 +1,31 @@ +{ + "key": [ + 107, + 101, + 121, + 49 + ], + "data": [ + 123, + 10, + 32, + 32, + 32, + 32, + 34, + 107, + 101, + 121, + 49, + 34, + 58, + 32, + 34, + 97, + 98, + 99, + 34, + 10, + 125 + ] +} \ No newline at end of file diff --git a/inputs/test_extract_hard/input.json b/inputs/test_extract_hard/input.json new file mode 100644 index 0000000..a80958f --- /dev/null +++ b/inputs/test_extract_hard/input.json @@ -0,0 +1,58 @@ +{ + "key": [ + 107, + 101, + 121, + 49 + ], + "data": [ + 123, + 10, + 32, + 32, + 32, + 32, + 34, + 107, + 101, + 121, + 49, + 34, + 58, + 32, + 34, + 123, + 125, + 97, + 44, + 98, + 99, + 125, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 34, + 107, + 101, + 121, + 50, + 34, + 58, + 32, + 34, + 92, + 34, + 97, + 98, + 99, + 92, + 34, + 34, + 10, + 125 + ] +} \ No newline at end of file diff --git a/json_examples/spotify_response.json b/json_examples/spotify_response.json new file mode 100644 index 0000000..6416884 --- /dev/null +++ b/json_examples/spotify_response.json @@ -0,0 +1,45 @@ +{ + "data": { + "me": { + "profile": { + "topArtists": { + "__typename": "ArtistPageV2", + "items": [ + { + "data": { + "__typename": "Artist", + "profile": { + "name": "Taylor Swift" + }, + "uri": "spotify:artist:06HL4z0CvFAxyc27GXpf02", + "visuals": { + "avatarImage": { + "sources": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab6761610000e5ebe672b5f553298dcdccb0e676", + "width": 640 + }, + { + "height": 160, + "url": "https://i.scdn.co/image/ab6761610000f178e672b5f553298dcdccb0e676", + "width": 160 + }, + { + "height": 320, + "url": "https://i.scdn.co/image/ab67616100005174e672b5f553298dcdccb0e676", + "width": 320 + } + ] + } + } + } + } + ], + "totalCount": 1 + } + } + } + }, + "extensions": {} +} \ No newline at end of file diff --git a/json_examples/test.json b/json_examples/test.json new file mode 100644 index 0000000..31a17d9 --- /dev/null +++ b/json_examples/test.json @@ -0,0 +1,3 @@ +{ + "key1": "abc" +} \ No newline at end of file diff --git a/json_examples/test_hard.json b/json_examples/test_hard.json new file mode 100644 index 0000000..7904a51 --- /dev/null +++ b/json_examples/test_hard.json @@ -0,0 +1,4 @@ +{ + "key1": "{}a,bc}", + "key2": "\"abc\"" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d9ff388..3b3ca90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "license": "Apache-2.0", "dependencies": { "circomkit": "^0.2.1", "circomlib": "^2.0.5"