Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ members = [
"examples/merkle-tree/guest",
"examples/hash-bench",
"examples/hash-bench/guest",
"examples/modexp",
"examples/modexp/guest",
"zklean-extractor",
]

[features]
host = ["jolt-sdk/host"]

Expand Down
12 changes: 12 additions & 0 deletions examples/modexp/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "modexp"
version = "0.1.0"
edition = "2021"

[dependencies]
jolt-sdk = { path = "../../jolt-sdk", features = ["host"] }
tracing-subscriber = "0.3"
tracing = "0.1"
guest = { package = "modexp-guest", path = "./guest" }

hex = "0.4.3"
10 changes: 10 additions & 0 deletions examples/modexp/guest/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "modexp-guest"
version = "0.1.0"
edition = "2021"

[features]
guest = []

[dependencies]
jolt = { package = "jolt-sdk", path = "../../../jolt-sdk", features = [] }
37 changes: 37 additions & 0 deletions examples/modexp/guest/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#![cfg_attr(feature = "guest", no_std)]

#[jolt::provable(memory_size = 10240, max_trace_length = 65536)]
fn modexp_chain(base: u128, exp: u128, modulus: u128, num_iters: u32) -> u128 {
// multiply modulo m without overflowing by using shift-add
fn mul_mod(mut a: u128, mut b: u128, m: u128) -> u128 {
let mut res: u128 = 0;
a %= m;
while b > 0 {
if (b & 1) == 1 {
res = (res + a) % m;
}
a = (a << 1) % m;
b >>= 1;
}
res
}
Comment on lines 6 to 19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical overflow bug in mul_mod: When modulus > u128::MAX / 2 + 1, the operations res + a (line 11) and a << 1 (line 13) can overflow before the modulo is applied, causing either:

  • Panic in debug builds
  • Silent incorrect results in release builds due to wrapping

Example: If modulus = (u128::MAX / 2) + 2, then res and a can each be up to modulus - 1, making res + a > u128::MAX.

Fix by using checked arithmetic:

fn mul_mod(mut a: u128, mut b: u128, m: u128) -> u128 {
    if m == 0 {
        panic!("modulus cannot be zero");
    }
    let mut res: u128 = 0;
    a %= m;
    while b > 0 {
        if (b & 1) == 1 {
            res = (res + a) % m;
        }
        a = (a + a) % m;  // Use addition instead of shift to avoid overflow
        b >>= 1;
    }
    res
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.


fn mod_pow(mut base: u128, mut exp: u128, m: u128) -> u128 {
let mut result: u128 = 1 % m;
base %= m;
while exp > 0 {
if (exp & 1) == 1 {
result = mul_mod(result, base, m);
}
base = mul_mod(base, base, m);
exp >>= 1;
}
result
}
Comment on lines 22 to 35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modulus validation missing: The mod_pow function doesn't validate that modulus != 0. Division by zero will occur on line 20 (1 % m) and line 21 (base %= m) if modulus is 0, causing a panic.

Add validation:

fn mod_pow(mut base: u128, mut exp: u128, m: u128) -> u128 {
    if m == 0 {
        panic!("modulus cannot be zero");
    }
    // rest of implementation
}
Suggested change
fn mod_pow(mut base: u128, mut exp: u128, m: u128) -> u128 {
let mut result: u128 = 1 % m;
base %= m;
while exp > 0 {
if (exp & 1) == 1 {
result = mul_mod(result, base, m);
}
base = mul_mod(base, base, m);
exp >>= 1;
}
result
}
fn mod_pow(mut base: u128, mut exp: u128, m: u128) -> u128 {
if m == 0 {
panic!("modulus cannot be zero");
}
let mut result: u128 = 1 % m;
base %= m;
while exp > 0 {
if (exp & 1) == 1 {
result = mul_mod(result, base, m);
}
base = mul_mod(base, base, m);
exp >>= 1;
}
result
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.


let mut out: u128 = 0;
for _ in 0..num_iters {
out = mod_pow(base, exp, modulus);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation of modexp_chain doesn't create a chain of operations - it repeatedly calculates the same modular exponentiation num_iters times without using the result of the previous calculation as input to the next one.

To create a proper chain where each result feeds into the next calculation, consider modifying the implementation to:

let mut out: u128 = base;
for _ in 0..num_iters {
    out = mod_pow(out, exp, modulus);
}

This way, each iteration will use the previous result as the base for the next modular exponentiation, creating a true chain of operations.

Suggested change
let mut out: u128 = 0;
for _ in 0..num_iters {
out = mod_pow(base, exp, modulus);
}
let mut out: u128 = base;
for _ in 0..num_iters {
out = mod_pow(out, exp, modulus);
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

out
}
31 changes: 31 additions & 0 deletions examples/modexp/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::time::Instant;
use tracing::info;

pub fn main() {
tracing_subscriber::fmt::init();

let target_dir = "/tmp/jolt-guest-targets";
let mut program = guest::compile_modexp_chain(target_dir);

let prover_preprocessing = guest::preprocess_prover_modexp_chain(&mut program);
let verifier_preprocessing =
guest::verifier_preprocessing_from_prover_modexp_chain(&prover_preprocessing);

let prove_modexp_chain = guest::build_prover_modexp_chain(program, prover_preprocessing);
let verify_modexp_chain = guest::build_verifier_modexp_chain(verifier_preprocessing);

let input_base = 123456789u128;
let input_exp = 65537u128;
let input_mod = 1000000007u128;
let iters = 100u32;
let native_output = guest::modexp_chain(input_base, input_exp, input_mod, iters);
let now = Instant::now();
let (output, proof, program_io) = prove_modexp_chain(input_base, input_exp, input_mod, iters);
info!("Prover runtime: {} s", now.elapsed().as_secs_f64());
let is_valid = verify_modexp_chain(input_base, input_exp, input_mod, iters, output, program_io.panic, proof);

assert_eq!(output, native_output, "output mismatch");
info!("output: {}", hex::encode(output.to_be_bytes()));
info!("native_output: {}", hex::encode(native_output.to_be_bytes()));
info!("valid: {is_valid}");
}
26 changes: 26 additions & 0 deletions jolt-core/benches/e2e_profiling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const CYCLES_PER_SHA256: f64 = 3396.0;
const CYCLES_PER_SHA3: f64 = 4330.0;
const CYCLES_PER_BTREEMAP_OP: f64 = 1550.0;
const CYCLES_PER_FIBONACCI_UNIT: f64 = 12.0;
const CYCLES_PER_MODEXP: f64 = 50000.0;
const SAFETY_MARGIN: f64 = 0.9; // Use 90% of max trace capacity

/// Calculate number of operations to target a specific cycle count
Expand All @@ -32,6 +33,8 @@ pub enum BenchType {
Sha2Chain,
#[strum(serialize = "SHA3 Chain")]
Sha3Chain,
#[strum(serialize = "modexp-chain")]
ModExp,
}

pub fn benchmarks(bench_type: BenchType) -> Vec<(tracing::Span, Box<dyn FnOnce()>)> {
Expand All @@ -41,6 +44,7 @@ pub fn benchmarks(bench_type: BenchType) -> Vec<(tracing::Span, Box<dyn FnOnce()
BenchType::Sha3 => sha3(),
BenchType::Sha2Chain => sha2_chain(),
BenchType::Sha3Chain => sha3_chain(),
BenchType::ModExp => modexp_chain(),
BenchType::Fibonacci => fibonacci(),
}
}
Expand Down Expand Up @@ -87,6 +91,18 @@ fn sha3_chain() -> Vec<(tracing::Span, Box<dyn FnOnce()>)> {
prove_example("sha3-chain-guest", inputs)
}

fn modexp_chain() -> Vec<(tracing::Span, Box<dyn FnOnce()>)> {
// simple u128 inputs for example
let mut inputs = vec![];
// base, exp, modulus
inputs.append(&mut postcard::to_stdvec(&123456789u128).unwrap());
inputs.append(&mut postcard::to_stdvec(&65537u128).unwrap());
inputs.append(&mut postcard::to_stdvec(&1000000007u128).unwrap());
let iters = scale_to_target_ops(((1 << 24) as f64 * SAFETY_MARGIN) as usize, CYCLES_PER_MODEXP);
inputs.append(&mut postcard::to_stdvec(&iters).unwrap());
prove_example("modexp-guest", inputs)
}

pub fn master_benchmark(
bench_type: BenchType,
bench_scale: usize,
Expand Down Expand Up @@ -129,6 +145,16 @@ pub fn master_benchmark(
]
.concat()
}),
BenchType::ModExp => ("modexp-chain", |target| {
let iterations = scale_to_target_ops(target, CYCLES_PER_MODEXP);
[
postcard::to_stdvec(&123456789u128).unwrap(),
postcard::to_stdvec(&65537u128).unwrap(),
postcard::to_stdvec(&1000000007u128).unwrap(),
postcard::to_stdvec(&iterations).unwrap(),
]
.concat()
}),
BenchType::BTreeMap => ("btreemap", |target| {
postcard::to_stdvec(&scale_to_target_ops(target, CYCLES_PER_BTREEMAP_OP)).unwrap()
}),
Expand Down
6 changes: 3 additions & 3 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[toolchain]
channel = "1.88"
targets = ["riscv32imac-unknown-none-elf", "riscv64imac-unknown-none-elf"]
channel = "nightly"
profile = "minimal"
components = ["cargo", "rustc", "clippy", "rustfmt"]
components = ["rust-src", "cargo", "rustc", "clippy", "rustfmt"]
targets = ["riscv32imac-unknown-none-elf", "riscv64imac-unknown-none-elf"]
Binary file added rustup-init.exe
Binary file not shown.