이 페이지는 Rand의 모든 증명 뒤에 있는 머신을 이해하기 위한 문서입니다. 머신이 무엇을 증명하는지, 프로그램의 한 번의 실행이 어떻게 밸리데이터가 밀리초 안에 검증할 수 있는 것으로 바뀌는지, 프라이버시가 어디에서 나오는지를 다룹니다. 증명에 드는 비용과 지갑이 증명을 만드는 방식은 Proofs를, 파라미터와 해시 구성은 Cryptography를, 프로그램이 작성되는 대상인 명령어 집합은 The RISC-V ISA를 참고하십시오.
모든 증명이 말하는 하나의 명제
zkVM은 체인이 실행하는 모든 프로그램에 대해 한 종류의 명제를 증명합니다.
다이제스트가
hc인 프로그램이base_pc에서 시작하여,H_IN으로 커밋된 비공개 입력과H_PUB으로 커밋된 공개 입력 세그먼트를 받아 실행되었고,2ᵗ − 1사이클 이내에 정지했으며, 최대GAS_LIMIT의 가스를 사용하고, 다음 여덟 개의 출력 워드를 기록했다.
차폐 전송은 하나의 고정된 프로그램, 즉 hidden-asset bundle guest에 대한 이 명제입니다. 이 프로그램의 hc는 제네시스 파일에 hc_bundle로 고정되어 있습니다. 체인 17부터는 전송 한 건마다 증명이 두 번 만들어집니다. auth guest(제네시스 파일의 hc_auth)에 대한 작은 두 번째 증명이 발신자가 지출 키를 보유하고 있음을 보여 줍니다(Proofs). 기밀 호출은 배포된 프로그램에 대한 동일한 명제입니다. 모든 경우에 머신, 검증자, 제약 집합이 하나씩뿐입니다. 따라서 프로그램을 배포하는 일은 암호학적 연산이 아니라 데이터 연산(워드와 hc를 저장하는 것)입니다. 프로그램별 회로도, 신뢰 설정(trusted setup)도 없습니다.
증명 시스템은 Plonky3로 구축한 STARK입니다. 해시 함수 외에는 아무것도 필요로 하지 않습니다. 설정 의식(ceremony)도 타원곡선도 없으므로 Shor 알고리즘이 깰 수 있는 대상이 없습니다.
파이프라인
secret input ──► run the program ──► execution trace ──► polynomials ──► commit ──► FRI ──► proof
(keys, (RV32IM emulator) (nine tables, (one per (8× code, (80 queries,
notes, paths) one row per step) column) Merkle) 20-bit grinding)
every validator: recompute the challenges from the Merkle roots, open 80 random positions,
check the rules there → ≈ 16 ms
위쪽 행은 발신자의 머신에서 한 번 실행되며, 번들에 약 100초, 그 옆의 인증 증명에 약 7초가 걸립니다. 아래쪽 행은 모든 밸리데이터에서 실행되며, 공개 필드와 증명만으로 수행됩니다.
1. 프로그램을 실행하고 모든 것을 기록하기
이 머신은 32비트 RISC-V 프로세서로, FENCE, CSR 명령어, EBREAK를 제외한 RV32IM입니다(The RISC-V ISA). 증명자는 먼저 에뮬레이터에서 프로그램을 실행하며 클록 틱마다 한 행씩 기록합니다. 이 기록이 **실행 트레이스(execution trace)**입니다. 트레이스를 가진 사람은 누구나 실행을 단계별로 재현할 수 있습니다. 이 방식의 나머지 부분은 검증자가 트레이스를 보유하지 않고도 그것이 올바르다는 확신을 얻을 수 있도록 존재합니다.
프로그램은 여덟 개의 시스템 콜을 통해서만 외부와 통신합니다.
| 시스템 콜 | 번호 | 기능 |
|---|---|---|
HALT | 0 | 정지합니다. 여덟 개의 출력 워드가 확정됩니다 |
WRITE_OUTPUT | 1 | 여덟 개의 공개 출력 워드 중 하나를 설정합니다 |
READ_INPUT | 2 | 비공개 입력의 워드 하나를 읽습니다 |
POSEIDON2 | 3 | 메모리의 버퍼를 제자리에서 해시합니다 |
KECCAK | 4 | Keccak-f[1600] 순열 한 번 |
SHA256 | 5 | SHA-256 압축 한 번 |
READ_PUBLIC | 6 | 공개 입력 세그먼트의 워드 하나를 읽습니다 |
POSEIDON2_LEN | 7 | 메시지 길이를 해시에 결합한 POSEIDON2(제약 집합 7) |
노트 커밋먼트, 널리파이어, Merkle 경로는 머신에 내장되어 있지 않습니다. 이들은 도메인 태그가 붙은 메시지를 메모리에 준비하고 POSEIDON2를 호출하는 평범한 게스트 코드입니다. 이 시스템 콜은 패딩을 하지 않으므로 [a]와 [a, 0]은 같은 해시가 됩니다. 제공되는 게스트는 고정 길이 메시지에만 이를 호출합니다. 길이가 달라질 수 있는 데이터를 해시하는 새 프로그램은 POSEIDON2_LEN을 사용합니다(The RISC-V ISA).
중단(abort)은 없습니다
프로세서에는 종료를 거부하는 명령어가 없으므로, 검사가 실패해도 프로그램이 멈출 수 없습니다. 대신 번들 게스트가 실패를 기록합니다. 검사가 실패할 때마다 bad라는 레지스터에 1을 OR하고, bad는 출력 다이제스트에 마지막으로 해시되는 워드입니다.
D = H(64; anchor, nf₀..₃, cm₀..₃, fee, burn_a, burn_r, burn_asset, time, c, bad)
c는 인증 증명도 함께 공개하는 인증 커밋먼트입니다(체인 17부터이며, 체인 14~16의 다이제스트에는 c가 없었습니다).
체인은 D의 프리이미지를 발신자로부터 받지 않습니다. 트랜잭션의 공개 필드로부터 프리이미지를 직접 재구성하고, bad = 0을 스스로 설정한 뒤 해시하여 비교합니다. 부정행위자는 검사가 실패한 실행에 대해 완전히 유효한 증명을 만들 수 있지만, 그 실행이 출력한 것은 H(…, 1)이고 체인이 찾는 것은 H(…, 0)입니다. 둘을 일치시키려면 해시 충돌이 필요합니다.
2. 트레이스를 아홉 개의 테이블로 나누기
프로세서 전체를 하나의 테이블로 기술하면 지나치게 넓어지므로, 트레이스는 전문화된 테이블로 나뉘고 하나의 배치로 함께 증명됩니다.
| 테이블 | 티어 t에서의 행 수 | 기록하는 내용 |
|---|---|---|
program | 선언값, ≥ 2⁷ | 프로그램의 명령어 워드를 회로 안에서 디코딩한 것이며, CPU가 가져올 수 있는 대상을 고정합니다 |
cpu | 2ᵗ | 사이클당 한 행이며, 앞에 프로그램, 비공개 입력, 공개 세그먼트를 해시하는 행이 놓입니다. 35개의 공개 값을 보유합니다 |
memory | 선언값, ≥ 2ᵗ⁺² | 레지스터와 RAM이며, (공간, 주소, 시간) 순으로 정렬됩니다 |
alu | 2ᵗ⁺¹ | 모든 산술, 논리, 시프트, 비교, 곱셈, 나눗셈을 바이트 리임(limb) 단위로 기록합니다 |
range, nibble | 각 256 | 고정된 룩업 테이블입니다. 바이트, 2의 거듭제곱, 4비트 AND/OR/XOR |
poseidon2 | 2ᵗ⁺² | 해시 순열이며, 라운드당 한 행입니다 |
input | 선언값, ≥ 2⁷ | 커밋된 비공개 입력 워드 |
public | 선언값, ≥ 2⁷ | 공개 세그먼트 워드(번들의 경우 트랜잭션 바인딩) |
keccak과 sha256이라는 두 개의 테이블은 프로그램이 해당 시스템 콜을 호출한 증명에만 존재하며, 존재할 때는 각각 최소 2⁷ 행입니다. 번들 게스트는 둘 다 사용하지 않습니다. 선언되는 모든 테이블의 최소 2⁷ 행 하한은 제약 집합 7과 함께 도입되었습니다(section 9).
티어(tier) t는 10, 12, 14, 16, 18, 20 중 하나이며 테이블의 크기를 정합니다. 모든 번들은 티어 14에서 증명됩니다. CPU 테이블은 16,384행이며 그중 게스트가 사용하는 것은 약 12,400행입니다. auth 게스트는 티어 10에서 250 사이클을 실행합니다. 나머지는 패딩이므로, 증명은 프로그램이 어떤 티어를 필요로 했는지는 드러내도 그 안에서 몇 단계를 밟았는지는 드러내지 않습니다(Tiers).
메모리 테이블은 RAM을 검증 가능하게 만드는 요소입니다. 주소 순, 그다음 시간 순으로 정렬되어 있으므로 “읽기는 해당 주소에 마지막으로 쓰인 값을 반환한다”는 규칙이 인접한 두 행 사이의 규칙이 됩니다.
3. 버스로 테이블을 연결하기
CPU가 사용한 합이 ALU가 계산한 합과 같다는 것을 검증자는 어떻게 알 수 있을까요? **버스(bus)**를 통해서입니다. CPU가 덧셈을 필요로 하면 메시지 (op, a, b, c)를 보내고, ALU는 자신이 계산하는 행마다 메시지를 하나씩 받습니다. 이 머신에는 열일곱 개의 버스가 있습니다.
| 버스 | 연결 대상 |
|---|---|
PROGRAM, PROGRAM_WORD | cpu ← program: 명령어 페치, 그리고 프로그램 다이제스트가 흡수하는 워드 |
MEMORY | cpu ↔ memory: 모든 읽기와 쓰기 |
ALU | cpu → alu |
RANGE8, POW2, AND4, OR4, XOR4 | 고정 테이블에 대한 바이트 범위 검사, 시프트, 비트 연산 |
POSEIDON2 | cpu → poseidon2: 순열당 메시지 하나 |
INPUT_DIGEST, INPUT_READ | cpu ← input: 입력 다이제스트가 흡수하는 워드와 프로그램의 읽기 |
PUBLIC_DIGEST, PUBLIC_READ | cpu ← public: 공개 세그먼트에 대한 동일한 쌍 |
KECCAK, SHA256 | cpu → 선택적 해시 테이블 |
BLIND | 모든 테이블 → 고정된 순환에서 다음 테이블: 첫 번째 행에서 각각 무작위 값 하나(제약 집합 7) |
버스는 **로그 도함수 논증(logarithmic-derivative argument, LogUp)**으로 검사됩니다. 테이블이 커밋된 뒤 무작위 값 α가 추출되고, 각 테이블은 자신이 보내거나 받는 메시지에 대해 ±1/(α − m)의 누적합을 유지합니다. 보낸 메시지가 모두 수신되었을 때, 즉 양쪽의 멀티셋이 같을 때에만 합이 정확히 상쇄됩니다. 위조된 메시지가 하나라도 있으면 잔여가 남고, 무작위 α는 압도적인 확률로 이를 드러냅니다. 메모리 버스는 이와 가까운 방식인 멀티셋 순열 검사를 사용합니다.
각 테이블은 누적합의 최종값인 **터미널(terminal)**을 공개하고, 검증자는 터미널들의 합이 0인지 검사합니다. 제약 집합 6까지는 터미널이 해당 테이블 내용의 검사 가능한 함수였습니다. 제약 집합 7부터는 모든 테이블이 다섯 개의 추가 열을 가지며, 첫 번째 행에서 무작위 값 하나를 BLIND 버스로 보내고 이웃 테이블의 값을 받습니다. 각 터미널은 챌린지 체(challenge field)의 균등한 원소만큼 이동하고, 합은 0으로 유지됩니다.
다이제스트 버스와 읽기 버스는 의도적으로 별개의 쌍입니다. LogUp은 소비자별이 아니라 메시지별로 균형을 맞추므로, 하나의 공유 버스를 쓰면 증명자가 다이제스트에서 워드를 빼면서도 그 워드의 읽기는 여전히 성공시킬 수 있게 됩니다.
4. 트레이스를 외부 세계에 고정하기
트레이스에서 서른다섯 개의 **공개 값(public values)**이 나오며, 모두 CPU 테이블에 있습니다.
| 값 | 내용 |
|---|---|
| 1 | 진입점 pc |
| 1 | 티어 |
| 8 | 출력 워드 (번들의 경우 다이제스트 D) |
| 8 | hc, 프로그램 다이제스트로, 프로그램 테이블의 워드로부터 트레이스 내부에서 계산됩니다 |
| 8 | H_IN, 비공개 입력 다이제스트로, 새로 생성한 128비트 솔트를 앞에 붙인 입력에 대해 계산됩니다 |
| 8 | H_PUB, 공개 세그먼트 다이제스트로, 솔트가 없습니다 |
| 1 | GAS_LIMIT, 증명자가 선언하는 가스 상한입니다 (제약 집합 8) |
hc는 회로 안에서 계산되므로, 다른 프로그램에 대한 증명이 번들 게스트에 대한 증명으로 통과할 수 없습니다. 검증자는 명령어 워드를 전혀 보지 못하며, hc를 자신이 기대하는 다이제스트와 비교합니다. H_IN에는 솔트가 있으므로, 입력을 추측할 수 있는 상대에게도 입력을 드러내지 않은 채 비공개 입력에 바인딩됩니다. H_PUB에는 솔트가 없으므로, 검증자는 이미 가지고 있는 워드로부터 이를 다시 계산할 수 있습니다. 번들의 경우 그 워드는 트랜잭션 바인딩, 즉 증명 바이트를 비운 트랜잭션에 대한 blake3("rand-tx-bind-1" ‖ chain_id ‖ tx)이므로, 목적지, 메모, 봉투 또는 체인 ID가 다른 트랜잭션에 복사한 증명은 검증을 통과하지 못합니다. chain 20(제네시스 binding_domain: 1)에서는 blake3("rand-tx-bind-2" ‖ genesis_hash ‖ chain_id ‖ tx)이므로, 같은 체인 ID를 쓰더라도 다른 체인에서는 증명이 검증되지 않습니다. 이 워드는 공개 입력이므로 회로에서는 바뀐 것이 없습니다. 인증 증명은 같은 여덟 워드에 대해 만들어집니다. chain 20의 호출 증명도 같은 방식으로 바인딩됩니다. 그 세그먼트는 프로그램의 배포 시점 공개 워드 뒤에 호출 바인딩 여덟 워드(rand-call-bind-2, chain 20 이전에는 rand-call-bind-1)가 이어지는 형태이므로, 다른 수수료 번들 아래로 복사한 호출 증명은 검증되지 않습니다.
가스는 실행의 무게를 재는 머신의 척도이며, 제약 집합 8부터 회로 안에 들어 있습니다. CPU 테이블에는 1에서 시작해 행마다 1을 더하고, Poseidon2 흡수 행에는 3, SHA256 행에는 64, KECCAK 행에는 192를 더하는 GAS 열이 있습니다. HALT 행에서 회로는 차이값의 범위 검사된 네 개의 림브를 통해 GAS ≤ GAS_LIMIT를 증명하며, 이 림브는 공개되지 않습니다. 상한은 공개되어 있지만 실행이 소비한 가스는 공개되지 않습니다. 검증자는 키를 만들기 전에, 증명 자체의 헤더가 필요로 할 수 있는 값을 넘는 상한을 거부합니다.
gas_max(t, k, s) = (2ᵗ − 1) + 2ᵗ⁻² + 191·(2ᵏ / 32) + 63·(2ˢ / 64)
여기서 k와 s는 각각 keccak 테이블과 sha256 테이블의 선언된 로그 높이이며, 테이블이 없으면 해당 항은 0입니다. 해시가 없는 티어 10 헤더의 상한은 1,279이고 티어 14 헤더의 상한은 20,479입니다. 체인은 호출의 가격을 그 호출이 선언한 상한으로 매깁니다(수수료).
5. 규칙을 다항식 항등식으로 바꾸기
모든 열은 Goldilocks 필드의 숫자 목록이며, 이는 p = 2⁶⁴ − 2³² + 1을 법으로 하는 정수입니다. n개의 숫자로 이루어진 목록은 차수가 n 미만인 다항식과 같은 것입니다. 즉, 그 숫자들을 필드의 부분군 {1, ω, ω², …} 위에 늘어놓았을 때 그것들을 지나는 다항식입니다. Goldilocks에는 2³²까지 2의 거듭제곱 크기의 부분군이 모두 있으므로, 이 변환은 고속 푸리에 변환으로 이루어집니다.
그러면 머신의 규칙은 열 다항식 사이의 방정식이 됩니다. “프로그램 카운터는 명령어가 점프하지 않는 한 4씩 증가한다”는 도식적으로 다음과 같이 됩니다.
is_jump(x)·(pc(ωx) − target(x)) + (1 − is_jump(x))·(pc(ωx) − pc(x) − 4) = 0
이는 부분군의 모든 x에 대해 성립하며, pc(ωx)는 “다음 행”을 뜻합니다. 한 테이블의 이러한 규칙 전체가 그 테이블의 AIR(대수적 중간 표현)입니다. CPU와 ALU의 규칙은 차수 8에 이르고, 나머지는 2에서 4에 머뭅니다.
부분군의 모든 점에서 0인 다항식은 Z(x) = xⁿ − 1로 나누어떨어집니다. 증명자는 모든 규칙을 무작위 가중치로 결합해 하나의 다항식 C(x)로 만들고 Z(x)로 나눕니다. 모든 규칙이 모든 행에서 성립하면 몫은 진짜 저차 다항식입니다. 한 규칙이 한 행에서라도 실패하면 그렇지 않습니다. “16,000 사이클 실행의 모든 단계가 올바랐는가?”는 “이 함수는 저차 다항식에 가까운가?”가 되었고, 이 질문은 무작위로 고른 몇 개의 값을 살펴보는 것으로 답할 수 있습니다.
6. 질문이 알려지기 전에 커밋하기
증명자는 검증자의 무작위 질문을 보기 전에 자신의 다항식을 확정해야 하며, 그렇지 않으면 답을 맞춤 제작할 수 있습니다. 증명자는 모든 열을 테이블보다 여덟 배 큰 도메인(블로업 8, 코드율 1/8) 위에서 평가하고, 그 평가값에 대한 Merkle 루트를 공개합니다. 이 중복성은 Reed–Solomon 코드입니다. 저차 다항식은 그 값의 임의의 8분의 1만으로 결정되므로, 트레이스의 단 한 행이라도 바꾸는 부정직한 증명자는 일관성을 유지하려면 확장된 코드워드의 대부분을 바꿔야 하고, 무작위 표본 검사는 그 손상된 곳을 찾아냅니다.
Merkle 트리는 Goldilocks 위의 Poseidon2를 사용합니다. 폭 8, S-박스 x⁷, 완전 라운드 4회, 부분 라운드 22회, 다시 완전 라운드 4회이며, 다이제스트는 네 개의 필드 원소(약 256비트)입니다. 모든 리프는 네 개 원소의 새로운 무작위 값과 함께 해시되므로, 루트는 그 아래의 값에 대해 아무것도 드러내지 않습니다. 같은 순열이 머신 내부에서 노트 커밋먼트와 널리파이어 해시에 쓰입니다. 86개의 라운드 상수는 노드 소스(poseidon2_constants.rs)에 리터럴 테이블로 들어 있으므로, 어떤 라이브러리 업그레이드로도 바꿀 수 없습니다.
7. FRI로 표본 검사하기
이제 검증자는 Merkle 루트를 가지고 있습니다. 검증자는 먼저 몫을 트레이스에 묶습니다. 테이블 도메인 밖의 무작위 점 z를 고르고, 그 점에서 모든 열의 값과 몫의 값을 요청한 뒤, C(z) = Q(z)·Z(z)를 직접 확인합니다. 남은 것은 커밋된 함수들이 실제로 저차 다항식임을 확인하는 일이며, 이것이 FRI가 검사하는 내용입니다.
FRI는 접기(folding)로 작동합니다. 다항식을 짝수 부분과 홀수 부분으로 나눕니다. f(x) = fₑ(x²) + x·fₒ(x²). 검증자가 준 무작위 β에 대해 f′(y) = fₑ(y) + β·fₒ(y)는 절반 크기의 도메인 위에서 절반의 차수를 가집니다. 증명자는 f′에 커밋하고, 새로운 β를 받아 다시 접으며(Rand는 라운드마다 최대 여덟 배로 접습니다), 상수만 남을 때까지 이를 반복해 그 상수를 평문으로 보냅니다. 쿼리는 첫 번째 레이어에서 무작위 위치 하나를 고르고 그것을 따라 내려가면서, 모든 레이어에서 위에서 열어 본 값들이 그 아래의 값으로 접히는지 확인합니다. 저차에서 멀리 떨어진 함수는 이 모든 약속을 일관되게 유지할 수 없습니다.
Rand는 80개의 쿼리를 실행합니다. 증명된 근접성 갭 정리 아래에서 각 쿼리는 부정직한 증명자에게 최대 9/16의 확률만 남기므로, 80개의 쿼리는 (9/16)⁸⁰ ≈ 2⁻⁶⁶·⁴를 줍니다. 배포된 STARK들이 의존하는 ethSTARK 추측 아래에서는 각 쿼리가 1/8을 남기며, 이는 훨씬 작습니다.
8. 대화 제거하기
위의 모든 것은 대화로 설명되었지만, 트랜잭션은 어떤 validator도 혼자서 검사할 수 있는 하나의 메시지여야 합니다. Fiat–Shamir 변환은 검증자를 제거합니다. 증명자는 각 “무작위” 챌린지를 Poseidon2 듀플렉스 스펀지로, 지금까지 커밋된 모든 것의 해시로 계산합니다. 어떤 커밋먼트를 바꾸면 그 뒤의 모든 챌린지가 바뀌므로, 증명자는 챌린지에 맞춰 커밋먼트를 고를 수 없습니다.
이로 인해 남는 틈은 두 가지 추가 조치로 닫습니다.
- 확장체.
2⁶⁴개 원소의 필드는 챌린지를 뽑기에는 너무 작습니다. 부정직한 증명자는 챌린지가 다항식의 소수의 나쁜 값 중 하나에 떨어지기만 하면 되고, 쿼리를 몇 번 실행하든 약N/2⁶⁴의 확률로 성공합니다. 그래서 트레이스는 Goldilocks에 머물지만, 모든 챌린지는 그 이차 확장체, 즉 약2¹²⁸개의 원소에서 뽑습니다. - 그라인딩. 쿼리 위치가 도출되기 전에, 증명자는 트랜스크립트와의 해시가 20개의 0 비트로 시작하는 논스를 찾아야 하며, 이는 약 100만 번의 해시입니다. 정직한 증명자는 이 비용을 한 번만 치릅니다. 위조에 걸리지 않을 때까지 쿼리 위치를 다시 뽑는 부정직한 증명자는 시도할 때마다 이 비용을 치릅니다. 이것은 Rand 전체에서 유일한 작업 증명이며, 합의와는 아무 관계가 없습니다.
합산하면, 쿼리에서 66.4비트에 그라인딩의 20비트를 더해 86비트가 증명되며, 추측 아래에서는 약 100비트이고 이때는 확장체의 항이 한계입니다.
9. 영지식은 어디서 오는가
STARK는 그 자체로는 영지식이 아닙니다. 쿼리 위치에서 열리는 값은 트레이스 다항식의 값이며, 주의하지 않으면 정보가 새어 나갑니다. Rand의 구성은 네 개의 층을 더합니다.
- 커밋된 모든 다항식은 무작위화됩니다(Plonky3의 hiding FRI는 네 개의 무작위 코드워드를 섞어 넣습니다). 그래서 열린 80개의 값은 트레이스를 본 적 없는 사람도 분포상 똑같이 재현할 수 있습니다.
- 모든 Merkle 리프는 솔트가 적용되므로, 루트와 인증 경로는 아무것도 드러내지 않습니다.
- 증명자의 무작위성은 증명마다 운영체제에서 새로 뽑으므로, 같은 결제에 대한 두 증명은 서로 무관해 보입니다.
- 선언된 모든 테이블은 최소
2⁷개의 행을 가지며 모든 LogUp 종단값은 블라인딩됩니다(제약 집합 7). 증명은 각 테이블을 80개의 쿼리 지점과 두 개의 도메인 밖 지점에서 열고, hiding 커밋먼트는 테이블의 행 수만큼의 무작위 행을 섞어 넣으므로, 테이블은 행이 최소 82개일 때에만 숨겨집니다. 128은 이를 만족하는 가장 작은 2의 거듭제곱입니다.
4계층은 정정입니다. 2026-09-27 검토에서는 프로덕션 증명으로부터 입력 워드가 31개 이하인 호출의 비공개 입력을 복구했고, 짧은 프로그램 테이블에서 프로그램의 fetch 횟수를 읽어냈으며, 번들의 실제 입력 슬롯과 더미 슬롯을 블라인딩되지 않은 터미널로 구별해 냈습니다. 해당 증명은 chain 15 및 그 이전 체인에서 만들어진 것입니다. Chain 16(constraint set 7, 분기 없는 번들 게스트 적용)에서 이 세 가지가 모두 해결되었고, chain 18에서 20까지 수정 사항이 유지됩니다.
보장 수준은 랜덤 오라클 모델에서의 통계적 영지식입니다. 증명이 숨기지 못하는 것은 정확히 다음과 같습니다. 증명의 티어, 선언된 테이블 높이, 그리고 가스 한도를 포함한 공개 값입니다. 선언된 keccak 또는 sha256 높이가 0이면 실행 중에 해당 해시를 한 번도 사용하지 않았다는 뜻이므로, 호출 증명의 헤더를 보면 그 호출이 해시를 사용했는지, 그리고 최소 높이를 넘는 경우에는 2배 이내의 정확도로 몇 번 사용했는지를 알 수 있습니다. 번들의 공개 값과 필드는 앵커, 네 개의 널리파이어, 네 개의 커밋먼트, 수수료, 소각량, 소각 자산, 시간, 인가 커밋먼트, 트랜잭션 바인딩입니다. 전송이 옮기는 자산은 비공개 입력이므로, zUSD 지급과 RAND 지급은 온체인에서 똑같아 보입니다. hc는 결정적이므로, 누구나 어떤 증명이 번들 게스트에 관한 것인지(가치를 옮기는 모든 증명이 해당됩니다), 또는 호출의 경우 배포된 어느 프로그램을 실행했는지 알 수 있습니다.
10. 검증자가 확인하는 사항
Machine::verify_public(hc, words, proof)은 비용이 낮은 순서대로 다음을 실행합니다.
- 증명이 선언한 티어와 테이블 높이가 범위 내에 있는지(
check_declared_heights)를 그것들로부터 어떤 크기도 산정하기 전에 확인하므로, 잘못된 헤더는 수 초가 걸리는 키 빌드가 아니라 몇 번의 비교만으로 거부됩니다. 번들은 두 해시 테이블 없이 티어 14를 선언해야 하며, 인가 증명은 티어 10을 선언해야 합니다. - 공개 값은 35개이며 각각 정규 필드 원소여야 합니다. 또한
pv[HC] = hc이고,pv[TIER]는 증명의 티어와 같으며, 모든 출력 워드는2³²미만이고,GAS_LIMIT는 헤더의gas_max이하여야 합니다. - 배치 STARK는 프로그램별이 아니라 shape
(tier, program, input, keccak, sha256, public heights)별로 캐시된 검증자 키에 대해 검증됩니다. H_PUB는words의 다이제스트와 같아야 하며, 번들의 경우 이는 노드가 직접 계산한 트랜잭션 바인딩입니다.
이 모든 확인에 앞서, 노드는 제출되거나 가십으로 전파된 모든 증명에 대해 정규 인코딩 여부를 점검합니다. 증명의 FRI 폴딩 스케줄은 헤더가 선언한 차수에 대해 정직한 증명자가 만드는 것과 같아야 합니다. fullnode v0.6.8(감사 v6, ZKV-4)부터 이 점검은 먼저 25비트를 초과하는 선언 차수를 거부합니다(MAX_HONEST_DEGREE_BITS, 메모리 테이블의 2²⁴ 상한에 영지식을 위한 두 배 확장을 더한 값으로, 수락되는 증명이 가질 수 있는 최대치입니다). 이 상한이 생기기 전에는 매우 큰 차수를 선언한 헤더가 노드로 하여금 이벤트 루프에서 방대한 폴딩 스케줄을 다시 유도하게 만들었고, 발신자가 원하는 만큼 오래 노드를 붙잡아 둘 수 있었습니다. 검증자는 어차피 그 증명을 거부했을 것이므로, 이 상한이 바꾸는 것은 거부하는 데 드는 비용뿐입니다. 이제 점검 내부에서 패닉이 발생하면 노드가 멈추지 않고 해당 트랜잭션 하나만 거부됩니다.
그런 다음 원장은 bad = 0으로 번들 다이제스트 D를 다시 계산하여 증명의 여덟 개 출력 워드와 비교합니다. chain 20에서는 또한 번들 증명의 GAS_LIMIT가 제네시스의 bundle_gas_limit인 20,479와 정확히 같고, 인가 증명의 값은 1,279일 것을 요구하므로, 모든 전송이 동일한 값을 선언합니다. 키가 준비된 상태에서는 전체 검증에 약 16 ms가 걸립니다.
파라미터와 선택 이유
아래의 모든 수치는 합의 사항입니다. 검증자가 이를 확인하며, 다른 값으로 만든 증명은 검증을 통과하지 못하고, 값을 바꾸는 것은 새로운 constraint set이자 하드 포크입니다(Constraint sets). FRI 프로파일은 증명이 아니라 제네시스 파일(chain 20에서는 fri_profile: production)이 정합니다.
| 매개변수 | 값 | 이 값을 택한 이유 |
|---|---|---|
| 기저 체 | Goldilocks, p = 2⁶⁴ − 2³² + 1 | 산술이 몇 개의 기계 명령어로 끝나는 64비트 소수이며, 2³²까지 2의 거듭제곱 크기마다 곱셈 부분군을 갖추고 있어 열을 다항식으로 바꾸는 일이 고속 푸리에 변환이 됩니다. 트레이스가 이 체 위에 놓이는 것은 가장 저렴한 체이기 때문입니다. |
| 챌린지 체 | 2차 확장체 F_p² (≈ 2¹²⁸) | Goldilocks 자체에서 챌린지를 뽑으면 약 N/2⁶⁴의 건전성 하한이 남으며, 쿼리를 아무리 늘려도 이를 낮출 수 없습니다. 확장체를 쓰면 이 항이 최대 2²⁵행의 트레이스에 대해 2⁻¹⁰⁰ 이하로 내려갑니다. 더 큰 체의 비용은 챌린지만 치르며, 트레이스는 치르지 않습니다. 128비트가 요구될 경우를 대비한 여유분으로는 3차 확장체(≈ 2¹⁹²)가 지정되어 있습니다. |
| 블로업 | 8 (비율 ρ = 1/8, log_blowup = 3) | 두 가지 이유가 동시에 작용합니다. CPU 테이블과 ALU 테이블에는 차수 8의 규칙이 있고, 영지식 플래그를 켜면 몫의 차수가 9가 되는데, 이는 블로업 8이 수용할 수 있는 최대치입니다. 차수가 더 높은 규칙을 쓰면 블로업 16이 필요해집니다. 또한 쿼리 하나가 부정행위를 잡아내는 힘은 비율에 따라 달라집니다. 쿼리당 증명된 값은 (1 + ρ)/2 = 9/16이고, 추측된 값은 ρ = 1/8입니다. 블로업을 키우면 쿼리 수를 줄일 수 있지만, 그 대가로 prover가 계산하고 메모리에 보관해야 하는 저차 확장이 비례하여 커집니다. |
| FRI 쿼리 | 80 | 증명된 건전성을 약 86비트로 유지하는 수치입니다. 한때 27로 줄인 적이 있으며(제약 집합 3과 4), 이는 추측 기준 100비트 목표(3·27 + 20 = 101)를 충족했지만 증명된 비트는 약 42에 그쳤습니다. 2026-09-12 zk 감사(발견 사항 ZM1)에서 80으로 되돌렸습니다. 증명 크기의 대부분은 쿼리가 차지하며, 번들 증명이 ≈ 1.50 MB인 것도 이 때문입니다. 증명 가능한 100비트를 달성하려면 쿼리 97개가 필요합니다. |
| 그라인딩 | 20비트, 쿼리 단계 | 쿼리 위치를 뽑기 전에 prover는 해시가 0 비트 20개로 시작하는 논스를 찾아야 합니다. 약 100만 번의 해시이며, 정직한 prover는 이 비용을 한 번만 치르고, 부정행위자는 위치를 다시 뽑을 때마다 치릅니다. 이는 20비트를 저렴하게 더해 줍니다. 그라인딩을 더 늘리면(34비트면 증명 가능한 100비트도 얻습니다) 정직한 증명이 모두 느려집니다. 커밋 단계에는 그라인딩이 없습니다. |
| 폴딩 아리티 | 라운드당 최대 8 (max_log_arity = 3) | 2가 아니라 8로 폴딩하면 FRI 레이어 수가 3분의 1이 되므로, 쿼리마다 여는 Merkle 경로가 줄어 증명이 작아지고, 레이어당 작업이 조금 늘어납니다. |
| 최종 다항식 | 상수 (log_final_poly_len = 0) | FRI는 끝까지 폴딩하여 값 하나를 평문으로 보냅니다. verifier 입장에서 가장 단순한 종료 검사입니다. |
| 해시 (회로, Merkle, 트랜스크립트) | Poseidon2, 너비 8, S-box x⁷, 4 + 22 + 4 라운드, rate 4, capacity 4 | 체 산술로서 저렴하도록 설계된 해시이므로, 증명 안에서 해시하는 일(커밋먼트, 널리파이어, Merkle 경로, 프로그램 다이제스트)이 수천 번의 비트 연산이 아니라 몇 개의 행으로 끝납니다. S-box 지수는 p − 1 = 2³²·3·5·17·257·65537과 공약수를 가지면 안 되며, 7이 이를 만족하는 가장 작은 값입니다. rate 4에 너비 8이면 약 256비트의 네 원소 다이제스트가 됩니다. 86개의 라운드 상수는 노드의 리터럴 테이블(poseidon2_constants.rs)에 들어 있습니다. |
| 다이제스트 너비 | 체 원소 4개 (≈ 256비트) | 프리이미지 역할(널리파이어, 커밋먼트)에는 충분합니다. Grover 공격에 대해 128비트입니다. 그러나 트랜스크립트에서 128비트 양자 충돌 저항성에는 충분하지 않습니다(2²⁵⁶/³ ≈ 2⁸⁵). 백서의 설계는 384비트 트랜스크립트 해시를 요구하지만, 현재 운영 중인 체인은 이를 채택하지 않았습니다. |
| 은닉 | FRI의 무작위 코드워드 4개, Merkle 리프당 솔트 원소 4개, 증명마다 새로 얻는 OS 난수 | 열린 80개 위치와 Merkle 경로를 공개 값만으로 시뮬레이션할 수 있게 하는 최소 조건이며, 이로써 증명은 통계적 영지식이 됩니다. 같은 결제에 대해 두 번 만든 증명은 서로 무관해 보입니다. |
| 테이블 하한 | 선언된 모든 테이블에 대해 2⁷행 (제약 집합 7) | 증명은 각 테이블에서 쿼리 지점 80개와 도메인 밖 지점 2개, 합계 82개를 열며, h행짜리 테이블은 무작위 행 h개로 블라인딩됩니다. 64행은 너무 적고 128이 다음 2의 거듭제곱입니다. 쿼리 수가 126을 넘으면 2⁸이 필요합니다. |
| 터미널 블라인드 | 테이블당 F_p²의 값 하나, BLIND 버스 위 (제약 집합 7) | LogUp 터미널은 원본 트레이스로부터 계산되므로, 블라인드가 없으면 해당 테이블에 대한 솔트 없는 커밋먼트가 됩니다. 블라인드가 확장체 원소인 이유는, 기저 체 좌표 하나만으로는 이동이 얇은 집합에 갇혀 후보 증인을 그에 대해 검사해 볼 수 있기 때문입니다. |
| 가스 가중치 | 행당 1, Poseidon2 absorb 행당 3, SHA256 행당 64, KECCAK 행당 192 (제약 집합 8) | 각 가중치는 해당 행이 다른 테이블로 끌어들이는 행 수를 센 것입니다. absorb 행은 32행짜리 순열 하나, SHA256 행은 466열 64행, KECCAK 행은 2,612열 32행과 메모리 100행입니다. |
| 최대 제약 차수 | 8 | 위의 블로업으로 정해집니다. 각 테이블의 차수는 테스트로 고정되어 있어, 새 규칙이 조용히 이를 초과할 수 없습니다. |
| 티어 | 10, 12, 14, 16, 18, 20 | 테이블의 크기는 각각 이전의 네 배인 여섯 개의 고정 단계로 정해지며, 증명은 티어만 드러냅니다. 즉 사이클 수가 아니라 실행 길이에 해당하는 거칠고 패딩된 구간입니다. 모든 번들은 티어 14, 모든 인증 증명은 티어 10이며, 호출은 fullnode v0.5.6부터 티어 14로 상한이 정해져 있습니다. |
| 최대 트레이스 | 2²⁴행 (영지식 비트를 켜면 2²⁵) | 확장체 항이 증명되는 크기입니다. 이보다 긴 트레이스는 성립하지 않는 건전성 주장 아래에서 증명되지 않고 거부됩니다. |
| 공개 값 | 체 원소 35개 | 진입 pc, 티어, 8워드 출력, 8워드 다이제스트 세 개(hc, H_IN, H_PUB), 선언된 가스 한도입니다. 프로그램, 비공개 입력, 공개 입력, 작업량의 상한을 명시하는 가장 작은 진술입니다. 제약 집합 7까지는 34개였습니다. |
| 증명 크기 상한 | 기본 2 MiB, chain 20에서는 4 MiB (max_proof_bytes) | 2 MiB는 해시 테이블 없이 배포된 모든 게스트의 증명보다 크고, keccak을 포함하는 증명(≈ 3.3 MB)보다 작습니다. chain 20의 제네시스는 chain 17 |
이 수치들이 합쳐서 의미하는 것: 증명된 건전성은 86비트, ethSTARK 추측 하에서는 약 100비트(여기서는 쿼리가 아니라 확장체가 한계입니다), 양자 적대자에 대해서는 약 85비트이며, 이는 256비트 트랜스크립트 해시로 정해집니다. 백서는 두 건전성 수치를 의도적으로 나란히 싣습니다. 배포된 모든 FRI-STARK는 마지막 비트들을 추측에 의존하므로, 배포 규모를 산정하는 독자는 어떤 가정이 몇 비트를 떠받치는지 알아야 하기 때문입니다.
증명에 대한 증명
번들 증명은 약 1.50 MB로 무겁고, 1.37 MB의 인증 증명이 그 옆에 함께 실리며, 모든 노드가 이를 전부 저장합니다. 구현된 해법은 재귀입니다. 진술이 “나는 이 증명들을 검증했다”인 증명입니다. fullnode는 이를 위해 더 작은 두 번째 머신인 재귀 VM을 갖고 있으며, 이 머신의 기본 워드는 Goldilocks 원소이고 명령어는 verifier 자체의 연산(체 산술, Poseidon2, Merkle 및 FRI 단계)입니다. 집계 프로그램은 번들 증명 N개의 전체 검증을 재현하고, 체인, aggregator, 논스에 묶인 공개 값 위의 다이제스트를 게시합니다. 그 안에서 번들 증명 하나를 검증하는 데 제약 집합 6에서는 1,968,619행, 제약 집합 7에서는 2,044,506행이 들었으며, 제약 집합 8에서는 다시 측정하지 않았습니다. 구현되어 있으나 지금까지의 모든 체인에서 제네시스에 의해 꺼져 있고, chain 20도 마찬가지입니다. 집계 프로그램은 번들 증명만 검증하므로, 인가를 분리한 체인에서는 인증 증명이 그대로 남습니다. 또한 프로덕션 증명은 계획보다 더 많은 메모리를 필요로 합니다. 제약 집합 7에서 측정한 결과 133 GB 이상입니다 (Upcoming: block aggregation).
이 방식이 주장하지 않는 것
- 양자 내성 마진은 128비트가 아니라 약 85비트입니다.
n비트 해시에 대한 양자 충돌 탐색의 비용은 약2ⁿ/³입니다. 백서의 설계는 384비트 트랜스크립트 해시를 요구하지만, 현재 운영 중인 체인은 256비트 Poseidon2 다이제스트를 사용합니다. 이를 넓히려면 새로운 제약 집합이 필요하며 하드 포크를 거쳐야 합니다. - 100비트는 추측이고, 86비트는 증명된 값입니다. 증명된 수치를 100으로 올리려면 97개의 쿼리가 필요하며, 이는 증명당 쿼리 데이터가 약 5분의 1 늘어나는 비용입니다.
- Poseidon2 상수는 시드로 추출한 값이며, 공개된 표준 인스턴스가 아닙니다. 이에 대해 알려진 공격은 없지만, 표준 상수 집합을 채택하면 체인상의 모든 해시가 바뀝니다.
- 일부 명제는 증명 안이 아니라 증명 옆에서 검증됩니다.
TIER공개 값은 검증자가 증명의 형태와 대조하지만 회로가 제약하지는 않으며(감사 ZL1), 번들 게스트의 오염(taint) 규율은 구조적 논증, 약화된 게스트 하니스, 변이 퍼징으로 뒷받침될 뿐 기계로 검증되지는 않았습니다. - 증명의 헤더는 숨겨지지 않습니다. 티어와 두 해시 테이블의 높이가 평문으로 드러나므로, 호출 증명을 보면 해당 호출이
KECCAK을 썼는지SHA256을 썼는지 알 수 있습니다. 두 테이블을 모두 패딩해 넣는 prover 옵션이 있지만 어떤 지갑도 이를 설정하지 않으며, 이 옵션을 쓰면 티어 10 증명이 약 1.3 MB에서 약 3.6 MB로 커집니다. POSEIDON2시스콜은 가변 길이 입력에 대해 단사(injective)가 아닙니다. 모든 커밋먼트, 널리파이어, 루트, 프로그램 다이제스트가 이에 의존하므로 현재 그대로 유지합니다. 아직POSEIDON2_LEN을 사용하는 배포된 게스트는 없습니다.- 증명은 느리며, 송신자 또는 송신자가 짝지은 prover가 수행합니다. 번들 하나에 노트북 CPU 기준 약 100 s, 5.7 GB가 소요됩니다. GPU 커널은 2026-09-28에 처음으로 GPU(NVIDIA H100)에서 실행되어 CPU 참조 구현과 일치했으며, 지갑은 여전히 CPU에서 증명합니다.
더 읽을거리
- 백서의 “The Zero-Knowledge Scheme: How a Rand Proof Works” 절(
randprotocol.tex)은 동일한 일곱 가지 구성 요소를 건전성 정리와 함께 설명합니다. - 구현 논문(
randprotocol_implementation.tex)은 명령어 집합과 시스콜, 번들 게스트, 스펀지와 다이제스트, 모든 테이블과 버스, 머신 구성, 재귀 VM을 규정합니다. - 코드: fullnode의
crates/randprotocol-zkvm/src/—emulator.rs,tables/,machine.rs,gas.rs,guests.rs,hash.rs,poseidon2_constants.rs.