Skip to content
Merged
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
50 changes: 49 additions & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,53 @@ pub fn build(b: *std.Build) void {
}),
});
if (is_apple_silicon) linkMetalFrameworks(exe.root_module);

// Build jolt-verifier Rust staticlib once; link it into every Zig
// compile that needs `extern fn jolt_verify` (the main exe and the
// exe unit tests, which share src/main.zig as their root).
const cargo_build = b.addSystemCommand(&.{
"cargo",
"build",
"--profile",
"release-staticlib",
"--manifest-path",
});
cargo_build.addFileArg(b.path("jolt-verifier/Cargo.toml"));

const linkJoltVerifier = struct {
fn call(
c: *std.Build.Step.Compile,
b_: *std.Build,
tgt: std.Build.ResolvedTarget,
apple_silicon: bool,
cargo_step: *std.Build.Step,
) void {
c.addLibraryPath(.{ .cwd_relative = b_.pathFromRoot("jolt-verifier/target/release-staticlib") });
c.root_module.linkSystemLibrary("jolt_verifier", .{ .preferred_link_mode = .static });
c.root_module.linkSystemLibrary("c", .{});
c.root_module.linkSystemLibrary("m", .{});
if (tgt.result.os.tag == .linux) {
c.root_module.linkSystemLibrary("pthread", .{});
c.root_module.linkSystemLibrary("dl", .{});
c.root_module.linkSystemLibrary("rt", .{});
// Rust's std pulls in panic-unwind symbols (_Unwind_*) that
// live in libgcc_s on GNU/Linux; without this, linking a
// Rust staticlib on Linux fails with undefined references.
c.root_module.linkSystemLibrary("gcc_s", .{});
} else if (tgt.result.os.tag != .macos) {
c.root_module.linkSystemLibrary("pthread", .{});
}
if (apple_silicon or tgt.result.os.tag == .macos) {
const fw_opts: std.Build.Module.LinkFrameworkOptions = .{};
c.root_module.linkFramework("Security", fw_opts);
c.root_module.linkFramework("CoreFoundation", fw_opts);
}
c.step.dependOn(cargo_step);
}
}.call;

linkJoltVerifier(exe, b, target, is_apple_silicon, &cargo_build.step);

b.installArtifact(exe);

// Run command
Expand All @@ -196,7 +243,7 @@ pub fn build(b: *std.Build) void {
if (is_apple_silicon) linkMetalFrameworks(lib_unit_tests.root_module);
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);

// Unit tests for the executable
// Unit tests for the executable (needs jolt-verifier staticlib for verify command)
const exe_unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
Expand All @@ -209,6 +256,7 @@ pub fn build(b: *std.Build) void {
}),
});
if (is_apple_silicon) linkMetalFrameworks(exe_unit_tests.root_module);
linkJoltVerifier(exe_unit_tests, b, target, is_apple_silicon, &cargo_build.step);
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);

// Test step
Expand Down
10 changes: 10 additions & 0 deletions jolt-verifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name = "jolt-verifier"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["staticlib", "lib"]

