You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two 64-byte trace-row types implement the same spec'd layout, and the prover materializes both, one per cycle:
jolt_riscv::JoltTraceRow (crates/jolt-riscv/src/trace_row.rs) — the proof-facing row, owner of specs/proof-trace-row-layout.md, consumed across jolt-witness, the kernels and tracer.
JoltTraceRow owns that rule in CapturedState::into_value_slots, with the class check surfacing as TraceRowError::StateClassMismatch. TraceRow open-codes it again with its own TraceRowMeta, RamAccessKind, immediate-sign bit and TraceRowError. This is the one-owner rule in CLAUDE.md:
Give each protocol formula, geometry or sizing law, schedule, and state transition one owner. Consumers call the canonical implementation instead of mirroring or open-coding it.
tracer also carries two independent Cycle → row conversions, each separately re-verifying the same memory-row collapse:
tracer/src/trace_row.rs — Cycle → JoltTraceRow, whose header says it is "where the final memory-row contract is verified"
crates/jolt-witness/src/backend/trace/mod.rs builds the full Vec<JoltTraceRow> while the &[TraceRow] slice is still live:
let physical = source.rows()…;// &[TraceRow] — 64 B/cyc, alive throughoutletmut trace_rows = Vec::new();for row in physical {let compact = Self::compact_trace_row(row,&inputs.preprocessing)?;
…trace_rows.push(compact);// Vec<JoltTraceRow> — another 64 B/cyc#[cfg(feature = "field-inline")]
raw_rows.push(row.clone());// a third copy under field-inline}
So 128 B/cycle, both vectors resident: 8 GiB at 2^26 where one type would need 4.
For scale, a fibonacci 2^26 A/B on 16-core Linux measured #1734 at 16.12 GiB peak RSS (its merge-base at 21.76 GiB). Retiring the duplicate should be worth roughly the 4 GiB — about a quarter of the current peak, and larger than several mechanisms #1734 does claim. This is a static estimate from the row width, not a measurement.
Corroboration that both vectors really are full-length and simultaneously resident: #1734's 160 → 64 B packing predicts 96 B/cyc × 2^26 = 6.0 GiB, and the measured delta was 5.64 GiB.
Why they differ today
JoltTraceRow refuses four things by design, and they are exactly the byte budget it spends on bytecode_pc (4) + _reserved (3):
TraceRow
JoltTraceRow
bytecode_pc
absent
required by from_components(state, instruction, bytecode_pc)
virtual_sequence_remaining
u16
—
register ids
recorded and decoded operand ids
operand-derived only
field_inline
Option<Arc<FieldInlineTraceData>>
would break Copy and the 64-byte assert
serde
via TraceRowWire
none
The binding one is bytecode_pc: jolt-program's row is produced before bytecode expansion, and the Cycle → JoltTraceRow path needs BytecodePreprocessing. JoltTraceRow's module doc states it is "built once after tracing and final bytecode expansion."
Proposed direction
Make bytecode_pc late-assignable on JoltTraceRow so the tracer can emit rows before bytecode expansion.
Move virtual_sequence_remaining, the recorded register ids, and field_inline into a side table keyed by cycle, keeping the row Copy and 64 B.
Give JoltTraceRow serde through a wire shim on the logical accessors — not a derive, which would freeze meta's bit packing, _reserved and the slot aliasing as a wire format and defeat the "physical storage is private and free to alias" invariant. TraceRow::TraceRowWire in perf(prover): ~2x lower peak prover memory at 2^25-2^26, byte-identical proofs #1734 is the shape to reuse.
Emit JoltTraceRow from tracer directly; delete jolt_program::TraceRow, RegisterState, RamAccess and compact_trace_row.
Preserve the recorded-vs-decoded register contract checks currently in TraceRow::new.
A cheaper intermediate, if the full unification stalls: have TraceRow pack and unpack through jolt_riscv::CapturedState (making into_value_slots public) so the aliasing law and its validation have one implementation. No memory change, but it retires the duplicated layout rule.
Sequencing
After #1734 (packs TraceRow to 64 B and would otherwise conflict throughout) and #1818 (removes jolt-prover-legacy).
Note #1818 does not remove the serde constraint — ProgramSummary moves to crates/jolt-host/src/analyze.rs and still holds pub trace: Vec<TraceRow> with Serialize/Deserialize. It stays public SDK surface: #[jolt::provable] generates analyze_*() -> ProgramSummary, and several examples read program_summary.trace.len() (one drops it to reclaim trace memory before proving). Worth asking whether that field should carry a whole trace at all — analyze() only reads instruction_kind() and trace_len() only needs a count.
Scope
Roughly 1,500–3,000 lines across ~20 files in 9 crates: jolt-riscv, jolt-program, tracer, jolt-witness, jolt-kernels, jolt-prover, jolt-host, jolt-tracer-x86, jolt-eval. Public SDK surface is affected, so it is semver-relevant, and it must stay byte-identical against the dory_byte_diff fixtures. Large enough to warrant a spec.
Summary
Two 64-byte trace-row types implement the same spec'd layout, and the prover materializes both, one per cycle:
jolt_riscv::JoltTraceRow(crates/jolt-riscv/src/trace_row.rs) — the proof-facing row, owner ofspecs/proof-trace-row-layout.md, consumed acrossjolt-witness, the kernels andtracer.jolt_program::execution::trace::TraceRow(crates/jolt-program/src/execution/trace/row.rs) — the execution-facing row, packed 160 → 64 B in perf(prover): ~2x lower peak prover memory at 2^25-2^26, byte-identical proofs #1734.They should converge on one type once #1734 and #1818 land.
The duplication
Both use the same four aliased
u64value slots under the three final row classes fromspecs/proof-trace-row-layout.md:rs1,rs2,rd_pre,rd_postrs1,ram_address,rd_pre,rd_post(= the RAM value)rs1,rs2(= RAM post),ram_pre,ram_addressJoltTraceRowowns that rule inCapturedState::into_value_slots, with the class check surfacing asTraceRowError::StateClassMismatch.TraceRowopen-codes it again with its ownTraceRowMeta,RamAccessKind, immediate-sign bit andTraceRowError. This is the one-owner rule in CLAUDE.md:traceralso carries two independentCycle → rowconversions, each separately re-verifying the same memory-row collapse:tracer/src/trace_row.rs—Cycle → JoltTraceRow, whose header says it is "where the final memory-row contract is verified"tracer/src/execution_backend.rs::trace_row_from_cycle—Cycle → TraceRowMemory cost
crates/jolt-witness/src/backend/trace/mod.rsbuilds the fullVec<JoltTraceRow>while the&[TraceRow]slice is still live:So 128 B/cycle, both vectors resident: 8 GiB at 2^26 where one type would need 4.
For scale, a fibonacci 2^26 A/B on 16-core Linux measured #1734 at 16.12 GiB peak RSS (its merge-base at 21.76 GiB). Retiring the duplicate should be worth roughly the 4 GiB — about a quarter of the current peak, and larger than several mechanisms #1734 does claim. This is a static estimate from the row width, not a measurement.
Corroboration that both vectors really are full-length and simultaneously resident: #1734's 160 → 64 B packing predicts 96 B/cyc × 2^26 = 6.0 GiB, and the measured delta was 5.64 GiB.
Why they differ today
JoltTraceRowrefuses four things by design, and they are exactly the byte budget it spends onbytecode_pc(4) +_reserved(3):TraceRowJoltTraceRowbytecode_pcfrom_components(state, instruction, bytecode_pc)virtual_sequence_remainingu16field_inlineOption<Arc<FieldInlineTraceData>>Copyand the 64-byte assertTraceRowWireThe binding one is
bytecode_pc:jolt-program's row is produced before bytecode expansion, and theCycle → JoltTraceRowpath needsBytecodePreprocessing.JoltTraceRow's module doc states it is "built once after tracing and final bytecode expansion."Proposed direction
bytecode_pclate-assignable onJoltTraceRowso the tracer can emit rows before bytecode expansion.virtual_sequence_remaining, the recorded register ids, andfield_inlineinto a side table keyed by cycle, keeping the rowCopyand 64 B.JoltTraceRowserde through a wire shim on the logical accessors — not a derive, which would freezemeta's bit packing,_reservedand the slot aliasing as a wire format and defeat the "physical storage is private and free to alias" invariant.TraceRow::TraceRowWirein perf(prover): ~2x lower peak prover memory at 2^25-2^26, byte-identical proofs #1734 is the shape to reuse.JoltTraceRowfromtracerdirectly; deletejolt_program::TraceRow,RegisterState,RamAccessandcompact_trace_row.TraceRow::new.A cheaper intermediate, if the full unification stalls: have
TraceRowpack and unpack throughjolt_riscv::CapturedState(makinginto_value_slotspublic) so the aliasing law and its validation have one implementation. No memory change, but it retires the duplicated layout rule.Sequencing
After #1734 (packs
TraceRowto 64 B and would otherwise conflict throughout) and #1818 (removesjolt-prover-legacy).Note #1818 does not remove the serde constraint —
ProgramSummarymoves tocrates/jolt-host/src/analyze.rsand still holdspub trace: Vec<TraceRow>withSerialize/Deserialize. It stays public SDK surface:#[jolt::provable]generatesanalyze_*() -> ProgramSummary, and several examples readprogram_summary.trace.len()(onedrops it to reclaim trace memory before proving). Worth asking whether that field should carry a whole trace at all —analyze()only readsinstruction_kind()andtrace_len()only needs a count.Scope
Roughly 1,500–3,000 lines across ~20 files in 9 crates:
jolt-riscv,jolt-program,tracer,jolt-witness,jolt-kernels,jolt-prover,jolt-host,jolt-tracer-x86,jolt-eval. Public SDK surface is affected, so it is semver-relevant, and it must stay byte-identical against thedory_byte_difffixtures. Large enough to warrant a spec.