Post

Foundations of ZK Programming

Cairo, Noir, and Halo2

Foundations of ZK Programming

ZKP DSL, Proving System

ZKP 관련 글을 보면 Circom, Noir, Cairo, Halo2 같은 이름이 계속 등장하고 있다.
하지만 “서로 뭐가 다른지, 어디에 강점이 있는지” 까지 한 번에 정리해서 설명해 주는 자료는 많지 않다.

이 글에서는 Circom, Noir, Cairo, Halo2에 대해 다음 관점에서 간단히 정리한다.

  • 왜 나왔는지
    • 각각 어떤 배경과 문제의식에서 등장한 도구인지
  • 어디에 속하는지
    • 회로/언어(DSL)에 가까운지, 프루빙 시스템·VM에 가까운지
  • 무슨 특징이 있는지
    • 표현 방식, 백엔드, 사용성, 어떤 타입의 ZK 시스템에 잘 맞는지
  • 어떻게 확인·테스트하면 되는지
    • 기본적인 회로 작성 → 증명 생성 → 검증 흐름을 어떻게 가져가면 되는지

즉, 세 도구를 “그냥 ZK 프레임워크들”로 보지 않고, 각각 담당하는 레이어와 특징을 비교해서 보는 것이 핵심 구조이다.

이 파트를 읽고 나면

  • Circom, Noir, Cairo, Halo2가 각각 어떤 목적과 배경을 가진 도구인지,
  • ZK 스택 안에서 어떤 역할과 특징을 갖고 있는지,
  • 간단한 예제로 어떻게 써 보고 테스트해 볼 수 있는지

를 한눈에 이해할 수 있게 되는 것이 목표이다.

즉, “이름만 아는 상태”에서 벗어나, 각 도구의 성격과 강점을 구분해서 볼 수 있을 것이다.

Cairo

1. Cairo란?

Cairo는 Circom, Noir와 같이 ZK 언어라는 점에서는 비슷하지만, 출발점과 목적이 꽤 다르다.

다른 것들이 “회로/증명 도구”에 가깝다면, Cairo는 애초에 “STARK 기반 롤업을 위한 전용 VM+언어 스택”으로 나왔다는 점이 가장 큰 차이다.

Cairo는 StarkWare가 StarkNet 같은 L2에서 대규모 계산을 STARK로 증명하기 위한 실행 환경으로 설계한 언어이다. 즉, 프로그램 전체를 STARK로 증명하는 VM에 초점이 있다.

Cairo VM 위에서 프로그램을 실행하고 이 VM이 규칙에 따라 이 상태에서 저 상태로 이동했다는 것을 STARK로 증명한다.
사용자는 회로를 직접 의식하기보다는 일반 프로그램을 쓰듯이 Cairo 코드를 작성하고, 그 실행 trace가 자동으로 증명된다.

또한 Cairo는 STARK를 전제로 한 언어/VM이다. 투명한 셋업, 해시 기반 보안 가정, 상대적으로 큰 증명 크기를 가지고 있다.

따라서 Cairo는 L2 dApp을 작성하는 “언어”에 더 가깝고, 그 결과물이 자동으로 STARK 증명과 연결되는 구조이다.


2. 사용법

설치

1
2
3
4
5
6
7
8
curl --proto '=https' --tlsv1.2 -sSf https://sh.starkup.dev | sh

source ~/.zshrc

starkup
scarb --version
snforge --version

scarb를 통해서 cairo 패키지를 사용할 수 있다.

1
scarb new hello_world

scarb new로 새로운 프로젝트를 만들 수 있다.

프로젝트 템플릿으로 많은 파일이 생기는데 Scarb.toml 파일을 통해 빌드 설정을 할 수 있다.

만약 오프체인에서 실행한 함수 결과를 zk proof를 만든다면 실행 파일로 만들어 (target.executable) 실행 트레이스에 대한 STARK 증명을 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## Scarb.toml
[package]
name = "equation_prover"
version = "0.1.0"
edition = "2024_07"

