GROTH16 | POSEIDON (CIRCOMLIB) | BN254 | R1CS (45K CONSTRAINTS)
Veridaq uses Groth16, the most widely deployed zk-SNARK proving system in production. It was introduced by Jens Groth in 2016 and optimized by Bellman and the Zcash team for the BLS12-381 curve. Veridaq uses BN254, the curve supported natively by the EVM.
Groth16 has three properties that make it ideal for academic credential verification. First, the proofs are constant size — exactly 3 group elements (about 256 bytes) regardless of the circuit size. Second, verification time is constant too — a single pairing check. Third, the prover does not need to interact with the verifier after submitting the proof.
The tradeoff is that Groth16 requires a trusted setup ceremony for each circuit. Veridaq uses the Hermez Powers of Tau ceremony for phase 1 (universal) and a circuit-specific phase 2. The parameters are public and verifiable.
Groth16 requires a structured reference string (SRS) generated through a multi-party computation. The ceremony has two phases.
Phase 1 is circuit-agnostic and produces the Powers of Tau. Veridaq uses the Hermez ceremony which had over 100 participants. Each participant contributed entropy and destroyed their toxic waste. As long as one participant was honest, the phase 1 parameters are secure.
Phase 2 is circuit-specific. It takes the phase 1 output and produces the proving key and verification key for the credential circuit. This phase runs locally using SnarkJS and applies a random beacon to ensure the toxic waste is destroyed even if the local machine was compromised.
If the trusted setup is compromised, an attacker could forge proofs for arbitrary inputs. This is why the ceremony must be reproducible and verifiable. Anyone can verify the phase 2 output against the phase 1 reference and the circuit R1CS.
Before any proof can be generated, the institution must submit a commitment to the CredentialRegistry contract. The commitment is a Poseidon hash of the student data combined with a random blinding factor.
Poseidon is a zero knowledge-friendly hash function designed specifically for use inside arithmetic circuits. Unlike SHA-256 or keccak256, which require thousands of R1CS constraints per hash, Poseidon compiles to approximately 100 constraints per permutation. This is why Veridaq uses Poseidon instead of keccak256.
// The commitment is computed off-chain by the backend
// It is the only value ever submitted on-chain
commitment = Poseidon(
nameHash, // sha256(full name) as field element
matricHash, // sha256(matric number) as field element
cgpa, // integer scaled by 100 (e.g. 450 for 4.50)
classification, // 0=PASS, 1=THIRD, 2=LOWER_CREDIT,
// 3=UPPER_CREDIT, 4=FIRST_CLASS
courseHash, // sha256(course code) as field element
graduationYear, // 4-digit integer
blindingFactor // random 256-bit scalar
)The blinding factor is critical. It is a cryptographically random 256-bit value generated on the backend using Node.js crypto.randomBytes. Without it, an attacker could brute force the commitment by hashing known student data and comparing against the on-chain value. With it, the commitment is computationally hiding.
The nullifier is computed separately and prevents the same credential from being verified twice. It binds the matriculation number to the institution key:
nullifier = Poseidon(matricHash, institutionKey)
In Groth16, inputs to the circuit are divided into private signals (known only to the prover) and public signals (visible on-chain in the verification transaction). The circuit template declares which signals are which.
Known only to the backend. Destroyed after proof generation. Never submitted on-chain.
Visible on-chain. The verifier contract reads these during verification.
The ClaimDecoder template inside the circuit maps the claimType signal to one of six constraint templates. Each template checks a different condition against the private input signals.
Employer submits the expected graduation year. The circuit checks the private graduationYear signal equals the threshold. This proves the candidate completed the programme in the stated year.
The circuit checks that the classification signal is greater than or equal to 2 (Lower Credit classification or higher). This is the standard minimum for most graduate jobs in Nigeria.
Same pattern but with a higher barrier. The employer wants candidates who graduated with Upper Credit or First Class. The circuit checks classification >= 3.
The strictest classification check. The circuit proves the candidate graduated with First Class Honours. The threshold is ignored for this claim type.
The employer sets a numeric CGPA threshold and the circuit proves the student's CGPA meets or exceeds it. The threshold is an integer scaled by 100. To check for CGPA >= 3.50, the employer submits threshold = 350.
The most sophisticated claim type. The employer specifies a course code. The circuit checks that the courseHash matches AND the classification represents a passing grade for that course. This is used when a specific prerequisite course is required.
Circom compiles the circuit to a Rank-1 Constraint System (R1CS) with approximately 45,000 constraints. Each constraint is of the form A * B = C where A, B, and C are linear combinations of the circuit signals.
The constraints fall into three groups. First, the poseidon hash constraints. Each poseidon permutation requires about 20 constraints per round, and the 3-round sponge configuration adds about 100 constraints per hash. The circuit hashes 7 elements for the commitment and 2 elements for the nullifier, so the total hash constraint count is approximately 9 * 100 = 900 constraints.
Second, the comparison constraints. The circuit needs to check cgpa >= threshold and classification >= threshold. These comparisons are implemented using bit decomposition and subtraction with borrow checking. Each comparison adds approximately 500 constraints.
Third, the equality constraints. The commitment equality check and nullifier equality check each add a single constraint: hasher.out === commitment. The ClaimDecoder output check adds another single constraint: claim.out === 1. The remaining constraints come from signal routing, template instantiation, and the Circom compiler's internal bookkeeping.
The ClaimDecoder is a Circom template that maps a claimType enum to the appropriate constraint template. It uses Circom's conditional signal routing to select from six possible claim templates without executing all of them.
When an employer submits a verification request, the backend runs the following sequence to generate and submit the proof.
The backend retrieves the encrypted record from PostgreSQL using the matriculation number as the lookup key. It decrypts the AES-256-GCM ciphertext using the encryption key from the environment. The raw student data exists in memory for the next 2 seconds, then is garbage collected.
The backend hashes the student name and matric number using sha256 and converts them to BN254 field elements. The CGPA is multiplied by 100 and floored to an integer. The classification is mapped to the 0-4 enum. courseHash is computed from the course code. These become the 8 private input signals.
The backend calls snarkjs.groth16.fullProve() with the private inputs, public inputs (commitment, nullifier, claimType, threshold), and the compiled WASM circuit. SnarkJS runs the prover inside Node.js using the WASM backend. Proof generation takes approximately 0.7 seconds on a modern CPU.
The backend encodes the proof (3 G1/G2 points) and the 4 public signals into a Solidity-compatible ABI format. It sends a transaction to the ZKVerifier contract on Base Sepolia via viem. The contract checks the proof using BN254 pairing precompiles at addresses 0x06, 0x07, and 0x08.
Before accepting the proof, the verifier calls RevocationRegistry.isRevoked(nullifier). If the nullifier has been revoked, the proof is rejected. This ensures that degrees rescinded by the university are immediately unverifiable, regardless of proof validity.
Measured on a 2023 laptop with Apple M3 Pro, 18 GB RAM, Node.js 22, and SnarkJS 0.7.4.
The on-chain cost depends on Base Sepolia gas prices and the ETH/USD exchange rate. At 0.1 gwei and $1,669/ETH, 236,000 gas costs approximately $0.01. This makes VERIDAQ practical for high-volume employer verification use cases.
The zero knowledge proof provides soundness: if the proof verifies, the prover possesses a valid witness that satisfies all circuit constraints. Soundness relies on the security of the Groth16 trusted setup and the BN254 curve.
Hash functions designed for traditional computing (SHA-256, keccak256) are extremely expensive inside arithmetic circuits. The following table shows the constraint count for each hash function in Circom:
| Hash Function | Constraints per Call | Constraints for 7 Inputs | Proof Time (est.) |
|---|---|---|---|
| Poseidon (3 rounds) | ~100 | ~700 | ~0.7s |
| Pedersen (BabyJubJub) | ~10,000 | ~70,000 | ~5s |
| SHA-256 | ~30,000 | ~210,000 | ~30s |
| Keccak256 | ~50,000 | ~350,000 | ~60s |
Using keccak256 instead of Poseidon would make proof generation take over a minute instead of under a second. The circuit would be 7 times larger and the proving key would be hundreds of megabytes. Poseidon is the correct choice for any ZKP system that needs to hash data inside the circuit.
| Approach | Privacy | Gas Cost | Verification Speed | Trust Model |
|---|---|---|---|---|
| VERIDAQ (Groth16) | Full ZK | ~236k gas | ~12 ms | Trusted setup + honest prover |
| PLONK | Full ZK | ~400k gas | ~15 ms | Universal trusted setup |
| STARK (on-chain) | Full ZK | ~5M gas | ~100 ms | No trusted setup |
| Plain hash on-chain | No privacy | ~50k gas | N/A | Centralized |
| Paper certificates | Full exposure | N/A | Weeks | Manual verification |