RISC Zero과 친해지기: zkVM 이야기

RISC Zero

Overview

이 문서는 RISC Zero (https://dev.risczero.com) 문서를 참고하여 실습 내용 및 관련 내용을 정리한 문서입니다.

Getting Started

image.png

위 그림은 RISC Zero를 사용하여 ZK 관련 로직을 사용하는 방법을 나타낸 그림입니다.

  1. zkVM을 위한 어플리케이션을 만들고
  2. zkVM을 위한 Proof를 생성합니다.
  3. 위에서 만든 Proof를 검증할 수 있도록 Onchain 로직과 결합합니다.

Writing your zkVM Application

Installation

Install

rzup 은 RISC Zero 툴체인 설치파일 입니다. cargo 를 사용하여 별도로 설치할 수 있지만 rzup 을 사용하여 설치하는 것을 추천합니다.

  1. 아래 커맨드를 실행하여 rzup 을 설치합니다.

     curl-L https://risczero.com/install | bash
        
    
  2. rzup 명령어를 수행하여 RISC Zero를 설치합니다.

     rzup
    

image.png

Manual Installation

cargo 를 이용한 수동 설치는 아래 과정을 따릅니다.

  1. 아래 명령어를 수행하여 RISC Zero를 설치합니다.

     # For ARM macOS
     cargo install cargo-binstall
     cargo binstall cargo-risczero
     cargo risczero install 
    
     # For x86-64 macOS
     cargo install cargo-binstall
     cargo binstall cargo-risczero
     cargo risczero build-toolchain
    
  • x86-64 macOS 의 경우 cargo risczero install 대신  cargo risczero build-toolchain 를 수행합니다.

Building zkVM Hello World

Step 1: Create a new project

cargo-risczero 를 사용하여 hello-world 라고 하는 새로운 프로젝트를 생성합니다. 또한 cargo-risczero—-guest-name 옵션을 사용하여 guest 프로그램의 이름을 지정할 수 있습니다.

cargo risczero new hello-world --guest-name hello_guest
cd hello-world

image.png

프로젝트 폴더 내부에서cargo run --release 명령어를 실행하여 프로젝트를 빌드하고 실행할 수 있습니다.

Step 2 (Host): Share private data as input with the guest

zkVM 혹은 prover는 Host에서 실행됩니다. host 코드는 hello-world/host/src/main.rs 에 위치합니다. Host는 prover를 구축하기 전에 ExecutorEnv 라고 하는 실행자 환경을 생성합니다. Host는 실행 전 guest에게 input 값을 제공합니다. guest가 readable memory를 관리하는 실행자 환경에 input 을 추가하여 수행합니다. prover가 프로그램을 실행할 때 입력값에 접근할 수 있게 됩니다.

use risc0_zkvm::{default_prover, ExecutorEnv};

fn main() {
    let input:u32 = 7;
    let env = ExecutorEnv::builder().write(&input).unwrap().build().unwrap();
}

Step 3 (Guest): Read input and commit output

Guest code는 methods/guest/src/main.rs 에 있습니다. 이 코드는 증명될 코드의 일부 입니다. 아래 코드에서 guest는 host의 입력 값을 읽은 다음 receipt의 journal에 커밋합니다.

  • journal
    • zkVM 어플리케이션의 public output 이 포함된 receipt의 일부.
  • receipt
    • receipt 는 guest program의 유효한 실행을 증명합니다. receipt는 receipt claim와 seal로 구성되어 있습니다. receipt claim은 journal과 기타 세부 정보가 포함되어 있으며 프로그램의 public output을 구성합니다. seal은 receipt claim의 유효성을 암호학적으로 증명하는 blob입니다.
use risc0_zkvm::guest::env;

fn main() {
    // read the input
let input:u32 = env::read();

    // do something with the input
    // writing to the journal makes it public
    env::commit(&input);
}

env::commit 함수는 public results를 journal에 커밋합니다. 한번 journal에 커밋이 되면 receipt를 가진 누구나 이 값을 확인할 수 있습니다.

Step 4 (Host): Generate a receipt and read its journal contents

Host가 receipt를 생성하고 journal의 컨텐츠를 추출하는 방법을 알아봅니다.

실제 사용시에 우리는 receipt를 누군가로 부터 획득하여 이를 검증하려 할 것입니다. 그리고 prove 함수는 receipt의 내부 검증을 수행합니다. receipt에서 journal을 추출한 후 host에 println 한줄을 추가하여 stdout으로 이를 출력 할 수 있습니다.

use methods::{HELLO_GUEST_ELF, HELLO_GUEST_ID};
use risc0_zkvm::{default_prover, ExecutorEnv};

fn main() {
let input:u32 = 15 *u32::pow(2, 27) + 1;
let env = ExecutorEnv::builder().write(&input).unwrap().build().unwrap();

    // Obtain the default prover.
let prover = default_prover();

    // Produce a receipt by proving the specified ELF binary.
let receipt = prover.prove(env, HELLO_GUEST_ELF).unwrap().receipt;

    // Extract journal of receipt
let output:u32 = receipt.journal.decode().unwrap();

    // Print, notice, after committing to a journal, the private input became public
    println!("Hello, world! I generated a proof of guest execution! {} is a public output from journal ", output);
}

image.png

Understanding I/O in the zkVM

Setting the Stage

zkVM에서 프로그래밍 하는 것은 host 와 guest, 두 세계간의 데이터를 이동시키는 것 이라고 생각할 수 있습니다. Host는 일반 프로그램과 같이 계산이 수행되고 Guest에서는 zk환경에서의 계산이 수행됩니다.

guest가 zk환경에서 작동하므로 host와 비교했을 때 제한적인 방법으로 데이터를 획득할 수 있습니다. Host 와 guest간 데이터를 보내는 유일한 방법은 Executor Environment 를 통하는 것 입니다. 이러한 데이터 전송은 file descriptors를 통해 이루어 지는데 zkVM에서는  stdinstdoutstderr, ,journal 의 4가지 가 존재합니다.

The zkVM Data Model

zkVM은 public data / private data 의 데이터 모델을 가지고 있습니다. “Public” 의 의미는 journal 에 포함되어 Proof의 일부가 되는 것을 의미하며 “Private”는 host와 guest만 접근 가능 합니다.

Sending data from the host to the guest

stdin file descriptor 는 host에서 guest로 Input data를 보내는데 사용됩니다. Host에서 write, write_slice 메소드를 사용하여 Executor Environment에 데이터를 설정하는 것이 가능합니다. Guest는 이에 대응되는 readread_slice 메소드를 통해 이를 읽을 수 있습니다.

use risc0_zkvm::ExecutorEnv;

let input = "Hello, guest!";
let env = ExecutorEnv::builder().write(&input)?.build()?;

Sending Private data from the guest

stdout ,stderr 모두 guest 에서 host로 private data를 보내는데 사용되며 write 메소드를 통해 host의 stdout 으로 보내는 방법이 간단한 방법입니다.

let data = "Hello, host!";
env::write(&data);

Sending Public data from the guest

공개하기 위한 데이터를 전송하기 위해 journal 으로 보내는 방법이 있습니다. 이는 commit , commit_slice 메소드를 통해 journal에 쓰기가 가능하며 이를 수행하면 Proof에 포함되며 Receipt를 통해 데이터에 접근할 수 있습니다.

let data = "Hello, journal!";
env::commit(&data);

Reading Private data in the host

Guest에서 데이터를 보낸 후 from_slice 메소드를 활용하여 host에서 읽을 수 있습니다.

let result: Type = from_slice(&output)?;

Reading Public data in the host

Public data를 읽는 것은 증명 과정 이후 Receipt 에 접근하는 것으로 읽을 수 있습니다. journal 인스턴스의 decode 메소드를 통해 가능합니다.

// Produce a receipt by proving the specified ELF binary.
let receipt = prover.prove(env, ELF).unwrap().receipt;
// Decode the journal to access the public data.
let public_data = receipt.journal.decode()?;

Sharing data structures between host and guest

Host 와 guest 사이 공통 데이터 구조를 공유하는 방법은 core 모듈을 포함하는 공통 데이터 구조를 포함하는 것 입니다.

JWT Validator 가 좋은 예시입니다.

Proof Composition

Start Building

proof composition 를 사용하기 위해 host에서 add_assumption() 함수를 호출하고 env::verify() 를 guest에서 호출합니다.

How It Works

proof composition은 ReceiptClaim구조체에 assumptions을 추가하고, assumptions을 resolveing 하는 것으로 동작합니다.

image.png

Adding Assumptions

env::verify() 가 guest 프로그램 내부에서 호출 되면 assumption이 ReceiptClaim 에 추가됩니다.

image.png

Resolve an Assumption

proof composition 를 해결하기 위해서 assumptions이 resolve 되어야 합니다. 이는 resolve 를 통해 수행됩니다. 이는 유저가 Prover::prove_with_opts 를 호출할 때 자동으로 호출 됩니다.

image.png

Generating Proofs for your zkVM Application

Proving Options

사용자는 Proof를 생성하기 위해 여러 옵션을 선택할 수 있습니다.

  • dev-mode: 실제 Proof이 생성되지 않음, 프로토타이핑을 위함
  • local proving: 유저의 CPU, GPU를 사용하여 Proof를 생성
  • remote proving: Bonsai를 통해서 Proof를 생성

Dev mode

risc0 프로젝트는 RISC0_DEV_MODE 환경 변수를 설정하여 개발 모드에서 실행되게 할 수 있습니다. 이는 fake receipt creation, pass-through ‘verification’ 를 지원히기 때문에 실제 개발 과정에 영향을 주지 않으면서 개발이 가능하게 합니다. receipts가 개발 모드에서 생성될 때도 journal 에 public outputs이 포함되어 있습니다.

하지만 Proving 과정이 우회되기 때문에 개발 모드에서 실행하여 획득한 recepit 는 실제 모드에서는 동작하지 않습니다.

Local Proving

사용자는 zkVM을 로컬로 실행하여 자체 하드웨어를 통해 Proof를 생성할 수 있습니다. Feature flags 를 통해 CPU 및 GPU를 사용하게 할 수 있습니다.

Remote Proving

Bonsai는 사용자가 Proof 생성에 자체 하드웨어를 사용하지 않고도 zkVM 애플리케이션에 대한 Proof를 생성할 수 있도록 합니다. 사용자는 실행하려는 zkVM 애플리케이션과 해당 프로그램의 입력을 지정하고 Bonsai는 Proof를 반환합니다.


Optimism fault proof 알아보기

Fault Proofs

https://docs.optimism.io/stack/protocol/fault-proofs/explainer

https://specs.optimism.io/fault-proof/cannon-fault-proof-vm.html


Overview

이 문서는 Fault Proof 관련 내용을 스터디 하여 관련 내용을 작성한 문서입니다. 이 문서는 Optimism 에서 작성된 문서를 참고하여 작성하였습니다.

Fault Proof Explainer

Fault Proofs는 Optimistic Rollup 시스템에서 중요한 부분입니다. Fault Proof는 상태에 대한 제안을 허가없이(Permissionless) 제출하고 이를 challenge할 수 있습니다.

Permissionless Proposals

Proposals 혹은 State ProposalsDisputeGameFactory contract를 통해 제출된 체인의 상태에 대한 클레임 입니다. Proposals는 여러 용도로 사용할 수 있지만 일반적으로 최종 사용자가 체인에서 한 행동에 대하여 증명하는데 일반적으로 사용됩니다.

Fault Proofs는 이러한 Proposals를 누구나 제출할 수 있으며 아무나에 의해 확정될 수 있습니다.

image.png

Permissionless Challenges

누구나 Proposals 를 제출할 수 있기 때문에 잘못된 Proposals에 이의를 제기할 수 있는 것이 중요합니다. Optimistic Rollup에서는 사용자의 Proposals가 잘못되었다고 생각하는 경우 이의를 제기할 수 있는 기간이 있습니다. 이 이의는 권한이 필요하지 않으며 누구나 제출할 수 있습니다. 모든 사용자는 체인에 대한 노드를 실행하고 challenger 도구를 사용하여 이러한 분쟁 프로세스에 참여할 수 있습니다.

FP System Components

Fault Proof System은 Fault Proof Program (FPP), Fault Proof Virtual Machine (FPVM), dispute game protocol 의 3가지로 구성됩니다. 이러한 구성 요소는 네트워크에서 악의적이거나 오류가 있는 활동에 challenge를 하여 시스템 내의 신뢰와 일관성을 유지합니다.

Fault Proof Program

FPP의 주요 기능은 L1 입력에서 L2 출력을 검증하기 위해 rollup state-transition 을 실행합니다. 이 검증 가능한 출력은 L1 의 분쟁이 있는 출력을 해결하는데 사용될 수 있습니다.

FPP는 op-node (Consensus Layer) 와 op-geth (Execution Layer) 의 결합으로 구성되어 있으며 이 같은 구성 때문에 단일 프로세스에서 consensus와 execution의 일부를 모두 가지고 있습니다. 이 는 일반적으로 op-geth 를 호출할 때 HTTP를 통해 호출되는 것이 아니라 직접적인 메소드 호출이 가능하다는 것입니다.

FPP는 동일한 입력 데이터를 사용하여 두번 호출 해도 동일한 출력 뿐만 아니라 동일한 프로그램 실행 추적이 가능하도록 결정론적 방식으로 실행할 수 있도록 설계 되었습니다. 이를 통해 분쟁 해결 프로세스의 일부를 온체인 VM 에서 실행이 가능합니다.

모든 데이터는 Preimage Oracle API를 통해서 가져오며 Preimage 는 온체인일때 FPVM을 통해 제공되거나 혹은 node에서 필요한 데이터를 다운로드할 수 있는 기본 host 구현을 통해 제공될 수 있습니다.

Fault Proof Virtual Machine

FPVM은 FPP와 분리되어 구현되어 있습니다. 이 같은 특성 때문에 두 컴포넌트는 병렬적으로 업그레이드가 가능합니다.

FPVM 내에서 실행되는 FPP (Client-side)는 L2 state-transition 을 표현하는 부분이며 FPVM과 FPP간 인터페이스는 표준화되어 문서화 되어 있습니다.

분리되어 구현하였기 때문에 FPVM은 최소한의 구현만으로 유지됩니다. EVM op-code 추가와 같은 Ethereum 프로토콜에 대한 변경은 FPVM에 영향을 끼치지않습니다. 대신 프로토콜이 변경되면 FPP를 업데이트하여 새로운 state-transition을 적용해 주어야 합니다.

FPVM은 lower-level 의 명령어 실행을 담당합니다. 또한 FPP를 에뮬레이션 해야합니다.

FPVM에 대한 요구사항은 낮습니다. 프로그램은 동기식이며 모든 입력은 동일한 Preimage Oracle 을 통해 로딩 되지만 이 모든 것은 L1 온체인에서 증명 되어야 합니다. 이를 위해 한번에 하나의 명령어만 증명됩니다.

Bisection game 은 full execution trace 를 증명하는 것에서 single instruction 만을 증명하는 것으로 작업을 축소시킬 수 있습니다.

instruction을 증명하는 것은 FPVM 마다 다를 수 있지만, 대부분은 Cannon 과 비슷합니다.

  • 명령을 실행하기 위해 VM은 therad-context와 유사한것은 에뮬레이션 합니다. 명령어는 메모리에서 읽고 해석되며 레지스터 파일과 메모리는 약간 변경될 수 있습니다.
  • Preimage Oracle과 기본 프로그램 런타임 요구사항을 지원하기 위해 Linux 시스템 호출의 subset도 지원합니다. Read/Write Syscall은 Preimage Oracle과의 상호작용을 가능하게 합니다. 프로그램은 Preimage에 대한 요청으로 hash를 쓰고 한번에 작은 청크들로 값을 읽습니다.

Cannon은 Optimism의 모든 분쟁에서 사용하는 기본 FPVM이며 MIPS는 dispute game의 모듈성으로 인해 구현될 수 있는 Cannon의 온체인 Smart contract 구현입니다.

Dispute Game Protocol

Dispute protocol 에서 다양한 타입의 dispute games은 DisputeGameFactory를 통해 생성, 관리 및 업그레이드 할 수 있습니다.

dispute games는 dispute protocol의 핵심 기본 요소입니다. 이는 간단한 state machine을 모델링하며, 분쟁을 제기할 수 있는 모든 정보에 대한 32byte commitment를 initialize 할 수 있습니다. 이 commitment에 대해서 true/false로 resolve하는 기능이 포함되어 있으며 이는 구현자가 정의하도록 남겨둡니다. dispute games은 두가지 기본 속성에 의존합니다.

  • Incentive Compatibility: 이 시스템은 false claim에 대해 페널티를 제공하고 truthful claim에 대해서는 합당한 보상을 제공합니다.
  • Resolution: 각 game은 claim에 대해서 확실하게 validate 한지 invalidate한지 결정할 수 있는 메커니즘이 존재합니다.

Optimism에서의 표준은 bisection game 이며 이는 dispute game의 한 종류입니다.

Fault Proof VM: Cannon

Cannon은 Optimism에서 표준으로 사용하는 Fault Proof Virtual Machine(FPVM)의 인스턴스입니다. Cannon에는 두 가지 주요 구성 요소가 있습니다.

  • Onchain MIPS.sol: 단일 MIPS의 명령어 실행을 검증하는 EVM 구현체
  • Offchain mipsevm: MIPS 명령어가 온체인에서 검증할 수 있도록 Proof를 생성하는 Go 구현체

Control Flow

image.png

OP-Challenger ↔ Cannon

active fault dispute game이 L2 block state transitions보다 낮은 depth에 도달하면 op-challengerCannon 을 실행하여 FPVM 내에서 MIPS 명령어 처리를 시작합니다. MIPS 명령어를 처리하는 과정에서 Cannon 은 FPVM 내에서 MIPS 명령어의 계산 결과에 대한 commitment인 state witness hash를 생성합니다. Bisection game 에서 op-challenger 는 active dispute를 식별할 수 있는 단일 MIPS 명령어가 될 때 까지 hash 를 생성해 제공합니다. 그러면 Cannon 은 MIPS 명령어를 onchain에서 실행하는데 필요한 모든 정보가 포함된 witness proof를 생성합니다. MIPS.sol에서 이 단일 MIPS 명령어를 onchain에서 실행하면 fault dispute game를 증명할 수 있습니다.

OP-Program ↔ Cannon

execution trace bisection이 시작되고 Cannon 이 시랭되면 Executable and Linkable Format (ELF) 바이너리가 로드되어 Cannon 내에서 실행됩니다. Cannon 내에는 MIPS R3000, 32비트 명령어 집합 아키텍처(ISA)를 처리하도록 빌드된 mipsevm이 있습니다. ELF 파일에는 MIPS 명령어가 포함되어 있으며 MIPS 명령어로 컴파일된 코드는 op-program 입니다.

op-program 은 MIPS 명령어로 컴파일되어 Cannon FPVM 내에서 실행되는 golang 코드 입니다. op-program 은 golang 코드 자체로 실행하든 Cannon 에서 실행하든 L2 state를 도출하는데 사용되는 모든 필수 데이터를 가져옵니다.

op-program은 동일한 입력이 동일한 출력 뿐만 아니고 동일한 execution traces을 생성하도록 설계 되었습니다. 이를 통해 fault dispute game의 모든 참가자가 op-program 을 실행하여 동일한 L2 oitput root state transition이 주어지면 동일한 execution traces를 생성할 수 있습니다. 이를 통해 체인에서 실행될 동일한 MIPS 명령어에 대해 동일한 witness proof가 생성될 수 있습니다.

Overview of Offchain Cannon components

image.png

mipsevm State and Memory

mipsevm 은 32-bit이기 때문에 주소 범위는 [0, 2^32-1]을 갖습니다. 메모리 구조는 일반적인 monolithic 구조이며 VM은 물리적 메모리와 직접 상호작용하는것 처럼 동작합니다.

mipsevm 의 경우 메모리가 어떻게 저장 되는지 중요하지 않습니다. Go runtime 내에서 전체 메모리를 보관할 수 있기 때문입니다. 하지만 MIPS 명령어를 온체인에서 실행하기 위해서는 작은 부분만 필요하도록 메모리를 표현하는 것이 중요합니다. 이는 비용 때문에 온체인에서 전체 32bit 메모리 공간을 전부 표현하는 것이 불가능 하기 때문입니다. 따라서 메모리는 Binary Merkel tree 구조에 저장됩니다. tree는 27 level의 고정된 depth를 가지며 각 Leaf 는 32byte입니다. 이는 전체 32bit 주소 공간을 포함할 수 있습니다.

Generating the Witness Proof

Cannon 은 dispute game에서 두가지 주요 구성 요소를 처리합니다.

  • execution trace bisection game 중 op-challenger 에게 제공할 state witness hashes 생성
  • single MIPS instruction 에 대한 witness proof 생성
  • witness.go 에서 MIPS 명령어 관련 정보를 인코딩하여 MIPS.sol에 전달할 수 있는 형태로 준비합니다.
  • Pre-image가 필요한 경우, 관련 키와 오프셋 정보를 OP-Challenger에 전하여 PreimageOracle.sol에 onchain으로 게시할 수 있도록 합니다.

Loading the ELF file

bisection game의 execution trace 부분이 시작되면 MIPS 명령어로 컴파일된 op-program 이 포함된 ELF 파일이 Cannon 내에서 실행됩니다. 그러나 op-programCannon 으로 가져와 실행하려면 바이너리 로더가 필요합니다. 바이너리 로더는 load_elf.gopatch.go로 구성됩니다.

load_elf.go 는 최상위 arguments를 분석하고 ELF 바이너리를 읽어 Cannon 에서 실행할 수 있게 합니다.

patch.go 는 ELF 파일의 헤더를 실제로 파싱 하는 역할을 하는데 이 헤더는 파일 내 어떤 프로그램이 있는지 메모리의 어느 위치에 있는지를 지정합니다.

ELF 파일을 로드하는 동안 발생하는 또 다른 단계는 호환되지 않는 함수의 바이너리를 패치하는 것 입니다. EVM 에서는 커널에 접근이 불가능하므로 Onchain, Offchain의 구현을 일치시켜야 하기 위해서 Offchain에서의 커널 엑세스도 Cannon내에서는 사용할 수 없도록 해야합니다.

Instruction Stepping

MIPS 바이너리가 Cannon 에 로드 되면 MIPS 명령어를 한 번에 하나씩 실행할 수 있습니다. run.go에는 MIPS 명령어를 단계별로 실행하는 코드가 포함되어 있습니다 또한 각 명령어 실행 전 추가 작업(로깅, 스냅샷 등)을 수행할 수 있습니다. run.go 내에서 StepFn은 실행할 MIPS 명령어를 시작하는 Wrapper 입니다. instrumented.go는 각 MIPS 명령어에 대해 시작할 인터페이스로 Step을 구현합니다. 또한 instrumented.go는 증인 증명에 대한 인코딩 정보와 사전 이미지 정보(MIPS 명령어에 필요한 경우)를 처리합니다.

mips.go

mips.go vs. MIPS.sol

Offchain mips.go와 Onchains Cannon MIPS.sol은 32비트 MIPS III 명령어를 실행할 때 비슷하게 동작합니다. 동일한 명령어, 메모리 및 레지스터 상태가 주어지면 정확히 동일한 결과를 생성해야 합니다. 하지만 두 구현체 사이엔 차이점이 존재합니다.

  1. MIPS.sol은 Onchain에서 실행되고 mips.go는 오프체인에서 실행됩니다.
    • MIPS.sol은 한번에 단일 명령어를 실행
    • mips.go는 한번에 모든 MIPS 명령어를 실행
  2. mipsevm 은 전체 32bit monolithic 메모리 공간을 포함하고 MIPS 명령어의 결과에 따라 메모리의 상태를 유지 관리 하지만 MIPS.sol 은 상태가 없고 전체 메모리 공간을 유지 관리하지 않습니다.
  3. mips.go는 PreimageOracle Server에 Preimage를 업로드를 해야합니다. 하지만 MIPS.sol은 그렇지 않습니다.

Preimage Oracle Interaction

Cannon은 PreimageOracle 서버와 상호작용하여 필요한 Pre-image를 관리합니다. MIPS 시스템 호출 명령어 실행 시 Pre-image 읽기/쓰기를 수행합니다. 필요한 경우 OP-Challenger에게 온체인 PreimageOracle.sol에 데이터를 제공하도록 지시합니다.

OP-Challenger Explainer

op-challenger 는 fault dispute system에서 honest actor로 동작하고 체인을 보호하고 dispute game이 항상 체인의 올바른 상태로 해결되도록 합니다. 구체적으로 op-challenger 는 아래와 같은 작업을 수행합니다.

  • dispute game을 모니터링 하고 상호작용함
  • proposal에 대한 valid output를 지킴
  • proposal에 대한 invalid output에 대해 challenge를 시도
  • claim을 resolve 함으로 써 op-proposer 을 지원
  • challenger and proposer 에 대한 자금 청구

Architecture

아래 그림은 op-challenger 가 수행하는 행동에 대한 다이어그램입니다.

image.png

Fault Detection Responses

op-challenger 는 각 claim의 validity를 체크하여 유효하지 않다고 생각되는 claim에만 응답을 하는데 이는 아래 로직을 따릅니다.

  1. trusted node가 동의하면 op-challenger 는 아무런 조치도 취하지 않습니다. 정직한 challenger는 disagree가 없는 클레임에 대해서 아무런 행동을 하지 않습니다.
  2. trusted node가 동의하지 않는다면 op-challenger 는 제한된 output에 대해 counter-claim을 제시합니다. 정직한 challenger는 rollup node가 canonical state와 동기화 되고 fault proof program이 올바르다고 가정하므로 모든 오류에 대응하기 위해 돈을 걸 수 있습니다.

Fault Dispute Game Responses

op-challenger 는 contract에 저장된 claim을 반복하여 조상이 자식보다 먼저 처리되도록 합니다. 각 클레임에 대해 정직한 challenger는 모든 claim에 대한 정직한 응답을 모두 추적하고 결정합니다.

Root Claim

root claim은 root claim에 대한 정직한 challenger의 state witness hash가 있는 경우에만 정직한 claim으로 여겨집니다.

Counter Claims

dispute game 에 새로운 claim이 생기면 정직한 challenger는 이를 처리하고 응답해야합니다. 정직한 challenger는 아래의 경우에만 응답을 합니다.

  1. claim이 정직한 응답의 child인 경우
  2. claim의 tract index보다 같거나 큰 trace index인 claim을 포함하는 정직한 응답

Possible Moves

challenger는 새로운 claim이 추가될 때 마다 각 게임을 모니터링 honest actor algorithm에 따라 유효한 제안은 통과시키고 유효하지 않은 제안은 차단합니다.

image.png

Resolution

한쪽의 FaultDisputeGame 의 Chess clock이 소진되면 정직한 challenger는 FaultDisputeGame 에 있는 resolveClaim 함수를 호출하는 것 입니다. root claim의 하위 game이 resolve되면 challenger는 마지막으로 resolve 함수를 호출하여 모든 game을 종료합니다.

FaultDisputeGame 는 resolve에 시간 제한을 두지 않습니다. 정직한 challenger는 그들이 반박한 claim에 붙은 자금 때문에 신속하게 해결하여 자금을 청산하고 보상을 확보하려는 경제적 동기를 갖습니다.

Fault Proof VM: MIPS.sol

https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/cannon/MIPS.sol

MIPS.sol smart contract는 32비트, Big-Endian, MIPS III 명령어 집합 아키텍처(ISA)를 포함하는 가상 머신(VM)의 Onchain Implementation 입니다. 이 smart contract는 Offchain mipsevm 의golang Implementation과 동일합니다.

Control Flow

FaultDisputeGame.solMIPS.sol과 상호 작용한 다음 MIPS.solPreimageOracle.sol을 호출합니다. MIPS.sol은 누군가가 step을 호출해야 할 때 게임의 최대 depth에서만 호출됩니다. FaultDisputeGame.sol은 active dispute에 대한 Fault Dispute Game의 배포된 인스턴스이고 PreimageOracle.sol은 Pre-images를 저장합니다.

  • Preimage는 L1과 L2 모두의 데이터를 포함하며, 여기에는 블록 헤더, 트랜잭션, 영수증, world state node 등의 정보가 포함됩니다. 이 Preimage는 실제 L2 상태를 계산하는 데 사용되는 유도 과정의 입력으로 활용되며, 이렇게 계산된 실제 L2 state는 이후 dispute game을 resolve 하는데 사용됩니다.
  • Fault Dispute Game은 큰 틀에서 현재 합의된 L2 state를 결정하고, 첫 번째로 이견이 발생하는 state가 발견될 때까지 L2 state를 순차적으로 진행합니다.

MIPS.sol 컨트랙트는 진행 중인 dispute game 인스턴스, 즉 FaultDisputeGame.sol 컨트랙트에 의해 호출되며 dispute game이 현재 dispute가 진행중인 state transition tree의 leaf node에 도달한 후에만 호출 됩니다. Leaf node는 단일 MIPS 명령어를 나타내며 (FPVM으로 Cannon을 사용하는 경우) 이는 체인에서 실행될 수 있습니다. 이 명령어 이전까지 합의 된 L2 state인 Preimage와 MIPS.sol contract에서 실행할 명령어 state가 주어지면 fault dispute games는 알맞은 사후 state(혹은 Image)를 결정할 수 있습니다. 이는 이후에 disputer가 제안한 state와 비교하여 fault dispute games의 결과를 결정하는데 사용합니다.


SP1과 친해지기: zkVM 이야기

SP1

Overview

https://docs.succinct.xyz/ 문서를 참조하여 실습 및 내용 요약 내용을 작성한 문서입니다.

Getting Started

Installation

Requirements

Install from prebuilt binaries

curl -L https://sp1.succinct.xyz | bash

image.png

위 명령어를 이용하여 정상적으로 실행했다면 아래 명령어를 통해 toolchain을 설치할 수 있다.

sp1up

image.png

위 명령어는 두 가지 항목을 설치하는데

  1. succicnt riscv32im-succinct-zkvm-elf 컴파일을 지원하는 간단한 toolchain
  2. cargo prove sp1 관련 command를 제공하는 CLI tool

CLI tool에 대한 설치는 아래 명령어로 확인할 수 있다.

cargo prove --version

image.png

Succinct Rust toolchain에 대한 설치는 아래 명령어 중 하나를 수행하여 확인할 수 있다.

RUSTUP_TOOLCHAIN=succinct cargo --version
cargo +succinct --version 

image.png

Create an SP1 Project

Option 1

Using cargo prove CLI

cargo prove new <name>

image.png

Option 2

Clone template repository

git clone https://github.com/succinctlabs/sp1-project-template.git

image.png

Project Overview

새 프로젝트는 아래와 같은 폴더 구조를 가지고 있습니다.


.
├── program
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── elf
│   │   └── riscv32im-succinct-zkvm-elf
│   └── src
│       └── main.rs
├── rust-toolchain
└── script
    ├── Cargo.lock
    ├── Cargo.toml
    ├── build.rs
    └── src
        └── bin
            ├── prove.rs
            └── vkey.rs

프로젝트 내 2가지 폴더가 있습니다.

  • program: zkVM 내에서 검증할 소스코드
  • script: Proof를 생성하고 검증하기 위한 소스코드

Generate Proofs

프로그램을 zkVM 에서 구동하기 위해 먼저 succinct Rust toolchain을 이용하여 RISC-V executable로 컴파일 해야합니다. 이를 ELF (Executable and Linkable Format) 라 합니다.

이를 컴파일 하기 위해서

cd program && cargo prove build

위 명령어를 수행하면 elf/riscv32im-succinct-zkvm-elf 경로에 결과물이 생성됩니다.

image.png

image.png

또한 script/build.rsprogram 에 변경사항이 있을 때 자동으로 ELF에 대한 빌드를 수행하므로 매번 수동으로 cargo prove build 를 수행할 필요가 없습니다.

image.png

Proof를 생성하기 위해 script/build.rs 를 통해서 생성한 ELF 파일을 가져와 SP1 zkVM 위에서 실행합니다. script 디렉토리 내 이미 관련 된 코드가 작성되어 있습니다.

우선 —-execute 옵션을 이용하여 Proof를 생성하지 않고 프로그램을 실행할 수 있습니다.

cd script
RUST_LOG=info cargo run --release -- --execute

image.png

--prove 옵션을 사용하여 Proof를 생성하고 이를 저장할 수 있습니다.

cd script
RUST_LOG=info cargo run --release -- --prove

image.png

Proof Generation Requirements

Local Proving

로컬에서 SP1 Proof를 생성하기 위해서 하드웨어 요구 스펙이 있습니다. 이는 Proof Type에 따라 다릅니다.

  Mock / Network Core / Compress PLONK (EVM)
CPU 1+, single-core perf matters 16+, more is better 32+, more is better
Memory 8GB+, more is better 32GB+, more if you have more cores 128GB+ (for PLONK)
Disk 20GB+ 20GB+ 100GB+ (for trusted setup)
EVM Compatible

Writing Programs

Setup

Create Project with CLI

Create an SP1 Project 의 Option 1 에 명시한 대로 아래 명령어를 통해 프로젝트를 생성하는 방법을 추천합니다.

cargo prove new <name>
cd <name>

Basic

Example: Fibonacci

https://github.com/succinctlabs/sp1/blob/main/examples/fibonacci/program/src/main.rs 에 있는 Fibonacci를 예시로 설명합니다.

Compiling Programs

Compile with CLI

SP1 프로그램을 작성 했다면 zkVM에서 실행할 수 있도록 ELF로 컴파일 해야합니다. 이는 cargo prove 명령어로 수행할 수 있습니다.

image.png

image.png

Build Script

코드를 변경할 때 자동으로 빌드를 수행하도록 하기 위해서 script 디렉토리에 build.rs 를 구성해야합니다.

use sp1_helper::build_program_with_args;

fn main() {
    build_program_with_args("../program", Default::default())
}

혹은 BuildArgs 구조체를 사용해 다양한 빌드 옵션을 구성할 수 있습니다

use sp1_helper::{build_program_with_args, BuildArgs};

fn main() {
    let args = BuildArgs {
        docker: true,
        output_directory: "./fibonacci-program".to_string(),
        ..Default::default()
    };
    build_program_with_args("../program", &args);
}

Inputs and Outputs

Real World Application에서 zero-knowledge proof를 사용할 때 대부분은 어떤 컨텍스트에 대해서 입출력에 대해서 검증하려고 하는 경우가 많다. 예를 들어

  1. Rollups : Tx 목록이 주어질 때 blockchain의 상태를 검증
  2. Coprocessors : block header가 주어질 때 smart contracts내 일부 스토리지에 대한 historical state를 검증

Reading Data

sp1_zkvm::io::read::<T> 메소드를 사용하여 데이터를 읽습니다. Read한 데이터는 기본적으로 Verifier에게 공개되지 않습니다.

let a = sp1_zkvm::io::read::<u32>();
let b = sp1_zkvm::io::read::<u64>();
let c = sp1_zkvm::io::read::<String>();

T 는 반드시 serde::Serialize 와 serde::Deserialize trait을 구현해야 합니다. 만약 byte를 직접 Read할 경우엔 sp1_zkvm::io::read_vec 메소드를 사용할 수 있습니다.

let my_vec = sp1_zkvm::io::read_vec();

Committing Data

데이터를 Commit 하면 데이터를 Verifier에게 공개하게 됩니다. 이는 sp1_zkvm::io::commit::<T> 메소드를 사용합니다.

sp1_zkvm::io::commit::<u32>(&a);
sp1_zkvm::io::commit::<u64>(&b);
sp1_zkvm::io::commit::<String>(&c);

T 는 반드시 serde::Serialize 와 serde::Deserialize trait을 구현해야 합니다. 만약 byte를 직접 Commit할 경우엔 sp1_zkvm::io::write_slice 메소드를 사용할 수 있습니다.

let mut my_slice = [0_u8; 32];
sp1_zkvm::io::commit_slice(&my_slice);

Creating Serializable Types

간단한 매크로를 사용하여 serde::Serialize 와 serde::Deserialize를 구현할 수 있습니다.

use serde::{Serialize, de::Deserialize};

#[derive(Serialize, Deserialize)]
struct MyStruct {
    a: u32,
    b: u64,
    c: String
}

Example

#![no_main]
sp1_zkvm::entrypoint!(main);

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct MyPointUnaligned {
    pub x: usize,
    pub y: usize,
    pub b: bool,
}

pub fn main() {
    let p1 = sp1_zkvm::io::read::<MyPointUnaligned>();
    println!("Read point: {:?}", p1);

    let p2 = sp1_zkvm::io::read::<MyPointUnaligned>();
    println!("Read point: {:?}", p2);

    let p3: MyPointUnaligned = MyPointUnaligned {
        x: p1.x + p2.x,
        y: p1.y + p2.y,
        b: p1.b && p2.b,
    };
    println!("Addition of 2 points: {:?}", p3);
    sp1_zkvm::io::commit(&p3);
}

Precompiles

Precompiles은 SP1 zkVM에 내장되어 있으며 타원곡선 및 해시함수 등 일반적으로 사용하는 연산에 대해 가속화합니다.

zkVM 내부에서 Precompiles는 ecall RISC-V 명령어를 통해 실행되는 system call로 노출됩니다. 각 Precompiles는 고유한 시스템 호출 번호를 가지고 있습니다.

Specification

//! Syscalls for the SP1 zkVM.
//!
//! Documentation for these syscalls can be found in the zkVM entrypoint
//! `sp1_zkvm::syscalls` module.

pub mod bls12381;
pub mod bn254;
pub mod ed25519;
pub mod io;
pub mod secp256k1;
pub mod unconstrained;
pub mod utils;
#[cfg(feature = "verify")]
pub mod verify;

extern "C" {
    /// Halts the program with the given exit code.
    pub fn syscall_halt(exit_code: u8) -> !;

    /// Writes the bytes in the given buffer to the given file descriptor.
    pub fn syscall_write(fd: u32, write_buf: *const u8, nbytes: usize);

    /// Reads the bytes from the given file descriptor into the given buffer.
    pub fn syscall_read(fd: u32, read_buf: *mut u8, nbytes: usize);

    /// Executes the SHA-256 extend operation on the given word array.
    pub fn syscall_sha256_extend(w: *mut [u32; 64]);

    /// Executes the SHA-256 compress operation on the given word array and a given state.
    pub fn syscall_sha256_compress(w: *mut [u32; 64], state: *mut [u32; 8]);

    /// Executes an Ed25519 curve addition on the given points.
    pub fn syscall_ed_add(p: *mut [u32; 16], q: *const [u32; 16]);

    /// Executes an Ed25519 curve decompression on the given point.
    pub fn syscall_ed_decompress(point: &mut [u8; 64]);

    /// Executes an Sepc256k1 curve addition on the given points.
    pub fn syscall_secp256k1_add(p: *mut [u32; 16], q: *const [u32; 16]);

    /// Executes an Secp256k1 curve doubling on the given point.
    pub fn syscall_secp256k1_double(p: *mut [u32; 16]);

    /// Executes an Secp256k1 curve decompression on the given point.
    pub fn syscall_secp256k1_decompress(point: &mut [u8; 64], is_odd: bool);

    /// Executes a Bn254 curve addition on the given points.
    pub fn syscall_bn254_add(p: *mut [u32; 16], q: *const [u32; 16]);

    /// Executes a Bn254 curve doubling on the given point.
    pub fn syscall_bn254_double(p: *mut [u32; 16]);

    /// Executes a BLS12-381 curve addition on the given points.
    pub fn syscall_bls12381_add(p: *mut [u32; 24], q: *const [u32; 24]);

    /// Executes a BLS12-381 curve doubling on the given point.
    pub fn syscall_bls12381_double(p: *mut [u32; 24]);

    /// Executes the Keccak-256 permutation on the given state.
    pub fn syscall_keccak_permute(state: *mut [u64; 25]);

    /// Executes an uint256 multiplication on the given inputs.
    pub fn syscall_uint256_mulmod(x: *mut [u32; 8], y: *const [u32; 8]);

    /// Enters unconstrained mode.
    pub fn syscall_enter_unconstrained() -> bool;

    /// Exits unconstrained mode.
    pub fn syscall_exit_unconstrained();

    /// Defers the verification of a valid SP1 zkVM proof.
    pub fn syscall_verify_sp1_proof(vkey: &[u32; 8], pv_digest: &[u8; 32]);

    /// Returns the length of the next element in the hint stream.
    pub fn syscall_hint_len() -> usize;

    /// Reads the next element in the hint stream into the given buffer.
    pub fn syscall_hint_read(ptr: *mut u8, len: usize);

    /// Allocates a buffer aligned to the given alignment.
    pub fn sys_alloc_aligned(bytes: usize, align: usize) -> *mut u8;

    /// Decompresses a BLS12-381 point.
    pub fn syscall_bls12381_decompress(point: &mut [u8; 96], is_odd: bool);

    /// Computes a big integer operation with a modulus.
    pub fn sys_bigint(
        result: *mut [u32; 8],
        op: u32,
        x: *const [u32; 8],
        y: *const [u32; 8],
        modulus: *const [u32; 8],
    );
}

Cycle Tracking

프로그램을 작성할때 이 프로그램이 엄라나 많은 RISC-V cycle을 사용하는지 아는 것은 성능 파악을 하는데 매우 중요합니다. SP1은 프로그램의 일부에서 사용된 RISC-V cycle을 추적할 수 있는 방법을 제공합니다.

Tracking Cycles with Annotations

프로그램의 일부에서 사용된 사이클 수를 추적하려면 println!("cycle-tracker-start: block name")println!("cycle-tracker-end: block name")문장(블록 이름은 시작과 끝 사이에서 동일해야 함)을 프로파일링하려는 프로그램 부분 주위에 넣거나 #[sp1_derive::cycle_tracker]함수에서 매크로를 사용할 수 있습니다.

//! A simple program that takes a number `n` as input, and writes the `n-1`th and `n`th fibonacci
//! number as an output.

// These two lines are necessary for the program to properly compile.
//
// Under the hood, we wrap your main function with some extra code so that it behaves properly
// inside the zkVM.
#![no_main]
sp1_zkvm::entrypoint!(main);

pub fn main() {
    // Read an input to the program.
    //
    // Behind the scenes, this compiles down to a system call which handles reading inputs
    // from the prover.
    let n = sp1_zkvm::io::read::<u32>();

    // Write n to public input
    sp1_zkvm::io::commit(&n);

    println!("cycle-tracker-start: fibonacci");
    // Compute the n'th fibonacci number, using normal Rust code.
    let mut a = 0;
    let mut b = 1;
    for _ in 0..n {
        let mut c = a + b;
        c %= 7919; // Modulus to prevent overflow.
        a = b;
        b = c;
    }
    println!("cycle-tracker-end: fibonacci");

    // Write the output of the program.
    //
    // Behind the scenes, this also compiles down to a system call which handles writing
    // outputs to the prover.
    sp1_zkvm::io::commit(&a);
    sp1_zkvm::io::commit(&b);
}

이를 사용하기 위해서 sp1-derive 를 dependencies에 추가해야 합니다.

[dependencies]
sp1-derive = "1.1.0"

script 내부의 Proof 생성 부분에서 logger를 utils::setup_logger() 를 통해 설정하고  RUST_LOG=info cargo run --release 명령어를 수행하면 아래와 같은 결과를 확인할 수 있습니다.

image.png

Tracking Cycles with Tracing

cycle-tracker 어노테이션은 코드의 특정 부분에 대한 cycle을 추적하는 간편한 방법 입니다. 하지만 때로는 모든 기능에 어노테이션을 작성하는 것 보단 프로그램 전체에 대해 추적하는 것이 더 편한 방법일 수 있습니다.

이를 위해서 script 디렉토리에서 아래 명령어를 수행하는 것으로 trace file을 생성할 수 있습니다.

TRACE_FILE=trace.log RUST_LOG=info cargo run --releases

그 다음 cargo prove CLI를 통해 trace file을 분석할 수 있습니다.

cargo prove trace --elf <path_to_program_elf> --trace <path_to_trace_file>

image.png

Proof Aggregation

SP1은 proof aggregation & recursion 을 지원합니다. 이는 SP1 내에서 SP1 Proof를 검증할 수 있도록 합니다.

  • 여러 SP1 Proof를 하나의 SP1 Proof으로 aggregate 함으로써 온체인 검증 코스트를 줄일 수 있습니다.
  • 여러 개의 증명으로 나눠진 로직을 증명합니다. 예를 들어, 각 블록을 개별적으로 증명하고 이 증명들을 집계하여 일련의 블록에 대한 최종 증명을 생성함으로써 롤업의 상태 전이 함수에 대한 진술을 증명하는 것과 같습니다.

SP1에서 어떤식으로 이를 수행하는지는 Example을 참고하세요

Verifying Proofs inside the zkVM

zkVM 내부에서 proof를 검증하기 위해 sp1_zkvm::lib::verify::verify_proof 함수를 사용합니다.

sp1_zkvm::lib::verify::verify_proof(vkey, public_values_digest);

Generating Proofs with Aggregation

SP1 zkVM 에 이미 존재하는 proof를 입력으로 제공하려면 SP1Stdin 를 사용할 수 있습니다.

let (input_pk, input_vk) = client.setup(PROOF_INPUT_ELF);
let (aggregation_pk, aggregation_vk) = client.setup(AGGREGATION_ELF);

// Generate a proof that will be recursively verified / aggregated. Note that we use the "compressed"
// proof type, which is necessary for aggregation.
let mut stdin = SP1Stdin::new();
let input_proof = client
    .prove(&input_pk, stdin)
    .compressed()
    .run()
    .expect("proving failed");

// Create a new stdin object to write the proof and the corresponding verifying key to.
let mut stdin = SP1Stdin::new();
stdin.write_proof(input_proof, input_vk);

// Generate a proof that will recusively verify / aggregate the input proof.
let aggregation_proof = client
    .prove(&aggregation_pk, stdin)
    .compressed()
    .run()
    .expect("proving failed");

Output

image.png

Generating Proofs

Setup

Create Project with CLI

Create an SP1 Project 의 Option 1 에 명시한 대로 아래 명령어를 통해 프로젝트를 생성하는 방법을 추천합니다.

cargo prove new <name>
cd <name>

Basics

proof를 생성하는데 필요한 메소드는 sp1_sdk crate에 포함되어 있습니다. ProverClient 를 사용하여 proving key와 verifying key를 설정한 다음 executeprove , verify 메소드를 사용하여 proof를 생성하거나 증명할 수 있습니다.

Example

use sp1_sdk::{utils, ProverClient, SP1ProofWithPublicValues, SP1Stdin};

/// The ELF we want to execute inside the zkVM.
const ELF: &[u8] = include_bytes!("../../program/elf/riscv32im-succinct-zkvm-elf");

fn main() {
    // Setup logging.
    utils::setup_logger();

    // Create an input stream and write '500' to it.
    let n = 1000u32;

    // The input stream that the program will read from using `sp1_zkvm::io::read`. Note that the
    // types of the elements in the input stream must match the types being read in the program.
    let mut stdin = SP1Stdin::new();
    stdin.write(&n);

    // Create a `ProverClient` method.
    let client = ProverClient::new();

    // Execute the program using the `ProverClient.execute` method, without generating a proof.
    let (_public_values, report) = client.execute(ELF, stdin.clone()).run().unwrap();
    println!(
        "Executed program with {} cycles",
        report.total_instruction_count()
    );

    // Generate the proof for the given program and input.
    let client = ProverClient::new();
    let (pk, vk) = client.setup(ELF);
    let mut proof = client.prove(&pk, stdin).run().unwrap();

    println!("generated proof");

    // Read and verify the output.
    // Note that this output is read from values commited to in the program
    // using `sp1_zkvm::io::commit`.
    let _ = proof.public_values.read::<u32>();
    let a = proof.public_values.read::<u32>();
    let b = proof.public_values.read::<u32>();

    println!("a: {}", a);
    println!("b: {}", b);

    // Verify proof and public values
    client.verify(&proof, &vk).expect("verification failed");

    // Test a round trip of proof serialization and deserialization.
    proof
        .save("proof-with-pis.bin")
        .expect("saving proof failed");
    let deserialized_proof =
        SP1ProofWithPublicValues::load("proof-with-pis.bin").expect("loading proof failed");

    // Verify the deserialized proof.
    client
        .verify(&deserialized_proof, &vk)
        .expect("verification failed");

    println!("successfully generated and verified proof for the program!")
}

Proof Types

SP1 zkVM에서 생성할 수 있는 여러 종류의 Proof type이 있습니다. 각 type은 증명 생성 시간, 검증 cost, proof 크기 등에서 차이가 있습니다.

Core (Default)

기본 prover 모드는 execution 사이즈에 비례하는 STARK 증명을 생성합니다. 검증 비용이나 proof 사이즈에 대해 크게 신경쓰지 않아도 될 때 사용합니다.

let client = ProverClient::new();
client.prove(&pk, stdin).run().unwrap();

Compressed

compressed prover 모드는 일정한 크기의 STARK 증명을 생성합니다. 검증 비용이나 Proof 사이즈에 대해 신경써야 할 경우 사용하세요.

이 모드는 SP1 내에서 SP1 Proof를 재귀적으로 검증하고자 하는 애플리케이션에 유용합니다.

let client = ProverClient::new();
client.prove(&pk, stdin).compressed().run().unwrap();

PLONK

PLONK prover 모드는 매우 작은 크기의 Proof size와 낮은 검증 비용을 가진 SNARK Proof를 생성합니다. 이 모드는 약 300,000 Gas (ethereum) 정도를 소모하여 온체인에서 검증할 수 있을 정도의 증명을 생성하는데 사용합니다.

let client = ProverClient::new();
client.prove(&pk, stdin).plonk().run().unwrap();

Onchain Verification

Setup

SP1 프로젝트 템플릿 에 Onchain verification 에 필요한 코드들이 포함되어 있습니다.

  • program 폴더는 Solidity 에서 디코딩 할 수 있는 출력을 작성하는 법을 보여 줍니다.
  • script 폴더는 SDK 를 사용하여 증명을 생성하고 이를 바이너리로 저장하는 방법을 보여 줍니다.
  • contract 폴더는 Solidity 를 사용하여 체인 상에서 증명을 검증하는 방법을 보여 줍니다.

Generating SP1 Proofs for Onchain Verification

기본적으로 SP1에서 생성된 증명은 크기가 일정하지 않고 Ethereum 에서 STARK Proof 에 대한 검증이 cost 가 많이 들기 때문에 체인 상에서 검증할 수 없습니다. 체인 상에서 검증할 수 있는 검증을 생성하기 위해서 성능이 뛰어난 STARK recursion 을 사용해 단일 STARK Proof로 만들고 이를 SNARK Proof로 래핑합니다. plonk Proof type 옵션을 사용하면 위 처럼 동작하는 방식으로 Proof를 생성할 수 있습니다.

Example

use sp1_sdk::{utils, ProverClient, SP1Stdin};

/// The ELF we want to execute inside the zkVM.
const ELF: &[u8] = include_bytes!("../../program/elf/riscv32im-succinct-zkvm-elf");

fn main() {
    // Setup logging.
    utils::setup_logger();

    // Create an input stream and write '500' to it.
    let n = 500u32;

    let mut stdin = SP1Stdin::new();
    stdin.write(&n);

    // Generate the proof for the given program and input.
    let client = ProverClient::new();
    let (pk, vk) = client.setup(ELF);
    let proof = client.prove(&pk, stdin).plonk().run().unwrap();

    println!("generated proof");

    // Get the public values as bytes.
    let public_values = proof.public_values.raw();
    println!("public values: {:?}", public_values);

    // Get the proof as bytes.
    let solidity_proof = proof.raw();
    println!("proof: {:?}", solidity_proof);

    // Verify proof and public values
    client.verify(&proof, &vk).expect("verification failed");

    // Save the proof.
    proof
        .save("proof-with-pis.bin")
        .expect("saving proof failed");

    println!("successfully generated and verified proof for the program!")
}

Solidity Verifier

SP1에서는 SP1 Proof를 온체인에서 검증하기 위한 컨트랙트를 제공합니다. 이 문서에서는 Foundry를 사용하여 작성합니다

Installation

forge install succinctlabs/sp1-contracts

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ISP1Verifier} from "@sp1-contracts/ISP1Verifier.sol";

