use crate::fs::FiatShamir;
use nebu::{Fp3, Goldilocks};
use super::{Air, Vals};
pub(crate) fn combine<A: Air>(
air: &A,
w: usize,
vals: &[Fp3],
ch: &[Fp3],
mu: &[Fp3],
scratch: &mut [Fp3],
) -> Fp3 {
let v = Vals {
local: &vals[..w],
next: &vals[w..2 * w],
publics: &vals[2 * w..],
};
for s in scratch.iter_mut() {
*s = Fp3::ZERO;
}
air.eval(&v, ch, scratch);
scratch
.iter()
.zip(mu)
.fold(Fp3::ZERO, |acc, (&c, &m)| acc + c * m)
}
pub(crate) fn interpolate(values: &[Fp3], x: Fp3) -> Fp3 {
let n = values.len();
let mut acc = Fp3::ZERO;
for (i, &vi) in values.iter().enumerate() {
let mut num = Fp3::ONE;
let mut den = Goldilocks::ONE;
for j in 0..n {
if j != i {
num *= x - Fp3::from_base(Goldilocks::new(j as u64));
den *= Goldilocks::new(i as u64) - Goldilocks::new(j as u64);
}
}
let d = den.inv();
acc += vi * Fp3::new(num.c0 * d, num.c1 * d, num.c2 * d);
}
acc
}
fn threads() -> usize {
std::thread::available_parallelism()
.map_or(1, |n| n.get())
.min(16)
}
#[allow(clippy::too_many_arguments)]
fn round<A: Air>(
air: &A,
w: usize,
cols: &[Vec<Fp3>],
eq: &[Fp3],
ch: &[Fp3],
mu: &[Fp3],
deg: usize,
on_cube: bool,
skip_one: bool,
) -> Vec<Fp3> {
let pairs = eq.len() / 2;
let first = if on_cube { 2 } else { 0 };
if on_cube && let Some(h) = round_mix(air, w, cols, eq, ch, mu, deg, first) {
return h;
}
let k = air.shape().constraints;
let workers = threads().min(pairs.max(1));
let chunk = pairs.div_ceil(workers);
let parts: Vec<Vec<Fp3>> = std::thread::scope(|s| {
let handles: Vec<_> = (0..workers)
.map(|wi| {
s.spawn(move || {
let mut h = vec![Fp3::ZERO; deg + 1];
let mut vals = vec![Fp3::ZERO; cols.len()];
let mut step = vec![Fp3::ZERO; cols.len()];
let mut scratch = vec![Fp3::ZERO; k];
let lo_i = wi * chunk;
let hi_i = ((wi + 1) * chunk).min(pairs);
let start = Fp3::from_base(Goldilocks::new(first as u64));
for i in lo_i..hi_i {
let (e0, e1) = (eq[2 * i], eq[2 * i + 1]);
for ((v, d), c) in vals.iter_mut().zip(step.iter_mut()).zip(cols) {
let (a, b) = (c[2 * i], c[2 * i + 1]);
*d = b - a;
*v = if first == 0 { a } else { a + start * *d };
}
let g = e0 + e1;
if g == Fp3::ZERO {
continue;
}
for (x, hx) in h.iter_mut().enumerate().take(deg).skip(first) {
if x > first {
for (v, d) in vals.iter_mut().zip(&step) {
*v += *d;
}
}
if x == 1 && skip_one {
continue;
}
*hx += g * combine(air, w, &vals, ch, mu, &mut scratch);
}
}
h
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("zerocheck worker"))
.collect()
});
let mut h = vec![Fp3::ZERO; deg + 1];
for p in parts {
for (a, b) in h.iter_mut().zip(p) {
*a += b;
}
}
h
}
#[allow(clippy::too_many_arguments)]
fn round_mix<A: Air>(
air: &A,
w: usize,
cols: &[Vec<Fp3>],
eq: &[Fp3],
ch: &[Fp3],
mu: &[Fp3],
deg: usize,
first: usize,
) -> Option<Vec<Fp3>> {
use super::mix::Mix;
let pairs = eq.len() / 2;
let k = air.shape().constraints;
{
let z = vec![Mix::B(Goldilocks::ZERO); cols.len()];
let mut out = vec![Mix::B(Goldilocks::ZERO); k];
let v = Vals {
local: &z[..w],
next: &z[w..2 * w],
publics: &z[2 * w..],
};
if !air.eval_mix(&v, ch, &mut out) {
return None;
}
}
let workers = threads().min(pairs.max(1));
let chunk = pairs.div_ceil(workers);
let parts: Vec<Vec<Fp3>> = std::thread::scope(|s| {
let handles: Vec<_> = (0..workers)
.map(|wi| {
s.spawn(move || {
let mut h = vec![Fp3::ZERO; deg + 1];
let mut vals = vec![Mix::B(Goldilocks::ZERO); cols.len()];
let mut step = vec![Mix::B(Goldilocks::ZERO); cols.len()];
let mut scratch = vec![Mix::B(Goldilocks::ZERO); k];
let start = Mix::B(Goldilocks::new(first as u64));
for i in wi * chunk..((wi + 1) * chunk).min(pairs) {
let (e0, e1) = (eq[2 * i], eq[2 * i + 1]);
for ((v, d), c) in vals.iter_mut().zip(step.iter_mut()).zip(cols) {
let (a, b) = (Mix::of(c[2 * i]), Mix::of(c[2 * i + 1]));
*d = b - a;
*v = a + start * *d;
}
let g = e0 + e1;
if g == Fp3::ZERO {
continue;
}
for (x, hx) in h.iter_mut().enumerate().take(deg).skip(first) {
if x > first {
for (v, d) in vals.iter_mut().zip(&step) {
*v += *d;
}
}
for sc in scratch.iter_mut() {
*sc = Mix::B(Goldilocks::ZERO);
}
let v = Vals {
local: &vals[..w],
next: &vals[w..2 * w],
publics: &vals[2 * w..],
};
air.eval_mix(&v, ch, &mut scratch);
let c = scratch
.iter()
.zip(mu)
.fold(Fp3::ZERO, |acc, (&c, &m)| acc + c.scale(m));
*hx += g * c;
}
}
h
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("zerocheck worker"))
.collect()
});
let mut h = vec![Fp3::ZERO; deg + 1];
for p in parts {
for (a, b) in h.iter_mut().zip(p) {
*a += b;
}
}
Some(h)
}
const DEVICE_MIN: usize = 1 << 10;
const CPU_TAIL: usize = 1 << 8;
fn program(g: &super::num::Graph) -> lens::rspcs::program::Program {
let c = g.compile();
let (ops, consts, inputs, outputs) = c.parts();
let consts = consts
.iter()
.map(|&x| lens::rspcs::program::from_fp3(x))
.collect();
lens::rspcs::program::Program::from_slots(ops, consts, inputs, outputs[0])
}
fn pack_flat(cols: &[Vec<Fp3>]) -> Vec<[u64; 3]> {
use lens::rspcs::program::from_fp3;
let len = cols.first().map_or(0, Vec::len);
let mut flat = vec![[0u64; 3]; cols.len() * len];
let per = cols.len().div_ceil(threads()).max(1);
std::thread::scope(ļææsļææ {
for (chunk, cs) in flat.chunks_mut(len * per).zip(cols.chunks(per)) {
s.spawn(move ļææļææ {
for (dst, c) in chunk.chunks_mut(len).zip(cs) {
for (d, v) in dst.iter_mut().zip(c) {
*d = from_fp3(*v);
}
}
});
}
});
flat
}
fn round_points(deg: usize, on_cube: bool, skip_one: bool) -> Vec<u64> {
let first = if on_cube { 2 } else { 0 };
(first..deg)
.filter(ļææ&xļææ !(x == 1 && skip_one))
.map(ļææxļææ x as u64)
.collect()
}
fn device_round(
p: &lens::rspcs::program::Program,
cols: &[Vec<Fp3>],
eq: &[Fp3],
deg: usize,
on_cube: bool,
skip_one: bool,
) -> Vec<Fp3> {
use lens::rspcs::program::{from_fp3, to_fp3};
let xs = round_points(deg, on_cube, skip_one);
let flat = pack_flat(cols);
let weights: Vec<[u64; 3]> = eq
.as_chunks::<2>()
.0
.iter()
.map(ļææeļææ from_fp3(e[0] + e[1]))
.collect();
let sums = lens::rspcs::backend::current().line_sums(p, &flat, &weights, &xs);
let mut q = vec![Fp3::ZERO; deg + 1];
for (&x, v) in xs.iter().zip(sums) {
q[x as usize] = to_fp3(v);
}
q
}
fn fold(v: &mut Vec<Fp3>, alpha: Fp3) {
let half = v.len() / 2;
for i in 0..half {
let a = v[2 * i];
v[i] = a + alpha * (v[2 * i + 1] - a);
}
v.truncate(half);
}
#[allow(clippy::type_complexity)]
pub(crate) fn prove<A: Air>(
air: &A,
w: usize,
cols: Vec<Vec<Fp3>>,
eq: Vec<Fp3>,
ch: &[Fp3],
mu: &[Fp3],
t: &mut impl FiatShamir,
) -> (Vec<Vec<Fp3>>, Vec<Fp3>, Vec<Fp3>) {
prove_packed(air, w, cols, None, eq, ch, mu, t)
}
pub(crate) fn packed(cols: &[Vec<Fp3>]) -> Vec<[u64; 3]> {
pack_flat(cols)
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub(crate) fn prove_packed<A: Air>(
air: &A,
w: usize,
mut cols: Vec<Vec<Fp3>>,
flat: Option<Vec<[u64; 3]>>,
mut eq: Vec<Fp3>,
ch: &[Fp3],
mu: &[Fp3],
t: &mut impl FiatShamir,
) -> (Vec<Vec<Fp3>>, Vec<Fp3>, Vec<Fp3>) {
let deg = air.shape().degree + 1;
let n = eq.len().trailing_zeros() as usize;
let mut msgs = Vec::with_capacity(n);
let mut point = Vec::with_capacity(n);
let mut claim = Fp3::ZERO;
let backend = lens::rspcs::backend::current();
let device = (backend.name() != "cpu")
.then(ļææļææ air.combine_graph(cols.len(), ch, mu))
.flatten()
.map(ļæægļææ program(&g));
let mut session = device
.as_ref()
.and_then(ļææpļææ backend.line_session(p, &flat.unwrap_or_else(ļææļææ pack_flat(&cols)), eq.len()));
if session.is_some() {
cols = Vec::new();
}
for r in 0..n {
if eq.len() / 2 <= CPU_TAIL
&& let Some(mut sess) = session.take()
{
let len = eq.len();
let flat = sess.columns();
cols = flat
.chunks_exact(len)
.map(ļææcļææ c.iter().map(ļææ&vļææ lens::rspcs::program::to_fp3(v)).collect())
.collect();
}
let (l0, l1) = eq
.as_chunks::<2>()
.0
.iter()
.find(ļææpļææ p[0] + p[1] != Fp3::ZERO)
.map_or((Fp3::ZERO, Fp3::ZERO), ļææpļææ {
let gi = (p[0] + p[1]).inv();
(p[0] * gi, p[1] * gi)
});
let skip_one = r > 0 && l1 != Fp3::ZERO;
let mut q = match (&mut session, &device) {
(Some(sess), _) => {
let xs = round_points(deg, r == 0, skip_one);
let weights: Vec<[u64; 3]> = eq
.as_chunks::<2>()
.0
.iter()
.map(ļææeļææ lens::rspcs::program::from_fp3(e[0] + e[1]))
.collect();
let mut q = vec![Fp3::ZERO; deg + 1];
for (&x, v) in xs.iter().zip(sess.sums(&weights, &xs)) {
q[x as usize] = lens::rspcs::program::to_fp3(v);
}
q
}
(None, Some(p)) if eq.len() >= DEVICE_MIN && eq.len() / 2 > CPU_TAIL => {
device_round(p, &cols, &eq, deg, r == 0, skip_one)
}
_ => round(air, w, &cols, &eq, ch, mu, deg, r == 0, skip_one),
};
if skip_one {
q[1] = (claim - l0 * q[0]) * l1.inv();
}
q[deg] = interpolate(&q[..deg], Fp3::from_base(Goldilocks::new(deg as u64)));
let h: Vec<Fp3> = q
.iter()
.enumerate()
.map(ļææ(x, &qx)ļææ {
let xf = Fp3::from_base(Goldilocks::new(x as u64));
(l0 + xf * (l1 - l0)) * qx
})
.collect();
let msg: Vec<Fp3> = core::iter::once(h[0])
.chain(h[2..].iter().copied())
.collect();
t.absorb_fp3_slice(&msg);
msgs.push(msg);
let alpha = t.squeeze_fp3();
claim = interpolate(&h, alpha);
match &mut session {
Some(sess) => sess.fold(lens::rspcs::program::from_fp3(alpha)),
None => {
for c in &mut cols {
fold(c, alpha);
}
}
}
fold(&mut eq, alpha);
point.push(alpha);
}
let evals = match &mut session {
Some(sess) => sess
.firsts()
.into_iter()
.map(lens::rspcs::program::to_fp3)
.collect(),
None => cols.iter().map(ļææcļææ c[0]).collect(),
};
(msgs, point, evals)
}
pub(crate) fn verify(
t: &mut impl FiatShamir,
msgs: &[Vec<Fp3>],
n: usize,
degree: usize,
) -> Option<(Vec<Fp3>, Fp3)> {
let deg = degree + 1;
if msgs.len() != n ļææļææ msgs.iter().any(ļææmļææ m.len() != deg) {
return None;
}
let mut claim = Fp3::ZERO;
let mut point = Vec::with_capacity(n);
for msg in msgs {
let mut h = Vec::with_capacity(deg + 1);
h.push(msg[0]);
h.push(claim - msg[0]);
h.extend_from_slice(&msg[1..]);
t.absorb_fp3_slice(msg);
let alpha = t.squeeze_fp3();
claim = interpolate(&h, alpha);
point.push(alpha);
}
Some((point, claim))
}
#[cfg(test)]
mod device_tests {
use super::*;
use crate::machine::air::{Constants, Machine};
use crate::recursion::relation::{COLS, PUBS, Relation, View};
fn e(k: u64) -> Fp3 {
Fp3::new(
Goldilocks::new(k * 31 + 7),
Goldilocks::new(k ^ 0x55),
Goldilocks::new(k * k + 1),
)
}
struct Device;
impl lens::rspcs::backend::Backend for Device {
fn name(&self) -> &'static str {
"device-test"
}
fn line_session<'a>(
&'a self,
p: &'a lens::rspcs::program::Program,
cols: &span>; 3,
len: usize,
) -> Option<Box<dyn lens::rspcs::backend::LineSession + 'a>> {
Some(Box::new(lens::rspcs::backend::CpuSession::new(
p, cols, len,
)))
}
}
#[test]
fn a_resident_session_proves_as_the_air() {
use crate::recursion::sponge::ProverTranscript;
let n = 10;
let constants = Constants {
fml0: 3,
obj0: 4,
p: 9,
output: [Goldilocks::new(5); 4],
cycles: 77,
root: [Goldilocks::ZERO; 4],
};
let rel = Relation::new(
Machine::new(constants, &[], 1 << n, 0, 1 << n),
[e(1), e(2)],
);
let view = View(&rel);
let inputs = 2 * COLS + PUBS;
let cols: Vec<Vec<Fp3>> = (0..inputs as u64)
.map(|c| (0..1u64 << n).map(|r| e(c * 97 + r)).collect())
.collect();
let eq: Vec<Fp3> = (0..1u64 << n).map(|r| e(r + 5000)).collect();
let ch = [e(11), e(12)];
let mu: Vec<Fp3> = (0..rel.constraints() as u64).map(|k| e(k + 9000)).collect();
let mut t1 = ProverTranscript::new(1);
let want = prove(&view, COLS, cols.clone(), eq.clone(), &ch, &mu, &mut t1);
let prev = lens::rspcs::backend::install(std::sync::Arc::new(Device));
let mut t2 = ProverTranscript::new(1);
let got = prove(&view, COLS, cols, eq, &ch, &mu, &mut t2);
lens::rspcs::backend::install(prev);
assert_eq!(got, want);
}
#[test]
fn device_rounds_equal_the_air_rounds() {
let n = 10;
let constants = Constants {
fml0: 3,
obj0: 4,
p: 9,
output: [Goldilocks::new(5); 4],
cycles: 77,
root: [Goldilocks::ZERO; 4],
};
let rel = Relation::new(
Machine::new(constants, &[], 1 << n, 0, 1 << n),
[e(1), e(2)],
);
let view = View(&rel);
let inputs = 2 * COLS + PUBS;
let cols: Vec<Vec<Fp3>> = (0..inputs as u64)
.map(|c| (0..1u64 << n).map(|r| e(c * 97 + r)).collect())
.collect();
let eq: Vec<Fp3> = (0..1u64 << n).map(|r| e(r + 5000)).collect();
let ch = [e(11), e(12)];
let mu: Vec<Fp3> = (0..rel.constraints() as u64).map(|k| e(k + 9000)).collect();
let g = view.combine_graph(inputs, &ch, &mu).expect("graph");
let p = program(&g);
let deg = view.shape().degree + 1;
for (cube, skip) in [(false, false), (false, true), (true, false)] {
let want = round(&view, COLS, &cols, &eq, &ch, &mu, deg, cube, skip);
let got = device_round(&p, &cols, &eq, deg, cube, skip);
for x in 0..deg {
if (cube && x < 2) || (skip && x == 1) {
continue;
}
assert_eq!(got[x], want[x], "x {x} cube {cube} skip {skip}");
}
}
}
}