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
43 changes: 32 additions & 11 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

// Opt-in build of the Rust Jolt verifier staticlib. Off by default so most
// CI jobs (and quick `zig build` / `zig build test` runs) skip the ~2 min
// cargo compile. Enable with `zig build -Dverify=true` to get the
// `zolt verify` command and its extern link to libjolt_verifier.a.
const enable_verifier = b.option(
bool,
"verify",
"Build and link the Rust Jolt verifier (enables `zolt verify`)",
) orelse false;

const build_options = b.addOptions();
build_options.addOption(bool, "enable_verifier", enable_verifier);
const build_options_mod = build_options.createModule();

const is_apple_silicon = target.result.os.tag == .macos and
target.result.cpu.arch == .aarch64;

Expand Down Expand Up @@ -166,6 +180,7 @@ pub fn build(b: *std.Build) void {
.imports = &.{
.{ .name = "zolt_pool", .module = zolt_pool_mod },
.{ .name = "zolt_arith", .module = zolt_arith_mod },
.{ .name = "build_options", .module = build_options_mod },
},
}),
});
Expand All @@ -174,14 +189,19 @@ pub fn build(b: *std.Build) void {
// 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"));
// Only created when `-Dverify=true`; otherwise the verify command
// compiles to a stub and nothing needs libjolt_verifier.a.
const cargo_build: ?*std.Build.Step.Run = if (enable_verifier) blk: {
const cmd = b.addSystemCommand(&.{
"cargo",
"build",
"--profile",
"release-staticlib",
"--manifest-path",
});
cmd.addFileArg(b.path("jolt-verifier/Cargo.toml"));
break :blk cmd;
} else null;

const linkJoltVerifier = struct {
fn call(
Expand Down Expand Up @@ -215,7 +235,7 @@ pub fn build(b: *std.Build) void {
}
}.call;

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

b.installArtifact(exe);

Expand Down Expand Up @@ -243,7 +263,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 (needs jolt-verifier staticlib for verify command)
// Unit tests for the executable (links jolt-verifier only when -Dverify=true)
const exe_unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
Expand All @@ -252,11 +272,12 @@ pub fn build(b: *std.Build) void {
.imports = &.{
.{ .name = "zolt_pool", .module = zolt_pool_mod },
.{ .name = "zolt_arith", .module = zolt_arith_mod },
.{ .name = "build_options", .module = build_options_mod },
},
}),
});
if (is_apple_silicon) linkMetalFrameworks(exe_unit_tests.root_module);
linkJoltVerifier(exe_unit_tests, b, target, is_apple_silicon, &cargo_build.step);
if (cargo_build) |cb| linkJoltVerifier(exe_unit_tests, b, target, is_apple_silicon, &cb.step);
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);

// Test step
Expand Down
211 changes: 113 additions & 98 deletions src/commands/verify.zig
Original file line number Diff line number Diff line change
@@ -1,116 +1,131 @@
//! Verify command: Verify a ZK proof using Jolt's RV64IMAC verifier via linked Rust library.
//! Verify command: Verify a ZK proof using Jolt's RV64IMAC verifier via linked
//! Rust library. When built without `-Dverify=true`, this compiles to a stub
//! that prints a helpful message and exits — the `extern fn jolt_verify`
//! declaration is gated behind a comptime `if` so it only exists (and only
//! requires libjolt_verifier.a) when the flag is set.

const std = @import("std");
const build_options = @import("build_options");

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;
const impl = if (build_options.enable_verifier) struct {
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", .{});
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;
// 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 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});
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;
// 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 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});
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);
// 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);
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);
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});
}
} 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();
// 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 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 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;
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);
},
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);
},
}
}
} else struct {
pub fn runVerifier(_: std.mem.Allocator, _: []const u8, _: []const u8) !void {
std.debug.print("Error: this build of zolt does not include the Jolt verifier.\n", .{});
std.debug.print("Rebuild with `zig build -Dverify=true` to enable the verify command.\n", .{});
return error.VerifierNotEnabled;
}
}
};

pub const runVerifier = impl.runVerifier;
Loading