/// @title Fibonacci.
/// @author Succinct Labs
/// @notice This contract implements a simple example of verifying the proof of a computing a
///         fibonacci number.
contract Fibonacci {
    /// @notice The address of the SP1 verifier contract.
    /// @dev This can either be a specific SP1Verifier for a specific version, or the
    ///      SP1VerifierGateway which can be used to verify proofs for any version of SP1.
    ///      For the list of supported verifiers on each chain, see:
    ///      https://github.com/succinctlabs/sp1-contracts/tree/main/contracts/deployments
    address public verifier;

    /// @notice The verification key for the fibonacci program.
    bytes32 public fibonacciProgramVKey;

    constructor(address _verifier, bytes32 _fibonacciProgramVKey) {
        verifier = _verifier;
        fibonacciProgramVKey = _fibonacciProgramVKey;
    }

    /// @notice The entrypoint for verifying the proof of a fibonacci number.
    /// @param _proofBytes The encoded proof.
    /// @param _publicValues The encoded public values.
    function verifyFibonacciProof(bytes calldata _publicValues, bytes calldata _proofBytes)
        public
        view
        returns (uint32, uint32, uint32)
    {
        ISP1Verifier(verifier).verifyProof(fibonacciProgramVKey, _publicValues, _proofBytes);
        (uint32 n, uint32 a, uint32 b) = abi.decode(_publicValues, (uint32, uint32, uint32));
        return (n, a, b);
    }
}

ISP1Verifier Interface

모든 verifier는 ISP1Verifier 인터페이스를 구현해야 합니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title SP1 Verifier Interface
/// @author Succinct Labs
/// @notice This contract is the interface for the SP1 Verifier.
interface ISP1Verifier {
    /// @notice Verifies a proof with given public values and vkey.
    /// @dev It is expected that the first 4 bytes of proofBytes must match the first 4 bytes of
    /// target verifier's VERIFIER_HASH.
    /// @param programVKey The verification key for the RISC-V program.
    /// @param publicValues The public values encoded as bytes.
    /// @param proofBytes The proof of the program execution the SP1 zkVM encoded as bytes.
    function verifyProof(
        bytes32 programVKey,
        bytes calldata publicValues,
        bytes calldata proofBytes
    ) external view;
}


Troubleshooting

  • cargo prove build 수행 시 cargo binary를 못 찾는 현상

    image.png

    • toolchain uninstall 후 install 재 수행 하여 해결

    image.png

  • Proof Aggregation 실행 중 에러

    Example 에 있는 aggregation을 템플릿에 옮겨 수행한 결과입니다.

    에러 뜸

      2024-08-22T07:13:00.992561Z  INFO generate fibonacci proof n=10:prove_core: close time.busy=4.70s time.idle=23.9ms
      2024-08-22T07:15:09.054993Z  INFO generate fibonacci proof n=10: close time.busy=128s time.idle=4.73s
      2024-08-22T07:15:09.062977Z  INFO aggregate the proofs:prove_core: clk = 0 pc = 0x2013b4
      thread '<unnamed>' panicked at /Users/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sp1-core-1.1.0/src/runtime/mod.rs:1197:13:
      Not all proofs were read. Proving will fail during recursion. Did you pass too many proofs in or forget to call verify_sp1_proof?
      note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
      thread 'main' panicked at /Users/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/sp1-core-1.1.0/src/utils/prove.rs:377:71:
      called `Result::unwrap()` on an `Err` value: Any { .. }
      2024-08-22T07:15:09.066269Z  INFO aggregate the proofs:prove_core: close time.busy=3.63ms time.idle=1.36ms
      2024-08-22T07:15:09.066537Z  INFO aggregate the proofs: close time.busy=11.5ms time.idle=1.04µs
    

    image.png

    sp1을 그대로 복붙하니까 정상적으로 수행됨, 아마 ELF가 꼬여서 그런 것 같음

    image.png

    https://github.com/repository-warehouse/sp1-aggregation-example


