# Run Jolt zkVM Verifier on-chain(IC) The canister is already running on the chain https://p6xvw-7iaaa-aaaap-aaana-cai.raw.ic0.app/ On April 9th, the A16z crypto team released the fastest zkVM for prover: Jolt, including [code](https://github.com/a16z/jolt), [examples](https://github.com/a16z/jolt/tree/main/examples), [documents](https://jolt.a16zcrypto.com/), [blogs](https://a16zcrypto.com/posts/tags/lasso-jolt), and papers([Lasso](https://eprint.iacr.org/2023/1216.pdf) and [Jolt](https://eprint.iacr.org/2023/1217.pdf)). ~~(The blogs contains a lot of insider information about the zk industry)~~ After running the examples, I tried integrating its verifier into the [IC](https://internetcomputer.org/) Canister.This thing is very meaningful and is actually an official [todo list](https://github.com/a16z/jolt/issues/209). IC is a high-performance blockchain Layer 1 that can use Rust to write smart contracts and compile them into WASM, and then deploy them to IC. Therefore, choosing IC has a high probability of success. ## Find verifier I went to look at the fibonacci example and found its verify function: ```rust= let (prove_fib, verify_fib) = guest::build_fib(); ``` Very good, I can get the `verify_fib` in the guest. When I go to `guest` to view, where is `build_fib`?🧐 ```rust= #![cfg_attr(feature = "guest", no_std)] #![no_main] #[jolt::provable] fn fib(n: u32) -> u128 { let mut a: u128 = 0; let mut b: u128 = 1; let mut sum: u128; for _ in 1..n { sum = a + b; a = b; b = sum; } b } ``` Well, Jolt uses macros to encapsulation and makes the Jolt develop-experience more ergonomic. To figure out the details, I need to expand the macro by `cargo expand -p fibonacci-guest --lib`, the whole code is [here](https://github.com/flyq/jolt_expand): <details> <summary><b>expanded code</b></summary> ```rust= #![cfg_attr(feature = "guest", no_std)] #![no_main] extern crate alloc; use alloc::rc::Rc; use alloc::vec::Vec; use jolt_core::{ host::Program, jolt::vm::{rv32i_vm::RV32IJoltVM, Jolt, JoltPreprocessing}, }; use jolt_sdk::{postcard, Proof, F, G, RV32IM}; pub fn build_fib() -> (impl Fn(u32) -> (u128, Proof), impl Fn(Proof) -> bool) { let (program, preprocessing) = preprocess_fib(); let program = Rc::new(program); let preprocessing = Rc::new(preprocessing); let program_cp = program.clone(); let preprocessing_cp = preprocessing.clone(); let prove_closure = move |n: u32| { let program = (*program).clone(); let preprocessing = (*preprocessing).clone(); prove_fib(program, preprocessing, n) }; let verify_closure = move |proof: Proof| { let _program = (*program_cp).clone(); let preprocessing = (*preprocessing_cp).clone(); RV32IJoltVM::verify(preprocessing, proof.proof, proof.commitments).is_ok() }; (prove_closure, verify_closure) } pub fn fib(n: u32) -> u128 { { let mut a: u128 = 0; let mut b: u128 = 1; let mut sum: u128; for _ in 1..n { sum = a + b; a = b; b = sum; } b } } pub fn analyze_fib(n: u32) -> (usize, Vec<(RV32IM, usize)>) { let mut program = Program::new("guest"); program.set_input(&n); program.trace_analyze() } pub fn preprocess_fib() -> (Program, JoltPreprocessing<F, G>) { let mut program = Program::new("guest"); program.set_func("fib"); let (bytecode, memory_init) = program.decode(); let preprocessing: JoltPreprocessing<F, G> = RV32IJoltVM::preprocess(bytecode, memory_init, 1 << 10, 1 << 10, 1 << 14); (program, preprocessing) } pub fn prove_fib( mut program: Program, preprocessing: JoltPreprocessing<F, G>, n: u32, ) -> (u128, Proof) { program.set_input(&n); let (io_device, bytecode_trace, instruction_trace, memory_trace, circuit_flags) = program.trace(); let output_bytes = io_device.outputs.clone(); let (jolt_proof, jolt_commitments) = RV32IJoltVM::prove( io_device, bytecode_trace, memory_trace, instruction_trace, circuit_flags, preprocessing, ); let ret_val = postcard::from_bytes::<u128>(&output_bytes).unwrap(); let proof = Proof { proof: jolt_proof, commitments: jolt_commitments, }; (ret_val, proof) } ``` </details> This code can't be run, because it [attempts to compile standard library](https://jolt.a16zcrypto.com/usage/troubleshooting.html#guest-attempts-to-compile-standard-library). But it provides `preprocessing`, `proof` and `verify` details. ```rust= #[derive(CanonicalSerialize, CanonicalDeserialize)] pub struct Proof { pub proof: RV32IJoltProof<F, G>, pub commitments: JoltCommitments<G>, } let preprocessing: JoltPreprocessing<F, G> = RV32IJoltVM::preprocess(bytecode, memory_init, 1 << 20, 1 << 20, 1 << 24); RV32IJoltVM::verify(preprocessing, proof.proof, proof.commitments).is_ok() ``` Therefore, the next work is simple. I only need to transfer the `JoltPreprocessing` and `Proof` data to IC Canister, and then call `RV32IJoltVM::verify` to complete the verification. ## Build smart contracts I implement a canister(smart contract), the project is here: https://github.com/flyq/jolt_verifier_canister/tree/master/src The main logic is that the smart contract accepts the serialized bytes of `proof` and `preprocess`, and then deserializes the data internally in the smart contract, and then performs verify. ```rust= use ark_bn254::{Fr, G1Projective}; use ark_serialize::CanonicalDeserialize; use jolt_core::jolt::vm::{rv32i_vm::RV32IJoltVM, Jolt, JoltPreprocessing}; use jolt_sdk::Proof; pub fn deserialize_proof(proof: &[u8]) -> Proof { Proof::deserialize_compressed(proof).unwrap() } pub fn deserialize_preprocessing(preprocessing: &[u8]) -> JoltPreprocessing<Fr, G1Projective> { JoltPreprocessing::deserialize_compressed(preprocessing).unwrap() } pub fn verify(preprocessing: JoltPreprocessing<Fr, G1Projective>, proof: Proof) -> bool { RV32IJoltVM::verify(preprocessing, proof.proof, proof.commitments).is_ok() } ``` In order to allow jolt to run in the WASM environment, I need to modify some contents of jolt. - [Delete criterion](https://github.com/flyq/jolt/commit/74b93dfe5f1fa9ad81a9196912a616bcddd1bb46), criterion is a library used for benchmarking in Rust, delete it has no side effects, and it cannot be compiled into Wasm. ```shell Rayon cannot be used when targeting wasi32. Try disabling default features. --> /Users/flyq/.cargo/registry/src/index.crates.io-6f17d22bba15001f/criterion-0.5.1/src/lib.rs:31:1 | 31 | compile_error!("Rayon cannot be used when targeting wasi32. Try disabling default features.... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` - [Save the proof data to file](https://github.com/flyq/jolt/commit/d66e8c5acc9ec6505e9e787da7182badd94c6884), so that I can use it later. ```rust= let (output, proof) = prove_fib(50); proof.save_to_file("./proof.bin").unwrap(); let is_valid = verify_fib(proof); ``` ## First Try ```shell= git clone https://github.com/flyq/jolt.git cd jolt cargo run --release -p fibonacci ``` It will run the fibonacci example, and compile the fibonacci program to executable file on the target platform(riscv32), and generate the proof and write the proof to `proof.bin` file, and verify the proof. compile result will store in here: ```shell= /tmp/jolt-guest-linkers: fibonacci-guest.ld /tmp/jolt-guest-target-fibonacci-guest-fib: CACHEDIR.TAG release riscv32i-unknown-none-elf ``` In order to easily obtain Preprocess from the compilation results and call canister, I constructed a [helper](https://github.com/flyq/jolt_verifier_canister/blob/master/helper/src/main.rs) program. If you set `Program.elf`, it will read the compilation results according to the `elf path`, otherwise it will try to recompile to riscv. ```rust= let mut program = Program::new("guest"); program.set_func("fib"); program.elf = Some( PathBuf::from_str( "/tmp/jolt-guest-target-fibonacci-guest-fib/riscv32i-unknown-none-elf/release/guest", ) .unwrap(), ); let (bytecode, memory_init) = program.decode(); ``` In short, I easily obtained `bytecode` and `memory_init`, and their sizes are relatively small (less than 2MB), which can meet the size requirements of IC smart contract calls (cannot be larger than 2MB). So, I put the `JoltPreprocessing` construct on the IC. As a result, the instructions required by this construct exceeded the [instruction limit](https://forum.dfinity.org/t/instruction-limit-is-crushing-me/22070/10?u=flyq) of a single call of the IC, and failed: ```rust= let preprocessing: JoltPreprocessing<Fr, G1Projective> = RV32IJoltVM::preprocess(bytecode, memory_init, 1 << 20, 1 << 20, 1 << 24); ``` ## Second Try This time, I tried constructing the `JoltPreprocessing` off-chain, serializing it, and uploading it to the IC. So, I need to [modify JoltPreprocessing](https://github.com/flyq/jolt/commit/b2449cf6fc80017493b220f2fd4e651873530bbb), in which adding `Default` and `De/Serialize` to `JoltPreprocessing`, and then I save the serialized JoltPreprocessing to file: ```rust= let preprocessing: JoltPreprocessing<Fr, G1Projective> = RV32IJoltVM::preprocess(bytecode, memory_init, 1 << 20, 1 << 20, 1 << 24); let file = File::create("preprocess.bin").unwrap(); preprocessing.serialize_compressed(file).unwrap(); ``` but It is too large(~48MB) to upload to IC canister: ```shell= ls -al preprocess.bin -rw-r--r-- 1 flyq staff 48386392 Apr 15 10:36 preprocess.bin ``` Then I tried to modify the size of the problem in the example, from solving `fib(50)` to `fib(3)`. The size is still about 48MB, much larger than 2MB. Then I tried to modify parameters such as the maximum memory from $2^{20}$ to $2^{10}$, but the size was still 48 MB: ```rust= let preprocessing: JoltPreprocessing<Fr, G1Projective> = RV32IJoltVM::preprocess(bytecode, memory_init, 1 << 10, 1 << 10, 1 << 14); ``` ## Third Try Thanks to [Dominic Wörner](https://twitter.com/domiwoe) and [Wyatt Benno](https://twitter.com/wyatt_benno) for the tips. This time I did the following work, trying to split the `Preprocess` into multiple data blocks, and then assemble them in advance in the canister. But when the `Proof` is uploaded and verified, the error of exceed instruction limit is still coming out. Therefore, I also need to upload the `Proof` bytes in advance, save the deserialized Proof, and then in a new update call, get the prepared Preprocess and Proof, directly call `verify`, and finally there is no instruction limit error and success, obtained a true verification result During this process, Canister requires `Proof` to implement the Clone trait, so I [added the Clone trait](https://github.com/flyq/jolt/commit/98d1645e8cb8faa9e4dce713d90edf8efd789d3d) to Proof in Jolt lib, and stirred up a hornet's nest. Finally, I added Clone to all 55 data structures that make up Proof and successfully passed the compilation. The advantage is that I completely know what components Proof and Commitment are composed of. ## Next Plan Waiting the optimizing Rust verifier: https://github.com/a16z/jolt/issues/216