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
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,59 @@ jobs:
- name: Verify with Jolt verifier
run: ./jolt-verifier/target/release/jolt-verifier --proof proof.bin --preprocessing preprocessing.bin

# ─── zolt-arith: differential fixture tests ────────────────────
zolt-arith-diff:
name: Differential Fixture Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: ${{ env.ZIG_VERSION }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: tools/zolt-arith-diff/arkworks-fixtures
- name: Run differential tests
run: zig build test-zolt-arith-diff -Doptimize=ReleaseSafe

# ─── zolt-arith: fixture freshness check ──────────────────────
zolt-arith-fixture-freshness:
name: Fixture Freshness Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: tools/zolt-arith-diff/arkworks-fixtures
- name: Regenerate fixtures
run: cargo run --release --manifest-path tools/zolt-arith-diff/arkworks-fixtures/Cargo.toml -- --out-dir testdata/zolt-arith-diff
- name: Check for drift
run: git diff --exit-code testdata/zolt-arith-diff/

# ─── zolt-arith: benchmark reporting (non-blocking) ───────────
zolt-arith-bench:
name: Arithmetic Benchmarks
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Verify ARM64
run: test "$(uname -m)" = "arm64"
- uses: mlugg/setup-zig@v2
with:
version: ${{ env.ZIG_VERSION }}
- name: Field microbench
run: zig build bench-zolt-arith-field 2>&1 | tee field_bench.txt
- name: Pairing microbench
run: zig build bench-zolt-arith-pairing 2>&1 | tee pairing_bench.txt
- name: Post benchmark summary
run: |
echo "## Benchmark Results" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
grep '\[BENCH\]' field_bench.txt pairing_bench.txt >> $GITHUB_STEP_SUMMARY || true
echo '```' >> $GITHUB_STEP_SUMMARY

# ─── Gate: all required checks must pass ─────────────────────────
ci-pass:
name: CI Pass
Expand All @@ -240,6 +293,8 @@ jobs:
- cli-smoke
- prove-verify-linux
- prove-verify-macos
- zolt-arith-diff
- zolt-arith-fixture-freshness
runs-on: ubuntu-latest
steps:
- name: Check all jobs
Expand All @@ -255,6 +310,8 @@ jobs:
"${{ needs.cli-smoke.result }}"
"${{ needs.prove-verify-linux.result }}"
"${{ needs.prove-verify-macos.result }}"
"${{ needs.zolt-arith-diff.result }}"
"${{ needs.zolt-arith-fixture-freshness.result }}"
)
for r in "${results[@]}"; do
if [[ "$r" != "success" ]]; then
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ node_modules/
jolt/
bench/results/
jolt-bench/target/
tools/zolt-arith-diff/arkworks-fixtures/target/
flamegraph.svg
139 changes: 139 additions & 0 deletions bench/zolt_arith/bench_harness.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! Shared microbenchmark harness for zolt-arith.
//! Zero external dependencies — only std.
//!
//! Usage:
//! const harness = @import("bench_harness.zig");
//! _ = harness.run("field_Fp", "mul", harness.Config.field, Fp, struct {
//! fn call(i: usize) Fp { return a[i % N].mul(b[i % N]); }
//! }.call);

const std = @import("std");

const MAX_SAMPLES: usize = 101;

pub const Config = struct {
warmup_iters: usize = 5_000,
sample_count: usize = 21,
iters_per_sample: usize = 100_000,

pub const field: Config = .{
.warmup_iters = 20_000,
.sample_count = 21,
.iters_per_sample = 100_000,
};

pub const pairing: Config = .{
.warmup_iters = 10,
.sample_count = 21,
.iters_per_sample = 100,
};
};

pub const Stats = struct {
min_ns: f64,
median_ns: f64,
mean_ns: f64,
p99_ns: f64,
stddev_ns: f64,
sample_count: usize,
iters_per_sample: usize,
};

