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 (
316 lines
8.2 KiB
Markdown
316 lines
8.2 KiB
Markdown
# 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:
|
|
|
|
```circom
|
|
// 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```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
|
|
|
|
```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)
|
|
|
|
```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:
|
|
|
|
```solidity
|
|
// 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:
|
|
|
|
```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:
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
1. **Never reuse salts** - Each proof should use a unique salt
|
|
2. **Validate inputs** - Check ranges before proving
|
|
3. **Use trusted setup** - Don't skip the ceremony
|
|
4. **Test thoroughly** - Verify proofs before deploying
|
|
5. **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
|
|
|
|
## Next Steps
|
|
|
|
- [ZK Applications Reference](../../reference/components/zk-applications.md)
|
|
- [ZK Receipt Attestation](../../reference/zk-receipt-attestation.md)
|
|
- [SDK Examples](sdk-examples.md)
|