ESLint v8 에서 v9으로 마이그레이션 하기

이 포스트는 제가 사용하고 있던 nestjs-boilerplate에 대해 ESLint v8에서 v9로 마이그레이션을 진행한 내용을 정리한 포스트 입니다.

ESLint v9

ESLint v9으로 업데이트가 되면서 몇몇 설정이 변경되었습니다. 제일 큰 변경점은 설정 파일이 .eslintrc 파일이 eslint.config.js 로 변경 되면서 설정 파일의 포맷이 모두 변경되었습니다.

v9가 막 업데이트 되었을 때는 아직 많은 플러그인들이 v9를 지원하지 않아서 업데이트를 하지 않았었는데, 이제는 대부분의 플러그인들이 v9를 지원하고 있어서 업데이트를 진행하였습니다.

ss

마이그레이션

우선 기존 사용하던 플러그인을 모두 업데이트 해주었습니다. 일부 플러그인이 v9을 지원히지 않아 삭제 혹은 대체한 플러그인도 존재합니다.

"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-security": "^3.0.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
"typescript-eslint": "8.0.0-alpha.10"
"typescript": "5.4.5",
"@eslint/js": "^9.7.0",
"@types/eslint__js": "^8.42.3",

"@typescript-eslint/eslint-plugin": "^7.11.0", // Removed
"@typescript-eslint/parser": "^7.11.0", // Removed
"eslint-plugin-import": "^12.1.0", // Removed

