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, examples, documents, blogs, and papers(Lasso and Jolt). (The blogs contains a lot of insider information about the zk industry)
After running the examples, I tried integrating its verifier into the IC Canister.This thing is very meaningful and is actually an official todo list.
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.
I went to look at the fibonacci example and found its verify function:
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
?🧐
#![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:
#![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)
}
This code can't be run, because it attempts to compile standard library. But it provides preprocessing
, proof
and verify
details.
#[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.
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.
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.
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....
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
let (output, proof) = prove_fib(50);
proof.save_to_file("./proof.bin").unwrap();
let is_valid = verify_fib(proof);
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:
/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 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.
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 of a single call of the IC, and failed:
let preprocessing: JoltPreprocessing<Fr, G1Projective> =
RV32IJoltVM::preprocess(bytecode, memory_init, 1 << 20, 1 << 20, 1 << 24);
This time, I tried constructing the JoltPreprocessing
off-chain, serializing it, and uploading it to the IC.
So, I need to modify JoltPreprocessing, in which adding Default
and De/Serialize
to JoltPreprocessing
, and then I save the serialized JoltPreprocessing to file:
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:
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
let preprocessing: JoltPreprocessing<Fr, G1Projective> =
RV32IJoltVM::preprocess(bytecode, memory_init, 1 << 10, 1 << 10, 1 << 14);
Thanks to Dominic Wörner and 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 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.
Waiting the optimizing Rust verifier: https://github.com/a16z/jolt/issues/216