taikoxyz / raiko

Multi-proofs for Taiko. SNARKS, STARKS and Trusted Execution Enclave. Our previous ZK-EVM circuits are deprecated.
Apache License 2.0
124 stars 91 forks source link

feat(raiko): generalized build pipeline for ZkVMs guests #133

Closed CeciliaZ030 closed 6 months ago

CeciliaZ030 commented 6 months ago

Problem:

Solution

I'm gonna make the build pipeline myself so that I don't want to bother about the upstream, and also to test and benchmark my code however i want. also. Here are some of the design considerations:

I'm making the build stage explicit, which means having a builder crate to replace build.rs. Within the pipeline, there are two steps: 1) configure the right flags and env to generate a correct command 2) execute the command and place the ELFs in some user-specified directories. Resulting file structure:

example
├── Cargo.lock
├── Cargo.toml
├── builder
│   ├── Cargo.toml
│   └── src
├── driver
│   ├── Cargo.toml
│   └── src
├── guest
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── methods
│   ├── src
│   └── target
└── target

Workflow

Build the ELFs

$ cd builder & cargo build --features <zkvm>

Run the ELFs

$ cd driver & cargo +nightly run --release --features <zkvm>

Builder

The resultant pipeline starts with parsing the Cargo manifest, then setup the Builder with the right flags.

let meta = parse_metadata(project);
let mut builder = GuestBuilder::new(&meta, "riscv32im-risc0-zkvm-elf", "risc0")
    .rust_flags(&[
        "passes=loweratomic",
        "link-arg=-Ttext=0x00200800",
        "link-arg=--fatal-warnings",
        "panic=abort",
    ])
    .cc_compiler(
        risc0_data()
            .unwrap()
            .join("cpp/bin/riscv32-unknown-elf-gcc"),
    )
    .c_flags(&["-march=rv32im", "-nostdlib"]);

The executor runs the desired action (e.g. cargo build or cargo test) and finally place the ELF to specified dest directory.

let executor = if !test {
    builder.build_command(profile, bins)
} else {
    builder.test_command(profile, bins)
};

executor
    .execute()
    .expect("Execution failed")
    .risc0_placement(dest)
    .expect("Failed to export Ris0 artifacts");

Test Harness

I implemented a light-weight harness to build test suits and run them in the ELF's main. The main with normal cargo test compilation is an alternative entrypoint is created that collide with the ZkVM entrypoint. The resulted test workflow looks like this:

// In program/src/main.rs
#![no_main]
sp1_zkvm::entrypoint!(main);
use harness::{zk_suits, zk_test, TestSuite};
pub fn main() {
    println!("Hello from example");

    #[cfg(test)]
    zk_suits!(test_ok, test_fail);
}

#[test]
pub fn test_ok() {
    assert_eq!(1, 1);
}

#[test]
pub fn test_fail() {
    assert_eq!(1, 2);
}

After you write tests in the guest program, compile them:

// In build.rs
pipeline::sp1::tests("../example-sp1", &["example", "foo"]);

This will gives you binaries equivalent to cargo test --no-run --bin example -- bin foo. Then in the host:

#[test]
fn test_example() {
    // Generate the proof for the given program.
    let mut client = ProverClient::new();
    let stdin = SP1Stdin::new();
    let (pk, vk) = client.setup(TEST_EXAMPLE);
    let proof = client.prove(&pk, stdin).expect("proving failed");
    client.verify(&proof, &vk).expect("verification failed");
}
CeciliaZ030 commented 6 months ago
  • some other minor things

? dude what's the point of making this a point lol

Brechtpd commented 6 months ago
  • some other minor things

? dude what's the point of making this a point lol

Minor things that I can't be bothered to type out. :duck: