Circom은 영지식 증명에 사용되는 '산술 회로'를 설계하기 위한 언어입니다. 이 회로를 통해 증명하고 싶은 논리나 규칙을 코드로 표현할 수 있습니다.
조금 어렵게 들릴 수 있지만, "어떤 비밀 정보(private input)를 공개하지 않으면서, 내가 그 비밀 정보를 알고 있다는 사실을 증명하는 프로그램"을 만드는 언어라고 생각하면 쉽습니다.
1. 핵심 개념: 회로, 신호, 제약 조건
Circom 코드는 3가지 핵심 요소로 이루어집니다.
회로 (Circuit)
Circom의 가장 기본 단위입니다. 우리는 템플릿(template)이라는 키워드를 사용해서 회로를 정의합니다. 마치 다른 언어의 함수나 클래스처럼, 재사용 가능한 로직의 묶음이라고 생각할 수 있습니다.
신호 (Signal)
회로의 입력(input)과 출력(output), 그리고 그 사이에서 계산되는 중간값들을 신호라고 부릅니다. 신호는 회로를 통해 흐르는 데이터입니다.
- input: 회로에 입력되는 신호입니다.
- public input: 증명을 검증하는 사람에게도 공개되는 입력값입니다. (예: 문제)
- private input: 증명하는 사람만 알고 있는 비밀 입력값입니다. (예: 문제의 해답)
- output: 회로의 계산 결과로 나오는 신호입니다. 보통 public으로 간주되어 공개됩니다.
- var: 회로 내부에서만 사용되는 중간 신호입니다. 일반 프로그래밍 언어의 변수와 달리, Circom의 모든 신호(var 포함)는 최종적으로 제약 조건 시스템의 일부가 되어 증명 과정에 영향을 줍니다.
제약 조건 (Constraint)
Circom의 심장과도 같은 가장 중요한 개념입니다. 제약 조건은 신호들 사이에 반드시 성립해야 하는 수학적 관계(방정식)를 정의합니다. 증명(proof)을 생성한다는 것은, 이 모든 제약 조건을 만족하는 신호값들을 찾았다는 것을 의미합니다.
- A === B: A와 B의 값이 반드시 같아야 한다는 제약 조건을 추가합니다. 이 연산자는 두 신호가 동일함을 '검증'하거나 '강제'할 때 사용되며, 주로 이미 계산된 두 값 사이의 관계를 확인할 때 유용합니다.
- A <-- B: B의 값을 A에 할당하되, 제약 조건은 추가하지 않습니다.
- A <== B: B의 값을 A에 할당하고, A === B 라는 제약 조건을 추가합니다. 가장 일반적으로 사용되는 연산자입니다.
간단 비유
- 회로(템플릿)는 자물쇠가 달린 "마법 상자"의 설계도입니다.
- 신호(signal)는 이 상자에 넣는 "비밀번호(private)"와 "공개된 힌트(public)"입니다.
- 제약 조건(constraint)은 "이 비밀번호를 넣으면 상자가 열린다"는 상자의 내부 규칙입니다.
우리는 이 설계도(Circom 코드)를 통해, 상자를 여는 비밀번호를 절대 보여주지 않으면서도, '나는 이 상자를 열 수 있는 올바른 비밀번호를 확실히 알고 있다'는 사실만을 완벽하게 증명할 수 있습니다.
2. 기본 문법과 구조
Circom 코드의 기본 구조를 살펴봅시다.
template Multiplier() {
// 입력 신호를 선언합니다.
signal input a;
// c는 a와 b의 곱과 같아야 한다는 규칙을 추가합니다.
c <== a * b;
// public 신호를 지정할 수 있습니다.
코드 해설
- pragma circom 2.0.0;: 사용할 Circom 컴파일러의 버전을 명시합니다.
- template Multiplier(): Multiplier라는 이름의 템플릿을 선언합니다.
- signal input a;: a라는 이름의 입력 신호를 선언합니다. public이나 private 키워드가 없으면 기본적으로 private으로 간주됩니다.
- signal output c;: c라는 이름의 출력 신호를 선언합니다.
- c <== a * b;: 가장 중요한 부분입니다. 출력 c는 반드시 입력 a와 b를 곱한 값과 같아야 한다는 제약 조건을 시스템에 추가합니다.
- component main = Multiplier();: Multiplier 템플릿의 인스턴스를 생성하고, 이를 회로의 main 컴포넌트로 지정합니다.
3. 간단한 예제: 제곱 검증 회로
이제 아주 간단한 예제를 통해 전체적인 흐름을 이해해 보겠습니다. "어떤 수 x를 제곱하면 y가 된다"는 것을 증명하는 회로를 만들어 봅시다.
여기서 y는 모두에게 공개된 값(public input)이고, x는 나만 아는 비밀 값(private input)입니다.
- SquareTest.circom
* 이 회로는 y == x*x 임을 증명합니다.
* 입력:
* - x (private): 나만 아는 비밀 값
* - y (public): 모두에게 공개된 값
* 출력: 없음
*/
template SquareTest() {
y === x * x;
코드 해설
- template SquareTest(): SquareTest라는 이름의 회로를 정의합니다.
- signal private input x;: 증명자만 아는 비밀 값 x를 입력으로 받습니다.
- signal public input y;: 모두가 아는 공개 값 y를 입력으로 받습니다.
- y === x * x;: 이 회로의 핵심 규칙입니다. 공개된 y의 값은, 비밀 값 x를 제곱한 값과 반드시 같아야 한다는 제약 조건을 추가합니다.
- component main { public [y] }: main 컴포넌트를 생성하며, 이 회로의 입력 신호 중 y가 외부로 공개되는 public input임을 명시합니다. 이렇게 선언된 public input은 나중에 증명을 검증할 때 사용됩니다.
동작 방식
- 증명자(Prover): y가 9라고 공개되어 있을 때, 비밀 값 x가 3이라는 것을 알고 있습니다. 이 x=3, y=9 값을 회로에 넣어 증명(proof)을 생성합니다. 9 === 3 * 3 이라는 제약 조건이 만족되기 때문에 증명 생성에 성공합니다.
- 검증자(Verifier): 증명자에게서 증명(proof)과 공개 값 y=9를 전달받습니다. 검증자는 비밀 값 x가 무엇인지는 전혀 모르지만, 전달받은 증명을 통해 "아, 이 사람은 y=9를 만족시키는 비밀 x값을 정말로 알고 있구나!"라는 사실을 100% 신뢰할 수 있게 됩니다.
만약 증명자가 x=4와 같이 엉뚱한 값을 사용하려 하면 9 === 4 * 4 (즉, 9 === 16) 라는 제약 조건이 깨지기 때문에 증명 생성 자체가 실패하게 됩니다.
4. 다음 단계는?
Circom의 기본 개념과 문법을 익혔으니, 이제 실제 도구들을 사용해 볼 차례입니다.
- 컴파일(Compile): 작성한 .circom 파일을 컴파일러를 이용해 회로의 수학적 표현(R1CS - Rank-1 Constraint System)으로 변환합니다. R1CS는 증명 시스템이 이해할 수 있는 표준화된 형식이라고 생각할 수 있습니다.
- Witness 계산: 회로의 입력값(private, public 포함)을 제공하여 모든 제약 조건을 만족하는 중간 신호값들을 계산합니다. 이 계산된 값의 집합을 witness라고 합니다.
- 증명 생성(Prove): 신뢰할 수 있는 설정(trusted setup) 단계에서 생성된 키(proving key)와 witness를 사용해 실제 암호학적 증명(proof)을 생성합니다.
- 증명 검증(Verify): verifying key와 public input, 그리고 proof를 이용해 해당 증명이 유효한지 검증합니다.
이 과정들은 snarkjs와 같은 커맨드라인 도구를 통해 진행할 수 있습니다.
댓글
댓글 쓰기