Skip to content

Repository files navigation

Zero-Knowledge Proof IPv6 Blocklist

A collection of zero-knowledge proof systems that allow a prover to demonstrate their IPv6 address is not on a public blocklist — without revealing the address itself.

Built with Circom 2, SnarkJS, and the Groth16 proving system.


Table of Contents


Overview

This project explores the practical application of zero-knowledge proofs for network access control. The core idea is:

A user wants to prove to a verifier that their IPv6 address is not on a known blocklist, without revealing what their address actually is.

The blocklist itself is public. The prover's IP address is the private witness. A Groth16 ZK-SNARK proof is generated and given to the verifier. The verifier can confirm the proof is valid without learning the IP address.


How It Works

The workflow at a high level:

  1. Circuit compilation — The .circom circuit is compiled to R1CS constraints and a WASM witness generator.
  2. Trusted setup — A multi-party Powers of Tau (PoT) ceremony generates the common reference string (CRS). Anyone can contribute entropy.
  3. Witness generation — The prover feeds their private IP address and the public blocklist into the WASM witness calculator to produce witness.wtns.
  4. Proof generationsnarkjs groth16 prove uses the final proving key (.zkey) and the witness to produce a proof.
  5. Verificationsnarkjs groth16 verify checks the proof against the public verification key and the public inputs (blocklist + output signal). No private data is revealed.

Repository Structure

Zero-Knowledge-Proofs/
├── Phase_1/
│   └── zkblocklist/              # Phase 1: linear scan circuit
│       ├── zkpblocklist.circom   # Circom circuit source
│       ├── setup1.sh             # End-to-end setup/prove/verify script
│       ├── input.json            # Prover inputs (blocklist + private IP)
│       ├── build/                # Compiled R1CS and WASM (generated)
│       ├── verification_key.json # Exported verification key
│       ├── zkpblocklist_proof.json
│       └── zkpblocklist_public.json
├── Phase_1.1/
│   ├── Merkle_Tree/
│   │   └── mt.cpp                # Standalone C++ SHA-256 Merkle tree (reference)
│   └── zkblocklistv1.1/
│       ├── zkpblocklistv1.1.circom  # Phase 1.1 Circom circuit
│       ├── setup1.1.sh              # End-to-end setup/prove/verify script
│       └── rustSMTgenerator/        # Rust tool: builds a Sparse Merkle Tree
│           ├── src/main.rs          # SMT construction + exclusion proof
│           ├── address.json         # The address to prove absent from the SMT
│           ├── smt_data.json        # SMT output (root, leaves, proof validity)
│           ├── testrs.sh            # Automated test harness for the SMT
│           └── cargo.toml
├── circomlib/                    # Circom standard library (submodule)
├── sparse-merkle-tree/           # Rust SMT library (submodule)
└── blake2b-rs/                   # Blake2b hasher for Rust (submodule)

Prerequisites

Install the following before using either phase:

Tool Purpose Install
Circom 2 Compiles .circom circuits to R1CS + WASM Build from source with Rust (cargo build --release)
Node.js (v16+) Runs witness generator and SnarkJS Download from nodejs.org
SnarkJS Trusted setup, proof generation, verification npm install -g snarkjs
Rust + Cargo Builds the Rust SMT generator (Phase 1.1) `curl https://sh.rustup.rs
Git Bash Runs the .sh scripts on Windows Included with Git for Windows
grep Used by setup scripts to parse JSON Included in Git Bash
Python 3 Used by the SMT test harness Download from python.org

Windows note: All .sh scripts must be run in Git Bash, not PowerShell or CMD. The circom compiler path in the scripts uses Windows-style paths — update them to match your local installation before running.


Phase 1 — Linear Scan Blocklist

Phase 1 Setup

Before running, open Phase_1/zkblocklist/setup1.sh and update these two variables to match your environment:

WORKDIR="C:/Users/yourname/path/to/Phase_1/zkblocklist"
CIRCOM_COMPILER="C:/path/to/circom.exe"

Also update the include path at the top of Phase_1/zkblocklist/zkpblocklist.circom:

include "C:/path/to/circomlib/circuits/comparators.circom";

Phase 1 Input Format

Edit Phase_1/zkblocklist/input.json before generating a proof. The file has two fields:

{
  "blocklist": [
    [8193, 3512, 34211, 0, 0, 35374, 880, 29492],
    [8193, 3512, 34211, 0, 0, 35374, 880, 30000],
    ...
  ],
  "ipv6Address": [8193, 3512, 34211, 0, 0, 35374, 880, 99999]
}
  • blocklist — A 2D array of 128 entries, each an IPv6 address represented as 8 decimal integers (one per 16-bit group). Pad unused entries with [0,0,0,0,0,0,0,0]. This is the public input.
  • ipv6Address — The prover's IPv6 address as 8 decimal integers. This is the private witness.

Converting an IPv6 address to decimal groups:

An IPv6 address like 2001:0db8:85a3:0000:0000:8a2e:0370:7334 maps to:

[0x2001, 0x0db8, 0x85a3, 0x0000, 0x0000, 0x8a2e, 0x0370, 0x7334]
= [8193, 3512, 34211, 0, 0, 35374, 880, 29492]

Phase 1 Running

Open Git Bash and run:

cd Phase_1/zkblocklist
bash setup1.sh

The script presents five options:

Option When to use
1 — Full new setup First time ever, or after changing the circuit and wanting to redo everything from scratch including the trusted setup
2 — Inputs changed only You only edited input.json — reuses existing proving key
3 — Circuit changed You modified the .circom file; recompiles and re-runs Groth16 setup using the existing .ptau
4 — Verify only Re-runs verification on existing proof files without regenerating anything
5 — Exit Aborts without doing anything

Option 1 interactive prompts:

  1. 4× random entropy strings — Typed input for each of the four Powers of Tau contributions. Use a long random string for each (e.g., keyboard mashing). More entropy = more secure ceremony.
  2. Username — A name to tag your zkey contribution. Can be any string.

Phase 1 Output Files

After a successful run the following files are written (or updated):

File Description
pot12_prepared.ptau Finalized Powers of Tau file for phase 2
zkpblocklist_final.zkey Final proving key
verification_key.json Verification key — give this to the verifier
witness.wtns Witness (private; do not share)
zkpblocklist_proof.json The ZK proof — give this to the verifier
zkpblocklist_public.json Public signals (blocklist + onBlocklist output) — give this to the verifier
build/zkpblocklist.r1cs R1CS constraint system (generated once per circuit)
build/zkpblocklist_js/ WASM witness generator (generated once per circuit)

To verify a proof on the verifier's machine (without needing the private key):

snarkjs groth16 verify verification_key.json zkpblocklist_public.json zkpblocklist_proof.json

The output signal onBlocklist in zkpblocklist_public.json is:

  • 1 — The address is on the blocklist
  • 0 — The address is not on the blocklist

Phase 1 Limitations

  • Not resistant to IP spoofing — The prover supplies their own IP address as private input. A dishonest prover can simply claim to have any address they want and generate a valid proof for that false address. There is no mechanism to bind the proof to the prover's actual network identity.
  • The blocklist is fixed at compile time at 128 entries. Changing the size requires recompiling the circuit and redoing the trusted setup.
  • The trusted setup is local and involves only one contributor per run, which is weaker than a large multi-party ceremony.

Phase 1.1 — Sparse Merkle Tree Blocklist

Phase 1.1 introduces a Sparse Merkle Tree (SMT) data structure as the basis for the blocklist. Instead of checking membership by comparing every entry, the prover provides a Merkle exclusion proof: a cryptographic proof that their key is absent from the tree.

The Rust program in rustSMTgenerator/ builds the SMT from the hardcoded blocklist, accepts a target IPv6 address from address.json, and outputs the SMT root and leaf data to smt_data.json. The Circom circuit (same core logic as Phase 1 at the current stage) is then used with this data.

Rust SMT Generator

The SMT is built using the sparse-merkle-tree crate (v0.6.1) with Blake2b hashing.

Blocklist is hardcoded in src/main.rs:

const IPV6ADDRESSES: [[u16; 8]; BLEN] = [
    [8193, 3512, 34211, 0, 0, 35374, 880, 29492],
    // ... more entries
];
static BLEN: usize = 7; // update this when adding entries

To add or remove blocklist entries, update both BLEN and the IPV6ADDRESSES array, then rebuild.

Target address is read from address.json:

[8193, 3512, 34211, 0, 0, 35374, 880, 12345]

A plain JSON array of 8 unsigned 16-bit integers.

To run the Rust generator:

cd Phase_1.1/zkblocklistv1.1/rustSMTgenerator
# Edit address.json with the target IPv6 address first
cargo run

The program prints whether the exclusion proof is valid and writes smt_data.json.

smt_data.json structure:

{
  "root": [/* 32-byte array */],
  "leaves": [
    { "key": [/* 32 bytes */], "value": [/* 32 bytes */] }
  ],
  "Exclusion proof valid": true
}

Note: The "Exclusion proof valid" field in smt_data.json is for testing purposes only (used by testrs.sh). The Circom verifier does not use this field — it only uses the key and value data.

Phase 1.1 Setup

Before running, update the path variables in Phase_1.1/zkblocklistv1.1/setup1.1.sh:

WORKDIR="C:/Users/yourname/path/to/Phase_1.1/zkblocklistv1.1"
CIRCOM_COMPILER="C:/path/to/circom.exe"

Also update the include path in Phase_1.1/zkblocklistv1.1/zkpblocklistv1.1.circom:

include "C:/path/to/circomlib/circuits/comparators.circom";

The circuit also requires an input JSON named zkpblocklistv1.1_input.json in the working directory. Its format is the same as Phase 1 input.json (blocklist array + ipv6Address).

Phase 1.1 Running

cd Phase_1.1/zkblocklistv1.1
bash setup1.1.sh

Options are the same as Phase 1:

Option Action
1 Full new setup (PoT ceremony, compile, prove, verify)
2 Inputs changed only
3 Circuit changed
4 Verify proof only
5 Exit

Testing the SMT

testrs.sh is an automated regression harness for the Rust SMT generator. It runs the exclusion proof multiple times with randomly generated addresses and verifies that:

  • Addresses not on the blocklist produce a valid exclusion proof (cargo run exits 0).
  • Addresses on the blocklist fail the exclusion proof (cargo run exits non-zero).
cd Phase_1.1/zkblocklistv1.1/rustSMTgenerator
bash testrs.sh

Results are appended to test_results.log. Requires Python 3 to generate random test addresses.

Phase 1.1 Limitations

  • The circuit still uses the same linear-scan approach as Phase 1. The SMT exclusion proof is generated in Rust but is not yet wired into the Circom verification constraints — the SMT root is not verified inside the circuit.
  • IP spoofing vulnerability from Phase 1 still applies.

Circuit Design

Both phases use the same ProveBlocklistExclusion template:

Inputs:
  blocklist[128][8]   — public: 128 IPv6 addresses, each as 8 uint16 values
  ipv6Address[8]      — private: the prover's IPv6 address

Output:
  onBlocklist         — 1 if ipv6Address matches any blocklist entry, 0 otherwise

The circuit works as follows:

  1. For each of the 128 blocklist entries, compare all 8 groups to the private IP using IsEqual components from circomlib.
  2. AND the 8 comparisons together to get a per-entry match signal.
  3. Propagate a running "any match found" signal across all 128 entries using the formula:
    sumval[i] = matches[i] + (1 - matches[i]) * sumval[i-1]
    
  4. Apply IsZero to each sumval[i] and take the cumulative product. If every sumval is zero (no match ever found), the product is 1 and onBlocklist = 0. Otherwise onBlocklist = 1.

The proving system is Groth16 over the BN128 curve, compiled with snarkjs powersoftau new bn128 14 (supporting up to $2^{14} = 16384$ constraints).


Security Considerations

  • Trust in the setup: The security of the Groth16 proof relies on the Powers of Tau ceremony being conducted with at least one honest participant. The setup scripts perform a local ceremony with a single contributor. For production use, a public multi-party ceremony (e.g., from the Hermez network or Perpetual Powers of Tau) should be used.
  • Private input is self-reported: As noted under Limitations, the prover supplies their own IP address. A malicious prover can generate a valid proof for any IP they choose.
  • Verification key integrity: The verifier should independently check that the verification_key.json was derived from the correct circuit and .ptau file. The setup script outputs: Give them the code, the finalized ptau file, and the r1cs file so they can check the verification key matches the circuit.
  • Salt in the SMT: The Rust SMT generator uses a hardcoded salt (SALT) when deriving keys. This salt is embedded in the source code and is not secret.

Submodules

This repository uses Git submodules:

Submodule Description
circomlib/ Standard Circom component library (comparators, hashes, etc.)
sparse-merkle-tree/ Rust SMT implementation used by Phase 1.1
blake2b-rs/ Blake2b hasher used by the Rust SMT

After cloning, initialize them with:

git submodule update --init --recursive

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages