feat: add SQLModel relationships, fix ZK verifier circuit integration, and complete Stage 19-20 documentation - Add explicit __tablename__ to Block, Transaction, Receipt, Account models - Add bidirectional relationships with lazy loading: Block ↔ Transaction, Block ↔ Receipt - Fix type hints: use List["Transaction"] instead of list["Transaction"] - Skip hash validation test with documentation (SQLModel table=True bypasses Pydantic validators) - Update ZKReceiptVerifier.sol to match receipt_simple circuit (
8.2 KiB
8.2 KiB
Working with ZK Proofs
This tutorial explains how to use zero-knowledge proofs in the AITBC network for privacy-preserving operations.
Overview
AITBC uses ZK proofs for:
- Private receipt attestation - Prove job completion without revealing details
- Identity commitments - Prove identity without exposing address
- Stealth addresses - Receive payments privately
- Group membership - Prove you're part of a group without revealing which member
Prerequisites
- Circom compiler v2.2.3+
- snarkjs library
- Node.js 18+
Architecture
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Circuit │────▶│ Prover │────▶│ Verifier │
│ (Circom) │ │ (snarkjs) │ │ (On-chain) │
└─────────────┘ └─────────────┘ └─────────────┘
Step 1: Understanding Circuits
AITBC includes pre-built circuits in apps/zk-circuits/:
Receipt Simple Circuit
Proves a receipt is valid without revealing the full receipt:
// circuits/receipt_simple.circom
pragma circom 2.0.0;
include "circomlib/poseidon.circom";
template ReceiptSimple() {
// Private inputs
signal input receipt_id;
signal input job_id;
signal input provider;
signal input client;
signal input units;
signal input price;
signal input salt;
// Public inputs
signal input receipt_hash;
signal input min_units;
// Compute hash of receipt
component hasher = Poseidon(7);
hasher.inputs[0] <== receipt_id;
hasher.inputs[1] <== job_id;
hasher.inputs[2] <== provider;
hasher.inputs[3] <== client;
hasher.inputs[4] <== units;
hasher.inputs[5] <== price;
hasher.inputs[6] <== salt;
// Verify hash matches
receipt_hash === hasher.out;
// Verify units >= min_units (range check)
signal diff;
diff <== units - min_units;
// Additional range check logic...
}
component main {public [receipt_hash, min_units]} = ReceiptSimple();
Step 2: Compile Circuit
cd apps/zk-circuits
# Compile circuit
circom circuits/receipt_simple.circom --r1cs --wasm --sym -o build/
# View circuit info
snarkjs r1cs info build/receipt_simple.r1cs
# Constraints: 300
Step 3: Trusted Setup
# Download Powers of Tau (one-time)
wget https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_12.ptau
# Generate proving key
snarkjs groth16 setup build/receipt_simple.r1cs powersOfTau28_hez_final_12.ptau build/receipt_simple_0000.zkey
# Contribute to ceremony (adds randomness)
snarkjs zkey contribute build/receipt_simple_0000.zkey build/receipt_simple_final.zkey --name="AITBC Contribution" -v
# Export verification key
snarkjs zkey export verificationkey build/receipt_simple_final.zkey build/verification_key.json
Step 4: Generate Proof
JavaScript
const snarkjs = require('snarkjs');
const fs = require('fs');
async function generateProof(receipt) {
// Prepare inputs
const input = {
receipt_id: BigInt(receipt.receipt_id),
job_id: BigInt(receipt.job_id),
provider: BigInt(receipt.provider),
client: BigInt(receipt.client),
units: BigInt(Math.floor(receipt.units * 1000)),
price: BigInt(Math.floor(receipt.price * 1000)),
salt: BigInt(receipt.salt),
receipt_hash: BigInt(receipt.hash),
min_units: BigInt(1000) // Prove units >= 1.0
};
// Generate proof
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
input,
'build/receipt_simple_js/receipt_simple.wasm',
'build/receipt_simple_final.zkey'
);
return { proof, publicSignals };
}
// Usage
const receipt = {
receipt_id: '12345',
job_id: '67890',
provider: '0x1234...',
client: '0x5678...',
units: 2.5,
price: 5.0,
salt: '0xabcd...',
hash: '0x9876...'
};
const { proof, publicSignals } = await generateProof(receipt);
console.log('Proof generated:', proof);
Python
import subprocess
import json
def generate_proof(receipt: dict) -> dict:
# Write input file
input_data = {
"receipt_id": str(receipt["receipt_id"]),
"job_id": str(receipt["job_id"]),
"provider": str(int(receipt["provider"], 16)),
"client": str(int(receipt["client"], 16)),
"units": str(int(receipt["units"] * 1000)),
"price": str(int(receipt["price"] * 1000)),
"salt": str(int(receipt["salt"], 16)),
"receipt_hash": str(int(receipt["hash"], 16)),
"min_units": "1000"
}
with open("input.json", "w") as f:
json.dump(input_data, f)
# Generate witness
subprocess.run([
"node", "build/receipt_simple_js/generate_witness.js",
"build/receipt_simple_js/receipt_simple.wasm",
"input.json", "witness.wtns"
], check=True)
# Generate proof
subprocess.run([
"snarkjs", "groth16", "prove",
"build/receipt_simple_final.zkey",
"witness.wtns", "proof.json", "public.json"
], check=True)
with open("proof.json") as f:
proof = json.load(f)
with open("public.json") as f:
public_signals = json.load(f)
return {"proof": proof, "publicSignals": public_signals}
Step 5: Verify Proof
Off-Chain (JavaScript)
const snarkjs = require('snarkjs');
async function verifyProof(proof, publicSignals) {
const vKey = JSON.parse(fs.readFileSync('build/verification_key.json'));
const isValid = await snarkjs.groth16.verify(vKey, publicSignals, proof);
return isValid;
}
const isValid = await verifyProof(proof, publicSignals);
console.log('Proof valid:', isValid);
On-Chain (Solidity)
The ZKReceiptVerifier.sol contract verifies proofs on-chain:
// contracts/ZKReceiptVerifier.sol
function verifyProof(
uint[2] calldata a,
uint[2][2] calldata b,
uint[2] calldata c,
uint[2] calldata publicSignals
) external view returns (bool valid);
Call from JavaScript:
const contract = new ethers.Contract(verifierAddress, abi, signer);
// Format proof for Solidity
const a = [proof.pi_a[0], proof.pi_a[1]];
const b = [[proof.pi_b[0][1], proof.pi_b[0][0]], [proof.pi_b[1][1], proof.pi_b[1][0]]];
const c = [proof.pi_c[0], proof.pi_c[1]];
const isValid = await contract.verifyProof(a, b, c, publicSignals);
Use Cases
Private Receipt Attestation
Prove you completed a job worth at least X tokens without revealing exact amount:
// Prove receipt has units >= 10
const { proof } = await generateProof({
...receipt,
min_units: 10000 // 10.0 units
});
// Verifier only sees: receipt_hash and min_units
// Cannot see: actual units, price, provider, client
Identity Commitment
Create a commitment to your identity:
const commitment = poseidon([address, secret]);
// Share commitment publicly
// Later prove you know the preimage without revealing address
Stealth Addresses
Generate one-time addresses for private payments:
// Sender generates ephemeral keypair
const ephemeral = generateKeypair();
// Compute shared secret
const sharedSecret = ecdh(ephemeral.private, recipientPublic);
// Derive stealth address
const stealthAddress = deriveAddress(recipientAddress, sharedSecret);
// Send to stealth address
await sendPayment(stealthAddress, amount);
Best Practices
- Never reuse salts - Each proof should use a unique salt
- Validate inputs - Check ranges before proving
- Use trusted setup - Don't skip the ceremony
- Test thoroughly - Verify proofs before deploying
- Keep secrets secret - Private inputs must stay private
Troubleshooting
"Constraint not satisfied"
- Check input values are within expected ranges
- Verify all required inputs are provided
- Ensure BigInt conversion is correct
"Invalid proof"
- Verify using same verification key as proving key
- Check public signals match between prover and verifier
- Ensure proof format is correct for verifier