diff --git a/circuits/extract.circom b/circuits/extract.circom index 03c7a79..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) { diff --git a/circuits/language.circom b/circuits/language.circom index 3528c80..8063503 100644 --- a/circuits/language.circom +++ b/circuits/language.circom @@ -30,12 +30,11 @@ template Syntax() { template Command() { // STATE = [read_write_value, parsing_string, parsing_number] - signal output NOTHING[3] <== [0, 0, -1 ]; // Command returned by switch if we want to do nothing, e.g. read a whitespace char while looking for a key signal output START_BRACE[3] <== [1, 0, 0 ]; // Command returned by switch if we hit a start brace `{` signal output END_BRACE[3] <== [-1, 0, -1 ]; // Command returned by switch if we hit a end brace `}` - signal output START_BRACKET[3] <== [2, 0, 0 ]; // TODO: Might want `in_value` to toggle. Command returned by switch if we hit a start bracket `[` (TODO: could likely be combined with end bracket) - signal output END_BRACKET[3] <== [-2, 0, -1 ]; // Command returned by switch if we hit a start bracket `]` - signal output QUOTE[3] <== [0, 1, 0 ]; // TODO: Mightn ot want this to toglle `parsing_array`. Command returned by switch if we hit a quote `"` + signal output START_BRACKET[3] <== [2, 0, 0 ]; // Command returned by switch if we hit a start bracket `[` + signal output END_BRACKET[3] <== [-2, 0, -1 ]; // Command returned by switch if we hit a start bracket `]` + signal output QUOTE[3] <== [0, 1, 0 ]; // Command returned by switch if we hit a quote `"` signal output COLON[3] <== [3, 0, 0 ]; // Command returned by switch if we hit a colon `:` signal output COMMA[3] <== [4, 0, -1 ]; // Command returned by switch if we hit a comma `,` signal output NUMBER[3] <== [256, 0, 1 ]; // Command returned by switch if we hit some decimal number (e.g., ASCII 48-57) diff --git a/circuits/parser.circom b/circuits/parser.circom index fce6110..bd157e2 100644 --- a/circuits/parser.circom +++ b/circuits/parser.circom @@ -1,21 +1,56 @@ +/* +# `parser` +This module consists of the core parsing components for generating proofs of selective disclosure in JSON. + +## Layout +The key ingredients of `parser` are: + - `StateUpdate`: has as input a current state of a stack-machine parser. + Also takes in a `byte` as input which combines with the current state + to produce the `next_*` states. + - `StateToMask`: Reads the current state to decide whether accept instruction tokens + or ignore them for the current task (e.g., ignore `[` if `parsing_string == 1`). + - `GetTopOfStack`: Helper function that yields the topmost allocated stack value + and a pointer (index) to that value. + - `RewriteStack`: Combines all the above data and produces the `next_stack`. + +`parser` brings in many functions from the `utils` module and `language`. +The inclusion of `langauge` allows for this file to (eventually) be generic over +a grammar for different applications (e.g., HTTP, YAML, TOML, etc.). + +## Testing +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"; /* -TODO: Change the values to push onto stack to be given by START_BRACE, COLON, etc. -*/ +This template is for updating the state of the parser from a current state to a next state. + +# Params: + - `MAX_STACK_HEIGHT`: the maximum stack height that can be used before triggering overflow. +# Inputs: + - `byte` : the byte value of ASCII that was read by the parser. + - `stack[MAX_STACK_HEIGHT][2]`: the stack machine's current stack. + - `parsing_number` : a bool flag that indicates whether the parser is currently parsing a string or not. + - `parsing_number` : a bool flag that indicates whether the parser is currently parsing a number or not. + +# Outputs: + - `next_stack[MAX_STACK_HEIGHT][2]`: the stack machine's stack after reading `byte`. + - `next_parsing_number` : a bool flag that indicates whether the parser is currently parsing a string or not after reading `byte`. + - `next_parsing_number` : a bool flag that indicates whether the parser is currently parsing a number or not after reading `byte`. +*/ template StateUpdate(MAX_STACK_HEIGHT) { - signal input byte; + signal input byte; // TODO: Does this need to be constrained within here? - signal input stack[MAX_STACK_HEIGHT][2]; // STACK -- how deep in a JSON nest we are and what type we are currently inside (e.g., `1` for object, `-1` for array). + signal input stack[MAX_STACK_HEIGHT][2]; signal input parsing_string; signal input parsing_number; - // TODO - // signal parsing_boolean; - // signal parsing_null; signal output next_stack[MAX_STACK_HEIGHT][2]; signal output next_parsing_string; @@ -24,74 +59,127 @@ template StateUpdate(MAX_STACK_HEIGHT) { component Syntax = Syntax(); component Command = Command(); - var read_write_value = 0; - var parsing_state[3] = [read_write_value, parsing_string, parsing_number]; - //--------------------------------------------------------------------------------------------// - //-State machine updating---------------------------------------------------------------------// - // * yield instruction based on what byte we read * - component matcher = SwitchArray(8, 3); - matcher.branches <== [Syntax.START_BRACE, Syntax.END_BRACE, Syntax.QUOTE, Syntax.COLON, Syntax.COMMA, Syntax.START_BRACKET, Syntax.END_BRACKET, Syntax.NUMBER ]; - matcher.vals <== [Command.START_BRACE, Command.END_BRACE, Command.QUOTE, Command.COLON, Command.COMMA, Command.START_BRACKET, Command.END_BRACKET, Command.NUMBER]; - component numeral_range_check = InRange(8); - numeral_range_check.in <== byte; - numeral_range_check.range <== [48, 57]; // ASCII NUMERALS - // log("isNumeral:", numeral_range_check.out); - signal IS_NUMBER <== numeral_range_check.out * Syntax.NUMBER; - matcher.case <== (1 - numeral_range_check.out) * byte + IS_NUMBER; // IF (NOT is_number) THEN byte ELSE 256 + // Break down what was read + // * read in a start brace `{` * + component readStartBrace = IsEqual(); + readStartBrace.in <== [byte, Syntax.START_BRACE]; + // * read in an end brace `}` * + component readEndBrace = IsEqual(); + readEndBrace.in <== [byte, Syntax.END_BRACE]; + // * read in a start bracket `[` * + component readStartBracket = IsEqual(); + readStartBracket.in <== [byte, Syntax.START_BRACKET]; + // * read in an end bracket `]` * + component readEndBracket = IsEqual(); + readEndBracket.in <== [byte, Syntax.END_BRACKET]; + // * read in a colon `:` * + component readColon = IsEqual(); + readColon.in <== [byte, Syntax.COLON]; + // * read in a comma `,` * + component readComma = IsEqual(); + readComma.in <== [byte, Syntax.COMMA]; + // * read in some delimeter * + signal readDelimeter <== readStartBrace.out + readEndBrace.out + readStartBracket.out + readEndBracket.out + + readColon.out + readComma.out; + // * read in some number * + component readNumber = InRange(8); + readNumber.in <== byte; + readNumber.range <== [48, 57]; // This is the range where ASCII digits are + // * read in a quote `"` * + component readQuote = IsEqual(); + readQuote.in <== [byte, Syntax.QUOTE]; + component readOther = IsZero(); + readOther.in <== readDelimeter + readNumber.out + readQuote.out; + //--------------------------------------------------------------------------------------------// + // Yield instruction based on what byte we read * + component readStartBraceInstruction = ScalarArrayMul(3); + readStartBraceInstruction.scalar <== readStartBrace.out; + readStartBraceInstruction.array <== Command.START_BRACE; + component readEndBraceInstruction = ScalarArrayMul(3); + readEndBraceInstruction.scalar <== readEndBrace.out; + readEndBraceInstruction.array <== Command.END_BRACE; + component readStartBracketInstruction = ScalarArrayMul(3); + readStartBracketInstruction.scalar <== readStartBracket.out; + readStartBracketInstruction.array <== Command.START_BRACKET; + component readEndBracketInstruction = ScalarArrayMul(3); + readEndBracketInstruction.scalar <== readEndBracket.out; + readEndBracketInstruction.array <== Command.END_BRACKET; + component readColonInstruction = ScalarArrayMul(3); + readColonInstruction.scalar <== readColon.out; + readColonInstruction.array <== Command.COLON; + component readCommaInstruction = ScalarArrayMul(3); + readCommaInstruction.scalar <== readComma.out; + readCommaInstruction.array <== Command.COMMA; + component readNumberInstruction = ScalarArrayMul(3); + readNumberInstruction.scalar <== readNumber.out; + readNumberInstruction.array <== Command.NUMBER; + component readQuoteInstruction = ScalarArrayMul(3); + readQuoteInstruction.scalar <== readQuote.out; + readQuoteInstruction.array <== Command.QUOTE; + component Instruction = GenericArrayAdd(3,8); + Instruction.arrays <== [readStartBraceInstruction.out, readEndBraceInstruction.out, + readStartBracketInstruction.out, readEndBracketInstruction.out, + readColonInstruction.out, readCommaInstruction.out, + readNumberInstruction.out, readQuoteInstruction.out]; + //--------------------------------------------------------------------------------------------// + // Apply state changing data // * get the instruction mask based on current state * - component mask = StateToMask(MAX_STACK_HEIGHT); - // mask.in <== parsing_state; - mask.in <== [matcher.out[0],parsing_string,parsing_number]; // TODO: This is awkward. Things need to be rewritten - - + component mask = StateToMask(MAX_STACK_HEIGHT); + mask.readDelimeter <== readDelimeter; + mask.readNumber <== readNumber.out; + mask.parsing_string <== parsing_string; + mask.parsing_number <== parsing_number; // * multiply the mask array elementwise with the instruction array * component mulMaskAndOut = ArrayMul(3); mulMaskAndOut.lhs <== mask.out; - mulMaskAndOut.rhs <== matcher.out; - // * add the masked instruction to the state to get new state * - component addToState = ArrayAdd(3); - addToState.lhs <== parsing_state; - addToState.rhs <== mulMaskAndOut.out; - - // * set the new state * + mulMaskAndOut.rhs <== [Instruction.out[0], Instruction.out[1], Instruction.out[2] - readOther.out]; + // * compute the new stack * component newStack = RewriteStack(MAX_STACK_HEIGHT); newStack.stack <== stack; - newStack.read_write_value <== addToState.out[0]; + newStack.read_write_value <== mulMaskAndOut.out[0]; + newStack.readStartBrace <== readStartBrace.out; + newStack.readStartBracket <== readStartBracket.out; + newStack.readEndBrace <== readEndBrace.out; + newStack.readEndBracket <== readEndBracket.out; + newStack.readColon <== readColon.out; + newStack.readComma <== readComma.out; + // * set all the next state of the parser * next_stack <== newStack.next_stack; - next_parsing_string <== addToState.out[1]; - next_parsing_number <== addToState.out[2]; - - // for(var i = 0; i < 4; i++) { - // log("matcher.out[",i,"]: ", matcher.out[i]); - // log("mask.out[",i,"]: ", mask.out[i]); - // log("mulMaskAndOut[",i,"]: ", mulMaskAndOut.out[i]); - // } - - //--------------------------------------------------------------------------------------------// - //-Constraints--------------------------------------------------------------------------------// - // * constrain bit flags * - // next_parsing_key * (1 - next_parsing_key) === 0; // - constrain that `next_parsing_key` remain a bit flag - // next_inside_key * (1 - next_inside_key) === 0; // - constrain that `next_inside_key` remain a bit flag - // next_parsing_value * (1 - next_parsing_value) === 0; // - constrain that `next_parsing_value` remain a bit flag - // next_inside_value * (1 - next_inside_value) === 0; // - constrain that `next_inside_value` remain a bit flag - // // * constrain `tree_depth` to never hit -1 (TODO: should always moves in 1 bit increments?) - // component isMinusOne = IsEqual(); - // isMinusOne.in[0] <== -1; - // isMinusOne.in[1] <== next_tree_depth; - // isMinusOne.out === 0; + next_parsing_string <== parsing_string + mulMaskAndOut.out[1]; + next_parsing_number <== parsing_number + mulMaskAndOut.out[2]; //--------------------------------------------------------------------------------------------// } +/* +This template is for updating the state of the parser from a current state to a next state. + +# Params: + - `n`: tunable parameter for the number of `parsing_states` needed (TODO: could be removed). + +# Inputs: + - `readDelimeter` : a bool flag that indicates whether the byte value read was a delimeter. + - `readNumber` : a bool flag that indicates whether the byte value read was a number. + - `parsing_number`: a bool flag that indicates whether the parser is currently parsing a string or not. + - `parsing_number`: a bool flag that indicates whether the parser is currently parsing a number or not. + +# Outputs: + - `out[3]`: an array of values fed to update the stack and the parsing state flags. + - 0: mask for `read_write_value` + - 1: mask for `parsing_string` + - 2: mask for `parsing_number` +*/ template StateToMask(n) { // TODO: Probably need to assert things are bits where necessary. - signal input in[3]; + signal input readDelimeter; + signal input readNumber; + signal input parsing_string; + signal input parsing_number; signal output out[3]; - signal read_write_value <== in[0]; - signal parsing_string <== in[1]; - signal parsing_number <== in[2]; + // `parsing_string` can change: + out[1] <== 1 - 2 * parsing_string; // `read_write_value`can change: IF NOT `parsing_string` out[0] <== (1 - parsing_string); @@ -99,38 +187,44 @@ template StateToMask(n) { // `parsing_string` can change: out[1] <== 1 - 2 * parsing_string; - // `parsing_number` can change: - component isDelimeter = InRange(8); - isDelimeter.in <== read_write_value; - isDelimeter.range[0] <== 1; - isDelimeter.range[1] <== 4; - component isNumber = IsEqual(); - isNumber.in <== [read_write_value, 256]; - component isParsingString = IsEqual(); - isParsingString.in[0] <== parsing_string; - isParsingString.in[1] <== 1; - component isParsingNumber = IsEqual(); - isParsingNumber.in[0] <== parsing_number; - isParsingNumber.in[1] <== 1; - component toParseNumber = Switch(16); - // TODO: Could combine this into something that returns arrays so that we can set the mask more easily. - toParseNumber.branches <== [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; - toParseNumber.vals <== [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]; - component stateToNum = Bits2Num(4); - stateToNum.in <== [isParsingString.out, isParsingNumber.out, isNumber.out, isDelimeter.out]; - // 1 2 4 8 - toParseNumber.case <== stateToNum.out; - // log("isNumber: ", isNumber.out); - // log("isParsingString: ", isParsingString.out); - // log("isParsingNumber: ", isParsingNumber.out); - // log("isDelimeter: ", isDelimeter.out); - // log("stateToNum: ", stateToNum.out); - // log("toParseNumber: ", toParseNumber.out); - - out[2] <== toParseNumber.out; + + //--------------------------------------------------------------------------------------------// + // `parsing_number` is more complicated to deal with + /* We have the possible relevant states below: + [isParsingString, isParsingNumber, readNumber, readDelimeter]; + 1 2 4 8 + 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 + possible. + Below is an optimized version that could instead be done with a `Switch` + */ + signal parsingNumberReadDelimeter <== parsing_number * (readDelimeter); + signal readNumberNotParsingNumber <== (1 - parsing_number) * readNumber; + signal notParsingStringAndParsingNumberReadDelimeterOrReadNumberNotParsingNumber <== (1 - parsing_string) * (parsingNumberReadDelimeter + readNumberNotParsingNumber); + // 10 above ^^^^^^^^^^^^^^^^^ 4 above ^^^^^^^^^^^^^^^^^^ + signal parsingNumberNotReadNumber <== parsing_number * (1 - readNumber) ; + signal parsingNumberNotReadNumberNotReadDelimeter <== parsingNumberNotReadNumber * (1-readDelimeter); + out[2] <== notParsingStringAndParsingNumberReadDelimeterOrReadNumberNotParsingNumber + parsingNumberNotReadNumberNotReadDelimeter; + // Sorry about the long names, but they hopefully read clearly! } // TODO: Check if underconstrained +/* +This template is for getting the values at the top of the stack as well as the pointer to the top. + +# Params: + - `n`: tunable parameter for the stack height. + +# Inputs: + - `stack[n][2]` : the stack to get the values and pointer of. + +# Outputs: + - `value[2]`: the value at the top of the stack + - `pointer` : the pointer for the top of stack index +*/ template GetTopOfStack(n) { signal input stack[n][2]; signal output value[2]; @@ -152,14 +246,39 @@ template GetTopOfStack(n) { pointer <== selector; } -// TODO: IMPORTANT NOTE, THE STACK IS CONSTRAINED TO 2**8 so the LessThan and GreaterThan work (could be changed) +// TODO: IMPORTANT NOTE, THE STACK IS CONSTRAINED TO 2**8 so the InRange work (could be changed) +/* +This template is for updating the stack given the current stack and the byte we read in `StateUpdate`. + +# Params: + - `n`: tunable parameter for the number of bits needed to represent the `MAX_STACK_HEIGHT`. + +# Inputs: + - `read_write_value` : what value should be pushed to or popped from the stack. + - `readStartBrace` : a bool flag that indicates whether the byte value read was a start brace `{`. + - `readEndBrace` : a bool flag that indicates whether the byte value read was a end brace `}`. + - `readStartBracket` : a bool flag that indicates whether the byte value read was a start bracket `[`. + - `readEndBracket` : a bool flag that indicates whether the byte value read was a end bracket `]`. + - `readColon` : a bool flag that indicates whether the byte value read was a colon `:`. + - `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. +*/ template RewriteStack(n) { assert(n < 2**8); signal input stack[n][2]; signal input read_write_value; + signal input readStartBrace; + signal input readStartBracket; + signal input readEndBrace; + signal input readEndBracket; + signal input readColon; + signal input readComma; + signal output next_stack[n][2]; - //-----------------------------------------------------------------------------// + //--------------------------------------------------------------------------------------------// // * scan value on top of stack * component topOfStack = GetTopOfStack(n); topOfStack.stack <== stack; @@ -173,63 +292,45 @@ template RewriteStack(n) { component inArray = IsEqual(); inArray.in[0] <== current_value[0]; inArray.in[1] <== 2; - //-----------------------------------------------------------------------------// + //--------------------------------------------------------------------------------------------// - //-----------------------------------------------------------------------------// - // * check what value was read * - // * read in a start brace `{` * - component readStartBrace = IsEqual(); - readStartBrace.in <== [read_write_value, 1]; - // * read in a start bracket `[` * - component readStartBracket = IsEqual(); - readStartBracket.in <== [read_write_value, 2]; - // * read in an end brace `}` * - component readEndBrace = IsEqual(); - readEndBrace.in <== [read_write_value, -1]; - // * read in an end bracket `]` * - component readEndBracket = IsEqual(); - readEndBracket.in <== [read_write_value, -2]; - // * read in a colon `:` * - component readColon = IsEqual(); - readColon.in[0] <== 3; - readColon.in[1] <== read_write_value; - // * read in a comma `,` * - component readComma = IsEqual(); - readComma.in[0] <== 4; - readComma.in[1] <== read_write_value; + //--------------------------------------------------------------------------------------------// // * composite signals * - signal readEndChar <== readEndBrace.out + readEndBracket.out; - signal readCommaInArray <== readComma.out * inArray.out; - signal readCommaNotInArray <== readComma.out * (1 - inArray.out); - //-----------------------------------------------------------------------------// + signal readCommaInArray <== readComma * inArray.out; + signal readCommaNotInArray <== readComma * (1 - inArray.out); + //--------------------------------------------------------------------------------------------// - //-----------------------------------------------------------------------------// + //--------------------------------------------------------------------------------------------// // * determine whether we are pushing or popping from the stack * component isPush = IsEqual(); - isPush.in <== [readStartBrace.out + readStartBracket.out, 1]; + isPush.in <== [readStartBrace + readStartBracket, 1]; component isPop = IsEqual(); - isPop.in <== [readEndBrace.out + readEndBracket.out, 1]; + isPop.in <== [readEndBrace + readEndBracket, 1]; // * set an indicator array for where we are pushing to or popping from* component indicator[n]; for(var i = 0; i < n; i++) { // Points indicator[i] = IsZero(); - indicator[i].in <== pointer - isPop.out - readColon.out - readComma.out - i; // Note, pointer points to unallocated region! + indicator[i].in <== pointer - isPop.out - readColon - readComma - i; // Note, pointer points to unallocated region! } - //-----------------------------------------------------------------------------// - + //--------------------------------------------------------------------------------------------// - signal stack_change_value[2] <== [(isPush.out + isPop.out) * read_write_value, readColon.out + readCommaInArray - readCommaNotInArray]; + //--------------------------------------------------------------------------------------------// + // * loop to modify the stack by rebuilding it * + signal stack_change_value[2] <== [(isPush.out + isPop.out) * read_write_value, readColon + readCommaInArray - readCommaNotInArray]; signal second_index_clear[n]; for(var i = 0; i < n; i++) { next_stack[i][0] <== stack[i][0] + indicator[i].out * stack_change_value[0]; - second_index_clear[i] <== stack[i][1] * readEndChar; + second_index_clear[i] <== stack[i][1] * (readEndBrace + readEndBracket); // Checking if we read some end char next_stack[i][1] <== stack[i][1] + indicator[i].out * (stack_change_value[1] - second_index_clear[i]); } + //--------------------------------------------------------------------------------------------// - // TODO: WE CAN'T LEAVE 8 HERE, THIS HAS TO DEPEND ON THE STACK HEIGHT AS IT IS THE NUM BITS NEEDED TO REPR STACK HEIGHT IN BINARY + //--------------------------------------------------------------------------------------------// + // * check for under or overflow component isUnderflowOrOverflow = InRange(8); isUnderflowOrOverflow.in <== pointer - isPop.out + isPush.out; isUnderflowOrOverflow.range <== [0,n]; isUnderflowOrOverflow.out === 1; + //--------------------------------------------------------------------------------------------// } \ No newline at end of file diff --git a/circuits/search.circom b/circuits/search.circom index 2aeff1b..cfd8c12 100644 --- a/circuits/search.circom +++ b/circuits/search.circom @@ -1,8 +1,9 @@ -pragma circom 2.1.9; +pragma circom 2.1.9; include "circomlib/circuits/mux1.circom"; include "./utils/hash.circom"; -include "utils.circom"; +include "./utils/operators.circom"; +include "./utils/array.circom"; /* SubstringSearch diff --git a/circuits/test/parser/values.test.ts b/circuits/test/parser/values.test.ts index 6661ad6..e8930bf 100644 --- a/circuits/test/parser/values.test.ts +++ b/circuits/test/parser/values.test.ts @@ -22,43 +22,91 @@ describe("StateUpdate :: Values", () => { }); } - //-TEST_1----------------------------------------------------------// - // idea: Read a number value after a key in an object. - // state: stack == [[1, 1], [0, 0], [0, 0], [0, 0]] - // read: `0` - // expect: stack --> [[1, 1], [0, 0], [0, 0], [0, 0]] - // parsing_number --> 1 - let read_number = { ...INITIAL_IN }; - read_number.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; - read_number.byte = Numbers.ZERO; - let read_number_out = { ...INITIAL_OUT }; - read_number_out.next_stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; - read_number_out.next_parsing_number = 1; - generatePassCase(read_number, read_number_out, ">>>> `0` read"); + describe("StateUpdate :: Values :: Number", () => { + //-TEST_1----------------------------------------------------------// + // idea: Read a number value after a key in an object. + // state: stack == [[1, 1], [0, 0], [0, 0], [0, 0]] + // read: `0` + // expect: stack --> [[1, 1], [0, 0], [0, 0], [0, 0]] + // parsing_number --> 1 + let read_number = { ...INITIAL_IN }; + read_number.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + read_number.byte = Numbers.ZERO; + let read_number_out = { ...INITIAL_OUT }; + read_number_out.next_stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + read_number_out.next_parsing_number = 1; + generatePassCase(read_number, read_number_out, ">>>> `0` read"); - // // TODO: Note that reading a space while reading a number will not throw an error! + // // TODO: Note that reading a space while reading a number will not throw an error! - //-TEST_2----------------------------------------------------------// - // idea: Inside a number value after a key in an object. - // state: pointer == 2, stack == [1,3,0,0], parsing_number == 1 - // read: `1` - // expect: pointer --> 2 - // stack --> [1,3,0,0] - // parsing_number --> 0 - let inside_number_continue = { ...INITIAL_IN }; - inside_number_continue.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; - inside_number_continue.parsing_number = 1; - inside_number_continue.byte = Numbers.ONE; - let inside_number_continue_out = { ...INITIAL_OUT }; - inside_number_continue_out.next_stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; - inside_number_continue_out.next_parsing_number = 1; - generatePassCase(inside_number_continue, inside_number_continue_out, ">>>> `1` read"); + //-TEST_2----------------------------------------------------------// + // idea: Inside a number value after a key in an object. + // state: stack == [[1, 1], [0, 0], [0, 0], [0, 0]], parsing_number == 1 + // read: `1` + // expect: stack --> [[1, 1], [0, 0], [0, 0], [0, 0]] + // parsing_number --> 0 + let inside_number_continue = { ...INITIAL_IN }; + inside_number_continue.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + inside_number_continue.parsing_number = 1; + inside_number_continue.byte = Numbers.ONE; + let inside_number_continue_out = { ...INITIAL_OUT }; + inside_number_continue_out.next_stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + inside_number_continue_out.next_parsing_number = 1; + generatePassCase(inside_number_continue, inside_number_continue_out, ">>>> `1` read"); + + //-TEST_2----------------------------------------------------------// + // idea: Inside a number value after a key in an object. + // state: stack == [[1, 1], [0, 0], [0, 0], [0, 0]], parsing_number == 1 + // read: `1` + // expect: stack --> [[1, 1], [0, 0], [0, 0], [0, 0]] + // parsing_number --> 0 + let inside_number_exit = { ...INITIAL_IN }; + inside_number_exit.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + inside_number_exit.parsing_number = 1; + inside_number_exit.byte = WhiteSpace.SPACE; + let inside_number_exit_out = { ...INITIAL_OUT }; + inside_number_exit_out.next_stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + inside_number_exit_out.next_parsing_number = 0; + generatePassCase(inside_number_exit, inside_number_exit_out, ">>>> ` ` read"); + + //-TEST_3----------------------------------------------------------// + // idea: Inside a number value after a key in an object. + // state: stack == [[1, 1], [0, 0], [0, 0], [0, 0]], parsing_number == 1 + // read: `$` + // expect: stack --> [[1, 1], [0, 0], [0, 0], [0, 0]] + // parsing_number --> 0 + let inside_number_exit2 = { ...INITIAL_IN }; + inside_number_exit2.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + inside_number_exit2.parsing_number = 1; + inside_number_exit2.byte = 36; // Dollar sign `$` + let inside_number_exit2_out = { ...INITIAL_OUT }; + inside_number_exit2_out.next_stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + inside_number_exit2_out.next_parsing_number = 0; + generatePassCase(inside_number_exit2, inside_number_exit2_out, ">>>> `$` read"); + }); + + describe("StateUpdate :: Values :: String", () => { + //-TEST_4----------------------------------------------------------// + // idea: Inside a string key inside an object + // state: stack == [[1, 0], [0, 0], [0, 0], [0, 0]], parsing_string == 1 + // read: `,` + // expect: stack --> [[1, 0], [0, 0], [0, 0], [0, 0]] + // parsing_string --> 0 + let inside_number = { ...INITIAL_IN }; + inside_number.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + inside_number.parsing_string = 1; + inside_number.byte = Delimiters.COMMA; + let inside_number_out = { ...INITIAL_OUT }; + inside_number_out.next_stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + inside_number_out.next_parsing_string = 1; + generatePassCase(inside_number, inside_number_out, ">>>> `,` read"); + }); describe("StateUpdate :: Values :: Array", () => { // Internal array parsing -----------------------------------------// //-TEST_10----------------------------------------------------------// - // init: stack == [[1, 0], [2, 0], [0, 0], [0, 0]] + // init: stack == [[1, 0], [2, 0], [0, 0], [0, 0]] // read: `,` // expext: stack --> [[1, 0], [2, 1], [0, 0], [0, 0]] let in_arr = { ...INITIAL_IN }; diff --git a/circuits/test/search.test.ts b/circuits/test/search.test.ts index 286137b..59692db 100644 --- a/circuits/test/search.test.ts +++ b/circuits/test/search.test.ts @@ -43,7 +43,10 @@ describe("search", () => { ); }); - it("wrong random_num input, correct key position: 2", async () => { + /// 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]; @@ -59,7 +62,7 @@ describe("search", () => { ); }); - it("data = inputs.json:data, key = inputs.json:key, r = hash(data+key)", async () => { + 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); @@ -89,7 +92,7 @@ describe("search", () => { console.log("#constraints:", await circuit.getConstraintCount()); }); - it("data = inputs.json:data, key = inputs.json:key, r = hash(data+key)", async () => { + it("data = witness.json:data, key = witness.json:key, r = hash(key+data)", async () => { await circuit.expectPass( { data: witness["data"], @@ -100,7 +103,7 @@ describe("search", () => { ); }); - it("data = inputs.json:data, key = inputs.json:key, r = hash(data+key), incorrect position", async () => { + it("data = witness.json:data, key = witness.json:key, r = hash(key+data), incorrect position", async () => { await circuit.expectFail( { data: witness["data"], @@ -124,17 +127,23 @@ describe("search", () => { console.log("#constraints:", await circuit.getConstraintCount()); }); - it("data = inputs.json:data, key = inputs.json:key", async () => { + it("data = witness.json:data, key = witness.json:key", async () => { await circuit.expectPass( { data: witness["data"], key: witness["key"] }, { position: 6 }, ); }); - it("data = inputs.json:data, key = wrong key", async () => { + 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/utils/array.circom b/circuits/utils/array.circom index dd80c1e..dc65b28 100644 --- a/circuits/utils/array.circom +++ b/circuits/utils/array.circom @@ -1,14 +1,23 @@ pragma circom 2.1.9; -/// @title Slice -/// @notice Extract a fixed portion of an array -/// @dev Unlike SelectSubArray, Slice uses compile-time known indices and doesn't pad the output -/// @dev Slice is more efficient for fixed ranges, while SelectSubArray offers runtime flexibility -/// @param n The length of the input array -/// @param start The starting index of the slice (inclusive) -/// @param end The ending index of the slice (exclusive) -/// @input in The input array of length n -/// @output out The sliced array of length (end - start) +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); @@ -20,4 +29,175 @@ template Slice(n, start, end) { 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 index 166f4b6..3d0ef40 100644 --- a/circuits/utils/hash.circom +++ b/circuits/utils/hash.circom @@ -3,14 +3,20 @@ pragma circom 2.1.9; include "circomlib/circuits/poseidon.circom"; include "./array.circom"; -/// @title PoseidonModular -/// @notice Circuit to calculate Poseidon hash of an arbitrary number of inputs -/// @notice Splits input into chunks of 16 elements (or less for the last chunk) and hashes them separately -/// @notice Then combines the chunk hashes using a binary tree structure -/// @notice from: https://github.com/zkemail/zk-email-verify/blob/main/packages/circuits/utils/hash.circom#L49 -/// @param numElements Number of elements in the input array -/// @input in: Array of numElements to be hashed -/// @output out: Poseidon hash of the input array +/// 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; 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/test_extract_sambhav/input.json b/inputs/test_extract_sambhav/input.json new file mode 100644 index 0000000..9e6cbba --- /dev/null +++ b/inputs/test_extract_sambhav/input.json @@ -0,0 +1,115 @@ +{ + "key": [ + 107, + 101, + 121, + 49 + ], + "data": [ + 123, + 10, + 32, + 32, + 32, + 32, + 34, + 101, + 120, + 116, + 114, + 97, + 99, + 116, + 34, + 58, + 32, + 123, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 102, + 105, + 108, + 101, + 34, + 58, + 32, + 34, + 101, + 120, + 116, + 114, + 97, + 99, + 116, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 116, + 101, + 109, + 112, + 108, + 97, + 116, + 101, + 34, + 58, + 32, + 34, + 69, + 120, + 116, + 114, + 97, + 99, + 116, + 34, + 44, + 10, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 32, + 34, + 112, + 97, + 114, + 97, + 109, + 115, + 34, + 58, + 32, + 34, + 34, + 10, + 32, + 32, + 32, + 32, + 125, + 10, + 125 + ] +} \ No newline at end of file diff --git a/json_examples/reddit_response.json b/json_examples/reddit_response.json new file mode 100644 index 0000000..163e6b4 --- /dev/null +++ b/json_examples/reddit_response.json @@ -0,0 +1,14 @@ +{ + "data": { + "redditorInfoByName": { + "id": "t2_bepsb", + "karma": { + "fromAwardsGiven": 0, + "fromAwardsReceived": 470, + "fromComments": 9583, + "fromPosts": 13228, + "total": 23281 + } + } + } +} \ No newline at end of file diff --git a/json_examples/sambhav_example.json b/json_examples/sambhav_example.json new file mode 100644 index 0000000..ab9c57e --- /dev/null +++ b/json_examples/sambhav_example.json @@ -0,0 +1,7 @@ +{ + "extract": { + "file": "extract", + "template": "Extract", + "params": "" + } +} \ No newline at end of file diff --git a/json_examples/test/value_array.json b/json_examples/test/value_array.json index e0dc999..bd8d014 100644 --- a/json_examples/test/value_array.json +++ b/json_examples/test/value_array.json @@ -1 +1 @@ -{ "k" : [ 420 , 69 , 4200 , 600 ], "b": [ "ab" , "ba", "ccc", "d" ] } \ No newline at end of file +{ "k" : [ 420 , 69 , 4200 , 600 ], "b": [ "ab" , "ba", "ccc", "d" ] } diff --git a/json_examples/test/value_array_object.json b/json_examples/test/value_array_object.json index 3a0a544..a4f9ddc 100644 --- a/json_examples/test/value_array_object.json +++ b/json_examples/test/value_array_object.json @@ -1 +1 @@ -{"a":[{"b":[1,4]},{"c":"b"}]} \ No newline at end of file +{"a":[{"b":[1,4]},{"c":"b"}]} diff --git a/json_examples/test/value_object.json b/json_examples/test/value_object.json index fdb08e1..335d6cf 100644 --- a/json_examples/test/value_object.json +++ b/json_examples/test/value_object.json @@ -1 +1 @@ -{ "a": { "d" : "e", "e": "c" }, "e": { "f": "a", "e": "2" }, "g": { "h": { "a": "c" }} } \ No newline at end of file +{ "a": { "d" : "e", "e": "c" }, "e": { "f": "a", "e": "2" }, "g": { "h": { "a": "c" }} } diff --git a/json_examples/test_depth.json b/json_examples/test_depth.json new file mode 100644 index 0000000..ad2f816 --- /dev/null +++ b/json_examples/test_depth.json @@ -0,0 +1,6 @@ +{ + "key1": "abc", + "key2": { + "key3": "def" + } +} \ No newline at end of file diff --git a/json_examples/test_two_key.json b/json_examples/test_two_key.json new file mode 100644 index 0000000..437a6c5 --- /dev/null +++ b/json_examples/test_two_key.json @@ -0,0 +1,4 @@ +{ + "key1": "abc", + "key2": "def" +} \ No newline at end of file