[cairo]
enable-gas = false          # 실행용 바이너리이므로 gas 비활성화

[dependencies]
cairo_execute = "2.13.1"    # 실행 + 증명용 플러그인

[[target.executable]]
name = "main"
function = "equation_prover::main"   # src/lib.cairo 안의 main을 엔트리로 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/lib.cairo

/// x^3 + x + 5 == 35 를 만족하는지 확인하는 함수이다.
fn check_equation(x: felt252) -> bool {
    // Cairo의 기본 타입 felt252 위에서 계산한다.
    let lhs = x * x * x + x + 5;
    lhs == 35
}

// 실행 가능한 엔트리 포인트이다.
#[executable]
fn main(x: felt252) -> bool {
    check_equation(x)
}

실행

비밀 값인 x를 대입하여 실행한 과정과 결과를 가지고 STARK를 사용해서 증명을 만든다

1
2
3
scarb execute -p equation_prover \
  --print-program-output \
  --arguments 3

#[executable] fn main 를 찾아서 빌드하고 실행한다. 실행 과정에서 생성된

  • air_public_input.json, air_private_input.json: AIR에 들어갈 공개/비공개 입력
  • trace.bin, memory.bin: 실행 트레이스와 메모리 스냅샷 이 존재한다.

증명

1
scarb prove --execution-id 1

위에서 만든 입력들과 실행 트레이스를 바탕으로 proof를 만든다.
proof.json 안에는 “어떤 입력 x에 대해 main(x)가 Cairo VM 규칙에 맞게 실행되어 true를 반환했다”는 STARK 증명이 들어 있다

검증

1
scarb verify --execution-id 1

proof를 사용해서 검증을 진행한다.

즉 어떤 \[x\]에 대해 \[x^3 + x + 5 = 35\] 이고, 그 \[x\]를 입력으로 하는 Cairo 프로그램 main이 규칙대로 실행되어 true를 반환했다는 사실이 STARK 증명으로 확인된다.

하지만 이러한 증명과 검증을 온체인에서 동일하게 진행할 수 없다.


3. References

Circom

1. Circom이란

Circom은 영지식 증명에 사용되는 산술 회로를 정의하기 위한 DSL이다.
개발자는 Circom 문법으로 회로를 작성하고, 이를 컴파일하여 실제 SNARK 프로토콜이 사용할 수 있는 형태의 제약식과 실행 코드를 얻을 수 있다.

Circom 컴파일러는 Rust로 구현된 컴파일러로, 하나의 Circom 회로로부터 다음과 같은 산출물을 생성한다.

  • R1CS 파일: 회로를 R1CS 형태로 변환한 결과로, 각 wire가 어떤 곱셈·덧셈 관계를 만족해야 하는지에 대한 제약식을 포함한다.
  • witness 생성 프로그램(C++ 또는 WebAssembly): 주어진 입력에 대해 회로를 실행하여, 모든 wire/signal에 대한 유효한 할당값(witness)을 효율적으로 계산하는 프로그램이다.

이 과정을 통해 사용자의 회로 정의가 실제 SNARK proving 시스템이 사용할 수 있는 제약식 + witness 계산기로 연결된다.


템플릿과 컴포넌트: 모듈형 회로 설계

Circom의 핵심 특징 중 하나는 모듈성이다. Circom에서는 회로를 template이라는 형태로 정의하고, 이를 component로 인스턴스화하여 더 큰 회로를 구성한다.

  • Template: 파라미터를 받을 수 있는 회로 설계도에 해당한다. 특정 연산(곱셈기, 비교기, 해시 라운드 등)을 일반화된 형태로 정의한다.
  • Component: template를 실제로 인스턴스화한 서브 회로이다. 하나의 큰 회로 안에서 template를 여러 번 재사용하여 구조적인 회로를 만들 수 있다.

