Your Compact circuit compiled fine. Your tests pass. But the proof server rejects your transaction.
compact-zkir-lint tells you why before your users do.
npx compact-zkir-lint -r contracts/src/artifacts/ addLiquidity (v2, k=13): 1 error(s)
instructions: 343 inputs: 5 constrain_bits: 12 cond_select: 8
guarded regions: 3 (max depth 2) proof payload: ~384KB
ERROR [DIV-001] inst 128: constrain_bits(bits=64) on arithmetic in conditional branch (guard=824)
1 error(s) | 4/11 circuits affected
# Scan a single circuit
npx compact-zkir-lint circuit.zkir
# Scan all circuits in your compiled artifacts
npx compact-zkir-lint -r contracts/src/artifacts/
# Profile proving time across environments
npx compact-zkir-lint --profile -r contracts/src/artifacts/
# CI-friendly: SARIF output, non-zero exit on errors
npx compact-zkir-lint -r contracts/src/artifacts/ --format sarif > results.sarifNo dependencies on Midnight packages. Reads the .zkir JSON files that the compiler already produces. Works offline.
Built-in timing estimates are rough. Measure real proving times on your hardware:
# Start a proof server
docker run -d -p 6300:6300 \
-v $HOME/.cache/midnight/zk-params:/root/.cache/midnight/zk-params \
ghcr.io/midnight-ntwrk/proof-server:8.0.3
# Generate fixtures and run benchmarks (first time: npm run benchmark:setup)
npm run benchmark -- -o profile.json
# Lint with real timing data
npx compact-zkir-lint --profile --profile-config profile.json -r contracts/src/artifacts/Measures k=10-16 directly against the proof server, extrapolates k=17-25 from the observed doubling rate. See the circuit profiling guide for output format, payload size limits, and configuration.
In ZK circuits, both branches of an if/else execute unconditionally — only the result is selected via cond_select. Constraints inside dead branches fire on invalid intermediate values, causing proof failures that JS testing can't catch.
compact-zkir-lint detects 16 patterns across four categories:
| Category | Rules | Severity |
|---|---|---|
| Divergence (DIV-*) | DIV-001 through DIV-005 | error / warn |
| Runtime (RT-*) | RT-001 through RT-004 | warn / info |
| Statistics (STATS-*) | STATS-001, STATS-002 | info |
| Performance (PERF-*) | PERF-001 through PERF-006 | error / warn / info |
See the full rules reference for details, examples, and fix guidance.
- Parses compiled
.zkirJSON files (v2 format, zero dependencies) - Builds a data-flow graph: which instruction produces which memory variable
- Tracks guard propagation: which variables are inside conditional branches
- Memoized zero-analysis: determines if dead-branch values default to zero (safe) vs non-zero (dangerous)
- Flags constraint instructions operating on branch-local values before they reach a
cond_selectmerge
- Rules reference — all 17 rules with examples and fix guidance
- Circuit profiling — estimate proving time, benchmark your proof server, payload sizes
- Branchless patterns — how to restructure code to avoid divergence
- CI integration — SARIF output, GitHub Actions, exit codes, profiling in CI
- Differential testing — JS vs ZKIR fuzz testing for deeper analysis
- Compatibility — version tracking and ZKIR v3 roadmap
- Benchmark tool — fixture generation, binary format, SDK packages
- OpenZeppelin for the Uint128.subU128 workaround that informed the DIV-001 fix patterns
- The LunarSwap team for the original bug report that led to this tool
Apache-2.0