ZK-proven photo authenticity — prove it's real, not AI, without revealing who you are.
ProofFrame is a zero-knowledge content authenticity system that proves photos came from a real camera — not AI — without revealing the photographer's identity.
The problem: AI-generated deepfakes are flooding the internet (1,500% growth 2023-2025). The existing solution, C2PA, proves photos are real but exposes the photographer's GPS, camera serial, and identity. Journalists, whistleblowers, and war correspondents can't use it safely.
ProofFrame solves both problems:
The system uses World ID for anti-Sybil (per-image nullifier — same human can attest different images but not the same one twice), on-chain ENS subdomains with text records (pixelHash, IPFS CID, transforms, metadata), Ledger Clear Signing (ERC-7730) displaying attestation fields on the hardware device, and IPFS for permanent image storage with content hash stored in the smart contract.
Live demo: https://proofframe.fly.dev
ProofFrame is built across 5 layers:
ZK Core: RISC Zero zkVM v3.0 compiles a Rust guest program to riscv32im. Inside the VM: ECDSA signature verification (k256 with bigint precompile), Merkle proof against device registry, PNG decode via the image crate (integer-only zlib, no float), pixel transforms (crop/grayscale/brightness), SHA-256 pixel hashing (accelerated precompile), and selective EXIF disclosure. The image crate's decode-to-pixels pipeline is the metadata firewall — it structurally cannot preserve EXIF/XMP/IPTC/C2PA.
Smart Contract: ImageAttestor.sol on Sepolia verifies the ZK proof, verifies World ID (per-image nullifier scoping via hash(appId, "attest_" + pixelHash)), stores the attestation with IPFS CID on-chain, and atomically creates an ENS subdomain via NameWrapper.setSubnodeRecord() with text records (pixelHash, fileHash, merkleRoot, ipfsCid, url, avatar, contenthash) set via PublicResolver.setText(). 14 Foundry tests. Zero msg.sender checks — fully permissionless.
Frontend: Next.js 14 with wagmi + ConnectKit for optional Ledger signing. The attest flow: upload image → /api/prove spawns the Rust host binary (RISC0_DEV_MODE=1) → World ID IDKit scan → /api/upload pins to IPFS via Infura → EIP-712 typed data signing on Ledger (shows all attestation fields) → /api/relay submits from relayer wallet.
ENS Integration: On-chain subdomains created inside attestImage() — not off-chain. The contract calls NameWrapper.setSubnodeRecord() then PublicResolver.setText() for 10+ text records including contenthash. Parent domain owner grants contract operator approval via setApprovalForAll(). Labels are first 16 hex chars of pixel hash (lowercased for ENS normalization).
Deployment: Fly.io with 3-stage Docker build (Rust binary + Next.js + node:20-slim runtime). Auto-deploy via GitHub Actions on merge to main. Dev mode proof generation (~3s) with r0vm included in runtime image.
Hacky parts: Three things are mocked for the hackathon demo: (1) Camera signature — we generate a fresh secp256k1 key at runtime instead of using a real camera secure element. The ZK guest verifies it identically; only the key source changes in production. (2) World ID — MockWorldID on Sepolia because real World App roots don't sync to testnet. (3) RISC Zero dev mode — proofs are generated with RISC0_DEV_MODE=1 (~3s) accepted by MockVerifier. Real Groth16 proofs require GPU. The image crate compiled for riscv32im with PNG-only features to avoid JPEG's float-heavy DCT. All mocks are swappable without changing the architecture — same interfaces, same data flow, just different trust anchors.
Hacky parts: Mock World ID with random nullifier generation to avoid DuplicateNullifier on Sepolia (real World App roots don't sync to testnet). The image crate compiled for riscv32im with PNG-only features to avoid JPEG's float-heavy DCT.

