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

Commit a2bb6e0

Browse files
hudem1julio4
andauthored
feat: ZkSnark use case example (Circom, Groth16, Snarkjs, Garaga) (#270)
* feat: ZkSnark use case example (Circom, Groth16, Snarkjs, Garaga) * feat(PR review): Move files to advanced-concepts/verify_proofs * fix: typos * fix: zksnark * feat: improve zksnark wording * fix: remove redundant paragraph --------- Co-authored-by: julio4 <30329843+julio4@users.noreply.github.com>
1 parent 950c47d commit a2bb6e0

File tree

14 files changed

+4528
-2
lines changed

14 files changed

+4528
-2
lines changed

Scarb.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ dependencies = [
128128
name = "factory"
129129
version = "0.1.0"
130130

131+
[[package]]
132+
name = "garaga"
133+
version = "0.15.5"
134+
source = "git+https://github.com/keep-starknet-strange/garaga.git?tag=v0.15.5#8cc51a86a84401b063b39520e2d67254baeaebe5"
135+
131136
[[package]]
132137
name = "hash_solidity_compatible"
133138
version = "0.1.0"
@@ -282,6 +287,15 @@ dependencies = [
282287
"snforge_std",
283288
]
284289

290+
[[package]]
291+
name = "snarkjs"
292+
version = "0.1.0"
293+
dependencies = [
294+
"garaga",
295+
"openzeppelin_token",
296+
"snforge_std",
297+
]
298+
285299
[[package]]
286300
name = "snforge_scarb_plugin"
287301
version = "0.38.3"

Scarb.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ openzeppelin_token = "1.0.0"
2323
openzeppelin_utils = "1.0.0"
2424
components = { path = "listings/applications/components" }
2525
pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib", tag = "2.9.1" }
26+
garaga = { git = "https://github.com/keep-starknet-strange/garaga.git", tag = "v0.15.5" }
2627

2728
[workspace.package]
2829
description = "Collection of examples of how to use the Cairo programming language to create smart contracts on Starknet."

_typos.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[default]
2-
extend-ignore-identifiers-re = ["requestor", "REQUESTOR", "Requestor"]
2+
extend-ignore-identifiers-re = ["requestor", "REQUESTOR", "Requestor", "groth", "Groth"]
33

44
[type.po]
55
extend-glob = ["*.po", "*.css", "*.js"]
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 = "snarkjs"
3+
version.workspace = true
4+
edition.workspace = true
5+
6+
[dependencies]
7+
starknet.workspace = true
8+
snforge_std.workspace = true
9+
openzeppelin_token.workspace = true
10+
garaga.workspace = true
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": "snarkjs",
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: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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::{
28+
Map, StoragePointerReadAccess, StoragePointerWriteAccess, StoragePathEntry,
29+
};
30+
31+
component!(path: ERC20Component, storage: erc20, event: ERC20Event);
32+
33+
#[abi(embed_v0)]
34+
impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
35+
36+
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
37+
38+
const MINT_WITH_PROOF_TOKEN_REWARD: u8 = 100;
39+
// used in the front end to generate the proof
40+
const PASSWORD_HASH: u256 =
41+
16260938803047823847354854419633652218467975114284208787981985448019235110758;
42+
43+
#[storage]
44+
struct Storage {
45+
#[substorage(v0)]
46+
erc20: ERC20Component::Storage,
47+
verifier_contract: IGroth16VerifierBN254Dispatcher,
48+
users_who_minted: Map<ContractAddress, bool>,
49+
}
50+
51+
#[event]
52+
#[derive(Drop, starknet::Event)]
53+
enum Event {
54+
#[flat]
55+
ERC20Event: ERC20Component::Event,
56+
}
57+
58+
#[constructor]
59+
fn constructor(
60+
ref self: ContractState,
61+
initial_supply: u256,
62+
recipient: ContractAddress,
63+
name: ByteArray,
64+
symbol: ByteArray,
65+
proof_verifier_address: ContractAddress,
66+
) {
67+
self.erc20.initializer(name, symbol);
68+
self.erc20.mint(recipient, initial_supply);
69+
70+
self
71+
.verifier_contract
72+
.write(IGroth16VerifierBN254Dispatcher { contract_address: proof_verifier_address });
73+
}
74+
75+
#[abi(embed_v0)]
76+
impl ZkERC20TokenImpl of super::IZkERC20Token<ContractState> {
77+
fn mint_with_proof(ref self: ContractState, full_proof: Span<felt252>) {
78+
let caller = get_caller_address();
79+
// Prevent a user from receiving tokens twice
80+
assert(!self.users_who_minted.entry(caller).read(), errors::ALREADY_MINTED);
81+
82+
// Verify the correctness of the proof by calling the verifier contract
83+
// If incorrect, execution of the verifier will fail or return an Option::None
84+
let proof_public_inputs = self
85+
.verifier_contract
86+
.read()
87+
.verify_groth16_proof_bn254(full_proof);
88+
assert(
89+
proof_public_inputs.is_some() && proof_public_inputs.unwrap().len() == 3,
90+
errors::PROOF_NOT_VERIFIED,
91+
);
92+
93+
// Verify the proof has been generated by the user calling this smart contract
94+
let user_address_dec: u256 = *proof_public_inputs.unwrap().at(1);
95+
let address_felt252: felt252 = caller.into();
96+
assert(address_felt252.into() == user_address_dec, errors::PROOF_ALREADY_USED);
97+
98+
// Mint tokens only if the proof is valid and has been generated by the user
99+
self.erc20.mint(caller, MINT_WITH_PROOF_TOKEN_REWARD.into());
100+
101+
self.users_who_minted.entry(caller).write(true);
102+
}
103+
104+
fn has_user_minted(self: @ContractState, address: ContractAddress) -> bool {
105+
self.users_who_minted.entry(address).read()
106+
}
107+
}
108+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod contract;

0 commit comments

Comments
 (0)