이러한 설계 방식은 다음과 같은 장점을 갖는다.

  • 작은 단위의 회로를 독립적으로 테스트·리뷰·오딧·형식 검증하기 쉽다.
  • 동일한 패턴의 회로를 여러 번 재사용할 수 있어, 큰 회로를 설계할 때 구조가 명확해진다.

또한 Circom 생태계에는 circomlib이라는 공개 라이브러리가 존재하여, 다음과 같은 다양한 템플릿을 바로 가져다 쓸 수 있다.

  • 비교기, 선택자 등 기본 연산
  • Poseidon, MiMC 등 해시 함수
  • 서명 검증 등 고수준 회로

개발자는 circomlib에 포함된 검증된 회로를 재사용하거나, 이를 참고해 자신의 템플릿을 정의하여 더 큰 시스템을 설계할 수 있다.


Proving 시스템과 도구 생태계

Circom은 회로 정의와 제약식/위트니스 생성까지를 담당하고, 실제 증명(proof)을 생성·검증하는 부분은 별도의 proving 라이브러리가 맡는다.

대표적으로 다음과 같은 구현체들이 있다.

  • snarkjs: JavaScript 및 WebAssembly로 구현된 라이브러리로, 브라우저나 Node.js 환경에서 Groth16 등의 proving 시스템을 사용할 수 있다.
  • wasmsnark: 네이티브 WebAssembly 환경을 위한 구현체이다.
  • rapidSnark: C++ 및 Intel 어셈블리로 작성된 고성능 prover로, 대규모 증명 생성에 적합하다.

Circom으로 작성된 회로(R1CS + witness 생성기)는 이들 proving 라이브러리와 결합되어, 최종적으로 zk-SNARK 증명 생성·검증 파이프라인을 구성하게 된다.


2. 사용법

설치 방법

1
git clone https://github.com/iden3/circom.git

다운받은 디렉토리에 들어가 rust를 사용하여 빌드한다.

1
cargo build --release

빌드된 바이너리를 자유롭게 사용하기 위해 path를 고정시킨다.

1
cargo install --path circom

snarkjs

만들어진 Circuit을 사용하여 zk proof를 만드는 패키지를 설치한다.

1
npm install -g snarkjs

3. 실제 사용법

이제 Groth16과 Circom을 사용하여 \[x^3 + x + 5 = 35\]을 만족하는 𝑥 를 알고 있다는 명제를 어떻게 영지식 증명으로 만드는지 살펴보자.

먼저, Template이라는 구조를 통해 circuit을 설계한다.

만든 Template를 인스턴스화 해서 실제 회로를 만드는 것이 component 이다.

컴파일

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma circom 2.0.0;

template Main() {
    signal input x;       // 비밀 값
    signal output out;    // 공개 출력

    signal sym1;
    signal y;
    signal sym2;

    sym1 <== x * x;
    y    <== sym1 * x;
    sym2 <== x + y;
    out  <== sym2 + 5;

    // 우리가 원하는 명제: out == 35
    out === 35;
}

component main = Main();

이렇게 만든 파일을 아래 명령어를 통해 컴파일한다.

1
2
## Circom 파일 컴파일
circom main.circom --r1cs --wasm --sym -o build

결과물로 나온 파일은 build/main.r1cs : R1CS 제약식, build/main_js/main.wasm : witness 계산용 wasm, build/main.sym : 디버깅용 심볼이 나온다.


즉 Circom는 회로를 R1CS 제약식으로 변환하고, 주어진 입력으로 witness를 계산하는 WebAssembly(wasm) 프로그램을 생성한다.

R1CS 분석

Circom에서 중요한 부분은 결국 우리가 작성한 circuit이 R1CS 제약식의 형태로 잘 나오는지다.

이를 확인하기 위해 snarkjs를 사용하여 확인 할 수 있다.

1
2
3
4
5
6
7
8
## constraint 개수, wire 개수 같은 요약 정보가 나온다.
snarkjs r1cs info build/main.r1cs

