Proving
Proving with Pico CLI and SDK APIs
Overview
Pico provides CLI and SDK tools to recursively prove the program to the developers.
Pico CLI provides a complete toolchain for compiling the RISC-V program and using Pico to complete end-to-end proof. Refer to the installation page to install the CLI toolchain. CLI default use the KoalaBear field for the backend proving, if you want to switch to other fields, read more details in Proving Backends Page.
Like the CLI, the Pico-SDK includes lower-level APIs that can prove the program directly. The prover package of the template project repository provides an example of how to import and initialize the SDK and quickly generate a RISC-V proof using the Pico SDK. In the Proving Steps Section, you can read more about VM e2e proving and the Gnark EVM proof generation for On-chain verification
Let's quickly go through the Pico SDK usage and generate a Fibonacci RISC-V proof.
Import
pico-sdk
# Cargo.toml
pico-sdk = { git = "https://github.com/brevis-network/pico" }
Execute the proving process and generate RISC-V proof.
// prover/src/main.rs
fn main() {
// Initialize logger
init_logger();
// Load the ELF file
let elf = load_elf("../elf/riscv32im-pico-zkvm-elf");
// Initialize the prover client
let client = DefaultProverClient::new(&elf);
// Initialize new stdin
let mut stdin_builder = client.new_stdin_builder();
// Set up input and generate proof
let n = 100u32;
stdin_builder.write(&n);
// Generate proof
let proof = client.prove_fast(stdin_builder).expect("Failed to generate proof");
// Decodes public values from the proof's public value stream.
let public_buffer = proof.pv_stream.unwrap();
let public_values = PublicValuesStruct::abi_decode(&public_buffer, true).unwrap();
// Verify the public values
verify_public_values(n, &public_values);
}
/// Verifies that the computed Fibonacci values match the public values.
fn verify_public_values(n: u32, public_values: &PublicValuesStruct) {
println!(
"Public value n: {:?}, a: {:?}, b: {:?}",
public_values.n, public_values.a, public_values.b
);
// Compute Fibonacci values locally
let (result_a, result_b) = fibonacci(0, 1, n);
// Assert that the computed values match the public values
assert_eq!(result_a, public_values.a, "Mismatch in value 'a'");
assert_eq!(result_b, public_values.b, "Mismatch in value 'b'");
}
Pico EmulatorStdin
Stdin Writer
Pico SDK supports writing the serializable object and bytes to Pico.
/// Write a serializable struct to the buffer.
pub fn write<T: Serialize>(&mut self, data: &T);
/// Write a slice of bytes to the buffer.
pub fn write_slice(&mut self, slice: &[u8]);
Examples:
use std::vec;
use pico_sdk::client::SDKProverClient;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct FibonacciInputs {
pub a: u32,
pub b: u32,
pub n: u32,
}
fn main() {
// Initialize the prover client
let client = SDKProverClient::new(&elf, false);
// Initialize new stdin
let mut stdin_builder = client.new_stdin_builder();
// example 1: write a u32 to the VM
let n = 100u32;
stdin_builder.write(&n);
// example 2: write a struct
let inputs = FibonacciInputs { a: 0, b: 1, n };
stdin_builder.write(&inputs);
// example 3: write a byte array
let bytes = vec![1, 2, 3, 4];
stdin_builder.write_slice(&bytes);
}
CLI input option
The prove
command --input
option can take a hex string or a file path. the hex string must be match the length of the read type. For example, the input n = 100u32
; the hex string should be 0x0A000000
in little-endian format.
RUST_LOG=info cargo pico prove --input "0x0A000000" --fast
Read
Corresponding to the writer functions, there are read_as and read_slice tools for io reading the serializable object or bytes into the program.
SDK examples:
use pico_sdk::io::{read_as, read_vec};
#[derive(Serialize, Deserialize)]
pub struct FibonacciInputs {
pub a: u32,
pub b: u32,
pub n: u32,
}
fn main() {
// example 1: read the u32 input `n`
let n: u32 = read_as();
// example 2: read FibonacciInputs struct
let inputs = read_as::<FibonacciInputs>();
// example 3: read a byte array
let bytes: Vec<u8> = read_vec();
}
End-to-end Proving
This section introduces more advanced CLI options and SDK APIs to complete the end-to-end proving process. The Proving process consists of multiple stages, including RISCV, RECURSION, and EVM Phases. Pico SDK includes various ProverClients in different proving backends. Here, we use the KoalaBearProverClient (based on STARK on KoalaBear) in the example code.
RISCV-Phase
Prove RISC-V programs and generate an uncompressed proof with the --fast option. The command is mainly used to test and debug the program.
CLI:
RUST_LOG cargo pico prove --fast
For example, when executing the fast proving with inputs in the Fibonacci, the input n
is a u32
data received through pico::sdk::read_as
, and it must be in little-endian format and filled to 4 bytes.
RUST_LOG=info cargo pico prove --input "0x0A000000" --fast
SDK:
// Initialize the SDK.
let client = DefaultProverClient::new(&elf);
// Initialize new stdin and write the inputs by builder.
let mut stdin_builder = client.new_stdin_builder();
// Set up input
let n = 100u32;
stdin_builder.write(&n);
let riscv_proof = client.prove_fast(stdin_builder).expect("Failed to generate proof");
Fast proving is implemented by using only one FRI query which drastically reduces the theoretical security bits. DO NOT USE THIS OPTION IN PRODUCTION. ATTACKERS MAY BE ABLE TO COMMIT TO INVALID TRACES.
RECURSION-Phase
CLI:
RUST_LOG cargo pico prove --field kb # kb: koalabear (default), bb:babebear
Proving without the --fast
argument will execute the prover up to and including the EMBED-Phase. The resulting proof can then be verified by the Gnark
proof verification circuit, which can then be verified on-chain via contract.
options:
--field
Specify the field, When without this option, default to Koalabear field.
kb: Koalabear
bb: Babybear
--output
You can specify the output path to generate the files prepared for the Gnark
verification and default is in the project root target/pico_out/
RUST_LOG cargo pico prove --output outputs
SDK:
// Initialize the SDK.
let client = DefaultProverClient::new(&elf);
// ... write to stdin as previously described
let (riscv_proof, embed_proof) = client.prove(stdin_builder)?;
let output_dir = PathBuf::from_str(&"./outputs").expect("the output dir is invalid");
client.write_onchain_data(output, &riscv_proof, &embed_proof)?;
Outputs
constraints.json
: The schema of the stark proof constraints is used to transform to Gnark circuit constraints.groth16_witness.json
: input witnessness of Gnark circuit.
EVM-Phase
The Pico CLI provides an EVM option to generate the program Groth16 proof and verifier Contracts. You must ensure the Docker has been installed when using the evm option.
CLI:
# Setup groth16 PK/VK if its never generated or a new version update.
cargo pico prove --evm --setup
# generate groth16 proof
cargo pico prove --evm
SDK:
// Initialize the SDK.
let client = KoalaBearProveVKClient::new(&elf);
let output_dir = PathBuf::from_str(&"./outputs").expect("the output dir is invalid");
// The second argument need_setup should be true when you haven't setup groth16 pk/vk yet.
// The last argument selects the proving backend: use "kb" for KoalaBear or "bb" for BabyBear.
prover_client.prove_evm(stdin_builder, true, output_path, "kb").expect("Failed to generate evm proof");
The outputs:
proof.data
: Groth16
proof generated by the Gnark Verifier Circuit.
pv_file
: The public values hex string; it's the input of Fibonacci
Contract
When executing EVM proving, the Gnark
Groth16
ProvingKey/VerificationKey is also generated at this step. The --setup
only needs to be executed once to make sure the PK/VK is generated.
EVM Verification
The generated inputs.json
format is as follows:
{
"riscvVKey": "bytes32",
"proof": "bytes32[]",
"publicValues": "bytes"
}
After parsing the input data, you can call the PicoVerifier.sol
as shown below:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title Pico Verifier Interface
/// @author Brevis Network
/// @notice This contract is the interface for the Pico Verifier.
interface IPicoVerifier {
/// @notice Verifies a proof with given public values and riscv verification key.
/// @param riscvVkey The verification key for the RISC-V program.
/// @param publicValues The public values encoded as bytes.
/// @param proof The proof of the riscv program execution in the Pico.
function verifyPicoProof(
bytes32 riscvVkey,
bytes calldata publicValues,
uint256[8] calldata proof
) external view;
}
The verifyPicoProof
function in PicoVerifier.sol
takes a RISC-V verification key, public values, and a Pico proof, using the Groth16 verifier to validate the proof and public inputs via the pairing algorithm. For the full implementation of the PicoVerifier, please refer to the repository here.
In production, you need to verify riscvVKey and parse the public values verified by PicoVerifier. You can refer to the Fibonacci.sol example in the repository here.
Last updated