Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions Cargo.lock

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

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

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

[dependencies]
jolt-sdk = { path = "../../jolt-sdk", features = ["host"] }
tracing-subscriber = "0.3"
tracing = "0.1"
guest = { package = "modexp-chain-guest", path = "./guest" }
num-bigint = { version = "0.4", default-features = false, features = ["std"] }
num-traits = "0.2"
hex = "0.4.3"
49 changes: 49 additions & 0 deletions examples/modexp-chain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Modexp Chain Example

This example demonstrates modular exponentiation (modexp) operations in Jolt, similar to the EVM's MODEXP precompile.

## Configuration

The example is configurable in several ways:

### Bit Length (Compile-time)

The bit length of the base, exponent, and modulus can be configured by changing the `BITLEN_BYTES` constant in `guest/src/lib.rs`:

```rust
// For 256-bit values (default)
const BITLEN_BYTES: usize = 32;

// For 512-bit values
const BITLEN_BYTES: usize = 64;

// For 1024-bit values
const BITLEN_BYTES: usize = 128;
```

### Number of Iterations (Runtime)

The number of iterations can be configured at runtime by passing the `iters` parameter to the `modexp_chain` function in `src/main.rs`:

```rust
let iters = 10; // Number of times to perform modexp
```

## Running the Example

```bash
cargo run --release -p modexp-chain
```

## Benchmarking

The example is also integrated into the e2e_profiling benchmark suite:

```bash
# Run the modexp-chain benchmark
cargo bench --bench e2e_profiling -- --bench-type modexp-chain
```

## Implementation

The modexp operation is implemented using the `num-bigint` crate's `modpow` method, which performs efficient modular exponentiation using the square-and-multiply algorithm.
13 changes: 13 additions & 0 deletions examples/modexp-chain/guest/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "modexp-chain-guest"
version = "0.1.0"
edition = "2021"

[features]
guest = []

[dependencies]
jolt = { package = "jolt-sdk", path = "../../../jolt-sdk", features = [] }
num-bigint = { version = "0.4", default-features = false, features = ["serde"] }
num-traits = { version = "0.2", default-features = false }
serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] }
40 changes: 40 additions & 0 deletions examples/modexp-chain/guest/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#![cfg_attr(feature = "guest", no_std)]

extern crate alloc;
use num_bigint::BigUint;
use num_traits::Zero;

// Configurable bit lengths for base, exponent, and modulus
// This can be changed at compile time to support different bit lengths:
// - 32 bytes = 256 bits (default, similar to EVM MODEXP)
// - 64 bytes = 512 bits
// - 128 bytes = 1024 bits
const BITLEN_BYTES: usize = 32;

#[jolt::provable(memory_size = 10240, max_trace_length = 4194304)]
fn modexp_chain(
base: [u8; BITLEN_BYTES],
exponent: [u8; BITLEN_BYTES],
modulus: [u8; BITLEN_BYTES],
num_iters: u32, // Configurable number of iterations
) -> [u8; BITLEN_BYTES] {
let mut result = BigUint::from_bytes_be(&base);
let exp = BigUint::from_bytes_be(&exponent);
let modulus_uint = BigUint::from_bytes_be(&modulus);

// Validate modulus is not zero to prevent division by zero
assert!(!modulus_uint.is_zero(), "Modulus cannot be zero");

// Perform modexp num_iters times, chaining the result
for _ in 0..num_iters {
result = result.modpow(&exp, &modulus_uint);
}

// Convert result back to fixed-size array, padding with zeros on the left
let result_bytes = result.to_bytes_be();
let mut output = [0u8; BITLEN_BYTES];
let len = result_bytes.len().min(BITLEN_BYTES);
let start_idx = BITLEN_BYTES - len;
output[start_idx..].copy_from_slice(&result_bytes[(result_bytes.len() - len)..]);
output
}
5 changes: 5 additions & 0 deletions examples/modexp-chain/guest/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#![cfg_attr(feature = "guest", no_std)]
#![no_main]

#[allow(unused_imports)]
use modexp_chain_guest::*;
42 changes: 42 additions & 0 deletions examples/modexp-chain/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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);

// Configurable inputs: 256-bit base, exponent, and modulus (default)
// These values can be changed to test different modexp scenarios
let base = [5u8; 32]; // 256-bit base
let exponent = [3u8; 32]; // 256-bit exponent
let modulus = [7u8; 32]; // 256-bit modulus
let iters = 10; // Configurable number of iterations

let native_output = guest::modexp_chain(base, exponent, modulus, iters);
let now = Instant::now();
let (output, proof, program_io) = prove_modexp_chain(base, exponent, modulus, iters);
info!("Prover runtime: {} s", now.elapsed().as_secs_f64());
let is_valid = verify_modexp_chain(
base,
exponent,
modulus,
iters,
output,
program_io.panic,
proof,
);

assert_eq!(output, native_output, "output mismatch");
info!("output: {}", hex::encode(output));
info!("native_output: {}", hex::encode(native_output));
info!("valid: {is_valid}");
}
28 changes: 28 additions & 0 deletions jolt-core/benches/e2e_profiling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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; // Estimated cycles per modexp operation
const SAFETY_MARGIN: f64 = 0.9; // Use 90% of max trace capacity

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

pub fn benchmarks(bench_type: BenchType) -> Vec<(tracing::Span, Box<dyn FnOnce()>)> {
Expand All @@ -40,6 +43,7 @@ pub fn benchmarks(bench_type: BenchType) -> Vec<(tracing::Span, Box<dyn FnOnce()
BenchType::Sha2Chain => sha2_chain(),
BenchType::Sha3Chain => sha3_chain(),
BenchType::Fibonacci => fibonacci(),
BenchType::ModexpChain => modexp_chain(),
}
}

Expand Down Expand Up @@ -85,6 +89,20 @@ fn sha3_chain() -> Vec<(tracing::Span, Box<dyn FnOnce()>)> {
prove_example("sha3-chain-guest", inputs)
}

fn modexp_chain() -> Vec<(tracing::Span, Box<dyn FnOnce()>)> {
let mut inputs = vec![];
// Base, exponent, and modulus (256 bits = 32 bytes each)
inputs.append(&mut postcard::to_stdvec(&[5u8; 32]).unwrap());
inputs.append(&mut postcard::to_stdvec(&[3u8; 32]).unwrap());
inputs.append(&mut postcard::to_stdvec(&[7u8; 32]).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-chain-guest", inputs)
}

pub fn master_benchmark(
bench_type: BenchType,
bench_scale: usize,
Expand Down Expand Up @@ -130,6 +148,16 @@ pub fn master_benchmark(
BenchType::BTreeMap => ("btreemap", |target| {
postcard::to_stdvec(&scale_to_target_ops(target, CYCLES_PER_BTREEMAP_OP)).unwrap()
}),
BenchType::ModexpChain => ("modexp-chain", |target| {
let iterations = scale_to_target_ops(target, CYCLES_PER_MODEXP);
[
postcard::to_stdvec(&[5u8; 32]).unwrap(),
postcard::to_stdvec(&[3u8; 32]).unwrap(),
postcard::to_stdvec(&[7u8; 32]).unwrap(),
postcard::to_stdvec(&iterations).unwrap(),
]
.concat()
}),
BenchType::Sha2 => panic!("Use sha2-chain instead"),
BenchType::Sha3 => panic!("Use sha3-chain instead"),
};
Expand Down