pub fn run(
comptime group: []const u8,
comptime op_name: []const u8,
config: Config,
comptime T: type,
comptime body: fn (usize) T,
) Stats {
const n = @min(config.sample_count, MAX_SAMPLES);
const iters = config.iters_per_sample;

// --- Warmup ---
var sink: T = undefined;
for (0..config.warmup_iters) |i| {
sink = body(i);
}
std.mem.doNotOptimizeAway(&sink);

// --- Collect samples ---
var samples: [MAX_SAMPLES]f64 = undefined;
for (0..n) |s| {
var timer = std.time.Timer.start() catch unreachable;
for (0..iters) |i| {
sink = body(i);
}
std.mem.doNotOptimizeAway(&sink);
const elapsed_ns: u64 = timer.read();
samples[s] = @as(f64, @floatFromInt(elapsed_ns)) /
@as(f64, @floatFromInt(iters));
}

// --- Compute stats ---
const slice = samples[0..n];
std.sort.pdq(f64, slice, {}, lessThanF64);

var sum: f64 = 0;
for (slice) |v| sum += v;
const mean = sum / @as(f64, @floatFromInt(n));

var var_sum: f64 = 0;
for (slice) |v| {
const d = v - mean;
var_sum += d * d;
}
const stddev = @sqrt(var_sum / @as(f64, @floatFromInt(n)));

const median = if (n % 2 == 1)
slice[n / 2]
else
(slice[n / 2 - 1] + slice[n / 2]) / 2.0;

const p99_idx = @as(usize, @intFromFloat(@ceil(0.99 * @as(f64, @floatFromInt(n))))) - 1;

const stats = Stats{
.min_ns = slice[0],
.median_ns = median,
.mean_ns = mean,
.p99_ns = slice[p99_idx],
.stddev_ns = stddev,
.sample_count = n,
.iters_per_sample = iters,
};

printResult(group, op_name, stats);
return stats;
}

fn lessThanF64(_: void, a: f64, b: f64) bool {
return a < b;
}

const FmtTime = struct { val: f64, unit: []const u8 };

fn formatTime(ns: f64) FmtTime {
if (ns >= 1_000_000.0) return .{ .val = ns / 1_000_000.0, .unit = "ms" };
if (ns >= 1_000.0) return .{ .val = ns / 1_000.0, .unit = "us" };
return .{ .val = ns, .unit = "ns" };
}

fn printResult(comptime group: []const u8, comptime op_name: []const u8, s: Stats) void {
const min_f = formatTime(s.min_ns);
const med_f = formatTime(s.median_ns);
const mean_f = formatTime(s.mean_ns);
const p99_f = formatTime(s.p99_ns);
const sd_f = formatTime(s.stddev_ns);

std.debug.print(
"[BENCH] group={s} op={s} min={d:.1}{s} median={d:.1}{s} mean={d:.1}{s} p99={d:.1}{s} stddev={d:.1}{s} samples={d}x{d}\n",
.{
group, op_name,
min_f.val, min_f.unit,
med_f.val, med_f.unit,
mean_f.val, mean_f.unit,
p99_f.val, p99_f.unit,
sd_f.val, sd_f.unit,
s.sample_count, s.iters_per_sample,
},
);
}
66 changes: 66 additions & 0 deletions bench/zolt_arith/field_micro.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const std = @import("std");
const zolt = @import("zolt");
const harness = @import("bench_harness.zig");

const Fr = zolt.field.BN254Scalar;
const Fp = zolt.field.BN254BaseField;

const ELEMENTS: usize = 128;

fn fillFieldInputs(comptime F: type, a: *[ELEMENTS]F, b: *[ELEMENTS]F) void {
for (0..ELEMENTS) |i| {
a[i] = F.fromU64(@as(u64, @intCast(i + 1)) *% 0x9E3779B185EBCA87 +% 17);
b[i] = F.fromU64(@as(u64, @intCast(i + 1)) *% 0xD6E8FEB86659FD93 +% 29);
}
}