## 여기서 main.x, main.sym1 같은 이름이 *.sym을 통해 매핑되어 나온다.
snarkjs r1cs print build/main.r1cs build/main.sym

## 제약식 전체를 JSON 구조로 볼 수 있다
snarkjs r1cs export json build/main.r1cs build/main.r1cs.json
1
2
3
4
5
6
snarkjs r1cs print build/main.r1cs build/main.sym
[INFO]  snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.x ] * [ main.x ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.sym1 ] = 0
[INFO]  snarkJS: [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.sym1 ] * [ main.x ] - [ 21888242871839275222246405745257275088548364400416034343698204186575808495616main.y ] = 0
[INFO]  snarkJS: [  ] * [  ] - [ 351 +21888242871839275222246405745257275088548364400416034343698204186575808495616main.out ] = 0
[INFO]  snarkJS: [  ] * [  ] - [ main.x +main.y +21888242871839275222246405745257275088548364400416034343698204186575808495616main.sym2 ] = 0
[INFO]  snarkJS: [  ] * [  ] - [ 51 +21888242871839275222246405745257275088548364400416034343698204186575808495616main.out +main.sym2 ] = 0

여기서 엄청나게 큰 수는 p - 1 즉 (-1)을 뜻하는 값이다 (mod 연산)

따라서 첫 번째 R1CS 식을 보면 \[[- main.x] * [main.x] - [-main.sym1] = 0\]이다.

즉 \[x^2 = sym_1\] 이라는 제약 조건이다.

나머지도 실제로 정리해 보면 아래 식과 같다

\[\begin {aligned} &x \cdot x = sym_1 \ \ &sym_1 \cdot x = y \ \ &(x + y) \cdot 1 = sym_2 \ \ &(sym_2 + 5) \cdot 1 = out \end {aligned}\]

Witness 생성

여기서부터는 Circom을 사용하진 않는다. Circom에서 만든 generate_witness.js와 wasm을 이용해서 input.json 에 내가 아는 x(비밀 값)을 넣고 Witness를 생성한다.

1
2
node build/main_js/generate_witness.js build/main_js/main.wasm \
     input.json build/witness.wtns

input.json 에 올바른 값을 넣지 않으면 assert가 뜬다.

1
2
3
4
// input.json
{
  "x": 3
}

여기서 나온 witness.wtns를 변환하면 constraint을 만족하는 유효한 witness가 나온다.
\[w = [1, out, x, sym_1, y,sym_2]\]

1
snarkjs wtns export json build/witness.wtns build/witness.json
1
2
3
4
5
6
7
8
9
// witness.json
[
 "1",
 "35",
 "3",
 "9",
 "27",
 "30"
]

Powers of Tau, Groth16 setup

Groth16을 사용하여 proof를 만들기 때문에 공개 파라미터 설정이 필요하다.

자세한 내용은 Groth16에서 설명되니 간단하게 명령어만 설명하겠다.

Powers of Tau

1) 새 pot 파일 생성

snarkjs powersoftau new bn128 12 pot12_0000.ptau

2) 기여 한 번

snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="first contribution" -v

3) phase2용으로 준비

snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau

4) 회로 + pot로 zkey 1차 생성

snarkjs groth16 setup build/main.r1cs pot12_final.ptau build/circuit_0000.zkey

5) 기여 한 번 더 (옵션이지만 보통 한다)

snarkjs zkey contribute build/circuit_0000.zkey build/circuit_final.zkey --name="circuit contribution" -v

6) 검증키 추출

snarkjs zkey export verificationkey \ build/circuit_final.zkey \ build/verification_key.json

증명 생성

증명자는 circuit에 대한 증명키와 witness를 바탕으로 증명을 만든다.

결과물로는 proof와 공개 입력값이 나온다.

