Jolt
For users of the Jolt zkVM.
This tutorial corresponds to the Jolt version associated with the following commit hash: "55c577f5a859eee843fd88d1d14bd2127b3ddd1d".
For information on the Jolt zkVM, please refer to the official Jolt documentation.
Introductionβ
Jolt is a general purpose zero-knowledge virtual machine built around the Lasso lookup argument. The Jolt zkVM proves correct execution of arbitrary Rust programs by compiling the Rust code to a RISCV target and then proving that the resulting execution trace follows the rules of the RISCV 32-bit ISA. Along with its modular design, Jolt's support for multiple polynomial commitment schemes makes it poised to deliver exceptionally competitive performance.
We will walk through the process of how to use the Jolt zkVM on Sindri. The sample guest code program used in this tutorial is based on the Sha3 hash chain example from the Jolt examples directory.
Project directory structureβ
Guest code being submitted to Sindri should be organized according to the following directory structure:
βββ πguest
β βββ πCargo.toml
β βββ πsindri.json
β βββ πsrc
β βββ πlib.rs
β βββ πutils.rs
Note: The utils.rs
file is optional but recommended.
Further information can be found in the guest code
tab below.
For detailed descriptions of these files, you can click through the different tabs here.
- sindri.json
- Cargo.toml
- guest code
Any guest code submitted to Sindri must include a sindri.json
file defining certain metadata about your guest code.
There are a few key pieces of information here that are required:
- Sindri currently supports two polynomial commitments schemes: HyperKZG and Zeromorph.
The
commitmentScheme
field in thesindri.json
file must be set to one of these two values:hyperkzg
orzeromorph
. - Sindri supports guest code that uses the Rust standard library and guest code written in a
no_std
environment. ThestdEnabled
field in thesindri.json
file must be set totrue
orfalse
depending on whether the standard library is enabled. - The
provingScheme
andcircuitType
must both be set tojolt
and thejoltVersion
field must be set to"0.1.0"
. - The
guestFunction
field must be set to the exact name of the function used in the guest code.
{
"name": "sha3_chain",
"circuitType": "jolt",
"provingScheme": "jolt",
"commitmentScheme": "hyperkzg",
"joltVersion": "0.1.0",
"stdEnabled": true,
"packageName": "guest",
"guestFunction": "sha3_chain"
}
Currently, Jolt does not have semantic versioning, so we select a version by pinning to the commit hash shown below.
[package]
name = "guest"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "guest"
path = "src/lib.rs"
[features]
guest = []
[dependencies]
jolt = { package = "jolt-sdk", git = "https://github.com/a16z/jolt", rev="55c577f5a859eee843fd88d1d14bd2127b3ddd1d", features = ["guest-std"] }
serde = { version = "1.0", features = ["derive"] }
sha3 = { version = "0.10.8"}
Note that for guest code with no_std
enabled, the imports for jolt
and serde
should be as follows:
[dependencies]
jolt = { package = "jolt-sdk", git = "https://github.com/a16z/jolt", rev="55c577f5a859eee843fd88d1d14bd2127b3ddd1d"}
serde = { version = "1.0", default-features=false, features = ["derive"] }
Inputs and outputsβ
The guest function signature in the lib.rs
file should only take one argument of type Input
and return a variable of type Output
.
The fields for these structs may be comprised of any serializable types.
These structs may be defined in the guest code, though by convention it is recommended to define them in a separate utils.rs
file.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Input {
pub input: [u8; 32],
pub num_iters: u32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Output {
pub output: [u8; 32],
}
If defined in the utils.rs
file, the Input
and Output
structs should be imported to lib.rs
with public visibility as follows:
pub mod utils;
pub use utils::{Input, Output};
Guest codeβ
Your guest program code should be contained in the lib.rs
file.
The lib.rs
file should contain the jolt::provable
attribute macro, which will be used to mark the function that will be proven.
For programs that do not use the standard library, the #![cfg_attr(feature = "guest", no_std)]
attribute should be included in the first line of the file.
#![no_main]
pub mod utils;
pub use utils::{Input, Output};
use sha3::{Digest, Keccak256};
#[jolt::provable]
fn sha3_chain(pre_image: Input) -> Output {
let mut hash = pre_image.input;
for _ in 0..pre_image.num_iters {
let mut hasher = Keccak256::new();
hasher.update(pre_image.input);
let res = &hasher.finalize();
hash = Into::<[u8; 32]>::into(*res);
}
let output = Output { output: hash };
output
}
Sindri APIβ
We'll walk through the steps to upload your Jolt guest code to Sindri, generate proofs, and verify proofs. We will use a Rust script to interact with the Sindri API.
Setupβ
Clone the Sindri resources repository and navigate to the sha3_chain
tutorial.
git clone https://github.com/Sindri-Labs/sindri-resources.git
cd sindri-resources/circuit_tutorials/jolt/sha3_chain
Make sure that the sha3_chain
project directory contains the following files:
π¦ sha3_chain
βββ πCargo.toml
βββ πinput.json
βββ πguest
β βββ πCargo.toml
β βββ πsindri.json
β βββ πsrc
β βββ πlib.rs
β βββ πutils.rs
βββ πREADME.md
βββ πrust-toolchain
βββ πrustfmt.toml
βββ πsample.env
βββ πsrc
βββ πmain.rs
You will also need your Sindri API key to authenticate all requests to the API, so make sure you have this ready (see #api-authentication).
Modify the sample.env
file to include your Sindri API key and rename it to .env
.
In order to run the compile, prove, and verify steps for the Sha3-chain guest code, run the following command from the root of the sha3_chain
directory:
cargo run --release
Upload & Compileβ
The main function in sha3_chain/src/main.rs
is comprised of three parts:
- A 'compile_guest_code' method to upload the circuit to Sindri and compile it.
- A 'prove_guest_code' method which generates a proof for the circuit.
- Additional code to verify the proof.
Compileβ
The compile_guest_code
method will upload the guest code to Sindri and compile the Rust code to a RISCV binary.
If the compile checks pass, a json
file will be outputted to data/compile_out.json
.
{
"circuit_id": "e4a70edb-6c6f-4d8a-89d8-628b5001cdef",
"circuit_name": "sha3_chain",
"circuit_type": "jolt",
"commitment_scheme": "hyperkzg",
"compute_time": "P0DT00H03M19.661830S",
"compute_time_sec": 199.66183,
"compute_times": {
"clean_up": 0.34369,
"compile": 186.02561,
"compile_guest": 10.88625,
"save_results": 2.40629,
"total": 199.66183
},
"date_created": "2024-10-10T06:31:50.903Z",
"error": null,
"file_size": 91557423,
"guest_function": "sha3_chain",
"has_smart_contract_verifier": false,
"has_verification_key": false,
"jolt_version": "0.1.0",
"meta": {},
"num_proofs": 0,
"package_name": "guest",
"project_name": "sha3_chain",
"proving_scheme": "jolt",
"public": false,
"queue_time": "P0DT00H00M00.669550S",
"queue_time_sec": 0.66955,
"status": "Ready",
"std_enabled": true,
"tags": ["latest"],
"team": "*******",
"team_avatar_url": "*******",
"team_slug": "*******",
"uploaded_file_name": "filename.filetype",
"verification_key": null,
"warnings": null
}
Proveβ
The prove_guest_code
method requires a path to a valid input file.
The input.json
file contains an array of 32 integers and a value indicating the number of hashing cycles to perform in the guest code.
{
"input": [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 29, 30, 31, 32
],
"num_iters": 5
}
The prove_guest_code
method will pull the circuit ID from the compile_out.json
file.
The proof data is returned to the user in the data/proof_out.json
file.
The proofs
field of this JSON file contains all fo the information required to verify the proof locally using Jolt's Rust verifier.
The public
field contains the public inputs to the zkVM.
{
"circuit_id": "e4a70edb-6c6f-4d8a-89d8-628b5001cdef",
"circuit_name": "sha3_chain",
"circuit_team": "******",
"circuit_team_avatar_url": "https://gravatar.com/avatar/758874998f5bd0c393da094e1967a72b?s=400&d=identicon&r=x",
"circuit_team_slug": "******",,
"circuit_type": "jolt",
"compute_time": "P0DT00H00M09.613499S",
"compute_time_sec": 9.613499,
"compute_times": {
"clean_up": 0.00815,
"file_setup": 0.48771,
"prove": 6.07049,
"save_results": 0.79601,
"total": 9.6135,
"verify_check": 2.25115
},
"date_created": "2024-10-10T06:35:12.185Z",
"error": null,
"file_size": 657653,
"has_smart_contract_calldata": false,
"has_verification_key": false,
"meta": {},
"perform_verify": false,
"project_name": "sha3_chain",
"proof": "******",
"proof_id": "720888a9-6762-41d1-833a-afe0b08cce8d",
"public": {
"inputs": {
"input": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32
],
"num_iters": 5
},
"memory_layout": {
"input_end": 2147471424,
"input_start": 2147467328,
"max_input_size": 4096,
"max_output_size": 4096,
"output_end": 2147475521,
"output_start": 2147471425,
"panic": 2147475522,
"ram_witness_offset": 16384
},
"outputs": {
"output": [
82,
179,
245,
63,
241,
150,
162,
142,
125,
45,
1,
40,
62,
249,
66,
112,
112,
189,
166,
65,
40,
251,
86,
48,
185,
123,
106,
177,
122,
143,
240,
168
]
},
"panic": false
},
"queue_time": "P0DT00H00M00.486548S",
"queue_time_sec": 0.486548,
"smart_contract_calldata": null,
"status": "Ready",
"team": "******",
"team_avatar_url": "https://gravatar.com/avatar/758874998f5bd0c393da094e1967a72b?s=400&d=identicon&r=x",
"team_slug": "******",
"verification_key": null,
"warnings": null
}
Verifyβ
In order to verify Jolt proofs, we need the following two Rust structs:
- The
Proof
struct, which contains the Joltproof
and Joltcommitment
fields. - The
Preprocessing
struct, which contains the Joltbytecode
andmemory_init
fields.
These structs can be deserialized from the proof_out.json
file in the data folder using the following code:
let proof_path: &str = "./data/prove_out.json";
let mut file = File::open(proof_path).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let proof_details: Value = serde_json::from_str(&contents).unwrap();
let json_data: JsonProofData = serde_json::from_value(proof_details["proof"].clone()).unwrap();
let (jolt_proof_struct, jolt_preprocessing_struct) =
deserialize_jolt_proof_data_from_base64::<Fr, HyperKZG<Bn254>>(json_data);
If you generated proofs using zeromorph
, you should replace HyperKZG<Bn254>
with Zeromorph<Bn254>
.
Before we can verify the proof, we need to recreate the preprocessing step that was performed by the Jolt zkVM when generating the proof.
let preprocessing = RV32IJoltVM::preprocess(
jolt_preprocessing_struct.bytecode,
jolt_preprocessing_struct.memory_init,
1 << 20,
1 << 20,
1 << 22,
);
Now, we can verify the proof as follows:
let verification_result = RV32IJoltVM::verify(
preprocessing,
jolt_proof_struct.proof,
jolt_proof_struct.commitments,
None,
);
We can also obtain the input and outputs of the guest program from the proof_out.json
file as follows:
let public_data = proof_details["public"].clone();
println!("Public data: {}", public_data);