diff --git a/Cargo.lock b/Cargo.lock index 61e711e8b8..24d6a8a584 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2272,6 +2272,29 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "modexp-chain" +version = "0.1.0" +dependencies = [ + "hex", + "jolt-sdk", + "modexp-chain-guest", + "num-bigint", + "num-traits", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "modexp-chain-guest" +version = "0.1.0" +dependencies = [ + "jolt-sdk", + "num-bigint", + "num-traits", + "serde", +] + [[package]] name = "muldiv" version = "0.1.0" @@ -2390,6 +2413,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index aa7db7b13c..ada4141d86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,8 @@ members = [ "examples/merkle-tree/guest", "examples/hash-bench", "examples/hash-bench/guest", + "examples/modexp-chain", + "examples/modexp-chain/guest", ] [features] diff --git a/examples/modexp-chain/Cargo.toml b/examples/modexp-chain/Cargo.toml new file mode 100644 index 0000000000..62c43127d6 --- /dev/null +++ b/examples/modexp-chain/Cargo.toml @@ -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" diff --git a/examples/modexp-chain/README.md b/examples/modexp-chain/README.md new file mode 100644 index 0000000000..81c42bfd99 --- /dev/null +++ b/examples/modexp-chain/README.md @@ -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. diff --git a/examples/modexp-chain/guest/Cargo.toml b/examples/modexp-chain/guest/Cargo.toml new file mode 100644 index 0000000000..b4a613b7fd --- /dev/null +++ b/examples/modexp-chain/guest/Cargo.toml @@ -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"] } diff --git a/examples/modexp-chain/guest/src/lib.rs b/examples/modexp-chain/guest/src/lib.rs new file mode 100644 index 0000000000..ea6f309755 --- /dev/null +++ b/examples/modexp-chain/guest/src/lib.rs @@ -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 +} diff --git a/examples/modexp-chain/guest/src/main.rs b/examples/modexp-chain/guest/src/main.rs new file mode 100644 index 0000000000..da28fab799 --- /dev/null +++ b/examples/modexp-chain/guest/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(feature = "guest", no_std)] +#![no_main] + +#[allow(unused_imports)] +use modexp_chain_guest::*; diff --git a/examples/modexp-chain/src/main.rs b/examples/modexp-chain/src/main.rs new file mode 100644 index 0000000000..d607ab9c24 --- /dev/null +++ b/examples/modexp-chain/src/main.rs @@ -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}"); +} diff --git a/jolt-core/benches/e2e_profiling.rs b/jolt-core/benches/e2e_profiling.rs index 7cece2d806..20ff31a3cd 100644 --- a/jolt-core/benches/e2e_profiling.rs +++ b/jolt-core/benches/e2e_profiling.rs @@ -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 @@ -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)> { @@ -40,6 +43,7 @@ pub fn benchmarks(bench_type: BenchType) -> Vec<(tracing::Span, Box sha2_chain(), BenchType::Sha3Chain => sha3_chain(), BenchType::Fibonacci => fibonacci(), + BenchType::ModexpChain => modexp_chain(), } } @@ -85,6 +89,20 @@ fn sha3_chain() -> Vec<(tracing::Span, Box)> { prove_example("sha3-chain-guest", inputs) } +fn modexp_chain() -> Vec<(tracing::Span, Box)> { + 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, @@ -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"), };