Skip to main content

zinc_piop/lookup/
structs.rs

1//! Data structures for the lookup protocol.
2//!
3//! Proof types, prover/verifier intermediates, instance types, and error
4//! definitions live here. Specification types (`LookupTableType`,
5//! `LookupColumnSpec`) live in `zinc-uair` and are re-exported from the
6//! parent module.
7
8use itertools::Itertools;
9use std::collections::BTreeMap;
10use thiserror::Error;
11use zinc_uair::LookupTableType;
12
13// ---------------------------------------------------------------------------
14// Per-group proof (BatchedDecompLogup)
15// ---------------------------------------------------------------------------
16
17/// Proof for one lookup group (columns sharing the same table type).
18///
19/// Does **not** contain a sumcheck proof — the sumcheck is shared via
20/// the protocol-level multi-degree sumcheck. This struct
21/// carries only the auxiliary vectors the verifier needs to reconstruct
22/// evaluations at the shared point.
23///
24/// Chunk vectors are **not** included — the verifier reconstructs them
25/// from the inverse witnesses: `c_k[j] = β − 1/u_k[j]`. Soundness
26/// follows from the PCS commitment binding the parent column.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct BatchedDecompLogupProof<F> {
29    /// Per-witness aggregated multiplicity vectors:
30    /// `aggregated_multiplicities[l][j] = Σ_k m_k^(l)[j]`.
31    pub aggregated_multiplicities: Vec<Vec<F>>,
32    /// Per-witness per-chunk inverse witness vectors:
33    /// `chunk_inverse_witnesses[l][k][i] = 1 / (β − chunk[l][k][i])`.
34    pub chunk_inverse_witnesses: Vec<Vec<Vec<F>>>,
35    /// Shared inverse table vector: `inverse_table[j] = 1 / (β − T[j])`.
36    pub inverse_table: Vec<F>,
37}
38
39impl<F> BatchedDecompLogupProof<F> {
40    /// Maps every field element through `f`, preserving structure — used to
41    /// lift elements into wire integers and to project wire integers back
42    /// into elements at the (de)serialization boundary.
43    pub fn try_map<T, E>(
44        &self,
45        f: impl FnMut(&F) -> Result<T, E> + Copy,
46    ) -> Result<BatchedDecompLogupProof<T>, E> {
47        Ok(BatchedDecompLogupProof {
48            aggregated_multiplicities: self
49                .aggregated_multiplicities
50                .iter()
51                .map(|v| v.iter().map(f).try_collect())
52                .try_collect()?,
53            chunk_inverse_witnesses: self
54                .chunk_inverse_witnesses
55                .iter()
56                .map(|vv| {
57                    vv.iter()
58                        .map(|v| v.iter().map(f).try_collect())
59                        .try_collect()
60                })
61                .try_collect()?,
62            inverse_table: self.inverse_table.iter().map(f).try_collect()?,
63        })
64    }
65}
66
67// ---------------------------------------------------------------------------
68// Per-group metadata (carried in the proof for the verifier)
69// ---------------------------------------------------------------------------
70
71/// Describes how a lookup witness column was derived from the trace.
72///
73/// Carried in [`LookupGroupMeta`] so the verifier can reconstruct the
74/// parent evaluation without re-receiving the lookup specs.
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub enum LookupWitnessSource {
77    /// Standard column lookup: parent eval = `up_evals[column_index]`.
78    Column {
79        /// Original trace column index.
80        column_index: usize,
81    },
82    /// Affine-combination lookup: parent eval = `Σ coeff·up_evals[col] +
83    /// offset`. Currently only needed for BitPoly
84    Affine {
85        /// `(column_index, coefficient)` pairs.
86        terms: Vec<(usize, i64)>,
87        /// Constant bit-polynomial offset encoded as a u32 bit pattern.
88        constant_offset_bits: u32,
89    },
90}
91
92/// Per-group metadata stored in the proof so the verifier can reconstruct
93/// tables and column layout without being passed the original lookup specs.
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct LookupGroupMeta {
96    /// Table type for this group (determines subtable generation).
97    pub table_type: LookupTableType,
98    /// Number of witness columns batched into this group (L).
99    pub num_columns: usize,
100    /// Number of rows in each witness vector (trace length).
101    pub witness_len: usize,
102    /// Per-witness source descriptors.
103    pub witness_sources: Vec<LookupWitnessSource>,
104}
105
106// ---------------------------------------------------------------------------
107// Complete lookup proof
108// ---------------------------------------------------------------------------
109
110/// Top-level proof: one [`BatchedDecompLogupProof`] per lookup group
111/// (groups formed by batching columns with the same [`LookupTableType`]).
112/// Carries [`LookupGroupMeta`] per group so the verifier needs no
113/// external specs.
114#[derive(Clone, Default, Debug, PartialEq, Eq)]
115pub struct BatchedLookupProof<F> {
116    /// Per-group proofs, in group order.
117    pub group_proofs: Vec<BatchedDecompLogupProof<F>>,
118    /// Per-group metadata needed by the verifier.
119    pub group_meta: Vec<LookupGroupMeta>,
120}
121
122impl<F> BatchedLookupProof<F> {
123    /// Maps every field element through `f`, preserving structure — used to
124    /// lift elements into wire integers and to project wire integers back
125    /// into elements at the (de)serialization boundary.
126    pub fn try_map<T, E>(
127        &self,
128        f: impl FnMut(&F) -> Result<T, E> + Copy,
129    ) -> Result<BatchedLookupProof<T>, E> {
130        Ok(BatchedLookupProof {
131            group_proofs: self
132                .group_proofs
133                .iter()
134                .map(|p| p.try_map(f))
135                .try_collect()?,
136            group_meta: self.group_meta.clone(),
137        })
138    }
139}
140
141// ---------------------------------------------------------------------------
142// Grouping utility
143// ---------------------------------------------------------------------------
144
145/// A group of columns that all look up into the same decomposed table.
146///
147/// Produced by [`group_lookup_specs`] and consumed by the pipeline.
148#[derive(Debug)]
149pub struct LookupGroup {
150    /// The shared table type.
151    pub table_type: LookupTableType,
152    /// Indices of all columns in this group.
153    pub column_indices: Vec<usize>,
154}
155
156/// Groups a list of [`LookupColumnSpec`](zinc_uair::LookupColumnSpec)s
157/// by their table type.
158///
159/// Columns with the same `LookupTableType` are batched into a single
160/// `BatchedDecompLogupProtocol` instance.
161pub fn group_lookup_specs(specs: &[zinc_uair::LookupColumnSpec]) -> Vec<LookupGroup> {
162    let mut map: BTreeMap<LookupTableType, Vec<usize>> = BTreeMap::new();
163    for spec in specs {
164        map.entry(spec.table_type.clone())
165            .or_default()
166            .push(spec.column_index);
167    }
168    map.into_iter()
169        .map(|(table_type, column_indices)| LookupGroup {
170            table_type,
171            column_indices,
172        })
173        .collect()
174}
175
176// ---------------------------------------------------------------------------
177// Errors
178// ---------------------------------------------------------------------------
179
180/// Errors from the lookup protocol.
181#[derive(Debug, Error)]
182pub enum LookupError {
183    #[error("lookup not implemented")]
184    NotImplemented,
185
186    #[error("witness entry not found in lookup table")]
187    WitnessNotInTable,
188
189    #[error("table inverse vector is incorrect at index {index}")]
190    TableInverseIncorrect { index: usize },
191
192    #[error("decomposition consistency check failed")]
193    DecompositionInconsistent,
194
195    #[error("multiplicity sum mismatch: expected {expected}, got {got}")]
196    MultiplicitySumMismatch { expected: u64, got: u64 },
197
198    #[error("final evaluation check failed")]
199    FinalEvaluationMismatch,
200}