diff --git a/circuits.json b/circuits.json index 8ed4b53..9b6742b 100644 --- a/circuits.json +++ b/circuits.json @@ -14,29 +14,12 @@ 25 ] }, - "json_mask_object_1024b": { - "file": "json/nivc/masker", - "template": "JsonMaskObjectNIVC", + "json_extraction_1024b": { + "file": "json/parser/hash_parser", + "template": "ParserHasher", "params": [ 1024, - 10, 10 ] - }, - "json_mask_array_index_1024b": { - "file": "json/nivc/masker", - "template": "JsonMaskArrayIndexNIVC", - "params": [ - 1024, - 10 - ] - }, - "json_extract_value_1024b": { - "file": "json/nivc/extractor", - "template": "MaskExtractFinal", - "params": [ - 1024, - 50 - ] } } \ No newline at end of file diff --git a/circuits/json/parser/hash_machine.circom b/circuits/json/parser/hash_machine.circom new file mode 100644 index 0000000..7eb4e83 --- /dev/null +++ b/circuits/json/parser/hash_machine.circom @@ -0,0 +1,431 @@ +/* +# `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/bits.circom"; +include "../../utils/operators.circom"; +include "../../utils/hash.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 StateUpdateHasher(MAX_STACK_HEIGHT) { + signal input byte; + + signal input stack[MAX_STACK_HEIGHT][2]; + signal input parsing_string; + signal input parsing_number; + signal input polynomial_input; + signal input monomial; + signal input tree_hash[MAX_STACK_HEIGHT][2]; + + signal output next_stack[MAX_STACK_HEIGHT][2]; + signal output next_parsing_string; + signal output next_parsing_number; + signal output next_monomial; + signal output next_tree_hash[MAX_STACK_HEIGHT][2]; + + component Command = Command(); + + // log("--------------------------------"); + // log("byte: ", byte); + // log("--------------------------------"); + + //--------------------------------------------------------------------------------------------// + // Break down what was read + // * read in a start brace `{` * + component readStartBrace = IsEqual(); + readStartBrace.in <== [byte, 123]; + // * read in an end brace `}` * + component readEndBrace = IsEqual(); + readEndBrace.in <== [byte, 125]; + // * read in a start bracket `[` * + component readStartBracket = IsEqual(); + readStartBracket.in <== [byte, 91]; + // * read in an end bracket `]` * + component readEndBracket = IsEqual(); + readEndBracket.in <== [byte, 93]; + // * read in a colon `:` * + component readColon = IsEqual(); + readColon.in <== [byte, 58]; + // * read in a comma `,` * + component readComma = IsEqual(); + readComma.in <== [byte, 44]; + // * 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, 34]; + 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]; + + next_parsing_string <== parsing_string + mulMaskAndOut.out[1]; + next_parsing_number <== parsing_number + mulMaskAndOut.out[2]; + + component newStack = RewriteStack(MAX_STACK_HEIGHT); + newStack.stack <== stack; + newStack.tree_hash <== tree_hash; + 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; + newStack.readQuote <== readQuote.out; + newStack.parsing_string <== parsing_string; + newStack.parsing_number <== parsing_number; + newStack.monomial <== monomial; + newStack.next_parsing_string <== next_parsing_string; + newStack.next_parsing_number <== next_parsing_number; + newStack.byte <== byte; + newStack.polynomial_input <== polynomial_input; + // * set all the next state of the parser * + next_stack <== newStack.next_stack; + next_tree_hash <== newStack.next_tree_hash; + next_monomial <== newStack.next_monomial; +} + +/* +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; + _ <== atTop.match; + 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 tree_hash[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 input readQuote; + + signal input parsing_number; + signal input parsing_string; + signal input next_parsing_string; + signal input next_parsing_number; + signal input byte; + signal input polynomial_input; + signal input monomial; + + signal output next_monomial; + signal output next_stack[n][2]; + signal output next_tree_hash[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 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 * + signal isPush <== IsEqual()([readStartBrace + readStartBracket, 1]); + signal isPop <== IsEqual()([readEndBrace + readEndBracket, 1]); + signal nextPointer <== pointer + isPush - isPop; + // // * set an indicator array for where we are pushing to or popping from * + signal indicator[n]; + signal tree_hash_indicator[n]; + for(var i = 0; i < n; i++) { + indicator[i] <== IsZero()(pointer - isPop - readColon - readComma - i); // Note, pointer points to unallocated region! + tree_hash_indicator[i] <== IsZero()(pointer - i - 1); + } + //--------------------------------------------------------------------------------------------// + + //--------------------------------------------------------------------------------------------// + // Hash the next_* states to produce hash we need + // TODO: This could be optimized -- we don't really need to do the index selector, we can just accumulate elsewhere + component stateHash[2]; + stateHash[0] = IndexSelector(n); + stateHash[0].index <== pointer - 1; + stateHash[1] = IndexSelector(n); + stateHash[1].index <== pointer - 1; + for(var i = 0 ; i < n ; i++) { + stateHash[0].in[i] <== tree_hash[i][0]; + stateHash[1].in[i] <== tree_hash[i][1]; + } + + signal is_object_key <== IsEqualArray(2)([current_value,[1,0]]); + signal is_object_value <== IsEqualArray(2)([current_value,[1,1]]); + signal is_array <== IsEqual()([current_value[0], 2]); + + signal not_to_hash <== IsZero()(parsing_string * next_parsing_string + next_parsing_number); + signal hash_0 <== is_object_key * stateHash[0].out; // TODO: I think these may not be needed + signal hash_1 <== (is_object_value + is_array) * stateHash[1].out; // TODO: I think these may not be needed + + signal monomial_is_zero <== IsZero()(monomial); + signal increased_power <== monomial * polynomial_input; + next_monomial <== (1 - not_to_hash) * (monomial_is_zero + increased_power); // if monomial is zero and to_hash, then this treats monomial as if it is 1, else we increment the monomial + signal option_hash <== hash_0 + hash_1 + byte * next_monomial; + + signal next_state_hash[2]; + next_state_hash[0] <== not_to_hash * (stateHash[0].out - option_hash) + option_hash; // same as: (1 - not_to_hash[i]) * option_hash[i] + not_to_hash[i] * hash[i]; + next_state_hash[1] <== not_to_hash * (stateHash[1].out - option_hash) + option_hash; + // ^^^^ next_state_hash is the previous value (state_hash) or it is the newly computed value (option_hash) + //--------------------------------------------------------------------------------------------// + + //--------------------------------------------------------------------------------------------// + // * loop to modify the stack and tree hash by rebuilding it * + signal stack_change_value[2] <== [(isPush + isPop) * read_write_value, readColon + readCommaInArray - readCommaNotInArray]; + signal second_index_clear[n]; + + signal still_parsing_string <== parsing_string * next_parsing_string; + signal still_parsing_object_key <== still_parsing_string * is_object_key; + signal end_kv <== (1 - parsing_string) * (readComma + readEndBrace + readEndBracket); + // signal not_array_and_not_object_value <== (1 - is_array) * (1 - is_object_value); + // signal not_array_and_not_object_value_and_not_end_kv <== not_array_and_not_object_value * (1 - end_kv); + // signal not_array_and_not_end_kv <== (1 - is_array) * (1 - end_kv); + signal to_change_zeroth <== (1 - is_array) * still_parsing_object_key + end_kv; + + signal not_end_char_for_first <== IsZero()(readColon + readComma + readQuote + (1-next_parsing_number)); + signal maintain_zeroth <== is_object_value * stateHash[0].out; + signal to_change_first <== is_object_value + is_array; + // signal tree_hash_change_value[2] <== [not_array_and_not_object_value_and_not_end_kv * next_state_hash[0], to_change_first * next_state_hash[1]]; + + signal to_clear_zeroth <== end_kv; + signal stopped_parsing_number <== IsEqual()([(parsing_number - next_parsing_number), 1]); + signal not_to_clear_first <== IsZero()(end_kv + readQuote * parsing_string + stopped_parsing_number); + signal to_clear_first <== (1 - not_to_clear_first); + signal tree_hash_change_value[2] <== [(1 - to_clear_zeroth) * next_state_hash[0], (1 - to_clear_first) * next_state_hash[1]]; + + signal to_update_hash[n][2]; + for(var i = 0 ; i < n ; i++) { + to_update_hash[i][0] <== tree_hash_indicator[i] * to_change_zeroth; + to_update_hash[i][1] <== tree_hash_indicator[i] * to_change_first; + } + /* + NOTE: + - The thing to do now is to make this so it clears off the value when we want and update it when we want. + - Let's us a "to_change_zeroth" and "to_clear_zeroth" together. You will need both to clear, just "change" to update + */ + for(var i = 0; i < n; i++) { + next_stack[i][0] <== stack[i][0] + indicator[i] * 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] * (stack_change_value[1] - second_index_clear[i]); + + next_tree_hash[i][0] <== tree_hash[i][0] + to_update_hash[i][0] * (tree_hash_change_value[0] - tree_hash[i][0]); + next_tree_hash[i][1] <== tree_hash[i][1] + to_update_hash[i][1] * (tree_hash_change_value[1] - tree_hash[i][1]); + } + //--------------------------------------------------------------------------------------------// + + // log("to_clear_zeroth = ", to_clear_zeroth); + // log("to_clear_first = ", to_clear_first); + // log("to_change_zeroth = ", to_change_zeroth); + // log("to_change_first = ", to_change_first); + // log("--------------------------------"); + + //--------------------------------------------------------------------------------------------// + // * check for under or overflow + signal isUnderflowOrOverflow <== InRange(8)(pointer - isPop + isPush, [0,n]); + isUnderflowOrOverflow === 1; + //--------------------------------------------------------------------------------------------// +} diff --git a/circuits/json/parser/hash_parser.circom b/circuits/json/parser/hash_parser.circom new file mode 100644 index 0000000..acb746a --- /dev/null +++ b/circuits/json/parser/hash_parser.circom @@ -0,0 +1,95 @@ +pragma circom 2.1.9; + +include "../../utils/bits.circom"; +include "hash_machine.circom"; + +template ParserHasher(DATA_BYTES, MAX_STACK_HEIGHT) { + signal input data[DATA_BYTES]; + signal input polynomial_input; + signal input sequence_digest; + + //--------------------------------------------------------------------------------------------// + // Initialze the parser + component State[DATA_BYTES]; + State[0] = StateUpdateHasher(MAX_STACK_HEIGHT); + for(var i = 0; i < MAX_STACK_HEIGHT; i++) { + State[0].stack[i] <== [0,0]; + State[0].tree_hash[i] <== [0,0]; + } + State[0].byte <== data[0]; + State[0].polynomial_input <== polynomial_input; + State[0].monomial <== 0; + State[0].parsing_string <== 0; + State[0].parsing_number <== 0; + + // Set up monomials for stack/tree digesting + signal monomials[4 * MAX_STACK_HEIGHT]; + monomials[0] <== 1; + for(var i = 1 ; i < 4 * MAX_STACK_HEIGHT ; i++) { + monomials[i] <== monomials[i - 1] * polynomial_input; + } + signal intermediate_digest[DATA_BYTES][4 * MAX_STACK_HEIGHT]; + signal state_digest[DATA_BYTES]; + + // Debugging + // for(var i = 0; i c.charCodeAt(0))); +} + +// Enum equivalent for JsonMaskType +export type JsonMaskType = + | { type: "Object", value: number[] } // Changed from Uint8Array to number[] + | { type: "ArrayIndex", value: number }; + +// Constants for the field arithmetic +const PRIME = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617"); +const ONE = BigInt(1); +const ZERO = BigInt(0); + +function modAdd(a: bigint, b: bigint): bigint { + return (a + b) % PRIME; +} + +function modMul(a: bigint, b: bigint): bigint { + return (a * b) % PRIME; +} + +export function jsonTreeHasher( + polynomialInput: bigint, + keySequence: JsonMaskType[], + targetValue: number[], // Changed from Uint8Array to number[] + maxStackHeight: number +): [Array<[bigint, bigint]>, Array<[bigint, bigint]>] { + if (keySequence.length >= maxStackHeight) { + throw new Error("Key sequence length exceeds max stack height"); + } + + const stack: Array<[bigint, bigint]> = []; + const treeHashes: Array<[bigint, bigint]> = []; + + for (const valType of keySequence) { + if (valType.type === "Object") { + stack.push([ONE, ONE]); + let stringHash = ZERO; + let monomial = ONE; + + for (const byte of valType.value) { + stringHash = modAdd(stringHash, modMul(monomial, BigInt(byte))); + monomial = modMul(monomial, polynomialInput); + } + treeHashes.push([stringHash, ZERO]); + } else { // ArrayIndex + treeHashes.push([ZERO, ZERO]); + stack.push([BigInt(2), BigInt(valType.value)]); + } + } + + let targetValueHash = ZERO; + let monomial = ONE; + + for (const byte of targetValue) { + targetValueHash = modAdd(targetValueHash, modMul(monomial, BigInt(byte))); + monomial = modMul(monomial, polynomialInput); + } + + treeHashes[keySequence.length - 1] = [treeHashes[keySequence.length - 1][0], targetValueHash]; + + return [stack, treeHashes]; +} + +export function compressTreeHash( + polynomialInput: bigint, + stackAndTreeHashes: [Array<[bigint, bigint]>, Array<[bigint, bigint]>] +): bigint { + const [stack, treeHashes] = stackAndTreeHashes; + + if (stack.length !== treeHashes.length) { + throw new Error("Stack and tree hashes must have the same length"); + } + + let accumulated = ZERO; + let monomial = ONE; + + for (let idx = 0; idx < stack.length; idx++) { + accumulated = modAdd(accumulated, modMul(stack[idx][0], monomial)); + monomial = modMul(monomial, polynomialInput); + + accumulated = modAdd(accumulated, modMul(stack[idx][1], monomial)); + monomial = modMul(monomial, polynomialInput); + + accumulated = modAdd(accumulated, modMul(treeHashes[idx][0], monomial)); + monomial = modMul(monomial, polynomialInput); + + accumulated = modAdd(accumulated, modMul(treeHashes[idx][1], monomial)); + monomial = modMul(monomial, polynomialInput); + } + + return accumulated; +} \ No newline at end of file diff --git a/circuits/test/json/parser/hash_parser.test.ts b/circuits/test/json/parser/hash_parser.test.ts new file mode 100644 index 0000000..71e899c --- /dev/null +++ b/circuits/test/json/parser/hash_parser.test.ts @@ -0,0 +1,162 @@ +import { poseidon2 } from "poseidon-lite"; +import { circomkit, WitnessTester, readJSONInputFile, strToBytes, JsonMaskType, jsonTreeHasher, compressTreeHash } from "../../common"; + +describe("Hash Parser", () => { + let hash_parser: WitnessTester<["data", "polynomial_input", "sequence_digest"]>; + + it(`input: array_only`, async () => { + let filename = "array_only"; + let [input, _keyUnicode, _output] = readJSONInputFile(`${filename}.json`, []); + const MAX_STACK_HEIGHT = 3; + + hash_parser = await circomkit.WitnessTester(`Parser`, { + file: "json/parser/hash_parser", + template: "ParserHasher", + params: [input.length, MAX_STACK_HEIGHT], + }); + console.log("#constraints:", await hash_parser.getConstraintCount()); + + // Test `42` in 0th slot + let polynomial_input = poseidon2([69, 420]); + let targetValue = strToBytes("42"); + let keySequence: JsonMaskType[] = [ + { type: "ArrayIndex", value: 0 }, + ]; + let [stack, treeHashes] = jsonTreeHasher(polynomial_input, keySequence, targetValue, MAX_STACK_HEIGHT); + let sequence_digest = compressTreeHash(polynomial_input, [stack, treeHashes]); + await hash_parser.expectPass({ + data: input, + polynomial_input, + sequence_digest, + }); + console.log("> First subtest passed."); + + // Test `"b"` in 1st slot object + polynomial_input = poseidon2([69, 420]); + targetValue = strToBytes("b"); + keySequence = [ + { type: "ArrayIndex", value: 1 }, + { type: "Object", value: strToBytes("a") }, + ]; + [stack, treeHashes] = jsonTreeHasher(polynomial_input, keySequence, targetValue, MAX_STACK_HEIGHT); + sequence_digest = compressTreeHash(polynomial_input, [stack, treeHashes]); + await hash_parser.expectPass({ + data: input, + polynomial_input, + sequence_digest, + }); + console.log("> Second subtest passed."); + }); + + it(`input: value_array`, async () => { + let filename = "value_array"; + let [input, _keyUnicode, _output] = readJSONInputFile(`${filename}.json`, []); + const MAX_STACK_HEIGHT = 3; + + hash_parser = await circomkit.WitnessTester(`Parser`, { + file: "json/parser/hash_parser", + template: "ParserHasher", + params: [input.length, MAX_STACK_HEIGHT], + }); + console.log("#constraints:", await hash_parser.getConstraintCount()); + + // Test `420` in "k"'s 0th slot + let polynomial_input = poseidon2([69, 420]); + let targetValue = strToBytes("420"); + let keySequence: JsonMaskType[] = [ + { type: "Object", value: strToBytes("k") }, + { type: "ArrayIndex", value: 0 }, + ]; + let [stack, treeHashes] = jsonTreeHasher(polynomial_input, keySequence, targetValue, MAX_STACK_HEIGHT); + let sequence_digest = compressTreeHash(polynomial_input, [stack, treeHashes]); + await hash_parser.expectPass({ + data: input, + polynomial_input, + sequence_digest, + }); + console.log("> First subtest passed."); + + // Test `"d"` in "b"'s 3rd slot + polynomial_input = poseidon2([69, 420]); + targetValue = strToBytes("d"); + keySequence = [ + { type: "Object", value: strToBytes("b") }, + { type: "ArrayIndex", value: 3 }, + ]; + [stack, treeHashes] = jsonTreeHasher(polynomial_input, keySequence, targetValue, MAX_STACK_HEIGHT); + sequence_digest = compressTreeHash(polynomial_input, [stack, treeHashes]); + await hash_parser.expectPass({ + data: input, + polynomial_input, + sequence_digest, + }); + console.log("> Second subtest passed."); + }); + + it(`input: value_array_object`, async () => { + let filename = "value_array_object"; + let [input, keyUnicode, output] = readJSONInputFile(`${filename}.json`, []); + hash_parser = await circomkit.WitnessTester(`Parser`, { + file: "json/parser/hash_parser", + template: "ParserHasher", + params: [input.length, 5], + }); + console.log("#constraints:", await hash_parser.getConstraintCount()); + + const polynomial_input = poseidon2([69, 420]); + const KEY0 = strToBytes("a"); + const KEY1 = strToBytes("b"); + const targetValue = strToBytes("4"); + + const keySequence: JsonMaskType[] = [ + { type: "Object", value: KEY0 }, + { type: "ArrayIndex", value: 0 }, + { type: "Object", value: KEY1 }, + { type: "ArrayIndex", value: 1 }, + ]; + + const [stack, treeHashes] = jsonTreeHasher(polynomial_input, keySequence, targetValue, 10); + const sequence_digest = compressTreeHash(polynomial_input, [stack, treeHashes]); + + await hash_parser.expectPass({ + data: input, + polynomial_input, + sequence_digest, + }); + }); + + it(`input: spotify`, async () => { + let filename = "spotify"; + let [input, keyUnicode, output] = readJSONInputFile(`${filename}.json`, []); + hash_parser = await circomkit.WitnessTester(`Parser`, { + file: "json/parser/hash_parser", + template: "ParserHasher", + params: [input.length, 5], + }); + console.log("#constraints:", await hash_parser.getConstraintCount()); + + const polynomial_input = poseidon2([69, 420]); + const KEY0 = strToBytes("data"); + const KEY1 = strToBytes("items"); + const KEY2 = strToBytes("profile"); + const KEY3 = strToBytes("name"); + const targetValue = strToBytes("Taylor Swift"); + + const keySequence: JsonMaskType[] = [ + { type: "Object", value: KEY0 }, + { type: "Object", value: KEY1 }, + { type: "ArrayIndex", value: 0 }, + { type: "Object", value: KEY2 }, + { type: "Object", value: KEY3 }, + ]; + + const [stack, treeHashes] = jsonTreeHasher(polynomial_input, keySequence, targetValue, 10); + const sequence_digest = compressTreeHash(polynomial_input, [stack, treeHashes]); + + await hash_parser.expectPass({ + data: input, + polynomial_input, + sequence_digest, + }); + }); +}) \ No newline at end of file diff --git a/examples/json/array_only.json b/examples/json/array_only.json new file mode 100644 index 0000000..a23b6b6 --- /dev/null +++ b/examples/json/array_only.json @@ -0,0 +1 @@ +[42,{"a":"b"},[0,1],"foobar"] \ No newline at end of file diff --git a/examples/json/response/reddit.json b/examples/json/reddit.json similarity index 100% rename from examples/json/response/reddit.json rename to examples/json/reddit.json diff --git a/examples/json/response/spotify.json b/examples/json/response/spotify.json deleted file mode 100644 index 6416884..0000000 --- a/examples/json/response/spotify.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "data": { - "me": { - "profile": { - "topArtists": { - "__typename": "ArtistPageV2", - "items": [ - { - "data": { - "__typename": "Artist", - "profile": { - "name": "Taylor Swift" - }, - "uri": "spotify:artist:06HL4z0CvFAxyc27GXpf02", - "visuals": { - "avatarImage": { - "sources": [ - { - "height": 640, - "url": "https://i.scdn.co/image/ab6761610000e5ebe672b5f553298dcdccb0e676", - "width": 640 - }, - { - "height": 160, - "url": "https://i.scdn.co/image/ab6761610000f178e672b5f553298dcdccb0e676", - "width": 160 - }, - { - "height": 320, - "url": "https://i.scdn.co/image/ab67616100005174e672b5f553298dcdccb0e676", - "width": 320 - } - ] - } - } - } - } - ], - "totalCount": 1 - } - } - } - }, - "extensions": {} -} \ No newline at end of file diff --git a/examples/json/spotify.json b/examples/json/spotify.json new file mode 100644 index 0000000..876df5e --- /dev/null +++ b/examples/json/spotify.json @@ -0,0 +1 @@ +{"data":{"items":[{"data":"Artist","profile":{"name":"Taylor Swift"}}]}} \ No newline at end of file diff --git a/examples/json/string_escape.json b/examples/json/string_escape.json new file mode 100644 index 0000000..d3853de --- /dev/null +++ b/examples/json/string_escape.json @@ -0,0 +1 @@ +{"a": "\"b\""} \ No newline at end of file diff --git a/examples/json/test/array_only.json b/examples/json/test/array_only.json deleted file mode 100644 index 23f1146..0000000 --- a/examples/json/test/array_only.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - 42, - { - "a": "b" - }, - [ - 0, - 1 - ], - "foobar" -] \ No newline at end of file diff --git a/examples/json/test/string_escape.json b/examples/json/test/string_escape.json deleted file mode 100644 index 8765251..0000000 --- a/examples/json/test/string_escape.json +++ /dev/null @@ -1 +0,0 @@ -{ "a": "\"b\"" } \ No newline at end of file diff --git a/examples/json/test/value_array.json b/examples/json/test/value_array.json deleted file mode 100644 index e0dc999..0000000 --- a/examples/json/test/value_array.json +++ /dev/null @@ -1 +0,0 @@ -{ "k" : [ 420 , 69 , 4200 , 600 ], "b": [ "ab" , "ba", "ccc", "d" ] } \ No newline at end of file diff --git a/examples/json/test/value_object.json b/examples/json/test/value_object.json deleted file mode 100644 index 8437b4d..0000000 --- a/examples/json/test/value_object.json +++ /dev/null @@ -1 +0,0 @@ -{ "a": { "d" : "e", "e": "c" }, "e": { "f": "a", "e": "2" }, "g": { "h": { "a": "c" }}, "ab": "foobar", "bc": 42, "dc": [ 0, 1, "a"] } \ No newline at end of file diff --git a/examples/json/value_array.json b/examples/json/value_array.json new file mode 100644 index 0000000..7713359 --- /dev/null +++ b/examples/json/value_array.json @@ -0,0 +1 @@ +{"k":[420,69,4200,600],"b":["ab","ba","ccc","d"]} \ No newline at end of file diff --git a/examples/json/test/value_array_object.json b/examples/json/value_array_object.json similarity index 100% rename from examples/json/test/value_array_object.json rename to examples/json/value_array_object.json diff --git a/examples/json/value_object.json b/examples/json/value_object.json new file mode 100644 index 0000000..10c330d --- /dev/null +++ b/examples/json/value_object.json @@ -0,0 +1 @@ +{"a":{"d":"e","e":"c"},"e":{"f":"a","e":"2"},"g":{"h":{"a":"c"}},"ab":"foobar","bc":42,"dc":[0,1,"a"]} \ No newline at end of file diff --git a/examples/json/response/venmo.json b/examples/json/venmo.json similarity index 100% rename from examples/json/response/venmo.json rename to examples/json/venmo.json