name: zk-circuit-audit description: Use when modifying deposit, withdrawal, settlement, or commitment code — audits off-chain hash functions against circuit constraints to prevent the #1 ZK vulnerability class
ZK Circuit Audit
Verify that off-chain commitment/nullifier computations match the Circom circuit constraints exactly.
The #1 ZK vulnerability (0xPARC bug tracker): hash function mismatch between off-chain code and circuit verification. If the off-chain deposit uses SHA256 but the circuit uses Poseidon, every proof will fail silently.
Announce at start: "Running ZK circuit audit on the affected files."
When to Trigger
This skill MUST run whenever you modify:
- Any file that computes
commitment,nullifierHash,secret, ornullifiervalues bot/orchestrator.mjs(deposit logic)bot/note-manager.mjs(key derivation)bot/proof-generator.mjs(proof inputs)matchmaker/proof-generator.mjs(settlement proofs)relayer/index.mjsorrelayer/batch-relay.mjs(proof serialization)- Any new module that touches ZK-related data
The Audit Checklist
1. Hash Function Consistency
Read the circuit files to confirm the expected hash:
circuits/withdraw.circom — uses Poseidon(2) for commitment, Poseidon(1) for nullifierHash
circuits/match.circom — uses Poseidon(2) for deposit commitment, Poseidon(1) for nullifier, Poseidon(3) for order commitment
Then scan the modified files for:
| Pattern | Expected | Danger |
|---|---|---|
commitment = ... |
Poseidon(secret, nullifier) via hash2() |
SHA256, keccak, or any non-Poseidon hash |
nullifierHash = ... |
Poseidon(nullifier) via hash1() |
SHA256 or direct hashing |
orderCommitment = ... |
Poseidon(price, amount, depositCommitment) via hash3() |
Any non-Poseidon |
Action: Grep for createHash, SHA256, keccak near any commitment/nullifier computation. If found, it's almost certainly wrong.
2. BN254 Field Safety
Circuit signals are BN254 field elements (< ~2^254). Off-chain BigInts from 32-byte buffers can exceed the field.
Rule: Any 32-byte HMAC/hash output converted to BigInt for circuit input MUST be truncated to 31 bytes first.
// CORRECT: 31-byte truncation
const secretBig = BigInt("0x" + secret.subarray(0, 31).toString("hex"));
// WRONG: full 32 bytes can exceed field
const secretBig = BigInt("0x" + secret.toString("hex"));
Action: Search for BigInt("0x" conversions in modified files. Verify they use .subarray(0, 31).
3. Proof Serialization
The on-chain Groth16 verifier expects a specific 256-byte layout with y-negation:
[0:32] pi_a.x (BE)
[32:64] BN254 - pi_a.y (y-negation)
[64:96] pi_b[0][1] (BE, x-swapped)
[96:128] pi_b[0][0] (BE)
[128:160] pi_b[1][1] (BE, y-swapped)
[160:192] pi_b[1][0] (BE)
[192:224] pi_c.x (BE)
[224:256] pi_c.y (BE)
Action: If proof serialization code is modified, verify the y-negation uses the correct BN254 prime: 21888242871839275222246405745257275088696311157297823662689037894645226208583n
4. Commitment Encoding for On-Chain
The on-chain deposit instruction expects the commitment as a 32-byte little-endian buffer:
// CORRECT: LE encoding for on-chain
function bigintToLE(bn) {
const buf = Buffer.alloc(32);
let tmp = bn;
for (let i = 0; i < 32; i++) {
buf[i] = Number(tmp & 0xffn);
tmp >>= 8n;
}
return buf;
}
Action: Verify deposit data uses LE encoding, NOT BE.
5. Run the Automated Check
./scripts/verify-circuit-compat.sh
This script checks SHA256 misuse, random secret generation, Poseidon import coverage, account ordering, and BN254 safety.
Quick Bash Verification
If you want to do a quick manual check:
# Find SHA256 near commitments (should return nothing)
grep -rn 'createHash.*sha256' bot/ matchmaker/ relayer/ --include='*.mjs' | grep -i 'commit\|nullif'
# Find randomBytes for secrets (should only be in non-critical paths)
grep -rn 'randomBytes' bot/ --include='*.mjs' | grep -i 'secret\|nullif'
# Verify Poseidon is imported in all critical modules
for f in bot/orchestrator.mjs bot/note-manager.mjs bot/proof-generator.mjs; do
grep -l 'poseidon\|hash2\|hash1' "$f" && echo " ✓ $f" || echo " ✗ $f MISSING POSEIDON"
done
Output
After the audit, report:
- PASS — all hash functions match circuits, field safety verified
- WARN — non-critical findings (e.g., SHA256 used for non-ZK purposes like discriminators)
- FAIL — hash mismatch found, field overflow possible, or serialization error
If FAIL, do NOT proceed with the commit until fixed.