-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathZKProofs.sol
More file actions
584 lines (489 loc) · 23.1 KB
/
Copy pathZKProofs.sol
File metadata and controls
584 lines (489 loc) · 23.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
// =============================================================================
// ZK PROOFS & ZK COPROCESSORS IN SMART CONTRACTS
// =============================================================================
// Zero-Knowledge Proofs (ZKPs) allow one party to prove a statement is true
// without revealing any information beyond the truth of the statement.
//
// For smart contracts, ZKPs enable:
// 1. PRIVACY — prove you know a secret without revealing it
// 2. SCALABILITY — prove off-chain computation was done correctly (ZK rollups)
// 3. ZK COPROCESSORS — prove facts about historical blockchain state off-chain
// 4. IDENTITY — prove you meet criteria (age, credit score) without revealing data
// 5. COMPLIANCE — prove regulatory compliance without exposing business data
//
// Types of ZK proof systems (2025-2026):
// - Groth16 : smallest proof, fastest verify, requires trusted setup per circuit
// - PLONK : universal trusted setup (e.g., Aztec's ceremony), slightly larger proof
// - STARKs : no trusted setup, quantum-resistant, larger proof, faster prove
// - Halo2 : no trusted setup, recursive proofs, used by Zcash & Scroll
//
// Key frameworks:
// - circom + snarkjs : circuit DSL + JS prover/verifier generation
// - gnark (Go) : fast Groth16/PLONK prover
// - SP1 (Succinct) : ZK proof of any Rust program (RISC-V)
// - RISC Zero : similar to SP1, with Bonsai proving service
// - Noir (Aztec) : high-level ZK DSL, Barretenberg backend
// - Halo2 (ZCash/PSE) : research-grade, used by Scroll/Taiko
//
// ZK Coprocessors (compute over historical chain data):
// - Axiom : prove facts about historical Ethereum state (via storage proofs)
// - Lagrange : coprocessor network with aggregation
// - Brevis : ZK coprocessor with Solidity SDK
// - Herodotus : storage proofs across chains
// =============================================================================
// =============================================================================
// SECTION 1 — GROTH16 VERIFIER INTERFACE
// =============================================================================
// Groth16 is the most widely deployed ZK proof system on Ethereum.
// The verifier contract is generated by circom/snarkjs and deployed once per circuit.
// Your application contract calls it to verify a proof.
// Standard Groth16 verifier interface (generated by snarkjs)
interface IGroth16Verifier {
function verifyProof(
uint256[2] calldata pA, // proof.pi_a
uint256[2][2] calldata pB, // proof.pi_b
uint256[2] calldata pC, // proof.pi_c
uint256[] calldata pubSignals // public inputs/outputs
) external view returns (bool);
}
// =============================================================================
// SECTION 2 — PLONK/UltraPLONK VERIFIER INTERFACE
// =============================================================================
// PLONK verifiers use a single trusted setup (powers-of-tau ceremony).
// The interface differs slightly from Groth16.
interface IPlonkVerifier {
function verify(
bytes calldata proof,
uint256[] calldata publicInputs
) external view returns (bool);
}
// =============================================================================
// SECTION 3 — GENERIC ZK VERIFIER WRAPPER
// =============================================================================
// A unified interface your contracts use, abstracting the proof system underneath.
// This lets you swap Groth16 → PLONK → STARK without changing your business logic.
interface IZKVerifier {
function verify(bytes calldata proof, bytes calldata publicInputs) external view returns (bool);
}
// Adapter: wraps a Groth16 verifier under the generic IZKVerifier interface
contract Groth16VerifierAdapter is IZKVerifier {
IGroth16Verifier public immutable verifier;
uint256 public immutable publicInputCount;
constructor(address _verifier, uint256 _publicInputCount) {
verifier = IGroth16Verifier(_verifier);
publicInputCount = _publicInputCount;
}
// proof encoding: abi.encode(pA[2], pB[2][2], pC[2])
// publicInputs encoding: abi.encode(uint256[N])
function verify(bytes calldata proof, bytes calldata publicInputs)
external
view
override
returns (bool)
{
(
uint256[2] memory pA,
uint256[2][2] memory pB,
uint256[2] memory pC
) = abi.decode(proof, (uint256[2], uint256[2][2], uint256[2]));
uint256[] memory signals = abi.decode(publicInputs, (uint256[]));
require(signals.length == publicInputCount, "Wrong public input count");
return verifier.verifyProof(pA, pB, pC, signals);
}
}
// =============================================================================
// SECTION 4 — ZK PROOF OF MEMBERSHIP (WHITELIST WITHOUT REVEALING WHO)
// =============================================================================
// Classic use case: prove you're in a set of N addresses without revealing which one.
// Circuit: "I know a secret s such that MerkleProof(s, root) is valid"
// On-chain: check the proof, use the nullifier to prevent double-use.
contract ZKWhitelist {
IZKVerifier public immutable verifier;
// Merkle root of whitelisted addresses (built off-chain by the admin)
bytes32 public merkleRoot;
// Nullifiers prevent the same membership proof from being used twice
mapping(bytes32 => bool) public usedNullifiers;
address public owner;
event ProofVerified(bytes32 nullifier, address indexed user);
event MerkleRootUpdated(bytes32 newRoot);
error InvalidProof();
error NullifierAlreadyUsed();
constructor(address _verifier) {
verifier = IZKVerifier(_verifier);
owner = msg.sender;
}
function updateMerkleRoot(bytes32 newRoot) external {
require(msg.sender == owner, "Not owner");
merkleRoot = newRoot;
emit MerkleRootUpdated(newRoot);
}
// User provides a ZK proof that their address is in the whitelist.
// The proof contains:
// Public inputs: [merkleRoot, nullifier]
// Private inputs: [secretKey, merklePathSiblings...]
// The circuit proves: hash(secretKey) = leaf ∈ tree(merkleRoot)
function proveAndAct(
bytes calldata proof,
bytes32 nullifier,
bytes32 claimedRoot
) external {
// Verify the claimed root matches current root
require(claimedRoot == merkleRoot, "Root mismatch");
// Prevent double-use of same membership proof
if (usedNullifiers[nullifier]) revert NullifierAlreadyUsed();
// Public inputs: [merkleRoot (as uint256), nullifier (as uint256)]
bytes memory publicInputs = abi.encode(
uint256(claimedRoot),
uint256(nullifier)
);
if (!verifier.verify(proof, publicInputs)) revert InvalidProof();
usedNullifiers[nullifier] = true;
emit ProofVerified(nullifier, msg.sender);
// Proceed with the action (transfer, access, etc.)
_grantAccess(msg.sender);
}
function _grantAccess(address user) internal virtual {
// Override in subclass to define what "access" means
}
}
// =============================================================================
// SECTION 5 — ZK AGE VERIFICATION (PRIVACY-PRESERVING KYC)
// =============================================================================
// Prove you're over 18 without revealing your birthdate or identity.
// Circuit: "I know a signed credential that certifies I am over 18"
// Public inputs: [issuerPublicKey, currentTimestamp, nullifier]
contract ZKAgeVerification {
IZKVerifier public immutable verifier;
// Trusted credential issuers (e.g., government ID provider)
mapping(address => bool) public trustedIssuers;
// Per-user nullifiers (user can only verify once per issuer claim)
mapping(bytes32 => bool) public verifiedNullifiers;
// Users who have passed age verification
mapping(address => bool) public ageVerified;
address public owner;
event AgeVerified(address indexed user, bytes32 nullifier);
error InvalidProof();
error NullifierUsed();
error UntrustedIssuer();
constructor(address _verifier) {
verifier = IZKVerifier(_verifier);
owner = msg.sender;
}
function trustIssuer(address issuer) external {
require(msg.sender == owner, "Not owner");
trustedIssuers[issuer] = true;
}
// Prove age ≥ 18 using a ZK proof of a signed credential.
// The circuit verifies:
// 1. credential.birthdate signed by trustedIssuer
// 2. currentDate - credential.birthdate >= 18 years
// 3. nullifier = hash(userSecret, credentialId) — user-specific, non-replayable
// None of the private data (birthdate, credential, userSecret) is revealed.
function verifyAge(
bytes calldata proof,
address issuer,
bytes32 nullifier
) external {
if (!trustedIssuers[issuer]) revert UntrustedIssuer();
if (verifiedNullifiers[nullifier]) revert NullifierUsed();
// Public inputs: [issuerAddress, currentTimestamp, nullifier, minAge=18]
bytes memory publicInputs = abi.encode(
uint256(uint160(issuer)),
block.timestamp,
uint256(nullifier),
uint256(18)
);
if (!verifier.verify(proof, publicInputs)) revert InvalidProof();
verifiedNullifiers[nullifier] = true;
ageVerified[msg.sender] = true;
emit AgeVerified(msg.sender, nullifier);
}
}
// =============================================================================
// SECTION 6 — ZK COPROCESSOR: AXIOM-STYLE HISTORICAL STATE PROOFS
// =============================================================================
// Axiom lets you prove facts about any historical Ethereum block/state/tx.
// Example: "this address held ≥ 1000 tokens at block N" — without re-syncing
// the entire chain. The proof is generated off-chain using Axiom SDK,
// then verified on-chain via their AxiomV2Query contract.
// Simplified Axiom callback interface
interface IAxiomV2Client {
function axiomV2Callback(
uint64 sourceChainId,
address callerAddress,
bytes32 querySchema,
uint256 queryId,
bytes32[] calldata axiomResults, // the proven values
bytes calldata extraData
) external;
}
// Query schema encoding for Axiom circuits
// Schema = keccak256(circuitId, circuitVersion)
contract AxiomHistoricalProver is IAxiomV2Client {
address public immutable axiomV2QueryAddress;
bytes32 public immutable querySchema; // identifies our specific circuit
// Proven historical balances: user => blockNumber => balance
mapping(address => mapping(uint256 => uint256)) public provenBalance;
event HistoricalBalanceProven(
address indexed user,
uint256 blockNumber,
uint256 balance
);
error NotAxiom();
error WrongSchema();
constructor(address _axiomQuery, bytes32 _schema) {
axiomV2QueryAddress = _axiomQuery;
querySchema = _schema;
}
// Called by Axiom after the ZK proof is verified on their contract.
// axiomResults contains the proven output values from the circuit.
function axiomV2Callback(
uint64 sourceChainId,
address callerAddress,
bytes32 _querySchema,
uint256 queryId,
bytes32[] calldata axiomResults,
bytes calldata extraData
) external override {
// Only Axiom's contract can call this
if (msg.sender != axiomV2QueryAddress) revert NotAxiom();
if (_querySchema != querySchema) revert WrongSchema();
// Decode results from the circuit's public outputs
// Our circuit outputs: [userAddress, blockNumber, tokenBalance]
address user = address(uint160(uint256(axiomResults[0])));
uint256 blockNumber = uint256(axiomResults[1]);
uint256 balance = uint256(axiomResults[2]);
provenBalance[user][blockNumber] = balance;
emit HistoricalBalanceProven(user, blockNumber, balance);
}
// Application logic: gate access based on proven historical balance
function claimRewardIfEarlyHolder(uint256 blockNumber) external {
uint256 balance = provenBalance[msg.sender][blockNumber];
require(balance >= 1000e18, "Was not an early holder with enough tokens");
// Distribute reward...
}
}
// =============================================================================
// SECTION 7 — RISC ZERO / SP1 STYLE: PROVE ARBITRARY COMPUTATION
// =============================================================================
// SP1 and RISC Zero can generate ZK proofs of arbitrary Rust/C programs.
// Example: prove that a complex sorting or ML inference was done correctly.
// The verifier on-chain is a universal verifier — one contract for all programs.
// SP1 verifier interface (simplified)
interface ISP1Verifier {
function verifyProof(
bytes32 programVKey, // identifies the specific Rust program
bytes calldata publicValues,
bytes calldata proofBytes
) external view;
}
// RISC Zero verifier interface (simplified)
interface IRiscZeroVerifier {
function verify(
bytes calldata seal, // the proof
bytes32 imageId, // identifies the guest program
bytes32 journalDigest // hash of public outputs
) external view;
}
// Example: use SP1 to prove off-chain sorting of 10,000 elements
// then use the proven sorted output on-chain
contract SP1SortingProver {
ISP1Verifier public immutable sp1Verifier;
bytes32 public immutable SORT_PROGRAM_VKEY; // hash of the Rust sort binary
// Proven sorted arrays stored on-chain
mapping(bytes32 => uint256[]) public provenSortedArrays;
event SortProven(bytes32 indexed inputHash, uint256[] sortedArray);
constructor(address _verifier, bytes32 _vkey) {
sp1Verifier = ISP1Verifier(_verifier);
SORT_PROGRAM_VKEY = _vkey;
}
// Submit a ZK proof that a given array was correctly sorted.
// publicValues = abi.encode(inputHash, sortedArray)
function submitSortProof(
bytes calldata publicValues,
bytes calldata proof
) external {
// SP1 verifies the proof against our sort program's vkey
sp1Verifier.verifyProof(SORT_PROGRAM_VKEY, publicValues, proof);
// Decode the proven public values
(bytes32 inputHash, uint256[] memory sortedArray) =
abi.decode(publicValues, (bytes32, uint256[]));
provenSortedArrays[inputHash] = sortedArray;
emit SortProven(inputHash, sortedArray);
}
}
// =============================================================================
// SECTION 8 — NULLIFIER PATTERN (PREVENT DOUBLE-SPEND/DOUBLE-USE)
// =============================================================================
// Nullifiers are the key primitive for ZK privacy systems.
// A nullifier is a deterministic value derived from a secret, used to
// mark a "note" or "credential" as spent without revealing which one.
//
// Used in: Tornado Cash, Zcash, Aztec, ZK airdrop systems
contract NullifierRegistry {
// Global nullifier set — once spent, cannot be spent again
mapping(bytes32 => bool) public nullifiers;
// Each ZK application registers a "topic" to namespace its nullifiers
mapping(address => bool) public registeredApps;
address public owner;
event NullifierSpent(bytes32 indexed nullifier, address indexed app);
error NullifierAlreadySpent();
error AppNotRegistered();
constructor() {
owner = msg.sender;
}
function registerApp(address app) external {
require(msg.sender == owner, "Not owner");
registeredApps[app] = true;
}
// ZK application marks a nullifier as spent after verifying proof
function spendNullifier(bytes32 nullifier) external {
if (!registeredApps[msg.sender]) revert AppNotRegistered();
if (nullifiers[nullifier]) revert NullifierAlreadySpent();
nullifiers[nullifier] = true;
emit NullifierSpent(nullifier, msg.sender);
}
function isSpent(bytes32 nullifier) external view returns (bool) {
return nullifiers[nullifier];
}
}
// =============================================================================
// SECTION 9 — ZK AIRDROP (PROVE ELIGIBILITY WITHOUT REVEALING ADDRESS)
// =============================================================================
// Airdrop where: merkle tree of eligible addresses is public,
// but each claim doesn't reveal WHICH address is claiming.
// The ZK proof shows: "I know an address in the tree and the secret for it."
// The nullifier = hash(secret) prevents double-claiming.
contract ZKAirdrop {
IZKVerifier public immutable verifier;
bytes32 public immutable merkleRoot;
NullifierRegistry public immutable nullifierRegistry;
address public immutable rewardToken;
uint256 public constant REWARD_AMOUNT = 1000e18;
mapping(address => bool) public claimed; // by receiving address
event Claimed(address indexed to, bytes32 nullifier);
error InvalidProof();
error AlreadyClaimed();
constructor(
address _verifier,
bytes32 _merkleRoot,
address _nullifierRegistry,
address _rewardToken
) {
verifier = IZKVerifier(_verifier);
merkleRoot = _merkleRoot;
nullifierRegistry = NullifierRegistry(_nullifierRegistry);
rewardToken = _rewardToken;
}
// Claim airdrop with ZK proof.
// Proof shows: I own a secret s such that leaf(s) is in tree(merkleRoot).
// Nullifier = hash(s, "airdrop_v1") — unique per claim, hides identity.
// The caller can be ANY address — no link between claimer and eligible address.
function claim(
address recipient,
bytes32 nullifier,
bytes calldata proof
) external {
// Public inputs: [merkleRoot, nullifier]
bytes memory publicInputs = abi.encode(uint256(merkleRoot), uint256(nullifier));
if (!verifier.verify(proof, publicInputs)) revert InvalidProof();
// Spend nullifier — prevents double claiming
nullifierRegistry.spendNullifier(nullifier);
// Send reward to recipient (could be a fresh wallet for privacy)
IERC20(rewardToken).transfer(recipient, REWARD_AMOUNT);
emit Claimed(recipient, nullifier);
}
}
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}
// =============================================================================
// SECTION 10 — PLONKY2/RECURSIVE PROOFS PATTERN
// =============================================================================
// Recursive proofs: one proof aggregates N proofs into 1.
// Used by: Polygon zkEVM, Scroll, zkSync Era, Aztec.
// On-chain verifier only needs to verify the single aggregated proof.
interface IRecursiveVerifier {
// Verify an aggregated proof that covers N individual proofs
function verifyAggregated(
bytes calldata aggregatedProof,
bytes32[] calldata individualPublicInputHashes
) external view returns (bool);
}
contract BatchZKProcessor {
IRecursiveVerifier public immutable verifier;
uint256 public processedBatches;
event BatchProcessed(uint256 batchId, uint256 count);
constructor(address _verifier) {
verifier = IRecursiveVerifier(_verifier);
}
// Process N user actions in one tx by verifying the aggregated proof.
// Gas cost: O(1) instead of O(N) if each action required its own proof.
function processBatch(
bytes calldata aggregatedProof,
bytes32[] calldata actionHashes, // public inputs for each action
bytes[] calldata encodedActions // decoded actions to execute
) external {
require(actionHashes.length == encodedActions.length, "Length mismatch");
// Single verification covers all N actions
require(
verifier.verifyAggregated(aggregatedProof, actionHashes),
"Batch proof invalid"
);
// Execute each proven action
for (uint256 i = 0; i < encodedActions.length; i++) {
_executeAction(encodedActions[i]);
}
uint256 batchId = ++processedBatches;
emit BatchProcessed(batchId, encodedActions.length);
}
function _executeAction(bytes memory actionData) internal virtual {
// Override in subclass
}
}
// =============================================================================
// SECTION 11 — ZK PROOF IN PRACTICE: WORKFLOW SUMMARY
// =============================================================================
//
// 1. CIRCUIT DESIGN (off-chain, e.g., circom or Rust for SP1)
// - Define private inputs (secrets) and public inputs (verifiable outputs)
// - Encode the computation as arithmetic constraints (R1CS, PLONKish)
// - Test with unit inputs before generating keys
//
// 2. TRUSTED SETUP (for Groth16/PLONK — not needed for STARKs)
// - Powers-of-Tau ceremony (public, done once)
// - Circuit-specific setup (Groth16: per circuit; PLONK: universal)
// - Store proving key (prover uses) and verification key (verifier uses)
//
// 3. VERIFIER DEPLOYMENT (on-chain)
// - snarkjs generates a Solidity verifier from the verification key
// - Deploy the verifier contract once per circuit
// - Your application contract calls verifier.verifyProof(...)
//
// 4. PROOF GENERATION (off-chain, user's device or proving service)
// - User provides private inputs + public inputs to the prover
// - Prover runs the circuit and outputs: proof + public signals
// - Submit proof + public signals to the on-chain application contract
//
// 5. ON-CHAIN VERIFICATION
// - Application contract calls IZKVerifier.verify(proof, publicInputs)
// - If verified: execute business logic (transfer, access grant, etc.)
// - Spend nullifier to prevent replay
// =============================================================================
// PROFESSIONAL CHECKLIST: ZK PROOF INTEGRATION AUDIT POINTS
// =============================================================================
//
// [ ] Verifier address: immutable or protected from replacement (proxy attack)
// [ ] Public inputs: all application-specific values are in publicInputs (not hidden)
// [ ] Nullifiers: every privacy-preserving action has a nullifier to prevent replay
// [ ] Root/schema versioning: old proofs for old roots should not be accepted
// [ ] Trusted setup: verify the ceremony was public and the toxic waste destroyed
// [ ] Circuit scope: the circuit ONLY proves what you think it proves — audit it
// [ ] Proof malleability: Groth16 proofs are malleable; use snarkjs with proper encoding
// [ ] Front-running: proof + nullifier submission can be front-run; add msg.sender to public inputs
// [ ] Input encoding: match abi.encode layout with circuit field element order exactly
// [ ] Gas cost: Groth16 on-chain verify ~250k gas; PLONK ~500k; STARK ~1M+ — plan accordingly
// [ ] Fallback: always have an admin key or governance bypass if ZK system has bugs
// [ ] Coprocessor trust: Axiom/SP1 proofs rely on the prover's integrity — check audits