Skip to content
This repository was archived by the owner on Aug 1, 2025. It is now read-only.

Commit 9bc0cda

Browse files
committed
feat: ZkSnark use case example (Circom, Groth16, Snarkjs, Garaga)
1 parent a87235b commit 9bc0cda

File tree

12 files changed

+4521
-0
lines changed

12 files changed

+4521
-0
lines changed

Scarb.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ version = "0.1.0"
9494
name = "factory"
9595
version = "0.1.0"
9696

97+
[[package]]
98+
name = "garaga"
99+
version = "0.14.0"
100+
source = "git+https://github.com/keep-starknet-strange/garaga.git#eb9be301d0cd61df8ffa546742e092b5e67e1505"
101+
97102
[[package]]
98103
name = "hash_solidity_compatible"
99104
version = "0.1.0"
@@ -312,3 +317,12 @@ version = "0.1.0"
312317
[[package]]
313318
name = "write_to_any_slot"
314319
version = "0.1.0"
320+
321+
[[package]]
322+
name = "zksnark_groth16"
323+
version = "0.1.0"
324+
dependencies = [
325+
"garaga",
326+
"openzeppelin",
327+
"snforge_std",
328+
]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
target
2+
node_modules
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "zksnark_groth16"
3+
version.workspace = true
4+
edition.workspace = true
5+
6+
[dependencies]
7+
starknet.workspace = true
8+
snforge_std.workspace = true
9+
openzeppelin.workspace = true
10+
garaga = { git = "https://github.com/keep-starknet-strange/garaga.git" }
11+
12+
[dev-dependencies]
13+
cairo_test.workspace = true
14+
15+
[scripts]
16+
test.workspace = true
17+
18+
[[target.starknet-contract]]
19+
sierra = true
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "zksnark_groth16",
3+
"version": "1.0.0",
4+
"description": "",
5+
"keywords": [],
6+
"author": "",
7+
"license": "ISC",
8+
"dependencies": {
9+
"circomlib": "^2.0.5"
10+
}
11+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
pragma circom 2.0.0;
2+
3+
include "../node_modules/circomlib/circuits/poseidon.circom";
4+
5+
template PasswordCheck() {
6+
// Public inputs
7+
signal input userAddress;
8+
signal input pwdHash;
9+
// Private input
10+
signal input pwd;
11+
12+
// (Public) output
13+
signal output uniqueToUser;
14+
15+
// Make sure password is the correct one by comparing its hash to the expected known hash
16+
component hasher = Poseidon(1);
17+
hasher.inputs[0] <== pwd;
18+
19+
hasher.out === pwdHash;
20+
21+
// Compute a number unique to user so that other users can't simply copy and use same proof
22+
// but instead have to execute this circuit to generate a proof unique to them
23+
component uniqueHasher = Poseidon(2);
24+
uniqueHasher.inputs[0] <== pwdHash;
25+
uniqueHasher.inputs[1] <== userAddress;
26+
27+
uniqueToUser <== uniqueHasher.out;
28+
}
29+
30+
component main {public [userAddress, pwdHash]} = PasswordCheck();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"pwdHash": "16260938803047823847354854419633652218467975114284208787981985448019235110758",
3+
"userAddress": "0xabcd",
4+
"pwd": "2468"
5+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use starknet::ContractAddress;
2+
3+
#[starknet::interface]
4+
trait IZkERC20Token<TContractState> {
5+
fn mint_with_proof(ref self: TContractState, full_proof: Span<felt252>);
6+
fn has_user_minted(self: @TContractState, address: ContractAddress) -> bool;
7+
}
8+
9+
#[starknet::interface]
10+
trait IGroth16VerifierBN254<TContractState> {
11+
fn verify_groth16_proof_bn254(
12+
self: @TContractState, full_proof_with_hints: Span<felt252>
13+
) -> Option<Span<u256>>;
14+
}
15+
16+
mod errors {
17+
pub const ALREADY_MINTED: felt252 = 'User has already minted tokens';
18+
pub const PROOF_NOT_VERIFIED: felt252 = 'Proof is not correct';
19+
pub const PROOF_ALREADY_USED: felt252 = 'Generate a proof unique to you';
20+
}
21+
22+
#[starknet::contract]
23+
pub mod ZkERC20Token {
24+
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
25+
use starknet::{ContractAddress, get_caller_address};
26+
use super::{errors, IGroth16VerifierBN254Dispatcher, IGroth16VerifierBN254DispatcherTrait};
27+
use starknet::storage::{Map, StoragePointerReadAccess, StoragePointerWriteAccess, StoragePathEntry};
28+
29+
component!(path: ERC20Component, storage: erc20, event: ERC20Event);
30+
31+
#[abi(embed_v0)]
32+
impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
33+
34+
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
35+
36+
const MINT_WITH_PROOF_TOKEN_REWARD: u8 = 100;
37+
// used in the front end to generate the proof
38+
const PASSWORD_HASH: u256 = 16260938803047823847354854419633652218467975114284208787981985448019235110758;
39+
40+
#[storage]
41+
struct Storage {
42+
#[substorage(v0)]
43+
erc20: ERC20Component::Storage,
44+
verifier_contract: IGroth16VerifierBN254Dispatcher,
45+
users_who_minted: Map<ContractAddress, bool>,
46+
}
47+
48+
#[event]
49+
#[derive(Drop, starknet::Event)]
50+
enum Event {
51+
#[flat]
52+
ERC20Event: ERC20Component::Event
53+
}
54+
55+
#[constructor]
56+
fn constructor(
57+
ref self: ContractState,
58+
initial_supply: u256,
59+
recipient: ContractAddress,
60+
name: ByteArray,
61+
symbol: ByteArray,
62+
proof_verifier_address: ContractAddress
63+
) {
64+
self.erc20.initializer(name, symbol);
65+
self.erc20.mint(recipient, initial_supply);
66+
67+
self.verifier_contract.write(IGroth16VerifierBN254Dispatcher { contract_address: proof_verifier_address });
68+
}
69+
70+
#[abi(embed_v0)]
71+
impl ZkERC20TokenImpl of super::IZkERC20Token<ContractState> {
72+
fn mint_with_proof(ref self: ContractState, full_proof: Span<felt252>) {
73+
let caller = get_caller_address();
74+
// Prevent a user from receiving tokens twice
75+
assert(!self.users_who_minted.entry(caller).read(), errors::ALREADY_MINTED);
76+
77+
// Verify the correctness of the proof by calling the verifier contract
78+
// If incorrect, execution of the verifier will fail or return an Option::None
79+
let proof_public_inputs = self.verifier_contract.read().verify_groth16_proof_bn254(full_proof);
80+
assert(proof_public_inputs.is_some() && proof_public_inputs.unwrap().len() == 3, errors::PROOF_NOT_VERIFIED);
81+
82+
// Verify the proof has been generated by the user calling this smart contract
83+
let user_address_dec: u256 = *proof_public_inputs.unwrap().at(1);
84+
let address_felt252: felt252 = caller.into();
85+
assert(address_felt252.into() == user_address_dec, errors::PROOF_ALREADY_USED);
86+
87+
// Mint tokens only if the proof is valid and has been generated by the user
88+
self.erc20.mint(caller, MINT_WITH_PROOF_TOKEN_REWARD.into());
89+
90+
self.users_who_minted.entry(caller).write(true);
91+
}
92+
93+
fn has_user_minted(self: @ContractState, address: ContractAddress) -> bool {
94+
self.users_who_minted.entry(address).read()
95+
}
96+
}
97+
98+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod contract;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use super::groth16_verifier_constants::{N_PUBLIC_INPUTS, vk, ic, precomputed_lines};
2+
3+
#[starknet::interface]
4+
trait IGroth16VerifierBN254<TContractState> {
5+
fn verify_groth16_proof_bn254(
6+
self: @TContractState, full_proof_with_hints: Span<felt252>,
7+
) -> Option<Span<u256>>;
8+
}
9+
10+
#[starknet::contract]
11+
mod Groth16VerifierBN254 {
12+
use starknet::SyscallResultTrait;
13+
use garaga::definitions::{G1Point, G1G2Pair};
14+
use garaga::groth16::{multi_pairing_check_bn254_3P_2F_with_extra_miller_loop_result};
15+
use garaga::ec_ops::{G1PointTrait, ec_safe_add};
16+
use garaga::ec_ops_g2::{G2PointTrait};
17+
use garaga::utils::calldata::{deserialize_full_proof_with_hints_bn254};
18+
use super::{N_PUBLIC_INPUTS, vk, ic, precomputed_lines};
19+
20+
const ECIP_OPS_CLASS_HASH: felt252 =
21+
0x70c1d1c709c75e3cf51d79d19cf7c84a0d4521f3a2b8bf7bff5cb45ee0dd289;
22+
use starknet::ContractAddress;
23+
24+
#[storage]
25+
struct Storage {}
26+
27+
#[abi(embed_v0)]
28+
impl IGroth16VerifierBN254 of super::IGroth16VerifierBN254<ContractState> {
29+
fn verify_groth16_proof_bn254(
30+
self: @ContractState, full_proof_with_hints: Span<felt252>,
31+
) -> Option<Span<u256>> {
32+
// DO NOT EDIT THIS FUNCTION UNLESS YOU KNOW WHAT YOU ARE DOING.
33+
// This function returns an Option for the public inputs if the proof is valid.
34+
// If the proof is invalid, the execution will either fail or return None.
35+
// Read the documentation to learn how to generate the full_proof_with_hints array given
36+
// a proof and a verifying key.
37+
let fph = deserialize_full_proof_with_hints_bn254(full_proof_with_hints);
38+
let groth16_proof = fph.groth16_proof;
39+
let mpcheck_hint = fph.mpcheck_hint;
40+
let small_Q = fph.small_Q;
41+
let msm_hint = fph.msm_hint;
42+
43+
groth16_proof.a.assert_on_curve(0);
44+
groth16_proof.b.assert_on_curve(0);
45+
groth16_proof.c.assert_on_curve(0);
46+
47+
let ic = ic.span();
48+
49+
let vk_x: G1Point = match ic.len() {
50+
0 => panic!("Malformed VK"),
51+
1 => *ic.at(0),
52+
_ => {
53+
// Start serialization with the hint array directly to avoid copying it.
54+
let mut msm_calldata: Array<felt252> = msm_hint;
55+
// Add the points from VK and public inputs to the proof.
56+
Serde::serialize(@ic.slice(1, N_PUBLIC_INPUTS), ref msm_calldata);
57+
Serde::serialize(@groth16_proof.public_inputs, ref msm_calldata);
58+
// Complete with the curve indentifier (0 for BN254):
59+
msm_calldata.append(0);
60+
61+
// Call the multi scalar multiplication endpoint on the Garaga ECIP ops contract
62+
// to obtain vk_x.
63+
let mut _vx_x_serialized = core::starknet::syscalls::library_call_syscall(
64+
ECIP_OPS_CLASS_HASH.try_into().unwrap(),
65+
selector!("msm_g1"),
66+
msm_calldata.span()
67+
)
68+
.unwrap_syscall();
69+
70+
ec_safe_add(
71+
Serde::<G1Point>::deserialize(ref _vx_x_serialized).unwrap(), *ic.at(0), 0
72+
)
73+
}
74+
};
75+
// Perform the pairing check.
76+
let check = multi_pairing_check_bn254_3P_2F_with_extra_miller_loop_result(
77+
G1G2Pair { p: vk_x, q: vk.gamma_g2 },
78+
G1G2Pair { p: groth16_proof.c, q: vk.delta_g2 },
79+
G1G2Pair { p: groth16_proof.a.negate(0), q: groth16_proof.b },
80+
vk.alpha_beta_miller_loop_result,
81+
precomputed_lines.span(),
82+
mpcheck_hint,
83+
small_Q
84+
);
85+
if check == true {
86+
return Option::Some(groth16_proof.public_inputs);
87+
} else {
88+
return Option::None;
89+
}
90+
}
91+
}
92+
}
93+

0 commit comments

Comments
 (0)