Skip to content

Commit 8a32f3a

Browse files
authored
Merge pull request #71 from MatteoMer/zolt-arith-fixtures-research
Add fixture-backed differential tests, benchmarks, squaring optimization, and fix G2 decompression
2 parents f265f5c + a1f37d4 commit 8a32f3a

66 files changed

Lines changed: 5225 additions & 76 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,59 @@ jobs:
225225
- name: Verify with Jolt verifier
226226
run: ./jolt-verifier/target/release/jolt-verifier --proof proof.bin --preprocessing preprocessing.bin
227227

228+
# ─── zolt-arith: differential fixture tests ────────────────────
229+
zolt-arith-diff:
230+
name: Differential Fixture Tests
231+
runs-on: ubuntu-latest
232+
steps:
233+
- uses: actions/checkout@v4
234+
- uses: mlugg/setup-zig@v2
235+
with:
236+
version: ${{ env.ZIG_VERSION }}
237+
- uses: dtolnay/rust-toolchain@stable
238+
- uses: Swatinem/rust-cache@v2
239+
with:
240+
workspaces: tools/zolt-arith-diff/arkworks-fixtures
241+
- name: Run differential tests
242+
run: zig build test-zolt-arith-diff -Doptimize=ReleaseSafe
243+
244+
# ─── zolt-arith: fixture freshness check ──────────────────────
245+
zolt-arith-fixture-freshness:
246+
name: Fixture Freshness Check
247+
runs-on: ubuntu-latest
248+
steps:
249+
- uses: actions/checkout@v4
250+
- uses: dtolnay/rust-toolchain@stable
251+
- uses: Swatinem/rust-cache@v2
252+
with:
253+
workspaces: tools/zolt-arith-diff/arkworks-fixtures
254+
- name: Regenerate fixtures
255+
run: cargo run --release --manifest-path tools/zolt-arith-diff/arkworks-fixtures/Cargo.toml -- --out-dir testdata/zolt-arith-diff
256+
- name: Check for drift
257+
run: git diff --exit-code testdata/zolt-arith-diff/
258+
259+
# ─── zolt-arith: benchmark reporting (non-blocking) ───────────
260+
zolt-arith-bench:
261+
name: Arithmetic Benchmarks
262+
runs-on: macos-15
263+
steps:
264+
- uses: actions/checkout@v4
265+
- name: Verify ARM64
266+
run: test "$(uname -m)" = "arm64"
267+
- uses: mlugg/setup-zig@v2
268+
with:
269+
version: ${{ env.ZIG_VERSION }}
270+
- name: Field microbench
271+
run: zig build bench-zolt-arith-field 2>&1 | tee field_bench.txt
272+
- name: Pairing microbench
273+
run: zig build bench-zolt-arith-pairing 2>&1 | tee pairing_bench.txt
274+
- name: Post benchmark summary
275+
run: |
276+
echo "## Benchmark Results" >> $GITHUB_STEP_SUMMARY
277+
echo '```' >> $GITHUB_STEP_SUMMARY
278+
grep '\[BENCH\]' field_bench.txt pairing_bench.txt >> $GITHUB_STEP_SUMMARY || true
279+
echo '```' >> $GITHUB_STEP_SUMMARY
280+
228281
# ─── Gate: all required checks must pass ─────────────────────────
229282
ci-pass:
230283
name: CI Pass
@@ -240,6 +293,8 @@ jobs:
240293
- cli-smoke
241294
- prove-verify-linux
242295
- prove-verify-macos
296+
- zolt-arith-diff
297+
- zolt-arith-fixture-freshness
243298
runs-on: ubuntu-latest
244299
steps:
245300
- name: Check all jobs
@@ -255,6 +310,8 @@ jobs:
255310
"${{ needs.cli-smoke.result }}"
256311
"${{ needs.prove-verify-linux.result }}"
257312
"${{ needs.prove-verify-macos.result }}"
313+
"${{ needs.zolt-arith-diff.result }}"
314+
"${{ needs.zolt-arith-fixture-freshness.result }}"
258315
)
259316
for r in "${results[@]}"; do
260317
if [[ "$r" != "success" ]]; then

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ node_modules/
99
jolt/
1010
bench/results/
1111
jolt-bench/target/
12+
tools/zolt-arith-diff/arkworks-fixtures/target/
1213
flamegraph.svg

bench/zolt_arith/bench_harness.zig

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//! Shared microbenchmark harness for zolt-arith.
2+
//! Zero external dependencies — only std.
3+
//!
4+
//! Usage:
5+
//! const harness = @import("bench_harness.zig");
6+
//! _ = harness.run("field_Fp", "mul", harness.Config.field, Fp, struct {
7+
//! fn call(i: usize) Fp { return a[i % N].mul(b[i % N]); }
8+
//! }.call);
9+
10+
const std = @import("std");
11+
12+
const MAX_SAMPLES: usize = 101;
13+
14+
pub const Config = struct {
15+
warmup_iters: usize = 5_000,
16+
sample_count: usize = 21,
17+
iters_per_sample: usize = 100_000,
18+
19+
pub const field: Config = .{
20+
.warmup_iters = 20_000,
21+
.sample_count = 21,
22+
.iters_per_sample = 100_000,
23+
};
24+
25+
pub const pairing: Config = .{
26+
.warmup_iters = 10,
27+
.sample_count = 21,
28+
.iters_per_sample = 100,
29+
};
30+
};
31+
32+
pub const Stats = struct {
33+
min_ns: f64,
34+
median_ns: f64,
35+
mean_ns: f64,
36+
p99_ns: f64,
37+
stddev_ns: f64,
38+
sample_count: usize,
39+
iters_per_sample: usize,
40+
};
41+
42+
pub fn run(
43+
comptime group: []const u8,
44+
comptime op_name: []const u8,
45+
config: Config,
46+
comptime T: type,
47+
comptime body: fn (usize) T,
48+
) Stats {
49+
const n = @min(config.sample_count, MAX_SAMPLES);
50+
const iters = config.iters_per_sample;
51+
52+
// --- Warmup ---
53+
var sink: T = undefined;
54+
for (0..config.warmup_iters) |i| {
55+
sink = body(i);
56+
}
57+
std.mem.doNotOptimizeAway(&sink);
58+
59+
// --- Collect samples ---
60+
var samples: [MAX_SAMPLES]f64 = undefined;
61+
for (0..n) |s| {
62+
var timer = std.time.Timer.start() catch unreachable;
63+
for (0..iters) |i| {
64+
sink = body(i);
65+
}
66+
std.mem.doNotOptimizeAway(&sink);
67+
const elapsed_ns: u64 = timer.read();
68+
samples[s] = @as(f64, @floatFromInt(elapsed_ns)) /
69+
@as(f64, @floatFromInt(iters));
70+
}
71+
72+
// --- Compute stats ---
73+
const slice = samples[0..n];
74+
std.sort.pdq(f64, slice, {}, lessThanF64);
75+
76+
var sum: f64 = 0;
77+
for (slice) |v| sum += v;
78+
const mean = sum / @as(f64, @floatFromInt(n));
79+
80+
var var_sum: f64 = 0;
81+
for (slice) |v| {
82+
const d = v - mean;
83+
var_sum += d * d;
84+
}
85+
const stddev = @sqrt(var_sum / @as(f64, @floatFromInt(n)));
86+
87+
const median = if (n % 2 == 1)
88+
slice[n / 2]
89+
else
90+
(slice[n / 2 - 1] + slice[n / 2]) / 2.0;
91+
92+
const p99_idx = @as(usize, @intFromFloat(@ceil(0.99 * @as(f64, @floatFromInt(n))))) - 1;
93+
94+
const stats = Stats{
95+
.min_ns = slice[0],
96+
.median_ns = median,
97+
.mean_ns = mean,
98+
.p99_ns = slice[p99_idx],
99+
.stddev_ns = stddev,
100+
.sample_count = n,
101+
.iters_per_sample = iters,
102+
};
103+
104+
printResult(group, op_name, stats);
105+
return stats;
106+
}
107+
108+
fn lessThanF64(_: void, a: f64, b: f64) bool {
109+
return a < b;
110+
}
111+
112+
const FmtTime = struct { val: f64, unit: []const u8 };
113+
114+
fn formatTime(ns: f64) FmtTime {
115+
if (ns >= 1_000_000.0) return .{ .val = ns / 1_000_000.0, .unit = "ms" };
116+
if (ns >= 1_000.0) return .{ .val = ns / 1_000.0, .unit = "us" };
117+
return .{ .val = ns, .unit = "ns" };
118+
}
119+
120+
fn printResult(comptime group: []const u8, comptime op_name: []const u8, s: Stats) void {
121+
const min_f = formatTime(s.min_ns);
122+
const med_f = formatTime(s.median_ns);
123+
const mean_f = formatTime(s.mean_ns);
124+
const p99_f = formatTime(s.p99_ns);
125+
const sd_f = formatTime(s.stddev_ns);
126+
127+
std.debug.print(
128+
"[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",
129+
.{
130+
group, op_name,
131+
min_f.val, min_f.unit,
132+
med_f.val, med_f.unit,
133+
mean_f.val, mean_f.unit,
134+
p99_f.val, p99_f.unit,
135+
sd_f.val, sd_f.unit,
136+
s.sample_count, s.iters_per_sample,
137+
},
138+
);
139+
}

bench/zolt_arith/field_micro.zig

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const std = @import("std");
2+
const zolt = @import("zolt");
3+
const harness = @import("bench_harness.zig");
4+
5+
const Fr = zolt.field.BN254Scalar;
6+
const Fp = zolt.field.BN254BaseField;
7+
8+
const ELEMENTS: usize = 128;
9+
10+
fn fillFieldInputs(comptime F: type, a: *[ELEMENTS]F, b: *[ELEMENTS]F) void {
11+
for (0..ELEMENTS) |i| {
12+
a[i] = F.fromU64(@as(u64, @intCast(i + 1)) *% 0x9E3779B185EBCA87 +% 17);
13+
b[i] = F.fromU64(@as(u64, @intCast(i + 1)) *% 0xD6E8FEB86659FD93 +% 29);
14+
}
15+
}
16+
17+
fn benchField(comptime F: type, comptime field_name: []const u8) void {
18+
const Ctx = struct {
19+
var a: [ELEMENTS]F = undefined;
20+
var b: [ELEMENTS]F = undefined;
21+
22+
fn add(i: usize) F {
23+
return a[i % ELEMENTS].add(b[i % ELEMENTS]);
24+
}
25+
fn sub(i: usize) F {
26+
return a[i % ELEMENTS].sub(b[i % ELEMENTS]);
27+
}
28+
fn mul(i: usize) F {
29+
return a[i % ELEMENTS].mul(b[i % ELEMENTS]);
30+
}
31+
fn square(i: usize) F {
32+
return a[i % ELEMENTS].square();
33+
}
34+
fn inverse(i: usize) F {
35+
return a[i % ELEMENTS].inverse() orelse F.zero();
36+
}
37+
fn toMontgomery(i: usize) F {
38+
return a[i % ELEMENTS].fromMontgomery().toMontgomery();
39+
}
40+
fn fromMontgomery(i: usize) F {
41+
return a[i % ELEMENTS].fromMontgomery();
42+
}
43+
fn sumOfProducts(i: usize) F {
44+
const idx = i % ELEMENTS;
45+
return F.sumOfProducts(.{ a[idx], b[idx] }, .{ b[idx], a[idx] });
46+
}
47+
};
48+
49+
fillFieldInputs(F, &Ctx.a, &Ctx.b);
50+
51+
const cfg = harness.Config.field;
52+
_ = harness.run(field_name, "add", cfg, F, Ctx.add);
53+
_ = harness.run(field_name, "sub", cfg, F, Ctx.sub);
54+
_ = harness.run(field_name, "mul", cfg, F, Ctx.mul);
55+
_ = harness.run(field_name, "square", cfg, F, Ctx.square);
56+
_ = harness.run(field_name, "inverse", cfg, F, Ctx.inverse);
57+
_ = harness.run(field_name, "toMontgomery", cfg, F, Ctx.toMontgomery);
58+
_ = harness.run(field_name, "fromMontgomery", cfg, F, Ctx.fromMontgomery);
59+
_ = harness.run(field_name, "sumOfProducts", cfg, F, Ctx.sumOfProducts);
60+
}
61+
62+
pub fn main() !void {
63+
std.debug.print("=== Zolt-Arith Field Microbench ===\n", .{});
64+
benchField(Fp, "field_Fp");
65+
benchField(Fr, "field_Fr");
66+
}

