Skip to main content

hekate_program/
linearized.rs

1// SPDX-FileCopyrightText: 2026 Andrei Kochergin <andrei@oumuamua.dev>
2// SPDX-FileCopyrightText: 2026 Oumuamua Labs <info@oumuamua.dev>
3// SPDX-License-Identifier: AGPL-3.0-only
4
5//! F₂-linear maps on GF(2^128) as linearized polynomials
6//! `Σ_j μ_j x^{2^j}`, and the outer-statement gadget that
7//! proves `Σ_c a_c φ(h_c)` on committed pad entries with
8//! a Frobenius-Horner chain of 127 squarings.
9
10use crate::predicate::{AffineRow, Unknown, WireRole};
11use alloc::vec;
12use alloc::vec::Vec;
13use core::marker::PhantomData;
14use hekate_math::{Block128, Flat, HardwareField, TowerField};
15
16pub const BITS: usize = 128;
17
18pub const CHAIN_ROWS: usize = 2 * (BITS - 1);
19
20const DUAL_KERNEL: [u8; 8] = [0x29, 0xb0, 0x58, 0x05, 0xa6, 0x53, 0xa4, 0x52];
21
22/// F₂-linear map tabulated per byte of its argument's
23/// raw bits; `new` reads `image` on basis vectors only.
24/// `apply` indexes the tables by its argument: variable-time.
25pub(crate) struct LinearMap<I, O> {
26    tables: Vec<[Flat<O>; 256]>,
27    input: PhantomData<I>,
28}
29
30impl<I, O> LinearMap<I, O>
31where
32    I: HardwareField + Into<Block128>,
33    O: HardwareField,
34{
35    pub(crate) fn new(image: impl Fn(Flat<I>) -> Flat<O>) -> Self {
36        let mut tables = vec![[Flat::from_raw(O::ZERO); 256]; BITS / 8];
37        for (w, table) in tables.iter_mut().enumerate() {
38            let images: [Flat<O>; 8] =
39                core::array::from_fn(|k| image(Flat::from_raw(I::from(1u128 << (8 * w + k)))));
40
41            for v in 1usize..256 {
42                table[v] = table[v & (v - 1)] + images[v.trailing_zeros() as usize];
43            }
44        }
45
46        Self {
47            tables,
48            input: PhantomData,
49        }
50    }
51
52    pub(crate) fn apply(&self, x: Flat<I>) -> Flat<O> {
53        let raw: Block128 = x.into_raw().into();
54
55        let mut acc = Flat::from_raw(O::ZERO);
56        for (table, byte) in self.tables.iter().zip(raw.0.to_le_bytes()) {
57            acc += table[byte as usize];
58        }
59
60        acc
61    }
62}
63
64/// `Δ = Σ_c a_c φ(h_c)` over the pad entries `(pad, a_c)`
65/// as the chain `X_127 = T'_127`, `X_j = T'_j + X_{j+1}²`,
66/// `T'_j = Σ_c σ^{-j}(μ_j a_c) h_c`, on squaring wires
67/// `first_wire + j - 1` for `j = 1..128`; `Δ = X_0`.
68pub struct RingGadget<F> {
69    pub ring: Vec<(u32, Flat<F>)>,
70    pub mu: Vec<Flat<F>>,
71    pub first_wire: u32,
72}
73
74impl<F: HardwareField> RingGadget<F> {
75    pub fn new(ring: Vec<(u32, Flat<F>)>, mu: Vec<Flat<F>>, first_wire: u32) -> Self {
76        Self {
77            ring,
78            mu,
79            first_wire,
80        }
81    }
82
83    fn wire(&self, j: usize) -> u32 {
84        self.first_wire + j as u32 - 1
85    }
86
87    /// Row of `Lhs_j` among the gadget's
88    /// [`CHAIN_ROWS`]; `Rhs_j` follows it.
89    pub fn row_of(&self, j: usize) -> usize {
90        2 * (j - 1)
91    }
92
93    /// `f(j, tie_j)` for `j = 127` down to `1`,
94    /// `tie_j[c] = σ^{-j}(μ_j a_c)`; the coefficients
95    /// of row `Lhs_j` on the pad entries.
96    pub fn for_each_tie_row(&self, mut f: impl FnMut(usize, &[Flat<F>])) {
97        let mut conj: Vec<Flat<F>> = self.ring.iter().map(|&(_, a)| a).collect();
98        let mut tie = vec![Flat::from_raw(F::ZERO); conj.len()];
99
100        for j in (1..BITS).rev() {
101            for a in conj.iter_mut() {
102                *a = *a * *a;
103            }
104
105            let mu_conj = frobenius(self.mu[j], BITS - j);
106            for (t, &a) in tie.iter_mut().zip(&conj) {
107                *t = a * mu_conj;
108            }
109
110            f(j, &tie);
111        }
112    }
113
114    /// `f(pad, w_c)` per pad entry, with
115    /// `w_c = Σ_j scales[row_of(j)] · tie_j[c]`
116    /// folded into one F₂-linear map of `a_c`.
117    pub fn for_each_tie_weight(&self, scales: &[Flat<F>], mut f: impl FnMut(u32, Flat<F>))
118    where
119        F: Into<Block128>,
120    {
121        let mut lambda = [Flat::from_raw(F::ZERO); BITS];
122        for (i, l) in lambda.iter_mut().enumerate().skip(1) {
123            let j = BITS - i;
124            *l = scales[self.row_of(j)] * frobenius(self.mu[j], i);
125        }
126
127        let map = LinearMap::new(|x: Flat<F>| {
128            let mut power = x;
129            let mut acc = Flat::from_raw(F::ZERO);
130
131            for &l in &lambda[1..] {
132                power = power * power;
133                acc += l * power;
134            }
135
136            acc
137        });
138
139        for &(pad, a) in &self.ring {
140            f(pad, map.apply(a));
141        }
142    }
143
144    /// Wire terms of the chain rows, in [`Self::row_of`] order:
145    /// `Lhs_j + Product_{j+1}` and `Rhs_j + Lhs_j`.
146    pub fn wire_rows(&self) -> Vec<AffineRow<F>> {
147        let one = Flat::from_raw(F::ONE);
148        let zero = Flat::from_raw(F::ZERO);
149
150        let mut rows = Vec::with_capacity(CHAIN_ROWS);
151        for j in 1..BITS {
152            let mut unknowns = vec![(
153                Unknown::Wire {
154                    mul: self.wire(j),
155                    role: WireRole::Lhs,
156                },
157                one,
158            )];
159
160            if j + 1 < BITS {
161                unknowns.push((
162                    Unknown::Wire {
163                        mul: self.wire(j + 1),
164                        role: WireRole::Product,
165                    },
166                    one,
167                ));
168            }
169
170            rows.push(AffineRow {
171                unknowns,
172                claims: Vec::new(),
173                constant: zero,
174            });
175
176            rows.push(AffineRow {
177                unknowns: vec![
178                    (
179                        Unknown::Wire {
180                            mul: self.wire(j),
181                            role: WireRole::Rhs,
182                        },
183                        one,
184                    ),
185                    (
186                        Unknown::Wire {
187                            mul: self.wire(j),
188                            role: WireRole::Lhs,
189                        },
190                        one,
191                    ),
192                ],
193                claims: Vec::new(),
194                constant: zero,
195            });
196        }
197
198        rows
199    }
200
201    /// `Δ` over the pad entries and the wire `Product_1`.
202    pub fn delta_form(&self) -> Vec<(Unknown, Flat<F>)> {
203        let mut form: Vec<(Unknown, Flat<F>)> = self
204            .ring
205            .iter()
206            .map(|&(pad, a)| (Unknown::Pad(pad), a * self.mu[0]))
207            .collect();
208
209        form.push((
210            Unknown::Wire {
211                mul: self.wire(1),
212                role: WireRole::Product,
213            },
214            Flat::from_raw(F::ONE),
215        ));
216
217        form
218    }
219
220    /// Honest `[X_j, X_j, X_j²]` for `j = 1..128`
221    /// from the pad values, indexed by wire.
222    pub fn wires(&self, pad_values: &[Flat<F>]) -> Vec<[Flat<F>; 3]> {
223        let mut wires = vec![[Flat::from_raw(F::ZERO); 3]; BITS - 1];
224        let mut x = Flat::from_raw(F::ZERO);
225
226        self.for_each_tie_row(|j, tie| {
227            x = dot(pad_values, tie) + x * x;
228            wires[j - 1] = [x, x, x * x];
229        });
230
231        wires
232    }
233
234    /// `Δ = Σ_c a_c φ(h_c)` on plaintext pad values.
235    pub fn delta(&self, pad_values: &[Flat<F>]) -> Flat<F> {
236        let mut x = Flat::from_raw(F::ZERO);
237        self.for_each_tie_row(|_, tie| x = dot(pad_values, tie) + x * x);
238
239        let mut t0 = Flat::from_raw(F::ZERO);
240        for (&h, &(_, a)) in pad_values.iter().zip(&self.ring) {
241            t0 += a * self.mu[0] * h;
242        }
243
244        t0 + x * x
245    }
246}
247
248/// Trace-dual of the tower basis:
249/// `Tr(dual_basis(u) · 2^w) = [u == w]`.
250pub fn dual_basis(u: usize) -> Block128 {
251    let byte = DUAL_KERNEL[u & 7] as u128;
252    let select = u >> 3;
253
254    let mut acc = 0u128;
255    for p in 0..16 {
256        if p & select == 0 {
257            acc |= byte << (8 * p);
258        }
259    }
260
261    Block128(acc)
262}
263
264/// `μ_j` with `Σ_u eq(r'',u)·bit_u(x) = Σ_j μ_j x^{2^j}`
265/// for every `x`, bits in the tower basis.
266pub fn linearized_coeffs(eq_mix: &[Block128]) -> Vec<Flat<Block128>> {
267    let eq: Vec<Flat<Block128>> = eq_mix.iter().map(|m| m.to_hardware()).collect();
268
269    let mut gamma: Vec<Flat<Block128>> = (0..BITS).map(|u| dual_basis(u).to_hardware()).collect();
270    let mut mu = Vec::with_capacity(BITS);
271
272    for _ in 0..BITS {
273        let mut acc = Flat::from_raw(Block128::ZERO);
274        for (g, m) in gamma.iter().zip(&eq) {
275            acc += *g * *m;
276        }
277
278        mu.push(acc);
279
280        for g in gamma.iter_mut() {
281            *g = *g * *g;
282        }
283    }
284
285    mu
286}
287
288fn dot<F: HardwareField>(a: &[Flat<F>], b: &[Flat<F>]) -> Flat<F> {
289    let mut acc = Flat::from_raw(F::ZERO);
290    for (&x, &y) in a.iter().zip(b) {
291        acc += x * y;
292    }
293
294    acc
295}
296
297fn frobenius<F: HardwareField>(x: Flat<F>, k: usize) -> Flat<F> {
298    let mut acc = x;
299    for _ in 0..k {
300        acc = acc * acc;
301    }
302
303    acc
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::expander::{eq_tensor_b, ring_batch_b};
310    use hekate_math::BinaryFieldExtras;
311
312    fn next(state: &mut u128) -> Block128 {
313        *state ^= *state << 13;
314        *state ^= *state >> 7;
315        *state ^= *state << 17;
316
317        Block128(*state)
318    }
319
320    /// Every chain row on the given values: wire terms
321    /// from `wire_rows()`, tie terms streamed onto `row_of(j)`.
322    fn chain_rows(
323        gadget: &RingGadget<Block128>,
324        pad: &[Flat<Block128>],
325        wires: &[[Flat<Block128>; 3]],
326    ) -> Vec<Flat<Block128>> {
327        let value = |u: Unknown| -> Flat<Block128> {
328            match u {
329                Unknown::Pad(i) => pad[i as usize],
330                Unknown::Wire { mul, role } => wires[mul as usize][role as usize],
331            }
332        };
333
334        let mut acc: Vec<Flat<Block128>> = gadget
335            .wire_rows()
336            .iter()
337            .map(|row| {
338                let mut acc = row.constant;
339                for &(u, coeff) in &row.unknowns {
340                    acc += coeff * value(u);
341                }
342
343                acc
344            })
345            .collect();
346
347        gadget.for_each_tie_row(|j, tie| {
348            for (&(pad_idx, _), &t) in gadget.ring.iter().zip(tie) {
349                acc[gadget.row_of(j)] += t * value(Unknown::Pad(pad_idx));
350            }
351        });
352
353        acc
354    }
355
356    #[test]
357    fn dual_basis_is_trace_dual() {
358        for u in 0..BITS {
359            for w in 0..BITS {
360                let tr = (dual_basis(u) * Block128(1u128 << w)).trace().get();
361                assert_eq!(tr == 1, u == w, "u={u} w={w}");
362            }
363        }
364    }
365
366    #[test]
367    fn coefficients_reproduce_bit_transposition() {
368        let mut state = 0x0f0f_1234_5678_9abc_def0_1122_3344_5566u128;
369
370        let r_mix: Vec<Block128> = (0..7).map(|_| next(&mut state)).collect();
371        let eq_mix = eq_tensor_b(&r_mix);
372        let mu = linearized_coeffs(&eq_mix);
373
374        for _ in 0..8 {
375            let x = next(&mut state);
376
377            let mut lin = Flat::from_raw(Block128::ZERO);
378            let mut power = x.to_hardware();
379
380            for m in &mu {
381                lin += *m * power;
382                power = power * power;
383            }
384
385            assert_eq!(lin.to_tower(), ring_batch_b(&[x], &eq_mix));
386        }
387    }
388
389    #[test]
390    fn wires_and_rows_evaluate_to_delta() {
391        let mut state = 0x7777_8888_9999_aaaa_bbbb_cccc_dddd_eeeeu128;
392
393        let r_mix: Vec<Block128> = (0..7).map(|_| next(&mut state)).collect();
394        let eq_mix = eq_tensor_b(&r_mix);
395        let mu = linearized_coeffs(&eq_mix);
396
397        let ring: Vec<(u32, Flat<Block128>)> = (0..5u32)
398            .map(|c| (c, next(&mut state).to_hardware()))
399            .collect();
400        let pad: Vec<Flat<Block128>> = (0..5).map(|_| next(&mut state).to_hardware()).collect();
401
402        let mut direct = Flat::from_raw(Block128::ZERO);
403        for (&(_, a), &h) in ring.iter().zip(&pad) {
404            direct += a * ring_batch_b(&[h.to_tower()], &eq_mix).to_hardware();
405        }
406
407        let gadget = RingGadget::new(ring, mu, 0);
408        assert_eq!(gadget.delta(&pad), direct);
409
410        let wires = gadget.wires(&pad);
411
412        for (i, v) in chain_rows(&gadget, &pad, &wires).iter().enumerate() {
413            assert_eq!(*v, Flat::from_raw(Block128::ZERO), "row {i}");
414        }
415
416        for wire in &wires {
417            assert_eq!(wire[0], wire[1]);
418            assert_eq!(wire[0] * wire[1], wire[2]);
419        }
420
421        let mut delta = Flat::from_raw(Block128::ZERO);
422        for &(u, coeff) in &gadget.delta_form() {
423            delta += coeff
424                * match u {
425                    Unknown::Pad(i) => pad[i as usize],
426                    Unknown::Wire { mul, role } => wires[mul as usize][role as usize],
427                };
428        }
429
430        assert_eq!(delta, direct);
431    }
432
433    /// A floating wire would let a prover pick its value freely;
434    /// every wire must be pinned by a row or a Hadamard triple.
435    #[test]
436    fn chain_is_rigid_under_single_wire_perturbation() {
437        let mut state = 0xdead_beef_0bad_f00d_1357_9bdf_2468_ace0u128;
438
439        let r_mix: Vec<Block128> = (0..7).map(|_| next(&mut state)).collect();
440        let mu = linearized_coeffs(&eq_tensor_b(&r_mix));
441
442        let ring: Vec<(u32, Flat<Block128>)> = (0..7u32)
443            .map(|c| (c, next(&mut state).to_hardware()))
444            .collect();
445        let pad: Vec<Flat<Block128>> = (0..7).map(|_| next(&mut state).to_hardware()).collect();
446
447        let gadget = RingGadget::new(ring, mu, 0);
448        let honest = gadget.wires(&pad);
449        let delta = next(&mut state).to_hardware();
450
451        let zero = Flat::from_raw(Block128::ZERO);
452
453        for target in 0..honest.len() {
454            for role in 0..3 {
455                let mut wires = honest.clone();
456                wires[target][role] += delta;
457
458                let row_broken = chain_rows(&gadget, &pad, &wires)
459                    .iter()
460                    .any(|acc| *acc != zero);
461                let hadamard_broken = wires.iter().any(|w| w[0] * w[1] != w[2]);
462
463                assert!(
464                    row_broken || hadamard_broken,
465                    "wire {target} role {role} perturbation went undetected"
466                );
467            }
468        }
469    }
470
471    #[test]
472    fn tie_weights_equal_scaled_tie_rows() {
473        let mut state = 0x0123_4567_89ab_cdef_fedc_ba98_7654_3210u128;
474        for len in [1u32, 9, 40] {
475            let r_mix: Vec<Block128> = (0..7).map(|_| next(&mut state)).collect();
476            let mu = linearized_coeffs(&eq_tensor_b(&r_mix));
477
478            let ring: Vec<(u32, Flat<Block128>)> = (0..len)
479                .map(|c| (3 * c + 1, next(&mut state).to_hardware()))
480                .collect();
481
482            let gadget = RingGadget::new(ring, mu, 0);
483
484            let scales: Vec<Flat<Block128>> = (0..CHAIN_ROWS)
485                .map(|_| next(&mut state).to_hardware())
486                .collect();
487
488            let mut expected: Vec<(u32, Flat<Block128>)> = gadget
489                .ring
490                .iter()
491                .map(|&(pad, _)| (pad, Flat::from_raw(Block128::ZERO)))
492                .collect();
493
494            gadget.for_each_tie_row(|j, tie| {
495                for ((_, w), &t) in expected.iter_mut().zip(tie) {
496                    *w += scales[gadget.row_of(j)] * t;
497                }
498            });
499
500            let mut folded = Vec::new();
501            gadget.for_each_tie_weight(&scales, |pad, w| folded.push((pad, w)));
502
503            assert_eq!(folded, expected);
504        }
505    }
506}