Production-ready Zero-Knowledge Proof Cookie Consent System with Groth16 on BLS12-381.
This system implements a privacy-preserving cookie banner that uses zero-knowledge proofs to verify user consent without storing user preferences on the server. The system is designed to be GDPR-compliant and privacy-first.
- Proof System: Groth16 on BLS12-381 β 192-byte proofs
- Hash Function: Poseidon (never SHA-256 or Keccak)
- Identity: Semaphore-style persistent identity with 32-byte
identitySecretin localStorage - Commitment:
Poseidon(oldConsent, timestamp, identitySecret) - Nullifier:
Poseidon(identitySecret, domainSalt)β prevents double-spending per domain - State: Sparse Merkle tree (depth 20) using Poseidon for consent state
- Enforcement:
- Monotonic consent:
newConsent β₯ oldConsent(8-bit bitfield) - Max consent age: 2 years enforced in-circuit via
currentTimepublic input
- Monotonic consent:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client (Browser) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ 32-byte identitySecret (localStorage) β
β β’ Poseidon hash computation β
β β’ Groth16 proof generation (snarkjs WASM) β
β β’ Banner UI (Vite + TypeScript, < 50 KB gzipped) β
βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
β
β 192-byte proof + 5 public signals
β
βββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββ
β Cloudflare Worker / Node.js Server β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ Groth16 proof verification β
β β’ Poseidon Merkle tree management β
β β’ Nullifier tracking (prevents replay attacks) β
β β’ Returns 200 OK β client hides banner forever β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Node.js 18+
- npm or yarn
- circom compiler (install via
npm install -g circom)
# Clone or navigate to the zkcookies directory
cd zkcookies
# Install dependencies
npm install
# Run setup (compiles circuit, runs phase-2 ceremony)
npm run setup
# Start development server (in one terminal)
npm run dev
# Start verification server (in another terminal)
npm run dev:serverOpen http://localhost:5173 in your browser.
Note: The setup process may take several minutes as it compiles the circuit and runs the trusted setup ceremony.
zkcookies/
βββ circuits/
β βββ consent.circom # Circom circuit definition
βββ src/
β βββ zk.ts # ZK proof generation logic
β βββ banner.ts # Banner UI and interaction
β βββ main.ts # Entry point
βββ worker/
β βββ verify.ts # Cloudflare Worker verifier
β βββ wrangler.toml # Cloudflare Worker config
βββ scripts/
β βββ setup.js # Setup script (circuit compilation)
βββ build/ # Generated files (circuit, keys)
βββ public/ # Static assets (WASM files)
βββ index.html # Demo page
βββ package.json
βββ vite.config.ts
βββ README.md
The npm run setup command:
- Compiles the Circom circuit β generates R1CS, WASM, and symbol files
- Runs Phase 1 ceremony β generates powers of tau (trusted setup)
- Runs Phase 2 ceremony β circuit-specific setup
- Exports verification key β for server-side verification
- Copies WASM files β to public directory for client use
import { CookieBanner } from './banner';
const banner = new CookieBanner({
apiEndpoint: 'https://your-api.com/verify',
domainSalt: BigInt('0x...'), // Domain-specific salt
onAccept: () => {
console.log('Consent accepted!');
},
onReject: () => {
console.log('Consent rejected.');
},
});
// Show banner if needed
if (banner.shouldShowBanner()) {
banner.show();
}Deploy the worker:
cd worker
wrangler deploySet the verification key as an environment variable:
wrangler secret put VERIFICATION_KEY
# Paste the contents of build/keys/verification_key.json- Privacy: User preferences never stored on server (only Merkle commitments)
- Unlinkability: Each proof uses a nullifier to prevent tracking
- Double-spend prevention: Nullifiers prevent replay attacks
- Monotonic consent: Consent can only increase, never decrease
- Expiry enforcement: 2-year max consent age enforced in-circuit
- No wallet required: Works in any browser without extensions
currentTime- Unix timestamp (public)domainSalt- Domain-specific salt (public)newConsentCommitment- Poseidon(newConsent, timestamp, identitySecret) (public)nullifier- Poseidon(identitySecret, domainSalt) (public)root- Merkle root (public)
- β Safari
- β Chrome
- β Firefox
- β Tor Browser
- β No WebGPU required
- β No experimental flags required
- β No browser extensions required
MIT License - see LICENSE file for details.
-
BLS12-381 vs BN254: The current setup uses BN254 (snarkjs limitation). For production BLS12-381, use a different toolchain (e.g., arkworks, bellman-bn254 with BLS12-381 support).
-
Trusted Setup: The setup script uses a single-contributor ceremony for MVP. In production, use a multi-party trusted setup ceremony.
-
Merkle Tree Storage: The worker uses in-memory storage. In production, use Cloudflare KV or Durable Objects for persistent tree state.
-
Poseidon Implementation: Ensure client and server use the same Poseidon implementation (from circomlib).
Circuit compilation fails:
- Ensure circom is installed:
npm install -g circom - Check that circomlib is installed:
npm install
Proof generation fails:
- Ensure WASM and zkey files are in the public directory
- Check browser console for errors
- Verify the circuit was compiled successfully
Server verification fails:
- Ensure verification key is correctly set in environment
- Check that proof format matches expected structure
- Verify nullifier hasn't been used before