bench/zolt_arith/pairing_micro.zig

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const std = @import("std");
2+
const zolt = @import("zolt");
3+
const harness = @import("bench_harness.zig");
4+
5+
const field = zolt.field;
6+
const p = field.pairing;
7+
8+
const Fp = field.BN254BaseField;
9+
const Fp12 = p.Fp12;
10+
const G1PointFp = p.G1PointFp;
11+
const G2Point = p.G2Point;
12+
const G2Projective = p.G2Projective;
13+
const G2Prepared = p.G2Prepared;
14+
15+
const NUM_PAIRS: usize = 4;
16+
17+
// File-scope inputs populated by setupInputs().
18+
var g1_points: [NUM_PAIRS]G1PointFp = undefined;
19+
var g2_points: [NUM_PAIRS]G2Point = undefined;
20+
var g2_prepared: [NUM_PAIRS]G2Prepared = undefined;
21+
var fp12_inputs: [NUM_PAIRS]Fp12 = undefined;
22+
23+
fn setupInputs() void {
24+
// G1: use generator (1, 2) for all pairs — pairing cost is
25+
// dominated by the fixed Miller loop, not the specific point.
26+
for (0..NUM_PAIRS) |i| {
27+
g1_points[i] = .{ .x = Fp.one(), .y = Fp.fromU64(2), .infinity = false };
28+
}
29+
30+
// G2: varied points via repeated doubling of generator.
31+
var g2_proj = G2Projective.fromAffine(G2Point.generator());
32+
for (0..NUM_PAIRS) |i| {
33+
g2_points[i] = g2_proj.toAffine();
34+
g2_prepared[i] = G2Prepared.fromG2Point(g2_points[i]);
35+
g2_proj = g2_proj.double();
36+
}
37+
38+
// Fp12: Miller loop outputs for finalExponentiation benchmark.
39+
for (0..NUM_PAIRS) |i| {
40+
fp12_inputs[i] = p.millerLoopArkworks(g1_points[i], g2_points[i]);
41+
}
42+
}
43+
44+
// --- Benchmark bodies ---
45+
46+
fn benchPairingFp(i: usize) Fp12 {
47+
const idx = i % NUM_PAIRS;
48+
return p.pairingFp(g1_points[idx], g2_points[idx]);
49+
}
50+
51+
fn benchMillerLoop(i: usize) Fp12 {
52+
const idx = i % NUM_PAIRS;
53+
return p.millerLoopArkworks(g1_points[idx], g2_points[idx]);
54+
}
55+
56+
fn benchMillerLoopPrepared(i: usize) Fp12 {
57+
const idx = i % NUM_PAIRS;
58+
return p.millerLoopPrepared(g1_points[idx], &g2_prepared[idx]);
59+
}
60+
61+
fn benchFinalExp(i: usize) Fp12 {
62+
const idx = i % NUM_PAIRS;
63+
return p.finalExponentiation(fp12_inputs[idx]);
64+
}
65+
66+
pub fn main() !void {
67+
std.debug.print("=== Zolt-Arith Pairing Microbench ===\n", .{});
68+
setupInputs();
69+
70+
const cfg = harness.Config.pairing;
71+
_ = harness.run("pairing", "pairingFp", cfg, Fp12, benchPairingFp);
72+
_ = harness.run("pairing", "millerLoop", cfg, Fp12, benchMillerLoop);
73+
_ = harness.run("pairing", "millerLoopPrepared", cfg, Fp12, benchMillerLoopPrepared);
74+
_ = harness.run("pairing", "finalExp", cfg, Fp12, benchFinalExp);
75+
}

0 commit comments

Comments
 (0)