diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ada78d..3943f72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,11 +25,11 @@ jobs: - name: Download and install Circom run: | - CIRCOM_VERSION=2.1.9 + 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 + circom --version - name: Run tests - run: npx mocha + run: npm run par-test diff --git a/circuits/http/parser_request/language.circom b/circuits/http/parser_request/language.circom new file mode 100644 index 0000000..a001d86 --- /dev/null +++ b/circuits/http/parser_request/language.circom @@ -0,0 +1,49 @@ +pragma circom 2.1.9; + +// All the possible request methods: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods + +template Syntax() { + //-Delimeters---------------------------------------------------------------------------------// + // - ASCII char `:` + signal output COLON <== 58; + // - ASCII char `;` + signal output SEMICOLON <== 59; + // - ASCII char `,` + signal output COMMA <== 44; + // - ASCII char `"` + signal output QUOTE <== 34; + //-White_space--------------------------------------------------------------------------------// + // - ASCII pair: `\r\n` + signal output CLRF <== [13, 10]; // https://www.rfc-editor.org/rfc/rfc2616#section-2.2 + // https://www.rfc-editor.org/rfc/rfc7230#section-3.5 + // - ASCII char: ` ` + signal output SPACE <== 32; + //-Escape-------------------------------------------------------------------------------------// + // - ASCII char: `\` + signal output ESCAPE <== 92; +} + +template RequestMethod() { + signal output GET[3] <== [71, 69, 84]; + // signal output HEAD[4] <== [72, 69, 65, 68]; + signal output POST[4] <== [80, 79, 83, 84]; + // signal output PUT <== 3; + // signal output DELETE <== 4; + // signal output CONNECT <== 5; + // signal output OPTIONS <== 6; + // signal output TRACE <== 7; + // signal output PATCH <== 8; +} + +// NOTE: Starting at 1 to avoid a false positive with a 0. +template RequestMethodTag() { + signal output GET <== 1; + // signal output HEAD <== 2; + signal output POST <== 3; + // signal output PUT <== 4; + // signal output DELETE <== 5; + // signal output CONNECT <== 6; + // signal output OPTIONS <== 7; + // signal output TRACE <== 8; + // signal output PATCH <== 9; +} \ No newline at end of file diff --git a/circuits/http/parser_request/machine.circom b/circuits/http/parser_request/machine.circom new file mode 100644 index 0000000..ee0cff1 --- /dev/null +++ b/circuits/http/parser_request/machine.circom @@ -0,0 +1,28 @@ +pragma circom 2.1.9; + +include "language.circom"; +include "../utils/array.circom"; + +template ParseMethod() { + signal input bytes[7]; + signal output MethodTag; + + component RequestMethod = RequestMethod(); + component RequestMethodTag = RequestMethodTag(); + + component IsGet = IsEqualArray(3); + for(var byte_idx = 0; byte_idx < 3; byte_idx++) { + IsGet.in[0][byte_idx] <== bytes[byte_idx]; + IsGet.in[1][byte_idx] <== RequestMethod.GET[byte_idx]; + } + signal TagGet <== IsGet.out * RequestMethodTag.GET; + + component IsPost = IsEqualArray(4); + for(var byte_idx = 0; byte_idx < 4; byte_idx++) { + IsPost.in[0][byte_idx] <== bytes[byte_idx]; + IsPost.in[1][byte_idx] <== RequestMethod.POST[byte_idx]; + } + signal TagPost <== IsPost.out * RequestMethodTag.POST; + + MethodTag <== TagGet + TagPost; +} \ No newline at end of file diff --git a/circuits/http/parser_request/parser.circom b/circuits/http/parser_request/parser.circom new file mode 100644 index 0000000..9fd9524 --- /dev/null +++ b/circuits/http/parser_request/parser.circom @@ -0,0 +1,24 @@ +pragma circom 2.1.9; + +include "../utils/bytes.circom"; +include "machine.circom"; + + +template Parser(DATA_BYTES) { + signal input data[DATA_BYTES]; + + signal output Method; + + //--------------------------------------------------------------------------------------------// + //-CONSTRAINTS--------------------------------------------------------------------------------// + //--------------------------------------------------------------------------------------------// + component dataASCII = ASCII(DATA_BYTES); + dataASCII.in <== data; + //--------------------------------------------------------------------------------------------// + + component ParseMethod = ParseMethod(); + for(var byte_idx = 0; byte_idx < 7; byte_idx++) { + ParseMethod.bytes[byte_idx] <== data[byte_idx]; + } + log("MethodTag: ", ParseMethod.MethodTag); +} \ No newline at end of file diff --git a/circuits/json/interpreter.circom b/circuits/json/interpreter.circom new file mode 100644 index 0000000..501877a --- /dev/null +++ b/circuits/json/interpreter.circom @@ -0,0 +1,322 @@ +pragma circom 2.1.9; + +include "./parser/parser.circom"; +include "./parser/language.circom"; +include "../utils/search.circom"; +include "../utils/array.circom"; +include "circomlib/circuits/mux1.circom"; +include "circomlib/circuits/gates.circom"; +include "@zk-email/circuits/utils/functions.circom"; +include "@zk-email/circuits/utils/array.circom"; + +// TODOs: +// - remove use of random_signal in key match from 100 +// + +/// Checks if current byte is inside a JSON key or not +/// +/// # Arguments +/// - `n`: maximum stack depth +/// +/// # Inputs +/// - `stack`: current stack state +/// - `parsing_string`: whether current byte is inside a string or not +/// - `parsing_number`: wheter current byte is inside a number or not +/// +/// # Output +/// - `out`: Returns `1` if current byte is inside a key +template InsideKey(n) { + signal input stack[n][2]; + signal input parsing_string; + signal input parsing_number; + + signal output out; + + component topOfStack = GetTopOfStack(n); + topOfStack.stack <== stack; + signal currentVal[2] <== topOfStack.value; + + signal parsingStringAndNotNumber <== parsing_string * (1 - parsing_number); + signal ifParsingKey <== currentVal[0] * (1-currentVal[1]); + + out <== ifParsingKey * parsingStringAndNotNumber; +} + +/// Checks if current byte is inside a JSON value or not +/// +/// # Arguments +/// - `n`: maximum stack depth +/// +/// # Inputs +/// - `stack`: current stack state +/// - `parsing_string`: whether current byte is inside a string or not +/// - `parsing_number`: wheter current byte is inside a number or not +/// +/// # Output +/// - `out`: Returns `1` if current byte is inside a value +template InsideValue(n) { + signal input stack[n][2]; + signal input parsing_string; + signal input parsing_number; + + signal output out; + + component topOfStack = GetTopOfStack(n); + topOfStack.stack <== stack; + signal currentVal[2] <== topOfStack.value; + + signal parsingStringXORNumber <== XOR()(parsing_string, parsing_number); + + signal ifParsingValue <== currentVal[0] * currentVal[1]; + + out <== ifParsingValue * parsingStringXORNumber; +} + +/// Checks if current byte is inside a JSON value at specified depth +/// +/// # Arguments +/// - `n`: maximum stack depth +/// - `depth`: stack height of parsed byte +/// +/// # Inputs +/// - `stack`: current stack state +/// - `parsing_string`: whether current byte is inside a string or not +/// - `parsing_number`: wheter current byte is inside a number or not +/// +/// # Output +/// - `out`: Returns `1` if current byte is inside a value +template InsideValueAtDepth(n, depth) { + signal input stack[n][2]; + signal input parsing_string; + signal input parsing_number; + + signal output out; + + signal ifParsingValue <== stack[depth][0] * stack[depth][1]; + signal parsingStringXORNumber <== XOR()(parsing_string, parsing_number); + + out <== ifParsingValue * parsingStringXORNumber; +} + +/// Checks if current byte is inside an array at specified index +/// +/// # Arguments +/// - `n`: maximum stack depth +/// - `index`: index of array element +/// +/// # Inputs +/// - `stack`: current stack state +/// - `parsing_string`: whether current byte is inside a string or not +/// - `parsing_number`: wheter current byte is inside a number or not +/// +/// # Output +/// - `out`: Returns `1` if current byte represents an array element at `index` +template InsideArrayIndex(n, index) { + signal input stack[n][2]; + signal input parsing_string; + signal input parsing_number; + + signal output out; + + component topOfStack = GetTopOfStack(n); + topOfStack.stack <== stack; + signal currentVal[2] <== topOfStack.value; + + signal insideArray <== IsEqual()([currentVal[0], 2]); + signal insideIndex <== IsEqual()([currentVal[1], index]); + signal insideArrayIndex <== insideArray * insideIndex; + signal parsingStringXORNumber <== XOR()(parsing_string, parsing_number); + + out <== insideArrayIndex * parsingStringXORNumber; +} + +/// Checks if current byte is inside an array index at specified depth +/// +/// # Arguments +/// - `n`: maximum stack depth +/// - `index`: array element index +/// - `depth`: stack height of parsed byte +/// +/// # Inputs +/// - `stack`: current stack state +/// - `parsing_string`: whether current byte is inside a string or not +/// - `parsing_number`: wheter current byte is inside a number or not +/// +/// # Output +/// - `out`: Returns `1` if current byte is inside an array index +template InsideArrayIndexAtDepth(n, index, depth) { + signal input stack[n][2]; + signal input parsing_string; + signal input parsing_number; + + signal output out; + + signal insideArray <== IsEqual()([stack[depth][0], 2]); + signal insideIndex <== IsEqual()([stack[depth][1], index]); + signal insideArrayIndex <== insideArray * insideIndex; + out <== insideArrayIndex * (parsing_string + parsing_number); +} + +/// Returns whether next key-value pair starts. +/// +/// # Arguments +/// - `n`: maximum stack depth +/// +/// # Inputs +/// - `stack`: current stack state +/// - `curr_byte`: current parsed byte +/// +/// # Output +/// - `out`: Returns `1` for next key-value pair. +template NextKVPair(n) { + signal input stack[n][2]; + signal input currByte; + signal output out; + + component topOfStack = GetTopOfStack(n); + topOfStack.stack <== stack; + signal currentVal[2] <== topOfStack.value; + + signal isNextPair <== IsEqualArray(2)([currentVal, [1, 0]]); + + component syntax = Syntax(); + signal isComma <== IsEqual()([currByte, syntax.COMMA]); // `, -> 44` + + out <== isNextPair*isComma ; +} + +/// Returns whether next key-value pair starts. +/// Applies following checks: +/// - get top of stack value and check whether parsing key: `[1, 0]` +/// - current byte = `,` +/// - current stack height is less than the key to be matched (it means that new key has started) +/// +/// # Arguments +/// - `n`: maximum stack depth +/// - `depth`: depth of matched key-value pair +/// +/// # Inputs +/// - `stack`: current stack state +/// - `curr_byte`: current parsed byte +/// +/// # Output +/// - `out`: Returns `1` for next key-value pair at specified depth. +template NextKVPairAtDepth(n, depth) { + signal input stack[n][2]; + signal input currByte; + signal output out; + + var logMaxDepth = log2Ceil(n); + + component topOfStack = GetTopOfStack(n); + topOfStack.stack <== stack; + signal currentVal[2] <== topOfStack.value; + signal pointer <== topOfStack.pointer; + + signal isNextPair <== IsEqualArray(2)([currentVal, [1, 0]]); + + // `, -> 44` + component syntax = Syntax(); + signal isComma <== IsEqual()([currByte, syntax.COMMA]); + // pointer <= depth + signal atLessDepth <== LessEqThan(logMaxDepth)([pointer, depth]); + // current depth is less than key depth + signal isCommaAtDepthLessThanCurrent <== isComma * atLessDepth; + + out <== isNextPair * isCommaAtDepthLessThanCurrent; +} + +/// Matches a JSON key at an `index` using Substring Matching +/// +/// # Arguments +/// - `dataLen`: parsed data length +/// - `keyLen`: key length +/// +/// # Inputs +/// - `data`: data bytes +/// - `key`: key bytes +/// - `r`: random number for substring matching. **Need to be chosen carefully.** +/// - `index`: data index to match from +/// - `parsing_key`: if current byte is inside a key +/// +/// # Output +/// - `out`: Returns `1` if `key` matches `data` at `index` +template KeyMatch(dataLen, keyLen) { + signal input data[dataLen]; + signal input key[keyLen]; + signal input r; + signal input index; + signal input parsing_key; + + component syntax = Syntax(); + + signal end_of_key <== IndexSelector(dataLen)(data, index + keyLen); + signal is_end_of_key_equal_to_quote <== IsEqual()([end_of_key, syntax.QUOTE]); + + signal start_of_key <== IndexSelector(dataLen)(data, index - 1); + signal is_start_of_key_equal_to_quote <== IsEqual()([start_of_key, syntax.QUOTE]); + + signal substring_match <== SubstringMatchWithIndex(dataLen, keyLen)(data, key, r, index); + + signal is_key_between_quotes <== is_start_of_key_equal_to_quote * is_end_of_key_equal_to_quote; + signal is_parsing_correct_key <== is_key_between_quotes * parsing_key; + + signal output out <== substring_match * is_parsing_correct_key; +} + +/// Matches a JSON key at an `index` using Substring Matching at specified depth +/// +/// # Arguments +/// - `dataLen`: parsed data length +/// - `n`: maximum stack height +/// - `keyLen`: key length +/// - `depth`: depth of key to be matched +/// +/// # Inputs +/// - `data`: data bytes +/// - `key`: key bytes +/// - `r`: random number for substring matching. **Need to be chosen carefully.** +/// - `index`: data index to match from +/// - `parsing_key`: if current byte is inside a key +/// - `stack`: parser stack output +/// +/// # Output +/// - `out`: Returns `1` if `key` matches `data` at `index` +template KeyMatchAtDepth(dataLen, n, keyLen, depth) { + signal input data[dataLen]; + signal input key[keyLen]; + signal input r; + signal input index; + signal input parsing_key; + signal input stack[n][2]; + + component topOfStack = GetTopOfStack(n); + topOfStack.stack <== stack; + signal pointer <== topOfStack.pointer; + + component syntax = Syntax(); + + // end of key equals `"` + signal end_of_key <== IndexSelector(dataLen)(data, index + keyLen); + signal is_end_of_key_equal_to_quote <== IsEqual()([end_of_key, syntax.QUOTE]); + + // start of key equals `"` + signal start_of_key <== IndexSelector(dataLen)(data, index - 1); + signal is_start_of_key_equal_to_quote <== IsEqual()([start_of_key, syntax.QUOTE]); + + // key matches + signal substring_match <== SubstringMatchWithIndex(dataLen, keyLen)(data, key, r, index); + + // key should be a string + signal is_key_between_quotes <== is_start_of_key_equal_to_quote * is_end_of_key_equal_to_quote; + + // is the index given correct? + signal is_parsing_correct_key <== is_key_between_quotes * parsing_key; + // is the key given by index at correct depth? + signal is_key_at_depth <== IsEqual()([pointer-1, depth]); + + signal is_parsing_correct_key_at_depth <== is_parsing_correct_key * is_key_at_depth; + // log("key match", index, end_of_key, is_end_of_key_equal_to_quote, substring_match); + + signal output out <== substring_match * is_parsing_correct_key_at_depth; +} \ No newline at end of file diff --git a/circuits/json/parser/language.circom b/circuits/json/parser/language.circom new file mode 100644 index 0000000..8063503 --- /dev/null +++ b/circuits/json/parser/language.circom @@ -0,0 +1,41 @@ +pragma circom 2.1.9; + +template Syntax() { + //-Delimeters---------------------------------------------------------------------------------// + // - ASCII char: `{` + signal output START_BRACE <== 123; + // - ASCII char: `}` + signal output END_BRACE <== 125; + // - ASCII char `[` + signal output START_BRACKET <== 91; + // - ASCII char `]` + signal output END_BRACKET <== 93; + // - ASCII char `"` + signal output QUOTE <== 34; + // - ASCII char `:` + signal output COLON <== 58; + // - ASCII char `,` + signal output COMMA <== 44; + //-White_space--------------------------------------------------------------------------------// + // - ASCII char: `\n` + signal output NEWLINE <== 10; + // - ASCII char: ` ` + signal output SPACE <== 32; + //-Escape-------------------------------------------------------------------------------------// + // - ASCII char: `\` + signal output ESCAPE <== 92; + //-Number_Remapping---------------------------------------------------------------------------// + signal output NUMBER <== 256; // past a u8 -- reserved for ANY numerical ASCII (48 - 57) +} + +template Command() { + // STATE = [read_write_value, parsing_string, parsing_number] + 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 ]; // 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) +} \ No newline at end of file diff --git a/circuits/json/parser/machine.circom b/circuits/json/parser/machine.circom new file mode 100644 index 0000000..78a38c6 --- /dev/null +++ b/circuits/json/parser/machine.circom @@ -0,0 +1,334 @@ +/* +# `machine` +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/array.circom"; +include "../../utils/bytes.circom"; +include "../../utils/operators.circom"; +include "language.circom"; + +/* +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; // TODO: Does this need to be constrained within here? + + 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(); + + //--------------------------------------------------------------------------------------------// + // 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.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 <== [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 <== 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 <== 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 readDelimeter; + signal input readNumber; + signal input parsing_string; + signal input parsing_number; + signal output out[3]; + + + // `read_write_value`can change: IF NOT `parsing_string` + out[0] <== (1 - parsing_string); + + // `parsing_string` can change: + out[1] <== 1 - 2 * parsing_string; + + + //--------------------------------------------------------------------------------------------// + // `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]; + signal output pointer; + + component isUnallocated[n]; + component atTop = SwitchArray(n,2); + var selector = 0; + for(var i = 0; i < n; i++) { + isUnallocated[i] = IsEqualArray(2); + isUnallocated[i].in[0] <== [0,0]; + isUnallocated[i].in[1] <== stack[i]; + selector += (1 - isUnallocated[i].out); + atTop.branches[i] <== i + 1; + atTop.vals[i] <== stack[i]; + } + atTop.case <== selector; + value <== atTop.out; + pointer <== selector; +} + +// 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; + signal pointer <== topOfStack.pointer; + signal current_value[2] <== topOfStack.value; + // * check if we are currently in a value of an object * + component inObjectValue = IsEqualArray(2); + inObjectValue.in[0] <== current_value; + inObjectValue.in[1] <== [1,1]; + // * check if value indicates currently in an array * + component inArray = IsEqual(); + inArray.in[0] <== current_value[0]; + inArray.in[1] <== 2; + //--------------------------------------------------------------------------------------------// + + //--------------------------------------------------------------------------------------------// + // * composite signals * + 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 + readStartBracket, 1]; + component isPop = IsEqual(); + 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 - readComma - i; // Note, pointer points to unallocated region! + } + //--------------------------------------------------------------------------------------------// + + //--------------------------------------------------------------------------------------------// + // * 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] * (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]); + } + //--------------------------------------------------------------------------------------------// + + //--------------------------------------------------------------------------------------------// + // * 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/json/parser/parser.circom b/circuits/json/parser/parser.circom new file mode 100644 index 0000000..c3cc7e8 --- /dev/null +++ b/circuits/json/parser/parser.circom @@ -0,0 +1,53 @@ +pragma circom 2.1.9; + +include "../../utils/bytes.circom"; +include "machine.circom"; + +template Parser(DATA_BYTES, MAX_STACK_HEIGHT) { + signal input data[DATA_BYTES]; + + // TODO: Add assertions on the inputs here! + + //--------------------------------------------------------------------------------------------// + //-CONSTRAINTS--------------------------------------------------------------------------------// + //--------------------------------------------------------------------------------------------// + component dataASCII = ASCII(DATA_BYTES); + dataASCII.in <== data; + //--------------------------------------------------------------------------------------------// + // Initialze the parser + component State[DATA_BYTES]; + State[0] = StateUpdate(MAX_STACK_HEIGHT); + State[0].byte <== data[0]; + for(var i = 0; i < MAX_STACK_HEIGHT; i++) { + State[0].stack[i] <== [0,0]; + } + State[0].parsing_string <== 0; + State[0].parsing_number <== 0; + + for(var data_idx = 1; data_idx < DATA_BYTES; data_idx++) { + State[data_idx] = StateUpdate(MAX_STACK_HEIGHT); + State[data_idx].byte <== data[data_idx]; + State[data_idx].stack <== State[data_idx - 1].next_stack; + State[data_idx].parsing_string <== State[data_idx - 1].next_parsing_string; + State[data_idx].parsing_number <== State[data_idx - 1].next_parsing_number; + + // // Debugging + // for(var i = 0; i `${key} = ${stringifyValue(value)}`) .join(", "); +} + +export function readInputFile(filename: string, key: any[]): [number[], number[][], number[]] { + const valueStringPath = join(__dirname, "..", "..", "..", "examples", "json", "test", filename); + + let input: number[] = []; + let output: number[] = []; + + let data = readFileSync(valueStringPath, 'utf-8'); + + let keyUnicode: number[][] = []; + for (let i = 0; i < key.length; i++) { + keyUnicode[i] = []; + let key_string = key[i].toString(); + for (let j = 0; j < key_string.length; j++) { + keyUnicode[i].push(key_string.charCodeAt(j)); + } + } + + const byteArray = []; + for (let i = 0; i < data.length; i++) { + byteArray.push(data.charCodeAt(i)); + } + input = byteArray; + + let jsonFile = JSON.parse(data); + let value: string = key.reduce((acc, key) => acc && acc[key], jsonFile).toString(); + for (let i = 0; i < value.length; i++) { + output.push(value.charCodeAt(i)); + } + + return [input, keyUnicode, output]; } \ No newline at end of file diff --git a/circuits/test/json/extractor/extractor.test.ts b/circuits/test/json/extractor/extractor.test.ts new file mode 100644 index 0000000..2a693ec --- /dev/null +++ b/circuits/test/json/extractor/extractor.test.ts @@ -0,0 +1,191 @@ +import { circomkit, WitnessTester, readInputFile } from "../../common"; +import { join } from "path"; +import { spawn } from "child_process"; + + +function executeCodegen(inputFilename: string, outputFilename: string) { + return new Promise((resolve, reject) => { + const inputPath = join(__dirname, "..", "..", "..", "..", "examples", "json", "test", "codegen", inputFilename); + + const codegen = spawn("cargo", ["run", "--bin", "codegen", "--", "--json-file", inputPath, "--output-filename", outputFilename]); + + codegen.stdout.on('data', (data) => { + console.log(`stdout: ${data}`); + }); + + codegen.stderr.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + codegen.on('close', (code) => { + if (code === 0) { + resolve(`child process exited with code ${code}`); // Resolve the promise if the process exits successfully + } else { + reject(new Error(`Process exited with code ${code}`)); // Reject if there's an error + } + }); + }); +} + +describe("ExtractValue", async () => { + let circuit: WitnessTester<["data", "key1"], ["value"]>; + + it("value_string: {\"a\": \"b\"}", async () => { + let filename = "value_string"; + await executeCodegen(`${filename}.json`, filename); + let [input, keyUnicode, output] = readInputFile(`${filename}.json`, ["k"]); + + circuit = await circomkit.WitnessTester(`Extract`, { + file: `circuits/main/${filename}`, + template: "ExtractStringValue", + params: [input.length, 1, 1, 0, 1], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass({ + data: input, key1: keyUnicode, + }, { + value: output, + }); + }); + + it("two_keys: {\"key1\": \"abc\", \"key2\": \"def\" }", async () => { + let filename = "two_keys" + await executeCodegen(`${filename}.json`, filename); + let [input, keyUnicode, output] = readInputFile(`${filename}.json`, ["key2"]); + + circuit = await circomkit.WitnessTester(`Extract`, { + file: `circuits/main/${filename}`, + template: "ExtractStringValue", + params: [input.length, 1, 4, 0, 3], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass({ data: input, key1: keyUnicode }, { value: output }); + }); + + it("value_number: {\"k\": 69 }", async () => { + let filename = "value_number"; + await executeCodegen(`${filename}.json`, filename); + let [input, keyUnicode, output] = readInputFile(`${filename}.json`, ["k"]); + + circuit = await circomkit.WitnessTester(`Extract`, { + file: `circuits/main/${filename}`, + template: "ExtractNumValue", + params: [input.length, 1, 1, 0, 2], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + let num = parseInt(output.map(num => String.fromCharCode(num)).join(''), 10); + + await circuit.expectPass({ data: input, key1: keyUnicode }, { value: num }); + }); + + it("value_array_string: { \"k\" : [ 420 , 69 , 4200 , 600 ], \"b\": [ \"ab\" , \"ba\", \"ccc\", \"d\" ] }", async () => { + let filename = "value_array_string"; + await executeCodegen(`${filename}.json`, filename); + + for (let i = 0; i < 4; i++) { + let [input, keyUnicode, output] = readInputFile("value_array.json", ["b", i]); + + circuit = await circomkit.WitnessTester(`Extract`, { + file: `circuits/main/${filename}`, + template: "ExtractStringValue", + params: [input.length, 2, 1, 0, i, 1, output.length], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass({ data: input, key1: keyUnicode[0] }, { value: output }); + } + }); + + it("value_array_number: { \"k\" : [ 420 , 69 , 4200 , 600 ], \"b\": [ \"ab\" , \"ba\", \"ccc\", \"d\" ] }", async () => { + let filename = "value_array_number"; + await executeCodegen(`${filename}.json`, filename); + + for (let i = 0; i < 4; i++) { + let [input, keyUnicode, output] = readInputFile("value_array.json", ["k", i]); + + circuit = await circomkit.WitnessTester(`Extract`, { + file: `circuits/main/${filename}`, + template: "ExtractNumValue", + params: [input.length, 2, 1, 0, i, 1, output.length], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + let num = parseInt(output.map(num => String.fromCharCode(num)).join(''), 10); + await circuit.expectPass({ data: input, key1: keyUnicode[0] }, { value: num }); + } + }); + + it("value_array_nested: { \"a\": [[1,0],[0,1,3]] }", async () => { + let filename = "value_array_nested"; + await executeCodegen(`${filename}.json`, filename); + let index_0 = 1; + let index_1 = 0; + let [input, keyUnicode, output] = readInputFile(`${filename}.json`, ["a", index_0, index_1]); + + circuit = await circomkit.WitnessTester(`Extract`, { + file: `circuits/main/${filename}`, + template: "ExtractNumValue", + params: [input.length, 3, 1, 0, index_0, 1, index_1, 2, 1], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + let num = parseInt(output.map(num => String.fromCharCode(num)).join(''), 10); + + // console.log("input", input, "key:", keyUnicode, "output:", output); + await circuit.expectPass({ data: input, key1: keyUnicode[0] }, { value: num }); + }); +}); + +describe("ExtractValueMultiDepth", () => { + let circuit: WitnessTester<["data", "key1", "key2"], ["value"]>; + + it("value_object: { \"a\": { \"d\" : \"e\", \"e\": \"c\" }, \"e\": { \"f\": \"a\", \"e\": \"2\" } }", async () => { + let filename = "value_object"; + + await executeCodegen(`${filename}.json`, filename); + + let [input, keyUnicode, output] = readInputFile(`${filename}.json`, ["e", "e"]); + + circuit = await circomkit.WitnessTester(`Extract`, { + file: `circuits/main/${filename}`, + template: "ExtractStringValue", + params: [input.length, 3, 1, 0, 1, 1, 1], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass({ data: input, key1: keyUnicode[0], key2: keyUnicode[1] }, { value: output }); + + let [input1, keyUnicode1, output1] = readInputFile("value_object.json", ["e", "f"]); + await circuit.expectPass({ data: input1, key1: keyUnicode1[0], key2: keyUnicode1[1] }, { value: output1 }); + }); + + +}); + +describe("ExtractValueArrayObject", () => { + let circuit: WitnessTester<["data", "key1", "key3"], ["value"]>; + + it("value_array_object: {\"a\":[{\"b\":[1,4]},{\"c\":\"b\"}]}", async () => { + let filename = "value_array_object"; + + await executeCodegen(`${filename}.json`, filename); + + let index_0 = 0; + let index_1 = 0; + let [input, keyUnicode, output] = readInputFile(`${filename}.json`, ["a", index_0, "b", index_1]); + + circuit = await circomkit.WitnessTester(`Extract`, { + file: `circuits/main/${filename}`, + template: "ExtractNumValue", + params: [input.length, 4, 1, 0, index_0, 1, 1, 2, index_1, 3, 1], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + let num = parseInt(output.map(num => String.fromCharCode(num)).join(''), 10); + + await circuit.expectPass({ data: input, key1: keyUnicode[0], key3: keyUnicode[2] }, { value: num }); + }); +}); \ No newline at end of file diff --git a/circuits/test/json/extractor/interpreter.test.ts b/circuits/test/json/extractor/interpreter.test.ts new file mode 100644 index 0000000..9ee8953 --- /dev/null +++ b/circuits/test/json/extractor/interpreter.test.ts @@ -0,0 +1,357 @@ +import { circomkit, WitnessTester, generateDescription, readInputFile } from "../../common"; +import { PoseidonModular } from "../../common/poseidon"; + +describe("Interpreter", async () => { + describe("InsideKey", async () => { + let circuit: WitnessTester<["stack", "parsing_string", "parsing_number"], ["out"]>; + + before(async () => { + circuit = await circomkit.WitnessTester(`InsideKey`, { + file: "circuits/json/interpreter", + template: "InsideKey", + params: [4], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + function generatePassCase(input: any, expected: any, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description} ${desc}`, async () => { + await circuit.expectPass(input, expected); + }); + } + + let input1 = { stack: [[1, 0], [2, 0], [3, 1], [1, 0]], parsing_string: 1, parsing_number: 0 }; + let output = { out: 1 }; + generatePassCase(input1, output, ""); + + let input2 = { stack: [[1, 0], [2, 0], [1, 0], [0, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input2, output, ""); + + let input3 = { stack: [[1, 0], [0, 0], [0, 0], [0, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input3, output, ""); + + // fail cases + + let input4 = { stack: [[1, 0], [2, 0], [3, 1], [1, 1]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input4, { out: 0 }, "invalid stack"); + + let input5 = { stack: [[1, 0], [2, 0], [3, 1], [1, 0]], parsing_string: 1, parsing_number: 1 }; + generatePassCase(input5, { out: 0 }, "parsing number as a key"); + }); + + describe("InsideValue", async () => { + let circuit: WitnessTester<["stack", "parsing_string", "parsing_number"], ["out"]>; + + before(async () => { + circuit = await circomkit.WitnessTester(`InsideValue`, { + file: "circuits/json/interpreter", + template: "InsideValue", + params: [4], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + function generatePassCase(input: any, expected: any, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description} ${desc}`, async () => { + await circuit.expectPass(input, expected); + }); + } + + let input1 = { stack: [[1, 0], [2, 0], [3, 1], [1, 1]], parsing_string: 1, parsing_number: 0 }; + let output = { out: 1 }; + generatePassCase(input1, output, ""); + + let input2 = { stack: [[1, 0], [2, 0], [1, 1], [0, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input2, output, ""); + + let input3 = { stack: [[1, 1], [0, 0], [0, 0], [0, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input3, output, ""); + + // fail cases + + let input4 = { stack: [[1, 0], [2, 0], [3, 1], [1, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input4, { out: 0 }, "invalid stack"); + + let input5 = { stack: [[1, 0], [2, 0], [3, 1], [1, 1]], parsing_string: 1, parsing_number: 1 }; + generatePassCase(input5, { out: 0 }, "parsing number and key both"); + }); + + describe("InsideValueAtDepth", async () => { + let circuit: WitnessTester<["stack", "parsing_string", "parsing_number"], ["out"]>; + + function generatePassCase(input: any, expected: any, depth: number, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description} ${desc}`, async () => { + circuit = await circomkit.WitnessTester(`InsideValueAtDepth`, { + file: "circuits/json/interpreter", + template: "InsideValueAtDepth", + params: [4, depth], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass(input, expected); + }); + } + + let input1 = { stack: [[1, 0], [2, 0], [3, 1], [1, 1]], parsing_string: 1, parsing_number: 0 }; + let output = { out: 1 }; + generatePassCase(input1, output, 3, ""); + + let input2 = { stack: [[1, 0], [2, 0], [1, 1], [1, 1]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input2, output, 2, ""); + + let input3 = { stack: [[1, 1], [0, 0], [0, 0], [1, 1]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input3, output, 0, ""); + + // fail cases + + let input4 = { stack: [[1, 0], [2, 0], [3, 1], [1, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input4, { out: 0 }, 0, "invalid stack"); + + let input5 = { stack: [[1, 0], [2, 0], [3, 1], [1, 1]], parsing_string: 1, parsing_number: 1 }; + generatePassCase(input5, { out: 0 }, 3, "parsing number and key both"); + }); + + describe("InsideArrayIndex", async () => { + let circuit: WitnessTester<["stack", "parsing_string", "parsing_number"], ["out"]>; + + function generatePassCase(input: any, expected: any, index: number, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description} ${desc}`, async () => { + circuit = await circomkit.WitnessTester(`InsideArrayIndex`, { + file: "circuits/json/interpreter", + template: "InsideArrayIndex", + params: [4, index], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass(input, expected); + }); + } + + let input1 = { stack: [[1, 0], [2, 0], [3, 1], [2, 1]], parsing_string: 1, parsing_number: 0 }; + let output = { out: 1 }; + generatePassCase(input1, output, 1, ""); + + let input2 = { stack: [[1, 0], [2, 0], [2, 3], [0, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input2, output, 3, ""); + + let input3 = { stack: [[2, 10], [0, 0], [0, 0], [0, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input3, output, 10, ""); + + // fail cases + + let input4 = { stack: [[1, 0], [2, 0], [3, 1], [1, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input4, { out: 0 }, 4, "invalid stack"); + + let input5 = { stack: [[1, 0], [2, 0], [3, 1], [1, 1]], parsing_string: 1, parsing_number: 1 }; + generatePassCase(input5, { out: 0 }, 4, "parsing number and key both"); + + let input6 = { stack: [[1, 0], [2, 0], [3, 1], [2, 4]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input6, { out: 0 }, 3, "incorrect index"); + }); + + describe("InsideArrayIndexAtDepth", async () => { + let circuit: WitnessTester<["stack", "parsing_string", "parsing_number"], ["out"]>; + + function generatePassCase(input: any, expected: any, index: number, depth: number, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description} ${desc}`, async () => { + circuit = await circomkit.WitnessTester(`InsideArrayIndexAtDepth`, { + file: "circuits/json/interpreter", + template: "InsideArrayIndexAtDepth", + params: [4, index, depth], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass(input, expected); + }); + } + + let input1 = { stack: [[1, 0], [2, 0], [3, 1], [2, 1]], parsing_string: 1, parsing_number: 0 }; + let output = { out: 1 }; + generatePassCase(input1, output, 1, 3, ""); + + let input2 = { stack: [[1, 0], [2, 0], [2, 3], [2, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input2, output, 3, 2, ""); + + let input3 = { stack: [[2, 10], [0, 0], [1, 0], [0, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input3, output, 10, 0, ""); + + // fail cases + + let input4 = { stack: [[1, 0], [2, 0], [3, 1], [1, 0]], parsing_string: 1, parsing_number: 0 }; + generatePassCase(input4, { out: 0 }, 4, 2, "invalid stack depth"); + + let input5 = { stack: [[1, 0], [2, 0], [3, 1], [1, 1]], parsing_string: 1, parsing_number: 1 }; + generatePassCase(input5, { out: 0 }, 4, 1, "parsing number and key both"); + }); + + describe("NextKVPair", async () => { + let circuit: WitnessTester<["stack", "currByte"], ["out"]>; + + before(async () => { + circuit = await circomkit.WitnessTester(`NextKVPair`, { + file: "circuits/json/interpreter", + template: "NextKVPair", + params: [4], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + function generatePassCase(input: any, expected: any, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description} ${desc}`, async () => { + await circuit.expectPass(input, expected); + }); + } + + let input1 = { stack: [[1, 0], [2, 0], [3, 1], [1, 0]], currByte: 44 }; + let output = { out: 1 }; + generatePassCase(input1, output, ""); + + let input2 = { stack: [[1, 0], [2, 0], [1, 0], [0, 0]], currByte: 44 }; + generatePassCase(input2, output, ""); + + let input3 = { stack: [[1, 0], [0, 0], [0, 0], [0, 0]], currByte: 44 }; + generatePassCase(input3, output, ""); + + let input4 = { stack: [[1, 0], [2, 0], [3, 1], [1, 1]], currByte: 44 }; + generatePassCase(input4, { out: 0 }, "invalid stack"); + + let input5 = { stack: [[1, 0], [2, 0], [3, 1], [1, 0]], currByte: 34 }; + generatePassCase(input5, { out: 0 }, "incorrect currByte"); + }); + + describe("NextKVPairAtDepth", async () => { + let circuit: WitnessTester<["stack", "currByte"], ["out"]>; + + function generatePassCase(input: any, expected: any, depth: number, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description} ${desc}`, async () => { + circuit = await circomkit.WitnessTester(`NextKVPairAtDepth`, { + file: "circuits/json/interpreter", + template: "NextKVPairAtDepth", + params: [4, depth], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass(input, expected); + }); + } + + let input1 = { stack: [[1, 0], [2, 0], [3, 1], [1, 0]], currByte: 44 }; + // output = 0 represents correct execution + let output = { out: 0 }; + generatePassCase(input1, output, 3, ""); + + // key depth is 2, and even if new-kv pair starts at depth greater than 2, it returns 0. + let input2 = { stack: [[1, 0], [2, 0], [1, 1], [1, 0]], currByte: 44 }; + generatePassCase(input2, output, 2, ""); + + let input3 = { stack: [[1, 0], [1, 0], [0, 0], [0, 0]], currByte: 44 }; + generatePassCase(input3, { out: 1 }, 3, "stack height less than specified"); + + let input4 = { stack: [[1, 0], [2, 0], [1, 0], [0, 0]], currByte: 34 }; + generatePassCase(input4, output, 2, "incorrect currByte"); + }); + + describe("KeyMatch", async () => { + let circuit: WitnessTester<["data", "key", "r", "index", "parsing_key"], ["out"]>; + + function generatePassCase(input: any, expected: any, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description} ${desc}`, async () => { + circuit = await circomkit.WitnessTester(`KeyMatch`, { + file: "circuits/json/interpreter", + template: "KeyMatch", + params: [input.data.length, input.key.length], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass(input, expected); + }); + } + + let input = readInputFile("value_array_object.json", ["a"]); + const concatenatedInput = input[1][0].concat(input[0]); + const hashResult = PoseidonModular(concatenatedInput); + + let output = { out: 1 }; + let input1 = { data: input[0], key: input[1][0], r: hashResult, index: 2, parsing_key: 1 }; + generatePassCase(input1, output, ""); + + let input2 = { data: input[0], key: [99], r: hashResult, index: 20, parsing_key: 1 }; + generatePassCase(input2, output, ""); + + // fail cases + + let input3 = { data: input[0], key: input[1][0], r: hashResult, index: 3, parsing_key: 1 }; + generatePassCase(input3, { out: 0 }, "wrong index"); + + let input4 = { data: input[0], key: [98], r: hashResult, index: 2, parsing_key: 1 }; + generatePassCase(input4, { out: 0 }, "wrong key"); + + let input5 = { data: input[0], key: [97], r: hashResult, index: 2, parsing_key: 0 }; + generatePassCase(input5, { out: 0 }, "not parsing key"); + }); + + describe("KeyMatchAtDepth", async () => { + let circuit: WitnessTester<["data", "key", "r", "index", "parsing_key", "stack"], ["out"]>; + + function generatePassCase(input: any, expected: any, depth: number, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description} ${desc}`, async () => { + circuit = await circomkit.WitnessTester(`KeyMatchAtDepth`, { + file: "circuits/json/interpreter", + template: "KeyMatchAtDepth", + params: [input.data.length, 4, input.key.length, depth], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + await circuit.expectPass(input, expected); + }); + } + + let input = readInputFile("value_array_object.json", ["a", 0, "b", 0]); + const concatenatedInput = input[1][0].concat(input[0]); + const hashResult = PoseidonModular(concatenatedInput); + + let output = { out: 1 }; + + let input1 = { data: input[0], key: input[1][0], r: hashResult, index: 2, parsing_key: 1, stack: [[1, 0], [0, 0], [0, 0], [0, 0]] }; + generatePassCase(input1, output, 0, ""); + + let input2 = { data: input[0], key: input[1][2], r: hashResult, index: 8, parsing_key: 1, stack: [[1, 1], [2, 0], [1, 0], [0, 0]] }; + generatePassCase(input2, output, 2, ""); + + let input3 = { data: input[0], key: [99], r: hashResult, index: 20, parsing_key: 1, stack: [[1, 1], [2, 1], [1, 1], [0, 0]] }; + generatePassCase(input3, { out: 1 }, 2, "wrong stack"); + + // fail cases + + let input4 = { data: input[0], key: input[1][1], r: hashResult, index: 3, parsing_key: 1, stack: [[1, 0], [2, 0], [1, 0], [0, 0]] }; + generatePassCase(input4, { out: 0 }, 2, "wrong key"); + + let input5 = { data: input[0], key: [97], r: hashResult, index: 12, parsing_key: 0, stack: [[1, 1], [2, 0], [1, 1], [0, 0]] }; + generatePassCase(input5, { out: 0 }, 3, "not parsing key"); + + let input6Data = input[0].slice(0); + let input6 = { data: input6Data.splice(1, 1, 35), key: input[1][0], r: hashResult, index: 2, parsing_key: 1, stack: [[1, 0], [0, 0], [0, 0], [0, 0]] }; + generatePassCase(input6, { out: 0 }, 0, "invalid key (not surrounded by quotes)"); + + let input7 = { data: input[0], key: input[1][0], r: hashResult, index: 2, parsing_key: 1, stack: [[1, 0], [0, 0], [0, 0], [0, 0]] }; + generatePassCase(input6, { out: 0 }, 1, "wrong depth"); + }); +}); \ No newline at end of file diff --git a/circuits/test/json/parser/index.ts b/circuits/test/json/parser/index.ts new file mode 100644 index 0000000..3bde941 --- /dev/null +++ b/circuits/test/json/parser/index.ts @@ -0,0 +1,56 @@ +// constants.ts + +export const Delimiters = { + // ASCII char: `{` + START_BRACE: 123, + // ASCII char: `}` + END_BRACE: 125, + // ASCII char `[` + START_BRACKET: 91, + // ASCII char `]` + END_BRACKET: 93, + // ASCII char `"` + QUOTE: 34, + // ASCII char `:` + COLON: 58, + // ASCII char `,` + COMMA: 44, +}; + +export const WhiteSpace = { + // ASCII char: `\n` + NEWLINE: 10, + // ASCII char: ` ` + SPACE: 32, +}; + +export const Numbers = { + ZERO: 48, + ONE: 49, + TWO: 50, + THREE: 51, + FOUR: 52, + FIVE: 53, + SIX: 54, + SEVEN: 55, + EIGHT: 56, + NINE: 57 +} + +export const Escape = { + // ASCII char: `\` + BACKSLASH: 92, +}; + +export const INITIAL_IN = { + byte: 0, + stack: [[0, 0], [0, 0], [0, 0], [0, 0]], + parsing_string: 0, + parsing_number: 0, +}; + +export const INITIAL_OUT = { + next_stack: INITIAL_IN.stack, + next_parsing_string: INITIAL_IN.parsing_string, + next_parsing_number: INITIAL_IN.parsing_number, +}; \ No newline at end of file diff --git a/circuits/test/json/parser/parsing_types.test.ts b/circuits/test/json/parser/parsing_types.test.ts new file mode 100644 index 0000000..a5783dc --- /dev/null +++ b/circuits/test/json/parser/parsing_types.test.ts @@ -0,0 +1,110 @@ +import { circomkit, WitnessTester, generateDescription } from "../../common"; +import { Delimiters, WhiteSpace, Numbers, Escape, INITIAL_IN, INITIAL_OUT } from '.'; + + + +describe("StateUpdate", () => { + let circuit: WitnessTester< + ["byte", "stack", "parsing_string", "parsing_number"], + ["next_stack", "next_parsing_string", "next_parsing_number"] + >; + + function generatePassCase(input: any, expected: any, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description}\n${desc}`, async () => { + await circuit.expectPass(input, expected); + }); + } + + before(async () => { + circuit = await circomkit.WitnessTester(`StateUpdate`, { + file: "circuits/json/parser/machine", + template: "StateUpdate", + params: [4], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + }); + + //-TEST_1----------------------------------------------------------// + // init: ZEROS then read `do_nothing` byte + // expect: ZEROS + generatePassCase(INITIAL_IN, INITIAL_OUT, ">>>> `NUL` read"); + + + //-TEST_2----------------------------------------------------------// + // state: stack == [[1, 0], [0, 0], [0, 0], [0, 0]] + // read: `"` + // expect: parsing_string --> 1 + let in_object_find_key = { ...INITIAL_IN }; + in_object_find_key.stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + in_object_find_key.byte = Delimiters.QUOTE; + let in_object_find_key_out = { ...INITIAL_OUT }; + in_object_find_key_out.next_stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + in_object_find_key_out.next_parsing_string = 1; + generatePassCase(in_object_find_key, + in_object_find_key_out, + ">>>> `\"` read" + ); + + //-TEST_3----------------------------------------------------------// + // state: stack = [[1, 0], [0, 0], [0, 0], [0, 0]], parsing_string == 1 + // read: ` ` + // expect: NIL + let in_key = { ...INITIAL_IN }; + in_key.stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + in_key.parsing_string = 1; + in_key.byte = WhiteSpace.SPACE; + let in_key_out = { ...INITIAL_OUT }; + in_key_out.next_stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + in_key_out.next_parsing_string = 1; + generatePassCase(in_key, in_key_out, ">>>> ` ` read"); + + //-TEST_4----------------------------------------------------------// + // init: stack == [[1, 0], [0, 0], [0, 0], [0, 0]] + // read: `"` + // expect: parsing_string --> 0 + // + let in_key_to_exit = { ...INITIAL_IN }; + in_key_to_exit.stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + in_key_to_exit.parsing_string = 1 + in_key_to_exit.byte = Delimiters.QUOTE; + let in_key_to_exit_out = { ...INITIAL_OUT }; + in_key_to_exit_out.next_stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + generatePassCase(in_key_to_exit, in_key_to_exit_out, "`\"` read"); + + //-TEST_5----------------------------------------------------------// + // state: stack == [[1, 1], [0, 0], [0, 0], [0, 0]] + // read: `"` + // expect: parsing_string --> 1 + let in_tree_find_value = { ...INITIAL_IN }; + in_tree_find_value.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + in_tree_find_value.byte = Delimiters.QUOTE; + let in_tree_find_value_out = { ...INITIAL_OUT }; + in_tree_find_value_out.next_stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + in_tree_find_value_out.next_parsing_string = 1; + generatePassCase(in_tree_find_value, + in_tree_find_value_out, + ">>>> `\"` read" + ); + + //-TEST_6----------------------------------------------------------// + // state: stack == [[1, 1], [0, 0], [0, 0], [0, 0]];, parsing_string == 1 + // read: `"` + // expect: parsing_string == 0, + let in_value_to_exit = { ...INITIAL_IN }; + in_value_to_exit.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + in_value_to_exit.parsing_string = 1; + in_value_to_exit.byte = Delimiters.QUOTE; + let in_value_to_exit_out = { ...INITIAL_OUT }; + in_value_to_exit_out.next_stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + generatePassCase(in_value_to_exit, + in_value_to_exit_out, + ">>>> `\"` is read" + ); + +}); + + + diff --git a/circuits/test/json/parser/stack.test.ts b/circuits/test/json/parser/stack.test.ts new file mode 100644 index 0000000..f719582 --- /dev/null +++ b/circuits/test/json/parser/stack.test.ts @@ -0,0 +1,225 @@ +import { circomkit, WitnessTester, generateDescription } from "../../common"; +import { Delimiters, WhiteSpace, Numbers, Escape, INITIAL_IN, INITIAL_OUT } from '.'; + +describe("GetTopOfStack", () => { + let circuit: WitnessTester<["stack"], ["value", "pointer"]>; + before(async () => { + circuit = await circomkit.WitnessTester(`GetTopOfStack`, { + file: "circuits/json/parser/machine", + template: "GetTopOfStack", + params: [4], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + function generatePassCase(input: any, expected: any) { + const description = generateDescription(input); + + it(`(valid) witness: ${description}`, async () => { + await circuit.expectPass(input, expected); + }); + } + + generatePassCase({ stack: [[1, 0], [2, 0], [3, 1], [4, 2]] }, { value: [4, 2], pointer: 4 }); + + generatePassCase({ stack: [[1, 0], [2, 1], [0, 0], [0, 0]] }, { value: [2, 1], pointer: 2 }); + + generatePassCase({ stack: [[0, 0], [0, 0], [0, 0], [0, 0]] }, { value: [0, 0], pointer: 0 }); +}); + +describe("StateUpdate :: RewriteStack", () => { + let circuit: WitnessTester< + ["byte", "stack", "parsing_string", "parsing_number"], + ["next_stack", "next_parsing_string", "next_parsing_number"] + >; + before(async () => { + circuit = await circomkit.WitnessTester(`GetTopOfStack`, { + file: "circuits/json/parser/machine", + template: "StateUpdate", + params: [4], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + + function generatePassCase(input: any, expected: any, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description}\n${desc}`, async () => { + await circuit.expectPass(input, expected); + }); + } + + function generateFailCase(input: any, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description}\n${desc}`, async () => { + await circuit.expectFail(input); + }); + } + + //-TEST_1----------------------------------------------------------// + // init: stack == [[0, 0], [0, 0], [0, 0], [0, 0]] + // read: `{` + // expect: stack --> [[1, 0], [0, 0], [0, 0], [0, 0]] + let read_start_brace = { ...INITIAL_IN }; + read_start_brace.byte = Delimiters.START_BRACE; + let read_start_brace_out = { ...INITIAL_OUT }; + read_start_brace_out.next_stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + generatePassCase(read_start_brace, + read_start_brace_out, + ">>>> `{` read" + ); + + //-TEST_2----------------------------------------------------------// + // state: stack == [[1, 0], [0, 0], [0, 0], [0, 0]] + // read: `{` + // expect: stack --> [[1, 0], [1, 0], [0, 0], [0, 0]] + let in_object = { ...INITIAL_IN }; + in_object.stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + in_object.byte = Delimiters.START_BRACE; + let in_object_out = { ...INITIAL_OUT }; + in_object_out.next_stack = [[1, 0], [1, 0], [0, 0], [0, 0]]; + generatePassCase(in_object, in_object_out, ">>>> `{` read"); + + //-TEST_3----------------------------------------------------------// + // state: stack == [[1, 0], [0, 0], [0, 0], [0, 0]] + // read: `}` + // expect: stack --> [[0, 0], [0, 0], [0, 0], [0, 0]] + let in_object_to_leave = { ...INITIAL_IN }; + in_object_to_leave.stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + in_object_to_leave.byte = Delimiters.END_BRACE; + let in_object_to_leave_out = { ...INITIAL_OUT }; + generatePassCase(in_object_to_leave, + in_object_to_leave_out, + ">>>> `}` read" + ); + + //-TEST_4----------------------------------------------------------// + // init: stack == [[1, 0], [0, 0], [0, 0], [0, 0]] + // read: `[` + // expect: stack --> [[1, 0], [2, 0], [0, 0], [0, 0]] + let in_object_to_read_start_bracket = { ...INITIAL_IN }; + in_object_to_read_start_bracket.byte = Delimiters.START_BRACKET; + in_object_to_read_start_bracket.stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + let in_object_to_read_start_bracket_out = { ...INITIAL_OUT }; + in_object_to_read_start_bracket_out.next_stack = [[1, 0], [2, 0], [0, 0], [0, 0]]; + generatePassCase(in_object_to_read_start_bracket, + in_object_to_read_start_bracket_out, + ">>>> `[` read" + ); + + //-TEST_5----------------------------------------------------------// + // init: stack == [[1, 0], [2, 0], [0, 0], [0, 0]] + // read: `]` + // expect: stack --> [[1, 0], [0, 0], [0, 0], [0, 0]] + let in_object_and_array = { ...INITIAL_IN }; + in_object_and_array.byte = Delimiters.END_BRACKET; + in_object_and_array.stack = [[1, 0], [2, 0], [0, 0], [0, 0]]; + let in_object_and_array_out = { ...INITIAL_OUT }; + in_object_and_array_out.next_stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + generatePassCase(in_object_and_array, + in_object_and_array_out, + ">>>> `]` read" + ); + + //-TEST_6-----------------------------------------------------------// + // state: stack == [[1, 0], [0, 0], [0, 0], [0, 0]] + // read: `:` + // expect: stack --> [[1, 1], [0, 0], [0, 0], [0, 0]] + let parsed_key_wait_to_parse_value = { ...INITIAL_IN }; + parsed_key_wait_to_parse_value.stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + parsed_key_wait_to_parse_value.byte = Delimiters.COLON; + let parsed_key_wait_to_parse_value_out = { ...INITIAL_OUT }; + parsed_key_wait_to_parse_value_out.next_stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + generatePassCase(parsed_key_wait_to_parse_value, + parsed_key_wait_to_parse_value_out, + ">>>> `:` read" + ); + + //-TEST_7----------------------------------------------------------// + // init: stack == [[1, 0], [0, 0], [0, 0], [0, 0]] + // expect: stack --> [[1, 0], [0, 0], [0, 0], [0, 0]] + let in_object_and_value = { ...INITIAL_IN }; + in_object_and_value.byte = Delimiters.COMMA; + in_object_and_value.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + let in_object_and_value_out = { ...INITIAL_OUT }; + in_object_and_value_out.next_stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + generatePassCase(in_object_and_value, + in_object_and_value_out, + ">>>> `,` read" + ); + + //-TEST_8----------------------------------------------------------// + // init: stack == [[1, 1], [0, 0], [0, 0], [0, 0]] + // read: `}` + // expect: stack --> [[0, 0], [0, 0], [0, 0], [0, 0]] + let in_object_and_value_to_leave_object = { ...INITIAL_IN }; + in_object_and_value_to_leave_object.byte = Delimiters.END_BRACE; + in_object_and_value_to_leave_object.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + let in_object_and_value_to_leave_object_out = { ...INITIAL_OUT }; + in_object_and_value_to_leave_object_out.next_stack = [[0, 0], [0, 0], [0, 0], [0, 0]]; + generatePassCase(in_object_and_value_to_leave_object, + in_object_and_value_to_leave_object_out, + ">>>> `}` read" + ); + + //-TEST_9----------------------------------------------------------// + // 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: pointer --> 2 + // stack --> [[1, 0], [0, 0], [0, 0], [0, 0]] + // parsing_number --> 0 + let inside_number = { ...INITIAL_IN }; + inside_number.stack = [[1, 1], [0, 0], [0, 0], [0, 0]]; + inside_number.parsing_number = 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]]; + generatePassCase(inside_number, inside_number_out, ">>>> `,` read"); + + + // TODO: FAIL CASES, ADD STACK UNDERFLOW CASES TOO and RENUMBER + //-TEST_3----------------------------------------------------------// + // state: INIT + // read: `}` + // expect: FAIL (stack underflow) + let read_end_brace = { ...INITIAL_IN }; + read_end_brace.byte = Delimiters.END_BRACE; + generateFailCase(read_end_brace, + ">>>> `}` read --> (stack underflow)" + ); + + //-TEST_9----------------------------------------------------------// + // init: stack == [[1, 0], [1, 0], [1, 0], [1, 0]] + // expect: FAIL, STACK OVERFLOW + let in_max_stack = { ...INITIAL_IN }; + in_max_stack.byte = Delimiters.START_BRACE; + in_max_stack.stack = [[1, 0], [1, 0], [1, 0], [1, 0]]; + generateFailCase(in_max_stack, ">>>> `{` read --> (stack overflow)"); + + //-TEST_10----------------------------------------------------------// + // init: stack == [[1, 0], [1, 0], [1, 0], [1, 0]] + // expect: FAIL, STACK OVERFLOW + let in_max_stack_2 = { ...INITIAL_IN }; + in_max_stack_2.byte = Delimiters.START_BRACKET; + in_max_stack_2.stack = [[1, 0], [1, 0], [1, 0], [1, 0]]; + generateFailCase(in_max_stack, ">>>> `[` read --> (stack overflow)"); + + // TODO: This requires a more careful check of the stack that popping clears the current value. Use an IsZero + // //-TEST_3----------------------------------------------------------// + // // init: stack == [1,0,0,0] + // // read: `]` + // // expect: FAIL, INVALID CHAR + // let in_object_to_read_start_bracket = { ...INITIAL_IN }; + // in_object_to_read_start_bracket.byte = Delimiters.START_BRACKET; + // in_object_to_read_start_bracket.pointer = 1; + // in_object_to_read_start_bracket.stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + // let in_object_to_read_start_bracket_out = { ...INITIAL_OUT }; + // in_object_to_read_start_bracket_out.next_pointer = 2; + // in_object_to_read_start_bracket_out.next_stack = [[1, 0], [2, 0], [0, 0], [0, 0]]; + // generatePassCase(in_object_to_read_start_bracket, + // in_object_to_read_start_bracket_out, + // ">>>> `[` read" + // ); +}); \ No newline at end of file diff --git a/circuits/test/json/parser/values.test.ts b/circuits/test/json/parser/values.test.ts new file mode 100644 index 0000000..2bca379 --- /dev/null +++ b/circuits/test/json/parser/values.test.ts @@ -0,0 +1,130 @@ +import { circomkit, WitnessTester, generateDescription } from "../../common"; +import { Delimiters, WhiteSpace, Numbers, Escape, INITIAL_IN, INITIAL_OUT } from '.'; + +describe("StateUpdate :: Values", () => { + let circuit: WitnessTester< + ["byte", "pointer", "stack", "parsing_string", "parsing_number"], + ["next_pointer", "next_stack", "next_parsing_string", "next_parsing_number"] + >; + before(async () => { + circuit = await circomkit.WitnessTester(`GetTopOfStack`, { + file: "circuits/json/parser/machine", + template: "StateUpdate", + params: [4], + }); + console.log("#constraints:", await circuit.getConstraintCount()); + }); + function generatePassCase(input: any, expected: any, desc: string) { + const description = generateDescription(input); + + it(`(valid) witness: ${description}\n${desc}`, async () => { + await circuit.expectPass(input, expected); + }); + } + + 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! + + //-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]] + // read: `,` + // expext: stack --> [[1, 0], [2, 1], [0, 0], [0, 0]] + let in_arr = { ...INITIAL_IN }; + in_arr.stack = [[1, 0], [2, 0], [0, 0], [0, 0]]; + in_arr.byte = Delimiters.COMMA; + let in_arr_out = { ...INITIAL_OUT }; + in_arr_out.next_stack = [[1, 0], [2, 1], [0, 0], [0, 0]]; + generatePassCase(in_arr, in_arr_out, ">>>> `,` read"); + + //-TEST_10----------------------------------------------------------// + // init: stack == [[1, 0], [2, 1], [0, 0], [0, 0]] + // read: `]` + // expect: stack --> [[1, 0], [0, 0], [0, 0], [0, 0]] + let in_arr_idx_to_leave = { ...INITIAL_IN }; + in_arr_idx_to_leave.stack = [[1, 0], [2, 1], [0, 0], [0, 0]]; + in_arr_idx_to_leave.byte = Delimiters.END_BRACKET; + let in_arr_idx_to_leave_out = { ...INITIAL_OUT }; + in_arr_idx_to_leave_out.next_stack = [[1, 0], [0, 0], [0, 0], [0, 0]]; + generatePassCase(in_arr_idx_to_leave, in_arr_idx_to_leave_out, ">>>> `]` read"); + }); +}); \ No newline at end of file diff --git a/circuits/test/search.test.ts b/circuits/test/utils/search.test.ts similarity index 89% rename from circuits/test/search.test.ts rename to circuits/test/utils/search.test.ts index 59692db..f0938db 100644 --- a/circuits/test/search.test.ts +++ b/circuits/test/utils/search.test.ts @@ -1,7 +1,7 @@ -import { circomkit, WitnessTester } from "./common"; +import { circomkit, WitnessTester } from "../common"; -import witness from "../../inputs/search/witness.json"; -import { PoseidonModular } from "./common/poseidon"; +import witness from "../../../inputs/search/witness.json"; +import { PoseidonModular } from "../common/poseidon"; describe("search", () => { describe("SubstringSearch", () => { @@ -14,7 +14,7 @@ describe("search", () => { const hashResult = PoseidonModular(concatenatedInput); circuit = await circomkit.WitnessTester(`SubstringSearch`, { - file: "circuits/search", + file: "circuits/utils/search", template: "SubstringSearch", params: [data.length, key.length], }); @@ -32,7 +32,7 @@ describe("search", () => { const hashResult = PoseidonModular(concatenatedInput); circuit = await circomkit.WitnessTester(`SubstringSearch`, { - file: "circuits/search", + file: "circuits/utils/search", template: "SubstringSearch", params: [data.length, key.length], }); @@ -51,7 +51,7 @@ describe("search", () => { const key = [1, 0]; circuit = await circomkit.WitnessTester(`SubstringSearch`, { - file: "circuits/search", + file: "circuits/utils/search", template: "SubstringSearch", params: [data.length, key.length], }); @@ -67,7 +67,7 @@ describe("search", () => { const hashResult = PoseidonModular(concatenatedInput); circuit = await circomkit.WitnessTester(`SubstringSearch`, { - file: "circuits/search", + file: "circuits/utils/search", template: "SubstringSearch", params: [witness["data"].length, witness["key"].length], }); @@ -81,11 +81,11 @@ describe("search", () => { }); describe("SubstringMatchWithIndex", () => { - let circuit: WitnessTester<["data", "key", "r", "start"]>; + let circuit: WitnessTester<["data", "key", "r", "start"], ["out"]>; before(async () => { circuit = await circomkit.WitnessTester(`SubstringSearch`, { - file: "circuits/search", + file: "circuits/utils/search", template: "SubstringMatchWithIndex", params: [787, 10], }); @@ -100,17 +100,19 @@ describe("search", () => { r: PoseidonModular(witness["key"].concat(witness["data"])), start: 6 }, + { out: 1 }, ); }); - it("data = witness.json:data, key = witness.json:key, r = hash(key+data), incorrect position", async () => { - await circuit.expectFail( + it("data = witness.json:data, key = witness.json:key, r = hash(key+data), output false", async () => { + await circuit.expectPass( { data: witness["data"], key: witness["key"], r: PoseidonModular(witness["key"].concat(witness["data"])), start: 98 }, + { out: 0 } ); }); }); @@ -120,7 +122,7 @@ describe("search", () => { before(async () => { circuit = await circomkit.WitnessTester(`SubstringSearch`, { - file: "circuits/search", + file: "circuits/utils/search", template: "SubstringMatch", params: [787, 10], }); diff --git a/circuits/utils/array.circom b/circuits/utils/array.circom index dc65b28..6a9ae48 100644 --- a/circuits/utils/array.circom +++ b/circuits/utils/array.circom @@ -200,4 +200,39 @@ template ScalarArrayMul(n) { for(var i = 0; i < n; i++) { out[i] <== scalar * array[i]; } +} + +// from: https://github.com/pluto/aes-proof/blob/main/circuits/aes-gcm/helper_functions.circom +template SumMultiple(n) { + signal input nums[n]; + signal output sum; + + signal sums[n]; + sums[0] <== nums[0]; + + for(var i=1; i=16" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -456,7 +487,6 @@ "resolved": "https://registry.npmjs.org/circom_runtime/-/circom_runtime-0.1.25.tgz", "integrity": "sha512-xBGsBFF5Uv6AKvbpgExYqpHfmfawH2HKe+LyjfKSRevqEV8u63i9KGHVIILsbJNW+0c5bm/66f0PUYQ7qZSkJA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "ffjavascript": "0.3.0" }, @@ -469,7 +499,6 @@ "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.3.0.tgz", "integrity": "sha512-l7sR5kmU3gRwDy8g0Z2tYBXy5ttmafRPFOqY7S6af5cq51JqJWt5eQ/lSR/rs2wQNbDYaYlQr5O+OSUf/oMLoQ==", "license": "GPL-3.0", - "peer": true, "dependencies": { "wasmbuilder": "0.0.16", "wasmcurves": "0.2.2", @@ -1802,7 +1831,6 @@ "resolved": "https://registry.npmjs.org/snarkjs/-/snarkjs-0.7.4.tgz", "integrity": "sha512-x4cOCR4YXSyBlLtfnUUwfbZrw8wFd/Y0lk83eexJzKwZB8ELdpH+10ts8YtDsm2/a3WK7c7p514bbE8NpqxW8w==", "license": "GPL-3.0", - "peer": true, "dependencies": { "@iden3/binfileutils": "0.0.12", "bfj": "^7.0.2", @@ -1824,7 +1852,6 @@ "resolved": "https://registry.npmjs.org/@iden3/binfileutils/-/binfileutils-0.0.12.tgz", "integrity": "sha512-naAmzuDufRIcoNfQ1d99d7hGHufLA3wZSibtr4dMe6ZeiOPV1KwOZWTJ1YVz4HbaWlpDuzVU72dS4ATQS4PXBQ==", "license": "GPL-3.0", - "peer": true, "dependencies": { "fastfile": "0.0.20", "ffjavascript": "^0.3.0" @@ -1835,7 +1862,6 @@ "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.3.0.tgz", "integrity": "sha512-l7sR5kmU3gRwDy8g0Z2tYBXy5ttmafRPFOqY7S6af5cq51JqJWt5eQ/lSR/rs2wQNbDYaYlQr5O+OSUf/oMLoQ==", "license": "GPL-3.0", - "peer": true, "dependencies": { "wasmbuilder": "0.0.16", "wasmcurves": "0.2.2", @@ -1847,7 +1873,6 @@ "resolved": "https://registry.npmjs.org/r1csfile/-/r1csfile-0.0.48.tgz", "integrity": "sha512-kHRkKUJNaor31l05f2+RFzvcH5XSa7OfEfd/l4hzjte6NL6fjRkSMfZ4BjySW9wmfdwPOtq3mXurzPvPGEf5Tw==", "license": "GPL-3.0", - "peer": true, "dependencies": { "@iden3/bigarray": "0.0.2", "@iden3/binfileutils": "0.0.12", diff --git a/package.json b/package.json index 62efd83..8f55708 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,11 @@ "version": "0.1.0", "license": "Apache-2.0", "scripts": { - "test": "npx mocha" + "test": "npx mocha", + "par-test": "npx mocha --parallel" }, "dependencies": { + "@zk-email/circuits": "^6.1.1", "circomkit": "^0.2.1", "circomlib": "^2.0.5" }, diff --git a/src/bin/codegen.rs b/src/bin/codegen.rs new file mode 100644 index 0000000..fc4b8fa --- /dev/null +++ b/src/bin/codegen.rs @@ -0,0 +1,508 @@ +use clap::Parser; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use std::str::FromStr; + +#[derive(Parser, Debug)] +#[command(name = "codegen")] +struct Args { + /// Path to the JSON file + #[arg(short, long)] + json_file: PathBuf, + + /// Output circuit file name + #[arg(short, long, default_value = "extractor")] + output_filename: String, +} + +#[derive(Debug, Deserialize)] +enum ValueType { + #[serde(rename = "string")] + String, + #[serde(rename = "number")] + Number, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +enum Key { + String(String), + Num(i64), +} + +#[derive(Debug, Deserialize)] +struct Data { + keys: Vec, + value_type: ValueType, +} + +const PRAGMA: &str = "pragma circom 2.1.9;\n\n"; + +fn extract_string(data: Data, circuit_buffer: &mut String) { + *circuit_buffer += "template ExtractStringValue(DATA_BYTES, MAX_STACK_HEIGHT, "; + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => *circuit_buffer += &format!("keyLen{}, depth{}, ", i + 1, i + 1), + Key::Num(_) => *circuit_buffer += &format!("index{}, depth{}, ", i + 1, i + 1), + } + } + *circuit_buffer += "maxValueLen) {\n"; + + *circuit_buffer += " signal input data[DATA_BYTES];\n\n"; + + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => { + *circuit_buffer += &format!(" signal input key{}[keyLen{}];\n", i + 1, i + 1) + } + Key::Num(_) => (), + } + } + + *circuit_buffer += r#" + signal output value[maxValueLen]; + + signal value_starting_index[DATA_BYTES]; +"#; + + // value_starting_index <== ExtractValue2(DATA_BYTES, MAX_STACK_HEIGHT, keyLen1, depth1, keyLen2, depth2, index3, depth3, index4, depth4, maxValueLen)(data, key1, key2); + { + *circuit_buffer += + " value_starting_index <== ExtractValue2(DATA_BYTES, MAX_STACK_HEIGHT, "; + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => *circuit_buffer += &format!("keyLen{}, depth{}, ", i + 1, i + 1), + Key::Num(_) => *circuit_buffer += &format!("index{}, depth{}, ", i + 1, i + 1), + } + } + *circuit_buffer += "maxValueLen)(data, "; + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => *circuit_buffer += &format!("key{}, ", i + 1), + Key::Num(_) => (), + } + } + circuit_buffer.pop(); + circuit_buffer.pop(); + *circuit_buffer += ");\n"; + } + + *circuit_buffer += r#" + log("value_starting_index", value_starting_index[DATA_BYTES-2]); + value <== SelectSubArray(DATA_BYTES, maxValueLen)(data, value_starting_index[DATA_BYTES-2]+1, maxValueLen); + + for (var i=0 ; i *circuit_buffer += &format!("keyLen{}, depth{}, ", i + 1, i + 1), + Key::Num(_) => *circuit_buffer += &format!("index{}, depth{}, ", i + 1, i + 1), + } + } + *circuit_buffer += "maxValueLen) {\n"; + + *circuit_buffer += " signal input data[DATA_BYTES];\n\n"; + + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => { + *circuit_buffer += &format!(" signal input key{}[keyLen{}];\n", i + 1, i + 1) + } + Key::Num(_) => (), + } + } + + *circuit_buffer += r#" + signal value_string[maxValueLen]; + signal output value; + + signal value_starting_index[DATA_BYTES]; +"#; + + // value_starting_index <== ExtractValue2(DATA_BYTES, MAX_STACK_HEIGHT, keyLen1, depth1, keyLen2, depth2, index3, depth3, index4, depth4, maxValueLen)(data, key1, key2); + { + *circuit_buffer += + " value_starting_index <== ExtractValue2(DATA_BYTES, MAX_STACK_HEIGHT, "; + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => *circuit_buffer += &format!("keyLen{}, depth{}, ", i + 1, i + 1), + Key::Num(_) => *circuit_buffer += &format!("index{}, depth{}, ", i + 1, i + 1), + } + } + *circuit_buffer += "maxValueLen)(data, "; + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => *circuit_buffer += &format!("key{}, ", i + 1), + Key::Num(_) => (), + } + } + circuit_buffer.pop(); + circuit_buffer.pop(); + *circuit_buffer += ");\n"; + } + + *circuit_buffer += r#" + log("value_starting_index", value_starting_index[DATA_BYTES-2]); + value_string <== SelectSubArray(DATA_BYTES, maxValueLen)(data, value_starting_index[DATA_BYTES-2], maxValueLen); + + for (var i=0 ; i Result<(), Box> { + let mut circuit_buffer = String::new(); + circuit_buffer += PRAGMA; + circuit_buffer += "include \"../json/interpreter.circom\";\n\n"; + + // template ExtractValue2(DATA_BYTES, MAX_STACK_HEIGHT, keyLen1, depth1, index2, depth2, keyLen3, depth3, index4, depth4, maxValueLen) { + { + circuit_buffer += "template ExtractValue2(DATA_BYTES, MAX_STACK_HEIGHT, "; + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => circuit_buffer += &format!("keyLen{}, depth{}, ", i + 1, i + 1), + Key::Num(_) => circuit_buffer += &format!("index{}, depth{}, ", i + 1, i + 1), + } + } + circuit_buffer += "maxValueLen) {\n"; + } + + /* + signal input data[DATA_BYTES]; + + signal input key1[keyLen1]; + signal input key3[keyLen3]; + */ + { + circuit_buffer += " signal input data[DATA_BYTES];\n\n"; + + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => { + circuit_buffer += &format!(" signal input key{}[keyLen{}];\n", i + 1, i + 1) + } + Key::Num(_) => (), + } + } + } + + /* + component rHasher = PoseidonModular(dataLen + keyLen1 + keyLen3); + for (var i = 0; i < keyLen1; i++) { + rHasher.in[i] <== key1[i]; + } + for (var i = 0; i < keyLen3; i++) { + rHasher.in[keyLen1 + i] <== key3[i]; + } + for (var i = 0; i < dataLen; i++) { + rHasher.in[i + keyLen1 + keyLen3] <== data[i]; + } + signal r <== rHasher.out; + */ + { + circuit_buffer += "\n // r must be secret, so either has to be derived from hash in the circuit or off the circuit\n component rHasher = PoseidonModular(DATA_BYTES + "; + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => circuit_buffer += &format!(" keyLen{} +", i + 1), + Key::Num(_) => (), + } + } + circuit_buffer.pop(); + circuit_buffer.pop(); + circuit_buffer += ");\n"; + + let mut key_len_counter_str = String::from_str("i")?; + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => { + circuit_buffer += &format!(" for (var i = 0 ; i < keyLen{} ; i++) {{\n rHasher.in[{}] <== key{}[i];\n }}\n", i+1, key_len_counter_str, i+1); + key_len_counter_str += &format!(" + keyLen{}", i + 1); + } + Key::Num(_) => (), + } + } + + circuit_buffer += &format!(" for (var i = 0 ; i < DATA_BYTES ; i++) {{\n rHasher.in[{}] <== data[i];\n }}\n", key_len_counter_str); + } + + circuit_buffer += r#" signal r <== rHasher.out; + + signal output value_starting_index[DATA_BYTES]; + + signal mask[DATA_BYTES]; + // mask[0] <== 0; + + var logDataLen = log2Ceil(DATA_BYTES); + + component State[DATA_BYTES]; + State[0] = StateUpdate(MAX_STACK_HEIGHT); + State[0].byte <== data[0]; + for(var i = 0; i < MAX_STACK_HEIGHT; i++) { + State[0].stack[i] <== [0,0]; + } + State[0].parsing_string <== 0; + State[0].parsing_number <== 0; + + signal parsing_key[DATA_BYTES]; + signal parsing_value[DATA_BYTES]; +"#; + + /* // signals for parsing string key and array index + signal parsing_key[DATA_BYTES]; + signal parsing_value[DATA_BYTES]; + signal parsing_object1_value[DATA_BYTES]; + signal parsing_array2[DATA_BYTES]; + signal is_key1_match[DATA_BYTES]; + signal is_key1_match_for_value[DATA_BYTES]; + is_key1_match_for_value[0] <== 0; + signal is_next_pair_at_depth1[DATA_BYTES]; + */ + { + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => { + circuit_buffer += + &format!(" signal parsing_object{}_value[DATA_BYTES];\n", i + 1) + } + Key::Num(_) => { + circuit_buffer += &format!(" signal parsing_array{}[DATA_BYTES];\n", i + 1) + } + } + } + + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => circuit_buffer += &format!(" signal is_key{}_match[DATA_BYTES];\n signal is_key{}_match_for_value[DATA_BYTES];\n is_key{}_match_for_value[0] <== 0;\n signal is_next_pair_at_depth{}[DATA_BYTES];\n", i+1, i+1, i+1, i+1), + Key::Num(_) => (), + } + } + } + + // debugging + circuit_buffer += r#" + signal is_value_match[DATA_BYTES]; + is_value_match[0] <== 0; + signal value_mask[DATA_BYTES]; + for(var data_idx = 1; data_idx < DATA_BYTES; data_idx++) { + // Debugging + for(var i = 0; i { + circuit_buffer += &format!(" parsing_object{}_value[data_idx-1] <== InsideValueAtDepth(MAX_STACK_HEIGHT, depth{})(State[data_idx].stack, State[data_idx].parsing_string, State[data_idx].parsing_number);\n", i+1, i+1); + } + Key::Num(_) => { + circuit_buffer += &format!(" parsing_array{}[data_idx-1] <== InsideArrayIndexAtDepth(MAX_STACK_HEIGHT, index{}, depth{})(State[data_idx].stack, State[data_idx].parsing_string, State[data_idx].parsing_number);\n", i+1, i+1, i+1); + } + } + } + } + + // parsing correct value = AND(all individual stack values) + // parsing_value[data_idx-1] <== MultiAND(4)([parsing_object1_value[data_idx-1], parsing_array2[data_idx-1], parsing_object3_value[data_idx-1], parsing_array4[data_idx-1]]); + { + circuit_buffer += &format!( + " // parsing correct value = AND(all individual stack values)\n parsing_value[data_idx-1] <== MultiAND({})([", + data.keys.len() + ); + + for (i, key) in data.keys.iter().take(data.keys.len() - 1).enumerate() { + match key { + Key::String(_) => { + circuit_buffer += &format!("parsing_object{}_value[data_idx-1], ", i + 1) + } + Key::Num(_) => circuit_buffer += &format!("parsing_array{}[data_idx-1], ", i + 1), + } + } + match data.keys[data.keys.len() - 1] { + Key::String(_) => { + circuit_buffer += + &format!("parsing_object{}_value[data_idx-1]]);\n", data.keys.len()) + } + Key::Num(_) => { + circuit_buffer += &format!("parsing_array{}[data_idx-1]]);\n", data.keys.len()) + } + } + + // optional debug logs + circuit_buffer += " // log(\"parsing value:\", "; + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => { + circuit_buffer += &format!("parsing_object{}_value[data_idx-1], ", i + 1) + } + Key::Num(_) => circuit_buffer += &format!("parsing_array{}[data_idx-1], ", i + 1), + } + } + circuit_buffer += "parsing_value[data_idx-1]);\n\n"; + } + + let mut num_objects = 0; + + /* + to get correct value, check: + - key matches at current index and depth of key is as specified + - whether next KV pair starts + - whether key matched for a value (propogate key match until new KV pair of lower depth starts) + is_key1_match[data_idx-1] <== KeyMatchAtDepth(DATA_BYTES, MAX_STACK_HEIGHT, keyLen1, depth1)(data, key1, r, data_idx-1, parsing_key[data_idx-1], State[data_idx].stack); + is_next_pair_at_depth1[data_idx-1] <== NextKVPairAtDepth(MAX_STACK_HEIGHT, depth1)(State[data_idx].stack, data[data_idx-1]); + is_key1_match_for_value[data_idx] <== Mux1()([is_key1_match_for_value[data_idx-1] * (1-is_next_pair_at_depth1[data_idx-1]), is_key1_match[data_idx-1] * (1-is_next_pair_at_depth1[data_idx-1])], is_key1_match[data_idx-1]); + */ + { + circuit_buffer += r#" + // to get correct value, check: + // - key matches at current index and depth of key is as specified + // - whether next KV pair starts + // - whether key matched for a value (propogate key match until new KV pair of lower depth starts) +"#; + + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => { + num_objects += 1; + circuit_buffer += &format!(" is_key{}_match[data_idx-1] <== KeyMatchAtDepth(DATA_BYTES, MAX_STACK_HEIGHT, keyLen{}, depth{})(data, key{}, r, data_idx-1, parsing_key[data_idx-1], State[data_idx].stack);\n", i+1, i+1, i+1, i+1); + circuit_buffer += &format!(" is_next_pair_at_depth{}[data_idx-1] <== NextKVPairAtDepth(MAX_STACK_HEIGHT, depth{})(State[data_idx].stack, data[data_idx-1]);\n", i+1, i+1); + circuit_buffer += &format!(" is_key{}_match_for_value[data_idx] <== Mux1()([is_key{}_match_for_value[data_idx-1] * (1-is_next_pair_at_depth{}[data_idx-1]), is_key{}_match[data_idx-1] * (1-is_next_pair_at_depth{}[data_idx-1])], is_key{}_match[data_idx-1]);\n", i+1, i+1, i+1, i+1, i+1, i+1); + circuit_buffer += &format!(" // log(\"is_key{}_match_for_value\", is_key{}_match_for_value[data_idx]);\n\n", i + 1, i + 1); + } + Key::Num(_) => (), + } + } + } + + // is_value_match[data_idx] <== MultiAND(2)([is_key1_match_for_value[data_idx], is_key3_match_for_value[data_idx]]); + { + circuit_buffer += &format!( + " is_value_match[data_idx] <== MultiAND({})([", + num_objects + ); + for (i, key) in data.keys.iter().enumerate() { + match key { + Key::String(_) => { + circuit_buffer += &format!("is_key{}_match_for_value[data_idx], ", i + 1) + } + Key::Num(_) => (), + } + } + + // remove last 2 chars `, ` from string buffer + circuit_buffer.pop(); + circuit_buffer.pop(); + circuit_buffer += "]);\n"; + } + + // debugging and output bytes + { + circuit_buffer += r#" // log("is_value_match", is_value_match[data_idx]); + + // mask[i] = data[i] * parsing_value[i] * is_key_match_for_value[i] + value_mask[data_idx-1] <== data[data_idx-1] * parsing_value[data_idx-1]; + mask[data_idx-1] <== value_mask[data_idx-1] * is_value_match[data_idx]; + log("mask", mask[data_idx-1]); + log("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + } + + // Debugging + for(var i = 0; i < MAX_STACK_HEIGHT; i++) { + log("State[", DATA_BYTES-1, "].stack[", i,"] ", "= [",State[DATA_BYTES -1].next_stack[i][0], "][", State[DATA_BYTES - 1].next_stack[i][1],"]" ); + } + log("State[", DATA_BYTES-1, "].parsing_string", "= ", State[DATA_BYTES-1].next_parsing_string); + log("State[", DATA_BYTES-1, "].parsing_number", "= ", State[DATA_BYTES-1].next_parsing_number); + log("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + + // signal value_starting_index[DATA_BYTES]; + signal is_zero_mask[DATA_BYTES]; + signal is_prev_starting_index[DATA_BYTES]; + value_starting_index[0] <== 0; + is_zero_mask[0] <== IsZero()(mask[0]); + for (var i=1 ; i extract_string(data, &mut circuit_buffer), + ValueType::Number => extract_number(data, &mut circuit_buffer), + } + + // write circuits to file + let mut file_path = std::env::current_dir()?; + file_path.push("circuits"); + file_path.push("main"); + file_path.push(format!("{}.circom", output_filename)); + + fs::write(&file_path, circuit_buffer)?; + + println!("Code generated at: {}", file_path.display()); + + Ok(()) +} + +pub fn main() -> Result<(), Box> { + let args = Args::parse(); + + let data = std::fs::read(&args.json_file)?; + let json_data: Data = serde_json::from_slice(&data)?; + + parse_json_request(json_data, args.output_filename)?; + + Ok(()) +}