1
2
3
4
5
snarkjs groth16 prove \
  build/circuit_final.zkey \
  build/witness.wtns \
  build/proof.json \
  build/public.json
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
{
 "pi_a": [
  "2292319416538939859201154273956115722590214103931389334485591721447225238910",
  "19111769558577349015868971779500990904290742705820502368384893433021281806467",
  "1"
 ],
 "pi_b": [
  [
   "3623544671139639729911711237732379685469066905141933193693026565106714625812",
   "20548909579081465004345388720650061102734787819110480924339461372281796710249"
  ],
  [
   "12677133574353034297782430001677023484863413520732045917635220787909690435757",
   "4838882721185144936352546280298927340185305702142561563330334152259051708952"
  ],
  [
   "1",
   "0"
  ]
 ],
 "pi_c": [
  "14187643094968055203162794487234713608924541262393801546395180119989228700015",
  "11409977509591853222081958976060609374311589537385913801060208734754872372279",
  "1"
 ],
 "protocol": "groth16",
 "curve": "bn128"
}
1
2
3
[
 "35"
]

검증 과정

검증자는 circuit에 대한 검증키와 공개 입력값, 증명을 바탕으로 검증을 진행한다.

1
2
3
4
snarkjs groth16 verify \
  build/verification_key.json \
  build/public.json \
  build/proof.json

4. References


Noir

1. Noir란

Noir 배경

초기 ZK DSL들은 회로에 많이 가까운 형태(R1CS 스타일, low-level DSL)에 머무르는 경우가 많았기 때문에, 일반적인 애플리케이션 개발자가 바로 쓰기에는 진입 장벽이 높다는 문제가 있었다.

또한, 특정 프로토콜이나 특정 proving backend에 강하게 묶인 언어가 많아서, “ZK 앱 코드는 그대로 두고, 뒤에 붙는 프루빙 시스템만 바꾼다”는 식의 설계가 쉽지 않았다.

Noir는 이런 문제의식에서 출발한 고수준 ZK 프로그래밍 언어이다.

Noir 목적

  • 일반 개발자가 Rust/TypeScript와 비슷한 감각으로 ZK 로직을 작성할 수 있게 하는 것
  • 특정 프로토콜에 종속되지 않고, 여러 proving backend에 붙을 수 있는 범용 DSL을 제공하는 것
  • “앱 로직”과 “프루빙 시스템 구현”을 분리해서, 앱 개발자는 Noir 코드에 집중하고 백엔드 팀은 Plonk, UltraPlonk, Halo2 등 다양한 proving 시스템을 선택·교체할 수 있게 하는 것

즉 Noir의 목적은, “ZK 회로 언어”를 넘어 “ZK 애플리케이션 언어”에 가까운 고수준 DSL을 제공하는 것이라고 정리할 수 있다.

Noir의 회로 표현 방식

Noir는 문법적으로 일반 프로그래밍 언어에 상당히 가깝다.

  • 정적 타입, 함수, 제어문(if, for) 등을 사용해 로직을 작성한다.
  • 개발자 입장에서는 “일반 함수형/시스템 언어”에 회로 제약이 추가된 느낌이다.
  • 내부적으로는 Noir 코드가 ACIR (Abstract Circuit Intermediate Representation) 라는 중간 표현으로 변환되고, 이 ACIR이 다시 각 proving backend의 회로 표현(R1CS, Plonkish constraint 등)으로 매핑되는 구조이다.

이 말은 곧:

  • Noir 코드는 논리/비즈니스 레벨을 표현하는 데 집중하고, 이게 R1CS에서 어떻게 flatten 되는지, 어떤 gate로 최적화되는지는 컴파일러 + backend가 담당한다는 뜻이다.

Circom이 회로 DSL에 더 가깝다면, Noir는 한 단계 위의 ZK-aware 일반 언어에 더 가까운 포지션이라고 볼 수 있다.


2. 사용법

설치

1
2
3
curl -L https://raw.githubusercontent.com/noir-lang/noirup/refs/heads/main/install | bash
source ~/.zshrc
noirup

