diff --git a/README.md b/README.md index d9c2438..0c643fe 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,102 @@ Implementation of ECDSA operations in circom. +## Set up for ASCS project goal +- Run `yarn` at the top level to install npm dependencies (`snarkjs` and `circomlib`). + +- Download `circom` version `>= 2.0.2` on your system. Installation instructions [here](https://docs.circom.io/getting-started/installation/). + +- Download ``ptau`` file with power 21 from the Hermez trusted setup from [this repository](https://github.com/iden3/snarkjs#7-prepare-phase-2) and copy it into the `circuits` subdirectory of the project, with the name `pot20_final.ptau`. + +- Build key and wittness by running `yarn build:groupsig` at the top level. This build process will create `r1cs` and `wasm` files for witness generation, as well as a `zkey` file (proving and verifying keys) in a the folder `./build/groupsig`. If no `zkey` file was generated and you are on windows, then: + 1. Install snarkjs gloablly like so: `npm install -g snarkjs` + 2. Instead of `yarn build:groupsig`, run `cd ./scripts/groupsig && ./windows_build_groupsig.sh` in Git Bash terminal + 3. Optional: move back to root folder (required for next step): `cd ../..` + +- Test set up by running groupsig demo through `yarn groupsig-demo` at the top level and follow the instructions in your terminal. [Randomly generated](https://privatekeys.pw/keys/ethereum/random) valid inputs for demo: + 1. private key: 0x3d87d34a290b124ad0b29b87053363d5dca57cd02650e4b1f4cc75e9c8275648 --> associated eth address: 0x68F3A3AfD9Cbf1cb27b5359b79B563A5E423115a + 2. addr1: 0x0F2D3bF9ce11737566E5bcef7222Df31C0D90395 + 3. addr2: 0x46a8801DA492f6d2eADbd3ec30f4255c29aB656b + 4. nonce: 6789 + +- For prototype: Build key and wittnesses for variable number of company admins by + 1. setting `SIZES=(2 3 4)` in script: `./scripts/groupsig/build_circuits.sh` (default is 2 to 4) + 2. executing the script by running ``cd ./scripts/groupsig && ./build_circuits.sh`` while root folder of repo +- For prototype demo run `yarn prototype-demo` at the top level and follow the instructions in your terminal. [Randomly generated](https://privatekeys.pw/keys/ethereum/random) valid inputs for demo: + 1. private key: 0x3d87d34a290b124ad0b29b87053363d5dca57cd02650e4b1f4cc75e9c8275648 --> associated eth address: 0x68F3A3AfD9Cbf1cb27b5359b79B563A5E423115a + 2. addr1: 0x0F2D3bF9ce11737566E5bcef7222Df31C0D90395 + 3. addr2: 0x46a8801DA492f6d2eADbd3ec30f4255c29aB656b + 4. addr3: 0xC8a6ab61Cc685586F3399857A4e80a75327fE4C0 + 6. msg: bafybeicn7i3soqdgr7dwnrwytgq4zxy7a5jpkizrvhm5mv6bgjd32wm3q4 + 6. nonce: 6789 + +## Build and test final circuit for ASCS project goal +**note**: the final circuit is in `./scripts/groupsig/private_secure_variable_groupsig.circom` + +1. executing the script by running ``cd ./scripts/groupsig && ./build_private_groupsig_circuits.sh`` while root folder of repo +2. test by running `node ./scripts/test_private_secure_variable_groupsig.js` while root folder of repo (note: the test script solely works for three group members which must be considered during the first step) +3. copy the `./build/groupsig/verifiers` folder and add the folder to `./packages/trust-anchor-did-ethr/contracts` of the [on-chain-ssi repo](https://github.com/ASCS-eV/on-chain-ssi/tree/main/packages/trust-anchor-did-ethr/contracts) + +*note*: the final build script was used to generate the ``./build/groupsig/p_m_4`` folder which was added as `circom-zkp-generator` in `./packages/trust-anchor-did-ethr/test` folder of the [on-chain-ssi repo](https://github.com/ASCS-eV/on-chain-ssi/tree/main/packages/trust-anchor-did-ethr/contracts)` + +## Requirements for ASCS project goal +1. Enhanced security: + 1. replay attack: solved with nonce logic + 2. domain seperation +2. Variable number of eth addresses as public input of circuit +3. Enhanced privacy by making the signature a private input of the circuit + +## Security analysis +The upper requirements ensure enhanced security but other security matters must be consdiered and are divide in security issues for production and for the potential future of the project: + +### Security issues for production +1. **Trusted Setup** +Circom with Groth16 requires a Phase 2 Trusted Setup. If the trust ceremony is not done correctly, or if the "Power of Tau" file is compromised (as it is the case for this prototype since we download a public ptau file), someone could generate fake proofs (forgeries) without knowing any private key. + +2. **MiMic vs. ECDSA** +MiMC is used because it is cheap and easy in zero-knowledge circuits and sufficient for demonstrating private-key ownership. However, it does not provide a standard digital signature. For real-world Ethereum applications, MiMC should ideally be replaced with in-circuit ECDSA verification to ensure compatibility, interoperability, and strong cryptographic guarantees. However, in-circuit ECDSA verification comes at the cost of more circuit constraints and thus worse performance. + +3. **Scalar Malleability** + - *Risk:* The curve order \\( n \\) is slightly smaller than the field size \\( 2^{256} \\). If the circuit does not enforce that the private key lies in the range \\( 1 \\leq d < n \\), a prover can submit values such as \\( d \\) and \\( d + n \\). Both values generate the same public key and Ethereum address. This allows multiple valid witnesses for the same identity, enabling proof malleability, replay attacks, and potential bypass of double-spending protections. + - *Recommendation:* Enforce a strict range check ensuring that the private key satisfies \\( 1 \\leq d < n \\), where \\( n \\) is the secp256k1 curve order. This should be implemented using multi-limb comparison circuits (e.g., `BigLessThan`) and zero-value checks to guarantee a unique canonical representation. + +4. **Non-existent Points** + - *Risk:* Without explicitly verifying that a derived public key lies on the secp256k1 curve (i.e., satisfies \\( y^2 = x^3 + 7 \\)), a prover may use mathematically invalid points. These β€œfake” points can sometimes be mapped through the public-key-to-address hashing process to a valid Ethereum address, enabling identity forgery without possession of a legitimate private key. + - *Recommendation:* Enforce point-on-curve validation by constraining the public key coordinates to satisfy the elliptic curve equation. If public keys are derived internally, ensure that the scalar multiplication circuit includes built-in curve membership checks. Prefer using audited elliptic curve components that guarantee valid point generation. + +5. **Edge Case Exploits** + - *Risk:* Special values such as `0`, `1`, or multiples of the curve order may trigger undefined or unintended behavior in elliptic curve arithmetic, including producing the point at infinity or degenerate keys. These edge cases can result in addresses that do not correspond to any real user but can still be claimed by a malicious prover. + - *Recommendation:* Explicitly forbid invalid scalar values by enforcing lower and upper bounds on private keys. Disallow zero and other degenerate values through dedicated zero-check constraints. Ensure that all elliptic curve operations are well-defined for the permitted input range. + +### Potential future security issues +1. **Deterministic Signatures (MiMC Vulnerability)** + - *Risk*: The groupsig circuit uses mimc(msg, privkey). If you ever sign the same msg with the same privkey but a different nonce, you aren't leaking the key, but you are creating a linkable trail. + - *Recommendation*: Ensure your attestation always includes all unique context (like a domain_separator) to ensure signatures cannot be "imported" from one app to another. + - *Note*: In ASCS's use case, we can neglect this security issue (as of now) because the msg always contains the CID of the digital data asset uploaded to IPFS and to be published on the digital data asset marketplace. Therefore, the CID ensures domain seperation! +2. **Forgery via Public Input Manipulation** + - *Risk*: The groupsig circuit proves membership in a list of ETH addresses but an attacker could take your valid proof and simply change the list of ETH addresses to different addresses. If the verifier doesn't check the entire set of addresses against a trusted root (like a Merkle Root), the proof is useless. + - *Recommendation*: Instead of passing a lsit of ETH addresses, pass a Merkle Root as a public input and use a Merkle Proof (private input) to prove your address is in the set. + - *Note*: In ASCS's use case, we can neglect this security issue (as of now) because the verifier is an on-chain smart contract which checks if the ETH addresses are actually part of the same and correct company by considering the did:ethr-based on-chain company structre. +3. **The "Frozen Heart" (Input Aliasing)** + - *Risk*: Circom signals are in a prime field.If msg or nonce are larger than the field size ($p \approx 2^{254}$), they will wrap around (modulo $p$). An attacker could provide a msg that is OriginalMsg + p, and the circuit would produce the exact same attestation. + - *Recommendation*: Always constrain your public inputs to be within a specific bit range (e.g., 253 bits) if they are derived from external data like Ethereum hashes. + - *Note*: TODO: think about if this is an issue for the ASCS's use case? + +## Potential optimizations +1. **Membership Proof Is O(m)** +The current circuit verifies group membership by computing the product \\((myAddr - addrs[0]) \\times (myAddr - addrs[1]) \\times \\dots \\times (myAddr - addrs[m-1])\\), which requires one multiplication per group member. This means that the proving cost and circuit size grow linearly with the number of addresses in the group. As the group becomes large, this approach quickly becomes impractical due to high constraint counts, slower proof generation, and higher verification costs. A more scalable alternative is to use a Merkle tree or cryptographic accumulator. In this design, all valid addresses are committed into a single root hash, and the prover supplies a Merkle proof showing that their address is included in the tree. This reduces the complexity from O(m) to O(log m), making it feasible to support thousands or millions of members. Merkle-based membership proofs are widely used in modern zero-knowledge systems and are well-supported by existing Circom libraries, making them a practical and secure upgrade. + +2. **Private Key Inside Circuit** +The circuit directly includes the full Ethereum private key as a private witness and uses it both to derive the address and to generate the message attestation. While this is functionally correct, it significantly increases circuit complexity and proof generation time, because elliptic-curve operations and full key handling are expensive in zero-knowledge environments. It also increases the risk surface: if the prover’s environment is compromised, exposure of the witness leaks the actual Ethereum private key, which may control real funds. A better approach is to avoid embedding the long-term private key in the circuit and instead use derived or committed secrets. For example, the user can generate a random secret, commit to it on-chain, and link it to their address through a one-time signature or registration step. The circuit then proves knowledge of this secret rather than the main private key. Another option is to use nullifiers or hierarchical keys, where a dedicated ZK key is derived from the Ethereum key and used only for proofs. These approaches reduce computational cost, improve security isolation, and prevent direct exposure of high-value private keys inside zero-knowledge circuits. + +3. **Improven Variable Group Size ZKP-Generation with Unified Circuit with Dynamic Group Padding** +Currently, the system generates separate WASM and ZKey pairs for every possible group size (`m`). This requires the `DIDMultisigController` to maintain a mapping of verifier contracts and forces the client to download specific proving keys based on the admin count. We propose moving to a **Unified Max-Size Circuit** (e.g., `MAX_M = 100`). Instead of a strict product check, the circuit will be updated to ignore "Null Addresses" (`0x0`). This is achieved by implementing a conditional product factor. The final constraint remains $\\prod factor_i = 0$, but it will only be satisfied if `myAddr` matches one of the **non-zero** entries in the provided array. Benefits are **Single Verifier:** Only one `Verifier.sol` needs to be deployed on-chain, **Simplified Client:** The frontend no longer needs to switch between different WASM/ZKey files, and **Privacy:** A fixed array size of 100 hides the *actual* number of admins in a group, providing better metadata private (e.g., a group of 2 looks identical to a group of 50 on-chain). However, the trade-offs are **Proof Generation Time:** Proving for 100 slots takes longer than proving for 2, and **Gas Cost:** The public input array sent to the `verifyProof` function will always be 100 elements long, increasing calldata gas costs. + +--- +--- +--- +# README OF CRICOM-ECSDSA + ## Project overview This repository provides proof-of-concept implementations of ECDSA operations in circom. **These implementations are for demonstration purposes only**. These circuits are not audited, and this is not intended to be used as a library for production-grade applications. @@ -9,8 +105,7 @@ This repository provides proof-of-concept implementations of ECDSA operations in Circuits can be found in `circuits`. `scripts` contains various utility scripts (most importantly, scripts for building a few example zkSNARKs using the ECDSA circuit primitives). `test` contains some unit tests for the circuits, mostly for witness generation. ## Install dependencies - -- Run `yarn` at the top level to install npm dependencies (`snarkjs` and `circomlib`). +- Run `yarn` at the top level to install npm dependencies (`snarkjs` and `circomlib`). - You'll also need `circom` version `>= 2.0.2` on your system. Installation instructions [here](https://docs.circom.io/getting-started/installation/). - If you want to build the `pubkeygen`, `eth_addr`, and `groupsig` circuits, you'll need to download a Powers of Tau file with `2^20` constraints and copy it into the `circuits` subdirectory of the project, with the name `pot20_final.ptau`. We do not provide such a file in this repo due to its large size. You can download and copy Powers of Tau files from the Hermez trusted setup from [this repository](https://github.com/iden3/snarkjs#7-prepare-phase-2). - If you want to build the `verify` circuits, you'll also need a Powers of Tau file that can support at least `2^21` constraints (place it in the same directory as above with the same naming convention). diff --git a/package.json b/package.json index 3c04cd6..6f983f2 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,12 @@ "scripts": { "build:pubkeygen": "cd ./scripts/pubkeygen && ./build_pubkeygen.sh", "build:groupsig": "cd ./scripts/groupsig && ./build_groupsig.sh", + "build-windows:groupsig": "cd ./scripts/groupsig && ./windows_build_groupsig.sh", "build:verify": "cd ./scripts/verify && ./build_verify.sh", "build:eth_addr": "cd ./scripts/eth_addr && ./build_eth_addr.sh", "test": "NODE_OPTIONS=--max_old_space_size=56000 mocha -r ts-node/register 'test/**/*.ts'", - "groupsig-demo": "npx ts-node scripts/groupsign_cli.ts" + "groupsig-demo": "npx ts-node scripts/groupsign_cli.ts", + "prototype-demo": "npx ts-node scripts/variable_groupsign_cli.ts" }, "repository": "git@github.com:0xPARC/circom-ecdsa.git", "author": "0xPARC ", @@ -29,5 +31,6 @@ "mocha": "^9.1.3", "ts-node": "^10.4.0", "typescript": "^4.5.4" - } + }, + "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" } diff --git a/scripts/groupsig/build_circuits.sh b/scripts/groupsig/build_circuits.sh new file mode 100644 index 0000000..16ae103 --- /dev/null +++ b/scripts/groupsig/build_circuits.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -e # Exit on error + +export MSYS_NO_PATHCONV=1 +PHASE1=../../circuits/pot20_final.ptau +CIRCUIT_NAME=secure_variable_groupsig +BUILD_BASE=../../build/groupsig + +# Check for Phase 1 +if [ ! -f "$PHASE1" ]; then + echo "Error: Phase 1 ptau file not found at $PHASE1" + exit 1 +fi + +# Sizes we want to support +SIZES=(2 3 4) + +for m in "${SIZES[@]}" +do + echo "-------------------------------------------" + echo "πŸ”¨ BUILDING FOR GROUP SIZE: m = $m" + echo "-------------------------------------------" + + # Create a specific directory for this size + TARGET_DIR="$BUILD_BASE/m_$m" + mkdir -p "$TARGET_DIR" + + # 1. Modify the circom file temporarily to set the group size + # This replaces the 'Main(64, 4, X)' line with the current size 'm' + sed -i "s/component main {public \[addrs, msg, nonce\]} = Main(64, 4, [0-9]*);/component main {public [addrs, msg, nonce]} = Main(64, 4, $m);/" "$CIRCUIT_NAME".circom + + echo "**** COMPILING ****" + circom "$CIRCUIT_NAME".circom --r1cs --wasm --output "$TARGET_DIR" + + echo "**** GENERATING ZKEY ****" + # Groth16 Setup + snarkjs groth16 setup "$TARGET_DIR/$CIRCUIT_NAME.r1cs" "$PHASE1" "$TARGET_DIR/temp_0.zkey" + + # Quick Contribution (using 'test' as entropy) + echo "test" | snarkjs zkey contribute "$TARGET_DIR/temp_0.zkey" "$TARGET_DIR/temp_1.zkey" --name="Builder" -v -e="entropy" + + # Final Beacon and ZKey + snarkjs zkey beacon "$TARGET_DIR/temp_1.zkey" "$TARGET_DIR/$CIRCUIT_NAME.zkey" 0102030405060708090a0b0c0d0e0f101112231415161718221a1b1c1d1e1f 10 + + echo "**** EXPORTING VKEY ****" + snarkjs zkey export verificationkey "$TARGET_DIR/$CIRCUIT_NAME.zkey" "$TARGET_DIR/vkey.json" + + # Cleanup intermediate files to save space + rm "$TARGET_DIR/temp_0.zkey" "$TARGET_DIR/temp_1.zkey" "$TARGET_DIR/$CIRCUIT_NAME.r1cs" + + echo "βœ… Done for m=$m" +done + +echo "-------------------------------------------" +echo "All builds complete in $BUILD_BASE" \ No newline at end of file diff --git a/scripts/groupsig/build_private_groupsig_circuits.sh b/scripts/groupsig/build_private_groupsig_circuits.sh new file mode 100644 index 0000000..e262b1a --- /dev/null +++ b/scripts/groupsig/build_private_groupsig_circuits.sh @@ -0,0 +1,181 @@ +#!/bin/bash +set -e + +export MSYS_NO_PATHCONV=1 + + +# -------------------------------- +# Config +# -------------------------------- + +PHASE1=../../circuits/pot20_final.ptau +CIRCUIT_NAME=private_secure_variable_groupsig +BUILD_BASE=../../build/groupsig +VERIFIER_DIR="$BUILD_BASE/verifiers" + + +# -------------------------------- +# Check Phase1 +# -------------------------------- + +if [ ! -f "$PHASE1" ]; then + echo "❌ Phase1 ptau not found: $PHASE1" + exit 1 +fi + + +# -------------------------------- +# User Input +# -------------------------------- + +read -p "Enter maximum group size (>=2): " MAX_M + +if ! [[ "$MAX_M" =~ ^[0-9]+$ ]] || [ "$MAX_M" -lt 2 ]; then + echo "❌ Invalid group size" + exit 1 +fi + + +read -p "Create Solidity verifiers? (y/n): " MAKE_VERIFIERS + + +if [[ "$MAKE_VERIFIERS" == "y" || "$MAKE_VERIFIERS" == "Y" ]]; then + MAKE_VERIFIERS=true + mkdir -p "$VERIFIER_DIR" +else + MAKE_VERIFIERS=false +fi + + +echo +echo "===================================" +echo " Max group size: $MAX_M" +echo " Generate verifiers: $MAKE_VERIFIERS" +echo "===================================" +echo + + +# -------------------------------- +# Build Loop +# -------------------------------- + +for (( m=2; m<=MAX_M; m++ )) +do + + echo "-------------------------------------------" + echo "πŸ”¨ BUILDING FOR GROUP SIZE: m = $m" + echo "-------------------------------------------" + + + TARGET_DIR="$BUILD_BASE/p_m_$m" + + mkdir -p "$TARGET_DIR" + + + # -------------------------------- + # Patch circuit + # -------------------------------- + + echo "βš™οΈ Setting group size..." + + sed -i \ + "s/component main {public \[addrs, pubHashHi, pubHashLo, nonce\]} = Main(64, 4, [0-9]*);/component main {public [addrs, pubHashHi, pubHashLo, nonce]} = Main(64, 4, $m);/" \ + "$CIRCUIT_NAME.circom" + + + # -------------------------------- + # Compile + # -------------------------------- + + echo "βš™οΈ Compiling..." + + circom "$CIRCUIT_NAME.circom" \ + --r1cs \ + --wasm \ + --sym \ + --output "$TARGET_DIR" + + + # -------------------------------- + # Groth16 Setup + # -------------------------------- + + echo "βš™οΈ Groth16 setup..." + + snarkjs groth16 setup \ + "$TARGET_DIR/$CIRCUIT_NAME.r1cs" \ + "$PHASE1" \ + "$TARGET_DIR/temp_0.zkey" + + + echo "test" | snarkjs zkey contribute \ + "$TARGET_DIR/temp_0.zkey" \ + "$TARGET_DIR/temp_1.zkey" \ + --name="Builder" \ + -v \ + -e="entropy" + + + snarkjs zkey beacon \ + "$TARGET_DIR/temp_1.zkey" \ + "$TARGET_DIR/$CIRCUIT_NAME.zkey" \ + 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f \ + 10 + + + # -------------------------------- + # Export vkey + # -------------------------------- + + echo "βš™οΈ Exporting verification key..." + + snarkjs zkey export verificationkey \ + "$TARGET_DIR/$CIRCUIT_NAME.zkey" \ + "$TARGET_DIR/vkey.json" + + + # -------------------------------- + # Export Solidity Verifier + # -------------------------------- + if [ "$MAKE_VERIFIERS" = true ]; then + + echo "βš™οΈ Exporting Solidity verifier..." + + # 1. Export with default name (usually 'Groth16Verifier') + snarkjs zkey export solidityverifier \ + "$TARGET_DIR/$CIRCUIT_NAME.zkey" \ + "$VERIFIER_DIR/VerifierM$m.sol" + + # 2. Use sed to rename the contract inside the file + # This replaces 'contract Groth16Verifier' with 'contract VerifierMm' + sed -i "s/contract Groth16Verifier/contract VerifierM$m/g" "$VERIFIER_DIR/VerifierM$m.sol" + + echo " β†’ $VERIFIER_DIR/VerifierM$m.sol (renamed to VerifierM$m)" + fi + + + # -------------------------------- + # Cleanup + # -------------------------------- + + rm "$TARGET_DIR/temp_0.zkey" + rm "$TARGET_DIR/temp_1.zkey" + rm "$TARGET_DIR/$CIRCUIT_NAME.r1cs" + + + echo "βœ… Done for m=$m" + echo + +done + + +echo "===================================" +echo " All builds complete" +echo " Output: $BUILD_BASE" +echo "===================================" + +if [ "$MAKE_VERIFIERS" = true ]; then + echo " Verifiers: $VERIFIER_DIR" +fi + +echo diff --git a/scripts/groupsig/groupsig.circom b/scripts/groupsig/groupsig.circom index d29eda0..b29d7e4 100644 --- a/scripts/groupsig/groupsig.circom +++ b/scripts/groupsig/groupsig.circom @@ -10,6 +10,7 @@ include "../../circuits/eth_addr.circom"; - addr2 (pub) - addr3 (pub) - msg (pub) + - nonce (pub) - privkey Intermediate values: @@ -24,15 +25,25 @@ include "../../circuits/eth_addr.circom"; - msgAttestation == mimc(msg, privkey) */ +// TODO: +// 1. add nonce for safegarding against replac attacks +// 2. make scalable so that instead of addr1, addr2, and addr3 an array is used called: addrs +// 3. fix: No domain separation issue casued by cross-context replay +// 4. check validity of public key + template Main(n, k) { assert(n * k >= 256); assert(n * (k-1) < 256); + // private inputs signal input privkey[k]; + + // public inputs signal input addr1; signal input addr2; signal input addr3; signal input msg; + signal input nonce; // to protect against replay attacks signal myAddr; @@ -50,21 +61,22 @@ template Main(n, k) { for (var i = 0; i < k; i++) { privToAddr.privkey[i] <== privkey[i]; } - myAddr <== privToAddr.addr; + myAddr <== privToAddr.addr; // enforces: "I know a private key whose Ethereum address is myAddr" // verify address is one of the provided signal temp; temp <== (myAddr - addr1) * (myAddr - addr2); - 0 === temp * (myAddr - addr3); + 0 === temp * (myAddr - addr3); // enforces: "I know myAddr is part of group (= addr1-3)" // produce signature - component mimcAttestation = MiMCSponge(k+1, 220, 1); + component mimcAttestation = MiMCSponge(k+2, 220, 1); //+2 bcause of msg and nonce mimcAttestation.ins[0] <== msg; + mimcAttestation.ins[1] <== nonce; // bind the proof to this specific nonce for (var i = 0; i < k; i++) { - mimcAttestation.ins[i+1] <== privkey[i]; + mimcAttestation.ins[i+2] <== privkey[i]; // enforces: "I know that the private key of myAddr signed msg" } mimcAttestation.k <== 0; msgAttestation <== mimcAttestation.outs[0]; } -component main {public [addr1, addr2, addr3, msg]} = Main(64, 4); \ No newline at end of file +component main {public [addr1, addr2, addr3, msg, nonce]} = Main(64, 4); \ No newline at end of file diff --git a/scripts/groupsig/input_groupsig.json b/scripts/groupsig/input_groupsig.json index 0b1179e..36fc495 100644 --- a/scripts/groupsig/input_groupsig.json +++ b/scripts/groupsig/input_groupsig.json @@ -1 +1 @@ -{"privkey": ["7", "0", "0", "0"], "addr1": "50", "addr2": "1210930943336347771396396330116102456817544228795", "addr3": "51", "msg": "42"} +{"privkey": ["7", "0", "0", "0"], "addr1": "50", "addr2": "1210930943336347771396396330116102456817544228795", "addr3": "51", "msg": "42", "nonce":"12345"} diff --git a/scripts/groupsig/private_secure_variable_groupsig.circom b/scripts/groupsig/private_secure_variable_groupsig.circom new file mode 100644 index 0000000..4d85c52 --- /dev/null +++ b/scripts/groupsig/private_secure_variable_groupsig.circom @@ -0,0 +1,173 @@ +pragma circom 2.0.2; + +include "../../node_modules/circomlib/circuits/mimcsponge.circom"; +include "../../node_modules/circomlib/circuits/bitify.circom"; +include "../../circuits/eth_addr.circom"; + +/* + Inputs: + - addrs[m] (pub) + - pubHashHi, pubHashLo (pub) // keccak(message) split + - nonce (pub) + + - privHashHi, privHashLo (priv) + - privkey + + Intermediate values: + - myAddr + + Output: + - msgAttestation + + Prove: + - PrivKeyToAddr(privkey) == myAddr + - myAddr ∈ addrs + - privHash == pubHash + - msgAttestation == mimc(hash, nonce, privkey) +*/ + + +template Main(n, k, m) { + + assert(n * k >= 256); + assert(n * (k-1) < 256); + + // -------------------------------------------------- + // Private Inputs + // -------------------------------------------------- + + signal input privkey[k]; + + // Private keccak hash (split) + signal input privHashHi; + signal input privHashLo; + + + // -------------------------------------------------- + // Public Inputs + // -------------------------------------------------- + + signal input addrs[m]; + + // Public keccak hash (split) + signal input pubHashHi; + signal input pubHashLo; + + signal input nonce; + + + // -------------------------------------------------- + // Internal + // -------------------------------------------------- + + signal myAddr; + + signal output msgAttestation; + + + // -------------------------------------------------- + // Validate private key size + // -------------------------------------------------- + + component n2bs[k]; + + for (var i = 0; i < k; i++) { + + n2bs[i] = Num2Bits( + i == k-1 ? 256 - (k-1)*n : n + ); + + n2bs[i].in <== privkey[i]; + } + + + // -------------------------------------------------- + // Compute Ethereum address + // -------------------------------------------------- + + component privToAddr = PrivKeyToAddr(n, k); + + for (var i = 0; i < k; i++) { + privToAddr.privkey[i] <== privkey[i]; + } + + myAddr <== privToAddr.addr; + + + // -------------------------------------------------- + // Group membership check + // -------------------------------------------------- + + signal products[m]; + + products[0] <== myAddr - addrs[0]; + + for (var i = 1; i < m; i++) { + products[i] <== products[i-1] * (myAddr - addrs[i]); + } + + 0 === products[m-1]; + + + // -------------------------------------------------- + // Enforce hash consistency + // -------------------------------------------------- + + // Proves: + // I know the preimage of pubHash + privHashHi === pubHashHi; + privHashLo === pubHashLo; + + + // -------------------------------------------------- + // Produce ZK signature + // -------------------------------------------------- + + /* + We sign: + + H( + hash_hi, + hash_lo, + nonce, + privkey + ) + */ + + component mimcAttestation = MiMCSponge( + k + 3, // hi, lo, nonce, privkey[] + 220, + 1 + ); + + + // Bind message hash + mimcAttestation.ins[0] <== privHashHi; + mimcAttestation.ins[1] <== privHashLo; + + // Bind nonce + mimcAttestation.ins[2] <== nonce; + + + // Bind private key + for (var i = 0; i < k; i++) { + mimcAttestation.ins[i+3] <== privkey[i]; + } + + mimcAttestation.k <== 0; + + msgAttestation <== mimcAttestation.outs[0]; +} + + + +// -------------------------------------------------- +// Instantiation +// -------------------------------------------------- + +component main {public [ + addrs, + pubHashHi, + pubHashLo, + nonce +]} = Main(64, 4, 4); diff --git a/scripts/groupsig/secure_variable_groupsig.circom b/scripts/groupsig/secure_variable_groupsig.circom new file mode 100644 index 0000000..209771d --- /dev/null +++ b/scripts/groupsig/secure_variable_groupsig.circom @@ -0,0 +1,85 @@ +pragma circom 2.0.2; + +include "../../node_modules/circomlib/circuits/mimcsponge.circom"; +include "../../node_modules/circomlib/circuits/bitify.circom"; +include "../../circuits/eth_addr.circom"; + +/* + Inputs: + - addrs[m] (pub) + - msg (pub) + - nonce (pub) + - privkey + + Intermediate values: + - myAddr (supposed to be addr of privkey) + + Output: + - msgAttestation + + Prove: + - PrivKeyToAddr(privkey) == myAddr + - (myAddr - addrs[0]) * (myAddr - addrs[1]) * ... * (myAddr - addrs[m-1]) == 0 + - msgAttestation == mimc(msg, nonce, privkey) +*/ + +// TODO: +// - Make msg private to avoid public being able to find out its signer by checking the public msg with every eth address in the public addrs[m] input +// - Adapt msg logic for use case and ensure domain separation by that msg is "publish-${cid}-for-${company}-on-${marketplace}" + +// n: bits per word, k: number of words for privkey, m: number of addresses in group +template Main(n, k, m) { + assert(n * k >= 256); + assert(n * (k-1) < 256); + + // private inputs + signal input privkey[k]; + + // public inputs + signal input addrs[m]; // Scalable array of addresses + signal input msg; + signal input nonce; // to protect against replay attacks + + signal myAddr; + + signal output msgAttestation; + + // check that privkey properly represents a 256-bit number + component n2bs[k]; + for (var i = 0; i < k; i++) { + n2bs[i] = Num2Bits(i == k-1 ? 256 - (k-1) * n : n); + n2bs[i].in <== privkey[i]; + } + + // compute addr + component privToAddr = PrivKeyToAddr(n, k); + for (var i = 0; i < k; i++) { + privToAddr.privkey[i] <== privkey[i]; + } + myAddr <== privToAddr.addr; // enforces: "I know a private key whose Ethereum address is myAddr" + + // verify address is one of the provided (SCALABLE MEMBERSHIP CHECK) + // We check: (myAddr - addrs[0]) * (myAddr - addrs[1]) * ... * (myAddr - addrs[m-1]) === 0 + // If myAddr is equal to any one of the addrs, the entire product becomes zero. + signal products[m]; + products[0] <== myAddr - addrs[0]; + for (var i = 1; i < m; i++) { + products[i] <== products[i-1] * (myAddr - addrs[i]); + } + + // The final result must be 0 + 0 === products[m-1]; // enforces: "I know myAddr is part of group (= addrs[m])" + + // produce signature + component mimcAttestation = MiMCSponge(k+2, 220, 1); //+2 because of msg and nonce + mimcAttestation.ins[0] <== msg; + mimcAttestation.ins[1] <== nonce; // bind the proof to this specific nonce + for (var i = 0; i < k; i++) { + mimcAttestation.ins[i+2] <== privkey[i]; // enforces: "I know that the private key of myAddr signed msg" + } + mimcAttestation.k <== 0; + msgAttestation <== mimcAttestation.outs[0]; +} + +// Set 'm' (third argument) to the number of addresses you want in your group +component main {public [addrs, msg, nonce]} = Main(64, 4, 4); \ No newline at end of file diff --git a/scripts/groupsig/windows_build_groupsig.sh b/scripts/groupsig/windows_build_groupsig.sh new file mode 100644 index 0000000..ae70240 --- /dev/null +++ b/scripts/groupsig/windows_build_groupsig.sh @@ -0,0 +1,72 @@ +#!/bin/bash +export MSYS_NO_PATHCONV=1 # To avoid path conversion issues on Windows Git Bash + +PHASE1=../../circuits/pot20_final.ptau +BUILD_DIR=../../build/groupsig +CIRCUIT_NAME=groupsig + +if [ -f "$PHASE1" ]; then + echo "Found Phase 1 ptau file" +else + echo "No Phase 1 ptau file found. Exiting..." + exit 1 +fi + +if [ ! -d "$BUILD_DIR" ]; then + echo "No build directory found. Creating build directory..." + mkdir -p "$BUILD_DIR" +fi + +echo "****COMPILING CIRCUIT****" +start=`date +%s` +circom "$CIRCUIT_NAME".circom --r1cs --wasm --sym --c --wat --output "$BUILD_DIR" +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****GENERATING WITNESS FOR SAMPLE INPUT****" +start=`date +%s` +node "$BUILD_DIR"/"$CIRCUIT_NAME"_js/generate_witness.js "$BUILD_DIR"/"$CIRCUIT_NAME"_js/"$CIRCUIT_NAME".wasm input_groupsig.json "$BUILD_DIR"/witness.wtns +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****GENERATING ZKEY 0****" +start=`date +%s` +snarkjs groth16 setup "$BUILD_DIR"/"$CIRCUIT_NAME".r1cs "$PHASE1" "$BUILD_DIR"/"$CIRCUIT_NAME"_0.zkey +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****CONTRIBUTE TO THE PHASE 2 CEREMONY****" +start=`date +%s` +echo "test" | snarkjs zkey contribute "$BUILD_DIR"/"$CIRCUIT_NAME"_0.zkey "$BUILD_DIR"/"$CIRCUIT_NAME"_1.zkey --name="1st Contributor Name" +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****GENERATING FINAL ZKEY****" +start=`date +%s` +snarkjs zkey beacon "$BUILD_DIR"/"$CIRCUIT_NAME"_1.zkey "$BUILD_DIR"/"$CIRCUIT_NAME".zkey 0102030405060708090a0b0c0d0e0f101112231415161718221a1b1c1d1e1f 10 -n="Final Beacon phase2" +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****VERIFYING FINAL ZKEY****" +start=`date +%s` +snarkjs zkey verify "$BUILD_DIR"/"$CIRCUIT_NAME".r1cs "$PHASE1" "$BUILD_DIR"/"$CIRCUIT_NAME".zkey +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****EXPORTING VKEY****" +start=`date +%s` +snarkjs zkey export verificationkey "$BUILD_DIR"/"$CIRCUIT_NAME".zkey "$BUILD_DIR"/vkey.json +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****GENERATING PROOF FOR SAMPLE INPUT****" +start=`date +%s` +snarkjs groth16 prove "$BUILD_DIR"/"$CIRCUIT_NAME".zkey "$BUILD_DIR"/witness.wtns "$BUILD_DIR"/proof.json "$BUILD_DIR"/public.json +end=`date +%s` +echo "DONE ($((end-start))s)" + +echo "****VERIFYING PROOF FOR SAMPLE INPUT****" +start=`date +%s` +snarkjs groth16 verify "$BUILD_DIR"/vkey.json "$BUILD_DIR"/public.json "$BUILD_DIR"/proof.json +end=`date +%s` +echo "DONE ($((end-start))s)" diff --git a/scripts/groupsign_cli.ts b/scripts/groupsign_cli.ts index 5a0b29c..4a37c06 100644 --- a/scripts/groupsign_cli.ts +++ b/scripts/groupsign_cli.ts @@ -1,6 +1,5 @@ const snarkjs = require('snarkjs'); const readline = require('readline'); -const util = require('util'); const { BigNumber, Wallet } = require('ethers'); const fs = require('fs'); const wc = require('../build/groupsig/groupsig_js/witness_calculator.js'); @@ -11,21 +10,14 @@ const vkey = './build/groupsig/vkey.json'; const wtnsFile = './build/groupsig/witness.wtns'; function isHex(str: string): boolean { - if (str.length % 2 !== 0) return false; - if (str.slice(0, 2) !== '0x') return false; + if (str.startsWith('0x')) str = str.slice(2); const allowedChars = '0123456789abcdefABCDEF'; - for (let i = 2; i < str.length; i++) + for (let i = 0; i < str.length; i++) if (!allowedChars.includes(str[i])) return false; return true; } -function isValidPrivateKey(privkey: string): boolean { - if (privkey.length !== 66) return false; - if (!isHex(privkey)) return false; - return true; -} - function isValidAddr(addr: string): boolean { if (addr.length !== 42) return false; if (!isHex(addr)) return false; @@ -53,23 +45,28 @@ async function generateWitness(inputs: any) { fs.writeFileSync(wtnsFile, buff); } +// --- MAIN LOGIC --- + async function run() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + // 1. Private Key Input const privKeyStr = await new Promise((res) => { rl.question("Enter an ETH private key:\n", (ans: string) => { res(ans); }) - }) + }); const wallet = new Wallet(privKeyStr); console.log(`Your address is: ${wallet.address}`); + // 2. Group Addresses Input const groupAddr1 = await new Promise((res) => { rl.question("Enter address 1 for your group:\n", (ans: string) => { res(ans); }) }); if (!isValidAddr(groupAddr1)) throw new Error('not a valid ETH address'); + const groupAddr2 = await new Promise((res) => { rl.question("Enter address 2 for your group:\n", (ans: string) => { res(ans); @@ -77,6 +74,7 @@ async function run() { }); if (!isValidAddr(groupAddr2)) throw new Error('not a valid ETH address'); + // Randomize position of your address in the group of 3 const idx1 = Math.floor(Math.random() * 3); let idx2 = Math.floor(Math.random() * 2); if (idx2 >= idx1) idx2++; @@ -87,8 +85,15 @@ async function run() { groupAddresses[idx2] = BigInt(groupAddr1); groupAddresses[idx3] = BigInt(groupAddr2); + // 3. Message and Nonce Input const msg = await new Promise((res) => { - rl.question("Enter a message to sign (number between 0 and babyjubjubprime - 1):\n", (ans: string) => { + rl.question("Enter a message to sign:\n", (ans: string) => { + res(ans); + }) + }); + + const nonce = await new Promise((res) => { + rl.question("Enter a nonce (e.g., 0, 1, 2...):\n", (ans: string) => { res(ans); }) }); @@ -98,41 +103,36 @@ async function run() { addr1: groupAddresses[0], addr2: groupAddresses[1], addr3: groupAddresses[2], - msg + msg, + nonce }; - console.log(input); - - // for some reason fullprove is broken currently: https://github.com/iden3/snarkjs/issues/107 + // 4. Witness and Proof Generation console.log('generating witness...'); - const wtnsStart = Date.now(); await generateWitness(input); - console.log(`generated witness. took ${Date.now() - wtnsStart}ms`); - const pfStart = Date.now(); console.log('generating proof...'); const { proof, publicSignals } = await snarkjs.groth16.prove(zkey, wtnsFile); - console.log(proof); - console.log(publicSignals); - console.log(`generated proof. took ${Date.now() - pfStart}ms`); - - const verifyStart = Date.now(); - console.log('verifying proof...'); + // 5. Verification const vkeyJson = JSON.parse(fs.readFileSync(vkey)); - const res = await snarkjs.groth16.verify(vkeyJson, publicSignals, proof); - if (res === true) { - console.log("Verification OK"); - console.log(`verified that one of these addresses signed ${publicSignals[4]}:`); - console.log(BigNumber.from(publicSignals[1]).toHexString()); - console.log(BigNumber.from(publicSignals[2]).toHexString()); - console.log(BigNumber.from(publicSignals[3]).toHexString()); + const verified = await snarkjs.groth16.verify(vkeyJson, publicSignals, proof); + + if (verified === true) { + console.log("-----------------------------------------"); + console.log("βœ… Verification SUCCESSFUL"); + console.log(`Message: ${publicSignals[4]}`); + console.log(`Nonce: ${publicSignals[5]}`); + console.log("Signed by one of these members:"); + console.log(`- ${BigNumber.from(publicSignals[1]).toHexString()}`); + console.log(`- ${BigNumber.from(publicSignals[2]).toHexString()}`); + console.log(`- ${BigNumber.from(publicSignals[3]).toHexString()}`); + console.log("-----------------------------------------"); } else { - console.log("Invalid proof"); + console.log("❌ Invalid proof"); } - console.log(`verification took ${Date.now() - verifyStart}ms`); process.exit(0); } -run(); +run(); \ No newline at end of file diff --git a/scripts/test_private_secure_variable_groupsig.js b/scripts/test_private_secure_variable_groupsig.js new file mode 100644 index 0000000..6e4a852 --- /dev/null +++ b/scripts/test_private_secure_variable_groupsig.js @@ -0,0 +1,285 @@ +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const snarkjs = require("snarkjs"); +const { ethers } = require("ethers"); + +// -------------------------------------------------- +// Utils +// -------------------------------------------------- + +function splitHash(hashHex) { + const hashBigInt = BigInt(hashHex); + const lo = hashBigInt & ((1n << 128n) - 1n); + const hi = hashBigInt >> 128n; + + return { + hi: hi.toString(), + lo: lo.toString() + }; +} + +function generatePrivKey() { + let privKey; + + do { + privKey = BigInt("0x" + crypto.randomBytes(32).toString("hex")); + } while (privKey === 0n || privKey >= ethers.constants.MaxUint256); + + return privKey; +} + +function privKeyToAddress(privKey) { + const wallet = new ethers.Wallet( + privKey.toString(16).padStart(64, "0") + ); + + return BigInt(wallet.address); +} + +function now() { + return process.hrtime.bigint(); +} + +function elapsedMs(start, end) { + return Number(end - start) / 1e6; +} + + +// -------------------------------------------------- +// Main +// -------------------------------------------------- + +async function main() { + + const TOTAL_START = now(); + + console.log("================================="); + console.log(" ZK GroupSig Performance Test (group size = 4)"); + console.log("=================================\n"); + + + // -------------------------------------------------- + // Message + // -------------------------------------------------- + + const nonce = 42; + + const cid = "QmTestCid123"; + const company = "0x1234567890123456789012345678901234567890"; + const marketplace = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; + + const msgStr = + `ZK_PUBLISH_V1-${nonce}-${cid}-for-${company}-on-${marketplace}`; + + const msgHash = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes(msgStr) + ); + + const { hi, lo } = splitHash(msgHash); + + + // -------------------------------------------------- + // Private key + // -------------------------------------------------- + + const privKey = generatePrivKey(); + + const privKeyWords = []; + + let pk = privKey; + const mask64 = (1n << 64n) - 1n; + + for (let i = 0; i < 4; i++) { + privKeyWords.push((pk & mask64).toString()); + pk >>= 64n; + } + + + // -------------------------------------------------- + // Group + // -------------------------------------------------- + + const addr = privKeyToAddress(privKey); + + const addrs = [ + addr, + addr + 1n, + addr + 2n, + addr + 3n + ]; + + + // -------------------------------------------------- + // Circuit input + // -------------------------------------------------- + + const input = { + privkey: privKeyWords, + + privHashHi: hi, + privHashLo: lo, + + pubHashHi: hi, + pubHashLo: lo, + + addrs: addrs.map(a => a.toString()), + + nonce: nonce.toString() + }; + + + // -------------------------------------------------- + // Paths + // -------------------------------------------------- + + const BASE = path.join( + __dirname, + "..", + "build", + "groupsig", + "p_m_4" + ); + + const wasmPath = path.join( + BASE, + "private_secure_variable_groupsig_js", + "private_secure_variable_groupsig.wasm" + ); + + const wcPath = path.join( + BASE, + "private_secure_variable_groupsig_js", + "witness_calculator.js" + ); + + const zkeyPath = path.join( + BASE, + "private_secure_variable_groupsig.zkey" + ); + + const vkeyPath = path.join( + BASE, + "vkey.json" + ); + + const wtnsPath = path.join( + BASE, + "bench.wtns" + ); + + + // -------------------------------------------------- + // Witness + // -------------------------------------------------- + + console.log("⚑ Generating witness..."); + + const tWitStart = now(); + + const builder = require(wcPath); + + const buffer = fs.readFileSync(wasmPath); + + const witnessCalculator = await builder(buffer); + + const witness = await witnessCalculator.calculateWTNSBin( + input, + 0 + ); + + fs.writeFileSync(wtnsPath, witness); + + const tWitEnd = now(); + + console.log("βœ… Witness done:", + elapsedMs(tWitStart, tWitEnd).toFixed(2), + "ms" + ); + + + // -------------------------------------------------- + // Proof + // -------------------------------------------------- + + console.log("\n⚑ Generating proof..."); + + const tProofStart = now(); + + const { proof, publicSignals } = + await snarkjs.groth16.prove( + zkeyPath, + wtnsPath + ); + + const tProofEnd = now(); + + console.log("βœ… Proof done:", + elapsedMs(tProofStart, tProofEnd).toFixed(2), + "ms" + ); + + + // -------------------------------------------------- + // Verify + // -------------------------------------------------- + + console.log("\n⚑ Verifying proof..."); + + const tVerStart = now(); + + const vkey = JSON.parse( + fs.readFileSync(vkeyPath) + ); + + const verified = await snarkjs.groth16.verify( + vkey, + publicSignals, + proof + ); + + const tVerEnd = now(); + + console.log("βœ… Verify done:", + elapsedMs(tVerStart, tVerEnd).toFixed(2), + "ms" + ); + + + // -------------------------------------------------- + // Summary + // -------------------------------------------------- + + const TOTAL_END = now(); + + console.log("\n================================="); + console.log(" Benchmark Results"); + console.log("=================================\n"); + + console.log("Witness: ", + elapsedMs(tWitStart, tWitEnd).toFixed(2), "ms"); + + console.log("Proof: ", + elapsedMs(tProofStart, tProofEnd).toFixed(2), "ms"); + + console.log("Verify: ", + elapsedMs(tVerStart, tVerEnd).toFixed(2), "ms"); + + console.log("Total: ", + elapsedMs(TOTAL_START, TOTAL_END).toFixed(2), "ms"); + + + console.log("\nVerified:", verified ? "YES βœ…" : "NO ❌"); + + console.log("\n=================================\n"); + + process.exit(0); +} + + +// -------------------------------------------------- + +main().catch(err => { + console.error("ERROR:", err); + process.exit(1); +}); diff --git a/scripts/variable_groupsign_cli.ts b/scripts/variable_groupsign_cli.ts new file mode 100644 index 0000000..2925548 --- /dev/null +++ b/scripts/variable_groupsign_cli.ts @@ -0,0 +1,162 @@ +const snarkjs = require('snarkjs'); +const readline = require('readline'); +const { BigNumber, Wallet } = require('ethers'); +const fs = require('fs'); +const path = require('path'); + +// --- PATH CONFIGURATION --- +// Use resolve to get absolute paths from the start +const PROJECT_ROOT = path.resolve(__dirname, '..'); +const BUILD_BASE = path.join(PROJECT_ROOT, 'build', 'groupsig'); +const wtnsFile = path.join(BUILD_BASE, 'witness.wtns'); + +// --- HELPER FUNCTIONS --- + +function getAvailableGroupSizes(): number[] { + if (!fs.existsSync(BUILD_BASE)) return []; + return fs.readdirSync(BUILD_BASE) + .filter((name: string) => name.startsWith('m_')) + .map((name: string) => parseInt(name.split('_')[1])) + .sort((a: number, b: number) => a - b); +} + +function isHex(str: string): boolean { + if (str.startsWith('0x')) str = str.slice(2); + const allowedChars = '0123456789abcdefABCDEF'; + for (let i = 0; i < str.length; i++) + if (!allowedChars.includes(str[i])) return false; + return true; +} + +function isValidAddr(addr: string): boolean { + if (addr.length !== 42) return false; + if (!isHex(addr)) return false; + return true; +} + +function toWordArray(x: bigint, nWords: number, bitsPerWord: number): string[] { + const res: string[] = []; + let remaining = x; + const base = 2n ** BigInt(bitsPerWord); + for (let i = 0; i < nWords; i++) { + res.push((remaining % base).toString()); + remaining /= base; + } + return res; +} + +async function generateWitness(inputs: any, wasmPath: string, sizeDir: string) { + const wcPath = path.join(sizeDir, 'secure_variable_groupsig_js', 'witness_calculator.js'); + + console.log(`Using WASM: ${wasmPath}`); + + if (!fs.existsSync(wcPath)) { + throw new Error(`witness_calculator.js not found at ${wcPath}`); + } + + // Dynamic require using absolute path + const builder = require(wcPath); + + const buffer = fs.readFileSync(wasmPath); + const witnessCalculator = await builder(buffer); + const buff = await witnessCalculator.calculateWTNSBin(inputs, 0); + fs.writeFileSync(wtnsFile, buff); +} + +// --- MAIN LOGIC --- + +async function run() { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const availableSizes = getAvailableGroupSizes(); + + if (availableSizes.length === 0) { + console.error(`❌ No compiled circuits found. Searched in: ${BUILD_BASE}`); + process.exit(1); + } + + console.log(`Found compiled circuits for group sizes: ${availableSizes.join(', ')}`); + + // 1. Private Key Input + const privKeyStr = await new Promise((res) => { + rl.question("Enter an ETH private key:\n", (ans: string) => res(ans)); + }); + const wallet = new Wallet(privKeyStr); + console.log(`Your address is: ${wallet.address}`); + + // 2. Select Group Size + const mStr = await new Promise((res) => { + rl.question(`How many total addresses (${availableSizes[0]}-${availableSizes[availableSizes.length-1]})?\n`, (ans: string) => res(ans)); + }); + const m = parseInt(mStr); + + if (!availableSizes.includes(m)) { + console.error(`❌ Circuit for size ${m} not found.`); + process.exit(1); + } + + // Define ABSOLUTE paths for the selected size + const sizeDir = path.join(BUILD_BASE, `m_${m}`); + const wasm = path.join(sizeDir, 'secure_variable_groupsig_js', 'secure_variable_groupsig.wasm'); + const zkey = path.join(sizeDir, 'secure_variable_groupsig.zkey'); + const vkey = path.join(sizeDir, 'vkey.json'); + + // 3. Collect Other Addresses + const otherAddrs: string[] = []; + for (let i = 0; i < m - 1; i++) { + const addr = await new Promise((res) => { + rl.question(`Enter address ${i + 1}/${m - 1} of other members:\n`, (ans: string) => res(ans)); + }); + if (!isValidAddr(addr)) throw new Error('Invalid address'); + otherAddrs.push(addr); + } + + // 4. Group Construction (Shuffle) + const groupAddresses: string[] = otherAddrs.map(a => BigInt(a).toString()); + const myIdx = Math.floor(Math.random() * m); + groupAddresses.splice(myIdx, 0, BigInt(wallet.address).toString()); + + // 5. Message & Nonce + const msg = await new Promise((res) => { + rl.question("Enter message:\n", (ans: string) => res(ans)); + }); + const nonce = await new Promise((res) => { + rl.question("Enter nonce:\n", (ans: string) => res(ans)); + }); + + const input = { + privkey: toWordArray(BigInt(privKeyStr), 4, 64), + addrs: groupAddresses, + msg, + nonce + }; + + // 6. Witness & Proof + console.log('⚑ Generating witness...'); + await generateWitness(input, wasm, sizeDir); + + console.log('⚑ Generating proof...'); + const { proof, publicSignals } = await snarkjs.groth16.prove(zkey, wtnsFile); + + // 7. Verify + const vkeyJson = JSON.parse(fs.readFileSync(vkey)); + const verified = await snarkjs.groth16.verify(vkeyJson, publicSignals, proof); + + if (verified) { + console.log("\n-----------------------------------------"); + console.log("βœ… Verification SUCCESSFUL"); + // Public signals for Main(n, k, m) are: + // [0] msgAttestation, [1...m] addrs, [m+1] msg, [m+2] nonce + console.log(`Message: ${publicSignals[m+1]}`); + console.log(`Nonce: ${publicSignals[m+2]}`); + console.log("Proved membership in group:"); + for (let i = 0; i < m; i++) { + console.log(`- ${BigNumber.from(publicSignals[i+1]).toHexString()}`); + } + console.log("-----------------------------------------"); + } else { + console.log("❌ Invalid proof"); + } + process.exit(0); +} + +run(); \ No newline at end of file