Skip to main content

Jolt

For users of the Jolt zkVM.

info

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.

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:

  1. Sindri currently supports two polynomial commitments schemes: HyperKZG and Zeromorph. The commitmentScheme field in the sindri.json file must be set to one of these two values: hyperkzg or zeromorph.
  2. Sindri supports guest code that uses the Rust standard library and guest code written in a no_std environment. The stdEnabled field in the sindri.json file must be set to true or false depending on whether the standard library is enabled.
  3. The provingScheme and circuitType must both be set to jolt and the joltVersion field must be set to "0.1.0".
  4. 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"
}

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:

  1. A 'compile_guest_code' method to upload the circuit to Sindri and compile it.
  2. A 'prove_guest_code' method which generates a proof for the circuit.
  3. 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:

  1. The Proof struct, which contains the Jolt proof and Jolt commitment fields.
  2. The Preprocessing struct, which contains the Jolt bytecode and memory_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);