Skip to main content

kimchi/
cached_prover_index.rs

1//! mmap-backed proving-key cache.
2//!
3//! The cache file is a self-describing binary layout:
4//!
5//! 1. File magic + format version + ark-ff version + identifier.
6//! 2. Fixed-size `ScalarHeader` holding all small POD metadata (public input
7//!    count, zk_rows, domain sizes + generators, endo, shift, etc.).
8//! 3. A section table pointing at variable-length POD payload sections
9//!    (sid, pruned gates, column evaluation arrays, lookup arrays).
10//!
11//! The readable side [`MmapProverIndex`] holds an `Arc<ReadOnlyMmap>` plus
12//! precomputed slice references into the mapping. Field-element accesses do
13//! not allocate; the OS page cache handles eviction under pressure.
14//!
15//! Only available with the `mmap_cache` feature.
16
17use ark_ff::PrimeField;
18use std::fmt;
19
20use crate::circuits::wires::PERMUTS;
21
22/// Magic bytes at file offset 0. Distinct from any existing cache format in
23/// the Mina stack so a wrong file is rejected immediately.
24pub const FILE_MAGIC: [u8; 8] = *b"MINAPK01";
25
26/// Current on-disk layout version. Bump on any incompatible change.
27///
28/// Version 2 switched field-element encoding from canonical form to
29/// Montgomery form so that the on-disk bytes match `Fp`'s in-memory
30/// layout exactly. That lets the reader construct `Vec<F>` via
31/// `Vec::from_raw_parts` pointing into the mmap (zero-copy), instead of
32/// running a per-element Montgomery reduction on load.
33///
34/// Version 3 added the `GateCoeffs` section. The prover never reads
35/// `CircuitGate::coeffs` (they are folded into `coefficients8`), but the
36/// debug-build `ProverIndex::verify` gate check does, so they must be
37/// preserved for a cached index to prove under `debug_assertions`.
38pub const FORMAT_VERSION: u32 = 3;
39
40/// Maximum length (in bytes) of the caller-supplied identifier stored in the
41/// file header. Sized to comfortably accommodate sha512 hex (128 bytes) plus
42/// a caller prefix. Stored length-prefixed inside a fixed-size field so the
43/// header layout is constant regardless of identifier length.
44pub const IDENTIFIER_MAX_LEN: usize = 512;
45
46/// Maximum length of the ark-ff version string recorded in the header.
47pub const ARK_FF_VERSION_MAX_LEN: usize = 32;
48
49/// Alignment (in bytes) applied to every payload section. `BigInt<4>` (four
50/// `u64` limbs) only needs 8-byte alignment for zero-copy `&[F]` casts via
51/// `from_raw_parts`; 32 is a conservative choice that comfortably covers it
52/// and matches the field element's on-disk size.
53pub const SECTION_ALIGNMENT: usize = 32;
54
55/// Size of one field element on disk: four little-endian `u64` limbs of
56/// Montgomery representation.
57pub const FIELD_ELEMENT_BYTES: usize = 32;
58
59// Compile-time sanity: we rely on little-endian hosts for direct byte-level
60// access into the mmap. Abort the build on big-endian targets rather than
61// silently corrupting data.
62#[cfg(target_endian = "big")]
63compile_error!(
64    "cached_prover_index requires a little-endian target; the on-disk \
65     layout stores field-element limbs as little-endian u64."
66);
67
68/// Tags identifying the different payload sections in the section table.
69/// Tag values are stable across format versions within the same major
70/// version and must never be reused for a different meaning.
71///
72/// The numbering is intentionally sparse: `Coefficients8Base` (0x10) and
73/// `PermutationCoefficients8Base` (0x30) are *bases* — the `i`-th column's tag
74/// is `base + i` (see [`coefficient_tag`] / [`permutation_coefficient_tag`]),
75/// so `0x11..=0x1E` and `0x31..=0x36` are implicitly reserved. The static
76/// assertions below guard the gaps up to the next explicit tag.
77#[repr(u32)]
78#[derive(Copy, Clone, Debug, PartialEq, Eq)]
79pub enum SectionTag {
80    /// `sid: Vec<F>`.
81    Sid = 0x01,
82    /// Packed `[PrunedGate]` array.
83    Gates = 0x02,
84    /// Per-gate coefficient vectors, in gate order: for each gate a `u32`
85    /// count followed by `count` field elements (four LE `u64` limbs each).
86    /// Needed only by the debug-build gate sanity check, not by the prover.
87    GateCoeffs = 0x03,
88    /// Coefficients 0..=14 over domain d8 (one tag per column, sparse:
89    /// occupies 0x10..=0x1E).
90    Coefficients8Base = 0x10,
91    /// Generic-gate selector over domain d4.
92    GenericSelector4 = 0x20,
93    /// Poseidon-gate selector over domain d8.
94    PoseidonSelector8 = 0x21,
95    /// Complete-add selector over domain d4.
96    CompleteAddSelector4 = 0x22,
97    /// Variable-base scalar-mul selector over domain d8.
98    MulSelector8 = 0x23,
99    /// Endo scalar-mul selector over domain d8.
100    EmulSelector8 = 0x24,
101    /// Endo-mul-scalar selector over domain d8.
102    EndomulScalarSelector8 = 0x25,
103    /// Permutation coefficients 0..=6 over domain d8 (one tag per column,
104    /// sparse: occupies 0x30..=0x36).
105    PermutationCoefficients8Base = 0x30,
106    /// Optional RangeCheck0 selector over domain d8.
107    RangeCheck0Selector8 = 0x40,
108    /// Optional RangeCheck1 selector over domain d8.
109    RangeCheck1Selector8 = 0x41,
110    /// Optional ForeignFieldAdd selector over domain d8.
111    ForeignFieldAddSelector8 = 0x42,
112    /// Optional ForeignFieldMul selector over domain d8.
113    ForeignFieldMulSelector8 = 0x43,
114    /// Optional Xor16 selector over domain d8.
115    XorSelector8 = 0x44,
116    /// Optional Rot64 selector over domain d8.
117    RotSelector8 = 0x45,
118    /// Concatenated `lookup_table8` payload: `count × d8_size × 32` bytes
119    /// of field elements. The count of inner arrays is stored in the
120    /// section-table entry's `elem_domain_size`, keeping the payload
121    /// 32-byte aligned for zero-copy slice construction.
122    LookupTable8 = 0x50,
123    /// `table_ids8` over d8. Present iff `LookupSelectorBits::TABLE_IDS8`.
124    TableIds8 = 0x51,
125    /// `lookup_selectors.xor` over d8. Presence per `LookupSelectorBits`.
126    LookupSelectorXor = 0x52,
127    /// `lookup_selectors.lookup` over d8.
128    LookupSelectorLookup = 0x53,
129    /// `lookup_selectors.range_check` over d8.
130    LookupSelectorRangeCheck = 0x54,
131    /// `lookup_selectors.ffmul` over d8.
132    LookupSelectorFfmul = 0x55,
133    /// `runtime_selector` over d8.
134    RuntimeSelector8 = 0x56,
135    /// `runtime_tables` spec: `u32` count, then `count × (id:i32, len:u32)`.
136    RuntimeTablesSpec = 0x57,
137    /// `runtime_table_offset`: 8-byte little-endian u64.
138    RuntimeTableOffset = 0x58,
139}
140
141impl SectionTag {
142    pub fn to_u32(self) -> u32 {
143        self as u32
144    }
145}
146
147// The coefficient tags occupy `0x10..0x10 + COLUMNS`; the next explicit tag is
148// `GenericSelector4 = 0x20`. Likewise permutation tags occupy `0x30..0x30 +
149// PERMUTS` before `RangeCheck0Selector8 = 0x40`. Turn a future increase of
150// COLUMNS/PERMUTS that would collide into a build error rather than a silent
151// tag clash.
152const _: () = assert!(
153    COLUMNS <= 0x20 - 0x10,
154    "coefficient section tags would collide with GenericSelector4 (0x20)"
155);
156const _: () = assert!(
157    PERMUTS <= 0x40 - 0x30,
158    "permutation-coefficient section tags would collide with RangeCheck0Selector8 (0x40)"
159);
160
161/// Returns the section tag for the `i`-th coefficient column (0..`COLUMNS`).
162pub fn coefficient_tag(i: usize) -> u32 {
163    assert!(i < COLUMNS, "coefficient index out of range");
164    SectionTag::Coefficients8Base as u32 + i as u32
165}
166
167/// Returns the section tag for the `i`-th permutation coefficient column
168/// (0..=6).
169pub fn permutation_coefficient_tag(i: usize) -> u32 {
170    assert!(i < PERMUTS, "permutation coefficient index out of range");
171    SectionTag::PermutationCoefficients8Base as u32 + i as u32
172}
173
174/// Fixed-size header holding all scalar-valued metadata.
175///
176/// Packed explicitly via a fixed byte layout rather than `#[repr(C)]` to keep
177/// the on-disk format independent of Rust's layout algorithm. All integer
178/// fields are little-endian; field elements are stored as four LE `u64`
179/// limbs matching `ark_ff::BigInt<4>`.
180#[derive(Clone, Debug)]
181pub struct ScalarHeader {
182    pub public: u32,
183    pub prev_challenges: u32,
184    pub zk_rows: u64,
185    pub max_poly_size: u64,
186    pub disable_gates_checks: bool,
187    /// `d1` domain size. `d2`, `d4`, `d8` are deterministic derivatives of
188    /// `d1` under `Radix2EvaluationDomain::new`, so they are not serialized.
189    pub domain_d1_size: u64,
190    /// Feature flags packed into a u32 bitmap. See [`FeatureFlagBits`].
191    pub feature_flags: u32,
192    /// Bitmap of which optional `ColumnEvaluations` selectors are present.
193    /// See [`OptionalSelectorBits`].
194    pub optional_selectors_present: u32,
195    /// Bitmap of which lookup selectors are present.
196    pub lookup_selectors_present: u32,
197    /// Whether the verifier_index_digest field below is populated.
198    pub has_verifier_index_digest: bool,
199    /// Group endomorphism coefficient, as 4 LE u64 limbs.
200    pub endo_limbs: [u64; 4],
201    /// Wire coordinate shifts, one per permutation column.
202    pub shift_limbs: [[u64; 4]; PERMUTS],
203    /// Optional verifier_index_digest field (populated iff
204    /// `has_verifier_index_digest`).
205    pub verifier_index_digest_limbs: [u64; 4],
206}
207
208impl ScalarHeader {
209    /// Serialized size in bytes. Must stay stable across format
210    /// `FORMAT_VERSION` revisions.
211    pub const SERIALIZED_SIZE: usize = 4   // public
212        + 4   // prev_challenges
213        + 8   // zk_rows
214        + 8   // max_poly_size
215        + 1   // disable_gates_checks
216        + 7   // padding to 8-byte boundary
217        + 8   // domain_d1_size
218        + 4   // feature_flags
219        + 4   // optional_selectors_present
220        + 4   // lookup_selectors_present
221        + 1   // has_verifier_index_digest
222        + 3   // padding
223        + 32  // endo_limbs
224        + 32 * PERMUTS // shift_limbs
225        + 32; // verifier_index_digest_limbs
226}
227
228/// Feature-flag bits packed into `ScalarHeader::feature_flags`.
229pub struct FeatureFlagBits;
230impl FeatureFlagBits {
231    pub const RANGE_CHECK_0: u32 = 1 << 0;
232    pub const RANGE_CHECK_1: u32 = 1 << 1;
233    pub const FOREIGN_FIELD_ADD: u32 = 1 << 2;
234    pub const FOREIGN_FIELD_MUL: u32 = 1 << 3;
235    pub const XOR: u32 = 1 << 4;
236    pub const ROT: u32 = 1 << 5;
237    pub const LOOKUP_PATTERN_XOR: u32 = 1 << 8;
238    pub const LOOKUP_PATTERN_LOOKUP: u32 = 1 << 9;
239    pub const LOOKUP_PATTERN_RANGE_CHECK: u32 = 1 << 10;
240    pub const LOOKUP_PATTERN_FOREIGN_FIELD_MUL: u32 = 1 << 11;
241    pub const LOOKUP_JOINT_USED: u32 = 1 << 12;
242    pub const LOOKUP_USES_RUNTIME_TABLES: u32 = 1 << 13;
243}
244
245/// Bits indicating which optional column-evaluation selectors are stored.
246pub struct OptionalSelectorBits;
247impl OptionalSelectorBits {
248    pub const RANGE_CHECK_0: u32 = 1 << 0;
249    pub const RANGE_CHECK_1: u32 = 1 << 1;
250    pub const FOREIGN_FIELD_ADD: u32 = 1 << 2;
251    pub const FOREIGN_FIELD_MUL: u32 = 1 << 3;
252    pub const XOR: u32 = 1 << 4;
253    pub const ROT: u32 = 1 << 5;
254}
255
256/// Bits indicating which optional `LookupConstraintSystem` sections are
257/// stored, packed into `ScalarHeader::lookup_selectors_present`.
258pub struct LookupSelectorBits;
259impl LookupSelectorBits {
260    pub const TABLE_IDS8: u32 = 1 << 0;
261    pub const SELECTOR_XOR: u32 = 1 << 1;
262    pub const SELECTOR_LOOKUP: u32 = 1 << 2;
263    pub const SELECTOR_RANGE_CHECK: u32 = 1 << 3;
264    pub const SELECTOR_FFMUL: u32 = 1 << 4;
265    pub const RUNTIME_SELECTOR: u32 = 1 << 5;
266    pub const RUNTIME_TABLES: u32 = 1 << 6;
267    pub const RUNTIME_TABLE_OFFSET: u32 = 1 << 7;
268}
269
270/// A gate record stripped down to the fields the prover reads at prove
271/// time: type tag and wire targets. Coefficients are deliberately omitted
272/// because they are folded into `ColumnEvaluations::coefficients8` at key
273/// generation time.
274///
275/// Wire coordinates are stored as `u32` rather than the in-memory `usize`
276/// so that the on-disk layout is identical on 32-bit and 64-bit hosts.
277#[repr(C)]
278#[derive(Clone, Copy, Debug, PartialEq, Eq)]
279pub struct PrunedGate {
280    /// Discriminant matching [`crate::circuits::gate::GateType`]. Stored as
281    /// u16 for compactness; the enum currently has < 32 variants.
282    pub typ_tag: u16,
283    /// Explicit padding to keep the layout stable across compilers.
284    _pad: [u8; 2],
285    /// Wire targets: (row, col) for each of the 7 permutation columns.
286    pub wires: [PrunedWire; PERMUTS],
287}
288
289impl PrunedGate {
290    pub const fn new(typ_tag: u16, wires: [PrunedWire; PERMUTS]) -> Self {
291        Self {
292            typ_tag,
293            _pad: [0; 2],
294            wires,
295        }
296    }
297}
298
299/// On-disk form of [`crate::circuits::wires::Wire`]: 4-byte row, 4-byte col.
300#[repr(C)]
301#[derive(Clone, Copy, Debug, PartialEq, Eq)]
302pub struct PrunedWire {
303    pub row: u32,
304    pub col: u32,
305}
306
307impl PrunedWire {
308    pub const fn new(row: u32, col: u32) -> Self {
309        Self { row, col }
310    }
311}
312
313/// One entry in the section table.
314#[derive(Clone, Copy, Debug)]
315pub struct SectionEntry {
316    pub tag: u32,
317    /// Byte offset from the start of the file.
318    pub offset: u64,
319    /// Length in bytes of the payload.
320    pub length: u64,
321    /// Domain size (number of field elements) for payload sections that are
322    /// `Evaluations<F, D<F>>`. 0 for sections that are not evaluations.
323    pub elem_domain_size: u32,
324    /// Reserved for future use; must be zero on write.
325    pub _reserved: u32,
326}
327
328impl SectionEntry {
329    pub const SERIALIZED_SIZE: usize = 4 + 8 + 8 + 4 + 4;
330}
331
332/// Errors surfaced by the cache read/write paths.
333#[derive(Debug)]
334pub enum CacheError {
335    Io(std::io::Error),
336    BadMagic {
337        found: [u8; 8],
338    },
339    UnsupportedFormatVersion {
340        found: u32,
341        supported: u32,
342    },
343    ArkFfVersionMismatch {
344        found: String,
345        expected: String,
346    },
347    IdentifierTooLong {
348        len: usize,
349        max: usize,
350    },
351    IdentifierMismatch {
352        found: String,
353        expected: String,
354    },
355    IdentifierInvalidUtf8,
356    DuplicateSectionTag {
357        tag: u32,
358    },
359    MissingSection {
360        tag: u32,
361    },
362    SectionLengthMismatch {
363        tag: u32,
364        expected: u64,
365        found: u64,
366    },
367    MisalignedSection {
368        tag: u32,
369        offset: u64,
370    },
371    TruncatedFile,
372    /// Field element count in a payload does not divide the payload length
373    /// cleanly.
374    PayloadNotFieldAligned {
375        tag: u32,
376        length: u64,
377    },
378    /// `ConstraintSystem::feature_flags` bitmap had an unknown bit set.
379    UnknownFeatureFlagBits {
380        bits: u32,
381    },
382    /// The lazily-built `LookupConstraintSystem` failed to materialise at
383    /// write time (e.g. a lookup-table id collision or an over-long table).
384    /// The wrapped string is the underlying `LookupError`'s message.
385    LookupConstraintSystem(String),
386    /// A pruned gate carried a type tag with no corresponding `GateType`.
387    UnknownGateType {
388        tag: u16,
389    },
390    /// The stored `d1` domain size could not be turned into an evaluation
391    /// domain (not a power of two, or too large for the field's 2-adicity).
392    InvalidDomainSize {
393        size: u64,
394    },
395}
396
397impl fmt::Display for CacheError {
398    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399        match self {
400            CacheError::Io(e) => write!(f, "i/o error: {e}"),
401            CacheError::BadMagic { found } => {
402                write!(f, "bad file magic: {found:?} (expected {FILE_MAGIC:?})")
403            }
404            CacheError::UnsupportedFormatVersion { found, supported } => write!(
405                f,
406                "unsupported cache format version {found} (this build supports {supported})"
407            ),
408            CacheError::ArkFfVersionMismatch { found, expected } => write!(
409                f,
410                "ark-ff version mismatch: file declares {found}, binary was built with {expected}"
411            ),
412            CacheError::IdentifierTooLong { len, max } => {
413                write!(f, "identifier length {len} exceeds maximum {max}")
414            }
415            CacheError::IdentifierMismatch { found, expected } => write!(
416                f,
417                "cache file identifier mismatch: file contains {found:?}, caller provided {expected:?}"
418            ),
419            CacheError::IdentifierInvalidUtf8 => write!(f, "cache file identifier is not valid UTF-8"),
420            CacheError::DuplicateSectionTag { tag } => {
421                write!(f, "duplicate section tag {tag:#x} in section table")
422            }
423            CacheError::MissingSection { tag } => {
424                write!(f, "required section tag {tag:#x} missing from section table")
425            }
426            CacheError::SectionLengthMismatch { tag, expected, found } => write!(
427                f,
428                "section {tag:#x} length mismatch: expected {expected}, found {found}"
429            ),
430            CacheError::MisalignedSection { tag, offset } => write!(
431                f,
432                "section {tag:#x} offset {offset} is not {SECTION_ALIGNMENT}-byte aligned"
433            ),
434            CacheError::TruncatedFile => write!(f, "cache file truncated before end of declared payload"),
435            CacheError::PayloadNotFieldAligned { tag, length } => write!(
436                f,
437                "section {tag:#x} payload length {length} is not a multiple of {FIELD_ELEMENT_BYTES}"
438            ),
439            CacheError::UnknownFeatureFlagBits { bits } => {
440                write!(f, "unknown bits {bits:#x} in feature-flags bitmap")
441            }
442            CacheError::LookupConstraintSystem(msg) => {
443                write!(f, "failed to build lookup constraint system for cache: {msg}")
444            }
445            CacheError::UnknownGateType { tag } => {
446                write!(f, "unknown gate type tag {tag} in pruned gate")
447            }
448            CacheError::InvalidDomainSize { size } => {
449                write!(f, "stored d1 domain size {size} is not a valid evaluation domain")
450            }
451        }
452    }
453}
454
455impl std::error::Error for CacheError {
456    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
457        match self {
458            CacheError::Io(e) => Some(e),
459            _ => None,
460        }
461    }
462}
463
464impl From<std::io::Error> for CacheError {
465    fn from(e: std::io::Error) -> Self {
466        CacheError::Io(e)
467    }
468}
469
470/// The ark-ff version string recorded in cache files produced by this
471/// binary. Read back at open time and compared to the reader's version;
472/// mismatched readers reject the file rather than silently corrupting.
473pub const ARK_FF_VERSION: &str = "ark-ff-0.5";
474
475/// Rounds `n` up to the nearest multiple of `SECTION_ALIGNMENT`.
476pub fn align_up(n: usize) -> usize {
477    (n + SECTION_ALIGNMENT - 1) & !(SECTION_ALIGNMENT - 1)
478}
479
480/// Padding required after a byte-run of length `n` to reach section
481/// alignment.
482pub fn alignment_padding(n: usize) -> usize {
483    align_up(n) - n
484}
485
486/// Returns the four LE u64 Montgomery-form limbs underlying `f`.
487///
488/// `ark-ff 0.5` defines `Fp<P, 4>(pub BigInt<4>, pub PhantomData<P>)` and
489/// `BigInt<4>(pub [u64; 4])`. Rust's default layout for a struct with one
490/// non-ZST field lays those out identically to `[u64; 4]`, and the
491/// `.0.0` path exposes the Montgomery limbs directly. `PrimeField`
492/// doesn't expose that path generically, so we do the reinterpretation
493/// via raw-pointer read after asserting the size and alignment invariants
494/// the cache relies on everywhere.
495///
496/// Reading Montgomery limbs is crucial for zero-copy: the on-disk bytes
497/// stored by [`write_field_slice`] must match `Fp`'s in-memory layout
498/// exactly so [`mmap_field_vec_unchecked`] can reinterpret the mapped bytes
499/// directly. If we stored the canonical form (via `into_bigint`) the
500/// mmap-backed Vec would contain values that look like canonical but
501/// the prover would treat as Montgomery — silent corruption.
502pub fn field_to_limbs<F: PrimeField>(f: &F) -> [u64; 4] {
503    debug_assert_eq!(
504        core::mem::size_of::<F>(),
505        FIELD_ELEMENT_BYTES,
506        "field_to_limbs assumes F has the memory layout of [u64; 4]"
507    );
508    debug_assert_eq!(
509        core::mem::align_of::<F>(),
510        core::mem::align_of::<[u64; 4]>(),
511        "field_to_limbs assumes F shares alignment with [u64; 4]"
512    );
513    // SAFETY: F is `Fp<MontBackend<..., 4>, 4>` in the Pasta stack, which
514    // has the exact memory layout of `[u64; 4]` (see docstring). The
515    // size + alignment debug asserts above protect against accidental
516    // instantiations with a different layout.
517    unsafe { core::ptr::read(f as *const F as *const [u64; 4]) }
518}
519
520/// Reconstructs a field element from its four LE u64 Montgomery-form
521/// limbs. Inverse of [`field_to_limbs`]; see that function's docstring
522/// for the layout assumptions.
523pub fn limbs_to_field<F: PrimeField>(limbs: &[u64; 4]) -> F {
524    debug_assert_eq!(
525        core::mem::size_of::<F>(),
526        FIELD_ELEMENT_BYTES,
527        "limbs_to_field assumes F has the memory layout of [u64; 4]"
528    );
529    debug_assert_eq!(
530        core::mem::align_of::<F>(),
531        core::mem::align_of::<[u64; 4]>(),
532        "limbs_to_field assumes F shares alignment with [u64; 4]"
533    );
534    // SAFETY: see `field_to_limbs`. `read` performs a bitwise copy; any
535    // trailing padding in F's layout (there is none for Fp<P, 4>) would
536    // be uninitialised, but since the Pasta Fp has no padding this is a
537    // full-bytes read.
538    unsafe { core::ptr::read(limbs as *const [u64; 4] as *const F) }
539}
540
541// ---------------------------------------------------------------------------
542// Binary-layout helpers
543// ---------------------------------------------------------------------------
544
545/// Byte offset (from file start) of the `num_sections` u32 in the fixed
546/// preamble. Callers outside this module shouldn't need this, but tests
547/// sometimes do.
548pub const PREAMBLE_SIZE: usize = 8   // magic
549    + 4   // format_version
550    + 4   // reserved_flags
551    + ARK_FF_VERSION_MAX_LEN
552    + 4   // identifier_len
553    + IDENTIFIER_MAX_LEN
554    + 4; // num_sections
555
556/// Total file offset at which the section table begins (immediately after
557/// the `ScalarHeader`).
558pub const SECTION_TABLE_OFFSET: usize = PREAMBLE_SIZE + ScalarHeader::SERIALIZED_SIZE;
559
560fn write_u32_le(out: &mut Vec<u8>, value: u32) {
561    out.extend_from_slice(&value.to_le_bytes());
562}
563
564fn write_u64_le(out: &mut Vec<u8>, value: u64) {
565    out.extend_from_slice(&value.to_le_bytes());
566}
567
568fn read_u32_le(bytes: &[u8]) -> Result<(u32, &[u8]), CacheError> {
569    if bytes.len() < 4 {
570        return Err(CacheError::TruncatedFile);
571    }
572    let (head, tail) = bytes.split_at(4);
573    let mut buf = [0u8; 4];
574    buf.copy_from_slice(head);
575    Ok((u32::from_le_bytes(buf), tail))
576}
577
578fn read_u64_le(bytes: &[u8]) -> Result<(u64, &[u8]), CacheError> {
579    if bytes.len() < 8 {
580        return Err(CacheError::TruncatedFile);
581    }
582    let (head, tail) = bytes.split_at(8);
583    let mut buf = [0u8; 8];
584    buf.copy_from_slice(head);
585    Ok((u64::from_le_bytes(buf), tail))
586}
587
588fn read_exact(bytes: &[u8], n: usize) -> Result<(&[u8], &[u8]), CacheError> {
589    if bytes.len() < n {
590        return Err(CacheError::TruncatedFile);
591    }
592    Ok(bytes.split_at(n))
593}
594
595/// Encodes `s` into a fixed-size zero-padded field. Errors if `s` is longer
596/// than `max`.
597fn pad_string(out: &mut Vec<u8>, s: &str, max: usize) -> Result<(), CacheError> {
598    if s.len() > max {
599        return Err(CacheError::IdentifierTooLong { len: s.len(), max });
600    }
601    out.extend_from_slice(s.as_bytes());
602    out.resize(out.len() + (max - s.len()), 0);
603    Ok(())
604}
605
606/// Reads a zero-terminated UTF-8 string from a fixed-size field.
607fn unpad_string(bytes: &[u8]) -> Result<&str, CacheError> {
608    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
609    core::str::from_utf8(&bytes[..end]).map_err(|_| CacheError::IdentifierInvalidUtf8)
610}
611
612impl ScalarHeader {
613    pub fn write(&self, out: &mut Vec<u8>) {
614        write_u32_le(out, self.public);
615        write_u32_le(out, self.prev_challenges);
616        write_u64_le(out, self.zk_rows);
617        write_u64_le(out, self.max_poly_size);
618        out.push(self.disable_gates_checks as u8);
619        // pad to 8-byte boundary
620        out.extend_from_slice(&[0u8; 7]);
621        write_u64_le(out, self.domain_d1_size);
622        write_u32_le(out, self.feature_flags);
623        write_u32_le(out, self.optional_selectors_present);
624        write_u32_le(out, self.lookup_selectors_present);
625        out.push(self.has_verifier_index_digest as u8);
626        out.extend_from_slice(&[0u8; 3]);
627        for limb in &self.endo_limbs {
628            write_u64_le(out, *limb);
629        }
630        for row in &self.shift_limbs {
631            for limb in row {
632                write_u64_le(out, *limb);
633            }
634        }
635        for limb in &self.verifier_index_digest_limbs {
636            write_u64_le(out, *limb);
637        }
638    }
639
640    pub fn read(bytes: &[u8]) -> Result<(Self, &[u8]), CacheError> {
641        let (public, bytes) = read_u32_le(bytes)?;
642        let (prev_challenges, bytes) = read_u32_le(bytes)?;
643        let (zk_rows, bytes) = read_u64_le(bytes)?;
644        let (max_poly_size, bytes) = read_u64_le(bytes)?;
645        let (disable_b, bytes) = read_exact(bytes, 1)?;
646        let disable_gates_checks = disable_b[0] != 0;
647        let (_pad, bytes) = read_exact(bytes, 7)?;
648        let (domain_d1_size, bytes) = read_u64_le(bytes)?;
649        let (feature_flags, bytes) = read_u32_le(bytes)?;
650        let (optional_selectors_present, bytes) = read_u32_le(bytes)?;
651        let (lookup_selectors_present, bytes) = read_u32_le(bytes)?;
652        let (has_vkd_b, bytes) = read_exact(bytes, 1)?;
653        let has_verifier_index_digest = has_vkd_b[0] != 0;
654        let (_pad, bytes) = read_exact(bytes, 3)?;
655        let mut endo_limbs = [0u64; 4];
656        let mut bytes = bytes;
657        for limb in endo_limbs.iter_mut() {
658            let (v, rest) = read_u64_le(bytes)?;
659            *limb = v;
660            bytes = rest;
661        }
662        let mut shift_limbs = [[0u64; 4]; PERMUTS];
663        for row in shift_limbs.iter_mut() {
664            for limb in row.iter_mut() {
665                let (v, rest) = read_u64_le(bytes)?;
666                *limb = v;
667                bytes = rest;
668            }
669        }
670        let mut verifier_index_digest_limbs = [0u64; 4];
671        for limb in verifier_index_digest_limbs.iter_mut() {
672            let (v, rest) = read_u64_le(bytes)?;
673            *limb = v;
674            bytes = rest;
675        }
676        Ok((
677            Self {
678                public,
679                prev_challenges,
680                zk_rows,
681                max_poly_size,
682                disable_gates_checks,
683                domain_d1_size,
684                feature_flags,
685                optional_selectors_present,
686                lookup_selectors_present,
687                has_verifier_index_digest,
688                endo_limbs,
689                shift_limbs,
690                verifier_index_digest_limbs,
691            },
692            bytes,
693        ))
694    }
695}
696
697impl SectionEntry {
698    pub fn write(&self, out: &mut Vec<u8>) {
699        write_u32_le(out, self.tag);
700        write_u64_le(out, self.offset);
701        write_u64_le(out, self.length);
702        write_u32_le(out, self.elem_domain_size);
703        write_u32_le(out, self._reserved);
704    }
705
706    pub fn read(bytes: &[u8]) -> Result<(Self, &[u8]), CacheError> {
707        let (tag, bytes) = read_u32_le(bytes)?;
708        let (offset, bytes) = read_u64_le(bytes)?;
709        let (length, bytes) = read_u64_le(bytes)?;
710        let (elem_domain_size, bytes) = read_u32_le(bytes)?;
711        let (_reserved, bytes) = read_u32_le(bytes)?;
712        Ok((
713            Self {
714                tag,
715                offset,
716                length,
717                elem_domain_size,
718                _reserved,
719            },
720            bytes,
721        ))
722    }
723}
724
725// ---------------------------------------------------------------------------
726// Preamble writer/reader
727// ---------------------------------------------------------------------------
728
729fn write_preamble(
730    out: &mut Vec<u8>,
731    identifier: &str,
732    num_sections: u32,
733) -> Result<(), CacheError> {
734    out.extend_from_slice(&FILE_MAGIC);
735    write_u32_le(out, FORMAT_VERSION);
736    write_u32_le(out, 0); // reserved flags
737    pad_string(out, ARK_FF_VERSION, ARK_FF_VERSION_MAX_LEN)?;
738    write_u32_le(out, identifier.len() as u32);
739    pad_string(out, identifier, IDENTIFIER_MAX_LEN)?;
740    write_u32_le(out, num_sections);
741    Ok(())
742}
743
744/// Parsed preamble data, returned to the reader along with the tail slice.
745pub struct Preamble {
746    pub format_version: u32,
747    pub ark_ff_version: String,
748    pub identifier: String,
749    pub num_sections: u32,
750}
751
752fn read_preamble(bytes: &[u8]) -> Result<(Preamble, &[u8]), CacheError> {
753    let (magic, bytes) = read_exact(bytes, 8)?;
754    let mut magic_arr = [0u8; 8];
755    magic_arr.copy_from_slice(magic);
756    if magic_arr != FILE_MAGIC {
757        return Err(CacheError::BadMagic { found: magic_arr });
758    }
759    let (format_version, bytes) = read_u32_le(bytes)?;
760    if format_version != FORMAT_VERSION {
761        return Err(CacheError::UnsupportedFormatVersion {
762            found: format_version,
763            supported: FORMAT_VERSION,
764        });
765    }
766    let (_reserved_flags, bytes) = read_u32_le(bytes)?;
767    let (ark_ff, bytes) = read_exact(bytes, ARK_FF_VERSION_MAX_LEN)?;
768    let ark_ff_version = unpad_string(ark_ff)?.to_owned();
769    if ark_ff_version != ARK_FF_VERSION {
770        return Err(CacheError::ArkFfVersionMismatch {
771            found: ark_ff_version,
772            expected: ARK_FF_VERSION.to_owned(),
773        });
774    }
775    let (id_len, bytes) = read_u32_le(bytes)?;
776    let (id_field, bytes) = read_exact(bytes, IDENTIFIER_MAX_LEN)?;
777    let id_len_usize = id_len as usize;
778    if id_len_usize > IDENTIFIER_MAX_LEN {
779        return Err(CacheError::IdentifierTooLong {
780            len: id_len_usize,
781            max: IDENTIFIER_MAX_LEN,
782        });
783    }
784    let identifier = core::str::from_utf8(&id_field[..id_len_usize])
785        .map_err(|_| CacheError::IdentifierInvalidUtf8)?
786        .to_owned();
787    let (num_sections, bytes) = read_u32_le(bytes)?;
788    Ok((
789        Preamble {
790            format_version,
791            ark_ff_version,
792            identifier,
793            num_sections,
794        },
795        bytes,
796    ))
797}
798
799// ---------------------------------------------------------------------------
800// Feature-flag bitmap round-trip
801// ---------------------------------------------------------------------------
802
803use crate::circuits::{
804    constraints::FeatureFlags,
805    lookup::lookups::{LookupFeatures, LookupPatterns},
806};
807
808fn pack_feature_flags(flags: &FeatureFlags) -> u32 {
809    let mut bits = 0u32;
810    if flags.range_check0 {
811        bits |= FeatureFlagBits::RANGE_CHECK_0;
812    }
813    if flags.range_check1 {
814        bits |= FeatureFlagBits::RANGE_CHECK_1;
815    }
816    if flags.foreign_field_add {
817        bits |= FeatureFlagBits::FOREIGN_FIELD_ADD;
818    }
819    if flags.foreign_field_mul {
820        bits |= FeatureFlagBits::FOREIGN_FIELD_MUL;
821    }
822    if flags.xor {
823        bits |= FeatureFlagBits::XOR;
824    }
825    if flags.rot {
826        bits |= FeatureFlagBits::ROT;
827    }
828    if flags.lookup_features.patterns.xor {
829        bits |= FeatureFlagBits::LOOKUP_PATTERN_XOR;
830    }
831    if flags.lookup_features.patterns.lookup {
832        bits |= FeatureFlagBits::LOOKUP_PATTERN_LOOKUP;
833    }
834    if flags.lookup_features.patterns.range_check {
835        bits |= FeatureFlagBits::LOOKUP_PATTERN_RANGE_CHECK;
836    }
837    if flags.lookup_features.patterns.foreign_field_mul {
838        bits |= FeatureFlagBits::LOOKUP_PATTERN_FOREIGN_FIELD_MUL;
839    }
840    if flags.lookup_features.joint_lookup_used {
841        bits |= FeatureFlagBits::LOOKUP_JOINT_USED;
842    }
843    if flags.lookup_features.uses_runtime_tables {
844        bits |= FeatureFlagBits::LOOKUP_USES_RUNTIME_TABLES;
845    }
846    bits
847}
848
849fn unpack_feature_flags(bits: u32) -> Result<FeatureFlags, CacheError> {
850    let known = FeatureFlagBits::RANGE_CHECK_0
851        | FeatureFlagBits::RANGE_CHECK_1
852        | FeatureFlagBits::FOREIGN_FIELD_ADD
853        | FeatureFlagBits::FOREIGN_FIELD_MUL
854        | FeatureFlagBits::XOR
855        | FeatureFlagBits::ROT
856        | FeatureFlagBits::LOOKUP_PATTERN_XOR
857        | FeatureFlagBits::LOOKUP_PATTERN_LOOKUP
858        | FeatureFlagBits::LOOKUP_PATTERN_RANGE_CHECK
859        | FeatureFlagBits::LOOKUP_PATTERN_FOREIGN_FIELD_MUL
860        | FeatureFlagBits::LOOKUP_JOINT_USED
861        | FeatureFlagBits::LOOKUP_USES_RUNTIME_TABLES;
862    let extra = bits & !known;
863    if extra != 0 {
864        return Err(CacheError::UnknownFeatureFlagBits { bits: extra });
865    }
866    Ok(FeatureFlags {
867        range_check0: bits & FeatureFlagBits::RANGE_CHECK_0 != 0,
868        range_check1: bits & FeatureFlagBits::RANGE_CHECK_1 != 0,
869        foreign_field_add: bits & FeatureFlagBits::FOREIGN_FIELD_ADD != 0,
870        foreign_field_mul: bits & FeatureFlagBits::FOREIGN_FIELD_MUL != 0,
871        xor: bits & FeatureFlagBits::XOR != 0,
872        rot: bits & FeatureFlagBits::ROT != 0,
873        lookup_features: LookupFeatures {
874            patterns: LookupPatterns {
875                xor: bits & FeatureFlagBits::LOOKUP_PATTERN_XOR != 0,
876                lookup: bits & FeatureFlagBits::LOOKUP_PATTERN_LOOKUP != 0,
877                range_check: bits & FeatureFlagBits::LOOKUP_PATTERN_RANGE_CHECK != 0,
878                foreign_field_mul: bits & FeatureFlagBits::LOOKUP_PATTERN_FOREIGN_FIELD_MUL != 0,
879            },
880            joint_lookup_used: bits & FeatureFlagBits::LOOKUP_JOINT_USED != 0,
881            uses_runtime_tables: bits & FeatureFlagBits::LOOKUP_USES_RUNTIME_TABLES != 0,
882        },
883    })
884}
885
886// ---------------------------------------------------------------------------
887// Field-element array (de)serialization
888// ---------------------------------------------------------------------------
889
890/// Serializes a slice of field elements as contiguous little-endian u64
891/// limbs. Output length is `elements.len() * FIELD_ELEMENT_BYTES`.
892fn write_field_slice<F: PrimeField>(out: &mut Vec<u8>, elements: &[F]) {
893    for elt in elements {
894        let limbs = field_to_limbs(elt);
895        for limb in &limbs {
896            out.extend_from_slice(&limb.to_le_bytes());
897        }
898    }
899}
900
901/// Validates that `bytes` holds exactly `count` field elements, i.e. that
902/// `bytes.len() == count * FIELD_ELEMENT_BYTES`. Purely a check — constructs
903/// nothing — so it is safe to call in the reader's fallible validation phase
904/// before any mmap-backed `Vec` exists. `tag` is only used for the error.
905fn validate_field_section(tag: u32, bytes: &[u8], count: usize) -> Result<(), CacheError> {
906    let expected = count
907        .checked_mul(FIELD_ELEMENT_BYTES)
908        .ok_or(CacheError::TruncatedFile)?;
909    if bytes.len() != expected {
910        return Err(CacheError::PayloadNotFieldAligned {
911            tag,
912            length: bytes.len() as u64,
913        });
914    }
915    Ok(())
916}
917
918/// Zero-copy: constructs a `Vec<F>` whose backing storage is the mmap
919/// region itself, via `Vec::from_raw_parts(mmap_ptr, len, len)`.
920///
921/// This is **infallible by design**: it performs no validation and simply
922/// reinterprets the bytes. All length/alignment validation must be done up
923/// front (see [`validate_field_section`]) so that this constructor — and
924/// therefore the first live mmap-backed `Vec` — is only ever reached once the
925/// entire file is known to be well-formed. That ordering is what makes the
926/// reader panic-free: an `Err` returned *after* one of these Vecs existed
927/// would drop it, calling the global allocator on mmap memory (undefined
928/// behaviour, observed as a `free(): invalid pointer` abort).
929///
930/// # Safety
931///
932/// - `bytes.len()` **must** equal `count * FIELD_ELEMENT_BYTES` (caller
933///   guarantees this via [`validate_field_section`]).
934/// - The returned `Vec<F>` **must never be dropped normally** and **must
935///   never grow**: both would call `dealloc`/`realloc` on the mmap pointer.
936///   Callers keep it inside a [`core::mem::ManuallyDrop`] container (see
937///   [`MmapProverIndex`]) so `Vec::drop` never runs.
938/// - `bytes` must be aligned for `F` (8-byte alignment suffices for
939///   `BigInt<4>`; the format's 32-byte section alignment covers this) and the
940///   underlying mmap must outlive the returned `Vec<F>`.
941unsafe fn mmap_field_vec_unchecked<F: PrimeField>(bytes: &[u8], count: usize) -> Vec<F> {
942    debug_assert_eq!(bytes.len(), count * FIELD_ELEMENT_BYTES);
943    // Alignment check: `BigInt<4>` = `[u64; 4]` needs 8-byte alignment.
944    // Section offsets are 32-byte aligned in the cache format so this
945    // should always hold; assert it defensively.
946    debug_assert!(
947        (bytes.as_ptr() as usize).is_multiple_of(core::mem::align_of::<F>()),
948        "mmap section pointer not aligned for F"
949    );
950    let ptr = bytes.as_ptr() as *mut F;
951    // SAFETY: the caller contract forbids dropping or growing the
952    // returned Vec; its lifetime must be bounded by the mmap's.
953    Vec::from_raw_parts(ptr, count, count)
954}
955
956// ---------------------------------------------------------------------------
957// GateType discriminant round-trip
958// ---------------------------------------------------------------------------
959
960use crate::circuits::{
961    gate::{CircuitGate, GateType},
962    wires::{GateWires, Wire},
963};
964
965fn gate_type_to_tag(t: GateType) -> u16 {
966    // Matches the order declared in circuits/gate.rs. Kept in sync manually
967    // so that ordering changes to GateType require a deliberate update here
968    // (and, ideally, a FORMAT_VERSION bump).
969    match t {
970        GateType::Zero => 0,
971        GateType::Generic => 1,
972        GateType::Poseidon => 2,
973        GateType::CompleteAdd => 3,
974        GateType::VarBaseMul => 4,
975        GateType::EndoMul => 5,
976        GateType::EndoMulScalar => 6,
977        GateType::Lookup => 7,
978        GateType::RangeCheck0 => 8,
979        GateType::RangeCheck1 => 9,
980        GateType::ForeignFieldAdd => 10,
981        GateType::ForeignFieldMul => 11,
982        GateType::Xor16 => 12,
983        GateType::Rot64 => 13,
984    }
985}
986
987fn gate_type_from_tag(t: u16) -> Option<GateType> {
988    Some(match t {
989        0 => GateType::Zero,
990        1 => GateType::Generic,
991        2 => GateType::Poseidon,
992        3 => GateType::CompleteAdd,
993        4 => GateType::VarBaseMul,
994        5 => GateType::EndoMul,
995        6 => GateType::EndoMulScalar,
996        7 => GateType::Lookup,
997        8 => GateType::RangeCheck0,
998        9 => GateType::RangeCheck1,
999        10 => GateType::ForeignFieldAdd,
1000        11 => GateType::ForeignFieldMul,
1001        12 => GateType::Xor16,
1002        13 => GateType::Rot64,
1003        _ => return None,
1004    })
1005}
1006
1007fn write_pruned_gate(out: &mut Vec<u8>, gate: &CircuitGate<impl PrimeField>) {
1008    let typ_tag = gate_type_to_tag(gate.typ);
1009    out.extend_from_slice(&typ_tag.to_le_bytes());
1010    out.extend_from_slice(&[0u8; 2]);
1011    for wire in &gate.wires {
1012        out.extend_from_slice(&(wire.row as u32).to_le_bytes());
1013        out.extend_from_slice(&(wire.col as u32).to_le_bytes());
1014    }
1015}
1016
1017/// Byte size of one pruned gate record on disk. Must match
1018/// `write_pruned_gate` exactly.
1019pub const PRUNED_GATE_SIZE: usize = 2 + 2 + 2 * 4 * PERMUTS;
1020
1021fn read_pruned_gate<F: PrimeField>(bytes: &[u8]) -> Result<CircuitGate<F>, CacheError> {
1022    if bytes.len() != PRUNED_GATE_SIZE {
1023        return Err(CacheError::TruncatedFile);
1024    }
1025    let mut typ_bytes = [0u8; 2];
1026    typ_bytes.copy_from_slice(&bytes[0..2]);
1027    let typ_tag = u16::from_le_bytes(typ_bytes);
1028    let typ = gate_type_from_tag(typ_tag).ok_or(CacheError::UnknownGateType { tag: typ_tag })?;
1029    let mut wires: GateWires = [Wire::default(); PERMUTS];
1030    for (i, wire) in wires.iter_mut().enumerate() {
1031        let base = 4 + i * 8;
1032        let mut row_bytes = [0u8; 4];
1033        let mut col_bytes = [0u8; 4];
1034        row_bytes.copy_from_slice(&bytes[base..base + 4]);
1035        col_bytes.copy_from_slice(&bytes[base + 4..base + 8]);
1036        *wire = Wire {
1037            row: u32::from_le_bytes(row_bytes) as usize,
1038            col: u32::from_le_bytes(col_bytes) as usize,
1039        };
1040    }
1041    Ok(CircuitGate::new(typ, wires, Vec::new()))
1042}
1043
1044// ---------------------------------------------------------------------------
1045// write_cache / read_cache
1046// ---------------------------------------------------------------------------
1047
1048use crate::{
1049    circuits::{
1050        constraints::{ColumnEvaluations, ConstraintSystem},
1051        domains::EvaluationDomains,
1052        lookup::{
1053            constraints::LookupConfiguration,
1054            index::{LookupConstraintSystem, LookupSelectors},
1055            lookups::LookupInfo,
1056            runtime_tables::RuntimeTableSpec,
1057        },
1058        wires::COLUMNS,
1059    },
1060    curve::KimchiCurve,
1061    linearization::expr_linearization,
1062    o1_utils::lazy_cache::LazyCache,
1063    prover_index::ProverIndex,
1064};
1065use ark_poly::{EvaluationDomain, Evaluations, Radix2EvaluationDomain};
1066use poly_commitment::SRS;
1067use std::{fs::OpenOptions, io::Write as _, os::unix::io::AsRawFd, path::Path, sync::Arc};
1068
1069/// Minimal read-only `MAP_SHARED` mmap wrapper built on libc. Keeps the
1070/// feature flag self-contained (no external `memmap2` crate required in
1071/// the kimchi-stubs vendored registry) and matches the subset of behavior
1072/// we need: open file, map read-only, munmap on drop.
1073struct ReadOnlyMmap {
1074    ptr: *const u8,
1075    len: usize,
1076}
1077
1078// Safety: the mapping is read-only and the target is never shared across
1079// threads without external synchronization. We don't hand out `&mut`
1080// references into it, so Send + Sync match what the Rust stdlib gives for
1081// `&[u8]`.
1082unsafe impl Send for ReadOnlyMmap {}
1083unsafe impl Sync for ReadOnlyMmap {}
1084
1085impl ReadOnlyMmap {
1086    fn map_file(file: &std::fs::File) -> std::io::Result<Self> {
1087        let len = file.metadata()?.len() as usize;
1088        if len == 0 {
1089            return Ok(Self {
1090                ptr: core::ptr::NonNull::<u8>::dangling().as_ptr(),
1091                len: 0,
1092            });
1093        }
1094        // Safety: we pass a null addr (kernel chooses), a valid fd, and
1095        // check the return value against MAP_FAILED.
1096        let ptr = unsafe {
1097            libc::mmap(
1098                core::ptr::null_mut(),
1099                len,
1100                libc::PROT_READ,
1101                libc::MAP_SHARED,
1102                file.as_raw_fd(),
1103                0,
1104            )
1105        };
1106        if ptr == libc::MAP_FAILED {
1107            return Err(std::io::Error::last_os_error());
1108        }
1109        Ok(Self {
1110            ptr: ptr as *const u8,
1111            len,
1112        })
1113    }
1114
1115    fn as_slice(&self) -> &[u8] {
1116        if self.len == 0 {
1117            &[]
1118        } else {
1119            // Safety: `ptr` + `len` were produced by a successful mmap and
1120            // the region is read-only. The lifetime is tied to `&self`.
1121            unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
1122        }
1123    }
1124}
1125
1126impl Drop for ReadOnlyMmap {
1127    fn drop(&mut self) {
1128        if self.len != 0 {
1129            // Safety: matched pair with the mmap call; ignore errors (we
1130            // can't recover from a munmap failure in Drop).
1131            unsafe {
1132                libc::munmap(self.ptr as *mut libc::c_void, self.len);
1133            }
1134        }
1135    }
1136}
1137
1138impl<const FULL_ROUNDS: usize, G, Srs> MmapProverIndex<FULL_ROUNDS, G, Srs>
1139where
1140    G: KimchiCurve<FULL_ROUNDS>,
1141{
1142    /// Advise the kernel that the mapped cache file's pages are no longer
1143    /// needed (`MADV_DONTNEED`). Reads into the mapping after this call
1144    /// will trigger fresh page faults that re-populate from disk.
1145    ///
1146    /// This simulates the real-world behaviour the zero-copy cache was
1147    /// designed for — pages evicted under memory pressure are silently
1148    /// re-faulted as the prover walks back through them. Used in tests
1149    /// (see `cached_index_prove_after_madv_dontneed`) to confirm that the
1150    /// construction doesn't somehow keep the data pinned in RAM via a
1151    /// stray owned copy.
1152    ///
1153    /// No-op on a zero-length mapping.
1154    pub fn madvise_dontneed(&self) {
1155        if self._mmap.len == 0 {
1156            return;
1157        }
1158        // Safety: matched with the live mmap this wrapper owns; MADV_DONTNEED
1159        // on a MAP_SHARED read-only region just drops the resident pages
1160        // (next access faults them back in from the file). Errors here are
1161        // advisory — if the kernel rejects the hint we simply don't get
1162        // the eviction we asked for; nothing breaks.
1163        unsafe {
1164            libc::madvise(
1165                self._mmap.ptr as *mut libc::c_void,
1166                self._mmap.len,
1167                libc::MADV_DONTNEED,
1168            );
1169        }
1170    }
1171}
1172
1173/// A [`ProverIndex`] whose large `Vec<F>` fields are backed by memory in a
1174/// live `mmap(2)` region instead of owned heap allocations.
1175///
1176/// The wrapper [`Deref`]s to `ProverIndex`, so anywhere the existing prover
1177/// takes `&ProverIndex` (e.g. `ProverProof::create`) it transparently
1178/// accepts an `MmapProverIndex` as well. The file pages can be evicted by
1179/// the kernel under memory pressure and re-faulted on demand.
1180///
1181/// # Lifetime and drop
1182///
1183/// The inner `ProverIndex` is stored in a [`ManuallyDrop`] because its
1184/// `Vec<F>` fields (sid; every `Evaluations.evals`; lookup data when
1185/// present) are constructed via `Vec::from_raw_parts(mmap_ptr, len, len)`
1186/// and point into the mapping. Running `Vec::drop` on them would call
1187/// `dealloc` on mmap memory, which is undefined behaviour.
1188///
1189/// Consequently, when `MmapProverIndex` is dropped, the inner
1190/// `ProverIndex`'s owned sub-allocations (the owned `gates` vector,
1191/// `linearization`, `powers_of_alpha`, and any LazyCache/Arc machinery)
1192/// leak. This notably includes the **`Arc<Srs>` strong count**: it is never
1193/// decremented, so the SRS is never freed for the life of the process even
1194/// if the caller drops its own clone. The lazily-recomputed
1195/// `precomputations` (the d4/d8 `DomainConstantEvaluations`, tens of MB) are
1196/// likewise owned heap allocations that leak and are *not* reclaimed by
1197/// `munmap`. This is acceptable for Mina's usage pattern — proving keys are
1198/// loaded once at daemon startup and held for the life of the process, so
1199/// cumulative leakage is bounded and the OS reclaims everything on exit. The
1200/// `Arc<ReadOnlyMmap>` held alongside is dropped normally, which calls
1201/// `munmap` and releases the mmap-backed field arrays (the bulk of the
1202/// on-disk key), but not the recomputed/owned allocations above.
1203///
1204/// A future refinement could replace the bulk leak with a manual per-
1205/// field tear-down: pattern-destructure the `ProverIndex`, drop the
1206/// owned fields explicitly, and `mem::forget` only the mmap-backed
1207/// Vecs. That's about 50 lines of `unsafe` and can be added without
1208/// affecting the on-disk format or the public API.
1209pub struct MmapProverIndex<const FULL_ROUNDS: usize, G: KimchiCurve<FULL_ROUNDS>, Srs> {
1210    index: core::mem::ManuallyDrop<ProverIndex<FULL_ROUNDS, G, Srs>>,
1211    _mmap: Arc<ReadOnlyMmap>,
1212}
1213
1214impl<const FULL_ROUNDS: usize, G, Srs> core::ops::Deref for MmapProverIndex<FULL_ROUNDS, G, Srs>
1215where
1216    G: KimchiCurve<FULL_ROUNDS>,
1217{
1218    type Target = ProverIndex<FULL_ROUNDS, G, Srs>;
1219    fn deref(&self) -> &Self::Target {
1220        &self.index
1221    }
1222}
1223
1224impl<const FULL_ROUNDS: usize, G, Srs> core::fmt::Debug for MmapProverIndex<FULL_ROUNDS, G, Srs>
1225where
1226    G: KimchiCurve<FULL_ROUNDS>,
1227{
1228    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1229        f.debug_struct("MmapProverIndex")
1230            .field("mmap_len", &self._mmap.len)
1231            .finish_non_exhaustive()
1232    }
1233}
1234
1235// The wrapper's thread-safety mirrors the wrapped ProverIndex. The mmap
1236// is read-only, so aliased reads from multiple threads are fine.
1237unsafe impl<const FULL_ROUNDS: usize, G, Srs> Send for MmapProverIndex<FULL_ROUNDS, G, Srs>
1238where
1239    G: KimchiCurve<FULL_ROUNDS>,
1240    ProverIndex<FULL_ROUNDS, G, Srs>: Send,
1241{
1242}
1243unsafe impl<const FULL_ROUNDS: usize, G, Srs> Sync for MmapProverIndex<FULL_ROUNDS, G, Srs>
1244where
1245    G: KimchiCurve<FULL_ROUNDS>,
1246    ProverIndex<FULL_ROUNDS, G, Srs>: Sync,
1247{
1248}
1249
1250/// Context tracked during a cache-write to record sections in parallel with
1251/// the growing payload buffer.
1252struct WriteContext {
1253    payload: Vec<u8>,
1254    sections: Vec<SectionEntry>,
1255    /// File offset where payload section bytes will start. Set once the
1256    /// preamble + ScalarHeader + section table size is known.
1257    payload_base: u64,
1258}
1259
1260impl WriteContext {
1261    fn new() -> Self {
1262        Self {
1263            payload: Vec::new(),
1264            sections: Vec::new(),
1265            payload_base: 0,
1266        }
1267    }
1268
1269    /// Append a field-slice payload, record a `SectionEntry`, and apply
1270    /// trailing alignment padding so the next section starts 32-byte
1271    /// aligned.
1272    fn push_field_section<F: PrimeField>(&mut self, tag: u32, domain_size: u32, data: &[F]) {
1273        let section_offset = self.payload_base + self.payload.len() as u64;
1274        let start = self.payload.len();
1275        write_field_slice(&mut self.payload, data);
1276        let length = (self.payload.len() - start) as u64;
1277        self.sections.push(SectionEntry {
1278            tag,
1279            offset: section_offset,
1280            length,
1281            elem_domain_size: domain_size,
1282            _reserved: 0,
1283        });
1284        let pad = alignment_padding(self.payload.len());
1285        self.payload.extend(std::iter::repeat_n(0u8, pad));
1286    }
1287
1288    fn push_raw_section(&mut self, tag: u32, payload: &[u8]) {
1289        self.push_raw_section_with_elem_count(tag, 0, payload);
1290    }
1291
1292    /// Like [`push_raw_section`] but records a caller-supplied value in the
1293    /// section-table entry's `elem_domain_size`. Used by `lookup_table8`,
1294    /// which encodes the count of inner evaluation arrays there rather
1295    /// than inline in the payload (inline would break the 32-byte
1296    /// alignment required for zero-copy field-element reads).
1297    fn push_raw_section_with_elem_count(
1298        &mut self,
1299        tag: u32,
1300        elem_domain_size: u32,
1301        payload: &[u8],
1302    ) {
1303        let section_offset = self.payload_base + self.payload.len() as u64;
1304        self.payload.extend_from_slice(payload);
1305        let length = payload.len() as u64;
1306        self.sections.push(SectionEntry {
1307            tag,
1308            offset: section_offset,
1309            length,
1310            elem_domain_size,
1311            _reserved: 0,
1312        });
1313        let pad = alignment_padding(self.payload.len());
1314        self.payload.extend(std::iter::repeat_n(0u8, pad));
1315    }
1316}
1317
1318/// Computes the total bytes consumed by the fixed preamble + `ScalarHeader`
1319/// + section-table-for-`n`-sections + trailing alignment pad.
1320fn fixed_region_size(num_sections: usize) -> usize {
1321    let raw = PREAMBLE_SIZE
1322        + ScalarHeader::SERIALIZED_SIZE
1323        + num_sections * SectionEntry::SERIALIZED_SIZE;
1324    align_up(raw)
1325}
1326
1327/// Writes a proving index to `path` in the mmap cache format.
1328///
1329/// The `identifier` is stored verbatim (bounded at `IDENTIFIER_MAX_LEN`
1330/// bytes) and must be supplied again on read for validation. Callers
1331/// typically pass a hash of the circuit's identifying key.
1332///
1333/// Writes are atomic: the file is staged at a unique per-writer temp path
1334/// (`path.tmp.<pid>.<n>`) and renamed into place, so concurrent readers of an
1335/// existing `path` see either the old or new content but never a half-written
1336/// file, and two concurrent writers cannot corrupt each other's staging file.
1337pub fn write_cache<const FULL_ROUNDS: usize, G, Srs>(
1338    identifier: &str,
1339    index: &ProverIndex<FULL_ROUNDS, G, Srs>,
1340    path: &Path,
1341) -> Result<(), CacheError>
1342where
1343    G: KimchiCurve<FULL_ROUNDS>,
1344    Srs: SRS<G>,
1345    G::BaseField: PrimeField,
1346{
1347    if identifier.len() > IDENTIFIER_MAX_LEN {
1348        return Err(CacheError::IdentifierTooLong {
1349            len: identifier.len(),
1350            max: IDENTIFIER_MAX_LEN,
1351        });
1352    }
1353
1354    let cs = &index.cs;
1355    // Grab the materialised LookupConstraintSystem (if any) once, up front.
1356    // A `Some(_)` here triggers emission of sections 0x50..0x58; `None` or a
1357    // `LookupError` leaves them out and keeps the file backward-compatible
1358    // with non-lookup readers.
1359    let lcs_result = cs.lookup_constraint_system.get();
1360    let lcs: Option<&LookupConstraintSystem<G::ScalarField>> = match lcs_result {
1361        Ok(opt) => opt.as_ref(),
1362        // A lazily-built lookup system that failed to materialise must abort
1363        // the write. Mapping it to `None` (the previous behaviour) would emit
1364        // a lookup-free cache file for a lookup circuit, silently converting a
1365        // hard prover error into a wrong proving key on the next read.
1366        Err(e) => return Err(CacheError::LookupConstraintSystem(e.to_string())),
1367    };
1368
1369    // Build scalar header.
1370    let (vkd_limbs, has_vkd) = match index.verifier_index_digest.as_ref() {
1371        Some(digest) => (field_to_limbs(digest), true),
1372        None => ([0u64; 4], false),
1373    };
1374
1375    let shift_limbs: [[u64; 4]; PERMUTS] = {
1376        let mut arr = [[0u64; 4]; PERMUTS];
1377        for (i, s) in cs.shift.iter().enumerate() {
1378            arr[i] = field_to_limbs(s);
1379        }
1380        arr
1381    };
1382
1383    let column_evaluations = index.column_evaluations.get();
1384    let optional_selectors_present = {
1385        let mut bits = 0u32;
1386        if column_evaluations.range_check0_selector8.is_some() {
1387            bits |= OptionalSelectorBits::RANGE_CHECK_0;
1388        }
1389        if column_evaluations.range_check1_selector8.is_some() {
1390            bits |= OptionalSelectorBits::RANGE_CHECK_1;
1391        }
1392        if column_evaluations.foreign_field_add_selector8.is_some() {
1393            bits |= OptionalSelectorBits::FOREIGN_FIELD_ADD;
1394        }
1395        if column_evaluations.foreign_field_mul_selector8.is_some() {
1396            bits |= OptionalSelectorBits::FOREIGN_FIELD_MUL;
1397        }
1398        if column_evaluations.xor_selector8.is_some() {
1399            bits |= OptionalSelectorBits::XOR;
1400        }
1401        if column_evaluations.rot_selector8.is_some() {
1402            bits |= OptionalSelectorBits::ROT;
1403        }
1404        bits
1405    };
1406
1407    // Compute the lookup-presence bitmap up front so we can stamp it into
1408    // the header; the actual section payloads are emitted further below.
1409    let lookup_selectors_present = match lcs {
1410        None => 0,
1411        Some(lcs) => {
1412            let mut bits = 0u32;
1413            if lcs.table_ids8.is_some() {
1414                bits |= LookupSelectorBits::TABLE_IDS8;
1415            }
1416            if lcs.lookup_selectors.xor.is_some() {
1417                bits |= LookupSelectorBits::SELECTOR_XOR;
1418            }
1419            if lcs.lookup_selectors.lookup.is_some() {
1420                bits |= LookupSelectorBits::SELECTOR_LOOKUP;
1421            }
1422            if lcs.lookup_selectors.range_check.is_some() {
1423                bits |= LookupSelectorBits::SELECTOR_RANGE_CHECK;
1424            }
1425            if lcs.lookup_selectors.ffmul.is_some() {
1426                bits |= LookupSelectorBits::SELECTOR_FFMUL;
1427            }
1428            if lcs.runtime_selector.is_some() {
1429                bits |= LookupSelectorBits::RUNTIME_SELECTOR;
1430            }
1431            if lcs.runtime_tables.is_some() {
1432                bits |= LookupSelectorBits::RUNTIME_TABLES;
1433            }
1434            if lcs.runtime_table_offset.is_some() {
1435                bits |= LookupSelectorBits::RUNTIME_TABLE_OFFSET;
1436            }
1437            bits
1438        }
1439    };
1440
1441    let header = ScalarHeader {
1442        public: cs.public as u32,
1443        prev_challenges: cs.prev_challenges as u32,
1444        zk_rows: cs.zk_rows,
1445        max_poly_size: index.max_poly_size as u64,
1446        disable_gates_checks: cs.disable_gates_checks,
1447        domain_d1_size: cs.domain.d1.size() as u64,
1448        feature_flags: pack_feature_flags(&cs.feature_flags),
1449        optional_selectors_present,
1450        lookup_selectors_present,
1451        has_verifier_index_digest: has_vkd,
1452        endo_limbs: field_to_limbs(&cs.endo),
1453        shift_limbs,
1454        verifier_index_digest_limbs: vkd_limbs,
1455    };
1456
1457    // Assemble payload in an offset-agnostic way. The section table offsets
1458    // are computed after we know the number of sections (they all live in
1459    // the fixed region that precedes payload_base).
1460    let mut ctx = WriteContext::new();
1461
1462    // sid as a field-slice section (elem_domain_size = sid length).
1463    ctx.push_field_section::<G::ScalarField>(SectionTag::Sid as u32, cs.sid.len() as u32, &cs.sid);
1464
1465    // Pruned gates.
1466    let mut gates_bytes = Vec::with_capacity(cs.gates.len() * PRUNED_GATE_SIZE);
1467    for gate in cs.gates.iter() {
1468        write_pruned_gate(&mut gates_bytes, gate);
1469    }
1470    ctx.push_raw_section(SectionTag::Gates as u32, &gates_bytes);
1471
1472    // Gate coefficients (see `SectionTag::GateCoeffs`). Encoded per gate as a
1473    // u32 count followed by that many field elements.
1474    let mut coeffs_bytes = Vec::new();
1475    for gate in cs.gates.iter() {
1476        write_u32_le(&mut coeffs_bytes, gate.coeffs.len() as u32);
1477        write_field_slice(&mut coeffs_bytes, &gate.coeffs);
1478    }
1479    ctx.push_raw_section(SectionTag::GateCoeffs as u32, &coeffs_bytes);
1480
1481    // Column evaluations: mandatory arrays.
1482    let d4_size = cs.domain.d4.size() as u32;
1483    let d8_size = cs.domain.d8.size() as u32;
1484    for (i, e) in column_evaluations.coefficients8.iter().enumerate() {
1485        ctx.push_field_section::<G::ScalarField>(coefficient_tag(i), d8_size, &e.evals);
1486    }
1487    for (i, e) in column_evaluations
1488        .permutation_coefficients8
1489        .iter()
1490        .enumerate()
1491    {
1492        ctx.push_field_section::<G::ScalarField>(permutation_coefficient_tag(i), d8_size, &e.evals);
1493    }
1494    ctx.push_field_section::<G::ScalarField>(
1495        SectionTag::GenericSelector4 as u32,
1496        d4_size,
1497        &column_evaluations.generic_selector4.evals,
1498    );
1499    ctx.push_field_section::<G::ScalarField>(
1500        SectionTag::PoseidonSelector8 as u32,
1501        d8_size,
1502        &column_evaluations.poseidon_selector8.evals,
1503    );
1504    ctx.push_field_section::<G::ScalarField>(
1505        SectionTag::CompleteAddSelector4 as u32,
1506        d4_size,
1507        &column_evaluations.complete_add_selector4.evals,
1508    );
1509    ctx.push_field_section::<G::ScalarField>(
1510        SectionTag::MulSelector8 as u32,
1511        d8_size,
1512        &column_evaluations.mul_selector8.evals,
1513    );
1514    ctx.push_field_section::<G::ScalarField>(
1515        SectionTag::EmulSelector8 as u32,
1516        d8_size,
1517        &column_evaluations.emul_selector8.evals,
1518    );
1519    ctx.push_field_section::<G::ScalarField>(
1520        SectionTag::EndomulScalarSelector8 as u32,
1521        d8_size,
1522        &column_evaluations.endomul_scalar_selector8.evals,
1523    );
1524    // Column evaluations: optional arrays.
1525    let push_optional =
1526        |ctx: &mut WriteContext, tag, opt: &Option<Evaluations<G::ScalarField, _>>| {
1527            if let Some(e) = opt {
1528                ctx.push_field_section::<G::ScalarField>(tag, d8_size, &e.evals);
1529            }
1530        };
1531    push_optional(
1532        &mut ctx,
1533        SectionTag::RangeCheck0Selector8 as u32,
1534        &column_evaluations.range_check0_selector8,
1535    );
1536    push_optional(
1537        &mut ctx,
1538        SectionTag::RangeCheck1Selector8 as u32,
1539        &column_evaluations.range_check1_selector8,
1540    );
1541    push_optional(
1542        &mut ctx,
1543        SectionTag::ForeignFieldAddSelector8 as u32,
1544        &column_evaluations.foreign_field_add_selector8,
1545    );
1546    push_optional(
1547        &mut ctx,
1548        SectionTag::ForeignFieldMulSelector8 as u32,
1549        &column_evaluations.foreign_field_mul_selector8,
1550    );
1551    push_optional(
1552        &mut ctx,
1553        SectionTag::XorSelector8 as u32,
1554        &column_evaluations.xor_selector8,
1555    );
1556    push_optional(
1557        &mut ctx,
1558        SectionTag::RotSelector8 as u32,
1559        &column_evaluations.rot_selector8,
1560    );
1561
1562    // Lookup sections, only emitted when a LookupConstraintSystem was
1563    // materialised on the constraint system. The reader gates its lookup
1564    // reconstruction on the same `lookup_selectors_present` bitmap that
1565    // was just stamped into the header, so leaving any of these out when
1566    // lcs is None is correct — the reader will skip them.
1567    if let Some(lcs) = lcs {
1568        // lookup_table8: one section containing the concatenated inner
1569        // arrays, each d8-sized. The count of inner arrays is stored in
1570        // the section-table entry's `elem_domain_size`; the payload
1571        // itself is pure field data so it stays 32-byte aligned for
1572        // zero-copy slice construction at read time.
1573        {
1574            let n = lcs.lookup_table8.len() as u32;
1575            let inner_len = d8_size as usize * FIELD_ELEMENT_BYTES;
1576            let mut bytes = Vec::with_capacity((n as usize) * inner_len);
1577            for e in lcs.lookup_table8.iter() {
1578                write_field_slice(&mut bytes, &e.evals);
1579            }
1580            ctx.push_raw_section_with_elem_count(SectionTag::LookupTable8 as u32, n, &bytes);
1581        }
1582        if let Some(e) = &lcs.table_ids8 {
1583            ctx.push_field_section::<G::ScalarField>(
1584                SectionTag::TableIds8 as u32,
1585                d8_size,
1586                &e.evals,
1587            );
1588        }
1589        push_optional(
1590            &mut ctx,
1591            SectionTag::LookupSelectorXor as u32,
1592            &lcs.lookup_selectors.xor,
1593        );
1594        push_optional(
1595            &mut ctx,
1596            SectionTag::LookupSelectorLookup as u32,
1597            &lcs.lookup_selectors.lookup,
1598        );
1599        push_optional(
1600            &mut ctx,
1601            SectionTag::LookupSelectorRangeCheck as u32,
1602            &lcs.lookup_selectors.range_check,
1603        );
1604        push_optional(
1605            &mut ctx,
1606            SectionTag::LookupSelectorFfmul as u32,
1607            &lcs.lookup_selectors.ffmul,
1608        );
1609        push_optional(
1610            &mut ctx,
1611            SectionTag::RuntimeSelector8 as u32,
1612            &lcs.runtime_selector,
1613        );
1614        if let Some(rts) = &lcs.runtime_tables {
1615            // Encoding: u32 count, then count × (id: i32 LE, len: u32 LE).
1616            // RuntimeTableSpec.len is usize in memory; we truncate to u32,
1617            // which is ample given real circuit sizes. The reader widens
1618            // it back to usize during reconstruction.
1619            let mut bytes = Vec::with_capacity(4 + rts.len() * 8);
1620            bytes.extend_from_slice(&(rts.len() as u32).to_le_bytes());
1621            for spec in rts {
1622                bytes.extend_from_slice(&spec.id.to_le_bytes());
1623                bytes.extend_from_slice(&(spec.len as u32).to_le_bytes());
1624            }
1625            ctx.push_raw_section(SectionTag::RuntimeTablesSpec as u32, &bytes);
1626        }
1627        if let Some(off) = lcs.runtime_table_offset {
1628            let bytes = (off as u64).to_le_bytes();
1629            ctx.push_raw_section(SectionTag::RuntimeTableOffset as u32, &bytes);
1630        }
1631    }
1632
1633    // Once we know how many sections there are, we can compute the file
1634    // layout and patch section offsets to absolute file positions.
1635    let num_sections = ctx.sections.len();
1636    let payload_base = fixed_region_size(num_sections) as u64;
1637    for s in ctx.sections.iter_mut() {
1638        s.offset += payload_base;
1639    }
1640    ctx.payload_base = payload_base;
1641
1642    // Build the file bytes: preamble + header + section table + pad to 32
1643    // + payload.
1644    let mut file = Vec::with_capacity(payload_base as usize + ctx.payload.len());
1645    write_preamble(&mut file, identifier, num_sections as u32)?;
1646    header.write(&mut file);
1647    for s in &ctx.sections {
1648        s.write(&mut file);
1649    }
1650    let fixed_region_end = PREAMBLE_SIZE
1651        + ScalarHeader::SERIALIZED_SIZE
1652        + num_sections * SectionEntry::SERIALIZED_SIZE;
1653    let pad = align_up(fixed_region_end) - fixed_region_end;
1654    file.extend(std::iter::repeat_n(0u8, pad));
1655    debug_assert_eq!(file.len() as u64, payload_base);
1656    file.extend_from_slice(&ctx.payload);
1657
1658    // Atomic write: write to a per-writer temp file, fsync, rename into place.
1659    // The temp name is unique (pid + process-local counter) so two concurrent
1660    // writers of the same `path` never share a staging file — a fixed
1661    // `path.tmp` would let one writer's truncate/rename corrupt the other's.
1662    let tmp_path = {
1663        static TMP_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1664        let uniq = TMP_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1665        let mut p = path.to_path_buf();
1666        let mut tmp_name = p.file_name().map(|f| f.to_owned()).unwrap_or_default();
1667        tmp_name.push(format!(".tmp.{}.{uniq}", std::process::id()));
1668        p.set_file_name(tmp_name);
1669        p
1670    };
1671    // Any failure between creating the staging file and renaming it into
1672    // place must remove it: each attempt stages at a fresh unique name, so
1673    // without cleanup a caller retrying a persistent failure (e.g. a full
1674    // disk) accumulates a full-size orphan per attempt. (A crash mid-write
1675    // can still orphan the file — only an external sweep can reclaim that.)
1676    let staged = (|| -> Result<(), CacheError> {
1677        let mut f = OpenOptions::new()
1678            .create(true)
1679            .write(true)
1680            .truncate(true)
1681            .open(&tmp_path)?;
1682        f.write_all(&file)?;
1683        f.sync_all()?;
1684        std::fs::rename(&tmp_path, path)?;
1685        Ok(())
1686    })();
1687    if let Err(e) = staged {
1688        let _ = std::fs::remove_file(&tmp_path);
1689        return Err(e);
1690    }
1691    // Best-effort: fsync the containing directory so the rename (the entry
1692    // that makes the new content visible) survives a crash. Failures here are
1693    // non-fatal — some filesystems reject directory fsync.
1694    let dir = match path.parent() {
1695        Some(d) if !d.as_os_str().is_empty() => d,
1696        _ => Path::new("."),
1697    };
1698    if let Ok(d) = std::fs::File::open(dir) {
1699        let _ = d.sync_all();
1700    }
1701    Ok(())
1702}
1703
1704/// Validated, not-yet-materialised lookup sections gathered during the
1705/// reader's validation phase. The `&[u8]` descriptors borrow the mmap; the
1706/// owned runtime-table data is parsed eagerly (it is not mmap-backed). All of
1707/// this is turned into a `LookupConstraintSystem` in the infallible
1708/// materialisation phase so no mmap-backed `Vec` is built before validation
1709/// completes. `_desc` fields are `(bytes, element_count)`.
1710struct LookupParts<'a> {
1711    lt_bytes: &'a [u8],
1712    /// Number of inner d8-sized arrays packed into `lt_bytes`.
1713    n: usize,
1714    d8_size: usize,
1715    inner_bytes: usize,
1716    table_ids8_desc: Option<(&'a [u8], usize)>,
1717    sel_xor_desc: Option<(&'a [u8], usize)>,
1718    sel_lookup_desc: Option<(&'a [u8], usize)>,
1719    sel_range_check_desc: Option<(&'a [u8], usize)>,
1720    sel_ffmul_desc: Option<(&'a [u8], usize)>,
1721    runtime_selector_desc: Option<(&'a [u8], usize)>,
1722    runtime_tables: Option<Vec<RuntimeTableSpec>>,
1723    runtime_table_offset: Option<usize>,
1724}
1725
1726/// Reads a cache file produced by [`write_cache`] and returns an
1727/// [`MmapProverIndex`]: a [`ProverIndex`]-compatible wrapper whose bulk
1728/// `Vec<F>` fields are backed by the mmap'd file rather than heap copies.
1729///
1730/// Reading is a single `mmap(2)` syscall; the prover reads field elements
1731/// directly from page-cache pages which the kernel can evict under
1732/// memory pressure and re-fault on demand. This is the "OS can evict
1733/// cached keys" behaviour the cache was designed for.
1734///
1735/// The returned `MmapProverIndex` [`Deref`]s to `&ProverIndex`, so existing
1736/// callers that pass `&ProverIndex<...>` into the prover continue to work
1737/// unchanged.
1738///
1739/// # Safety of the construction
1740///
1741/// This function constructs `Vec<F>` values via
1742/// `Vec::from_raw_parts(mmap_ptr, len, len)`, pointing directly into the
1743/// mmap. Those Vecs are bound by the lifetime of the mmap — which
1744/// [`MmapProverIndex`] guarantees by holding an `Arc<ReadOnlyMmap>`
1745/// alongside the wrapped index, and by wrapping the inner `ProverIndex`
1746/// in `ManuallyDrop` so `Vec::drop` can never run on them. See the
1747/// docstring on [`MmapProverIndex`] for the full invariant.
1748///
1749/// Identifier and ark-ff version must match the values the file was
1750/// produced with, otherwise a descriptive error is returned.
1751pub fn read_cache<const FULL_ROUNDS: usize, G, Srs>(
1752    identifier: &str,
1753    path: &Path,
1754    srs: Arc<Srs>,
1755) -> Result<MmapProverIndex<FULL_ROUNDS, G, Srs>, CacheError>
1756where
1757    G: KimchiCurve<FULL_ROUNDS>,
1758    Srs: SRS<G>,
1759    G::BaseField: ark_ff::PrimeField,
1760{
1761    let file = std::fs::File::open(path)?;
1762    // The mmap requires that the underlying file not be mutated in place by
1763    // external writers while the mapping is live. Callers must uphold this;
1764    // [`write_cache`] does, by staging to a temp file and renaming, which
1765    // keeps the original inode alive for existing readers.
1766    let mmap = Arc::new(ReadOnlyMmap::map_file(&file)?);
1767    // SAFETY: we hold `mmap` in an Arc for the lifetime of the returned
1768    // MmapProverIndex; the `bytes` slice is therefore valid for as long as
1769    // the constructed `Vec<F>` views remain reachable. Since the returned
1770    // index lives inside `ManuallyDrop`, those Vecs never drop on their
1771    // own, so we never attempt to dealloc this mmap memory.
1772    let bytes: &[u8] = mmap.as_slice();
1773
1774    // Preamble + identifier validation.
1775    let (preamble, rest) = read_preamble(bytes)?;
1776    if preamble.identifier != identifier {
1777        return Err(CacheError::IdentifierMismatch {
1778            found: preamble.identifier,
1779            expected: identifier.to_owned(),
1780        });
1781    }
1782
1783    // ScalarHeader.
1784    let (header, mut rest) = ScalarHeader::read(rest)?;
1785
1786    // Section table.
1787    let mut sections: std::collections::BTreeMap<u32, SectionEntry> = Default::default();
1788    for _ in 0..preamble.num_sections {
1789        let (entry, tail) = SectionEntry::read(rest)?;
1790        rest = tail;
1791        if entry.offset % SECTION_ALIGNMENT as u64 != 0 {
1792            return Err(CacheError::MisalignedSection {
1793                tag: entry.tag,
1794                offset: entry.offset,
1795            });
1796        }
1797        if sections.insert(entry.tag, entry).is_some() {
1798            return Err(CacheError::DuplicateSectionTag { tag: entry.tag });
1799        }
1800    }
1801
1802    // Helper to slice a section payload out of the mapping.
1803    let section_bytes = |tag: u32| -> Result<&[u8], CacheError> {
1804        let entry = sections
1805            .get(&tag)
1806            .ok_or(CacheError::MissingSection { tag })?;
1807        let start = entry.offset as usize;
1808        let end = start
1809            .checked_add(entry.length as usize)
1810            .ok_or(CacheError::TruncatedFile)?;
1811        if end > bytes.len() {
1812            return Err(CacheError::TruncatedFile);
1813        }
1814        Ok(&bytes[start..end])
1815    };
1816
1817    // Rebuild EvaluationDomains from d1_size.
1818    let d1_size = header.domain_d1_size as usize;
1819    let domain = EvaluationDomains::<G::ScalarField>::create(d1_size).map_err(|_| {
1820        CacheError::InvalidDomainSize {
1821            size: header.domain_d1_size,
1822        }
1823    })?;
1824    let d4 = domain.d4;
1825    let d8 = domain.d8;
1826
1827    // --- Validation phase -------------------------------------------------
1828    // Everything below that can fail runs BEFORE any mmap-backed `Vec<F>` is
1829    // constructed. We only collect validated `(bytes, count)` descriptors
1830    // here; the actual `Vec::from_raw_parts` views are built in the
1831    // infallible materialisation phase further down. This ordering is a
1832    // memory-safety requirement: an early `?` return once a mmap-backed Vec
1833    // existed would drop it and free mmap memory through the global allocator
1834    // (observed as a `free(): invalid pointer` process abort).
1835
1836    // Fetches a section and validates it holds exactly `elem_domain_size`
1837    // field elements, returning the raw bytes + count. Constructs no Vec.
1838    let field_section = |tag: u32| -> Result<(&[u8], usize), CacheError> {
1839        let entry = sections
1840            .get(&tag)
1841            .ok_or(CacheError::MissingSection { tag })?;
1842        let count = entry.elem_domain_size as usize;
1843        let b = section_bytes(tag)?;
1844        validate_field_section(tag, b, count)?;
1845        Ok((b, count))
1846    };
1847    let optional_field_section =
1848        |tag: u32, mask: u32, present: u32| -> Result<Option<(&[u8], usize)>, CacheError> {
1849            if present & mask != 0 {
1850                Ok(Some(field_section(tag)?))
1851            } else {
1852                Ok(None)
1853            }
1854        };
1855
1856    // sid.
1857    let sid_desc = field_section(SectionTag::Sid as u32)?;
1858
1859    // Gates (owned Vec, not mmap-backed — safe to build/drop fallibly here).
1860    let gates_bytes = section_bytes(SectionTag::Gates as u32)?;
1861    if gates_bytes.len() % PRUNED_GATE_SIZE != 0 {
1862        return Err(CacheError::PayloadNotFieldAligned {
1863            tag: SectionTag::Gates as u32,
1864            length: gates_bytes.len() as u64,
1865        });
1866    }
1867    let mut gates = Vec::with_capacity(gates_bytes.len() / PRUNED_GATE_SIZE);
1868    for chunk in gates_bytes.as_chunks::<PRUNED_GATE_SIZE>().0 {
1869        gates.push(read_pruned_gate::<G::ScalarField>(chunk)?);
1870    }
1871    // Restore each gate's coefficient vector from the GateCoeffs section.
1872    // These are owned Vecs, so parsing them fallibly is safe. Their only
1873    // consumer is the `cfg!(debug_assertions)` gate check in
1874    // `ProverProof::create` (per-gate `verify()` reads `gate.coeffs`), so
1875    // only debug builds materialise them — in release they would be tens of
1876    // MB of never-read heap per key, leaked on drop by the `ManuallyDrop`
1877    // design. The section is still fully validated in every build so a
1878    // corrupt file is rejected identically; note that a release-loaded index
1879    // re-exported through [`write_cache`] therefore writes empty coeffs,
1880    // which only debug-build gate checks would miss.
1881    {
1882        let mut cursor = section_bytes(SectionTag::GateCoeffs as u32)?;
1883        for gate in gates.iter_mut() {
1884            let (count, rest) = read_u32_le(cursor)?;
1885            let (fields, rest) = read_exact(rest, count as usize * FIELD_ELEMENT_BYTES)?;
1886            if cfg!(debug_assertions) {
1887                let mut coeffs = Vec::with_capacity(count as usize);
1888                for chunk in fields.as_chunks::<FIELD_ELEMENT_BYTES>().0 {
1889                    let mut limbs = [0u64; 4];
1890                    for (i, limb) in limbs.iter_mut().enumerate() {
1891                        let mut b = [0u8; 8];
1892                        b.copy_from_slice(&chunk[i * 8..i * 8 + 8]);
1893                        *limb = u64::from_le_bytes(b);
1894                    }
1895                    coeffs.push(limbs_to_field::<G::ScalarField>(&limbs));
1896                }
1897                gate.coeffs = coeffs;
1898            }
1899            cursor = rest;
1900        }
1901    }
1902
1903    // Column-evaluation descriptors (validated, not yet materialised).
1904    let mut coeff_descs: Vec<(&[u8], usize)> = Vec::with_capacity(COLUMNS);
1905    for i in 0..COLUMNS {
1906        coeff_descs.push(field_section(coefficient_tag(i))?);
1907    }
1908    let coeff_descs: [(&[u8], usize); COLUMNS] = coeff_descs
1909        .try_into()
1910        .map_err(|_| CacheError::TruncatedFile)?;
1911
1912    let mut perm_descs: Vec<(&[u8], usize)> = Vec::with_capacity(PERMUTS);
1913    for i in 0..PERMUTS {
1914        perm_descs.push(field_section(permutation_coefficient_tag(i))?);
1915    }
1916    let perm_descs: [(&[u8], usize); PERMUTS] = perm_descs
1917        .try_into()
1918        .map_err(|_| CacheError::TruncatedFile)?;
1919
1920    let generic_selector4_desc = field_section(SectionTag::GenericSelector4 as u32)?;
1921    let poseidon_selector8_desc = field_section(SectionTag::PoseidonSelector8 as u32)?;
1922    let complete_add_selector4_desc = field_section(SectionTag::CompleteAddSelector4 as u32)?;
1923    let mul_selector8_desc = field_section(SectionTag::MulSelector8 as u32)?;
1924    let emul_selector8_desc = field_section(SectionTag::EmulSelector8 as u32)?;
1925    let endomul_scalar_selector8_desc = field_section(SectionTag::EndomulScalarSelector8 as u32)?;
1926
1927    let opt = header.optional_selectors_present;
1928    let range_check0_desc = optional_field_section(
1929        SectionTag::RangeCheck0Selector8 as u32,
1930        OptionalSelectorBits::RANGE_CHECK_0,
1931        opt,
1932    )?;
1933    let range_check1_desc = optional_field_section(
1934        SectionTag::RangeCheck1Selector8 as u32,
1935        OptionalSelectorBits::RANGE_CHECK_1,
1936        opt,
1937    )?;
1938    let ffadd_desc = optional_field_section(
1939        SectionTag::ForeignFieldAddSelector8 as u32,
1940        OptionalSelectorBits::FOREIGN_FIELD_ADD,
1941        opt,
1942    )?;
1943    let ffmul_desc = optional_field_section(
1944        SectionTag::ForeignFieldMulSelector8 as u32,
1945        OptionalSelectorBits::FOREIGN_FIELD_MUL,
1946        opt,
1947    )?;
1948    let xor_desc = optional_field_section(
1949        SectionTag::XorSelector8 as u32,
1950        OptionalSelectorBits::XOR,
1951        opt,
1952    )?;
1953    let rot_desc = optional_field_section(
1954        SectionTag::RotSelector8 as u32,
1955        OptionalSelectorBits::ROT,
1956        opt,
1957    )?;
1958
1959    let feature_flags = unpack_feature_flags(header.feature_flags)?;
1960    let shift: [G::ScalarField; PERMUTS] = {
1961        let mut arr = [G::ScalarField::from(0u64); PERMUTS];
1962        for (i, limbs) in header.shift_limbs.iter().enumerate() {
1963            arr[i] = limbs_to_field::<G::ScalarField>(limbs);
1964        }
1965        arr
1966    };
1967    let endo = limbs_to_field::<G::ScalarField>(&header.endo_limbs);
1968
1969    // Lookup descriptors, gated on the same presence bitmap stamped at write
1970    // time. Owned data (runtime tables / offset) is parsed here; field arrays
1971    // stay as validated descriptors until materialisation.
1972    let lp = header.lookup_selectors_present;
1973    let has_lookup = lp != 0 || sections.contains_key(&(SectionTag::LookupTable8 as u32));
1974    let lookup: Option<LookupParts> = if has_lookup {
1975        // lookup_table8: `n` inner arrays of d8 size packed in one section.
1976        // count == 0 is valid (empty lookup table).
1977        let lt_entry =
1978            sections
1979                .get(&(SectionTag::LookupTable8 as u32))
1980                .ok_or(CacheError::MissingSection {
1981                    tag: SectionTag::LookupTable8 as u32,
1982                })?;
1983        let n = lt_entry.elem_domain_size as usize;
1984        let d8_size = d8.size();
1985        let inner_bytes = d8_size * FIELD_ELEMENT_BYTES;
1986        let lt_bytes = section_bytes(SectionTag::LookupTable8 as u32)?;
1987        let expected_total = n * inner_bytes;
1988        if lt_bytes.len() != expected_total {
1989            return Err(CacheError::SectionLengthMismatch {
1990                tag: SectionTag::LookupTable8 as u32,
1991                expected: expected_total as u64,
1992                found: lt_bytes.len() as u64,
1993            });
1994        }
1995
1996        let table_ids8_desc = optional_field_section(
1997            SectionTag::TableIds8 as u32,
1998            LookupSelectorBits::TABLE_IDS8,
1999            lp,
2000        )?;
2001        let sel_xor_desc = optional_field_section(
2002            SectionTag::LookupSelectorXor as u32,
2003            LookupSelectorBits::SELECTOR_XOR,
2004            lp,
2005        )?;
2006        let sel_lookup_desc = optional_field_section(
2007            SectionTag::LookupSelectorLookup as u32,
2008            LookupSelectorBits::SELECTOR_LOOKUP,
2009            lp,
2010        )?;
2011        let sel_range_check_desc = optional_field_section(
2012            SectionTag::LookupSelectorRangeCheck as u32,
2013            LookupSelectorBits::SELECTOR_RANGE_CHECK,
2014            lp,
2015        )?;
2016        let sel_ffmul_desc = optional_field_section(
2017            SectionTag::LookupSelectorFfmul as u32,
2018            LookupSelectorBits::SELECTOR_FFMUL,
2019            lp,
2020        )?;
2021        let runtime_selector_desc = optional_field_section(
2022            SectionTag::RuntimeSelector8 as u32,
2023            LookupSelectorBits::RUNTIME_SELECTOR,
2024            lp,
2025        )?;
2026
2027        let runtime_tables: Option<Vec<RuntimeTableSpec>> =
2028            if lp & LookupSelectorBits::RUNTIME_TABLES != 0 {
2029                let rt_bytes = section_bytes(SectionTag::RuntimeTablesSpec as u32)?;
2030                if rt_bytes.len() < 4 {
2031                    return Err(CacheError::TruncatedFile);
2032                }
2033                let mut count_buf = [0u8; 4];
2034                count_buf.copy_from_slice(&rt_bytes[..4]);
2035                let rn = u32::from_le_bytes(count_buf) as usize;
2036                let expected = 4 + rn * 8;
2037                if rt_bytes.len() != expected {
2038                    return Err(CacheError::SectionLengthMismatch {
2039                        tag: SectionTag::RuntimeTablesSpec as u32,
2040                        expected: expected as u64,
2041                        found: rt_bytes.len() as u64,
2042                    });
2043                }
2044                let mut out = Vec::with_capacity(rn);
2045                for i in 0..rn {
2046                    let base = 4 + i * 8;
2047                    let mut id_buf = [0u8; 4];
2048                    let mut len_buf = [0u8; 4];
2049                    id_buf.copy_from_slice(&rt_bytes[base..base + 4]);
2050                    len_buf.copy_from_slice(&rt_bytes[base + 4..base + 8]);
2051                    out.push(RuntimeTableSpec {
2052                        id: i32::from_le_bytes(id_buf),
2053                        len: u32::from_le_bytes(len_buf) as usize,
2054                    });
2055                }
2056                Some(out)
2057            } else {
2058                None
2059            };
2060
2061        let runtime_table_offset: Option<usize> =
2062            if lp & LookupSelectorBits::RUNTIME_TABLE_OFFSET != 0 {
2063                let off_bytes = section_bytes(SectionTag::RuntimeTableOffset as u32)?;
2064                if off_bytes.len() != 8 {
2065                    return Err(CacheError::SectionLengthMismatch {
2066                        tag: SectionTag::RuntimeTableOffset as u32,
2067                        expected: 8,
2068                        found: off_bytes.len() as u64,
2069                    });
2070                }
2071                let mut buf = [0u8; 8];
2072                buf.copy_from_slice(off_bytes);
2073                Some(u64::from_le_bytes(buf) as usize)
2074            } else {
2075                None
2076            };
2077
2078        Some(LookupParts {
2079            lt_bytes,
2080            n,
2081            d8_size,
2082            inner_bytes,
2083            table_ids8_desc,
2084            sel_xor_desc,
2085            sel_lookup_desc,
2086            sel_range_check_desc,
2087            sel_ffmul_desc,
2088            runtime_selector_desc,
2089            runtime_tables,
2090            runtime_table_offset,
2091        })
2092    } else {
2093        None
2094    };
2095
2096    // --- Materialisation phase (infallible) -------------------------------
2097    // Nothing below returns `Err`, so every mmap-backed `Vec<F>` built here is
2098    // guaranteed to reach the `ManuallyDrop` index and never run its
2099    // destructor. Each descriptor was length-validated above.
2100    //
2101    // SAFETY (applies to every `mmap_field_vec_unchecked` call below): the
2102    // descriptor byte length was checked to equal `count * FIELD_ELEMENT_BYTES`;
2103    // the mmap outlives the returned index (held via `_mmap`); and the index
2104    // is stored in `ManuallyDrop` so these Vecs never drop or grow.
2105    let make_eval = |(b, c): (&[u8], usize),
2106                     d: Radix2EvaluationDomain<G::ScalarField>|
2107     -> Evaluations<G::ScalarField, Radix2EvaluationDomain<G::ScalarField>> {
2108        Evaluations::<G::ScalarField, _>::from_vec_and_domain(
2109            unsafe { mmap_field_vec_unchecked::<G::ScalarField>(b, c) },
2110            d,
2111        )
2112    };
2113    let make_opt_eval = |desc: Option<(&[u8], usize)>,
2114                         d: Radix2EvaluationDomain<G::ScalarField>| {
2115        desc.map(|x| make_eval(x, d))
2116    };
2117
2118    let sid = unsafe { mmap_field_vec_unchecked::<G::ScalarField>(sid_desc.0, sid_desc.1) };
2119
2120    let coefficients8: [Evaluations<G::ScalarField, _>; COLUMNS] =
2121        coeff_descs.map(|desc| make_eval(desc, d8));
2122    let permutation_coefficients8: [Evaluations<G::ScalarField, _>; PERMUTS] =
2123        perm_descs.map(|desc| make_eval(desc, d8));
2124
2125    let column_evaluations = ColumnEvaluations::<G::ScalarField> {
2126        permutation_coefficients8,
2127        coefficients8,
2128        generic_selector4: make_eval(generic_selector4_desc, d4),
2129        poseidon_selector8: make_eval(poseidon_selector8_desc, d8),
2130        complete_add_selector4: make_eval(complete_add_selector4_desc, d4),
2131        mul_selector8: make_eval(mul_selector8_desc, d8),
2132        emul_selector8: make_eval(emul_selector8_desc, d8),
2133        endomul_scalar_selector8: make_eval(endomul_scalar_selector8_desc, d8),
2134        range_check0_selector8: make_opt_eval(range_check0_desc, d8),
2135        range_check1_selector8: make_opt_eval(range_check1_desc, d8),
2136        foreign_field_add_selector8: make_opt_eval(ffadd_desc, d8),
2137        foreign_field_mul_selector8: make_opt_eval(ffmul_desc, d8),
2138        xor_selector8: make_opt_eval(xor_desc, d8),
2139        rot_selector8: make_opt_eval(rot_desc, d8),
2140    };
2141
2142    let precomputations = Arc::new(LazyCache::new({
2143        let precomputations_domain = domain;
2144        let zk_rows = header.zk_rows;
2145        move || {
2146            Arc::new(
2147                crate::circuits::domain_constant_evaluation::DomainConstantEvaluations::create(
2148                    precomputations_domain,
2149                    zk_rows,
2150                )
2151                .expect("domain constant evaluations"),
2152            )
2153        }
2154    }));
2155
2156    let lookup_constraint_system = match lookup {
2157        None => Arc::new(LazyCache::new(|| {
2158            Ok::<
2159                Option<LookupConstraintSystem<G::ScalarField>>,
2160                crate::circuits::lookup::index::LookupError,
2161            >(None)
2162        })),
2163        Some(parts) => {
2164            let mut lookup_table8: Vec<Evaluations<G::ScalarField, _>> =
2165                Vec::with_capacity(parts.n);
2166            for i in 0..parts.n {
2167                let start = i * parts.inner_bytes;
2168                let end = start + parts.inner_bytes;
2169                let evals = unsafe {
2170                    mmap_field_vec_unchecked::<G::ScalarField>(
2171                        &parts.lt_bytes[start..end],
2172                        parts.d8_size,
2173                    )
2174                };
2175                lookup_table8.push(Evaluations::from_vec_and_domain(evals, d8));
2176            }
2177            // LookupInfo is a pure function of `feature_flags.lookup_features`.
2178            let lookup_info = LookupInfo::create(feature_flags.lookup_features);
2179            let lcs = LookupConstraintSystem {
2180                lookup_table: Vec::new(),
2181                lookup_table8,
2182                table_ids: None,
2183                table_ids8: make_opt_eval(parts.table_ids8_desc, d8),
2184                lookup_selectors: LookupSelectors {
2185                    xor: make_opt_eval(parts.sel_xor_desc, d8),
2186                    lookup: make_opt_eval(parts.sel_lookup_desc, d8),
2187                    range_check: make_opt_eval(parts.sel_range_check_desc, d8),
2188                    ffmul: make_opt_eval(parts.sel_ffmul_desc, d8),
2189                },
2190                runtime_selector: make_opt_eval(parts.runtime_selector_desc, d8),
2191                runtime_tables: parts.runtime_tables,
2192                runtime_table_offset: parts.runtime_table_offset,
2193                configuration: LookupConfiguration::new(lookup_info),
2194            };
2195            Arc::new(LazyCache::new(move || Ok(Some(lcs))))
2196        }
2197    };
2198
2199    let cs = ConstraintSystem {
2200        public: header.public as usize,
2201        prev_challenges: header.prev_challenges as usize,
2202        domain,
2203        gates: Arc::new(gates),
2204        zk_rows: header.zk_rows,
2205        feature_flags,
2206        sid,
2207        shift,
2208        endo,
2209        lookup_constraint_system,
2210        precomputations,
2211        disable_gates_checks: header.disable_gates_checks,
2212    };
2213
2214    // Re-derive linearization and powers_of_alpha.
2215    let (linearization, powers_of_alpha) = expr_linearization(Some(&cs.feature_flags), true);
2216
2217    let verifier_index_digest = if header.has_verifier_index_digest {
2218        Some(limbs_to_field::<G::BaseField>(
2219            &header.verifier_index_digest_limbs,
2220        ))
2221    } else {
2222        None
2223    };
2224
2225    let cs = Arc::new(cs);
2226    let column_evaluations_cache = Arc::new(LazyCache::new({
2227        let ce = column_evaluations;
2228        move || ce
2229    }));
2230    // Force evaluation so the LazyCache is populated (downstream code may
2231    // call `.get()` without checking status).
2232    column_evaluations_cache.get();
2233
2234    let index = ProverIndex {
2235        cs,
2236        linearization,
2237        powers_of_alpha,
2238        srs,
2239        max_poly_size: header.max_poly_size as usize,
2240        column_evaluations: column_evaluations_cache,
2241        verifier_index: None,
2242        verifier_index_digest,
2243    };
2244    Ok(MmapProverIndex {
2245        index: core::mem::ManuallyDrop::new(index),
2246        _mmap: mmap,
2247    })
2248}