Circuit 2: Fairness Audit
The fairness audit circuit proves that a batch of inference queries satisfies the fairness threshold.
Purpose
Section titled “Purpose”When an audit is requested, the provider must prove:
- Weights match certified hash - Same model as registered
- Samples are in batch - Each sampled query exists in the committed Merkle tree
- Samples are fair - Sampled predictions satisfy demographic parity
Constants
Section titled “Constants”global NUM_FEATURES: u32 = 14; // Input features per queryglobal SAMPLE_SIZE: u32 = 10; // Random samples per auditglobal TREE_DEPTH: u32 = 7; // Merkle tree depth (128 leaves max)Public Inputs
Section titled “Public Inputs”| Input | Type | Description |
|---|---|---|
weights_hash | Field | Certified model weights hash |
batch_merkle_root | Field | Committed batch root |
fairness_threshold | u32 | Max allowed disparity |
sample_indices | [u32; 10] | Random sample indices from contract |
Private Inputs
Section titled “Private Inputs”| Input | Type | Description |
|---|---|---|
weights | [Field; N] | Model weights |
samples | [Query; 10] | Sampled query records |
merkle_proofs | [MerkleProof; 10] | Proofs for each sample |
Leaf Hash Format
Section titled “Leaf Hash Format”Queries are hashed using Poseidon to create Merkle leaves:
// Build leaf data: [features[0..13], prediction, sensitiveAttr]let mut leaf_data: [Field; 16] = [0; 16];for i in 0..14 { leaf_data[i] = query.features[i];}leaf_data[14] = query.prediction; // Binary: 0 or 1leaf_data[15] = query.sensitive_attr;
// Hash in two chunkslet hash1 = poseidon8(leaf_data[0..8]);let hash2 = poseidon8(leaf_data[8..16]);let leaf_hash = poseidon2([hash1, hash2]);Verification Logic
Section titled “Verification Logic”fn main( weights_hash: pub Field, batch_merkle_root: pub Field, fairness_threshold: pub u32, sample_indices: pub [u32; SAMPLE_SIZE], weights: [Field; WEIGHT_COUNT], samples: [Query; SAMPLE_SIZE], merkle_proofs: [MerkleProof; SAMPLE_SIZE]) { // 1. Verify weights match certified hash assert(poseidon_hash(weights) == weights_hash);
// 2. Verify each sample is in batch for i in 0..SAMPLE_SIZE { let leaf = compute_leaf_hash(samples[i]); let proof = merkle_proofs[i]; assert(verify_merkle_proof(leaf, proof, batch_merkle_root)); }
// 3. Compute fairness on samples let mut group_0_pos = 0; let mut group_0_total = 0; let mut group_1_pos = 0; let mut group_1_total = 0;
for i in 0..SAMPLE_SIZE { if samples[i].sensitive_attr == 0 { group_0_total += 1; if samples[i].prediction == 1 { group_0_pos += 1; } } else { group_1_total += 1; if samples[i].prediction == 1 { group_1_pos += 1; } } }
// 4. Check threshold let rate_0 = group_0_pos * 100 / group_0_total; let rate_1 = group_1_pos * 100 / group_1_total; let disparity = if rate_0 > rate_1 { rate_0 - rate_1 } else { rate_1 - rate_0 };
assert(disparity <= fairness_threshold);}Audit Flow
Section titled “Audit Flow”- Challenger calls
requestAudit(batchId)with stake - Contract generates random
sampleIndicesusing block hash - Provider SDK detects
AuditRequestedevent - Provider generates ZK proof with sampled queries
- Proof sent to attestation service (TEE)
- Attestation service verifies and signs
- Provider submits attestation via
submitAuditProof() - Contract verifies signature and records result
SDK Integration
Section titled “SDK Integration”// Auto-handle audits in ProviderSDKprovider.watchAuditRequests(async (result) => { console.log(`Audit ${result.auditId}: ${result.passed ? 'PASSED' : 'FAILED'}`); console.log(`TX: ${result.txHash}`);});