Use when modifying deposit, withdrawal, settlement, or commitment code — audits off-chain hash functions against circuit constraints to prevent the
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."
This skill MUST run whenever you modify:
commitment, nullifierHash, secret, or nullifier valuesbot/orchestrator.mjs (deposit logic)bot/note-manager.mjs (key derivation)bot/proof-generator.mjs (proof inputs)matchmaker/proof-generator.mjs (settlement proofs)relayer/index.mjs or relayer/batch-relay.mjs (proof serialization)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.
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).
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
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.
./scripts/verify-circuit-compat.sh
This script checks SHA256 misuse, random secret generation, Poseidon import coverage, account ordering, and BN254 safety.
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
After the audit, report:
If FAIL, do NOT proceed with the commit until fixed.