Skip to content

Commit 7b4aaee

Browse files
committed
feat(fuzz): session 59 — combined-slot fuzzers complete (PLONK + Phase-3) + docs
Closes the combined-slot fuzz harness gap from session 56 — adds 4 more combined harnesses (one per Phase-2/Phase-3 system), refactors the slot-splitting helper into the lib so all 5 combined fuzzers share it, and updates the audit-pack docs to mirror sessions 58 + 59. mosaic-fuzz/src/lib.rs - New `pub fn split_three_slots(data: &[u8])` — extracted from the session-56 Halo2 combined fuzzer; returns the same `(vk, proof, public_inputs)` triple from a length-prefixed layout (`[vk_len: u16 LE] [vk_bytes] [proof_len: u16 LE] [proof_bytes] [public_inputs ...]`). Returns `None` if any length prefix runs off the buffer. mosaic-fuzz/fuzz_targets/fuzz_halo2_combined.rs - Rewritten to import `split_three_slots` from the lib instead of defining a private copy, eliminating the duplicate parser. 4 new combined fuzz_target files in `fuzz_targets/`: - fuzz_plonk_combined — narrowest cross-slot surface (744 B / 768 B fixed envelopes); value is in catching parser confusions between them. - fuzz_hyperplonk_combined — pins the `vk.num_variables == proof.sumcheck_rounds` cross-check. - fuzz_nova_combined — pins both `vk.variant == proof.variant` (FoldingVariant 3-way tag) AND `vk.n_public == proof.n_public == public_inputs.len() / 32`. - fuzz_stark_combined — richest cross-check fingerprint: field_id, trace_log_height, trace_width, log_blowup must all agree across slots; a coordinated lie on any would route to a wrong-shape Merkle path or FRI fold chain. mosaic-fuzz/Cargo.toml - 4 new `[[bin]]` entries plus the 5 from session 58. README.md (Status table) - Fuzz row updated to "23 targets across all 6 production verifiers" with the new 4×4 + 3 inventory breakdown. CHANGELOG.md - New [Unreleased] subsection "Added — sessions 58-59" with per-session breakdowns and the full 23-target inventory table. - "Planned beyond session 59" block trimmed — both fuzz follow-ups (per-system PI fuzzers + remaining combined fuzzers) are now done. The remaining named pre-audit gaps are Phase-3 differential testing, chunked::dispatch integration, HyperPlonk Zeromorph reduction, and external audit commission. After sessions 47-59 the audit-coverage milestone has these dimensions: - Property tests: 137 across 12 crates (sessions 36-52) - BPF CU bench: 7 systems (sessions 47, 49) - Host criterion bench: 5 systems (session 51) - Fuzz harnesses: 23 targets across 6 systems (sessions 54-59) Local sanity: `cargo check -p mosaic-fuzz --lib` clean.
1 parent 8570c10 commit 7b4aaee

9 files changed

Lines changed: 241 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,16 +169,72 @@ After sessions 54-56 the fuzz harness inventory is **14 targets**:
169169
- 5 per-system vk_bytes (same five)
170170
- 1 combined-slot Halo2
171171

