cargo-risczero 를 사용하여 hello-world 라고 하는 새로운 프로젝트를 생성합니다. 또한 cargo-risczero 는 —-guest-name 옵션을 사용하여 guest 프로그램의 이름을 지정할 수 있습니다.
cargo risczero new hello-world --guest-name hello_guest
cd hello-world
프로젝트 폴더 내부에서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가 프로그램을 실행할 때 입력값에 접근할 수 있게 됩니다.
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입니다.
userisc0_zkvm::guest::env;fnmain(){// read the inputletinput:u32=env::read();// do something with the input// writing to the journal makes it publicenv::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으로 이를 출력 할 수 있습니다.
usemethods::{HELLO_GUEST_ELF,HELLO_GUEST_ID};userisc0_zkvm::{default_prover,ExecutorEnv};fnmain(){letinput:u32=15*u32::pow(2,27)+1;letenv=ExecutorEnv::builder().write(&input).unwrap().build().unwrap();// Obtain the default prover.letprover=default_prover();// Produce a receipt by proving the specified ELF binary.letreceipt=prover.prove(env,HELLO_GUEST_ELF).unwrap().receipt;// Extract journal of receiptletoutput:u32=receipt.journal.decode().unwrap();// Print, notice, after committing to a journal, the private input became publicprintln!("Hello, world! I generated a proof of guest execution! {} is a public output from journal ",output);}
zkVM에서 프로그래밍 하는 것은 host 와 guest, 두 세계간의 데이터를 이동시키는 것 이라고 생각할 수 있습니다. Host는 일반 프로그램과 같이 계산이 수행되고 Guest에서는 zk환경에서의 계산이 수행됩니다.
guest가 zk환경에서 작동하므로 host와 비교했을 때 제한적인 방법으로 데이터를 획득할 수 있습니다. Host 와 guest간 데이터를 보내는 유일한 방법은 Executor Environment 를 통하는 것 입니다. 이러한 데이터 전송은 file descriptors를 통해 이루어 지는데 zkVM에서는 stdin, stdout, stderr, ,journal 의 4가지 가 존재합니다.
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는 이에 대응되는 read 및 read_slice 메소드를 통해 이를 읽을 수 있습니다.
공개하기 위한 데이터를 전송하기 위해 journal 으로 보내는 방법이 있습니다. 이는 commit , commit_slice 메소드를 통해 journal에 쓰기가 가능하며 이를 수행하면 Proof에 포함되며 Receipt를 통해 데이터에 접근할 수 있습니다.
letdata="Hello, journal!";env::commit(&data);
Reading Private data in the host
Guest에서 데이터를 보낸 후 from_slice 메소드를 활용하여 host에서 읽을 수 있습니다.
letresult:Type=from_slice(&output)?;
Reading Public data in the host
Public data를 읽는 것은 증명 과정 이후 Receipt 에 접근하는 것으로 읽을 수 있습니다. journal 인스턴스의 decode 메소드를 통해 가능합니다.
// Produce a receipt by proving the specified ELF binary.letreceipt=prover.prove(env,ELF).unwrap().receipt;// Decode the journal to access the public data.letpublic_data=receipt.journal.decode()?;
Sharing data structures between host and guest
Host 와 guest 사이 공통 데이터 구조를 공유하는 방법은 core 모듈을 포함하는 공통 데이터 구조를 포함하는 것 입니다.
proof composition 를 사용하기 위해 host에서 add_assumption() 함수를 호출하고 env::verify() 를 guest에서 호출합니다.
How It Works
proof composition은 ReceiptClaim구조체에 assumptions을 추가하고, assumptions을 resolveing 하는 것으로 동작합니다.
Adding Assumptions
env::verify() 가 guest 프로그램 내부에서 호출 되면 assumption이 ReceiptClaim 에 추가됩니다.
Resolve an Assumption
proof composition 를 해결하기 위해서 assumptions이 resolve 되어야 합니다. 이는 resolve 를 통해 수행됩니다. 이는 유저가 Prover::prove_with_opts 를 호출할 때 자동으로 호출 됩니다.
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를 반환합니다.
이 문서는 Fault Proof 관련 내용을 스터디 하여 관련 내용을 작성한 문서입니다. 이 문서는 Optimism 에서 작성된 문서를 참고하여 작성하였습니다.
Fault Proof Explainer
Fault Proofs는 Optimistic Rollup 시스템에서 중요한 부분입니다. Fault Proof는 상태에 대한 제안을 허가없이(Permissionless) 제출하고 이를 challenge할 수 있습니다.
Permissionless Proposals
Proposals 혹은 State Proposals 는 DisputeGameFactory contract를 통해 제출된 체인의 상태에 대한 클레임 입니다. Proposals는 여러 용도로 사용할 수 있지만 일반적으로 최종 사용자가 체인에서 한 행동에 대하여 증명하는데 일반적으로 사용됩니다.
Fault Proofs는 이러한 Proposals를 누구나 제출할 수 있으며 아무나에 의해 확정될 수 있습니다.
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
OP-Challenger ↔ Cannon
active fault dispute game이 L2 block state transitions보다 낮은 depth에 도달하면 op-challenger 는 Cannon 을 실행하여 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
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-program을 Cannon 으로 가져와 실행하려면 바이너리 로더가 필요합니다. 바이너리 로더는 load_elf.go와 patch.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 명령어를 실행할 때 비슷하게 동작합니다. 동일한 명령어, 메모리 및 레지스터 상태가 주어지면 정확히 동일한 결과를 생성해야 합니다. 하지만 두 구현체 사이엔 차이점이 존재합니다.
MIPS.sol은 Onchain에서 실행되고 mips.go는 오프체인에서 실행됩니다.
MIPS.sol은 한번에 단일 명령어를 실행
mips.go는 한번에 모든 MIPS 명령어를 실행
mipsevm 은 전체 32bit monolithic 메모리 공간을 포함하고 MIPS 명령어의 결과에 따라 메모리의 상태를 유지 관리 하지만 MIPS.sol 은 상태가 없고 전체 메모리 공간을 유지 관리하지 않습니다.
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 가 수행하는 행동에 대한 다이어그램입니다.
Fault Detection Responses
op-challenger 는 각 claim의 validity를 체크하여 유효하지 않다고 생각되는 claim에만 응답을 하는데 이는 아래 로직을 따릅니다.
trusted node가 동의하면 op-challenger 는 아무런 조치도 취하지 않습니다. 정직한 challenger는 disagree가 없는 클레임에 대해서 아무런 행동을 하지 않습니다.
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는 아래의 경우에만 응답을 합니다.
claim이 정직한 응답의 child인 경우
claim의 tract index보다 같거나 큰 trace index인 claim을 포함하는 정직한 응답
Possible Moves
challenger는 새로운 claim이 추가될 때 마다 각 게임을 모니터링 honest actor algorithm에 따라 유효한 제안은 통과시키고 유효하지 않은 제안은 차단합니다.
Resolution
한쪽의 FaultDisputeGame 의 Chess clock이 소진되면 정직한 challenger는 FaultDisputeGame 에 있는 resolveClaim 함수를 호출하는 것 입니다. root claim의 하위 game이 resolve되면 challenger는 마지막으로 resolve 함수를 호출하여 모든 game을 종료합니다.
FaultDisputeGame 는 resolve에 시간 제한을 두지 않습니다. 정직한 challenger는 그들이 반박한 claim에 붙은 자금 때문에 신속하게 해결하여 자금을 청산하고 보상을 확보하려는 경제적 동기를 갖습니다.
MIPS.sol smart contract는 32비트, Big-Endian, MIPS III 명령어 집합 아키텍처(ISA)를 포함하는 가상 머신(VM)의 Onchain Implementation 입니다. 이 smart contract는 Offchain mipsevm 의golang Implementation과 동일합니다.
Control Flow
FaultDisputeGame.sol은 MIPS.sol과 상호 작용한 다음 MIPS.sol이 PreimageOracle.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의 결과를 결정하는데 사용합니다.
#![no_main]sp1_zkvm::entrypoint!(main);useserde::{Deserialize,Serialize};#[derive(Serialize,Deserialize,Debug,PartialEq)]structMyPointUnaligned{pubx:usize,puby:usize,pubb:bool,}pubfnmain(){letp1=sp1_zkvm::io::read::<MyPointUnaligned>();println!("Read point: {:?}",p1);letp2=sp1_zkvm::io::read::<MyPointUnaligned>();println!("Read point: {:?}",p2);letp3: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.pubmodbls12381;pubmodbn254;pubmoded25519;pubmodio;pubmodsecp256k1;pubmodunconstrained;pubmodutils;#[cfg(feature="verify")]pubmodverify;extern"C"{/// Halts the program with the given exit code.pubfnsyscall_halt(exit_code:u8)->!;/// Writes the bytes in the given buffer to the given file descriptor.pubfnsyscall_write(fd:u32,write_buf:*constu8,nbytes:usize);/// Reads the bytes from the given file descriptor into the given buffer.pubfnsyscall_read(fd:u32,read_buf:*mutu8,nbytes:usize);/// Executes the SHA-256 extend operation on the given word array.pubfnsyscall_sha256_extend(w:*mut[u32;64]);/// Executes the SHA-256 compress operation on the given word array and a given state.pubfnsyscall_sha256_compress(w:*mut[u32;64],state:*mut[u32;8]);/// Executes an Ed25519 curve addition on the given points.pubfnsyscall_ed_add(p:*mut[u32;16],q:*const[u32;16]);/// Executes an Ed25519 curve decompression on the given point.pubfnsyscall_ed_decompress(point:&mut[u8;64]);/// Executes an Sepc256k1 curve addition on the given points.pubfnsyscall_secp256k1_add(p:*mut[u32;16],q:*const[u32;16]);/// Executes an Secp256k1 curve doubling on the given point.pubfnsyscall_secp256k1_double(p:*mut[u32;16]);/// Executes an Secp256k1 curve decompression on the given point.pubfnsyscall_secp256k1_decompress(point:&mut[u8;64],is_odd:bool);/// Executes a Bn254 curve addition on the given points.pubfnsyscall_bn254_add(p:*mut[u32;16],q:*const[u32;16]);/// Executes a Bn254 curve doubling on the given point.pubfnsyscall_bn254_double(p:*mut[u32;16]);/// Executes a BLS12-381 curve addition on the given points.pubfnsyscall_bls12381_add(p:*mut[u32;24],q:*const[u32;24]);/// Executes a BLS12-381 curve doubling on the given point.pubfnsyscall_bls12381_double(p:*mut[u32;24]);/// Executes the Keccak-256 permutation on the given state.pubfnsyscall_keccak_permute(state:*mut[u64;25]);/// Executes an uint256 multiplication on the given inputs.pubfnsyscall_uint256_mulmod(x:*mut[u32;8],y:*const[u32;8]);/// Enters unconstrained mode.pubfnsyscall_enter_unconstrained()->bool;/// Exits unconstrained mode.pubfnsyscall_exit_unconstrained();/// Defers the verification of a valid SP1 zkVM proof.pubfnsyscall_verify_sp1_proof(vkey:&[u32;8],pv_digest:&[u8;32]);/// Returns the length of the next element in the hint stream.pubfnsyscall_hint_len()->usize;/// Reads the next element in the hint stream into the given buffer.pubfnsyscall_hint_read(ptr:*mutu8,len:usize);/// Allocates a buffer aligned to the given alignment.pubfnsys_alloc_aligned(bytes:usize,align:usize)->*mutu8;/// Decompresses a BLS12-381 point.pubfnsyscall_bls12381_decompress(point:&mut[u8;96],is_odd:bool);/// Computes a big integer operation with a modulus.pubfnsys_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);pubfnmain(){// Read an input to the program.//// Behind the scenes, this compiles down to a system call which handles reading inputs// from the prover.letn=sp1_zkvm::io::read::<u32>();// Write n to public inputsp1_zkvm::io::commit(&n);println!("cycle-tracker-start: fibonacci");// Compute the n'th fibonacci number, using normal Rust code.letmuta=0;letmutb=1;for_in0..n{letmutc=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 명령어를 수행하면 아래와 같은 결과를 확인할 수 있습니다.
Tracking Cycles with Tracing
cycle-tracker 어노테이션은 코드의 특정 부분에 대한 cycle을 추적하는 간편한 방법 입니다. 하지만 때로는 모든 기능에 어노테이션을 작성하는 것 보단 프로그램 전체에 대해 추적하는 것이 더 편한 방법일 수 있습니다.
이를 위해서 script 디렉토리에서 아래 명령어를 수행하는 것으로 trace file을 생성할 수 있습니다.
TRACE_FILE=trace.log RUST_LOG=info cargo run --releases
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.letmutstdin=SP1Stdin::new();letinput_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.letmutstdin=SP1Stdin::new();stdin.write_proof(input_proof,input_vk);// Generate a proof that will recusively verify / aggregate the input proof.letaggregation_proof=client.prove(&aggregation_pk,stdin).compressed().run().expect("proving failed");
Output
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를 설정한 다음 execute, prove , verify 메소드를 사용하여 proof를 생성하거나 증명할 수 있습니다.
Example
usesp1_sdk::{utils,ProverClient,SP1ProofWithPublicValues,SP1Stdin};/// The ELF we want to execute inside the zkVM.constELF:&[u8]=include_bytes!("../../program/elf/riscv32im-succinct-zkvm-elf");fnmain(){// Setup logging.utils::setup_logger();// Create an input stream and write '500' to it.letn=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.letmutstdin=SP1Stdin::new();stdin.write(&n);// Create a `ProverClient` method.letclient=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.letclient=ProverClient::new();let(pk,vk)=client.setup(ELF);letmutproof=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>();leta=proof.public_values.read::<u32>();letb=proof.public_values.read::<u32>();println!("a: {}",a);println!("b: {}",b);// Verify proof and public valuesclient.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");letdeserialized_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 사이즈에 대해 크게 신경쓰지 않아도 될 때 사용합니다.
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
usesp1_sdk::{utils,ProverClient,SP1Stdin};/// The ELF we want to execute inside the zkVM.constELF:&[u8]=include_bytes!("../../program/elf/riscv32im-succinct-zkvm-elf");fnmain(){// Setup logging.utils::setup_logger();// Create an input stream and write '500' to it.letn=500u32;letmutstdin=SP1Stdin::new();stdin.write(&n);// Generate the proof for the given program and input.letclient=ProverClient::new();let(pk,vk)=client.setup(ELF);letproof=client.prove(&pk,stdin).plonk().run().unwrap();println!("generated proof");// Get the public values as bytes.letpublic_values=proof.public_values.raw();println!("public values: {:?}",public_values);// Get the proof as bytes.letsolidity_proof=proof.raw();println!("proof: {:?}",solidity_proof);// Verify proof and public valuesclient.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
pragmasolidity^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.
contractFibonacci{/// @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
addresspublicverifier;/// @notice The verification key for the fibonacci program.
bytes32publicfibonacciProgramVKey;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.
functionverifyFibonacciProof(bytescalldata_publicValues,bytescalldata_proofBytes)publicviewreturns(uint32,uint32,uint32){ISP1Verifier(verifier).verifyProof(fibonacciProgramVKey,_publicValues,_proofBytes);(uint32n,uint32a,uint32b)=abi.decode(_publicValues,(uint32,uint32,uint32));return(n,a,b);}}
ISP1Verifier Interface
모든 verifier는 ISP1Verifier 인터페이스를 구현해야 합니다.
// SPDX-License-Identifier: MIT
pragmasolidity^0.8.20;/// @title SP1 Verifier Interface
/// @author Succinct Labs
/// @notice This contract is the interface for the SP1 Verifier.
interfaceISP1Verifier{/// @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.
functionverifyProof(bytes32programVKey,bytescalldatapublicValues,bytescalldataproofBytes)externalview;}
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
.eslintrc 와 .eslintignore 파일을 참조하여 자동으로 설정파일을 만들어 주는 플러그인입니다. 제가 사용하는 설정의 경우 완벽하게 마이그레이션 되진 않았고 일부 수정이 필요한 부분이 있었습니다. 그래도 대부분의 설정이 마이그레이션 되어서 편하게 업데이트를 진행할 수 있었습니다.
아래 코드는 마이그레이션된 설정 파일입니다. 제가 개인적으로 사용하고 있는 설정이어서 다른 프로젝트에 적용하기 위해서는 수정이 필요할 수 있습니다.
vscode에 ESLint v9의 설정파일을 적용하기 위해서 eslint.useFlatConfig 설정을 활성화할 필요가 있습니다.
위 설정을 활성화하면 `eslint.config.js’ 파일을 참고하여 설정을 적용할 수 있습니다.
마치며
ESLint v9로 업데이트를 진행하면서 몇몇 설정이 변경되었지만, 대부분의 설정은 그대로 사용할 수 있었습니다. 마이그레이션도 Migrator의 존재 때문에 쉽게 진행할 수 있었고, vscode에서도 설정을 쉽게 적용할 수 있어서 편하게 업데이트를 진행할 수 있었습니다.
지금은 충분히 v9로 마이그레이션을 고려해도 좋을 만큼 충분히 안정화 되었다고 생각합니다.
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 명령어를 이용하여 새로운 프로젝트를 생성해 줍니다.
그리고 생성된 프로젝트의 Cargo.toml 파일에 아래와 같이 dependencies를 추가해 줍니다.
request / response 객체를 serialize / deserialize 하기 위해 serde 라이브러리를 사용합니다.
src/lib.rs 에 POST를 처리하기 위한 로직을 구현합니다.
경우의 수가 많지 않아(0,1,2,3,4 만 사용) 모든 경우를 array에 넣고 모든 경우를 시도하는 방법으로 구현하였습니다.
usehttp::StatusCode;useserde::{Deserialize,Serialize};usespin_sdk::{http::{IntoResponse,Method,Request,Response},http_component,};#[derive(Debug,Deserialize,Serialize)]structGameResponse{cows:u64,bulls:u64,gameId:String,guesses:u64,solved:bool,}#[http_component]asyncfnhandle_request(_req:http::Request<Vec<u8>>)->anyhow::Result<implIntoResponse>{letguess_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",];letstatus=StatusCode::OK;letbody="Done".to_string();leturi=format!("https://bulls-n-cows.fermyon.app/api?guess=012");letreq=Request::builder().method(Method::Get).uri(uri).build();// Send the request and await the responseletres:Response=spin_sdk::http::send(req).await?;letresponse:GameResponse=serde_json::from_slice(res.body().to_vec().as_slice()).unwrap();ifresponse.solved{println!("{:?}",response);Ok(Response::builder().status(status).header("content-type","application/json").body(body).build())}else{letgame_id=response.gameId;forguessinguess_numbers.iter(){leturi=format!("https://bulls-n-cows.fermyon.app/api?guess={}&id={}",guess,game_id);letreq=Request::builder().method(Method::Get).uri(uri).build();// Send the request and await the responseletres:Response=spin_sdk::http::send(req).await?;letresponse:GameResponse=serde_json::from_slice(res.body().to_vec().as_slice()).unwrap();ifresponse.solved{println!("{:?}",response);break;}}Ok(Response::builder().status(status).header("content-type","application/json").body(body).build())}}