다른 프로젝트를 참고하려고 서칭을 하던 도중 ESLint에서 직접 Migrator를 제공하고 있어서 이를 이용하여 마이그레이션을 진행하였습니다.

ESLint Configuration Migrator

.eslintrc.eslintignore 파일을 참조하여 자동으로 설정파일을 만들어 주는 플러그인입니다. 제가 사용하는 설정의 경우 완벽하게 마이그레이션 되진 않았고 일부 수정이 필요한 부분이 있었습니다. 그래도 대부분의 설정이 마이그레이션 되어서 편하게 업데이트를 진행할 수 있었습니다.

아래 코드는 마이그레이션된 설정 파일입니다. 제가 개인적으로 사용하고 있는 설정이어서 다른 프로젝트에 적용하기 위해서는 수정이 필요할 수 있습니다.

import { FlatCompat } from "@eslint/eslintrc";
import eslintJs from "@eslint/js";
import pluginSecurity from "eslint-plugin-security";
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
import globals from "globals";
import path from "path";
import eslintTs from "typescript-eslint";
import tseslint from "typescript-eslint";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
    baseDirectory: __dirname,
    recommendedConfig: eslintJs.configs.recommended
});

export default eslintTs.config(
    eslintJs.configs.recommended,
    ...eslintTs.configs.recommendedTypeChecked,
    ...compat.extends(
        "plugin:@typescript-eslint/eslint-recommended",
        "plugin:@typescript-eslint/recommended",
        "prettier"
    ),
    {
        ignores: ["**/types.d.ts", "**/*.js", "**/node_modules/"]
    },
    {
        languageOptions: {
            globals: {
                ...globals.es2020,
                ...globals.node,
                Atomics: "readonly",
                SharedArrayBuffer: "readonly"
            },
            parserOptions: {
                project: "tsconfig.json"
            }
        },
        settings: {
            "import/resolver": {
                node: {
                    extensions: [".js", ".jsx", ".ts", ".tsx"]
                }
            }
        },

        plugins: {
            "@typescript-eslint": tseslint.plugin,
            "simple-import-sort": simpleImportSortPlugin,
            security: pluginSecurity.configs.recommended,
        },
        rules: {
            "no-empty-pattern": "off",
            "@typescript-eslint/no-unsafe-return": "off",
            "@typescript-eslint/no-unsafe-call": "off",
            "@typescript-eslint/no-unsafe-member-access": "off",
            "@typescript-eslint/no-unsafe-assignment": "off",
            "@typescript-eslint/no-unsafe-argument": "off",
            "@typescript-eslint/no-explicit-any": "off",
            "simple-import-sort/imports": "error",
            "simple-import-sort/exports": "error",
            semi: "error"
        }
    }
);