172-
### Planned beyond session 56
172+
### Added — sessions 58-59 (fuzz harness completion)
173+
174+
#### Session 58 — per-system public-input fuzzers
175+
Added 5 more harnesses targeting the public-input parser of each
176+
Phase-2 / Phase-3 system: `fuzz_{plonk, hyperplonk, halo2, nova,
177+
stark}_public_inputs`. Each pins its system's PI invariants:
178+
179+
- PLONK + HyperPlonk + Halo2 + Nova: `len % 32 == 0`,
180+
`len / 32 == vk.n_public`, every 32-byte chunk in Fr range.
181+
- STARK: length must be a multiple of
182+
`field_id.field_elem_bytes()` (8 for Goldilocks, 4 for
183+
BabyBear / Mersenne31).
184+
185+
Halo2's PI feeds round-1 of the Fiat-Shamir absorb sequence — a
186+
regression in PI parsing would cascade into every challenge and
187+
break the verifier's identity check. The session-37 challenges
188+
proptests already pin the cascade for valid PI; this fuzzer pins
189+
the rejection path for invalid PI across the full byte-buffer space.
190+
191+
#### Session 59 — combined-slot fuzzers for the remaining 4 systems
192+
Adds combined-slot harnesses for PLONK, HyperPlonk, Nova, and
193+
FRI-STARK, completing the cross-slot interaction surface coverage
194+
the session-56 Halo2 template demonstrated. Refactored the
195+
`split_three_slots` helper out of the Halo2 dump-target into
196+
`mosaic-fuzz::lib` so all 5 combined-fuzzer binaries share the
197+
same length-prefix parser.
198+
199+
Each new combined fuzzer pins system-specific cross-checks that
200+
single-slot harnesses can't reach (because both slots must lie
201+
in a coordinated way for the bug to surface):
202+
203+
- PLONK: 744 B / 768 B fixed envelopes (narrowest cross-slot
204+
surface; value is in catching parser confusions between the
205+
two envelopes).
206+
- HyperPlonk: `vk.num_variables == proof.sumcheck_rounds`
207+
cross-check.
208+
- Nova: `vk.variant == proof.variant` (FoldingVariant 3-way) +
209+
`vk.n_public == proof.n_public == public_inputs.len() / 32`.
210+
- STARK: richest cross-check fingerprint of any verifier:
211+
`vk.field_id == proof.field_id`,
212+
`vk.trace_log_height == proof.trace_log_height`,
213+
`vk.trace_width == proof.trace_width`,
214+
`vk.log_blowup == proof.log_blowup`. A coordinated lie on any
215+
of these would route the verifier to a wrong-shape Merkle path
216+
or FRI fold chain.
217+
218+
After sessions 58-59 the fuzz harness inventory is **23 targets**
219+
across all 6 production verifier surfaces:
220+
221+
Phase-1 Groth16 (3 original)
222+
fuzz_groth16_proof_bytes, fuzz_vk_bytes, fuzz_public_inputs
223+
224+
Phase-2 KZG-PLONK (4)
225+
fuzz_plonk_{proof_bytes, vk_bytes, public_inputs, combined}
226+
227+
Phase-3 HyperPlonk + Halo2 + Nova + FRI-STARK (16)
228+
fuzz_hyperplonk_{proof_bytes, vk_bytes, public_inputs, combined}
229+
fuzz_halo2_{proof_bytes, vk_bytes, public_inputs, combined}
230+
fuzz_nova_{proof_bytes, vk_bytes, public_inputs, combined}
231+
fuzz_stark_{proof_bytes, vk_bytes, public_inputs, combined}
232+
233+
### Planned beyond session 59
173234

