diff --git a/.gitignore b/.gitignore
index f62a47e40e..51daaa88e2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,9 @@ fastcrypto/Cargo.lock
 fastcrypto-derive/Cargo.lock
 
 # .DS_Store files
-.DS_Store
\ No newline at end of file
+.DS_Store
+
+# JS
+node_modules/
+
+fastcrypto/openid-zkp-auth/artifacts/
diff --git a/zklogin-circuits/README.md b/zklogin-circuits/README.md
new file mode 100644
index 0000000000..1dbfe86f11
--- /dev/null
+++ b/zklogin-circuits/README.md
@@ -0,0 +1,82 @@
+# zkLogin circuits
+
+Install via `tsc` and `npm install`
+
+## Filetree Description
+
+```bash
+circuits/
+    zklogin.circom # Main circuit code
+    zklogin_wrapper.circom # Circom runner
+    helpers/
+        base64.circom
+        hasher.circom
+        jwtchecks.circom
+        misc.circom
+        sha256.circom
+        strings.circom
+src/
+    circuitutils.ts # Circuit utilities
+    constants.ts # Circuit params
+    decideparams.ts # A script to decide circuit params based on real JWTs
+    jwtutils.ts # JWT utilities
+    prove.ts # Helper script to run the ZKP using a given zkey, vkey, JWT
+    utils.ts # Generic utilities
+    verify.ts
+test/
+    testutils.js # Test utilities
+    xyz.circom.test.js # testing circom code
+    abc.test.js # testing js code
+testvectors/
+    realJWTs.ts # Real JWTs
+    sampleWalletInputs.json
+    sampleZKPInputs.json
+```
+
+## Steps to generate a OpenID signature (using snarkJS prover)
+
+1. Create a folder named `artifacts` inside `openid-zkp-auth`.
+
+```
+cd fastcrypto/openid-zkp-auth && mkdir artifacts
+```
+
+2. Get pre-generated trusted setup and place it inside `artifacts`.
+
+```
+cd artifacts && wget https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_20.ptau
+```
+
+3. Generate R1CS and witness generator (WASM): 
+
+```
+cd ../ && circom circuits/zklogin.circom --r1cs --wasm --output artifacts
+
+# maybe need
+npm install
+```
+
+4. Run circuit-specific trusted setup: `cd artifacts && snarkjs groth16 setup zklogin.r1cs powersOfTau28_hez_final_20.ptau zklogin.zkey`
+
+5. Export verification key: `snarkjs zkey export verificationkey zklogin.zkey zklogin.vkey`
+
+6. Create a folder named `proof` inside the `artifacts` directory. Generate a OpenID signature: ``npm run prove <provider> <jwt>``. The last two arguments are optional. `provider` can be either `google` (default) or `twitch`. Default JWTs for both are in `testvectors/realJWTs.js`.
+
+It generates three files: the zk proof (`zkp.proof`), auxiliary inputs to the verifier (`aux.json`) and public inputs to the ZKP (`public.json`) inside the `proof` folder.
+
+## Tests
+
+``tsc; npm test``
+
+# Steps to access the back-end
+
+The back-end takes `WalletInputs` as input and returns `PartialZKLoginSig`. Both these structs are defined in `src/common.ts`.
+
+## Example via cURL
+
+```
+1. cd testvectors
+2. curl -X POST -H "Content-Type: application/json" -d @./sampleWalletInputs.json http://185.209.177.123:8000/zkp
+```
+
+Note that the command reads the `WalletInputs` from the JSON file `testvectors/sampleWalletInputs.json`.
diff --git a/zklogin-circuits/circuits/helpers/base64.circom b/zklogin-circuits/circuits/helpers/base64.circom
new file mode 100644
index 0000000000..51331c5aee
--- /dev/null
+++ b/zklogin-circuits/circuits/helpers/base64.circom
@@ -0,0 +1,144 @@
+pragma circom 2.1.3;
+
+include "misc.circom";
+
+function asciiToBase64Url(i) {
+    var base64 = 0;
+    if (i >= 65 && i <= 90) { // A to Z
+        base64 = i - 65;
+    } else if (i >= 97 && i <= 122) { // a to z
+        base64 = i - 71;
+    } else if (i >= 48 && i <= 57) { // 0 to 9
+        base64 = i + 4;
+    } else if (i == 45) { // -
+        base64 = 62;
+    } else if (i == 95) { // _
+        base64 = 63;
+    }
+    return base64;
+}
+
+/**
+Takes as input a base64url character and outputs the corresponding 6-bit value. 
+If not a valid base64 character, outputs 0.
+
+Cost: 73 constraints
+
+- in:   The base64url character. Assumed to be a 8-bit number, i.e., in [0, 256)
+        NOTE: Behavior is undefined otherwise.
+- out:  The 6-bit value
+*/
+template Base64URLToBits() {
+    signal input in;
+    signal output out[6];
+
+    // Compute the base64 representation of in, e.g., if in = 66 ('B') then outascii = 1
+    //  This step does not add any constraints. 
+    //  We check the correctness of the base64 representation next.
+    signal outascii;
+    outascii <-- asciiToBase64Url(in);
+
+    component lt91;
+    lt91 = LessThan(8);
+    lt91.in[0] <== in;
+    lt91.in[1] <== 91;
+
+    component gt64;
+    gt64 = GreaterThan(8);
+    gt64.in[0] <== in;
+    gt64.in[1] <== 64;
+
+    // If in is in [65, 90], then outascii = in - 65 (line 8 of asciiToBase64Url)
+    component forceequal1;
+    forceequal1 = MyForceEqualIfEnabled();
+    forceequal1.enabled <== lt91.out * gt64.out;
+    forceequal1.in[0] <== outascii;
+    forceequal1.in[1] <== in - 65;
+
+    component lt123;
+    lt123 = LessThan(8);
+    lt123.in[0] <== in;
+    lt123.in[1] <== 123;
+
+    component gt96;
+    gt96 = GreaterThan(8);
+    gt96.in[0] <== in;
+    gt96.in[1] <== 96;
+
+    // If in is in [97, 122], then outascii = in - 71 (line 10 of asciiToBase64Url)
+    component forceequal2;
+    forceequal2 = MyForceEqualIfEnabled();
+    forceequal2.enabled <== lt123.out * gt96.out;
+    forceequal2.in[0] <== outascii;
+    forceequal2.in[1] <== in - 71;
+
+    component lt58;
+    lt58 = LessThan(8);
+    lt58.in[0] <== in;
+    lt58.in[1] <== 58;
+
+    component gt47;
+    gt47 = GreaterThan(8);
+    gt47.in[0] <== in;
+    gt47.in[1] <== 47;
+
+    // If in is in [48, 57], then outascii = in + 4 (line 12 of asciiToBase64Url)
+    component forceequal3;
+    forceequal3 = MyForceEqualIfEnabled();
+    forceequal3.enabled <== lt58.out * gt47.out;
+    forceequal3.in[0] <== outascii;
+    forceequal3.in[1] <== in + 4;
+
+    component eq45;
+    eq45 = IsEqual();
+    eq45.in[0] <== in;
+    eq45.in[1] <== 45;
+
+    // If in is 45, then outascii = 62 (line 14 of asciiToBase64Url)
+    component forceequal4;
+    forceequal4 = MyForceEqualIfEnabled();
+    forceequal4.enabled <== eq45.out;
+    forceequal4.in[0] <== outascii;
+    forceequal4.in[1] <== 62;
+
+    component eq95;
+    eq95 = IsEqual();
+    eq95.in[0] <== in;
+    eq95.in[1] <== 95;
+
+    // If in is 95, then outascii = 63 (line 16 of asciiToBase64Url)
+    component forceequal5;
+    forceequal5 = MyForceEqualIfEnabled();
+    forceequal5.enabled <== eq95.out;
+    forceequal5.in[0] <== outascii;
+    forceequal5.in[1] <== 63;
+
+    // Note: any = 0 happens only if all the enabled signals are 0. 
+    //  This is because all the enabled signals are guaranteed to be either 0 or 1.
+    var any = 1 - (forceequal1.enabled + forceequal2.enabled + forceequal3.enabled + forceequal4.enabled + forceequal5.enabled);
+
+    // If in is not a valid base64url character, then outascii = 0
+    component forceequal6;
+    forceequal6 = MyForceEqualIfEnabled();
+    forceequal6.enabled <== any;
+    forceequal6.in[0] <== outascii;
+    forceequal6.in[1] <== 0;
+
+    component convert = Num2BitsBE(6);
+    convert.in <== outascii;
+    for (var i = 0; i < 6; i++) {
+        out[i] <== convert.out[i];
+    }
+}
+
+template MultiBase64URLToBits(n) {
+    signal input in[n];
+    signal output out[n * 6];
+
+    for (var i = 0; i < n; i++) {
+        var bits[6] = Base64URLToBits()(in[i]);
+        for (var j = 0; j < 6; j++) {
+            out[i * 6 + j] <== bits[j];
+        }
+    }
+}
\ No newline at end of file
diff --git a/zklogin-circuits/circuits/helpers/hasher.circom b/zklogin-circuits/circuits/helpers/hasher.circom
new file mode 100644
index 0000000000..0ca5cbeee5
--- /dev/null
+++ b/zklogin-circuits/circuits/helpers/hasher.circom
@@ -0,0 +1,33 @@
+pragma circom 2.1.3;
+
+include "../../node_modules/circomlib/circuits/poseidon.circom";
+
+template Hasher(nInputs) {
+    signal input in[nInputs];
+    signal output out;
+
+    component pos1, pos2;
+    // See https://github.com/iden3/circomlib/blob/master/circuits/poseidon.circom#L78. 
+    // It implicitly forces t <= 17 or nInputs <= 16. 
+    // TODO: The current limitation of 15 comes from the rust library we use (https://github.com/Lightprotocol/light-poseidon).
+    if (nInputs <= 15) {
+        out <== Poseidon(nInputs)(in);
+    } else if (nInputs <= 30) {
+        pos1 = Poseidon(15);
+        pos2 = Poseidon(nInputs - 15);
+
+        for (var i = 0; i < 15; i++) {
+            pos1.inputs[i] <== in[i];
+        }
+        for (var i = 15; i < nInputs; i++) {
+            pos2.inputs[i - 15] <== in[i];
+        }
+
+        out <== Poseidon(2)([
+            pos1.out,
+            pos2.out
+        ]);
+    } else { // Yet to be implemented
+        1 === 0;
+    }
+}
\ No newline at end of file
diff --git a/zklogin-circuits/circuits/helpers/jwtchecks.circom b/zklogin-circuits/circuits/helpers/jwtchecks.circom
new file mode 100644
index 0000000000..7b2dbc7fae
--- /dev/null
+++ b/zklogin-circuits/circuits/helpers/jwtchecks.circom
@@ -0,0 +1,151 @@
+pragma circom 2.1.3;
+
+include "strings.circom";
+include "misc.circom";
+include "hasher.circom";
+
+template StringChecker(maxLen) {
+    signal input str[maxLen];
+    signal input len;
+
+    // first char is a quote
+    str[0] === 34; // '"'
+
+    // last char is a quote
+    signal last_quote <== SingleMultiplexer(maxLen)(str, len - 1);
+    last_quote === 34; // '"'
+
+    // str[i] == 0 for all i >= len
+    signal sigt[maxLen] <== GTBitVector(maxLen)(len - 1);
+    for (var i = 0; i < maxLen; i++) {
+        sigt[i] * str[i] === 0;
+    }
+}
+
+template isWhitespace() {
+    signal input c;
+
+    signal is_space <== IsEqual()([c, 0x20]); // 0x20 = decimal 32
+    signal is_tab <== IsEqual()([c, 0x09]); // 0x09 = decimal 9
+    signal is_newline <== IsEqual()([c, 0x0A]); // 0x0A = decimal 10 
+    signal is_carriage_return <== IsEqual()([c, 0x0D]); // 0x0D = decimal 13
+
+    signal output is_whitespace <== is_space + is_tab + is_newline + is_carriage_return;
+}
+
+/**
+ExtendedClaimParser
+
+string ws : ws value ws E
+     |    |    |   |    |
+     n    c    vs  ve   l => n = name_len, c = ':', vs = value_start, ve = value_end, l = length
+**/
+template ExtendedClaimParser(maxExtendedClaimLen, maxKeyClaimNameLen, maxKeyClaimValueLen) {
+    assert(maxExtendedClaimLen > 0);
+    assert(maxKeyClaimNameLen > 0);
+    assert(maxKeyClaimValueLen > 0);
+    assert(maxExtendedClaimLen == maxKeyClaimNameLen + maxKeyClaimValueLen + 2); // +2 for colon and comma/brace
+
+    // TODO: Add range checks for all inputs
+    signal input extended_claim[maxExtendedClaimLen];
+    signal input length;
+
+    signal input name_len;
+    signal input colon_index;
+    signal input value_start;
+    signal input value_len;
+
+    signal output name[maxKeyClaimNameLen] <== Slice(maxExtendedClaimLen, maxKeyClaimNameLen)(
+        extended_claim,
+        0,
+        name_len
+    );
+    // Is name a valid JSON string? (All JSON keys are strings)
+    StringChecker(maxKeyClaimNameLen)(name, name_len); 
+
+    signal output value[maxKeyClaimValueLen] <== Slice(maxExtendedClaimLen, maxKeyClaimValueLen)(
+        extended_claim,
+        value_start,
+        value_len
+    );
+    // Is value a valid JSON string?
+    StringChecker(maxKeyClaimValueLen)(value, value_len);
+    // NOTE: In theory, JSON values need not be strings. But we only use this parser for parsing strings, so this is fine.
+
+    // Whitespaces
+    signal is_whitespace[maxExtendedClaimLen];
+    for (var i = 0; i < maxExtendedClaimLen; i++) {
+        is_whitespace[i] <== isWhitespace()(extended_claim[i]);
+    }
+
+    signal is_gt_n[maxExtendedClaimLen] <== GTBitVector(maxExtendedClaimLen)(name_len - 1);
+    signal is_lt_c[maxExtendedClaimLen] <== LTBitVector(maxExtendedClaimLen)(colon_index);
+    signal selector1[maxExtendedClaimLen] <== vectorAND(maxExtendedClaimLen)(is_gt_n, is_lt_c);
+    for (var i = 0; i < maxExtendedClaimLen; i++) {
+        selector1[i] * (1 - is_whitespace[i]) === 0;
+    }
+
+    signal is_gt_c[maxExtendedClaimLen] <== GTBitVector(maxExtendedClaimLen)(colon_index);
+    signal is_lt_vs[maxExtendedClaimLen] <== LTBitVector(maxExtendedClaimLen)(value_start);
+    signal selector2[maxExtendedClaimLen] <== vectorAND(maxExtendedClaimLen)(is_gt_c, is_lt_vs);
+    for (var i = 0; i < maxExtendedClaimLen; i++) {
+        selector2[i] * (1 - is_whitespace[i]) === 0;
+    }
+
+    signal is_gt_ve[maxExtendedClaimLen] <== GTBitVector(maxExtendedClaimLen)(value_start + value_len - 1);
+    signal is_lt_l[maxExtendedClaimLen] <== LTBitVector(maxExtendedClaimLen)(length - 1);
+    signal selector3[maxExtendedClaimLen] <== vectorAND(maxExtendedClaimLen)(is_gt_ve, is_lt_l);
+    for (var i = 0; i < maxExtendedClaimLen; i++) {
+        selector3[i] * (1 - is_whitespace[i]) === 0;
+    }
+
+    // Colon is at index colon_index
+    signal colon <== SingleMultiplexer(maxExtendedClaimLen)(extended_claim, colon_index);
+    colon === 58; // ':'
+
+    // Last char is either end-brace or comma
+    signal last_char <== SingleMultiplexer(maxExtendedClaimLen)(extended_claim, length - 1);
+    (last_char - 125) * (last_char - 44) === 0; // '}' or ','
+}
+
+/**
+NonceChecker: Checks that the extended claim string representing nonce is valid.
+    a) The first 9 chars are '"nonce":"'
+    b) The last two chars are '"}' or '",'
+    c) The chars in between are base64url encoded bits of the nonce
+
+Construction Params:
+    nonceValueLength: length of the extended claim string representing nonce
+    nonceBitLen: length of the nonce in bits
+**/
+template NonceChecker(nonceValueLength, nonceBitLen) {
+    assert(nonceValueLength > 0);
+    assert(nonceBitLen > 0);
+
+    signal input expected_nonce;
+    signal expected_bits[nonceBitLen] <== Num2BitsBE(nonceBitLen)(expected_nonce);
+
+    signal input actual_nonce[nonceValueLength];
+
+    // first char is a quote
+    actual_nonce[0] === 34; // '"'
+
+    // last char is a quote
+    actual_nonce[nonceValueLength - 1] === 34; // '"'
+
+    // Remove the 9 character prefix and two character suffix to get the actual nonce
+    var nonceLength = nonceValueLength - 2;
+    var value[nonceLength];
+    for (var i = 0; i < nonceLength; i++) {
+        value[i] = actual_nonce[i + 1];
+    }
+
+    // Convert the base64url encoded nonce to bits
+    signal actual_bits[6 * nonceLength] <== MultiBase64URLToBits(nonceLength)(value);
+
+    // Check every bit of expected nonce against the actual nonce
+    assert(6 * nonceLength >= nonceBitLen);
+    for (var i = 0; i < nonceBitLen; i++) {
+        expected_bits[i] === actual_bits[i];
+    }
+}
diff --git a/zklogin-circuits/circuits/helpers/misc.circom b/zklogin-circuits/circuits/helpers/misc.circom
new file mode 100644
index 0000000000..2aca8be011
--- /dev/null
+++ b/zklogin-circuits/circuits/helpers/misc.circom
@@ -0,0 +1,338 @@
+pragma circom 2.1.3;
+
+include "../../node_modules/circomlib/circuits/comparators.circom";
+include "../../node_modules/circomlib/circuits/gates.circom";
+include "../../node_modules/circomlib/circuits/mux2.circom";
+include "../../node_modules/circomlib/circuits/multiplexer.circom";
+
+include "hasher.circom";
+
+/**
+DivideMod2Power: Returns quotient (in / 2^p) and remainder (in % 2^p)
+
+Range checks:
+    - 0 <= in < 2^n (Checked in Num2Bits)
+    - 0 < p < n
+**/
+template DivideMod2Power(n, p) {
+    assert(n <= 252); // n <= log(p) - 2
+    assert(p < n);
+    assert(p > 0);
+
+    signal input in;
+    component toBits = Num2Bits(n);
+    toBits.in <== in;
+
+    component fromBitsQ = Bits2Num(n - p);
+    for (var i = 0; i < n - p; i++) {
+        fromBitsQ.in[i] <== toBits.out[i + p];
+    }
+    signal output quotient <== fromBitsQ.out;
+
+    component fromBitsR = Bits2Num(p);
+    for (var i = 0; i < p; i++) {
+        fromBitsR.in[i] <== toBits.out[i];
+    }
+    signal output remainder <== fromBitsR.out;
+}
+
+/**
+RemainderMod4: Calculates in % 4.
+
+Construction Params:
+    - n:  The bitwidth of in. 
+                
+Range checks:
+    - 0 <= in < 2^n (Checked in Num2Bits)
+**/
+template RemainderMod4(n) {
+    assert(n <= 252); // n <= log(p) - 2
+
+    signal input in;
+    signal output out;
+
+    component toBits = Num2Bits(n);
+    toBits.in <== in;
+    out <== 2 * toBits.out[1] + toBits.out[0];
+}
+
+/**
+RangeCheck: Checks if 0 <= in <= max.
+
+Construction params:
+    - n: The bitwidth of in and max.
+    - max: The maximum value that in can take.
+
+Range checks:
+    - 0 <= in (Checked in Num2Bits)
+    - in <= max
+**/
+template RangeCheck(n, max) {
+    assert(n <= 252); // n <= log(p) - 2
+    assert(max >= 0);
+    assert(numBits(max) <= n);
+
+    signal input in;
+    var unusedVar[n] = Num2Bits(n)(in);
+
+    signal leq <== LessEqThan(n)([in, max]);
+    leq === 1;
+}
+
+// Returns the number of bits needed to represent a number.
+// Helper function intended to operate only over construction params.
+function numBits(a) {
+    assert(a >= 0);
+    if (a == 0 || a == 1) {
+        return 1;
+    }
+    return 1 + numBits(a >> 1);
+}
+
+/**
+Num2BitsBE: Converts a number to a list of bits, in big-endian order.
+
+Range checks:
+    - 0 <= in < 2^n (Like with Num2Bits).
+**/
+template Num2BitsBE(n) {
+    signal input in;
+    signal output out[n];
+    var lc1 = 0;
+
+    var e2 = 1;
+    for (var i = 0; i < n; i++) {
+        var b = (n - 1) - i;
+        out[b] <-- (in >> i) & 1;
+        out[b] * (out[b] - 1 ) === 0;
+        lc1 += out[b] * e2;
+        e2 = e2 + e2;
+    }
+
+    lc1 === in;
+}
+
+/**
+Bits2NumBE: Converts a list of bits to a number, in big-endian order.
+
+Note: It is assumed that each input bit is either 0 or 1.
+**/
+template Bits2NumBE(n) {
+    signal input in[n];
+    signal output out;
+    var lc1=0;
+
+    var e2 = 1;
+    for (var i = 0; i < n; i++) {
+        lc1 += in[(n - 1) - i] * e2;
+        e2 = e2 + e2;
+    }
+
+    lc1 ==> out;
+}
+
+/**
+Segments2NumBE: Converts a list of w-bit segments to a number, in big-endian order.
+
+Note: It is assumed that each input segment is a number between 0 and 2^w - 1.
+**/
+template Segments2NumBE(n, w) {
+    assert(n * w <= 253); 
+
+    signal input in[n];
+    signal output out;
+    var lc1=0;
+
+    var e2 = 1;
+    for (var i = 0; i < n; i++) {
+        lc1 += in[(n - 1) - i] * e2;
+        e2 = e2 * (1 << w);
+    }
+
+    lc1 ==> out;
+}
+
+/**
+MyForceEqualIfEnabled: Optimized version of the official ForceEqualIfEnabled 
+
+If enabled is 1, then in[0] == in[1]. Otherwise, in[0] and in[1] can be anything.
+
+Original: https://github.com/iden3/circomlib/blob/master/circuits/comparators.circom#L48
+**/
+template MyForceEqualIfEnabled() {
+    signal input enabled;
+    signal input in[2];
+
+    (in[1] - in[0]) * enabled === 0;
+}
+
+/**
+Sum: Calculates the sum of all the input elements.
+**/
+template Sum(n) {
+    signal input nums[n];
+    signal output sum;
+
+    var lc;
+    for (var i = 0; i < n; i++) {
+        lc += nums[i];
+    }
+    sum <== lc;
+}
+
+/**
+OneBitVector: Given an index, returns a vector of size n with a 1 in the index-th position.
+
+out[i] = 1 if i = index, 0 otherwise.
+
+Range checks:
+    - index in [0, n). Fails otherwise.
+**/
+template OneBitVector(n) {
+    signal input index;
+    signal output out[n];
+
+    component X = Decoder(n);
+    X.inp <== index;
+
+    X.success === 1;
+    out <== X.out;
+}
+
+/**
+GTBitVector: Given an index, returns a vector of size n with a 1 in all indices greater than index.
+
+out[i] = 1 if i > index, 0 otherwise
+
+Range checks:
+    - index in [0, n). Fails otherwise.
+**/
+template GTBitVector(n) {
+    signal input index;
+    signal output out[n];
+
+    signal eq[n] <== OneBitVector(n)(index);
+
+    out[0] <== 0;
+    for (var i = 1; i < n; i++) {
+        out[i] <== eq[i - 1] + out[i - 1];
+    }
+}
+
+/**
+LTBitVector: Given an index, returns a vector of size n with a 1 in all indices less than index.
+
+out[i] = 1 if i < index, 0 otherwise
+
+Range checks:
+    - index in (0, n]. Fails otherwise.
+**/
+template LTBitVector(n) {
+    signal input index;
+    signal output out[n];
+
+    signal eq[n] <== OneBitVector(n)(index - 1);
+
+    out[n-1] <== eq[n-1];
+    for (var i = n-2; i >= 0; i--) {
+        out[i] <== eq[i] + out[i + 1];
+    }
+}
+
+/**
+SingleMultiplexer: Given a list of inputs, selects one of them based on the value of sel.
+
+More precisely, out = inp[sel].
+
+Range checks:
+    - 0 <= sel < nIn (Checked in OneBitVector)
+**/
+template SingleMultiplexer(nIn) {
+    signal input inp[nIn];
+    signal input sel;
+    signal output out;
+
+    component dec = OneBitVector(nIn);
+    sel ==> dec.index;
+    EscalarProduct(nIn)(inp, dec.out) ==> out;
+}
+
+// Returns the number of output chunks (each of size outWidth) needed to represent inBits.
+function getBaseConvertedOutputSize(inBits, outWidth) {
+    var outCount = inBits \ outWidth;
+    if (inBits % outWidth != 0) {
+        outCount++;
+    }
+    return outCount;
+}
+
+/**
+ConvertBase: Given a list of numbers, each of a specified bitwidth, 
+        converts it into another list of numbers with a different bitwidth.
+
+- inWidth: The bitwidth of each input number.
+- inCount: The number of input numbers.
+- outWidth: The bitwidth of each output number.
+- outCount: The number of output numbers. (Should be inCount * inWidth / outWidth, rounded up.)
+*/
+template ConvertBase(inWidth, inCount, outWidth, outCount) {
+    signal input in[inCount];
+    signal output out[outCount];
+
+    var inBits = inCount * inWidth;
+    var myOutCount = getBaseConvertedOutputSize(inBits, outWidth);
+    assert(myOutCount == outCount);
+
+    component expander[inCount];
+    for (var i = 0; i < inCount; i++) {
+        expander[i] = Num2BitsBE(inWidth);
+        expander[i].in <== in[i];
+    }
+
+    component compressor[outCount];
+    for (var i = 0; i < outCount; i++) {
+        compressor[i] = Bits2NumBE(outWidth);
+    }
+
+    for(var i = 0; i < inBits; i++) {
+        var oB = i % outWidth;
+        var o = (i - oB) \ outWidth;
+
+        var mB = i % inWidth;
+        var m = (i - mB) \ inWidth;
+
+        compressor[o].in[oB] <== expander[m].out[mB];
+    }
+
+    if (inBits % outWidth != 0) {
+        var outExtra = inBits % outWidth;
+        for(var i = outExtra; i < outWidth; i++) {
+            compressor[outCount - 1].in[i] <== 0;
+        }
+    }
+
+    for(var i = 0; i < outCount; i++) {
+        out[i] <== compressor[i].out;
+    }
+}
+
+template MapToField(maxLen) {
+    var inWidth = 8;
+    var packWidth = 248; // smallest multiple of 8 less than 256
+    signal input str[maxLen];
+
+    var outCount = getBaseConvertedOutputSize(maxLen * inWidth, packWidth);
+    signal packed[outCount] <== ConvertBase(inWidth, maxLen, packWidth, outCount)(str);
+    signal output str_F <== Hasher(outCount)(packed);
+}
+
+template vectorAND(n) {
+    signal input a[n];
+    signal input b[n];
+
+    signal output out[n];
+
+    for (var i = 0; i < n; i++) {
+        out[i] <== a[i] * b[i];
+    }
+}
\ No newline at end of file
diff --git a/zklogin-circuits/circuits/helpers/sha256.circom b/zklogin-circuits/circuits/helpers/sha256.circom
new file mode 100644
index 0000000000..217a9caadc
--- /dev/null
+++ b/zklogin-circuits/circuits/helpers/sha256.circom
@@ -0,0 +1,178 @@
+pragma circom 2.1.3;
+
+include "../../node_modules/circomlib/circuits/sha256/constants.circom";
+include "../../node_modules/circomlib/circuits/sha256/sha256compression.circom";
+include "../../node_modules/circomlib/circuits/comparators.circom";
+include "misc.circom";
+
+/**
+SHA256 Unsafe
+    Calculates the SHA256 hash of the input, using a signal to select the output round corresponding to the number of
+    non-empty input blocks. This implementation is referred to as "unsafe", as it relies upon the caller to ensure that
+    the input is padded correctly, and to ensure that the num_sha2_blocks input corresponds to the actual terminating data block.
+    Crafted inputs could result in Length Extension Attacks.
+
+    Construction Parameters:
+    - nBlocks: Maximum number of 512-bit blocks for payload input
+    
+    Inputs:
+    - in:           An array of blocks exactly nBlocks in length, each block containing an array of exactly 512 bits.
+                    Padding of the input according to RFC4634 Section 4.1 is left to the caller.
+    - num_sha2_blocks:   A number representing the number of 64-byte blocks to consider from the input.
+    
+    Outputs:
+    - out:          An array of 256 bits corresponding to the SHA256 output. 
+                    We hash the blocks starting from in[0] upto in[num_sha2_blocks-1] (inclusive).
+*/
+template Sha256_unsafe(nBlocks) {
+    signal input in[nBlocks][512];
+    signal input num_sha2_blocks;
+    
+    signal output out[256];
+
+    component ha0 = H(0);
+    component hb0 = H(1);
+    component hc0 = H(2);
+    component hd0 = H(3);
+    component he0 = H(4);
+    component hf0 = H(5);
+    component hg0 = H(6);
+    component hh0 = H(7);
+
+    component sha256compression[nBlocks];
+
+    for(var i = 0; i < nBlocks; i++) {
+        sha256compression[i] = Sha256compression();
+        if (i==0) {
+            for(var k = 0; k < 32; k++) {
+                sha256compression[i].hin[0*32+k] <== ha0.out[k];
+                sha256compression[i].hin[1*32+k] <== hb0.out[k];
+                sha256compression[i].hin[2*32+k] <== hc0.out[k];
+                sha256compression[i].hin[3*32+k] <== hd0.out[k];
+                sha256compression[i].hin[4*32+k] <== he0.out[k];
+                sha256compression[i].hin[5*32+k] <== hf0.out[k];
+                sha256compression[i].hin[6*32+k] <== hg0.out[k];
+                sha256compression[i].hin[7*32+k] <== hh0.out[k];
+            }
+        } else {
+            for(var k = 0; k < 32; k++) {
+                sha256compression[i].hin[32*0+k] <== sha256compression[i-1].out[32*0+31-k];
+                sha256compression[i].hin[32*1+k] <== sha256compression[i-1].out[32*1+31-k];
+                sha256compression[i].hin[32*2+k] <== sha256compression[i-1].out[32*2+31-k];
+                sha256compression[i].hin[32*3+k] <== sha256compression[i-1].out[32*3+31-k];
+                sha256compression[i].hin[32*4+k] <== sha256compression[i-1].out[32*4+31-k];
+                sha256compression[i].hin[32*5+k] <== sha256compression[i-1].out[32*5+31-k];
+                sha256compression[i].hin[32*6+k] <== sha256compression[i-1].out[32*6+31-k];
+                sha256compression[i].hin[32*7+k] <== sha256compression[i-1].out[32*7+31-k];
+            }
+        }
+
+        for (var k = 0; k < 512; k++) {
+            sha256compression[i].inp[k] <== in[i][k];
+        }
+    }
+    
+    // Collapse the hashing result at the terminating data block
+    component totals[256];
+    signal eqs[nBlocks] <== OneBitVector(nBlocks)(num_sha2_blocks - 1);
+
+    // For each bit of the output
+    for(var k = 0; k < 256; k++) {
+        totals[k] = Sum(nBlocks);
+
+        // For each possible block
+        for (var i = 0; i < nBlocks; i++) {
+            // eqs[i].out is 1 if the index matches. As such, at most one input to totals is not 0.
+            // The bit corresponding to the terminating data block will be raised
+            totals[k].nums[i] <== eqs[i] * sha256compression[i].out[k];
+        }
+        out[k] <== totals[k].sum;
+    }
+}
+
+/**
+SHA2_Wrapper
+
+    Calculates the SHA2 hash of an arbitrarily shaped input using SHA256_unsafe internally.
+    Additionally, it packs the output and checks that all inputs after num_sha2_blocks are indeed 0.
+
+    Construction Parameters:
+    - inWidth:      Width of each input segment in bits
+    - inCount:      Number of input segments
+
+    Inputs:
+    - in:           An array of segments exactly inCount in length, each segment containing an array of exactly inWidth bits.
+                    Padding of the input according to RFC4634 Section 4.1 is left to the caller.
+    - num_sha2_blocks:    An integer corresponding to the terminating block of the input, which contains the message padding.
+
+    Outputs:
+    - hash:         An array of size 2 corresponding to the SHA256 output as of the terminating block split into two.
+                    The first element contains the first 128 bits of the hash, and the second element contains the last 128 bits.
+*/
+template Sha2_wrapper(inWidth, inCount, outWidth, outCount) {
+    // Segments must divide evenly into 512 bit blocks
+    var inBits = inCount * inWidth;
+    assert(inBits % 512 == 0);
+
+    signal input in[inCount];
+    signal input num_sha2_blocks;
+
+    assert(outWidth * outCount == 256);
+    signal output hash[outCount];
+
+    // The content is decomposed to 512-bit blocks for SHA-256
+    var nBlocks = (inWidth * inCount) / 512;
+    component sha256 = Sha256_unsafe(nBlocks);
+
+    // How many segments are in each block
+    assert(inWidth <= 512);
+    assert(512 % inWidth == 0);
+    var nSegments = 512 / inWidth;
+    component sha256_blocks[nBlocks][nSegments];
+
+    // For each 512-bit block going into SHA-256
+    for (var b = 0; b < nBlocks; b++) {
+        // For each segment going into that block
+        for (var s = 0; s < nSegments; s++) {
+            // The index from the content is offset by the block we're composing times the number of segments per block,
+            // s is then the segment offset within the block.
+            var payloadIndex = (b * nSegments) + s;
+            
+            // Decompose each segment into an array of individual bits
+            sha256_blocks[b][s] = Num2BitsBE(inWidth);
+            sha256_blocks[b][s].in <== in[payloadIndex];
+            
+            // The bit index going into the current SHA-256 block is offset by the segment number times the bit width
+            // of each content segment. sOffset + i is then the bit offset within the block (0-511).
+            var sOffset = s * inWidth;
+            for (var i = 0; i < inWidth; i++) {
+                sha256.in[b][sOffset + i] <== sha256_blocks[b][s].out[i];
+            }
+        }
+    }
+    sha256.num_sha2_blocks <== num_sha2_blocks;
+
+    /**
+        Pack the output of the SHA-256 hash into a vector of size outCount where each element has outWidth bits.
+    **/
+    component hash_packer[outCount];
+    for (var i = 0; i < outCount; i++) {
+        hash_packer[i] = Bits2NumBE(outWidth);
+        for (var j = 0; j < outWidth; j++) {
+            hash_packer[i].in[j] <== sha256.out[i * outWidth + j];
+        }
+        hash_packer[i].out ==> hash[i];
+    }
+
+    /**
+        Verify that content[i] for all blocks >= num_sha2_blocks is zero.
+    **/
+    signal gte[nBlocks] <== GTBitVector(nBlocks)(num_sha2_blocks - 1);
+
+    for (var b = 0; b < nBlocks; b++) {
+        for (var s = 0; s < nSegments; s++) {
+            var payloadIndex = (b * nSegments) + s;
+            gte[b] * in[payloadIndex] === 0;
+        }
+    }
+}
\ No newline at end of file
diff --git a/zklogin-circuits/circuits/helpers/strings.circom b/zklogin-circuits/circuits/helpers/strings.circom
new file mode 100644
index 0000000000..c4e34fcb36
--- /dev/null
+++ b/zklogin-circuits/circuits/helpers/strings.circom
@@ -0,0 +1,299 @@
+pragma circom 2.1.3;
+
+include "../../node_modules/circomlib/circuits/comparators.circom";
+include "../../node_modules/circomlib/circuits/multiplexer.circom";
+include "../../node_modules/circomlib/circuits/gates.circom";
+include "misc.circom";
+include "base64.circom";
+
+/**
+SliceFixed: Returns a fixed-length slice of an array.
+More precisely, in[index:index+outLen] (both inclusive).
+
+Cost: inLen + outLen * inLen
+
+Range checks:
+    - index in [0, inLen)
+    - index + outLen in [0, inLen]
+    - outLen > 0
+**/
+template SliceFixed(inLen, outLen) {
+    assert(outLen > 0);
+
+    signal input in[inLen];
+    signal input index;
+    signal output out[outLen];
+
+    RangeCheck(numBits(inLen), inLen - 1)(index); // index in [0, inLen - 1]
+    RangeCheck(numBits(inLen), inLen)(index + outLen); // index + outLen in [0, inLen]
+
+    // eqs[index] = 1, 0 otherwise
+    signal eqs[inLen] <== OneBitVector(inLen)(index);
+    for(var i = 0; i < outLen; i++) {
+        // arr[i + index] = 1 (if i + index < inLen), 0 otherwise
+        var arr[inLen];
+        for (var j = 0; j < inLen; j++) {
+            if (j < i) {
+                arr[j] = 0;
+            } else {
+                arr[j] = eqs[j - i];
+            }
+        }
+        out[i] <== EscalarProduct(inLen)(arr, in);
+    }
+}
+
+/**
+Slice: Returns a variable-length slice of an array.
+More precisely, in[index:index+length] + [0] * (outLen - length).
+
+Cost: Roughly (inLen + outLen + outLen * inLen)
+
+Range checks:
+    - index in [0, inLen)
+    - length in (0, outLen]
+    - index + length in [0, inLen]
+    - outLen > 0
+**/
+template Slice(inLen, outLen) {
+    assert(outLen > 0);
+
+    signal input in[inLen];
+    signal input index;
+    signal input length;
+
+    RangeCheck(numBits(inLen), inLen - 1)(index); // index in [0, inLen - 1]
+    RangeCheck(numBits(outLen), outLen - 1)(length - 1); // length in [1, outLen]
+    RangeCheck(numBits(inLen), inLen)(index + length); // index + length in [0, inLen]
+
+    signal output out[outLen];
+
+    // eqs[i] = 1 if i = index, 0 otherwise
+    signal eqs[inLen] <== OneBitVector(inLen)(index);
+    // lt[i] = 1 if i < length, 0 otherwise
+    signal lts[outLen] <== LTBitVector(outLen)(length);
+
+    signal tmp[outLen];
+    for(var i = 0; i < outLen; i++) {
+        var arr[inLen];
+        for (var j = 0; j < inLen; j++) {
+            if (j < i) {
+                arr[j] = 0;
+            } else {
+                arr[j] = eqs[j - i];
+            }
+        }
+        tmp[i] <== EscalarProduct(inLen)(arr, in);
+        out[i] <== tmp[i] * lts[i];
+    }
+}
+
+// Return -1 if x is not a power of 2. Else, return log2(x)
+function logBase2(x) {
+    var res = 0;
+    while (x > 1) {
+        if (x % 2 == 1) {
+            return -1;
+        }
+        x = x >> 1;
+        res = res + 1;
+    }
+    return res;
+}
+
+function ceil(n, q) {
+    if (n % q == 0) {
+        return n \ q;
+    } else {
+        return (n \ q) + 1;
+    }
+}
+
+/**
+SliceGrouped: Same functionality as Slice but more efficient!
+Works by grouping the inputs and then slicing the grouped array.
+
+Cost: Roughly inLen / g + outLen / g + (outLen * inLen / 2 * g) where g = numsPerGroup. 
+TODO: Add multiplexer, ConvertBase costs.
+
+NOTE: Unlike Slice, this circuit might return extra characters at the end of the array.
+In particular, if (index + length - 1) % g != 1 and outLen is big enough, then upto g-1 more characters 
+than expected might be returned.
+
+Range checks:
+    - index in [0, inLen)
+    - length in (0, outLen]
+    - index + length in [0, inLen]
+    - numsPerGroup is a power of 2 (checked at compile time)
+**/
+template SliceGrouped(inWidth, numsPerGroup, inLen, outLen) {
+    assert(inLen % numsPerGroup == 0);
+
+    signal input in[inLen];
+    signal input index;
+    signal input length;
+
+    RangeCheck(numBits(inLen), inLen - 1)(index); // index in [0, inLen - 1]
+    RangeCheck(numBits(outLen), outLen - 1)(length - 1); // length in [1, outLen]
+    RangeCheck(numBits(inLen), inLen)(index + length); // index + length in [0, inLen]
+
+    var groupedInWidth = numsPerGroup * inWidth;
+    assert(groupedInWidth < 253);
+
+    var groupedInLen = inLen \ numsPerGroup;
+
+    signal inGrouped[groupedInLen];
+    for (var i = 0; i < groupedInLen; i++) {
+        var arr[numsPerGroup];
+        for (var j = 0; j < numsPerGroup; j++) {
+            arr[j] = in[i * numsPerGroup + j];
+        }
+        inGrouped[i] <== Segments2NumBE(numsPerGroup, inWidth)(arr);
+    }
+
+    signal startIdxByP, startIdxModP, endIdxByP, _endIdxModP; // P = numsPerGroup
+
+    var L = logBase2(numsPerGroup);
+    assert(L != -1); // This requirement comes from DivideMod2Power
+    var maxNumBits = numBits(inLen);
+    (startIdxByP, startIdxModP) <== DivideMod2Power(maxNumBits, L)(index);
+    (endIdxByP, _endIdxModP) <== DivideMod2Power(maxNumBits, L)(index + length - 1);
+
+    /** 
+        Q) Given a list of elements chunked into groups of size g and length of the sublist n, 
+            return the maximum number of groups that the sublist can span.
+        A) If the sublist starts at the beginning of a group, then the number of groups is ceil(n / g).
+        In the worst case, the sublist starts at the end of a group. In this case, the first element
+            of the sublist is the last element of the group. Thus, the number of groups is 
+            ceil((n-1) / g) + 1.
+    **/
+    var groupedOutLen = 1 + ceil(outLen - 1, numsPerGroup); // See explanation above
+    signal outGrouped[groupedOutLen] <== Slice(groupedInLen, groupedOutLen)(
+        inGrouped, startIdxByP, endIdxByP - startIdxByP + 1
+    );
+
+    var X = numsPerGroup * groupedOutLen;
+    signal outFinal[X] <== ConvertBase(groupedInWidth, groupedOutLen, inWidth, X)(outGrouped);
+
+    // Assertion only for illustrative purposes. It is always true if groupedOutLen is set as above.
+    assert((outLen - 1) + (numsPerGroup - 1) <= X - 1); 
+
+    signal outOptions[numsPerGroup][outLen];
+    for (var i = 0; i < outLen; i++) {
+        for (var j = 0; j < numsPerGroup; j++) {
+            outOptions[j][i] <== outFinal[i + j];
+        }
+    }
+    signal output out[outLen] <== Multiplexer(outLen, numsPerGroup)(outOptions, startIdxModP);
+
+    // e.g., if numsPerGroup == 16, we could do the following (which is slightly more efficient):
+    // signal outOptions[outLen][16];
+    // for (var i = 0; i < outLen; i++) {
+    //     for (var j = 0; j < 16; j++) {
+    //         outOptions[i][j] <== outFinal[i + j];
+    //     }
+    // }
+    // signal selector[4] <== Num2Bits(4)(startIdxModP);
+    // signal output out[outLen] <== MultiMux4(outLen)(outOptions, selector);
+}
+
+/**
+Checks if an ASCII-encoded substring exists in a Base64-encoded string.
+
+Cost: Slice is the costliest since b64StrLen is big in practice.
+
+Construction Parameters:
+    b64StrLen:              The length of the Base64-encoded string
+    maxA:                   The maximum length of the ASCII-encoded substring
+                            (must be a multiple of 3)
+
+Input:
+    b64Str[b64StrLen]:      The Base64-encoded string to search in
+    lenB:                   The length of the Base64-encoded substring to check
+    BIndex:                 The index of the first character of the
+                            Base64-encoded substring to check. For the check to 
+                            work, it should represent just the part of b64Str that 
+                            contains A.
+    A[maxA]:                The ASCII-encoded substring to search for padded with 0s
+                            e.g., A = ,"sub":"12345",0000 and lenA = 15
+    lenA:                   The length of the ASCII-encoded substring
+    payloadIndex:           The index of the first character of the payload
+
+Output:
+    The function checks if the ASCII-encoded substring exists in the
+    Base64-encoded string with an offset of 0, 1, or 2.
+
+Range checks:
+    0 < lenB <= maxB (checked in Slice)
+    0 <= BIndex < b64StrLen (checked in Slice)
+    0 <= BIndex + lenB <= b64StrLen (checked in Slice)
+    maxB <= b64StrLen (checked in Slice)
+    0 < lenA <= maxA (checked in LTBitVector)
+    payloadIndex <= BIndex (checked in RemainderMod4)
+*/
+template ASCIISubstrExistsInB64(b64StrLen, maxA, groupWidth) {
+    assert(maxA % 3 == 0); // for simplicity
+    var maxB = 1 + ((maxA / 3) * 4); // max(b64Len(maxA, i)) for any i
+
+    signal input b64Str[b64StrLen];
+    signal input lenB;
+    signal input BIndex;
+    signal B[maxB] <== SliceGrouped(8, groupWidth, b64StrLen, maxB)(
+        b64Str, BIndex, lenB
+    );
+
+    var B_bit_len = maxB * 6;
+    signal B_in_bits[B_bit_len] <== MultiBase64URLToBits(maxB)(B);
+
+    signal input A[maxA];
+    signal input lenA;
+
+    var A_bit_len = 8 * maxA;
+    signal A_in_bits[A_bit_len];
+    for (var i = 0; i < maxA; i++) {
+        var X[8] = Num2BitsBE(8)(A[i]);
+        for (var j = 0; j < 8; j++) {
+            A_in_bits[i * 8 + j] <== X[j];
+        }
+    }
+
+    signal input payloadIndex;
+    var BIndexInPayload = BIndex - payloadIndex;
+    signal expectedOffset <== RemainderMod4(numBits(b64StrLen))(BIndexInPayload);
+    signal eq_0 <== IsEqual()([expectedOffset, 0]);
+    signal eq_1 <== IsEqual()([expectedOffset, 1]);
+    signal eq_2 <== IsEqual()([expectedOffset, 2]);
+    eq_0 + eq_1 + eq_2 === 1; // ensure offset is 0, 1, or 2
+
+    signal tmp[maxA] <== LTBitVector(maxA)(lenA);
+
+    signal enabled_0[maxA];
+    // A_bit_len <= B_bit_len is guaranteed by the condition on maxB
+    assert(A_bit_len <= B_bit_len);
+    for (var i = 0; i < A_bit_len; i++) {
+        if (i % 8 == 0) {
+            enabled_0[i \ 8] <== tmp[i \ 8] * eq_0;
+        }
+        MyForceEqualIfEnabled()(enabled_0[i \ 8], [A_in_bits[i], B_in_bits[i]]);
+    }
+
+    signal enabled_1[maxA];
+    // A_bit_len + 2 <= B_bit_len is guaranteed by the condition on maxB
+    assert(A_bit_len + 2 <= B_bit_len);
+    for (var i = 0; i < A_bit_len; i++) {
+        if (i % 8 == 0) {
+            enabled_1[i \ 8] <== tmp[i \ 8] * eq_1;
+        }
+        MyForceEqualIfEnabled()(enabled_1[i \ 8], [A_in_bits[i], B_in_bits[i + 2]]);
+    }
+
+    signal enabled_2[maxA];
+    // A_bit_len + 4 <= B_bit_len is guaranteed by the condition on maxB
+    assert(A_bit_len + 4 <= B_bit_len);
+    for (var i = 0; i < A_bit_len; i++) {
+        if (i % 8 == 0) {
+            enabled_2[i \ 8] <== tmp[i \ 8] * eq_2;
+        }
+        MyForceEqualIfEnabled()(enabled_2[i \ 8], [A_in_bits[i], B_in_bits[i + 4]]);
+    }
+}
\ No newline at end of file
diff --git a/zklogin-circuits/circuits/zklogin.circom b/zklogin-circuits/circuits/zklogin.circom
new file mode 100644
index 0000000000..8b109c4c86
--- /dev/null
+++ b/zklogin-circuits/circuits/zklogin.circom
@@ -0,0 +1,7 @@
+pragma circom 2.1.3;
+
+include "zkloginMain.circom";
+
+component main {
+    public [all_inputs_hash]
+} = ZKLogin(64*12, 66, 10);
\ No newline at end of file
diff --git a/zklogin-circuits/circuits/zkloginMain.circom b/zklogin-circuits/circuits/zkloginMain.circom
new file mode 100644
index 0000000000..01c97927ce
--- /dev/null
+++ b/zklogin-circuits/circuits/zkloginMain.circom
@@ -0,0 +1,246 @@
+pragma circom 2.1.3;
+
+include "helpers/sha256.circom";
+include "helpers/misc.circom";
+include "helpers/strings.circom";
+include "helpers/hasher.circom";
+include "helpers/jwtchecks.circom";
+
+/**
+ZKLogin circuit
+
+Constraints (rough): (maxContentLength/64)*30k + maxContentLength * (maxExtKeyClaimLength + maxNonceLength)
+The first term is incurred by Sha2_wrapper and the second term is incurred by Slice.
+
+    Construction params:
+    - maxContentLength:         Maximum length of the JWT + SHA2 padding in bytes. Must be a multiple of 64.
+    - maxExtKeyClaimLength:     Maximum length of the extended_key_claim (in ascii)
+
+    Private Inputs:
+    - content[inCount]:         X in bytes where X is the 
+                                decoded JWT header + decoded JWT payload + SHA-2 padding + zeroes
+
+    - extended_key_claim[maxExtKeyClaimLength]: 
+                                The claim name and value for the first claim_length_ascii characters and 0s for the rest
+    - claim_length_ascii:       Length of the extended_key_claim in ASCII, e.g., for ',"sub":12345,' it is 13
+    - claim_index_b64:          The index of extended_key_claim encoded into Base64 in the JWT payload
+    - claim_length_b64:         The length of extended_key_claim in Base64
+    - key_claim_name_length:    Length of the key_claim_name in ASCII, e.g., for "sub" it is 3
+    - subject_pin:              A 128-bit PIN to keep the extended_key_claim private
+
+    - extended_nonce[maxNonceLength]: 
+                                The nonce for the first nonce_length_ascii characters and 0s for the rest
+    - nonce_claim_index_b64:    The index of extended_nonce encoded into Base64 in the JWT payload
+    - nonce_length_b64:         The length of extended_nonce in Base64
+
+    - mask[inCount]:            A binary mask over X, i.e., mask[i] = 0 or 1
+    - jwt_randomness:           A 128-bit random number to keep the sensitive parts of JWT hidden.
+
+    Circuit signals revealed to the verifier along with the ZK proof:
+    - jwt_sha2_hash:            The SHA2 hash of the JWT header + JWT payload + SHA-2 padding
+    - num_sha2_blocks:          Number of SHA2 (64-byte) blocks the SHA2-padded JWT consumes
+    - key_claim_name_F:         MapToField(key_claim_name)  
+    - address_seed:             Poseidon([value, PIN]). It will be used to derive 
+                                    subject_addr = Blake2b(flag, iss, key_claim_name_F, address_seed)
+    - payload_start_index:      The index of the payload in the content
+    - payload_len:              The length of the payload
+    - masked_content:           The content with the values of "iss" and "aud" is expected (though chosen by the prover). 
+                                    Rest of it is masked.
+    - eph_public_key[2]:        The ephemeral public key split into two 128-bit values
+    - max_epoch:                The maximum epoch for which the eph_public_key is valid
+
+    Public Inputs:
+    - all_inputs_hash:          H(jwt_sha2_hash[2] || masked_content_hash || payload_start_index || payload_len
+                                  eph_public_key[2] || max_epoch || num_sha2_blocks || key_claim_name_F || address_seed)
+
+Notes:
+- nonce = Poseidon([eph_public_key, max_epoch, jwt_randomness])
+*/
+template ZKLogin(maxContentLength, maxExtKeyClaimLength, maxKeyClaimNameLen) {
+    var inWidth = 8; // input is in bytes
+    var inCount = maxContentLength;
+    signal input content[inCount];
+
+    /**
+     1. SHA2(content)
+    */
+    signal input num_sha2_blocks;
+    var hashCount = 2;
+    var hashWidth = 256 / hashCount;
+    signal jwt_sha2_hash[hashCount] <== Sha2_wrapper(inWidth, inCount, hashWidth, hashCount)(
+        content, 
+        num_sha2_blocks
+    );
+
+    /**
+     2. Checks on extended_key_claim
+        a) Is it in the JWT payload?
+        b) Is extended_key_claim[i] == 0 for all i >= claim_length_ascii?
+        c) Is the prefix and suffix of extended_key_claim well-formed?
+
+    Note that the OpenID standard permits extended_key_claim to be any valid JSON member. 
+    But the below logic is more restrictive: it assumes that the exact same string is 
+        returned by the server every time the user logs in.
+    */
+    var subInWidth = 8;
+    signal input extended_key_claim[maxExtKeyClaimLength];
+    signal input claim_length_ascii; // Check if we ensure it is >= 1 and <= maxExtKeyClaimLength
+
+    signal input claim_index_b64;
+    signal input claim_length_b64;
+
+    signal input payload_start_index;
+
+    var numsPerGroup = 16; // since inWidth is 8, this is the maximum we can set
+    ASCIISubstrExistsInB64(
+        inCount,
+        maxExtKeyClaimLength,
+        numsPerGroup
+    )(
+        b64Str <== content,
+        BIndex <== claim_index_b64,
+        lenB <== claim_length_b64,
+        A <== extended_key_claim,
+        lenA <== claim_length_ascii,
+        payloadIndex <== payload_start_index
+    );
+
+    signal input key_claim_name_length;
+    signal input key_claim_colon_index;
+    signal input key_claim_value_start;
+    signal input key_claim_value_length;
+
+
+    var maxKeyClaimNameLenWithQuotes = maxKeyClaimNameLen + 2;
+    signal key_claim_name_with_quotes[maxKeyClaimNameLen + 2];
+
+    var maxKeyClaimValueLen = maxExtKeyClaimLength - maxKeyClaimNameLen - 6;
+    signal key_claim_value_with_quotes[maxKeyClaimValueLen + 2];
+
+    (key_claim_name_with_quotes, key_claim_value_with_quotes) <== ExtendedClaimParser(
+        maxExtKeyClaimLength, maxKeyClaimNameLen + 2, maxKeyClaimValueLen + 2
+    )(
+        extended_claim <== extended_key_claim, 
+        length <== claim_length_ascii,
+        name_len <== key_claim_name_length,
+        colon_index <== key_claim_colon_index,
+        value_start <== key_claim_value_start,
+        value_len <== key_claim_value_length
+    );
+
+    // MapToFields for later use
+    signal key_claim_name[maxKeyClaimNameLen] <== Slice(maxKeyClaimNameLen + 2, maxKeyClaimNameLen)(
+        key_claim_name_with_quotes,
+        1,
+        key_claim_name_length - 2
+    );
+    signal key_claim_name_F <== MapToField(maxKeyClaimNameLen)(key_claim_name);
+
+    signal key_claim_value[maxKeyClaimValueLen] <== Slice(maxKeyClaimValueLen + 2, maxKeyClaimValueLen)(
+        key_claim_value_with_quotes,
+        1,
+        key_claim_value_length - 2
+    );
+    signal key_claim_value_F <== MapToField(maxKeyClaimValueLen)(key_claim_value);
+
+    /**
+     3. Derive address from extended_key_claim. We exclude its last character because it can be either ',' or '}'.
+    **/
+    signal input subject_pin;
+    signal address_seed <== Hasher(2)([
+        key_claim_value_F, subject_pin
+    ]);
+
+    /** 
+      4. Masking 
+    **/
+    signal input mask[inCount];
+    signal masked_content[inCount];
+
+    for(var i = 0; i < inCount; i++) {
+        // Ensure mask is binary
+        mask[i] * (1 - mask[i]) === 0;
+        // If mask is 0, then replace with '=' (ASCII 61) to avoid conflicts with base64 characters
+        masked_content[i] <== content[i] * mask[i] + (1 - mask[i]) * 61;
+    }
+    signal masked_content_hash <== MapToField(inCount)(masked_content);
+
+    /**
+     5. Checks on extended_nonce
+        a) Is it in the JWT payload? 
+        b) Calculate nonce from public key, epoch, and randomness
+        c) Check that nonce appears in extended_nonce
+    **/
+    var extNonceLength = 43 + 11; // 43 for Base64 encoding of 256 bits + 11 for prefix and suffix
+
+    // 5a) Is it in the JWT payload?
+    signal input extended_nonce[extNonceLength];
+
+    signal input nonce_claim_index_b64;
+    signal input nonce_length_b64;
+
+    ASCIISubstrExistsInB64(
+        inCount,
+        extNonceLength,
+        numsPerGroup
+    )(
+        b64Str <== content,
+        BIndex <== nonce_claim_index_b64,
+        lenB <== nonce_length_b64,
+        A <== extended_nonce,
+        lenA <== extNonceLength,
+        payloadIndex <== payload_start_index
+    );
+
+    // 5b) Calculate nonce
+    signal input eph_public_key[2];
+    signal input max_epoch;
+    signal input jwt_randomness;
+    component size_checker_2 = Num2Bits(128);
+    size_checker_2.in <== jwt_randomness; // ensure it is 16 bytes
+
+    signal nonce <== Hasher(4)([
+        eph_public_key[0],
+        eph_public_key[1],
+        max_epoch,
+        jwt_randomness
+    ]);
+
+    // 5c) Check that nonce appears in extended_nonce
+    // NonceChecker(extNonceLength, 256)(
+    //     expected_nonce <== nonce,
+    //     actual_extended_nonce <== extended_nonce
+    // );
+
+    /**
+       6. Misc checks: 
+            - Ensure mask[i] == 1 for all i >= payload_start_index + payload_len
+    **/
+    signal input payload_len;
+    signal payload_len_actual <== payload_start_index + payload_len;
+
+    // set pllt[i] = 1 if i >= payload_len_actual, 0 otherwise
+    signal plgt[inCount] <== GTBitVector(inCount)(payload_len_actual);
+    for (var i = 0; i < inCount; i++) {
+        plgt[i] * (1 - mask[i]) === 0; // if pllt[i] == 1, then mask[i] == 1
+    }
+
+    /**
+       7. Hash all signals revealed to the verifier outside
+    **/
+    signal input all_inputs_hash;
+    signal all_inputs_hash_actual <== Hasher(11)([
+        jwt_sha2_hash[0],
+        jwt_sha2_hash[1],
+        masked_content_hash,
+        payload_start_index,
+        payload_len,
+        eph_public_key[0],
+        eph_public_key[1],
+        max_epoch,
+        num_sha2_blocks,
+        key_claim_name_F,
+        address_seed
+    ]);
+    all_inputs_hash === all_inputs_hash_actual;
+}
\ No newline at end of file
diff --git a/zklogin-circuits/create-backend.md b/zklogin-circuits/create-backend.md
new file mode 100644
index 0000000000..1f478d6623
--- /dev/null
+++ b/zklogin-circuits/create-backend.md
@@ -0,0 +1,7 @@
+# Steps to create a back-end service that returns zkLogin proofs
+
+1. Follow the instructions given at https://github.com/mskd12/rapidsnark#compile-prover-in-server-mode  and https://github.com/mskd12/rapidsnark#launch-prover-in-server-mode. You'd also need to copy the zklogin.dat file along with the binary to the `build` folder.
+
+2. Currently, the code above assumes that fastcrypto is cloned at `~`. (See https://github.com/mskd12/rapidsnark/blob/main/src/fullprover.cpp#L120)
+
+3. Set LD_LIBRARY_PATH appropriately before running the `proverServer` binary. The required file is in depends. So if rapidsnark was installed at `~`, then you'd need to set `export LD_LIBRARY_PATH=/home/ubuntu/rapidsnark/depends/pistache/build/src/` and then run `export $LD_LIBRARY_PATH`
diff --git a/zklogin-circuits/package-lock.json b/zklogin-circuits/package-lock.json
new file mode 100644
index 0000000000..78562f83c5
--- /dev/null
+++ b/zklogin-circuits/package-lock.json
@@ -0,0 +1,3068 @@
+{
+  "name": "openid-zkp-auth",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "openid-zkp-auth",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "bigint-buffer": "^1.1.5",
+        "circomlib": "^2.0.5",
+        "jsonwebtoken": "^9.0.0",
+        "jwk-to-pem": "^2.0.5",
+        "node-rsa": "^1.1.1",
+        "snarkjs": "^0.7.0",
+        "temp": "^0.9.4",
+        "yargs": "^17.7.2"
+      },
+      "bin": {
+        "zkpinputs": "js/genZKPinputs.js"
+      },
+      "devDependencies": {
+        "@types/jsonwebtoken": "^9.0.2",
+        "@types/jwk-to-pem": "^2.0.1",
+        "@types/node": "^20.2.5",
+        "@types/yargs": "^17.0.24",
+        "circom_tester": "^0.0.19",
+        "circomlibjs": "^0.1.7",
+        "jose": "^4.3.7",
+        "mocha": "^10.2.0",
+        "node-fetch": "^3.3.1",
+        "node-jose": "^2.0.0",
+        "poseidon-lite": "^0.2.0",
+        "typescript": "^5.0.4"
+      }
+    },
+    "node_modules/@ethersproject/abi": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz",
+      "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/address": "^5.7.0",
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/constants": "^5.7.0",
+        "@ethersproject/hash": "^5.7.0",
+        "@ethersproject/keccak256": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "@ethersproject/strings": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/abstract-provider": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz",
+      "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/networks": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "@ethersproject/transactions": "^5.7.0",
+        "@ethersproject/web": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/abstract-signer": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz",
+      "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.7.0",
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/address": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz",
+      "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/keccak256": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/rlp": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/base64": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz",
+      "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bytes": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/basex": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.7.0.tgz",
+      "integrity": "sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/bignumber": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz",
+      "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "bn.js": "^5.2.1"
+      }
+    },
+    "node_modules/@ethersproject/bytes": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz",
+      "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/logger": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/constants": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz",
+      "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/contracts": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.7.0.tgz",
+      "integrity": "sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/abi": "^5.7.0",
+        "@ethersproject/abstract-provider": "^5.7.0",
+        "@ethersproject/abstract-signer": "^5.7.0",
+        "@ethersproject/address": "^5.7.0",
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/constants": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "@ethersproject/transactions": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/hash": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz",
+      "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.7.0",
+        "@ethersproject/address": "^5.7.0",
+        "@ethersproject/base64": "^5.7.0",
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/keccak256": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "@ethersproject/strings": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/hdnode": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.7.0.tgz",
+      "integrity": "sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.7.0",
+        "@ethersproject/basex": "^5.7.0",
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/pbkdf2": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "@ethersproject/sha2": "^5.7.0",
+        "@ethersproject/signing-key": "^5.7.0",
+        "@ethersproject/strings": "^5.7.0",
+        "@ethersproject/transactions": "^5.7.0",
+        "@ethersproject/wordlists": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/json-wallets": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz",
+      "integrity": "sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.7.0",
+        "@ethersproject/address": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/hdnode": "^5.7.0",
+        "@ethersproject/keccak256": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/pbkdf2": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "@ethersproject/random": "^5.7.0",
+        "@ethersproject/strings": "^5.7.0",
+        "@ethersproject/transactions": "^5.7.0",
+        "aes-js": "3.0.0",
+        "scrypt-js": "3.0.1"
+      }
+    },
+    "node_modules/@ethersproject/keccak256": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz",
+      "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bytes": "^5.7.0",
+        "js-sha3": "0.8.0"
+      }
+    },
+    "node_modules/@ethersproject/logger": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz",
+      "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ]
+    },
+    "node_modules/@ethersproject/networks": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz",
+      "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/logger": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/pbkdf2": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz",
+      "integrity": "sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/sha2": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/properties": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz",
+      "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/logger": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/providers": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz",
+      "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.7.0",
+        "@ethersproject/abstract-signer": "^5.7.0",
+        "@ethersproject/address": "^5.7.0",
+        "@ethersproject/base64": "^5.7.0",
+        "@ethersproject/basex": "^5.7.0",
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/constants": "^5.7.0",
+        "@ethersproject/hash": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/networks": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "@ethersproject/random": "^5.7.0",
+        "@ethersproject/rlp": "^5.7.0",
+        "@ethersproject/sha2": "^5.7.0",
+        "@ethersproject/strings": "^5.7.0",
+        "@ethersproject/transactions": "^5.7.0",
+        "@ethersproject/web": "^5.7.0",
+        "bech32": "1.1.4",
+        "ws": "7.4.6"
+      }
+    },
+    "node_modules/@ethersproject/random": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.7.0.tgz",
+      "integrity": "sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/rlp": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz",
+      "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/sha2": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.7.0.tgz",
+      "integrity": "sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "hash.js": "1.1.7"
+      }
+    },
+    "node_modules/@ethersproject/signing-key": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz",
+      "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "bn.js": "^5.2.1",
+        "elliptic": "6.5.4",
+        "hash.js": "1.1.7"
+      }
+    },
+    "node_modules/@ethersproject/solidity": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz",
+      "integrity": "sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/keccak256": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/sha2": "^5.7.0",
+        "@ethersproject/strings": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/strings": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz",
+      "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/constants": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/transactions": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz",
+      "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/address": "^5.7.0",
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/constants": "^5.7.0",
+        "@ethersproject/keccak256": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "@ethersproject/rlp": "^5.7.0",
+        "@ethersproject/signing-key": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/units": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.7.0.tgz",
+      "integrity": "sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/constants": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/wallet": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz",
+      "integrity": "sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.7.0",
+        "@ethersproject/abstract-signer": "^5.7.0",
+        "@ethersproject/address": "^5.7.0",
+        "@ethersproject/bignumber": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/hash": "^5.7.0",
+        "@ethersproject/hdnode": "^5.7.0",
+        "@ethersproject/json-wallets": "^5.7.0",
+        "@ethersproject/keccak256": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "@ethersproject/random": "^5.7.0",
+        "@ethersproject/signing-key": "^5.7.0",
+        "@ethersproject/transactions": "^5.7.0",
+        "@ethersproject/wordlists": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/web": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz",
+      "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/base64": "^5.7.0",
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "@ethersproject/strings": "^5.7.0"
+      }
+    },
+    "node_modules/@ethersproject/wordlists": {
+      "version": "5.7.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.7.0.tgz",
+      "integrity": "sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/hash": "^5.7.0",
+        "@ethersproject/logger": "^5.7.0",
+        "@ethersproject/properties": "^5.7.0",
+        "@ethersproject/strings": "^5.7.0"
+      }
+    },
+    "node_modules/@iden3/bigarray": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/@iden3/bigarray/-/bigarray-0.0.2.tgz",
+      "integrity": "sha512-Xzdyxqm1bOFF6pdIsiHLLl3HkSLjbhqJHVyqaTxXt3RqXBEnmsUmEW47H7VOi/ak7TdkRpNkxjyK5Zbkm+y52g=="
+    },
+    "node_modules/@iden3/binfileutils": {
+      "version": "0.0.11",
+      "resolved": "https://registry.npmjs.org/@iden3/binfileutils/-/binfileutils-0.0.11.tgz",
+      "integrity": "sha512-LylnJoZ0CTdgErnKY8OxohvW4K+p6UHD3sxt+3P9AmMyBQjYR4IpoqoYZZ+9aMj89cmCQ21UvdhndAx04er3NA==",
+      "dependencies": {
+        "fastfile": "0.0.20",
+        "ffjavascript": "^0.2.48"
+      }
+    },
+    "node_modules/@types/jsonwebtoken": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+      "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/jwk-to-pem": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@types/jwk-to-pem/-/jwk-to-pem-2.0.1.tgz",
+      "integrity": "sha512-QXmRPhR/LPzvXBHTPfG2BBfMTkNLUD7NyRcPft8m5xFCeANa1BZyLgT0Gw+OxdWx6i1WCpT27EqyggP4UUHMrA==",
+      "dev": true
+    },
+    "node_modules/@types/node": {
+      "version": "20.2.5",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz",
+      "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==",
+      "dev": true
+    },
+    "node_modules/@types/yargs": {
+      "version": "17.0.24",
+      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
+      "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==",
+      "dev": true,
+      "dependencies": {
+        "@types/yargs-parser": "*"
+      }
+    },
+    "node_modules/@types/yargs-parser": {
+      "version": "21.0.0",
+      "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz",
+      "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
+      "dev": true
+    },
+    "node_modules/aes-js": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz",
+      "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==",
+      "dev": true
+    },
+    "node_modules/ansi-colors": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+      "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true
+    },
+    "node_modules/asn1": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
+      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+      "dependencies": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
+    "node_modules/asn1.js": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+      "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+      "dependencies": {
+        "bn.js": "^4.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "node_modules/asn1.js/node_modules/bn.js": {
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+      "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
+    },
+    "node_modules/assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+      "dev": true,
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/async": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+      "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
+    },
+    "node_modules/available-typed-arrays": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+      "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/b4a": {
+      "version": "1.6.4",
+      "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
+      "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw=="
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/base64url": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
+      "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
+      "dev": true,
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/bech32": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
+      "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==",
+      "dev": true
+    },
+    "node_modules/bfj": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz",
+      "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==",
+      "dependencies": {
+        "bluebird": "^3.5.5",
+        "check-types": "^11.1.1",
+        "hoopy": "^0.1.4",
+        "tryer": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 8.0.0"
+      }
+    },
+    "node_modules/bigint-buffer": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz",
+      "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "bindings": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/bindings": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+      "dependencies": {
+        "file-uri-to-path": "1.0.0"
+      }
+    },
+    "node_modules/blake-hash": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/blake-hash/-/blake-hash-2.0.0.tgz",
+      "integrity": "sha512-Igj8YowDu1PRkRsxZA7NVkdFNxH5rKv5cpLxQ0CVXSIA77pVYwCPRQJ2sMew/oneUpfuYRyjG6r8SmmmnbZb1w==",
+      "dev": true,
+      "hasInstallScript": true,
+      "dependencies": {
+        "node-addon-api": "^3.0.0",
+        "node-gyp-build": "^4.2.2",
+        "readable-stream": "^3.6.0"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/blake2b": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/blake2b/-/blake2b-2.1.4.tgz",
+      "integrity": "sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A==",
+      "dev": true,
+      "dependencies": {
+        "blake2b-wasm": "^2.4.0",
+        "nanoassert": "^2.0.0"
+      }
+    },
+    "node_modules/blake2b-wasm": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz",
+      "integrity": "sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==",
+      "dependencies": {
+        "b4a": "^1.0.1",
+        "nanoassert": "^2.0.0"
+      }
+    },
+    "node_modules/bluebird": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
+    },
+    "node_modules/bn.js": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
+      "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==",
+      "dev": true
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dev": true,
+      "dependencies": {
+        "fill-range": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="
+    },
+    "node_modules/browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true
+    },
+    "node_modules/buffer": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.2.1"
+      }
+    },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+    },
+    "node_modules/call-bind": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+      "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+      "dev": true,
+      "dependencies": {
+        "function-bind": "^1.1.1",
+        "get-intrinsic": "^1.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/camelcase": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+      "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/chai": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz",
+      "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==",
+      "dev": true,
+      "dependencies": {
+        "assertion-error": "^1.1.0",
+        "check-error": "^1.0.2",
+        "deep-eql": "^4.1.2",
+        "get-func-name": "^2.0.0",
+        "loupe": "^2.3.1",
+        "pathval": "^1.1.1",
+        "type-detect": "^4.0.5"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/chalk/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
+      "dev": true,
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/check-types": {
+      "version": "11.2.2",
+      "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.2.tgz",
+      "integrity": "sha512-HBiYvXvn9Z70Z88XKjz3AEKd4HJhBXsa3j7xFnITAzoS8+q6eIGi8qDB8FKPBAjtuxjI/zFpwuiCb8oDtKOYrA=="
+    },
+    "node_modules/child_process": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz",
+      "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==",
+      "dev": true
+    },
+    "node_modules/chokidar": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ],
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/circom_runtime": {
+      "version": "0.1.22",
+      "resolved": "https://registry.npmjs.org/circom_runtime/-/circom_runtime-0.1.22.tgz",
+      "integrity": "sha512-V/XYZWBhbZY8SotkaGH4FbiDYAZ8a1Md++MBiKPDOuWS/NIJB+Q+XIiTC8zKMgoDaa9cd2OiTvsC9J6te7twNg==",
+      "dependencies": {
+        "ffjavascript": "0.2.57"
+      },
+      "bin": {
+        "calcwit": "calcwit.js"
+      }
+    },
+    "node_modules/circom_runtime/node_modules/ffjavascript": {
+      "version": "0.2.57",
+      "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.2.57.tgz",
+      "integrity": "sha512-V+vxZ/zPNcthrWmqfe/1YGgqdkTamJeXiED0tsk7B84g40DKlrTdx47IqZuiygqAVG6zMw4qYuvXftIJWsmfKQ==",
+      "dependencies": {
+        "wasmbuilder": "0.0.16",
+        "wasmcurves": "0.2.0",
+        "web-worker": "^1.2.0"
+      }
+    },
+    "node_modules/circom_runtime/node_modules/wasmcurves": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/wasmcurves/-/wasmcurves-0.2.0.tgz",
+      "integrity": "sha512-3e2rbxdujOwaod657gxgmdhZNn+i1qKdHO3Y/bK+8E7bV8ttV/fu5FO4/WLBACF375cK0QDLOP+65Na63qYuWA==",
+      "dependencies": {
+        "wasmbuilder": "0.0.16"
+      }
+    },
+    "node_modules/circom_tester": {
+      "version": "0.0.19",
+      "resolved": "https://registry.npmjs.org/circom_tester/-/circom_tester-0.0.19.tgz",
+      "integrity": "sha512-SNHaBsGxcBH6XsVWfsRbRPA7NF8m8AMKJI9dtJJCFGUtOTT2+zsoIqAwi50z6XCnO4TtjyXq7AeXa1PLHqT0tw==",
+      "dev": true,
+      "dependencies": {
+        "chai": "^4.3.6",
+        "child_process": "^1.0.2",
+        "ffjavascript": "^0.2.56",
+        "fnv-plus": "^1.3.1",
+        "r1csfile": "^0.0.41",
+        "snarkjs": "0.5.0",
+        "tmp-promise": "^3.0.3",
+        "util": "^0.12.4"
+      }
+    },
+    "node_modules/circom_tester/node_modules/circom_runtime": {
+      "version": "0.1.21",
+      "resolved": "https://registry.npmjs.org/circom_runtime/-/circom_runtime-0.1.21.tgz",
+      "integrity": "sha512-qTkud630B/GK8y76hnOaaS1aNuF6prfV0dTrkeRsiJKnlP1ryQbP2FWLgDOPqn6aKyaPlam+Z+DTbBhkEzh8dA==",
+      "dev": true,
+      "dependencies": {
+        "ffjavascript": "0.2.56"
+      },
+      "bin": {
+        "calcwit": "calcwit.js"
+      }
+    },
+    "node_modules/circom_tester/node_modules/ffjavascript": {
+      "version": "0.2.56",
+      "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.2.56.tgz",
+      "integrity": "sha512-em6G5Lrj7ucIqj4TYEgyoHs/j99Urwwqa4+YxEVY2hggnpRimVj+noX5pZQTxI1pvtiekZI4rG65JBf0xraXrg==",
+      "dev": true,
+      "dependencies": {
+        "wasmbuilder": "0.0.16",
+        "wasmcurves": "0.2.0",
+        "web-worker": "^1.2.0"
+      }
+    },
+    "node_modules/circom_tester/node_modules/snarkjs": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/snarkjs/-/snarkjs-0.5.0.tgz",
+      "integrity": "sha512-KWz8mZ2Y+6wvn6GGkQo6/ZlKwETdAGohd40Lzpwp5TUZCn6N6O4Az1SuX1rw/qREGL6Im+ycb19suCFE8/xaKA==",
+      "dev": true,
+      "dependencies": {
+        "@iden3/binfileutils": "0.0.11",
+        "bfj": "^7.0.2",
+        "blake2b-wasm": "^2.4.0",
+        "circom_runtime": "0.1.21",
+        "ejs": "^3.1.6",
+        "fastfile": "0.0.20",
+        "ffjavascript": "0.2.56",
+        "js-sha3": "^0.8.0",
+        "logplease": "^1.2.15",
+        "r1csfile": "0.0.41"
+      },
+      "bin": {
+        "snarkjs": "build/cli.cjs"
+      }
+    },
+    "node_modules/circom_tester/node_modules/wasmcurves": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/wasmcurves/-/wasmcurves-0.2.0.tgz",
+      "integrity": "sha512-3e2rbxdujOwaod657gxgmdhZNn+i1qKdHO3Y/bK+8E7bV8ttV/fu5FO4/WLBACF375cK0QDLOP+65Na63qYuWA==",
+      "dev": true,
+      "dependencies": {
+        "wasmbuilder": "0.0.16"
+      }
+    },
+    "node_modules/circomlib": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/circomlib/-/circomlib-2.0.5.tgz",
+      "integrity": "sha512-O7NQ8OS+J4eshBuoy36z/TwQU0YHw8W3zxZcs4hVwpEll3e4hDm3mgkIPqItN8FDeLEKZFK3YeT/+k8TiLF3/A=="
+    },
+    "node_modules/circomlibjs": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/circomlibjs/-/circomlibjs-0.1.7.tgz",
+      "integrity": "sha512-GRAUoAlKAsiiTa+PA725G9RmEmJJRc8tRFxw/zKktUxlQISGznT4hH4ESvW8FNTsrGg/nNd06sGP/Wlx0LUHVg==",
+      "dev": true,
+      "dependencies": {
+        "blake-hash": "^2.0.0",
+        "blake2b": "^2.1.3",
+        "ethers": "^5.5.1",
+        "ffjavascript": "^0.2.45"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+    },
+    "node_modules/data-uri-to-buffer": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+      "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/debug/node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
+    "node_modules/decamelize": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+      "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/deep-eql": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
+      "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
+      "dev": true,
+      "dependencies": {
+        "type-detect": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/diff": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+      "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/ejs": {
+      "version": "3.1.9",
+      "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
+      "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
+      "dependencies": {
+        "jake": "^10.8.5"
+      },
+      "bin": {
+        "ejs": "bin/cli.js"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/elliptic": {
+      "version": "6.5.4",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
+      "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
+      "dependencies": {
+        "bn.js": "^4.11.9",
+        "brorand": "^1.1.0",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.1",
+        "inherits": "^2.0.4",
+        "minimalistic-assert": "^1.0.1",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "node_modules/elliptic/node_modules/bn.js": {
+      "version": "4.12.0",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+      "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/es6-promise": {
+      "version": "4.2.8",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+      "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
+      "dev": true
+    },
+    "node_modules/escalade": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ethers": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz",
+      "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "dependencies": {
+        "@ethersproject/abi": "5.7.0",
+        "@ethersproject/abstract-provider": "5.7.0",
+        "@ethersproject/abstract-signer": "5.7.0",
+        "@ethersproject/address": "5.7.0",
+        "@ethersproject/base64": "5.7.0",
+        "@ethersproject/basex": "5.7.0",
+        "@ethersproject/bignumber": "5.7.0",
+        "@ethersproject/bytes": "5.7.0",
+        "@ethersproject/constants": "5.7.0",
+        "@ethersproject/contracts": "5.7.0",
+        "@ethersproject/hash": "5.7.0",
+        "@ethersproject/hdnode": "5.7.0",
+        "@ethersproject/json-wallets": "5.7.0",
+        "@ethersproject/keccak256": "5.7.0",
+        "@ethersproject/logger": "5.7.0",
+        "@ethersproject/networks": "5.7.1",
+        "@ethersproject/pbkdf2": "5.7.0",
+        "@ethersproject/properties": "5.7.0",
+        "@ethersproject/providers": "5.7.2",
+        "@ethersproject/random": "5.7.0",
+        "@ethersproject/rlp": "5.7.0",
+        "@ethersproject/sha2": "5.7.0",
+        "@ethersproject/signing-key": "5.7.0",
+        "@ethersproject/solidity": "5.7.0",
+        "@ethersproject/strings": "5.7.0",
+        "@ethersproject/transactions": "5.7.0",
+        "@ethersproject/units": "5.7.0",
+        "@ethersproject/wallet": "5.7.0",
+        "@ethersproject/web": "5.7.1",
+        "@ethersproject/wordlists": "5.7.0"
+      }
+    },
+    "node_modules/fastfile": {
+      "version": "0.0.20",
+      "resolved": "https://registry.npmjs.org/fastfile/-/fastfile-0.0.20.tgz",
+      "integrity": "sha512-r5ZDbgImvVWCP0lA/cGNgQcZqR+aYdFx3u+CtJqUE510pBUVGMn4ulL/iRTI4tACTYsNJ736uzFxEBXesPAktA=="
+    },
+    "node_modules/fetch-blob": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+      "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "paypal",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "dependencies": {
+        "node-domexception": "^1.0.0",
+        "web-streams-polyfill": "^3.0.3"
+      },
+      "engines": {
+        "node": "^12.20 || >= 14.13"
+      }
+    },
+    "node_modules/ffjavascript": {
+      "version": "0.2.59",
+      "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.2.59.tgz",
+      "integrity": "sha512-QssOEUv+wilz9Sg7Zaj6KWAm7QceOAEsFuEBTltUsDo1cjn11rA/LGYvzFBPbzNfxRlZxwgJ7uxpCQcdDlrNfw==",
+      "dependencies": {
+        "wasmbuilder": "0.0.16",
+        "wasmcurves": "0.2.1",
+        "web-worker": "^1.2.0"
+      }
+    },
+    "node_modules/file-uri-to-path": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
+    },
+    "node_modules/filelist": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
+      "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
+      "dependencies": {
+        "minimatch": "^5.0.1"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dev": true,
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+      "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+      "dev": true,
+      "bin": {
+        "flat": "cli.js"
+      }
+    },
+    "node_modules/fnv-plus": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/fnv-plus/-/fnv-plus-1.3.1.tgz",
+      "integrity": "sha512-Gz1EvfOneuFfk4yG458dJ3TLJ7gV19q3OM/vVvvHf7eT02Hm1DleB4edsia6ahbKgAYxO9gvyQ1ioWZR+a00Yw==",
+      "dev": true
+    },
+    "node_modules/for-each": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+      "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+      "dev": true,
+      "dependencies": {
+        "is-callable": "^1.1.3"
+      }
+    },
+    "node_modules/formdata-polyfill": {
+      "version": "4.0.10",
+      "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+      "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+      "dev": true,
+      "dependencies": {
+        "fetch-blob": "^3.1.2"
+      },
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
+      "dev": true,
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+      "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+      "dev": true,
+      "dependencies": {
+        "function-bind": "^1.1.1",
+        "has": "^1.0.3",
+        "has-proto": "^1.0.1",
+        "has-symbols": "^1.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/glob": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/glob/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+      "dev": true,
+      "dependencies": {
+        "get-intrinsic": "^1.1.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "dependencies": {
+        "function-bind": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+      "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+      "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+      "dev": true,
+      "dependencies": {
+        "has-symbols": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+      "dependencies": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "node_modules/hoopy": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
+      "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==",
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "node_modules/is-arguments": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+      "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-callable": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-generator-function": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+      "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+      "dev": true,
+      "dependencies": {
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-plain-obj": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+      "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-typed-array": {
+      "version": "1.1.10",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
+      "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+      "dev": true,
+      "dependencies": {
+        "available-typed-arrays": "^1.0.5",
+        "call-bind": "^1.0.2",
+        "for-each": "^0.3.3",
+        "gopd": "^1.0.1",
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-unicode-supported": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/jake": {
+      "version": "10.8.7",
+      "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
+      "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==",
+      "dependencies": {
+        "async": "^3.2.3",
+        "chalk": "^4.0.2",
+        "filelist": "^1.0.4",
+        "minimatch": "^3.1.2"
+      },
+      "bin": {
+        "jake": "bin/cli.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/jake/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/jake/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/jose": {
+      "version": "4.14.4",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz",
+      "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/panva"
+      }
+    },
+    "node_modules/js-sha3": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+      "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/jsonwebtoken": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
+      "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
+      "dependencies": {
+        "jws": "^3.2.2",
+        "lodash": "^4.17.21",
+        "ms": "^2.1.1",
+        "semver": "^7.3.8"
+      },
+      "engines": {
+        "node": ">=12",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+      "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+      "dependencies": {
+        "buffer-equal-constant-time": "1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jwk-to-pem": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz",
+      "integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==",
+      "dependencies": {
+        "asn1.js": "^5.3.0",
+        "elliptic": "^6.5.4",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+      "dependencies": {
+        "jwa": "^1.4.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
+    "node_modules/log-symbols": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+      "dev": true,
+      "dependencies": {
+        "chalk": "^4.1.0",
+        "is-unicode-supported": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/logplease": {
+      "version": "1.2.15",
+      "resolved": "https://registry.npmjs.org/logplease/-/logplease-1.2.15.tgz",
+      "integrity": "sha512-jLlHnlsPSJjpwUfcNyUxXCl33AYg2cHhIf9QhGL2T4iPT0XPB+xP1LRKFPgIg1M/sg9kAJvy94w9CzBNrfnstA=="
+    },
+    "node_modules/long": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+      "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
+      "dev": true
+    },
+    "node_modules/loupe": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
+      "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==",
+      "dev": true,
+      "dependencies": {
+        "get-func-name": "^2.0.0"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+    },
+    "node_modules/minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="
+    },
+    "node_modules/minimatch": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
+      "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/mkdirp": {
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+      "dependencies": {
+        "minimist": "^1.2.6"
+      },
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      }
+    },
+    "node_modules/mocha": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
+      "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
+      "dev": true,
+      "dependencies": {
+        "ansi-colors": "4.1.1",
+        "browser-stdout": "1.3.1",
+        "chokidar": "3.5.3",
+        "debug": "4.3.4",
+        "diff": "5.0.0",
+        "escape-string-regexp": "4.0.0",
+        "find-up": "5.0.0",
+        "glob": "7.2.0",
+        "he": "1.2.0",
+        "js-yaml": "4.1.0",
+        "log-symbols": "4.1.0",
+        "minimatch": "5.0.1",
+        "ms": "2.1.3",
+        "nanoid": "3.3.3",
+        "serialize-javascript": "6.0.0",
+        "strip-json-comments": "3.1.1",
+        "supports-color": "8.1.1",
+        "workerpool": "6.2.1",
+        "yargs": "16.2.0",
+        "yargs-parser": "20.2.4",
+        "yargs-unparser": "2.0.0"
+      },
+      "bin": {
+        "_mocha": "bin/_mocha",
+        "mocha": "bin/mocha.js"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mochajs"
+      }
+    },
+    "node_modules/mocha/node_modules/cliui": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+      "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+      "dev": true,
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
+    "node_modules/mocha/node_modules/yargs": {
+      "version": "16.2.0",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+      "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+      "dev": true,
+      "dependencies": {
+        "cliui": "^7.0.2",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.0",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^20.2.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
+    "node_modules/nanoassert": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
+      "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA=="
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
+      "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
+      "dev": true,
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/node-addon-api": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
+      "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==",
+      "dev": true
+    },
+    "node_modules/node-domexception": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+      "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "github",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "engines": {
+        "node": ">=10.5.0"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz",
+      "integrity": "sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==",
+      "dev": true,
+      "dependencies": {
+        "data-uri-to-buffer": "^4.0.0",
+        "fetch-blob": "^3.1.4",
+        "formdata-polyfill": "^4.0.10"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/node-fetch"
+      }
+    },
+    "node_modules/node-forge": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
+      "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 6.13.0"
+      }
+    },
+    "node_modules/node-gyp-build": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz",
+      "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==",
+      "dev": true,
+      "bin": {
+        "node-gyp-build": "bin.js",
+        "node-gyp-build-optional": "optional.js",
+        "node-gyp-build-test": "build-test.js"
+      }
+    },
+    "node_modules/node-jose": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.2.0.tgz",
+      "integrity": "sha512-XPCvJRr94SjLrSIm4pbYHKLEaOsDvJCpyFw/6V/KK/IXmyZ6SFBzAUDO9HQf4DB/nTEFcRGH87mNciOP23kFjw==",
+      "dev": true,
+      "dependencies": {
+        "base64url": "^3.0.1",
+        "buffer": "^6.0.3",
+        "es6-promise": "^4.2.8",
+        "lodash": "^4.17.21",
+        "long": "^5.2.0",
+        "node-forge": "^1.2.1",
+        "pako": "^2.0.4",
+        "process": "^0.11.10",
+        "uuid": "^9.0.0"
+      }
+    },
+    "node_modules/node-rsa": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz",
+      "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==",
+      "dependencies": {
+        "asn1": "^0.2.4"
+      }
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/pako": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+      "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
+      "dev": true
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pathval": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+      "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+      "dev": true,
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/poseidon-lite": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/poseidon-lite/-/poseidon-lite-0.2.0.tgz",
+      "integrity": "sha512-vivDZnGmz8W4G/GzVA72PXkfYStjilu83rjjUfpL4PueKcC8nfX6hCPh2XhoC5FBgC6y0TA3YuUeUo5YCcNoig==",
+      "dev": true
+    },
+    "node_modules/process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
+    "node_modules/r1csfile": {
+      "version": "0.0.41",
+      "resolved": "https://registry.npmjs.org/r1csfile/-/r1csfile-0.0.41.tgz",
+      "integrity": "sha512-Q1WDF3u1vYeAwjHo4YuddkA8Aq0TulbKjmGm99+Atn13Lf5fTsMZBnBV9T741w8iSyPFG6Uh6sapQby77sREqA==",
+      "dev": true,
+      "dependencies": {
+        "@iden3/bigarray": "0.0.2",
+        "@iden3/binfileutils": "0.0.11",
+        "fastfile": "0.0.20",
+        "ffjavascript": "0.2.56"
+      }
+    },
+    "node_modules/r1csfile/node_modules/ffjavascript": {
+      "version": "0.2.56",
+      "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.2.56.tgz",
+      "integrity": "sha512-em6G5Lrj7ucIqj4TYEgyoHs/j99Urwwqa4+YxEVY2hggnpRimVj+noX5pZQTxI1pvtiekZI4rG65JBf0xraXrg==",
+      "dev": true,
+      "dependencies": {
+        "wasmbuilder": "0.0.16",
+        "wasmcurves": "0.2.0",
+        "web-worker": "^1.2.0"
+      }
+    },
+    "node_modules/r1csfile/node_modules/wasmcurves": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/wasmcurves/-/wasmcurves-0.2.0.tgz",
+      "integrity": "sha512-3e2rbxdujOwaod657gxgmdhZNn+i1qKdHO3Y/bK+8E7bV8ttV/fu5FO4/WLBACF375cK0QDLOP+65Na63qYuWA==",
+      "dev": true,
+      "dependencies": {
+        "wasmbuilder": "0.0.16"
+      }
+    },
+    "node_modules/randombytes": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "dev": true,
+      "dependencies": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "dev": true,
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "2.6.3",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+      "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "node_modules/scrypt-js": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
+      "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==",
+      "dev": true
+    },
+    "node_modules/semver": {
+      "version": "7.5.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
+      "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/serialize-javascript": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
+      "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+      "dev": true,
+      "dependencies": {
+        "randombytes": "^2.1.0"
+      }
+    },
+    "node_modules/snarkjs": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/snarkjs/-/snarkjs-0.7.0.tgz",
+      "integrity": "sha512-Vu5W+0Va6X1xvlCllpZ2r3/S7MafnL6IrAv09lk/F+VNDHuHEHx3xopR9Kr70p2KpbBBJ/HB9VCDZWism8WGlA==",
+      "dependencies": {
+        "@iden3/binfileutils": "0.0.11",
+        "bfj": "^7.0.2",
+        "blake2b-wasm": "^2.4.0",
+        "circom_runtime": "0.1.22",
+        "ejs": "^3.1.6",
+        "fastfile": "0.0.20",
+        "ffjavascript": "0.2.59",
+        "js-sha3": "^0.8.0",
+        "logplease": "^1.2.15",
+        "r1csfile": "0.0.45"
+      },
+      "bin": {
+        "snarkjs": "build/cli.cjs"
+      }
+    },
+    "node_modules/snarkjs/node_modules/ffjavascript": {
+      "version": "0.2.57",
+      "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.2.57.tgz",
+      "integrity": "sha512-V+vxZ/zPNcthrWmqfe/1YGgqdkTamJeXiED0tsk7B84g40DKlrTdx47IqZuiygqAVG6zMw4qYuvXftIJWsmfKQ==",
+      "dependencies": {
+        "wasmbuilder": "0.0.16",
+        "wasmcurves": "0.2.0",
+        "web-worker": "^1.2.0"
+      }
+    },
+    "node_modules/snarkjs/node_modules/r1csfile": {
+      "version": "0.0.45",
+      "resolved": "https://registry.npmjs.org/r1csfile/-/r1csfile-0.0.45.tgz",
+      "integrity": "sha512-YKIp4D441aZ6OoI9y+bfAyb2j4Cl+OFq/iiX6pPWDrL4ZO968h0dq0w07i65edvrTt7/G43mTnl0qEuLXyp/Yw==",
+      "dependencies": {
+        "@iden3/bigarray": "0.0.2",
+        "@iden3/binfileutils": "0.0.11",
+        "fastfile": "0.0.20",
+        "ffjavascript": "0.2.57"
+      }
+    },
+    "node_modules/snarkjs/node_modules/wasmcurves": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/wasmcurves/-/wasmcurves-0.2.0.tgz",
+      "integrity": "sha512-3e2rbxdujOwaod657gxgmdhZNn+i1qKdHO3Y/bK+8E7bV8ttV/fu5FO4/WLBACF375cK0QDLOP+65Na63qYuWA==",
+      "dependencies": {
+        "wasmbuilder": "0.0.16"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "dev": true,
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "dev": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/temp": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
+      "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
+      "dependencies": {
+        "mkdirp": "^0.5.1",
+        "rimraf": "~2.6.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/tmp": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+      "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+      "dev": true,
+      "dependencies": {
+        "rimraf": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8.17.0"
+      }
+    },
+    "node_modules/tmp-promise": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
+      "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
+      "dev": true,
+      "dependencies": {
+        "tmp": "^0.2.0"
+      }
+    },
+    "node_modules/tmp/node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/tryer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
+      "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA=="
+    },
+    "node_modules/type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.1.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz",
+      "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==",
+      "dev": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/util": {
+      "version": "0.12.5",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+      "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+      "dev": true,
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "is-arguments": "^1.0.4",
+        "is-generator-function": "^1.0.7",
+        "is-typed-array": "^1.1.3",
+        "which-typed-array": "^1.1.2"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true
+    },
+    "node_modules/uuid": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+      "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
+      "dev": true,
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/wasmbuilder": {
+      "version": "0.0.16",
+      "resolved": "https://registry.npmjs.org/wasmbuilder/-/wasmbuilder-0.0.16.tgz",
+      "integrity": "sha512-Qx3lEFqaVvp1cEYW7Bfi+ebRJrOiwz2Ieu7ZG2l7YyeSJIok/reEQCQCuicj/Y32ITIJuGIM9xZQppGx5LrQdA=="
+    },
+    "node_modules/wasmcurves": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/wasmcurves/-/wasmcurves-0.2.1.tgz",
+      "integrity": "sha512-9ciO7bUE5bgpbOcdK7IO3enrSVIKHwrQmPibok4GLJWaCA7Wyqc9PRYnu5HbiFv9NDFNqVKPtU5R6Is5KujBLg==",
+      "dependencies": {
+        "wasmbuilder": "0.0.16"
+      }
+    },
+    "node_modules/web-streams-polyfill": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
+      "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/web-worker": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz",
+      "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA=="
+    },
+    "node_modules/which-typed-array": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
+      "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+      "dev": true,
+      "dependencies": {
+        "available-typed-arrays": "^1.0.5",
+        "call-bind": "^1.0.2",
+        "for-each": "^0.3.3",
+        "gopd": "^1.0.1",
+        "has-tostringtag": "^1.0.0",
+        "is-typed-array": "^1.1.10"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/workerpool": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
+      "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
+      "dev": true
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+    },
+    "node_modules/ws": {
+      "version": "7.4.6",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
+      "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
+      "dev": true,
+      "engines": {
+        "node": ">=8.3.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
+    "node_modules/yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "20.2.4",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
+      "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs-unparser": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+      "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+      "dev": true,
+      "dependencies": {
+        "camelcase": "^6.0.0",
+        "decamelize": "^4.0.0",
+        "flat": "^5.0.2",
+        "is-plain-obj": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs/node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    }
+  }
+}
diff --git a/zklogin-circuits/package.json b/zklogin-circuits/package.json
new file mode 100644
index 0000000000..df463a03be
--- /dev/null
+++ b/zklogin-circuits/package.json
@@ -0,0 +1,38 @@
+{
+  "name": "openid-zkp-auth",
+  "version": "1.0.0",
+  "description": "Verify OIDC JWT with SNARKs",
+  "scripts": {
+    "test": "tsc; mocha --max-old-space-size=16000 -t 10000s",
+    "prove": "node js/prove.js"
+  },
+  "bin": {
+    "zkpinputs": "js/genZKPinputs.js"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "bigint-buffer": "^1.1.5",
+    "circomlib": "^2.0.5",
+    "jsonwebtoken": "^9.0.0",
+    "jwk-to-pem": "^2.0.5",
+    "node-rsa": "^1.1.1",
+    "snarkjs": "^0.7.0",
+    "temp": "^0.9.4",
+    "yargs": "^17.7.2"
+  },
+  "devDependencies": {
+    "@types/jsonwebtoken": "^9.0.2",
+    "@types/jwk-to-pem": "^2.0.1",
+    "@types/node": "^20.2.5",
+    "@types/yargs": "^17.0.24",
+    "circom_tester": "^0.0.19",
+    "circomlibjs": "^0.1.7",
+    "jose": "^4.3.7",
+    "mocha": "^10.2.0",
+    "node-fetch": "^3.3.1",
+    "node-jose": "^2.0.0",
+    "poseidon-lite": "^0.2.0",
+    "typescript": "^5.0.4"
+  }
+}
diff --git a/zklogin-circuits/src/circuitutils.ts b/zklogin-circuits/src/circuitutils.ts
new file mode 100644
index 0000000000..e0f2fa9cc5
--- /dev/null
+++ b/zklogin-circuits/src/circuitutils.ts
@@ -0,0 +1,296 @@
+import crypto from "crypto";
+import { toBigIntBE, toBufferBE } from 'bigint-buffer';
+
+import * as utils from './utils';
+import * as jwtutils from './jwtutils';
+
+import { PartialAuxInputs, KCCheckInputs, ZKInputs, constants, bit, NonceCheckInputs, WalletInputs, CircuitConstants } from './common';
+
+const claimsToReveal = constants.claimsToReveal;
+const devVars = constants.dev;
+const nWidth = constants.inWidth;
+const packWidth = constants.pack_width;
+const poseidonHash = require('./utils').poseidonHash;
+
+// https://datatracker.ietf.org/doc/html/rfc4634#section-4.1
+function padMessage(bits: bit[]): bit[] {
+    const L = bits.length;
+    const K = (512 + 448 - (L % 512 + 1)) % 512;
+
+    bits = bits.concat([1]);
+    if(K > 0) {
+        bits = bits.concat(Array(K).fill(0));
+    }
+    bits = bits.concat(utils.buffer2BitArray(Buffer.from(L.toString(16).padStart(16, '0'), 'hex')));
+    return bits;
+}
+
+function genJwtMask(input: string, fields: string[]): bit[] {
+    const [header, payload] = input.split('.');
+    var payloadMask = Array(payload.length).fill(0);
+    for(const field of fields) {
+        var [start, len] = jwtutils.indicesOfB64(payload, field);
+        for(var i = 0; i < len; i++) {
+            payloadMask[start + i] = 1;
+        }
+    }
+
+    return Array(header.length + 1).fill(1).concat(payloadMask);
+}
+  
+function genSha256Inputs(input: string, nCount: number, nWidth = 8) {
+    var segments = utils.arrayChunk(padMessage(utils.buffer2BitArray(Buffer.from(input))), nWidth);
+    const num_sha2_blocks = (segments.length * nWidth) / 512;
+
+    if ((segments.length * nWidth) % 512 != 0) {
+        throw new Error("Padding error: Padded message length is not a multiple of 512");
+    }
+    
+    if(segments.length < nCount) {
+        segments = segments.concat(Array(nCount-segments.length).fill(Array(nWidth).fill(0)));
+    }
+    
+    if(segments.length > nCount) {
+        throw new Error(`Padded message (${segments.length}) exceeds maximum length supported by circuit (${nCount})`);
+    }
+    
+    return {
+        ["content"]: segments.map(bits => toBigIntBE(utils.bitArray2Buffer(bits))),
+        "num_sha2_blocks": num_sha2_blocks
+    };
+}
+
+async function computeNonce(
+    ephemeral_public_key = devVars.ephPK, 
+    max_epoch = devVars.maxEpoch, 
+    jwt_randomness = devVars.jwtRand,
+) {
+    const eph_public_key_0 = ephemeral_public_key / 2n**128n;
+    const eph_public_key_1 = ephemeral_public_key % 2n**128n;
+
+    const buildPoseidon = require("circomlibjs").buildPoseidon;
+    const poseidon = await buildPoseidon();
+    const bignum = poseidonHash([
+        eph_public_key_0,
+        eph_public_key_1,
+        max_epoch,
+        jwt_randomness
+    ], poseidon);
+
+    const Z = toBufferBE(bignum, 32); // padded to 32 bytes
+    const nonce = Z.toString('base64url');
+
+    if (nonce.length != constants.nonceLen) {
+        throw new Error(`Length of nonce ${nonce} (${nonce.length}) is not equal to ${constants.nonceLen}`);
+    }
+
+    return nonce;
+}
+
+async function genNonceCheckInputs(
+    payload: string, payloadIndex: number,
+    ephemeral_public_key = devVars.ephPK, 
+    max_epoch = devVars.maxEpoch, 
+    jwt_randomness = devVars.jwtRand,
+): Promise<NonceCheckInputs> {
+    const decoded_payload = Buffer.from(payload, 'base64url').toString();
+    const extended_nonce = jwtutils.getExtendedClaim(decoded_payload, "nonce");
+    const [start, len] = jwtutils.indicesOfB64(payload, 'nonce');
+
+    if (extended_nonce.length != constants.extNonceLen) {
+        throw new Error(`Length of nonce claim ${extended_nonce} (${extended_nonce.length}) is not equal to ${constants.extNonceLen} characters`);
+    }
+
+    const eph_public_key: [bigint, bigint] = [ephemeral_public_key / 2n**128n, ephemeral_public_key % 2n**128n];
+
+    return {
+        "extended_nonce": extended_nonce.split('').map(c => c.charCodeAt(0)),
+        "nonce_claim_index_b64": start + payloadIndex,
+        "nonce_length_b64": len,
+        "eph_public_key": eph_public_key,
+        "max_epoch": max_epoch,
+        "jwt_randomness": jwt_randomness
+    };
+}
+
+async function genKeyClaimCheckInputs(
+    payload: string, maxExtClaimLen: number, 
+    payloadIndex: number, userPIN: bigint,
+    keyClaimName = "sub"
+): Promise<KCCheckInputs> {
+    const decoded_payload = Buffer.from(payload, 'base64url').toString();
+    const parse_inputs = genExtClaimParserInputs(decoded_payload, keyClaimName, maxExtClaimLen);
+
+    const [start, len] = jwtutils.indicesOfB64(payload, keyClaimName);
+
+    return {
+        "extended_key_claim": parse_inputs.extended_claim,
+        "claim_length_ascii": parse_inputs.length,
+        "claim_index_b64": start + payloadIndex,
+        "claim_length_b64": len,
+        "subject_pin": userPIN,
+        "key_claim_name_length": parse_inputs.name_len,
+        "key_claim_colon_index": parse_inputs.colon_index,
+        "key_claim_value_start": parse_inputs.value_start,
+        "key_claim_value_length": parse_inputs.value_len
+    };
+}
+
+function genExtClaimParserInputs(
+    payload: string,
+    claim_name: string,
+    max_extended_claim_length: number
+) {
+    const output = jwtutils.parseClaim(payload, claim_name);
+    const extended_claim = output.extended_claim;
+
+    return {
+        "extended_claim": utils.strToVec(extended_claim, max_extended_claim_length),
+        "length": extended_claim.length,
+        "name_len": claim_name.length + 2, // +2 for the quotes
+        "colon_index": output.colon_index,
+        "value_start": output.value_index,
+        "value_len": output.raw_value.length
+    }
+}
+
+async function sanityChecks(
+    payload: string,
+    I: WalletInputs
+) {
+    const nonce = await computeNonce(I.eph_public_key, I.max_epoch, I.jwt_rand);
+
+    const decoded_payload = Buffer.from(payload, 'base64url').toString();
+    const json_nonce = JSON.parse(decoded_payload).nonce;
+    if (json_nonce !== nonce) {
+        throw new Error(`Nonce in the JSON ${json_nonce} does not match computed nonce ${nonce}`);
+    }
+}
+
+/**
+ * 
+ * @param I The wallet inputs
+ * @param P The circuit constants
+ * @param dev a boolean indicating whether to perform sanity checks (default: true)
+ * @returns 
+ */
+async function genZKLoginInputs(
+    I: WalletInputs,
+    P: CircuitConstants,
+    dev = true
+): Promise<[ZKInputs, PartialAuxInputs]> {
+    var zk_inputs = new ZKInputs();
+    var aux_inputs = new PartialAuxInputs();
+
+    // set SHA-2 inputs
+    const unsigned_jwt = I.unsigned_jwt;
+    let sha256inputs = genSha256Inputs(
+        unsigned_jwt,
+        P.max_padded_unsigned_jwt_len,
+        nWidth
+    );
+    zk_inputs.content = sha256inputs.content;
+    zk_inputs.num_sha2_blocks = sha256inputs.num_sha2_blocks;
+
+    // set indices
+    zk_inputs.payload_start_index = unsigned_jwt.split('.')[0].length + 1; // 4x+1, 4x, 4x-1
+    const payload = unsigned_jwt.split('.')[1];
+    zk_inputs.payload_len = payload.length;
+
+    // set the key claim inputs
+    const key_claim_inputs = await genKeyClaimCheckInputs(
+        payload,
+        P.max_extended_key_claim_len,
+        zk_inputs.payload_start_index,
+        I.user_pin,
+        I.key_claim_name
+    );
+    zk_inputs.extended_key_claim = key_claim_inputs.extended_key_claim;
+    zk_inputs.claim_length_ascii = key_claim_inputs.claim_length_ascii;
+    zk_inputs.claim_index_b64 = key_claim_inputs.claim_index_b64;
+    zk_inputs.claim_length_b64 = key_claim_inputs.claim_length_b64;
+    zk_inputs.subject_pin = key_claim_inputs.subject_pin;
+    zk_inputs.key_claim_name_length = key_claim_inputs.key_claim_name_length;
+    zk_inputs.key_claim_colon_index = key_claim_inputs.key_claim_colon_index;
+    zk_inputs.key_claim_value_start = key_claim_inputs.key_claim_value_start;
+    zk_inputs.key_claim_value_length = key_claim_inputs.key_claim_value_length;
+
+    // set hash
+    const hash = BigInt("0x" + crypto.createHash("sha256").update(unsigned_jwt).digest("hex"));
+    aux_inputs.jwt_sha2_hash = [hash / 2n**128n, hash % 2n**128n];
+
+    // masking 
+    zk_inputs.mask = genJwtMask(unsigned_jwt, claimsToReveal).concat(Array(P.max_padded_unsigned_jwt_len - unsigned_jwt.length).fill(1));
+    aux_inputs.masked_content = utils.applyMask(zk_inputs.content.map(Number), zk_inputs.mask); 
+
+    // set nonce-related inputs
+    const nonce_inputs = await genNonceCheckInputs(
+        payload, zk_inputs.payload_start_index,
+        I.eph_public_key, I.max_epoch, I.jwt_rand
+    );
+    zk_inputs.extended_nonce = nonce_inputs.extended_nonce;
+    zk_inputs.nonce_claim_index_b64 = nonce_inputs.nonce_claim_index_b64;
+    zk_inputs.nonce_length_b64 = nonce_inputs.nonce_length_b64;
+    zk_inputs.eph_public_key = nonce_inputs.eph_public_key;
+    zk_inputs.max_epoch = nonce_inputs.max_epoch;
+    zk_inputs.jwt_randomness = nonce_inputs.jwt_randomness;
+
+    // derive address seed
+    const decoded_payload = Buffer.from(payload, 'base64url').toString();
+    const key_claim_value = jwtutils.getClaimValue(decoded_payload, I.key_claim_name);
+    aux_inputs.addr_seed = await utils.deriveAddrSeed(
+        key_claim_value, I.user_pin, P.max_key_claim_value_len
+    );
+    console.log(`Seed ${aux_inputs.addr_seed} derived from ID ${key_claim_value} and PIN ${I.user_pin}`);
+
+    aux_inputs.payload_start_index = zk_inputs.payload_start_index;
+    aux_inputs.payload_len = zk_inputs.payload_len;
+    aux_inputs.eph_public_key = zk_inputs.eph_public_key;
+    aux_inputs.max_epoch = zk_inputs.max_epoch;
+    aux_inputs.num_sha2_blocks = zk_inputs.num_sha2_blocks;
+    aux_inputs.key_claim_name = I.key_claim_name;
+
+    zk_inputs.all_inputs_hash = await calcAllInputsHash(aux_inputs, P.max_key_claim_name_len);
+
+    if (dev) {
+        sanityChecks(payload, I);
+    }
+  
+    return [zk_inputs, aux_inputs];
+}
+
+async function calcAllInputsHash(
+    aux_inputs: PartialAuxInputs,
+    maxKeyClaimNameLen: number,
+): Promise<bigint> {
+    const buildPoseidon = require("circomlibjs").buildPoseidon;
+    const poseidon = await buildPoseidon();
+
+    const packed = utils.pack(aux_inputs.masked_content.map(BigInt), 8, packWidth); // TODO: BigInt -> Number -> BigInt
+    const masked_content_hash = poseidonHash(packed, poseidon);
+
+    const key_claim_name_F = await utils.mapToField(aux_inputs.key_claim_name, maxKeyClaimNameLen);
+
+    return poseidonHash([
+        aux_inputs.jwt_sha2_hash[0],
+        aux_inputs.jwt_sha2_hash[1],
+        masked_content_hash,
+        aux_inputs.payload_start_index,
+        aux_inputs.payload_len,
+        aux_inputs.eph_public_key[0], 
+        aux_inputs.eph_public_key[1], 
+        aux_inputs.max_epoch,
+        aux_inputs.num_sha2_blocks,
+        key_claim_name_F,
+        aux_inputs.addr_seed
+    ], poseidon);
+}
+
+export {
+    padMessage,
+    genJwtMask,
+    genSha256Inputs,
+    genZKLoginInputs,
+    genExtClaimParserInputs,
+    computeNonce
+}
diff --git a/zklogin-circuits/src/common.ts b/zklogin-circuits/src/common.ts
new file mode 100644
index 0000000000..bc88f552ca
--- /dev/null
+++ b/zklogin-circuits/src/common.ts
@@ -0,0 +1,114 @@
+export type bit = 0 | 1;
+
+export interface WalletInputs {
+    unsigned_jwt: string,
+    eph_public_key: bigint,
+    max_epoch: number,
+    jwt_rand: bigint,
+    user_pin: bigint,
+    key_claim_name: string
+}
+
+export interface KCCheckInputs {
+    "extended_key_claim": number[];
+    "claim_length_ascii": number;
+    "claim_index_b64": number;
+    "claim_length_b64": number;
+    "subject_pin": bigint;
+    "key_claim_name_length": number;
+    "key_claim_colon_index": number;
+    "key_claim_value_start": number;
+    "key_claim_value_length": number;
+}
+
+export interface NonceCheckInputs {
+    "extended_nonce": number[];
+    "nonce_claim_index_b64": number;
+    "nonce_length_b64": number;
+    "eph_public_key": [bigint, bigint];
+    "max_epoch": number;
+    "jwt_randomness": bigint;
+}
+
+export class ZKInputs implements KCCheckInputs, NonceCheckInputs {
+    "content": bigint[];
+    "num_sha2_blocks": number;
+
+    "payload_start_index": number;
+    "payload_len": number;
+
+    "mask": bit[];
+
+    // KCCheckInputs
+    "extended_key_claim": number[];
+    "claim_length_ascii": number;
+    "claim_index_b64": number;
+    "claim_length_b64": number;
+    "subject_pin": bigint;
+    "key_claim_name_length": number;
+    "key_claim_colon_index": number;
+    "key_claim_value_start": number;
+    "key_claim_value_length": number;
+
+    // NonceCheckInputs
+    "extended_nonce": number[];
+    "nonce_claim_index_b64": number;
+    "nonce_length_b64": number;
+    "eph_public_key": [bigint, bigint];
+    "max_epoch": number;
+    "jwt_randomness": bigint;
+
+    "all_inputs_hash": bigint;
+}
+
+// AuxInputs minus JWT signature
+export class PartialAuxInputs {
+    "masked_content": number[];
+    "jwt_sha2_hash": [bigint, bigint];
+    "payload_start_index": number;
+    "payload_len": number;
+    "eph_public_key": [bigint, bigint];
+    "max_epoch": number;
+    "num_sha2_blocks": number;
+    "key_claim_name": string;
+    "addr_seed": bigint;
+}
+
+// ZKLoginSig minus the tx signature
+export class PartialZKLoginSig {
+    "zkproof": any;
+    "public_inputs": any;
+    "auxiliary_inputs": PartialAuxInputs;
+}
+
+export interface CircuitConstants {
+    max_padded_unsigned_jwt_len: number,
+    max_extended_key_claim_len: number,
+    max_key_claim_name_len: number,
+    max_key_claim_value_len: number,
+}
+
+export const circuit_params: CircuitConstants = {
+    max_padded_unsigned_jwt_len: 64*12,
+    max_extended_key_claim_len: 66, // name + value + 6 chars (four '"', one ':' and one ',' / '}')
+    max_key_claim_name_len: 10,
+    max_key_claim_value_len: 50,
+};
+
+export const constants = {
+    P: 21888242871839275222246405745257275088548364400416034343698204186575808495617n,
+    flag: 5,
+    inWidth: 8,
+    // const eph_public_key = BigInt("0x" + crypto.randomBytes(32).toString('hex'));
+    dev: { // NOTE: Constants meant to be used for dev
+        pin: 283089722053851751073973683904920435104n,
+        ephPK: 0x0d7dab358c8dadaa4efa0049a75b07436555b10a368219bb680f70571349d775n,
+        maxEpoch: 10000,
+        jwtRand: 100681567828351849884072155819400689117n
+    },
+    pack_width: 248,
+    maskValue: '='.charCodeAt(0),
+    nonceLen: Math.ceil(256 / 6), // 43
+    extNonceLen: Math.ceil(256 / 6) + 11, // 11 for prefix and suffix
+    claimsToReveal: ["iss", "aud"]
+}
diff --git a/zklogin-circuits/src/decideparams.js b/zklogin-circuits/src/decideparams.js
new file mode 100644
index 0000000000..e6503775a6
--- /dev/null
+++ b/zklogin-circuits/src/decideparams.js
@@ -0,0 +1,64 @@
+const jwtutils = require("./jwtutils");
+
+const GOOGLE = require("../testvectors/realJWTs").google;
+const TWITCH = require("../testvectors/realJWTs").twitch;
+const BUFFER1 = 0.15; // 15 percent
+const BUFFER2 = 0.15; // 15 percent
+
+// JWT header + JWT payload + SHA2 padding 
+function computeNumSHA2Blocks(jwt) {
+    const header = jwt.split('.')[0];
+    const payload = jwt.split('.')[1];
+
+    const jwtMaxLen = header.length + payload.length;
+
+    const L = jwtMaxLen * 8;
+    const K = (512 + 448 - (L % 512 + 1)) % 512;
+
+    const paddingLen = 1 + K + 64;
+
+    if ((L + paddingLen) % 512 !== 0) {
+        throw new Error("Shouldn't happen... Invalid implementation");
+    }
+
+    return (L + paddingLen) / 512;
+}
+
+function computeSubLen(jwt) {
+    const payload = Buffer.from(jwt.split('.')[1], 'base64url').toString();
+    return jwtutils.getExtendedClaim(payload, "sub").length;
+}
+
+function computeAudLen(jwt) {
+    const payload = Buffer.from(jwt.split('.')[1], 'base64url').toString();
+    return jwtutils.getExtendedClaim(payload, "aud").length;
+}
+
+function decide(jwt, buffer1, buffer2) {
+    const p1 = computeNumSHA2Blocks(jwt);
+    // TODO: Move from floor to ceil?
+    const maxSHA2Blocks = Math.floor(p1 * (1 + buffer1));
+    console.log(`SHA2 blocks: ${p1}, Max SHA2 blocks: ${maxSHA2Blocks}`);
+
+    const p2 = computeSubLen(jwt);
+    var maxSubLen = Math.floor(p2 * (1 + buffer2));
+
+    // Round maxSubLen to the nearest multiple of 3
+    maxSubLen = Math.ceil(maxSubLen / 3) * 3;
+    console.log(`Sub length: ${p2}, Max sub length: ${maxSubLen}`);
+
+    const p3 = computeAudLen(jwt);
+    var maxAudLen = Math.floor(p3 * (1 + buffer2));
+
+    // Round maxAudLen to the nearest multiple of 3
+    maxAudLen = Math.ceil(maxAudLen / 3) * 3;
+    console.log(`Aud length: ${p3}, Max aud length: ${maxAudLen}`);
+}
+
+if (require.main === module) {
+    console.log("GOOGLE");
+    decide(GOOGLE.jwt, BUFFER1, BUFFER2);
+
+    console.log("TWITCH");
+    decide(TWITCH.jwt, BUFFER1, BUFFER2);
+}
\ No newline at end of file
diff --git a/zklogin-circuits/src/genAddrSeed.ts b/zklogin-circuits/src/genAddrSeed.ts
new file mode 100644
index 0000000000..a9c2b76497
--- /dev/null
+++ b/zklogin-circuits/src/genAddrSeed.ts
@@ -0,0 +1,27 @@
+import { deriveAddrSeed } from './utils';
+import { constants } from './common';
+
+if (require.main === module) {
+    const claimValue = process.argv[2];
+    if (!claimValue) {
+        console.log("Usage: ts-node genAddrSeed.ts <claim_value>");
+        process.exit(1);
+    }
+
+    let pin = process.argv[3];
+    if (!pin) {
+        pin = constants.dev.pin.toString();
+        console.log("Using default pin:", pin);
+    }
+
+    (async () => {
+        try {
+            const res = await deriveAddrSeed(claimValue, BigInt(pin));
+            console.log(res.toString());
+            process.exit(0);
+        }
+        catch (err) {
+            console.error("Error:", (err as Error).message);
+        }
+    })();
+}
diff --git a/zklogin-circuits/src/genNonce.ts b/zklogin-circuits/src/genNonce.ts
new file mode 100644
index 0000000000..4a8a76a9cd
--- /dev/null
+++ b/zklogin-circuits/src/genNonce.ts
@@ -0,0 +1,14 @@
+import * as circuitutils from './circuitutils';
+
+if (require.main === module) {
+    (async () => {
+        try {
+            const res = await circuitutils.computeNonce();
+            console.log("nonce", res);
+            process.exit(0);
+        }
+        catch (err) {
+            console.error("Error:", (err as Error).message);
+        }
+    })();
+}
\ No newline at end of file
diff --git a/zklogin-circuits/src/genZKPinputs.ts b/zklogin-circuits/src/genZKPinputs.ts
new file mode 100644
index 0000000000..0191d35b61
--- /dev/null
+++ b/zklogin-circuits/src/genZKPinputs.ts
@@ -0,0 +1,92 @@
+#!/usr/bin/env node
+
+import fs from 'fs';
+import * as circuit from './circuitutils';
+import * as utils from './utils';
+import { WalletInputs, circuit_params} from './common';
+
+function readJsonFile(filename: string): WalletInputs {
+    try {
+        if (!fs.existsSync(filename)) {
+            throw new Error(`File doesn't exist: ${filename}`);
+        }
+
+        const rawdata = fs.readFileSync(filename);
+        const jsonData = JSON.parse(rawdata.toString());
+
+        // Validate JSON structure
+        const requiredFields = ['jwt', 'eph_public_key', 'max_epoch', 'jwt_rand', 'user_pin', 'key_claim_name'];
+        for (let field of requiredFields) {
+            if (!jsonData.hasOwnProperty(field)) {
+                throw new Error(`Missing required field: ${field}`);
+            }
+        }
+
+        const walletinputs: WalletInputs = {
+            unsigned_jwt: jsonData.jwt,
+            eph_public_key: BigInt(jsonData.eph_public_key),
+            max_epoch: Number(jsonData.max_epoch),
+            jwt_rand: BigInt(jsonData.jwt_rand),
+            user_pin: BigInt(jsonData.user_pin),
+            key_claim_name: jsonData.key_claim_name,
+        }
+
+        return walletinputs;
+    } catch (err) {
+        console.error(`Error reading or parsing JSON file: ${(err as Error).message}`);
+        process.exit(1);  // Exit with failure status
+    }
+}
+
+async function genZKPInputs (
+    walletinputs: WalletInputs,
+    ZKP_INPUTS_FILE_PATH: string,
+    AUX_INPUTS_FILE_PATH: string
+) {
+    const [zk_inputs, auxiliary_inputs] = await circuit.genZKLoginInputs(
+        walletinputs, 
+        circuit_params
+    );
+
+    console.log(`Writing inputs to ${ZKP_INPUTS_FILE_PATH}...`);
+    utils.writeJSONToFile(zk_inputs, ZKP_INPUTS_FILE_PATH);
+
+    console.log(`Writing auxiliary inputs to ${AUX_INPUTS_FILE_PATH}...`);
+    utils.writeJSONToFile(auxiliary_inputs, AUX_INPUTS_FILE_PATH);
+};
+
+if (require.main === module) {
+    if (process.argv.length < 3) {
+        console.log("Usage: node genZKPinputs.js <walletinputs.json> <zkinputs.json> <auxinputs.json>");
+        console.log("Last two arguments are optional. If not specified, default filenames will be used.");
+        process.exit(1);
+    }
+
+    const data = readJsonFile(process.argv[2]);
+
+    let zk_inputs_file = process.argv[3];
+    // If no output file is specified, use the default
+    if (!zk_inputs_file) {
+        zk_inputs_file = "zkinputs.json";
+    }
+
+    let aux_inputs_file = process.argv[4];
+    // If no output file is specified, use the default
+    if (!aux_inputs_file) {
+        aux_inputs_file = "auxinputs.json";
+    }
+
+    // Log the data object
+    console.log(data);
+
+    (async () => {
+        try {
+            await genZKPInputs(
+                data, zk_inputs_file, aux_inputs_file
+            );
+            process.exit(0);
+        } catch (error) {
+            console.error("Error in processJWT:", error);
+        }
+    })();
+}
diff --git a/zklogin-circuits/src/jwtutils.ts b/zklogin-circuits/src/jwtutils.ts
new file mode 100644
index 0000000000..240c3b3be6
--- /dev/null
+++ b/zklogin-circuits/src/jwtutils.ts
@@ -0,0 +1,319 @@
+function removeSig(jwt: string): string {
+    const parts = jwt.split('.');
+    if (parts.length != 3) {
+        throw new Error("Invalid JWT: " + jwt);
+    }
+    return parts[0] + "." + parts[1];
+}
+
+function getClaimValue(decoded_payload: string, claim: string): any {
+    const json_input = JSON.parse(decoded_payload);
+    if (!json_input.hasOwnProperty(claim)) {
+        throw new Error("Field " + claim + " not found in " + decoded_payload);
+    }
+    return json_input[claim];
+}
+
+// A copy of getExtendedClaim but returns more things. TODO: Refactor and merge the two.
+function parseClaim(decoded_payload: string, claim_name: string) {
+    const raw_value = JSON.stringify(getClaimValue(decoded_payload, claim_name));
+    const kv_pair = `"${claim_name}":${raw_value}`;
+    const claimStart = decoded_payload.indexOf(`"${claim_name}"`);
+    const kv_pair_expanded = kv_pair + decoded_payload[claimStart + kv_pair.length];
+
+    if (!decoded_payload.includes(kv_pair_expanded)) {
+        throw new Error("Unimplemented. The string " + kv_pair_expanded + " is not found in " + decoded_payload);
+    }
+
+    var lastchar = kv_pair_expanded[kv_pair_expanded.length - 1];
+    if (!(lastchar == '}' || lastchar == ',')) {
+        throw new Error("Something is wrong with the decoded payload");
+    }
+    return {
+        "raw_value": raw_value,
+        "extended_claim": kv_pair_expanded,
+        "colon_index": kv_pair_expanded.indexOf(":"),
+        "value_index": kv_pair_expanded.indexOf(raw_value), // index of value in the extended claim
+    };
+}
+
+/**
+ * Returns a claim as it appears in the decoded JWT.
+ * We take a conservative approach, e.g., assume that the claim value does not have spaces. 
+ * In such cases, the code will fail.
+ * The last character after the claim value is also returned (either a comma or a closing brace).
+ * 
+ * @param {*} decoded_payload e.g., {"sub":"1234567890","name":"John Doe","iat":1516239022} 
+ * @param {*} claim e.g., sub
+ * @returns e.g., "sub":"1234567890",
+ */
+function getExtendedClaim(decoded_payload: string, claim: string): string {
+    const json_input = JSON.parse(decoded_payload);
+    if (!json_input.hasOwnProperty(claim)) {
+        throw new Error("Field " + claim + " not found in " + decoded_payload);
+    }
+
+    const field_value = JSON.stringify(json_input[claim]);
+    const kv_pair = `"${claim}":${field_value}`;
+    const claimStart = decoded_payload.indexOf(`"${claim}"`);
+    const kv_pair_expanded = kv_pair + decoded_payload[claimStart + kv_pair.length];
+
+    if (decoded_payload.includes(kv_pair_expanded)) {
+        var lastchar = kv_pair_expanded[kv_pair_expanded.length - 1];
+        if (!(lastchar == '}' || lastchar == ',')) {
+            throw new Error("Something is wrong with the decoded payload");
+        }
+        return kv_pair_expanded;
+    }
+
+    // Facebook is escaping the '/' characters in the JWT payload
+    const escaped_field_value = field_value.replace(/([/])/g, '\\$1');
+    const escaped_kv_pair = `"${claim}":${escaped_field_value}`;
+    const escaped_kv_pair_expanded = escaped_kv_pair + 
+        decoded_payload[claimStart + escaped_kv_pair.length];
+    
+    if (decoded_payload.includes(escaped_kv_pair_expanded)) {
+        var lastchar = escaped_kv_pair_expanded[escaped_kv_pair_expanded.length - 1];
+        if (!(lastchar == '}' || lastchar == ',')) {
+            throw new Error("Something is wrong with the decoded payload");
+        }
+
+        return escaped_kv_pair_expanded;
+    }
+
+    throw new Error("Fields " + kv_pair_expanded + " or " + escaped_kv_pair_expanded + " not found in " + decoded_payload);
+}
+
+/**
+ * @param {String} payload  A Base64 encoded string, e.g., a JWT
+ * @param {String} field    A claim string, e.g., "sub" (without quotes)
+ * @returns [start, length] The start index and length of the (base64) encoded 
+ *                          claim string in the input. 
+ * 
+ * The returned indices are tight, i.e., both payload[start] and payload[start + length - 1] 
+ *  contain at least some bits of the claim string.
+ */
+function indicesOfB64(payload: string, field: string): [number, number] {
+    const decoded = Buffer.from(payload, 'base64url').toString();
+    
+    const kv_pair = getExtendedClaim(decoded, field);
+    const start_index_ascii = decoded.indexOf(kv_pair);
+    const length_b64 = b64Len(kv_pair.length, start_index_ascii);
+    const start_b64 = b64Index(start_index_ascii);
+
+    // test
+    const expectedB64Variant = payload.slice(start_b64, start_b64 + length_b64);
+    if (payload.indexOf(expectedB64Variant) == -1) {
+        throw new Error("Field " + kv_pair + " not found in the Base64");
+    }
+
+    return [start_b64, length_b64];
+}
+
+// If a character appears at an index i in a string S, 
+//  return the index at which it would appear in the base64 representation of S
+function b64Index(i: number): number {
+    var q = 4 * Math.floor(i / 3);
+
+    if (i % 3 == 0) {
+        /**
+         * - - - - <=> . . .
+         * - - - - <=> . . .
+         * X x     <=> i  
+         */
+        return q;
+    } else if (i % 3 == 1) {
+        /**
+         * - - - - <=> . . .
+         * - - - - <=> . . .
+         * _ X x   <=> . i  
+         */
+        return q + 1;
+    } else if (i % 3 == 2) {
+        /**
+         * - - - - <=> . . .
+         * - - - - <=> . . .
+         * _ _ X x <=> . . i  
+         */
+        return q + 2;
+    } else {
+        throw new Error(`Something is wrong with the index ${i}`);
+    }
+}
+
+// Given an ascii string of length n starting at index i, 
+//  return the length of its base64 representation
+function b64Len(n: number, i: number): number {
+    var q = 4 * Math.floor(n / 3);
+    if (i % 3 == 0) {
+        if (n % 3 == 0) {
+            /**
+             * a - - => 4
+             * - - b => 4
+             */
+            return q;
+        } else if (n % 3 == 1) {
+            /**
+             * a - - => 4
+             * - - - => 4
+             * b     => 2
+             */
+            return q + 2;
+        } else {
+            /**
+             * a - - => 4
+             * - - - => 4
+             * - b   => 3
+             */
+            return q + 3;
+        }
+    } else if (i % 3 == 1) {
+        if (n % 3 == 0) {
+            /**
+             *   a - => 3
+             * - - - => 4
+             * b     => 2
+             */
+            return q + 1;
+        } else if (n % 3 == 1) {
+            /**
+             *   a - => 3
+             * - - - => 4
+             * - b   => 3
+             */
+            return q + 2;
+        } else {
+            /**
+             *   a - => 3
+             * - - - => 4
+             * - - b => 4
+             */
+            return q + 3;
+        }
+    } else if (i % 3 == 2) {
+        if (n % 3 == 0) {
+            /**
+             *     a => 2
+             * - - - => 4
+             * - b   => 3
+             */
+            return q + 1;
+        } else if (n % 3 == 1) {
+            /**
+             *     a => 2
+             * - - - => 4
+             * - - b => 4
+             */
+            return q + 2;
+        } else {
+            /**
+             *     a => 2
+             * - - - => 4
+             * - - - => 4
+             * b     => 2
+             */
+            return q + 4;
+        }
+    } else {
+        throw new Error(`Something is wrong with the index ${i}`);
+    }
+}
+
+function base64UrlCharTo6Bits(base64UrlChar: string): number[] {
+    if (base64UrlChar.length !== 1) {
+        throw new Error('Invalid base64Url character: ' + base64UrlChar);
+    }
+
+    // Define the base64URL character set
+    const base64UrlCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+  
+    // Find the index of the input character in the base64URL character set
+    const index = base64UrlCharacterSet.indexOf(base64UrlChar);
+
+    if (index === -1) {
+        throw new Error('Invalid base64Url character: ' + base64UrlChar);
+    }
+  
+    // Convert the index to a 6-bit binary string
+    const binaryString = index.toString(2).padStart(6, '0');
+  
+    // Convert the binary string to an array of bits
+    const bits = Array.from(binaryString).map(Number);
+  
+    return bits;
+}
+
+function base64UrlStringToBitVector(base64UrlString: string) {
+    let bitVector: number[] = [];
+    for (let i = 0; i < base64UrlString.length; i++) {
+        const base64UrlChar = base64UrlString.charAt(i);
+        const bits = base64UrlCharTo6Bits(base64UrlChar);
+        bitVector = bitVector.concat(bits);
+    }
+    return bitVector;
+}
+  
+/**
+ * Decode a Base64URL substring `s` that appears at index `i` of a valid Base64URL string.
+ *
+ * @param {string} s - a Base64URL substring
+ * @param {number} i - the index at which `s` appears in the Base64URL string
+ * @returns {string} the decoded string
+ *  
+ * Like before, we assume tight packing, i.e., both s[i] and s[i + s.length - 1] carry 
+ *  non-zero bits of the encoded string.
+ */
+function decodeBase64URL(s: string, i: number): string {
+    if (s.length < 2) {
+        throw new Error(`Input (s = ${s}) is not tightly packed because s.length < 2`);
+    }
+    var bits = base64UrlStringToBitVector(s);
+
+    const first_char_offset = i % 4;
+    if (first_char_offset == 0) {
+        // skip
+    } else if (first_char_offset == 1) {
+        bits = bits.slice(2);
+    } else if (first_char_offset == 2) {
+        bits = bits.slice(4);
+    } else { // (offset == 3)
+        throw new Error(`Input (s = ${s}) is not tightly packed because i%4 = 3 (i = ${i}))`);
+    }
+
+    const last_char_offset = (i + s.length - 1) % 4;
+    if (last_char_offset == 3) {
+        // skip
+    } else if (last_char_offset == 2) {
+        bits = bits.slice(0, bits.length - 2);
+    } else if (last_char_offset == 1) {
+        bits = bits.slice(0, bits.length - 4);
+    } else { // (offset == 0)
+        throw new Error(`Input (s = ${s}) is not tightly packed because (i + s.length - 1)%4 = 0 (i = ${i}))`);
+    }
+
+    if (bits.length % 8 != 0) {
+        throw new Error(`We should never reach here...`);
+    }
+
+    var bytes = [];
+    for (let i = 0; i < bits.length; i += 8) {
+        const bitChunk = bits.slice(i, i + 8);
+    
+        // Convert the 8-bit chunk to a byte and add it to the bytes array
+        const byte = parseInt(bitChunk.join(''), 2);
+        bytes.push(byte);
+    }
+    
+    return Buffer.from(bytes).toString();
+}
+
+export {
+    removeSig,
+    getClaimValue,
+    parseClaim,
+    getExtendedClaim,
+    indicesOfB64,
+    b64Len,
+    decodeBase64URL,
+    base64UrlCharTo6Bits
+}
\ No newline at end of file
diff --git a/zklogin-circuits/src/prove.ts b/zklogin-circuits/src/prove.ts
new file mode 100644
index 0000000000..ca297e4e35
--- /dev/null
+++ b/zklogin-circuits/src/prove.ts
@@ -0,0 +1,213 @@
+const snarkjs = require("snarkjs");
+
+import fs from 'fs';
+import * as circuit from './circuitutils';
+import { constants, circuit_params as P, PartialZKLoginSig, WalletInputs } from './common';
+import * as utils from './utils';
+import * as verifier from './verify';
+import { GOOGLE, TWITCH } from "../testvectors/realJWTs";
+import yargs from 'yargs';
+import { hideBin } from 'yargs/helpers';
+import { JWK } from 'jwk-to-pem';
+
+const ARTIFACTS_DIR = "./artifacts";
+const PROJ_NAME = "zklogin";
+const PROOF_DIR = ARTIFACTS_DIR + "/proof";
+
+const groth16Prove = async (inputs: any, wasm_file: string, zkey_file: string) => {
+    const { proof, publicSignals } = await snarkjs.groth16.fullProve(
+        inputs, 
+        wasm_file, 
+        zkey_file
+    );
+
+    return { proof, publicSignals };
+}
+
+const groth16Verify = async (proof: any, public_inputs: any, vkey_file: string) => {
+    const vkey = JSON.parse(fs.readFileSync(vkey_file, 'utf-8'));
+
+    const res = await snarkjs.groth16.verify(vkey, public_inputs, proof);
+
+    if (res === true) {
+        console.log("Verification OK");
+    } else {
+        throw new Error("Invalid Proof");
+    }
+}
+
+// Generate a ZKP for a JWT. If a JWK is provided, the JWT is verified first (sanity check).
+async function zkOpenIDProve(
+    jwt: string,
+    ephPK: bigint,
+    maxEpoch: number,
+    jwtRand: bigint,
+    userPIN: bigint, 
+    keyClaim='sub',
+    jwk?: JWK,
+    write_to_file=false
+): Promise<PartialZKLoginSig> {
+    // Check if the JWT is a valid OpenID Connect ID Token if a JWK is provided
+    if (typeof jwk !== 'undefined') {
+        console.log("Verifying JWT with JWK...");
+        verifier.verifyJwt(jwt, jwk);
+    }
+
+    console.time('prove');
+    // Split the JWT into its three parts
+    const [header, payload, signature] = jwt.split('.');
+    const input = header + '.' + payload;
+
+    const I : WalletInputs = {
+        unsigned_jwt: input,
+        eph_public_key: ephPK,
+        max_epoch: maxEpoch,
+        jwt_rand: jwtRand,
+        user_pin: userPIN,
+        key_claim_name: keyClaim
+    };
+
+    var [inputs, auxiliary_inputs] = await circuit.genZKLoginInputs(I, P);
+
+    // Generate ZKP
+    console.log("Generating ZKP...");
+    const WASM_FILE_PATH = `${ARTIFACTS_DIR}/${PROJ_NAME}_js/${PROJ_NAME}.wasm`;
+    const ZKEY_FILE_PATH = `${ARTIFACTS_DIR}/${PROJ_NAME}.zkey`;
+    const { proof, publicSignals: public_signals } = await groth16Prove(inputs, WASM_FILE_PATH, ZKEY_FILE_PATH);
+
+    console.timeEnd('prove');
+    if (write_to_file) {
+        const PROOF_FILE_PATH = `${PROOF_DIR}/zkp.json`;
+        const AUX_INPUTS_FILE_PATH = `${PROOF_DIR}/aux.json`;
+        const PUBLIC_INPUTS_FILE_PATH = `${PROOF_DIR}/public.json`;
+
+        console.log("Writing proof...");
+        utils.writeJSONToFile(proof, PROOF_FILE_PATH);
+        utils.writeJSONToFile(public_signals, PUBLIC_INPUTS_FILE_PATH);
+        utils.writeJSONToFile(auxiliary_inputs, AUX_INPUTS_FILE_PATH);
+    }
+
+    return { 
+        "zkproof": proof, 
+        "public_inputs": public_signals,
+        "auxiliary_inputs": auxiliary_inputs
+    }
+};
+
+// Not a full implementation: only implements some of the checks. 
+// For a full implementation, see the Authenticator code in Rust. 
+const zkOpenIDVerify = async (proof: PartialZKLoginSig) => {
+    const { zkproof, public_inputs, auxiliary_inputs: auxiliary_inputs } = proof; 
+
+    // Verify ZKP
+    console.log("Verifying ZKP...");
+    console.time('zk verify');
+    const VKEY_FILE_PATH = `${ARTIFACTS_DIR}/${PROJ_NAME}.vkey`;
+    await groth16Verify(zkproof, public_inputs, VKEY_FILE_PATH);
+    console.timeEnd('zk verify');
+
+    verifier.verifyAuxInputs(auxiliary_inputs, P.max_padded_unsigned_jwt_len);
+}
+
+type CliArgs = {
+    provider: string;
+    jwt: string;
+    eph_public_key: string;
+    max_epoch: string;
+    jwt_rand: string;
+    user_pin: string;
+    key_claim_name: string;
+    public_key_path: string;
+};
+
+if (require.main === module) {
+    const argv = yargs(hideBin(process.argv))
+        .option('provider', {
+            alias: 'p',
+            type: 'string',
+            description: 'Specify the provider',
+            default: 'google',
+            choices: ['google', 'twitch']
+        })
+        .option('jwt', {
+            alias: 'j',
+            type: 'string',
+            description: 'JWT token'
+        })
+        .option('eph_public_key', {
+            alias: 'e',
+            type: 'string',
+            description: 'Ephemeral public key',
+            default: constants.dev.ephPK,
+        })
+        .option('max_epoch', {
+            alias: 'm',
+            type: 'string',
+            description: 'Max epoch',
+            default: constants.dev.maxEpoch,
+        })
+        .option('jwt_rand', {
+            alias: 'r',
+            type: 'string',
+            description: 'JWT rand',
+            default: constants.dev.jwtRand,
+        })
+        .option('user_pin', {
+            alias: 'u',
+            type: 'string',
+            description: 'User PIN',
+            default: constants.dev.pin,
+        })
+        .option('key_claim_name', {
+            alias: 'k',
+            type: 'string',
+            description: 'Key claim name',
+            default: 'sub',
+        })
+        .option('public_key_path', {
+            alias: 'pk',
+            type: 'string',
+            description: 'Public key path',
+        })
+        .help()
+        .argv as unknown as CliArgs;
+
+    argv.jwt = argv.jwt || (argv.provider === "google" ? GOOGLE["jwt"] : TWITCH["jwt"]);
+
+    let jwk: JWK | undefined;
+    if (!argv.public_key_path) {
+        jwk = (argv.provider === "google") ? GOOGLE.jwk : TWITCH.jwk;
+    } else {
+        fs.readFile(argv.public_key_path, "utf8", (err, jwkJson) => {
+            if (err) {
+                console.error("Error reading JWK:", err.message);
+                process.exit(1);
+            }
+            jwk = JSON.parse(jwkJson);
+        });
+    }
+
+    console.log(`Provider -> ${argv.provider}`);
+    console.log(`JWT -> ${argv.jwt}`);
+    console.log(`Ephemeral public key -> ${argv.eph_public_key}`);
+    console.log(`Max epoch -> ${argv.max_epoch}`);
+    console.log(`JWT rand -> ${argv.jwt_rand}`);
+    console.log(`User PIN -> ${argv.user_pin}`);
+    console.log(`Key claim name -> ${argv.key_claim_name}`);
+    console.log(`Public key path -> ${argv.public_key_path}`);
+
+    (async () => {
+        try {
+            const proof = await zkOpenIDProve(argv.jwt, BigInt(argv.eph_public_key), Number(argv.max_epoch),
+                BigInt(argv.jwt_rand), BigInt(argv.user_pin), argv.key_claim_name, jwk, true);
+            console.log("--------------------");
+
+            // Verify the proof
+            await zkOpenIDVerify(proof);
+
+            process.exit(0);
+        } catch (error) {
+            console.error("Error in processJWT:", error);
+        }
+    })();
+}
\ No newline at end of file
diff --git a/zklogin-circuits/src/utils.ts b/zklogin-circuits/src/utils.ts
new file mode 100644
index 0000000000..a8f207427c
--- /dev/null
+++ b/zklogin-circuits/src/utils.ts
@@ -0,0 +1,162 @@
+import { constants, bit, circuit_params } from './common';
+import * as fs from 'fs';
+
+const pack_width = constants.pack_width;
+
+function getNumFieldElements(asciiSize: number, packWidth = pack_width): number {
+    if (packWidth % 8 !== 0) throw new Error("packWidth must be a multiple of 8");
+
+    const packWidthInBytes = packWidth / 8;
+    return Math.ceil(asciiSize / packWidthInBytes);
+}
+
+function arrayChunk<T>(array: T[], chunk_size: number): T[][] {
+    return Array(Math.ceil(array.length / chunk_size)).fill(undefined).map((_, index) => index * chunk_size).
+                map(begin => array.slice(begin, begin + chunk_size));
+}
+
+function trimEndByChar(string: string, character: string) {
+    if (character.length !== 1) throw new Error("character must be a single character");
+
+    const arr = Array.from(string);
+    const last = arr.reverse().findIndex(char => char !== character);
+    return string.substring(0, string.length - last);
+}
+
+function buffer2BitArray(b: Buffer): bit[] {
+    return b.reduce((bitArray: bit[], byte) => {
+        const binaryString = byte.toString(2).padStart(8, '0');
+        const bitValues = binaryString.split('').map(bit => bit === '1' ? 1 : 0);
+        return bitArray.concat(bitValues);
+    }, []);
+}
+
+function bitArray2Buffer(a: bit[]): Buffer {
+    return Buffer.from(arrayChunk(a, 8).map(byte => parseInt(byte.join(''), 2)))
+}
+
+function bigIntArray2Bits(arr: bigint[], intSize = 16): bit[] {
+    return arr.reduce((bitArray: bit[], n) => {
+        const binaryString = n.toString(2).padStart(intSize, '0');
+        const bitValues = binaryString.split('').map(bit => bit === '1' ? 1 : 0);
+        return bitArray.concat(bitValues);
+    }, []);
+}
+
+function bigIntArray2Buffer(arr: bigint[], intSize=16): Buffer {
+    return bitArray2Buffer(bigIntArray2Bits(arr, intSize));
+}
+
+// Pack into an array of chunks each outWidth bits
+function pack(inArr: bigint[], inWidth: number, outWidth: number): bigint[] {
+    const bits = bigIntArray2Bits(inArr, inWidth);
+
+    const extra_bits = bits.length % outWidth == 0 ? 0 : outWidth - (bits.length % outWidth);
+    const bits_padded = bits.concat(Array(extra_bits).fill(0));
+    if (bits_padded.length % outWidth != 0) throw new Error("Invalid logic");
+
+    const packed = arrayChunk(bits_padded, outWidth).map(chunk => BigInt("0b" + chunk.join('')));
+    return packed;
+}
+
+// Pack into exactly outCount chunks of outWidth bits each
+function pack2(inArr: bigint[], inWidth: number, outWidth: number, outCount: number): bigint[] {
+    const packed = pack(inArr, inWidth, outWidth);
+    if (packed.length > outCount) throw new Error("packed is big enough");
+
+    return packed.concat(Array(outCount - packed.length).fill(0));
+}
+
+function padWithZeroes<T>(inArr: T[], outCount: number) {
+    if (inArr.length > outCount) throw new Error("inArr is big enough");
+
+    const extra_zeroes = outCount - inArr.length;
+    const arr_padded = inArr.concat(Array(extra_zeroes).fill(0));
+    return arr_padded;
+}
+
+// A helper function to convert a string into a format that can be fed into the circuit
+function strToVec(str: string, maxLen: number): number[] {
+    const arr = Array.from(str).map(c => c.charCodeAt(0));
+    return padWithZeroes(arr, maxLen);
+}
+
+// Poseidon is marked as any because circomlibjs does not have typescript definitions
+function poseidonHash(inputs: bigint[], poseidon: any) {
+    if (inputs.length == 1) {
+        return poseidon.F.toObject(poseidon([inputs]))
+    } else if (inputs.length <= 15) {
+        return poseidon.F.toObject(poseidon(inputs))
+    } else if (inputs.length <= 30) {
+        const hash1 = poseidon(inputs.slice(0, 15));
+        const hash2 = poseidon(inputs.slice(15));
+        return poseidon.F.toObject(poseidon([hash1, hash2]));
+    } else {
+        throw new Error(`Yet to implement: Unable to hash a vector of length ${inputs.length}`);
+    }
+}
+
+/**
+ * 
+ * @param input A Base64-decoded JWT chunked into bytes
+ * @param mask A vector of 0s and 1s of the same length as input
+ * @returns 
+ */
+function applyMask(input: number[], mask: bit[]) {
+    if (input.length != mask.length) {
+        throw new Error("Input and mask must be of the same length");
+    }
+    return input.map((charCode, index) => (mask[index] == 1) 
+                ? charCode
+                : constants.maskValue
+            );
+}
+
+async function deriveAddrSeed(
+    claim_value: string, pin: bigint,
+    maxKeyClaimValueLen = circuit_params.max_key_claim_value_len,
+    packWidth = pack_width
+): Promise<bigint> {
+    const claim_val_F = await mapToField(claim_value, maxKeyClaimValueLen, packWidth);
+    const buildPoseidon = require("circomlibjs").buildPoseidon;
+    const poseidon = await buildPoseidon();
+
+    return poseidonHash([
+        claim_val_F, pin
+    ], poseidon);
+}
+
+// Map str into a field element after padding it to maxSize chars
+async function mapToField(str: string, maxSize: number, packWidth=pack_width) {
+    if (str.length > maxSize) {
+        throw new Error(`String ${str} is longer than ${maxSize} chars`);
+    }
+
+    const numElements = getNumFieldElements(maxSize, packWidth);
+    const packed = pack2(str.split('').map(c => BigInt(c.charCodeAt(0))), 8, packWidth, numElements);
+
+    const buildPoseidon = require("circomlibjs").buildPoseidon;
+    const poseidon = await buildPoseidon();
+    return poseidonHash(packed, poseidon);
+}
+
+function writeJSONToFile(inputs: object, file_name = "inputs.json") {
+    fs.writeFileSync(file_name, JSON.stringify(inputs, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2));
+}
+
+export {
+    arrayChunk,
+    trimEndByChar,
+    buffer2BitArray,
+    bitArray2Buffer,
+    bigIntArray2Bits,
+    bigIntArray2Buffer,
+    applyMask,
+    padWithZeroes,
+    strToVec,
+    pack,
+    deriveAddrSeed,
+    poseidonHash,
+    writeJSONToFile,
+    mapToField
+}
diff --git a/zklogin-circuits/src/verify.ts b/zklogin-circuits/src/verify.ts
new file mode 100644
index 0000000000..a14f509ea5
--- /dev/null
+++ b/zklogin-circuits/src/verify.ts
@@ -0,0 +1,121 @@
+import * as jwt from "jsonwebtoken";
+import jwkToPem, { JWK } from "jwk-to-pem";
+import * as jwtutils from './jwtutils';
+import { toBigIntBE } from "bigint-buffer";
+import { PartialAuxInputs } from "./common";
+
+// JWT Token, JWK Public Key
+const verifyJwt = (token: string, jwkPublicKey: JWK) => {
+    try {
+        // Convert the JWK to PEM format
+        const publicKey = jwkToPem(jwkPublicKey);
+
+        const verifyOptions: jwt.VerifyOptions = {
+            algorithms: ["RS256"],
+            ignoreExpiration: true
+        };
+
+        const decoded = jwt.verify(token, publicKey, verifyOptions);
+        console.log("JWT is valid:", decoded);
+    } catch (error) {
+        console.error("Invalid JWT:", (error as Error).message);
+    }
+};
+
+// A partial implementation of the on-chain proof verification logic. Only checks the masked_content.
+const verifyAuxInputs = (auxiliary_inputs: PartialAuxInputs, MAX_JWT_LENGTH: number) => {
+    checkMaskedContent(
+        auxiliary_inputs["masked_content"],
+        auxiliary_inputs["num_sha2_blocks"],
+        auxiliary_inputs["payload_start_index"],
+        auxiliary_inputs["payload_len"],
+        MAX_JWT_LENGTH
+    );
+}
+
+function checkMaskedContent (
+    masked_content: number[], 
+    num_sha2_blocks: number,
+    expected_payload_start_index: number,
+    expected_payload_len: number,
+    expected_length: number
+) {
+    if (masked_content.length != expected_length) throw new Error("Invalid length");
+    if (num_sha2_blocks * 64 > masked_content.length) throw new Error("Invalid last block");
+
+    // Process any extra padding
+    const extra_padding = masked_content.slice(num_sha2_blocks * 64);
+    console.log("Length of extra padding:", extra_padding.length);
+    if (extra_padding.length !== 0) {
+        if (extra_padding.some(e => e != 0)) throw new Error("Invalid extra padding");
+        masked_content = masked_content.slice(0, num_sha2_blocks * 64);
+    }
+
+    // Process header
+    const header_length = masked_content.indexOf('.'.charCodeAt(0));
+    if (header_length == -1 || header_length != expected_payload_start_index - 1) throw new Error("Invalid header length");
+
+    const encodedHeader = masked_content.slice(0, header_length).map(e => String.fromCharCode(e)).join('');
+    const header = Buffer.from(encodedHeader, 'base64url').toString('utf8');
+    // console.log("header", header);
+    // ...JSON Parse header...
+
+    // Process SHA-2 padding
+    const payload_and_sha2pad = masked_content.slice(header_length + 1);
+    const header_and_payload_len_in_bits_bigint =  toBigIntBE(Buffer.from(payload_and_sha2pad.slice(-8)));
+    if (header_and_payload_len_in_bits_bigint > Number.MAX_SAFE_INTEGER) { // 2^53 - 1
+        throw new Error("Too large header_and_payload_len_in_bits");
+    }
+    // casting to a number should work for our use case as the numbers aren't big
+    const header_and_payload_len_in_bits = Number(header_and_payload_len_in_bits_bigint);
+    if (header_and_payload_len_in_bits % 8 != 0) throw new Error("Invalid header_and_payload_len_in_bits");
+    const header_and_payload_len = header_and_payload_len_in_bits / 8;
+
+    const payload_len = header_and_payload_len - expected_payload_start_index;
+    if (payload_len != expected_payload_len) throw new Error(`Invalid payload length: ${payload_len} != ${expected_payload_len}`);
+
+    const payload = payload_and_sha2pad.slice(0, payload_len);
+    const sha2pad = payload_and_sha2pad.slice(payload_len);
+
+    if (sha2pad[0] != 128) throw new Error("Invalid sha2pad start byte");
+    if (sha2pad.slice(1, -8).some(e => e != 0)) throw new Error("Invalid sha2pad");
+    // TODO: Check that the length of sha2pad.slice(1, -8) satisfies 4.1(b) from https://datatracker.ietf.org/doc/html/rfc4634#section-4.1
+
+    // Process payload
+    const maskedPayload = payload.map(e => String.fromCharCode(e)).join('');
+    console.log("Masked payload:", maskedPayload);
+    const claims = extractClaims(maskedPayload);
+    console.log("Revealed claims:", claims);
+
+    for (const claim of claims) {
+        if (claim[0] !== '"') {
+            // First character of each extracted_claim must be '"' (extractClaims omits partial bits at the start)
+            console.log("Invalid claim", claim);
+            throw new Error("Invalid claim");
+        }
+
+        if (!(claim.slice(-1) === '}' || claim.slice(-1) === ',')) {
+            // Last character of each extracted_claim must be '}' or ','
+            console.log("Invalid claim", claim);
+            throw new Error("Invalid claim");
+        }
+    }
+}
+
+// Extracts the claims from the masked payload.
+// 1. Extract continguous sets of non-masked characters
+// 2. For each group of Base64 chars, find its starting index and prefix-pad with enough '0's before Base64 decoding.
+function extractClaims(maskedPayload: string) {
+    return maskedPayload.split(/=+/).filter(e => e !== '').map(
+        e => {
+            const pos = maskedPayload.indexOf(e);
+            return jwtutils.decodeBase64URL(e, pos % 4);
+        }
+    );
+}
+
+export {
+    verifyJwt, 
+    verifyAuxInputs,
+    extractClaims
+};
\ No newline at end of file
diff --git a/zklogin-circuits/test/base64.circom.test.js b/zklogin-circuits/test/base64.circom.test.js
new file mode 100644
index 0000000000..8a8186f6d3
--- /dev/null
+++ b/zklogin-circuits/test/base64.circom.test.js
@@ -0,0 +1,36 @@
+const path = require("path");
+const assert = require("chai").assert;
+
+const testutils = require("./testutils");
+const jwtutils = require("../js/src/jwtutils");
+
+describe("Base64 checks", () => {
+    before(async() => {
+        cir = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "base64.circom"), "Base64URLToBits");
+        await cir.loadSymbols();
+    });
+
+    it("Should convert all valid base64 url characters", async () => {
+        const input = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+
+        for (let i = 0; i < input.length; i++) {
+            const witness = await cir.calculateWitness({ "in": input.charCodeAt(i) });
+            const output = testutils.getWitnessArray(witness, cir.symbols, "main.out");
+            assert.deepEqual(output.map(Number), jwtutils.base64UrlCharTo6Bits(input.charAt(i)));
+        }
+    })
+
+    it("Should fail for non-base64 characters", async () => {
+        const base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+        const ascii = base64.split('').map(x => x.charCodeAt(0));
+
+        // generate all possible 8-bit values that are not valid base64 characters
+        for (let i = 0; i < 256; i++) {
+            if (!ascii.includes(i)) {
+                const witness = await cir.calculateWitness({ "in": i });
+                const output = testutils.getWitnessArray(witness, cir.symbols, "main.out");
+                assert.deepEqual(output, [ 0n, 0n, 0n, 0n, 0n, 0n ]);
+            }
+        }
+    })
+})
\ No newline at end of file
diff --git a/zklogin-circuits/test/hasher.circom.test.js b/zklogin-circuits/test/hasher.circom.test.js
new file mode 100644
index 0000000000..ba6698514b
--- /dev/null
+++ b/zklogin-circuits/test/hasher.circom.test.js
@@ -0,0 +1,141 @@
+const chai = require("chai");
+const crypto = require("crypto");
+const path = require("path");
+const assert = chai.assert;
+
+const utils = require("../js/src/utils");
+
+const testutils = require("./testutils");
+
+const buildPoseidon = require("circomlibjs").buildPoseidon;
+
+const poseidonlite = require("poseidon-lite");
+
+const poseidonNumToHashFN = [
+    undefined,
+    poseidonlite.poseidon1,
+    poseidonlite.poseidon2,
+    poseidonlite.poseidon3,
+    poseidonlite.poseidon4,
+    poseidonlite.poseidon5,
+    poseidonlite.poseidon6,
+    poseidonlite.poseidon7,
+    poseidonlite.poseidon8,
+    poseidonlite.poseidon9,
+    poseidonlite.poseidon10,
+    poseidonlite.poseidon11,
+    poseidonlite.poseidon12,
+    poseidonlite.poseidon13,
+    poseidonlite.poseidon14,
+    poseidonlite.poseidon15,
+];
+
+function litePoseidonHash(inputs) {
+    const hashFN = poseidonNumToHashFN[inputs.length];
+    if (hashFN) {
+        return hashFN(inputs);
+    } else if (inputs.length <= 30) {
+        const hash1 = litePoseidonHash(inputs.slice(0, 15));
+        const hash2 = litePoseidonHash(inputs.slice(15));
+        return litePoseidonHash([hash1, hash2]);
+    } else {
+        throw new Error(
+            `Yet to implement: Unable to hash a vector of length ${inputs.length}`
+        );
+    }
+}
+
+describe("Zk-friendly hashing (Poseidon) tests", () => {
+    const P = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617");
+    const circuit_path = path.join(__dirname, "../circuits/helpers", "hasher.circom");
+
+    before(async () => {
+        poseidon = await buildPoseidon();
+    });
+
+    it("Hashes a single value", async () => {
+        cir = await testutils.genMain(circuit_path, "Hasher", [1]);
+        await cir.loadSymbols();
+        input = [1];
+        const expected_hash = utils.poseidonHash(input, poseidon);
+
+        const witness = await cir.calculateWitness({ "in": input });
+        
+        assert.deepEqual(testutils.getWitnessValue(witness, cir.symbols, "main.out"), expected_hash);
+        assert.deepEqual(litePoseidonHash(input), expected_hash);
+    });
+
+    it("Hashes two values", async () => {
+        cir = await testutils.genMain(circuit_path, "Hasher", [2]);
+        await cir.loadSymbols();
+        input = [1, 2];
+        const expected_hash = utils.poseidonHash(input, poseidon);
+        
+        const witness = await cir.calculateWitness({ "in": input });
+        
+        assert.deepEqual(testutils.getWitnessValue(witness, cir.symbols, "main.out"), expected_hash);
+        assert.deepEqual(litePoseidonHash(input), expected_hash);
+    });
+
+    it("Hashes 15 values", async () => {
+        cir = await testutils.genMain(circuit_path, "Hasher", [15]);
+        await cir.loadSymbols();
+        input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
+        const expected_hash = utils.poseidonHash(input, poseidon);
+        
+        const witness = await cir.calculateWitness({ "in": input });
+        
+        assert.deepEqual(testutils.getWitnessValue(witness, cir.symbols, "main.out"), expected_hash);
+        assert.deepEqual(litePoseidonHash(input), expected_hash);
+    });
+
+    it("Hashes 16 values", async () => {
+        cir = await testutils.genMain(circuit_path, "Hasher", [16]);
+        await cir.loadSymbols();
+        input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
+        const expected_hash = utils.poseidonHash(input, poseidon);
+        
+        const witness = await cir.calculateWitness({ "in": input });
+        
+        assert.deepEqual(testutils.getWitnessValue(witness, cir.symbols, "main.out"), expected_hash);
+        assert.deepEqual(litePoseidonHash(input), expected_hash);
+    });
+
+    it("Hashes 30 values", async () => {
+        cir = await testutils.genMain(circuit_path, "Hasher", [30]);
+        await cir.loadSymbols();
+        input = [];
+        for (let i = 0; i < 30; i++) {
+            input.push(i);
+        }
+        const expected_hash = utils.poseidonHash(input, poseidon);
+        
+        const witness = await cir.calculateWitness({ "in": input });
+        
+        assert.deepEqual(testutils.getWitnessValue(witness, cir.symbols, "main.out"), expected_hash);
+        assert.deepEqual(litePoseidonHash(input), expected_hash);
+    });
+
+    it("Nonce test", async () => {
+        cir = await testutils.genMain(circuit_path, "Hasher", [4]);
+        await cir.loadSymbols();
+
+        const ephPubKey = BigInt("0x" + crypto.randomBytes(32).toString('hex'));
+        const maxEpoch = 100;
+        const randomness = BigInt("0x" + crypto.randomBytes(31).toString('hex'));
+
+        assert.isTrue(randomness < P);
+
+        // Breaking it into two chunks to avoid overflow in case ephPubKey > P
+        const ephPubKey_0 = ephPubKey % 2n**128n;
+        const ephPubKey_1 = ephPubKey / 2n**128n;
+
+        assert.isTrue(ephPubKey_0 < P);
+        assert.isTrue(ephPubKey_1 < P);
+
+        const nonceExpected = utils.poseidonHash([ephPubKey_0, ephPubKey_1, maxEpoch, randomness], poseidon);
+        const witness = await cir.calculateWitness({in: [ephPubKey_0, ephPubKey_1, maxEpoch, randomness]}, true);
+        const nonceActual = testutils.getWitnessValue(witness, cir.symbols, "main.out");
+        assert.deepEqual(nonceActual, nonceExpected);
+    });
+});
diff --git a/zklogin-circuits/test/jwtchecks.circom.test.js b/zklogin-circuits/test/jwtchecks.circom.test.js
new file mode 100644
index 0000000000..e4f48b3db3
--- /dev/null
+++ b/zklogin-circuits/test/jwtchecks.circom.test.js
@@ -0,0 +1,74 @@
+const chai = require("chai");
+const path = require("path");
+const assert = chai.assert;
+
+const params = require('../js/src/common').circuit_params;
+const utils = require('../js/src/utils');
+const testutils = require("./testutils");
+const circuitutils = require("../js/src/circuitutils");
+
+describe("Extended claim parser", () => {
+    it("Without any whitespaces", async () => {
+        const maxKeyClaimNameLenWithQuotes = 20;
+        const maxKeyClaimValueLenWithQuotes = 30;
+        const maxExtendedClaimLen = maxKeyClaimNameLenWithQuotes + maxKeyClaimValueLenWithQuotes + 2;
+        const circuit = await testutils.genMain(
+            path.join(__dirname, "../circuits/helpers", "jwtchecks.circom"), "ExtendedClaimParser", [
+                maxExtendedClaimLen, maxKeyClaimNameLenWithQuotes, maxKeyClaimValueLenWithQuotes
+            ]
+        );
+        await circuit.loadSymbols();
+
+        const payload = JSON.stringify({
+            'sub': '1234',
+            'email': 'abcd@example.com',
+            'name': 'John Doe'
+        });
+
+        const inputs = circuitutils.genExtClaimParserInputs(payload, "email", maxExtendedClaimLen);
+        const witness = await circuit.calculateWitness(inputs, true);
+
+        const parsed_name = testutils.getWitnessArray(witness, circuit.symbols, "main.name").map(Number);
+        const parsed_value = testutils.getWitnessArray(witness, circuit.symbols, "main.value").map(Number);
+
+        assert.deepEqual(parsed_name, utils.strToVec('"email"', maxKeyClaimNameLenWithQuotes));
+        assert.deepEqual(parsed_value, utils.strToVec('"abcd@example.com"', maxKeyClaimValueLenWithQuotes));
+    });
+
+    it("With whitespaces, newlines and tabs", async () => {
+        const maxKeyClaimNameLenWithQuotes = 20;
+        const maxKeyClaimValueLenWithQuotes = 30;
+        const maxExtendedClaimLen = maxKeyClaimNameLenWithQuotes + maxKeyClaimValueLenWithQuotes + 2;
+        const circuit = await testutils.genMain(
+            path.join(__dirname, "../circuits/helpers", "jwtchecks.circom"), "ExtendedClaimParser", [
+                maxExtendedClaimLen, maxKeyClaimNameLenWithQuotes, maxKeyClaimValueLenWithQuotes
+            ]
+        );
+        await circuit.loadSymbols();
+
+        const name = '"email"'; // name.length <= maxKeyClaimNameLenWithQuotes
+        const value = '"abcd@example.com"'; // value.length <= maxKeyClaimValueLenWithQuotes
+        const extended_claim = name + '   :   \n\t ' + value + '    ,';
+        assert.isAtMost(extended_claim.length, maxExtendedClaimLen);
+        const inputs = {
+            "extended_claim": utils.padWithZeroes(extended_claim.split('').map(c => c.charCodeAt()), maxExtendedClaimLen),
+            "name_len": name.length,
+            "colon_index": extended_claim.indexOf(':'),
+            "value_start": extended_claim.indexOf(value),
+            "value_len": value.length,
+            "length": extended_claim.length,
+        };
+
+        const witness = await circuit.calculateWitness(inputs, true);
+        await circuit.checkConstraints(witness);
+
+        const parsed_name = testutils.getWitnessArray(witness, circuit.symbols, "main.name").map(Number);
+        const parsed_value = testutils.getWitnessArray(witness, circuit.symbols, "main.value").map(Number);
+
+        assert.deepEqual(parsed_name, utils.strToVec('"email"', maxKeyClaimNameLenWithQuotes));
+        assert.deepEqual(parsed_value, utils.strToVec('"abcd@example.com"', maxKeyClaimValueLenWithQuotes));
+    });
+
+    // TODO: Add failing tests
+    // TODO: Add tests for corner cases: extended_claim_len = maxExtLength, ...
+});
\ No newline at end of file
diff --git a/zklogin-circuits/test/main.circom.test.js b/zklogin-circuits/test/main.circom.test.js
new file mode 100644
index 0000000000..ef1763cd17
--- /dev/null
+++ b/zklogin-circuits/test/main.circom.test.js
@@ -0,0 +1,305 @@
+const chai = require("chai");
+const path = require("path");
+const assert = chai.assert;
+const expect = chai.expect;
+
+const jwtutils = require("../js/src/jwtutils");
+const circuitutils = require("../js/src/circuitutils");
+const constants = require('../js/src/common').constants;
+const devVars = constants.dev;
+const utils = require("../js/src/utils");
+const verify = require('../js/src/verify');
+
+const testutils = require("./testutils");
+
+const circuit_constants = require('../js/src/common').circuit_params;
+const dev_inputs = {
+    unsigned_jwt: "",
+    eph_public_key: constants.dev.ephPK,
+    max_epoch: constants.dev.maxEpoch,
+    jwt_rand: constants.dev.jwtRand,
+    user_pin: constants.dev.pin,
+    key_claim_name : "sub",
+}
+
+async function genCircuit(params = circuit_constants) {
+    return await testutils.genMain(
+        path.join(__dirname, "../circuits", "zkloginMain.circom"), "ZKLogin", [
+            params.max_padded_unsigned_jwt_len,
+            params.max_extended_key_claim_len,
+            params.max_key_claim_name_len
+        ]
+    );
+}
+
+async function genProof(
+    circuit, I, P = circuit_constants
+) {
+    var [inputs, auxiliary_inputs] = await circuitutils.genZKLoginInputs(I, P);
+    utils.writeJSONToFile(inputs, "inputs.json");
+
+    const w = await circuit.calculateWitness(inputs, true);
+    await circuit.checkConstraints(w);
+
+    verify.verifyAuxInputs(
+        auxiliary_inputs,
+        P.max_padded_unsigned_jwt_len
+    );
+
+    return [inputs, auxiliary_inputs];
+}
+
+describe("JWT Proof", function() {
+    const GOOGLE = require("../js/testvectors/realJWTs").GOOGLE;
+    const TWITCH = require("../js/testvectors/realJWTs").TWITCH;
+
+    const test_vectors = {
+        google: {
+            jwt: GOOGLE["jwt"],
+            jwk: GOOGLE["jwk"]
+        },
+        twitch: {
+            jwt: TWITCH["jwt"],
+            jwk: TWITCH["jwk"]
+        }
+    }
+
+    for (const [provider, values] of Object.entries(test_vectors)) {
+        describe(provider, async function() {
+            it.skip("Verify JWT with JWK", async function() {
+                verify.verifyJwt(values["jwt"], values["jwk"]);
+            });
+
+            it("Prove w/ sub", async function() {
+                const circuit = await genCircuit();
+                const I = {
+                    ...dev_inputs,
+                    unsigned_jwt: jwtutils.removeSig(values["jwt"])
+                }
+                await genProof(circuit, I);
+            });
+
+            it("Prove w/ email", async function() {
+                const circuit = await genCircuit();
+                const I = {
+                    ...dev_inputs,
+                    unsigned_jwt: jwtutils.removeSig(values["jwt"]),
+                    key_claim_name: "email" 
+                }
+                await genProof(circuit, I);
+            });
+        });
+    }
+});
+
+// Stringify and convert to base64. 
+// Note: Signature is omitted as this function is only meant for testing.
+function constructJWT(header, payload) {
+    header = JSON.stringify(header);
+    payload = JSON.stringify(payload);
+    const b64header = Buffer.from(header).toString('base64url');
+    const b64payload = Buffer.from(payload).toString('base64url');
+
+    if (b64header.slice(-1) === '=' || b64payload.slice(-1) === '=') {
+        throw new Error("Unexpected '=' in base64url string");
+    }
+
+    return b64header + "." + b64payload + ".";
+}
+
+describe("Tests with crafted JWTs", () => { 
+    const header = {
+        "alg":"RS256",
+        "kid":"827917329",
+        "typ":"JWT"
+    };
+    const claim_string = '"sub":"4840061",';
+    const pin = devVars.pin;
+    const nonce = "GCwq2zCuqtsa1BhaAc2SElwUoYv8jKhE6vs6Vmepu2M";
+    const payload = { // Resembles Google's JWT
+        iss: 'google.com',
+        azp: 'example.com',
+        aud: 'example.com',
+        sub: '4840061',
+        email: 'example@gmail.com',
+        nonce: nonce,
+        iat: 4,
+        exp: 4,
+        jti: 'a8a0728a'
+    };
+
+    const jwt = constructJWT(header, payload);
+    const unsigned_jwt = jwtutils.removeSig(jwt);
+
+    const test_params = {
+        ...circuit_constants,
+        "max_padded_unsigned_jwt_len": 64 * 6,
+    }
+
+    const seed_sub = 3933397123257831927251308270714554907807704888576094124721682124818019353989n;
+    const seed_email = 1973999242154691951111604273911528395925144468932358877866874679764640280443n;
+
+    before(async () => {
+        expect(jwtutils.getExtendedClaim(JSON.stringify(payload), "sub")).equals(claim_string);
+        expect(claim_string.length).at.most(test_params.max_extended_key_claim_len);
+        expect(await circuitutils.computeNonce()).equals(nonce);
+        expect(payload.sub.length).at.most(test_params.max_key_claim_value_len);
+        expect(await utils.deriveAddrSeed(payload.sub, pin, test_params.max_key_claim_value_len)).equals(seed_sub);
+        expect(payload.email.length).at.most(test_params.max_key_claim_value_len);
+        expect(await utils.deriveAddrSeed(payload.email, pin, test_params.max_key_claim_value_len)).equals(seed_email);
+        console.log("JWT: ", jwt);
+    });
+
+    beforeEach(async () => {
+        circuit = await genCircuit(test_params);
+    });
+
+    it("No change", async function() {
+        const inputs = {
+            ...dev_inputs,
+            unsigned_jwt: unsigned_jwt,
+        };
+        const [_, aux] = await genProof(
+            circuit,
+            inputs,
+            test_params
+        );
+        expect(aux["addr_seed"]).equals(seed_sub);
+    });
+
+    it("Sub claim comes first!", async function() {
+        const new_payload = {
+            sub: '4840061',
+            iss: 'google.com',
+            azp: 'example.com',
+            aud: 'example.com',
+            nonce: nonce,
+            iat: 4,
+            exp: 4,
+            jti: 'a8a0728a'
+        };
+        const new_jwt = constructJWT(header, new_payload);
+        const inputs = {
+            ...dev_inputs,
+            unsigned_jwt: jwtutils.removeSig(new_jwt),
+        };
+        const [_, aux] = await genProof(
+            circuit,
+            inputs,
+            test_params
+        );
+        expect(aux["addr_seed"]).equals(seed_sub);
+    });
+
+    it("Sub claim comes last!", async function() {
+        const new_payload = {
+            iss: 'google.com',
+            azp: 'example.com',
+            aud: 'example.com',
+            nonce: nonce,
+            iat: 4,
+            exp: 4,
+            jti: 'a8a0728a',
+            sub: '4840061'
+        };
+        const new_jwt = constructJWT(header, new_payload);
+        const inputs = {
+            ...dev_inputs,
+            unsigned_jwt: jwtutils.removeSig(new_jwt),
+        };
+        const [_, aux] = await genProof(
+            circuit,
+            inputs,
+            test_params
+        );
+        expect(aux["addr_seed"]).equals(seed_sub);
+    });
+
+    it("Order of claims is jumbled!", async function() {
+        const new_payload = {
+            iat: 4,
+            iss: 'google.com',
+            aud: 'example.com',
+            jti: 'a8a0728a',
+            exp: 4,
+            sub: '4840061',
+            azp: 'example.com',
+            nonce: nonce,
+        };
+        const new_jwt = constructJWT(header, new_payload);
+        const inputs = {
+            ...dev_inputs,
+            unsigned_jwt: jwtutils.removeSig(new_jwt),
+        };
+        const [_, aux] = await genProof(
+            circuit,
+            inputs,
+            test_params
+        );
+        expect(aux["addr_seed"]).equals(seed_sub);
+    });
+
+    it("(Fail) Nonce has invalid value!", async () => {
+        const new_payload = {
+            sub: '4840061',
+            iss: 'google.com',
+            azp: 'example.com',
+            aud: 'example.com',
+            nonce: 'JMi6c_3qXn1H8UX5la1P6YDwThkN5LZxqagTyjfiYwU', // incorrect nonce
+            iat: 4,
+            exp: 4,
+            jti: 'a8a0728a'
+        };
+        const new_jwt = constructJWT(header, new_payload);
+        try {
+            const inputs = {
+                ...dev_inputs,
+                unsigned_jwt: jwtutils.removeSig(new_jwt),
+            }
+
+            var [zk_inputs, ] = await circuitutils.genZKLoginInputs(
+                inputs,
+                test_params,
+                false // set to false to turn off JS sanity checks
+            );
+            await circuit.calculateWitness(zk_inputs, true);
+        } catch (error) {
+            assert.include(error.message, 'Error in template NonceChecker');
+        }
+    });
+
+    // TODO: Test with an email of length 50
+    it("No change w/ email", async function() {
+        const inputs = {
+            ...dev_inputs,
+            unsigned_jwt: jwtutils.removeSig(jwt),
+            key_claim_name: "email",
+        };
+        const [_, aux] = await genProof(
+            circuit,
+            inputs,
+            test_params
+        );
+        expect(aux["addr_seed"]).equals(seed_email);
+    });
+
+    it.skip("With spaces and tabs", async function () {
+        const payload = `{"iss":"google.com","azp":"example.com","aud":"example.com",		"sub":  "4840061"   
+        ,"email":"example@gmail.com","nonce":"GCwq2zCuqtsa1BhaAc2SElwUoYv8jKhE6vs6Vmepu2M","iat":4,"exp":4,"jti":"a8a0728a"}`;
+        expect(JSON.parse(payload).sub).to.equal("4840061"); // the spaces don't matter
+
+        const b64header = Buffer.from(JSON.stringify(header)).toString('base64url');
+        const b64payload = Buffer.from(payload).toString('base64url');
+
+        const inputs = {
+            ...dev_inputs,
+            unsigned_jwt: `${b64header}.${b64payload}`,
+        };
+        const [_, aux] = await genProof(
+            circuit,
+            inputs,
+            test_params
+        );
+        expect(aux["addr_seed"]).equals(seed_sub);
+    })
+});
\ No newline at end of file
diff --git a/zklogin-circuits/test/masking.test.js b/zklogin-circuits/test/masking.test.js
new file mode 100644
index 0000000000..91947b038d
--- /dev/null
+++ b/zklogin-circuits/test/masking.test.js
@@ -0,0 +1,162 @@
+const chai = require("chai");
+const assert = chai.assert;
+
+const circuit = require("../js/src/circuitutils");
+const utils = require("../js/src/utils");
+const jwtutils = require("../js/src/jwtutils");
+const verify = require("../js/src/verify");
+
+const GOOGLE1 = require("../js/testvectors/realJWTs").GOOGLE.jwt;
+const GOOGLE2 = require("../js/testvectors/realJWTs").GOOGLE_OLD.jwt;
+const FB = require("../js/testvectors/realJWTs").FACEBOOK.jwt;
+
+function getAllClaims(jwt) {
+    const payload = Buffer.from(jwt.split('.')[1], 'base64url').toString();
+    const json = JSON.parse(payload);
+    return Object.keys(json);
+}
+
+function maskTesting(jwt, claimsToReveal, print=false) {
+    if (print) console.log("Reveal:", claimsToReveal);
+    var mask = circuit.genJwtMask(jwt, claimsToReveal);
+    if (print) console.log("Mask:", mask.join(''));
+
+    var masked_jwt = utils.applyMask(new Uint8Array(Buffer.from(jwt)) , mask);
+    masked_jwt = Buffer.from(masked_jwt).toString();
+    if (print) console.log("Masked JWT:", masked_jwt);
+
+    const header_length = masked_jwt.indexOf('.');
+    if (header_length == -1) throw new Error("Invalid header length");
+
+    const encoded_header = masked_jwt.slice(0, header_length);
+    // const extracted_header = Buffer.from(encoded_header, 'base64url').toString('utf8');
+    if (encoded_header !== jwt.split('.')[0]) {
+        console.log("header", encoded_header, "\njwt", jwt.split('.')[0]);
+        throw new Error("Header not found in masked JWT");
+    }
+
+    const encoded_payload = masked_jwt.slice(header_length + 1);
+    const extracted_claims = verify.extractClaims(encoded_payload);
+
+    // We just check that each full claim string is present (somewhere) in the masked JWT. In practice, these would need to parsed out.
+    // Note that some claims might not be at the start of an extracted_claim, e.g., if consecutive claims are being revealed.
+    for (const claim of claimsToReveal) {
+        const claim_string = jwtutils.getExtendedClaim(Buffer.from(jwt.split('.')[1], 'base64url').toString(), claim);
+        if (!extracted_claims.some(e => e.includes(claim_string))) {
+            console.log("Can't find claim", claim, "in", extracted_claims);
+            throw new Error("Claim not found in masked JWT");
+        }
+    }
+
+    for (const claim of extracted_claims) {
+        if (claim[0] !== '"') {
+            // First character of each extracted_claim must be '"' (extractClaims omits partial bits at the start)
+            console.log("Invalid claim", claim);
+            throw new Error("Invalid claim");
+        }
+
+        if (!(claim.slice(-1) === '}' || claim.slice(-1) === ',')) {
+            // Last character of each extracted_claim must be '}' or ','
+            console.log("Invalid claim", claim);
+            throw new Error("Invalid claim");
+        }
+    }
+
+    if (print) console.log("Extracted claims:", extracted_claims);
+    if (print) console.log('\n');
+    return extracted_claims;
+}
+
+function subsets(array) {
+    return array.reduce(
+        (subsets, value) => subsets.concat(
+            subsets.map(set => [value, ...set])
+        ),
+        [[]]
+    );
+}
+
+describe("Masking with dummy JWTs", function() {
+    // Creates a JWT-like string from a header and payload
+    const constructJWT = (header, payload) => {
+        jwt = utils.trimEndByChar(Buffer.from(header).toString('base64url'), '=') 
+                    + '.' + utils.trimEndByChar(Buffer.from(payload).toString('base64url'), '=');
+        return jwt;
+    }
+
+    it(("#1"), function() {
+        header = '{"kid":abc}';
+        payload = '{"iss":123,"azp":"gogle","iat":7890,"exp":101112}';
+
+        // Create a JWT
+        jwt = constructJWT(header, payload);
+
+        // Test for all possible subsets of claims
+        const claims = getAllClaims(jwt);
+        assert.deepEqual(claims.sort(), ["iss", "azp", "iat", "exp"].sort());
+        for (const subset of subsets(claims)) {
+            maskTesting(jwt, subset);
+        }
+    });
+
+    it(("#2"), function() {
+        header = '{"kid":abc}';
+        payload = '{"iss":1234,"azp":"gogle","iat":7890,"exp":101112}';
+        jwt = constructJWT(header, payload);
+        const claims = getAllClaims(jwt);
+        for (const subset of subsets(claims)) {
+            maskTesting(jwt, subset);
+        }
+    });
+
+    it(("#3"), function() {
+        header = '{"kid":abc}';
+        payload = '{"iss":12345,"azp":"gogle","iat":7890,"exp":101112}';
+        jwt = constructJWT(header, payload);
+        const claims = getAllClaims(jwt);
+        for (const subset of subsets(claims)) {
+            maskTesting(jwt, subset);
+        }
+    });
+})
+
+describe("Masking with real JWTs", function() {
+    it("Google", function() {
+        const jwt = GOOGLE1.split('.').slice(0,2).join('.');
+        const claims = getAllClaims(jwt);
+        assert.deepEqual(claims.sort(), [
+            'iss',   'azp',
+            'aud',   'sub',
+            'nonce', 'iat',
+            'exp',   'jti',
+            'email', 'email_verified'
+          ].sort());
+        for (const subset of subsets(claims)) {
+            maskTesting(jwt, subset);
+        }
+    });
+
+    it("Google again", function() {
+        const jwt = GOOGLE2.split('.').slice(0,2).join('.');
+        const claims = getAllClaims(jwt);
+        for (const subset of subsets(claims)) {
+            maskTesting(jwt, subset);
+        }
+    });
+
+    it("Facebook", function() {
+        const jwt = FB.split('.').slice(0,2).join('.');
+        const claims = getAllClaims(jwt);
+        assert.deepEqual(claims.sort(), [
+            'aud',         'exp',
+            'family_name', 'given_name',
+            'iat',         'iss',
+            'jti',         'name',
+            'nonce',       'picture',
+            'sub'
+          ].sort());
+        for (const subset of subsets(claims)) {
+            maskTesting(jwt, subset);
+        }
+    });
+});
\ No newline at end of file
diff --git a/zklogin-circuits/test/misc.circom.test.js b/zklogin-circuits/test/misc.circom.test.js
new file mode 100644
index 0000000000..7b8aa92374
--- /dev/null
+++ b/zklogin-circuits/test/misc.circom.test.js
@@ -0,0 +1,497 @@
+const chai = require("chai");
+const path = require("path");
+const assert = chai.assert;
+const expect = chai.expect;
+
+const utils = require("../js/src/utils");
+
+const testutils = require("./testutils");
+
+describe("Num2BitsBE(8)", () => {
+    beforeEach(async () => {
+        cir = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "Num2BitsBE", [8]);
+        await cir.loadSymbols();
+    });
+
+    it ("Check 0", async () => {
+        const witness = await cir.calculateWitness({"in": 0}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessArray(witness, cir.symbols, "main.out");
+        assert.deepEqual(out, [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]);
+    });
+
+    it ("Check 1", async () => {
+        const witness = await cir.calculateWitness({"in": 1}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessArray(witness, cir.symbols, "main.out");
+        assert.deepEqual(out, [0n, 0n, 0n, 0n, 0n, 0n, 0n, 1n]);
+    });
+
+    it ("Check 255", async () => {
+        const witness = await cir.calculateWitness({"in": 255}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessArray(witness, cir.symbols, "main.out");
+        assert.deepEqual(out, [1n, 1n, 1n, 1n, 1n, 1n, 1n, 1n]);
+    });
+
+    it("Check 256: must throw an error", async () => {
+        try {
+            const witness = await cir.calculateWitness({"in": 256}, true);
+            await cir.checkConstraints(witness);
+            assert.fail("Should have failed");
+        } catch (error) {
+            assert.include(error.message, "Error in template Num2BitsBE");
+        }
+    });
+
+    it ("Check -1: must throw an error", async () => {
+        try {
+            const witness = await cir.calculateWitness({"in": -1}, true);
+            await cir.checkConstraints(witness);
+            assert.fail("Should have failed");
+        } catch (error) {
+            assert.include(error.message, "Error in template Num2BitsBE");
+        }
+    });
+})
+
+describe("Num2BitsBE(256)", async () => {
+    const L = 256;
+    const P = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
+
+    cir = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "Num2BitsBE", [L]);
+    await cir.loadSymbols();
+    const crypto = require("crypto");
+    const num = BigInt("0x" + crypto.randomBytes(32).toString('hex')) % P;
+
+    const witness = await cir.calculateWitness({"in": num}, true);
+    await cir.checkConstraints(witness);
+    const out = testutils.getWitnessArray(witness, cir.symbols, "main.out").map(x => Number(x));
+    assert.equal(out.length, L);
+
+    const out1 = (num % P).toString(2).padStart(L, '0').split('').map(x => Number(x));
+    assert.equal(out1.length, L);
+
+    assert.deepEqual(out, out1);
+})
+
+// Note: Although it is an offical circuit, tests for illustrative purposes and my own sanity
+describe("Num2Bits", () => {
+    beforeEach(async () => {
+        cir = await testutils.genMain(path.join(__dirname, "../node_modules/circomlib/circuits", "bitify.circom"), "Num2Bits", [8]);
+        await cir.loadSymbols();
+    });
+
+    it ("Check 0", async () => {
+        const witness = await cir.calculateWitness({"in": 0}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessArray(witness, cir.symbols, "main.out");
+        assert.deepEqual(out, [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]);
+    });
+
+    it ("Check 1", async () => {
+        const witness = await cir.calculateWitness({"in": 1}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessArray(witness, cir.symbols, "main.out");
+        assert.deepEqual(out, [1n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]);
+    });
+
+    it ("Check 255", async () => {
+        const witness = await cir.calculateWitness({"in": 255}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessArray(witness, cir.symbols, "main.out");
+        assert.deepEqual(out, [1n, 1n, 1n, 1n, 1n, 1n, 1n, 1n]);
+    });
+
+    it("Check 256: must throw an error", async () => {
+        try {
+            const witness = await cir.calculateWitness({"in": 256}, true);
+            await cir.checkConstraints(witness);
+            assert.fail("Should have failed");
+        } catch (error) {
+            assert.include(error.message, "Error in template Num2Bits");
+        }
+    });
+
+    it ("Check -1: must throw an error", async () => {
+        try {
+            const witness = await cir.calculateWitness({"in": -1}, true);
+            await cir.checkConstraints(witness);
+            assert.fail("Should have failed");
+        } catch (error) {
+            assert.include(error.message, "Error in template Num2Bits");
+        }
+    });
+});
+
+describe("Bits2NumBE", () => {
+    before (async () => {
+        cir = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "Bits2NumBE", [8]);
+        await cir.loadSymbols();
+    });
+
+    it ("Check 0", async () => {
+        const witness = await cir.calculateWitness({"in": [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessValue(witness, cir.symbols, "main.out");
+        assert.equal(out, 0n);
+    });
+
+    it ("Check 1", async () => {
+        const witness = await cir.calculateWitness({"in": [0n, 0n, 0n, 0n, 0n, 0n, 0n, 1n]}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessValue(witness, cir.symbols, "main.out");
+        assert.equal(out, 1n);
+    });
+
+    it ("Check 255", async () => {
+        const witness = await cir.calculateWitness({"in": [1n, 1n, 1n, 1n, 1n, 1n, 1n, 1n]}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessValue(witness, cir.symbols, "main.out");
+        assert.equal(out, 255n);
+    });
+});
+
+describe("Segments2NumBE", () => {
+    before (async () => {
+        cir = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "Segments2NumBE", [8, 3]);
+        await cir.loadSymbols();
+    });
+
+    it ("Check 0", async () => {
+        const witness = await cir.calculateWitness({"in": [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessValue(witness, cir.symbols, "main.out");
+        assert.equal(out, 0n);
+    });
+
+    it ("Check 8", async () => {
+        const witness = await cir.calculateWitness({"in": [0n, 0n, 0n, 0n, 0n, 0n, 1n, 0n]}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessValue(witness, cir.symbols, "main.out");
+        assert.equal(out, 8n);
+    });
+
+    it ("Check 511", async () => {
+        const witness = await cir.calculateWitness({"in": [0n, 0n, 0n, 0n, 0n, 7n, 7n, 7n]}, true);
+        await cir.checkConstraints(witness);
+        const out = testutils.getWitnessValue(witness, cir.symbols, "main.out");
+        assert.equal(out, 511n);
+    });
+})
+
+describe("ConvertBase checks", () => {
+    it("Checking ConvertBase Case 0: input and output should be same", async () => {
+        cir_fixed = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "ConvertBase", [4, 4, 4, 4]);
+        await cir_fixed.loadSymbols();
+        input = [1,2,3,4];
+        const witness = await cir_fixed.calculateWitness({ "in": input });
+        
+        const out = testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out").map(e => Number(e) - '0');
+        assert.deepEqual(out, [1, 2, 3, 4]);
+        assert.deepEqual(out, utils.pack(input, 4, 4).map(Number));
+    });
+
+    it("Checking ConvertBase Case 1: Output width is multiple of input width", async () => {
+        cir_fixed = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "ConvertBase", [4, 4, 8, 2]);
+        await cir_fixed.loadSymbols();
+        input = [1,2,3,4];
+        const witness = await cir_fixed.calculateWitness({ "in": input });
+        
+        const out = testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out").map(e => Number(e) - '0');
+        assert.deepEqual(out, [18, 52]);
+        assert.deepEqual(out, utils.pack(input, 4, 8).map(Number));
+    });
+
+    it("Checking ConvertBase Case 2: Output width is not a multiple of input width", async () => {
+        cir_fixed = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "ConvertBase", [4, 4, 6, 3]);
+        await cir_fixed.loadSymbols();
+        input = [1,2,3,4];
+        const witness = await cir_fixed.calculateWitness({ "in": input });
+        
+        const out = testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out").map(e => Number(e) - '0');
+        assert.deepEqual(out, [4, 35, 16]);
+        assert.deepEqual(out, utils.pack(input, 4, 6).map(Number));
+    });
+
+    it("Checking ConvertBase Case 3: Edge case - just one input", async () => {  
+        cir_fixed = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "ConvertBase", [1, 1, 6, 1]);
+        await cir_fixed.loadSymbols();
+        input = [1];
+        const witness = await cir_fixed.calculateWitness({ "in": input });
+        
+        const out = testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out").map(e => Number(e) - '0');
+        assert.deepEqual(out, [32]);
+        assert.deepEqual(out, utils.pack(input, 1, 6).map(Number));
+    });
+
+    it("Checking ConvertBase Case 4: Edge case - just one output", async () => {
+        cir_fixed = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "ConvertBase", [4, 4, 16, 1]);
+        await cir_fixed.loadSymbols();
+        input = [1, 2, 3, 4];
+        const witness = await cir_fixed.calculateWitness({ "in": input });
+        
+        const out = testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out").map(e => Number(e) - '0');
+        assert.deepEqual(out, [4660]);
+        assert.deepEqual(out, utils.pack(input, 4, 16).map(Number));
+    });
+
+    it("Checking ConvertBase Case 5: Assert fail for myOutCount != outCount", async () => {
+        try {
+            cir_fixed = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "ConvertBase", [4, 4, 16, 2]);
+            assert.fail("Should have failed");
+        } catch (error) {
+            assert.include(error.message, "assert(myOutCount == outCount)");
+        }
+    });
+
+    it("Checking ConvertBase Case 6: Another test of correct padding", async () => {
+        cir_fixed = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "ConvertBase", [4, 4, 7, 3]);
+        await cir_fixed.loadSymbols();
+        input = [7,1,8,2];
+        const witness = await cir_fixed.calculateWitness({ "in": input });
+        
+        const out = testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out").map(e => Number(e) - '0');
+        assert.deepEqual(out, [56, 96, 64]);
+        assert.deepEqual(out, utils.pack(input, 4, 7).map(Number));
+    });
+});
+
+describe("RemainderMod4 checks", () => {
+    it("Positive + Negative cases", async () => {
+        cir_fixed = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "RemainderMod4", [3]);
+        await cir_fixed.loadSymbols();
+        inputs = [1,2,3,4,5,6,7];
+        for (let i = 0; i < inputs.length; i++) { // Should pass
+            const witness = await cir_fixed.calculateWitness({ "in": inputs[i] });
+            await cir_fixed.checkConstraints(witness);
+            const out = testutils.getWitnessValue(witness, cir_fixed.symbols, "main.out");
+            assert.deepEqual(out, BigInt(inputs[i] % 4));
+        }
+
+        try {
+            const witness = await cir_fixed.calculateWitness({ "in": 8 });
+            await cir_fixed.checkConstraints(witness);
+            assert.fail("Should have failed");
+        } 
+        catch (error) {
+            assert.include(error.message, 'Error in template Num2Bits', error.message); // Num2Bits does the length check
+        }
+    })
+});
+
+describe("DivideMod2Power checks", () => {
+    it("With 2^2 = 4", async () => {
+        circuit = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "DivideMod2Power", [4, 2]);
+        await circuit.loadSymbols();
+
+        for (let i = 0; i < 16; i++) {
+            const w = await circuit.calculateWitness({ "in": i });
+            await circuit.checkConstraints(w);
+            const quotient = testutils.getWitnessValue(w, circuit.symbols, "main.quotient");
+            const remainder = testutils.getWitnessValue(w, circuit.symbols, "main.remainder");
+
+            assert.deepEqual(quotient, BigInt(Math.floor(i / 4)));
+            assert.deepEqual(remainder, BigInt(i % 4));
+        }
+    })
+})
+
+describe("Number to bit vector checks", () => {
+    it("OneBitVector", async () => {
+        circuit = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "OneBitVector", [4]);
+        await circuit.loadSymbols();
+
+        // Success
+        for (let i = 0; i < 4; i++) {
+            const w = await circuit.calculateWitness({ "index": i });
+            await circuit.checkConstraints(w);
+            var ans = [0n, 0n, 0n, 0n];
+            ans[i] = 1n;
+            assert.sameOrderedMembers(
+                testutils.getWitnessArray(w, circuit.symbols, "main.out"), ans
+            );
+        }
+
+        // Failure
+        for (let i of [-1, 4, 8]) {
+            try {
+                const w = await circuit.calculateWitness({ "index": i });
+                await circuit.checkConstraints(w);
+                assert.fail("Should have failed");
+            } catch (error) {
+                assert.include(error.message, 'Error in template OneBitVector', error.message);
+            }
+        }
+    })
+
+    it("GTBitVector", async () => {
+        circuit = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "GTBitVector", [4]);
+        await circuit.loadSymbols();
+
+        // Success
+        for (let i = 0; i < 4; i++) {
+            const w = await circuit.calculateWitness({ "index": i });
+            await circuit.checkConstraints(w);
+            var ans = [0n, 0n, 0n, 0n];
+            for (let j = i + 1; j < 4; j++) {
+                ans[j] = 1n;
+            }
+            assert.sameOrderedMembers(
+                testutils.getWitnessArray(w, circuit.symbols, "main.out"), ans
+            );
+        }
+
+        // Failure
+        for (let i of [-1, 4, 5, 8]) {
+            try {
+                const w = await circuit.calculateWitness({ "index": i });
+                await circuit.checkConstraints(w);
+                assert.fail(`Should have failed: index = ${i}`);
+            } catch (error) {
+                assert.include(error.message, 'Error in template GTBitVector', error.message);
+            }
+        }
+    })
+
+    it("LTBitVector", async () => {
+        circuit = await testutils.genMain(path.join(__dirname, "../circuits/helpers", "misc.circom"), "LTBitVector", [4]);
+        await circuit.loadSymbols();
+
+        // Success
+        for (let i = 1; i <= 4; i++) {
+            const w = await circuit.calculateWitness({ "index": i });
+            await circuit.checkConstraints(w);
+            var ans = [0n, 0n, 0n, 0n];
+            for (let j = 0; j < i; j++) {
+                ans[j] = 1n;
+            }
+            assert.sameOrderedMembers(
+                testutils.getWitnessArray(w, circuit.symbols, "main.out"), ans
+            );
+        }
+
+        // Failure
+        for (let i of [-1, 0, 5, 8]) {
+            try {
+                const w = await circuit.calculateWitness({ "index": i });
+                await circuit.checkConstraints(w);
+                assert.fail("Should have failed");
+            } catch (error) {
+                assert.include(error.message, 'Error in template LTBitVector', error.message);
+            }
+        }
+    })
+});
+
+describe("RangeCheck", () => {
+    async function genCircuit(nBits, max) {
+        return await testutils.genMain(
+            path.join(__dirname, "../circuits/helpers", "misc.circom"), 
+            "RangeCheck", 
+            [nBits, max]
+        );
+    }
+
+    it("Positive", async () => {
+        var nBits = 4;
+        var max = 8;
+
+        circuit = await genCircuit(nBits, max);
+
+        for (var i = 0; i <= max; i++) {
+            const w = await circuit.calculateWitness({ "in": 0});
+            await circuit.checkConstraints(w);    
+        }
+    })
+
+    describe("Negative", async () => {
+        var nBits = 4;
+        var max = 8;
+
+        before(async () => {
+            circuit = await genCircuit(nBits, max);
+        });
+
+        it("max < in < 2^nBits", async () => {
+            for (var i of [9, 10, 11, 15]) { // 
+                try {
+                    const w = await circuit.calculateWitness({ "in": i});
+                    await circuit.checkConstraints(w);
+                    assert.fail("Should have failed");
+                } catch (error) {
+                    assert.include(error.message, 'Error in template RangeCheck');
+                }
+            }
+        });
+
+        it("in < 0", async () => {
+            for (var i of [-1, -2, -3, -4]) {
+                try {
+                    const w = await circuit.calculateWitness({ "in": i});
+                    await circuit.checkConstraints(w);
+                    assert.fail("Should have failed");
+                } catch (error) {
+                    assert.include(error.message, 'Error in template RangeCheck');
+                }
+            }
+        });
+
+        it("in >= 2^nBits", async () => {
+            for (var i of [16, 17, 18, 19, 20]) {
+                try {
+                    const w = await circuit.calculateWitness({ "in": i});
+                    await circuit.checkConstraints(w);
+                    assert.fail("Should have failed");
+                } catch (error) {
+                    assert.include(error.message, 'Error in template RangeCheck');
+                }
+            }
+        });
+    });
+});
+
+describe("mapToField", () => {
+    var circuit;
+    const maxStrLen = 20;
+
+    before(async () => {
+        circuit = await testutils.genMain(
+            path.join(__dirname, "../circuits/helpers", "misc.circom"), "MapToField", [maxStrLen]
+        );
+        await circuit.loadSymbols();
+    });
+
+    it("Normal string", async () => {
+        const str = 'abracadabra';
+        const witness = await circuit.calculateWitness({
+            "str": utils.strToVec(str, maxStrLen)
+        }, true);
+        await circuit.checkConstraints(witness);
+
+        const x = testutils.getWitnessValue(witness, circuit.symbols, "main.str_F");
+        expect(x).equals(await utils.mapToField(str, maxStrLen));
+    });
+
+    it("empty string", async () => {
+        const str = '';
+        const witness = await circuit.calculateWitness({
+            "str": utils.strToVec(str, maxStrLen)
+        }, true);
+        await circuit.checkConstraints(witness);
+
+        const x = testutils.getWitnessValue(witness, circuit.symbols, "main.str_F");
+        expect(x).equals(await utils.mapToField(str, maxStrLen));
+    });
+
+    it("String length == maxLen", async () => {
+        const str = 'a'.repeat(maxStrLen);
+        const witness = await circuit.calculateWitness({
+            "str": utils.strToVec(str, maxStrLen)
+        }, true);
+        await circuit.checkConstraints(witness);
+
+        const x = testutils.getWitnessValue(witness, circuit.symbols, "main.str_F");
+        expect(x).equals(await utils.mapToField(str, maxStrLen));
+    });
+});
\ No newline at end of file
diff --git a/zklogin-circuits/test/sha256.circom.test.js b/zklogin-circuits/test/sha256.circom.test.js
new file mode 100644
index 0000000000..b7bcbc361b
--- /dev/null
+++ b/zklogin-circuits/test/sha256.circom.test.js
@@ -0,0 +1,107 @@
+const chai = require("chai");
+const path = require("path");
+const assert = chai.assert;
+const crypto = require("crypto");
+
+const {toBigIntBE} = require('bigint-buffer');
+
+const utils = require("../js/src/utils");
+const circuit = require("../js/src/circuitutils");
+
+const testutils = require("./testutils");
+const inWidth = require("../js/src/common").constants.inWidth;
+
+describe("Unsafe SHA256", () => {
+    const nBlocks = 4;
+    const nWidth = inWidth;
+    const nCount = nBlocks * 512 / nWidth;
+
+    const outWidth = 128;
+    const outCount = 256 / outWidth;
+
+    const bytesToBlock = 512/8; // 64
+    var cir;
+
+    before(async() => {
+        cir = await testutils.genMain(
+            path.join(__dirname, "../circuits/helpers", "sha256.circom"),
+            "Sha2_wrapper",
+            [nWidth, nCount, outWidth, outCount]
+        );
+        await cir.loadSymbols();
+    });
+
+    async function test(i, expected_num_sha2_blocks) {
+        const input = crypto.randomBytes(i * bytesToBlock);
+
+        const hash = crypto.createHash("sha256").update(input).digest("hex");
+
+        var inputs = circuit.genSha256Inputs(input, nCount, nWidth);
+
+        var final_inputs = {};
+        final_inputs["in"] = inputs["content"];
+        final_inputs["num_sha2_blocks"] = inputs["num_sha2_blocks"];
+
+        assert.equal(final_inputs["num_sha2_blocks"], expected_num_sha2_blocks);
+        console.log(`num_sha2_blocks = ${final_inputs["num_sha2_blocks"]}`);
+
+        const witness = await cir.calculateWitness(final_inputs, true);
+
+        const hash2 = testutils.getWitnessBuffer(witness, cir.symbols, "main.hash", outWidth).toString("hex");
+        console.log(`hash = ${hash2}`);
+
+        assert.equal(hash2, hash);
+    }
+
+    it(`Hashing produces expected output for 0.5 block`, async () => {
+        await test(0.5, 1); // num_sha2_blocks = 1
+    });
+
+    it(`Hashing produces expected output for 1 block`, async () => {
+        await test(1, 2);
+    });
+
+    it(`Hashing produces expected output for 2 blocks`, async () => {
+        await test(2, 3);
+    });
+
+    it(`Corner case: num_sha2_blocks = nBlocks`, async () => {
+        await test(nBlocks - 1, nBlocks);
+    });
+
+    it(`Fails when the last byte is non-zero`, async () => {
+        const input = crypto.randomBytes(1 * bytesToBlock);
+
+        var inputs = circuit.genSha256Inputs(input, nCount, nWidth);
+
+        var final_inputs = {};
+        final_inputs["in"] = inputs["content"];
+        final_inputs["num_sha2_blocks"] = inputs["num_sha2_blocks"];
+
+        final_inputs["in"][final_inputs["in"].length - 1] = 1n; // Make the last byte non-zero
+        try {
+            await cir.calculateWitness(final_inputs, true);
+            assert.fail("Should have thrown an error");
+        } catch (e) {
+            assert.include(e.message, "Error in template Sha2_wrapper");
+        }
+    });
+
+    it(`Fails when the first byte post SHA2-padding is non-zero`, async () => {
+        const input = crypto.randomBytes(1 * bytesToBlock);
+
+        var inputs = circuit.genSha256Inputs(input, nCount, nWidth);
+        var final_inputs = {};
+        final_inputs["in"] = inputs["content"];
+        final_inputs["num_sha2_blocks"] = inputs["num_sha2_blocks"];
+
+        var num_sha2_blocks = final_inputs["num_sha2_blocks"];
+        final_inputs["in"][num_sha2_blocks * 512 / nWidth] = 1n; // Make the first byte post SHA2-padding non-zero
+        try {
+            await cir.calculateWitness(final_inputs, true);
+            assert.fail("Should have thrown an error");
+        } catch (e) {
+            assert.include(e.message, "Error in template Sha2_wrapper");
+        }
+    });
+});
\ No newline at end of file
diff --git a/zklogin-circuits/test/strings.circom.test.js b/zklogin-circuits/test/strings.circom.test.js
new file mode 100644
index 0000000000..18f2efa0d1
--- /dev/null
+++ b/zklogin-circuits/test/strings.circom.test.js
@@ -0,0 +1,574 @@
+const chai = require("chai");
+const path = require("path");
+const assert = chai.assert;
+const expect = chai.expect;
+
+const jwtutils = require("../js/src/jwtutils");
+const utils = require("../js/src/utils");
+
+const testutils = require("./testutils");
+
+describe("Slices", () => {
+    const file = path.join(__dirname, "../circuits/helpers", "strings.circom");
+
+    describe("Fixed length", () => {
+        it("(6, 2)", async () => {
+            cir_fixed = await testutils.genMain(file, "SliceFixed", [6, 2]);
+            await cir_fixed.loadSymbols();
+            input = [1,2,3,4,5,6];
+            
+            const witness = await cir_fixed.calculateWitness({ "in": input, "index": 1 });
+            
+            assert.sameOrderedMembers(testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out"), [2n, 3n]);
+        });
+    
+        it("(6, 6)", async () => {
+            cir_fixed = await testutils.genMain(file, "SliceFixed", [6, 6]);
+            await cir_fixed.loadSymbols();
+            input = [3,1,5,9,2,6];
+            
+            const witness = await cir_fixed.calculateWitness({ "in": input, "index": 0 });
+            
+            assert.sameOrderedMembers(testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out"), [3n, 1n, 5n, 9n, 2n, 6n]);
+        });
+    
+        it("Fails when OOB: index >= inputLength", async () => {
+            cir_fixed = await testutils.genMain(file, "SliceFixed", [6, 4]);
+            await cir_fixed.loadSymbols();
+            input = [3,1,5,9,2,6];
+            
+            try {
+                await cir_fixed.calculateWitness({ "in": input, "index": 6 });
+                assert.fail("Should have failed");
+            } catch (e) {
+                assert.include(e.message, "Error in template RangeCheck");
+            }
+        });
+
+        it("Slice outside: index + outputLength > inputLength", async () => {
+            cir_fixed = await testutils.genMain(file, "SliceFixed", [6, 4]);
+            await cir_fixed.loadSymbols();
+            input = [3,1,5,9,2,6];
+            
+            try {
+                await cir_fixed.calculateWitness({ "in": input, "index": 4 });
+                assert.fail("Should have failed");
+            } catch (e) {
+                assert.include(e.message, "Error in template RangeCheck");
+            }
+        });
+    })
+
+    describe("Variable length", () => {
+        it("(6, 4), 2", async () => {
+            cir_fixed = await testutils.genMain(file, "Slice", [6, 4]);
+            await cir_fixed.loadSymbols();
+            input = [1,2,3,4,5,6];
+            
+            const witness = await cir_fixed.calculateWitness({ "in": input, "index": 1, "length": 2 });
+            
+            assert.sameOrderedMembers(testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out"), [2n, 3n, 0n, 0n]);
+        });
+        
+        it("Corner case: length = outputLength", async () => {
+            cir_fixed = await testutils.genMain(file, "Slice", [6, 4]);
+            await cir_fixed.loadSymbols();
+            input = [1,2,3,4,5,6];
+            
+            const witness = await cir_fixed.calculateWitness({ "in": input, "index": 1, "length": 4 });
+            
+            assert.sameOrderedMembers(testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out"), [2n, 3n, 4n, 5n]);
+        });
+
+        it("Fails when index >= inputLength", async () => {
+            cir_fixed = await testutils.genMain(file, "Slice", [6, 4]);
+            await cir_fixed.loadSymbols();
+            input = [1,2,3,4,5,6];
+            
+            try {
+                await cir_fixed.calculateWitness({ "in": input, "index": 6, "length": 4 });
+                assert.fail("Should have failed");
+            } catch (e) {
+                assert.include(e.message, "Error in template RangeCheck");
+            }
+        });
+
+        it("Fails when length > outputLength", async () => {
+            cir_fixed = await testutils.genMain(file, "Slice", [6, 4]);
+            await cir_fixed.loadSymbols();
+            input = [1,2,3,4,5,6];
+            
+            try {
+                await cir_fixed.calculateWitness({ "in": input, "index": 1, "length": 5 });
+                assert.fail("Should have failed");
+            } catch (e) {
+                assert.include(e.message, "Error in template RangeCheck");
+            }
+        });
+
+        it("Fails when length = 0", async () => {
+            cir_fixed = await testutils.genMain(file, "Slice", [6, 4]);
+            await cir_fixed.loadSymbols();
+            input = [1,2,3,4,5,6];
+
+            try {
+                await cir_fixed.calculateWitness({ "in": input, "index": 1, "length": 0 });
+                assert.fail("Should have failed");
+            } catch (e) {
+                assert.include(e.message, "Error in template RangeCheck");
+            }
+        });
+
+        it("Fails when index + length > inputLength", async () => {
+            cir_fixed = await testutils.genMain(file, "Slice", [6, 4]);
+            await cir_fixed.loadSymbols();
+            input = [1,2,3,4,5,6];
+            try {
+                await cir_fixed.calculateWitness({ "in": input, "index": 4, "length": 4 });
+                assert.fail("Should have failed");
+            } catch (e) {
+                assert.include(e.message, "Error in template RangeCheck");
+            }
+        });
+
+        it("Slice outside:  inputLength >= index + length > outputLength", async () => {
+            cir_fixed = await testutils.genMain(file, "Slice", [8, 6]);
+            await cir_fixed.loadSymbols();
+            input = [1,2,3,4,5,6,7,8];
+            const witness = await cir_fixed.calculateWitness({ "in": input, "index": 4, "length": 4 });
+            assert.sameOrderedMembers(testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out"), [5n, 6n, 7n, 8n, 0n, 0n]);
+        });
+    });
+
+    describe("Variable length with SliceGrouped", () => {
+        var template_name = "SliceGrouped";
+
+        describe("numsPerGroup = 2", () => {
+            var inWidth = 3;
+            var numsPerGroup = 2;
+            var inLen = 6;
+            var outLen = 4;
+            var params = [inWidth, numsPerGroup, inLen, outLen];
+    
+            before(() => {
+                expect(inLen % numsPerGroup).to.equal(0);
+            });
+    
+            it("(6, 4), 2", async () => {
+                cir_fixed = await testutils.genMain(file, template_name, params);
+                await cir_fixed.loadSymbols();
+                input = [1,2,3,4,5,6];
+                const witness = await cir_fixed.calculateWitness({ "in": input, "index": 1, "length": 2 });
+                assert.sameOrderedMembers(
+                    testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out").slice(0, 2),
+                    [2n, 3n]
+                );
+            });
+    
+            it("Corner case: length = outputLength", async () => {
+                cir_fixed = await testutils.genMain(file, template_name, params);
+                await cir_fixed.loadSymbols();
+                input = [1,2,3,4,5,6];
+                const witness = await cir_fixed.calculateWitness({ "in": input, "index": 1, "length": 4 });
+                assert.sameOrderedMembers(
+                    testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out"),
+                    [2n, 3n, 4n, 5n]
+                );
+            });
+    
+            it("Slice outside: inputLength = (index + length) > outputLength", async () => {
+                cir_fixed = await testutils.genMain(file, template_name, [4, numsPerGroup, 8, 6]);
+                await cir_fixed.loadSymbols();
+                input = [1,2,3,4,5,6,7,8];
+                const witness = await cir_fixed.calculateWitness({ "in": input, "index": 4, "length": 4 });
+                assert.sameOrderedMembers(
+                    testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out").slice(0, 4),
+                    [5n, 6n, 7n, 8n]
+                );
+            });
+    
+            it("Fails when length = 0", async () => {
+                cir_fixed = await testutils.genMain(file, template_name, params);
+                await cir_fixed.loadSymbols();
+                input = [1,2,3,4,5,6];
+    
+                try {
+                    await cir_fixed.calculateWitness({ "in": input, "index": 1, "length": 0 });
+                    assert.fail("Should have failed");
+                } catch (e) {
+                    assert.include(e.message, "Error in template RangeCheck");
+                }
+            });
+    
+            it("Fails when index >= inputLength", async () => {
+                cir_fixed = await testutils.genMain(file, template_name, params);
+                await cir_fixed.loadSymbols();
+                input = [1,2,3,4,5,6];
+                try {
+                    await cir_fixed.calculateWitness({ "in": input, "index": 6, "length": 4 });
+                    assert.fail("Should have failed");
+                } catch (e) {
+                    assert.include(e.message, "Error in template RangeCheck");
+                }
+            });
+    
+            it("Fails when length > outputLength", async () => {
+                cir_fixed = await testutils.genMain(file, template_name, params);
+                await cir_fixed.loadSymbols();
+                input = [1,2,3,4,5,6];
+                try {
+                    await cir_fixed.calculateWitness({ "in": input, "index": 1, "length": 5 });
+                    assert.fail("Should have failed");
+                } catch (e) {
+                    assert.include(e.message, "Error in template RangeCheck");
+                }
+            });
+    
+            it("Fails when index + length > inputLength", async () => {
+                cir_fixed = await testutils.genMain(file, template_name, params);
+                await cir_fixed.loadSymbols();
+                input = [1,2,3,4,5,6];
+                try {
+                    await cir_fixed.calculateWitness({ "in": input, "index": 4, "length": 4 });
+                    assert.fail("Should have failed");
+                } catch (e) {
+                    assert.include(e.message, "Error in template RangeCheck");
+                }
+            });    
+        });
+
+        describe("numsPerGroup = 4", () => {
+            var inWidth = 4;
+            var numsPerGroup = 4;
+
+            describe("OutLen tests", () => {
+                for (var outLen = 1; outLen <= 20; outLen += 1) {
+                    (function(outLen) {
+                        it(`outLen: ${outLen}`, async () => {
+                            var inLen = 12;
+                            expect(inLen % numsPerGroup).to.equal(0);
+                            var params = [inWidth, numsPerGroup, inLen, outLen];
+                            cir_fixed = await testutils.genMain(file, template_name, params);
+                            await cir_fixed.loadSymbols();
+                            input = [1,2,3,4,5,6,7,8,9,10,11,12];
+                            try {
+                                const witness = await cir_fixed.calculateWitness({ "in": input, "index": 1, "length": 4 });
+                                const output = testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out");
+                                assert.sameOrderedMembers(
+                                    output.slice(0, 4),
+                                    [2n, 3n, 4n, 5n]
+                                );
+                                if (outLen < 4) {
+                                    assert.fail("Should have failed");
+                                }
+                            } catch (e) {
+                                assert.include(e.message, "Error in template RangeCheck");
+                            }
+                        });
+                    })(outLen);
+                }    
+            });
+
+            describe("Index tests", () => {
+                var inLen = 12;
+                var outLen = 8;
+                
+                for (var index = 0; index < 12; index += 1) {
+                    (function(index) {
+                        it(`index: ${index}`, async () => {
+                            expect(inLen % numsPerGroup).to.equal(0);
+                            var params = [inWidth, numsPerGroup, inLen, outLen];
+                            cir_fixed = await testutils.genMain(file, template_name, params);
+                            await cir_fixed.loadSymbols();
+                            input = [1,2,3,4,5,6,7,8,9,10,11,12];
+                            try {
+                                const witness = await cir_fixed.calculateWitness({ "in": input, "index": index, "length": 4 });
+                                const output = testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out");    
+                                assert.sameOrderedMembers(
+                                    output.slice(0, 4).map(Number),
+                                    input.slice(index, index + 4)
+                                );
+                                if (index + 4 > inLen) {
+                                    assert.fail("Should have failed");
+                                }
+                            } catch (e) {
+                                assert.include(e.message, "Error in template RangeCheck");
+                            }
+                        });
+                    })(index);
+                }    
+            });
+
+            describe("Length tests", () => {
+                var inLen = 12;
+                var outLen = 8;
+                for (var length = 1; length <= 12; length += 1) {
+                    (function(length) {
+                        it(`length: ${length}`, async () => {
+                            expect(inLen % numsPerGroup).to.equal(0);
+                            var params = [inWidth, numsPerGroup, inLen, outLen];
+                            cir_fixed = await testutils.genMain(file, template_name, params);
+                            await cir_fixed.loadSymbols();
+                            input = [1,2,3,4,5,6,7,8,9,10,11,12];
+                            try {
+                                const witness = await cir_fixed.calculateWitness({ "in": input, "index": 1, "length": length });
+                                const output = testutils.getWitnessArray(witness, cir_fixed.symbols, "main.out");    
+                                assert.sameOrderedMembers(
+                                    output.slice(0, length).map(Number),
+                                    input.slice(1, 1 + length)
+                                );
+                                if (1 + length > inLen || length > outLen) {
+                                    assert.fail("Should have failed");
+                                }
+                            } catch (e) {
+                                assert.include(e.message, "Error in template RangeCheck");
+                            }
+                        });
+                    })(length);
+                }
+            });
+
+        });
+    });
+
+})
+
+describe("ASCIISubstrExistsInB64" , () => {
+    const numsPerGroup = 8;
+
+    async function genCircuit(maxJwtLen, maxA, numsPerGroup) {
+        return await testutils.genMain(
+            path.join(__dirname, "../circuits/helpers", "strings.circom"),
+            "ASCIISubstrExistsInB64",
+            [maxJwtLen, maxA, numsPerGroup]
+        );
+    }
+
+    async function genProof(circuit, jwt, maxJwtLen, A, maxA, indexB, lenB, payloadIndex, lenA=A.length) {
+        assert.isTrue(jwt.length <= maxJwtLen, "JWT too long");
+        assert.isTrue(A.length <= maxA, "A too long");
+
+        const inputs = {
+            "b64Str":  utils.padWithZeroes(jwt.split("").map(c => c.charCodeAt(0)), maxJwtLen), // pad with 0s
+            "lenB": lenB,
+            "BIndex": indexB,
+            "A": utils.padWithZeroes(A.split("").map(c => c.charCodeAt(0)), maxA), // pad with 0s
+            "lenA": lenA,
+            "payloadIndex": payloadIndex
+        };
+
+        const witness = await circuit.calculateWitness(inputs);
+        await circuit.checkConstraints(witness);
+    }
+
+    describe("lenA < maxA", () => {
+        const maxJwtLen = 200;
+        const A = '"sub":"4840061"}';
+        const maxA = A.length + 14;
+        const maxB = 1 + ((maxA / 3) * 4);
+        var circuit;
+
+        before(() => {
+            assert.equal(maxA % 3, 0);
+            assert.isTrue(maxB <= maxJwtLen);
+            assert.equal(maxJwtLen % numsPerGroup, 0);
+        })
+
+        describe("Simple JWTs", () => {
+            // Prefixes chosen such that index of A in the JWT is 0, 1, 2
+            const prefixes = ['{   ', '{', '{ '];
+            const decoded_jwts = prefixes.map(prefix => prefix + A);
+            const jwts = decoded_jwts.map(jwt => Buffer.from(jwt).toString("base64url"));
+
+            const X = jwts.map(jwt => jwtutils.indicesOfB64(jwt, 'sub'));
+            const indicesB = X.map(tuple => tuple[0]);
+            const lensB = X.map(tuple => tuple[1]);
+
+            before(async () => {
+                assert.equal(maxA % 3, 0);
+                assert.isTrue(maxB <= maxJwtLen);
+                for (let i = 0; i < decoded_jwts.length; i++) {
+                    assert.deepEqual(decoded_jwts[i].indexOf(A) % 4 , i);
+                    assert.deepEqual(jwtutils.getExtendedClaim(decoded_jwts[i], 'sub'), A);
+                    assert.deepEqual(jwtutils.decodeBase64URL(
+                        jwts[i].slice(indicesB[i], indicesB[i] + lensB[i]),
+                        indicesB[i]
+                    ), A);
+                }
+            });
+
+            for (let offset = 0; offset < 3; offset++) {
+                it(`Succeeds when A is at offset ${offset}`, async () => {
+                    circuit = await genCircuit(maxJwtLen, maxA, numsPerGroup);
+                    await genProof(circuit, jwts[offset], maxJwtLen, A, maxA, indicesB[offset], lensB[offset], 0);
+                });
+    
+                it(`Fails when substring index is either large or small. A offset ${offset}`, async () => {
+                    for (let i in [1, -1]) {
+                        try {
+                            circuit = await genCircuit(maxJwtLen, maxA, numsPerGroup);
+                            await genProof(circuit, jwts[offset], maxJwtLen, A, maxA, indicesB[offset] + i, lensB[offset], 0);
+                            assert.fail("Should have failed");
+                        } catch (e) {
+                            assert.include(e.message, "Error in template ASCIISubstrExistsInB64");
+                        }
+                    }
+                });
+
+                // NOTE: Removed "Fails when lenB is small" tests after the change to SliceGrouped as it might succeed in some cases now.
+    
+                it(`Succeeds when lenB is large. A offset ${offset}`, async() => {
+                    circuit = await genCircuit(maxJwtLen, maxA, numsPerGroup);
+                    await genProof(circuit, jwts[offset], maxJwtLen, A, maxA, indicesB[offset], lensB[offset] + 1, 0);
+                });
+            }
+    
+            it("Fails when substring index < payload index", async () => {
+                try {
+                    circuit = await genCircuit(maxJwtLen, maxA, numsPerGroup);
+                    await genProof(circuit, jwts[0], maxJwtLen, A, maxA, indicesB[0], lensB[0], indicesB[0] + 1);
+                    assert.fail("Should have failed");
+                } catch (e) {
+                    assert.include(e.message, "Error in template RemainderMod4");
+                    assert.include(e.message, "Error in template Num2Bits");
+                }
+            });    
+        });
+
+        describe("Bigger JWTs", async () => {
+            const payload = JSON.stringify({
+                "iat": 1616421600,
+                "exp": 1616425200,
+                "name": "John Doe",
+                "sub": "4840061"
+            });
+            const encoded_payload = Buffer.from(payload).toString("base64url");
+
+            it("No header", async () => {
+                circuit = await genCircuit(maxJwtLen, maxA, numsPerGroup);
+                const jwt = encoded_payload;
+                [index, len] = jwtutils.indicesOfB64(jwt, 'sub');
+                await genProof(circuit, jwt, maxJwtLen, A, maxA, index, len, 0);    
+            });
+
+            it("With header", async () => {
+                circuit = await genCircuit(maxJwtLen, maxA, numsPerGroup);
+                const header = JSON.stringify({
+                    "alg": "RS256",
+                    "typ": "JWT"
+                });
+                const jwt = Buffer.from(header).toString("base64url") + "." + encoded_payload;
+                [index, len] = jwtutils.indicesOfB64(encoded_payload, 'sub');
+                const payload_index = jwt.indexOf(encoded_payload);
+                await genProof(circuit, jwt, maxJwtLen, A, maxA, index + payload_index, len, payload_index);
+            });
+
+            it("lenB = maxB", async () => {
+                circuit = await genCircuit(maxJwtLen, maxA, numsPerGroup);
+                const jwt = encoded_payload;
+                [index, len] = jwtutils.indicesOfB64(jwt, 'sub');
+                await genProof(circuit, jwt, maxJwtLen, A, maxA, index, maxB, 0);
+            });
+
+            it("Fails when lenB > maxB or lenB < 0", async () => {
+                const jwt = encoded_payload;
+                [index, len] = jwtutils.indicesOfB64(jwt, 'sub');
+                for (lenB of [-1, maxB + 1]) {
+                    circuit = await genCircuit(maxJwtLen, maxA, numsPerGroup);
+                    try {
+                        await genProof(circuit, jwt, maxJwtLen, A, maxA, index, lenB, 0);
+                        assert.fail("Should have failed");
+                    } catch (e) {
+                        assert.include(e.message, "Error in template RangeCheck");
+                        assert.include(e.message, "Error in template Slice");
+                    }
+                }
+            })
+
+            it("Fails when lenA > maxA or lenA < 0", async () => {
+                const jwt = encoded_payload;
+                [index, len] = jwtutils.indicesOfB64(jwt, 'sub');
+                for (lenA of [maxA + 1, -1]) {
+                    circuit = await genCircuit(maxJwtLen, maxA, numsPerGroup);
+                    try {
+                        await genProof(circuit, jwt, maxJwtLen, A, maxA, index, len, 0, lenA);
+                        assert.fail("Should have failed");
+                    } catch (e) {
+                        assert.include(e.message, "Error in template LTBitVector");
+                    }
+                }
+            });
+
+        });
+    });
+
+    it("lenA = maxA", async () => {
+        const maxJwtLen = 200;
+        const A = '"sub":"484061",'; // 15 chars
+        assert.deepEqual(A.length % 3, 0);
+
+        const lenA = A.length;
+        const maxA = lenA;
+        const maxB = 1 + ((maxA / 3) * 4);
+        const header = JSON.stringify({
+            "alg": "RS256",
+            "typ": "JWT"
+        });
+        const payload = JSON.stringify({
+            "sub": "484061",
+            "iat": 1616421600,
+            "exp": 1616425200,
+            "name": "John Doe"
+        });
+        assert.deepEqual(jwtutils.getExtendedClaim(payload, 'sub'), A);
+        const encoded_payload = Buffer.from(payload).toString("base64url");
+        const jwt = Buffer.from(header).toString("base64url") + "." + encoded_payload;
+
+        const payload_index = jwt.indexOf(encoded_payload);
+
+        [index, len] = jwtutils.indicesOfB64(encoded_payload, 'sub');
+        assert.isAtMost(len, maxB);
+
+        const circuit = await genCircuit(maxJwtLen, maxA, numsPerGroup);
+        await genProof(circuit, jwt, maxJwtLen, A, maxA, index + payload_index, len, payload_index);
+    });
+
+    it("Nonce", async() => {
+        const maxJwtLen = 200;
+        const bignum = 8679359968269066238270369971672891012793979385072768529748854974904529914083n;
+        const numbits = bignum.toString(2).length;
+        expect(numbits).to.be.at.most(254);
+
+        const bignum_in_base64 = Buffer.from(bignum.toString(16), "hex").toString("base64url");
+        chai.expect(bignum_in_base64.length).to.equal(Math.ceil(numbits / 6));
+
+        const A = '"nonce":"' + bignum_in_base64 + '",'; // <= 11 + Math.ceil(254/6) = 54 chars 
+
+        const lenA = A.length;
+        const maxA = lenA;
+        expect(maxA).to.be.at.most(54);
+        const maxB = 1 + ((maxA / 3) * 4);
+
+        const header = JSON.stringify({
+            "alg": "RS256",
+            "typ": "JWT"
+        });
+        const payload = JSON.stringify({
+            "sub": "484061",
+            "iat": 1616421600,
+            "exp": 1616425200,
+            "nonce": bignum_in_base64,
+            "name": "John Doe"
+        });
+        assert.deepEqual(jwtutils.getExtendedClaim(payload, 'nonce'), A);
+
+        const encoded_payload = Buffer.from(payload).toString("base64url");
+        const jwt = Buffer.from(header).toString("base64url") + "." + encoded_payload;
+        const payload_index = jwt.indexOf(encoded_payload);
+        const [index, len] = jwtutils.indicesOfB64(encoded_payload, 'nonce');
+        assert.isAtMost(len, maxB);
+
+        const circuit = await genCircuit(maxJwtLen, maxA, numsPerGroup);
+        await genProof(circuit, jwt, maxJwtLen, A, maxA, index + payload_index, len, payload_index);
+    });
+});
diff --git a/zklogin-circuits/test/testutils.js b/zklogin-circuits/test/testutils.js
new file mode 100644
index 0000000000..07082879b8
--- /dev/null
+++ b/zklogin-circuits/test/testutils.js
@@ -0,0 +1,57 @@
+const temp = require("temp");
+const path = require("path");
+const fs = require("fs");
+
+const circom_wasm = require("circom_tester").wasm;
+const utils = require("../js/src/utils");
+
+function getWitnessValue(witness, symbols, varName) {
+    return witness[symbols[varName]['varIdx']];
+}
+
+function getWitnessMap(witness, symbols, arrName) {
+    return Object.entries(symbols).filter(([index, symbol]) => index.startsWith(arrName)).map(([index, symbol]) => Object.assign({}, symbol, { "name": index, "value": witness[symbol['varIdx']] }) );
+}
+
+function getWitnessArray(witness, symbols, arrName) {
+    return Object.entries(symbols).filter(([index, symbol]) => index.startsWith(`${arrName}[`)).map(([index, symbol]) => witness[symbol['varIdx']] );
+}
+
+function getWitnessBuffer(witness, symbols, arrName, varSize=1) {
+    const witnessArray = getWitnessArray(witness, symbols, arrName);
+    if(varSize == 1) {
+        return utils.bitArray2Buffer(witnessArray);
+    } else {
+        return utils.bigIntArray2Buffer(witnessArray, varSize);
+    }
+}
+
+async function genMain(template_file, template_name, params = [], file_name) {
+    temp.track();
+    
+    const temp_circuit = await temp.open({prefix: template_name, suffix: ".circom"});
+    const include_path = path.relative(temp_circuit.path, template_file);
+    const params_string = JSON.stringify(params).slice(1, -1);
+    
+    fs.writeSync(temp_circuit.fd, `
+pragma circom 2.0.0;
+
+include "${include_path}";
+
+component main = ${template_name}(${params_string});
+    `);
+
+    if (file_name !== undefined) {
+      fs.copyFileSync(temp_circuit.path, file_name);
+    }
+    
+    return circom_wasm(temp_circuit.path);
+}
+
+module.exports = {
+    getWitnessValue: getWitnessValue,
+    getWitnessMap: getWitnessMap,
+    getWitnessArray: getWitnessArray,
+    getWitnessBuffer: getWitnessBuffer,
+    genMain: genMain
+}
\ No newline at end of file
diff --git a/zklogin-circuits/test/utils.test.js b/zklogin-circuits/test/utils.test.js
new file mode 100644
index 0000000000..6b5b47c4ee
--- /dev/null
+++ b/zklogin-circuits/test/utils.test.js
@@ -0,0 +1,255 @@
+// Tests for important functions in utils.js and jwtutils.js
+
+const chai = require("chai");
+const assert = chai.assert;
+const expect = chai.expect;
+const crypto = require("crypto");
+
+const circuit = require("../js/src/circuitutils");
+const utils = require("../js/src/utils");
+const jwtutils = require("../js/src/jwtutils");
+
+describe("Circuit Utilities", () => {
+    it("Buffer to/from bit array works as expected", async () => {
+        const input = crypto.randomBytes(20*32).toString("hex");
+        
+        const bits = utils.buffer2BitArray(Buffer.from(input));
+        const buffer = utils.bitArray2Buffer(bits);
+        
+        assert.equal(input, buffer.toString());
+    });
+    
+    it("rfc4634#4.1 padding conforms: L % 512 = 0", async () => {
+        const input = crypto.randomBytes(512/8/2).toString("hex");
+        
+        const bits = utils.buffer2BitArray(Buffer.from(input));
+        const padded = circuit.padMessage(bits);
+        
+        assert.equal(bits.length, 512);
+        assert.equal(padded.length, 1024); // Padding a 448+-bit message requires an additional block
+        assert.equal(1, padded.slice(-512, -511)); // Padding begins with 1
+        assert.equal(bits.length, parseInt(padded.slice(-64).join(''), 2)); // base2(L)
+    });
+    
+    it("rfc4634#4.1 padding conforms: L % 512 = 65", async () => {
+        const input = crypto.randomBytes(512/8/2).toString("hex");
+        
+        const bits = utils.buffer2BitArray(Buffer.from(input)).slice(0, 447);
+        const padded = circuit.padMessage(bits);
+        
+        assert.equal(bits.length, 447);
+        assert.equal(padded.length, 512);
+        assert.equal(1, padded.slice(-65, -64)); // Padding begins with 1
+        assert.equal(bits.length, parseInt(padded.slice(-64).join(''), 2));
+    });
+    
+    it("rfc4634#4.1 padding conforms: L % 512 = 100", async () => {
+        const input = crypto.randomBytes(512/8/2).toString("hex");
+        
+        const bits = utils.buffer2BitArray(Buffer.from(input)).slice(0, 412);
+        const padded = circuit.padMessage(bits);
+        
+        assert.equal(bits.length, 412);
+        assert.equal(padded.length, 512);
+        assert.equal(1, padded.slice(-100, -99)); // Padding begins with 1
+        assert.equal(bits.length, parseInt(padded.slice(-64).join(''), 2));
+    });
+});
+
+describe("JWT utils tests", () => {
+    const getExtendedClaim = jwtutils.getExtendedClaim;
+    const b64Len = jwtutils.b64Len;
+    const indicesOfB64 = jwtutils.indicesOfB64;
+    const decodeBase64URL = jwtutils.decodeBase64URL;
+
+    describe("getExtendedClaim", () => {
+        it("Normal strings", () => {
+            // Without quotes, not end
+            assert.deepEqual(
+                getExtendedClaim('{"iss":12345,"sub":45678,"aud":"https://example.com"}', "iss"),
+                '"iss":12345,'
+            );
+
+            // With quotes, not end
+            assert.deepEqual(
+                getExtendedClaim('{"iss":"12345","sub":45678,"aud":"https://example.com"}', "iss"),
+                '"iss":"12345",'
+            );
+
+            // Without quotes, end
+            assert.deepEqual(
+                getExtendedClaim('{"iss":12345,"sub":45678}', "sub"),
+                '"sub":45678}'
+            );
+    
+            // With quotes, end
+            assert.deepEqual(
+                getExtendedClaim('{"iss":12345,"sub":45678,"aud":"https://example.com"}', "aud"),
+                '"aud":"https://example.com"}'
+            );
+        })
+
+        it("With escapes", () => {
+            assert.deepEqual(
+                getExtendedClaim('{"iss":"https:\\/\\/www.facebook.com","sub":45678,"aud":12345}', "iss"),
+                '"iss":"https:\\/\\/www.facebook.com",'
+            );
+    
+            assert.deepEqual(
+                getExtendedClaim('{"iss":"https:\\/\\/www.facebook.com","sub":45678,"picture":"https:\\/\\/platform-lookaside.fbsbx.com\\/platform\\/profilepic\\/?asid=708562611009525&height=100&width=100&ext=1684596798&hash=AeRIgRL_XooqrdDidNY"}', "picture"),
+                '"picture":"https:\\/\\/platform-lookaside.fbsbx.com\\/platform\\/profilepic\\/?asid=708562611009525&height=100&width=100&ext=1684596798&hash=AeRIgRL_XooqrdDidNY"}'
+            );
+        });
+    });
+
+    it("b64Len", () => {
+        assert.deepEqual(b64Len(0, 0), 0, "Test case 1");
+        assert.deepEqual(b64Len(3, 0), 4, "Test case 2");
+        assert.deepEqual(b64Len(3, 1), 5, "Test case 3");
+        assert.deepEqual(b64Len(3, 2), 5, "Test case 4");
+        assert.deepEqual(b64Len(6, 0), 8, "Test case 5");
+        assert.deepEqual(b64Len(6, 1), 9, "Test case 6");
+        assert.deepEqual(b64Len(6, 2), 9, "Test case 7");
+        assert.deepEqual(b64Len(9, 0), 12, "Test case 8");
+        assert.deepEqual(b64Len(9, 1), 13, "Test case 9");
+        assert.deepEqual(b64Len(9, 2), 13, "Test case 10");
+    });
+
+    describe("decodeBase64URL", () => {
+        it("Corner case: Two length strings", () => {
+            const input = Buffer.from("H").toString('base64url');
+            assert.deepEqual(input.length, 2);
+            assert.deepEqual(decodeBase64URL(input, 0), 'H');
+
+            const input2 = Buffer.from("He").toString('base64url').slice(1);
+            assert.deepEqual(input2.length, 2);
+            assert.deepEqual(decodeBase64URL(input2, 1), 'e');
+
+            const input3 = Buffer.from("Hel").toString('base64url').slice(2);
+            assert.deepEqual(input3.length, 2);
+            assert.deepEqual(decodeBase64URL(input3, 2), 'l');
+        });
+
+        it('should decode a tightly packed base64URL string with i % 4 == 0', () => {
+            const input = Buffer.from("Hello, world!").toString('base64url');
+            const i = 0;
+            const expected = "Hello, world!";
+
+            const result = decodeBase64URL(input, i);
+            assert.deepEqual(result, expected);
+        });
+
+        it('should decode a tightly packed base64URL string with i % 4 == 1', () => {
+            const input = Buffer.from("Hello, world").toString('base64url').slice(1);
+            const i = 1;
+            const expected = 'ello, world';
+        
+            const result = decodeBase64URL(input, i);
+            assert.deepEqual(result, expected);
+        });
+
+        it('should decode a tightly packed base64URL string with i % 4 == 2', () => {
+            const input = Buffer.from("Hello, world").toString('base64url').slice(2);
+            const i = 2;
+            const expected = 'llo, world';
+        
+            const result = decodeBase64URL(input, i);
+            assert.deepEqual(result, expected);
+        });
+
+        it('should throw an error when i % 4 == 3', () => {
+            const input = Buffer.from("Hello, world").toString('base64url');
+        
+            try {
+                decodeBase64URL(input, 3);
+                assert.fail();
+            } catch (e) {
+                assert.include(e.message, "not tightly packed because i%4 = 3");
+            }
+        });
+
+        it('should throw an error when (i + s.length - 1) % 4 == 0', () => {
+            const input = Buffer.from("Hello, world").toString('base64url').slice(1);
+            const i = 2;
+            assert.deepEqual((i + input.length - 1) % 4, 0);
+            try {
+                decodeBase64URL(input, i);
+                assert.fail();
+            } catch (e) {
+                assert.include(e.message, "not tightly packed because (i + s.length - 1)%4 = 0");
+            }
+        });
+
+        it("Base64url testing", () => {
+            // this input has a different base64 and base64url encoding
+            const extendedInput = ',' + 'abc/?' + '}';
+            const b64 = utils.trimEndByChar(Buffer.from(extendedInput).toString('base64'), '=');
+            const Base64URL = Buffer.from(extendedInput).toString('base64url');
+            assert.isTrue(b64 !== Base64URL);
+
+            assert.deepEqual(decodeBase64URL(Base64URL, 0), extendedInput);
+        })
+    })
+
+    describe("indicesOfB64, b64Len, b64Index", () => {
+        it("Crafted JWTs", () => {
+            const sub_claim = '"sub":45678';
+            [
+                '{"sub":45678,"iss":12345}', // At the start
+                '{"iss":12345,"sub":45678}', // At the end
+                '{"iss":12345,"sub":45678,"aud":"https://example.com"}' // In the middle
+            ].forEach(input => {
+                assert.isTrue(input.includes(sub_claim));
+                const sub_claim_with_last_char = getExtendedClaim(input, "sub");
+                assert.deepEqual(sub_claim_with_last_char.slice(0, -1), sub_claim);
+                assert.isTrue(sub_claim_with_last_char.slice(-1) === ',' || sub_claim_with_last_char.slice(-1) === '}');
+    
+                jwt = Buffer.from(input).toString('base64url');
+                const [start, len] = indicesOfB64(jwt, "sub");
+    
+                const substr = jwt.slice(start, start + len);
+                const decoded = decodeBase64URL(substr, start % 4);
+                assert.deepEqual(decoded, sub_claim_with_last_char);
+            });    
+        })
+
+        it("Google JWT", () => {
+            const jwt = require('../js/testvectors/realJWTs').GOOGLE.jwt;
+            const payload = jwt.split('.')[1];
+            const decoded_payload = Buffer.from(payload, 'base64url').toString();
+            const sub_claim_with_last_char = getExtendedClaim(decoded_payload, "sub");
+            assert.deepEqual(sub_claim_with_last_char, '"sub":"110463452167303598383",');
+
+            const [start, len] = indicesOfB64(payload, "sub");
+            const substr = payload.slice(start, start + len);
+            const decoded = decodeBase64URL(substr, start % 4);
+            assert.deepEqual(decoded, sub_claim_with_last_char);    
+        })
+
+        it("Twitch JWT", () => {
+            const jwt = require('../js/testvectors/realJWTs').TWITCH.jwt;
+            const payload = jwt.split('.')[1];
+            const decoded_payload = Buffer.from(payload, 'base64url').toString();
+
+            const sub_claim_with_last_char = getExtendedClaim(decoded_payload, "sub");
+            assert.deepEqual(sub_claim_with_last_char, '"sub":"904448692",');
+            const [start, len] = indicesOfB64(payload, "sub");
+            const substr = payload.slice(start, start + len);
+            const decoded = decodeBase64URL(substr, start % 4);
+            assert.deepEqual(decoded, sub_claim_with_last_char);
+        })
+
+        it.skip("Twitch JWT with username", () => {
+            const jwt = require('../testvectors/realJWTs').twitch.jwt;
+            const payload = jwt.split('.')[1];
+            const decoded_payload = Buffer.from(payload, 'base64url').toString();
+
+            const username = getExtendedClaim(decoded_payload, "preferred_username");
+            assert.deepEqual(username, '"preferred_username":"joyqvq"}');
+            const [start2, len2] = indicesOfB64(payload, "preferred_username");
+            const substr2 = payload.slice(start2, start2 + len2);
+            const decoded2 = decodeBase64URL(substr2, start2 % 4);
+            assert.deepEqual(decoded2, username);
+        })
+    })
+});
\ No newline at end of file
diff --git a/zklogin-circuits/testvectors/realJWTs.ts b/zklogin-circuits/testvectors/realJWTs.ts
new file mode 100644
index 0000000000..70d5d7fc40
--- /dev/null
+++ b/zklogin-circuits/testvectors/realJWTs.ts
@@ -0,0 +1,82 @@
+import { JWK } from "jwk-to-pem";
+
+class TestVector {
+    "jwt": string;
+    "jwk": JWK;
+    header?: any;
+    payload?: any;
+};
+
+const GOOGLE: TestVector = { // From extension
+    jwt: "eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5YWZkYTM2ODJlYmYwOWViMzA1NWMxYzRiZDM5Yjc1MWZiZjgxOTUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI1NzU1MTkyMDQyMzctbXNvcDllcDQ1dTJ1bzk4aGFwcW1uZ3Y4ZDg0cWRjOGsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI1NzU1MTkyMDQyMzctbXNvcDllcDQ1dTJ1bzk4aGFwcW1uZ3Y4ZDg0cWRjOGsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTA0NjM0NTIxNjczMDM1OTgzODMiLCJlbWFpbCI6IndhbmdxaWFveWkuam95QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJub25jZSI6IkdDd3EyekN1cXRzYTFCaGFBYzJTRWx3VW9ZdjhqS2hFNnZzNlZtZXB1Mk0iLCJpYXQiOjE2ODMzMjMyNjksImV4cCI6MTY4MzMyNjg2OSwianRpIjoiMDEzMzA2YjY1MmY0Zjg1MjUxZTU1OGVhNGFhOWJkYWI3ZmQxNzk3ZiJ9.",
+    jwk: {
+        "e": "AQAB",
+        "kty": "RSA",
+        "n": "t0VFy4n4MGtbMWJKk5qfCY2WGBja2WSWQ2zsLziSx9p1QE0QgXtr1x85PnQYaYrAvOBiXm2mrxWnZ42MxaUUu9xyykTDxsNWHK--ufchdaqJwfqd5Ecu-tHvFkMIs2g39pmG8QfXJHKMqczKrvcHHJrpTqZuos1uhYM9gxOLVP8wTAUPNqa1caiLbsszUC7yaMO3LY1WLQST79Z8u5xttKXShXFv1CCNs8-7vQ1IB5DWQSR2um1KV4t42d31Un4-8cNiURx9HmJNJzOXbTG-vDeD6sapFf5OGDsCLO4YvzzkzTsYBIQy_p88qNX0a6AeU13enxhbasSc-ApPqlxBdQ"
+    },
+    header: {
+        "alg": "RS256",
+        "kid": "c9afda3682ebf09eb3055c1c4bd39b751fbf8195",
+        "typ": "JWT"
+    },
+    payload: {
+        "iss": "https://accounts.google.com",
+        "azp": "575519204237-msop9ep45u2uo98hapqmngv8d84qdc8k.apps.googleusercontent.com",
+        "aud": "575519204237-msop9ep45u2uo98hapqmngv8d84qdc8k.apps.googleusercontent.com",
+        "sub": "110463452167303598383",
+        "email": "wangqiaoyi.joy@gmail.com",
+        "email_verified": true,
+        "nonce": "GCwq2zCuqtsa1BhaAc2SElwUoYv8jKhE6vs6Vmepu2M",
+        "iat": 1683323269,
+        "exp": 1683326869,
+        "jti": "013306b652f4f85251e558ea4aa9bdab7fd1797f"
+    }
+}
+
+const FACEBOOK: TestVector = {
+    jwt: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjhhYjM3MTc1MjdhZTQwMWRlNWRjMGRmNGY5ZjJmZTZkNjUwY2NhYWUifQ.eyJpc3MiOiJodHRwczpcL1wvd3d3LmZhY2Vib29rLmNvbSIsImF1ZCI6IjEyNDE1NTkzNjY3NTUyMTQiLCJzdWIiOiI3MDg1NjI2MTEwMDk1MjUiLCJpYXQiOjE2ODIwMDQ3OTcsImV4cCI6MTY4MjAwODM5NywianRpIjoiR3NFMy5iNTY2YmI4Mjg4ZTQ3ZTMxMjIwZDM3NzY4ZWJlMWM5NTIzOTM0YzYxZmE3ZjkzYWM3MWI3NTcxZjg5M2Q3NzZlIiwibm9uY2UiOiIxNjYzNzkxODgxMzkwODA2MDI2MTg3MDUyODkwMzk5NDAzODcyMTY2OTc5OTYxMzgwMzYwMTYxNjY3ODE1NTUxMjE4MTI3MzI4OTQ3NyIsImdpdmVuX25hbWUiOiJKb3kiLCJmYW1pbHlfbmFtZSI6IldhbmciLCJuYW1lIjoiSm95IFdhbmciLCJwaWN0dXJlIjoiaHR0cHM6XC9cL3BsYXRmb3JtLWxvb2thc2lkZS5mYnNieC5jb21cL3BsYXRmb3JtXC9wcm9maWxlcGljXC8_YXNpZD03MDg1NjI2MTEwMDk1MjUmaGVpZ2h0PTEwMCZ3aWR0aD0xMDAmZXh0PTE2ODQ1OTY3OTgmaGFzaD1BZVJJZ1JMX1hvb3FyZERpZE5ZIn0.taavbVRWSJYQAGVfADLb0Un1gHakURX1lbGO7wjOjRgOZxnoF_1fAOE9QoSftZPMpg4-WhYYl-sC0SxETX6rW9lULT7oNomuO8Jm0kgyxeITvi7oWK_QLt8VWJZPAM2ZP2-xEFR92juQKTnbsuAB14fl2gXKlt_QZDtAmi3Gno0By94E7bWmSPd1mQJA1M0GUu4LDNZe0_mGHEQ9ygamyQVfB9u3STTeb1HOfNKO3HXmwmTJRhdIuc_z96wWBf8-JR52d1gAL0MWL5my6yPyvqtpfti-8-jWYPUuR-KOzhdj-IsaGZMzgJUJZg7wg6z9_P2Uqn3Muh-BSzTNYxVYPQ",
+    jwk: {
+        "kty": "RSA", 
+        "n": "xirBDhLBy2BlSheGTJx3_jWdUado6QHTD5_rZK3_26m02zCFGqkHbJJihDwwTdmpVanrw0cb_7OIlw0hZjVNPnjhPHnwy-zT1XYK7qAfVBD0wm9GJhGsAe5dWSCjee7U-uRHAijNYXBeQn5Oh2w1KCCDr6Ccgk65xk5cuMCanKtAf6yXzVnKZJyvBxSzZD4p0Bw8MSOzSXY5srvUSGuCeajV8D-IDaP4LEXWswLUJKD3DrOlKH8QHHUhnPHjXAUZId_PMttmK32TqkWJeL0sjJ2KT68QHXCgL2RDWCg_TiOM8yx_LVGpeg0mLaadw0UNVT_nfmH_1s1S9XVpEu42Xw", 
+        "e": "AQAB"
+    }
+}
+
+const GOOGLE_OLD: TestVector = { // from playground
+    jwt: 'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijk4NmVlOWEzYjc1MjBiNDk0ZGY1NGZlMzJlM2U1YzRjYTY4NWM4OWQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTc5MTI3MzU2NTg1NDEzMzY2NDYiLCJhdF9oYXNoIjoicm9aYm11cUVXdmNHSThDR2N1SnJmUSIsImlhdCI6MTY3OTY3NDE0NSwiZXhwIjoxNjc5Njc3NzQ1fQ.G8cciXefORmYvdwrfVAO6DjDy7DUWe6NxyanGg4w7EQBu8Ab7PJAeXhU7HL5w_LtTgiLA3Ew07RRzuNuaFITvs_m9lIolxHOl0BZSyGIGlI9BRiBFQQK2OZ2b8xetWz3B1mezcwlrrQMgbLQI0puuaA6917h_3MjIgZu_bQkjQH3Lwl3kkZWp0W-PRuK20KAQneNFB9ehTvSeRkImIr5QlZU6LMb7M3rI_-gP6ePRryAN9UCGBASzNEYLaQz-eMIdYFw-WmqkesTX1IDLQT0n44BhG9-9mWIA6kNRSBo9FV89VGKvYION9PTDds1vsf5h3smBQZjourR2H5pLJ_MUA',
+    jwk: {
+        "e": "AQAB",
+        "kty": "RSA",
+        "n": "onb-s1Mvbpti06Sp-ZsHH5eeJxdvMhRgfmx5zK7cVlcAajI_0rKu8ylU2CkfgPlMe9-8W5ayozm1h2yx2ToS7P7qoR1sMINXbKxobu8xy9zOBuFAr3WvEoor6lo0Qp747_4bN1sVU6GBEBEXLjb8vHN-o_yoBv8NSB_yP7XbEaS3U5MJ4V2s5o7LziIIRP9PtzF0m3kWm7DuyEzGvCaW8s9bOiMd3eZyXXyfKjlBB727eBXgwqcV-PttECRw6JCLO-11__lmqfKIj5CBw18Pb4ZrNwBa-XrGXfHSSAJXFkR4LR7Bj24sWzlOcKXN2Ew4h3WDJfxtN_StNSYoagyaFQ"
+    }
+}
+
+const TWITCH: TestVector = {
+    jwt: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ.eyJhdWQiOiJkMzFpY3FsNmw4eHpwYTdlZjMxenR4eXNzNDZvY2siLCJleHAiOjE2ODMzMjQyNjgsImlhdCI6MTY4MzMyMzM2OCwiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiwic3ViIjoiOTA0NDQ4NjkyIiwiZW1haWwiOiJ3YW5nam95am95MjAxOUBnbWFpbC5jb20iLCJub25jZSI6IkdDd3EyekN1cXRzYTFCaGFBYzJTRWx3VW9ZdjhqS2hFNnZzNlZtZXB1Mk0ifQ.",
+    jwk: {
+        "e": "AQAB",
+        "kty": "RSA",
+        "n": "6lq9MQ-q6hcxr7kOUp-tHlHtdcDsVLwVIw13iXUCvuDOeCi0VSuxCCUY6UmMjy53dX00ih2E4Y4UvlrmmurK0eG26b-HMNNAvCGsVXHU3RcRhVoHDaOwHwU72j7bpHn9XbP3Q3jebX6KIfNbei2MiR0Wyb8RZHE-aZhRYO8_-k9G2GycTpvc-2GBsP8VHLUKKfAs2B6sW3q3ymU6M0L-cFXkZ9fHkn9ejs-sqZPhMJxtBPBxoUIUQFTgv4VXTSv914f_YkNw-EjuwbgwXMvpyr06EyfImxHoxsZkFYB-qBYHtaMxTnFsZBr6fn8Ha2JqT1hoP7Z5r5wxDu3GQhKkHw"
+    },
+    header: {
+        "alg": "RS256",
+        "typ": "JWT",
+        "kid": "1"
+    },
+    payload: {
+        "aud": "d31icql6l8xzpa7ef31ztxyss46ock",
+        "exp": 1683324268,
+        "iat": 1683323368,
+        "iss": "https://id.twitch.tv/oauth2",
+        "sub": "904448692",
+        "email": "wangjoyjoy2019@gmail.com",
+        "nonce": "GCwq2zCuqtsa1BhaAc2SElwUoYv8jKhE6vs6Vmepu2M"
+    }
+}
+
+export {
+    GOOGLE,
+    GOOGLE_OLD,
+    FACEBOOK,
+    TWITCH
+}
\ No newline at end of file
diff --git a/zklogin-circuits/testvectors/sampleWalletInputs.json b/zklogin-circuits/testvectors/sampleWalletInputs.json
new file mode 100644
index 0000000000..d09ee3e10a
--- /dev/null
+++ b/zklogin-circuits/testvectors/sampleWalletInputs.json
@@ -0,0 +1,8 @@
+{
+    "jwt": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImM5YWZkYTM2ODJlYmYwOWViMzA1NWMxYzRiZDM5Yjc1MWZiZjgxOTUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI1NzU1MTkyMDQyMzctbXNvcDllcDQ1dTJ1bzk4aGFwcW1uZ3Y4ZDg0cWRjOGsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI1NzU1MTkyMDQyMzctbXNvcDllcDQ1dTJ1bzk4aGFwcW1uZ3Y4ZDg0cWRjOGsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTA0NjM0NTIxNjczMDM1OTgzODMiLCJlbWFpbCI6IndhbmdxaWFveWkuam95QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJub25jZSI6IkdDd3EyekN1cXRzYTFCaGFBYzJTRWx3VW9ZdjhqS2hFNnZzNlZtZXB1Mk0iLCJpYXQiOjE2ODMzMjMyNjksImV4cCI6MTY4MzMyNjg2OSwianRpIjoiMDEzMzA2YjY1MmY0Zjg1MjUxZTU1OGVhNGFhOWJkYWI3ZmQxNzk3ZiJ9.",
+    "eph_public_key": "6102104556984239574011716987889141562529524682626013161214470481167897974645",
+    "max_epoch": 10000,
+    "jwt_rand": "100681567828351849884072155819400689117",
+    "user_pin": "283089722053851751073973683904920435104",
+    "key_claim_name": "sub"
+}
diff --git a/zklogin-circuits/testvectors/sampleZKPInputs.json b/zklogin-circuits/testvectors/sampleZKPInputs.json
new file mode 100644
index 0000000000..b500f78181
--- /dev/null
+++ b/zklogin-circuits/testvectors/sampleZKPInputs.json
@@ -0,0 +1,1683 @@
+{
+  "content": [
+    "101",
+    "121",
+    "74",
+    "104",
+    "98",
+    "71",
+    "99",
+    "105",
+    "79",
+    "105",
+    "74",
+    "83",
+    "85",
+    "122",
+    "73",
+    "49",
+    "78",
+    "105",
+    "73",
+    "115",
+    "73",
+    "109",
+    "116",
+    "112",
+    "90",
+    "67",
+    "73",
+    "54",
+    "73",
+    "109",
+    "77",
+    "53",
+    "89",
+    "87",
+    "90",
+    "107",
+    "89",
+    "84",
+    "77",
+    "50",
+    "79",
+    "68",
+    "74",
+    "108",
+    "89",
+    "109",
+    "89",
+    "119",
+    "79",
+    "87",
+    "86",
+    "105",
+    "77",
+    "122",
+    "65",
+    "49",
+    "78",
+    "87",
+    "77",
+    "120",
+    "89",
+    "122",
+    "82",
+    "105",
+    "90",
+    "68",
+    "77",
+    "53",
+    "89",
+    "106",
+    "99",
+    "49",
+    "77",
+    "87",
+    "90",
+    "105",
+    "90",
+    "106",
+    "103",
+    "120",
+    "79",
+    "84",
+    "85",
+    "105",
+    "76",
+    "67",
+    "74",
+    "48",
+    "101",
+    "88",
+    "65",
+    "105",
+    "79",
+    "105",
+    "74",
+    "75",
+    "86",
+    "49",
+    "81",
+    "105",
+    "102",
+    "81",
+    "46",
+    "101",
+    "121",
+    "74",
+    "112",
+    "99",
+    "51",
+    "77",
+    "105",
+    "79",
+    "105",
+    "74",
+    "111",
+    "100",
+    "72",
+    "82",
+    "119",
+    "99",
+    "122",
+    "111",
+    "118",
+    "76",
+    "50",
+    "70",
+    "106",
+    "89",
+    "50",
+    "57",
+    "49",
+    "98",
+    "110",
+    "82",
+    "122",
+    "76",
+    "109",
+    "100",
+    "118",
+    "98",
+    "50",
+    "100",
+    "115",
+    "90",
+    "83",
+    "53",
+    "106",
+    "98",
+    "50",
+    "48",
+    "105",
+    "76",
+    "67",
+    "74",
+    "104",
+    "101",
+    "110",
+    "65",
+    "105",
+    "79",
+    "105",
+    "73",
+    "49",
+    "78",
+    "122",
+    "85",
+    "49",
+    "77",
+    "84",
+    "107",
+    "121",
+    "77",
+    "68",
+    "81",
+    "121",
+    "77",
+    "122",
+    "99",
+    "116",
+    "98",
+    "88",
+    "78",
+    "118",
+    "99",
+    "68",
+    "108",
+    "108",
+    "99",
+    "68",
+    "81",
+    "49",
+    "100",
+    "84",
+    "74",
+    "49",
+    "98",
+    "122",
+    "107",
+    "52",
+    "97",
+    "71",
+    "70",
+    "119",
+    "99",
+    "87",
+    "49",
+    "117",
+    "90",
+    "51",
+    "89",
+    "52",
+    "90",
+    "68",
+    "103",
+    "48",
+    "99",
+    "87",
+    "82",
+    "106",
+    "79",
+    "71",
+    "115",
+    "117",
+    "89",
+    "88",
+    "66",
+    "119",
+    "99",
+    "121",
+    "53",
+    "110",
+    "98",
+    "50",
+    "57",
+    "110",
+    "98",
+    "71",
+    "86",
+    "49",
+    "99",
+    "50",
+    "86",
+    "121",
+    "89",
+    "50",
+    "57",
+    "117",
+    "100",
+    "71",
+    "86",
+    "117",
+    "100",
+    "67",
+    "53",
+    "106",
+    "98",
+    "50",
+    "48",
+    "105",
+    "76",
+    "67",
+    "74",
+    "104",
+    "100",
+    "87",
+    "81",
+    "105",
+    "79",
+    "105",
+    "73",
+    "49",
+    "78",
+    "122",
+    "85",
+    "49",
+    "77",
+    "84",
+    "107",
+    "121",
+    "77",
+    "68",
+    "81",
+    "121",
+    "77",
+    "122",
+    "99",
+    "116",
+    "98",
+    "88",
+    "78",
+    "118",
+    "99",
+    "68",
+    "108",
+    "108",
+    "99",
+    "68",
+    "81",
+    "49",
+    "100",
+    "84",
+    "74",
+    "49",
+    "98",
+    "122",
+    "107",
+    "52",
+    "97",
+    "71",
+    "70",
+    "119",
+    "99",
+    "87",
+    "49",
+    "117",
+    "90",
+    "51",
+    "89",
+    "52",
+    "90",
+    "68",
+    "103",
+    "48",
+    "99",
+    "87",
+    "82",
+    "106",
+    "79",
+    "71",
+    "115",
+    "117",
+    "89",
+    "88",
+    "66",
+    "119",
+    "99",
+    "121",
+    "53",
+    "110",
+    "98",
+    "50",
+    "57",
+    "110",
+    "98",
+    "71",
+    "86",
+    "49",
+    "99",
+    "50",
+    "86",
+    "121",
+    "89",
+    "50",
+    "57",
+    "117",
+    "100",
+    "71",
+    "86",
+    "117",
+    "100",
+    "67",
+    "53",
+    "106",
+    "98",
+    "50",
+    "48",
+    "105",
+    "76",
+    "67",
+    "74",
+    "122",
+    "100",
+    "87",
+    "73",
+    "105",
+    "79",
+    "105",
+    "73",
+    "120",
+    "77",
+    "84",
+    "65",
+    "48",
+    "78",
+    "106",
+    "77",
+    "48",
+    "78",
+    "84",
+    "73",
+    "120",
+    "78",
+    "106",
+    "99",
+    "122",
+    "77",
+    "68",
+    "77",
+    "49",
+    "79",
+    "84",
+    "103",
+    "122",
+    "79",
+    "68",
+    "77",
+    "105",
+    "76",
+    "67",
+    "74",
+    "108",
+    "98",
+    "87",
+    "70",
+    "112",
+    "98",
+    "67",
+    "73",
+    "54",
+    "73",
+    "110",
+    "100",
+    "104",
+    "98",
+    "109",
+    "100",
+    "120",
+    "97",
+    "87",
+    "70",
+    "118",
+    "101",
+    "87",
+    "107",
+    "117",
+    "97",
+    "109",
+    "57",
+    "53",
+    "81",
+    "71",
+    "100",
+    "116",
+    "89",
+    "87",
+    "108",
+    "115",
+    "76",
+    "109",
+    "78",
+    "118",
+    "98",
+    "83",
+    "73",
+    "115",
+    "73",
+    "109",
+    "86",
+    "116",
+    "89",
+    "87",
+    "108",
+    "115",
+    "88",
+    "51",
+    "90",
+    "108",
+    "99",
+    "109",
+    "108",
+    "109",
+    "97",
+    "87",
+    "86",
+    "107",
+    "73",
+    "106",
+    "112",
+    "48",
+    "99",
+    "110",
+    "86",
+    "108",
+    "76",
+    "67",
+    "74",
+    "117",
+    "98",
+    "50",
+    "53",
+    "106",
+    "90",
+    "83",
+    "73",
+    "54",
+    "73",
+    "107",
+    "100",
+    "68",
+    "100",
+    "51",
+    "69",
+    "121",
+    "101",
+    "107",
+    "78",
+    "49",
+    "99",
+    "88",
+    "82",
+    "122",
+    "89",
+    "84",
+    "70",
+    "67",
+    "97",
+    "71",
+    "70",
+    "66",
+    "89",
+    "122",
+    "74",
+    "84",
+    "82",
+    "87",
+    "120",
+    "51",
+    "86",
+    "87",
+    "57",
+    "90",
+    "100",
+    "106",
+    "104",
+    "113",
+    "83",
+    "50",
+    "104",
+    "70",
+    "78",
+    "110",
+    "90",
+    "122",
+    "78",
+    "108",
+    "90",
+    "116",
+    "90",
+    "88",
+    "66",
+    "49",
+    "77",
+    "107",
+    "48",
+    "105",
+    "76",
+    "67",
+    "74",
+    "112",
+    "89",
+    "88",
+    "81",
+    "105",
+    "79",
+    "106",
+    "69",
+    "50",
+    "79",
+    "68",
+    "77",
+    "122",
+    "77",
+    "106",
+    "77",
+    "121",
+    "78",
+    "106",
+    "107",
+    "115",
+    "73",
+    "109",
+    "86",
+    "52",
+    "99",
+    "67",
+    "73",
+    "54",
+    "77",
+    "84",
+    "89",
+    "52",
+    "77",
+    "122",
+    "77",
+    "121",
+    "78",
+    "106",
+    "103",
+    "50",
+    "79",
+    "83",
+    "119",
+    "105",
+    "97",
+    "110",
+    "82",
+    "112",
+    "73",
+    "106",
+    "111",
+    "105",
+    "77",
+    "68",
+    "69",
+    "122",
+    "77",
+    "122",
+    "65",
+    "50",
+    "89",
+    "106",
+    "89",
+    "49",
+    "77",
+    "109",
+    "89",
+    "48",
+    "90",
+    "106",
+    "103",
+    "49",
+    "77",
+    "106",
+    "85",
+    "120",
+    "90",
+    "84",
+    "85",
+    "49",
+    "79",
+    "71",
+    "86",
+    "104",
+    "78",
+    "71",
+    "70",
+    "104",
+    "79",
+    "87",
+    "74",
+    "107",
+    "89",
+    "87",
+    "73",
+    "51",
+    "90",
+    "109",
+    "81",
+    "120",
+    "78",
+    "122",
+    "107",
+    "51",
+    "90",
+    "105",
+    "74",
+    "57",
+    "128",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "20",
+    "216",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0",
+    "0"
+  ],
+  "num_sha2_blocks": 11,
+  "payload_start_index": 103,
+  "payload_len": 564,
+  "extended_key_claim": [
+    34,
+    115,
+    117,
+    98,
+    34,
+    58,
+    34,
+    49,
+    49,
+    48,
+    52,
+    54,
+    51,
+    52,
+    53,
+    50,
+    49,
+    54,
+    55,
+    51,
+    48,
+    51,
+    53,
+    57,
+    56,
+    51,
+    56,
+    51,
+    34,
+    44,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0
+  ],
+  "claim_length_ascii": 30,
+  "claim_index_b64": 368,
+  "claim_length_b64": 41,
+  "subject_pin": "283089722053851751073973683904920435104",
+  "key_claim_name_length": 3,
+  "mask": [
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    0,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    0,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1,
+    1
+  ],
+  "extended_nonce": [
+    34,
+    110,
+    111,
+    110,
+    99,
+    101,
+    34,
+    58,
+    34,
+    71,
+    67,
+    119,
+    113,
+    50,
+    122,
+    67,
+    117,
+    113,
+    116,
+    115,
+    97,
+    49,
+    66,
+    104,
+    97,
+    65,
+    99,
+    50,
+    83,
+    69,
+    108,
+    119,
+    85,
+    111,
+    89,
+    118,
+    56,
+    106,
+    75,
+    104,
+    69,
+    54,
+    118,
+    115,
+    54,
+    86,
+    109,
+    101,
+    112,
+    117,
+    50,
+    77,
+    34,
+    44
+  ],
+  "nonce_claim_index_b64": 484,
+  "nonce_length_b64": 73,
+  "eph_public_key": [
+    "17932473587154777519561053972421347139",
+    "134696963602902907403122104327765350261"
+  ],
+  "max_epoch": 10000,
+  "jwt_randomness": "100681567828351849884072155819400689117",
+  "all_inputs_hash": "2487117669597822357956926047501254969190518860900347921480370492048882803688"
+}
\ No newline at end of file
diff --git a/zklogin-circuits/tsconfig.json b/zklogin-circuits/tsconfig.json
new file mode 100644
index 0000000000..d0a441aebd
--- /dev/null
+++ b/zklogin-circuits/tsconfig.json
@@ -0,0 +1,110 @@
+{
+  "compilerOptions": {
+    /* Visit https://aka.ms/tsconfig to read more about this file */
+
+    /* Projects */
+    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
+    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
+    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
+    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
+    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
+
+    /* Language and Environment */
+    "target": "es2020",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
+    // "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
+    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
+    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
+    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
+    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
+
+    /* Modules */
+    "module": "commonjs",                                /* Specify what module code is generated. */
+    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
+    // "moduleResolution": "node10",                     /* Specify how TypeScript looks up a file from a given module specifier. */
+    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
+    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
+    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
+    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
+    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
+    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
+    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
+    // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
+    // "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
+    // "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
+    // "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
+    // "resolveJsonModule": true,                        /* Enable importing .json files. */
+    // "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
+    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
+
+    /* JavaScript Support */
+    "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
+    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+
+    /* Emit */
+    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
+    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
+    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
+    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
+    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+    "outDir": "./js",                                   /* Specify an output folder for all emitted files. */
+    // "removeComments": true,                           /* Disable emitting comments. */
+    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
+    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
+    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
+    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
+    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
+    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
+    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
+    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
+    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
+    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
+
+    /* Interop Constraints */
+    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
+    // "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
+    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
+    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
+    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
+
+    /* Type Checking */
+    "strict": true,                                      /* Enable all strict type-checking options. */
+    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
+    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
+    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
+    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
+    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
+    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
+    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
+    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
+    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
+    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
+    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
+    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
+    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
+    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
+    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
+
+    /* Completeness */
+    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
+    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
+  },
+  "include": ["src/**/*"]
+}