The #[jolt::provable] macro is the primary way to create provable functions in Jolt. It transforms a regular Rust function into a full zkVM program with generated host-side proving and verification infrastructure.
Basic Usage
#![cfg_attr(feature = "guest" , no_std)]
#[jolt :: provable]
fn fibonacci ( n : u32 ) -> u128 {
let mut a = 0 u128 ;
let mut b = 1 u128 ;
for _ in 1 .. n {
let sum = a + b ;
a = b ;
b = sum ;
}
b
}
This generates several host-side functions:
compile_fibonacci(target_dir) - Compiles guest to RISC-V
preprocess_shared_fibonacci(program) - Shared preprocessing
preprocess_prover_fibonacci(shared) - Prover-specific preprocessing
preprocess_verifier_fibonacci(shared, generators) - Verifier-specific preprocessing
build_prover_fibonacci(program, preprocessing) - Returns proving closure
build_verifier_fibonacci(preprocessing) - Returns verification closure
prove_fibonacci(...) - Direct proof generation
analyze_fibonacci(...) - Program analysis without proving
Parameters
The macro accepts configuration parameters:
Memory Configuration
#[jolt :: provable(
heap_size = 1048576, // 1 MB heap (default: 4096)
stack_size = 4096, // 4 KB stack (default: 4096)
max_input_size = 4096, // Max input bytes (default: 4096)
max_output_size = 4096, // Max output bytes (default: 4096)
max_untrusted_advice_size = 0, // Untrusted advice (default: 0)
max_trusted_advice_size = 0, // Trusted advice (default: 0)
)]
fn my_function ( input : Vec < u8 >) -> Vec < u8 > {
// Function body
}
Memory sizes must be powers of 2 or the linker will fail. These values determine the guest’s memory layout.
Trace Configuration
#[jolt :: provable(
max_trace_length = 1048576 // Max execution steps (default: 1048576)
)]
fn compute ( x : u64 ) -> u64 {
// Complex computation
}
The trace length affects:
Maximum program complexity
Preprocessing time
Proof generation time
Development Features
#[jolt :: provable(
backtrace = "full" , // Enable backtraces: "full" or "simple"
profile = "release" // Build profile: "release" or "dev"
)]
fn debug_function ( x : u32 ) -> u32 {
x * 2
}
Backtraces and debug profiles increase guest binary size and execution time. Use only during development.
Function Signatures
Regular parameters become public inputs to the proof:
#[jolt :: provable]
fn verify_hash ( data : Vec < u8 >, expected_hash : [ u8 ; 32 ]) -> bool {
let computed = sha256 ( & data );
computed == expected_hash
}
Both data and expected_hash are public inputs that must be provided by both prover and verifier.
Trusted Advice
Wrap parameters in TrustedAdvice<T> to mark them as committed advice:
#[jolt :: provable]
fn prove_knowledge (
public_key : [ u8 ; 32 ],
secret_key : jolt :: TrustedAdvice <[ u8 ; 32 ]>
) -> bool {
derive_public_key ( & secret_key ) == public_key
}
The prover commits to trusted advice before seeing the challenge. The verifier receives only the commitment.
Untrusted Advice
Wrap parameters in UntrustedAdvice<T> for witness data:
#[jolt :: provable]
fn verify_membership (
root : [ u8 ; 32 ],
leaf : [ u8 ; 32 ],
proof : jolt :: UntrustedAdvice < Vec <[ u8 ; 32 ]>>
) -> bool {
// Verify Merkle proof
verify_merkle_proof ( root , leaf , & proof )
}
Untrusted advice is provided by the prover but must be verified by the guest.
See Runtime Advice for the difference between trusted and untrusted advice.
Return Values
Return values become public outputs:
#[jolt :: provable]
fn compute_hash ( data : Vec < u8 >) -> [ u8 ; 32 ] {
sha256 ( & data )
}
Both prover and verifier know the output value. For private outputs, write to trusted advice instead.
Generated Functions
The macro generates a complete proving/verification workflow:
Compilation
let mut program = compile_my_function ( "/tmp/jolt-guest" );
Compiles the guest function to RISC-V ELF binary. The binary is cached in the target directory.
Preprocessing
Preprocessing happens in phases:
// Phase 1: Shared preprocessing (used by both prover and verifier)
let shared = preprocess_shared_my_function ( & mut program );
// Phase 2: Prover preprocessing
let prover_preprocessing = preprocess_prover_my_function ( shared . clone ());
// Phase 2: Verifier preprocessing
let verifier_generators = /* commitment scheme setup */ ;
let verifier_preprocessing = preprocess_verifier_my_function (
shared ,
verifier_generators
);
See Preprocessing for details.
Building Closures
Create reusable proving/verification closures:
// Build prover
let prove = build_prover_my_function ( program , prover_preprocessing );
// Build verifier
let verify = build_verifier_my_function ( verifier_preprocessing );
// Use multiple times
let ( output1 , proof1 , _io1 ) = prove ( input1 );
let ( output2 , proof2 , _io2 ) = prove ( input2 );
let valid1 = verify ( input1 , output1 , false , proof1 );
let valid2 = verify ( input2 , output2 , false , proof2 );
Direct Proving
For one-off proofs:
let ( output , proof , program_io ) = prove_my_function (
program ,
preprocessing ,
public_input ,
untrusted_advice ,
);
Analysis
Analyze program execution without generating a proof:
let summary = analyze_my_function ( input );
println! ( "Cycles: {}" , summary . total_cycles);
println! ( "Memory used: {}" , summary . memory_used);
Complete Example
Guest (guest/src/lib.rs):
#![cfg_attr(feature = "guest" , no_std)]
#[jolt :: provable(
heap_size = 32768,
max_trace_length = 65536,
max_input_size = 8192,
max_output_size = 32
)]
fn hash_chain ( data : Vec < u8 >, iterations : u32 ) -> [ u8 ; 32 ] {
let mut hash = sha256 ( & data );
for _ in 0 .. iterations {
hash = sha256 ( & hash );
}
hash
}
Host (src/main.rs):
use guest ::* ;
fn main () {
// Compile
let mut program = compile_hash_chain ( "/tmp/jolt" );
// Preprocess (once per program)
let shared = preprocess_shared_hash_chain ( & mut program );
let preprocessing = preprocess_prover_hash_chain ( shared );
// Build prover
let prove = build_prover_hash_chain ( program , preprocessing );
// Prove
let data = b"Hello, Jolt!" . to_vec ();
let iterations = 100 ;
let ( hash , proof , _ ) = prove ( data . clone (), iterations );
// Verify
let verifier_preprocessing = /* ... */ ;
let verify = build_verifier_hash_chain ( verifier_preprocessing );
assert! ( verify ( data , iterations , hash , false , proof ));
}
Best Practices
Set memory limits appropriately
Overallocation wastes proving time. Measure actual usage with analyze_* before finalizing limits.
Large public inputs increase verification time. Use advice for large witness data.
Use trusted advice for secrets
Don’t pass secrets as public inputs. Use TrustedAdvice<T> instead.
Preprocessing is expensive. Reuse the same preprocessing for multiple proofs of the same program.
Limitations
Function must have a determinate signature (no generics in macro itself)
All types must implement Serialize + Deserialize (via postcard)
Advice types must implement AdviceTapeIO
Function cannot be async or have special ABIs