fn benchField(comptime F: type, comptime field_name: []const u8) void {
const Ctx = struct {
var a: [ELEMENTS]F = undefined;
var b: [ELEMENTS]F = undefined;

fn add(i: usize) F {
return a[i % ELEMENTS].add(b[i % ELEMENTS]);
}
fn sub(i: usize) F {
return a[i % ELEMENTS].sub(b[i % ELEMENTS]);
}
fn mul(i: usize) F {
return a[i % ELEMENTS].mul(b[i % ELEMENTS]);
}
fn square(i: usize) F {
return a[i % ELEMENTS].square();
}
fn inverse(i: usize) F {
return a[i % ELEMENTS].inverse() orelse F.zero();
}
fn toMontgomery(i: usize) F {
return a[i % ELEMENTS].fromMontgomery().toMontgomery();
}
fn fromMontgomery(i: usize) F {
return a[i % ELEMENTS].fromMontgomery();
}
fn sumOfProducts(i: usize) F {
const idx = i % ELEMENTS;
return F.sumOfProducts(.{ a[idx], b[idx] }, .{ b[idx], a[idx] });
}
};

fillFieldInputs(F, &Ctx.a, &Ctx.b);

const cfg = harness.Config.field;
_ = harness.run(field_name, "add", cfg, F, Ctx.add);
_ = harness.run(field_name, "sub", cfg, F, Ctx.sub);
_ = harness.run(field_name, "mul", cfg, F, Ctx.mul);
_ = harness.run(field_name, "square", cfg, F, Ctx.square);
_ = harness.run(field_name, "inverse", cfg, F, Ctx.inverse);
_ = harness.run(field_name, "toMontgomery", cfg, F, Ctx.toMontgomery);
_ = harness.run(field_name, "fromMontgomery", cfg, F, Ctx.fromMontgomery);
_ = harness.run(field_name, "sumOfProducts", cfg, F, Ctx.sumOfProducts);
}

pub fn main() !void {
std.debug.print("=== Zolt-Arith Field Microbench ===\n", .{});
benchField(Fp, "field_Fp");
benchField(Fr, "field_Fr");
}
75 changes: 75 additions & 0 deletions bench/zolt_arith/pairing_micro.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const std = @import("std");
const zolt = @import("zolt");
const harness = @import("bench_harness.zig");

const field = zolt.field;
const p = field.pairing;

const Fp = field.BN254BaseField;
const Fp12 = p.Fp12;
const G1PointFp = p.G1PointFp;
const G2Point = p.G2Point;
const G2Projective = p.G2Projective;
const G2Prepared = p.G2Prepared;

const NUM_PAIRS: usize = 4;

// File-scope inputs populated by setupInputs().
var g1_points: [NUM_PAIRS]G1PointFp = undefined;
var g2_points: [NUM_PAIRS]G2Point = undefined;
var g2_prepared: [NUM_PAIRS]G2Prepared = undefined;
var fp12_inputs: [NUM_PAIRS]Fp12 = undefined;

fn setupInputs() void {
// G1: use generator (1, 2) for all pairs — pairing cost is
// dominated by the fixed Miller loop, not the specific point.
for (0..NUM_PAIRS) |i| {
g1_points[i] = .{ .x = Fp.one(), .y = Fp.fromU64(2), .infinity = false };
}

// G2: varied points via repeated doubling of generator.
var g2_proj = G2Projective.fromAffine(G2Point.generator());
for (0..NUM_PAIRS) |i| {
g2_points[i] = g2_proj.toAffine();
g2_prepared[i] = G2Prepared.fromG2Point(g2_points[i]);
g2_proj = g2_proj.double();
}

// Fp12: Miller loop outputs for finalExponentiation benchmark.
for (0..NUM_PAIRS) |i| {
fp12_inputs[i] = p.millerLoopArkworks(g1_points[i], g2_points[i]);
}
}

// --- Benchmark bodies ---

fn benchPairingFp(i: usize) Fp12 {
const idx = i % NUM_PAIRS;
return p.pairingFp(g1_points[idx], g2_points[idx]);
}

fn benchMillerLoop(i: usize) Fp12 {
const idx = i % NUM_PAIRS;
return p.millerLoopArkworks(g1_points[idx], g2_points[idx]);
}

fn benchMillerLoopPrepared(i: usize) Fp12 {
const idx = i % NUM_PAIRS;
return p.millerLoopPrepared(g1_points[idx], &g2_prepared[idx]);
}

fn benchFinalExp(i: usize) Fp12 {
const idx = i % NUM_PAIRS;
return p.finalExponentiation(fp12_inputs[idx]);
}

pub fn main() !void {
std.debug.print("=== Zolt-Arith Pairing Microbench ===\n", .{});
setupInputs();

const cfg = harness.Config.pairing;
_ = harness.run("pairing", "pairingFp", cfg, Fp12, benchPairingFp);
_ = harness.run("pairing", "millerLoop", cfg, Fp12, benchMillerLoop);
_ = harness.run("pairing", "millerLoopPrepared", cfg, Fp12, benchMillerLoopPrepared);
_ = harness.run("pairing", "finalExp", cfg, Fp12, benchFinalExp);
}
Loading
Loading