vscode에 적용

vscode에 ESLint v9의 설정파일을 적용하기 위해서 eslint.useFlatConfig 설정을 활성화할 필요가 있습니다.

vscode

위 설정을 활성화하면 `eslint.config.js’ 파일을 참고하여 설정을 적용할 수 있습니다.

마치며

ESLint v9로 업데이트를 진행하면서 몇몇 설정이 변경되었지만, 대부분의 설정은 그대로 사용할 수 있었습니다. 마이그레이션도 Migrator의 존재 때문에 쉽게 진행할 수 있었고, vscode에서도 설정을 쉽게 적용할 수 있어서 편하게 업데이트를 진행할 수 있었습니다.

지금은 충분히 v9로 마이그레이션을 고려해도 좋을 만큼 충분히 안정화 되었다고 생각합니다.


2023 Advent-of-spin Challenge 4

이 포스트는 advent-of-spin에 업로드 된 Challenge 4에 대해서 진행했던 내용을 정리한 포스트 입니다.

Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.

Spec

  • Bulls’n’Cows 게임을 진행합니다. 숫자야구와 동일한 게임입니다.
  • https://bulls-n-cows.fermyon.app/api API를 통해서 게임을 진행할 수 있으며 이 게임을 진행할 수 있는 로직을 구현합니다.
  • https://bulls-n-cows.fermyon.app/api?guess=012 식으로 query에 guess 를 통해서 값을 전달할 수 있으며 응답은 아래와 같습니다.
  • 이후 동일한 게임을 이어서 할 때 • https://bulls-n-cows.fermyon.app/api?guess=012&id=1836633b-8dd1-41c2-b084-644b37c6fd1b 처럼 id를 같이 전달합니다.

Work

  • 이번 챌린지는 로직 구현에 초점을 맞추어 진행합니다. 라우터는 GET 하나만 뚫어서 진행합니다.
  • spin new 명령어를 이용하여 새로운 프로젝트를 생성해 줍니다.

    spin new

  • 그리고 생성된 프로젝트의 Cargo.toml 파일에 아래와 같이 dependencies를 추가해 줍니다.

    dependencies

    • request / response 객체를 serialize / deserialize 하기 위해 serde 라이브러리를 사용합니다.
  • src/lib.rs 에 POST를 처리하기 위한 로직을 구현합니다.

    • 경우의 수가 많지 않아(0,1,2,3,4 만 사용) 모든 경우를 array에 넣고 모든 경우를 시도하는 방법으로 구현하였습니다.
      use http::StatusCode;
      use serde::{Deserialize, Serialize};
      use spin_sdk::{
          http::{IntoResponse, Method, Request, Response},
          http_component,
      };
    
      #[derive(Debug, Deserialize, Serialize)]
      struct GameResponse {
          cows: u64,
          bulls: u64,
          gameId: String,
          guesses: u64,
          solved: bool,
      }
    
      #[http_component]
      async fn handle_request(_req: http::Request<Vec<u8>>) -> anyhow::Result<impl IntoResponse> {
          let guess_numbers = [
              "013", "014", "021", "023", "024", "031", "032", "034", "102", "103", "104", "120",
              "123", "124", "130", "132", "134", "201", "203", "204", "210", "213", "214", "230", "231",
              "234", "301", "302", "304", "310", "312", "314", "320", "321", "324",
          ];
    
          let status = StatusCode::OK;
          let body = "Done".to_string();
    
          let uri = format!("https://bulls-n-cows.fermyon.app/api?guess=012");
    
          let req = Request::builder().method(Method::Get).uri(uri).build();
    
          // Send the request and await the response
          let res: Response = spin_sdk::http::send(req).await?;
    
          let response: GameResponse = serde_json::from_slice(res.body().to_vec().as_slice()).unwrap();     
    
          if response.solved {
              println!("{:?}", response);
              Ok(Response::builder()
              .status(status)
              .header("content-type", "application/json")
              .body(body)
              .build())
          }
          else {
              let game_id = response.gameId;
              for guess in guess_numbers.iter() {
                  let uri = format!("https://bulls-n-cows.fermyon.app/api?guess={}&id={}", guess, game_id);
                
                  let req = Request::builder().method(Method::Get).uri(uri).build();
                
                  // Send the request and await the response
                  let res: Response = spin_sdk::http::send(req).await?;
                
                  let response: GameResponse = serde_json::from_slice(res.body().to_vec().as_slice()).unwrap();     
            
                  if response.solved {
                      println!("{:?}", response);
                      break;
                  }
              }
              Ok(Response::builder()
                  .status(status)
                  .header("content-type", "application/json")
                  .body(body)
                  .build())
            
              }
          }