[dependencies]
# Pinned to a16z/jolt commit b10e80ac
jolt-core = { git = "https://github.com/a16z/jolt.git", rev = "b10e80ac", features = ["host"], default-features = false }
Expand All @@ -25,3 +28,10 @@ allocative = { git = "https://github.com/facebookexperimental/allocative", rev =
[profile.release]
opt-level = 3
lto = "fat"

# Profile for building the staticlib linked by Zig.
# LTO must be off so the .a contains native object code (not LLVM bitcode)
# that a non-Rust linker can consume.
[profile.release-staticlib]
inherits = "release"
lto = "off"
73 changes: 73 additions & 0 deletions jolt-verifier/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use ark_serialize::CanonicalDeserialize;
use jolt_core::curve::Bn254Curve;
use jolt_core::poly::commitment::dory::DoryCommitmentScheme;
use jolt_core::zkvm::verifier::JoltVerifierPreprocessing;
use jolt_core::zkvm::{RV64IMACProof, RV64IMACVerifier, Serializable};
use std::io::Cursor;

type PreprocessingType = JoltVerifierPreprocessing<ark_bn254::Fr, Bn254Curve, DoryCommitmentScheme>;

/// Verify a Jolt proof via C FFI.
///
/// Returns:
/// 0 — proof is valid
/// 1 — proof is invalid (verification failed)
/// 2 — deserialization or other error
///
/// `proof_ptr` / `proof_len` — serialized RV64IMACProof bytes
/// `preprocessing_ptr` / `preprocessing_len` — serialized JoltVerifierPreprocessing bytes
/// `io_ptr` / `io_len` — serialized JoltDevice bytes (program I/O sidecar);
/// pass null / 0 for empty I/O
#[no_mangle]
pub extern "C" fn jolt_verify(
proof_ptr: *const u8,
proof_len: usize,
preprocessing_ptr: *const u8,
preprocessing_len: usize,
io_ptr: *const u8,
io_len: usize,
) -> i32 {
// Safety: caller guarantees valid pointers and lengths
let proof_bytes = unsafe { std::slice::from_raw_parts(proof_ptr, proof_len) };
let preprocessing_bytes =
unsafe { std::slice::from_raw_parts(preprocessing_ptr, preprocessing_len) };

// Deserialize preprocessing
let mut pp_cursor = Cursor::new(preprocessing_bytes);
let preprocessing = match PreprocessingType::deserialize_compressed(&mut pp_cursor) {
Ok(pp) => pp,
Err(_) => return 2,
};

// Deserialize proof
let proof = match RV64IMACProof::deserialize_from_bytes(proof_bytes) {
Ok(p) => p,
Err(_) => return 2,
};

// Deserialize program I/O (or fall back to empty JoltDevice)
let program_io = if !io_ptr.is_null() && io_len > 0 {
let io_bytes = unsafe { std::slice::from_raw_parts(io_ptr, io_len) };
let mut io_cursor = Cursor::new(io_bytes);
match common::jolt_device::JoltDevice::deserialize_compressed(&mut io_cursor) {
Ok(dev) => dev,
Err(_) => return 2,
}
} else {
common::jolt_device::JoltDevice {
memory_layout: preprocessing.shared.memory_layout.clone(),
..Default::default()
}
};

// Build verifier and run verification
let verifier = match RV64IMACVerifier::new(&preprocessing, proof, program_io, None, None) {
Ok(v) => v,
Err(_) => return 2,
};

match verifier.verify() {
Ok(()) => 0,
Err(_) => 1,
}
}
62 changes: 62 additions & 0 deletions src/cli/args.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub const Command = enum {
version,
run,
prove,
verify,
unknown,
};

Expand All @@ -35,6 +36,13 @@ pub const ProveArgs = struct {
had_first_arg: bool = false,
};

pub const VerifyArgs = struct {
proof_path: ?[]const u8 = null,
preprocessing_path: ?[]const u8 = null,
show_help: bool = false,
had_first_arg: bool = false,
};

pub fn printHelp() void {
std.debug.print(
\\Zolt zkVM v{s}
Expand All @@ -47,10 +55,12 @@ pub fn printHelp() void {
\\ help Show this help message
\\ version Show version information
\\ prove [opts] <elf> Generate ZK proof for ELF binary
\\ verify [opts] Verify a proof against preprocessing
\\ run [opts] <elf> Run RISC-V ELF binary in the emulator
\\
\\EXAMPLES:
\\ zolt prove -o proof.bin program.elf # Generate and save a proof
\\ zolt verify --proof proof.bin --preprocessing pp.bin # Verify a proof
\\ zolt run program.elf # Execute a RISC-V binary
\\ zolt run --trace program.elf # Show execution trace
\\
Expand All @@ -73,6 +83,8 @@ pub fn parseCommand(arg: []const u8) Command {
return .run;
} else if (std.mem.eql(u8, arg, "prove")) {
return .prove;
} else if (std.mem.eql(u8, arg, "verify")) {
return .verify;
}
return .unknown;
}
Expand Down Expand Up @@ -216,6 +228,55 @@ pub fn printProveHelp() void {
std.debug.print(" --input-hex HEX Set input as hex bytes (e.g., 20 for input 32)\n", .{});
}

/// Parse verify sub-command arguments from an argument iterator.
/// The first non-flag argument after "verify" should be passed as `first_arg`.
pub fn parseVerifyArgs(args: anytype, first_arg: []const u8) VerifyArgs {
var result = VerifyArgs{};

if (std.mem.eql(u8, first_arg, "--help") or std.mem.eql(u8, first_arg, "-h")) {
result.show_help = true;
return result;
}

result.had_first_arg = true;

// Process first arg
if (std.mem.startsWith(u8, first_arg, "-")) {
processVerifyFlag(&result, first_arg, args);
} else {
// Positional: treat as proof path
result.proof_path = first_arg;
}

// Process remaining args
while (args.next()) |next_arg| {
if (std.mem.startsWith(u8, next_arg, "-")) {
processVerifyFlag(&result, next_arg, args);
} else if (result.proof_path == null) {
result.proof_path = next_arg;
}
}

return result;
}

fn processVerifyFlag(result: *VerifyArgs, flag: []const u8, args: anytype) void {
if (std.mem.eql(u8, flag, "--proof") or std.mem.eql(u8, flag, "-p")) {
result.proof_path = args.next();
} else if (std.mem.eql(u8, flag, "--preprocessing") or std.mem.eql(u8, flag, "-P")) {
result.preprocessing_path = args.next();
}
}

pub fn printVerifyHelp() void {
std.debug.print("Usage: zolt verify [options] --proof <file> --preprocessing <file>\n\n", .{});
std.debug.print("Verify a ZK proof against preprocessing data.\n", .{});
std.debug.print("Calls the Jolt RV64IMAC verifier directly via linked Rust library.\n\n", .{});
std.debug.print("Options:\n", .{});
std.debug.print(" -p, --proof F Path to proof file (required)\n", .{});
std.debug.print(" -P, --preprocessing F Path to preprocessing file (required)\n", .{});
}

test "command parsing" {
try std.testing.expect(parseCommand("help") == .help);
try std.testing.expect(parseCommand("-h") == .help);
Expand All @@ -224,6 +285,7 @@ test "command parsing" {
try std.testing.expect(parseCommand("-v") == .version);
try std.testing.expect(parseCommand("run") == .run);
try std.testing.expect(parseCommand("prove") == .prove);
try std.testing.expect(parseCommand("verify") == .verify);
try std.testing.expect(parseCommand("unknown_cmd") == .unknown);
}

Expand Down
116 changes: 116 additions & 0 deletions src/commands/verify.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//! Verify command: Verify a ZK proof using Jolt's RV64IMAC verifier via linked Rust library.

const std = @import("std");

extern fn jolt_verify(
proof_ptr: [*]const u8,
proof_len: usize,
preprocessing_ptr: [*]const u8,
preprocessing_len: usize,
io_ptr: ?[*]const u8,
io_len: usize,
) callconv(.c) i32;

pub fn runVerifier(allocator: std.mem.Allocator, proof_path: []const u8, preprocessing_path: []const u8) !void {
std.debug.print("Zolt zkVM Verifier\n", .{});
std.debug.print("==================\n\n", .{});

// Load preprocessing
std.debug.print("Loading preprocessing: {s}\n", .{preprocessing_path});
const preprocessing_bytes = blk: {
const f = std.fs.cwd().openFile(preprocessing_path, .{}) catch |err| {
std.debug.print("Error: failed to open preprocessing file: {s}\n", .{@errorName(err)});
return err;
};
defer f.close();
const stat = try f.stat();
const buf = try allocator.alloc(u8, stat.size);
const n = try f.readAll(buf);
if (n != stat.size) {
std.debug.print("Error: short read on preprocessing file\n", .{});
allocator.free(buf);
return error.ShortRead;
}
break :blk buf;
};
defer allocator.free(preprocessing_bytes);
std.debug.print(" Preprocessing loaded: {} bytes\n", .{preprocessing_bytes.len});

// Load proof
std.debug.print("Loading proof: {s}\n", .{proof_path});
const proof_bytes = blk: {
const f = std.fs.cwd().openFile(proof_path, .{}) catch |err| {
std.debug.print("Error: failed to open proof file: {s}\n", .{@errorName(err)});
return err;
};
defer f.close();
const stat = try f.stat();
const buf = try allocator.alloc(u8, stat.size);
const n = try f.readAll(buf);
if (n != stat.size) {
std.debug.print("Error: short read on proof file\n", .{});
allocator.free(buf);
return error.ShortRead;
}
break :blk buf;
};
defer allocator.free(proof_bytes);
std.debug.print(" Proof loaded: {} bytes\n", .{proof_bytes.len});

// Try to load program I/O sidecar (<proof_path>.io)
const io_path = try std.fmt.allocPrint(allocator, "{s}.io", .{proof_path});
defer allocator.free(io_path);

var io_bytes: ?[]u8 = null;
defer if (io_bytes) |b| allocator.free(b);

if (std.fs.cwd().openFile(io_path, .{})) |f| {
defer f.close();
const stat = try f.stat();
const buf = try allocator.alloc(u8, stat.size);
const n = try f.readAll(buf);
if (n == stat.size) {
io_bytes = buf;
std.debug.print(" IO sidecar loaded: {s} ({} bytes)\n", .{ io_path, buf.len });
} else {
allocator.free(buf);
}
} else |_| {
std.debug.print(" No IO sidecar at {s}; assuming empty I/O\n", .{io_path});
}

// Call the Rust verifier
std.debug.print("\nVerifying...\n", .{});
var timer = try std.time.Timer.start();

const io_ptr: ?[*]const u8 = if (io_bytes) |b| b.ptr else null;
const io_len: usize = if (io_bytes) |b| b.len else 0;

const result = jolt_verify(
proof_bytes.ptr,
proof_bytes.len,
preprocessing_bytes.ptr,
preprocessing_bytes.len,
io_ptr,
io_len,
);

const elapsed = timer.read();
const elapsed_ms = @as(f64, @floatFromInt(elapsed)) / 1_000_000.0;

switch (result) {
0 => {
std.debug.print("VERIFIED: proof is valid\n", .{});
std.debug.print("Time: {d:.2} ms\n", .{elapsed_ms});
},
1 => {
std.debug.print("FAILED: proof is invalid\n", .{});
std.debug.print("Time: {d:.2} ms\n", .{elapsed_ms});
std.process.exit(1);
},
else => {
std.debug.print("ERROR: deserialization or internal error (code {})\n", .{result});
std.process.exit(2);
},
}
}
Loading
Loading