Critical Security Mistakes ZK Engineers Make
ZK circuits are unforgiving. A single missing constraint can allow provers to generate valid proofs for false statements. Learn from these critical mistakes.
Constraint Mistakes
Errors in circuit constraints.
Signals that do not appear in any constraint let provers choose arbitrary values. This breaks the entire proof.
What happens: Prover can generate valid proofs for any output regardless of actual inputs.
Fix: Audit every signal. Each must appear in at least one constraint or be derived from constrained values.
Treating a signal as boolean without constraining it to 0 or 1. Field elements have many other values.
What happens: Prover can use non-boolean values, breaking conditional logic.
Fix: Always add `signal * (signal - 1) === 0` for any boolean signal.
Array lookups where the index is not constrained to valid range.
What happens: Out-of-bounds access can leak information or cause unexpected behavior.
Fix: Add range checks for all array indices.
Not constraining every step of Merkle path or getting left/right ordering wrong.
What happens: Invalid proofs accepted. Non-existent leaves proven to exist.
Fix: Verify each hash step. Test with known roots.
Arithmetic Mistakes
Finite field arithmetic errors.
Assuming arithmetic works like integers. Field arithmetic wraps at the modulus.
What happens: Large numbers wrap to small numbers. Comparisons break. Security assumptions fail.
Fix: Add range checks. Be explicit about field size assumptions.
Not checking that denominators are non-zero before division.
What happens: Undefined behavior. Prover might be able to exploit edge cases.
Fix: Constrain denominators to be non-zero.
Field elements have two square roots. Not specifying which one is expected.
What happens: Prover can use either root, potentially breaking logic.
Fix: Constrain which square root by adding sign or range constraints.
Decomposing to bits but not constraining that bits reconstruct to original value.
What happens: Bits do not represent the actual value. Proofs become meaningless.
Fix: Always add reconstruction constraint: sum of bits * powers of 2 == original.
Cryptographic Primitive Mistakes
Errors using hashes and signatures.
Hash inputs that are not fully constrained allow hash manipulation.
What happens: Prover can adjust unconstrained inputs to produce desired hash.
Fix: Every hash input must be fully constrained by circuit logic.
Verifying only part of a signature or not including the message.
What happens: Signature forgery. Proofs for unauthorized actions.
Fix: Complete signature verification including message binding.
Nullifiers that can be computed multiple ways for the same underlying item.
What happens: Double-spending. Same asset used multiple times with different nullifiers.
Fix: Nullifier derivation must be deterministic and unique.
Not validating that public keys are valid points on the curve.
What happens: Invalid keys can cause signature verification to misbehave.
Fix: Validate all external curve points are on the correct curve and subgroup.
Setup and Verification Mistakes
Errors in proof system setup and verification.
Using setup parameters generated by one party. That party can create fake proofs.
What happens: Setup generator can forge proofs for any statement.
Fix: Use multi-party ceremonies (Powers of Tau) for trusted setup.
Using the same proving key for different circuits.
What happens: Proofs from one circuit might be valid for another, breaking security.
Fix: Each circuit requires its own setup. Never reuse keys.
Public inputs ordered differently between prover and verifier.
What happens: Verification succeeds for wrong values. Proofs prove wrong things.
Fix: Strict public input ordering. Test prover/verifier integration.
Proofs that can be replayed to repeat an action.
What happens: Same proof used multiple times. Actions repeated unintentionally.
Fix: Include nonces, timestamps, or nullifiers in public inputs.
Application Integration Mistakes
Errors connecting ZK proofs to applications.
Proof proves something but does not specify what action it authorizes.
What happens: Proof generated for one action used to authorize another.
Fix: Include action identifier in public inputs.
Accepting proofs without recording their nullifiers.
What happens: Same proof used repeatedly. Double-spending.
Fix: Store all spent nullifiers. Check before accepting proofs.
Accepting proofs from clients without validating the public inputs make sense.
What happens: Malicious clients submit proofs with manipulated public inputs.
Fix: Validate public inputs server-side independent of the proof.
Performance and Implementation Mistakes
Errors causing inefficiency or bugs in production.
Using multiplications in constraints where additions suffice.
What happens: Proving time increases significantly. Higher costs.
Fix: Optimize constraint degree. Use linear constraints when possible.
Witness computation without time limits can hang indefinitely.
What happens: Malicious inputs cause denial of service. Provers hang.
Fix: Timeout on witness generation. Validate input complexity.
Mixing big and little endian in bit operations.
What happens: Values computed incorrectly. Proofs verify wrong data.
Fix: Standardize endianness across all operations. Document clearly.
Circuits that allocate based on input size without limits.
What happens: Large inputs crash prover. Denial of service.
Fix: Fixed maximum input sizes. Validate before circuit execution.
Leaving console logs or debug constraints in deployed circuits.
What happens: Information leakage. Unnecessary constraint overhead.
Fix: Strip debug code for production builds. Code review.
Test private keys or constants left in production code.
What happens: Anyone can use test keys to forge proofs.
Fix: Separate test and production configurations. CI/CD checks.
Testing and Auditing Mistakes
Errors in verification and quality assurance.
Testing only valid proofs without testing rejection of invalid ones.
What happens: Invalid proofs might be accepted. Soundness broken.
Fix: Test malicious inputs. Fuzz testing. Adversarial tests.
Relying solely on manual review for critical circuits.
What happens: Subtle constraint bugs missed by human reviewers.
Fix: Use formal verification tools like ecne, Circomspect.
Not testing boundary values and special cases.
What happens: Edge case inputs break circuit logic.
Fix: Test min/max values. Zero values. Field boundary values.
Assuming circuit behaves identically across Groth16, PLONK, etc.
What happens: Bugs appear only in specific proof system deployment.
Fix: Test with target proof system. Understand system differences.
Dismissing Circom or other compiler warnings as noise.
What happens: Warnings often indicate real constraint issues.
Fix: Zero-warning policy. Investigate all warnings.