174235
- Fixture-driven differential testing for the four Phase-3 bodies
175236
(Espresso HyperPlonk, PSE Halo2, sonobe Nova, Plonky3 STARK).
176237
**Last named pre-audit gap on the Phase-3 verifier track.**
177-
- Per-system public-input fuzzers (PLONK + Phase-3) — near-trivial
178-
follow-up to session 55, deferred only to keep commit boundaries
179-
clean.
180-
- Combined-slot fuzzers for the remaining 4 systems (HyperPlonk,
181-
Nova, STARK, PLONK) — copy of the session-56 Halo2 template.
182238
- `mosaic-program::chunked::dispatch` integration tests via
183239
`solana-program-test` with synthesized `AccountInfo`.
184240
- HyperPlonk full Zeromorph / PST / Gemini reduction (canonical

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ frozen CU budgets.
5959
| Property-test coverage (proptest, sessions 36-52) | ✅ 534 lib tests across 12 crates (+137 proptest in audit-coverage sweep) ||
6060
| BPF CU regression bench (`bpf-bench`) | ✅ 7 systems: Groth16 (single + batch), KZG-PLONK, HyperPlonk, Halo2, Nova, FRI-STARK ||
6161
| Host criterion bench (wall-clock baseline) | ✅ 5 systems: Groth16, HyperPlonk, Halo2, Nova, FRI-STARK ||
62-
| Fuzz harnesses (sessions 54-56) |14 targets across all 6 production verifiers (5 proof-bytes, 5 vk-bytes, 1 combined-slot Halo2, 3 original Groth16) ||
62+
| Fuzz harnesses (sessions 54-59) |23 targets across all 6 production verifiers (5 proof-bytes + 5 vk-bytes + 5 public-inputs + 5 combined-slot for PLONK + 4 Phase-3, plus 3 original Groth16) ||
6363
| External audit | 🔴 Not yet commissioned ||
6464

6565
See [`AUDIT.md`](AUDIT.md) for audit history and [`SECURITY.md`](SECURITY.md)

crates/mosaic-fuzz/Cargo.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,33 @@ path = "fuzz_targets/fuzz_stark_public_inputs.rs"
160160
test = false
161161
doc = false
162162

163+
# ───────────────────────────────────────────────────────────────────────
164+
# Session 59 — combined-slot fuzzers for the remaining 4 systems
165+
# ───────────────────────────────────────────────────────────────────────
166+
167+
[[bin]]
168+
name = "fuzz_plonk_combined"
169+
path = "fuzz_targets/fuzz_plonk_combined.rs"
170+
test = false
171+
doc = false
172+
173+
[[bin]]
174+
name = "fuzz_hyperplonk_combined"
175+
path = "fuzz_targets/fuzz_hyperplonk_combined.rs"
176+
test = false
177+
doc = false
178+
179+
[[bin]]
180+
name = "fuzz_nova_combined"
181+
path = "fuzz_targets/fuzz_nova_combined.rs"
182+
test = false
183+
doc = false
184+
185+
[[bin]]
186+
name = "fuzz_stark_combined"
187+
path = "fuzz_targets/fuzz_stark_combined.rs"
188+
test = false
189+
doc = false
190+
163191
[lints]
164192
workspace = true

crates/mosaic-fuzz/fuzz_targets/fuzz_halo2_combined.rs

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -37,40 +37,11 @@
3737

3838
use libfuzzer_sys::fuzz_target;
3939
use mosaic_core::{proof_system::ProofSystem, syscall::host::HostBackend};
40+
use mosaic_fuzz::split_three_slots;
4041
use mosaic_halo2::Halo2KzgBn254;
4142

42-
fn split_three(data: &[u8]) -> Option<(&[u8], &[u8], &[u8])> {
43-
let mut cursor = data;
44-
45-
// vk_len (u16 LE).
46-
if cursor.len() < 2 {
47-
return None;
48-
}
49-
let (lp, rest) = cursor.split_at(2);
50-
let vk_len = u16::from_le_bytes([lp[0], lp[1]]) as usize;
51-
if rest.len() < vk_len {
52-
return None;
53-
}
54-
let (vk, rest) = rest.split_at(vk_len);
55-
cursor = rest;
56-
57-
// proof_len (u16 LE).
58-
if cursor.len() < 2 {
59-
return None;
60-
}
61-
let (lp, rest) = cursor.split_at(2);
62-
let proof_len = u16::from_le_bytes([lp[0], lp[1]]) as usize;
63-
if rest.len() < proof_len {
64-
return None;
65-
}
66-
let (proof, public_inputs) = rest.split_at(proof_len);
67-
68-
// Whatever remains is the public-inputs slot.
69-
Some((vk, proof, public_inputs))
70-
}
71-
7243
fuzz_target!(|data: &[u8]| {
73-
let Some((vk, proof, public_inputs)) = split_three(data) else {
44+
let Some((vk, proof, public_inputs)) = split_three_slots(data) else {
7445
return;
7546
};
7647
let backend = HostBackend::new();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//! Fuzz harness — HyperPlonk-KZG combined-slot fuzzer.
2+
//!
3+
//! Session 59 — cross-slot interaction surface for HyperPlonk. The
4+
//! `vk.num_variables == proof.sumcheck_rounds` cross-check is the
5+
//! core invariant a combined fuzzer can hit that single-slot
6+
//! harnesses can't: both slots must lie about the same shape
7+
//! parameter for the bug to surface. The harness explores that
8+
//! coordinated misalignment automatically.
9+
10+
#![no_main]
11+
12+
use libfuzzer_sys::fuzz_target;
13+
use mosaic_core::{proof_system::ProofSystem, syscall::host::HostBackend};
14+
use mosaic_fuzz::split_three_slots;
15+
use mosaic_hyperplonk::HyperPlonkKzgBn254;
16+
17+
fuzz_target!(|data: &[u8]| {
18+
let Some((vk, proof, public_inputs)) = split_three_slots(data) else {
19+
return;
20+
};
21+
let backend = HostBackend::new();
22+
let v = HyperPlonkKzgBn254::new(&backend);
23+
let _ = ProofSystem::verify(&v, vk, proof, public_inputs);
24+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//! Fuzz harness — Nova folding combined-slot fuzzer.
2+
//!
3+
//! Session 59 — cross-slot interaction surface for Nova. Two cross-
4+
//! checks need coordinated misalignment to surface:
5+
//!
6+
//! - `vk.variant == proof.variant` (FoldingVariant 3-way tag must
7+
//! agree across both slots).
8+
//! - `vk.n_public == proof.n_public == public_inputs.len() / 32`.
9+
//!
10+
//! The combined fuzzer can vary all three slots independently and
11+
//! hit coordinated lies that single-slot harnesses can't reach.
12+
13+
#![no_main]
14+
15+
use libfuzzer_sys::fuzz_target;
16+
use mosaic_core::{proof_system::ProofSystem, syscall::host::HostBackend};
17+
use mosaic_fuzz::split_three_slots;
18+
use mosaic_nova::NovaFolding;
19+
20+
fuzz_target!(|data: &[u8]| {
21+
let Some((vk, proof, public_inputs)) = split_three_slots(data) else {
22+
return;
23+
};
24+
let backend = HostBackend::new();
25+
let v = NovaFolding::new(&backend);
26+
let _ = ProofSystem::verify(&v, vk, proof, public_inputs);
27+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//! Fuzz harness — KZG-PLONK combined-slot fuzzer.
2+
//!
3+
//! Session 59 — cross-slot interaction surface for PLONK. Mirrors
4+
//! the session-56 Halo2 combined-fuzzer template with PLONK as the
5+
//! verifier under test. Both PLONK's VK (744 B fixed) and proof
6+
//! (768 B fixed) are fixed-length envelopes, so the cross-slot
7+
//! interaction surface is narrower than Halo2's; the value is in
8+
//! catching length-mismatch routing bugs that the per-slot harnesses
9+
//! can't reach (e.g. a parser that confuses the two 744 B / 768 B
10+
//! envelopes).
11+
12+
#![no_main]
13+
14+
use libfuzzer_sys::fuzz_target;
15+
use mosaic_core::{proof_system::ProofSystem, syscall::host::HostBackend};
16+
use mosaic_fuzz::split_three_slots;
17+
use mosaic_plonk::PlonkKzgBn254;
18+
19+
fuzz_target!(|data: &[u8]| {
20+
let Some((vk, proof, public_inputs)) = split_three_slots(data) else {
21+
return;
22+
};
23+
let backend = HostBackend::new();
24+
let v = PlonkKzgBn254::new(&backend);
25+
let _ = ProofSystem::verify(&v, vk, proof, public_inputs);
26+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//! Fuzz harness — FRI-STARK combined-slot fuzzer.
2+
//!
3+
//! Session 59 — cross-slot interaction surface for FRI-STARK. The
4+
//! richest cross-check fingerprint of any verifier in the workspace:
5+
//!
6+
//! - `vk.field_id == proof.field_id` (StarkFieldId 3-way tag).
7+
//! - `vk.trace_log_height == proof.trace_log_height`
8+
//! - `vk.trace_width == proof.trace_width`
9+
//! - `vk.log_blowup == proof.log_blowup`
10+
//!
11+
//! Each must agree across slots; a coordinated lie on any of these
12+
//! would route the verifier to a wrong-shape Merkle path or FRI
13+
//! fold chain. The combined fuzzer explores every such coordinated
14+
//! misalignment automatically.
15+
16+
#![no_main]
17+
18+
use libfuzzer_sys::fuzz_target;
19+
use mosaic_core::{proof_system::ProofSystem, syscall::host::HostBackend};
20+
use mosaic_fuzz::split_three_slots;
21+
use mosaic_stark::FriStark;
22+
23+
fuzz_target!(|data: &[u8]| {
24+
let Some((vk, proof, public_inputs)) = split_three_slots(data) else {
25+
return;
26+
};
27+
let backend = HostBackend::new();
28+
let v = FriStark::new(&backend);
29+
let _ = ProofSystem::verify(&v, vk, proof, public_inputs);
30+
});

crates/mosaic-fuzz/src/lib.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,47 @@ impl Default for SharedFixtures {
6060
}
6161
}
6262

63+
/// Split a libfuzzer input buffer into three length-prefixed slots
64+
/// `(vk_bytes, proof_bytes, public_inputs_bytes)`. Returns `None` if
65+
/// any length prefix runs off the end of the buffer.
66+
///
67+
/// Layout: `[vk_len: u16 LE] [vk_bytes] [proof_len: u16 LE]
68+
/// [proof_bytes] [public_inputs ...]`
69+
///
70+
/// Used by `fuzz_*_combined.rs` (sessions 56, 59) to explore a
71+
/// coordinate in `(vk, proof, pi)` space rather than the 1-D slice
72+
/// the per-slot harnesses cover. See the rationale comment in
73+
/// `fuzz_halo2_combined.rs` for why combined fuzzers complement
74+
/// the single-slot variants.
75+
pub fn split_three_slots(data: &[u8]) -> Option<(&[u8], &[u8], &[u8])> {
76+
let cursor = data;
77+
78+
// vk_len (u16 LE).
79+
if cursor.len() < 2 {
80+
return None;
81+
}
82+
let (lp, rest) = cursor.split_at(2);
83+
let vk_len = u16::from_le_bytes([lp[0], lp[1]]) as usize;
84+
if rest.len() < vk_len {
85+
return None;
86+
}
87+
let (vk, rest) = rest.split_at(vk_len);
88+
89+
// proof_len (u16 LE).
90+
if rest.len() < 2 {
91+
return None;
92+
}
93+
let (lp, rest) = rest.split_at(2);
94+
let proof_len = u16::from_le_bytes([lp[0], lp[1]]) as usize;
95+
if rest.len() < proof_len {
96+
return None;
97+
}
98+
let (proof, public_inputs) = rest.split_at(proof_len);
99+
100+
// Whatever remains is the public-inputs slot.
101+
Some((vk, proof, public_inputs))
102+
}
103+
63104
// ─────────────────────────────────────────────────────────────────────
64105
// Session 54 — Phase-2 + Phase-3 fixture builders.
65106
//

0 commit comments

Comments
 (0)