새 프로젝트 생성

noir는 쉽게 프로젝트를 생성하여 template 파일들을 제공한다.

1
nargo new practice1
  • src/main.nr 는 simple boilerplate circuit 을 포함한다.
1
2
3
4
5
6
7
8
9
10
11
12
fn main(x: Field, out: pub Field) {
    let y = x * x * x + x + 5;

    assert(y == out);
}

#[test]
fn test_main() {
    main(3, 35);

}

main 함수에 ACIR 회로가 정의되어 있으며 이를 컴파일하여 증명을 만든다.

  • Nargo.toml environmental options, dependencies, and others 같은 프로젝트에 대한 정보를 갖는다.
1
2
3
4
5
6
[package]
name = "practice1"
type = "bin"
authors = [""]

[dependencies]
  • type
    • bin: 프로젝트 output이 실행 가능한 바이너리로, main 함수가 entry point이다.
    • lib: 다른 프로젝트에서 활용 가능한 구조로 ACIR로 컴파일 되지 않는다.
    • contract: bin 타입과 비슷하지만 main 함수가 없다. Aztec Network에 올라가는 contract이다.

환경 check

1
nargo check

프로젝트에 정의된 패키지와 의존성에 오류가 없는지 체크한다.
결과물로 회로에 대해 input을 받는 Prover.toml이 생긴다.

1
2
3
// Prover.toml
out = ""
x = ""

Circuit compile

1
nargo compile

main 함수에 정의된 코드로 어떤 순서로 실행되고, 제약이 생기는지 ACIR(Abstract Circuit Intermediate Representation) 형식으로 컴파일한다.

Circuit execute

Prover.toml 원하는 out 값과 비밀 값인 x를 작성한다.

1
2
3
// Prover.toml
out = "35"
x = "3"

그리고 컴파일된 ACIR 파일을 바탕으로 input 값을 회로에 넣어 실행한다.

1
nargo execute

결과물로 ./target/<project>.gz 형식으로 witness가 나온다.

Noir는 여기까지 진행되고 나머지는 Proving Backend에서 처리된다.

Proving Backend

보통 Noir를 통해 만든 witness를 가지고 Barretenberg라는 backend를 사용한다.

설치

1
2
3
curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/refs/heads/next/barretenberg/bbup/install | bash
source ~/.zshrc
bbup

Noir를 통해 만든 witness와 circuit을 통해 bb를 사용하여 증명을 생성한다.

Prove & Verify key

circuit과 witness를 인자로 넣어 Proof를 만든다. 또한 verify key 옵션을 넣어 검증할 때 witness 없이 검증할 수 있는 vk를 만든다.

1
bb prove -b ./target/practice1.json -w ./target/practice1.gz --write_vk -o target

결과물로 proof, vk, public_input이 생성된다.

검증

1
bb verify -p ./target/proof -k ./target/vk

만들어진 vk와 proof를 통해 검증할 수 있다.

3. References


Halo2

1. Halo2란?

Zcash의 초기 zk-SNARK 생태계는 거의 전부 Groth16 위에서 돌아가고 있었다.
Groth16은 증명 크기가 작고 검증이 빠르다는 큰 장점이 있지만, 실제 운영을 하다 보니 몇 가지 한계가 드러났다.

  • 회로별(trusted setup per circuit) 셋업이 필요하다.
  • 회로를 조금만 바꿔도 다시 ceremony를 해야 한다. (phase2)
  • 실서비스에서 자주 회로를 업데이트하려면 부담이 크다.
  • 재귀(Recursive SNARK)와 집계(Aggregation) 설계가 불편하다.

Groth16도 재귀가 가능은 하지만 구조상 복잡하고, 새로운 요구에 최적화되어 있지 않다.

또한 복잡한 연산(lookup, 테이블 기반 연산, custom gate 최적화 등)을 표현하기가 점점 불편해졌다.

이 배경에서 나온 것이 Halo1 이다.

Sean Bowe가 제안한 프로토콜로, inner-product argument(IPA) 기반 polynomial commitment를 사용하고, 신뢰 설정 없이 재귀 SNARK를 가능하게 하는 아이디어를 제시하였다.

핵심 포인트는 증명을 또 다른 증명 안에 넣어도 보안이 유지되는 구조, universal / transparent한 설정으로도 재귀가 가능한 것이다.

Zcash 입장에서는 우리가 새로운 회로를 계속 도입하고, 재귀/집계를 적극적으로 쓰려면 Groth16이 아니라 Halo 계열이 필요하다는 방향성이 생겼고, 여기서 실제 개발에 쓰기 위한 개선판으로 등장한 것이 Halo2이다.

Halo2는 Zcash 팀이 Halo 아이디어와 PLONKish Arithmetization을 결합해 만든, 유연한 회로 표현과 재귀/집계를 지원하는 SNARK 프루빙 시스템이다.


2. 사용법

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
/// The full circuit implementation.
///
/// In this struct we store the private input variables. We use `Option<F>` because
/// they won't have any value during key generation. During proving, if any of these
/// were `None` we would get an error.
#[derive(Default)]
struct MyCircuit<F: Field> {
    constant: F,
    a: Value<F>,
    b: Value<F>,
}

impl<F: Field> Circuit<F> for MyCircuit<F> {
    // Since we are using a single chip for everything, we can just reuse its config.
    type Config = FieldConfig;
    type FloorPlanner = SimpleFloorPlanner;

    fn without_witnesses(&self) -> Self {
        Self::default()
    }

    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
        // We create the two advice columns that FieldChip uses for I/O.
        let advice = [meta.advice_column(), meta.advice_column()];

        // We also need an instance column to store public inputs.
        let instance = meta.instance_column();

        // Create a fixed column to load constants.
        let constant = meta.fixed_column();

        FieldChip::configure(meta, advice, instance, constant)
    }

    fn synthesize(
        &self,
        config: Self::Config,
        mut layouter: impl Layouter<F>,
    ) -> Result<(), Error> {
        let field_chip = FieldChip::<F>::construct(config);

        // Load our private values into the circuit.
        let a = field_chip.load_private(layouter.namespace(|| "load a"), self.a)?;
        let b = field_chip.load_private(layouter.namespace(|| "load b"), self.b)?;

        // Load the constant factor into the circuit.
        let constant =
            field_chip.load_constant(layouter.namespace(|| "load constant"), self.constant)?;

        // We only have access to plain multiplication.
        // We could implement our circuit as:
        //     asq  = a*a
        //     bsq  = b*b
        //     absq = asq*bsq
        //     c    = constant*asq*bsq
        //
        // but it's more efficient to implement it as:
        //     ab   = a*b
        //     absq = ab^2
        //     c    = constant*absq
        let ab = field_chip.mul(layouter.namespace(|| "a * b"), a, b)?;
        let absq = field_chip.mul(layouter.namespace(|| "ab * ab"), ab.clone(), ab)?;
        let c = field_chip.mul(layouter.namespace(|| "constant * absq"), constant, absq)?;

        // Expose the result as a public input to the circuit.
        field_chip.expose_public(layouter.namespace(|| "expose c"), c, 0)
    }
}
  • configure에서 회로 구조(제약식)를 정의하고,
  • synthesize에서 실제 값(witness)을 어떻게 넣을지를 정한다.

한 번 회로 구성이 정해지면:

  • keygen_pk(params, vk, \&circuit)
  • keygen_vk(params, \&circuit)

같은 API로 proving key / verification key를 생성한다.

4) 증명 생성 / 검증

  • 증명 생성:
    • create_proof(params, pk, circuit, public_inputs, rng)
  • 검증:
    • verify_proof(params, vk, public_inputs, proof, strategy)

3. References

This post is licensed under CC BY 4.0 by the author.