diff --git a/Cargo.lock b/Cargo.lock index 63a9b63e6..828521c2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1604,6 +1604,7 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_cbor", + "sha3", "tempfile", "test-log", "thiserror", diff --git a/crypto/crypto/src/merkle_tree/cap.rs b/crypto/crypto/src/merkle_tree/cap.rs new file mode 100644 index 000000000..b23cb3669 --- /dev/null +++ b/crypto/crypto/src/merkle_tree/cap.rs @@ -0,0 +1,1279 @@ +//! Merkle caps: authentication paths that stop `c` levels below the root. +//! +//! A tree of depth `D` (so `2^D` padded leaves) has, at height `c`, the `2^c` +//! nodes that sit `c` levels below its root — its **cap**. A proof can carry a +//! tree's cap once and cut every authentication path of that tree to its first +//! `D − c` siblings: the verifier folds a path up to the cap node +//! `cap[index >> (D − c)]` instead of the root, and checks once per tree that +//! the cap hashes up to the committed root (`2^c − 1` compressions). +//! +//! **The root stays the commitment.** Nothing about the transcript changes: +//! the root is what is absorbed, and a second cap with the same root is a +//! compression collision. Any capped acceptance extends to a full-path +//! acceptance (append the cap-to-root computation above the cap node), so the +//! query-phase bound is the one the full paths had. +//! +//! **Four checks are load-bearing** — dropping any one is a soundness break: +//! 1. `cap.len() == 2^c` and `cap_root(cap) == root`, once per tree +//! ([`verify_cap`], run by [`CappedRoot::from_owner`]); +//! 2. every path is exactly `D − c` siblings long, and the owner path exactly +//! `D − c + 2^c` ([`verify_merkle_path_to_cap_from_leaf_hash`], +//! [`split_owner_path`]); +//! 3. the cap node is `cap[index >> (D − c)]` with `index < 2^D`, the index +//! being the transcript's; +//! 4. `c` itself is a verifier constant ([`CapPolicy::height`] of public shape +//! data), never read from the proof. +//! +//! At `c = 0` the cap is `[root]` and the capped check is exactly +//! [`verify_merkle_path_from_leaf_hash`] plus the two exact-length checks. +//! +//! **Wire encoding (the owner path).** A tree's cap rides at the end of the +//! authentication path of that tree's first opening in proof order +//! ([`embed_cap`] / [`split_owner_path`]); every other opening of the tree +//! carries exactly `D − c` siblings. At `c = 0` nothing moves, so the default +//! proof bytes are today's by construction. + +use alloc::vec::Vec; +use core::fmt; +use core::str::FromStr; + +use super::proof::{Proof, verify_merkle_path_from_leaf_hash}; +use super::traits::IsMerkleTreeBackend; + +/// The tallest cap any policy may ask for. A proof-size guard: a cap costs +/// `2^c` digests per tree. The `Auto` policy never exceeds 3. +pub const MAX_CAP_HEIGHT: usize = 16; + +/// Why a cap operation refused its input. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CapError { + /// The cap height exceeds the tree depth, or [`MAX_CAP_HEIGHT`]. + CapTooTall { cap_height: usize, depth: usize }, + /// A path did not have the exact length the shape requires. + PathLength { expected: usize, got: usize }, + /// A cap whose length is not a power of two. + CapLength(usize), + /// A cap with no opening to carry it. + NoOwner, +} + +impl fmt::Display for CapError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CapTooTall { cap_height, depth } => write!( + f, + "cap height {cap_height} exceeds the tree depth {depth} or the maximum {MAX_CAP_HEIGHT}" + ), + Self::PathLength { expected, got } => { + write!( + f, + "authentication path has {got} nodes, expected {expected}" + ) + } + Self::CapLength(len) => write!(f, "cap of {len} nodes is not a power of two"), + Self::NoOwner => write!(f, "a cap needs at least one opening to carry it"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CapError {} + +/// `log2(cap.len())` when the cap is a non-empty power of two no taller than +/// [`MAX_CAP_HEIGHT`]. +fn cap_height_of(cap: &[N]) -> Option { + let len = cap.len(); + if !len.is_power_of_two() { + return None; + } + let c = len.ilog2() as usize; + (c <= MAX_CAP_HEIGHT).then_some(c) +} + +/// `c ≤ depth`, `c ≤ MAX_CAP_HEIGHT`, and `depth` small enough that `2^depth` +/// is a `usize`. +fn shape_ok(depth: usize, cap_height: usize) -> bool { + cap_height <= depth && cap_height <= MAX_CAP_HEIGHT && depth < usize::BITS as usize +} + +impl Proof { + /// Keep the first `depth − cap_height` siblings of a full path. + /// + /// Refuses unless the path is exactly `depth` long and the cap fits the + /// tree, so a path that was already cut, or one of another tree, is not + /// silently cut again. + pub fn truncate_to_cap(&mut self, depth: usize, cap_height: usize) -> Result<(), CapError> { + if !shape_ok(depth, cap_height) { + return Err(CapError::CapTooTall { cap_height, depth }); + } + if self.merkle_path.len() != depth { + return Err(CapError::PathLength { + expected: depth, + got: self.merkle_path.len(), + }); + } + self.merkle_path.truncate(depth - cap_height); + Ok(()) + } +} + +/// The root of a cap: the standard bottom-up build over the cap as leaves, +/// `2^c − 1` compressions (none at `c = 0`). `None` unless `cap.len()` is a +/// power of two `≥ 1` no taller than [`MAX_CAP_HEIGHT`]. +pub fn cap_root(cap: &[B::Node]) -> Option { + cap_height_of(cap)?; + let mut level: Vec = cap.to_vec(); + while level.len() > 1 { + level = level + .chunks_exact(2) + .map(|pair| B::hash_new_parent(&pair[0], &pair[1])) + .collect(); + } + level.pop() +} + +/// `cap.len() == 2^cap_height` and the cap hashes up to `root`. +pub fn verify_cap( + cap: &[B::Node], + root: &B::Node, + cap_height: usize, +) -> bool { + if cap_height > MAX_CAP_HEIGHT || cap.len() != 1usize << cap_height { + return false; + } + cap_root::(cap).is_some_and(|r| &r == root) +} + +/// The capped inclusion check for one opening. +/// +/// `c = log2(cap.len())`. Accepts iff +/// `siblings.len() == depth − c`, `index < 2^depth`, `cap.len() == 2^c ≤ 2^depth`, +/// and the existing fold ([`verify_merkle_path_from_leaf_hash`], unchanged) +/// of `leaf_hash` along `siblings` lands on `cap[index >> (depth − c)]`. +/// +/// It does NOT check the cap against a root — that is [`verify_cap`], once per +/// tree. [`CappedRoot`] ties the two together. +pub fn verify_merkle_path_to_cap_from_leaf_hash( + siblings: &[B::Node], + cap: &[B::Node], + depth: usize, + index: usize, + leaf_hash: B::Node, +) -> bool { + let Some(c) = cap_height_of(cap) else { + return false; + }; + if !shape_ok(depth, c) || siblings.len() != depth - c || index >> depth != 0 { + return false; + } + verify_merkle_path_from_leaf_hash::(siblings, &cap[index >> (depth - c)], index, leaf_hash) +} + +/// Split an owner path (the wire encoding) into `(siblings, cap)`. +/// +/// At `c = 0` the whole path is siblings (its length must be `depth`) and the +/// cap is empty — the caller uses the root. At `c ≥ 1` the length must be +/// exactly `depth − c + 2^c`. `None` on any other length or shape. +pub fn split_owner_path(path: &[N], depth: usize, cap_height: usize) -> Option<(&[N], &[N])> { + if !shape_ok(depth, cap_height) { + return None; + } + let siblings = depth - cap_height; + let expected = if cap_height == 0 { + depth + } else { + siblings + (1usize << cap_height) + }; + (path.len() == expected).then(|| path.split_at(siblings)) +} + +/// Prover side of the owner-path encoding: cut every path of one tree to +/// `depth − c` siblings and append the cap to `paths[0]`, the tree's first +/// opening in proof order. `c = log2(cap.len())`. +/// +/// Every path must be a full `depth`-long path (checked). At `c = 0` (a cap of +/// one node, the root) it changes nothing. +pub fn embed_cap( + paths: &mut [&mut Vec], + depth: usize, + cap: &[N], +) -> Result<(), CapError> { + let c = cap_height_of(cap).ok_or(CapError::CapLength(cap.len()))?; + if !shape_ok(depth, c) { + return Err(CapError::CapTooTall { + cap_height: c, + depth, + }); + } + for path in paths.iter() { + if path.len() != depth { + return Err(CapError::PathLength { + expected: depth, + got: path.len(), + }); + } + } + if c == 0 { + return Ok(()); + } + let Some((owner, rest)) = paths.split_first_mut() else { + return Err(CapError::NoOwner); + }; + owner.truncate(depth - c); + owner.extend_from_slice(cap); + for path in rest { + path.truncate(depth - c); + } + Ok(()) +} + +/// One tree's authenticated cap: built once per tree, then used for every +/// opening of that tree. +/// +/// The only constructors are [`CappedRoot::uncapped`] (`c = 0`, the cap is the +/// root itself) and [`CappedRoot::from_owner`], which runs [`verify_cap`] +/// against the root before it hands the cap out — so a `CappedRoot` never +/// holds an unauthenticated cap. +#[derive(Debug, Clone, Copy)] +pub struct CappedRoot<'a, N> { + depth: usize, + cap_height: usize, + cap: &'a [N], +} + +impl<'a, N: PartialEq + Eq + Clone> CappedRoot<'a, N> { + /// `c = 0`: every path must be exactly `depth` long and fold to `root`. + pub fn uncapped(root: &'a N, depth: usize) -> Self { + Self { + depth, + cap_height: 0, + cap: core::slice::from_ref(root), + } + } + + /// Split the owner path, authenticate its cap against `root` once, and + /// return the owner's own siblings. + /// + /// Only the cap is checked here. The owner's opening is still an opening: + /// the caller must run [`verify`](Self::verify) on the returned siblings + /// like on any other path. `None` on a wrong length or a cap that does not + /// hash to `root`. + pub fn from_owner>( + root: &'a N, + owner_path: &'a [N], + depth: usize, + cap_height: usize, + ) -> Option<(Self, &'a [N])> { + let (siblings, cap) = split_owner_path(owner_path, depth, cap_height)?; + if cap_height == 0 { + return Some((Self::uncapped(root, depth), siblings)); + } + if !verify_cap::(cap, root, cap_height) { + return None; + } + Some(( + Self { + depth, + cap_height, + cap, + }, + siblings, + )) + } + + /// Check one opening: exactly `depth − c` siblings folding `leaf_hash` at + /// `index` onto its cap node. + pub fn verify>( + &self, + siblings: &[N], + index: usize, + leaf_hash: N, + ) -> bool { + verify_merkle_path_to_cap_from_leaf_hash::( + siblings, self.cap, self.depth, index, leaf_hash, + ) + } + + pub fn depth(&self) -> usize { + self.depth + } + + pub fn cap_height(&self) -> usize { + self.cap_height + } + + /// The authenticated cap (`[root]` at `c = 0`). + pub fn cap(&self) -> &'a [N] { + self.cap + } +} + +// =========================================================================== +// The cap-height policy +// =========================================================================== + +/// How tall a cap each tree gets. A proof-format parameter: the prover and +/// every verifier (host and in-guest) derive the same height from public +/// shape data through [`CapPolicy::height`]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum CapPolicy { + /// No cap: every path runs to the root. Today's format. + #[default] + Off, + /// The height that minimises the in-guest verifier's cost-law price + /// ([`AUTO_WEIGHTS`]); 3 for a tree opened ≥ 20 times, 2 for 4–19, 0 + /// below, clamped to the tree depth. + Auto, + /// This height for every opened tree, clamped to its depth and to + /// [`MAX_CAP_HEIGHT`]. `Fixed(0)` is `Off`. + Fixed(u8), +} + +/// Per-row prices (ns) of the in-guest verifier operations a cap trades, from +/// the node cost law (421 ns/instruction + 5.63 ns/cell) and the committed +/// widths of the chips that execute them. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CapWeights { + /// One two-to-one compression (an `LFM_HASH` row). + pub compress: u64, + /// One two-way `Select`. + pub select: u64, + /// One `Unpack` (a digest compared as lanes). + pub unpack: u64, + /// One hinted word. + pub hint: u64, + /// One digest-equals-root comparison. + pub compare: u64, +} + +/// The weights [`CapPolicy::Auto`] optimises. ⚠ A FORMAT CONSTANT: changing +/// any of them changes the cap heights, and so the proofs, of every tree under +/// `Auto`. Pinned by the policy tests. +pub const AUTO_WEIGHTS: CapWeights = CapWeights { + compress: 2251, + select: 567, + unpack: 528, + hint: 460, + compare: 3789, +}; + +/// The in-guest saving (ns, cost-law units) of a height-`c` cap on a tree +/// opened `openings` times. Integer arithmetic only, so every verifier +/// reproduces it exactly. +/// +/// Signed and bounded: the gain is negative for few openings, so it is an +/// `i128`, and every term fits it for any `usize` opening count because +/// `cap_height` is refused past [`MAX_CAP_HEIGHT`] (the result is then +/// `i128::MIN`, a height no argmax picks) — no shift or product can overflow on +/// any target, 32-bit `wasm` included. `gain(o, 0) = 0`; for `c ≥ 1`: +/// +/// ```text +/// o·( c·(compress + select) − (2^c − 1)·select − unpack ) +/// − ( (2^c − 1)·compress + 2^c·hint + compare ) +/// ``` +/// +/// Per opening the walk loses `c` levels (a `Select` and a compression each), +/// the cap mux adds `2^c − 1` selects and the variable-cell compare one +/// `Unpack`; per tree the cap costs `2^c − 1` compressions to its root, `2^c` +/// hints and one root compare. +pub fn cap_gain(weights: &CapWeights, openings: usize, cap_height: usize) -> i128 { + if cap_height == 0 { + return 0; + } + if cap_height > MAX_CAP_HEIGHT { + return i128::MIN; + } + let o = openings as i128; + let c = cap_height as i128; + let nodes = 1i128 << cap_height; + let w = |x: u64| x as i128; + let per_opening = c * (w(weights.compress) + w(weights.select)) + - (nodes - 1) * w(weights.select) + - w(weights.unpack); + let per_tree = (nodes - 1) * w(weights.compress) + nodes * w(weights.hint) + w(weights.compare); + o * per_opening - per_tree +} + +impl CapPolicy { + /// True when this policy caps nothing (`Off` or `Fixed(0)`). + pub const fn is_off(self) -> bool { + matches!(self, Self::Off | Self::Fixed(0)) + } + + /// The cap height of a tree of `depth` levels opened `openings` times. + /// Always `≤ depth` and `≤ MAX_CAP_HEIGHT`, and 0 for an unopened tree. + /// + /// `Auto` is a fixed table, stated directly — 3 for a tree opened at + /// least [`AUTO_CAP3_MIN_OPENINGS`] times, 2 from + /// [`AUTO_CAP2_MIN_OPENINGS`], 0 below — then clamped to the depth. No + /// arithmetic runs at all, so no verifier can disagree on an overflow. + /// The table is the argmax of [`cap_gain`] under [`AUTO_WEIGHTS`] for + /// every opening count (pinned by a test over all counts up to 10^6 and + /// at the `usize` extremes). + pub fn height(self, openings: usize, depth: usize) -> usize { + if openings == 0 { + return 0; + } + let limit = depth.min(MAX_CAP_HEIGHT); + match self { + Self::Off => 0, + Self::Fixed(c) => (c as usize).min(limit), + Self::Auto => { + let c = if openings >= AUTO_CAP3_MIN_OPENINGS { + 3 + } else if openings >= AUTO_CAP2_MIN_OPENINGS { + 2 + } else { + 0 + }; + c.min(limit) + } + } + } +} + +/// `Auto` gives a height-3 cap to a tree opened at least this many times +/// ⚠ A FORMAT CONSTANT, like [`AUTO_WEIGHTS`]. +pub const AUTO_CAP3_MIN_OPENINGS: usize = 20; + +/// `Auto` gives a height-2 cap to a tree opened at least this many times and +/// fewer than [`AUTO_CAP3_MIN_OPENINGS`]. ⚠ A FORMAT CONSTANT. +pub const AUTO_CAP2_MIN_OPENINGS: usize = 4; + +impl fmt::Display for CapPolicy { + /// `off`, `auto`, or the fixed height (`Fixed(0)` prints `off`) — the + /// spelling the `LAMBDA_VM_ZF_*CAP` knobs accept. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Off | Self::Fixed(0) => f.write_str("off"), + Self::Auto => f.write_str("auto"), + Self::Fixed(c) => write!(f, "{c}"), + } + } +} + +/// A cap-policy spelling that is none of `off`, `auto`, `0..=16`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParseCapPolicyError; + +impl fmt::Display for ParseCapPolicyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "expected `off`, `auto`, or a cap height 0..={MAX_CAP_HEIGHT}" + ) + } +} + +impl FromStr for CapPolicy { + type Err = ParseCapPolicyError; + + /// `off` | `auto` | an integer `0..=MAX_CAP_HEIGHT` (`0` is `off`). + /// Exact spellings only: no case folding, no whitespace. + fn from_str(s: &str) -> Result { + match s { + "off" => Ok(Self::Off), + "auto" => Ok(Self::Auto), + _ => { + // `u8::from_str` accepts a leading `+`; the knob does not. + if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) { + return Err(ParseCapPolicyError); + } + match s.parse::() { + Ok(0) => Ok(Self::Off), + Ok(c) if c as usize <= MAX_CAP_HEIGHT => Ok(Self::Fixed(c)), + _ => Err(ParseCapPolicyError), + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::merkle_tree::backends::types::BatchKeccak256Backend; + use crate::merkle_tree::merkle::MerkleTree; + use alloc::string::ToString; + use math::field::{element::FieldElement, goldilocks::GoldilocksField}; + + type F = GoldilocksField; + type Fe = FieldElement; + type K = BatchKeccak256Backend; + type Node = [u8; 32]; + + fn leaves(n: usize, salt: u64) -> Vec> { + (0..n as u64) + .map(|i| vec![Fe::from(i * 7 + salt), Fe::from(i ^ 0x55 ^ salt)]) + .collect() + } + + fn tree(n: usize, salt: u64) -> MerkleTree { + MerkleTree::::build(&leaves(n, salt)).expect("non-empty") + } + + /// The leaf data at position `p` of the padded tree (padding repeats the + /// last leaf). + fn leaf_at(data: &[Vec], p: usize) -> &Vec { + &data[p.min(data.len() - 1)] + } + + const LEAF_COUNTS: &[usize] = &[1, 2, 3, 4, 5, 8, 16, 32, 64, 128, 256, 512, 1024]; + + // ---------------------------------------------------------------- primitive + + #[test] + fn cap_is_the_heap_slice_and_hashes_to_the_root() { + for &n in LEAF_COUNTS { + let t = tree(n, 1); + let d = t.depth().unwrap(); + assert_eq!(1usize << d, n.next_power_of_two(), "n={n}"); + for c in 0..=d { + let cap = t.cap(c).unwrap(); + assert_eq!(cap.len(), 1 << c); + assert_eq!( + &cap[..], + &t.nodes()[(1 << c) - 1..(2 << c) - 1], + "n={n} c={c}" + ); + assert_eq!(cap_root::(&cap), Some(t.root), "n={n} c={c}"); + assert!(verify_cap::(&cap, &t.root, c), "n={n} c={c}"); + } + assert_eq!(t.cap(0).unwrap(), vec![t.root]); + assert!(t.cap(d + 1).is_none(), "c > depth must be None (n={n})"); + } + } + + #[test] + fn root_only_tree_has_no_depth_and_no_cap() { + let t = MerkleTree::::from_root([7u8; 32]); + assert_eq!(t.depth(), None); + assert!(t.cap(0).is_none()); + } + + /// Every leaf of every tree verifies against its cap node at every height, + /// and at `c = 0` the capped check agrees with the full-path check. + fn every_leaf_verifies(verify: impl Fn(&[Node], &[Node], usize, usize, Node) -> bool) -> bool { + for &n in LEAF_COUNTS { + let data = leaves(n, 2); + let t = MerkleTree::::build(&data).unwrap(); + let d = t.depth().unwrap(); + for c in 0..=d { + let cap = t.cap(c).unwrap(); + for p in 0..(1usize << d) { + let mut proof = t.get_proof_by_pos(p).unwrap(); + let full = proof.merkle_path.clone(); + proof.truncate_to_cap(d, c).unwrap(); + assert_eq!(proof.merkle_path.len(), d - c); + let leaf = K::hash_data(leaf_at(&data, p)); + if !verify(&proof.merkle_path, &cap, d, p, leaf) { + return false; + } + if c == 0 { + assert!(verify_merkle_path_from_leaf_hash::( + &full, &t.root, p, leaf + )); + } + } + } + } + true + } + + fn real_verify(s: &[Node], cap: &[Node], d: usize, i: usize, l: Node) -> bool { + verify_merkle_path_to_cap_from_leaf_hash::(s, cap, d, i, l) + } + + #[test] + fn every_leaf_verifies_against_its_cap_node() { + assert!(every_leaf_verifies(real_verify)); + } + + #[test] + fn cap_taller_than_the_tree_is_refused_everywhere() { + let t = tree(8, 3); + let d = 3; + let full = t.get_proof_by_pos(0).unwrap(); + let mut p = full.clone(); + assert_eq!( + p.truncate_to_cap(d, d + 1), + Err(CapError::CapTooTall { + cap_height: 4, + depth: 3 + }) + ); + let big_cap = vec![[0u8; 32]; 16]; + assert!(!verify_merkle_path_to_cap_from_leaf_hash::( + &[], + &big_cap, + d, + 0, + [0u8; 32] + )); + assert!(split_owner_path(&big_cap, d, d + 1).is_none()); + let mut a = full.merkle_path.clone(); + assert!(embed_cap(&mut [&mut a], d, &big_cap).is_err()); + assert!(!verify_cap::(&big_cap, &t.root, MAX_CAP_HEIGHT + 1)); + } + + #[test] + fn truncate_refuses_a_path_that_is_not_full_length() { + let t = tree(16, 4); + let mut p = t.get_proof_by_pos(5).unwrap(); + p.truncate_to_cap(4, 2).unwrap(); + // Already cut: a second cut must not silently shorten it further. + assert_eq!( + p.truncate_to_cap(4, 2), + Err(CapError::PathLength { + expected: 4, + got: 2 + }) + ); + } + + #[test] + fn cap_root_needs_a_power_of_two() { + assert!(cap_root::(&[]).is_none()); + assert!(cap_root::(&[[1u8; 32]; 3]).is_none()); + assert_eq!(cap_root::(&[[1u8; 32]]), Some([1u8; 32])); + } + + // ------------------------------------------------------ owner-path encoding + + #[test] + fn split_owner_path_takes_exact_lengths_only() { + let d: usize = 6; + for c in 0..=d { + let want = if c == 0 { d } else { d - c + (1 << c) }; + for len in want.saturating_sub(1)..=want + 1 { + let path = vec![0u8; len]; + let got = split_owner_path(&path, d, c); + if len == want { + let (s, cap) = got.unwrap(); + assert_eq!(s.len(), d - c); + assert_eq!(cap.len(), if c == 0 { 0 } else { 1 << c }); + } else { + assert!(got.is_none(), "c={c} len={len}"); + } + } + } + } + + #[test] + fn embed_then_split_round_trips_and_every_opening_verifies() { + let data = leaves(64, 5); + let t = MerkleTree::::build(&data).unwrap(); + let d = 6; + let positions = [17usize, 3, 63, 0, 17]; + for c in 0..=d { + let cap = t.cap(c).unwrap(); + let mut paths: Vec> = positions + .iter() + .map(|&p| t.get_proof_by_pos(p).unwrap().merkle_path) + .collect(); + let full0 = paths[0].clone(); + { + let mut refs: Vec<&mut Vec> = paths.iter_mut().collect(); + embed_cap(&mut refs, d, &cap).unwrap(); + } + if c == 0 { + assert_eq!(paths[0], full0, "c = 0 must be a no-op"); + } else { + assert_eq!(paths[0].len(), d - c + (1 << c)); + assert_eq!(&paths[0][d - c..], &cap[..]); + } + for p in &paths[1..] { + assert_eq!(p.len(), d - c); + } + let (check, owner) = CappedRoot::from_owner::(&t.root, &paths[0], d, c).unwrap(); + assert_eq!(check.cap_height(), c); + assert_eq!(owner.len(), d - c); + assert!(check.verify::(owner, positions[0], K::hash_data(&data[positions[0]]))); + for (path, &pos) in paths[1..].iter().zip(&positions[1..]) { + assert!(check.verify::(path, pos, K::hash_data(&data[pos]))); + } + } + } + + #[test] + fn embed_refuses_short_paths_and_an_empty_owner_list() { + let t = tree(16, 6); + let cap = t.cap(2).unwrap(); + let mut short = vec![[0u8; 32]; 3]; + assert_eq!( + embed_cap(&mut [&mut short], 4, &cap), + Err(CapError::PathLength { + expected: 4, + got: 3 + }) + ); + assert_eq!(embed_cap::(&mut [], 4, &cap), Err(CapError::NoOwner)); + assert_eq!( + embed_cap(&mut [&mut vec![[0u8; 32]; 4]], 4, &cap[..3]), + Err(CapError::CapLength(3)) + ); + } + + // ------------------------------------------------------------- tamper tests + + struct Fixture { + data: Vec>, + t: MerkleTree, + d: usize, + c: usize, + } + + fn fixture() -> Fixture { + let data = leaves(256, 9); + let t = MerkleTree::::build(&data).unwrap(); + Fixture { + data, + t, + d: 8, + c: 3, + } + } + + impl Fixture { + fn owner_path(&self, pos: usize) -> Vec { + let mut p = self.t.get_proof_by_pos(pos).unwrap().merkle_path; + embed_cap(&mut [&mut p], self.d, &self.t.cap(self.c).unwrap()).unwrap(); + p + } + fn path(&self, pos: usize) -> Vec { + let mut p = self.t.get_proof_by_pos(pos).unwrap(); + p.truncate_to_cap(self.d, self.c).unwrap(); + p.merkle_path + } + fn leaf(&self, pos: usize) -> Node { + K::hash_data(&self.data[pos]) + } + } + + #[test] + fn a_flipped_cap_byte_is_rejected() { + let f = fixture(); + let honest = f.owner_path(10); + assert!(CappedRoot::from_owner::(&f.t.root, &honest, f.d, f.c).is_some()); + for k in 0..(1 << f.c) { + let mut owner = honest.clone(); + owner[f.d - f.c + k][0] ^= 1; + assert!( + CappedRoot::from_owner::(&f.t.root, &owner, f.d, f.c).is_none(), + "k={k}" + ); + assert!(!verify_cap::(&owner[f.d - f.c..], &f.t.root, f.c)); + } + } + + #[test] + fn a_flipped_path_node_is_rejected() { + let f = fixture(); + let owner = f.owner_path(10); + let (check, _) = CappedRoot::from_owner::(&f.t.root, &owner, f.d, f.c).unwrap(); + let honest = f.path(77); + assert!(check.verify::(&honest, 77, f.leaf(77))); + for k in 0..honest.len() { + let mut p = honest.clone(); + p[k][31] ^= 0x80; + assert!(!check.verify::(&p, 77, f.leaf(77)), "k={k}"); + } + } + + #[test] + fn swapped_cap_nodes_are_rejected() { + let f = fixture(); + let mut owner = f.owner_path(10); + let base = f.d - f.c; + owner.swap(base, base + 5); + assert!(CappedRoot::from_owner::(&f.t.root, &owner, f.d, f.c).is_none()); + } + + #[test] + fn a_cap_from_another_tree_is_rejected() { + let f = fixture(); + let other = tree(256, 1234); + let mut owner = f.path(10); + owner.extend(other.cap(f.c).unwrap()); + assert!(CappedRoot::from_owner::(&f.t.root, &owner, f.d, f.c).is_none()); + // And another tree's cap cannot vouch for this tree's openings even + // when paired with that tree's own root. + let (check, _) = CappedRoot::from_owner::(&other.root, &owner, f.d, f.c).unwrap(); + assert!(!check.verify::(&f.path(77), 77, f.leaf(77))); + } + + #[test] + fn an_index_with_a_flipped_top_bit_is_rejected() { + let f = fixture(); + let owner = f.owner_path(10); + let (check, _) = CappedRoot::from_owner::(&f.t.root, &owner, f.d, f.c).unwrap(); + let pos = 77usize; + let path = f.path(pos); + assert!(check.verify::(&path, pos, f.leaf(pos))); + for bit in 0..f.d { + let wrong = pos ^ (1 << bit); + assert!(!check.verify::(&path, wrong, f.leaf(pos)), "bit={bit}"); + } + // Past the tree: an index ≥ 2^D is refused, not wrapped. + assert!(!check.verify::(&path, pos + (1 << f.d), f.leaf(pos))); + } + + #[test] + fn a_path_one_node_too_long_or_short_is_rejected() { + let f = fixture(); + let owner = f.owner_path(10); + let (check, _) = CappedRoot::from_owner::(&f.t.root, &owner, f.d, f.c).unwrap(); + let path = f.path(77); + assert!(!check.verify::(&path[..path.len() - 1], 77, f.leaf(77))); + let mut long = path.clone(); + long.push(path[0]); + assert!(!check.verify::(&long, 77, f.leaf(77))); + // The full, uncut path is also refused under a cap. + let full = f.t.get_proof_by_pos(77).unwrap().merkle_path; + assert!(!check.verify::(&full, 77, f.leaf(77))); + // And the owner path one node short or long. + assert!(CappedRoot::from_owner::(&f.t.root, &owner[1..], f.d, f.c).is_none()); + let mut owner_long = owner.clone(); + owner_long.push(owner[0]); + assert!(CappedRoot::from_owner::(&f.t.root, &owner_long, f.d, f.c).is_none()); + } + + #[test] + fn a_cap_moved_to_the_second_opening_is_rejected() { + let f = fixture(); + let cap = f.t.cap(f.c).unwrap(); + // Query 0 carries a plain path, query 1 the cap: the wrong owner. + let q0 = f.path(10); + let mut q1 = f.path(77); + q1.extend(cap.iter().copied()); + assert!(CappedRoot::from_owner::(&f.t.root, &q0, f.d, f.c).is_none()); + // Even with a correctly authenticated cap in hand, query 1's path + // (siblings + cap) is the wrong length for a non-owner. + let owner = f.owner_path(10); + let (check, _) = CappedRoot::from_owner::(&f.t.root, &owner, f.d, f.c).unwrap(); + assert!(!check.verify::(&q1, 77, f.leaf(77))); + } + + #[test] + fn a_proof_capped_at_one_height_fails_at_another() { + let f = fixture(); + let owner = f.owner_path(10); // c = 3 + for other in [0, 1, 2, 4] { + assert!( + CappedRoot::from_owner::(&f.t.root, &owner, f.d, other).is_none(), + "c=3 proof accepted at c={other}" + ); + } + } + + #[test] + fn uncapped_is_the_full_path_check_plus_exact_length() { + let data = leaves(32, 11); + let t = MerkleTree::::build(&data).unwrap(); + let check = CappedRoot::uncapped(&t.root, 5); + for (p, value) in data.iter().enumerate() { + let path = t.get_proof_by_pos(p).unwrap().merkle_path; + let leaf = K::hash_data(value); + assert!(check.verify::(&path, p, leaf)); + assert!(!check.verify::(&path[..4], p, leaf)); + let mut long = path.clone(); + long.push(path[0]); + assert!(!check.verify::(&long, p, leaf)); + } + } + + // ----------------------------------------------------------- mutation tests + // + // Each load-bearing check has a property function that the named test runs + // against the real primitive, and a mutation test that runs the SAME + // function against a copy with that one check removed and asserts it + // fails. So removing the check from the real code makes the named test + // fail: the check is shown to carry the property, not just to be present. + + /// A toy backend whose leaf hash is the identity, so an internal node can + /// be presented as a leaf — the forgery a missing length check admits. + struct IdentityLeaf; + impl IsMerkleTreeBackend for IdentityLeaf { + type Node = u64; + type Data = u64; + fn hash_data(leaf: &u64) -> u64 { + *leaf + } + fn hash_new_parent(a: &u64, b: &u64) -> u64 { + a.wrapping_mul(0x9E37_79B9_7F4A_7C15).rotate_left(17) ^ b.wrapping_add(0x0123_4567_89AB) + } + } + + type U64Verify = fn(&[u64], &[u64], usize, usize, u64) -> bool; + + /// A path one level short, whose "leaf" is really the internal node over + /// leaves 0 and 1, must not verify at index 0. + fn rejects_short_path_forgery(verify: U64Verify) -> bool { + let d = 6; + let c = 2; + let data: Vec = (0..64u64).map(|i| i * 1_000_003 + 17).collect(); + let t = MerkleTree::::build(&data).unwrap(); + let cap = t.cap(c).unwrap(); + let full = t.get_proof_by_pos(0).unwrap().merkle_path; + // Honest opening of leaf 0 verifies. + assert!(verify(&full[..d - c], &cap, d, 0, data[0])); + // The forgery: claim the parent of leaves 0 and 1 as the value at + // index 0, with the path from that parent up to the cap. + let internal = IdentityLeaf::hash_new_parent(&data[0], &data[1]); + assert_ne!( + internal, data[0], + "the forged value must not be the real leaf" + ); + !verify(&full[1..d - c], &cap, d, 0, internal) + } + + #[test] + fn exact_length_check_rejects_a_short_path_forgery() { + assert!(rejects_short_path_forgery( + verify_merkle_path_to_cap_from_leaf_hash:: + )); + } + + #[test] + fn mutation_without_the_length_check_admits_the_forgery() { + fn mutant(s: &[u64], cap: &[u64], d: usize, i: usize, l: u64) -> bool { + let c = cap.len().ilog2() as usize; + // Mutation: no `siblings.len() == depth − c` check. + verify_merkle_path_from_leaf_hash::(s, &cap[i >> (d - c)], i, l) + } + assert!(!rejects_short_path_forgery(mutant)); + } + + #[test] + fn mutation_with_the_wrong_cap_index_fails_honest_openings() { + fn mutant(s: &[Node], cap: &[Node], d: usize, i: usize, l: Node) -> bool { + let c = cap.len().ilog2() as usize; + if s.len() != d - c || c == d { + // `d − c − 1` would underflow; keep the mutant defined there. + return verify_merkle_path_to_cap_from_leaf_hash::(s, cap, d, i, l); + } + // Mutation: `index >> (D − c − 1)` instead of `index >> (D − c)`. + cap.get(i >> (d - c - 1)) + .is_some_and(|node| verify_merkle_path_from_leaf_hash::(s, node, i, l)) + } + assert!(!every_leaf_verifies(mutant)); + } + + /// A cap made from another tree, carried by the owner path next to the + /// honest root, must not let a leaf of that other tree verify. + /// `CappedRoot::from_owner`'s shape, so a mutant can stand in for it. + type FromOwner = for<'a> fn( + &'a Node, + &'a [Node], + usize, + usize, + ) -> Option<(CappedRoot<'a, Node>, &'a [Node])>; + + fn rejects_forged_cap(from_owner: FromOwner) -> bool { + let d = 8; + let c = 3; + let honest = tree(256, 21); + let forged_data = leaves(256, 99); + let forged = MerkleTree::::build(&forged_data).unwrap(); + let mut owner = forged.get_proof_by_pos(40).unwrap().merkle_path; + embed_cap(&mut [&mut owner], d, &forged.cap(c).unwrap()).unwrap(); + match from_owner(&honest.root, &owner, d, c) { + None => true, + Some((check, siblings)) => { + !check.verify::(siblings, 40, K::hash_data(&forged_data[40])) + } + } + } + + #[test] + fn cap_to_root_check_rejects_a_forged_cap() { + assert!(rejects_forged_cap( + |r, p, d, c| CappedRoot::from_owner::(r, p, d, c) + )); + } + + #[test] + fn mutation_without_the_cap_to_root_check_admits_a_forged_cap() { + fn mutant<'a>( + _root: &'a Node, + path: &'a [Node], + d: usize, + c: usize, + ) -> Option<(CappedRoot<'a, Node>, &'a [Node])> { + let (siblings, cap) = split_owner_path(path, d, c)?; + // Mutation: no `verify_cap(cap, root)`. + Some(( + CappedRoot { + depth: d, + cap_height: c, + cap, + }, + siblings, + )) + } + assert!(!rejects_forged_cap(mutant)); + } + + // ------------------------------------------- the only-rejecting-check fixtures + // + // A tamper that some OTHER check also rejects cannot show a + // check is load-bearing — removing it leaves the test green. These two + // fixtures are built so that exactly one check rejects them, on the real + // keccak backend (no toy hash): delete that check and the test fails. + + /// Heap index of the ancestor at `height` levels above leaf `pos` in a tree + /// of depth `d` (`height = 0` is the leaf itself). + fn ancestor(d: usize, pos: usize, height: usize) -> usize { + (1usize << (d - height)) - 1 + (pos >> height) + } + + /// M1(a). The real internal node one level above leaf `pos`, presented as a + /// "leaf hash" with the path from that node upward — one sibling short. + /// At `pos = 0` and `pos = 2^D − 1` the index bits the fold consumes stay + /// consistent after the shift (all 0 / all 1), so the length-agnostic fold + /// ACCEPTS: only `siblings.len() == D − c` rejects it. Hash-agnostic — the + /// node is read out of the tree, not forged — and at `c = 0` it is exactly + /// the uncapped exact-length case. + #[test] + fn an_internal_node_as_leaf_hash_is_rejected_only_by_the_length_check() { + let t = tree(64, 5); + let d = t.depth().unwrap(); + assert_eq!(d, 6); + for c in 0..d { + let cap = t.cap(c).unwrap(); + for pos in [0usize, (1 << d) - 1] { + let full = t.get_proof_by_pos(pos).unwrap().merkle_path; + let node = t.nodes()[ancestor(d, pos, 1)]; + let forged = &full[1..d - c]; + // The length-agnostic fold accepts the forgery: no other check + // stands between it and acceptance. + assert!( + verify_merkle_path_from_leaf_hash::(forged, &cap[pos >> (d - c)], pos, node), + "c={c} pos={pos}: fixture precondition, the fold alone accepts" + ); + // The real check refuses it. + assert!( + !verify_merkle_path_to_cap_from_leaf_hash::(forged, &cap, d, pos, node), + "c={c} pos={pos}: an internal node passed for a leaf" + ); + if c == 0 { + assert!(!CappedRoot::uncapped(&t.root, d).verify::(forged, pos, node)); + } + } + } + } + + /// M1(b). Few openings under a tall cap: with 3 queries and `c = 3`, at + /// least 5 of the 8 cap nodes are reached by no query. Flipping one of those + /// leaves every per-query check green, so only the cap-to-root check + /// (`verify_cap`, run by `from_owner`) rejects it. + #[test] + fn an_unreached_cap_node_is_rejected_only_by_the_cap_to_root_check() { + let f = fixture(); + let queries = [10usize, 20, 30]; // all under cap node 0 (pos >> 5 == 0) + let reached: Vec = queries.iter().map(|q| q >> (f.d - f.c)).collect(); + let unreached = (0..1usize << f.c) + .find(|k| !reached.contains(k)) + .expect("some cap node is unreached"); + let mut owner = f.owner_path(queries[0]); + owner[f.d - f.c + unreached][0] ^= 1; + let (siblings0, tampered_cap) = split_owner_path(&owner, f.d, f.c).unwrap(); + // Every query still verifies against the tampered cap: no per-query + // check sees the unreached node. + assert!(verify_merkle_path_to_cap_from_leaf_hash::( + siblings0, + tampered_cap, + f.d, + queries[0], + f.leaf(queries[0]) + )); + for &q in &queries[1..] { + assert!( + verify_merkle_path_to_cap_from_leaf_hash::( + &f.path(q), + tampered_cap, + f.d, + q, + f.leaf(q) + ), + "q={q}: fixture precondition, per-query checks pass" + ); + } + // Only the cap-to-root check rejects it. + assert!(!verify_cap::(tampered_cap, &f.t.root, f.c)); + assert!(CappedRoot::from_owner::(&f.t.root, &owner, f.d, f.c).is_none()); + } + + // ------------------------------------------------------------ policy pins + + #[test] + fn auto_heights_are_pinned() { + let deep = 30; + for (openings, want) in [ + (0, 0), + (1, 0), + (3, 0), + (4, 2), + (19, 2), + (20, 3), + (110, 3), + (112, 3), + (224, 3), + (10_000, 3), + ] { + assert_eq!(CapPolicy::Auto.height(openings, deep), want, "o={openings}"); + } + } + + #[test] + fn auto_never_goes_past_three_under_the_pinned_weights() { + for o in 0..5_000 { + assert!(CapPolicy::Auto.height(o, 40) <= 3, "o={o}"); + } + // c = 4 loses to c = 3 on both the per-opening and the per-tree term. + assert!(cap_gain(&AUTO_WEIGHTS, 1_000_000, 4) < cap_gain(&AUTO_WEIGHTS, 1_000_000, 3)); + } + + /// The argmax of the cost law over every height `0..=MAX_CAP_HEIGHT`, ties + /// to the smaller height (`gain(·, 0) = 0`). + fn cost_law_argmax(openings: usize, limit: usize) -> usize { + let mut best = (0usize, 0i128); + for c in 1..=limit { + let g = cap_gain(&AUTO_WEIGHTS, openings, c); + if g > best.1 { + best = (c, g); + } + } + best.0 + } + + /// `Auto` is a fixed table; this pins that the table is + /// the cost-law argmax for every opening count, so the table and the + /// weights cannot drift apart. + #[test] + fn the_auto_table_is_the_cost_law_argmax_at_every_opening_count() { + for o in 0..=1_000_000usize { + assert_eq!( + CapPolicy::Auto.height(o, MAX_CAP_HEIGHT), + cost_law_argmax(o, MAX_CAP_HEIGHT), + "o={o}" + ); + } + for o in [usize::MAX, usize::MAX / 2, 1 << 40, u32::MAX as usize] { + assert_eq!(CapPolicy::Auto.height(o, 64), 3, "o={o}"); + assert_eq!(cost_law_argmax(o, MAX_CAP_HEIGHT), 3, "o={o}"); + } + } + + /// Clamping the table to the depth is not the same function as + /// an argmax bounded by the depth, at exactly one point: 4 openings of a + /// depth-1 tree, where the table says 1 and the bounded argmax 0 (a c = 1 + /// cap loses 68 ns there). The table is the rule; this pins the one + /// difference so any other one is a failure. + #[test] + fn the_depth_clamped_table_differs_from_a_bounded_argmax_at_one_point() { + let mut diffs = Vec::new(); + for d in 0..=6usize { + for o in 0..5_000usize { + if CapPolicy::Auto.height(o, d) != cost_law_argmax(o, d.min(MAX_CAP_HEIGHT)) { + diffs.push((o, d)); + } + } + } + assert_eq!(diffs, vec![(4, 1)]); + } + + #[test] + fn the_cost_law_is_bounded_for_every_input() { + // No overflow at the `usize` extremes and the tallest height. + let top = cap_gain(&AUTO_WEIGHTS, usize::MAX, MAX_CAP_HEIGHT); + assert!(top < 0, "a height-16 cap loses at any opening count"); + assert!(cap_gain(&AUTO_WEIGHTS, usize::MAX, 3) > 0); + // A height past the maximum is refused, never shifted. + for c in [MAX_CAP_HEIGHT + 1, 127, 128, usize::MAX] { + assert_eq!(cap_gain(&AUTO_WEIGHTS, 1_000, c), i128::MIN, "c={c}"); + } + } + + #[test] + fn heights_clamp_to_the_depth() { + for d in 0..6 { + assert_eq!(CapPolicy::Auto.height(110, d), d.min(3), "d={d}"); + } + assert_eq!(CapPolicy::Fixed(5).height(110, 2), 2); + assert_eq!(CapPolicy::Fixed(5).height(110, 9), 5); + assert_eq!(CapPolicy::Fixed(16).height(1, 40), 16); + assert_eq!(CapPolicy::Fixed(200).height(1, 40), MAX_CAP_HEIGHT); + assert_eq!( + CapPolicy::Fixed(5).height(0, 9), + 0, + "an unopened tree has no cap" + ); + } + + #[test] + fn off_and_fixed_zero_are_zero_everywhere() { + for o in 0..300 { + for d in 0..24 { + assert_eq!(CapPolicy::Off.height(o, d), 0); + assert_eq!(CapPolicy::Fixed(0).height(o, d), 0); + } + } + assert!(CapPolicy::Off.is_off()); + assert!(CapPolicy::Fixed(0).is_off()); + assert!(!CapPolicy::Auto.is_off()); + assert!(!CapPolicy::Fixed(1).is_off()); + assert_eq!(CapPolicy::default(), CapPolicy::Off); + } + + #[test] + fn auto_weights_are_pinned() { + assert_eq!( + AUTO_WEIGHTS, + CapWeights { + compress: 2251, + select: 567, + unpack: 528, + hint: 460, + compare: 3789, + } + ); + // The gains the pinned heights rest on. + assert_eq!(cap_gain(&AUTO_WEIGHTS, 20, 2), 55_758); + assert_eq!(cap_gain(&AUTO_WEIGHTS, 20, 3), 55_914); + assert_eq!(cap_gain(&AUTO_WEIGHTS, 19, 2), 52_351); + assert_eq!(cap_gain(&AUTO_WEIGHTS, 19, 3), 51_957); + assert_eq!(cap_gain(&AUTO_WEIGHTS, 4, 1), -68); + assert_eq!(cap_gain(&AUTO_WEIGHTS, 4, 2), 1_246); + } + + #[test] + fn policy_spellings_parse_and_print() { + for (s, want) in [ + ("off", CapPolicy::Off), + ("auto", CapPolicy::Auto), + ("0", CapPolicy::Off), + ("1", CapPolicy::Fixed(1)), + ("16", CapPolicy::Fixed(16)), + ] { + assert_eq!(s.parse::(), Ok(want), "{s}"); + } + for bad in [ + "", "17", "256", "-1", "+3", " 3", "3 ", "Auto", "OFF", "on", "3.0", "x", + ] { + assert!(bad.parse::().is_err(), "{bad:?} must be refused"); + } + assert_eq!(CapPolicy::Off.to_string(), "off"); + assert_eq!(CapPolicy::Fixed(0).to_string(), "off"); + assert_eq!(CapPolicy::Auto.to_string(), "auto"); + assert_eq!(CapPolicy::Fixed(7).to_string(), "7"); + for p in [ + CapPolicy::Off, + CapPolicy::Auto, + CapPolicy::Fixed(1), + CapPolicy::Fixed(16), + ] { + assert_eq!(p.to_string().parse::(), Ok(p)); + } + } +} diff --git a/crypto/crypto/src/merkle_tree/merkle.rs b/crypto/crypto/src/merkle_tree/merkle.rs index 447654907..5b19f2f54 100644 --- a/crypto/crypto/src/merkle_tree/merkle.rs +++ b/crypto/crypto/src/merkle_tree/merkle.rs @@ -256,6 +256,39 @@ where self.nodes.get(idx) } + /// `log2` of the padded leaf count: the number of siblings on a full + /// authentication path. `None` on a root-only tree + /// ([`from_root`](Self::from_root)), whose shape is not known here. + pub fn depth(&self) -> Option { + if self.is_root_only() { + return None; + } + // `node_count = 2·leaves − 1` with `leaves` a power of two (every + // constructor guarantees it), so `leaves = (node_count + 1) / 2`. + let leaves = self.node_count().div_ceil(2); + Some(leaves.ilog2() as usize) + } + + /// The Merkle cap at height `cap_height`: the `2^cap_height` nodes that + /// sit `cap_height` levels below the root, left to right (heap indices + /// `[2^c − 1, 2^{c+1} − 1)`). Height 0 is `[root]`; height `depth` is the + /// leaf-hash layer. + /// + /// `None` on a root-only tree, and when `cap_height > depth` — a cap taller + /// than the tree is not representable, and a caller asking for one has a + /// policy bug that must fail closed rather than be clamped here. Reads go + /// through the node accessor, so a disk-spilled tree works too. + pub fn cap(&self, cap_height: usize) -> Option> { + let depth = self.depth()?; + if cap_height > depth { + return None; + } + let start = (1usize << cap_height) - 1; + (start..2 * start + 1) + .map(|i| self.node_get(i).cloned()) + .collect() + } + /// Read-only access to the full node buffer in standard layout: /// `nodes[0..leaves_len - 1]` are inner nodes (root at index 0) and /// `nodes[leaves_len - 1..]` are the leaves. diff --git a/crypto/crypto/src/merkle_tree/mod.rs b/crypto/crypto/src/merkle_tree/mod.rs index 99ea82dea..363ccaa7c 100644 --- a/crypto/crypto/src/merkle_tree/mod.rs +++ b/crypto/crypto/src/merkle_tree/mod.rs @@ -1,4 +1,5 @@ pub mod backends; +pub mod cap; pub mod merkle; pub mod proof; pub mod traits; diff --git a/crypto/math-cuda/kernels/blake3.cu b/crypto/math-cuda/kernels/blake3.cu index 3b30e25f6..680a42afe 100644 --- a/crypto/math-cuda/kernels/blake3.cu +++ b/crypto/math-cuda/kernels/blake3.cu @@ -476,6 +476,29 @@ extern "C" __global__ void blake3_fri_leaves_ext3( h.finalize(leaves_out + tid * 32); } +// FRI GROUP-leaf hashing (S3): leaf `tid` hashes the `group` consecutive ext3 +// values `evals[tid*group .. (tid+1)*group]` of an interleaved eval vector (the +// `3*group` contiguous u64s at `evals_interleaved + tid*group*3`), each as its +// canonical big-endian components. The host `Batched` leaf over the group; at +// `group = 2` exactly `blake3_fri_leaves_ext3`'s message. A group of 2^d values +// is 24*2^d bytes, several blocks from d = 2 on — the chain handles any length. +// Twin of `keccak_fri_group_leaves_ext3`. +extern "C" __global__ void blake3_fri_group_leaves_ext3( + const uint64_t *evals_interleaved, // 3 * num_leaves * group u64s + uint64_t num_leaves, + uint64_t group, // ext3 values per leaf (2^d) + uint8_t *leaves_out) { + uint64_t tid = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= num_leaves) return; + + const uint64_t *g = evals_interleaved + tid * group * 3; + + Blake3Chain h; + h.init(); + for (uint64_t i = 0; i < 3 * group; ++i) h.push_felt(g[i]); + h.finalize(leaves_out + tid * 32); +} + // Row-major ROW-PAIR leaf hashing: the row-major analog of // `blake3_leaves_base_row_pair_batched`. Leaf `tid` hashes row // `reverse_index(2*tid)` then row `reverse_index(2*tid+1)`, each `m` lanes read @@ -537,6 +560,32 @@ extern "C" __global__ void blake3_leaves_base_row_major_row_pair_range( h.finalize(hashed_leaves_out + tid * 32); } +// Row-major ONE-ROW leaf hashing (S2, rows_per_leaf = 1): leaf `tid` hashes the +// single row `reverse_index(tid)`, columns `[col_start, col_end)` of the +// row-major buffer (`m` the full row stride). Byte stream = the CPU +// `commit_rows_bit_reversed_subset_with(.., 1)`. Twin of +// `keccak256_leaves_base_row_major_row_range`. +extern "C" __global__ void blake3_leaves_base_row_major_row_range( + const uint64_t *data, + uint64_t m, + uint64_t col_start, + uint64_t col_end, + uint64_t num_rows, + uint64_t log_num_rows, + uint8_t *hashed_leaves_out) +{ + uint64_t tid = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= num_rows) return; + + uint64_t br = __brevll(tid) >> (64 - log_num_rows); + const uint64_t *row = data + br * m; + + Blake3Chain h; + h.init(); + for (uint64_t c = col_start; c < col_end; ++c) h.push_felt(row[c]); + h.finalize(hashed_leaves_out + tid * 32); +} + // --------------------------------------------------------------------------- // Merkle parent / level compressors. // diff --git a/crypto/math-cuda/kernels/keccak.cu b/crypto/math-cuda/kernels/keccak.cu index 300bc5a7a..923a978b6 100644 --- a/crypto/math-cuda/kernels/keccak.cu +++ b/crypto/math-cuda/kernels/keccak.cu @@ -464,6 +464,37 @@ extern "C" __global__ void keccak_fri_leaves_ext3( finalize_keccak256(st, rate_pos, leaves_out + tid * 32); } +// --------------------------------------------------------------------------- +// FRI GROUP-leaf hashing (S3, higher-arity committed FRI layers). +// +// Leaf `tid` hashes the `group` consecutive ext3 values +// `evals[tid*group .. (tid+1)*group]` of an interleaved eval vector — the +// `3*group` contiguous u64s at `evals_interleaved + tid*group*3` — each value +// as its three components in canonical big-endian order. That is the host +// `Batched` leaf over the group (`hash_data_from_slices(group, [])`), and at +// `group = 2` exactly `keccak_fri_leaves_ext3`'s byte stream. No bit reversal. +// --------------------------------------------------------------------------- +extern "C" __global__ void keccak_fri_group_leaves_ext3( + const uint64_t *evals_interleaved, // 3 * num_leaves * group u64s + uint64_t num_leaves, + uint64_t group, // ext3 values per leaf (2^d) + uint8_t *leaves_out) { + uint64_t tid = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= num_leaves) return; + + uint64_t st[25]; + #pragma unroll + for (int i = 0; i < 25; ++i) st[i] = 0; + uint32_t rate_pos = 0; + + const uint64_t *g = evals_interleaved + tid * group * 3; + for (uint64_t i = 0; i < 3 * group; ++i) { + absorb_lane(st, rate_pos, bswap64(goldilocks::canonical(g[i]))); + } + + finalize_keccak256(st, rate_pos, leaves_out + tid * 32); +} + // --------------------------------------------------------------------------- // Merkle inner-tree pair hash: one level of the inner Merkle tree. // @@ -652,3 +683,43 @@ extern "C" __global__ void keccak256_leaves_base_row_major_row_pair_range( } finalize_keccak256(st, rate_pos, hashed_leaves_out + tid * 32); } + +// --------------------------------------------------------------------------- +// Row-major ONE-ROW leaf hashing (S2, rows_per_leaf = 1). +// +// Leaf `tid` hashes the single row `reverse_index(tid)` (bit reversal over +// `log_num_rows` bits), columns `[col_start, col_end)` of the contiguous +// row-major buffer (`data + br * m`, `m` the full row stride), as canonical +// big-endian lanes. `num_leaves = num_rows`. Byte layout equals the CPU +// `commit_rows_bit_reversed_subset_with(data, m, col_start, col_end, 1)`; the +// whole row (`[0, m)`) is `commit_rows_bit_reversed_with(data, m, 1)`. +// +// NOT the row-pair kernels at another width: those read rows `brev(2·tid)` and +// `brev(2·tid + 1)` over `log_num_rows` bits, which is a different row set, +// so one row per leaf needs its own read pattern. +// --------------------------------------------------------------------------- +extern "C" __global__ void keccak256_leaves_base_row_major_row_range( + const uint64_t *data, + uint64_t m, + uint64_t col_start, + uint64_t col_end, + uint64_t num_rows, + uint64_t log_num_rows, + uint8_t *hashed_leaves_out) +{ + uint64_t tid = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= num_rows) return; + + uint64_t br = __brevll(tid) >> (64 - log_num_rows); + const uint64_t *row = data + br * m; + + uint64_t st[25]; + #pragma unroll + for (int i = 0; i < 25; ++i) st[i] = 0; + + uint32_t rate_pos = 0; + for (uint64_t c = col_start; c < col_end; ++c) { + absorb_lane(st, rate_pos, bswap64(goldilocks::canonical(row[c]))); + } + finalize_keccak256(st, rate_pos, hashed_leaves_out + tid * 32); +} diff --git a/crypto/math-cuda/kernels/rpx.cu b/crypto/math-cuda/kernels/rpx.cu index d9bfb5587..6c3bbff14 100644 --- a/crypto/math-cuda/kernels/rpx.cu +++ b/crypto/math-cuda/kernels/rpx.cu @@ -687,6 +687,29 @@ extern "C" __global__ void rpx_fri_leaves_ext3( rpx::store_digest_be(digest, leaves_out + tid * 32); } +// FRI GROUP-leaf hashing (S3): leaf `tid` absorbs the `group` consecutive ext3 +// values `evals[tid*group .. (tid+1)*group]` of an interleaved eval vector — the +// `3*group` contiguous felts at `evals_interleaved + tid*group*3` — in order, a +// sponge over `3*group` felts (the count keys the padding). The host +// `AlgebraicBatchBackend` leaf over the group; at `group = 2` exactly +// `rpx_fri_leaves_ext3`'s six felts. Twin of `keccak_fri_group_leaves_ext3`. +extern "C" __global__ void rpx_fri_group_leaves_ext3( + const uint64_t *evals_interleaved, // 3 * num_leaves * group u64s + uint64_t num_leaves, + uint64_t group, // ext3 values per leaf (2^d) + uint8_t *leaves_out) { + uint64_t tid = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= num_leaves) return; + const uint64_t *g = evals_interleaved + tid * group * 3; + + rpx::Sponge sp; + sp.init(3 * group); + for (uint64_t i = 0; i < 3 * group; ++i) sp.absorb(g[i]); + uint64_t digest[rpx::DIGEST_FELTS]; + sp.finalize(digest); + rpx::store_digest_be(digest, leaves_out + tid * 32); +} + // Row-major ROW-PAIR leaf hashing: leaf `tid` absorbs row `reverse_index(2*tid)` // then row `reverse_index(2*tid+1)`, each `m` lanes read contiguously from // `data + br * m`. `m` is the row stride in u64s: base trace = column count, @@ -746,6 +769,33 @@ extern "C" __global__ void rpx_leaves_base_row_major_row_pair_range( rpx::store_digest_be(digest, hashed_leaves_out + tid * 32); } +// Row-major ONE-ROW leaf hashing (S2, rows_per_leaf = 1): leaf `tid` absorbs the +// single row `reverse_index(tid)`, columns `[col_start, col_end)` of the +// row-major buffer (`m` the full row stride) — a sponge over +// `col_end - col_start` felts (the count keys the padding). The CPU +// `commit_rows_bit_reversed_subset_with(.., 1)`. Twin of +// `keccak256_leaves_base_row_major_row_range`. +extern "C" __global__ void rpx_leaves_base_row_major_row_range( + const uint64_t *data, + uint64_t m, + uint64_t col_start, + uint64_t col_end, + uint64_t num_rows, + uint64_t log_num_rows, + uint8_t *hashed_leaves_out) { + uint64_t tid = (uint64_t)blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= num_rows) return; + uint64_t br = __brevll(tid) >> (64 - log_num_rows); + const uint64_t *row = data + br * m; + + rpx::Sponge sp; + sp.init(col_end - col_start); + for (uint64_t c = col_start; c < col_end; ++c) sp.absorb(row[c]); + uint64_t digest[rpx::DIGEST_FELTS]; + sp.finalize(digest); + rpx::store_digest_be(digest, hashed_leaves_out + tid * 32); +} + // --------------------------------------------------------------------------- // COSET leaf hashing — the WHIR shape, and the two kernels the per-table branch // has no twin for. diff --git a/crypto/math-cuda/src/blake3.rs b/crypto/math-cuda/src/blake3.rs index 49a8e9fbd..a28b35a7e 100644 --- a/crypto/math-cuda/src/blake3.rs +++ b/crypto/math-cuda/src/blake3.rs @@ -601,6 +601,22 @@ pub fn build_comp_poly_tree_from_slabs_dev( m: usize, lde_size: usize, ) -> Result { + build_comp_poly_tree_from_slabs_dev_rpl(stream, buf, m, lde_size, 2) +} + +/// [`build_comp_poly_tree_from_slabs_dev`] with `rows_per_leaf` rows per leaf +/// (2 = row pair, 1 = S2 one row: `lde_size` leaves, the one-row ext3 kernel). +pub fn build_comp_poly_tree_from_slabs_dev_rpl( + stream: &Arc, + buf: &CudaSlice, + m: usize, + lde_size: usize, + rows_per_leaf: usize, +) -> Result { + assert!( + rows_per_leaf == 1 || rows_per_leaf == 2, + "rows_per_leaf must be 1 or 2" + ); // Same sticky hook as the keccak twin: the comp-tree cliff test arms one // counter and must reach it under whichever hash the build pins. #[cfg(feature = "test-faults")] @@ -608,7 +624,7 @@ pub fn build_comp_poly_tree_from_slabs_dev( assert!(m > 0); assert!(lde_size.is_power_of_two() && lde_size >= 2); assert_eq!(buf.len(), 3 * m * lde_size, "slab buffer shape"); - let num_leaves = lde_size / 2; + let num_leaves = lde_size / rows_per_leaf; let tight_total_nodes = 2 * num_leaves - 1; let be = backend()?; @@ -619,7 +635,12 @@ pub fn build_comp_poly_tree_from_slabs_dev( { let mut leaves_view = nodes_dev.slice_mut(leaves_offset_bytes..leaves_offset_bytes + num_leaves * 32); - launch_ext3_row_pair( + let launch = if rows_per_leaf == 2 { + launch_ext3_row_pair + } else { + launch_leaves_ext3 + }; + launch( stream.as_ref(), buf, lde_size as u64, @@ -648,6 +669,15 @@ pub fn build_comp_poly_tree_from_slabs_dev( /// stages through the same pinned de-interleave buffer for the same reason. pub fn build_comp_poly_tree_from_evals_ext3_keep( parts_interleaved: &[&[u64]], +) -> Result { + build_comp_poly_tree_from_evals_ext3_keep_rpl(parts_interleaved, 2) +} + +/// [`build_comp_poly_tree_from_evals_ext3_keep`] with `rows_per_leaf` rows per +/// leaf (2 = row pair, 1 = S2 one row). +pub fn build_comp_poly_tree_from_evals_ext3_keep_rpl( + parts_interleaved: &[&[u64]], + rows_per_leaf: usize, ) -> Result { #[cfg(feature = "test-faults")] crate::faults::check_sticky(&crate::faults::FAULT_COMP_TREE_STICKY)?; @@ -685,7 +715,7 @@ pub fn build_comp_poly_tree_from_evals_ext3_keep( stream.synchronize()?; drop(staging); - build_comp_poly_tree_from_slabs_dev(&stream, &buf, m, lde_size) + build_comp_poly_tree_from_slabs_dev_rpl(&stream, &buf, m, lde_size, rows_per_leaf) } /// Build a FRI-layer Merkle tree on device under BLAKE3 from an interleaved ext3 diff --git a/crypto/math-cuda/src/device.rs b/crypto/math-cuda/src/device.rs index 9d010151a..0f50e2167 100644 --- a/crypto/math-cuda/src/device.rs +++ b/crypto/math-cuda/src/device.rs @@ -205,6 +205,8 @@ pub struct Backend { // keccak.cubin pub keccak256_leaves_base_row_major_row_pair: CudaFunction, pub keccak256_leaves_base_row_major_row_pair_range: CudaFunction, + /// S2 one-row leaves (`rows_per_leaf = 1`): row `reverse_index(i)`, a column range. + pub keccak256_leaves_base_row_major_row_range: CudaFunction, pub keccak256_leaves_base_batched: CudaFunction, pub keccak256_leaves_base_coset: CudaFunction, pub keccak256_leaves_ext3_coset: CudaFunction, @@ -213,6 +215,8 @@ pub struct Backend { pub grind_search: CudaFunction, pub keccak_comp_poly_leaves_ext3: CudaFunction, pub keccak_fri_leaves_ext3: CudaFunction, + /// S3 group-leaf FRI layers: `group` consecutive ext3 values per leaf. + pub keccak_fri_group_leaves_ext3: CudaFunction, pub keccak_merkle_level: CudaFunction, pub keccak_merkle_tail: CudaFunction, pub merkle_gather_paths: CudaFunction, @@ -227,11 +231,15 @@ pub struct Backend { // yet — they exist so the GPU can follow the CPU's hash switch (PA-PLAN §6.1). pub blake3_leaves_base_row_major_row_pair: CudaFunction, pub blake3_leaves_base_row_major_row_pair_range: CudaFunction, + /// S2 one-row leaves (`rows_per_leaf = 1`): row `reverse_index(i)`, a column range. + pub blake3_leaves_base_row_major_row_range: CudaFunction, pub blake3_leaves_base_batched: CudaFunction, pub blake3_leaves_base_row_pair_batched: CudaFunction, pub blake3_leaves_ext3_batched: CudaFunction, pub blake3_comp_poly_leaves_ext3: CudaFunction, pub blake3_fri_leaves_ext3: CudaFunction, + /// S3 group-leaf FRI layers: `group` consecutive ext3 values per leaf. + pub blake3_fri_group_leaves_ext3: CudaFunction, pub blake3_merkle_level: CudaFunction, pub blake3_merkle_tail: CudaFunction, pub blake3_compress_probe_6r: CudaFunction, @@ -249,11 +257,15 @@ pub struct Backend { // tests check against the host `Rpx256`. pub rpx_leaves_base_row_major_row_pair: CudaFunction, pub rpx_leaves_base_row_major_row_pair_range: CudaFunction, + /// S2 one-row leaves (`rows_per_leaf = 1`): row `reverse_index(i)`, a column range. + pub rpx_leaves_base_row_major_row_range: CudaFunction, pub rpx_leaves_base_batched: CudaFunction, pub rpx_leaves_base_row_pair_batched: CudaFunction, pub rpx_leaves_ext3_batched: CudaFunction, pub rpx_comp_poly_leaves_ext3: CudaFunction, pub rpx_fri_leaves_ext3: CudaFunction, + /// S3 group-leaf FRI layers: `group` consecutive ext3 values per leaf. + pub rpx_fri_group_leaves_ext3: CudaFunction, pub rpx_merkle_level: CudaFunction, pub rpx_merkle_tail: CudaFunction, pub rpx_permute_probe: CudaFunction, @@ -870,6 +882,8 @@ impl Backend { .load_function("keccak256_leaves_base_row_major_row_pair")?, keccak256_leaves_base_row_major_row_pair_range: keccak .load_function("keccak256_leaves_base_row_major_row_pair_range")?, + keccak256_leaves_base_row_major_row_range: keccak + .load_function("keccak256_leaves_base_row_major_row_range")?, keccak256_leaves_base_batched: keccak.load_function("keccak256_leaves_base_batched")?, keccak256_leaves_base_coset: keccak.load_function("keccak256_leaves_base_coset")?, keccak256_leaves_ext3_coset: keccak.load_function("keccak256_leaves_ext3_coset")?, @@ -879,6 +893,7 @@ impl Backend { grind_search: keccak.load_function("grind_search")?, keccak_comp_poly_leaves_ext3: keccak.load_function("keccak_comp_poly_leaves_ext3")?, keccak_fri_leaves_ext3: keccak.load_function("keccak_fri_leaves_ext3")?, + keccak_fri_group_leaves_ext3: keccak.load_function("keccak_fri_group_leaves_ext3")?, keccak_merkle_level: keccak.load_function("keccak_merkle_level")?, keccak_merkle_tail: keccak.load_function("keccak_merkle_tail")?, merkle_gather_paths: keccak.load_function("merkle_gather_paths")?, @@ -886,12 +901,15 @@ impl Backend { .load_function("blake3_leaves_base_row_major_row_pair")?, blake3_leaves_base_row_major_row_pair_range: blake3 .load_function("blake3_leaves_base_row_major_row_pair_range")?, + blake3_leaves_base_row_major_row_range: blake3 + .load_function("blake3_leaves_base_row_major_row_range")?, blake3_leaves_base_batched: blake3.load_function("blake3_leaves_base_batched")?, blake3_leaves_base_row_pair_batched: blake3 .load_function("blake3_leaves_base_row_pair_batched")?, blake3_leaves_ext3_batched: blake3.load_function("blake3_leaves_ext3_batched")?, blake3_comp_poly_leaves_ext3: blake3.load_function("blake3_comp_poly_leaves_ext3")?, blake3_fri_leaves_ext3: blake3.load_function("blake3_fri_leaves_ext3")?, + blake3_fri_group_leaves_ext3: blake3.load_function("blake3_fri_group_leaves_ext3")?, blake3_merkle_level: blake3.load_function("blake3_merkle_level")?, blake3_merkle_tail: blake3.load_function("blake3_merkle_tail")?, blake3_compress_probe_6r: blake3.load_function("blake3_compress_probe_6r")?, @@ -906,12 +924,15 @@ impl Backend { .load_function("rpx_leaves_base_row_major_row_pair")?, rpx_leaves_base_row_major_row_pair_range: rpx .load_function("rpx_leaves_base_row_major_row_pair_range")?, + rpx_leaves_base_row_major_row_range: rpx + .load_function("rpx_leaves_base_row_major_row_range")?, rpx_leaves_base_batched: rpx.load_function("rpx_leaves_base_batched")?, rpx_leaves_base_row_pair_batched: rpx .load_function("rpx_leaves_base_row_pair_batched")?, rpx_leaves_ext3_batched: rpx.load_function("rpx_leaves_ext3_batched")?, rpx_comp_poly_leaves_ext3: rpx.load_function("rpx_comp_poly_leaves_ext3")?, rpx_fri_leaves_ext3: rpx.load_function("rpx_fri_leaves_ext3")?, + rpx_fri_group_leaves_ext3: rpx.load_function("rpx_fri_group_leaves_ext3")?, rpx_merkle_level: rpx.load_function("rpx_merkle_level")?, rpx_merkle_tail: rpx.load_function("rpx_merkle_tail")?, rpx_permute_probe: rpx.load_function("rpx_permute_probe")?, diff --git a/crypto/math-cuda/src/fri.rs b/crypto/math-cuda/src/fri.rs index 12e36b917..da86b5829 100644 --- a/crypto/math-cuda/src/fri.rs +++ b/crypto/math-cuda/src/fri.rs @@ -312,6 +312,248 @@ impl FriCommitState { }; Ok((layer_evals, out, tree)) } + + /// One binary fold of the current codeword with `zeta_raw` into a fresh + /// buffer (the same `fri_fold_ext3` launch as [`Self::fold_and_commit_layer`]), + /// then the twiddle update for the halved domain. The output becomes the + /// current codeword; the input is released once no caller holds it. + fn fold_once(&mut self, be: &crate::device::Backend, zeta_raw: [u64; 3]) -> Result<()> { + let n_out = self.current_n / 2; + assert!(n_out >= 1, "fold_once: nothing left to fold"); + let zeta_dev = self.stream.clone_htod(&zeta_raw)?; + let cfg = LaunchConfig { + grid_dim: ((n_out as u32).div_ceil(128), 1, 1), + block_dim: (128, 1, 1), + shared_mem_bytes: 0, + }; + let n_out_u64 = n_out as u64; + // SAFETY: the fold kernel writes all 3 * n_out slots before any read. + let mut out = unsafe { self.stream.alloc::(3 * n_out) }?; + unsafe { + self.stream + .launch_builder(&be.fri_fold_ext3) + .arg(self.current.as_ref()) + .arg(&n_out_u64) + .arg(&self.inv_tw) + .arg(&zeta_dev) + .arg(&mut out) + .launch(cfg)?; + } + // `new[j] = old[2j]^2` into a fresh buffer (see `fold_and_commit_layer` + // for why not in place). + let tw_next = n_out / 2; + if tw_next > 0 { + // SAFETY: the update kernel writes all tw_next slots. + let mut tw_out = unsafe { self.stream.alloc::(tw_next) }?; + let cfg = LaunchConfig { + grid_dim: ((tw_next as u32).div_ceil(128), 1, 1), + block_dim: (128, 1, 1), + shared_mem_bytes: 0, + }; + let tw_next_u64 = tw_next as u64; + unsafe { + self.stream + .launch_builder(&be.fri_update_twiddles) + .arg(&self.inv_tw) + .arg(&mut tw_out) + .arg(&tw_next_u64) + .launch(cfg)?; + } + self.inv_tw = tw_out; + } + self.current = Arc::new(out); + self.current_n = n_out; + Ok(()) + } + + /// The S3 (higher-arity committed FRI) step: fold the current codeword + /// `zeta_powers.len()` times — fold `ℓ` with `zeta_powers[ℓ]`, which the + /// caller sets to `ζ^{2^ℓ}` — then commit the result as a layer whose leaf + /// `g` hashes the `2^group_log` consecutive ext3 values + /// `[g·2^group_log, (g+1)·2^group_log)` (the configured hash's `Batched` + /// leaf over the group), with the pair-hash inner tree on top. + /// + /// The fold count and the group size are separate on purpose: committed + /// layer `j` is reached by the PREVIOUS layer's `d_{j−1}` folds and grouped + /// by its own `d_j`. Only the last fold's output is kept; the + /// intermediate codewords are released as the chain advances. + /// + /// Returns what [`Self::fold_and_commit_layer`] returns: the layer's evals + /// (host copy only when `want_host`), the resident evals, and the resident + /// tree with its root D2H'd. + #[allow(clippy::type_complexity)] + pub fn fold_and_commit_group( + &mut self, + zeta_powers: &[[u64; 3]], + group_log: u32, + want_host: bool, + ) -> Result<( + Option>, + Arc>, + crate::lde::GpuMerkleTree, + )> { + #[cfg(feature = "test-faults")] + check_fault_injection()?; + let be = backend()?; + for &z in zeta_powers { + self.fold_once(be, z)?; + } + let n = self.current_n; + assert!( + group_log >= 1 && (n >> group_log) >= 2 && (n >> group_log) << group_log == n, + "fold_and_commit_group: a layer of {n} values cannot hold >= 2 groups of 2^{group_log}" + ); + let num_leaves = n >> group_log; + let nodes_dev = commit_group_leaves( + &self.stream, + be, + self.hash, + self.current.as_ref(), + num_leaves, + 1u64 << group_log, + )?; + + let n_evals = 3 * n; + let pending = if want_host { + Some(crate::device::async_dtoh_via( + &self.stream, + be.pinned_staging(), + &be.ctx, + self.current.as_ref(), + n_evals, + )?) + } else { + None + }; + // The pageable root copy drains the stream, the evals DMA included. + let mut root = [0u8; 32]; + self.stream + .memcpy_dtoh(&nodes_dev.slice(0..32), &mut root)?; + let layer_evals = match pending { + Some(p) => { + let mut v = vec![0u64; n_evals]; + p.wait_into_u64(&mut v)?; + Some(v) + } + None => None, + }; + let tree = crate::lde::GpuMerkleTree { + nodes: Arc::new(nodes_dev), + leaves_len: num_leaves, + root, + }; + Ok((layer_evals, Arc::clone(&self.current), tree)) + } + + /// Fold the current codeword `zeta_powers.len()` times (fold `ℓ` with + /// `zeta_powers[ℓ]`) and copy the result to the host, with no commitment: + /// the S3 fold into the terminal codeword after the last committed layer. + pub fn fold_to_host(&mut self, zeta_powers: &[[u64; 3]]) -> Result> { + #[cfg(feature = "test-faults")] + check_fault_injection()?; + let be = backend()?; + for &z in zeta_powers { + self.fold_once(be, z)?; + } + let out = self.stream.clone_dtoh(self.current.as_ref())?; + self.stream.synchronize()?; + Ok(out) + } +} + +/// Hash `num_leaves` group leaves of `group` consecutive ext3 values each from +/// the interleaved `evals` (`3 · num_leaves · group` u64) and build the inner +/// tree on top: the full `(2·num_leaves − 1) · 32`-byte node buffer, root at 0. +fn commit_group_leaves( + stream: &Arc, + be: &crate::device::Backend, + hash: DeviceHash, + evals: &CudaSlice, + num_leaves: usize, + group: u64, +) -> Result> { + assert!(num_leaves >= 2 && num_leaves.is_power_of_two()); + assert!(evals.len() as u64 >= 3 * num_leaves as u64 * group); + let tight_total_nodes = 2 * num_leaves - 1; + // SAFETY: the leaf kernel writes the leaves [num_leaves-1, 2*num_leaves-1) + // and the inner-level walk every node [0, num_leaves-1) before any read. + let mut nodes_dev = unsafe { stream.alloc::(tight_total_nodes * 32) }?; + let leaves_offset_bytes = (num_leaves - 1) * 32; + { + let mut leaves_view = + nodes_dev.slice_mut(leaves_offset_bytes..leaves_offset_bytes + num_leaves * 32); + let num_leaves_u64 = num_leaves as u64; + let (kernel, cfg) = match hash { + DeviceHash::Keccak256 => ( + &be.keccak_fri_group_leaves_ext3, + crate::merkle::keccak_launch_cfg(num_leaves_u64), + ), + DeviceHash::Blake3 => ( + &be.blake3_fri_group_leaves_ext3, + crate::blake3::blake3_launch_cfg(num_leaves_u64), + ), + DeviceHash::Rpx256 => ( + &be.rpx_fri_group_leaves_ext3, + crate::rpx::rpx_launch_cfg(num_leaves_u64), + ), + DeviceHash::Rpo256 | DeviceHash::Poseidon => { + unimplemented!("{hash:?} device commit not yet ported (FRI group leaves)") + } + }; + unsafe { + stream + .launch_builder(kernel) + .arg(evals) + .arg(&num_leaves_u64) + .arg(&group) + .arg(&mut leaves_view) + .launch(cfg)?; + } + } + match hash { + DeviceHash::Keccak256 => crate::merkle::build_inner_tree_levels( + stream.as_ref(), + be, + &mut nodes_dev, + num_leaves, + DeviceHash::Keccak256, + )?, + DeviceHash::Blake3 => { + crate::blake3::build_inner_tree_levels(stream.as_ref(), be, &mut nodes_dev, num_leaves)? + } + DeviceHash::Rpx256 => { + crate::rpx::build_inner_tree_levels(stream.as_ref(), be, &mut nodes_dev, num_leaves)? + } + DeviceHash::Rpo256 | DeviceHash::Poseidon => { + unimplemented!("{hash:?} device commit not yet ported (FRI group inner tree levels)") + } + } + Ok(nodes_dev) +} + +/// Parity harness (not a production path): commit an interleaved ext3 eval +/// vector as an S3 group-leaf FRI layer — leaf `g` = the `2^group_log` +/// consecutive values from `g·2^group_log` — under `hash`, and return the full +/// host node buffer (`(2·num_leaves − 1) · 32` bytes, standard layout) so tests +/// can compare it node for node with the host tree. Production commits through +/// [`FriCommitState::fold_and_commit_group`], over the same kernels. +pub fn build_fri_group_tree_from_evals_ext3( + evals: &[u64], + group_log: u32, + hash: DeviceHash, +) -> Result> { + assert!(evals.len().is_multiple_of(3)); + let n = evals.len() / 3; + let num_leaves = n >> group_log; + assert!(num_leaves << group_log == n, "whole groups only"); + let be = backend()?; + let stream = be.next_stream(); + let evals_dev = stream.clone_htod(evals)?; + let nodes_dev = + commit_group_leaves(&stream, be, hash, &evals_dev, num_leaves, 1u64 << group_log)?; + let out = stream.clone_dtoh(&nodes_dev)?; + stream.synchronize()?; + Ok(out) } /// Gather interleaved ext3 elements at `positions` from a resident evals diff --git a/crypto/math-cuda/src/lde.rs b/crypto/math-cuda/src/lde.rs index b518187c5..958b81d46 100644 --- a/crypto/math-cuda/src/lde.rs +++ b/crypto/math-cuda/src/lde.rs @@ -1067,6 +1067,186 @@ fn build_inner_tree_levels_for( } } +/// Hash the leaves of a row-major commit over `buf` (`num_rows` rows of stride +/// `m`), columns `[col_start, col_end)`, `rows_per_leaf` rows per leaf, into +/// `leaves_out` (`num_rows / rows_per_leaf` leaves), with the kernel family +/// `hash` selects: +/// +/// - `rows_per_leaf = 2` (today): leaf `i` = rows `reverse_index(2i)`, +/// `reverse_index(2i + 1)` — the row-pair kernels, the full-row one when the +/// range is the whole row (so the default launches exactly what it did). +/// - `rows_per_leaf = 1` (S2): leaf `i` = the row `reverse_index(i)` — the +/// one-row kernels (`*_leaves_base_row_major_row_range`). The CPU twin is +/// `commit_rows_bit_reversed_subset_with(.., rows_per_leaf)`. +#[allow(clippy::too_many_arguments)] +pub(crate) fn launch_row_major_leaves( + hash: DeviceHash, + stream: &CudaStream, + be: &Backend, + buf: &CudaSlice, + m: u64, + col_start: u64, + col_end: u64, + num_rows: u64, + rows_per_leaf: usize, + leaves_out: &mut CudaViewMut<'_, u8>, +) -> Result<()> { + assert!( + rows_per_leaf == 1 || rows_per_leaf == 2, + "rows_per_leaf must be 1 or 2" + ); + // Every kernel derives rows as `__brevll(..) >> (64 - log_num_rows)`, UB at + // `log_num_rows == 0`. + assert!(num_rows >= 2 && num_rows.is_power_of_two()); + assert!( + col_start < col_end && col_end <= m, + "column range in bounds" + ); + let log_num_rows = num_rows.trailing_zeros() as u64; + let full = col_start == 0 && col_end == m; + if rows_per_leaf == 2 { + return match (hash, full) { + (DeviceHash::Keccak256, true) => launch_keccak_base_row_major_row_pair( + stream, + be, + buf, + m, + num_rows, + log_num_rows, + leaves_out, + ), + (DeviceHash::Keccak256, false) => launch_keccak_base_row_major_row_pair_range( + stream, + be, + buf, + m, + col_start, + col_end, + num_rows, + log_num_rows, + leaves_out, + ), + (DeviceHash::Blake3, true) => crate::blake3::launch_leaves_base_row_major_row_pair( + stream, + be, + buf, + m, + num_rows, + log_num_rows, + leaves_out, + ), + (DeviceHash::Blake3, false) => { + crate::blake3::launch_leaves_base_row_major_row_pair_range( + stream, + be, + buf, + m, + col_start, + col_end, + num_rows, + log_num_rows, + leaves_out, + ) + } + (DeviceHash::Rpx256, true) => crate::rpx::launch_leaves_base_row_major_row_pair( + stream, + be, + buf, + m, + num_rows, + log_num_rows, + leaves_out, + ), + (DeviceHash::Rpx256, false) => crate::rpx::launch_leaves_base_row_major_row_pair_range( + stream, + be, + buf, + m, + col_start, + col_end, + num_rows, + log_num_rows, + leaves_out, + ), + (DeviceHash::Rpo256 | DeviceHash::Poseidon, _) => { + unimplemented!("{hash:?} device commit not yet ported (row-major row-pair leaves)") + } + }; + } + // One row per leaf: one thread per row. + let (kernel, cfg) = match hash { + DeviceHash::Keccak256 => ( + &be.keccak256_leaves_base_row_major_row_range, + keccak_launch_cfg(num_rows), + ), + DeviceHash::Blake3 => ( + &be.blake3_leaves_base_row_major_row_range, + crate::blake3::blake3_launch_cfg(num_rows), + ), + DeviceHash::Rpx256 => ( + &be.rpx_leaves_base_row_major_row_range, + crate::rpx::rpx_launch_cfg(num_rows), + ), + DeviceHash::Rpo256 | DeviceHash::Poseidon => { + unimplemented!("{hash:?} device commit not yet ported (row-major one-row leaves)") + } + }; + unsafe { + stream + .launch_builder(kernel) + .arg(buf) + .arg(&m) + .arg(&col_start) + .arg(&col_end) + .arg(&num_rows) + .arg(&log_num_rows) + .arg(leaves_out) + .launch(cfg)?; + } + Ok(()) +} + +/// Row-major leaf hashing of a HOST row-major matrix under `hash` with +/// `rows_per_leaf` rows per leaf, columns `[col_start, col_end)`: the leaf +/// hashes alone (`num_rows / rows_per_leaf` × 32 bytes). A parity harness for +/// [`launch_row_major_leaves`] against the CPU leaf spec; nothing on a proving +/// path calls it. +pub fn row_major_leaves( + hash: DeviceHash, + data: &[u64], + m: usize, + col_start: usize, + col_end: usize, + num_rows: usize, + rows_per_leaf: usize, +) -> Result> { + assert!(num_rows.is_power_of_two() && num_rows >= 2); + assert!(rows_per_leaf == 1 || rows_per_leaf == 2); + let total = num_rows + .checked_mul(m) + .expect("num_rows * m overflows usize"); + assert!(data.len() >= total); + let be = backend()?; + let stream = be.next_stream(); + let data_dev = stream.clone_htod(&data[..total])?; + let mut out_dev = stream.alloc_zeros::((num_rows / rows_per_leaf) * 32)?; + launch_row_major_leaves( + hash, + stream.as_ref(), + be, + &data_dev, + m as u64, + col_start as u64, + col_end as u64, + num_rows as u64, + rows_per_leaf, + &mut out_dev.as_view_mut(), + )?; + let out = stream.clone_dtoh(&out_dev)?; + stream.synchronize()?; + Ok(out) +} + #[allow(clippy::type_complexity)] #[allow(clippy::too_many_arguments)] fn coset_lde_row_major_inner( @@ -1079,6 +1259,7 @@ fn coset_lde_row_major_inner( what: &str, retain_trace_col_major: bool, retain_host_lde: bool, + rows_per_leaf: usize, ) -> Result<( GpuMerkleTree, CudaSlice, @@ -1097,13 +1278,17 @@ fn coset_lde_row_major_inner( let lde_size = n * blowup_factor; assert_u32_domain(lde_size, what); - // Row-pair trace commit: one Merkle leaf per bit-reversed row pair (rows 2i, - // 2i+1), matching the CPU `commit_bit_reversed(.., ROWS_PER_LEAF=2)` and the - // verifier's `verify_opening_pair`. `lde_size` is a power of two >= 2, so it - // is always even. - let num_leaves = lde_size / 2; + // Trace commit with `rows_per_leaf` bit-reversed rows per Merkle leaf: row + // pairs (rows 2i, 2i+1) today, matching the CPU `commit_bit_reversed(.., + // ROWS_PER_LEAF=2)` and the verifier's `verify_opening_pair`; one row (S2) + // under `rows_per_leaf = 1`. `lde_size` is a power of two >= 2, so it is + // always a multiple of either. + assert!( + rows_per_leaf == 1 || rows_per_leaf == 2, + "rows_per_leaf must be 1 or 2" + ); + let num_leaves = lde_size / rows_per_leaf; let nodes_bytes = TreeCommit::FullTree.total_nodes_bytes(num_leaves); - let log_lde = lde_size.trailing_zeros() as u64; let lde_u64 = lde_size as u64; let cols_u64 = total_cols as u64; @@ -1122,45 +1307,25 @@ fn coset_lde_row_major_inner( )?; // Leaf hashing + Merkle on-device, with the kernel family `hash` selects. - // Each row-pair leaf reads two bit-reversed rows of `total_cols` consecutive - // u64s (`lde_u64` is the bit-reverse modulus; the kernel emits - // `lde_size / 2` leaves). + // Each leaf reads `rows_per_leaf` bit-reversed rows of `total_cols` + // consecutive u64s (`lde_u64` is the bit-reverse modulus; the kernel emits + // `lde_size / rows_per_leaf` leaves). let mut nodes_dev = unsafe { stream.alloc::(nodes_bytes) }?; let leaves_offset = TreeCommit::FullTree.leaves_offset_bytes(num_leaves); { let mut leaves_view = nodes_dev.slice_mut(leaves_offset..leaves_offset + num_leaves * 32); - match hash { - DeviceHash::Keccak256 => launch_keccak_base_row_major_row_pair( - stream.as_ref(), - be, - &buf, - cols_u64, - lde_u64, - log_lde, - &mut leaves_view, - )?, - DeviceHash::Blake3 => crate::blake3::launch_leaves_base_row_major_row_pair( - stream.as_ref(), - be, - &buf, - cols_u64, - lde_u64, - log_lde, - &mut leaves_view, - )?, - DeviceHash::Rpx256 => crate::rpx::launch_leaves_base_row_major_row_pair( - stream.as_ref(), - be, - &buf, - cols_u64, - lde_u64, - log_lde, - &mut leaves_view, - )?, - DeviceHash::Rpo256 | DeviceHash::Poseidon => { - unimplemented!("{hash:?} device commit not yet ported (row-major row-pair leaves)") - } - } + launch_row_major_leaves( + hash, + stream.as_ref(), + be, + &buf, + cols_u64, + 0, + cols_u64, + lde_u64, + rows_per_leaf, + &mut leaves_view, + )?; } build_inner_tree_levels_for(hash, stream.as_ref(), be, &mut nodes_dev, num_leaves)?; @@ -1281,6 +1446,34 @@ pub fn coset_lde_row_major_with_merkle_tree_keep( blowup_factor: usize, weights: &[u64], retain_host_lde: bool, +) -> Result<(GpuLdeBase, Vec)> { + coset_lde_row_major_with_merkle_tree_keep_rpl( + row_major, + predev, + hash, + n, + m, + blowup_factor, + weights, + retain_host_lde, + 2, + ) +} + +/// [`coset_lde_row_major_with_merkle_tree_keep`] with `rows_per_leaf` rows per +/// Merkle leaf: 2 is today's row pair, 1 the S2 one-row tree (twice the +/// leaves, `(2·lde − 1)·32` node bytes instead of `(lde − 1)·32`). +#[allow(clippy::too_many_arguments)] +pub fn coset_lde_row_major_with_merkle_tree_keep_rpl( + row_major: &[u64], + predev: Option<&CudaSlice>, + hash: DeviceHash, + n: usize, + m: usize, + blowup_factor: usize, + weights: &[u64], + retain_host_lde: bool, + rows_per_leaf: usize, ) -> Result<(GpuLdeBase, Vec)> { let input = match predev { Some(d) if d.len() == row_major.len() => InnerInput::Dev(d), @@ -1296,6 +1489,7 @@ pub fn coset_lde_row_major_with_merkle_tree_keep( "coset_lde_row_major lde_size", true, retain_host_lde, + rows_per_leaf, )?; let handle = GpuLdeBase { buf: Arc::new(col_major_dev), @@ -1337,6 +1531,39 @@ pub fn coset_lde_row_major_split_trees( split_col: usize, build_precomputed: bool, retain_host_lde: bool, +) -> Result<(Option>, GpuLdeBase, Vec)> { + coset_lde_row_major_split_trees_rpl( + row_major, + predev, + hash, + n, + m, + blowup_factor, + weights, + split_col, + build_precomputed, + retain_host_lde, + 2, + ) +} + +/// [`coset_lde_row_major_split_trees`] with `rows_per_leaf` rows per Merkle +/// leaf in BOTH subset trees (a table has one leaf layout): 2 = row pair, +/// 1 = S2 one row. +#[allow(clippy::type_complexity)] +#[allow(clippy::too_many_arguments)] +pub fn coset_lde_row_major_split_trees_rpl( + row_major: &[u64], + predev: Option<&CudaSlice>, + hash: DeviceHash, + n: usize, + m: usize, + blowup_factor: usize, + weights: &[u64], + split_col: usize, + build_precomputed: bool, + retain_host_lde: bool, + rows_per_leaf: usize, ) -> Result<(Option>, GpuLdeBase, Vec)> { assert!(split_col > 0 && split_col < m, "split inside the row"); assert!(n.is_power_of_two(), "n must be a power of two"); @@ -1348,10 +1575,13 @@ pub fn coset_lde_row_major_split_trees( assert_eq!(row_major.len(), n * m, "row-major input shape"); let lde_size = n * blowup_factor; assert_u32_domain(lde_size, "coset_lde_row_major_split lde_size"); - let num_leaves = lde_size / 2; + assert!( + rows_per_leaf == 1 || rows_per_leaf == 2, + "rows_per_leaf must be 1 or 2" + ); + let num_leaves = lde_size / rows_per_leaf; let nodes_bytes = TreeCommit::FullTree.total_nodes_bytes(num_leaves); let leaves_offset = TreeCommit::FullTree.leaves_offset_bytes(num_leaves); - let log_lde = lde_size.trailing_zeros() as u64; let lde_u64 = lde_size as u64; let cols_u64 = m as u64; @@ -1371,44 +1601,18 @@ pub fn coset_lde_row_major_split_trees( { let mut leaves_view = nodes_dev.slice_mut(leaves_offset..leaves_offset + num_leaves * 32); - match hash { - DeviceHash::Keccak256 => launch_keccak_base_row_major_row_pair_range( - stream.as_ref(), - be, - &buf, - cols_u64, - col_start, - col_end, - lde_u64, - log_lde, - &mut leaves_view, - )?, - DeviceHash::Blake3 => crate::blake3::launch_leaves_base_row_major_row_pair_range( - stream.as_ref(), - be, - &buf, - cols_u64, - col_start, - col_end, - lde_u64, - log_lde, - &mut leaves_view, - )?, - DeviceHash::Rpx256 => crate::rpx::launch_leaves_base_row_major_row_pair_range( - stream.as_ref(), - be, - &buf, - cols_u64, - col_start, - col_end, - lde_u64, - log_lde, - &mut leaves_view, - )?, - DeviceHash::Rpo256 | DeviceHash::Poseidon => unimplemented!( - "{hash:?} device commit not yet ported (row-major row-pair leaves, column range)" - ), - } + launch_row_major_leaves( + hash, + stream.as_ref(), + be, + &buf, + cols_u64, + col_start, + col_end, + lde_u64, + rows_per_leaf, + &mut leaves_view, + )?; } build_inner_tree_levels_for(hash, stream.as_ref(), be, &mut nodes_dev, num_leaves)?; Ok(nodes_dev) @@ -1491,6 +1695,31 @@ pub fn coset_lde_ext3_row_major_with_merkle_tree_keep( blowup_factor: usize, weights: &[u64], retain_host_lde: bool, +) -> Result<(GpuLdeExt3, Vec)> { + coset_lde_ext3_row_major_with_merkle_tree_keep_rpl( + row_major, + hash, + n, + m, + blowup_factor, + weights, + retain_host_lde, + 2, + ) +} + +/// [`coset_lde_ext3_row_major_with_merkle_tree_keep`] with `rows_per_leaf` rows per Merkle leaf (2 = row pair, 1 = S2 +/// one row). +#[allow(clippy::too_many_arguments)] +pub fn coset_lde_ext3_row_major_with_merkle_tree_keep_rpl( + row_major: &[u64], + hash: DeviceHash, + n: usize, + m: usize, + blowup_factor: usize, + weights: &[u64], + retain_host_lde: bool, + rows_per_leaf: usize, ) -> Result<(GpuLdeExt3, Vec)> { let (tree, col_major_dev, lde_out, _, ready) = coset_lde_row_major_inner( InnerInput::Host(row_major), @@ -1502,6 +1731,7 @@ pub fn coset_lde_ext3_row_major_with_merkle_tree_keep( "coset_lde_ext3_row_major lde_size", false, retain_host_lde, + rows_per_leaf, )?; let handle = GpuLdeExt3 { buf: Arc::new(col_major_dev), @@ -1525,6 +1755,31 @@ pub fn coset_lde_ext3_row_major_with_merkle_tree_keep_dev( blowup_factor: usize, weights: &[u64], retain_host_lde: bool, +) -> Result<(GpuLdeExt3, Vec)> { + coset_lde_ext3_row_major_with_merkle_tree_keep_dev_rpl( + input_dev, + hash, + n, + m, + blowup_factor, + weights, + retain_host_lde, + 2, + ) +} + +/// [`coset_lde_ext3_row_major_with_merkle_tree_keep_dev`] with `rows_per_leaf` rows per Merkle leaf (2 = row pair, 1 = S2 +/// one row). +#[allow(clippy::too_many_arguments)] +pub fn coset_lde_ext3_row_major_with_merkle_tree_keep_dev_rpl( + input_dev: &CudaSlice, + hash: DeviceHash, + n: usize, + m: usize, + blowup_factor: usize, + weights: &[u64], + retain_host_lde: bool, + rows_per_leaf: usize, ) -> Result<(GpuLdeExt3, Vec)> { let (tree, col_major_dev, lde_out, _, ready) = coset_lde_row_major_inner( InnerInput::Dev(input_dev), @@ -1536,6 +1791,7 @@ pub fn coset_lde_ext3_row_major_with_merkle_tree_keep_dev( "coset_lde_ext3_row_major_dev lde_size", false, retain_host_lde, + rows_per_leaf, )?; let handle = GpuLdeExt3 { buf: Arc::new(col_major_dev), diff --git a/crypto/math-cuda/src/merkle.rs b/crypto/math-cuda/src/merkle.rs index 8510200ce..036d9c525 100644 --- a/crypto/math-cuda/src/merkle.rs +++ b/crypto/math-cuda/src/merkle.rs @@ -440,6 +440,47 @@ pub fn gather_merkle_paths_dev( Ok(host) } +/// Read the Merkle cap at height `cap_height` off a device-resident tree: the +/// `2^c` nodes `c` levels below the root, left to right, as `2^c * 32` bytes. +/// +/// No kernel: the device heap has the host layout (root at node 0, the level +/// with `2^c` nodes at `[2^c - 1, 2^{c+1} - 1)`), so the cap is one D2H of the +/// heap slice `[(2^c - 1) * 32, (2^{c+1} - 1) * 32)`. The +/// same nodes `MerkleTree::cap` returns on the host tree, byte for byte. +/// `cap_height = 0` is the root. Runs on the caller's `stream`, after the work +/// already queued on it, and waits for the copy. +/// +/// Panics on a shape no caller may pass (the same contract as +/// [`gather_merkle_paths_dev`]): `leaves_len` not a power of two, a cap taller +/// than the tree, or a node buffer too short for the heap it claims to hold. +pub fn read_cap_dev( + nodes_dev: &CudaSlice, + leaves_len: usize, + cap_height: usize, + stream: &Arc, +) -> Result> { + assert!( + leaves_len.is_power_of_two(), + "read_cap_dev: leaves_len must be a power of two" + ); + let depth = leaves_len.trailing_zeros() as usize; + assert!( + cap_height <= depth, + "read_cap_dev: cap height {cap_height} exceeds the tree depth {depth}" + ); + let start = ((1usize << cap_height) - 1) * 32; + let end = ((2usize << cap_height) - 1) * 32; + assert!( + end <= nodes_dev.len(), + "read_cap_dev: node buffer of {} bytes is shorter than the cap slice end {end}", + nodes_dev.len() + ); + let mut host = vec![0u8; end - start]; + stream.memcpy_dtoh(&nodes_dev.slice(start..end), &mut host)?; + stream.synchronize()?; + Ok(host) +} + /// Build the composition Merkle tree on device. `parts_interleaved` is /// `num_parts` slices, each an ext3 LDE column interleaved as /// `[a0,a1,a2, b0,b1,b2, ...]` of length `3*lde_size`. Leaves hash row pairs, so @@ -447,7 +488,12 @@ pub fn gather_merkle_paths_dev( /// and the stream it was built on. Used by the device keep wrapper below. fn build_comp_poly_tree_nodes_dev( parts_interleaved: &[&[u64]], + rows_per_leaf: usize, ) -> Result<(CudaSlice, usize, Arc)> { + assert!( + rows_per_leaf == 1 || rows_per_leaf == 2, + "rows_per_leaf must be 1 or 2" + ); assert!(!parts_interleaved.is_empty()); let m = parts_interleaved.len(); let ext3_elems = parts_interleaved[0].len() / 3; @@ -461,7 +507,7 @@ fn build_comp_poly_tree_nodes_dev( } let lde_size = ext3_elems; assert!(lde_size.is_power_of_two() && lde_size >= 2); - let num_leaves = lde_size / 2; + let num_leaves = lde_size / rows_per_leaf; let tight_total_nodes = 2 * num_leaves - 1; let be = backend()?; @@ -495,9 +541,16 @@ fn build_comp_poly_tree_nodes_dev( let num_rows_u64 = lde_size as u64; let log_num_rows = lde_size.trailing_zeros() as u64; let cfg = keccak_launch_cfg(num_leaves as u64); + // Row pairs: rows `2i`, `2i+1` of every part; one row (S2): the row + // `reverse_index(i)` alone — the one-row ext3 kernel, same arguments. + let kernel = if rows_per_leaf == 2 { + &be.keccak_comp_poly_leaves_ext3 + } else { + &be.keccak256_leaves_ext3_batched + }; unsafe { stream - .launch_builder(&be.keccak_comp_poly_leaves_ext3) + .launch_builder(kernel) .arg(&buf) .arg(&col_stride_u64) .arg(&num_parts_u64) @@ -528,12 +581,28 @@ pub fn build_comp_poly_tree_from_slabs_dev( m: usize, lde_size: usize, ) -> Result { + build_comp_poly_tree_from_slabs_dev_rpl(stream, buf, m, lde_size, 2) +} + +/// [`build_comp_poly_tree_from_slabs_dev`] with `rows_per_leaf` rows per leaf +/// (2 = row pair, 1 = S2 one row: `lde_size` leaves). +pub fn build_comp_poly_tree_from_slabs_dev_rpl( + stream: &Arc, + buf: &CudaSlice, + m: usize, + lde_size: usize, + rows_per_leaf: usize, +) -> Result { + assert!( + rows_per_leaf == 1 || rows_per_leaf == 2, + "rows_per_leaf must be 1 or 2" + ); #[cfg(feature = "test-faults")] crate::faults::check_sticky(&crate::faults::FAULT_COMP_TREE_STICKY)?; assert!(m > 0); assert!(lde_size.is_power_of_two() && lde_size >= 2); assert_eq!(buf.len(), 3 * m * lde_size, "slab buffer shape"); - let num_leaves = lde_size / 2; + let num_leaves = lde_size / rows_per_leaf; let tight_total_nodes = 2 * num_leaves - 1; let be = backend()?; @@ -547,9 +616,16 @@ pub fn build_comp_poly_tree_from_slabs_dev( let num_rows_u64 = lde_size as u64; let log_num_rows = lde_size.trailing_zeros() as u64; let cfg = keccak_launch_cfg(num_leaves as u64); + // Row pairs: rows `2i`, `2i+1` of every part; one row (S2): the row + // `reverse_index(i)` alone — the one-row ext3 kernel, same arguments. + let kernel = if rows_per_leaf == 2 { + &be.keccak_comp_poly_leaves_ext3 + } else { + &be.keccak256_leaves_ext3_batched + }; unsafe { stream - .launch_builder(&be.keccak_comp_poly_leaves_ext3) + .launch_builder(kernel) .arg(buf) .arg(&col_stride_u64) .arg(&num_parts_u64) @@ -582,10 +658,20 @@ pub fn build_comp_poly_tree_from_slabs_dev( /// tree to host. `leaves_len = lde_size / 2` (row pair leaves). pub fn build_comp_poly_tree_from_evals_ext3_keep( parts_interleaved: &[&[u64]], +) -> Result { + build_comp_poly_tree_from_evals_ext3_keep_rpl(parts_interleaved, 2) +} + +/// [`build_comp_poly_tree_from_evals_ext3_keep`] with `rows_per_leaf` rows per +/// leaf (2 = row pair, 1 = S2 one row: `lde_size` leaves). +pub fn build_comp_poly_tree_from_evals_ext3_keep_rpl( + parts_interleaved: &[&[u64]], + rows_per_leaf: usize, ) -> Result { #[cfg(feature = "test-faults")] crate::faults::check_sticky(&crate::faults::FAULT_COMP_TREE_STICKY)?; - let (nodes_dev, num_leaves, stream) = build_comp_poly_tree_nodes_dev(parts_interleaved)?; + let (nodes_dev, num_leaves, stream) = + build_comp_poly_tree_nodes_dev(parts_interleaved, rows_per_leaf)?; let mut root = [0u8; 32]; stream.memcpy_dtoh(&nodes_dev.slice(0..32), &mut root)?; stream.synchronize()?; diff --git a/crypto/math-cuda/src/rpx.rs b/crypto/math-cuda/src/rpx.rs index 74dda58b6..475dc7d5c 100644 --- a/crypto/math-cuda/src/rpx.rs +++ b/crypto/math-cuda/src/rpx.rs @@ -582,6 +582,22 @@ pub fn build_comp_poly_tree_from_slabs_dev( m: usize, lde_size: usize, ) -> Result { + build_comp_poly_tree_from_slabs_dev_rpl(stream, buf, m, lde_size, 2) +} + +/// [`build_comp_poly_tree_from_slabs_dev`] with `rows_per_leaf` rows per leaf +/// (2 = row pair, 1 = S2 one row: `lde_size` leaves, the one-row ext3 kernel). +pub fn build_comp_poly_tree_from_slabs_dev_rpl( + stream: &Arc, + buf: &CudaSlice, + m: usize, + lde_size: usize, + rows_per_leaf: usize, +) -> Result { + assert!( + rows_per_leaf == 1 || rows_per_leaf == 2, + "rows_per_leaf must be 1 or 2" + ); // Same sticky hook as the keccak and BLAKE3 twins: the comp-tree cliff test // arms one counter and must reach it under whichever hash the build pins. #[cfg(feature = "test-faults")] @@ -589,7 +605,7 @@ pub fn build_comp_poly_tree_from_slabs_dev( assert!(m > 0); assert!(lde_size.is_power_of_two() && lde_size >= 2); assert_eq!(buf.len(), 3 * m * lde_size, "slab buffer shape"); - let num_leaves = lde_size / 2; + let num_leaves = lde_size / rows_per_leaf; let tight_total_nodes = 2 * num_leaves - 1; let be = backend()?; @@ -600,7 +616,12 @@ pub fn build_comp_poly_tree_from_slabs_dev( { let mut leaves_view = nodes_dev.slice_mut(leaves_offset_bytes..leaves_offset_bytes + num_leaves * 32); - launch_ext3_row_pair( + let launch = if rows_per_leaf == 2 { + launch_ext3_row_pair + } else { + launch_leaves_ext3 + }; + launch( stream.as_ref(), buf, lde_size as u64, @@ -629,6 +650,15 @@ pub fn build_comp_poly_tree_from_slabs_dev( /// stages through the same pinned de-interleave buffer for the same reason. pub fn build_comp_poly_tree_from_evals_ext3_keep( parts_interleaved: &[&[u64]], +) -> Result { + build_comp_poly_tree_from_evals_ext3_keep_rpl(parts_interleaved, 2) +} + +/// [`build_comp_poly_tree_from_evals_ext3_keep`] with `rows_per_leaf` rows per +/// leaf (2 = row pair, 1 = S2 one row). +pub fn build_comp_poly_tree_from_evals_ext3_keep_rpl( + parts_interleaved: &[&[u64]], + rows_per_leaf: usize, ) -> Result { #[cfg(feature = "test-faults")] crate::faults::check_sticky(&crate::faults::FAULT_COMP_TREE_STICKY)?; @@ -666,7 +696,7 @@ pub fn build_comp_poly_tree_from_evals_ext3_keep( stream.synchronize()?; drop(staging); - build_comp_poly_tree_from_slabs_dev(&stream, &buf, m, lde_size) + build_comp_poly_tree_from_slabs_dev_rpl(&stream, &buf, m, lde_size, rows_per_leaf) } /// Build a FRI-layer Merkle tree on device under RPX from an interleaved ext3 diff --git a/crypto/math-cuda/src/whir.rs b/crypto/math-cuda/src/whir.rs index 40785c084..1b63622ca 100644 --- a/crypto/math-cuda/src/whir.rs +++ b/crypto/math-cuda/src/whir.rs @@ -729,6 +729,37 @@ impl DeviceCodeword { }) } + /// [`paths`](Self::paths) and the tree's Merkle cap at `cap_height`, from + /// ONE rebuild: the cap is the heap slice `[2^c − 1, 2^{c+1} − 1)` of the + /// same node buffer the paths are gathered from (root at node 0, the host + /// `MerkleTree` layout), so the two cannot come from different trees. + /// + /// Returns `(paths, cap)`: the paths exactly as [`paths`](Self::paths) + /// returns them (full depth — the caller cuts them to the cap), and + /// `2^cap_height` 32-byte nodes, left to right. `cap_height = 0` gives the + /// root. No kernel: the cap is a device-to-host copy of `2^c` nodes. + pub fn paths_and_cap( + &self, + log_folding: usize, + positions: &[u32], + cap_height: usize, + hash: crate::DeviceHash, + ) -> Result<(Vec, Vec)> { + self.with_tree(log_folding, hash, |nodes, num_leaves| { + assert!( + num_leaves.is_power_of_two() && cap_height <= num_leaves.trailing_zeros() as usize, + "a cap of height {cap_height} does not fit a tree of {num_leaves} leaves" + ); + let paths = + crate::merkle::gather_merkle_paths_dev(nodes, num_leaves, positions, &self.stream)?; + let start = ((1usize << cap_height) - 1) * 32; + let end = ((2usize << cap_height) - 1) * 32; + let cap = self.stream.clone_dtoh(&nodes.slice(start..end))?; + self.stream.synchronize()?; + Ok((paths, cap)) + }) + } + /// The fold blocks `indices` open — `block` values at stride `num_leaves` /// from each — gathered where they lie, one launch and one copy back. /// diff --git a/crypto/math-cuda/tests/fri_group_tree.rs b/crypto/math-cuda/tests/fri_group_tree.rs new file mode 100644 index 000000000..5bdd51d42 --- /dev/null +++ b/crypto/math-cuda/tests/fri_group_tree.rs @@ -0,0 +1,336 @@ +//! S3 group-leaf FRI layers on the device. +//! +//! - The group-leaf trees (`build_fri_group_tree_from_evals_ext3`, the kernels +//! `FriCommitState::fold_and_commit_group` commits with) equal the host tree +//! node for node: leaf `g` = the configuration's `Batched` leaf over the +//! `2^d` consecutive values from `g·2^d`, parents the pair hash. Keccak and +//! Blake3 against the host backends, d = 1..=6 at several sizes. +//! - Against the checked-in S3 vectors (`crypto/stark/tests/vectors/zf_fri`): +//! (c) the first-leaf digest and the layer root of the KAT codeword for +//! d = 1..=6 under Keccak, Blake3 AND RPX (the host RPX backend lives in the +//! prover crate; the vector is its output); (b) the KAT codeword folded d +//! times on the device with ζ, ζ², … equals the vector's `folded`. +//! - At d = 1 the group kernel IS the legacy pair-leaf kernel (the two-element +//! invariant, on the device), under all three hashes. +//! +//! Needs a GPU. + +use crypto::merkle_tree::merkle::MerkleTree; +use crypto::merkle_tree::traits::IsStreamingLeafBackend; +use math::fft::bit_reversing::in_place_bit_reverse_permute; +use math::fft::roots_of_unity::get_powers_of_primitive_root_coset; +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; +use math::field::goldilocks::GoldilocksField; +use math_cuda::DeviceHash; +use math_cuda::fri::{FriCommitState, build_fri_group_tree_from_evals_ext3}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; +use stark::config::{Blake3StarkHash, KeccakStarkHash, StarkHash}; + +type F = GoldilocksField; +type E = Degree3GoldilocksExtensionField; +type Fp = FieldElement; +type Fp3 = FieldElement; + +fn vectors_dir() -> std::path::PathBuf { + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../stark/tests/vectors/zf_fri") +} + +fn read_vector(name: &str) -> String { + std::fs::read_to_string(vectors_dir().join(name)).unwrap_or_else(|e| panic!("{name}: {e}")) +} + +/// Every decimal integer in `s`, in order (the vectors' ext limbs). +fn u64s(s: &str) -> Vec { + let mut out = Vec::new(); + let mut cur: Option = None; + for ch in s.chars() { + match ch.to_digit(10) { + Some(d) => cur = Some(cur.unwrap_or(0) * 10 + u64::from(d)), + None => { + if let Some(v) = cur.take() { + out.push(v); + } + } + } + } + if let Some(v) = cur { + out.push(v); + } + out +} + +/// The value of `"key": [...]` on `line`, up to the bracket that closes it. +fn json_array<'a>(line: &'a str, key: &str) -> &'a str { + let start = line + .find(&format!("\"{key}\": [")) + .unwrap_or_else(|| panic!("no {key}")) + + key.len() + + 4; + let mut depth = 0i32; + for (i, ch) in line[start..].char_indices() { + match ch { + '[' => depth += 1, + ']' => { + depth -= 1; + if depth == 0 { + return &line[start..start + i + 1]; + } + } + _ => {} + } + } + panic!("unterminated {key}") +} + +fn hex32(s: &str) -> [u8; 32] { + let mut out = [0u8; 32]; + for (i, b) in out.iter_mut().enumerate() { + *b = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).expect("hex"); + } + out +} + +/// The vector (b) KAT codeword as interleaved limbs, and per d its ζ and the +/// folded codeword. +#[allow(clippy::type_complexity)] +fn fold_vector() -> (Vec, Vec<(u32, [u64; 3], Vec)>) { + let text = read_vector("b_group_folds.json"); + let codeword_line = text + .lines() + .find(|l| l.trim_start().starts_with("\"codeword\"")) + .expect("codeword line"); + let codeword = u64s(json_array(codeword_line, "codeword")); + assert_eq!(codeword.len(), 3 * 128); + let mut folds = Vec::new(); + for line in text.lines().filter(|l| l.contains("\"folded\"")) { + let d = u64s(&line[..line.find("\"zeta\"").expect("zeta")])[0] as u32; + let z = u64s(json_array(line, "zeta")); + let folded = u64s(json_array(line, "folded")); + assert_eq!(folded.len(), 3 * (128 >> d)); + folds.push((d, [z[0], z[1], z[2]], folded)); + } + assert_eq!(folds.len(), 6); + (codeword, folds) +} + +/// The vector (c) digests for `hash`: per d, (first leaf, layer root). +fn leaf_vector(hash: &str) -> Vec<(u32, [u8; 32], [u8; 32])> { + let text = read_vector(&format!("c_leaf_digests_{hash}.json")); + let mut out = Vec::new(); + for line in text.lines().filter(|l| l.contains("\"first_leaf\"")) { + let field = |key: &str| { + let at = line.find(&format!("\"{key}\": \"")).expect(key) + key.len() + 5; + hex32(&line[at..at + 64]) + }; + let d = u64s(&line[..line.find("\"first_leaf\"").expect("first_leaf")])[0] as u32; + out.push((d, field("first_leaf"), field("layer_root"))); + } + assert_eq!(out.len(), 6, "{hash}: d = 1..=6"); + out +} + +fn limbs(v: &[Fp3]) -> Vec { + v.iter() + .flat_map(|e| { + let c = e.value(); + [c[0].canonical(), c[1].canonical(), c[2].canonical()] + }) + .collect() +} + +fn from_limbs(v: &[u64]) -> Vec { + v.chunks_exact(3) + .map(|c| Fp3::new([Fp::from(c[0]), Fp::from(c[1]), Fp::from(c[2])])) + .collect() +} + +/// The host group tree: `H::Batched` leaves over consecutive groups, the pair +/// hash above (as `stark::fri::group_tree`). +fn host_group_nodes(evals: &[Fp3], group: usize) -> Vec<[u8; 32]> { + let leaves: Vec<[u8; 32]> = evals + .chunks_exact(group) + .map(|g| as IsStreamingLeafBackend>::hash_data_from_slices(g, &[])) + .collect(); + MerkleTree::>::build_from_hashed_leaves(leaves) + .expect("tree") + .nodes() + .to_vec() +} + +fn assert_nodes_eq(device: &[u8], host: &[[u8; 32]], what: &str) { + assert_eq!(device.len(), host.len() * 32, "{what}: node count"); + for (i, h) in host.iter().enumerate() { + assert_eq!(&device[i * 32..(i + 1) * 32], &h[..], "{what}: node {i}"); + } +} + +fn random_evals(n: usize, seed: u64) -> Vec { + let mut rng = ChaCha8Rng::seed_from_u64(seed); + (0..n) + .map(|_| { + Fp3::new([ + Fp::from_raw(rng.r#gen::()), + Fp::from_raw(rng.r#gen::()), + Fp::from_raw(rng.r#gen::()), + ]) + }) + .collect() +} + +fn host_parity(hash: DeviceHash, name: &str) { + for d in 1..=6u32 { + for extra in [1u32, 4, 9] { + let n = 1usize << (d + extra); + let evals = random_evals(n, 1000 + u64::from(d * 16 + extra)); + let raw: Vec = evals + .iter() + .flat_map(|e| { + let c = e.value(); + [*c[0].value(), *c[1].value(), *c[2].value()] + }) + .collect(); + let device = build_fri_group_tree_from_evals_ext3(&raw, d, hash).expect("device"); + assert_nodes_eq( + &device, + &host_group_nodes::(&evals, 1 << d), + &format!("{name} d={d} n=2^{}", d + extra), + ); + } + } +} + +#[test] +fn group_tree_matches_host_keccak() { + host_parity::(DeviceHash::Keccak256, "keccak"); +} + +#[test] +fn group_tree_matches_host_blake3() { + host_parity::(DeviceHash::Blake3, "blake3"); +} + +#[test] +fn group_tree_matches_the_leaf_vectors() { + let (codeword, _) = fold_vector(); + for (hash, name) in [ + (DeviceHash::Keccak256, "keccak"), + (DeviceHash::Blake3, "blake3"), + (DeviceHash::Rpx256, "rpx"), + ] { + for (d, first_leaf, root) in leaf_vector(name) { + let nodes = build_fri_group_tree_from_evals_ext3(&codeword, d, hash).expect("device"); + let num_leaves = 128usize >> d; + let leaf0 = (num_leaves - 1) * 32; + assert_eq!( + &nodes[leaf0..leaf0 + 32], + &first_leaf, + "{name} d={d}: first leaf" + ); + assert_eq!(&nodes[..32], &root, "{name} d={d}: layer root"); + } + } +} + +#[test] +fn group_of_two_is_the_pair_leaf_kernel() { + let evals = random_evals(1 << 12, 77); + let raw = limbs(&evals); + for (hash, pair) in [ + ( + DeviceHash::Keccak256, + math_cuda::merkle::build_fri_layer_tree_from_evals_ext3(&raw).expect("keccak"), + ), + ( + DeviceHash::Blake3, + math_cuda::blake3::build_fri_layer_tree_from_evals_ext3(&raw).expect("blake3"), + ), + ( + DeviceHash::Rpx256, + math_cuda::rpx::build_fri_layer_tree_from_evals_ext3(&raw).expect("rpx"), + ), + ] { + let group = build_fri_group_tree_from_evals_ext3(&raw, 1, hash).expect("group"); + assert_eq!(group, pair, "{hash:?}: d = 1 group tree != pair tree"); + } +} + +/// `compute_coset_twiddles_inv`: the inverses of the coset points at the even +/// bit-reversed positions (`o·ω^i`, `i < n/2`, bit-reversed, inverted). +fn fold_twiddles(offset: u64, n: usize) -> Vec { + let mut pts = get_powers_of_primitive_root_coset::( + n.trailing_zeros() as u64, + n / 2, + &Fp::from(offset), + ) + .expect("roots"); + in_place_bit_reverse_permute(&mut pts); + pts.iter() + .map(|p| p.inv().expect("nonzero").canonical()) + .collect() +} + +fn zeta_powers(z: [u64; 3], d: u32) -> Vec<[u64; 3]> { + let mut zeta = from_limbs(&z)[0]; + let mut out = Vec::new(); + for level in 0..d { + out.push(limbs(&[zeta]).try_into().expect("3 limbs")); + if level + 1 < d { + zeta = zeta.square(); + } + } + out +} + +/// (b): the device folds (`fold_to_host`, the S3 terminal step) reproduce the +/// vector's `folded` for d = 1..=6; and `fold_and_commit_group` with no fold +/// commits the KAT codeword itself to the (c) root, with d − 1 folds then a +/// group of 2 to the root of the folded codeword's pair tree. +#[test] +fn device_folds_match_the_fold_vector() { + let (codeword, folds) = fold_vector(); + let tw = fold_twiddles(3, 128); + for (d, z, folded) in &folds { + let mut st = + FriCommitState::new(&codeword, &tw, 128, DeviceHash::Keccak256).expect("state"); + let out = st.fold_to_host(&zeta_powers(*z, *d)).expect("fold"); + assert_eq!(limbs(&from_limbs(&out)), *folded, "d={d}: folded codeword"); + } + for (hash, name) in [ + (DeviceHash::Keccak256, "keccak"), + (DeviceHash::Blake3, "blake3"), + (DeviceHash::Rpx256, "rpx"), + ] { + for (d, _, root) in leaf_vector(name) { + let mut st = FriCommitState::new(&codeword, &tw, 128, hash).expect("state"); + let (host, _, tree) = st.fold_and_commit_group(&[], d, true).expect("commit"); + assert_eq!(tree.root, root, "{name} d={d}: zero-fold group commit root"); + assert_eq!(tree.leaves_len, 128 >> d); + assert_eq!( + host.expect("drained"), + codeword, + "{name} d={d}: layer evals" + ); + } + // Folds then a commit: d folds, then groups of 2 over the result. + for (d, z, folded) in folds.iter().filter(|(d, _, _)| *d <= 5) { + let mut st = FriCommitState::new(&codeword, &tw, 128, hash).expect("state"); + let (host, _, tree) = st + .fold_and_commit_group(&zeta_powers(*z, *d), 1, true) + .expect("commit"); + let host = host.expect("drained"); + assert_eq!( + limbs(&from_limbs(&host)), + *folded, + "{name} d={d}: layer evals" + ); + let expect = build_fri_group_tree_from_evals_ext3(folded, 1, hash).expect("tree"); + assert_eq!( + &tree.root[..], + &expect[..32], + "{name} d={d}: folded layer root" + ); + } + } +} diff --git a/crypto/math-cuda/tests/host_kat/blake3_host_kat.cpp b/crypto/math-cuda/tests/host_kat/blake3_host_kat.cpp index 42b0b05f4..2def5c3c3 100644 --- a/crypto/math-cuda/tests/host_kat/blake3_host_kat.cpp +++ b/crypto/math-cuda/tests/host_kat/blake3_host_kat.cpp @@ -694,6 +694,55 @@ void row_major_leaf_kernels_read_the_specified_bytes() { printf("row-major leaf kernels: read pattern matches the CPU leaf spec, all column ranges\n"); } +// The row-major ONE-ROW kernel (S2, rows_per_leaf = 1): leaf `i` is the single +// row `reverse_index(i)` over `log_n` bits, every non-empty column range. Also +// the control that it is NOT the row-pair kernel's first row: at n >= 4 the +// one-row leaf 1 is row brev(1) = n/2, the row-pair leaf 0's second row, never +// row brev(2) (the pair kernel's leaf 1 first row). +void row_major_one_row_kernel_reads_the_specified_bytes() { + for (uint32_t log_n : {1u, 2u, 4u, 6u}) { + for (uint64_t m : {1ull, 5ull, 13ull}) { + uint64_t n = 1ull << log_n; + std::vector data(n * m); + for (size_t i = 0; i < data.size(); ++i) data[i] = sample(log_n * 11 + m, i); + for (uint64_t cs = 0; cs < m; ++cs) { + for (uint64_t ce = cs + 1; ce <= m; ++ce) { + std::vector out(n * 32, 0); + CUDA_HOST_FOR_EACH_THREAD(t, n) { + blake3_leaves_base_row_major_row_range(data.data(), m, cs, ce, n, log_n, + out.data()); + } + std::vector> want(n); + for (uint64_t leaf = 0; leaf < n; ++leaf) { + uint64_t br = reverse_index(leaf, log_n); + for (uint64_t c = cs; c < ce; ++c) push_be(want[leaf], data[br * m + c]); + } + check_leaves(out, want, "blake3_leaves_base_row_major_row_range"); + } + } + } + } + // The one-row tree has TWICE the leaves of the row-pair tree over the same + // rows, and its leaves are not the pair tree's: a one-row kernel that read + // row pairs would match neither the spec above nor differ here. + { + const uint32_t log_n = 4; + const uint64_t n = 1ull << log_n, m = 3; + std::vector data(n * m); + for (size_t i = 0; i < data.size(); ++i) data[i] = sample(0x0E, i); + std::vector one(n * 32, 0), pair((n / 2) * 32, 0); + CUDA_HOST_FOR_EACH_THREAD(t, n) { + blake3_leaves_base_row_major_row_range(data.data(), m, 0, m, n, log_n, one.data()); + } + CUDA_HOST_FOR_EACH_THREAD(t, n / 2) { + blake3_leaves_base_row_major_row_pair(data.data(), m, n, log_n, pair.data()); + } + check(memcmp(one.data(), pair.data(), 32) != 0, + "a one-row leaf must not equal the row-pair leaf over the same first row"); + } + printf("row-major one-row kernel: read pattern matches the CPU one-row leaf spec, all column ranges\n"); +} + // The full-range ranged kernel must be the unranged one — the same bytes by two // code paths. A cheap check that the range arithmetic has no off-by-one at the // boundary it is most likely to have one at. @@ -767,6 +816,7 @@ int main() { fri_leaf_kernel_reads_the_specified_bytes(); row_major_leaf_kernels_read_the_specified_bytes(); the_full_range_variant_equals_the_plain_one(); + row_major_one_row_kernel_reads_the_specified_bytes(); leaves_depend_on_data_and_row(); if (failures != 0) { printf("\n*** %d FAILURE(S) ***\n", failures); diff --git a/crypto/math-cuda/tests/host_kat/rpx_host_kat.cpp b/crypto/math-cuda/tests/host_kat/rpx_host_kat.cpp index 5f69ae403..9590ce867 100644 --- a/crypto/math-cuda/tests/host_kat/rpx_host_kat.cpp +++ b/crypto/math-cuda/tests/host_kat/rpx_host_kat.cpp @@ -999,6 +999,35 @@ void row_major_leaf_kernels_read_the_specified_felts() { printf("row-major leaf kernels: read pattern + node encoding match the CPU leaf spec, all column ranges\n"); } +// The row-major ONE-ROW kernel (S2, rows_per_leaf = 1): leaf `i` absorbs the +// single row `reverse_index(i)` over `log_n` bits, every non-empty column range +// (the felt count keys the padding, so each range length is its own sponge). +void row_major_one_row_kernel_reads_the_specified_felts() { + for (uint32_t log_n : {1u, 2u, 4u, 6u}) { + for (uint64_t m : {1ull, 5ull, 13ull}) { + const uint64_t n = 1ull << log_n; + std::vector data(n * m); + uint64_t seed = log_n * 11 + m; + for (size_t i = 0; i < data.size(); ++i) data[i] = sample(seed, i); + for (uint64_t cs = 0; cs < m; ++cs) { + for (uint64_t ce = cs + 1; ce <= m; ++ce) { + std::vector out(n * 32, 0); + CUDA_HOST_FOR_EACH_THREAD(t, n) { + rpx_leaves_base_row_major_row_range(data.data(), m, cs, ce, n, log_n, out.data()); + } + std::vector> want(n); + for (uint64_t leaf = 0; leaf < n; ++leaf) { + const uint64_t br = reverse_index(leaf, log_n); + for (uint64_t c = cs; c < ce; ++c) want[leaf].push_back(data[br * m + c]); + } + check_leaves(out, want, "rpx_leaves_base_row_major_row_range"); + } + } + } + } + printf("row-major one-row kernel: read pattern + node encoding match the CPU one-row leaf spec, all column ranges\n"); +} + // The host parent over two nodes: decode big-endian, compress, encode. void expected_parent(const uint8_t *left, const uint8_t *right, uint8_t out[32]) { uint64_t l[4], r[4], d[4]; @@ -1207,6 +1236,7 @@ int main() { fri_leaf_kernel_reads_the_specified_felts(); coset_leaf_kernels_read_the_specified_felts(); row_major_leaf_kernels_read_the_specified_felts(); + row_major_one_row_kernel_reads_the_specified_felts(); merkle_compressors_match_the_host_parent(); permute_probe_matches_the_oracle_table(); printf("\n-- layer 8: the proof-of-work grind kernel against the host predicate --\n"); diff --git a/crypto/math-cuda/tests/merkle_cap.rs b/crypto/math-cuda/tests/merkle_cap.rs new file mode 100644 index 000000000..283bcb54c --- /dev/null +++ b/crypto/math-cuda/tests/merkle_cap.rs @@ -0,0 +1,143 @@ +//! Parity: `read_cap_dev` must return, for every cap height, exactly the nodes +//! the host `MerkleTree::cap` returns — the `2^c` nodes `c` levels below the +//! root, left to right, byte for byte. This is the gate for reading a +//! device-resident tree's Merkle cap in the STARK R4 cap post-pass +//! instead of copying the whole tree. + +use crypto::merkle_tree::backends::field_element_vector::FieldElementVectorBackend; +use crypto::merkle_tree::merkle::MerkleTree; +use math::field::goldilocks::GoldilocksField; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; +use sha3::Keccak256; + +type CpuTree = MerkleTree>; + +fn random_leaves(leaves_len: usize, seed: u64) -> Vec<[u8; 32]> { + let mut rng = ChaCha8Rng::seed_from_u64(seed); + (0..leaves_len) + .map(|_| { + let mut arr = [0u8; 32]; + rng.fill(&mut arr[..]); + arr + }) + .collect() +} + +fn flat(leaves: &[[u8; 32]]) -> Vec { + leaves.iter().flat_map(|l| l.iter().copied()).collect() +} + +/// Every height `c <= min(depth, 6)` of a keccak tree with `2^log_n` leaves: +/// the device read equals the host cap, and the heap slice of the device's own +/// node buffer. +fn keccak_cap_parity(log_n: u32, seed: u64) { + let leaves_len = 1usize << log_n; + let leaves = random_leaves(leaves_len, seed); + let gpu_nodes = math_cuda::merkle::build_merkle_tree_on_device(&flat(&leaves)).unwrap(); + let cpu_tree = CpuTree::build_from_hashed_leaves(leaves).unwrap(); + + let be = math_cuda::device::backend().unwrap(); + let stream = be.next_stream(); + let nodes_dev = stream.clone_htod(&gpu_nodes).unwrap(); + stream.synchronize().unwrap(); + + let depth = log_n as usize; + for c in 0..=depth.min(6) { + let got = math_cuda::merkle::read_cap_dev(&nodes_dev, leaves_len, c, &stream).unwrap(); + let want: Vec = cpu_tree + .cap(c) + .unwrap() + .iter() + .flat_map(|n| n.iter().copied()) + .collect(); + assert_eq!(got.len(), (1 << c) * 32, "log_n={log_n} c={c}"); + assert_eq!(got, want, "keccak cap mismatch: log_n={log_n} c={c}"); + assert_eq!( + got, + gpu_nodes[((1 << c) - 1) * 32..((2 << c) - 1) * 32], + "log_n={log_n} c={c}: not the heap slice" + ); + } + let root = math_cuda::merkle::read_cap_dev(&nodes_dev, leaves_len, 0, &stream).unwrap(); + assert_eq!(root, cpu_tree.root.to_vec(), "c = 0 is the root"); +} + +#[test] +fn keccak_cap_matches_the_host_cap_small() { + for log_n in 1u32..=8 { + keccak_cap_parity(log_n, 300 + log_n as u64); + } +} + +#[test] +fn keccak_cap_matches_the_host_cap_large() { + for log_n in [12u32, 18, 22] { + keccak_cap_parity(log_n, 9000 + log_n as u64); + } +} + +/// RPX trees: the read is hash-agnostic (a D2H of the heap slice), so it is +/// pinned against the device builder's own full node buffer, whose layout the +/// existing RPX tree parity tests pin against the host. +#[test] +fn rpx_cap_is_the_heap_slice() { + for log_n in [1u32, 2, 5, 10, 16] { + let leaves_len = 1usize << log_n; + // RPX digests are four canonical Goldilocks limbs; reduce the random + // bytes below the modulus so the device hashes valid field elements. + let leaves: Vec<[u8; 32]> = random_leaves(leaves_len, 77 + log_n as u64) + .into_iter() + .map(|mut l| { + for limb in l.chunks_exact_mut(8) { + limb[7] &= 0x7f; + } + l + }) + .collect(); + let gpu_nodes = math_cuda::rpx::build_merkle_tree_on_device(&flat(&leaves)).unwrap(); + let be = math_cuda::device::backend().unwrap(); + let stream = be.next_stream(); + let nodes_dev = stream.clone_htod(&gpu_nodes).unwrap(); + stream.synchronize().unwrap(); + let depth = log_n as usize; + for c in 0..=depth.min(6) { + let got = math_cuda::merkle::read_cap_dev(&nodes_dev, leaves_len, c, &stream).unwrap(); + assert_eq!( + got, + gpu_nodes[((1 << c) - 1) * 32..((2 << c) - 1) * 32], + "rpx: log_n={log_n} c={c}" + ); + } + } +} + +/// The resident tree a real R2 commit keeps (`GpuMerkleTree`): the cap read +/// off it equals its full node buffer's heap slice, and `c = 0` its root. +#[test] +fn a_kept_composition_tree_serves_its_cap() { + let lde_size = 1usize << 12; + let mut rng = ChaCha8Rng::seed_from_u64(4242); + let parts: Vec> = (0..2) + .map(|_| { + (0..3 * lde_size) + .map(|_| rng.gen_range(0..0xFFFF_FFFF_0000_0001u64)) + .collect() + }) + .collect(); + let refs: Vec<&[u64]> = parts.iter().map(|p| p.as_slice()).collect(); + let tree = math_cuda::merkle::build_comp_poly_tree_from_evals_ext3_keep(&refs).unwrap(); + let be = math_cuda::device::backend().unwrap(); + let stream = be.next_stream(); + let all = stream.clone_dtoh(tree.nodes.as_ref()).unwrap(); + stream.synchronize().unwrap(); + let depth = tree.leaves_len.trailing_zeros() as usize; + assert_eq!(tree.leaves_len, lde_size / 2); + for c in 0..=depth.min(6) { + let got = + math_cuda::merkle::read_cap_dev(&tree.nodes, tree.leaves_len, c, &stream).unwrap(); + assert_eq!(got, all[((1 << c) - 1) * 32..((2 << c) - 1) * 32], "c={c}"); + } + let root = math_cuda::merkle::read_cap_dev(&tree.nodes, tree.leaves_len, 0, &stream).unwrap(); + assert_eq!(root, tree.root.to_vec()); +} diff --git a/crypto/math-cuda/tests/whir_cap.rs b/crypto/math-cuda/tests/whir_cap.rs new file mode 100644 index 000000000..330ffda26 --- /dev/null +++ b/crypto/math-cuda/tests/whir_cap.rs @@ -0,0 +1,239 @@ +//! W1 on the device: `DeviceCodeword::paths_and_cap` returns the paths and the +//! Merkle cap of ONE rebuilt tree, and both equal the host tree's. +//! +//! Needs a GPU (`make test-math-cuda`). The reference is `multilinear`'s host +//! commitment over the same codeword (`CodewordCommitment::new` on a base +//! codeword hashes on the host): its full paths (`open_many`) and its +//! owner-encoded capped paths (`open_many_capped`), whose first path ends with +//! the host tree's cap. +//! +//! Three regimes for the leaf layer, because the cap must come from the tree +//! that was built whatever built its leaves: SERVED from the retained layer +//! (same blocking as the commit), REHASHED (another blocking, so the retained +//! layer's key does not match), and EVICTED (the layer reclaimed by the +//! allocator's evictor before the opening). In each, the cap costs no extra +//! tree build: `tree_builds` rises by exactly one per call. + +use math::field::element::FieldElement; +use math::field::goldilocks::GoldilocksField as F; +use math_cuda::DeviceHash; +use multilinear::mle::Mle; +use multilinear::whir::{self, Domain}; +use multilinear::whir_commit::CodewordCommitment; +use multilinear::whir_hash::{DeviceHashKey, KeccakWhir, RpxWhir, WhirHash}; +use std::sync::Mutex; + +type FE = FieldElement; + +/// Every test here commits, and eviction moves the process-wide reservation +/// total, so they take turns (the `whir_tree_cache.rs` pattern). +static DEVICE_GLOBALS: Mutex<()> = Mutex::new(()); + +fn exclusive() -> std::sync::MutexGuard<'static, ()> { + DEVICE_GLOBALS.lock().unwrap_or_else(|e| e.into_inner()) +} + +/// Mirror of `DeviceHashKey::into_math_cuda` (cuda-gated on `multilinear`). +fn key() -> DeviceHash { + match H::DEVICE { + DeviceHashKey::Keccak256 => DeviceHash::Keccak256, + DeviceHashKey::Rpx256 => DeviceHash::Rpx256, + } +} + +fn poly(num_vars: usize, seed: u64) -> Mle { + let evals: Vec = (0..(1u64 << num_vars)) + .map(|i| FE::from(i.wrapping_mul(6364136223846793005).wrapping_add(seed) >> 11)) + .collect(); + Mle::new(evals).expect("power of two") +} + +fn nodes(bytes: &[u8]) -> Vec<[u8; 32]> { + bytes + .chunks_exact(32) + .map(|n| n.try_into().expect("32 bytes")) + .collect() +} + +/// The device result against the host tree at blocking `k`, every cap height +/// up to `min(depth, 6)`. `before_each(c)` runs before the call at height `c` +/// (the EVICTED regime evicts there, so every height meets a rebuilt tree). +fn assert_matches_host( + name: &str, + device: &math_cuda::whir::DeviceCodeword, + host: &CodewordCommitment, + k: usize, + positions: &[usize], + mut before_each: impl FnMut(usize), + expect_leaf_pass: impl Fn(u64) -> bool, +) { + let depth = host.depth(); + let full = host.open_many(positions).expect("host paths"); + let pos32: Vec = positions.iter().map(|p| *p as u32).collect(); + for c in 0..=depth.min(6) { + before_each(c); + let builds = device.tree_builds(); + let passes = device.leaf_passes(); + let (paths, cap) = device + .paths_and_cap(k, &pos32, c, key::()) + .unwrap_or_else(|e| panic!("{name} k={k} c={c}: paths_and_cap: {e:?}")); + assert_eq!( + device.tree_builds(), + builds + 1, + "{name} k={k} c={c}: paths and cap must come from ONE tree build" + ); + assert!( + expect_leaf_pass(device.leaf_passes() - passes), + "{name} k={k} c={c}: unexpected leaf-pass count {} -> {}", + passes, + device.leaf_passes() + ); + let paths = nodes(&paths); + let cap = nodes(&cap); + assert_eq!(cap.len(), 1 << c, "{name} k={k} c={c}: cap length"); + if c == 0 { + assert_eq!( + cap[0], + host.root(), + "{name} k={k}: the height-0 cap is the root" + ); + } + // Full paths: byte-identical to the host tree's, query by query. + for (q, opening) in full.iter().enumerate() { + assert_eq!( + &paths[q * depth..(q + 1) * depth], + opening.proof.merkle_path.as_slice(), + "{name} k={k} c={c}: path {q}" + ); + } + // The owner encoding the host produces ends with the host tree's cap. + let capped = host + .open_many_capped(positions, c, true) + .expect("host capped paths"); + if c > 0 { + assert_eq!( + &capped[0].proof.merkle_path[depth - c..], + cap.as_slice(), + "{name} k={k} c={c}: the device cap is the host tree's cap" + ); + } + } +} + +fn setup( + num_vars: usize, + k_commit: usize, +) -> (math_cuda::whir::DeviceCodeword, Vec, Domain) { + let f = poly(num_vars, 3); + let raw: Vec = f.evals().iter().map(|v| *v.value()).collect(); + let (device, _root) = math_cuda::whir::commit_codeword(&raw, 2, k_commit, false, key::()) + .expect("device commit (needs a GPU)"); + let domain = Domain::::new(num_vars + 2).expect("domain"); + let host_codeword = + whir::encode::(&whir::lift_coefficients(&f), &domain).expect("encode"); + (device, host_codeword, domain) +} + +/// SERVED and REHASHED, k = 1..5, both hashes. +#[test] +fn paths_and_cap_are_the_host_trees_served_or_rehashed() { + let _exclusive = exclusive(); + fn run(name: &str) { + let num_vars = 12; + for k_commit in 1..=5usize { + let (device, host_codeword, _) = setup::(num_vars, k_commit); + let leaves = (host_codeword.len()) >> k_commit; + let positions = [0usize, 1, leaves / 3, leaves - 1]; + let host = + CodewordCommitment::<_, H>::new(&host_codeword, k_commit).expect("host commit"); + // Same blocking as the commit: the retained layer is served. + assert_matches_host( + name, + &device, + &host, + k_commit, + &positions, + |_| {}, + |d| d == 0, + ); + // Another blocking: the layer does not match, the leaves are hashed. + let k_other = if k_commit == 5 { 3 } else { k_commit + 1 }; + let other = + CodewordCommitment::<_, H>::new(&host_codeword, k_other).expect("host commit"); + let leaves = host_codeword.len() >> k_other; + let positions = [0usize, leaves / 2, leaves - 1]; + assert_matches_host( + name, + &device, + &other, + k_other, + &positions, + |_| {}, + |d| d == 1, + ); + } + } + run::("keccak"); + run::("rpx"); +} + +/// EVICTED: the retained layer is reclaimed by the allocator's evictor, and +/// the next opening rebuilds the whole tree — its cap still the host's. +/// +/// A rebuild RE-CAPTURES the layer (by design: the evictor keeps the codeword's +/// registry entry because "the mutex lives with the codeword and may refill", +/// `math-cuda/src/whir.rs`), so a second opening after one eviction is SERVED, +/// not rebuilt. The eviction therefore runs before EVERY cap height: each +/// height meets a rebuilt tree and pays exactly one leaf pass, and the layer is +/// back after each rebuild (the next eviction's precondition says so). +#[test] +fn paths_and_cap_after_the_retained_layer_is_evicted() { + let _exclusive = exclusive(); + let be = math_cuda::device::backend().expect("eviction test needs a GPU"); + let k = 4; + let (device, host_codeword, _) = setup::(14, k); + let layer_bytes = device.retained_leaf_bytes(); + assert!(layer_bytes > 0, "precondition: the commit retained a layer"); + + let mut evictions = 0usize; + let evict = |c: usize| { + assert_eq!( + device.retained_leaf_bytes(), + layer_bytes, + "c={c}: the layer is retained (by the commit, or re-captured by the last rebuild)" + ); + let gap = layer_bytes / 2; + let hog_bytes = be + .vram_budget_bytes() + .saturating_sub(be.reserved_bytes()) + .saturating_sub(gap); + let hog = math_cuda::device::reserve(hog_bytes).expect("the hog reservation cannot fail"); + let got = math_cuda::device::reserve(layer_bytes) + .expect("the reserve must succeed by evicting the retained layer"); + assert_eq!( + device.retained_leaf_bytes(), + 0, + "c={c}: the layer was evicted" + ); + drop(got); + drop(hog); + evictions += 1; + }; + + let host = CodewordCommitment::<_, RpxWhir>::new(&host_codeword, k).expect("host commit"); + let leaves = host_codeword.len() >> k; + let positions = [0usize, 5, leaves / 2, leaves - 1]; + assert_matches_host("rpx evicted", &device, &host, k, &positions, evict, |d| { + d == 1 + }); + assert_eq!( + evictions, + host.depth().min(6) + 1, + "one eviction per cap height" + ); + assert_eq!( + device.retained_leaf_bytes(), + layer_bytes, + "the last rebuild re-captured the layer" + ); +} diff --git a/crypto/math-cuda/tests/whir_commit.rs b/crypto/math-cuda/tests/whir_commit.rs index ac20f5b91..1b2ed6048 100644 --- a/crypto/math-cuda/tests/whir_commit.rs +++ b/crypto/math-cuda/tests/whir_commit.rs @@ -91,7 +91,7 @@ fn parity(num_vars: usize, log_blowup: usize, log_folding: usize) { for index in [0, 1, device.num_leaves() / 3, device.num_leaves() - 1] { let opening = device.open(index).expect("open"); assert!( - verify_opening::<_, H>(&device.root(), index, &opening), + verify_opening::<_, H>(&device.root(), device.depth(), index, &opening), "device opening at {index} does not verify under {}", H::NAME ); @@ -103,8 +103,8 @@ fn parity(num_vars: usize, log_blowup: usize, log_folding: usize) { } } -/// The shapes: both sides of the fused-8-level NTT threshold, a fold width that -/// is not the whole blowup, and the Möbius windows — below the contiguous +/// The shapes: both sides of the fused-8-level NTT threshold, fold widths 1 to +/// 6 (6 = the `first6` schedule's first round), and the Möbius windows — below the contiguous /// kernel, exactly one window, one window plus a tiled level, and several full /// tiles with a partial one on top. fn every_shape() { @@ -118,6 +118,13 @@ fn every_shape() { parity::(9, 1, 2); parity::(13, 2, 5); parity::(17, 1, 4); + + // ★ k = 6, the widest fold the stack runs (W2 `first6`: tree 0's leaves + // are 64 base felts, 512 bytes, one leaf-kernel thread each). A production + // height's shape, a smaller one, and four leaves. + parity::(14, 2, 6); + parity::(12, 2, 6); + parity::(7, 1, 6); } #[test] diff --git a/crypto/math-cuda/tests/whir_fold.rs b/crypto/math-cuda/tests/whir_fold.rs index 2a3decdcc..55570fc3c 100644 --- a/crypto/math-cuda/tests/whir_fold.rs +++ b/crypto/math-cuda/tests/whir_fold.rs @@ -85,6 +85,7 @@ fn the_device_ext3_commit_matches_the_host() { assert!( multilinear::whir_commit::verify_opening::<_, KeccakWhir>( &device.root(), + device.depth(), index, &opening ), @@ -92,3 +93,50 @@ fn the_device_ext3_commit_matches_the_host() { ); } } + +/// ★ Six levels in one residency (W2 `first6`: the first round folds 6 +/// variables of the base codeword), against the host arm. +/// +/// ⚠ Called on the device ENTRY POINT, not through `whir::fold_codeword_k`: +/// that wrapper returns `None` below its size threshold or under +/// `LAMBDA_VM_NO_GPU_WHIR_FOLD` and falls back to the host silently, so a +/// comparison through it can be the host against itself. This one either runs +/// the kernels or fails. +#[test] +fn the_device_folds_six_levels_as_the_host_does() { + for (num_vars, log_blowup) in [(14, 2), (12, 2), (7, 1)] { + let (cw, domain) = codeword(num_vars, log_blowup); + let alphas: Vec = (1..=6).map(challenge).collect(); + let (host, host_domain) = + whir::fold_codeword_k_on_host::(&cw, &domain, &alphas) + .expect("host fold"); + + // The arguments `multilinear::gpu::fold_codeword_k` builds. + let two_inv = *FE::from(2u64).inv().expect("2 is invertible").value(); + let mut g_inv = domain.generator().inv().expect("a generator is invertible"); + let mut g_invs = Vec::with_capacity(alphas.len()); + for _ in 0..alphas.len() { + g_invs.push(*g_inv.value()); + g_inv = g_inv.square(); + } + let raw_alphas: Vec = alphas + .iter() + .flat_map(|a| a.value().iter().map(|c| *c.value())) + .collect(); + let raw: Vec = cw.iter().map(|v| *v.value()).collect(); + let device = math_cuda::whir::fold_codeword_base(&raw, two_inv, &g_invs, &raw_alphas) + .unwrap_or_else(|e| panic!("device fold at 2^{num_vars} (needs a GPU): {e:?}")); + let device: Vec = device + .chunks_exact(3) + .map(|c| FE3::new([FE::from_raw(c[0]), FE::from_raw(c[1]), FE::from_raw(c[2])])) + .collect(); + + assert_eq!(host.len(), cw.len() >> 6); + assert_eq!(device.len(), host.len()); + assert_eq!( + device, host, + "the six-level base fold differs at 2^{num_vars}" + ); + assert_eq!(host_domain.log_size(), num_vars + log_blowup - 6); + } +} diff --git a/crypto/math-cuda/tests/whir_tree_cache.rs b/crypto/math-cuda/tests/whir_tree_cache.rs index a6494ae49..0f12cef18 100644 --- a/crypto/math-cuda/tests/whir_tree_cache.rs +++ b/crypto/math-cuda/tests/whir_tree_cache.rs @@ -265,7 +265,7 @@ fn the_openings_verify_against_the_device_commitment() { for index in [0, 1, host.num_leaves() / 3, host.num_leaves() - 1] { let opening = host.open(index).expect("open"); assert!( - verify_opening::<_, H>(&root, index, &opening), + verify_opening::<_, H>(&root, host.depth(), index, &opening), "{name}: opening {index} does not verify against the device root" ); } diff --git a/crypto/multilinear/src/constraint_argument.rs b/crypto/multilinear/src/constraint_argument.rs index 155e55310..ad2c17296 100644 --- a/crypto/multilinear/src/constraint_argument.rs +++ b/crypto/multilinear/src/constraint_argument.rs @@ -926,6 +926,7 @@ mod tests { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: crate::whir_chain::ChainFormat::DEFAULT, } } @@ -1129,6 +1130,7 @@ mod tests { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: crate::whir_chain::ChainFormat::DEFAULT, }; // Domain in Goldilocks, values in its degree-3 extension. let trace = CommittedTrace::::commit(columns, &cfg).unwrap(); @@ -1179,6 +1181,7 @@ mod tests { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: crate::whir_chain::ChainFormat::DEFAULT, }; let trace = CommittedTrace::::commit(columns, &cfg).unwrap(); let roots = trace.roots(); diff --git a/crypto/multilinear/src/gpu.rs b/crypto/multilinear/src/gpu.rs index f039fa38e..3a1bd9c53 100644 --- a/crypto/multilinear/src/gpu.rs +++ b/crypto/multilinear/src/gpu.rs @@ -107,6 +107,9 @@ type SumcheckRounds = ( #[cfg(feature = "cuda")] const COMMIT_THRESHOLD: usize = 1 << 16; +/// Per query an authentication path, and the tree's Merkle cap. +pub(crate) type PathsAndCap = (Vec>, Vec<[u8; 32]>); + /// A byte buffer of Merkle nodes, relabelled as nodes without copying. /// /// A tree over a stacked polynomial is hundreds of megabytes; chunking it into @@ -2199,6 +2202,44 @@ impl DeviceCodeword { Some(nodes.chunks_exact(depth).map(<[_]>::to_vec).collect()) } + /// [`paths`](Self::paths) and the tree's Merkle cap at `cap_height` (its + /// `2^cap_height` nodes that height below the root, left to right), both + /// from ONE rebuild of the tree — so the cap and the paths are of the same + /// tree, whether its leaf layer was hashed or served from retention. + pub(crate) fn paths_and_cap( + &self, + log_folding: usize, + indices: &[usize], + cap_height: usize, + hash: crate::whir_hash::DeviceHashKey, + ) -> Option { + let leaves = self.0.elements() >> log_folding; + let depth = leaves.trailing_zeros() as usize; + if indices.iter().any(|index| *index >= leaves) || cap_height > depth { + return None; + } + let positions: Vec = indices.iter().map(|index| *index as u32).collect(); + let (bytes, cap_bytes) = self + .0 + .paths_and_cap(log_folding, &positions, cap_height, hash.into_math_cuda()) + .ok()?; + let nodes = nodes_in_place(bytes)?; + // `2^c` nodes: copied rather than reinterpreted in place, because a + // cap is a few hundred bytes and its allocation's capacity is not ours + // to vouch for. + if cap_bytes.len() != 32usize << cap_height { + return None; + } + let cap: Vec<[u8; 32]> = cap_bytes + .chunks_exact(32) + .map(|node| <[u8; 32]>::try_from(node).ok()) + .collect::>()?; + if cap.len() != 1usize << cap_height { + return None; + } + Some((nodes.chunks_exact(depth).map(<[_]>::to_vec).collect(), cap)) + } + /// The blocks `indices` open, gathered where they lie — one launch and one /// copy back for the whole round. pub(crate) fn cosets( @@ -2333,6 +2374,16 @@ impl DeviceCodeword { match self.0 {} } + pub(crate) fn paths_and_cap( + &self, + _log_folding: usize, + _indices: &[usize], + _cap_height: usize, + _hash: crate::whir_hash::DeviceHashKey, + ) -> Option { + match self.0 {} + } + pub(crate) fn cosets( &self, _indices: &[usize], diff --git a/crypto/multilinear/src/lib.rs b/crypto/multilinear/src/lib.rs index 914761f02..cfcc39584 100644 --- a/crypto/multilinear/src/lib.rs +++ b/crypto/multilinear/src/lib.rs @@ -24,6 +24,8 @@ pub mod uneven; pub mod uni_skip; pub mod virtual_poly; pub mod whir; +#[cfg(test)] +mod whir_cap_tests; pub mod whir_chain; pub mod whir_commit; pub mod whir_eval; @@ -107,6 +109,14 @@ pub enum Error { QueryCountMismatch { expected: usize, got: usize }, #[error("query {query}: the Merkle opening does not match the commitment")] OpeningRejected { query: usize }, + /// A tree's Merkle cap, carried by its first opening, is the wrong length + /// or does not hash to the tree's root. + #[error("a Merkle cap does not authenticate against its root")] + CapRejected, + /// The prover could not cut its paths to the cap: a policy asked for a cap + /// taller than the tree, or a path had the wrong length. + #[error("could not embed the Merkle cap: {reason}")] + CapEmbedFailed { reason: &'static str }, #[error("query {query}: the folded block does not match the committed successor")] FoldInconsistent { query: usize }, #[error("the folded codeword and the sumcheck disagree on the evaluation")] diff --git a/crypto/multilinear/src/stacked_eval.rs b/crypto/multilinear/src/stacked_eval.rs index 9f27e8b0a..50613c8bc 100644 --- a/crypto/multilinear/src/stacked_eval.rs +++ b/crypto/multilinear/src/stacked_eval.rs @@ -494,6 +494,7 @@ mod tests { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: crate::whir_chain::ChainFormat::DEFAULT, } } diff --git a/crypto/multilinear/src/whir_cap_tests.rs b/crypto/multilinear/src/whir_cap_tests.rs new file mode 100644 index 000000000..221f4cc34 --- /dev/null +++ b/crypto/multilinear/src/whir_cap_tests.rs @@ -0,0 +1,402 @@ +//! W1 — the Merkle cap on WHIR chains, end to end on the +//! host: every tree's paths stop `c` levels below its root, and the tree's cap +//! rides on its first opening in proof order (the owner path). +//! +//! Round-level fixtures that need the query positions (the unreached cap node +//! and the leaf forged from an internal node) live in `whir_round::tests`, +//! where the query draw is reachable. + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use math::field::{ + element::FieldElement, extensions_goldilocks::Degree3GoldilocksExtensionField as Ext, + goldilocks::GoldilocksField as F, +}; + +use crate::{ + Error, + mle::Mle, + whir::Domain, + whir_chain::{ + CapPolicy, ChainConfig, ChainFormat, ChainProof, ChainRound, GrindBits, RoundOpenings, + commit, prove, verify, + }, + whir_commit::Commitment, + whir_hash::{KeccakWhir, RpxWhir, WhirHash}, +}; + +type FE = FieldElement; +type EE = FieldElement; + +fn config(log_folding: usize, num_queries: usize, cap: CapPolicy) -> ChainConfig { + ChainConfig { + log_blowup: 2, + log_folding, + num_queries, + grind: GrindBits::default(), + format: ChainFormat { + cap, + ..ChainFormat::DEFAULT + }, + } +} + +struct Chain { + proof: ChainProof, + root: Commitment, + z: Vec, + y: EE, + domain: Domain, +} + +/// A base-field polynomial proved over the cubic tower, as production does: +/// round 0's blocks are base, every later one extension. +fn prove_chain(num_vars: usize, cfg: &ChainConfig, seed: u64) -> Chain { + let f = Mle::new( + (0..(1u64 << num_vars)) + .map(|i| FE::from((i.wrapping_add(seed)).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 11)) + .collect(), + ) + .unwrap(); + let z: Vec = (0..num_vars) + .map(|i| EE::from(101 + 3 * i as u64 + seed)) + .collect(); + let y = f.evaluate_in(&z).unwrap(); + let (commitment, domain) = commit::(&f, cfg, true).unwrap(); + let proof = prove::( + &f, + &z, + &commitment, + &domain, + cfg, + &mut DefaultTranscript::::new(b"whir-cap"), + ) + .unwrap(); + Chain { + proof, + root: commitment.root(), + z, + y, + domain, + } +} + +fn check( + c: &Chain, + proof: &ChainProof, + cfg: &ChainConfig, +) -> Result<(), Error> { + verify::( + proof, + &c.root, + &c.z, + c.y, + &c.domain, + cfg, + &mut DefaultTranscript::::new(b"whir-cap"), + ) +} + +/// Path lengths of round `r`'s current and successor openings. +fn path_lens(round: &ChainRound) -> (Vec, Vec) { + match &round.openings { + RoundOpenings::Base(p) => ( + p.current + .iter() + .map(|o| o.proof.merkle_path.len()) + .collect(), + p.next.iter().map(|o| o.proof.merkle_path.len()).collect(), + ), + RoundOpenings::Extension(p) => ( + p.current + .iter() + .map(|o| o.proof.merkle_path.len()) + .collect(), + p.next.iter().map(|o| o.proof.merkle_path.len()).collect(), + ), + } +} + +fn current_path(round: &mut ChainRound, i: usize) -> &mut Vec { + match &mut round.openings { + RoundOpenings::Base(p) => &mut p.current[i].proof.merkle_path, + RoundOpenings::Extension(p) => &mut p.current[i].proof.merkle_path, + } +} + +fn next_path(round: &mut ChainRound, i: usize) -> &mut Vec { + match &mut round.openings { + RoundOpenings::Base(p) => &mut p.next[i].proof.merkle_path, + RoundOpenings::Extension(p) => &mut p.next[i].proof.merkle_path, + } +} + +/// Tree depths of a chain: tree `t` has `D_t − k_t` levels. +fn tree_depths(cfg: &ChainConfig, num_vars: usize) -> Vec { + let mut d = num_vars + cfg.log_blowup; + cfg.schedule(num_vars) + .iter() + .map(|k| { + d -= k; + d + }) + .collect() +} + +const SHAPES: [(usize, usize); 4] = [(6, 2), (5, 2), (3, 4), (9, 3)]; +const POLICIES: [CapPolicy; 4] = [ + CapPolicy::Fixed(1), + CapPolicy::Fixed(2), + CapPolicy::Fixed(3), + CapPolicy::Auto, +]; + +#[test] +fn tree_caps_follow_the_policy_and_are_zero_by_default() { + // The production chain: blowup 2, fold 4, 128 bits, uniform 20-bit grinds. + let mut cfg = ChainConfig::with_security(2, 4, 25, 128, GrindBits::uniform(20)); + assert_eq!(cfg.num_queries, 112); + assert_eq!(tree_depths(&cfg, 25), vec![23, 19, 15, 11, 7, 3, 2]); + assert_eq!(cfg.tree_caps(25), vec![0; 7], "the default caps nothing"); + cfg.format.cap = CapPolicy::Fixed(0); + assert_eq!(cfg.tree_caps(25), vec![0; 7]); + cfg.format.cap = CapPolicy::Auto; + // 112 and 224 openings: 3, clamped to the depth of the last tree. + assert_eq!(cfg.tree_caps(25), vec![3, 3, 3, 3, 3, 3, 2]); + cfg.format.cap = CapPolicy::Fixed(5); + assert_eq!(cfg.tree_caps(25), vec![5, 5, 5, 5, 5, 3, 2]); + + // Q = 3: tree 0 is opened 3 times (auto 0), every later tree 6 (auto 2). + let small = config(2, 3, CapPolicy::Auto); + assert_eq!(tree_depths(&small, 6), vec![6, 4, 2]); + assert_eq!(small.tree_caps(6), vec![0, 2, 2]); + // One round: the only tree is opened Q times. + let one = config(4, 25, CapPolicy::Auto); + assert_eq!(one.schedule(3), vec![3]); + assert_eq!( + one.tree_caps(3), + vec![2], + "25 openings -> 3, clamped to depth 2" + ); +} + +/// Round trips at every policy, one- and multi-round schedules, a remainder +/// last round, base and extension rounds, both hashes; every path carries +/// exactly `depth − c` siblings and the owner exactly `2^c` more. +#[test] +fn capped_chains_round_trip_with_the_owner_path_lengths() { + fn run() { + for (num_vars, k) in SHAPES { + for q in [3usize, 25] { + for policy in POLICIES { + let cfg = config(k, q, policy); + let caps = cfg.tree_caps(num_vars); + let depths = tree_depths(&cfg, num_vars); + let c = prove_chain::(num_vars, &cfg, 5); + let tag = format!("{} S={num_vars} k={k} Q={q} {policy}", H::NAME); + for (r, round) in c.proof.rounds.iter().enumerate() { + let (cur, nxt) = path_lens(round); + let owner = |t: usize, i: usize, owned: bool| { + depths[t] - caps[t] + + if owned && i == 0 && caps[t] > 0 { + 1 << caps[t] + } else { + 0 + } + }; + for (i, len) in cur.iter().enumerate() { + assert_eq!(*len, owner(r, i, r == 0), "{tag}: round {r} current {i}"); + } + for (i, len) in nxt.iter().enumerate() { + assert_eq!(*len, owner(r + 1, i, true), "{tag}: round {r} next {i}"); + } + } + check::(&c, &c.proof, &cfg).unwrap_or_else(|e| panic!("{tag}: {e:?}")); + } + } + } + } + run::(); + run::(); +} + +/// The default moves no byte: `Off` and `Fixed(0)` give the same archived +/// proof, and every path is the full depth. +#[test] +fn the_default_format_is_byte_identical_to_a_zero_cap() { + for (num_vars, k) in SHAPES { + let off = prove_chain::(num_vars, &config(k, 3, CapPolicy::Off), 1); + let zero = prove_chain::(num_vars, &config(k, 3, CapPolicy::Fixed(0)), 1); + let a = rkyv::to_bytes::(&off.proof).unwrap(); + let b = rkyv::to_bytes::(&zero.proof).unwrap(); + assert_eq!(a.as_slice(), b.as_slice(), "S={num_vars} k={k}"); + let cfg = config(k, 3, CapPolicy::Off); + let depths = tree_depths(&cfg, num_vars); + for (r, round) in off.proof.rounds.iter().enumerate() { + let (cur, nxt) = path_lens(round); + assert!(cur.iter().all(|l| *l == depths[r])); + assert!(nxt.iter().all(|l| *l == depths[r + 1])); + } + } +} + +/// The cap changes no transcript value. The same witness under +/// `Off`, `Fixed(3)` and `Auto` (no grinding, so the nonces are fixed) gives +/// the same sumchecks, roots, out-of-domain values, nonces and final value; +/// only the paths differ. +#[test] +fn the_cap_moves_no_transcript_value() { + for (num_vars, k) in SHAPES { + let base = prove_chain::(num_vars, &config(k, 25, CapPolicy::Off), 3); + for policy in [CapPolicy::Fixed(3), CapPolicy::Auto] { + let capped = prove_chain::(num_vars, &config(k, 25, policy), 3); + assert_eq!(capped.root, base.root); + assert_eq!(capped.proof.final_value, base.proof.final_value); + for (a, b) in capped.proof.rounds.iter().zip(&base.proof.rounds) { + assert_eq!(a.next_root, b.next_root); + assert_eq!(a.ood_value, b.ood_value); + assert_eq!(a.nonces, b.nonces); + let ev = |r: &ChainRound| -> Vec { + r.sumcheck + .iter() + .flat_map(|s| s.evaluations.clone()) + .collect() + }; + assert_eq!(ev(a), ev(b)); + } + } + } +} + +fn expect_err(c: &Chain, forged: &ChainProof, cfg: &ChainConfig, what: &str) -> Error { + match check::(c, forged, cfg) { + Ok(()) => panic!("{what}: the forgery verified"), + Err(e) => e, + } +} + +/// The tamper arm. The chain is `[2, 2, 2]` over 6 variables at `Fixed(2)`: +/// trees of depth 6, 4, 2, every one capped at 2. +#[test] +fn a_tampered_cap_or_owner_path_is_rejected() { + let cfg = config(2, 3, CapPolicy::Fixed(2)); + assert_eq!(cfg.tree_caps(6), vec![2, 2, 2]); + let c = prove_chain::(6, &cfg, 9); + check::(&c, &c.proof, &cfg).unwrap(); + let depths = [6usize, 4, 2]; + + // Tree 0's cap node: rides at the end of round 0's first current path. + for j in 0..4 { + let mut forged = c.proof.clone(); + current_path(&mut forged.rounds[0], 0)[depths[0] - 2 + j][5] ^= 1; + assert!(matches!( + expect_err(&c, &forged, &cfg, "tree-0 cap"), + Error::CapRejected + )); + } + // Tree t's cap node, t = 1, 2: rides on round t − 1's first successor path. + for (t, depth) in depths.iter().enumerate().skip(1) { + let mut forged = c.proof.clone(); + next_path(&mut forged.rounds[t - 1], 0)[depth - 2 + 1][0] ^= 1; + assert!(matches!( + expect_err(&c, &forged, &cfg, "tree-t cap"), + Error::CapRejected + )); + } + // Round t's first CURRENT opening carrying a cap as well: its path must + // be exactly depth − c, so a second cap for the same tree is refused. + for (t, depth) in depths.iter().enumerate().skip(1) { + let mut forged = c.proof.clone(); + let cap = next_path(&mut forged.rounds[t - 1], 0)[depth - 2..].to_vec(); + current_path(&mut forged.rounds[t], 0).extend(cap); + assert!(matches!( + expect_err(&c, &forged, &cfg, "a second cap on round t's current[0]"), + Error::OpeningRejected { query: 0 } + )); + } + // The owner path one short (a cap node dropped) and one long. + let mut forged = c.proof.clone(); + current_path(&mut forged.rounds[0], 0).pop(); + assert!(matches!( + expect_err(&c, &forged, &cfg, "owner short"), + Error::CapRejected + )); + let mut forged = c.proof.clone(); + let extra = current_path(&mut forged.rounds[0], 0)[0]; + current_path(&mut forged.rounds[0], 0).push(extra); + assert!(matches!( + expect_err(&c, &forged, &cfg, "owner long"), + Error::CapRejected + )); + // A non-owner path one node long, and one short. + let mut forged = c.proof.clone(); + let extra = current_path(&mut forged.rounds[0], 1)[0]; + current_path(&mut forged.rounds[0], 1).push(extra); + assert!(matches!( + expect_err(&c, &forged, &cfg, "non-owner long"), + Error::OpeningRejected { query: 1 } + )); + let mut forged = c.proof.clone(); + // Tree 1 (depth 4, two siblings below its cap), as round 0's successor. + assert!(next_path(&mut forged.rounds[0], 2).pop().is_some()); + assert!(matches!( + expect_err(&c, &forged, &cfg, "non-owner short"), + Error::OpeningRejected { query: 2 } + )); + // The cap moved from the first opening to the second. + let mut forged = c.proof.clone(); + let cap: Vec = current_path(&mut forged.rounds[0], 0) + .drain(depths[0] - 2..) + .collect(); + current_path(&mut forged.rounds[0], 1).extend(cap); + expect_err(&c, &forged, &cfg, "cap moved to query 1"); + // A sibling below the cap. + let mut forged = c.proof.clone(); + current_path(&mut forged.rounds[1], 2)[0][0] ^= 1; + assert!(matches!( + expect_err(&c, &forged, &cfg, "sibling"), + Error::OpeningRejected { query: 2 } + )); + + // A proof made under one policy is refused under another: the cap height + // is the verifier's constant, never read from the proof. + expect_err( + &c, + &c.proof, + &config(2, 3, CapPolicy::Fixed(1)), + "c=2 read as c=1", + ); + expect_err( + &c, + &c.proof, + &config(2, 3, CapPolicy::Fixed(3)), + "c=2 read as c=3", + ); + expect_err( + &c, + &c.proof, + &config(2, 3, CapPolicy::Off), + "c=2 read as off", + ); + let off = prove_chain::(6, &config(2, 3, CapPolicy::Off), 9); + expect_err(&off, &off.proof, &cfg, "off read as c=2"); +} + +/// A one-round chain has only the final round: tree 0's owner is its first +/// current opening there, and a flipped cap node is still refused. +#[test] +fn a_one_round_chain_carries_its_cap_on_the_final_openings() { + let cfg = config(4, 25, CapPolicy::Auto); + assert_eq!(cfg.tree_caps(3), vec![2]); + let c = prove_chain::(3, &cfg, 2); + assert_eq!(c.proof.rounds.len(), 1); + check::(&c, &c.proof, &cfg).unwrap(); + let mut forged = c.proof.clone(); + let path = current_path(&mut forged.rounds[0], 0); + // depth 2, cap 2: no siblings, four cap nodes. + assert_eq!(path.len(), 4); + path[3][7] ^= 1; + assert!(matches!( + check::(&c, &forged, &cfg), + Err(Error::CapRejected) + )); +} diff --git a/crypto/multilinear/src/whir_chain.rs b/crypto/multilinear/src/whir_chain.rs index d614b92e1..35076b775 100644 --- a/crypto/multilinear/src/whir_chain.rs +++ b/crypto/multilinear/src/whir_chain.rs @@ -40,6 +40,7 @@ //! claim chains just as an evaluation does. use crypto::fiat_shamir::is_transcript::IsTranscript; +pub use crypto::merkle_tree::cap::CapPolicy; use math::{ field::{ element::FieldElement, @@ -55,9 +56,9 @@ use crate::{ poly::Composed, sumcheck::{self, RoundProof as SumcheckRoundProof}, whir::{Domain, encode, fold_codeword_k, lift_coefficients}, - whir_commit::{Codeword, CodewordCommitment, Commitment, fold_coset, verify_opening}, + whir_commit::{Codeword, CodewordCommitment, Commitment, fold_coset, verify_opening_capped}, whir_hash::{GrindingDigest, WhirHash}, - whir_round::{self, RoundCommitments, RoundConfig, RoundProof}, + whir_round::{self, RoundCaps, RoundCommitments, RoundConfig, RoundProof, TreeCheck}, }; /// `w(x)·f(x)`, the shape every group's sumcheck runs over. @@ -176,6 +177,14 @@ impl GrindBits { } /// Blowup, fold factor, query count and proof of work. +/// +/// `format` is the proof FORMAT ([`ChainFormat`]: the W1 cap and W2 fold +/// levers); its default is today's format. Like the rest of the config it is +/// a verifier-side constant, never read from a proof. The fold schedule is +/// absorbed into the statement through [`ChainConfig::fold_word`], whose value +/// at the default is `log_folding` itself (today's bytes); the rest of the +/// format is not absorbed (`push_config` binds it as `_`): absorbing it would +/// move every transcript at the default. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ChainConfig { /// `log2` of the code's inverse rate. @@ -186,8 +195,104 @@ pub struct ChainConfig { pub num_queries: usize, /// Proof of work before each redrawable challenge. pub grind: GrindBits, + /// The chain's proof format. [`ChainFormat::DEFAULT`] = today. + pub format: ChainFormat, +} + +/// The proof-format levers of a WHIR chain. Grouped so a literal +/// `ChainConfig` names the format in one line (`format: ChainFormat::DEFAULT`) +/// and a lever added later touches this struct only. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct ChainFormat { + /// Merkle cap policy for the chain's commitment trees (W1). `Off` = today. + pub cap: CapPolicy, + /// Per-round fold schedule (W2). `Uniform` = today (`log_folding` every + /// round, the remainder last). + pub folds: WhirFolds, +} + +impl ChainFormat { + /// Today's format: every lever off. + pub const DEFAULT: Self = Self { + cap: CapPolicy::Off, + folds: WhirFolds::Uniform, + }; + + /// True when this is today's format (`Fixed(0)` counts as `Off`). + pub fn is_default(&self) -> bool { + self.cap.is_off() && self.folds == WhirFolds::Uniform + } +} + +/// Which WHIR format levers THIS build implements. A lever that is only +/// parsed must not be selectable (see `stark::proof::options:: +/// MERKLE_CAP_IMPLEMENTED`). Each flag is flipped in the commit that +/// makes the lever real. +/// +/// W1 (the Merkle cap) is real: host prover and verifier ([`ChainConfig:: +/// tree_caps`], the owner-path encoding), the device (`paths_and_cap`), and +/// the in-guest verifier and its cost model (`prover::lfm::whir_chain`). +pub const WHIR_CAP_IMPLEMENTED: bool = true; + +/// The widest fold any round of a chain may take. +/// +/// The stack is tested up to it and no further: the GPU commit/fold parity +/// (`math-cuda` `whir_commit`/`whir_fold`, k = 6) and the in-guest fold +/// emitter (`lfm::whir_fold_tests`, k = 5 and 6). `k0 = 7` loses on in-guest +/// instructions, so nothing above 6 is opened. +pub const MAX_FOLD: usize = 6; + +/// The per-round fold schedule of a chain (W2). +/// +/// ★ Why a FIRST fold and not a list. A config serves chains of every height +/// (`chain_config` takes the tallest stack, and each chain folds its own +/// `num_vars`), so a per-round list would have to say what a shorter chain +/// does with it. The lever is the first fold alone — +/// tree 0 is the only base-field tree, opened `Q` times rather than `2Q`, and +/// every variable it takes shortens every later tree — so the schedule is +/// "`k0`, then today's uniform walk", a function of `(k0, log_folding, +/// num_vars)` at every height. There is no DP. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum WhirFolds { + /// `log_folding` variables every round, the remainder last. Today's format. + #[default] + Uniform, + /// The first round folds `k0` variables (all of them when the chain has + /// fewer); every later round is today's walk: `log_folding`, the remainder + /// last. `first5` / `first6`. + First(FirstFold), +} + +/// See [`WHIR_CAP_IMPLEMENTED`]. W2 is in: the host chain, the statement word, +/// `agrees_with`, the production config, the GPU parity at k = 6 and the +/// in-guest gates at k = 5 and 6. +pub const WHIR_FOLDS_IMPLEMENTED: bool = true; + +/// A first-round fold, `1 ..= MAX_FOLD`. Constructed only through +/// [`FirstFold::new`], so a fold of 0 or wider than the tested stack is not a +/// value a config can hold. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct FirstFold(u8); + +impl FirstFold { + /// `None` outside `1..=MAX_FOLD`. + pub const fn new(k: usize) -> Option { + if k >= 1 && k <= MAX_FOLD { + Some(Self(k as u8)) + } else { + None + } + } + + pub const fn get(self) -> usize { + self.0 as usize + } } +/// The top bit of a non-uniform [`ChainConfig::fold_word`]. A uniform word is +/// `log_folding`, far below it, so no non-default word equals a default one. +pub const FOLD_WORD_TAG: u64 = 1 << 63; + impl ChainConfig { /// Parameters for a security target, in the **same regime the univariate /// prover uses**: the Johnson bound, `proximity = 1 − √rate − 1/300`, so @@ -210,28 +315,134 @@ impl ChainConfig { security_bits: u8, grind: GrindBits, ) -> Self { - let rounds = num_vars.div_ceil(log_folding.max(1)).max(1); + Self::with_security_folds( + log_blowup, + log_folding, + WhirFolds::Uniform, + num_vars, + security_bits, + grind, + ) + } + + /// [`with_security`](Self::with_security) under a fold schedule. + /// + /// ★ THE ONE Q RULE. The query count is one number for every chain the + /// config serves, so the union bound is charged the WORST round count any + /// chain of at most `num_vars` variables has under `folds`. For `Uniform` + /// that is `ceil(num_vars / log_folding)` — today's count, exactly — and a + /// first fold `k0 >= log_folding` never has more rounds than that at any + /// height, so it never raises Q (`first5`/`first6` keep 112 at 25). + /// + /// The rate is `2^-log_blowup` in every round whatever the schedule (the + /// domain loses `k_r` bits as the message loses `k_r` variables), so the + /// per-query bits do not move; only `rounds` does. The disclaimer on + /// [`with_security`](Self::with_security) applies unchanged. + pub fn with_security_folds( + log_blowup: usize, + log_folding: usize, + folds: WhirFolds, + num_vars: usize, + security_bits: u8, + grind: GrindBits, + ) -> Self { + let mut config = Self { + log_blowup, + log_folding, + num_queries: 0, + grind, + format: ChainFormat { + folds, + ..ChainFormat::DEFAULT + }, + }; + let rounds = (1..=num_vars) + .map(|m| config.rounds(m)) + .max() + .unwrap_or(0) + .max(1); // ★ Integers, not `f64`. The arithmetic and its provenance are in // [`crate::query_count`]; what matters here is that the count a // verifier has to reproduce no longer needs floating point to // reproduce it, and that the answers did not move — the shipped // posture's 110 / 112 / 113 are pinned in both places. - let num_queries = + config.num_queries = crate::query_count::num_queries(log_blowup, rounds, security_bits, grind.query); + config + } - Self { - log_blowup, - log_folding, - num_queries, - grind, + /// Rounds a chain of `num_vars` variables runs: `schedule(num_vars).len()`. + pub fn rounds(&self, num_vars: usize) -> usize { + self.schedule(num_vars).len() + } + + /// The statement's fold word: what the three host absorbs and the LFM's + /// `push_config` write where they wrote `log_folding`. + /// + /// - `Uniform` → `log_folding`: `4u64` at the default, today's bytes. + /// - `First(k0)` → `FOLD_WORD_TAG | log_folding << 52 | 1 << 48 | k0`: + /// a prefix encoding with a one-entry prefix (tail + /// `log_folding`, length 1, the fold in the low nibble). + /// + /// Every chain's schedule is a function of this word and its own + /// `num_vars`, which the statement already binds, so binding the word + /// binds every schedule — including the heights where two policies give + /// the same schedule (`first6` and `uniform4` at `num_vars <= 4`), where + /// only the word tells the proofs apart. + pub fn fold_word(&self) -> u64 { + match self.format.folds { + WhirFolds::Uniform => self.log_folding as u64, + WhirFolds::First(k0) => { + FOLD_WORD_TAG + | ((self.log_folding as u64 & 0x7ff) << 52) + | (1 << 48) + | k0.get() as u64 + } } } + /// The Merkle cap height of each of the chain's `R` commitment trees, tree + /// `t` being the one round `t` opens as its current codeword (W1). + /// + /// Tree `t` has depth `D_t − k_t` (its leaves are round `t`'s domain + /// folded by that round's `k`) and is opened `Q` times when `t = 0` (round + /// 0's current openings) and `2Q` times after (round `t − 1`'s successor + /// openings and round `t`'s current ones, the last tree included). The + /// height is [`CapPolicy::height`] of those two public numbers, so the + /// prover, the host verifier and the in-guest emitter derive the same + /// heights from the config alone. All zero at the default. + pub fn tree_caps(&self, num_vars: usize) -> Vec { + let mut domain_log = num_vars + self.log_blowup; + self.schedule(num_vars) + .iter() + .enumerate() + .map(|(t, &k)| { + domain_log -= k; + let openings = if t == 0 { + self.num_queries + } else { + 2 * self.num_queries + }; + self.format.cap.height(openings, domain_log) + }) + .collect() + } + /// Variables folded in each round: `log_folding` until the remainder. + /// + /// Under [`WhirFolds::First`] the first round takes `k0` (or everything, + /// when there is less), and the rest is this same walk. pub fn schedule(&self, num_vars: usize) -> Vec { let step = self.log_folding.max(1); let mut left = num_vars; let mut out = Vec::new(); + if let WhirFolds::First(k0) = self.format.folds { + let take = k0.get().min(left); + if take > 0 { + out.push(take); + left -= take; + } + } while left > 0 { let take = step.min(left); out.push(take); @@ -708,6 +919,8 @@ where // several (`stacked_eval::prove` runs one per commitment). crate::whir_split::bump(&crate::whir_split::CHAIN_COUNT); let schedule = config.schedule(num_vars); + // One cap height per tree; all zero at the default format. + let caps = config.tree_caps(num_vars); // The codeword comes out of the commitment rather than being encoded // again: it is the same array, and the NTT is not cheap. let mut current = Current::::Base(commitment); @@ -811,26 +1024,41 @@ where log_folding: k, }; let __wc_q = crate::whir_split::mark(); - let openings = match (¤t, &next) { - (Current::Base(held), Some(next)) => { - RoundOpenings::Base(whir_round::prove(*held, next, &round_config, transcript)?) - } - (Current::Base(held), None) => RoundOpenings::Base(final_openings::( - held, - &round_config, - transcript, - )?), - (Current::Extension(held), Some(next)) => { - RoundOpenings::Extension(whir_round::prove(held, next, &round_config, transcript)?) - } - (Current::Extension(held), None) => { - RoundOpenings::Extension(final_openings::( + // Tree `r` is opened under `caps[r]`, and carries its cap on its first + // opening in proof order: round 0's first current opening for tree 0, + // round `r − 1`'s first successor opening for every later tree. + let round_caps = RoundCaps { + current: caps[r], + current_owner: r == 0, + next: caps.get(r + 1).copied().unwrap_or(0), + }; + let openings = + match (¤t, &next) { + (Current::Base(held), Some(next)) => RoundOpenings::Base(whir_round::prove( + *held, + next, + &round_config, + round_caps, + transcript, + )?), + (Current::Base(held), None) => RoundOpenings::Base(final_openings::( held, &round_config, + round_caps, transcript, - )?) - } - }; + )?), + (Current::Extension(held), Some(next)) => RoundOpenings::Extension( + whir_round::prove(held, next, &round_config, round_caps, transcript)?, + ), + (Current::Extension(held), None) => { + RoundOpenings::Extension(final_openings::( + held, + &round_config, + round_caps, + transcript, + )?) + } + }; crate::whir_split::add(&crate::whir_split::QUERIES, __wc_q); rounds.push(ChainRound { @@ -926,6 +1154,7 @@ where fn final_openings( current: &CodewordCommitment, config: &RoundConfig, + caps: RoundCaps, transcript: &mut T, ) -> Result, Error> where @@ -943,7 +1172,7 @@ where // ⛔ ONE `open_many` here, not two: the final round has no successor to // open. That is the `− 1` in arm F's `2R − 1`. Ok(RoundProof { - current: current.open_many(&queries)?, + current: current.open_many_capped(&queries, caps.current, caps.current_owner)?, next: Vec::new(), }) } @@ -983,9 +1212,9 @@ where /// `weight_at` is the weight's closed form, evaluated at the concatenation of /// every round's challenges. #[allow(clippy::too_many_arguments)] -pub fn verify_weighted( - proof: &ChainProof, - root: &Commitment, +pub fn verify_weighted<'a, F, E, T, W, H>( + proof: &'a ChainProof, + root: &'a Commitment, weight_at: W, y: FieldElement, num_vars: usize, @@ -1012,7 +1241,14 @@ where let mut claim = y; let mut alphas: Vec> = Vec::with_capacity(num_vars); - let mut current_root = *root; + // Tree 0 is authenticated by the cap round 0's first current opening + // carries; every later tree by the check the round that committed it + // returned. A verifier constant per tree, derived from the config alone. + let caps = config.tree_caps(num_vars); + let mut current: TreeCheck<'a> = TreeCheck::Owner { + root, + cap_height: caps[0], + }; let mut current_domain = domain.clone(); // Each round's out-of-domain claim, and how many variables were bound when // it entered the weight — the challenges after that are where its `eq` @@ -1069,11 +1305,12 @@ where check_grind::(transcript, config.grind.query, round.nonces.query)?; let commitments = RoundCommitments { - current_root: ¤t_root, + current, next_root, next_num_leaves: next_domain.size() >> next_k, + next_cap_height: caps[r + 1], }; - match &round.openings { + let next_check = match &round.openings { RoundOpenings::Base(openings) => whir_round::verify::( openings, commitments, @@ -1090,8 +1327,8 @@ where &round_config, transcript, )?, - } - current_root = *next_root; + }; + current = TreeCheck::Checked(next_check); } (None, None, None) => { transcript.append_field_element(&proof.final_value); @@ -1099,7 +1336,7 @@ where match &round.openings { RoundOpenings::Base(openings) => verify_final::( openings, - ¤t_root, + current, ¤t_domain, &group.point, &round_config, @@ -1108,7 +1345,7 @@ where )?, RoundOpenings::Extension(openings) => verify_final::( openings, - ¤t_root, + current, ¤t_domain, &group.point, &round_config, @@ -1153,9 +1390,9 @@ where } /// The last round: every queried block must fold to the constant that was sent. -fn verify_final( - openings: &RoundProof, - current_root: &Commitment, +fn verify_final<'a, F, C, N, T, H>( + openings: &'a RoundProof, + current: TreeCheck<'a>, current_domain: &Domain, alphas: &[FieldElement], config: &RoundConfig, @@ -1177,10 +1414,22 @@ where }); } let num_leaves = current_domain.size() >> config.log_folding; + let depth = num_leaves.trailing_zeros() as usize; + // The tree's check, built once from its first opening, after the count + // guard above, so indexing it never panics. With no openings there is nothing to check. + let Some(first) = openings.current.first() else { + return Ok(()); + }; + let (check, first_siblings) = current.open::(depth, first)?; for (i, opening) in openings.current.iter().enumerate() { let q = transcript.sample_u64(num_leaves as u64) as usize; - if !verify_opening::(current_root, q, opening) { + let siblings = if i == 0 { + first_siblings + } else { + opening.proof.merkle_path.as_slice() + }; + if !verify_opening_capped::(&check, q, opening, siblings) { return Err(Error::OpeningRejected { query: i }); } if fold_coset::(&opening.values, current_domain, q, alphas)? != *final_value { @@ -1229,6 +1478,7 @@ mod tests { log_folding, num_queries: 3, grind: GrindBits::default(), + format: ChainFormat::DEFAULT, } } @@ -1338,6 +1588,295 @@ mod tests { assert_eq!(config(1).schedule(3), vec![1, 1, 1]); } + // --------------------------------------------------------------- + // W2: the first-fold schedule. + // --------------------------------------------------------------- + + fn first(k0: usize, log_folding: usize) -> ChainConfig { + ChainConfig { + format: ChainFormat { + folds: WhirFolds::First(FirstFold::new(k0).unwrap()), + ..ChainFormat::DEFAULT + }, + ..config(log_folding) + } + } + + /// Today's `schedule` body, verbatim, as the reference the default must + /// reproduce. + fn uniform_reference(log_folding: usize, num_vars: usize) -> Vec { + let step = log_folding.max(1); + let mut left = num_vars; + let mut out = Vec::new(); + while left > 0 { + let take = step.min(left); + out.push(take); + left -= take; + } + out + } + + #[test] + fn the_schedule_is_uniform_by_default() { + for k in 1..=MAX_FOLD { + for n in 0..=40 { + assert_eq!( + config(k).schedule(n), + uniform_reference(k, n), + "k={k} n={n}" + ); + assert_eq!(config(k).rounds(n), uniform_reference(k, n).len()); + } + } + assert_eq!(ChainFormat::DEFAULT.folds, WhirFolds::Uniform); + assert_eq!(WhirFolds::default(), WhirFolds::Uniform); + } + + #[test] + fn with_security_folds_uniform_is_with_security() { + for k in 1..=MAX_FOLD { + for n in 0..=40 { + for grind in [GrindBits::default(), GrindBits::uniform(20)] { + assert_eq!( + ChainConfig::with_security_folds(2, k, WhirFolds::Uniform, n, 128, grind), + ChainConfig::with_security(2, k, n, 128, grind), + "k={k} n={n}" + ); + } + } + } + // And today's production numbers. + let today = ChainConfig::with_security(2, 4, 25, 128, GrindBits::uniform(20)); + assert_eq!((today.rounds(25), today.num_queries), (7, 112)); + } + + #[test] + fn the_default_fold_word_is_log_folding() { + for k in 1..=MAX_FOLD { + assert_eq!(config(k).fold_word(), k as u64); + } + assert_eq!(config(4).fold_word().to_le_bytes(), 4u64.to_le_bytes()); + } + + /// The first-fold schedules, by hand, and the clamp at small heights. + #[test] + fn the_first_fold_schedules() { + let (f5, f6) = (first(5, 4), first(6, 4)); + assert_eq!(f6.schedule(25), vec![6, 4, 4, 4, 4, 3]); + assert_eq!(f6.schedule(24), vec![6, 4, 4, 4, 4, 2]); + assert_eq!(f6.schedule(23), vec![6, 4, 4, 4, 4, 1]); + assert_eq!(f5.schedule(25), vec![5, 4, 4, 4, 4, 4]); + assert_eq!(f5.schedule(24), vec![5, 4, 4, 4, 4, 3]); + assert_eq!(f5.schedule(23), vec![5, 4, 4, 4, 4, 2]); + // The first round takes everything when there is less than k0. + assert_eq!(f6.schedule(0), Vec::::new()); + assert_eq!(f6.schedule(3), vec![3]); + assert_eq!(f6.schedule(6), vec![6]); + assert_eq!(f6.schedule(7), vec![6, 1]); + assert_eq!(f5.schedule(9), vec![5, 4]); + assert_eq!(f5.schedule(11), vec![5, 4, 2]); + assert_eq!(f6.schedule(9), vec![6, 3]); + // Every schedule covers exactly `n`, starts at min(k0, n), then walks + // today's uniform body over the rest; no fold exceeds MAX_FOLD. + for k0 in 1..=MAX_FOLD { + for k in 1..=4 { + let c = first(k0, k); + for n in 0..=40 { + let s = c.schedule(n); + assert_eq!(s.iter().sum::(), n); + assert!(s.iter().all(|&x| (1..=MAX_FOLD).contains(&x))); + if n > 0 { + assert_eq!(s[0], k0.min(n)); + assert_eq!(s[1..], uniform_reference(k, n - s[0])[..]); + } + } + } + } + } + + #[test] + fn a_first_fold_outside_the_tested_stack_is_unconstructible() { + assert!(FirstFold::new(0).is_none()); + assert!(FirstFold::new(MAX_FOLD + 1).is_none()); + assert!(FirstFold::new(64).is_none()); + for k in 1..=MAX_FOLD { + assert_eq!(FirstFold::new(k).unwrap().get(), k); + } + } + + #[test] + fn every_fold_word_is_distinct_and_the_non_default_ones_are_tagged() { + let mut seen = std::collections::HashSet::new(); + for k in 1..=MAX_FOLD { + let uniform = config(k).fold_word(); + assert_eq!(uniform & FOLD_WORD_TAG, 0); + assert!(seen.insert(uniform)); + for k0 in 1..=MAX_FOLD { + let w = first(k0, k).fold_word(); + assert_ne!(w & FOLD_WORD_TAG, 0, "k={k} k0={k0}"); + assert!(seen.insert(w), "k={k} k0={k0}: {w:#x} repeats"); + } + } + // The production words, spelled out. + assert_eq!(first(6, 4).fold_word(), 0x8041_0000_0000_0006); + assert_eq!(first(5, 4).fold_word(), 0x8041_0000_0000_0005); + } + + /// No accepted first fold raises the round count at any height, so the + /// union bound is never charged more and Q never rises; at the production + /// tallest (25) it is today's 112. + #[test] + fn a_first_fold_never_raises_the_query_count() { + let g = GrindBits::uniform(20); + for k0 in [5usize, 6] { + let folds = WhirFolds::First(FirstFold::new(k0).unwrap()); + for tallest in 1..=32 { + let today = ChainConfig::with_security(2, 4, tallest, 128, g); + let arm = ChainConfig::with_security_folds(2, 4, folds, tallest, 128, g); + assert_eq!(arm.format.folds, folds); + for m in 1..=tallest { + assert!(arm.rounds(m) <= today.rounds(m), "k0={k0} m={m}"); + } + assert!( + arm.num_queries <= today.num_queries, + "k0={k0} tallest={tallest}" + ); + } + let at25 = ChainConfig::with_security_folds(2, 4, folds, 25, 128, g); + assert_eq!((at25.rounds(25), at25.num_queries), (6, 112), "k0={k0}"); + } + // The rule charges the WORST height, not the tallest: a first fold + // narrower than the uniform one has more rounds at some shorter chain, + // and Q follows that one. + let narrow = ChainConfig::with_security_folds( + 2, + 4, + WhirFolds::First(FirstFold::new(1).unwrap()), + 8, + 128, + GrindBits::default(), + ); + let worst = (1..=8).map(|m| narrow.rounds(m)).max().unwrap(); + assert_eq!(worst, 3); + assert_eq!( + narrow.num_queries, + crate::query_count::num_queries(2, worst, 128, 0) + ); + } + + fn run_with(cfg: &ChainConfig, num_vars: usize) -> Result, Error> { + let f = pseudo_mle(num_vars, 13); + let z = point(num_vars); + let y = f.evaluate(&z).unwrap(); + let (commitment, domain) = commit::(&f, cfg, true)?; + let proof = + prove::(&f, &z, &commitment, &domain, cfg, &mut transcript())?; + verify::( + &proof, + &commitment.root(), + &z, + y, + &domain, + cfg, + &mut transcript(), + )?; + Ok(proof) + } + + #[test] + fn a_first_fold_proof_verifies_and_opens_its_wide_block() { + for (k0, n) in [(5, 9), (5, 11), (6, 9), (6, 11), (6, 6), (6, 3)] { + let cfg = first(k0, 4); + let proof = run_with(&cfg, n).unwrap_or_else(|e| panic!("k0={k0} n={n}: {e:?}")); + assert_eq!(proof.rounds.len(), cfg.rounds(n)); + for (round, &k) in proof.rounds.iter().zip(&cfg.schedule(n)) { + assert_eq!(round.sumcheck.len(), k); + for opening in current_blocks(&round.openings) { + assert_eq!(opening.values.len(), 1 << k); + } + } + } + } + + #[test] + fn a_tampered_wide_base_block_is_rejected() { + let cfg = first(6, 4); + let num_vars = 11; + let f = pseudo_mle(num_vars, 83); + let z = point(num_vars); + let y = f.evaluate(&z).unwrap(); + let (commitment, domain) = commit::(&f, &cfg, true).unwrap(); + let honest = + prove::(&f, &z, &commitment, &domain, &cfg, &mut transcript()) + .unwrap(); + assert_eq!( + current_blocks(&honest.rounds[0].openings)[0].values.len(), + 64 + ); + // The last value of the 64-wide block, so the check must read all of it. + let mut proof = honest.clone(); + current_blocks_mut(&mut proof.rounds[0].openings)[0].values[63] += FE::one(); + let err = verify::( + &proof, + &commitment.root(), + &z, + y, + &domain, + &cfg, + &mut transcript(), + ) + .unwrap_err(); + assert!( + matches!( + err, + Error::OpeningRejected { .. } | Error::FoldInconsistent { .. } + ), + "{err:?}" + ); + } + + /// A proof made under one schedule is refused under another: the round + /// count (or the first round's sumcheck length) disagrees. + #[test] + fn a_first_fold_proof_is_refused_under_the_uniform_schedule() { + let num_vars = 11; + let (f6, uniform) = (first(6, 4), config(4)); + let f = pseudo_mle(num_vars, 13); + let z = point(num_vars); + let y = f.evaluate(&z).unwrap(); + let (commitment, domain) = commit::(&f, &f6, true).unwrap(); + let proof = + prove::(&f, &z, &commitment, &domain, &f6, &mut transcript()) + .unwrap(); + let err = verify::( + &proof, + &commitment.root(), + &z, + y, + &domain, + &uniform, + &mut transcript(), + ) + .unwrap_err(); + assert!(matches!(err, Error::RoundCountMismatch { .. }), "{err:?}"); + // And the other way round. + let (commitment, domain) = commit::(&f, &uniform, true).unwrap(); + let proof = + prove::(&f, &z, &commitment, &domain, &uniform, &mut transcript()) + .unwrap(); + let err = verify::( + &proof, + &commitment.root(), + &z, + y, + &domain, + &f6, + &mut transcript(), + ) + .unwrap_err(); + assert!(matches!(err, Error::RoundCountMismatch { .. }), "{err:?}"); + } + /// The point of chaining: a query opens a block of `2^k`, not the message. #[test] fn a_block_is_the_fold_size_not_the_message() { diff --git a/crypto/multilinear/src/whir_commit.rs b/crypto/multilinear/src/whir_commit.rs index b1a72e7fa..e2cd5fcca 100644 --- a/crypto/multilinear/src/whir_commit.rs +++ b/crypto/multilinear/src/whir_commit.rs @@ -3,7 +3,12 @@ //! The pre-image of folded index `j` is the stride-`N/2^k` coset //! `{ j, j + N/2^k, …, j + (2^k - 1)·N/2^k }`. -use crypto::merkle_tree::{merkle::MerkleTree, proof::Proof, traits::IsMerkleTreeBackend}; +use crypto::merkle_tree::{ + cap::{CappedRoot, embed_cap}, + merkle::MerkleTree, + proof::Proof, + traits::IsMerkleTreeBackend, +}; use math::{ field::{ element::FieldElement, @@ -284,6 +289,11 @@ where 1usize << (self.log_domain_size - self.log_folding) } + /// Siblings on a full authentication path: `log2(num_leaves)`. + pub fn depth(&self) -> usize { + self.log_domain_size - self.log_folding + } + pub fn log_folding(&self) -> usize { self.log_folding } @@ -307,6 +317,26 @@ where /// in a single pass over the codeword, and a round asks for a hundred of /// them. pub fn open_many(&self, indices: &[usize]) -> Result>, Error> { + self.open_many_capped(indices, 0, false) + } + + /// [`open_many`](Self::open_many) under a Merkle cap of height + /// `cap_height` (the owner-path encoding, `crypto::merkle_tree::cap`). + /// + /// Every path is cut to `depth − cap_height` siblings. When `owner` is + /// set, this call's first opening is the tree's first opening in proof + /// order and carries the tree's cap (`2^cap_height` nodes) after its + /// siblings. At `cap_height = 0` this is exactly `open_many`, whatever + /// `owner` says. + /// + /// On a device the cap is read from the tree the paths are gathered from, + /// inside the same rebuild, so it costs no extra tree build. + pub fn open_many_capped( + &self, + indices: &[usize], + cap_height: usize, + owner: bool, + ) -> Result>, Error> { let num_leaves = self.num_leaves(); // ★ ONE CALL, ONE DEVICE TREE REBUILD. Counted rather than inferred: // `whir_round::prove` opens the current commitment AND its successor, @@ -319,7 +349,7 @@ where // bookkeeping and read ~100% every time. What competes with the rebuild // is the COSET GATHER, which is the next statement, not a nested one. let __wq_tree = crate::whir_split::mark(); - let proofs = self.paths(indices)?; + let proofs = self.paths_capped(indices, cap_height, owner)?; crate::whir_split::add(&crate::whir_split::TREE_REBUILD, __wq_tree); let block = 1usize << self.log_folding; @@ -398,6 +428,76 @@ where } } + /// [`paths`](Self::paths), cut to the cap and, for the owner, with the + /// cap appended to the first path. + fn paths_capped( + &self, + indices: &[usize], + cap_height: usize, + owner: bool, + ) -> Result>, Error> { + if cap_height == 0 { + return self.paths(indices); + } + let depth = self.depth(); + if cap_height > depth { + return Err(Error::CapEmbedFailed { + reason: "cap taller than the tree", + }); + } + let embed_failed = |_: crypto::merkle_tree::cap::CapError| Error::CapEmbedFailed { + reason: "path or cap of the wrong length", + }; + let (mut proofs, cap) = if owner { + match &self.codeword { + Codeword::Device(device) => { + let num_leaves = self.num_leaves(); + if let Some(&bad) = indices.iter().find(|index| **index >= num_leaves) { + return Err(Error::QueryOutOfRange { + index: bad, + bound: num_leaves, + }); + } + // ONE rebuild: the cap comes from the tree the paths are + // gathered from. + let (paths, cap) = device + .paths_and_cap(self.log_folding, indices, cap_height, H::DEVICE) + .ok_or(Error::DeviceFailed { + stage: "opening paths and cap", + })?; + let proofs: Vec> = paths + .into_iter() + .map(|merkle_path| Proof { merkle_path }) + .collect(); + (proofs, Some(cap)) + } + Codeword::Host(_) => { + let cap = self.tree.cap(cap_height).ok_or(Error::CapEmbedFailed { + reason: "the host tree has no cap at this height", + })?; + (self.paths(indices)?, Some(cap)) + } + } + } else { + (self.paths(indices)?, None) + }; + match cap { + Some(cap) => { + let mut refs: Vec<&mut Vec> = + proofs.iter_mut().map(|p| &mut p.merkle_path).collect(); + embed_cap(&mut refs, depth, &cap).map_err(embed_failed)?; + } + None => { + for proof in &mut proofs { + proof + .truncate_to_cap(depth, cap_height) + .map_err(embed_failed)?; + } + } + } + Ok(proofs) + } + /// Opens the block that folds onto `index`. pub fn open(&self, index: usize) -> Result, Error> { let num_leaves = self.num_leaves(); @@ -431,15 +531,50 @@ where /// because "which hash authenticated this path" is the whole content of the /// call. A verifier reading a proof under the wrong `H` gets `false` here, not /// a different-but-plausible answer. -pub fn verify_opening(root: &Commitment, index: usize, opening: &CosetOpening) -> bool +/// +/// `depth` is the tree's depth (`log2` of its leaf count), a verifier +/// constant: the path must be exactly that long and `index < 2^depth`. A path +/// of any other length is refused before it is folded, so a leaf hash can +/// never be compared with an internal node. +pub fn verify_opening( + root: &Commitment, + depth: usize, + index: usize, + opening: &CosetOpening, +) -> bool where F: IsField + 'static, H: WhirHash, FieldElement: AsBytes + Sync + Send, { - opening - .proof - .verify::>(root, index, &opening.values) + verify_opening_capped::( + &CappedRoot::uncapped(root, depth), + index, + opening, + &opening.proof.merkle_path, + ) +} + +/// Checks an opening against one tree's authenticated cap. +/// +/// `siblings` is the opening's path with any cap split off — the whole +/// `opening.proof.merkle_path` for every opening but a tree's owner, whose +/// siblings [`CappedRoot::from_owner`] returns. It must be exactly +/// `depth − c` long and fold `hash(values)` at `index` onto +/// `cap[index >> (depth − c)]`. At `c = 0` the cap is the root, and this is +/// [`verify_opening`]. +pub fn verify_opening_capped( + check: &CappedRoot<'_, Commitment>, + index: usize, + opening: &CosetOpening, + siblings: &[Commitment], +) -> bool +where + F: IsField + 'static, + H: WhirHash, + FieldElement: AsBytes + Sync + Send, +{ + check.verify::>(siblings, index, Backend::::hash_data(&opening.values)) } /// One level of a block's fold. @@ -584,7 +719,7 @@ mod tests { let opening = commitment.open(j).unwrap(); assert_eq!(opening.values.len(), 2); assert!( - verify_opening::(&root, j, &opening), + verify_opening::(&root, commitment.depth(), j, &opening), "leaf {j}" ); } @@ -598,7 +733,12 @@ mod tests { let mut opening = commitment.open(2).unwrap(); opening.values[0] += FE::one(); - assert!(!verify_opening::(&root, 2, &opening)); + assert!(!verify_opening::( + &root, + commitment.depth(), + 2, + &opening + )); } #[test] @@ -607,7 +747,12 @@ mod tests { let commitment = CodewordCommitment::::new(&cw, 1).unwrap(); let root = commitment.root(); let opening = commitment.open(2).unwrap(); - assert!(!verify_opening::(&root, 3, &opening)); + assert!(!verify_opening::( + &root, + commitment.depth(), + 3, + &opening + )); } #[test] diff --git a/crypto/multilinear/src/whir_eval.rs b/crypto/multilinear/src/whir_eval.rs index 63c6c8e11..349fe2e6d 100644 --- a/crypto/multilinear/src/whir_eval.rs +++ b/crypto/multilinear/src/whir_eval.rs @@ -246,7 +246,7 @@ where let queries = sample_queries(transcript, config.num_queries, num_leaves); for (i, (&q, opening)) in queries.iter().zip(&proof.openings).enumerate() { - if !verify_opening::(root, q, opening) { + if !verify_opening::(root, num_leaves.trailing_zeros() as usize, q, opening) { return Err(Error::OpeningRejected { query: i }); } if fold_coset::(&opening.values, domain, q, alphas)? != proof.final_value { diff --git a/crypto/multilinear/src/whir_round.rs b/crypto/multilinear/src/whir_round.rs index 66ea3c42a..d1962ed55 100644 --- a/crypto/multilinear/src/whir_round.rs +++ b/crypto/multilinear/src/whir_round.rs @@ -5,6 +5,7 @@ //! chosen to match. Consistency holds only where the queries land. use crypto::fiat_shamir::is_transcript::IsTranscript; +use crypto::merkle_tree::cap::CappedRoot; use math::{ field::{ element::FieldElement, @@ -16,10 +17,15 @@ use math::{ use crate::{ Error, whir::Domain, - whir_commit::{CodewordCommitment, Commitment, CosetOpening, fold_coset, leaf_and_slot}, + whir_commit::{ + CodewordCommitment, Commitment, CosetOpening, fold_coset, leaf_and_slot, + verify_opening_capped, + }, whir_hash::WhirHash, }; +type Backend = ::Backend; + /// How hard a round is to cheat. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct RoundConfig { @@ -29,14 +35,90 @@ pub struct RoundConfig { pub log_folding: usize, } -/// What the verifier already knows about the two codewords: their roots, and -/// how the successor was blocked. +/// How a tree's openings are authenticated in a round. +/// +/// A tree is authenticated ONCE: tree 0 by the +/// cap its first opening in round 0 carries, and tree `t ≥ 1` by the cap its +/// first opening as round `t − 1`'s SUCCESSOR carries. Round `t` then opens +/// tree `t` as its current tree against that stored check, and never re-reads +/// a cap from its own first opening. +#[derive(Clone, Copy, Debug)] +pub enum TreeCheck<'a> { + /// Authenticated in an earlier round. + Checked(CappedRoot<'a, Commitment>), + /// Owned by this round: its first opening carries its cap of height + /// `cap_height` (none at 0). + Owner { + root: &'a Commitment, + cap_height: usize, + }, +} + +impl<'a> TreeCheck<'a> { + /// The tree's check and the siblings of `first`, the tree's first opening + /// in this round. + /// + /// An [`Owner`](Self::Owner) tree's cap is split off `first`'s path and + /// authenticated against the root here — once, before any opening of the + /// tree is checked against it. At `cap_height = 0` there is no cap: the + /// whole path is siblings, and the per-query check enforces its length. + /// A [`Checked`](Self::Checked) tree must have the depth this round + /// derives, and `first` carries no cap: its whole path is siblings. + pub(crate) fn open( + self, + depth: usize, + first: &'a CosetOpening, + ) -> Result<(CappedRoot<'a, Commitment>, &'a [Commitment]), Error> + where + C: IsField + 'static, + FieldElement: AsBytes + Sync + Send, + H: WhirHash, + { + let path = first.proof.merkle_path.as_slice(); + match self { + TreeCheck::Checked(check) => { + if check.depth() != depth { + return Err(Error::CapRejected); + } + Ok((check, path)) + } + TreeCheck::Owner { + root, + cap_height: 0, + } => Ok((CappedRoot::uncapped(root, depth), path)), + TreeCheck::Owner { root, cap_height } => { + CappedRoot::from_owner::>(root, path, depth, cap_height) + .ok_or(Error::CapRejected) + } + } + } +} + +/// What the verifier already knows about the two codewords: how the current +/// tree is authenticated, the successor's root, how the successor was blocked, +/// and its cap height. #[derive(Clone, Copy, Debug)] pub struct RoundCommitments<'a> { - pub current_root: &'a Commitment, + pub current: TreeCheck<'a>, pub next_root: &'a Commitment, /// Leaves in the successor's tree, needed to locate a position in it. pub next_num_leaves: usize, + /// The successor tree's cap height. Its cap rides this round's first + /// successor opening, which is that tree's first opening in proof order. + pub next_cap_height: usize, +} + +/// The cap heights a round opens its two trees under (prover side). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct RoundCaps { + /// The current tree's cap height. + pub current: usize, + /// True when this round's first current opening is the current tree's + /// first opening in proof order (round 0), and so carries its cap. + pub current_owner: bool, + /// The successor tree's cap height. The successor is always owned by the + /// round that commits it. + pub next: usize, } /// The openings one round sends. @@ -79,10 +161,14 @@ where /// /// `current` and `next` must already be committed, and `next` must be the fold /// of `current` by `alphas` — [`verify`] is what checks that claim. +/// +/// `caps` is the Merkle cap each tree is opened under ([`RoundCaps`]); the +/// default is no cap on either. pub fn prove( current: &CodewordCommitment, next: &CodewordCommitment, config: &RoundConfig, + caps: RoundCaps, transcript: &mut T, ) -> Result, Error> where @@ -106,23 +192,28 @@ where crate::whir_split::add(&crate::whir_split::QUERY_SAMPLE, __wq_sample); Ok(RoundProof { - current: current.open_many(&queries)?, - next: next.open_many(&leaves)?, + current: current.open_many_capped(&queries, caps.current, caps.current_owner)?, + next: next.open_many_capped(&leaves, caps.next, true)?, }) } /// Checks a round against the two commitments. /// /// Re-derives the queries from the transcript, so the prover could not have -/// chosen them. -pub fn verify( - proof: &RoundProof, - commitments: RoundCommitments<'_>, +/// chosen them. Returns the successor tree's authenticated check, which the +/// next round opens its current tree against. +/// +/// ⚠ ORDER. The opening counts are checked before any opening is indexed or +/// any cap is read, so a proof with too few openings is refused and never +/// panics. +pub fn verify<'a, F, C, N, T, H>( + proof: &'a RoundProof, + commitments: RoundCommitments<'a>, domain: &Domain, alphas: &[FieldElement], config: &RoundConfig, transcript: &mut T, -) -> Result<(), Error> +) -> Result, Error> where F: IsFFTField + IsPrimeField + IsSubFieldOf + IsSubFieldOf, C: IsField + IsSubFieldOf + 'static, @@ -146,6 +237,36 @@ where } let num_leaves = domain.size() >> config.log_folding; + let current_depth = num_leaves.trailing_zeros() as usize; + if !commitments.next_num_leaves.is_power_of_two() { + return Err(Error::NotPowerOfTwo(commitments.next_num_leaves)); + } + let next_depth = commitments.next_num_leaves.trailing_zeros() as usize; + + // Both trees' checks, each built once from the tree's first opening — + // after the count guard above, so index 0 exists (M2). With no openings + // (`num_queries == 0`) no cap exists either: `CapPolicy::height` is 0 for + // an unopened tree, and the successor's check is its bare root. + let (current_check, current_first, next_check, next_first) = + match (proof.current.first(), proof.next.first()) { + (Some(cur), Some(nxt)) => { + let (current_check, current_first) = + commitments.current.open::(current_depth, cur)?; + let (next_check, next_first) = TreeCheck::Owner { + root: commitments.next_root, + cap_height: commitments.next_cap_height, + } + .open::(next_depth, nxt)?; + (current_check, current_first, next_check, next_first) + } + _ => { + if commitments.next_cap_height != 0 { + return Err(Error::CapRejected); + } + return Ok(CappedRoot::uncapped(commitments.next_root, next_depth)); + } + }; + let queries = sample_queries(transcript, config.num_queries, num_leaves); for (i, (&q, (cur, nxt))) in queries @@ -153,11 +274,19 @@ where .zip(proof.current.iter().zip(&proof.next)) .enumerate() { - if !crate::whir_commit::verify_opening::(commitments.current_root, q, cur) { + let (cur_siblings, nxt_siblings) = if i == 0 { + (current_first, next_first) + } else { + ( + cur.proof.merkle_path.as_slice(), + nxt.proof.merkle_path.as_slice(), + ) + }; + if !verify_opening_capped::(¤t_check, q, cur, cur_siblings) { return Err(Error::OpeningRejected { query: i }); } let (leaf, slot) = leaf_and_slot(q, commitments.next_num_leaves); - if !crate::whir_commit::verify_opening::(commitments.next_root, leaf, nxt) { + if !verify_opening_capped::(&next_check, leaf, nxt, nxt_siblings) { return Err(Error::OpeningRejected { query: i }); } @@ -171,7 +300,7 @@ where } } - Ok(()) + Ok(next_check) } #[cfg(test)] @@ -180,6 +309,10 @@ mod tests { use crypto::fiat_shamir::default_transcript::DefaultTranscript; use math::field::goldilocks::GoldilocksField as F; + use crypto::merkle_tree::{ + cap::verify_merkle_path_to_cap_from_leaf_hash, traits::IsMerkleTreeBackend, + }; + use crate::{ mle::Mle, whir::{encode, fold_codeword_k, monomial_coefficients}, @@ -224,19 +357,33 @@ mod tests { } } + fn commitments<'a>( + fx: &Fixture, + current_root: &'a Commitment, + next_root: &'a Commitment, + ) -> RoundCommitments<'a> { + RoundCommitments { + current: TreeCheck::Owner { + root: current_root, + cap_height: 0, + }, + next_root, + next_num_leaves: fx.next.num_leaves(), + next_cap_height: 0, + } + } + fn run(fx: &Fixture, proof: &RoundProof) -> Result<(), Error> { + let (current_root, next_root) = (fx.current.root(), fx.next.root()); verify::( proof, - RoundCommitments { - current_root: &fx.current.root(), - next_root: &fx.next.root(), - next_num_leaves: fx.next.num_leaves(), - }, + commitments(fx, ¤t_root, &next_root), &fx.domain, &fx.alphas, &fx.config, &mut transcript(), ) + .map(|_| ()) } #[test] @@ -244,7 +391,14 @@ mod tests { for k in 1..=3usize { let mut fx = fixture(4, 2, k); fx.config.num_queries = 4; - let proof = prove(&fx.current, &fx.next, &fx.config, &mut transcript()).unwrap(); + let proof = prove( + &fx.current, + &fx.next, + &fx.config, + RoundCaps::default(), + &mut transcript(), + ) + .unwrap(); run(&fx, &proof).unwrap_or_else(|e| panic!("k={k}: {e:?}")); } } @@ -261,7 +415,14 @@ mod tests { #[test] fn a_tampered_current_opening_is_rejected() { let fx = fixture(4, 2, 2); - let mut proof = prove(&fx.current, &fx.next, &fx.config, &mut transcript()).unwrap(); + let mut proof = prove( + &fx.current, + &fx.next, + &fx.config, + RoundCaps::default(), + &mut transcript(), + ) + .unwrap(); proof.current[0].values[0] += FE::one(); assert!(matches!( @@ -273,7 +434,14 @@ mod tests { #[test] fn a_tampered_successor_opening_is_rejected() { let fx = fixture(4, 2, 2); - let mut proof = prove(&fx.current, &fx.next, &fx.config, &mut transcript()).unwrap(); + let mut proof = prove( + &fx.current, + &fx.next, + &fx.config, + RoundCaps::default(), + &mut transcript(), + ) + .unwrap(); proof.next[1].values[0] += FE::one(); assert!(matches!( @@ -296,7 +464,14 @@ mod tests { let other_cw = encode(&monomial_coefficients(&other), &other_domain).unwrap(); fx.next = CodewordCommitment::new(&other_cw, 1).unwrap(); - let proof = prove(&fx.current, &fx.next, &fx.config, &mut transcript()).unwrap(); + let proof = prove( + &fx.current, + &fx.next, + &fx.config, + RoundCaps::default(), + &mut transcript(), + ) + .unwrap(); assert!(matches!( run(&fx, &proof).unwrap_err(), Error::FoldInconsistent { .. } @@ -306,7 +481,14 @@ mod tests { #[test] fn the_wrong_folding_randomness_is_rejected() { let mut fx = fixture(4, 2, 2); - let proof = prove(&fx.current, &fx.next, &fx.config, &mut transcript()).unwrap(); + let proof = prove( + &fx.current, + &fx.next, + &fx.config, + RoundCaps::default(), + &mut transcript(), + ) + .unwrap(); fx.alphas[0] += FE::one(); assert!(matches!( @@ -319,17 +501,21 @@ mod tests { fn a_proof_replayed_under_another_transcript_is_rejected() { // Queries are redrawn, so the openings no longer line up with them. let fx = fixture(4, 2, 2); - let proof = prove(&fx.current, &fx.next, &fx.config, &mut transcript()).unwrap(); + let proof = prove( + &fx.current, + &fx.next, + &fx.config, + RoundCaps::default(), + &mut transcript(), + ) + .unwrap(); run(&fx, &proof).unwrap(); let mut other = DefaultTranscript::::new(b"a-different-statement"); + let (current_root, next_root) = (fx.current.root(), fx.next.root()); let result = verify::( &proof, - RoundCommitments { - current_root: &fx.current.root(), - next_root: &fx.next.root(), - next_num_leaves: fx.next.num_leaves(), - }, + commitments(&fx, ¤t_root, &next_root), &fx.domain, &fx.alphas, &fx.config, @@ -341,7 +527,14 @@ mod tests { #[test] fn a_proof_with_too_few_openings_is_rejected() { let fx = fixture(4, 2, 2); - let mut proof = prove(&fx.current, &fx.next, &fx.config, &mut transcript()).unwrap(); + let mut proof = prove( + &fx.current, + &fx.next, + &fx.config, + RoundCaps::default(), + &mut transcript(), + ) + .unwrap(); proof.current.pop(); assert!(matches!( @@ -356,7 +549,14 @@ mod tests { #[test] fn randomness_of_the_wrong_arity_is_rejected() { let mut fx = fixture(4, 2, 2); - let proof = prove(&fx.current, &fx.next, &fx.config, &mut transcript()).unwrap(); + let proof = prove( + &fx.current, + &fx.next, + &fx.config, + RoundCaps::default(), + &mut transcript(), + ) + .unwrap(); fx.alphas.pop(); assert!(matches!( @@ -373,8 +573,241 @@ mod tests { // The knob is real: it changes how many openings travel. let mut fx = fixture(4, 2, 1); fx.config.num_queries = 7; - let proof = prove(&fx.current, &fx.next, &fx.config, &mut transcript()).unwrap(); + let proof = prove( + &fx.current, + &fx.next, + &fx.config, + RoundCaps::default(), + &mut transcript(), + ) + .unwrap(); assert_eq!(proof.current.len(), 7); run(&fx, &proof).unwrap(); } + + // ---------------------------------------------------------------- caps + + type B = ::Backend; + + /// A round opened and checked under caps, both trees. + fn run_capped( + fx: &Fixture, + proof: &RoundProof, + caps: RoundCaps, + label: &[u8], + ) -> Result<(), Error> { + let (current_root, next_root) = (fx.current.root(), fx.next.root()); + verify::( + proof, + RoundCommitments { + current: TreeCheck::Owner { + root: ¤t_root, + cap_height: caps.current, + }, + next_root: &next_root, + next_num_leaves: fx.next.num_leaves(), + next_cap_height: caps.next, + }, + &fx.domain, + &fx.alphas, + &fx.config, + &mut DefaultTranscript::::new(label), + ) + .map(|_| ()) + } + + #[test] + fn a_capped_round_verifies_and_carries_its_caps_on_the_first_openings() { + // current: 32 leaves (depth 5); successor: 16 leaves (depth 4). + let mut fx = fixture(4, 2, 1); + fx.config.num_queries = 5; + for (c_cur, c_next) in [(0, 0), (1, 0), (0, 2), (3, 2), (5, 4)] { + let caps = RoundCaps { + current: c_cur, + current_owner: true, + next: c_next, + }; + let proof = prove(&fx.current, &fx.next, &fx.config, caps, &mut transcript()).unwrap(); + for (i, (cur, nxt)) in proof.current.iter().zip(&proof.next).enumerate() { + let own = |c: usize| if i == 0 && c > 0 { 1usize << c } else { 0 }; + assert_eq!(cur.proof.merkle_path.len(), 5 - c_cur + own(c_cur)); + assert_eq!(nxt.proof.merkle_path.len(), 4 - c_next + own(c_next)); + } + run_capped(&fx, &proof, caps, b"whir-round-test") + .unwrap_or_else(|e| panic!("caps ({c_cur}, {c_next}): {e:?}")); + } + } + + /// A cap node no query reaches, flipped. Every + /// per-query check still accepts against the forged cap — shown below — + /// so ONLY the cap-to-root check can refuse it. The error names it. + #[test] + fn a_cap_node_no_query_reaches_is_refused_by_the_cap_check_alone() { + let mut fx = fixture(4, 2, 1); + fx.config.num_queries = 2; + let (d_cur, c_cur, d_next, c_next) = (5usize, 3usize, 4usize, 2usize); + let caps = RoundCaps { + current: c_cur, + current_owner: true, + next: c_next, + }; + let proof = prove(&fx.current, &fx.next, &fx.config, caps, &mut transcript()).unwrap(); + run_capped(&fx, &proof, caps, b"whir-round-test").unwrap(); + let queries = sample_queries::(&mut transcript(), 2, fx.current.num_leaves()); + + // The current tree. + let reached: Vec = queries.iter().map(|q| q >> (d_cur - c_cur)).collect(); + let j = (0..1 << c_cur).find(|j| !reached.contains(j)).unwrap(); + let mut forged = proof.clone(); + forged.current[0].proof.merkle_path[d_cur - c_cur + j][0] ^= 1; + let cap = forged.current[0].proof.merkle_path[d_cur - c_cur..].to_vec(); + for (q, opening) in queries.iter().zip(&forged.current) { + let siblings = &opening.proof.merkle_path[..d_cur - c_cur]; + assert!( + verify_merkle_path_to_cap_from_leaf_hash::( + siblings, + &cap, + d_cur, + *q, + B::hash_data(&opening.values) + ), + "every query must still fold onto the forged cap, or this is not the fixture" + ); + } + assert!(matches!( + run_capped(&fx, &forged, caps, b"whir-round-test"), + Err(Error::CapRejected) + )); + + // The successor tree: leaf `q mod 16`. + let leaves: Vec = queries + .iter() + .map(|q| leaf_and_slot(*q, fx.next.num_leaves()).0) + .collect(); + let reached: Vec = leaves.iter().map(|l| l >> (d_next - c_next)).collect(); + let j = (0..1 << c_next).find(|j| !reached.contains(j)).unwrap(); + let mut forged = proof.clone(); + forged.next[0].proof.merkle_path[d_next - c_next + j][0] ^= 1; + let cap = forged.next[0].proof.merkle_path[d_next - c_next..].to_vec(); + for (leaf, opening) in leaves.iter().zip(&forged.next) { + let siblings = &opening.proof.merkle_path[..d_next - c_next]; + assert!(verify_merkle_path_to_cap_from_leaf_hash::( + siblings, + &cap, + d_next, + *leaf, + B::hash_data(&opening.values) + )); + } + assert!(matches!( + run_capped(&fx, &forged, caps, b"whir-round-test"), + Err(Error::CapRejected) + )); + } + + /// The WHIR analogue of the STARK's exact path-length check: a leaf forged from an + /// INTERNAL node. Under keccak a 64-byte block (eight base values at + /// `k = 3`) is a valid parent input, so values whose bytes are the level-1 + /// node's two children hash to that node, and a path one sibling short + /// then folds to the root — the raw fold accepts it (asserted). At index 0 + /// or all-ones the shifted index bits agree with the true ones, so the + /// fixture needs a statement whose first query lands there. + /// + /// Only the exact-length check can refuse it: without it the Merkle check + /// passes and the round fails later at the FOLD (`FoldInconsistent`), so + /// the `OpeningRejected { query: 0 }` this asserts is the length check's. + #[test] + fn a_leaf_forged_from_an_internal_node_is_refused_by_the_path_length_alone() { + const P: u64 = 0xFFFF_FFFF_0000_0001; + let fx = fixture(4, 2, 3); + let depth = fx.current.depth(); + let leaves = fx.current.num_leaves(); + assert_eq!((depth, leaves), (3, 8)); + let root = fx.current.root(); + + let (label, proof, forged) = (0u64..256) + .find_map(|i| { + let label = format!("m1a-{i}"); + let mut proof = prove( + &fx.current, + &fx.next, + &fx.config, + RoundCaps::default(), + &mut DefaultTranscript::::new(label.as_bytes()), + ) + .ok()?; + let q0 = sample_queries::( + &mut DefaultTranscript::::new(label.as_bytes()), + 1, + leaves, + )[0]; + if q0 != 0 && q0 != leaves - 1 { + return None; + } + let honest = &proof.current[0]; + let leaf = B::hash_data(&honest.values); + let s0 = honest.proof.merkle_path[0]; + let (l, r) = if q0 % 2 == 0 { (leaf, s0) } else { (s0, leaf) }; + let values: Vec = l + .iter() + .chain(r.iter()) + .copied() + .collect::>() + .chunks_exact(8) + .map(|c| u64::from_be_bytes(c.try_into().unwrap())) + .map(|v| (v < P).then(|| FE::from(v))) + .collect::>()?; + assert_eq!( + B::hash_data(&values), + B::hash_new_parent(&l, &r), + "the forged block must hash to the level-1 node" + ); + let forged = CosetOpening { + values, + proof: crypto::merkle_tree::proof::Proof { + merkle_path: honest.proof.merkle_path[1..].to_vec(), + }, + }; + assert!( + crypto::merkle_tree::proof::verify_merkle_path_from_leaf_hash::( + &forged.proof.merkle_path, + &root, + q0, + B::hash_data(&forged.values) + ), + "the raw fold must accept the short path, or the fixture tests nothing" + ); + assert!(!crate::whir_commit::verify_opening::( + &root, depth, q0, &forged + )); + proof.current[0] = forged.clone(); + Some((label, proof, forged)) + }) + .expect("a statement whose first query is 0 or all-ones"); + assert_eq!(forged.proof.merkle_path.len(), depth - 1); + assert!(matches!( + run_capped(&fx, &proof, RoundCaps::default(), label.as_bytes()), + Err(Error::OpeningRejected { query: 0 }) + )); + } + + /// M2: a proof with no openings at all is refused by the count guard, not + /// by a panic on the owner's index. + #[test] + fn a_capped_round_with_no_openings_is_refused_without_panicking() { + let mut fx = fixture(4, 2, 1); + fx.config.num_queries = 3; + let caps = RoundCaps { + current: 2, + current_owner: true, + next: 2, + }; + let mut proof = prove(&fx.current, &fx.next, &fx.config, caps, &mut transcript()).unwrap(); + proof.current.clear(); + proof.next.clear(); + assert!(matches!( + run_capped(&fx, &proof, caps, b"whir-round-test"), + Err(Error::QueryCountMismatch { .. }) + )); + } } diff --git a/crypto/multilinear/tests/host_fallback_counter.rs b/crypto/multilinear/tests/host_fallback_counter.rs index d807a1c75..ad4248228 100644 --- a/crypto/multilinear/tests/host_fallback_counter.rs +++ b/crypto/multilinear/tests/host_fallback_counter.rs @@ -58,6 +58,7 @@ fn every_commit_is_counted_on_exactly_one_side() { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: multilinear::whir_chain::ChainFormat::DEFAULT, }; multilinear::gpu::reset_call_counters(); diff --git a/crypto/multilinear/tests/whir_cap_device.rs b/crypto/multilinear/tests/whir_cap_device.rs new file mode 100644 index 000000000..b6a30ef91 --- /dev/null +++ b/crypto/multilinear/tests/whir_cap_device.rs @@ -0,0 +1,126 @@ +//! W1 end to end on the device: a WHIR chain whose codeword stays on the card +//! (commit, folds, and every opening's tree rebuilt there) proves, under the +//! `Auto` cap, the SAME bytes as the chain over a host-held codeword, and the +//! host verifier accepts it. +//! +//! ```text +//! cargo test --release -p multilinear --features cuda --test whir_cap_device +//! ``` +//! +//! Needs a GPU. The device path is asserted TAKEN (the first commitment's +//! codeword is on the card), so a card that declined could not turn this into +//! a host-against-host comparison. +#![cfg(feature = "cuda")] + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as Ext; +use math::field::goldilocks::GoldilocksField as F; +use multilinear::mle::Mle; +use multilinear::whir::{Domain, encode, lift_coefficients}; +use multilinear::whir_chain::{ + CapPolicy, ChainConfig, ChainFormat, GrindBits, RoundOpenings, commit, prove, verify, +}; +use multilinear::whir_commit::CodewordCommitment; +use multilinear::whir_hash::{KeccakWhir, RpxWhir, WhirHash}; + +type FE = FieldElement; +type EE = FieldElement; + +fn run(cap: CapPolicy) { + // 2^16 evaluations at blowup 4: a 2^18 codeword, above the device commit + // threshold, so the chain's first tree lives on the card. + let num_vars = 16; + let cfg = ChainConfig { + log_blowup: 2, + log_folding: 4, + num_queries: 25, + grind: GrindBits::default(), + format: ChainFormat { + cap, + ..ChainFormat::DEFAULT + }, + }; + let f = Mle::new( + (0..(1u64 << num_vars)) + .map(|i| FE::from(i.wrapping_mul(6364136223846793005).wrapping_add(17) >> 11)) + .collect(), + ) + .unwrap(); + let z: Vec = (0..num_vars).map(|i| EE::from(301 + i as u64)).collect(); + let y = f.evaluate_in(&z).unwrap(); + let tag = format!("{} cap={cap}", H::NAME); + + let (device, domain) = commit::(&f, &cfg, true).unwrap(); + assert!( + device.codeword().device().is_some(), + "{tag}: the commit must have stayed on the card, or this compares the host with itself" + ); + let device_proof = prove::( + &f, + &z, + &device, + &domain, + &cfg, + &mut DefaultTranscript::::new(b"whir-cap-device"), + ) + .unwrap(); + + let host_domain = Domain::::new(num_vars + cfg.log_blowup).unwrap(); + let host = CodewordCommitment::::from_codeword_on_host( + encode::(&lift_coefficients(&f), &host_domain).unwrap(), + cfg.schedule(num_vars)[0], + ) + .unwrap(); + assert_eq!(host.root(), device.root(), "{tag}: roots"); + let host_proof = prove::( + &f, + &z, + &host, + &host_domain, + &cfg, + &mut DefaultTranscript::::new(b"whir-cap-device"), + ) + .unwrap(); + + let a = rkyv::to_bytes::(&device_proof).unwrap(); + let b = rkyv::to_bytes::(&host_proof).unwrap(); + assert_eq!( + a.as_slice(), + b.as_slice(), + "{tag}: the device chain must prove the host chain's bytes" + ); + + // The owner path of tree 0 carries its cap. + let caps = cfg.tree_caps(num_vars); + let depth0 = num_vars + cfg.log_blowup - cfg.schedule(num_vars)[0]; + if let RoundOpenings::Base(p) = &device_proof.rounds[0].openings { + let extra = if caps[0] > 0 { 1usize << caps[0] } else { 0 }; + assert_eq!( + p.current[0].proof.merkle_path.len(), + depth0 - caps[0] + extra + ); + assert_eq!(p.current[1].proof.merkle_path.len(), depth0 - caps[0]); + } else { + panic!("{tag}: round 0 opens base blocks"); + } + + verify::( + &device_proof, + &device.root(), + &z, + y, + &domain, + &cfg, + &mut DefaultTranscript::::new(b"whir-cap-device"), + ) + .unwrap_or_else(|e| panic!("{tag}: the host verifier refused the device proof: {e:?}")); +} + +#[test] +fn the_device_chain_proves_the_host_chains_bytes_under_the_cap() { + for cap in [CapPolicy::Off, CapPolicy::Auto, CapPolicy::Fixed(5)] { + run::(cap); + run::(cap); + } +} diff --git a/crypto/stark/Cargo.toml b/crypto/stark/Cargo.toml index ca11c97ad..87de3725b 100644 --- a/crypto/stark/Cargo.toml +++ b/crypto/stark/Cargo.toml @@ -48,6 +48,9 @@ test-log = { version = "0.2.11", features = ["log"] } bincode = "1" rand = { version = "0.8.5", features = ["std"] } rand_chacha = "0.3.1" +# Digests of the default-format golden proofs (tests::zf_golden_tests); the +# version the `crypto` crate already links. +sha3 = "0.10.8" [features] test-utils = [] diff --git a/crypto/stark/benches/profile_prover.rs b/crypto/stark/benches/profile_prover.rs index f5438877e..a91de425b 100644 --- a/crypto/stark/benches/profile_prover.rs +++ b/crypto/stark/benches/profile_prover.rs @@ -22,6 +22,7 @@ fn main() { coset_offset: 3, grinding_factor: 0, fri_final_poly_log_degree: 7, + format: stark::proof::options::ProofFormat::DEFAULT, }; let num_columns = 16; diff --git a/crypto/stark/benches/prover_benchmark.rs b/crypto/stark/benches/prover_benchmark.rs index c152e7dbb..087019cc2 100644 --- a/crypto/stark/benches/prover_benchmark.rs +++ b/crypto/stark/benches/prover_benchmark.rs @@ -62,6 +62,7 @@ fn benchmark_proof_options() -> ProofOptions { coset_offset: 3, grinding_factor: 0, fri_final_poly_log_degree: 7, + format: stark::proof::options::ProofFormat::DEFAULT, } } diff --git a/crypto/stark/src/device_set.rs b/crypto/stark/src/device_set.rs index 81771fbdd..1ae496ca4 100644 --- a/crypto/stark/src/device_set.rs +++ b/crypto/stark/src/device_set.rs @@ -37,6 +37,21 @@ pub const fn full_tree_bytes(lde_size: u64) -> u64 { lde_size.saturating_sub(1).saturating_mul(MERKLE_NODE_BYTES) } +/// `(2 · leaves − 1) · 32` for the tree over `lde_size` rows with +/// `rows_per_leaf` rows per leaf: [`full_tree_bytes`] at 2 (today's row pair), +/// `(2 · lde − 1) · 32` — twice the leaves, about twice the bytes — at 1 (the +/// S2 one-row tree). +pub const fn tree_bytes_for(lde_size: u64, rows_per_leaf: u64) -> u64 { + if rows_per_leaf <= 1 { + lde_size + .saturating_mul(2) + .saturating_sub(1) + .saturating_mul(MERKLE_NODE_BYTES) + } else { + full_tree_bytes(lde_size) + } +} + /// Bytes of `cols` ext3 columns over `rows` rows. pub const fn ext3_bytes(rows: u64, cols: u64) -> u64 { rows.saturating_mul(cols).saturating_mul(EXT3_BYTES) @@ -86,6 +101,18 @@ pub fn commit_device_set( base_cols: usize, blowup: usize, snapshot: bool, +) -> CommitDeviceSet { + commit_device_set_rpl(n, base_cols, blowup, snapshot, 2) +} + +/// [`commit_device_set`] for a tree with `rows_per_leaf` rows per leaf (2 = +/// row pair, 1 = the S2 one-row tree, whose node buffer is twice as large). +pub fn commit_device_set_rpl( + n: usize, + base_cols: usize, + blowup: usize, + snapshot: bool, + rows_per_leaf: usize, ) -> CommitDeviceSet { let n = n as u64; let cols = base_cols as u64; @@ -93,7 +120,7 @@ pub fn commit_device_set( CommitDeviceSet { lde_bytes: base_bytes(lde, cols), snapshot_bytes: if snapshot { base_bytes(n, cols) } else { 0 }, - tree_bytes: full_tree_bytes(lde), + tree_bytes: tree_bytes_for(lde, rows_per_leaf as u64), scratch_bytes: n .saturating_mul(BASE_BYTES) .saturating_add(INPLACE_TRANSPOSE_SCRATCH_CAP_BYTES), @@ -156,6 +183,17 @@ impl TableDeviceSet { /// Size one table's rounds-2–4 device set for `shape`. pub fn table_device_set(shape: TableShape) -> TableDeviceSet { + table_device_set_rpl(shape, 2) +} + +/// [`table_device_set`] for a table whose trace trees (main, aux, +/// composition) carry `rows_per_leaf` rows per leaf: at 1 (S2) every trace +/// tree is the one-row tree, about twice the node bytes. The FRI trees double +/// their bound too: under one row the chain starts at the LDE itself (the +/// input tree over the DEEP codeword, `lde / 2^{d_0}` leaves), so the layer +/// trees together hold fewer than `2 · lde` nodes, which is +/// [`tree_bytes_for`]`(lde, 1)`. +pub fn table_device_set_rpl(shape: TableShape, rows_per_leaf: usize) -> TableDeviceSet { let TableShape { n, blowup, @@ -164,7 +202,8 @@ pub fn table_device_set(shape: TableShape) -> TableDeviceSet { num_parts, num_eval_points, } = shape; - let main = commit_device_set(n, main_cols, blowup, true); + let main = commit_device_set_rpl(n, main_cols, blowup, true, rows_per_leaf); + let rpl = rows_per_leaf as u64; let (n, k, aux, parts) = ( n as u64, num_eval_points as u64, @@ -177,17 +216,17 @@ pub fn table_device_set(shape: TableShape) -> TableDeviceSet { } else { ext3_bytes(lde, aux) .saturating_add(ext3_bytes(n, aux + 1)) - .saturating_add(full_tree_bytes(lde)) + .saturating_add(tree_bytes_for(lde, rpl)) }; let composition_bytes = if parts == 0 { 0 } else { - ext3_bytes(lde, 1 + parts).saturating_add(full_tree_bytes(lde)) + ext3_bytes(lde, 1 + parts).saturating_add(tree_bytes_for(lde, rpl)) }; let deep_fri_bytes = ext3_bytes(n, k) .saturating_add(ext3_bytes(lde, 1 + k)) .saturating_add(ext3_bytes(lde, 2)) - .saturating_add(full_tree_bytes(lde)); + .saturating_add(tree_bytes_for(lde, rpl)); TableDeviceSet { main, aux_bytes, @@ -249,6 +288,48 @@ mod tests { /// The dispatch layer's row floor (`gpu_lde::DEFAULT_GPU_LDE_THRESHOLD`). const FLOOR: usize = 1 << 14; + /// S2: a one-row tree has twice the leaves, so its node + /// buffer is `(2·lde − 1)·32` against the row pair's `(lde − 1)·32` — + /// +`lde·32` bytes per tree (128 MiB at an LDE of 2^22) — and + /// the table device set grows by that per trace tree plus the FRI bound; + /// the default (`rows_per_leaf = 2`) is the old model exactly. + #[test] + fn a_one_row_tree_doubles_the_node_buffer_and_the_default_is_unchanged() { + let lde: u64 = 1 << 22; + assert_eq!(tree_bytes_for(lde, 2), full_tree_bytes(lde)); + assert_eq!(tree_bytes_for(lde, 1), (2 * lde - 1) * MERKLE_NODE_BYTES); + assert_eq!(tree_bytes_for(lde, 1) - tree_bytes_for(lde, 2), lde * 32); + assert_eq!(tree_bytes_for(lde, 1) - tree_bytes_for(lde, 2), 128 << 20); + + let n = 1usize << 20; + assert_eq!( + commit_device_set_rpl(n, 49, 4, true, 2), + commit_device_set(n, 49, 4, true) + ); + let one = commit_device_set_rpl(n, 49, 4, true, 1); + assert_eq!(one.tree_bytes, tree_bytes_for(lde, 1)); + assert_eq!( + one.total() - commit_device_set(n, 49, 4, true).total(), + lde * 32 + ); + + let shape = TableShape { + n, + blowup: 4, + main_cols: 49, + aux_cols: 13, + num_parts: 2, + num_eval_points: 2, + }; + assert_eq!(table_device_set_rpl(shape, 2), table_device_set(shape)); + // Main, aux and composition trees, and the FRI tree bound: four + // node buffers, each `lde · 32` larger. + assert_eq!( + table_device_set_rpl(shape, 1).total() - table_device_set(shape).total(), + 4 * lde * 32 + ); + } + /// The synthetic over-budget table: 2^22 rows x 612 columns at blowup 2. /// Its LDE alone is 38.25 GiB; with the snapshot and the tree the commit's /// device set is 57.9 GiB against a 25.6 GiB budget. diff --git a/crypto/stark/src/examples/bus_permutation.rs b/crypto/stark/src/examples/bus_permutation.rs new file mode 100644 index 000000000..c4f54291f --- /dev/null +++ b/crypto/stark/src/examples/bus_permutation.rs @@ -0,0 +1,117 @@ +//! A self-balancing LogUp table built on [`AirWithBuses`] — the AIR type every +//! production VM table is — for tests that must prove on the DEVICE. +//! +//! The CUDA composition arm evaluates constraints from the AIR's captured IR +//! (`AIR::constraint_program`). `AirWithBuses` supplies it; the hand-written +//! example AIRs (`LogReadOnlyRAP`, `FibonacciRAP`, …) do not, and the trait's +//! default panics by design. A test whose trace crosses the GPU LDE threshold +//! therefore needs an AIR like this one, whatever it is testing. +//! +//! Layout: four main columns `a, b, c, d` with `b` a permutation of `a` and `d` +//! a permutation of `c`; four interactions (`a` sent and `b` received on one +//! bus, `c` sent and `d` received on another), so the aux trace has one +//! committed term column and the accumulated column. The table balances on its +//! own: its bus contribution is zero, which is what a single-table verify +//! expects. + +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; +use math::field::goldilocks::GoldilocksField; + +use crate::constraints::builder::EmptyConstraints; +use crate::lookup::{ + AirWithBuses, AuxiliaryTraceBuildData, BusInteraction, Multiplicity, + NullBoundaryConstraintBuilder, Packing, +}; +use crate::proof::options::ProofOptions; +use crate::trace::TraceTable; + +type F = GoldilocksField; +type E = Degree3GoldilocksExtensionField; +type FE = FieldElement; + +/// The AIR: no table constraints of its own, the LogUp ones from the framework. +pub type BusPermutationAir = + AirWithBuses; + +const BUS_AB: u64 = 1; +const BUS_CD: u64 = 2; + +/// The AIR under `options`. +pub fn bus_permutation_air(options: &ProofOptions) -> BusPermutationAir { + let one = |col: usize| Packing::Direct.columns(&[col]); + AirWithBuses::new( + 4, + AuxiliaryTraceBuildData { + interactions: vec![ + BusInteraction::sender(BUS_AB, Multiplicity::One, one(0)), + BusInteraction::receiver(BUS_AB, Multiplicity::One, one(1)), + BusInteraction::sender(BUS_CD, Multiplicity::One, one(2)), + BusInteraction::receiver(BUS_CD, Multiplicity::One, one(3)), + ], + }, + options, + 1, + EmptyConstraints, + ) +} + +/// A `rows`-row trace (`rows` a power of two, at least 2): `b` is `a` +/// reversed, `d` is `c` rotated by one row. +pub fn bus_permutation_trace(rows: usize) -> TraceTable { + assert!( + rows.is_power_of_two() && rows >= 2, + "rows must be a power of two ≥ 2" + ); + let a: Vec = (0..rows as u64).map(|i| FE::from(i + 1)).collect(); + let b: Vec = a.iter().rev().cloned().collect(); + let c: Vec = (0..rows as u64) + .map(|i| FE::from((i * 7919) % 4099 + 1)) + .collect(); + let d: Vec = (0..rows).map(|i| c[(i + 1) % rows]).collect(); + TraceTable::from_columns_main(vec![a, b, c, d], 1) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::prover::{IsStarkProver, Prover}; + use crate::traits::AIR; + use crate::verifier::{IsStarkVerifier, Verifier}; + use crypto::fiat_shamir::default_transcript::DefaultTranscript; + + /// The two properties the device tests rely on: the AIR hands out a + /// constraint program (the CUDA composition arm's input), and an honest + /// trace proves and verifies as a single table (the bus balances to zero). + #[test] + fn the_bus_permutation_table_round_trips_and_has_a_constraint_program() { + let options = ProofOptions::default_test_options(); + let air = bus_permutation_air(&options); + assert!(!air.constraint_program().nodes.is_empty()); + let mut trace = bus_permutation_trace(64); + let proof = Prover::prove(&air, &mut trace, &(), &mut DefaultTranscript::::new(&[])) + .expect("an honest trace proves"); + assert!(proof.lde_trace_aux_merkle_root.is_some(), "a LogUp table"); + assert!(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]) + )); + } + + /// Non-vacuity of the balance: `d` no longer a permutation of `c` makes the + /// table's bus contribution non-zero, and the single-table verify refuses. + #[test] + fn an_unbalanced_bus_permutation_table_is_rejected() { + let options = ProofOptions::default_test_options(); + let air = bus_permutation_air(&options); + let mut trace = bus_permutation_trace(64); + trace.set_main(5, 3, FE::from(999_999u64)); + let rejected = + match Prover::prove(&air, &mut trace, &(), &mut DefaultTranscript::::new(&[])) { + Err(_) => true, + Ok(proof) => !Verifier::verify(&proof, &air, &mut DefaultTranscript::::new(&[])), + }; + assert!(rejected, "an unbalanced table must not verify"); + } +} diff --git a/crypto/stark/src/examples/mod.rs b/crypto/stark/src/examples/mod.rs index 770540e83..14c4d001b 100644 --- a/crypto/stark/src/examples/mod.rs +++ b/crypto/stark/src/examples/mod.rs @@ -1,3 +1,4 @@ +pub mod bus_permutation; pub mod dummy_air; pub mod fibonacci_2_cols_shifted; pub mod fibonacci_2_columns; diff --git a/crypto/stark/src/examples/read_only_memory_logup.rs b/crypto/stark/src/examples/read_only_memory_logup.rs index 9068e7276..ef4696201 100644 --- a/crypto/stark/src/examples/read_only_memory_logup.rs +++ b/crypto/stark/src/examples/read_only_memory_logup.rs @@ -3,13 +3,15 @@ //! use std::marker::PhantomData; +use std::sync::OnceLock; use crate::{ + constraint_ir::ConstraintProgram, constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, builder::{ - ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, num_base_from_meta, - run_transition_prover, run_transition_verifier, + CaptureBuilder, ConstraintBuilder, ConstraintMeta, ConstraintSet, RowDomain, + num_base_from_meta, run_transition_prover, run_transition_verifier, }, }, context::AirContext, @@ -96,6 +98,11 @@ where { context: AirContext, meta: Vec, + /// The captured IR of [`LogReadOnlyRAPConstraints`], built on first use by + /// [`AIR::constraint_program`] (the CUDA composition arm needs it once main + /// and aux are device-resident). Same body as the folders, so the device + /// evaluates the same polynomials as the CPU path. + program: OnceLock>, phantom: PhantomData<(F, E)>, } @@ -148,6 +155,7 @@ where Self { context, meta, + program: OnceLock::new(), phantom: PhantomData, } } @@ -279,6 +287,16 @@ where num_base_from_meta(&ConstraintSet::::meta(&LogReadOnlyRAPConstraints)) } + fn constraint_program(&self) -> &ConstraintProgram { + // Prover/GPU/tests only (the verify path never calls this): capture + // the single constraint body once. + self.program.get_or_init(|| { + let mut cb = CaptureBuilder::::new(); + LogReadOnlyRAPConstraints.eval(&mut cb); + cb.finish(num_base_from_meta(&self.meta)).0 + }) + } + fn context(&self) -> &AirContext { &self.context } diff --git a/crypto/stark/src/fri/capture.rs b/crypto/stark/src/fri/capture.rs new file mode 100644 index 000000000..b875c954b --- /dev/null +++ b/crypto/stark/src/fri/capture.rs @@ -0,0 +1,75 @@ +//! Test-only capture of the verifier's FRI challenges and DEEP values, for the +//! exported test vectors (`tests/vectors/zf_fri`, the README's (d)): a vector +//! carries a proof AND the ζ, ι and DEEP values a correct verifier derives +//! from it, so the device prover and the in-guest verifier can check each stage separately. +//! +//! Compiled only for tests and the `test-utils` feature. Thread-local: the +//! host verifier is sequential on the calling thread, so [`capture`] sees +//! exactly the verification it wraps. + +use core::any::Any; +use core::cell::RefCell; +use std::vec::Vec; + +use math::field::element::FieldElement; +use math::field::traits::IsField; + +/// What one table's verification derived: ζ (every folding challenge), ι +/// (the query pair indices) and the DEEP values p₀(υ), p₀(−υ) per query. +#[derive(Clone, Debug)] +pub struct FriCapture { + pub zetas: Vec>, + pub iotas: Vec, + pub deep: Vec>, + pub deep_sym: Vec>, +} + +thread_local! { + static ACTIVE: RefCell>>> = const { RefCell::new(None) }; +} + +/// Run `f` (a verification) and return its result with one record per table +/// verified, in order. Records are `FriCapture` for the proof's extension +/// field; downcast with [`FriCapture::from_any`]. +pub fn capture(f: impl FnOnce() -> T) -> (T, Vec>) { + ACTIVE.with(|a| *a.borrow_mut() = Some(Vec::new())); + let out = f(); + let records = ACTIVE.with(|a| a.borrow_mut().take()).unwrap_or_default(); + (out, records) +} + +impl FriCapture { + pub fn from_any(record: &dyn Any) -> Option<&Self> { + record.downcast_ref::() + } +} + +/// Start a table's record with its challenges (called after the replay). +pub(crate) fn record_challenges(zetas: &[FieldElement], iotas: &[usize]) { + ACTIVE.with(|a| { + if let Some(records) = a.borrow_mut().as_mut() { + records.push(Box::new(FriCapture:: { + zetas: zetas.to_vec(), + iotas: iotas.to_vec(), + deep: Vec::new(), + deep_sym: Vec::new(), + })); + } + }); +} + +/// Add the DEEP values to the current table's record. +pub(crate) fn record_deep( + deep: &[FieldElement], + deep_sym: &[FieldElement], +) { + ACTIVE.with(|a| { + if let Some(records) = a.borrow_mut().as_mut() + && let Some(last) = records.last_mut() + && let Some(rec) = last.downcast_mut::>() + { + rec.deep = deep.to_vec(); + rec.deep_sym = deep_sym.to_vec(); + } + }); +} diff --git a/crypto/stark/src/fri/device_parity.rs b/crypto/stark/src/fri/device_parity.rs new file mode 100644 index 000000000..b7834ed53 --- /dev/null +++ b/crypto/stark/src/fri/device_parity.rs @@ -0,0 +1,443 @@ +//! Device-vs-host parity for the FRI commit and query phases under a proof +//! format's fold layout — the S3 group encoding and today's pair encoding. +//! +//! Compiled for `cuda` builds with tests or `test-utils`; every entry needs a +//! GPU and a lowered `LAMBDA_VM_GPU_LDE_THRESHOLD` (the device commit admits +//! only LDEs at or above it), so the callers are `#[ignore]`d box tests. The +//! stark crate instantiates them under Keccak and Blake3, the prover crate +//! under the production RPX pin (`tests::zf_rpx_device_tests`). +//! +//! What one [`fri_parity`] call pins, device against the host CPU loop +//! (`commit_phase_cpu_with_layout` / the host query walks) over one random +//! codeword and one transcript: +//! - the terminal coefficients, every committed layer's root, and (when the +//! device drained them) every layer's evaluations; +//! - the transcript after the commit phase (one more sampled element); +//! - per query (random pair indices plus both ends of the range) every +//! opened value — the whole group under the group encoding, the sibling +//! under the pair one — and every authentication path. + +use std::format; +use std::string::String; +use std::vec::Vec; + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use crypto::fiat_shamir::is_transcript::IsTranscript; +use crypto::merkle_tree::cap::CapPolicy; +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; +use math::field::goldilocks::GoldilocksField; + +use crate::config::StarkHash; +use crate::fri::fri_commitment::FriLayer; +use crate::fri::fri_decommit::FriDecommitment; +use crate::fri::fri_functions::compute_coset_twiddles_inv; +use crate::fri::schedule::{FRI_SCHEDULE_DMAX, fri_chain_start, fri_schedule}; +use crate::fri::terminal::FriFoldLayout; +use crate::fri::vectors::splitmix64; +use crate::proof::options::{FriMode, FriScheduleOverride, OneRowMode, ProofFormat, ProofOptions}; + +type F = GoldilocksField; +type E = Degree3GoldilocksExtensionField; +type Felt = FieldElement; +type Ext = FieldElement; + +/// Uneven and extreme shapes the DP does not pick at production sizes but the +/// format admits (override): `d = 6` (DMAX, a 64-value group, a multi-block +/// leaf under every hash), and unequal neighbours (a fold-count off-by-one +/// between the commit and the pending folds shows only there). +pub const EXTRA_SHAPES: &[&[u8]] = &[ + // A lone 16-group layer: a DP schedule until the objective priced every + // emitted row, kept so the sweep's coverage does not shrink. + &[4], + &[6], + &[1, 6], + &[6, 1], + &[3, 1, 3], + &[1, 3], + &[2, 5, 1], + &[1, 1, 1], +]; + +/// Every distinct fold schedule the DP produces for an LDE of `2^B`, `B ≤ 23` +/// (the S3 chain from `B − 1`), at terminal logs 4 (the vectors), 9 (base +/// legs) and 10 (LFM proofs), 3 and 110 queries, cap off and auto; then +/// [`EXTRA_SHAPES`]. Empty schedules (nothing committed) are skipped: the +/// device commit declines them before sampling. +pub fn dp_shapes() -> Vec> { + let mut out: Vec> = Vec::new(); + for t in [4u32, 9, 10] { + for q in [3u64, 110] { + for cap in [CapPolicy::Off, CapPolicy::Auto] { + for b in 2..=23u32 { + let b0 = fri_chain_start(b, false); + let s = fri_schedule(b0, t.min(b), q, cap, FRI_SCHEDULE_DMAX); + if !s.is_empty() && !out.contains(&s) { + out.push(s); + } + } + } + } + } + for s in EXTRA_SHAPES { + if !out.iter().any(|x| x.as_slice() == *s) { + out.push(s.to_vec()); + } + } + out +} + +/// Options for a group-encoded (dp) proof at blowup `2^blowup_log`, terminal +/// `k`, `queries` queries and cap policy `cap` (which the DP's objective +/// reads), with an optional explicit schedule. +pub fn dp_options( + blowup_log: u32, + k: u8, + queries: usize, + cap: CapPolicy, + schedule: Option<&[u8]>, +) -> ProofOptions { + ProofOptions { + blowup_factor: 1u8 << blowup_log, + fri_number_of_queries: queries, + coset_offset: 3, + grinding_factor: 0, + fri_final_poly_log_degree: k, + format: ProofFormat { + merkle_cap: cap, + fri_mode: FriMode::Dp, + fri_schedule_override: schedule + .map(|s| FriScheduleOverride::new(s).expect("override fits")), + ..ProofFormat::DEFAULT + }, + } +} + +/// Today's options (pair encoding) at the same shape parameters. +pub fn pair_options(blowup_log: u32, k: u8) -> ProofOptions { + ProofOptions { + blowup_factor: 1u8 << blowup_log, + fri_number_of_queries: 3, + coset_offset: 3, + grinding_factor: 0, + fri_final_poly_log_degree: k, + format: ProofFormat::DEFAULT, + } +} + +/// The smallest `(lde_log, options)` whose layout at blowup 2, `k = 1` (so a +/// terminal of 4) has exactly `schedule` as its committed folds. +pub fn smallest_case(schedule: &[u8]) -> (u32, ProofOptions) { + let sum: u32 = schedule.iter().map(|&d| u32::from(d)).sum(); + // b0 = lde_log − 1 = terminal_log + Σd, terminal_log = 1 + 1. + (sum + 3, dp_options(1, 1, 3, CapPolicy::Off, Some(schedule))) +} + +fn raw(v: &[Ext]) -> Vec<[u64; 3]> { + v.iter() + .map(|e| { + let c = e.value(); + [c[0].canonical(), c[1].canonical(), c[2].canonical()] + }) + .collect() +} + +/// Device-vs-host parity of the FRI commit and query phases under +/// `options`' fold layout for a random codeword of `2^lde_log` values. +/// `resident` keeps the device layers' evals resident only (the device-only +/// envelope's shape), so the device query phase gathers them on device. +/// +/// `options.format.one_row == On` runs the S2 layout: layer 0 is +/// the input tree committed from the codeword itself before any challenge, +/// and the query indexes range over the whole LDE (`Auto` is resolved per +/// table from an AIR, so it is not a codeword-level case: treated as off). +/// +/// `Err` names the first mismatch, or the device declining (threshold, +/// budget, a wiring gate) — never a silent pass. +pub fn fri_parity( + lde_log: u32, + options: &ProofOptions, + resident: bool, + seed: u64, +) -> Result { + let blowup_log = options.blowup_factor.trailing_zeros(); + let k = u32::from(options.fri_final_poly_log_degree); + let one_row = options.format.one_row == OneRowMode::On; + let layout = FriFoldLayout::for_options(lde_log, blowup_log, options, one_row) + .map_err(|e| format!("layout: {e}"))?; + let n = 1usize << lde_log; + let mut rng = seed; + let evals: Vec = (0..n) + .map(|_| { + Ext::new([ + Felt::from(splitmix64(&mut rng)), + Felt::from(splitmix64(&mut rng)), + Felt::from(splitmix64(&mut rng)), + ]) + }) + .collect(); + let offset = Felt::from(options.coset_offset); + let tw = compute_coset_twiddles_inv::(&offset, n); + let t0 = DefaultTranscript::::new(&seed.to_le_bytes()); + + let mut t_cpu = t0.clone(); + let (cpu_coeffs, cpu_layers) = crate::fri::commit_phase_cpu_with_layout::( + evals.clone(), + &mut t_cpu, + &offset, + n, + blowup_log, + k, + &layout, + &tw, + ); + + let mut t_gpu = t0.clone(); + let device = if resident { + crate::gpu_lde::try_fri_commit_gpu_resident::>( + &evals, &mut t_gpu, &offset, n, blowup_log, k, &layout, &tw, + ) + } else { + crate::gpu_lde::try_fri_commit_gpu::>( + &evals, &mut t_gpu, &offset, n, blowup_log, k, &layout, &tw, + ) + }; + let (gpu_coeffs, gpu_layers) = device.ok_or_else(|| { + format!( + "the device FRI commit declined at LDE 2^{lde_log} (schedule {:?}): \ + run with a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD <= {n}", + layout.schedule + ) + })?; + + let what = format!( + "LDE 2^{lde_log}, schedule {:?}, legacy {}, one_row {one_row}, resident {resident}", + layout.schedule, + layout.is_legacy() + ); + if gpu_coeffs != cpu_coeffs { + return Err(format!("{what}: terminal coefficients differ")); + } + if gpu_layers.len() != cpu_layers.len() || cpu_layers.len() != layout.num_committed { + return Err(format!( + "{what}: {} device layers, {} host layers", + gpu_layers.len(), + cpu_layers.len() + )); + } + for (j, (g, c)) in gpu_layers.iter().zip(&cpu_layers).enumerate() { + if g.merkle_tree.root != c.merkle_tree.root { + return Err(format!("{what}: layer {j} root differs")); + } + if g.gpu_tree.as_ref().map(|t| t.root) != Some(c.merkle_tree.root) { + return Err(format!("{what}: layer {j} resident tree root differs")); + } + if resident { + if !g.evaluation.is_empty() || g.gpu_evals.is_none() { + return Err(format!("{what}: layer {j} is not device-only")); + } + } else if raw(&g.evaluation) != raw(&c.evaluation) { + return Err(format!("{what}: layer {j} evaluations differ")); + } + } + let (after_cpu, after_gpu): (Ext, Ext) = + (t_cpu.sample_field_element(), t_gpu.sample_field_element()); + if after_cpu != after_gpu { + return Err(format!("{what}: the transcripts diverged")); + } + + // Queries: random indices and both ends of the range — pair indices below + // `N / 2`, or (one row) trace leaves over the whole LDE. + let bound = if one_row { n } else { n / 2 }; + let mut iotas: Vec = (0..40) + .map(|_| (splitmix64(&mut rng) % bound as u64) as usize) + .collect(); + iotas.push(0); + iotas.push(bound - 1); + let (cpu_q, gpu_q) = queries::(&cpu_layers, &gpu_layers, &iotas, &layout); + let gpu_q = gpu_q.ok_or_else(|| format!("{what}: the device query phase declined"))?; + for (q, (a, b)) in cpu_q.iter().zip(&gpu_q).enumerate() { + if raw(&a.layers_evaluations_sym) != raw(&b.layers_evaluations_sym) { + return Err(format!( + "{what}: query {q} (iota {}) values differ", + iotas[q] + )); + } + if a.layers_auth_paths.len() != b.layers_auth_paths.len() + || a.layers_auth_paths + .iter() + .zip(&b.layers_auth_paths) + .any(|(x, y)| x.merkle_path != y.merkle_path) + { + return Err(format!( + "{what}: query {q} (iota {}) paths differ", + iotas[q] + )); + } + if a.layers_evaluations_sym.len() != layout.opened_values_per_query() { + return Err(format!("{what}: query {q} opens the wrong value count")); + } + } + Ok(format!( + "{what}: {} layers, {} queries equal", + layout.num_committed, + iotas.len() + )) +} + +#[allow(clippy::type_complexity)] +fn queries( + cpu: &[FriLayer>], + gpu: &[FriLayer>], + iotas: &[usize], + layout: &FriFoldLayout, +) -> (Vec>, Option>>) { + if layout.is_legacy() { + // Host layers carry no device tree, so `query_phase` walks them on + // the host; the device layers take the device gather. + ( + crate::fri::query_phase::(cpu, iotas), + crate::gpu_lde::try_fri_query_phase_gpu::>(gpu, iotas), + ) + } else { + ( + crate::fri::query_phase_groups_host::(cpu, iotas, layout), + crate::gpu_lde::try_fri_query_phase_gpu_groups::>(gpu, iotas, layout), + ) + } +} + +/// One parity case: an LDE log and the options whose layout it runs under. +pub type Case = (u32, ProofOptions); + +/// The shape sweep: every [`dp_shapes`] entry at its [`smallest_case`]. +pub fn sweep_cases() -> Vec { + dp_shapes().iter().map(|s| smallest_case(s)).collect() +} + +/// Production sizes at the DP's own schedules: base legs (blowup 4, k = 7, +/// T = 9) at B = 14, 19, 21, 23 and an LFM-shaped proof (k = 8, T = 10) at +/// B = 22, 110 queries, cap auto (the objective the DP prices); and today's +/// pair encoding at B = 21. +pub fn production_cases() -> Vec { + let mut cases: Vec = [14u32, 19, 21, 23] + .iter() + .map(|&b| (b, dp_options(2, 7, 110, CapPolicy::Auto, None))) + .collect(); + cases.push((22, dp_options(2, 8, 110, CapPolicy::Auto, None))); + cases.push((21, pair_options(2, 7))); + cases +} + +/// Device-only layers (no host copy of any layer's evals, so the query phase +/// gathers the groups off the resident evals): uneven shapes, the DMAX group, +/// a DP schedule at B = 16, and today's encoding. +pub fn resident_cases() -> Vec { + let mut cases: Vec = [&[3u8, 1, 3][..], &[6, 1], &[1, 6]] + .iter() + .map(|s| smallest_case(s)) + .collect(); + cases.push((16, dp_options(2, 7, 110, CapPolicy::Auto, None))); + cases.push((14, pair_options(2, 7))); + cases +} + +/// Today's encoding through the layout-taking drive: one committed layer and +/// up, blowup 2 and 4. +pub fn legacy_cases() -> Vec { + let mut cases: Vec = [4u32, 5, 8, 12] + .iter() + .map(|&b| (b, pair_options(1, 1))) + .collect(); + cases.extend([10u32, 16].iter().map(|&b| (b, pair_options(2, 3)))); + cases +} + +/// `options` with one-row openings on (S2): the FRI chain starts at the LDE +/// itself and layer 0 is the input tree. +pub fn with_one_row(mut options: ProofOptions) -> ProofOptions { + options.format.one_row = OneRowMode::On; + options +} + +/// The smallest `(lde_log, options)` whose ONE-ROW layout at blowup 2, `k = 1` +/// (a terminal of 4) has exactly `schedule` as its committed folds (the chain +/// starts at the LDE, so one bit shorter than [`smallest_case`]). +pub fn smallest_one_row_case(schedule: &[u8]) -> (u32, ProofOptions) { + let sum: u32 = schedule.iter().map(|&d| u32::from(d)).sum(); + ( + sum + 2, + with_one_row(dp_options(1, 1, 3, CapPolicy::Off, Some(schedule))), + ) +} + +/// S2 on the device: every [`dp_shapes`] entry at its +/// [`smallest_one_row_case`] (d_0 = the input tree's group), today's pair +/// schedule with one-row openings (group encoding at d = 1) at blowup 2 and +/// 4, and production sizes at the DP's own one-row schedules (base legs at +/// B = 14, 19, 21, 23, an LFM-shaped B = 22; Q = 110, cap auto). +pub fn one_row_cases() -> Vec { + let mut cases: Vec = dp_shapes() + .iter() + .map(|s| smallest_one_row_case(s)) + .collect(); + cases.extend( + [4u32, 5, 8, 12] + .iter() + .map(|&b| (b, with_one_row(pair_options(1, 1)))), + ); + cases.extend( + [10u32, 16] + .iter() + .map(|&b| (b, with_one_row(pair_options(2, 3)))), + ); + cases.extend([14u32, 19, 21, 23].iter().map(|&b| { + ( + b, + with_one_row(dp_options(2, 7, 110, CapPolicy::Auto, None)), + ) + })); + cases.push(( + 22, + with_one_row(dp_options(2, 8, 110, CapPolicy::Auto, None)), + )); + cases +} + +/// S2 device-only layers: the input tree's evals ARE the resident codeword, +/// so the query phase gathers layer 0's groups off it. +pub fn one_row_resident_cases() -> Vec { + let mut cases: Vec = [&[3u8, 1, 3][..], &[6, 1], &[1, 6]] + .iter() + .map(|s| smallest_one_row_case(s)) + .collect(); + cases.push(( + 16, + with_one_row(dp_options(2, 7, 110, CapPolicy::Auto, None)), + )); + cases.push((14, with_one_row(pair_options(2, 7)))); + cases +} + +/// Run [`fri_parity`] over `cases` (seeds `seed_base + i`), printing one +/// `FRIDEV` line per case; `Err` lists every failing case. +pub fn run_cases( + name: &str, + cases: &[Case], + resident: bool, + seed_base: u64, +) -> Result> { + let mut failures = Vec::new(); + for (i, (b, opts)) in cases.iter().enumerate() { + match fri_parity::(*b, opts, resident, seed_base + i as u64) { + Ok(msg) => std::println!("FRIDEV {name} {msg}"), + Err(e) => failures.push(e), + } + } + if failures.is_empty() { + std::println!("FRIDEV {name}: {} cases equal", cases.len()); + Ok(cases.len()) + } else { + Err(failures) + } +} diff --git a/crypto/stark/src/fri/group.rs b/crypto/stark/src/fri/group.rs new file mode 100644 index 000000000..310b202fd --- /dev/null +++ b/crypto/stark/src/fri/group.rs @@ -0,0 +1,232 @@ +//! Group-leaf FRI layers (S3): a committed layer of fold exponent `d` groups +//! `2^d` consecutive bit-reversed evaluations per leaf. +//! +//! # Why a group is a coset, and how it folds +//! +//! A layer of length `n = 2^b` on the coset `o_b·⟨ω_n⟩` stores, at position +//! `p`, the value at `o_b·ω_n^{br_b(p)}`. Bit reversal over `b` bits moves the +//! low `d` bits of `p = g·2^d + t` to the top, so the group of leaf `g` holds +//! the full fiber `x_g·⟨ω_{2^d}⟩` of `x ↦ x^{2^d}`, `x_g = o_b·ω_n^{br_{b−d}(g)}`, +//! in bit-reversed order (`point(g·2^d + t) = x_g·ω_{2^d}^{br_d(t)}`), and +//! `x_g^{2^d}` is the point at position `g` of the layer folded `d` times. +//! +//! The prover folds a committed layer with ONE challenge `ζ` as `d` successive +//! binary folds with `ζ, ζ², …, ζ^{2^{d−1}}` (the unchanged binary fold, so the +//! arity-`2^d` fold `2^d·Σ ζ^i f_i` of Haböck 2022/1216 eq. (3)). The verifier +//! runs the same `d` levels on the group alone ([`group_fold`]): at level `ℓ` +//! the pair `(2j, 2j+1)` sits at `(X, −X)`, +//! `X = x_g^{2^ℓ}·ω_{2^{d−ℓ}}^{br_{d−ℓ−1}(j)}`, and +//! `u'_j = (u_{2j} + u_{2j+1}) + ζ^{2^ℓ}·X⁻¹·(u_{2j} − u_{2j+1})`, exactly the +//! prover's `fold_evaluations_in_place` restricted to one fiber. +//! +//! # What a query checks per layer (the load-bearing checks) +//! +//! 1. the group is the leaf: hashed in full (`H::Batched` over the `2^d` +//! values) and authenticated against the layer root at `leaf = p >> d`, +//! with the exact path length; +//! 2. the slot check `group[p & (2^d − 1)] == v` — the round-consistency check +//! tying this layer to the value the previous fold produced; +//! 3. the group fold with `ζ_j` gives the value at `p >> d` of the next layer. +//! +//! Dropping 1 or 2 is a soundness break; `fri_group_tests` has a named test +//! that turns red for each (M1, M2). + +use crypto::merkle_tree::traits::IsStreamingLeafBackend; +use math::fft::bit_reversing::reverse_index; +use math::field::element::FieldElement; +use math::field::traits::{IsFFTField, IsField, IsSubFieldOf}; +use math::traits::AsBytes; + +use crate::config::Commitment; +use crate::fri::terminal::FriFoldLayout; +use crate::merkle_caps::TreeCheck; + +/// Verifier mutations for the load-bearing tests (M1, M2). Test builds only; +/// production has no switch. Thread-local: the host verifier is sequential, +/// so a test that sets one affects only its own verification. +#[cfg(test)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum GroupMutation { + None, + /// M1: skip `group[slot] == v`. + SkipSlotCheck, + /// M2: skip the group's Merkle authentication. + SkipLeafAuth, +} + +#[cfg(test)] +thread_local! { + pub(crate) static GROUP_MUTATION: core::cell::Cell = + const { core::cell::Cell::new(GroupMutation::None) }; +} + +#[inline] +fn mutated(_m: u8) -> bool { + #[cfg(test)] + { + let m = match _m { + 1 => GroupMutation::SkipSlotCheck, + _ => GroupMutation::SkipLeafAuth, + }; + GROUP_MUTATION.with(|c| c.get() == m) + } + #[cfg(not(test))] + { + false + } +} + +/// `ω_{2^d}^t` for `t < 2^d`, `ω_{2^d}` the field's primitive `2^d`-th root — +/// the same root the LDE domain's `ω_N^{N/2^d}` is (both are powers of the +/// field's two-adic generator; `fri_group_tests::group_leaf_is_a_coset` checks +/// it against the prover's own domain). +pub(crate) fn roots_of_unity_table(d: u32) -> Option>> { + let w = F::get_primitive_root_of_unity(u64::from(d)).ok()?; + let n = 1usize << d; + let mut out = Vec::with_capacity(n); + let mut acc = FieldElement::::one(); + for _ in 0..n { + out.push(acc.clone()); + acc = &acc * &w; + } + Some(out) +} + +/// The group fold (see the module docs): `d = log2(group.len())` binary folds +/// of the `2^d` values of one leaf with `ζ, ζ², …`, given `x_g⁻¹` (the inverse +/// of the leaf's coset base) and `roots = roots_of_unity_table(d)`. Returns +/// the value at the leaf's position in the layer folded `d` times. +pub(crate) fn group_fold( + group: &[FieldElement], + zeta: &FieldElement, + x_g_inv: &FieldElement, + roots: &[FieldElement], +) -> FieldElement +where + F: IsField + IsSubFieldOf, + E: IsField, +{ + let n = group.len(); + debug_assert!(n.is_power_of_two() && roots.len() == n); + let d = n.trailing_zeros(); + let mut vals = group.to_vec(); + let mut xinv = x_g_inv.clone(); + let mut z = zeta.clone(); + for level in 0..d { + let half = vals.len() / 2; + // Pair j of this level: X⁻¹ = x_g^{−2^ℓ} · ω_{2^d}^{−2^ℓ·br_{d−ℓ−1}(j)}. + for j in 0..half { + let br = if half > 1 { + reverse_index(j, half as u64) + } else { + 0 + }; + // 2^ℓ·br < 2^{d−1} < n: already reduced. + let e = br << level; + let c = &roots[(n - e) % n]; + let x_inv_j = &xinv * c; + let lo = &vals[2 * j]; + let hi = &vals[2 * j + 1]; + let sum = lo + hi; + let diff = lo - hi; + vals[j] = &sum + &(&x_inv_j * &(&z * &diff)); + } + vals.truncate(half); + xinv = xinv.square(); + z = z.square(); + } + vals.swap_remove(0) +} + +/// The FRI checks of one query under a group-encoded layout (every format but +/// the legacy one): per committed layer `j`, the group is authenticated at +/// `leaf = p >> d_j` by `checks[j]` — the layer tree's check, built once per +/// tree at the layout's depth with its Merkle cap (`TreeCheck`; exact path +/// length `depth − c`, query 0 the cap's owner) — with path `paths(j)`, the +/// slot check `group[p & (2^{d_j} − 1)] == v` holds, and `v` becomes the group +/// fold with layer `j`'s challenge; finally `terminal[p] == v`. +/// +/// Layer `j`'s challenge is `zetas[j + 1]` for row pairs (`zetas[0]` drove the +/// uncommitted fold 0) and `zetas[j]` under one row (layer 0 is the committed +/// DEEP codeword, so no fold precedes it) — [`FriFoldLayout::num_zetas`]. +/// +/// * `v` / `y_inv`: the query's value at committed layer 0 and the inverse of +/// its point there (row pairs: fold 0 already applied by the caller; one +/// row: the DEEP value at `x_r` and `x_r⁻¹` — the layer-0 slot check is then +/// the input-slot check `group₀[slot] == DEEP(x_r)`); +/// * `query`: the query's position in proof order (query 0 is every capped +/// layer's owner opening); +/// * `iota`: the query's position in committed layer 0; +/// * `values`: the flat per-query group values (the proof's +/// `layers_evaluations_sym` under this encoding), length already checked by +/// the caller to be `layout.opened_values_per_query()`; +/// * `roots_tables[d]`: `roots_of_unity_table(d)` for every `d` in the schedule. +#[allow(clippy::too_many_arguments)] +pub(crate) fn verify_query_groups<'p, F, E, B>( + layout: &FriFoldLayout, + checks: &[TreeCheck<'_>], + query: usize, + paths: impl Fn(usize) -> &'p [Commitment], + values: &[FieldElement], + zetas: &[FieldElement], + iota: usize, + mut v: FieldElement, + mut y_inv: FieldElement, + terminal_codeword: &[FieldElement], + roots_tables: &[Vec>], +) -> bool +where + F: IsFFTField + IsSubFieldOf, + E: IsField, + FieldElement: AsBytes + Sync + Send, + B: IsStreamingLeafBackend, +{ + if checks.len() != layout.num_committed + || values.len() != layout.opened_values_per_query() + || zetas.len() != layout.num_zetas() + { + return false; + } + let zeta_offset = usize::from(!layout.one_row); + let mut index = iota; + let mut offset = 0usize; + let mut ok = true; + for (j, &d) in layout.schedule.iter().enumerate() { + let d = u32::from(d); + let n = 1usize << d; + let group = &values[offset..offset + n]; + offset += n; + let leaf = index >> d; + let slot = index & (n - 1); + + // (2) the slot check. + if group[slot] != v && !mutated(1) { + ok = false; + } + // (1) the group is the leaf, authenticated with the exact depth. + let leaf_hash = B::hash_data_from_slices(group, &[]); + if !checks[j].verify::(query, paths(j), leaf, leaf_hash) && !mutated(2) { + ok = false; + } + // (3) fold: x_g⁻¹ = y⁻¹ · ω_{2^d}^{br_d(slot)}. + let Some(table) = roots_tables.get(d as usize) else { + return false; + }; + if table.len() != n { + return false; + } + let br_slot = if n > 1 { + reverse_index(slot, n as u64) + } else { + 0 + }; + let x_g_inv = &y_inv * &table[br_slot]; + v = group_fold::(group, &zetas[j + zeta_offset], &x_g_inv, table); + for _ in 0..d { + y_inv = y_inv.square(); + } + index = leaf; + } + let terminal_ok = terminal_codeword.get(index).is_some_and(|t| &v == t); + ok & terminal_ok +} diff --git a/crypto/stark/src/fri/mod.rs b/crypto/stark/src/fri/mod.rs index 0458b9b93..fb79b9d70 100644 --- a/crypto/stark/src/fri/mod.rs +++ b/crypto/stark/src/fri/mod.rs @@ -1,7 +1,15 @@ +#[cfg(any(test, feature = "test-utils"))] +pub mod capture; +#[cfg(all(feature = "cuda", any(test, feature = "test-utils")))] +pub mod device_parity; pub mod fri_commitment; pub mod fri_decommit; pub(crate) mod fri_functions; +pub(crate) mod group; +pub mod schedule; pub(crate) mod terminal; +#[cfg(any(test, feature = "test-utils"))] +pub mod vectors; use crypto::fiat_shamir::is_transcript::IsStarkTranscript; use crypto::merkle_tree::merkle::MerkleTree; @@ -10,10 +18,13 @@ use math::field::traits::{IsFFTField, IsField, IsSubFieldOf}; use math::traits::AsBytes; use crate::config::StarkHash; +use crate::fri::terminal::FriFoldLayout; use self::fri_commitment::FriLayer; use self::fri_decommit::FriDecommitment; use self::fri_functions::{fold_evaluations_in_place, update_twiddles_in_place}; +#[cfg(feature = "parallel")] +use rayon::prelude::*; /// FRI commit phase from pre-computed bit-reversed evaluations, skipping the /// initial FFT. Stops folding when the remaining codeword encodes a polynomial @@ -39,12 +50,78 @@ pub fn commit_phase_from_evaluations< T: IsStarkTranscript + Clone, H: StarkHash, >( - mut evals: Vec>, + evals: Vec>, + transcript: &mut T, + coset_offset: &FieldElement, + domain_size: usize, + blowup_log: u32, + final_poly_log_degree: u32, + inv_twiddles: &[FieldElement], +) -> (Vec>, Vec>>) +where + FieldElement: AsBytes + Sync + Send, + FieldElement: AsBytes + Sync + Send, +{ + // Today's layout: pair layers, the all-ones schedule. + let layout = FriFoldLayout::new( + evals.len().trailing_zeros(), + blowup_log, + final_poly_log_degree, + ); + commit_phase_with_layout::( + evals, + transcript, + coset_offset, + domain_size, + blowup_log, + final_poly_log_degree, + &layout, + inv_twiddles, + ) +} + +/// [`commit_phase_from_evaluations`] under an explicit fold layout (the proof +/// format's; see [`FriFoldLayout::for_options`]). +/// +/// Transcript, per committed layer `j` with fold exponent `d_j`: sample `ζ`, +/// fold `d_{j−1}` times with `ζ, ζ², …` (`d_{−1} = 1`: fold 0 is the binary +/// fold of the DEEP pair), commit the result with leaves of `2^{d_j}` values, +/// append the root. Then, when anything folds, sample the final `ζ` and fold +/// `d_last` times into the terminal codeword. At the all-ones schedule this is +/// exactly today's loop (sample, fold once, commit pairs, append). +/// +/// One-row layouts (S2): `d_{−1} = 0` — layer 0 is the DEEP codeword itself, +/// committed with groups of `2^{d_0}` and its root absorbed with NO challenge +/// before it; every later layer is "sample ζ, fold, commit, append" as above. +/// So `m` committed layers draw `m` challenges (the last one the final fold's), +/// against `m + 1` for row pairs. +/// +/// Leaves: the legacy encoding commits `[a, b]` pairs with `H::Pair`; the +/// group encoding hashes each `2^d`-value group with `H::Batched` (the two +/// agree on a two-element leaf, `StarkHash`'s invariant) and builds the tree +/// from those leaf hashes with `H::Pair`'s parent hash — the parent hash both +/// families share, which today's layer trees already rely on (built with +/// `H::Pair`, verified with `H::Batched`). +/// +/// The device arm (`try_fri_commit_gpu`) runs both encodings: today's loop for +/// the legacy one and its group twin otherwise — one-row layouts included, +/// whose input tree the device commits from the codeword before any +/// challenge. When it declines, the CPU loop +/// ([`commit_phase_cpu_with_layout`]) runs. +#[allow(clippy::type_complexity, clippy::too_many_arguments)] +pub(crate) fn commit_phase_with_layout< + F: IsFFTField + IsSubFieldOf + 'static, + E: IsField + 'static + Send + Sync, + T: IsStarkTranscript + Clone, + H: StarkHash, +>( + evals: Vec>, transcript: &mut T, coset_offset: &FieldElement, domain_size: usize, blowup_log: u32, final_poly_log_degree: u32, + layout: &FriFoldLayout, inv_twiddles: &[FieldElement], ) -> (Vec>, Vec>>) where @@ -58,36 +135,70 @@ where // snapshots the transcript before mutating it so a mid-loop cudarc // error restores state and lets the CPU loop below run as if the GPU // had never been tried. + // Try the GPU early-termination FRI commit first. `try_fri_commit_gpu` + // drives the same commit phase on-device (Goldilocks + Ext3, above the + // LDE size threshold, and only when folding actually happens) and returns + // `Some` with the final-polynomial coefficients. It returns `None` on any + // precondition miss or cudarc error — restoring the transcript first — so + // the CPU path below then runs as if the GPU had never been tried. #[cfg(feature = "cuda")] - { - // Try the GPU early-termination FRI commit first. `try_fri_commit_gpu` - // drives the same commit phase on-device (Goldilocks + Ext3, above the - // LDE size threshold, and only when folding actually happens) and returns - // `Some` with the final-polynomial coefficients. It returns `None` on any - // precondition miss or cudarc error — restoring the transcript first — so - // the CPU path below then runs as if the GPU had never been tried. - if let Some(result) = crate::gpu_lde::try_fri_commit_gpu::>( - &evals, - transcript, - coset_offset, - domain_size, - blowup_log, - final_poly_log_degree, - inv_twiddles, - ) { - return result; - } + if let Some(result) = crate::gpu_lde::try_fri_commit_gpu::>( + &evals, + transcript, + coset_offset, + domain_size, + blowup_log, + final_poly_log_degree, + layout, + inv_twiddles, + ) { + return result; } + commit_phase_cpu_with_layout::( + evals, + transcript, + coset_offset, + domain_size, + blowup_log, + final_poly_log_degree, + layout, + inv_twiddles, + ) +} +/// The CPU loop of [`commit_phase_with_layout`], with no device arm: what every +/// build runs when the device declines, and the host reference the device +/// parity tests compare against. +#[allow(clippy::type_complexity, clippy::too_many_arguments)] +pub(crate) fn commit_phase_cpu_with_layout< + F: IsFFTField + IsSubFieldOf + 'static, + E: IsField + 'static + Send + Sync, + T: IsStarkTranscript + Clone, + H: StarkHash, +>( + mut evals: Vec>, + transcript: &mut T, + coset_offset: &FieldElement, + domain_size: usize, + blowup_log: u32, + final_poly_log_degree: u32, + layout: &FriFoldLayout, + inv_twiddles: &[FieldElement], +) -> (Vec>, Vec>>) +where + FieldElement: AsBytes + Sync + Send, + FieldElement: AsBytes + Sync + Send, +{ debug_assert_eq!(evals.len(), domain_size); // Caller-enforced twiddle sizing (Domain::fri_inv_twiddles): the folding // loop below indexes `inv_twiddles[..len/2]` per layer. debug_assert_eq!(inv_twiddles.len(), evals.len() / 2); - // Fold layout, shared with the GPU prover and the verifier — see `FriFoldLayout`. - let layout = crate::fri::terminal::FriFoldLayout::new( - evals.len().trailing_zeros(), - blowup_log, - final_poly_log_degree, + // The fold layout, shared with the GPU prover and the verifier — see + // `FriFoldLayout`. It was built for this codeword's size. + let _ = (blowup_log, final_poly_log_degree); + debug_assert_eq!( + layout.total_folds, + evals.len().trailing_zeros() - layout.terminal_len.trailing_zeros() ); let num_committed = layout.num_committed; @@ -96,37 +207,51 @@ where let mut inv_twiddles = inv_twiddles.to_vec(); let mut fri_layer_list = Vec::with_capacity(num_committed); + // Folds still owed before the next commit: fold 0 is the binary fold of + // the DEEP pair, so one; after committing layer `j`, `d_j`. Under one-row + // openings (S2) the DEEP codeword itself is layer 0 (the input tree), so + // nothing is owed before it and its root is absorbed BEFORE the first + // folding challenge (a root absorbed after its challenge + // would let the prover pick the codeword after seeing it). + let mut pending: u32 = if layout.one_row { 0 } else { 1 }; + // Commit `num_committed` folded layers to the transcript. - for _ in 0..num_committed { - // <<<< Receive challenge 𝜁ₖ - let zeta = transcript.sample_field_element(); + for &d in &layout.schedule { + if pending > 0 { + // <<<< Receive challenge 𝜁ₖ + let zeta = transcript.sample_field_element(); - // Fold evaluations in-place (no FFT needed). - fold_evaluations_in_place(&mut evals, &zeta, &inv_twiddles); + // Fold `pending` times with 𝜁, 𝜁², … (evaluation form, no FFT). + fold_times(&mut evals, &zeta, pending, &mut inv_twiddles); + } - // Build the Merkle tree from consecutive pairs. - let leaves: Vec<[FieldElement; 2]> = evals - .chunks_exact(2) - .map(|chunk| [chunk[0].clone(), chunk[1].clone()]) - .collect(); - let merkle_tree = MerkleTree::>::build(&leaves) - .expect("FRI commit: Merkle tree construction must succeed"); + let merkle_tree = if layout.is_legacy() { + // Build the Merkle tree from consecutive pairs. + let leaves: Vec<[FieldElement; 2]> = evals + .chunks_exact(2) + .map(|chunk| [chunk[0].clone(), chunk[1].clone()]) + .collect(); + MerkleTree::>::build(&leaves) + } else { + group_tree::(&evals, 1usize << d) + } + .expect("FRI commit: Merkle tree construction must succeed"); let root = merkle_tree.root; fri_layer_list.push(FriLayer::new(&evals, merkle_tree)); // >>>> Send commitment: [pₖ] transcript.append_bytes(&root); - // Update twiddles for the next level. - update_twiddles_in_place(&mut inv_twiddles); + pending = u32::from(d); } - // One final fold to reach the terminal codeword (size terminal_len), unless - // already there (total_folds == 0 means initial_len == terminal_len). + // The final folds to reach the terminal codeword (size terminal_len), + // unless already there (total_folds == 0 means initial_len == terminal_len; + // then `pending` is 0 under one row too, as the schedule is empty). if layout.total_folds > 0 { // <<<< Receive challenge: 𝜁_final let zeta = transcript.sample_field_element(); - fold_evaluations_in_place(&mut evals, &zeta, &inv_twiddles); + fold_times(&mut evals, &zeta, pending, &mut inv_twiddles); } debug_assert_eq!( evals.len(), @@ -157,6 +282,46 @@ where (final_poly_coeffs, fri_layer_list) } +/// `n` binary folds of `evals` with `ζ, ζ², …, ζ^{2^{n−1}}`, each followed by +/// the twiddle update for the halved domain. `n = 1` is one plain fold (the +/// trailing twiddle update only prepares a fold that may never come). +pub(crate) fn fold_times, E: IsField>( + evals: &mut Vec>, + zeta: &FieldElement, + n: u32, + inv_twiddles: &mut Vec>, +) { + let mut z = zeta.clone(); + for level in 0..n { + fold_evaluations_in_place(evals, &z, inv_twiddles); + update_twiddles_in_place(inv_twiddles); + if level + 1 < n { + z = z.square(); + } + } +} + +/// A group-leaf layer tree: leaf `g` = `H::Batched` over `evals[g·n .. (g+1)·n]`. +pub(crate) fn group_tree( + evals: &[FieldElement], + n: usize, +) -> Option>> +where + E: IsField + 'static + Send + Sync, + FieldElement: AsBytes + Sync + Send, + H: StarkHash, +{ + use crypto::merkle_tree::traits::IsStreamingLeafBackend; + let hash = |g: &[FieldElement]| { + as IsStreamingLeafBackend>::hash_data_from_slices(g, &[]) + }; + #[cfg(feature = "parallel")] + let leaves: Vec<_> = evals.par_chunks_exact(n).map(hash).collect(); + #[cfg(not(feature = "parallel"))] + let leaves: Vec<_> = evals.chunks_exact(n).map(hash).collect(); + MerkleTree::>::build_from_hashed_leaves(leaves) +} + /// Open every committed layer at each query index, producing one /// [`FriDecommitment`] per query. /// @@ -218,3 +383,67 @@ where .collect() } } + +/// [`query_phase`] under an explicit fold layout. The legacy encoding is +/// [`query_phase`] itself (device arm included); the group encoding opens, per +/// committed layer `j`, the whole group `evaluation[leaf·2^{d_j} ..][..2^{d_j}]` +/// (the query's own value included) and the path of +/// `leaf = p >> d_j`, then moves to `p >> d_j` — on the device when the layers +/// are device-resident (`try_fri_query_phase_gpu_groups`), else by the host +/// walk ([`query_phase_groups_host`]). +pub(crate) fn query_phase_with_layout( + fri_layers: &[FriLayer>], + iotas: &[usize], + layout: &FriFoldLayout, +) -> Vec> +where + FieldElement: AsBytes + Sync + Send, +{ + if layout.is_legacy() { + return query_phase::(fri_layers, iotas); + } + #[cfg(feature = "cuda")] + if let Some(decommits) = + crate::gpu_lde::try_fri_query_phase_gpu_groups::>(fri_layers, iotas, layout) + { + return decommits; + } + query_phase_groups_host::(fri_layers, iotas, layout) +} + +/// The host walk of [`query_phase_with_layout`]'s group encoding over host +/// layer trees (the device parity tests' reference). +pub(crate) fn query_phase_groups_host( + fri_layers: &[FriLayer>], + iotas: &[usize], + layout: &FriFoldLayout, +) -> Vec> +where + FieldElement: AsBytes + Sync + Send, +{ + debug_assert_eq!(fri_layers.len(), layout.num_committed); + iotas + .iter() + .map(|&iota| { + let mut values = Vec::with_capacity(layout.opened_values_per_query()); + let mut paths = Vec::with_capacity(fri_layers.len()); + let mut index = iota; + for (layer, &d) in fri_layers.iter().zip(&layout.schedule) { + let n = 1usize << d; + let leaf = index >> d; + values.extend_from_slice(&layer.evaluation[leaf * n..(leaf + 1) * n]); + paths.push( + layer + .merkle_tree + .get_proof_by_pos(leaf) + .expect("FRI query: leaf index within the layer tree"), + ); + index = leaf; + } + FriDecommitment { + layers_auth_paths: paths, + layers_evaluations_sym: values, + } + }) + .collect() +} diff --git a/crypto/stark/src/fri/schedule.rs b/crypto/stark/src/fri/schedule.rs new file mode 100644 index 000000000..d601934e3 --- /dev/null +++ b/crypto/stark/src/fri/schedule.rs @@ -0,0 +1,562 @@ +//! The FRI fold schedule: which fold exponents the committed FRI layers use. +//! +//! Today every committed FRI layer folds by 2 (a pair leaf). A fold schedule +//! `[d_1, .., d_m]` generalises that: committed layer `j` folds by `2^{d_j}` +//! (a group leaf of `2^{d_j}` extension values), and `Σ d_j` covers the bits +//! between the first committed layer and the terminal codeword. The all-ones +//! schedule is today's protocol exactly. +//! +//! The schedule is a **format constant**: the prover, the verifier and the +//! in-guest verifier must derive the same one from public shape parameters +//! only, never from a proof. So the dynamic program below is integer-only, +//! with a fixed tie rule, and its inputs are all public: +//! +//! * `b0` — log2 length of the first committed layer (`lde_log − 1` when fold 0 +//! is the uncommitted binary fold of the trace pair, `lde_log` when the DEEP +//! codeword itself is committed); +//! * `terminal_log` — log2 length of the terminal codeword; +//! * `num_queries` — FRI query count (every FRI tree is opened once per query); +//! * the active Merkle-cap policy ([`CapPolicy`]; `Off` caps nothing); +//! * `dmax` — the largest fold exponent the program may choose. +//! +//! # The objective: the cost law of every emitted row +//! +//! The DP minimises the in-guest verifier's price of the FRI leg under the +//! SAME cost-law weights the cap policy optimises ([`AUTO_WEIGHTS`], ns per +//! row from the node law 421 ns/instruction + 5.63 ns/cell and each chip's +//! committed width). Per query per committed layer it prices EVERY row the +//! in-guest group-layer emitter (`prover/src/lfm/fri.rs::emit_group_layer`) +//! and its opening's hints emit — [`fri_group_layer_rows`], at a tree of +//! `depth` levels (uncapped): +//! +//! ```text +//! leaf(d)·compress absorb the 2^d-value group leaf +//! + depth·(compress + select) the authentication walk (a Select and a compression per level) +//! + (2^d − 1)·select the slot mux picking the query's value out of the group +//! + 2·XALU the slot check (assert_eq_ext: esub + ediv) +//! + (2^d − 1)·fold the group fold: 2^d − 1 binary folds (5 XALU each) +//! + d·twiddle the twiddle chain: one base mul per fold level +//! + d·(select + BALU) x_g⁻¹ = y⁻¹·ω^{br(slot)}: a constant Select and a base mul per slot bit +//! + max(0, d − 2)·XALU + [d ≥ 2]·BALU fold-level scaling (emul_base per level of > 2 pairs; one base mul at 2 pairs) +//! + 8·BALU + 1·unpack the root compare (walked digest unpacked, four lowered asserts) +//! + 2^d·unpack + 2^d·hint the group's values: hinted, unpacked into the leaf +//! + packs(d)·unpack the leaf's 3·2^d felts packed four to a word (LFM_LANES rows) +//! + depth·hint the path's siblings +//! − cap_gain(Q, c(depth)) / Q − c·hint what the tree's cap saves, per query (0 without a cap): +//! the cap policy's own gain, plus the c sibling hints a +//! capped path does not carry +//! ``` +//! +//! `leaf(d) = max(1, ⌈3·2^d / 8⌉)` (an ext3 group at the RPX rate of 8 felts); +//! the digest is the production one-cell (algebraic) digest. Each row kind is +//! priced at one weight: `SELECT`, `LFM_HASH` (compress), `Unpack` and hint +//! at the cap policy's (a `Pack` is an `LFM_LANES` row, as an `Unpack` is, +//! and is priced like one), `XALU` at [`XALU_ROW_NS`], `BALU` at [`BALU_ROW_NS`]. +//! The in-guest emitter's tests pin "emitted rows == [`fri_group_layer_rows`]" kind by +//! kind against its emitter (`lfm::fri_group_tests`), capped and uncapped. +//! Costs are kept in units of `1/Q` ns so every term is an integer. + +use crypto::merkle_tree::cap::{AUTO_WEIGHTS, CapPolicy, CapWeights, cap_gain}; + +use crate::proof::options::{FriMode, FriScheduleOverride, ProofOptions}; + +/// Largest fold exponent the schedule may choose (a 64-value group leaf). +pub const FRI_SCHEDULE_DMAX: u32 = 6; + +/// Leaf absorption rate of the cost model, in base-field elements per +/// permutation (the RPX sponge rate). +pub const FRI_LEAF_RATE_FELTS: u64 = 8; + +/// Extension degree of the FRI codeword values. +pub const FRI_EXTENSION_DEGREE: u64 = 3; + +/// `XALU` rows of one binary FRI fold in-guest: `edsl::fri_fold` emits +/// `eadd, esub, emul, emul_base, eadd`. +pub const FRI_FOLD_XALU_ROWS: u64 = 5; + +/// `BALU` rows of one step of the twiddle chain in-guest (one base `mul`). +pub const FRI_TWIDDLE_BALU_ROWS: u64 = 1; + +/// `SELECT` rows of one two-way select of the slot mux (an ext value is one +/// cell, so one `Select` instruction). +pub const FRI_SLOT_SELECT_ROWS: u64 = 1; + +/// `XALU` rows of the slot check: `assert_eq_ext` lowers to an `esub` and an +/// `ediv` by zero. +pub const FRI_SLOT_ASSERT_XALU_ROWS: u64 = 2; + +/// `SELECT` rows of one slot bit of the `x_g` derivation (`x_g⁻¹ = +/// y⁻¹·ω_{2^d}^{br(slot)}`): the bit picks `1` or a constant. +pub const FRI_XG_SELECT_ROWS: u64 = 1; + +/// `BALU` rows of one slot bit of the `x_g` derivation (one base `mul`). +pub const FRI_XG_BALU_ROWS: u64 = 1; + +/// `BALU` rows of one opening's root (or cap-node) compare at the production +/// one-cell digest: four lowered `assert_eq`s, a `sub` and a `div` each. +pub const FRI_ROOT_COMPARE_BALU_ROWS: u64 = 8; + +/// `Unpack` rows of one opening's root compare: the walked digest's lanes (a +/// capped compare unpacks the muxed cap node too, which the cap's gain prices). +pub const FRI_ROOT_COMPARE_UNPACK_ROWS: u64 = 1; + +/// Felts one `Pack` row assembles into a word for the algebraic leaf sponge. +pub const FRI_LEAF_PACK_FELTS: u64 = 4; + +/// Cost-law price (ns) of one `XALU` row: 421 + 5.63 × 18 committed cells +/// (the `LFM_XALU` cliff in the census, `+18874368` cells per `2^20` rows). +pub const XALU_ROW_NS: u64 = 522; + +/// Cost-law price (ns) of one `BALU` row: 421 + 5.63 × 10 committed cells +/// (the `LFM_BALU` cliff, `+5242880` cells per `2^19` rows). +pub const BALU_ROW_NS: u64 = 477; + +/// The per-row prices the schedule DP weighs. `cap` is the cap policy's own +/// weights, so the two levers optimise one objective. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FriCostWeights { + /// Compression, select, unpack, hint and compare prices (the cap policy's). + pub cap: CapWeights, + /// One binary fold in-guest ([`FRI_FOLD_XALU_ROWS`] `XALU` rows). + pub fold: u64, + /// One step of the twiddle chain in-guest ([`FRI_TWIDDLE_BALU_ROWS`] `BALU` rows). + pub twiddle: u64, + /// One `XALU` row. + pub xalu: u64, + /// One `BALU` row. + pub balu: u64, +} + +/// The weights the schedule DP optimises. ⚠ A FORMAT CONSTANT: changing any of +/// them can change the schedule, and so the proofs, of every table under +/// `LAMBDA_VM_ZF_FRI=dp`. Pinned by `fri_schedule_tests`. +pub const FRI_COST_WEIGHTS: FriCostWeights = FriCostWeights { + cap: AUTO_WEIGHTS, + fold: FRI_FOLD_XALU_ROWS * XALU_ROW_NS, + twiddle: FRI_TWIDDLE_BALU_ROWS * BALU_ROW_NS, + xalu: XALU_ROW_NS, + balu: BALU_ROW_NS, +}; + +/// The rows one query's opening of one committed FRI layer emits in-guest, by +/// chip kind. `hashes` counts two-to-one compressions and leaf-absorption +/// permutations alike (both are `LFM_HASH` rows, priced `compress`); `packs` +/// and `unpacks` are both `LFM_LANES` rows, priced `unpack`. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct FriLayerRows { + pub selects: u64, + pub xalu: u64, + pub balu: u64, + pub hashes: u64, + pub unpacks: u64, + pub packs: u64, + pub hints: u64, +} + +impl FriLayerRows { + /// The cost-law price (ns) of these rows under `weights`. + pub fn price(&self, weights: &FriCostWeights) -> u64 { + let w = &weights.cap; + [ + (self.selects, w.select), + (self.xalu, weights.xalu), + (self.balu, weights.balu), + (self.hashes, w.compress), + (self.unpacks, w.unpack), + (self.packs, w.unpack), + (self.hints, w.hint), + ] + .iter() + .fold(0u64, |acc, &(n, p)| acc.saturating_add(n.saturating_mul(p))) + } +} + +/// ★ The rows one query's opening of a GROUP layer (fold exponent `d`, tree +/// `depth` levels deep, cap height `cap_height`, clamped to the depth) emits +/// in-guest: `emit_group_layer` plus the opening's hints (see the module +/// docs; `lfm::fri_group_tests` pins these kind by kind against the emitter). +/// +/// A cap of height `c` walks `depth − c` levels, hints `depth − c` siblings, +/// muxes the cap node (`2^c − 1` selects) and unpacks it for the compare. +pub fn fri_group_layer_rows(d: u32, depth: u32, cap_height: u32) -> FriLayerRows { + let d = d.min(63); + let n = 1u64 << d; + let c = cap_height.min(depth).min(63); + let walk = u64::from(depth - c); + let cap_mux = (1u64 << c) - 1; + let d64 = u64::from(d); + FriLayerRows { + selects: (n - 1) * FRI_SLOT_SELECT_ROWS + walk + cap_mux + d64 * FRI_XG_SELECT_ROWS, + xalu: (n - 1) * FRI_FOLD_XALU_ROWS + FRI_SLOT_ASSERT_XALU_ROWS + d64.saturating_sub(2), + balu: d64 * FRI_TWIDDLE_BALU_ROWS + + d64 * FRI_XG_BALU_ROWS + + u64::from(d >= 2) + + FRI_ROOT_COMPARE_BALU_ROWS, + hashes: fri_leaf_blocks(d) + walk, + unpacks: n + FRI_ROOT_COMPARE_UNPACK_ROWS + u64::from(c > 0), + packs: fri_leaf_packs(d), + hints: n + walk, + } +} + +/// The rows one query's opening of a layer under TODAY's pair encoding +/// (`FriFormat::is_legacy`: one sibling value per layer, no slot check, no +/// `x_g`) emits in-guest (`prover/src/lfm/fri.rs::emit_pair_layer`): the +/// parity select, the pair leaf, the walk and compare, one squaring of the +/// point, one fold; hints: the sibling value and the path. +pub fn fri_pair_layer_rows(depth: u32, cap_height: u32) -> FriLayerRows { + let c = cap_height.min(depth).min(63); + let walk = u64::from(depth - c); + FriLayerRows { + selects: FRI_SLOT_SELECT_ROWS + walk + ((1u64 << c) - 1), + xalu: FRI_FOLD_XALU_ROWS, + balu: FRI_TWIDDLE_BALU_ROWS + FRI_ROOT_COMPARE_BALU_ROWS, + hashes: fri_leaf_blocks(1) + walk, + unpacks: 2 + FRI_ROOT_COMPARE_UNPACK_ROWS + u64::from(c > 0), + packs: fri_leaf_packs(1), + hints: 1 + walk, + } +} + +/// Log2 length of the first committed FRI layer for an LDE of `2^lde_log`. +/// +/// Row-pair openings (`one_row == false`) consume the first fold uncommitted, +/// so the chain starts at `lde_log − 1`; one-row openings commit the DEEP +/// codeword itself, so the chain starts at `lde_log`. +pub fn fri_chain_start(lde_log: u32, one_row: bool) -> u32 { + if one_row { + lde_log + } else { + lde_log.saturating_sub(1) + } +} + +/// `Pack` rows that assemble one group leaf's `3·2^d` felts into words (four +/// per word, the tail zero-padded) before the sponge absorbs them. +pub fn fri_leaf_packs(d: u32) -> u64 { + let felts = FRI_EXTENSION_DEGREE.saturating_mul(1u64.checked_shl(d).unwrap_or(u64::MAX)); + felts.div_ceil(FRI_LEAF_PACK_FELTS) +} + +/// Permutations to absorb one group leaf of `2^d` extension values. +pub fn fri_leaf_blocks(d: u32) -> u64 { + let felts = FRI_EXTENSION_DEGREE.saturating_mul(1u64.checked_shl(d).unwrap_or(u64::MAX)); + felts.div_ceil(FRI_LEAF_RATE_FELTS).max(1) +} + +/// `Q ×` the per-query cost-law price (ns) of one committed GROUP layer of +/// fold exponent `d` whose tree has `depth` levels (the layer is +/// `2^{depth + d}` values long), under `weights` and the cap policy `cap`: +/// every emitted row ([`fri_group_layer_rows`]) of the uncapped opening, minus +/// the cap's gain and the sibling hints the cap removes. See the module docs. +pub fn fri_layer_cost_q( + weights: &FriCostWeights, + d: u32, + depth: u32, + num_queries: u64, + cap: CapPolicy, +) -> u64 { + layer_cost_q( + weights, + &fri_group_layer_rows(d, depth, 0), + depth, + num_queries, + cap, + ) +} + +/// `Q ×` the per-query price of one committed layer under TODAY's pair +/// encoding ([`fri_pair_layer_rows`]), under `weights` and `cap`. +pub fn fri_pair_layer_cost_q( + weights: &FriCostWeights, + depth: u32, + num_queries: u64, + cap: CapPolicy, +) -> u64 { + layer_cost_q( + weights, + &fri_pair_layer_rows(depth, 0), + depth, + num_queries, + cap, + ) +} + +/// `Q × price(uncapped)` minus the cap's gain ([`cap_gain`], the cap policy's +/// own function) and the `c` sibling hints per query a capped path omits. In +/// i128, clamped into u64 (non-negative: a cap never saves more than the walk +/// it shortens, but the clamp keeps that a non-assumption). +fn layer_cost_q( + weights: &FriCostWeights, + uncapped: &FriLayerRows, + depth: u32, + num_queries: u64, + cap: CapPolicy, +) -> u64 { + let q = num_queries as i128; + let per_query = uncapped.price(weights) as i128; + let queries = usize::try_from(num_queries).unwrap_or(usize::MAX); + let c = cap.height(queries, depth as usize); + let hints_saved = q.saturating_mul(c as i128 * weights.cap.hint as i128); + let total = q.saturating_mul(per_query) - cap_gain(&weights.cap, queries, c) - hints_saved; + u64::try_from(total.max(0)).unwrap_or(u64::MAX) +} + +/// `Q ×` the per-query cost of an arbitrary schedule starting at `b0` under a +/// per-layer cost function `layer_cost_q(d, depth)`, or `None` if a fold +/// exponent is zero or the schedule folds past zero bits. +pub fn fri_schedule_cost_by( + b0: u32, + schedule: &[u8], + layer_cost_q: &dyn Fn(u32, u32) -> u64, +) -> Option { + let mut b = b0; + let mut cost = 0u64; + for &d in schedule { + let d = u32::from(d); + if d == 0 { + return None; + } + b = b.checked_sub(d)?; + cost = cost.saturating_add(layer_cost_q(d, b)); + } + Some(cost) +} + +/// [`fri_schedule_cost_by`] under the production objective +/// ([`FRI_COST_WEIGHTS`], [`fri_layer_cost_q`]) — group layers. +pub fn fri_schedule_cost_q( + b0: u32, + schedule: &[u8], + num_queries: u64, + cap: CapPolicy, +) -> Option { + fri_schedule_cost_by(b0, schedule, &|d, depth| { + fri_layer_cost_q(&FRI_COST_WEIGHTS, d, depth, num_queries, cap) + }) +} + +/// The optimum a schedule DP picks, with its cost. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FriScheduleChoice { + /// `Q ×` the per-query cost (see the module docs). + pub cost_q: u64, + /// Number of committed trees (`schedule.len()`). + pub trees: u32, + /// Fold exponents, first committed layer first. + pub schedule: Vec, +} + +/// The schedule DP over an arbitrary per-layer cost `layer_cost_q(d, depth)`: +/// +/// ```text +/// best(T) = (0, 0, []) +/// best(b > T) = min over d ∈ [1, min(dmax, b − T)] of +/// (layer_cost_q(d, b − d) + best(b − d).cost, best(b − d).trees + 1, [d] ++ best(b − d).sched) +/// ``` +/// +/// compared lexicographically on `(cost, trees)`; ties go to the smallest `d` +/// (the first reached). Equivalently, the result is the lexicographically +/// smallest schedule among the `(cost, trees)`-optimal ones. It lands exactly +/// on `terminal_log`: `Σ schedule == b0 − terminal_log`, and the schedule is +/// empty when `b0 ≤ terminal_log`. A `dmax` of 0 is treated as 1; `dmax` is +/// capped at 32. +pub fn fri_schedule_by( + b0: u32, + terminal_log: u32, + dmax: u32, + layer_cost_q: &dyn Fn(u32, u32) -> u64, +) -> FriScheduleChoice { + if b0 <= terminal_log { + return FriScheduleChoice { + cost_q: 0, + trees: 0, + schedule: Vec::new(), + }; + } + let dmax = dmax.clamp(1, 32); + let span = (b0 - terminal_log) as usize; + // best[i] = optimum from b = terminal_log + i down to the terminal, stored + // as (cost, trees, first fold exponent); the schedule is recovered by + // following the first exponents. + let mut best: Vec<(u64, u32, u32)> = Vec::with_capacity(span + 1); + best.push((0, 0, 0)); + for i in 1..=span { + let b = terminal_log + i as u32; + let mut cand: Option<(u64, u32, u32)> = None; + for d in 1..=dmax.min(i as u32) { + let (rest_cost, rest_trees, _) = best[i - d as usize]; + let cost = layer_cost_q(d, b - d).saturating_add(rest_cost); + let trees = rest_trees + 1; + // Strictly better only: ties keep the smaller `d` reached first. + if cand.is_none_or(|(c, t, _)| (cost, trees) < (c, t)) { + cand = Some((cost, trees, d)); + } + } + // `d = 1` is always admissible (i ≥ 1, dmax ≥ 1), so `cand` is set. + best.push(cand.unwrap_or((u64::MAX, u32::MAX, 1))); + } + let (cost_q, trees, _) = best[span]; + let mut schedule = Vec::with_capacity(trees as usize); + let mut i = span; + while i > 0 { + let d = best[i].2; + schedule.push(d as u8); + i -= d as usize; + } + FriScheduleChoice { + cost_q, + trees, + schedule, + } +} + +/// The production schedule DP: [`fri_schedule_by`] under the cost-law +/// objective ([`FRI_COST_WEIGHTS`]) with the cap policy `cap`. +pub fn fri_schedule_with_cost( + b0: u32, + terminal_log: u32, + num_queries: u64, + cap: CapPolicy, + dmax: u32, +) -> FriScheduleChoice { + fri_schedule_by(b0, terminal_log, dmax, &|d, depth| { + fri_layer_cost_q(&FRI_COST_WEIGHTS, d, depth, num_queries, cap) + }) +} + +/// The schedule of [`fri_schedule_with_cost`]. +pub fn fri_schedule( + b0: u32, + terminal_log: u32, + num_queries: u64, + cap: CapPolicy, + dmax: u32, +) -> Vec { + fri_schedule_with_cost(b0, terminal_log, num_queries, cap, dmax).schedule +} + +/// Today's schedule: every committed layer folds by 2. +pub fn legacy_fri_schedule(b0: u32, terminal_log: u32) -> Vec { + vec![1; b0.saturating_sub(terminal_log) as usize] +} + +/// Why a proof format cannot be laid out for a table. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FriFormatError { + /// The schedule override does not cover this table's committed folds + /// exactly, or has an exponent outside `1..=FRI_SCHEDULE_DMAX`. + ScheduleOverrideMismatch, +} + +impl core::fmt::Display for FriFormatError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::ScheduleOverrideMismatch => { + f.write_str("the FRI schedule override does not cover this table's committed folds") + } + } + } +} + +/// Everything the fold layout needs to know about the proof format, for one +/// table. All fields are verifier-side constants; none is ever read from a +/// proof. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FriFormat { + pub mode: FriMode, + /// The RESOLVED one-row choice for this table (never `Auto`). + pub one_row: bool, + /// FRI query count (the opening count of every FRI tree). + pub num_queries: u64, + /// The active Merkle-cap policy (an input of the DP: the cap changes each layer's path cost). + pub cap: CapPolicy, + /// An explicit schedule that replaces the DP's under [`FriMode::Dp`]. + pub schedule_override: Option, +} + +impl FriFormat { + /// Today's format: pair layers, row-pair openings. The query count and cap + /// policy are unused by the all-ones schedule. + pub const LEGACY: Self = Self { + mode: FriMode::Pair, + one_row: false, + num_queries: 0, + cap: CapPolicy::Off, + schedule_override: None, + }; + + /// The format of a table proved under `options` whose trace trees use + /// the RESOLVED leaf layout `one_row` (the table's + /// [`crate::leaf_layout::table_leaf_layout`]; `options.format.one_row` may + /// be `Auto`, which only the caller can resolve, from the AIR's widths). + pub fn from_options(options: &ProofOptions, one_row: bool) -> Self { + Self { + mode: options.format.fri_mode, + one_row, + num_queries: options.fri_number_of_queries as u64, + cap: options.format.merkle_cap, + schedule_override: options.format.fri_schedule_override, + } + } + + /// Whether the proof uses today's FRI encoding: one sibling value per + /// committed layer, pair leaves. True exactly for pair + /// layers with row-pair openings; any other format carries every layer's + /// full group, even where the schedule is all ones. Decided by the format, + /// never by the schedule's values. + pub fn is_legacy(&self) -> bool { + self.mode == FriMode::Pair && !self.one_row + } + + /// `Q ×` the per-query in-guest price of this table's whole FRI chain for + /// an LDE of `2^lde_log` folding to a terminal of `2^terminal_log`: every + /// committed layer of [`Self::schedule`] (group layers, or today's pair + /// layers when [`Self::is_legacy`]) plus, for row-pair openings, the + /// uncommitted fold 0 (one fold; the group encoding also squares the + /// point once into the first layer's `y⁻¹`, where the pair encoding + /// squares inside each layer). + pub fn chain_cost_q(&self, lde_log: u32, terminal_log: u32) -> u64 { + let w = &FRI_COST_WEIGHTS; + let (q, cap) = (self.num_queries, self.cap); + let b0 = fri_chain_start(lde_log, self.one_row); + let schedule = self.schedule(lde_log, terminal_log); + let layers = if self.is_legacy() { + fri_schedule_cost_by(b0, &schedule, &|_, depth| { + fri_pair_layer_cost_q(w, depth, q, cap) + }) + } else { + fri_schedule_cost_q(b0, &schedule, q, cap) + } + .unwrap_or(u64::MAX); + let fold0 = if !self.one_row && lde_log > terminal_log { + let per_query = if self.is_legacy() { + w.fold + } else { + w.fold + w.twiddle + }; + q.saturating_mul(per_query) + } else { + 0 + }; + layers.saturating_add(fold0) + } + + /// The committed-layer fold schedule for an LDE of `2^lde_log` folding to a + /// terminal of `2^terminal_log` (the override's, verbatim, when one is + /// set under `Dp`; the layout checks that it fits). + pub fn schedule(&self, lde_log: u32, terminal_log: u32) -> Vec { + let b0 = fri_chain_start(lde_log, self.one_row); + match (self.mode, self.schedule_override) { + (FriMode::Pair, _) => legacy_fri_schedule(b0, terminal_log), + (FriMode::Dp, Some(o)) => o.as_slice().to_vec(), + (FriMode::Dp, None) => fri_schedule( + b0, + terminal_log, + self.num_queries, + self.cap, + FRI_SCHEDULE_DMAX, + ), + } + } +} diff --git a/crypto/stark/src/fri/terminal.rs b/crypto/stark/src/fri/terminal.rs index 716fbcf3d..ac93790f9 100644 --- a/crypto/stark/src/fri/terminal.rs +++ b/crypto/stark/src/fri/terminal.rs @@ -9,6 +9,9 @@ use math::field::element::FieldElement; use math::field::traits::{IsFFTField, IsField, IsSubFieldOf}; use math::polynomial::Polynomial; +use crate::fri::schedule::{FRI_SCHEDULE_DMAX, FriFormat, FriFormatError}; +use crate::proof::options::ProofOptions; + /// The FRI early-termination fold layout. /// /// Derived identically by the CPU prover (`commit_phase_from_evaluations`), the @@ -16,11 +19,16 @@ use math::polynomial::Polynomial; /// Keeping the arithmetic in one place is load-bearing: the three callers must /// agree exactly or proofs fail to verify, and a CPU/GPU disagreement would /// surface only on GPU machines. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// +/// The committed layers follow a fold schedule (`crate::fri::schedule`): layer +/// `j` folds by `2^{schedule[j]}`. Today's layout ([`Self::new`]) is the +/// all-ones schedule, built through the same constructor as every other format. +#[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct FriFoldLayout { /// Folds from the LDE codeword down to the terminal codeword. pub(crate) total_folds: u32, - /// Committed (Merkle-rooted) FRI layers = `total_folds - 1`, or 0 when there + /// Committed (Merkle-rooted) FRI layers = `schedule.len()`. Row-pair + /// layout: `total_folds - 1` under the all-ones schedule, or 0 when there /// is no fold or only a single final fold. pub(crate) num_committed: usize, /// Terminal codeword length = `2^(blowup_log + effective_k)`. @@ -28,10 +36,24 @@ pub(crate) struct FriFoldLayout { /// Terminal polynomial log-degree bound actually used, `min(k, trace_bits)`. /// This is the verifier's `expected_k` and the prover's `effective_log_degree`. pub(crate) effective_k: u32, + /// Fold exponent of each committed layer, first committed layer first. + /// Invariant (checked by every constructor): `(one_row ? 0 : 1) + + /// Σ schedule == total_folds` whenever `total_folds >= 1`, and empty + /// otherwise; every entry is in `1..=FRI_SCHEDULE_DMAX`. + pub(crate) schedule: Vec, + /// Whether the DEEP codeword itself is committed (one-row openings): then + /// the chain starts at the LDE size and there is no uncommitted fold 0. + pub(crate) one_row: bool, + /// Today's FRI encoding ([`FriFormat::is_legacy`]): pair-leaf layer trees + /// and one sibling value per committed layer per query. `false` = group + /// leaves (`H::Batched` over `2^d` values) and the full group per layer — + /// decided by the format, never by the schedule's values, so a `Dp` + /// schedule that happens to be all ones still uses the group encoding. + pub(crate) legacy_encoding: bool, } impl FriFoldLayout { - /// Derive the layout from the LDE codeword size. + /// Today's layout, derived from the LDE codeword size. /// /// * `lde_log` — log2 of the LDE (deep-composition) codeword length. /// * `blowup_log` — log2 of the LDE blowup factor. @@ -42,16 +64,156 @@ impl FriFoldLayout { /// size for traces too small to fold that far (the `.min(lde_log)`). /// Computing `blowup_log + k` in `u32` (both small) sidesteps the /// `1 << (blowup_log + k)` overflow an out-of-range `k` would otherwise cause. + /// + /// This is [`Self::for_format`] at [`FriFormat::LEGACY`]: pair layers, + /// row-pair openings, the all-ones schedule. pub(crate) fn new(lde_log: u32, blowup_log: u32, k: u32) -> Self { let terminal_log = (blowup_log + k).min(lde_log); + let schedule = FriFormat::LEGACY.schedule(lde_log, terminal_log); + // The all-ones schedule covers the committed folds by construction. + Self::assemble(lde_log, blowup_log, terminal_log, false, schedule) + } + + /// The layout under an explicit proof format. `total_folds`, + /// `terminal_len` and `effective_k` do not depend on the format; only the + /// split of the folds into committed layers does. + /// + /// `None` only for a schedule override that does not cover the committed + /// folds exactly (the DP and the all-ones schedules land on the terminal + /// by construction). + pub(crate) fn for_format( + lde_log: u32, + blowup_log: u32, + k: u32, + fmt: &FriFormat, + ) -> Option { + let terminal_log = (blowup_log + k).min(lde_log); + let schedule = fmt.schedule(lde_log, terminal_log); + let mut layout = Self::assemble(lde_log, blowup_log, terminal_log, fmt.one_row, schedule); + layout.legacy_encoding = fmt.is_legacy(); + layout.schedule_is_consistent().then_some(layout) + } + + /// The layout of a table proved under `options` over an LDE of + /// `2^lde_log` with blowup `2^blowup_log`, whose trace trees use the + /// resolved leaf layout `one_row`: what the prover and the host verifier + /// both build. The format comes from `options` and the table's AIR — a + /// verifier-side constant — never from a proof. + pub(crate) fn for_options( + lde_log: u32, + blowup_log: u32, + options: &ProofOptions, + one_row: bool, + ) -> Result { + let fmt = FriFormat::from_options(options, one_row); + Self::for_format( + lde_log, + blowup_log, + u32::from(options.fri_final_poly_log_degree), + &fmt, + ) + .ok_or(FriFormatError::ScheduleOverrideMismatch) + } + + /// The layout for a caller-supplied schedule, or `None` if the schedule + /// does not cover exactly the committed folds (or has an exponent outside + /// `1..=FRI_SCHEDULE_DMAX`). The encoding is the group encoding unless + /// the schedule is today's (row pair, all ones), where it is legacy. + #[cfg(test)] + pub(crate) fn from_schedule( + lde_log: u32, + blowup_log: u32, + k: u32, + one_row: bool, + schedule: Vec, + ) -> Option { + let terminal_log = (blowup_log + k).min(lde_log); + let mut layout = Self::assemble(lde_log, blowup_log, terminal_log, one_row, schedule); + layout.legacy_encoding = !one_row && layout.schedule.iter().all(|&d| d == 1); + layout.schedule_is_consistent().then_some(layout) + } + + /// Whether this layout uses today's FRI encoding (see + /// [`Self::legacy_encoding`]). The device FRI arms branch on this: today's + /// pair loop, or its group-leaf twin. + pub(crate) fn is_legacy(&self) -> bool { + self.legacy_encoding + } + + /// Log2 length of committed layer `j` (0-based): the chain start minus the + /// bits the earlier committed layers consumed. + pub(crate) fn layer_log_len(&self, lde_log: u32, j: usize) -> u32 { + let consumed: u32 = self.schedule[..j].iter().map(|&d| u32::from(d)).sum(); + crate::fri::schedule::fri_chain_start(lde_log, self.one_row) - consumed + } + + /// Depth of committed layer `j`'s tree: its length over `2^{d_j}` leaves. + pub(crate) fn layer_depth(&self, lde_log: u32, j: usize) -> u32 { + self.layer_log_len(lde_log, j) - u32::from(self.schedule[j]) + } + + /// Folding challenges a proof of this layout draws: one per committed + /// layer plus the final fold's for row pairs (fold 0 consumes the first), + /// one per committed layer for one row (layer 0, the input tree, is + /// committed before any challenge); none when nothing folds. + pub(crate) fn num_zetas(&self) -> usize { + if self.total_folds == 0 { + 0 + } else { + self.num_committed + usize::from(!self.one_row) + } + } + + /// Depth of every committed layer's tree, in layer order. + pub(crate) fn layer_depths(&self, lde_log: u32) -> Vec { + (0..self.num_committed) + .map(|j| self.layer_depth(lde_log, j) as usize) + .collect() + } + + /// Opened values per query in the flat `layers_evaluations_sym` vector: + /// one per layer (legacy) or every layer's full group. + pub(crate) fn opened_values_per_query(&self) -> usize { + if self.legacy_encoding { + self.num_committed + } else { + self.schedule.iter().map(|&d| 1usize << d).sum() + } + } + + fn assemble( + lde_log: u32, + blowup_log: u32, + terminal_log: u32, + one_row: bool, + schedule: Vec, + ) -> Self { let total_folds = lde_log - terminal_log; Self { total_folds, - num_committed: total_folds.saturating_sub(1) as usize, + num_committed: schedule.len(), terminal_len: 1usize << terminal_log, effective_k: terminal_log - blowup_log, + schedule, + one_row, + legacy_encoding: !one_row, } } + + /// The constructor invariant (see [`Self::schedule`]). + fn schedule_is_consistent(&self) -> bool { + let entries_ok = self + .schedule + .iter() + .all(|&d| d >= 1 && u32::from(d) <= FRI_SCHEDULE_DMAX); + let covered: u64 = self.schedule.iter().map(|&d| u64::from(d)).sum(); + let expected = if self.total_folds == 0 { + 0 + } else { + u64::from(self.total_folds) - u64::from(!self.one_row) + }; + entries_ok && covered == expected + } } /// Prover side: given a FRI terminal codeword in **bit-reversed** order, diff --git a/crypto/stark/src/fri/vectors.rs b/crypto/stark/src/fri/vectors.rs new file mode 100644 index 000000000..0906f26a0 --- /dev/null +++ b/crypto/stark/src/fri/vectors.rs @@ -0,0 +1,620 @@ +//! The S3 test vectors the host prover exports ((a)–(d) in the README) for the +//! device prover and the in-guest verifier, checked in under +//! `crypto/stark/tests/vectors/zf_fri/` (see the README there). +//! +//! Compiled only for tests and the `test-utils` feature. Everything here is +//! deterministic: the KAT inputs come from [`splitmix64`], proofs are made at +//! `grinding_factor = 0`. `tests::zf_fri_vectors` (Keccak, Blake3) and the +//! prover crate's `tests::zf_rpx_vectors` (RPX) regenerate every file in memory +//! and require it byte-equal to the checked-in one. + +use std::fmt::Write as _; +use std::path::PathBuf; +use std::string::String; +use std::vec::Vec; + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use crypto::merkle_tree::cap::CapPolicy; +use crypto::merkle_tree::traits::IsStreamingLeafBackend; +use math::fft::bit_reversing::reverse_index; +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; +use math::field::goldilocks::GoldilocksField; +use math::field::traits::IsFFTField; + +use crate::config::StarkHash; +use crate::examples::read_only_memory_logup::{ + LogReadOnlyPublicInputs, LogReadOnlyRAP, read_only_logup_trace, +}; +use crate::fri::capture::{FriCapture, capture}; +use crate::fri::fri_functions::compute_coset_twiddles_inv; +use crate::fri::group::{group_fold, roots_of_unity_table}; +use crate::fri::schedule::{ + FRI_COST_WEIGHTS, FRI_SCHEDULE_DMAX, fri_chain_start, fri_schedule_with_cost, +}; +use crate::fri::terminal::FriFoldLayout; +use crate::proof::options::{FriMode, FriScheduleOverride, ProofFormat, ProofOptions}; +use crate::prover::{GenericProver, IsStarkProver}; +use crate::trace::TraceTable; +use crate::traits::AIR; +use crate::verifier::{GenericVerifier, IsStarkVerifier}; + +type F = GoldilocksField; +type E = Degree3GoldilocksExtensionField; +type Felt = FieldElement; +type Ext = FieldElement; + +/// The vectors directory: `crypto/stark/tests/vectors/zf_fri`. +pub fn vectors_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/zf_fri") +} + +/// One vector file: its name in [`vectors_dir`] and its exact bytes. +pub struct VectorFile { + pub name: String, + pub bytes: Vec, +} + +/// Compare `files` with the checked-in ones (byte equality), or write them +/// when `write` is set. Returns the names that differ or are missing. +pub fn check_or_write(files: &[VectorFile], write: bool) -> Vec { + let dir = vectors_dir(); + let mut bad = Vec::new(); + for f in files { + let path = dir.join(&f.name); + if write { + std::fs::create_dir_all(&dir).expect("create the vectors directory"); + std::fs::write(&path, &f.bytes).expect("write a vector file"); + } else if std::fs::read(&path).ok().as_deref() != Some(f.bytes.as_slice()) { + bad.push(f.name.clone()); + } + } + bad +} + +/// SplitMix64: the KAT input generator (stated in the README so any consumer can +/// regenerate the inputs without this crate). +pub fn splitmix64(state: &mut u64) -> u64 { + *state = state.wrapping_add(0x9e37_79b9_7f4a_7c15); + let mut z = *state; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + z ^ (z >> 31) +} + +/// An ext3 element from three SplitMix64 outputs, each reduced mod p. +pub(crate) fn next_ext(state: &mut u64) -> Ext { + Ext::new([ + Felt::from(splitmix64(state)), + Felt::from(splitmix64(state)), + Felt::from(splitmix64(state)), + ]) +} + +fn limbs(e: &Ext) -> [u64; 3] { + let v = e.value(); + [v[0].canonical(), v[1].canonical(), v[2].canonical()] +} + +fn ext_json(e: &Ext) -> String { + let [a, b, c] = limbs(e); + format!("[{a},{b},{c}]") +} + +fn exts_json(v: &[Ext]) -> String { + let items: Vec = v.iter().map(ext_json).collect(); + format!("[{}]", items.join(",")) +} + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +// --------------------------------------------------------------------------- +// (a) schedules +// --------------------------------------------------------------------------- + +/// (a) The production fold schedules: T ∈ {4, 9, 10}, B = 6..=24, Q ∈ {3, 110}, +/// cap off / auto, the S3 chain (from B − 1) and the S2 chain (from B), each +/// with its cost-law cost (Q × ns) — the DP's output as the format constant it is. +pub fn schedules_json() -> VectorFile { + let mut s = String::from("{\n \"generator\": \"stark::fri::vectors::schedules_json\",\n"); + let w = FRI_COST_WEIGHTS; + let _ = writeln!( + s, + " \"weights_ns\": {{\"compress\": {}, \"select\": {}, \"unpack\": {}, \"hint\": {}, \"compare\": {}, \"fold\": {}, \"twiddle\": {}, \"xalu\": {}, \"balu\": {}}},", + w.cap.compress, + w.cap.select, + w.cap.unpack, + w.cap.hint, + w.cap.compare, + w.fold, + w.twiddle, + w.xalu, + w.balu + ); + let _ = writeln!(s, " \"dmax\": {FRI_SCHEDULE_DMAX},"); + s.push_str(" \"rows\": [\n"); + let mut rows = Vec::new(); + for t in [4u32, 9, 10] { + for q in [3u64, 110] { + for (cap_name, cap) in [("off", CapPolicy::Off), ("auto", CapPolicy::Auto)] { + for b in 6..=24u32 { + for (chain, one_row) in [("s3", false), ("s2", true)] { + let b0 = fri_chain_start(b, one_row); + let c = fri_schedule_with_cost(b0, t, q, cap, FRI_SCHEDULE_DMAX); + rows.push(format!( + " {{\"terminal_log\": {t}, \"queries\": {q}, \"cap\": \"{cap_name}\", \"lde_log\": {b}, \"chain\": \"{chain}\", \"b0\": {b0}, \"schedule\": {:?}, \"cost_q_ns\": {}}}", + c.schedule, c.cost_q + )); + } + } + } + } + } + s.push_str(&rows.join(",\n")); + s.push_str("\n ]\n}\n"); + VectorFile { + name: "a_schedules.json".into(), + bytes: s.into_bytes(), + } +} + +// --------------------------------------------------------------------------- +// (b) group-fold KATs +// --------------------------------------------------------------------------- + +/// The KAT codeword: `2^KAT_LOG` ext3 values from SplitMix64 seed +/// [`KAT_SEED`] (value i = three consecutive outputs), read as a bit-reversed +/// layer on the coset `3·⟨ω_{2^KAT_LOG}⟩`. +pub const KAT_LOG: u32 = 7; +pub const KAT_SEED: u64 = 0x5a46_4652_4933; + +pub fn kat_codeword() -> Vec { + let mut st = KAT_SEED; + (0..1usize << KAT_LOG).map(|_| next_ext(&mut st)).collect() +} + +/// ζ of the fold KAT for exponent `d`: SplitMix64 seeded `KAT_SEED + d`. +pub fn kat_zeta(d: u32) -> Ext { + let mut st = KAT_SEED + u64::from(d); + next_ext(&mut st) +} + +/// (b) For d = 1..=6: the KAT codeword folded d times with ζ, ζ², … (the +/// prover's commit loop), and the verifier's group fold of every group from +/// its slot-0 point (equal by construction; both listed so a device kernel +/// can be checked against either). +pub fn group_fold_json() -> VectorFile { + let o = Felt::from(3u64); + let n = 1usize << KAT_LOG; + let cw = kat_codeword(); + let w = F::get_primitive_root_of_unity(u64::from(KAT_LOG)).expect("root"); + let mut s = String::from("{\n \"generator\": \"stark::fri::vectors::group_fold_json\",\n"); + let _ = writeln!( + s, + " \"layer_log\": {KAT_LOG},\n \"coset_offset\": 3,\n \"codeword\": {},", + exts_json(&cw) + ); + s.push_str(" \"folds\": [\n"); + let mut items = Vec::new(); + for d in 1..=6u32 { + let zeta = kat_zeta(d); + let mut folded = cw.clone(); + let mut tw = compute_coset_twiddles_inv::(&o, n); + crate::fri::fold_times(&mut folded, &zeta, d, &mut tw); + let roots = roots_of_unity_table::(d).expect("table"); + let by_group: Vec = (0..n >> d) + .map(|g| { + // slot 0: y = x_g, so x_g⁻¹ = y⁻¹. + let y = &o * w.pow(reverse_index(g << d, n as u64) as u64); + group_fold::( + &cw[g << d..(g + 1) << d], + &zeta, + &y.inv().expect("nonzero"), + &roots, + ) + }) + .collect(); + assert_eq!( + folded, by_group, + "the prover's folds and the group fold agree" + ); + items.push(format!( + " {{\"d\": {d}, \"zeta\": {}, \"folded\": {}}}", + ext_json(&zeta), + exts_json(&folded) + )); + } + s.push_str(&items.join(",\n")); + s.push_str("\n ]\n}\n"); + VectorFile { + name: "b_group_folds.json".into(), + bytes: s.into_bytes(), + } +} + +// --------------------------------------------------------------------------- +// (c) group-leaf digests +// --------------------------------------------------------------------------- + +/// (c) Under hash `H` (named `hash_name`), for d = 1..=6: the leaf digest of +/// the KAT codeword's first group (`H::Batched` over its 2^d values) and the +/// root of the whole KAT codeword committed as a group-leaf layer tree. +pub fn leaf_digests_json(hash_name: &str) -> VectorFile { + let cw = kat_codeword(); + let mut s = format!( + "{{\n \"generator\": \"stark::fri::vectors::leaf_digests_json\",\n \"hash\": \"{hash_name}\",\n \"codeword\": \"b_group_folds.json codeword\",\n \"leaves\": [\n" + ); + let mut items = Vec::new(); + for d in 1..=6u32 { + let n = 1usize << d; + let leaf = + as IsStreamingLeafBackend>::hash_data_from_slices(&cw[..n], &[]); + let tree = crate::fri::group_tree::(&cw, n).expect("tree"); + items.push(format!( + " {{\"d\": {d}, \"first_leaf\": \"{}\", \"layer_root\": \"{}\"}}", + hex(&leaf), + hex(&tree.root) + )); + } + s.push_str(&items.join(",\n")); + s.push_str("\n ]\n}\n"); + VectorFile { + name: format!("c_leaf_digests_{hash_name}.json"), + bytes: s.into_bytes(), + } +} + +// --------------------------------------------------------------------------- +// (d) small proofs per format +// --------------------------------------------------------------------------- + +/// The (d) proof shape: `LogReadOnlyRAP` (ext3, one aux column), 2^10 rows, +/// blowup 4 (B = 12), k = 2, grinding 0, coset offset 3; Q = 3, or +/// [`CAPPED_QUERIES`] for the capped formats. +pub const PROOF_ROWS: usize = 1 << 10; + +/// The query count of the capped (d) formats: the `auto` cap policy caps a +/// tree opened at least 20 times at height 3, so a Q = 3 proof +/// carries no cap at all. +pub const CAPPED_QUERIES: usize = 20; + +pub fn proof_options(format: ProofFormat, queries: usize) -> ProofOptions { + ProofOptions { + blowup_factor: 4, + fri_number_of_queries: queries, + coset_offset: 3, + grinding_factor: 0, + fri_final_poly_log_degree: 2, + format, + } +} + +/// The formats of (d), with their query counts: `pair` (today), `dp` (the +/// DP's schedule) and `dp_3_1_3` (an explicit uneven schedule, to catch +/// fold-count bugs), all at Q = 3; and `cap_pair` / `cap_dp` (the `auto` Merkle +/// cap on every tree, with today's FRI and with the DP's schedule) at +/// Q = [`CAPPED_QUERIES`] — the combined S1 × S3 vector. +pub fn proof_formats() -> Vec<(&'static str, ProofFormat, usize)> { + let dp = ProofFormat { + fri_mode: FriMode::Dp, + ..ProofFormat::DEFAULT + }; + let cap = ProofFormat { + merkle_cap: CapPolicy::Auto, + ..ProofFormat::DEFAULT + }; + vec![ + ("pair", ProofFormat::DEFAULT, 3), + ("dp", dp, 3), + ( + "dp_3_1_3", + ProofFormat { + fri_schedule_override: FriScheduleOverride::new(&[3, 1, 3]), + ..dp + }, + 3, + ), + ("cap_pair", cap, CAPPED_QUERIES), + ( + "cap_dp", + ProofFormat { + fri_mode: FriMode::Dp, + ..cap + }, + CAPPED_QUERIES, + ), + ] +} + +fn logup_case( + format: ProofFormat, + queries: usize, +) -> ( + LogReadOnlyRAP, + TraceTable, + LogReadOnlyPublicInputs, +) { + let rows = PROOF_ROWS; + let addr: Vec = (0..rows).map(|i| Felt::from((i % 5) as u64 + 1)).collect(); + let val: Vec = (0..rows) + .map(|i| Felt::from(((i % 5) as u64 + 1) * 10)) + .collect(); + let trace: TraceTable = read_only_logup_trace(addr, val); + let cols = trace.columns_main(); + let pi = LogReadOnlyPublicInputs { + a0: cols[0][0], + v0: cols[1][0], + a_sorted_0: cols[2][0], + v_sorted_0: cols[3][0], + m0: cols[4][0], + }; + ( + LogReadOnlyRAP::::new(&proof_options(format, queries)), + trace, + pi, + ) +} + +/// (d) Under hash `H`: per format, the proof's rkyv bytes (`.rkyv`) and a JSON +/// with everything a verifier derives from it — layout, ζ, ι, DEEP values, +/// roots, terminal coefficients, and per query per layer the leaf, slot and +/// opened values. +pub fn proof_vectors(hash_name: &str) -> Vec { + let mut out = Vec::new(); + for (fmt_name, format, queries) in proof_formats() { + out.extend(proof_files::( + hash_name, fmt_name, format, queries, "d_proof", + )); + } + out +} + +/// The formats of (e) (S2): one-row openings with the pair schedule +/// (`one_row_pair`: all-ones groups, the input tree committed in pairs) and +/// with an explicit uneven schedule from the input tree (`one_row_3_2_1_2`, +/// `Σ = 8 = B − T`). +pub fn one_row_proof_formats() -> Vec<(&'static str, ProofFormat)> { + let on = ProofFormat { + one_row: crate::proof::options::OneRowMode::On, + ..ProofFormat::DEFAULT + }; + vec![ + ("one_row_pair", on), + ( + "one_row_3_2_1_2", + ProofFormat { + fri_mode: FriMode::Dp, + fri_schedule_override: FriScheduleOverride::new(&[3, 2, 1, 2]), + ..on + }, + ), + ] +} + +/// (e) The S2 proofs under hash `H`: the (d) shape (at `Q = 3`) proved with one-row +/// openings. Per query the JSON adds the trace leaf (`r`, a leaf index over +/// the whole LDE) and its path length (`log2(lde)`); `deep` is DEEP at the +/// ONE point `x_r` (there is no `deep_sym`), and layer 0 is the input tree +/// (the DEEP codeword itself; its root is `fri_roots[0]`, absorbed before the +/// first ζ, so `zetas` has one entry per layer). +pub fn one_row_proof_vectors(hash_name: &str) -> Vec { + let mut out = Vec::new(); + for (fmt_name, format) in one_row_proof_formats() { + out.extend(proof_files::(hash_name, fmt_name, format, 3, "e_proof")); + } + out +} + +/// (e) One-row trace-leaf digests under hash `H`: a KAT base matrix (16 rows × +/// 5 columns) and ext3 matrix (16 rows × 2 columns) from SplitMix64 seed +/// [`KAT_SEED`] + 100 / + 200, read as bit-reversed LDE columns, committed +/// with one row per leaf AND with row pairs (today's): every leaf digest and +/// the root of each. One row: leaf `i` = the row at bit-reversed position `i` +/// (columns in order, big-endian bytes / the `Batched` felt stream); row pair: +/// rows `2i`, `2i + 1`. +pub fn one_row_leaf_digests_json(hash_name: &str) -> VectorFile { + const ROWS: usize = 16; + let mut st = KAT_SEED + 100; + let base: Vec> = (0..5) + .map(|_| (0..ROWS).map(|_| Felt::from(splitmix64(&mut st))).collect()) + .collect(); + let mut st = KAT_SEED + 200; + let ext: Vec> = (0..2) + .map(|_| (0..ROWS).map(|_| next_ext(&mut st)).collect()) + .collect(); + let mut s = format!( + "{{\n \"generator\": \"stark::fri::vectors::one_row_leaf_digests_json\",\n \"hash\": \"{hash_name}\",\n \"rows\": {ROWS},\n" + ); + let base_json: Vec = base + .iter() + .map(|c| { + let v: Vec = c.iter().map(|x| x.canonical().to_string()).collect(); + format!("[{}]", v.join(",")) + }) + .collect(); + let _ = writeln!(s, " \"base_columns\": [{}],", base_json.join(",")); + let ext_cols: Vec = ext.iter().map(|c| exts_json(c)).collect(); + let _ = writeln!(s, " \"ext_columns\": [{}],", ext_cols.join(",")); + let mut items = Vec::new(); + for (layout_name, rows_per_leaf) in [("row", 1usize), ("row_pair", 2)] { + let b = crate::commitment::leaves_bit_reversed_grouped::>( + &base, + rows_per_leaf, + ); + let (_, b_root) = + crate::commitment::commit_bit_reversed_with::>(&base, rows_per_leaf) + .expect("base tree"); + let e = + crate::commitment::leaves_bit_reversed_grouped::>(&ext, rows_per_leaf); + let (_, e_root) = + crate::commitment::commit_bit_reversed_with::>(&ext, rows_per_leaf) + .expect("ext tree"); + let hexes = |v: &[crate::config::Commitment]| { + let h: Vec = v.iter().map(|x| format!("\"{}\"", hex(x))).collect(); + format!("[{}]", h.join(",")) + }; + items.push(format!( + " {{\"layout\": \"{layout_name}\", \"rows_per_leaf\": {rows_per_leaf}, \"base_leaves\": {}, \"base_root\": \"{}\", \"ext_leaves\": {}, \"ext_root\": \"{}\"}}", + hexes(&b), + hex(&b_root), + hexes(&e), + hex(&e_root) + )); + } + s.push_str(" \"layouts\": [\n"); + s.push_str(&items.join(",\n")); + s.push_str("\n ]\n}\n"); + VectorFile { + name: format!("e_leaf_digests_{hash_name}.json"), + bytes: s.into_bytes(), + } +} + +/// One proof's `{prefix}_{hash}_{format}.{json,rkyv}` pair (the (d) and (e) +/// files). The table's leaf layout is resolved as the prover and verifier +/// resolve it; the JSON keeps the (d) schema for row pairs byte for byte and +/// adds the one-row fields otherwise. +fn proof_files( + hash_name: &str, + fmt_name: &str, + format: ProofFormat, + queries: usize, + prefix: &str, +) -> Vec { + let mut out = Vec::new(); + { + let (air, mut trace, pi) = logup_case(format, queries); + let one_row = crate::leaf_layout::table_leaf_layout(&air, PROOF_ROWS).is_one_row(); + let proof = GenericProver::::prove( + &air, + &mut trace, + &pi, + &mut DefaultTranscript::::new(&[]), + ) + .expect("proving must succeed"); + let (ok, records) = capture(|| { + GenericVerifier::::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + ) + }); + assert!(ok, "the {prefix} proof must verify"); + let rec = FriCapture::::from_any(records[0].as_ref()).expect("one ext3 record"); + let bytes = rkyv::to_bytes::(&proof) + .expect("rkyv") + .to_vec(); + let lde_log = PROOF_ROWS.trailing_zeros() + 2; + let layout = + FriFoldLayout::for_options(lde_log, 2, air.options(), one_row).expect("layout"); + let stem = format!("{prefix}_{hash_name}_{fmt_name}"); + + let mut s = format!( + "{{\n \"generator\": \"stark::fri::vectors::proof_vectors\",\n \"hash\": \"{hash_name}\",\n \"format\": \"{fmt_name}\",\n \"proof_rkyv\": \"{stem}.rkyv\",\n \"proof_rkyv_len\": {},\n", + bytes.len() + ); + let _ = writeln!( + s, + " \"air\": \"LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))\",\n \"trace_rows\": {PROOF_ROWS},\n \"lde_log\": {lde_log},\n \"blowup\": 4,\n \"fri_final_poly_log_degree\": 2,\n \"queries\": {queries},\n \"grinding_factor\": 0,\n \"coset_offset\": 3," + ); + if !format.merkle_cap.is_off() { + // The capped formats only (the Q = 3 files are unchanged): the + // policy and every tree's height, from the verifier's own + // `StarkCaps`. Each capped tree's cap rides at the end of query + // 0's path (the owner path), so that `path_len` is `D − c + 2^c`. + let caps = crate::merkle_caps::StarkCaps::for_options( + air.options(), + lde_log as usize, + one_row, + ) + .expect("caps"); + let _ = writeln!( + s, + " \"merkle_cap\": \"{}\",\n \"trace_tree_depth\": {},\n \"trace_cap\": {},\n \"fri_tree_depths\": {:?},\n \"fri_caps\": {:?},", + format.merkle_cap, caps.trace_depth, caps.trace, caps.fri_depths, caps.fri + ); + } + if one_row { + let _ = writeln!( + s, + " \"one_row\": true,\n \"query_bound\": {},\n \"trace_tree_depth\": {lde_log},", + 1u64 << lde_log + ); + } + let _ = writeln!( + s, + " \"legacy_encoding\": {},\n \"total_folds\": {},\n \"terminal_len\": {},\n \"schedule\": {:?},", + layout.is_legacy(), + layout.total_folds, + layout.terminal_len, + layout.schedule + ); + let roots: Vec = proof + .fri_layers_merkle_roots + .iter() + .map(|r| format!("\"{}\"", hex(r))) + .collect(); + let _ = writeln!(s, " \"fri_roots\": [{}],", roots.join(",")); + let _ = writeln!(s, " \"zetas\": {},", exts_json(&rec.zetas)); + let _ = writeln!( + s, + " \"terminal_coeffs\": {},", + exts_json(&proof.fri_final_poly_coeffs) + ); + s.push_str(" \"queries_detail\": [\n"); + let mut qs = Vec::new(); + for (qi, &iota) in rec.iotas.iter().enumerate() { + let dec = &proof.query_list[qi]; + let mut layers = Vec::new(); + let mut index = iota; + let mut off = 0usize; + for (j, &d) in layout.schedule.iter().enumerate() { + let (leaf, slot, n) = if layout.is_legacy() { + (index >> 1, index & 1, 1usize) + } else { + (index >> d, index & ((1 << d) - 1), 1usize << d) + }; + layers.push(format!( + "{{\"layer\": {j}, \"d\": {d}, \"position\": {index}, \"leaf\": {leaf}, \"slot\": {slot}, \"values\": {}, \"path_len\": {}}}", + exts_json(&dec.layers_evaluations_sym[off..off + n]), + dec.layers_auth_paths[j].merkle_path.len() + )); + off += n; + index = if layout.is_legacy() { + index >> 1 + } else { + index >> d + }; + } + if one_row { + let opening = &proof.deep_poly_openings[qi]; + qs.push(format!( + " {{\"iota\": {iota}, \"trace_leaf\": {iota}, \"trace_path_len\": {}, \"deep\": {}, \"terminal_position\": {index}, \"layers\": [{}]}}", + opening.main_trace_polys.proof.merkle_path.len(), + ext_json(&rec.deep[qi]), + layers.join(", ") + )); + } else { + qs.push(format!( + " {{\"iota\": {iota}, \"deep\": {}, \"deep_sym\": {}, \"terminal_position\": {index}, \"layers\": [{}]}}", + ext_json(&rec.deep[qi]), + ext_json(&rec.deep_sym[qi]), + layers.join(", ") + )); + } + } + s.push_str(&qs.join(",\n")); + s.push_str("\n ]\n}\n"); + out.push(VectorFile { + name: format!("{stem}.json"), + bytes: s.into_bytes(), + }); + out.push(VectorFile { + name: format!("{stem}.rkyv"), + bytes, + }); + } + out +} diff --git a/crypto/stark/src/gpu_lde.rs b/crypto/stark/src/gpu_lde.rs index 41d48d36b..e51bdc1a9 100644 --- a/crypto/stark/src/gpu_lde.rs +++ b/crypto/stark/src/gpu_lde.rs @@ -110,7 +110,7 @@ const _: () = { }; /// The `math_cuda` dispatch key for `B`'s hash. -fn device_hash_of() -> math_cuda::DeviceHash { +pub(crate) fn device_hash_of() -> math_cuda::DeviceHash { device_hash_for(B::COMMITMENT_HASH) } @@ -212,7 +212,8 @@ fn gpu_device_only_threshold() -> usize { // so the dispatch layer's callers keep one path. use crate::device_set::BASE_BYTES; pub use crate::device_set::{ - Admission, CommitDeviceSet, admit_bytes, commit_device_set, ext3_bytes, full_tree_bytes, + Admission, CommitDeviceSet, admit_bytes, commit_device_set, commit_device_set_rpl, ext3_bytes, + full_tree_bytes, tree_bytes_for, }; /// The process predicate: `gpu_lde_threshold()` as the floor and the card's @@ -534,11 +535,15 @@ pub fn reset_all_gpu_call_counters() { GPU_LOGUP_CALLS.store(0, Ordering::Relaxed); GPU_COMPOSITION_CALLS.store(0, Ordering::Relaxed); GPU_OPENING_GATHER_CALLS.store(0, Ordering::Relaxed); + GPU_CAP_READ_CALLS.store(0, Ordering::Relaxed); GPU_DEVICE_ONLY_CALLS.store(0, Ordering::Relaxed); GPU_DEVICE_ONLY_DOWNGRADES.store(0, Ordering::Relaxed); GPU_RESIDENT_AUX_RETRIES.store(0, Ordering::Relaxed); GPU_RESIDENT_AUX_DOWNGRADES.store(0, Ordering::Relaxed); GPU_COMPOSITION_PARTS_DOWNLOADS.store(0, Ordering::Relaxed); + GPU_ONE_ROW_TREES.store(0, Ordering::Relaxed); + GPU_ONE_ROW_TREE_PEAK_BYTES.store(0, Ordering::Relaxed); + GPU_ONE_ROW_FRI_CALLS.store(0, Ordering::Relaxed); #[cfg(feature = "cuda")] crypto::grinding::reset_gpu_grind_calls(); } @@ -610,6 +615,15 @@ pub fn gpu_opening_gather_calls() -> u64 { GPU_OPENING_GATHER_CALLS.load(Ordering::Relaxed) } +/// Merkle caps read off a device-resident tree ([`read_cap_dev`]) — one per +/// capped tree whose nodes live on the device (main, aux, composition, FRI +/// layer). Zero under the default format, where no tree is capped; under a +/// cap policy a device prove with this at zero never took the device arm. +pub(crate) static GPU_CAP_READ_CALLS: AtomicU64 = AtomicU64::new(0); +pub fn gpu_cap_read_calls() -> u64 { + GPU_CAP_READ_CALLS.load(Ordering::Relaxed) +} + /// Tables whose round-1 LDE was kept device-only (host trace D2H skipped) — the /// Stage-3 full-residency win. Incremented once per main trace that took the /// `device_only` path. Zero means every table kept its host copy (gate never @@ -1312,6 +1326,25 @@ pub fn try_commit_row_major( blowup_factor: usize, coset_offset: &FieldElement, ) -> Option +where + F: IsFFTField + 'static, + B: DeviceTreeBackend, +{ + try_commit_row_major_with::(table, row_major, rows, cols, blowup_factor, coset_offset, 2) +} + +/// [`try_commit_row_major`] with `rows_per_leaf` rows per Merkle leaf: 2 is +/// today's row pair, 1 the S2 one-row root (the host twin is +/// `commit_bit_reversed_with(.., rows_per_leaf)`). +pub fn try_commit_row_major_with( + table: &str, + row_major: &[FieldElement], + rows: usize, + cols: usize, + blowup_factor: usize, + coset_offset: &FieldElement, + rows_per_leaf: usize, +) -> Option where F: IsFFTField + 'static, B: DeviceTreeBackend, @@ -1335,6 +1368,7 @@ where // The artifact build never reads the evaluations — only the root — so // the row-major D2H is skipped entirely. false, + rows_per_leaf, )?; Some(tree.root) } @@ -1350,6 +1384,7 @@ pub(crate) fn try_expand_leaf_and_tree_row_major_keep( blowup_factor: usize, weights: &[FieldElement], retain_host_lde: bool, + rows_per_leaf: usize, ) -> Option<( MerkleTree, math_cuda::lde::GpuLdeBase, @@ -1381,7 +1416,7 @@ where base_cols: m, blowup: blowup_factor, }; - let set = commit_device_set(n, m, blowup_factor, true); + let set = commit_device_set_rpl(n, m, blowup_factor, true, rows_per_leaf); admit_commit(lde_size, &shape, &set)?; let raw: &[u64] = unsafe { from_raw_parts(row_major.as_ptr() as *const u64, n * m) }; @@ -1390,11 +1425,12 @@ where GPU_LDE_CALLS.fetch_add(m as u64, Ordering::Relaxed); GPU_LEAF_HASH_CALLS.fetch_add(1, Ordering::Relaxed); GPU_MERKLE_TREE_CALLS.fetch_add(1, Ordering::Relaxed); + note_one_row_trees(rows_per_leaf, 1, set.tree_bytes); // The keep path keeps the Merkle tree resident on device (in `handle.tree`). // `retain_host_lde=false` additionally skips the row-major D2H (device-only). // Admitted means the device path is the only path: a failure here aborts. - let (handle, lde_u64) = match math_cuda::lde::coset_lde_row_major_with_merkle_tree_keep( + let (handle, lde_u64) = match math_cuda::lde::coset_lde_row_major_with_merkle_tree_keep_rpl( raw, predev, device_hash_of::(), @@ -1403,6 +1439,7 @@ where blowup_factor, &weights_u64, retain_host_lde, + rows_per_leaf, ) { Ok(v) => v, Err(e) => { @@ -1478,6 +1515,7 @@ pub(crate) fn try_expand_split_trees_row_major_keep( split_col: usize, build_precomputed: bool, want_host: bool, + rows_per_leaf: usize, ) -> Option<( Option>, MerkleTree, @@ -1509,7 +1547,7 @@ where base_cols: m, blowup: blowup_factor, }; - let set = commit_device_set(n, m, blowup_factor, true); + let set = commit_device_set_rpl(n, m, blowup_factor, true, rows_per_leaf); admit_commit(lde_size, &shape, &set)?; let raw: &[u64] = unsafe { from_raw_parts(row_major.as_ptr() as *const u64, n * m) }; @@ -1518,9 +1556,10 @@ where GPU_LDE_CALLS.fetch_add(m as u64, Ordering::Relaxed); GPU_LEAF_HASH_CALLS.fetch_add(1 + build_precomputed as u64, Ordering::Relaxed); GPU_MERKLE_TREE_CALLS.fetch_add(1 + build_precomputed as u64, Ordering::Relaxed); + note_one_row_trees(rows_per_leaf, 1 + build_precomputed as u64, set.tree_bytes); // Admitted means the device path is the only path: a failure here aborts. - let (pre_nodes, handle, lde_u64) = match math_cuda::lde::coset_lde_row_major_split_trees( + let (pre_nodes, handle, lde_u64) = match math_cuda::lde::coset_lde_row_major_split_trees_rpl( raw, predev, device_hash_of::(), @@ -1531,6 +1570,7 @@ where split_col, build_precomputed, want_host, + rows_per_leaf, ) { Ok(v) => v, Err(e) => { @@ -1573,6 +1613,7 @@ where /// Row-major ext3 GPU path: single H2D → row-major NTT (m*3 base-field cols) → /// row-major Keccak → Merkle → single D2H → transpose to GpuLdeExt3 handle. /// Same optimization as the base-field path: no extract_columns, no CPU transpose. +#[allow(clippy::too_many_arguments)] pub(crate) fn try_expand_leaf_and_tree_ext3_row_major_keep( table: &str, row_major: &[FieldElement], @@ -1581,6 +1622,7 @@ pub(crate) fn try_expand_leaf_and_tree_ext3_row_major_keep( blowup_factor: usize, weights: &[FieldElement], retain_host_lde: bool, + rows_per_leaf: usize, ) -> Option<( MerkleTree, math_cuda::lde::GpuLdeExt3, @@ -1610,7 +1652,7 @@ where base_cols: m3, blowup: blowup_factor, }; - let set = commit_device_set(n, m3, blowup_factor, false); + let set = commit_device_set_rpl(n, m3, blowup_factor, false, rows_per_leaf); admit_commit(lde_size, &shape, &set)?; let raw: &[u64] = unsafe { from_raw_parts(row_major.as_ptr() as *const u64, n * m3) }; @@ -1619,11 +1661,12 @@ where GPU_LDE_CALLS.fetch_add((m * 3) as u64, Ordering::Relaxed); GPU_LEAF_HASH_CALLS.fetch_add(1, Ordering::Relaxed); GPU_MERKLE_TREE_CALLS.fetch_add(1, Ordering::Relaxed); + note_one_row_trees(rows_per_leaf, 1, set.tree_bytes); // The keep path keeps the Merkle tree resident on device (in `handle.tree`). // `retain_host_lde=false` additionally skips the row-major D2H (device-only). // Admitted means the device path is the only path: a failure here aborts. - let (handle, lde_u64) = match math_cuda::lde::coset_lde_ext3_row_major_with_merkle_tree_keep( + let (handle, lde_u64) = match math_cuda::lde::coset_lde_ext3_row_major_with_merkle_tree_keep_rpl( raw, device_hash_of::(), n, @@ -1631,6 +1674,7 @@ where blowup_factor, &weights_u64, retain_host_lde, + rows_per_leaf, ) { Ok(v) => v, Err(e) => { @@ -1718,6 +1762,41 @@ pub fn gpu_merkle_tree_calls() -> u64 { GPU_MERKLE_TREE_CALLS.load(Ordering::Relaxed) } +/// S2 one-row trees (`rows_per_leaf = 1`) the device built: trace trees (main, +/// the preprocessed split's subsets, aux plain and resident), composition +/// trees, and FRI input trees over the resident DEEP codeword. A one-row table +/// that fell back to the host leaves this unmoved, so the device-parity tests +/// assert on it (a fallback is a FAIL, never a pass). +static GPU_ONE_ROW_TREES: AtomicU64 = AtomicU64::new(0); +pub fn gpu_one_row_trees() -> u64 { + GPU_ONE_ROW_TREES.load(Ordering::Relaxed) +} + +/// The largest single one-row tree's node buffer the device was asked for, +/// in bytes (the admission's own term, `(2 · lde − 1) · 32`) — what a +/// one-row table adds over its row-pair twin, per tree, for the 0-fallback +/// gate. +static GPU_ONE_ROW_TREE_PEAK_BYTES: AtomicU64 = AtomicU64::new(0); +pub fn gpu_one_row_tree_peak_bytes() -> u64 { + GPU_ONE_ROW_TREE_PEAK_BYTES.load(Ordering::Relaxed) +} + +/// Device FRI commits under a one-row layout (S2): the input tree committed +/// from the DEEP codeword on device, then the group chain. +static GPU_ONE_ROW_FRI_CALLS: AtomicU64 = AtomicU64::new(0); +pub fn gpu_one_row_fri_calls() -> u64 { + GPU_ONE_ROW_FRI_CALLS.load(Ordering::Relaxed) +} + +/// Count `trees` one-row trees of `tree_bytes` node bytes each (no-op for +/// row pairs). +fn note_one_row_trees(rows_per_leaf: usize, trees: u64, tree_bytes: u64) { + if rows_per_leaf == 1 { + GPU_ONE_ROW_TREES.fetch_add(trees, Ordering::Relaxed); + GPU_ONE_ROW_TREE_PEAK_BYTES.fetch_max(tree_bytes, Ordering::Relaxed); + } +} + // ============================================================================ // PR-3: R2 composition-parts LDE + Merkle commit + R3 OOD barycentric // ============================================================================ @@ -1839,6 +1918,7 @@ where /// recomputes on CPU. pub(crate) fn try_build_comp_poly_tree_gpu( lde_parts: &[Vec>], + rows_per_leaf: usize, ) -> Option<(MerkleTree, math_cuda::lde::GpuMerkleTree)> where E: IsField + 'static, @@ -1855,9 +1935,9 @@ where return None; } // The parts are re-uploaded (`m` ext3 columns over the LDE) and one full - // row-pair tree is built. - let bytes = ext3_bytes(lde_size as u64, lde_parts.len() as u64) - .saturating_add(full_tree_bytes(lde_size as u64)); + // tree (`rows_per_leaf` rows per leaf) is built. + let tree_bytes = tree_bytes_for(lde_size as u64, rows_per_leaf as u64); + let bytes = ext3_bytes(lde_size as u64, lde_parts.len() as u64).saturating_add(tree_bytes); if !admit_transient(lde_size, bytes, "R2 composition tree") { return None; } @@ -1881,13 +1961,19 @@ where // tree (`gather_proofs_dev`); the returned host tree is root only. let dev_tree = match match device_hash_of::() { math_cuda::DeviceHash::Keccak256 => { - math_cuda::merkle::build_comp_poly_tree_from_evals_ext3_keep(&raw_parts) + math_cuda::merkle::build_comp_poly_tree_from_evals_ext3_keep_rpl( + &raw_parts, + rows_per_leaf, + ) } math_cuda::DeviceHash::Blake3 => { - math_cuda::blake3::build_comp_poly_tree_from_evals_ext3_keep(&raw_parts) + math_cuda::blake3::build_comp_poly_tree_from_evals_ext3_keep_rpl( + &raw_parts, + rows_per_leaf, + ) } math_cuda::DeviceHash::Rpx256 => { - math_cuda::rpx::build_comp_poly_tree_from_evals_ext3_keep(&raw_parts) + math_cuda::rpx::build_comp_poly_tree_from_evals_ext3_keep_rpl(&raw_parts, rows_per_leaf) } math_cuda::DeviceHash::Rpo256 | math_cuda::DeviceHash::Poseidon => unimplemented!( "{:?} device commit not yet ported (comp-poly tree from ext3 evals)", @@ -1897,8 +1983,9 @@ where Ok(t) => t, Err(_) => return None, }; - debug_assert_eq!(dev_tree.leaves_len, lde_size / 2); + debug_assert_eq!(dev_tree.leaves_len, lde_size / rows_per_leaf); GPU_COMP_POLY_TREE_CALLS.fetch_add(1, Ordering::Relaxed); + note_one_row_trees(rows_per_leaf, 1, tree_bytes); let host = MerkleTree::::from_root(dev_tree.root); Some((host, dev_tree)) } @@ -1908,6 +1995,7 @@ where /// host pack + H2D re-upload of data that is already on device. pub(crate) fn try_build_comp_poly_tree_gpu_from_dev( handle: &math_cuda::lde::GpuLdeExt3, + rows_per_leaf: usize, ) -> Option<(MerkleTree, math_cuda::lde::GpuMerkleTree)> where E: IsField + 'static, @@ -1920,9 +2008,10 @@ where return None; } // Only the tree is fresh: the parts are already resident. + let tree_bytes = tree_bytes_for(handle.lde_size as u64, rows_per_leaf as u64); if !admit_transient( handle.lde_size, - full_tree_bytes(handle.lde_size as u64), + tree_bytes, "R2 composition tree (resident parts)", ) { return None; @@ -1931,23 +2020,30 @@ where let stream = be.next_stream(); handle.wait_ready_on(&stream).ok()?; let dev_tree = match device_hash_of::() { - math_cuda::DeviceHash::Keccak256 => math_cuda::merkle::build_comp_poly_tree_from_slabs_dev( - &stream, - handle.buf.as_ref(), - handle.m, - handle.lde_size, - ), - math_cuda::DeviceHash::Blake3 => math_cuda::blake3::build_comp_poly_tree_from_slabs_dev( - &stream, - handle.buf.as_ref(), - handle.m, - handle.lde_size, - ), - math_cuda::DeviceHash::Rpx256 => math_cuda::rpx::build_comp_poly_tree_from_slabs_dev( + math_cuda::DeviceHash::Keccak256 => { + math_cuda::merkle::build_comp_poly_tree_from_slabs_dev_rpl( + &stream, + handle.buf.as_ref(), + handle.m, + handle.lde_size, + rows_per_leaf, + ) + } + math_cuda::DeviceHash::Blake3 => { + math_cuda::blake3::build_comp_poly_tree_from_slabs_dev_rpl( + &stream, + handle.buf.as_ref(), + handle.m, + handle.lde_size, + rows_per_leaf, + ) + } + math_cuda::DeviceHash::Rpx256 => math_cuda::rpx::build_comp_poly_tree_from_slabs_dev_rpl( &stream, handle.buf.as_ref(), handle.m, handle.lde_size, + rows_per_leaf, ), math_cuda::DeviceHash::Rpo256 | math_cuda::DeviceHash::Poseidon => unimplemented!( "{:?} device commit not yet ported (comp-poly tree from resident slabs)", @@ -1956,6 +2052,7 @@ where } .ok()?; GPU_COMP_POLY_TREE_CALLS.fetch_add(1, Ordering::Relaxed); + note_one_row_trees(rows_per_leaf, 1, tree_bytes); let host = MerkleTree::::from_root(dev_tree.root); Some((host, dev_tree)) } @@ -2912,6 +3009,7 @@ pub(crate) fn try_expand_leaf_and_tree_ext3_row_major_keep_dev( blowup_factor: usize, weights: &[FieldElement], retain_host_lde: bool, + rows_per_leaf: usize, ) -> Option<( MerkleTree, math_cuda::lde::GpuLdeExt3, @@ -2936,15 +3034,22 @@ where base_cols: ra.num_aux_cols * 3, blowup: blowup_factor, }; - let set = commit_device_set(ra.num_rows, ra.num_aux_cols * 3, blowup_factor, false); + let set = commit_device_set_rpl( + ra.num_rows, + ra.num_aux_cols * 3, + blowup_factor, + false, + rows_per_leaf, + ); admit_resident_commit(&shape, &set)?; let weights_u64 = unsafe { weights_to_u64::(weights) }; GPU_LDE_CALLS.fetch_add((ra.num_aux_cols * 3) as u64, Ordering::Relaxed); GPU_LEAF_HASH_CALLS.fetch_add(1, Ordering::Relaxed); GPU_MERKLE_TREE_CALLS.fetch_add(1, Ordering::Relaxed); + note_one_row_trees(rows_per_leaf, 1, set.tree_bytes); - let (handle, lde_u64) = math_cuda::lde::coset_lde_ext3_row_major_with_merkle_tree_keep_dev( + let (handle, lde_u64) = math_cuda::lde::coset_lde_ext3_row_major_with_merkle_tree_keep_dev_rpl( &ra.buf, device_hash_of::(), ra.num_rows, @@ -2952,6 +3057,7 @@ where blowup_factor, &weights_u64, retain_host_lde, + rows_per_leaf, ) .inspect_err(|e| { // Surface the swallowed driver error (e.g. OOM): the caller drains the @@ -3517,6 +3623,60 @@ pub(crate) fn gather_proofs_dev( Some(proofs) } +/// Read the height-`cap_height` Merkle cap of a device-resident tree: the +/// nodes `MerkleTree::cap` returns on the host tree, +/// byte for byte, since the device heap has the host layout. The R4 cap +/// post-pass calls it for every capped tree whose host tree is root-only. +/// +/// Fails closed with a message, never a panic: a cap taller than the tree or a +/// cudarc error is an `Err` the caller turns into a `ProvingError`. `stream` +/// is the stream the tree's own openings were gathered on (the table's bound +/// stream; a fresh backend stream for the FRI layers, as the FRI query phase +/// uses). +pub(crate) fn read_cap_dev( + tree: &math_cuda::lde::GpuMerkleTree, + cap_height: usize, + stream: &Arc, +) -> Result, String> { + if !tree.leaves_len.is_power_of_two() { + return Err(format!( + "device tree has {} leaves, not a power of two", + tree.leaves_len + )); + } + let depth = tree.leaves_len.trailing_zeros() as usize; + if cap_height > depth { + return Err(format!( + "cap height {cap_height} exceeds the device tree depth {depth}" + )); + } + if tree.nodes.len() < ((2usize << cap_height) - 1) * 32 { + return Err(format!( + "device node buffer of {} bytes is too short for a height-{cap_height} cap", + tree.nodes.len() + )); + } + let bytes = math_cuda::merkle::read_cap_dev(&tree.nodes, tree.leaves_len, cap_height, stream) + .map_err(|e| format!("cudarc: {e:?}"))?; + let cap: Vec = bytes + .chunks_exact(32) + .map(|c| { + let mut node: Commitment = [0u8; 32]; + node.copy_from_slice(c); + node + }) + .collect(); + if cap.len() != 1 << cap_height { + return Err(format!( + "device cap read returned {} nodes, expected {}", + cap.len(), + 1usize << cap_height + )); + } + GPU_CAP_READ_CALLS.fetch_add(1, Ordering::Relaxed); + Ok(cap) +} + /// R3 OOD device-side context: bundles the inverted denominators, the /// coset_points upload (used by every barycentric kernel for this batch), /// and the stream so producer + consumers serialize naturally. Hoisting @@ -3608,7 +3768,7 @@ where /// a byte-identical pre-GPU transcript state and produces the same proof /// it would have produced had the GPU never been tried. This requires the /// concrete transcript type to support snapshot semantics via `Clone`. -#[allow(clippy::type_complexity)] +#[allow(clippy::type_complexity, clippy::too_many_arguments)] pub(crate) fn try_fri_commit_gpu( evals: &[FieldElement], transcript: &mut T, @@ -3616,8 +3776,80 @@ pub(crate) fn try_fri_commit_gpu( domain_size: usize, blowup_log: u32, final_poly_log_degree: u32, + layout: &crate::fri::terminal::FriFoldLayout, inv_twiddles: &[FieldElement], ) -> Option<(Vec>, Vec>)> +where + F: IsFFTField + IsField + IsSubFieldOf + 'static, + E: IsField + 'static + Send + Sync, + FieldElement: AsBytes, + FieldElement: AsBytes, + T: IsStarkTranscript + Clone, + B: DeviceTreeBackend, +{ + // Host-evals entry: the caller works with host copies, keep draining them. + try_fri_commit_gpu_evals::( + evals, + transcript, + coset_offset, + domain_size, + blowup_log, + final_poly_log_degree, + layout, + inv_twiddles, + true, + ) +} + +/// [`try_fri_commit_gpu`] with the layers' host copies optional: `want_host = +/// false` keeps each committed layer's evals resident only (`gpu_evals`), the +/// shape the device-only envelope produces — so the device query phase's +/// resident gathers can be tested from host evals. +#[cfg(any(test, feature = "test-utils"))] +#[allow(clippy::type_complexity, clippy::too_many_arguments)] +pub(crate) fn try_fri_commit_gpu_resident( + evals: &[FieldElement], + transcript: &mut T, + coset_offset: &FieldElement, + domain_size: usize, + blowup_log: u32, + final_poly_log_degree: u32, + layout: &crate::fri::terminal::FriFoldLayout, + inv_twiddles: &[FieldElement], +) -> Option<(Vec>, Vec>)> +where + F: IsFFTField + IsField + IsSubFieldOf + 'static, + E: IsField + 'static + Send + Sync, + FieldElement: AsBytes, + FieldElement: AsBytes, + T: IsStarkTranscript + Clone, + B: DeviceTreeBackend, +{ + try_fri_commit_gpu_evals::( + evals, + transcript, + coset_offset, + domain_size, + blowup_log, + final_poly_log_degree, + layout, + inv_twiddles, + false, + ) +} + +#[allow(clippy::type_complexity, clippy::too_many_arguments)] +fn try_fri_commit_gpu_evals( + evals: &[FieldElement], + transcript: &mut T, + coset_offset: &FieldElement, + domain_size: usize, + blowup_log: u32, + final_poly_log_degree: u32, + layout: &crate::fri::terminal::FriFoldLayout, + inv_twiddles: &[FieldElement], + want_host: bool, +) -> Option<(Vec>, Vec>)> where F: IsFFTField + IsField + IsSubFieldOf + 'static, E: IsField + 'static + Send + Sync, @@ -3641,8 +3873,12 @@ where return None; } // The evals upload, the geometric layer chain (bounded by one more - // codeword) and the layer trees (bounded by one full tree). - let bytes = ext3_bytes(n0 as u64, 2).saturating_add(full_tree_bytes(n0 as u64)); + // codeword) and the layer trees (bounded by one full tree; under one row + // the chain starts at the codeword itself, so by the one-row bound). + let bytes = ext3_bytes(n0 as u64, 2).saturating_add(tree_bytes_for( + n0 as u64, + if layout.one_row { 1 } else { 2 }, + )); if !admit_transient(n0, bytes, "R4 FRI commit") { return None; } @@ -3676,7 +3912,6 @@ where Ok(s) => s, Err(_) => return None, }; - // Host-evals entry: the caller works with host copies, keep draining them. fri_commit_gpu_drive::( state, transcript, @@ -3684,19 +3919,21 @@ where n0, blowup_log, final_poly_log_degree, - true, + layout, + want_host, ) } /// [`try_fri_commit_gpu`] entered from a device-resident DEEP codeword /// (already in FRI order): no evals H2D at all. -#[allow(clippy::type_complexity)] +#[allow(clippy::type_complexity, clippy::too_many_arguments)] pub(crate) fn try_fri_commit_gpu_from_dev( codeword: math_cuda::deep::GpuDeepCodeword, transcript: &mut T, coset_offset: &FieldElement, blowup_log: u32, final_poly_log_degree: u32, + layout: &crate::fri::terminal::FriFoldLayout, inv_twiddles: &[FieldElement], want_host: bool, ) -> Option<(Vec>, Vec>)> @@ -3718,8 +3955,13 @@ where if !n0.is_power_of_two() || n0 < 2 { return None; } - // The layer chain and its trees; the codeword is already resident. - let bytes = ext3_bytes(n0 as u64, 1).saturating_add(full_tree_bytes(n0 as u64)); + // The layer chain and its trees; the codeword is already resident. Under + // one row the input tree is built over the codeword itself (the one-row + // tree bound covers it and every later layer). + let bytes = ext3_bytes(n0 as u64, 1).saturating_add(tree_bytes_for( + n0 as u64, + if layout.one_row { 1 } else { 2 }, + )); if !admit_transient(n0, bytes, "R4 FRI commit (resident)") { return None; } @@ -3747,15 +3989,24 @@ where n0, blowup_log, final_poly_log_degree, + layout, want_host, ) } -/// The shared FRI commit loop over an initialized device state: per committed -/// layer sample ζ, fold + commit on device, D2H root/evals; then the terminal -/// fold and CPU coefficient extraction. Restores the transcript and returns -/// `None` on any mid-loop cudarc failure so the CPU path reruns cleanly. -#[allow(clippy::type_complexity)] +/// The shared FRI commit loop over an initialized device state, under the +/// proof format's fold `layout` (the caller's, built for this codeword by +/// [`crate::fri::terminal::FriFoldLayout::for_options`]): per committed layer +/// sample ζ, fold + commit on device, D2H root/evals; then the terminal folds +/// and CPU coefficient extraction. Restores the transcript and returns `None` +/// on any mid-loop cudarc failure so the CPU path reruns cleanly. +/// +/// The legacy encoding runs today's loop (one fold and a pair-leaf commit per +/// layer); the group encoding runs [`fri_commit_gpu_drive_groups`], the device +/// twin of `commit_phase_with_layout`'s pending-fold loop — one-row layouts +/// (S2) included, whose layer 0 is the input tree committed from the codeword +/// itself with no challenge before it. +#[allow(clippy::type_complexity, clippy::too_many_arguments)] fn fri_commit_gpu_drive( mut state: math_cuda::fri::FriCommitState, transcript: &mut T, @@ -3763,6 +4014,7 @@ fn fri_commit_gpu_drive( n0: usize, blowup_log: u32, final_poly_log_degree: u32, + layout: &crate::fri::terminal::FriFoldLayout, want_host: bool, ) -> Option<(Vec>, Vec>)> where @@ -3788,12 +4040,17 @@ where // produced had this dispatch never been called. let transcript_snapshot = transcript.clone(); - // Fold layout, shared with the CPU prover and the verifier — see `FriFoldLayout`. - let layout = crate::fri::terminal::FriFoldLayout::new( - n0.trailing_zeros(), - blowup_log, - final_poly_log_degree, - ); + // Fold layout, shared with the CPU prover and the verifier — see + // `FriFoldLayout`. It must be this codeword's: a layout built for another + // size degrades to the CPU path instead of committing a wrong chain. + if layout.terminal_len == 0 + || n0 + .trailing_zeros() + .checked_sub(layout.terminal_len.trailing_zeros()) + != Some(layout.total_folds) + { + return None; + } // The GPU path only runs above gpu_lde_threshold(). Two cases fall back to // the CPU path (which handles both correctly): tiny clamped traces // (total_folds == 0), and terminal_len == 1 (blowup_log + k == 0), whose @@ -3802,6 +4059,28 @@ where if layout.total_folds == 0 || layout.terminal_len < 2 { return None; } + // One-row layouts are never the legacy encoding (FriFoldLayout: one row ⇒ + // group encoding), so they always take the group drive below. + debug_assert!(!layout.one_row || !layout.is_legacy()); + if !layout.is_legacy() { + return fri_commit_gpu_drive_groups::( + state, + transcript, + transcript_snapshot, + coset_offset, + layout, + want_host, + ); + } + // Today's encoding: the layout is today's (the all-ones schedule). + debug_assert_eq!( + *layout, + crate::fri::terminal::FriFoldLayout::new( + n0.trailing_zeros(), + blowup_log, + final_poly_log_degree + ) + ); let num_committed = layout.num_committed; let mut fri_layer_list: Vec> = Vec::with_capacity(num_committed); @@ -3880,6 +4159,120 @@ where Some((final_poly_coeffs, fri_layer_list)) } +/// The raw limbs of `ζ, ζ², …, ζ^{2^{n−1}}`: the challenges of `n` successive +/// binary folds, squared on the host exactly as the CPU loop's `fold_times` +/// squares them. +fn zeta_powers_raw(zeta: &FieldElement, n: u32) -> Vec<[u64; 3]> { + let mut out = Vec::with_capacity(n as usize); + let mut z = zeta.clone(); + for level in 0..n { + // SAFETY: E == Ext3 (asserted by the drive before any call); its + // backing is [u64; 3]. + let p = &z as *const FieldElement as *const u64; + out.push(unsafe { [*p, *p.add(1), *p.add(2)] }); + if level + 1 < n { + z = z.square(); + } + } + out +} + +/// The group-encoding (S3) device commit loop: the device twin of +/// [`crate::fri::commit_phase_with_layout`]'s pending-fold loop. Per committed +/// layer `j` with exponent `d_j`: sample ζ, fold `d_{j−1}` times on device with +/// `ζ, ζ², …` (`d_{−1} = 1`, the binary fold 0 of the DEEP pair), commit the +/// result with leaves of `2^{d_j}` consecutive values, append the root; then +/// sample the final ζ and fold `d_last` times into the terminal codeword. +/// +/// One-row layouts (S2): `d_{−1} = 0` — layer 0 is the INPUT TREE, the resident +/// DEEP codeword itself committed with groups of `2^{d_0}` (a zero-fold group +/// commit, the group kernels), its root appended with NO challenge +/// sampled before it (the CPU loop's `pending = 0`); every later +/// layer is as above. +/// Transcript order, ζ powers, fold arithmetic and leaf bytes are the CPU +/// loop's, so the two produce the same proof (the parity tests pin it). +#[allow(clippy::type_complexity)] +fn fri_commit_gpu_drive_groups( + mut state: math_cuda::fri::FriCommitState, + transcript: &mut T, + transcript_snapshot: T, + coset_offset: &FieldElement, + layout: &crate::fri::terminal::FriFoldLayout, + want_host: bool, +) -> Option<(Vec>, Vec>)> +where + F: IsFFTField + IsField + IsSubFieldOf + 'static, + E: IsField + 'static + Send + Sync, + FieldElement: AsBytes, + FieldElement: AsBytes, + T: IsStarkTranscript + Clone, + B: DeviceTreeBackend, +{ + let mut fri_layer_list: Vec> = Vec::with_capacity(layout.num_committed); + // Folds owed before the next commit: fold 0 is the binary fold of the DEEP + // pair, so one; after committing layer `j`, `d_j`. Under one row nothing + // is owed before the input tree, and no challenge is drawn for it. + let mut pending: u32 = if layout.one_row { 0 } else { 1 }; + for &d in &layout.schedule { + let powers = if pending > 0 { + // <<<< Receive challenge zeta_j + let zeta: FieldElement = transcript.sample_field_element(); + zeta_powers_raw(&zeta, pending) + } else { + Vec::new() + }; + let (layer_evals_u64, evals_dev, dev_tree) = + match state.fold_and_commit_group(&powers, u32::from(d), want_host) { + Ok(v) => v, + Err(_) => { + *transcript = transcript_snapshot; + return None; + } + }; + let evaluation = layer_evals_u64 + .map(|v| u64_to_ext3_vec::(&v)) + .unwrap_or_default(); + let root = dev_tree.root; + fri_layer_list.push(FriLayer { + evaluation, + merkle_tree: MerkleTree::::from_root(root), + gpu_tree: Some(dev_tree), + gpu_evals: (!want_host).then_some(evals_dev), + }); + // >>>> Send commitment: [p_j] + transcript.append_bytes(&root); + pending = u32::from(d); + } + + // The final folds into the terminal codeword (total_folds > 0 here). + let zeta_final: FieldElement = transcript.sample_field_element(); + let terminal_evals_u64 = match state.fold_to_host(&zeta_powers_raw(&zeta_final, pending)) { + Ok(v) => v, + Err(_) => { + *transcript = transcript_snapshot; + return None; + } + }; + debug_assert_eq!(terminal_evals_u64.len(), layout.terminal_len * 3); + let terminal_codeword = u64_to_ext3_vec::(&terminal_evals_u64); + let terminal_offset = coset_offset.pow(1u64 << layout.total_folds); + let final_poly_coeffs = crate::fri::terminal::coeffs_from_terminal_codeword::( + &terminal_codeword, + &terminal_offset, + layout.effective_k, + ); + // >>>> Send the final polynomial coefficients. + for c in &final_poly_coeffs { + transcript.append_field_element(c); + } + + GPU_FRI_CALLS.fetch_add(1, Ordering::Relaxed); + if layout.one_row { + GPU_ONE_ROW_FRI_CALLS.fetch_add(1, Ordering::Relaxed); + } + Some((final_poly_coeffs, fri_layer_list)) +} + /// GPU FRI query phase: gather each layer's paths on device instead of walking /// host trees. For layer `l` and query `iota` the opened position is /// `(iota >> l) >> 1`, matching [`crate::fri::query_phase`]. Paths for all @@ -3985,6 +4378,128 @@ where Some(decommits) } +/// GPU FRI query phase for the group encoding (S3): the device twin of +/// [`crate::fri::query_phase_with_layout`]'s group branch. Per committed layer +/// `j` and query at position `p` the opened leaf is `p >> d_j`, its path is +/// gathered on device (one batched call per layer), and the opened values are +/// the whole group `[leaf·2^{d_j}, (leaf+1)·2^{d_j})` — read from the host evals +/// when the commit drained them, else one batched device gather per layer off +/// the resident evals; then `p ← p >> d_j`. +/// +/// Returns `None` when there are no layers or the layers are host trees (CPU +/// commit), so the caller takes the host walk. Resident layers have root-only +/// host trees, so a failed gather there is a hard abort, as in +/// [`try_fri_query_phase_gpu`]. +pub(crate) fn try_fri_query_phase_gpu_groups( + fri_layers: &[FriLayer], + iotas: &[usize], + layout: &crate::fri::terminal::FriFoldLayout, +) -> Option>> +where + E: IsField + 'static, + FieldElement: AsBytes + Sync + Send, + B: DeviceTreeBackend, +{ + if fri_layers.is_empty() { + return None; + } + let first_resident = fri_layers[0].gpu_tree.is_some(); + debug_assert!( + fri_layers + .iter() + .all(|l| l.gpu_tree.is_some() == first_resident), + "FRI layer residency must be all or nothing" + ); + if !first_resident { + return None; + } + assert_eq!( + fri_layers.len(), + layout.schedule.len(), + "one committed FRI layer per schedule entry" + ); + let stream = math_cuda::device::backend() + .expect("cuda backend for device-resident FRI query") + .next_stream(); + + // Per query, the position at each committed layer. + let positions: Vec> = iotas + .iter() + .map(|&iota| { + let mut p = iota; + layout + .schedule + .iter() + .map(|&d| { + let here = p; + p >>= d; + here + }) + .collect() + }) + .collect(); + + let mut per_layer_proofs: Vec>> = Vec::with_capacity(fri_layers.len()); + let mut per_layer_groups: Vec>>> = + Vec::with_capacity(fri_layers.len()); + for (j, (layer, &d)) in fri_layers.iter().zip(&layout.schedule).enumerate() { + let tree = layer + .gpu_tree + .as_ref() + .expect("FRI layers are device-resident as a group"); + let leaves: Vec = positions.iter().map(|p| p[j] >> d).collect(); + per_layer_proofs.push( + gather_proofs_dev(tree, &leaves, &stream) + .expect("device FRI-layer gather failed; resident tree has no host fallback"), + ); + per_layer_groups.push(if layer.evaluation.is_empty() { + let evals_dev = layer + .gpu_evals + .as_ref() + .expect("device-only FRI layer without resident evals"); + let n = 1usize << d; + let group_positions: Vec = leaves + .iter() + .flat_map(|&leaf| (leaf * n..(leaf + 1) * n).map(|x| x as u32)) + .collect(); + let raw = math_cuda::fri::gather_ext3_at(evals_dev, &group_positions, &stream) + .expect("device FRI group gather failed; no host fallback"); + Some( + crate::constraint_ir::gpu_interp::ext3_u64_to_field::(&raw) + .expect("resident FRI evals are Goldilocks ext3"), + ) + } else { + None + }); + } + + let values_per_query = layout.opened_values_per_query(); + let decommits = positions + .iter() + .enumerate() + .map(|(q, pos)| { + let mut values = Vec::with_capacity(values_per_query); + let mut paths = Vec::with_capacity(fri_layers.len()); + for (j, (layer, &d)) in fri_layers.iter().zip(&layout.schedule).enumerate() { + let n = 1usize << d; + match &per_layer_groups[j] { + Some(g) => values.extend_from_slice(&g[q * n..(q + 1) * n]), + None => { + let leaf = pos[j] >> d; + values.extend_from_slice(&layer.evaluation[leaf * n..(leaf + 1) * n]); + } + } + paths.push(per_layer_proofs[j][q].clone()); + } + FriDecommitment { + layers_auth_paths: paths, + layers_evaluations_sym: values, + } + }) + .collect(); + Some(decommits) +} + /// The abort itself, on a real device. `LAMBDA_VM_VRAM_BUDGET_MB` is read once /// at backend init, so this test runs in its own process with the budget /// lowered to 1 GiB — the shape is then over budget on any card while its host @@ -4030,6 +4545,7 @@ mod admission_box_tests { blowup, &weights, true, + 2, ); panic!( "the over-budget commit returned {} instead of aborting", @@ -4097,6 +4613,7 @@ mod split_tree_tests { split, true, true, + 2, ) .expect("GPU split path must engage above the threshold"); let pre_tree = pre_tree.expect("precomputed tree was requested"); diff --git a/crypto/stark/src/leaf_layout.rs b/crypto/stark/src/leaf_layout.rs new file mode 100644 index 000000000..a2134d87a --- /dev/null +++ b/crypto/stark/src/leaf_layout.rs @@ -0,0 +1,335 @@ +//! The trace-tree leaf layout of one table's proof (S2). +//! +//! Today every trace, precomputed, aux and composition tree commits one LDE +//! row PAIR per leaf (`commitment::ROWS_PER_LEAF = 2`): leaf `i` hashes the +//! bit-reversed rows `2i` and `2i + 1`, the points `x` and `−x`, and a query +//! opens that pair to rebuild the DEEP pair for the uncommitted FRI fold 0. +//! +//! Under one-row openings ([`LeafLayout::Row`]) every such tree commits ONE row +//! per leaf, the DEEP codeword itself is committed as FRI layer 0 (the "input +//! tree"), a query index ranges over the whole LDE (`r ∈ [0, N)`, bound `N`), +//! and the verifier computes DEEP at the one point `x_r` and checks it against +//! the input group's slot. +//! +//! The layout is decided PER TABLE by the proof format +//! ([`crate::proof::options::OneRowMode`]): `Off` = row pairs, `On` = one row, +//! `Auto` = whichever [`one_row_is_cheaper`] prices lower for this table's +//! committed widths and LDE size. Every input is AIR metadata or the trace +//! length the verifier already trusts for the FRI layout; none is read from +//! the proof's bytes. A proof may therefore mix layouts across tables, and +//! each table's layout is a verifier-side constant. +//! +//! [`LeafLayout::query_rows`] is the ONE place a query index becomes LDE rows: +//! every opening site, prover and verifier, goes through it. + +use crypto::merkle_tree::cap::{CapPolicy, cap_gain}; +use math::fft::bit_reversing::reverse_index; +use math::field::traits::{IsFFTField, IsField, IsSubFieldOf}; + +use crate::fri::schedule::{FRI_COST_WEIGHTS, FriFormat}; +use crate::proof::options::{OneRowMode, ProofOptions}; +use crate::traits::AIR; + +/// How many LDE rows one trace-tree leaf holds. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum LeafLayout { + /// Two bit-reversed rows per leaf, `(x, −x)`. Today's layout. + #[default] + RowPair, + /// One bit-reversed row per leaf (S2). + Row, +} + +impl LeafLayout { + /// `Row` iff `one_row`. + pub const fn from_one_row(one_row: bool) -> Self { + if one_row { Self::Row } else { Self::RowPair } + } + + /// Whether this is the one-row layout. + pub const fn is_one_row(self) -> bool { + matches!(self, Self::Row) + } + + /// Rows per leaf: 2 (today, [`crate::commitment::ROWS_PER_LEAF`]) or 1. + pub const fn rows_per_leaf(self) -> usize { + match self { + Self::RowPair => crate::commitment::ROWS_PER_LEAF, + Self::Row => 1, + } + } + + /// The exclusive bound of a query index over an LDE of `lde_len` points: + /// a leaf index, so `lde / 2` for row pairs and `lde` for one row + /// (under one row `r` must be uniform over ALL of `D₀`). + pub fn query_bound(self, lde_len: u64) -> u64 { + match self { + Self::RowPair => lde_len >> 1, + #[cfg(test)] + Self::Row + if M3_PAIR_BOUND_AT_LDE.load(core::sync::atomic::Ordering::SeqCst) == lde_len => + { + lde_len >> 1 + } + Self::Row => lde_len, + } + } + + /// Depth of a trace tree over an LDE of `2^lde_log` rows: one level per + /// bit of the leaf index (`log2(lde) − 1` for row pairs, `log2(lde)` for + /// one row; 0 when the leaf hash is the root). + pub const fn tree_depth(self, lde_log: usize) -> usize { + match self { + Self::RowPair => lde_log.saturating_sub(1), + Self::Row => lde_log, + } + } + + /// The LDE storage rows (natural-order indices into the LDE columns) that + /// query `q` opens: `(row, Some(sym_row))` for a row pair — the rows at + /// bit-reversed positions `2q` and `2q + 1`, the points `x` and `−x` — + /// and `(row, None)` for one row, the row at bit-reversed position `q`. + /// + /// The single site where a query index becomes rows. + pub fn query_rows(self, q: usize, lde_len: usize) -> (usize, Option) { + let n = lde_len as u64; + match self { + Self::RowPair => (reverse_index(q * 2, n), Some(reverse_index(q * 2 + 1, n))), + Self::Row => (reverse_index(q, n), None), + } + } +} + +/// Mutation M3, test builds only: sample one-row query indexes +/// over the row-pair bound `N / 2` — for an LDE of exactly this many points +/// (0 = off). Prover and verifier both read it, so a mutated proof still +/// verifies; only `one_row_tests`' bound test sees the bias, which is what +/// makes that test load-bearing. Process-global (the prover samples on worker +/// threads) and keyed by the LDE size, so it touches only the M3 test's own +/// shape (an LDE no other one-row test uses), never a concurrent test's proof. +#[cfg(test)] +pub(crate) static M3_PAIR_BOUND_AT_LDE: core::sync::atomic::AtomicU64 = + core::sync::atomic::AtomicU64::new(0); + +/// The committed widths of one table, in base-field elements per LDE row, per +/// tree. `0` = the tree does not exist. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct TableWidths { + /// The precomputed tree (preprocessed tables only). + pub precomputed: u64, + /// The main tree (every main column, or the multiplicities of a + /// preprocessed table). + pub main: u64, + /// The aux tree. + pub aux: u64, + /// The composition tree (every part). + pub composition: u64, + /// `XALU` rows of ONE in-guest DEEP point for this table + /// ([`deep_point_xalu_rows`]); row pairs evaluate DEEP at two points, one + /// row at one. + pub deep_point_rows: u64, +} + +/// `XALU` rows the in-guest verifier emits for DEEP at ONE query point +/// (`prover/src/lfm/deep.rs::emit_deep_point`), for a table whose DEEP +/// reconstruction folds `num_surviving` trace openings (the pruned OOD grid, +/// [`crate::ood::OodLayout::num_surviving`]) over `num_eval_points` OOD rows +/// and `num_parts` composition parts: +/// +/// ```text +/// per OOD row r: (|cols_r| − 1) Horner steps + [r ≥ 1] block scale +/// + numerator esub + denominator esub + ediv + (emul | emul_add) +/// parts: (P − 1) Horner steps + emul + esub + esub + ediv + emul_add +/// total: num_surviving + 4·E + P + 3 +/// ``` +/// +/// One `XALU` row per opened value plus a per-point constant; the prover +/// crate's `lfm::fri_group_tests` pins it against the emitter. +pub const fn deep_point_xalu_rows(num_surviving: u64, num_eval_points: u64, num_parts: u64) -> u64 { + num_surviving + 4 * num_eval_points + num_parts + 3 +} + +impl TableWidths { + /// The widths of `air`'s trees for a trace of `trace_length` rows. Main + /// and precomputed columns are base-field elements; aux columns and + /// composition parts are `FieldExtension` elements, each + /// `ext_degree::()` base elements wide. + pub fn of( + air: &dyn AIR, + trace_length: usize, + ) -> Self + where + F: IsFFTField + IsSubFieldOf + Send + Sync, + E: IsField + Send + Sync, + { + let precomputed = if air.is_preprocessed() { + air.num_precomputed_columns() + } else { + 0 + }; + let main = air.trace_layout().0.saturating_sub(precomputed); + let aux = air.num_auxiliary_rap_columns(); + let parts = if trace_length == 0 { + 0 + } else { + air.composition_poly_degree_bound(trace_length) / trace_length + }; + let ext = ext_degree::(); + let ctx = air.context(); + let num_eval_points = ctx.transition_offsets.len() * air.step_size(); + let ood = crate::ood::OodLayout::new( + ctx.trace_columns, + num_eval_points, + air.step_size(), + air.trace_ood_next_row_columns(), + ); + Self { + precomputed: precomputed as u64, + main: main as u64, + aux: (aux as u64).saturating_mul(ext), + composition: (parts as u64).saturating_mul(ext), + deep_point_rows: deep_point_xalu_rows( + ood.num_surviving() as u64, + num_eval_points as u64, + parts as u64, + ), + } + } +} + +/// Base-field elements per `E` element (3 for the Goldilocks cubic +/// extension, 1 when `E = F`). +fn ext_degree() -> u64 { + let f = core::mem::size_of::().max(1); + let e = core::mem::size_of::(); + (e / f).max(1) as u64 +} + +/// `Q ×` the per-query cost-law price of opening one trace tree whose leaf +/// holds `felts` base elements and whose tree is `depth` deep, under `cap`: +/// the leaf absorption, the walk (a compression and a select per level), and +/// minus what the tree's cap saves — the terms and weights of the FRI schedule +/// objective ([`crate::fri::schedule`]), applied to a trace tree. +pub fn trace_tree_cost_q(felts: u64, depth: u32, num_queries: u64, cap: CapPolicy) -> u64 { + if felts == 0 { + return 0; + } + let w = &FRI_COST_WEIGHTS.cap; + let blocks = felts + .div_ceil(crate::fri::schedule::FRI_LEAF_RATE_FELTS) + .max(1) as i128; + let per_query = + blocks * w.compress as i128 + i128::from(depth) * (w.compress as i128 + w.select as i128); + let queries = usize::try_from(num_queries).unwrap_or(usize::MAX); + let c = cap.height(queries, depth as usize); + let total = (num_queries as i128).saturating_mul(per_query) - cap_gain(w, queries, c); + u64::try_from(total.max(0)).unwrap_or(u64::MAX) +} + +/// `Q ×` the per-query price of one table's openings (every trace tree plus +/// the FRI chain) under `one_row`, for an LDE of `2^lde_log` rows with blowup +/// `2^blowup_log` and terminal log-degree `k`, under `options`' FRI mode, cap +/// policy and query count. +/// +/// Row pairs: every tree's leaf holds two rows and is `lde_log − 1` deep, the +/// FRI chain starts at `lde_log − 1` with the uncommitted fold 0 +/// ([`FriFormat::chain_cost_q`]), and DEEP is evaluated at TWO points (`υ`, +/// `−υ`). One row: every leaf holds one row and is `lde_log` deep, the FRI +/// chain (layer 0 = the committed DEEP codeword) starts at `lde_log`, and DEEP +/// is evaluated at ONE point. A DEEP point costs +/// [`TableWidths::deep_point_rows`] `XALU` rows. +pub fn table_openings_cost_q( + widths: &TableWidths, + options: &ProofOptions, + lde_log: u32, + blowup_log: u32, + one_row: bool, +) -> u64 { + let q = options.fri_number_of_queries as u64; + let cap = options.format.merkle_cap; + let layout = LeafLayout::from_one_row(one_row); + let rows = layout.rows_per_leaf() as u64; + let depth = layout.tree_depth(lde_log as usize) as u32; + let trees = [ + widths.precomputed, + widths.main, + widths.aux, + widths.composition, + ] + .iter() + .map(|&w| trace_tree_cost_q(w.saturating_mul(rows), depth, q, cap)) + .fold(0u64, u64::saturating_add); + + let terminal_log = (blowup_log + u32::from(options.fri_final_poly_log_degree)).min(lde_log); + let fmt = FriFormat { + mode: options.format.fri_mode, + one_row, + num_queries: q, + cap, + schedule_override: options.format.fri_schedule_override, + }; + let chain = fmt.chain_cost_q(lde_log, terminal_log); + let deep_points: u64 = if one_row { 1 } else { 2 }; + let deep = q + .saturating_mul(deep_points) + .saturating_mul(widths.deep_point_rows) + .saturating_mul(FRI_COST_WEIGHTS.xalu); + trees.saturating_add(chain).saturating_add(deep) +} + +/// The per-table `auto` rule: one row iff it is STRICTLY cheaper than row pairs +/// under [`table_openings_cost_q`] (a tie keeps today's layout). +pub fn one_row_is_cheaper( + widths: &TableWidths, + options: &ProofOptions, + lde_log: u32, + blowup_log: u32, +) -> bool { + table_openings_cost_q(widths, options, lde_log, blowup_log, true) + < table_openings_cost_q(widths, options, lde_log, blowup_log, false) +} + +/// The leaf layout of a table with committed `widths` over an LDE of +/// `2^lde_log` rows (blowup `2^blowup_log`) under `options`' format. +pub fn resolve_leaf_layout( + widths: &TableWidths, + options: &ProofOptions, + lde_log: u32, + blowup_log: u32, +) -> LeafLayout { + match options.format.one_row { + OneRowMode::Off => LeafLayout::RowPair, + OneRowMode::On => LeafLayout::Row, + OneRowMode::Auto => { + LeafLayout::from_one_row(one_row_is_cheaper(widths, options, lde_log, blowup_log)) + } + } +} + +/// ★ The leaf layout of `air`'s proof over a trace of `trace_length` rows — +/// what the prover and the host verifier both call. The format comes from +/// `air.options()` (a verifier-side constant), the widths from the AIR, and +/// the length from the trace (the verifier's `proof.trace_length()`, the same +/// value its FRI layout already trusts). +pub fn table_leaf_layout( + air: &dyn AIR, + trace_length: usize, +) -> LeafLayout +where + F: IsFFTField + IsSubFieldOf + Send + Sync, + E: IsField + Send + Sync, +{ + let options = air.options(); + if options.format.one_row == OneRowMode::Off { + return LeafLayout::RowPair; + } + let blowup = options.blowup_factor as usize; + let lde_log = (trace_length.saturating_mul(blowup)).trailing_zeros(); + let blowup_log = blowup.trailing_zeros(); + resolve_leaf_layout( + &TableWidths::of(air, trace_length), + options, + lde_log, + blowup_log, + ) +} diff --git a/crypto/stark/src/lib.rs b/crypto/stark/src/lib.rs index 8888b30a6..bdfe6c87d 100644 --- a/crypto/stark/src/lib.rs +++ b/crypto/stark/src/lib.rs @@ -21,9 +21,11 @@ pub mod gpu_lde; pub mod grinding; #[cfg(feature = "instruments")] pub mod instruments; +pub mod leaf_layout; #[cfg(feature = "cuda")] pub mod logup_gpu; pub mod lookup; +pub mod merkle_caps; pub mod multilinear_air; pub mod multilinear_logup; pub mod multilinear_table; @@ -35,6 +37,8 @@ pub mod prove_split; pub mod prover; pub mod r4_denoms; pub mod residency_mode; +#[cfg(all(feature = "cuda", any(test, feature = "test-utils")))] +pub mod s2_device_parity; #[cfg(feature = "disk-spill")] pub mod storage_mode; pub mod table; diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index daba6fb0d..664c0510f 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -847,17 +847,34 @@ impl BusValue { /// tree each, and there are two dozen of them. /// /// [`precomputed_columns`]: crate::traits::AIR::precomputed_columns +/// +/// # One root per leaf layout (S2) +/// +/// The root depends on the trace trees' leaf layout +/// ([`crate::leaf_layout::LeafLayout`]), so a commitment carries a separate, +/// separately cached source for the one-row layout. [`get`](Self::get) is +/// today's (row-pair) root, unchanged; [`get_for`](Self::get_for) serves +/// either and returns `None` for a layout this commitment has no source for +/// (the prover then refuses and the verifier rejects; never a silent recompute). #[derive(Clone)] pub struct LazyCommitment { value: std::sync::Arc>, #[allow(clippy::type_complexity)] build: std::sync::Arc crate::config::Commitment + Send + Sync>, + /// The one-row root: `None` = no source (every constructor but + /// [`with_one_row`](Self::with_one_row)). + #[allow(clippy::type_complexity)] + one_row: Option<( + std::sync::Arc>>, + std::sync::Arc Option + Send + Sync>, + )>, } impl std::fmt::Debug for LazyCommitment { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LazyCommitment") .field("computed", &self.value.get().is_some()) + .field("one_row_source", &self.one_row.is_some()) .finish() } } @@ -870,6 +887,7 @@ impl LazyCommitment { Self { value: std::sync::Arc::new(cell), build: std::sync::Arc::new(|| [0u8; 32]), + one_row: None, } } @@ -879,12 +897,45 @@ impl LazyCommitment { Self { value: std::sync::Arc::new(std::sync::OnceLock::new()), build: std::sync::Arc::new(build), + one_row: None, } } + /// This commitment plus a source for the ONE-ROW layout's root, computed + /// on the first [`get_for`](Self::get_for)`(Row)` and cached like the + /// row-pair one. The source returns `None` when it has no root for that + /// layout (e.g. a static table with no one-row entry): a hard miss, never + /// a fallback to the row-pair root. + pub fn with_one_row( + mut self, + build: impl Fn() -> Option + Send + Sync + 'static, + ) -> Self { + self.one_row = Some(( + std::sync::Arc::new(std::sync::OnceLock::new()), + std::sync::Arc::new(build), + )); + self + } + + /// Today's (row-pair) root. pub fn get(&self) -> crate::config::Commitment { *self.value.get_or_init(|| (self.build)()) } + + /// The root under `layout`; `None` when this commitment has no source for + /// it. + pub fn get_for( + &self, + layout: crate::leaf_layout::LeafLayout, + ) -> Option { + match layout { + crate::leaf_layout::LeafLayout::RowPair => Some(self.get()), + crate::leaf_layout::LeafLayout::Row => { + let (cell, build) = self.one_row.as_ref()?; + *cell.get_or_init(|| build()) + } + } + } } pub struct AirWithBuses< @@ -1100,6 +1151,17 @@ impl< self } + /// Give this AIR's preprocessed commitment a ONE-ROW (S2) root: `root` is + /// what [`AIR::precomputed_commitment_for`](crate::traits::AIR::precomputed_commitment_for) + /// returns for [`LeafLayout::Row`](crate::leaf_layout::LeafLayout::Row) + /// (`None` = a hard miss). A no-op on an AIR that is not preprocessed. + pub fn with_one_row_commitment(mut self, root: Option) -> Self { + if let Some(c) = self.preprocessed_commitment.take() { + self.preprocessed_commitment = Some(c.with_one_row(move || root)); + } + self + } + /// Supply a constraint program captured at BUILD time, so this AIR never /// has to capture one. /// @@ -1552,6 +1614,18 @@ where .unwrap_or([0u8; 32]) } + fn precomputed_commitment_for( + &self, + layout: crate::leaf_layout::LeafLayout, + ) -> Option { + match &self.preprocessed_commitment { + Some(c) => c.get_for(layout), + // Not preprocessed: the row-pair answer is the trait's zero root + // (never compared); there is no one-row root to give. + None => (!layout.is_one_row()).then_some([0u8; 32]), + } + } + fn precomputed_columns(&self) -> Vec>> { self.precomputed_columns .as_ref() diff --git a/crypto/stark/src/merkle_caps.rs b/crypto/stark/src/merkle_caps.rs new file mode 100644 index 000000000..9f182f4a7 --- /dev/null +++ b/crypto/stark/src/merkle_caps.rs @@ -0,0 +1,235 @@ +//! Merkle caps of a univariate STARK proof (lever S1). +//! +//! Every tree of a proof is opened once per query: the trace trees (main, +//! precomputed, aux), the composition tree and each committed FRI layer. Under +//! a cap policy a tree of depth `D` gets a height-`c` cap +//! (`CapPolicy::height(num_queries, D)`); its `2^c` cap nodes ride at the end +//! of the authentication path of the tree's FIRST opening in proof order (the +//! "owner path"), and every path of that tree is cut to `D − c` siblings. +//! +//! [`StarkCaps`] is the one place the heights are computed from public shape +//! data (the policy, the query count, `log2(lde)` and the committed FRI layer +//! count). The prover embeds with it and the verifier checks with it, so the +//! split point of every path is a verifier constant, never read from a proof. +//! +//! [`TreeCheck`] is the verifier's per-tree check: built ONCE per tree (the +//! owner path's length and its cap-to-root check), then used for every query. +//! At `c = 0` it never touches the owner opening and is exactly the uncapped +//! exact-length check, so the legacy format verifies the same bytes. + +use crypto::merkle_tree::cap::{CapPolicy, CappedRoot}; +use crypto::merkle_tree::traits::IsMerkleTreeBackend; + +use crate::config::Commitment; + +/// The cap height of every tree of one table's proof. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StarkCaps { + /// Depth of the trace, precomputed, aux and composition trees. + pub trace_depth: usize, + /// Cap height of those four trees (they share depth and opening count). + pub trace: usize, + /// Depth of committed FRI layer `i`. + pub fri_depths: Vec, + /// Cap height of committed FRI layer `i`. + pub fri: Vec, +} + +impl StarkCaps { + /// Depth of the trace, precomputed, aux and composition trees: a leaf is a + /// row PAIR, so `lde / 2` leaves and `log2(lde) − 1` levels (0 for a + /// two-point LDE, where the leaf hash is the root). + pub fn trace_tree_depth(lde_log: usize) -> usize { + lde_log.saturating_sub(1) + } + + /// Depth of committed FRI layer `i`: it holds `lde / 2^(i+1)` values in + /// pair leaves, so `log2(lde) − i − 2` levels. + pub fn fri_layer_depth(lde_log: usize, layer: usize) -> usize { + lde_log.saturating_sub(layer + 2) + } + + /// The heights for a proof with `num_queries` queries over an LDE of + /// `2^lde_log` points and `num_committed` committed FRI layers of today's + /// PAIR layout (layer `i` is `log2(lde) − i − 2` deep). Every tree is + /// opened `num_queries` times. A proof under a fold schedule + /// (`fri = dp`) has other layer depths: use [`Self::for_options`]. + pub fn new( + policy: CapPolicy, + num_queries: usize, + lde_log: usize, + num_committed: usize, + ) -> Self { + let trace_depth = Self::trace_tree_depth(lde_log); + let fri_depths: Vec = (0..num_committed) + .map(|i| Self::fri_layer_depth(lde_log, i)) + .collect(); + let fri = fri_depths + .iter() + .map(|&d| policy.height(num_queries, d)) + .collect(); + Self { + trace_depth, + trace: policy.height(num_queries, trace_depth), + fri_depths, + fri, + } + } + + /// The heights of a table proved under `options` over an LDE of + /// `2^lde_log` points, with the table's RESOLVED leaf layout `one_row` + /// (the same resolution `FriFoldLayout::for_options` takes): the trace + /// trees are the layout's depth and the committed FRI layers are the ones + /// the proof format's fold layout commits, so under a fold schedule + /// (`fri = dp`) layer `j` is `layer_depth(j)` deep, not `log2(lde) − j − 2`. + /// + /// This is the one public entry point the in-guest verifier checks its own + /// cap heights against (row pairs only there: `one_row = false`). `Err` + /// for a format that cannot be laid out. + pub fn for_options( + options: &crate::proof::options::ProofOptions, + lde_log: usize, + one_row: bool, + ) -> Result { + let blowup_log = (options.blowup_factor as u32).trailing_zeros(); + let layout = crate::fri::terminal::FriFoldLayout::for_options( + lde_log as u32, + blowup_log, + options, + one_row, + )?; + Ok(Self::from_layout( + options.format.merkle_cap, + options.fri_number_of_queries, + lde_log, + &layout, + )) + } + + /// The heights over an explicit FRI fold layout (the prover's and the + /// verifier's route: each holds the layout it built from the options). + /// The trace trees take the layout's leaf layout (row pairs: `log2(lde) − + /// 1`; one row: `log2(lde)`) and committed layer `j` is + /// `layout.layer_depth(lde_log, j)` deep: `log2(lde) − j − 2` under the + /// all-ones row-pair schedule (so this is [`Self::new`] there), the group + /// tree's depth under any other. + pub(crate) fn from_layout( + policy: CapPolicy, + num_queries: usize, + lde_log: usize, + layout: &crate::fri::terminal::FriFoldLayout, + ) -> Self { + Self::with_depths( + policy, + num_queries, + crate::leaf_layout::LeafLayout::from_one_row(layout.one_row).tree_depth(lde_log), + layout.layer_depths(lde_log as u32), + ) + } + + /// The heights for trace trees of depth `trace_depth` and committed FRI + /// layers of depths `fri_depths`, every tree opened `num_queries` times. + /// + /// The general form of [`Self::new`], for any leaf layout and FRI + /// schedule: the caller passes the depths its layout implies + /// ([`crate::leaf_layout::LeafLayout::tree_depth`] and the FRI layout's + /// per-layer depths). At row pairs and the all-ones schedule those are + /// exactly [`Self::new`]'s. + pub fn with_depths( + policy: CapPolicy, + num_queries: usize, + trace_depth: usize, + fri_depths: Vec, + ) -> Self { + let fri = fri_depths + .iter() + .map(|&d| policy.height(num_queries, d)) + .collect(); + Self { + trace_depth, + trace: policy.height(num_queries, trace_depth), + fri_depths, + fri, + } + } + + /// True when some tree has a cap (`c > 0`). + pub fn any(&self) -> bool { + self.trace > 0 || self.fri.iter().any(|&c| c > 0) + } +} + +/// The verifier's check for one tree: its authenticated cap, plus the owner +/// opening's own siblings when the tree is capped. +#[derive(Clone, Copy, Debug)] +pub struct TreeCheck<'a> { + capped: CappedRoot<'a, Commitment>, + /// `Some` iff `c > 0`: the owner path minus its cap. Query 0 of this tree + /// is checked with these siblings. + owner_siblings: Option<&'a [Commitment]>, +} + +impl<'a> TreeCheck<'a> { + /// Build the check of one tree of depth `depth` and cap height + /// `cap_height` against `root`. + /// + /// At `c = 0` the owner opening is never read (`owner_path` is not + /// called): the check is the exact-length full-path check, and the default + /// format touches no index a count guard has not covered. At `c > 0`, + /// `owner_path` must return the tree's first opening's path (`None` when + /// the proof has none, which rejects); its length must be exactly + /// `D − c + 2^c` and its cap must hash to `root`. + pub fn build>( + root: &'a Commitment, + depth: usize, + cap_height: usize, + owner_path: impl FnOnce() -> Option<&'a [Commitment]>, + ) -> Option { + if cap_height == 0 { + return Some(Self { + capped: CappedRoot::uncapped(root, depth), + owner_siblings: None, + }); + } + let (capped, siblings) = + CappedRoot::from_owner::(root, owner_path()?, depth, cap_height)?; + Some(Self { + capped, + owner_siblings: Some(siblings), + }) + } + + /// Check query `query`'s opening of this tree: `path` as the proof carries + /// it, the transcript's leaf `index`, and the leaf hash of the opened + /// values. Query 0 of a capped tree is the owner: its siblings are the + /// owner path minus the cap (split once in [`build`](Self::build)); every + /// other query's path must be exactly `D − c` long. + pub fn verify>( + &self, + query: usize, + path: &[Commitment], + index: usize, + leaf_hash: Commitment, + ) -> bool { + let siblings = match (query, self.owner_siblings) { + (0, Some(owner)) => owner, + _ => path, + }; + self.capped.verify::(siblings, index, leaf_hash) + } + + pub fn cap_height(&self) -> usize { + self.capped.cap_height() + } +} + +/// The checks of every tree of one table's proof. +#[derive(Clone, Debug)] +pub struct TableTreeChecks<'a> { + pub main: TreeCheck<'a>, + pub precomputed: Option>, + pub aux: Option>, + pub composition: TreeCheck<'a>, + /// One per committed FRI layer, in layer order. + pub fri: Vec>, +} diff --git a/crypto/stark/src/multilinear_air.rs b/crypto/stark/src/multilinear_air.rs index bf4457a9b..5173cbf4f 100644 --- a/crypto/stark/src/multilinear_air.rs +++ b/crypto/stark/src/multilinear_air.rs @@ -1736,6 +1736,7 @@ mod tests { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: multilinear::whir_chain::ChainFormat::DEFAULT, }; // Domain in the base field, columns in the degree-3 extension. let n_stack = constraint_argument::one_stack(num_vars, layout.columns.len()); diff --git a/crypto/stark/src/multilinear_table.rs b/crypto/stark/src/multilinear_table.rs index 674288098..4c350dd0d 100644 --- a/crypto/stark/src/multilinear_table.rs +++ b/crypto/stark/src/multilinear_table.rs @@ -1670,6 +1670,7 @@ mod tests { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: multilinear::whir_chain::ChainFormat::DEFAULT, } } diff --git a/crypto/stark/src/proof/options.rs b/crypto/stark/src/proof/options.rs index 15e2c8909..1262b38a5 100644 --- a/crypto/stark/src/proof/options.rs +++ b/crypto/stark/src/proof/options.rs @@ -1,4 +1,7 @@ use core::fmt; +use core::str::FromStr; + +pub use crypto::merkle_tree::cap::CapPolicy; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::wasm_bindgen; @@ -39,6 +42,19 @@ impl fmt::Display for ProofOptionsError { /// - `coset_offset`: the offset for the coset /// - `grinding_factor`: the number of leading zeros that we want for the Hash(hash || nonce) /// - `fri_final_poly_log_degree`: log2 degree bound at which FRI terminates folding +/// - `format`: the proof FORMAT ([`ProofFormat`], the ZF proof-format levers). +/// Its default is the legacy format (every lever off), byte for byte. +/// +/// # The format is not serialized +/// +/// `format` is skipped by serde and rkyv (and restored to its default on +/// deserialize), so a serialized `ProofOptions` has exactly the bytes it had +/// before the field existed. Nothing repo-wide was found to serialize a +/// `ProofOptions` into pinned bytes (the one by-value holder, `AirContext`, +/// derives neither), and skipping it makes that true by construction rather +/// than by search. The format is a verifier-side constant: it comes from the +/// code that builds the options, never from bytes a prover supplied — a +/// proof never carries it. #[cfg_attr(feature = "wasm", wasm_bindgen)] #[derive( Clone, @@ -58,9 +74,256 @@ pub struct ProofOptions { /// polynomial has degree < 2^fri_final_poly_log_degree; the prover sends those /// 2^k coefficients instead of folding to a constant. pub fri_final_poly_log_degree: u8, + /// The proof format. [`ProofFormat::DEFAULT`] = the legacy format (the + /// production format is stamped on by the prover crate). Not serialized. + #[serde(skip)] + #[rkyv(with = rkyv::with::Skip)] + #[cfg_attr(feature = "wasm", wasm_bindgen(skip))] + pub format: ProofFormat, +} + +/// The proof-format levers of a univariate STARK proof. Grouped so a literal +/// `ProofOptions` names the format in one line (`format: ProofFormat::DEFAULT`) +/// and a lever added later touches this struct only. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct ProofFormat { + /// Merkle cap policy for every tree of the proof (S1). `Off` = today. + pub merkle_cap: CapPolicy, + /// FRI fold schedule of the committed layers (S3). `Pair` = today. + pub fri_mode: FriMode, + /// One-row trace openings with a committed FRI input (S2). `Off` = today. + pub one_row: OneRowMode, + /// An explicit committed-layer fold schedule that replaces the DP's under + /// [`FriMode::Dp`] (ignored under [`FriMode::Pair`]). `None` = the DP. + /// + /// A TEST HOOK: it lets round-trip tests prove and verify schedules the DP + /// never picks (unequal neighbouring exponents such as `[1, 3]`, the only + /// shape that catches a fold-count off-by-one). No knob sets it — the + /// `ZF FORMAT` parser always leaves it `None` — and like every format + /// field it is a verifier-side constant, never read from a proof. A + /// schedule that does not cover the table's committed folds exactly is a + /// proving error and a verification failure, never a silent fallback. + pub fri_schedule_override: Option, +} + +impl ProofFormat { + /// This crate's default: every lever off, i.e. [`Self::LEGACY`]. + /// + /// ⚠ NOT the production format. The prover crate's + /// `zf_format::ZfFormat::DEFAULT` (the measured configuration) is stamped + /// onto the options at the production sites; a `ProofOptions` built here + /// without a format, or deserialized (the format is not serialized), is + /// the legacy format. + pub const DEFAULT: Self = Self::LEGACY; + + /// The legacy format: every lever off. The only format the RV64 + /// recursion guest verifies. + pub const LEGACY: Self = Self { + merkle_cap: CapPolicy::Off, + fri_mode: FriMode::Pair, + one_row: OneRowMode::Off, + fri_schedule_override: None, + }; + + /// True when this is this crate's default format, [`Self::LEGACY`] + /// (`Fixed(0)` counts as `Off`). + pub fn is_default(&self) -> bool { + self.is_legacy() + } + + /// True when every lever is off (`Fixed(0)` counts as `Off`): the proof + /// this produces is the legacy format, byte for byte. + pub fn is_legacy(&self) -> bool { + self.merkle_cap.is_off() + && self.fri_mode == FriMode::Pair + && self.one_row == OneRowMode::Off + && self.fri_schedule_override.is_none() + } +} + +/// Longest schedule a [`FriScheduleOverride`] holds. +pub const FRI_SCHEDULE_OVERRIDE_MAX: usize = 32; + +/// An explicit FRI fold schedule (see [`ProofFormat::fri_schedule_override`]): +/// the fold exponent of each committed layer, first committed layer first. +/// Fixed capacity so [`ProofFormat`] stays `Copy`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct FriScheduleOverride { + len: u8, + exponents: [u8; FRI_SCHEDULE_OVERRIDE_MAX], } +impl FriScheduleOverride { + /// `None` if `schedule` is longer than [`FRI_SCHEDULE_OVERRIDE_MAX`]. The + /// exponents themselves are validated where the layout is built (each in + /// `1..=FRI_SCHEDULE_DMAX`, summing to the table's committed folds). + pub fn new(schedule: &[u8]) -> Option { + if schedule.len() > FRI_SCHEDULE_OVERRIDE_MAX { + return None; + } + let mut exponents = [0u8; FRI_SCHEDULE_OVERRIDE_MAX]; + exponents[..schedule.len()].copy_from_slice(schedule); + Some(Self { + len: schedule.len() as u8, + exponents, + }) + } + + /// The schedule. + pub fn as_slice(&self) -> &[u8] { + &self.exponents[..self.len as usize] + } +} + +/// How the committed FRI layers fold (S3). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum FriMode { + /// One binary fold per committed layer, pair leaves. Today's format. + #[default] + Pair, + /// Folds of `2^d` per committed layer, `d` chosen by the verifier-side DP. + Dp, +} + +impl FriMode { + /// The knob spelling (`LAMBDA_VM_ZF_FRI`). + pub const fn name(self) -> &'static str { + match self { + Self::Pair => "pair", + Self::Dp => "dp", + } + } +} + +impl fmt::Display for FriMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} + +impl FromStr for FriMode { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "pair" => Ok(Self::Pair), + "dp" => Ok(Self::Dp), + _ => Err(()), + } + } +} + +/// Whether the trace trees commit one LDE row per leaf (S2). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum OneRowMode { + /// Row-pair leaves, the DEEP pair rebuilt from trace openings. Today's format. + #[default] + Off, + /// One-row leaves and a committed FRI-input tree for every table. + On, + /// Per table, whichever the cost model prefers from the AIR's widths. + Auto, +} + +impl OneRowMode { + /// The knob spelling (`LAMBDA_VM_ZF_ONE_ROW`). + pub const fn name(self) -> &'static str { + match self { + Self::Off => "0", + Self::On => "1", + Self::Auto => "auto", + } + } +} + +impl fmt::Display for OneRowMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} + +impl FromStr for OneRowMode { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "0" => Ok(Self::Off), + "1" => Ok(Self::On), + "auto" => Ok(Self::Auto), + _ => Err(()), + } + } +} + +/// Which format levers THIS build implements. A lever that is only parsed — +/// its field exists so the option structs and the `ZF FORMAT` banner stay +/// stable before the lever lands — must not be selectable, or a run +/// could print a non-default format and prove the default one. Each flag +/// is flipped in the commit that makes the lever real. +/// +/// The Merkle cap is real on the host and device STARK provers, the host +/// verifier and the LFM in-guest STARK verifier +/// (`lfm::merkle_cap::CapCells`, one caps arena per sub-proof), on pair and on +/// group-leaf (`Dp`) FRI layers alike. The RV64 recursion guest stays +/// legacy-only: its archived verifier refuses any other format. +pub const MERKLE_CAP_IMPLEMENTED: bool = true; + +/// `FriMode::Dp` (S3) is implemented on the prover paths and the host verifier: +/// - the CPU prover (group-leaf layer commits, the scheduled folds, group +/// openings) and the host verifier (`multi_verify` / `multi_verify_archived`); +/// - on a `cuda` build the device FRI arms (DEEP→FRI on device, the device +/// layer commit, the device query gather) run both encodings: the group +/// loop (`gpu_lde::fri_commit_gpu_drive_groups`, +/// `math_cuda::fri::FriCommitState::fold_and_commit_group`) is the CPU +/// loop's device twin, byte for byte (`tests::zf_fri_device_tests`). +/// +/// - the in-guest (LFM) STARK verifier (G1 + G2): `lfm::fri::FriShape` takes +/// the same schedule, and the emitter verifies group layers (slot check, +/// group leaf, group fold), so an LFM wrap or node verifies a `Dp` proof. +/// +/// NOT implemented: the RV64 recursion guest (legacy-only; it +/// refuses a non-legacy format). +pub const FRI_MODE_IMPLEMENTED: bool = true; + +/// `OneRowMode::{On, Auto}` (S2) is implemented on the prover (CPU and +/// device) and the host verifier: +/// - the CPU prover (one-row trace, precomputed, aux and composition trees; +/// the DEEP codeword committed as FRI layer 0 before the first challenge; +/// query indexes over the whole LDE; one-row openings) and the host +/// verifier (`multi_verify` / `multi_verify_archived`), with the per-table +/// `Auto` rule (`crate::leaf_layout`); +/// - the preprocessed roots: static one-row twins at blowup 4 +/// (`STATIC_BLOWUP_FACTORS_ONE_ROW` in the prover crate), every computed +/// root at run time, the LFM artifacts' one-row roots and the registry +/// policy (a one-row format never reads `LFM_REGISTRY`); a table with no +/// root for its layout is a proving error and a verifier reject, never a recompute +/// — e.g. `one_row = 1` at blowup 2, 8 or 16 fails on BITWISE; +/// - the device: one-row trees for the fused main commit, +/// the preprocessed split, the aux commits (host input and resident) and the +/// composition tree, device openings at row `r`, the LFM artifact commit, +/// and the input tree committed from the resident DEEP codeword before the +/// first challenge — each proof byte-identical to the CPU one; a one-row +/// table may be device-only like a row-pair one, and under `Auto` one proof +/// mixes both layouts on the device. +/// +/// NOT implemented: the in-guest (LFM) verifier of a one-row proof (an emitter +/// asked for one refuses at emit time, `lfm::fri::FriShape::from_options`), +/// and the RV64 recursion guest (legacy-only). A block run under +/// `LAMBDA_VM_ZF_ONE_ROW` therefore proves and host-verifies its STARK and +/// LFM proofs but cannot recurse over one-row STARK proofs yet. +pub const ONE_ROW_IMPLEMENTED: bool = true; + impl ProofOptions { + /// True when every format field is at this crate's default (the legacy + /// format): the proof this produces is the legacy format, byte for + /// byte. + pub fn has_default_format(&self) -> bool { + self.format.is_default() + } + + /// True when every lever is off: [`ProofFormat::LEGACY`]. + pub fn has_legacy_format(&self) -> bool { + self.format.is_legacy() + } + /// Default proof options used for testing purposes. /// These options should never be used in production. pub fn default_test_options() -> Self { @@ -70,6 +333,7 @@ impl ProofOptions { coset_offset: 3, grinding_factor: 1, fri_final_poly_log_degree: DEFAULT_FRI_FINAL_POLY_LOG_DEGREE, + format: ProofFormat::DEFAULT, } } } @@ -130,6 +394,7 @@ impl GoldilocksCubicProofOptions { coset_offset: 3, grinding_factor, fri_final_poly_log_degree: DEFAULT_FRI_FINAL_POLY_LOG_DEGREE, + format: ProofFormat::DEFAULT, }) } } diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index a457f0995..ca4329fb2 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -26,6 +26,7 @@ use rayon::prelude::{IntoParallelIterator, ParallelIterator}; #[cfg(feature = "debug-checks")] use crate::debug::validate_trace; use crate::fri; +use crate::leaf_layout::LeafLayout; use crate::lookup::LOGUP_NUM_CHALLENGES; use crate::proof::stark::{DeepPolynomialOpenings, PolynomialOpenings}; use crate::residency_mode::ResidencyMode; @@ -44,7 +45,6 @@ use super::proof::stark::{DeepPolynomialOpening, MultiProof, StarkProof}; use super::trace::TraceTable; use super::traits::AIR; use crypto::merkle_tree::merkle::MerkleTree; -#[cfg(feature = "cuda")] use crypto::merkle_tree::proof::Proof; use crypto::merkle_tree::traits::{IsMerkleTreeBackend, IsStreamingLeafBackend}; @@ -102,6 +102,12 @@ pub enum ProvingError { /// proof an honest verifier always rejects — fail fast on the prover side /// with a localized error instead. PrecomputedCommitmentMismatch, + /// The AIR has no preprocessed commitment for the table's leaf layout + /// (S2: a one-row layout whose static root was never generated). A hard + /// error, never a silent recompute: proving on would either + /// take the other layout's root — a proof every verifier rejects — or + /// rebuild a whole preprocessed LDE and tree behind the operator's back. + PrecomputedCommitmentMissing(String), /// I/O failure while spilling prover state (traces, LDE, Merkle trees) to disk: /// out of disk space, fd exhaustion, or mmap failure. #[cfg(feature = "disk-spill")] @@ -227,7 +233,13 @@ where /// the O(n) scan for the least-recently-used entry costs less than any ordering /// structure would. type PrecomputedTreeMap = - std::collections::HashMap)>; + std::collections::HashMap)>; + +/// The cache key: the root AND the trees' rows per leaf (S2). The root alone +/// already differs between leaf layouts (a one-row leaf hashes other bytes), +/// so two layouts cannot alias; the layout is in the key anyway so that +/// argument is not a hash-collision argument. +type PrecomputedTreeKey = (Commitment, usize); fn precomputed_tree_cache() -> &'static Mutex { static CACHE: OnceLock> = OnceLock::new(); @@ -284,7 +296,7 @@ pub fn precomputed_tree_cache_stats() -> (usize, u64, u64, u64) { /// a check that cannot fail. fn precomputed_tree_insert_capped( map: &mut PrecomputedTreeMap, - root: Commitment, + root: PrecomputedTreeKey, tree: Arc, cap: Option, ) { @@ -365,7 +377,9 @@ pub fn precomputed_tree_cache_hit_miss() -> (u64, u64) { pub(crate) fn precomputed_tree_cache_get( root: &Commitment, + rows_per_leaf: usize, ) -> Option>> { + let root = &(*root, rows_per_leaf); let mut cache = precomputed_tree_cache().lock().unwrap(); let out = cache .get(root) @@ -392,11 +406,12 @@ pub(crate) fn precomputed_tree_cache_get( pub(crate) fn precomputed_tree_cache_put( root: Commitment, + rows_per_leaf: usize, tree: Arc>, ) { precomputed_tree_insert_capped( &mut precomputed_tree_cache().lock().unwrap(), - root, + (root, rows_per_leaf), tree as Arc, precomputed_tree_cache_cap(), ); @@ -1307,6 +1322,42 @@ pub trait IsStarkProver< col_start: usize, col_end: usize, ) -> Option<(MerkleTree>, Commitment)> + where + FieldElement: AsBytes + Sync + Send + math::traits::ByteConversion, + E: IsField, + { + Self::commit_rows_bit_reversed_subset_with( + data, + num_cols, + col_start, + col_end, + crate::commitment::ROWS_PER_LEAF, + ) + } + + /// [`Self::commit_rows_bit_reversed`] with `rows_per_leaf` rows per leaf + /// (the table's [`LeafLayout`]): 2 = today's row pairs, 1 = one row (S2). + fn commit_rows_bit_reversed_with( + data: &[FieldElement], + num_cols: usize, + rows_per_leaf: usize, + ) -> Option<(MerkleTree>, Commitment)> + where + FieldElement: AsBytes + Sync + Send + math::traits::ByteConversion, + E: IsField, + { + Self::commit_rows_bit_reversed_subset_with(data, num_cols, 0, num_cols, rows_per_leaf) + } + + /// [`Self::commit_rows_bit_reversed_subset`] with `rows_per_leaf` rows per + /// leaf: leaf `i` hashes the bit-reversed rows `R·i .. R·i + R − 1`. + fn commit_rows_bit_reversed_subset_with( + data: &[FieldElement], + num_cols: usize, + col_start: usize, + col_end: usize, + rows_per_leaf: usize, + ) -> Option<(MerkleTree>, Commitment)> where FieldElement: AsBytes + Sync + Send + math::traits::ByteConversion, E: IsField, @@ -1327,17 +1378,19 @@ pub trait IsStarkProver< "num_rows must be a power of two for reverse_index" ); - // Local alias for the canonical constant, used several times below. - const ROWS_PER_LEAF: usize = crate::commitment::ROWS_PER_LEAF; - let num_leaves = num_rows / ROWS_PER_LEAF; + debug_assert!(rows_per_leaf == 1 || rows_per_leaf == 2); + if rows_per_leaf == 0 || !num_rows.is_multiple_of(rows_per_leaf) { + return None; + } + let num_leaves = num_rows / rows_per_leaf; let subset_cols = col_end - col_start; let byte_len = as ByteConversion>::BYTE_LEN; - let leaf_bytes = ROWS_PER_LEAF * subset_cols * byte_len; + let leaf_bytes = rows_per_leaf * subset_cols * byte_len; let hash_leaf = |buf: &mut [u8], leaf_idx: usize| -> Commitment { let mut offset = 0; - for k in 0..ROWS_PER_LEAF { - let br_idx = reverse_index(ROWS_PER_LEAF * leaf_idx + k, num_rows as u64); + for k in 0..rows_per_leaf { + let br_idx = reverse_index(rows_per_leaf * leaf_idx + k, num_rows as u64); let row_start = br_idx * num_cols; let row = &data[row_start + col_start..row_start + col_end]; for elem in row.iter() { @@ -1381,6 +1434,27 @@ pub trait IsStarkProver< air: &impl AIR, num_precomputed_cols: usize, ) -> Option + where + FieldElement: AsBytes + Sync + Send, + FieldElement: AsBytes + Sync + Send, + { + Self::compute_precomputed_commitment_for_testing_with( + trace, + air, + num_precomputed_cols, + LeafLayout::RowPair, + ) + } + + /// [`Self::compute_precomputed_commitment_for_testing`] under an explicit + /// leaf layout (S2's one-row root of the same columns). + #[cfg(any(test, feature = "test-utils"))] + fn compute_precomputed_commitment_for_testing_with( + trace: &TraceTable, + air: &impl AIR, + num_precomputed_cols: usize, + layout: LeafLayout, + ) -> Option where FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, @@ -1394,7 +1468,7 @@ pub trait IsStarkProver< let (_, commitment) = crate::commitment::commit_bit_reversed_with::< Field, H::Batched, - >(&evals, crate::commitment::ROWS_PER_LEAF)?; + >(&evals, layout.rows_per_leaf())?; Some(commitment) } @@ -1538,15 +1612,21 @@ pub trait IsStarkProver< /// /// `precomputed`: if present, the leading `num_cols` columns are committed /// as a separate Merkle tree (the precomputed split for preprocessed - /// tables) and the root is checked against the AIR-hardcoded commitment. - /// `table` is the AIR's name, for the device diagnostics. - #[allow(clippy::type_complexity)] + /// tables) and the root is checked against the AIR-hardcoded commitment + /// OF `layout`. `table` is the AIR's name, for the device diagnostics. + /// + /// `layout` is the table's trace-tree leaf layout: every arm (the fused + /// and split device commits and the CPU one) builds its trees with + /// `layout.rows_per_leaf()` rows per leaf, so a one-row table (S2) commits + /// on the device like a row-pair one. + #[allow(clippy::type_complexity, clippy::too_many_arguments)] fn commit_main_trace( #[cfg_attr(not(feature = "cuda"), allow(unused_variables))] table: &str, trace: &TraceTable, domain: &Domain, twiddles: &LdeTwiddles, precomputed: Option<(Commitment, usize)>, + layout: LeafLayout, #[cfg(feature = "cuda")] device_only: bool, #[cfg(feature = "disk-spill")] storage_mode: StorageMode, #[cfg_attr(not(feature = "cuda"), allow(unused_variables))] residency: ResidencyMode, @@ -1563,6 +1643,7 @@ pub trait IsStarkProver< // commit is recomputed on the host, so the buffer the tree was built // from must be the host one. Same posture as disk-spill — the mode is // for CPU proving and forces the host path per table. + let rows_per_leaf = layout.rows_per_leaf(); #[cfg(feature = "cuda")] if precomputed.is_none() && !residency.recomputes_main_lde() { let (trace_slice, num_cols) = trace.main_data_row_major(); @@ -1588,6 +1669,7 @@ pub trait IsStarkProver< domain.blowup_factor, &twiddles.coset_weights, !device_only, + rows_per_leaf, ) { #[cfg(feature = "instruments")] @@ -1635,7 +1717,10 @@ pub trait IsStarkProver< let cache_ok = true; let cached_pre = cache_ok .then(|| { - precomputed_tree_cache_get::>(&expected_precomputed_root) + precomputed_tree_cache_get::>( + &expected_precomputed_root, + rows_per_leaf, + ) }) .flatten(); #[cfg(feature = "instruments")] @@ -1656,6 +1741,7 @@ pub trait IsStarkProver< num_precomputed, cached_pre.is_none(), !device_only, + rows_per_leaf, ) { #[cfg(feature = "instruments")] @@ -1680,6 +1766,7 @@ pub trait IsStarkProver< if cache_ok { precomputed_tree_cache_put::>( expected_precomputed_root, + rows_per_leaf, Arc::clone(&tree), ); } @@ -1727,8 +1814,9 @@ pub trait IsStarkProver< let commit = match precomputed { None => { #[allow(unused_mut)] - let (mut tree, root) = Self::commit_rows_bit_reversed(&main_data, total_cols) - .ok_or(ProvingError::EmptyCommitment)?; + let (mut tree, root) = + Self::commit_rows_bit_reversed_with(&main_data, total_cols, rows_per_leaf) + .ok_or(ProvingError::EmptyCommitment)?; #[cfg(feature = "disk-spill")] Self::spill_tree(&mut tree, storage_mode, "main Merkle tree")?; TableCommit::plain(tree, root) @@ -1746,7 +1834,10 @@ pub trait IsStarkProver< let cache_ok = true; let precomputed_tree = match cache_ok .then(|| { - precomputed_tree_cache_get::>(&expected_precomputed_root) + precomputed_tree_cache_get::>( + &expected_precomputed_root, + rows_per_leaf, + ) }) .flatten() { @@ -1755,11 +1846,12 @@ pub trait IsStarkProver< Some(tree) => tree, None => { #[allow(unused_mut)] - let (mut tree, root) = Self::commit_rows_bit_reversed_subset( + let (mut tree, root) = Self::commit_rows_bit_reversed_subset_with( &main_data, total_cols, 0, num_precomputed, + rows_per_leaf, ) .ok_or(ProvingError::EmptyCommitment)?; if root != expected_precomputed_root { @@ -1771,6 +1863,7 @@ pub trait IsStarkProver< if cache_ok { precomputed_tree_cache_put::>( expected_precomputed_root, + rows_per_leaf, Arc::clone(&tree), ); } @@ -1778,11 +1871,12 @@ pub trait IsStarkProver< } }; #[allow(unused_mut)] - let (mut mult_tree, mult_root) = Self::commit_rows_bit_reversed_subset( + let (mut mult_tree, mult_root) = Self::commit_rows_bit_reversed_subset_with( &main_data, total_cols, num_precomputed, total_cols, + rows_per_leaf, ) .ok_or(ProvingError::EmptyCommitment)?; #[cfg(feature = "disk-spill")] @@ -2522,6 +2616,10 @@ pub trait IsStarkProver< let __ps_r2c = crate::prove_split::mark(); #[cfg(feature = "instruments")] let t_sub = Instant::now(); + // The table's leaf layout (S2): the composition tree, device or host, + // carries `leaf_layout.rows_per_leaf()` rows per leaf. + let leaf_layout = + crate::leaf_layout::table_leaf_layout(air, domain.interpolation_domain_size); // GPU fast path for the comp-poly Merkle commit: hash straight from // the resident parts handle when R2 kept one (no host pack + H2D // re-upload); otherwise wrap the host eval Vecs. Either way the tree @@ -2537,13 +2635,16 @@ pub trait IsStarkProver< crate::gpu_lde::try_build_comp_poly_tree_gpu_from_dev::< FieldExtension, H::Batched, - >(h) + >(h, leaf_layout.rows_per_leaf()) }) .or_else(|| { crate::gpu_lde::try_build_comp_poly_tree_gpu::< FieldExtension, H::Batched, - >(&lde_composition_poly_parts_evaluations) + >( + &lde_composition_poly_parts_evaluations, + leaf_layout.rows_per_leaf(), + ) }) { Some((host_tree, dev_tree)) => { let root = host_tree.root; @@ -2574,7 +2675,7 @@ pub trait IsStarkProver< H::Batched, >( &lde_composition_poly_parts_evaluations, - crate::commitment::ROWS_PER_LEAF, + leaf_layout.rows_per_leaf(), ) .ok_or(ProvingError::EmptyCommitment)?; (tree, root, None) @@ -2584,7 +2685,7 @@ pub trait IsStarkProver< let (composition_poly_merkle_tree, composition_poly_root) = crate::commitment::commit_bit_reversed_with::>( &lde_composition_poly_parts_evaluations, - crate::commitment::ROWS_PER_LEAF, + leaf_layout.rows_per_leaf(), ) .ok_or(ProvingError::EmptyCommitment)?; crate::prove_split::add(&crate::prove_split::R2_COMMIT, __ps_r2c); @@ -2755,11 +2856,25 @@ pub trait IsStarkProver< round_3_result: &Round3, z: &FieldElement, transcript: &mut (impl IsStarkTranscript + Clone), - ) -> Round4 + ) -> Result, ProvingError> where FieldElement: AsBytes, FieldElement: AsBytes, { + // The FRI fold layout of this table's proof format (a verifier-side + // constant built from the options, the same call the verifier makes). + // A format this build cannot lay out is refused here, before anything + // enters the transcript. + let leaf_layout = + crate::leaf_layout::table_leaf_layout(air, domain.interpolation_domain_size); + let fri_layout = crate::fri::terminal::FriFoldLayout::for_options( + domain.lde_roots_of_unity_coset.len().trailing_zeros(), + domain.blowup_factor.trailing_zeros(), + air.options(), + leaf_layout.is_one_row(), + ) + .map_err(|e| ProvingError::WrongParameter(format!("FRI format: {e}")))?; + let coset_offset_u64 = air.context().proof_options.coset_offset; let coset_offset = FieldElement::::from(coset_offset_u64); @@ -2800,33 +2915,39 @@ pub trait IsStarkProver< let __ps_df = crate::prove_split::mark(); #[cfg(feature = "instruments")] let t_sub = Instant::now(); + // Device FRI implements the pair and group (S3) encodings and the + // one-row layout (S2), whose input tree is committed from the resident + // codeword before the first challenge. #[cfg(feature = "cuda")] - let precomputed_fri = Self::try_compute_deep_dev( - &round_1_result.lde_trace, - composition_parts, - round_3_result, - z, - domain, - &domain.trace_primitive_root, - &gammas, - &trace_term_coeffs, - ) - .and_then(|dw| { - crate::gpu_lde::try_fri_commit_gpu_from_dev::< - Field, - FieldExtension, - _, - H::Pair, - >( - dw, - transcript, - &coset_offset, - domain.blowup_factor.trailing_zeros(), - air.options().fri_final_poly_log_degree as u32, - domain.fri_inv_twiddles(), - !round_1_result.lde_trace.host_trace_empty(), + let precomputed_fri = { + Self::try_compute_deep_dev( + &round_1_result.lde_trace, + composition_parts, + round_3_result, + z, + domain, + &domain.trace_primitive_root, + &gammas, + &trace_term_coeffs, ) - }); + .and_then(|dw| { + crate::gpu_lde::try_fri_commit_gpu_from_dev::< + Field, + FieldExtension, + _, + H::Pair, + >( + dw, + transcript, + &coset_offset, + domain.blowup_factor.trailing_zeros(), + air.options().fri_final_poly_log_degree as u32, + &fri_layout, + domain.fri_inv_twiddles(), + !round_1_result.lde_trace.host_trace_empty(), + ) + }) + }; #[cfg(not(feature = "cuda"))] #[allow(clippy::type_complexity)] let precomputed_fri: Option<( @@ -2875,13 +2996,14 @@ pub trait IsStarkProver< // FRI commit phase from pre-computed evaluations #[cfg(feature = "instruments")] let t_sub = Instant::now(); - let res = fri::commit_phase_from_evaluations::( + let res = fri::commit_phase_with_layout::( lde_evals, transcript, &coset_offset, domain_size, domain.blowup_factor.trailing_zeros(), air.options().fri_final_poly_log_degree as u32, + &fri_layout, domain.fri_inv_twiddles(), ); #[cfg(feature = "instruments")] @@ -2920,17 +3042,56 @@ pub trait IsStarkProver< crate::prove_split::add(&crate::prove_split::R4_GRIND, __ps_g); let __ps_q = crate::prove_split::mark(); let number_of_queries = air.options().fri_number_of_queries; - let iotas = Self::sample_query_indexes(number_of_queries, domain, transcript); + let iotas = Self::sample_query_indexes(number_of_queries, domain, leaf_layout, transcript); - let query_list = fri::query_phase::(&fri_layers, &iotas); + let mut query_list = + fri::query_phase_with_layout::(&fri_layers, &iotas, &fri_layout); let fri_layers_merkle_roots: Vec<_> = fri_layers .iter() .map(|layer| layer.merkle_tree.root) .collect(); - let deep_poly_openings = - Self::open_deep_composition_poly(domain, round_1_result, round_2_result, &iotas); + let mut deep_poly_openings = Self::open_deep_composition_poly( + domain, + round_1_result, + round_2_result, + &iotas, + leaf_layout, + ); + + // Merkle caps: a post-pass over the finished + // openings. The heights are the verifier's (`StarkCaps`, public shape + // only); nothing is absorbed, so the transcript is the uncapped one. + // At the default format every height is 0 and this is skipped. + // + // Every depth is the layout's: the trace trees' from the table's leaf + // layout (row pairs `log2(lde) − 1`, one row `log2(lde)`) and each FRI + // layer's from the fold layout (a group tree under a fold schedule), + // so a capped `fri = dp` or one-row proof caps the trees it committed. + let caps = crate::merkle_caps::StarkCaps::from_layout( + air.options().format.merkle_cap, + number_of_queries, + domain_size.trailing_zeros() as usize, + &fri_layout, + ); + if caps.fri.len() != fri_layers.len() { + return Err(ProvingError::WrongParameter(format!( + "Merkle cap: the FRI layout commits {} layers, the prover built {}", + caps.fri.len(), + fri_layers.len() + ))); + } + if caps.any() { + Self::embed_stark_caps( + &caps, + round_1_result, + round_2_result, + &fri_layers, + &mut deep_poly_openings, + &mut query_list, + )?; + } crate::prove_split::add(&crate::prove_split::R4_QUERIES, __ps_q); #[cfg(feature = "instruments")] @@ -2939,23 +3100,228 @@ pub trait IsStarkProver< crate::instruments::store_r4_sub(r4_fft_dur, r4_merkle_dur, other_dur_1, queries_dur); } - Round4 { + Ok(Round4 { fri_final_poly_coeffs, fri_layers_merkle_roots, deep_poly_openings, query_list, nonce, + }) + } + + /// Embed every capped tree's cap into its owner path and cut every path of + /// that tree to `depth − c` siblings. + /// + /// Per tree: read the cap (the host tree's heap slice; see + /// [`Self::tree_cap`] for a device-resident tree), then + /// [`embed_cap`](crypto::merkle_tree::cap::embed_cap) over the tree's + /// paths in proof order, so query 0 is the owner. Every path must be the + /// full `depth` long (checked), so a tree whose depth disagrees with the + /// verifier's constant fails here instead of producing a proof the + /// verifier rejects. + fn embed_stark_caps( + caps: &crate::merkle_caps::StarkCaps, + round_1_result: &Round1, + round_2_result: &Round2, + fri_layers: &[crate::fri::fri_commitment::FriLayer< + FieldExtension, + H::Pair, + >], + deep_poly_openings: &mut [DeepPolynomialOpening], + query_list: &mut [FriDecommitment], + ) -> Result<(), ProvingError> + where + FieldElement: AsBytes, + FieldElement: AsBytes, + { + fn embed<'p>( + paths: impl Iterator>>, + depth: usize, + cap: &[Commitment], + what: &str, + ) -> Result<(), ProvingError> { + let mut paths: Vec<&mut Vec> = + paths.collect::>().ok_or_else(|| { + ProvingError::WrongParameter(format!( + "Merkle cap: an opening of the {what} tree is missing" + )) + })?; + crypto::merkle_tree::cap::embed_cap(&mut paths, depth, cap).map_err(|e| { + ProvingError::WrongParameter(format!("Merkle cap of the {what} tree: {e}")) + }) + } + + // The device arm of `tree_cap`: read the cap off the resident tree on + // `stream`. `None` when the tree is not device-resident. + #[cfg(feature = "cuda")] + fn dev<'t>( + tree: Option<&'t math_cuda::lde::GpuMerkleTree>, + stream: impl FnOnce() -> Option> + 't, + ) -> impl FnOnce(usize) -> Option, String>> + 't { + move |c| { + tree.map(|tree| { + let stream = stream().ok_or("no CUDA stream for the device cap read")?; + crate::gpu_lde::read_cap_dev(tree, c, &stream) + }) + } } + #[cfg(feature = "cuda")] + let lde_trace = &round_1_result.lde_trace; + + let (depth, c) = (caps.trace_depth, caps.trace); + if c > 0 { + #[cfg(feature = "cuda")] + let main_dev = dev(lde_trace.gpu_main().and_then(|h| h.tree.as_ref()), || { + lde_trace.bound_stream() + }); + #[cfg(not(feature = "cuda"))] + let main_dev = |_| None; + let main_cap = Self::tree_cap(&round_1_result.main.tree, depth, c, "main", main_dev)?; + embed( + deep_poly_openings + .iter_mut() + .map(|o| Some(&mut o.main_trace_polys.proof.merkle_path)), + depth, + &main_cap, + "main", + )?; + if let Some(tree) = round_1_result.main.precomputed_tree.as_ref() { + // Always a full host tree (the process-wide cache; its openings + // walk it on the host too), so there is no device arm. + let cap = Self::tree_cap(tree, depth, c, "precomputed", |_| None)?; + embed( + deep_poly_openings.iter_mut().map(|o| { + o.precomputed_trace_polys + .as_mut() + .map(|p| &mut p.proof.merkle_path) + }), + depth, + &cap, + "precomputed", + )?; + } + if let Some(aux) = round_1_result.aux.as_ref() { + #[cfg(feature = "cuda")] + let aux_dev = dev(lde_trace.gpu_aux().and_then(|h| h.tree.as_ref()), || { + lde_trace.bound_stream() + }); + #[cfg(not(feature = "cuda"))] + let aux_dev = |_| None; + let cap = Self::tree_cap(&aux.tree, depth, c, "aux", aux_dev)?; + embed( + deep_poly_openings + .iter_mut() + .map(|o| o.aux_trace_polys.as_mut().map(|p| &mut p.proof.merkle_path)), + depth, + &cap, + "aux", + )?; + } + #[cfg(feature = "cuda")] + let comp_dev = dev(round_2_result.gpu_composition_tree.as_ref(), || { + lde_trace.bound_stream() + }); + #[cfg(not(feature = "cuda"))] + let comp_dev = |_| None; + let cap = Self::tree_cap( + &round_2_result.composition_poly_merkle_tree, + depth, + c, + "composition", + comp_dev, + )?; + embed( + deep_poly_openings + .iter_mut() + .map(|o| Some(&mut o.composition_poly.proof.merkle_path)), + depth, + &cap, + "composition", + )?; + } + + for (i, layer) in fri_layers.iter().enumerate() { + let (depth, c) = (caps.fri_depths[i], caps.fri[i]); + if c == 0 { + continue; + } + let what = format!("FRI layer {i}"); + // A fresh backend stream, as the device FRI query phase reads the + // same resident layer trees (`try_fri_query_phase_gpu`). + #[cfg(feature = "cuda")] + let layer_dev = dev(layer.gpu_tree.as_ref(), || { + math_cuda::device::backend().ok().map(|b| b.next_stream()) + }); + #[cfg(not(feature = "cuda"))] + let layer_dev = |_| None; + let cap = Self::tree_cap(&layer.merkle_tree, depth, c, &what, layer_dev)?; + embed( + query_list + .iter_mut() + .map(|q| q.layers_auth_paths.get_mut(i).map(|p| &mut p.merkle_path)), + depth, + &cap, + &what, + )?; + } + Ok(()) } + /// The height-`c` cap of one tree of depth `depth`. + /// + /// A full host tree serves it from its heap (`MerkleTree::cap`, disk-spill + /// safe), after checking the tree's depth is the verifier's. A root-only + /// host tree means the nodes are device-resident: `device(c)` reads the + /// cap off the resident tree, and `None` from it (no resident tree) is a + /// hard error naming the tree — never a skipped cap, which would ship + /// full-length paths the verifier rejects with no pointer to the cause. + fn tree_cap( + host: &MerkleTree, + depth: usize, + c: usize, + what: &str, + device: impl FnOnce(usize) -> Option, String>>, + ) -> Result, ProvingError> + where + B: IsMerkleTreeBackend, + { + if !host.is_root_only() { + if host.depth() != Some(depth) { + return Err(ProvingError::WrongParameter(format!( + "Merkle cap: the {what} tree has depth {:?}, the format expects {depth}", + host.depth() + ))); + } + return host.cap(c).ok_or_else(|| { + ProvingError::WrongParameter(format!( + "Merkle cap: height {c} does not fit the {what} tree (depth {depth})" + )) + }); + } + match device(c) { + Some(Ok(cap)) => Ok(cap), + Some(Err(e)) => Err(ProvingError::DevicePath(format!( + "Merkle cap: reading the height-{c} cap of the device-resident {what} tree \ + failed: {e}" + ))), + None => Err(ProvingError::DevicePath(format!( + "Merkle cap: the {what} tree is device-resident (its host tree is root-only) \ + and no device cap read is wired for it" + ))), + } + } + + /// The query indexes: trace-tree leaf indexes, uniform below + /// [`LeafLayout::query_bound`] (`lde / 2` today, `lde` under one row). fn sample_query_indexes( number_of_queries: usize, domain: &Domain, + leaf_layout: LeafLayout, transcript: &mut impl IsStarkTranscript, ) -> Vec { - let domain_size = domain.lde_roots_of_unity_coset.len() as u64; + let bound = leaf_layout.query_bound(domain.lde_roots_of_unity_coset.len() as u64); (0..number_of_queries) - .map(|_| (transcript.sample_u64(domain_size >> 1)) as usize) + .map(|_| (transcript.sample_u64(bound)) as usize) .collect::>() } @@ -3238,6 +3604,7 @@ pub trait IsStarkProver< composition_poly_merkle_tree: &MerkleTree>, lde_composition_poly_evaluations: &[Vec>], index: usize, + leaf_layout: LeafLayout, ) -> PolynomialOpenings where FieldElement: AsBytes + Sync + Send, @@ -3246,28 +3613,39 @@ pub trait IsStarkProver< let proof = composition_poly_merkle_tree .get_proof_by_pos(index) .expect("FRI query index in bounds"); + Self::composition_opening_from_proof( + proof, + lde_composition_poly_evaluations, + index, + leaf_layout, + ) + } - let lde_composition_poly_parts_evaluation: Vec<_> = lde_composition_poly_evaluations - .iter() - .flat_map(|part| { - vec![ - part[reverse_index(index * 2, part.len() as u64)].clone(), - part[reverse_index(index * 2 + 1, part.len() as u64)].clone(), - ] - }) - .collect(); - + /// The composition parts' values at query `index` (the rows + /// [`LeafLayout::query_rows`] names) with an already-built Merkle proof: + /// both rows for a row pair, the one row (and an empty `evaluations_sym`) + /// for one row. + fn composition_opening_from_proof( + proof: Proof, + lde_composition_poly_evaluations: &[Vec>], + index: usize, + leaf_layout: LeafLayout, + ) -> PolynomialOpenings + where + FieldElement: AsBytes + Sync + Send, + FieldElement: AsBytes + Sync + Send, + { + let rows = + |part: &Vec>| leaf_layout.query_rows(index, part.len()); PolynomialOpenings { proof, - evaluations: lde_composition_poly_parts_evaluation - .clone() - .into_iter() - .step_by(2) + evaluations: lde_composition_poly_evaluations + .iter() + .map(|part| part[rows(part).0].clone()) .collect(), - evaluations_sym: lde_composition_poly_parts_evaluation - .into_iter() - .skip(1) - .step_by(2) + evaluations_sym: lde_composition_poly_evaluations + .iter() + .filter_map(|part| rows(part).1.map(|r| part[r].clone())) .collect(), } } @@ -3275,40 +3653,25 @@ pub trait IsStarkProver< /// Like [`Self::open_composition_poly`] but uses a Merkle proof already /// gathered from the resident device composition tree /// ([`crate::gpu_lde::gather_proofs_dev`]) instead of walking a host tree. - /// Row-pair leaf: one proof at position `index` authenticates both rows. + /// One proof at position `index` authenticates the leaf: both rows of a + /// row pair, or the one row (S2). #[cfg(feature = "cuda")] fn open_composition_poly_with_proof( proof: Proof, lde_composition_poly_evaluations: &[Vec>], index: usize, + leaf_layout: LeafLayout, ) -> PolynomialOpenings where FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { - let lde_composition_poly_parts_evaluation: Vec<_> = lde_composition_poly_evaluations - .iter() - .flat_map(|part| { - vec![ - part[reverse_index(index * 2, part.len() as u64)].clone(), - part[reverse_index(index * 2 + 1, part.len() as u64)].clone(), - ] - }) - .collect(); - - PolynomialOpenings { + Self::composition_opening_from_proof( proof, - evaluations: lde_composition_poly_parts_evaluation - .clone() - .into_iter() - .step_by(2) - .collect(), - evaluations_sym: lde_composition_poly_parts_evaluation - .into_iter() - .skip(1) - .step_by(2) - .collect(), - } + lde_composition_poly_evaluations, + index, + leaf_layout, + ) } /// Computes values and validity proofs of the evaluations of trace polynomials at @@ -3319,6 +3682,7 @@ pub trait IsStarkProver< domain: &Domain, tree: &MerkleTree>, challenge: usize, + leaf_layout: LeafLayout, gather: G, ) -> PolynomialOpenings where @@ -3326,29 +3690,33 @@ pub trait IsStarkProver< FieldElement: AsBytes + Sync + Send, G: Fn(usize) -> Vec>, { - let domain_size = domain.lde_roots_of_unity_coset.len() as u64; - // Rows `2·challenge` and `2·challenge+1` are committed together as the - // single leaf at position `challenge`; one Merkle path authenticates both - // the queried row and its symmetric counterpart. + // Row pairs: rows `2·challenge` and `2·challenge+1` are committed + // together as the single leaf at position `challenge`; one Merkle path + // authenticates both the queried row and its symmetric counterpart. + // One row: the leaf at `challenge` is the row alone, and there is no + // symmetric row. + let (row, sym) = leaf_layout.query_rows(challenge, domain.lde_roots_of_unity_coset.len()); PolynomialOpenings { proof: tree .get_proof_by_pos(challenge) .expect("FRI query index in bounds"), - evaluations: gather(reverse_index(challenge * 2, domain_size)), - evaluations_sym: gather(reverse_index(challenge * 2 + 1, domain_size)), + evaluations: gather(row), + evaluations_sym: sym.map(&gather).unwrap_or_default(), } } /// Like [`Self::open_polys_with`], but uses a Merkle proof already gathered /// from the resident device tree (see [`crate::gpu_lde::gather_proofs_dev`]) - /// instead of walking a host tree. Row-pair leaf: one proof at position - /// `challenge` authenticates both the queried row and its symmetric - /// counterpart. Evaluations still come from the host LDE columns via `gather`. + /// instead of walking a host tree. One proof at position `challenge` + /// authenticates the leaf: the queried row and its symmetric counterpart + /// (row pair), or the one row (S2). Evaluations still come from the host + /// LDE columns via `gather`. #[cfg(feature = "cuda")] fn open_polys_with_proofs( domain: &Domain, proof: Proof, challenge: usize, + leaf_layout: LeafLayout, gather: G, ) -> PolynomialOpenings where @@ -3356,11 +3724,11 @@ pub trait IsStarkProver< FieldElement: AsBytes + Sync + Send, G: Fn(usize) -> Vec>, { - let domain_size = domain.lde_roots_of_unity_coset.len() as u64; + let (row, sym) = leaf_layout.query_rows(challenge, domain.lde_roots_of_unity_coset.len()); PolynomialOpenings { proof, - evaluations: gather(reverse_index(challenge * 2, domain_size)), - evaluations_sym: gather(reverse_index(challenge * 2 + 1, domain_size)), + evaluations: gather(row), + evaluations_sym: sym.map(&gather).unwrap_or_default(), } } @@ -3381,17 +3749,37 @@ pub trait IsStarkProver< } } - /// Slice out query `qi`'s even/odd row (each `ncols` field elements) from the - /// row-major device gather `[even(q0), odd(q0), even(q1), odd(q1), ...]`. + /// The LDE rows the device gathers for `queries`, in query order: per + /// query the rows [`LeafLayout::query_rows`] names — `[row, sym]` for a + /// row pair, `[row]` for one row (S2). [`Self::device_rows`] slices the + /// gather back per query. + #[cfg(feature = "cuda")] + fn device_query_rows(queries: &[usize], lde_len: usize, leaf_layout: LeafLayout) -> Vec { + queries + .iter() + .flat_map(|&c| { + let (row, sym) = leaf_layout.query_rows(c, lde_len); + core::iter::once(row as u32).chain(sym.map(|r| r as u32)) + }) + .collect() + } + + /// Slice out query `qi`'s rows (each `ncols` field elements) from the + /// row-major device gather of [`Self::device_query_rows`]: `(row, sym)` + /// for a row pair (`[row(q0), sym(q0), row(q1), sym(q1), ...]`), `(row, + /// [])` for one row (`[row(q0), row(q1), ...]`). #[cfg(feature = "cuda")] - fn device_row_pair( + fn device_rows( vals: &[FieldElement], qi: usize, ncols: usize, + leaf_layout: LeafLayout, ) -> (Vec>, Vec>) { - let even = vals[(2 * qi) * ncols..(2 * qi + 1) * ncols].to_vec(); - let odd = vals[(2 * qi + 1) * ncols..(2 * qi + 2) * ncols].to_vec(); - (even, odd) + let per = leaf_layout.rows_per_leaf(); + let at = |k: usize| vals[(per * qi + k) * ncols..(per * qi + k + 1) * ncols].to_vec(); + let row = at(0); + let sym = if per == 2 { at(1) } else { Vec::new() }; + (row, sym) } /// Gather every query's row-pair off a device-resident LDE (a small D2H of @@ -3453,6 +3841,7 @@ pub trait IsStarkProver< ncols: usize, col_range: std::ops::Range, what: &str, + leaf_layout: LeafLayout, gather: G, ) -> PolynomialOpenings where @@ -3474,7 +3863,7 @@ pub trait IsStarkProver< !tree.is_root_only(), "R4 {what} opening fell back to a root-only host tree (nodes device-resident)" ); - return Self::open_polys_with(domain, tree, challenge, gather); + return Self::open_polys_with(domain, tree, challenge, leaf_layout, gather); }; let proof = proofs[qi].clone(); let Some(dev_vals) = dev_values else { @@ -3484,10 +3873,16 @@ pub trait IsStarkProver< !lde_trace.host_trace_empty(), "R4 {what} opening fell back to the host gather, but it is device-only (empty)" ); - return Self::open_polys_with_proofs(domain, proof, challenge, gather); + return Self::open_polys_with_proofs(domain, proof, challenge, leaf_layout, gather); + }; + let (even, odd) = Self::device_rows(dev_vals, qi, ncols, leaf_layout); + // `odd` is empty for one row (no symmetric row). + let odd = if odd.is_empty() { + odd + } else { + odd[col_range.clone()].to_vec() }; - let (even, odd) = Self::device_row_pair(dev_vals, qi, ncols); - let (even, odd) = (even[col_range.clone()].to_vec(), odd[col_range].to_vec()); + let even = even[col_range].to_vec(); // Cross-check the device gather against the host LDE. Skipped under // device-only (host trace empty): the gather was proven bit-identical // while the host copy was resident, and there is nothing to check @@ -3495,9 +3890,8 @@ pub trait IsStarkProver< // --release, and gather failure modes — stride/offset/layout — are // systematic, so one query catches them); debug checks every query. if (cfg!(debug_assertions) || qi == 0) && !lde_trace.host_trace_empty() { - let domain_size = domain.lde_roots_of_unity_coset.len() as u64; - let r_even = reverse_index(challenge * 2, domain_size); - let r_odd = reverse_index(challenge * 2 + 1, domain_size); + let domain_size = domain.lde_roots_of_unity_coset.len(); + let (r_even, r_odd) = leaf_layout.query_rows(challenge, domain_size); assert_eq!( even, gather(r_even), @@ -3505,19 +3899,21 @@ pub trait IsStarkProver< ); assert_eq!( odd, - gather(r_odd), + r_odd.map(&gather).unwrap_or_default(), "device {what}-row gather mismatch (odd), query {qi}" ); } Self::open_polys_from_values(proof, even, odd) } - /// Open the deep composition polynomial on a list of indexes and their symmetric elements. + /// Open the deep composition polynomial on a list of indexes and their + /// symmetric elements (row pairs) or at the indexes alone (one row, S2). fn open_deep_composition_poly( domain: &Domain, round_1_result: &Round1, round_2_result: &Round2, indexes_to_open: &[usize], + leaf_layout: LeafLayout, ) -> DeepPolynomialOpenings where FieldElement: AsBytes, @@ -3532,22 +3928,17 @@ pub trait IsStarkProver< let num_precomputed_cols = main_commit.num_precomputed_cols; let total_cols = lde_trace.num_main_cols(); - // Row-pair LDE positions for every query, `[even(q0), odd(q0), ...]`. - // Each query opens the leaf at `challenge`, which pairs LDE rows + // The LDE rows of every query's leaf: `[row(q0), sym(q0), ...]` for + // row pairs — the leaf at `challenge` pairs LDE rows // `reverse_index(2·challenge)` (the queried point) and - // `reverse_index(2·challenge+1)` (its symmetric `-x` point). + // `reverse_index(2·challenge+1)` (its symmetric `-x` point) — and + // `[row(q0), row(q1), ...]` for one row (S2), the leaf at `challenge` + // being the row `reverse_index(challenge)` alone. #[cfg(feature = "cuda")] let domain_size = domain.lde_roots_of_unity_coset.len() as u64; #[cfg(feature = "cuda")] - let query_rows: Vec = indexes_to_open - .iter() - .flat_map(|&c| { - [ - reverse_index(c * 2, domain_size) as u32, - reverse_index(c * 2 + 1, domain_size) as u32, - ] - }) - .collect(); + let query_rows: Vec = + Self::device_query_rows(indexes_to_open, domain_size as usize, leaf_layout); // R4 trace proofs from the resident device trees, gathered in one batch // over all query positions instead of walking the host trees (byte @@ -3568,7 +3959,7 @@ pub trait IsStarkProver< let stream = lde_trace .bound_stream() .expect("bound stream for device-resident main-tree opening"); - // Row-pair leaves: one proof per query at position `challenge`. + // One proof per query at leaf `challenge` (either layout). crate::gpu_lde::gather_proofs_dev(tree, indexes_to_open, &stream) .expect("device main-tree gather failed; resident tree has no host fallback") }); @@ -3583,13 +3974,14 @@ pub trait IsStarkProver< let stream = lde_trace .bound_stream() .expect("bound stream for device-resident aux-tree opening"); - // Row-pair leaves: one proof per query at position `challenge`. + // One proof per query at leaf `challenge` (either layout). crate::gpu_lde::gather_proofs_dev(tree, indexes_to_open, &stream) .expect("device aux-tree gather failed; resident tree has no host fallback") }); - // Composition tree: openings open a single position `index` (row pair - // leaf), so gather one proof per query challenge from the device tree. + // Composition tree: openings open a single position `index` (a row + // pair or one-row leaf), so gather one proof per query challenge from + // the device tree. #[cfg(feature = "cuda")] let comp_dev_proofs: Option>> = round_2_result.gpu_composition_tree.as_ref().map(|tree| { @@ -3705,13 +4097,14 @@ pub trait IsStarkProver< total_cols, num_precomputed_cols..total_cols, "multiplicity", + leaf_layout, |row| { lde_trace.gather_main_row_range(row, num_precomputed_cols, total_cols) }, ) } #[cfg(not(feature = "cuda"))] - Self::open_polys_with(domain, &main_commit.tree, *index, |row| { + Self::open_polys_with(domain, &main_commit.tree, *index, leaf_layout, |row| { lde_trace.gather_main_row_range(row, num_precomputed_cols, total_cols) }) } else { @@ -3728,12 +4121,13 @@ pub trait IsStarkProver< total_cols, 0..total_cols, "main", + leaf_layout, |row| lde_trace.gather_main_row(row), ) } #[cfg(not(feature = "cuda"))] { - Self::open_polys_with(domain, &main_commit.tree, *index, |row| { + Self::open_polys_with(domain, &main_commit.tree, *index, leaf_layout, |row| { lde_trace.gather_main_row(row) }) } @@ -3749,17 +4143,20 @@ pub trait IsStarkProver< { match main_dev_values.as_ref() { Some(vals) => { - let (even, odd) = Self::device_row_pair(vals, qi, total_cols); - let (even, odd) = ( - even[..num_precomputed_cols].to_vec(), - odd[..num_precomputed_cols].to_vec(), - ); + let (even, odd) = Self::device_rows(vals, qi, total_cols, leaf_layout); + let even = even[..num_precomputed_cols].to_vec(); + // Empty for one row (no symmetric row). + let odd = if odd.is_empty() { + odd + } else { + odd[..num_precomputed_cols].to_vec() + }; // Query 0 stays a release canary, same rationale // as `open_trace_polys_device`. if (cfg!(debug_assertions) || qi == 0) && !lde_trace.host_trace_empty() { - let r_even = reverse_index(*index * 2, domain_size); - let r_odd = reverse_index(*index * 2 + 1, domain_size); + let (r_even, r_odd) = + leaf_layout.query_rows(*index, domain_size as usize); assert_eq!( even, lde_trace.gather_main_row_range( @@ -3771,7 +4168,13 @@ pub trait IsStarkProver< ); assert_eq!( odd, - lde_trace.gather_main_row_range(r_odd, 0, num_precomputed_cols), + r_odd + .map(|r| lde_trace.gather_main_row_range( + r, + 0, + num_precomputed_cols + )) + .unwrap_or_default(), "device precomputed-row gather mismatch (odd), query {qi}" ); } @@ -3788,14 +4191,14 @@ pub trait IsStarkProver< "R4 precomputed opening fell back to the host gather, \ but it is device-only (empty)" ); - Self::open_polys_with(domain, tree, *index, |row| { + Self::open_polys_with(domain, tree, *index, leaf_layout, |row| { lde_trace.gather_main_row_range(row, 0, num_precomputed_cols) }) } } } #[cfg(not(feature = "cuda"))] - Self::open_polys_with(domain, tree, *index, |row| { + Self::open_polys_with(domain, tree, *index, leaf_layout, |row| { lde_trace.gather_main_row_range(row, 0, num_precomputed_cols) }) }); @@ -3805,7 +4208,8 @@ pub trait IsStarkProver< { match (&comp_dev_proofs, &comp_dev_values) { (Some(proofs), Some(vals)) => { - let (even, odd) = Self::device_row_pair(vals, qi, comp_num_parts); + let (even, odd) = + Self::device_rows(vals, qi, comp_num_parts, leaf_layout); // Cross-check against the host part evals while // they are still resident (absent under full // residency, where the gather is the only source). @@ -3821,6 +4225,7 @@ pub trait IsStarkProver< proofs[qi].clone(), composition_parts, *index, + leaf_layout, ); assert_eq!( even, expected.evaluations, @@ -3850,12 +4255,14 @@ pub trait IsStarkProver< proofs[qi].clone(), composition_parts, *index, + leaf_layout, ) } _ => Self::open_composition_poly( &round_2_result.composition_poly_merkle_tree, composition_parts, *index, + leaf_layout, ), } } @@ -3865,6 +4272,7 @@ pub trait IsStarkProver< &round_2_result.composition_poly_merkle_tree, composition_parts, *index, + leaf_layout, ) } }; @@ -3883,12 +4291,13 @@ pub trait IsStarkProver< lde_trace.num_aux_cols(), 0..lde_trace.num_aux_cols(), "aux", + leaf_layout, |row| lde_trace.gather_aux_row(row), ) } #[cfg(not(feature = "cuda"))] { - Self::open_polys_with(domain, &aux.tree, *index, |row| { + Self::open_polys_with(domain, &aux.tree, *index, leaf_layout, |row| { lde_trace.gather_aux_row(row) }) } @@ -3983,6 +4392,13 @@ pub trait IsStarkProver< domains.push(domain); twiddle_caches.push(twiddles); } + // Each table's trace-tree leaf layout (S2): a verifier-side constant + // from the AIR's format and widths and the trace length — the call + // the verifier makes with the proof's trace length. + let leaf_layouts: Vec = air_trace_pairs + .iter() + .map(|(air, trace, _)| crate::leaf_layout::table_leaf_layout(*air, trace.num_rows())) + .collect(); let k = table_parallelism(num_airs); @@ -4035,7 +4451,17 @@ pub trait IsStarkProver< // dispatch layer admits the commit against. let main_estimates: Vec = table_shapes .iter() - .map(|s| crate::device_set::commit_device_set(s.n, s.main_cols, s.blowup, true).total()) + .zip(&leaf_layouts) + .map(|(s, l)| { + crate::device_set::commit_device_set_rpl( + s.n, + s.main_cols, + s.blowup, + true, + l.rows_per_leaf(), + ) + .total() + }) .collect(); // The AIR names, for the driver threads' panic payloads: a device abort @@ -4120,12 +4546,25 @@ pub trait IsStarkProver< let domain = &domains[idx]; let twiddles = &twiddle_caches[idx]; - let precomputed = air - .is_preprocessed() - .then(|| (air.precomputed_commitment(), air.num_precomputed_columns())); + let layout = leaf_layouts[idx]; + // The root of THIS layout; a layout the AIR has no root for is + // refused here, before anything is committed. + let precomputed = if air.is_preprocessed() { + let root = air.precomputed_commitment_for(layout).ok_or_else(|| { + ProvingError::PrecomputedCommitmentMissing(format!( + "table {}: no precomputed commitment for the {layout:?} leaf layout", + air.name() + )) + })?; + Some((root, air.num_precomputed_columns())) + } else { + None + }; // Stage-3 device-only gate: when it holds, `commit_main_trace` - // keeps the R1 LDE device-resident and skips the host D2H. + // keeps the R1 LDE device-resident and skips the host D2H. A + // one-row table (S2) is no exception: its device trees and + // openings follow its leaf layout. #[cfg(feature = "cuda")] let device_only = Self::device_only_for(*air, domain); @@ -4135,6 +4574,7 @@ pub trait IsStarkProver< domain, twiddles, precomputed, + layout, #[cfg(feature = "cuda")] device_only, #[cfg(feature = "disk-spill")] @@ -4266,7 +4706,13 @@ pub trait IsStarkProver< let peak_estimates: Vec = air_trace_pairs .iter() .enumerate() - .map(|(idx, _)| crate::device_set::table_device_set(table_shapes[idx]).total()) + .map(|(idx, _)| { + crate::device_set::table_device_set_rpl( + table_shapes[idx], + leaf_layouts[idx].rows_per_leaf(), + ) + .total() + }) .collect(); // The fused phase's own walk, separate from R1's because the aux @@ -4369,6 +4815,7 @@ pub trait IsStarkProver< // committed on the host, skipping the aux D2H here would // leave a device-only trace with no main handle to serve // it. + let layout = leaf_layouts[idx]; #[cfg(feature = "cuda")] let device_only = Self::device_only_for(*air, domain) && gpu_main_cells[idx].lock().unwrap().is_some(); @@ -4396,6 +4843,7 @@ pub trait IsStarkProver< domain.blowup_factor, &twiddles.coset_weights, !device_only, + layout.rows_per_leaf(), ) }; let mut expanded = expand(trace.aux_resident().expect("checked above")); @@ -4443,7 +4891,8 @@ pub trait IsStarkProver< } // Fused GPU path (cuda only): row-major ext3 NTT — single - // H2D, no column extraction, no CPU transpose. + // H2D, no column extraction, no CPU transpose. The tree + // follows the table's leaf layout. #[cfg(feature = "cuda")] { let (trace_slice, num_cols) = trace.aux_data_row_major(); @@ -4467,6 +4916,7 @@ pub trait IsStarkProver< domain.blowup_factor, &twiddles.coset_weights, !device_only, + layout.rows_per_leaf(), ) { #[cfg(feature = "instruments")] @@ -4504,9 +4954,12 @@ pub trait IsStarkProver< #[cfg(feature = "instruments")] let t_sub = Instant::now(); #[allow(unused_mut)] - let (mut tree, root) = - Self::commit_rows_bit_reversed(&aux_data, total_cols) - .ok_or(ProvingError::EmptyCommitment)?; + let (mut tree, root) = Self::commit_rows_bit_reversed_with( + &aux_data, + total_cols, + layout.rows_per_leaf(), + ) + .ok_or(ProvingError::EmptyCommitment)?; #[cfg(feature = "disk-spill")] Self::spill_tree(&mut tree, storage_mode, "aux Merkle tree")?; let commit = TableCommit::plain(tree, root); @@ -5314,7 +5767,7 @@ pub trait IsStarkProver< &round_3_result, &z, transcript, - ); + )?; #[cfg(feature = "instruments")] { @@ -5553,10 +6006,10 @@ mod precomputed_tree_cache_tests { } } - fn root(n: u8) -> Commitment { + fn root(n: u8) -> PrecomputedTreeKey { let mut c = [0u8; COMMITMENT_SIZE]; c[0] = n; - c + (c, crate::commitment::ROWS_PER_LEAF) } fn tree(n: u64) -> Arc> { Arc::new(MerkleTree::::build(&[n, n + 1]).expect("two leaves build a tree")) @@ -5565,7 +6018,7 @@ mod precomputed_tree_cache_tests { tree(n) as Arc } fn keys(m: &PrecomputedTreeMap) -> Vec { - let mut k: Vec = m.keys().map(|c| c[0]).collect(); + let mut k: Vec = m.keys().map(|(c, _)| c[0]).collect(); k.sort_unstable(); k } @@ -5654,6 +6107,23 @@ mod precomputed_tree_cache_tests { assert_eq!(m.len(), 201, "an unset cap must not evict anything"); } + /// The leaf layout is part of the key (S2): one root under two layouts is + /// two entries, never a hit on the other layout's tree. + #[test] + fn the_leaf_layout_is_part_of_the_key() { + let mut m = PrecomputedTreeMap::new(); + let (c, _) = root(5); + precomputed_tree_insert_capped(&mut m, (c, 2), erased(1), None); + precomputed_tree_insert_capped(&mut m, (c, 1), erased(2), None); + assert_eq!(m.len(), 2); + let got = |k: &PrecomputedTreeKey| { + m.get(k) + .and_then(|(_, any)| Arc::clone(any).downcast::>().ok()) + .map(|t| t.root) + }; + assert_ne!(got(&(c, 2)), got(&(c, 1))); + } + /// ⓘ `0` is read as UNSET, not as "cache nothing" — a zero-size cache would /// miss on every lookup, which is a typo nobody means to make. #[test] diff --git a/crypto/stark/src/s2_device_parity.rs b/crypto/stark/src/s2_device_parity.rs new file mode 100644 index 000000000..bcd96c08d --- /dev/null +++ b/crypto/stark/src/s2_device_parity.rs @@ -0,0 +1,631 @@ +//! Device-vs-host parity for S2's one-row trees and openings. +//! +//! Compiled for `cuda` builds with tests or `test-utils`; every entry needs a +//! GPU, so the callers are `#[ignore]`d box tests. The stark crate instantiates +//! them under Keccak and Blake3 (`tests::zf_s2_device_tests`), the prover crate +//! under the production RPX pin (`tests::zf_rpx_device_tests`). +//! +//! Each entry builds a tree on the device at `rows_per_leaf` 1 (and, as the +//! control, 2) and pins against the host commit over the SAME evaluations: +//! - the root, and the device tree's leaf count (`lde / rows_per_leaf`); +//! - the authentication path of scattered leaves and both ends, gathered off +//! the resident tree (`gather_proofs_dev`, the production opening path), +//! against the host tree's; +//! - where the entry keeps an LDE handle, the device row gather at the rows a +//! query opens (`LeafLayout::query_rows`), against the host rows; +//! - that the one-row root differs from the row-pair root (a device path that +//! ignored the layout would equal it). +//! +//! The LDE itself is parity-pinned by the existing fused-commit tests, so the +//! host reference consumes the evaluations the device returned: this isolates +//! the leaf layout and the tree. +//! +//! Every entry returns `Err` when the device declines (threshold, budget), so a +//! host fallback is a failure, never a pass. + +use std::format; +use std::string::String; +use std::sync::Arc; +use std::vec; +use std::vec::Vec; + +use crypto::merkle_tree::merkle::MerkleTree; +use crypto::merkle_tree::traits::IsMerkleTreeBackend; +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; +use math::field::goldilocks::GoldilocksField; + +use crate::config::{Commitment, StarkHash}; +use crate::fri::vectors::splitmix64; +use crate::leaf_layout::LeafLayout; +use crate::prover::{GenericProver, IsStarkProver}; + +type F = GoldilocksField; +type E = Degree3GoldilocksExtensionField; +type Felt = FieldElement; +type Ext = FieldElement; +type P = GenericProver; + +const LAYOUTS: [LeafLayout; 2] = [LeafLayout::Row, LeafLayout::RowPair]; + +fn base_values(count: usize, seed: &mut u64) -> Vec { + (0..count).map(|_| Felt::from(splitmix64(seed))).collect() +} + +fn ext_values(count: usize, seed: &mut u64) -> Vec { + (0..count) + .map(|_| { + Ext::new([ + Felt::from(splitmix64(seed)), + Felt::from(splitmix64(seed)), + Felt::from(splitmix64(seed)), + ]) + }) + .collect() +} + +/// Leaves to open: both ends, their neighbours and a spread of random ones. +fn open_positions(num_leaves: usize, seed: &mut u64) -> Vec { + let mut p = vec![0, 1, num_leaves / 2, num_leaves - 2, num_leaves - 1]; + p.extend((0..16).map(|_| (splitmix64(seed) % num_leaves as u64) as usize)); + p +} + +/// The resident device tree against the host tree over the same leaves: the +/// root, the leaf count, and every opened path gathered on device. +fn check_tree( + what: &str, + dev: &math_cuda::lde::GpuMerkleTree, + host: &MerkleTree, + host_root: &Commitment, + num_leaves: usize, + seed: &mut u64, +) -> Result<(), String> +where + B: IsMerkleTreeBackend, +{ + if dev.root != *host_root { + return Err(format!("{what}: device root differs from the host root")); + } + if dev.leaves_len != num_leaves { + return Err(format!( + "{what}: device tree has {} leaves, the layout needs {num_leaves}", + dev.leaves_len + )); + } + let stream = math_cuda::device::backend() + .map_err(|e| format!("{what}: no cuda backend: {e:?}"))? + .next_stream(); + let positions = open_positions(num_leaves, seed); + let proofs = crate::gpu_lde::gather_proofs_dev(dev, &positions, &stream) + .ok_or_else(|| format!("{what}: the device path gather failed"))?; + for (pos, proof) in positions.iter().zip(&proofs) { + let want = host + .get_proof_by_pos(*pos) + .ok_or_else(|| format!("{what}: host tree has no leaf {pos}"))?; + if proof.merkle_path != want.merkle_path { + return Err(format!("{what}: the path of leaf {pos} differs")); + } + } + Ok(()) +} + +/// Leaf count of a tree over `lde` rows under `layout`. +fn leaves_of(lde: usize, layout: LeafLayout) -> usize { + lde / layout.rows_per_leaf() +} + +/// The fused main commit (`try_expand_leaf_and_tree_row_major_keep`, the R1 +/// main arm and the LFM artifact commit) over a random `n × m` base trace at +/// `blowup`, at one row and row pairs: tree parity, plus the device row gather +/// at the one-row query rows (the R4 main opening values). +pub fn main_tree_parity( + n: usize, + m: usize, + blowup: usize, + seed: u64, +) -> Result { + let mut rng = seed; + let data = base_values(n * m, &mut rng); + let weights = base_values(n, &mut rng); + let lde_len = n * blowup; + let mut roots = Vec::new(); + for layout in LAYOUTS { + let what = format!("main {n}x{m} blowup {blowup} {layout:?}"); + let (tree, handle, lde) = + crate::gpu_lde::try_expand_leaf_and_tree_row_major_keep::>( + "s2_device_parity", + "S2 main parity", + &data, + None, + n, + m, + blowup, + &weights, + true, + layout.rows_per_leaf(), + ) + .ok_or_else(|| format!("{what}: the device commit declined"))?; + let (host, host_root) = + P::::commit_rows_bit_reversed_with(&lde, m, layout.rows_per_leaf()) + .ok_or_else(|| format!("{what}: host commit failed"))?; + if tree.root != host_root { + return Err(format!("{what}: returned root-only tree differs")); + } + let dev = handle + .tree + .as_ref() + .ok_or_else(|| format!("{what}: no resident tree"))?; + check_tree( + &what, + dev, + &host, + &host_root, + leaves_of(lde_len, layout), + &mut rng, + )?; + // The R4 opening values: the rows a query opens, off the resident LDE. + let queries = open_positions(leaves_of(lde_len, layout), &mut rng); + let rows: Vec = queries + .iter() + .flat_map(|&q| { + let (row, sym) = layout.query_rows(q, lde_len); + core::iter::once(row as u32).chain(sym.map(|r| r as u32)) + }) + .collect(); + let stream = math_cuda::device::backend() + .map_err(|e| format!("{what}: {e:?}"))? + .next_stream(); + let got = math_cuda::barycentric::gather_rows_base_on_device(&handle, &rows, &stream) + .map_err(|e| format!("{what}: device row gather failed: {e:?}"))?; + for (i, &r) in rows.iter().enumerate() { + let want: Vec = lde[r as usize * m..(r as usize + 1) * m] + .iter() + .map(|x| x.canonical()) + .collect(); + let have: Vec = got[i * m..(i + 1) * m] + .iter() + .map(|&x| Felt::from(x).canonical()) + .collect(); + if have != want { + return Err(format!("{what}: device row gather differs at LDE row {r}")); + } + } + roots.push(host_root); + } + if roots[0] == roots[1] { + return Err(format!( + "main {n}x{m}: the one-row root equals the row-pair root" + )); + } + Ok(format!( + "main {n}x{m} blowup {blowup}: one-row and row-pair trees equal the host, \ + one-row tree {lde_len} leaves" + )) +} + +/// The preprocessed split commit (`try_expand_split_trees_row_major_keep`): +/// the precomputed tree (full host tree) and the multiplicity tree (resident) +/// at one row and row pairs, against the host subset commits. +pub fn split_tree_parity( + n: usize, + m: usize, + split: usize, + blowup: usize, + seed: u64, +) -> Result { + let mut rng = seed; + let data = base_values(n * m, &mut rng); + let weights = base_values(n, &mut rng); + let lde_len = n * blowup; + let mut roots = Vec::new(); + for layout in LAYOUTS { + let rpl = layout.rows_per_leaf(); + let what = format!("split {n}x{m} at {split} blowup {blowup} {layout:?}"); + let (pre, mult, handle, lde) = + crate::gpu_lde::try_expand_split_trees_row_major_keep::>( + "s2_device_parity", + &data, + None, + n, + m, + blowup, + &weights, + split, + true, + true, + rpl, + ) + .ok_or_else(|| format!("{what}: the device commit declined"))?; + let pre = pre.ok_or_else(|| format!("{what}: no precomputed tree"))?; + let (host_pre, host_pre_root) = + P::::commit_rows_bit_reversed_subset_with(&lde, m, 0, split, rpl) + .ok_or_else(|| format!("{what}: host precomputed commit failed"))?; + let (host_mult, host_mult_root) = + P::::commit_rows_bit_reversed_subset_with(&lde, m, split, m, rpl) + .ok_or_else(|| format!("{what}: host multiplicity commit failed"))?; + if pre.root != host_pre_root { + return Err(format!("{what}: precomputed root differs")); + } + let num_leaves = leaves_of(lde_len, layout); + for pos in open_positions(num_leaves, &mut rng) { + if pre.get_proof_by_pos(pos).map(|p| p.merkle_path) + != host_pre.get_proof_by_pos(pos).map(|p| p.merkle_path) + { + return Err(format!("{what}: precomputed path of leaf {pos} differs")); + } + } + if mult.root != host_mult_root { + return Err(format!("{what}: multiplicity root differs")); + } + let dev = handle + .tree + .as_ref() + .ok_or_else(|| format!("{what}: no resident tree"))?; + check_tree( + &what, + dev, + &host_mult, + &host_mult_root, + num_leaves, + &mut rng, + )?; + roots.push(host_pre_root); + } + if roots[0] == roots[1] { + return Err(format!( + "split {n}x{m}: the one-row root equals the row-pair root" + )); + } + Ok(format!( + "split {n}x{m} at {split} blowup {blowup}: both subset trees equal the host at both layouts" + )) +} + +/// The aux commits: the fused ext3 commit from a host trace +/// (`try_expand_leaf_and_tree_ext3_row_major_keep`) and from a resident aux +/// trace (`..._keep_dev`, the LogUp aux path), at one row and row pairs; the +/// two must agree with each other and with the host commit, and the device +/// ext3 row gather must return the rows a query opens. +pub fn aux_tree_parity( + n: usize, + m: usize, + blowup: usize, + seed: u64, +) -> Result { + let mut rng = seed; + let data = ext_values(n * m, &mut rng); + let weights = base_values(n, &mut rng); + let lde_len = n * blowup; + let raw: Vec = data + .iter() + .flat_map(|x| x.value().iter().map(|c| c.canonical()).collect::>()) + .collect(); + let mut roots = Vec::new(); + for layout in LAYOUTS { + let rpl = layout.rows_per_leaf(); + let what = format!("aux {n}x{m} blowup {blowup} {layout:?}"); + let (tree, handle, lde) = crate::gpu_lde::try_expand_leaf_and_tree_ext3_row_major_keep::< + F, + E, + H::Batched, + >( + "s2_device_parity", &data, n, m, blowup, &weights, true, rpl + ) + .ok_or_else(|| format!("{what}: the device commit declined"))?; + let (host, host_root) = P::::commit_rows_bit_reversed_with(&lde, m, rpl) + .ok_or_else(|| format!("{what}: host commit failed"))?; + if tree.root != host_root { + return Err(format!("{what}: returned root-only tree differs")); + } + let dev = handle + .tree + .as_ref() + .ok_or_else(|| format!("{what}: no resident tree"))?; + check_tree( + &what, + dev, + &host, + &host_root, + leaves_of(lde_len, layout), + &mut rng, + )?; + + // The resident arm over the same trace (uploaded as the LogUp build + // would leave it: row-major ext3). + let be = math_cuda::device::backend().map_err(|e| format!("{what}: {e:?}"))?; + let stream = be.next_stream(); + let buf = stream + .clone_htod(&raw) + .map_err(|e| format!("{what}: upload failed: {e:?}"))?; + stream.synchronize().map_err(|e| format!("{what}: {e:?}"))?; + let ra = math_cuda::logup::ResidentAux { + buf: Arc::new(buf), + num_aux_cols: m, + num_rows: n, + table_contribution: [0; 3], + }; + let (rtree, rhandle, _) = + crate::gpu_lde::try_expand_leaf_and_tree_ext3_row_major_keep_dev::>( + "s2_device_parity", + &ra, + blowup, + &weights, + true, + rpl, + ) + .ok_or_else(|| format!("{what}: the resident aux commit declined"))?; + if rtree.root != host_root { + return Err(format!( + "{what}: the resident aux root differs from the host root" + )); + } + let rdev = rhandle + .tree + .as_ref() + .ok_or_else(|| format!("{what}: no resident aux tree"))?; + check_tree( + &format!("{what} (resident)"), + rdev, + &host, + &host_root, + leaves_of(lde_len, layout), + &mut rng, + )?; + + let queries = open_positions(leaves_of(lde_len, layout), &mut rng); + let rows: Vec = queries + .iter() + .flat_map(|&q| { + let (row, sym) = layout.query_rows(q, lde_len); + core::iter::once(row as u32).chain(sym.map(|r| r as u32)) + }) + .collect(); + let got = math_cuda::barycentric::gather_rows_ext3_on_device(&handle, &rows, &stream) + .map_err(|e| format!("{what}: device ext3 row gather failed: {e:?}"))?; + let got = crate::constraint_ir::gpu_interp::ext3_u64_to_field::(&got) + .ok_or_else(|| format!("{what}: gather is not ext3"))?; + for (i, &r) in rows.iter().enumerate() { + if got[i * m..(i + 1) * m] != lde[r as usize * m..(r as usize + 1) * m] { + return Err(format!( + "{what}: device ext3 row gather differs at LDE row {r}" + )); + } + } + roots.push(host_root); + } + if roots[0] == roots[1] { + return Err(format!( + "aux {n}x{m}: the one-row root equals the row-pair root" + )); + } + Ok(format!( + "aux {n}x{m} blowup {blowup}: host-input and resident trees equal the host at both layouts" + )) +} + +/// The composition trees: from host part evaluations +/// (`try_build_comp_poly_tree_gpu`) and from resident part slabs +/// (`try_build_comp_poly_tree_gpu_from_dev`), at one row and row pairs, +/// against `commit_bit_reversed_with` over the parts; and the device gather of +/// the parts at a query's rows. +pub fn composition_tree_parity( + lde_len: usize, + parts: usize, + seed: u64, +) -> Result { + let mut rng = seed; + let evals: Vec> = (0..parts).map(|_| ext_values(lde_len, &mut rng)).collect(); + // The resident layout: part `c` component `k` is the slab `(c·3 + k)`. + let mut slabs = vec![0u64; 3 * parts * lde_len]; + for (c, part) in evals.iter().enumerate() { + for (r, x) in part.iter().enumerate() { + for (k, comp) in x.value().iter().enumerate() { + slabs[(c * 3 + k) * lde_len + r] = comp.canonical(); + } + } + } + let be = math_cuda::device::backend().map_err(|e| format!("composition: {e:?}"))?; + let stream = be.next_stream(); + let buf = stream + .clone_htod(&slabs) + .map_err(|e| format!("composition: upload failed: {e:?}"))?; + stream + .synchronize() + .map_err(|e| format!("composition: {e:?}"))?; + let handle = math_cuda::lde::GpuLdeExt3 { + buf: Arc::new(buf), + m: parts, + lde_size: lde_len, + tree: None, + ready: None, + }; + let mut roots = Vec::new(); + for layout in LAYOUTS { + let rpl = layout.rows_per_leaf(); + let what = format!("composition lde {lde_len} parts {parts} {layout:?}"); + let (host, host_root) = + crate::commitment::commit_bit_reversed_with::>(&evals, rpl) + .ok_or_else(|| format!("{what}: host commit failed"))?; + let (tree, dev) = + crate::gpu_lde::try_build_comp_poly_tree_gpu::>(&evals, rpl) + .ok_or_else(|| format!("{what}: the device tree (host parts) declined"))?; + if tree.root != host_root { + return Err(format!("{what}: returned root-only tree differs")); + } + check_tree( + &what, + &dev, + &host, + &host_root, + leaves_of(lde_len, layout), + &mut rng, + )?; + let (rtree, rdev) = + crate::gpu_lde::try_build_comp_poly_tree_gpu_from_dev::>(&handle, rpl) + .ok_or_else(|| format!("{what}: the device tree (resident parts) declined"))?; + if rtree.root != host_root { + return Err(format!("{what}: the resident-parts root differs")); + } + check_tree( + &format!("{what} (resident parts)"), + &rdev, + &host, + &host_root, + leaves_of(lde_len, layout), + &mut rng, + )?; + // The R4 composition opening values off the resident parts. + let queries = open_positions(leaves_of(lde_len, layout), &mut rng); + let rows: Vec = queries + .iter() + .flat_map(|&q| { + let (row, sym) = layout.query_rows(q, lde_len); + core::iter::once(row as u32).chain(sym.map(|r| r as u32)) + }) + .collect(); + let got = math_cuda::barycentric::gather_rows_ext3_on_device(&handle, &rows, &stream) + .map_err(|e| format!("{what}: device parts gather failed: {e:?}"))?; + let got = crate::constraint_ir::gpu_interp::ext3_u64_to_field::(&got) + .ok_or_else(|| format!("{what}: gather is not ext3"))?; + for (i, &r) in rows.iter().enumerate() { + let want: Vec = evals.iter().map(|p| p[r as usize]).collect(); + if got[i * parts..(i + 1) * parts] != want[..] { + return Err(format!( + "{what}: device parts gather differs at LDE row {r}" + )); + } + } + roots.push(host_root); + } + if roots[0] == roots[1] { + return Err(format!( + "composition lde {lde_len}: the one-row root equals the row-pair root" + )); + } + Ok(format!( + "composition lde {lde_len} parts {parts}: host-parts and resident-parts trees equal the host at both layouts" + )) +} + +/// The (e) leaf-digest KAT (`fri::vectors::one_row_leaf_digests_json`): the +/// same 16-row base (5 columns) and ext3 (2 columns) matrices, hashed by the +/// device row-major leaf kernels at one row and row pairs, against the CPU +/// leaves the checked-in `e_leaf_digests_{hash}.json` was generated from. +pub fn leaf_digest_parity() -> Result { + const ROWS: usize = 16; + let mut st = crate::fri::vectors::KAT_SEED + 100; + let base: Vec> = (0..5) + .map(|_| (0..ROWS).map(|_| Felt::from(splitmix64(&mut st))).collect()) + .collect(); + let mut st = crate::fri::vectors::KAT_SEED + 200; + let ext: Vec> = (0..2) + .map(|_| { + (0..ROWS) + .map(|_| crate::fri::vectors::next_ext(&mut st)) + .collect() + }) + .collect(); + // Row-major u64 views (an ext3 element = three consecutive u64). + let base_rm: Vec = (0..ROWS) + .flat_map(|r| base.iter().map(move |c| c[r].canonical())) + .collect(); + let ext_rm: Vec = (0..ROWS) + .flat_map(|r| { + ext.iter().flat_map(move |c| { + c[r].value() + .iter() + .map(|x| x.canonical()) + .collect::>() + }) + }) + .collect(); + let hash = crate::gpu_lde::device_hash_of::>(); + for layout in LAYOUTS { + let rpl = layout.rows_per_leaf(); + let want_b = crate::commitment::leaves_bit_reversed_grouped::>(&base, rpl); + let want_e = crate::commitment::leaves_bit_reversed_grouped::>(&ext, rpl); + let got_b = math_cuda::lde::row_major_leaves(hash, &base_rm, 5, 0, 5, ROWS, rpl) + .map_err(|e| format!("leaf KAT base {layout:?}: {e:?}"))?; + let got_e = math_cuda::lde::row_major_leaves(hash, &ext_rm, 6, 0, 6, ROWS, rpl) + .map_err(|e| format!("leaf KAT ext3 {layout:?}: {e:?}"))?; + let flat = |v: &[Commitment]| v.iter().flatten().copied().collect::>(); + if got_b != flat(&want_b) { + return Err(format!( + "leaf KAT base {layout:?}: device leaves differ from the CPU" + )); + } + if got_e != flat(&want_e) { + return Err(format!( + "leaf KAT ext3 {layout:?}: device leaves differ from the CPU" + )); + } + } + Ok(String::from( + "the (e) leaf-digest KAT: device leaves equal the CPU at both layouts", + )) +} + +/// Every tree entry at the shapes the box runs: narrow and wide, blowup 2 and +/// 4, the LDE floor (2^14) and a production-sized 2^20 LDE. Prints one +/// `S2DEV` line per case and a summary line; `Err` lists every failure. +pub fn run_tree_parity(name: &str) -> Result> { + let mut results: Vec> = vec![leaf_digest_parity::()]; + for (i, &(n, m, blowup)) in [ + (1usize << 12, 1usize, 4usize), + (1 << 13, 20, 2), + (1 << 12, 134, 4), + (1 << 18, 7, 4), + ] + .iter() + .enumerate() + { + results.push(main_tree_parity::(n, m, blowup, 0x5230_0000 + i as u64)); + } + for (i, &(n, m, split, blowup)) in [(1usize << 12, 5usize, 2usize, 4usize), (1 << 18, 9, 4, 4)] + .iter() + .enumerate() + { + results.push(split_tree_parity::( + n, + m, + split, + blowup, + 0x5231_0000 + i as u64, + )); + } + for (i, &(n, m, blowup)) in [ + (1usize << 12, 1usize, 4usize), + (1 << 13, 13, 2), + (1 << 18, 5, 4), + ] + .iter() + .enumerate() + { + results.push(aux_tree_parity::(n, m, blowup, 0x5232_0000 + i as u64)); + } + for (i, &(lde, parts)) in [(1usize << 14, 1usize), (1 << 14, 2), (1 << 20, 2)] + .iter() + .enumerate() + { + results.push(composition_tree_parity::( + lde, + parts, + 0x5233_0000 + i as u64, + )); + } + let total = results.len(); + let mut failures = Vec::new(); + for r in results { + match r { + Ok(msg) => std::println!("S2DEV {name} {msg}"), + Err(e) => failures.push(e), + } + } + if failures.is_empty() { + std::println!("S2DEV {name}: {total} tree cases equal"); + Ok(total) + } else { + Err(failures) + } +} diff --git a/crypto/stark/src/tests/cap_fri_matrix_tests.rs b/crypto/stark/src/tests/cap_fri_matrix_tests.rs new file mode 100644 index 000000000..f4278816a --- /dev/null +++ b/crypto/stark/src/tests/cap_fri_matrix_tests.rs @@ -0,0 +1,267 @@ +//! Merkle caps (S1) composed with group-leaf FRI layers (S3) on the host path: +//! a round-trip matrix {cap off, fixed, auto} × {pair, dp, +//! dp with an uneven override}, at a query count where `auto` caps (Q ≥ 20). +//! +//! Under a fold schedule a committed FRI layer is a GROUP tree whose depth is +//! the layout's (`FriFoldLayout::layer_depth`), not today's +//! `log2(lde) − j − 2`. The cap of each layer is taken at that depth, by the +//! prover (`StarkCaps::from_layout` in round 4) and by the verifier (the +//! per-tree `TreeCheck` the group path authenticates with). These tests pin: +//! - every cell of the matrix proves and verifies, owned and archived; +//! - every FRI layer's paths have the capped shape at the LAYOUT's depth, +//! computed here independently from the schedule; +//! - every cap node of a capped group layer is bound, and an unreached one is +//! rejected by the cap-to-root check alone (on a group tree); +//! - a proof made under one (cap, fri) format fails under the others. + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use crypto::merkle_tree::cap::{CapPolicy, verify_cap}; +use math::field::element::FieldElement; +use math::field::goldilocks::GoldilocksField; + +use crate::config::{Commitment, DefaultStarkHash, StarkHash}; +use crate::examples::simple_addition::{ + SimpleAdditionAIR, SimpleAdditionPublicInputs, simple_addition_trace, +}; +use crate::fri::capture::{FriCapture, capture}; +use crate::fri::schedule::FriFormat; +use crate::merkle_caps::StarkCaps; +use crate::proof::options::{FriMode, FriScheduleOverride, ProofFormat, ProofOptions}; +use crate::proof::stark::{MultiProof, StarkProof}; +use crate::prover::{IsStarkProver, Prover}; +use crate::traits::AIR; +use crate::verifier::{IsStarkVerifier, Verifier}; + +type F = GoldilocksField; +type FE = FieldElement; +type PI = SimpleAdditionPublicInputs; +type Proof = StarkProof; +/// The leaf backend the FRI layer trees are verified with (the FRI values +/// live in the proof's extension, which is `F` for this AIR). +type Leaf = ::Batched; + +/// 1024 rows at blowup 2 with `k = 2`: LDE 2^11, terminal 2^3, so the +/// committed chain covers 10 → 3 (seven bits). +const ROWS: usize = 1024; +const LDE_LOG: u32 = 11; +const TERMINAL_LOG: u32 = 3; +/// `auto` caps at height 3 from 20 openings on. +const QUERIES: usize = 24; + +fn options(cap: CapPolicy, fri: FriMode, over: Option<&[u8]>, queries: usize) -> ProofOptions { + let mut o = ProofOptions::default_test_options(); + o.blowup_factor = 2; + o.fri_number_of_queries = queries; + o.grinding_factor = 0; + o.fri_final_poly_log_degree = 2; + o.format = ProofFormat { + merkle_cap: cap, + fri_mode: fri, + fri_schedule_override: over.and_then(FriScheduleOverride::new), + ..ProofFormat::DEFAULT + }; + o +} + +fn prove(opts: &ProofOptions) -> (SimpleAdditionAIR, Proof) { + let air = SimpleAdditionAIR::::new(opts); + let pub_inputs = SimpleAdditionPublicInputs { + a: FE::from(1u64), + b: FE::from(2u64), + }; + let mut trace = simple_addition_trace::(ROWS); + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .expect("proving must succeed"); + (air, proof) +} + +fn verifies(air: &SimpleAdditionAIR, proof: &Proof) -> bool { + Verifier::verify(proof, air, &mut DefaultTranscript::::new(&[])) +} + +fn verifies_archived(air: &SimpleAdditionAIR, proof: &Proof) -> bool { + let multi = MultiProof { + proofs: vec![proof.clone()], + }; + let bytes = rkyv::to_bytes::(&multi).unwrap(); + let archived = rkyv::access::< + crate::proof::stark::ArchivedMultiProof, + rkyv::rancor::Error, + >(&bytes) + .unwrap(); + let airs: Vec<&dyn AIR> = vec![air]; + Verifier::multi_verify_archived( + &airs, + archived, + &mut DefaultTranscript::::new(&[]), + &FE::zero(), + ) +} + +/// The committed layers' tree depths, from the schedule alone: the chain +/// starts at `lde_log − 1` and layer `j`'s tree is its length over `2^{d_j}` +/// leaves. Independent of `FriFoldLayout::layer_depth`. +fn schedule_depths(opts: &ProofOptions) -> (Vec, Vec) { + let schedule = FriFormat::from_options(opts, false).schedule(LDE_LOG, TERMINAL_LOG); + let mut b = LDE_LOG as usize - 1; + let depths = schedule + .iter() + .map(|&d| { + b -= d as usize; + b + }) + .collect(); + (schedule, depths) +} + +fn fri_paths(proof: &Proof, layer: usize) -> Vec<&Vec> { + proof + .query_list + .iter() + .map(|q| &q.layers_auth_paths[layer].merkle_path) + .collect() +} + +const FORMATS: [(&str, FriMode, Option<&[u8]>); 3] = [ + ("pair", FriMode::Pair, None), + ("dp", FriMode::Dp, None), + ("dp [3,1,3]", FriMode::Dp, Some(&[3, 1, 3])), +]; + +#[test] +fn the_cap_and_fri_matrix_round_trips_owned_and_archived() { + for cap in [CapPolicy::Off, CapPolicy::Fixed(2), CapPolicy::Auto] { + for (name, fri, over) in FORMATS { + let opts = options(cap, fri, over, QUERIES); + let (air, proof) = prove(&opts); + let (schedule, depths) = schedule_depths(&opts); + let caps = StarkCaps::for_options(&opts, LDE_LOG as usize, false).expect("layout"); + assert_eq!( + caps.fri_depths, depths, + "cap={cap} fri={name}: the caps' FRI depths are the layout's" + ); + assert_eq!(proof.fri_layers_merkle_roots.len(), schedule.len()); + for (j, root) in proof.fri_layers_merkle_roots.iter().enumerate() { + let (d, c) = (depths[j], caps.fri[j]); + assert_eq!(c, cap.height(QUERIES, d), "cap={cap} fri={name} layer {j}"); + let paths = fri_paths(&proof, j); + let owner = if c == 0 { d } else { d - c + (1 << c) }; + assert_eq!( + paths[0].len(), + owner, + "cap={cap} fri={name} layer {j} owner" + ); + for p in &paths[1..] { + assert_eq!(p.len(), d - c, "cap={cap} fri={name} layer {j}"); + } + if c > 0 { + assert!( + verify_cap::(&paths[0][d - c..], root, c), + "cap={cap} fri={name} layer {j}: the owner's cap hashes to the root" + ); + } + } + if cap != CapPolicy::Off { + assert!( + caps.fri.iter().any(|&c| c > 0), + "cap={cap} fri={name}: some FRI layer must be capped" + ); + } + assert!(verifies(&air, &proof), "cap={cap} fri={name}"); + assert!( + verifies_archived(&air, &proof), + "cap={cap} fri={name}: archived" + ); + } + } +} + +#[test] +fn every_cap_node_of_a_capped_group_layer_is_bound() { + let opts = options(CapPolicy::Auto, FriMode::Dp, Some(&[3, 1, 3]), QUERIES); + let (air, honest) = prove(&opts); + assert!(verifies(&air, &honest)); + let (_, depths) = schedule_depths(&opts); + // Layer 0 folds by 8 (depth 7) and layer 2 by 8 (depth 3): both capped at 3. + for j in [0usize, 2] { + let (d, c) = (depths[j], CapPolicy::Auto.height(QUERIES, depths[j])); + assert_eq!(c, 3, "layer {j}"); + for k in 0..(1usize << c) { + let mut bad = honest.clone(); + bad.query_list[0].layers_auth_paths[j].merkle_path[d - c + k][5] ^= 1; + assert!( + !verifies(&air, &bad) && !verifies_archived(&air, &bad), + "group layer {j}: cap node {k} flipped" + ); + } + // A later query's path, cut at the cap (layer 2's tree is all cap: + // depth 3 at c = 3, so its paths are empty). + assert_eq!( + honest.query_list[7].layers_auth_paths[j].merkle_path.len(), + d - c + ); + if d > c { + let mut bad = honest.clone(); + bad.query_list[7].layers_auth_paths[j].merkle_path[0][0] ^= 1; + assert!(!verifies(&air, &bad), "group layer {j}: query 7 sibling"); + } + } +} + +/// An unreached cap node on a group tree: with three queries and a height-3 cap on +/// FRI layer 0, at least five of its eight cap nodes are reached by no query. +/// Flipping one leaves every per-query fold untouched (each still lands on its +/// own cap node), so only the cap-to-root check of the group layer's +/// `TreeCheck` rejects the proof. +#[test] +fn an_unreached_cap_node_of_a_group_layer_is_rejected_by_the_cap_to_root_check_alone() { + let opts = options(CapPolicy::Fixed(3), FriMode::Dp, Some(&[3, 1, 3]), 3); + let (air, honest) = prove(&opts); + let (ok, records) = + capture(|| Verifier::verify(&honest, &air, &mut DefaultTranscript::::new(&[]))); + assert!(ok); + let rec = FriCapture::::from_any(records[0].as_ref()).expect("one record"); + let (schedule, depths) = schedule_depths(&opts); + let (d0, d, c) = (schedule[0] as usize, depths[0], 3usize); + // Layer 0's leaf is `iota >> d0`; its cap node is `leaf >> (d − c)`. + let reached: Vec = rec.iotas.iter().map(|i| (i >> d0) >> (d - c)).collect(); + let unreached: Vec = (0..8).filter(|k| !reached.contains(k)).collect(); + assert!(unreached.len() >= 5, "3 queries reach at most 3 of 8 nodes"); + for k in unreached { + let mut bad = honest.clone(); + bad.query_list[0].layers_auth_paths[0].merkle_path[d - c + k][3] ^= 1; + assert!(!verifies(&air, &bad), "unreached cap node {k}"); + assert!( + !verifies_archived(&air, &bad), + "unreached cap node {k}: archived" + ); + } +} + +/// The (cap, fri) format is a verifier constant: a proof made under one fails +/// under every other cell of the matrix. +#[test] +fn a_proof_made_under_one_cap_and_fri_format_fails_under_another() { + let cells = [ + (CapPolicy::Off, FriMode::Pair), + (CapPolicy::Auto, FriMode::Pair), + (CapPolicy::Off, FriMode::Dp), + (CapPolicy::Auto, FriMode::Dp), + ]; + for (i, &(cap, fri)) in cells.iter().enumerate() { + let (_, proof) = prove(&options(cap, fri, None, QUERIES)); + for (k, &(cap_v, fri_v)) in cells.iter().enumerate() { + let air = SimpleAdditionAIR::::new(&options(cap_v, fri_v, None, QUERIES)); + assert_eq!( + verifies(&air, &proof), + i == k, + "proved at ({cap}, {fri:?}), verified at ({cap_v}, {fri_v:?})" + ); + } + } +} diff --git a/crypto/stark/src/tests/fri_group_tests.rs b/crypto/stark/src/tests/fri_group_tests.rs new file mode 100644 index 000000000..b0ac56418 --- /dev/null +++ b/crypto/stark/src/tests/fri_group_tests.rs @@ -0,0 +1,618 @@ +//! S3 (group-leaf FRI layers) on the CPU prover and host verifier: the +//! round trips U4–U6, the tamper tests T1–T3, the load-bearing mutations M1–M2 and the +//! differential of the group path at the all-ones schedule against the legacy +//! path. + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use crypto::fiat_shamir::is_transcript::IsTranscript; +use math::fft::bit_reversing::{in_place_bit_reverse_permute, reverse_index}; +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; +use math::field::goldilocks::GoldilocksField; +use math::field::traits::IsFFTField; +use math::polynomial::Polynomial; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; + +use crate::config::{Blake3StarkHash, KeccakStarkHash, StarkHash}; +use crate::fri::fri_functions::compute_coset_twiddles_inv; +use crate::fri::group::{ + GROUP_MUTATION, GroupMutation, group_fold, roots_of_unity_table, verify_query_groups, +}; +use crate::fri::terminal::{FriFoldLayout, terminal_codeword_from_coeffs}; +use crate::fri::{commit_phase_with_layout, fold_times, query_phase_with_layout}; +use crate::merkle_caps::TreeCheck; +use crate::proof::options::{FriMode, FriScheduleOverride, ProofFormat}; +use crate::traits::AIR; + +use super::zf_golden_tests::{ + fingerprint, golden_options, prove_logup, prove_multi, prove_simple_addition, verify_logup, + verify_multi, verify_simple_addition, +}; + +type F = GoldilocksField; +type E = Degree3GoldilocksExtensionField; +type Felt = FieldElement; +type Ext = FieldElement; + +fn rand_ext(rng: &mut ChaCha20Rng) -> Ext { + Ext::new([ + Felt::from(rng.r#gen::()), + Felt::from(rng.r#gen::()), + Felt::from(rng.r#gen::()), + ]) +} + +/// The point at position `p` of a bit-reversed layer of length `2^b` on the +/// coset `o·⟨ω_{2^b}⟩`. +fn point(o: &Felt, b: u32, p: usize) -> Felt { + let w = F::get_primitive_root_of_unity(u64::from(b)).unwrap(); + o * w.pow(reverse_index(p, 1u64 << b) as u64) +} + +/// A bit-reversed coset codeword of a random ext3 polynomial with `num_coeffs` +/// coefficients over `2^b` points; returns (codeword, coefficients). +fn random_codeword( + rng: &mut ChaCha20Rng, + b: u32, + num_coeffs: usize, + o: &Felt, +) -> (Vec, Vec) { + let coeffs: Vec = (0..num_coeffs).map(|_| rand_ext(rng)).collect(); + let poly = Polynomial::new(&coeffs); + let n = 1usize << b; + let mut cw = Polynomial::evaluate_offset_fft::( + &poly, + n / num_coeffs.next_power_of_two(), + Some(num_coeffs), + o, + ) + .expect("fft"); + assert_eq!(cw.len(), n); + in_place_bit_reverse_permute(&mut cw); + (cw, coeffs) +} + +fn dp_with(schedule: Option<&[u8]>) -> ProofFormat { + ProofFormat { + fri_mode: FriMode::Dp, + fri_schedule_override: schedule.map(|s| FriScheduleOverride::new(s).unwrap()), + ..ProofFormat::DEFAULT + } +} + +// --------------------------------------------------------------------------- +// U5: a group leaf is a coset, and its base folds to the next layer's point. +// --------------------------------------------------------------------------- + +#[test] +fn group_leaf_is_a_coset() { + let o = Felt::from(3u64); + for b in 1..=10u32 { + for d in 1..=b.min(6) { + let roots = roots_of_unity_table::(d).unwrap(); + // The table's root is the layer domain's ω_{2^b}^{2^{b−d}}. + let w_b = F::get_primitive_root_of_unity(u64::from(b)).unwrap(); + assert_eq!(roots[1], w_b.pow(1u64 << (b - d)), "b={b} d={d}"); + let o_next = o.pow(1u64 << d); + for g in 0..(1usize << (b - d)) { + let x_g = &o * w_b.pow(reverse_index(g, 1u64 << (b - d)) as u64); + for t in 0..(1usize << d) { + let want = &x_g * &roots[reverse_index(t, 1u64 << d)]; + assert_eq!(point(&o, b, (g << d) + t), want, "b={b} d={d} g={g} t={t}"); + } + // x_g^{2^d} is position g of the layer folded d times. + assert_eq!( + x_g.pow(1u64 << d), + point(&o_next, b - d, g), + "b={b} d={d} g={g}" + ); + } + } + } +} + +// --------------------------------------------------------------------------- +// U4: the group fold = d binary folds with ζ, ζ², … = 2^d·Σ ζ^i f_i. +// --------------------------------------------------------------------------- + +#[test] +fn group_fold_equals_d_binary_folds() { + let mut rng = ChaCha20Rng::seed_from_u64(0x5334); + let o = Felt::from(3u64); + let b = 9u32; + let n = 1usize << b; + let (codeword, coeffs) = random_codeword(&mut rng, b, n, &o); + for d in 1..=6u32 { + let zeta = rand_ext(&mut rng); + // Prover: d binary folds with ζ^{2^ℓ} (`fold_times`, the commit loop's). + let mut folded = codeword.clone(); + let mut tw = compute_coset_twiddles_inv::(&o, n); + fold_times(&mut folded, &zeta, d, &mut tw); + assert_eq!(folded.len(), n >> d); + + let roots = roots_of_unity_table::(d).unwrap(); + let o_next = o.pow(1u64 << d); + let two_d = Felt::from(1u64 << d); + for g in 0..(n >> d) { + let group = &codeword[g << d..(g + 1) << d]; + // Verifier: the group fold from ANY slot's point gives the same value. + for s in 0..(1usize << d) { + let y_inv = point(&o, b, (g << d) + s).inv().unwrap(); + let x_g_inv = &y_inv * &roots[reverse_index(s, 1u64 << d)]; + assert_eq!( + group_fold::(group, &zeta, &x_g_inv, &roots), + folded[g], + "d={d} g={g} slot={s}" + ); + } + // The polynomial identity: 2^d · Σ_i ζ^i f_i(Y), f(X) = Σ X^i f_i(X^{2^d}). + let y = point(&o_next, b - d, g); + let mut acc = Ext::zero(); + let mut zp = Ext::one(); + for i in 0..(1usize << d) { + let mut fi = Ext::zero(); + let mut yp = Felt::one(); + for k in (i..n).step_by(1 << d) { + fi += &yp * &coeffs[k]; + yp = &yp * &y; + } + acc += &zp * &fi; + zp = &zp * ζ + } + assert_eq!(folded[g], &two_d * &acc, "d={d} g={g}: 2^d·Σζ^i f_i"); + } + } +} + +// --------------------------------------------------------------------------- +// FRI-level harness (M1, M2): commit a codeword, open queries, verify with the +// same group checks the host verifier runs. +// --------------------------------------------------------------------------- + +struct FriRun { + layout: FriFoldLayout, + lde_log: u32, + roots: Vec<[u8; 32]>, + zetas: Vec, + coeffs: Vec, + decommitments: Vec>, + iotas: Vec, + terminal_offset: Felt, +} + +/// FRI over `committed` (what the prover commits); the transcript is replayed +/// to recover ζ. `lde_log` 10, blowup 4 (log 2), k 1: the chain covers 9 → 3, +/// schedule `[3, 1, 2]`. +fn fri_run(committed: &[Ext], o: &Felt) -> FriRun { + let lde_log = 10u32; + let n = 1usize << lde_log; + assert_eq!(committed.len(), n); + let layout = FriFoldLayout::from_schedule(lde_log, 2, 1, false, vec![3, 1, 2]).unwrap(); + assert!(!layout.is_legacy()); + let tw = compute_coset_twiddles_inv::(o, n); + let mut transcript = DefaultTranscript::::new(&[7]); + let (coeffs, layers) = commit_phase_with_layout::( + committed.to_vec(), + &mut transcript, + o, + n, + 2, + 1, + &layout, + &tw, + ); + let roots: Vec<[u8; 32]> = layers.iter().map(|l| l.merkle_tree.root).collect(); + let mut replay = DefaultTranscript::::new(&[7]); + let mut zetas = Vec::new(); + for r in &roots { + zetas.push(replay.sample_field_element()); + replay.append_bytes(r); + } + zetas.push(replay.sample_field_element()); + let iotas: Vec = (0..n / 2).step_by(37).collect(); + let decommitments = query_phase_with_layout::(&layers, &iotas, &layout); + FriRun { + terminal_offset: o.pow(1u64 << layout.total_folds), + layout, + lde_log, + roots, + zetas, + coeffs, + decommitments, + iotas, + } +} + +/// Verify every query of `run` with DEEP values read from `deep` (the +/// codeword the VERIFIER believes in, bit-reversed). +fn fri_accepts(run: &FriRun, deep: &[Ext], o: &Felt) -> bool { + let terminal = terminal_codeword_from_coeffs::( + &run.coeffs, + &run.terminal_offset, + run.layout.terminal_len, + ); + let tables: Vec> = (0..=6) + .map(|d| roots_of_unity_table::(d).unwrap()) + .collect(); + // One uncapped check per layer tree, at the layout's group-tree depth. + let checks: Vec> = run + .roots + .iter() + .enumerate() + .map(|(j, root)| { + let depth = run.layout.layer_depth(run.lde_log, j) as usize; + TreeCheck::build::>(root, depth, 0, || None).unwrap() + }) + .collect(); + run.iotas + .iter() + .zip(&run.decommitments) + .all(|(&iota, dec)| { + let x = point(o, run.lde_log, 2 * iota); + let x_inv = x.inv().unwrap(); + let (p0, p0s) = (&deep[2 * iota], &deep[2 * iota + 1]); + let v = (p0 + p0s) + &x_inv * &run.zetas[0] * (p0 - p0s); + verify_query_groups::>( + &run.layout, + &checks, + 0, + |j| dec.layers_auth_paths[j].merkle_path.as_slice(), + &dec.layers_evaluations_sym, + &run.zetas, + iota, + v, + x_inv.square(), + &terminal, + &tables, + ) + }) +} + +fn with_mutation(m: GroupMutation, f: impl FnOnce() -> T) -> T { + GROUP_MUTATION.with(|c| c.set(m)); + let out = f(); + GROUP_MUTATION.with(|c| c.set(GroupMutation::None)); + out +} + +/// A low-degree ext3 codeword on LDE 2^10, blowup 4 (256 coefficients). +fn low_degree(seed: u64, o: &Felt) -> Vec { + let mut rng = ChaCha20Rng::seed_from_u64(seed); + random_codeword(&mut rng, 10, 256, o).0 +} + +#[test] +fn honest_fri_run_is_accepted() { + let o = Felt::from(3u64); + let p0 = low_degree(1, &o); + let run = fri_run::(&p0, &o); + assert_eq!(run.roots.len(), 3); + assert!(fri_accepts::(&run, &p0, &o)); + let run = fri_run::(&p0, &o); + assert!(fri_accepts::(&run, &p0, &o)); +} + +/// M1 — the slot check is load-bearing. A prover commits FRI for +/// `p₀ + c` (still low degree, so every layer and the terminal are +/// consistent) while the trace openings say `p₀`: only `group[slot] == v` at +/// the first committed layer ties FRI to the DEEP value. With it the forgery +/// is rejected; with it skipped (the mutation) it is ACCEPTED. +#[test] +fn m1_the_slot_check_is_load_bearing() { + let o = Felt::from(3u64); + let p0 = low_degree(2, &o); + let c = Ext::new([Felt::from(5u64), Felt::from(6u64), Felt::from(7u64)]); + let shifted: Vec = p0.iter().map(|v| v + &c).collect(); + let run = fri_run::(&shifted, &o); + assert!( + fri_accepts::(&run, &shifted, &o), + "control: FRI of p0 + c is honest for p0 + c" + ); + assert!( + !fri_accepts::(&run, &p0, &o), + "the slot check must reject" + ); + assert!( + with_mutation(GroupMutation::SkipSlotCheck, || fri_accepts::< + KeccakStarkHash, + >(&run, &p0, &o)), + "without the slot check the forgery is accepted (the check is load-bearing)" + ); +} + +/// M2 — the group's Merkle authentication is load-bearing. Replacing a layer +/// root (with the challenges kept) leaves the fold chain consistent; only the +/// authentication of the group against the root rejects it. +#[test] +fn m2_the_group_authentication_is_load_bearing() { + let o = Felt::from(3u64); + let p0 = low_degree(3, &o); + let mut run = fri_run::(&p0, &o); + run.roots[1] = [0xAB; 32]; + assert!( + !fri_accepts::(&run, &p0, &o), + "authentication must reject" + ); + assert!( + with_mutation( + GroupMutation::SkipLeafAuth, + || fri_accepts::(&run, &p0, &o) + ), + "without authentication the foreign root is accepted (the check is load-bearing)" + ); +} + +// --------------------------------------------------------------------------- +// U6: prove / verify round trips at fri = dp. +// --------------------------------------------------------------------------- + +/// SimpleAddition at `rows`, blowup `blowup`, k = 1, under `format`: returns +/// the proof's committed-layer count and the values per query, after asserting +/// it verifies. +fn round_trip_simple(rows: usize, blowup: u8, format: ProofFormat) -> (usize, usize) { + let o = golden_options(blowup, 1, 9, format); + let (air, proof) = prove_simple_addition::(rows, &o); + assert!( + verify_simple_addition::(&air, &proof), + "rows {rows} blowup {blowup} {format:?}: an honest proof must verify" + ); + let layers = proof.fri_layers_merkle_roots.len(); + let values = proof.query_list[0].layers_evaluations_sym.len(); + for q in &proof.query_list { + assert_eq!(q.layers_auth_paths.len(), layers); + assert_eq!(q.layers_evaluations_sym.len(), values); + } + (layers, values) +} + +#[test] +fn dp_round_trips_at_every_fold_count() { + // k = 1: total_folds = log2(rows) + blowup_log − (blowup_log + 1). + for blowup in [2u8, 4] { + for log_rows in 1..=10u32 { + let rows = 1usize << log_rows; + let (layers, values) = + round_trip_simple::(rows, blowup, dp_with(None)); + let lde_log = log_rows + blowup.trailing_zeros(); + let o = golden_options(blowup, 1, 9, dp_with(None)); + let l = + FriFoldLayout::for_options(lde_log, blowup.trailing_zeros(), &o, false).unwrap(); + assert_eq!(layers, l.num_committed, "rows {rows}"); + assert_eq!(values, l.opened_values_per_query(), "rows {rows}"); + } + } + // A shape where the DP picks a non-trivial schedule is exercised. + let o = golden_options(4, 1, 9, dp_with(None)); + let l = FriFoldLayout::for_options(12, 2, &o, false).unwrap(); + assert!( + l.schedule.iter().any(|&d| d > 1), + "schedule {:?}", + l.schedule + ); +} + +#[test] +fn dp_round_trips_under_explicit_schedules() { + // rows 2^9, blowup 4, k 1: lde_log 11, chain from 10 to T = 3: 7 bits. + for sched in [ + &[1u8, 3, 3][..], + &[3, 1, 3], + &[2, 1, 2, 2], + &[1, 1, 1, 1, 1, 1, 1], + &[6, 1], + &[1, 6], + &[4, 3], + ] { + for blake in [false, true] { + let (layers, values) = if blake { + round_trip_simple::(512, 4, dp_with(Some(sched))) + } else { + round_trip_simple::(512, 4, dp_with(Some(sched))) + }; + assert_eq!(layers, sched.len(), "{sched:?}"); + assert_eq!( + values, + sched.iter().map(|&d| 1usize << d).sum::(), + "{sched:?}" + ); + } + } + // An override that does not fit is a proving error, never a fallback. + let o = golden_options(4, 1, 9, dp_with(Some(&[3, 1]))); + let air = crate::examples::simple_addition::SimpleAdditionAIR::::new(&o); + let mut trace = crate::examples::simple_addition::simple_addition_trace::(512); + let pi = crate::examples::simple_addition::SimpleAdditionPublicInputs { + a: Felt::from(1u64), + b: Felt::from(2u64), + }; + use crate::prover::IsStarkProver; + let res = crate::prover::GenericProver::::prove( + &air, + &mut trace, + &pi, + &mut DefaultTranscript::::new(&[]), + ); + assert!( + res.is_err(), + "a schedule that does not cover the folds must be refused" + ); +} + +#[test] +fn dp_round_trips_ext3_aux_and_multi_table() { + for (rows, blowup) in [(16usize, 2u8), (128, 4), (512, 2)] { + let lde_log = rows.trailing_zeros() + blowup.trailing_zeros(); + // Committed chain: from lde_log − 1 down to T = blowup_log + k (k = 1). + let span = (lde_log - 1 - (blowup.trailing_zeros() + 1)) as u8; + // An uneven explicit schedule where there is room for one. + let explicit = if span >= 3 { + vec![2u8, span - 2] + } else { + vec![span] + }; + for format in [dp_with(None), dp_with(Some(&explicit))] { + let o = golden_options(blowup, 1, 7, format); + let (air, proof, _) = prove_logup::(rows, &o); + assert!( + verify_logup::(&air, &proof), + "logup rows {rows} {format:?}" + ); + let (air, proof, _) = prove_logup::(rows, &o); + assert!( + verify_logup::(&air, &proof), + "logup keccak rows {rows}" + ); + } + } + let o = golden_options(2, 1, 6, dp_with(None)); + let multi = prove_multi::(&o); + assert!(verify_multi::(&o, &multi)); + assert!( + multi + .proofs + .iter() + .any(|p| !p.fri_layers_merkle_roots.is_empty()) + ); +} + +/// The format is a verifier-side constant: a dp proof does not verify under +/// pair options, nor a pair proof under dp options. +#[test] +fn the_format_is_a_verifier_constant() { + let dp = golden_options(4, 1, 9, dp_with(None)); + let pair = golden_options(4, 1, 9, ProofFormat::DEFAULT); + let (_, dp_proof) = prove_simple_addition::(1024, &dp); + let (_, pair_proof) = prove_simple_addition::(1024, &pair); + let dp_air = crate::examples::simple_addition::SimpleAdditionAIR::::new(&dp); + let pair_air = crate::examples::simple_addition::SimpleAdditionAIR::::new(&pair); + assert!(verify_simple_addition::( + &dp_air, &dp_proof + )); + assert!(verify_simple_addition::( + &pair_air, + &pair_proof + )); + assert!(!verify_simple_addition::( + &pair_air, &dp_proof + )); + assert!(!verify_simple_addition::( + &dp_air, + &pair_proof + )); +} + +// --------------------------------------------------------------------------- +// The group path at the all-ones schedule vs the legacy path. +// --------------------------------------------------------------------------- + +/// Proving under `dp` with an all-ones schedule runs the GROUP code path (group +/// trees via `H::Batched`, full-group encoding, the group verifier) where the +/// legacy format runs the pair path. Every root, the terminal polynomial, every +/// trace/composition opening and every FRI path must be identical (so ζ and ι +/// are too), and each two-value group must be exactly the legacy pair: the +/// legacy sibling is the group entry that is not the query's own value. +#[test] +fn generic_path_at_all_ones_equals_legacy() { + for blowup in [2u8, 4] { + let rows = 256usize; + let lde_log = rows.trailing_zeros() + blowup.trailing_zeros(); + let span = (lde_log - 1 - (blowup.trailing_zeros() + 1)) as usize; + let ones = vec![1u8; span]; + let legacy_o = golden_options(blowup, 1, 9, ProofFormat::DEFAULT); + let group_o = golden_options(blowup, 1, 9, dp_with(Some(&ones))); + let (_, legacy, _) = prove_logup::(rows, &legacy_o); + let (air, group, _) = prove_logup::(rows, &group_o); + assert!(verify_logup::(&air, &group)); + + assert_eq!( + legacy.fri_layers_merkle_roots, + group.fri_layers_merkle_roots + ); + assert_eq!(legacy.fri_final_poly_coeffs, group.fri_final_poly_coeffs); + let a = fingerprint!(&legacy); + let b = fingerprint!(&group); + assert_eq!( + a.openings, b.openings, + "trace/composition openings (so every ι) equal" + ); + assert_eq!(a.fri_roots, b.fri_roots); + assert_ne!( + a.proof, b.proof, + "the encodings differ (full groups vs siblings)" + ); + for (lq, gq) in legacy.query_list.iter().zip(&group.query_list) { + assert_eq!(lq.layers_auth_paths.len(), span); + for j in 0..span { + assert_eq!( + lq.layers_auth_paths[j].merkle_path, gq.layers_auth_paths[j].merkle_path, + "layer {j}: same leaf, same path" + ); + let pair = &gq.layers_evaluations_sym[2 * j..2 * j + 2]; + let sym = &lq.layers_evaluations_sym[j]; + assert!( + pair.contains(sym), + "layer {j}: the legacy sibling is in the group" + ); + } + } + } +} + +// --------------------------------------------------------------------------- +// T1–T3: tamper tests on a dp proof with non-trivial groups. +// --------------------------------------------------------------------------- + +#[test] +fn tampering_any_fri_value_path_or_root_is_rejected() { + let format = dp_with(Some(&[3, 2, 2])); + let o = golden_options(4, 1, 5, format); + let (air, honest, _) = prove_logup::(512, &o); + assert!(verify_logup::(&air, &honest)); + let values = honest.query_list[0].layers_evaluations_sym.len(); + assert_eq!(values, 8 + 4 + 4); + let bump = Ext::new([Felt::one(), Felt::zero(), Felt::zero()]); + + // T1/T2: every value of query 0's groups (the slot value and every other). + for i in 0..values { + let mut p = honest.clone(); + p.query_list[0].layers_evaluations_sym[i] += bump; + assert!( + !verify_logup::(&air, &p), + "value {i} tampered" + ); + } + // A value of the LAST query too. + let last = honest.query_list.len() - 1; + let mut p = honest.clone(); + p.query_list[last].layers_evaluations_sym[values - 1] += bump; + assert!(!verify_logup::(&air, &p)); + // One path sibling per layer. + for j in 0..3 { + let mut p = honest.clone(); + p.query_list[0].layers_auth_paths[j].merkle_path[0][0] ^= 1; + assert!(!verify_logup::(&air, &p), "layer {j} path"); + } + // A layer root. + for j in 0..3 { + let mut p = honest.clone(); + p.fri_layers_merkle_roots[j][5] ^= 1; + assert!(!verify_logup::(&air, &p), "layer {j} root"); + } + // T3: the flat value vector one short / one long (checked before the loop, + // so neither panics). + let mut p = honest.clone(); + p.query_list[0].layers_evaluations_sym.pop(); + assert!(!verify_logup::(&air, &p)); + let mut p = honest.clone(); + p.query_list[0].layers_evaluations_sym.push(Ext::zero()); + assert!(!verify_logup::(&air, &p)); + // A path of the wrong length (the exact depth is a verifier constant). + let mut p = honest.clone(); + p.query_list[0].layers_auth_paths[1].merkle_path.pop(); + assert!(!verify_logup::(&air, &p)); + // One layer too few. + let mut p = honest.clone(); + p.fri_layers_merkle_roots.pop(); + assert!(!verify_logup::(&air, &p)); +} diff --git a/crypto/stark/src/tests/fri_schedule_tests.rs b/crypto/stark/src/tests/fri_schedule_tests.rs new file mode 100644 index 000000000..a29a7aa0a --- /dev/null +++ b/crypto/stark/src/tests/fri_schedule_tests.rs @@ -0,0 +1,1298 @@ +//! Tests for the FRI fold schedule (`crate::fri::schedule`) and the generalised +//! `FriFoldLayout` (U1–U3). +//! +//! Two objectives appear here. The PRODUCTION one is the cost law +//! (`FRI_COST_WEIGHTS`): U1 pins its schedules as the Rust DP computes them, U2 +//! checks it against brute force. The design model's PERMUTATION objective +//! (Merkle permutations per layer only) is kept as a second instance of the generic DP +//! (`fri_schedule_by`), pinned against an independently computed table: it shows the DP +//! machinery reproduces an independent model exactly, and documents how far +//! the two objectives' schedules differ. + +use crate::fri::schedule::{ + BALU_ROW_NS, FRI_COST_WEIGHTS, FRI_FOLD_XALU_ROWS, FRI_SCHEDULE_DMAX, FriFormat, + FriFormatError, XALU_ROW_NS, fri_chain_start, fri_group_layer_rows, fri_layer_cost_q, + fri_leaf_blocks, fri_pair_layer_cost_q, fri_pair_layer_rows, fri_schedule, fri_schedule_by, + fri_schedule_cost_by, fri_schedule_cost_q, fri_schedule_with_cost, legacy_fri_schedule, +}; +use crate::fri::terminal::FriFoldLayout; +use crate::proof::options::{ + CapPolicy, FriMode, FriScheduleOverride, OneRowMode, ProofFormat, ProofOptions, +}; +use crypto::merkle_tree::cap::{AUTO_WEIGHTS, cap_gain}; + +const Q: u64 = 110; + +// --------------------------------------------------------------------------- +// Cap-height functions for the permutation objective. +// --------------------------------------------------------------------------- + +/// A cap-height function that caps nothing. +fn no_cap(_depth: u32) -> u32 { + 0 +} + +/// The cap rule the design model's table was computed with: +/// `c = argmax_{0 ≤ c ≤ depth} (Q·c − (2^c − 1))`, ties to the smaller `c`. +fn cap_design_model(depth: u32) -> u32 { + let (mut best, mut best_c) = (0i64, 0u32); + for c in 0..=depth.min(62) { + let v = Q as i64 * i64::from(c) - ((1i64 << c) - 1); + if v > best { + (best, best_c) = (v, c); + } + } + best_c +} + +/// The adopted policy: every FRI tree is opened once per query. +fn cap_auto(depth: u32) -> u32 { + CapPolicy::Auto.height(Q as usize, depth as usize) as u32 +} + +#[test] +fn cap_auto_heights_match_cap_md() { + // The `CapPolicy::Auto` table, at a depth large enough not to clamp. + for (openings, want) in [(1, 0), (3, 0), (4, 2), (19, 2), (20, 3), (110, 3), (224, 3)] { + assert_eq!( + CapPolicy::Auto.height(openings, 20), + want, + "openings {openings}" + ); + } + // Clamped to depth. + for depth in 0..8 { + assert_eq!(cap_auto(depth), depth.min(3), "depth {depth}"); + } + // The design model's rule reaches 7 at Q = 110. + assert_eq!(cap_design_model(20), 7); + assert_eq!(cap_design_model(5), 5); +} + +/// The design model's per-layer cost, `Q ×` permutations: `Q·leaf(d) + Q·(depth − +/// c) + 2^c − 1`. +fn perm_layer_q(d: u32, depth: u32, q: u64, cap: &dyn Fn(u32) -> u32) -> u64 { + let c = cap(depth).min(depth); + q * fri_leaf_blocks(d) + q * u64::from(depth - c) + (1u64 << c) - 1 +} + +fn perm_schedule( + b0: u32, + t: u32, + cap: &dyn Fn(u32) -> u32, +) -> crate::fri::schedule::FriScheduleChoice { + fri_schedule_by(b0, t, FRI_SCHEDULE_DMAX, &|d, depth| { + perm_layer_q(d, depth, Q, cap) + }) +} + +fn perm_cost(b0: u32, schedule: &[u8], cap: &dyn Fn(u32) -> u32) -> Option { + fri_schedule_cost_by(b0, schedule, &|d, depth| perm_layer_q(d, depth, Q, cap)) +} + +// --------------------------------------------------------------------------- +// Cost-model primitives. +// --------------------------------------------------------------------------- + +#[test] +fn leaf_blocks() { + // ⌈3·2^d / 8⌉, at least 1. + let want = [1u64, 1, 2, 3, 6, 12, 24]; + for (d, w) in want.iter().enumerate() { + assert_eq!(fri_leaf_blocks(d as u32), *w, "d = {d}"); + } +} + +/// The objective's weights are a format constant: the cap +/// policy's weights plus the in-guest XALU and BALU row prices (a fold is 5 +/// XALU rows, a twiddle one BALU row). +#[test] +fn cost_weights_are_pinned() { + assert_eq!(FRI_COST_WEIGHTS.cap, AUTO_WEIGHTS); + assert_eq!( + ( + FRI_COST_WEIGHTS.cap.compress, + FRI_COST_WEIGHTS.cap.select, + FRI_COST_WEIGHTS.cap.unpack, + FRI_COST_WEIGHTS.cap.hint, + FRI_COST_WEIGHTS.cap.compare + ), + (2251, 567, 528, 460, 3789) + ); + assert_eq!( + (FRI_FOLD_XALU_ROWS, XALU_ROW_NS, BALU_ROW_NS), + (5, 522, 477) + ); + assert_eq!( + (FRI_COST_WEIGHTS.fold, FRI_COST_WEIGHTS.twiddle), + (2610, 477) + ); + assert_eq!( + (FRI_COST_WEIGHTS.xalu, FRI_COST_WEIGHTS.balu), + (XALU_ROW_NS, BALU_ROW_NS) + ); + assert_eq!( + FRI_COST_WEIGHTS.fold, + FRI_FOLD_XALU_ROWS * FRI_COST_WEIGHTS.xalu + ); + assert_eq!(FRI_COST_WEIGHTS.twiddle, FRI_COST_WEIGHTS.balu); + // The node cost law, 421 ns/instruction + 5.63 ns/cell, at the committed + // widths (XALU 18, BALU 10 cells), rounded to the nearest ns. + assert_eq!(((421.0f64 + 5.63 * 18.0).round()) as u64, XALU_ROW_NS); + assert_eq!(((421.0f64 + 5.63 * 10.0).round()) as u64, BALU_ROW_NS); +} + +/// One layer's cost written out by hand (every emitted row). +#[test] +fn layer_cost_by_hand() { + // d = 3, depth 10, no cap: + // leaf 3·2251 + walk 10·(2251+567) + slot mux 7·567 + folds 7·2610 + // + twiddles 3·477 + x_g 3·(567+477) + scaling 1·522 + 1·477 + // + slot assert 2·522 + compare 8·477 + 528 + values 8·(528+460) + // + leaf packs 6·528 + siblings 10·460. + let per_query = 3 * 2251 + + 10 * (2251 + 567) + + 7 * 567 + + 7 * 2610 + + 3 * 477 + + 3 * (567 + 477) + + 522 + + 477 + + 2 * 522 + + 8 * 477 + + 528 + + 8 * (528 + 460) + + 6 * 528 + + 10 * 460; + assert_eq!( + fri_layer_cost_q(&FRI_COST_WEIGHTS, 3, 10, Q, CapPolicy::Off), + Q * per_query + ); + // Auto cap at Q = 110 is c = 3 on a 10-deep tree: minus its gain and the + // three sibling hints per query the capped path omits. + let gain = cap_gain(&AUTO_WEIGHTS, 110, 3); + assert!(gain > 0); + assert_eq!( + fri_layer_cost_q(&FRI_COST_WEIGHTS, 3, 10, Q, CapPolicy::Auto), + Q * per_query - gain as u64 - Q * 3 * 460 + ); + // d = 1 at depth 0 under the group encoding: leaf, mux select, fold, + // twiddle, x_g select + mul, slot assert, compare, 2 values, 2 packs. + assert_eq!( + fri_layer_cost_q(&FRI_COST_WEIGHTS, 1, 0, 1, CapPolicy::Off), + 2251 + 567 + + 2610 + + 477 + + (567 + 477) + + 2 * 522 + + (8 * 477 + 528) + + 2 * (528 + 460) + + 2 * 528 + ); + // Today's pair layer at depth 0: parity select, leaf, fold, squaring, + // compare, two unpacks, two packs and one hinted sibling value. + assert_eq!( + fri_pair_layer_cost_q(&FRI_COST_WEIGHTS, 0, 1, CapPolicy::Off), + 567 + 2251 + 2610 + 477 + (8 * 477 + 528) + 2 * 528 + 2 * 528 + 460 + ); +} + +/// The row model's kinds at `d = 1..=6`, written out (the in-guest tests pin +/// the same numbers against the emitter, `lfm::fri_group_tests`). +#[test] +fn group_layer_rows_by_hand() { + // (d, selects, XALU, BALU, hashes, unpacks, packs, hints) at depth 2, + // uncapped. + let want = [ + (1u32, 4u64, 7u64, 10u64, 3u64, 3u64, 2u64, 4u64), + (2, 7, 17, 13, 4, 5, 3, 6), + (3, 12, 38, 15, 5, 9, 6, 10), + (4, 21, 79, 17, 8, 17, 12, 18), + (5, 38, 160, 19, 14, 33, 24, 34), + (6, 71, 321, 21, 26, 65, 48, 66), + ]; + for (d, sel, xalu, balu, hashes, unpacks, packs, hints) in want { + let r = fri_group_layer_rows(d, 2, 0); + assert_eq!( + ( + r.selects, r.xalu, r.balu, r.hashes, r.unpacks, r.packs, r.hints + ), + (sel, xalu, balu, hashes, unpacks, packs, hints), + "d = {d}" + ); + } + // A cap of c on the same tree: c fewer walk levels and sibling hints, + // 2^c − 1 cap-mux selects, one more unpack. + let r = fri_group_layer_rows(3, 2, 2); + assert_eq!( + (r.selects, r.hashes, r.unpacks, r.hints), + (12 - 2 + 3, 5 - 2, 9 + 1, 10 - 2) + ); + // The cap height is clamped to the depth. + assert_eq!(fri_group_layer_rows(3, 2, 9), fri_group_layer_rows(3, 2, 2)); + let p = fri_pair_layer_rows(2, 0); + assert_eq!( + ( + p.selects, p.xalu, p.balu, p.hashes, p.unpacks, p.packs, p.hints + ), + (3, 5, 9, 3, 3, 2, 3) + ); +} + +#[test] +fn schedule_cost_rejects_malformed_schedules() { + let cap = CapPolicy::Off; + assert_eq!(fri_schedule_cost_q(10, &[], Q, cap), Some(0)); + assert_eq!(fri_schedule_cost_q(10, &[0, 1], Q, cap), None); + assert_eq!(fri_schedule_cost_q(3, &[2, 2], Q, cap), None); + assert!(fri_schedule_cost_q(4, &[2, 2], Q, cap).is_some()); +} + +// --------------------------------------------------------------------------- +// The design model (permutation objective): its schedule table, reproduced. +// --------------------------------------------------------------------------- + +/// (B, today, S3 from B−1, S2+S3 from B); each entry = (cost·Q, schedule). +/// Generated by an independent Python reproduction of the design model in exact +/// integer units, and cross-checked against a second independent +/// implementation for the OFF and MODEL caps (cost / 110). +type Row = ( + u32, + (u64, &'static [u8]), + (u64, &'static [u8]), + (u64, &'static [u8]), +); + +/// T = 9, cap = OFF: (B, today, S3, S2+S3), each (cost·Q, schedule). +const PIN_T9_CAP_OFF: &[Row] = &[ + (6, (0, &[]), (0, &[]), (0, &[])), + (7, (0, &[]), (0, &[]), (0, &[])), + (8, (0, &[]), (0, &[]), (0, &[])), + (9, (0, &[]), (0, &[]), (0, &[])), + (10, (0, &[]), (0, &[]), (1100, &[1])), + (11, (1100, &[1]), (1100, &[1]), (1210, &[2])), + (12, (2310, &[1, 1]), (1210, &[2]), (1320, &[3])), + (13, (3630, &[1, 1, 1]), (1320, &[3]), (1650, &[4])), + (14, (5060, &[1, 1, 1, 1]), (1650, &[4]), (2310, &[5])), + (15, (6600, &[1, 1, 1, 1, 1]), (2310, &[5]), (2970, &[3, 3])), + ( + 16, + (8250, &[1, 1, 1, 1, 1, 1]), + (2970, &[3, 3]), + (3300, &[4, 3]), + ), + ( + 17, + (10010, &[1, 1, 1, 1, 1, 1, 1]), + (3300, &[4, 3]), + (3740, &[4, 4]), + ), + ( + 18, + (11880, &[1, 1, 1, 1, 1, 1, 1, 1]), + (3740, &[4, 4]), + (4400, &[5, 4]), + ), + ( + 19, + (13860, &[1, 1, 1, 1, 1, 1, 1, 1, 1]), + (4400, &[5, 4]), + (5170, &[5, 5]), + ), + ( + 20, + (15950, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (5170, &[5, 5]), + (5720, &[4, 4, 3]), + ), + ( + 21, + (18150, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (5720, &[4, 4, 3]), + (6270, &[4, 4, 4]), + ), + ( + 22, + (20460, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (6270, &[4, 4, 4]), + (6930, &[5, 4, 4]), + ), + ( + 23, + (22880, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (6930, &[5, 4, 4]), + (7700, &[5, 5, 4]), + ), + ( + 24, + (25410, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (7700, &[5, 5, 4]), + (8580, &[5, 5, 5]), + ), +]; +/// T = 10, cap = OFF: (B, today, S3, S2+S3), each (cost·Q, schedule). +const PIN_T10_CAP_OFF: &[Row] = &[ + (6, (0, &[]), (0, &[]), (0, &[])), + (7, (0, &[]), (0, &[]), (0, &[])), + (8, (0, &[]), (0, &[]), (0, &[])), + (9, (0, &[]), (0, &[]), (0, &[])), + (10, (0, &[]), (0, &[]), (0, &[])), + (11, (0, &[]), (0, &[]), (1210, &[1])), + (12, (1210, &[1]), (1210, &[1]), (1320, &[2])), + (13, (2530, &[1, 1]), (1320, &[2]), (1430, &[3])), + (14, (3960, &[1, 1, 1]), (1430, &[3]), (1760, &[4])), + (15, (5500, &[1, 1, 1, 1]), (1760, &[4]), (2420, &[5])), + (16, (7150, &[1, 1, 1, 1, 1]), (2420, &[5]), (3190, &[3, 3])), + ( + 17, + (8910, &[1, 1, 1, 1, 1, 1]), + (3190, &[3, 3]), + (3520, &[4, 3]), + ), + ( + 18, + (10780, &[1, 1, 1, 1, 1, 1, 1]), + (3520, &[4, 3]), + (3960, &[4, 4]), + ), + ( + 19, + (12760, &[1, 1, 1, 1, 1, 1, 1, 1]), + (3960, &[4, 4]), + (4620, &[5, 4]), + ), + ( + 20, + (14850, &[1, 1, 1, 1, 1, 1, 1, 1, 1]), + (4620, &[5, 4]), + (5390, &[5, 5]), + ), + ( + 21, + (17050, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (5390, &[5, 5]), + (6050, &[4, 4, 3]), + ), + ( + 22, + (19360, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (6050, &[4, 4, 3]), + (6600, &[4, 4, 4]), + ), + ( + 23, + (21780, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (6600, &[4, 4, 4]), + (7260, &[5, 4, 4]), + ), + ( + 24, + (24310, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (7260, &[5, 4, 4]), + (8030, &[5, 5, 4]), + ), +]; +/// T = 9, cap = MODEL: (B, today, S3, S2+S3), each (cost·Q, schedule). +const PIN_T9_CAP_MODEL: &[Row] = &[ + (6, (0, &[]), (0, &[]), (0, &[])), + (7, (0, &[]), (0, &[]), (0, &[])), + (8, (0, &[]), (0, &[]), (0, &[])), + (9, (0, &[]), (0, &[]), (0, &[])), + (10, (0, &[]), (0, &[]), (457, &[1])), + (11, (457, &[1]), (457, &[1]), (567, &[2])), + (12, (1024, &[1, 1]), (567, &[2]), (677, &[3])), + (13, (1701, &[1, 1, 1]), (677, &[3]), (1007, &[4])), + (14, (2488, &[1, 1, 1, 1]), (1007, &[4]), (1464, &[3, 2])), + ( + 15, + (3385, &[1, 1, 1, 1, 1]), + (1464, &[3, 2]), + (1684, &[3, 3]), + ), + ( + 16, + (4392, &[1, 1, 1, 1, 1, 1]), + (1684, &[3, 3]), + (2014, &[4, 3]), + ), + ( + 17, + (5509, &[1, 1, 1, 1, 1, 1, 1]), + (2014, &[4, 3]), + (2454, &[4, 4]), + ), + ( + 18, + (6736, &[1, 1, 1, 1, 1, 1, 1, 1]), + (2454, &[4, 4]), + (3021, &[3, 3, 3]), + ), + ( + 19, + (8073, &[1, 1, 1, 1, 1, 1, 1, 1, 1]), + (3021, &[3, 3, 3]), + (3351, &[4, 3, 3]), + ), + ( + 20, + (9520, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (3351, &[4, 3, 3]), + (3791, &[4, 4, 3]), + ), + ( + 21, + (11077, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (3791, &[4, 4, 3]), + (4341, &[4, 4, 4]), + ), + ( + 22, + (12744, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (4341, &[4, 4, 4]), + (5001, &[5, 4, 4]), + ), + ( + 23, + (14521, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (5001, &[5, 4, 4]), + (5458, &[4, 4, 3, 3]), + ), + ( + 24, + (16408, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (5458, &[4, 4, 3, 3]), + (6008, &[4, 4, 4, 3]), + ), +]; +/// T = 10, cap = MODEL: (B, today, S3, S2+S3), each (cost·Q, schedule). +const PIN_T10_CAP_MODEL: &[Row] = &[ + (6, (0, &[]), (0, &[]), (0, &[])), + (7, (0, &[]), (0, &[]), (0, &[])), + (8, (0, &[]), (0, &[]), (0, &[])), + (9, (0, &[]), (0, &[]), (0, &[])), + (10, (0, &[]), (0, &[]), (0, &[])), + (11, (0, &[]), (0, &[]), (567, &[1])), + (12, (567, &[1]), (567, &[1]), (677, &[2])), + (13, (1244, &[1, 1]), (677, &[2]), (787, &[3])), + (14, (2031, &[1, 1, 1]), (787, &[3]), (1117, &[4])), + (15, (2928, &[1, 1, 1, 1]), (1117, &[4]), (1684, &[3, 2])), + ( + 16, + (3935, &[1, 1, 1, 1, 1]), + (1684, &[3, 2]), + (1904, &[3, 3]), + ), + ( + 17, + (5052, &[1, 1, 1, 1, 1, 1]), + (1904, &[3, 3]), + (2234, &[4, 3]), + ), + ( + 18, + (6279, &[1, 1, 1, 1, 1, 1, 1]), + (2234, &[4, 3]), + (2674, &[4, 4]), + ), + ( + 19, + (7616, &[1, 1, 1, 1, 1, 1, 1, 1]), + (2674, &[4, 4]), + (3334, &[5, 4]), + ), + ( + 20, + (9063, &[1, 1, 1, 1, 1, 1, 1, 1, 1]), + (3334, &[5, 4]), + (3681, &[4, 3, 3]), + ), + ( + 21, + (10620, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (3681, &[4, 3, 3]), + (4121, &[4, 4, 3]), + ), + ( + 22, + (12287, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (4121, &[4, 4, 3]), + (4671, &[4, 4, 4]), + ), + ( + 23, + (14064, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (4671, &[4, 4, 4]), + (5331, &[5, 4, 4]), + ), + ( + 24, + (15951, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (5331, &[5, 4, 4]), + (5898, &[4, 4, 3, 3]), + ), +]; +/// T = 9, cap = AUTO: (B, today, S3, S2+S3), each (cost·Q, schedule). +const PIN_T9_CAP_AUTO: &[Row] = &[ + (6, (0, &[]), (0, &[]), (0, &[])), + (7, (0, &[]), (0, &[]), (0, &[])), + (8, (0, &[]), (0, &[]), (0, &[])), + (9, (0, &[]), (0, &[]), (0, &[])), + (10, (0, &[]), (0, &[]), (777, &[1])), + (11, (777, &[1]), (777, &[1]), (887, &[2])), + (12, (1664, &[1, 1]), (887, &[2]), (997, &[3])), + (13, (2661, &[1, 1, 1]), (997, &[3]), (1327, &[4])), + (14, (3768, &[1, 1, 1, 1]), (1327, &[4]), (1987, &[5])), + (15, (4985, &[1, 1, 1, 1, 1]), (1987, &[5]), (2324, &[3, 3])), + ( + 16, + (6312, &[1, 1, 1, 1, 1, 1]), + (2324, &[3, 3]), + (2654, &[4, 3]), + ), + ( + 17, + (7749, &[1, 1, 1, 1, 1, 1, 1]), + (2654, &[4, 3]), + (3094, &[4, 4]), + ), + ( + 18, + (9296, &[1, 1, 1, 1, 1, 1, 1, 1]), + (3094, &[4, 4]), + (3754, &[5, 4]), + ), + ( + 19, + (10953, &[1, 1, 1, 1, 1, 1, 1, 1, 1]), + (3754, &[5, 4]), + (4311, &[4, 3, 3]), + ), + ( + 20, + (12720, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (4311, &[4, 3, 3]), + (4751, &[4, 4, 3]), + ), + ( + 21, + (14597, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (4751, &[4, 4, 3]), + (5301, &[4, 4, 4]), + ), + ( + 22, + (16584, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (5301, &[4, 4, 4]), + (5961, &[5, 4, 4]), + ), + ( + 23, + (18681, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (5961, &[5, 4, 4]), + (6731, &[5, 5, 4]), + ), + ( + 24, + (20888, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (6731, &[5, 5, 4]), + (7288, &[4, 4, 4, 3]), + ), +]; +/// T = 10, cap = AUTO: (B, today, S3, S2+S3), each (cost·Q, schedule). +const PIN_T10_CAP_AUTO: &[Row] = &[ + (6, (0, &[]), (0, &[]), (0, &[])), + (7, (0, &[]), (0, &[]), (0, &[])), + (8, (0, &[]), (0, &[]), (0, &[])), + (9, (0, &[]), (0, &[]), (0, &[])), + (10, (0, &[]), (0, &[]), (0, &[])), + (11, (0, &[]), (0, &[]), (887, &[1])), + (12, (887, &[1]), (887, &[1]), (997, &[2])), + (13, (1884, &[1, 1]), (997, &[2]), (1107, &[3])), + (14, (2991, &[1, 1, 1]), (1107, &[3]), (1437, &[4])), + (15, (4208, &[1, 1, 1, 1]), (1437, &[4]), (2097, &[5])), + (16, (5535, &[1, 1, 1, 1, 1]), (2097, &[5]), (2544, &[3, 3])), + ( + 17, + (6972, &[1, 1, 1, 1, 1, 1]), + (2544, &[3, 3]), + (2874, &[4, 3]), + ), + ( + 18, + (8519, &[1, 1, 1, 1, 1, 1, 1]), + (2874, &[4, 3]), + (3314, &[4, 4]), + ), + ( + 19, + (10176, &[1, 1, 1, 1, 1, 1, 1, 1]), + (3314, &[4, 4]), + (3974, &[5, 4]), + ), + ( + 20, + (11943, &[1, 1, 1, 1, 1, 1, 1, 1, 1]), + (3974, &[5, 4]), + (4641, &[4, 3, 3]), + ), + ( + 21, + (13820, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (4641, &[4, 3, 3]), + (5081, &[4, 4, 3]), + ), + ( + 22, + (15807, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (5081, &[4, 4, 3]), + (5631, &[4, 4, 4]), + ), + ( + 23, + (17904, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (5631, &[4, 4, 4]), + (6291, &[5, 4, 4]), + ), + ( + 24, + (20111, &[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]), + (6291, &[5, 4, 4]), + (7061, &[5, 5, 4]), + ), +]; + +fn check_pin(name: &str, terminal_log: u32, cap: &dyn Fn(u32) -> u32, rows: &[Row]) { + assert_eq!(rows.len(), 19, "{name}: B = 6..=24"); + for &(b, (today_q, today), (s3_q, s3), (s2_q, s2)) in rows { + let ctx = format!("{name} B={b}"); + // today: the all-ones chain from B − 1 (no committed layer when B − 1 ≤ T). + let b0 = fri_chain_start(b, false); + assert_eq!(legacy_fri_schedule(b0, terminal_log), today, "{ctx} today"); + assert_eq!(perm_cost(b0, today, cap), Some(today_q), "{ctx} today cost"); + // S3: the DP from B − 1. + let got = perm_schedule(b0, terminal_log, cap); + assert_eq!(got.schedule, s3, "{ctx} S3 schedule"); + assert_eq!(got.cost_q, s3_q, "{ctx} S3 cost"); + assert_eq!(got.trees as usize, s3.len(), "{ctx} S3 trees"); + // S2+S3: the DP from B (the DEEP codeword is committed). + let b0 = fri_chain_start(b, true); + let got = perm_schedule(b0, terminal_log, cap); + assert_eq!(got.schedule, s2, "{ctx} S2+S3 schedule"); + assert_eq!(got.cost_q, s2_q, "{ctx} S2+S3 cost"); + } +} + +#[test] +fn design_model_reproduces_the_fri_md_table() { + check_pin("T9 cap off", 9, &no_cap, PIN_T9_CAP_OFF); + check_pin("T10 cap off", 10, &no_cap, PIN_T10_CAP_OFF); + check_pin("T9 cap model", 9, &cap_design_model, PIN_T9_CAP_MODEL); + check_pin("T10 cap model", 10, &cap_design_model, PIN_T10_CAP_MODEL); + check_pin("T9 cap auto", 9, &cap_auto, PIN_T9_CAP_AUTO); + check_pin("T10 cap auto", 10, &cap_auto, PIN_T10_CAP_AUTO); +} + +/// Spot checks tying the design-model pins to the model's printed table +/// (costs there are per query, i.e. cost·Q / 110, rounded to two decimals). +#[test] +fn design_model_pins_match_fri_md_table() { + let per_query = |cost_q: u64| (cost_q as f64 / Q as f64 * 100.0).round() / 100.0; + let today = legacy_fri_schedule(20, 9); + assert_eq!(per_query(perm_cost(20, &today, &no_cap).unwrap()), 165.0); + assert_eq!( + per_query(perm_cost(20, &today, &cap_design_model).unwrap()), + 100.70 + ); + let c = perm_schedule(20, 9, &no_cap); + assert_eq!((per_query(c.cost_q), c.schedule), (52.0, vec![4, 4, 3])); + let c = perm_schedule(20, 9, &cap_design_model); + assert_eq!((per_query(c.cost_q), c.schedule), (34.46, vec![4, 4, 3])); + let c = perm_schedule(21, 9, &cap_design_model); + assert_eq!((per_query(c.cost_q), c.schedule), (39.46, vec![4, 4, 4])); + assert_eq!(perm_schedule(18, 9, &no_cap).schedule, vec![5, 4]); + assert_eq!( + perm_schedule(18, 9, &cap_design_model).schedule, + vec![3, 3, 3] + ); + let c = perm_schedule(20, 10, &cap_design_model); + assert_eq!((per_query(c.cost_q), c.schedule), (33.46, vec![4, 3, 3])); + let c = perm_schedule(21, 10, &cap_design_model); + assert_eq!((per_query(c.cost_q), c.schedule), (37.46, vec![4, 4, 3])); +} + +// --------------------------------------------------------------------------- +// U1: the PRODUCTION schedules (cost-law objective), pinned from the Rust DP. +// The schedule is a format constant: a change here is a format change. +// --------------------------------------------------------------------------- + +/// (B, S3 schedule from B − 1, S2+S3 schedule from B) for B = 6..=24. +type CostRow = (u32, &'static [u8], &'static [u8]); + +/// Generated by `print_cost_law_schedule_table` (below, `--ignored`) from the +/// Rust DP, with every emitted row priced. The whole table was cross-checked +/// against an independent Python reproduction of the objective: identical. +/// An independent derivation of the cost law gives [2,2] / [3,3,3] / +/// [3,3,3,2] / [3,3,3,3,2] at B = 14 / 19 / 21 / 24, T = 9 — the Auto rows. +const PIN_COST_T9_CAP_OFF: &[CostRow] = &[ + (6, &[], &[]), + (7, &[], &[]), + (8, &[], &[]), + (9, &[], &[]), + (10, &[], &[1]), + (11, &[1], &[2]), + (12, &[2], &[3]), + (13, &[3], &[2, 2]), + (14, &[2, 2], &[3, 2]), + (15, &[3, 2], &[3, 3]), + (16, &[3, 3], &[3, 2, 2]), + (17, &[3, 2, 2], &[3, 3, 2]), + (18, &[3, 3, 2], &[3, 3, 3]), + (19, &[3, 3, 3], &[4, 3, 3]), + (20, &[4, 3, 3], &[3, 3, 3, 2]), + (21, &[3, 3, 3, 2], &[3, 3, 3, 3]), + (22, &[3, 3, 3, 3], &[4, 3, 3, 3]), + (23, &[4, 3, 3, 3], &[3, 3, 3, 3, 2]), + (24, &[3, 3, 3, 3, 2], &[3, 3, 3, 3, 3]), +]; +const PIN_COST_T10_CAP_OFF: &[CostRow] = &[ + (6, &[], &[]), + (7, &[], &[]), + (8, &[], &[]), + (9, &[], &[]), + (10, &[], &[]), + (11, &[], &[1]), + (12, &[1], &[2]), + (13, &[2], &[3]), + (14, &[3], &[2, 2]), + (15, &[2, 2], &[3, 2]), + (16, &[3, 2], &[3, 3]), + (17, &[3, 3], &[4, 3]), + (18, &[4, 3], &[3, 3, 2]), + (19, &[3, 3, 2], &[3, 3, 3]), + (20, &[3, 3, 3], &[4, 3, 3]), + (21, &[4, 3, 3], &[3, 3, 3, 2]), + (22, &[3, 3, 3, 2], &[3, 3, 3, 3]), + (23, &[3, 3, 3, 3], &[4, 3, 3, 3]), + (24, &[4, 3, 3, 3], &[3, 3, 3, 3, 2]), +]; +const PIN_COST_T9_CAP_AUTO: &[CostRow] = &[ + (6, &[], &[]), + (7, &[], &[]), + (8, &[], &[]), + (9, &[], &[]), + (10, &[], &[1]), + (11, &[1], &[2]), + (12, &[2], &[3]), + (13, &[3], &[2, 2]), + (14, &[2, 2], &[3, 2]), + (15, &[3, 2], &[3, 3]), + (16, &[3, 3], &[3, 2, 2]), + (17, &[3, 2, 2], &[3, 3, 2]), + (18, &[3, 3, 2], &[3, 3, 3]), + (19, &[3, 3, 3], &[3, 3, 2, 2]), + (20, &[3, 3, 2, 2], &[3, 3, 3, 2]), + (21, &[3, 3, 3, 2], &[3, 3, 3, 3]), + (22, &[3, 3, 3, 3], &[4, 3, 3, 3]), + (23, &[4, 3, 3, 3], &[3, 3, 3, 3, 2]), + (24, &[3, 3, 3, 3, 2], &[3, 3, 3, 3, 3]), +]; +const PIN_COST_T10_CAP_AUTO: &[CostRow] = &[ + (6, &[], &[]), + (7, &[], &[]), + (8, &[], &[]), + (9, &[], &[]), + (10, &[], &[]), + (11, &[], &[1]), + (12, &[1], &[2]), + (13, &[2], &[3]), + (14, &[3], &[2, 2]), + (15, &[2, 2], &[3, 2]), + (16, &[3, 2], &[3, 3]), + (17, &[3, 3], &[3, 2, 2]), + (18, &[3, 2, 2], &[3, 3, 2]), + (19, &[3, 3, 2], &[3, 3, 3]), + (20, &[3, 3, 3], &[4, 3, 3]), + (21, &[4, 3, 3], &[3, 3, 3, 2]), + (22, &[3, 3, 3, 2], &[3, 3, 3, 3]), + (23, &[3, 3, 3, 3], &[4, 3, 3, 3]), + (24, &[4, 3, 3, 3], &[3, 3, 3, 3, 2]), +]; + +fn check_cost_pin(name: &str, terminal_log: u32, cap: CapPolicy, rows: &[CostRow]) { + assert_eq!(rows.len(), 19, "{name}: B = 6..=24"); + for &(b, s3, s2) in rows { + let ctx = format!("{name} B={b}"); + let got = fri_schedule( + fri_chain_start(b, false), + terminal_log, + Q, + cap, + FRI_SCHEDULE_DMAX, + ); + assert_eq!(got, s3, "{ctx} S3"); + let got = fri_schedule( + fri_chain_start(b, true), + terminal_log, + Q, + cap, + FRI_SCHEDULE_DMAX, + ); + assert_eq!(got, s2, "{ctx} S2+S3"); + } +} + +#[test] +fn schedule_pinned_table() { + check_cost_pin("T9 cap off", 9, CapPolicy::Off, PIN_COST_T9_CAP_OFF); + check_cost_pin("T10 cap off", 10, CapPolicy::Off, PIN_COST_T10_CAP_OFF); + check_cost_pin("T9 cap auto", 9, CapPolicy::Auto, PIN_COST_T9_CAP_AUTO); + check_cost_pin("T10 cap auto", 10, CapPolicy::Auto, PIN_COST_T10_CAP_AUTO); +} + +/// Prints the U1 table in the `fri_schedule_cost_pins.rs` format. Run with +/// `-- --ignored --nocapture` to regenerate after a DELIBERATE objective change. +#[test] +#[ignore = "generator for fri_schedule_cost_pins.rs"] +fn print_cost_law_schedule_table() { + for (name, t, cap) in [ + ("T9_CAP_OFF", 9, CapPolicy::Off), + ("T10_CAP_OFF", 10, CapPolicy::Off), + ("T9_CAP_AUTO", 9, CapPolicy::Auto), + ("T10_CAP_AUTO", 10, CapPolicy::Auto), + ] { + println!("const PIN_COST_{name}_DATA: [CostRow; 19] = ["); + for b in 6..=24u32 { + let s3 = fri_schedule(fri_chain_start(b, false), t, Q, cap, FRI_SCHEDULE_DMAX); + let s2 = fri_schedule(fri_chain_start(b, true), t, Q, cap, FRI_SCHEDULE_DMAX); + println!(" ({b}, &{s3:?}, &{s2:?}),"); + } + println!("];"); + } +} + +/// B = 21, T = 9, no cap, S3: every candidate schedule's cost by hand, so the +/// pinned optimum is shown to be one, not merely reproduced. +#[test] +fn cost_law_b21_by_hand() { + let layer = |d: u64, depth: u64| -> u64 { + let leaf = (3u64 << d).div_ceil(8).max(1); + let n = 1u64 << d; + let g = n - 1; + let extras = d * (567 + 477) + + d.saturating_sub(2) * 522 + + u64::from(d >= 2) * 477 + + 2 * 522 + + 8 * 477 + + 528 + + n * (528 + 460) + + (3 * n).div_ceil(4) * 528 + + depth * 460; + Q * (leaf * 2251 + depth * (2251 + 567) + g * (567 + 2610) + d * 477 + extras) + }; + let cost = |sched: &[u64]| { + let mut b = 20u64; + sched + .iter() + .map(|&d| { + b -= d; + layer(d, b) + }) + .sum::() + }; + let got = fri_schedule_with_cost(20, 9, Q, CapPolicy::Off, FRI_SCHEDULE_DMAX); + let as_u64: Vec = got.schedule.iter().map(|&d| u64::from(d)).collect(); + assert_eq!(got.cost_q, cost(&as_u64)); + // It beats the permutation objective's choice and today's. + assert!(got.cost_q <= cost(&[4, 4, 3])); + assert!(got.cost_q < cost(&[1; 11])); +} + +// --------------------------------------------------------------------------- +// U2: brute-force optimality for b₀ ≤ 16, under the production objective. +// --------------------------------------------------------------------------- + +/// Independent oracle for one layer's cost-law price (the module docs' +/// formula, written out again from the CAPPED rows priced directly plus the +/// cap's once-per-tree cost, rather than the uncapped rows minus the gain). +fn oracle_layer_q(d: u32, depth: u32, q: u64, cap: CapPolicy) -> u64 { + let (wc, ws, wu, wh, wq) = (2251i128, 567i128, 528i128, 460i128, 3789i128); + let (xalu, balu) = (522i128, 477i128); + let leaf = i128::from((3u64 << d).div_ceil(8).max(1) as u32); + let n = 1i128 << d; + let d = i128::from(d); + let c = cap.height(q as usize, depth as usize) as i128; + let walk = i128::from(depth) - c; + let cap_nodes = 1i128 << c; + let selects = (n - 1) + walk + (cap_nodes - 1) + d; + let xalus = 5 * (n - 1) + 2 + (d - 2).max(0); + let balus = d + d + i128::from(d >= 2) + 8; + let hashes = leaf + walk; + let unpacks = n + 1 + i128::from(c > 0); + let packs = (3 * n + 3) / 4; + let hints = n + walk; + let per_query = selects * ws + + xalus * xalu + + balus * balu + + hashes * wc + + (unpacks + packs) * wu + + hints * wh; + let per_tree = if c == 0 { + 0 + } else { + (cap_nodes - 1) * wc + cap_nodes * wh + wq + }; + (q as i128 * per_query + per_tree) as u64 +} + +/// Every composition of `b0 − t` into parts in `1..=dmax`, with its cost; +/// returns the minimum under (cost, trees, schedule) lexicographic order. +fn brute_force(b0: u32, t: u32, q: u64, cap: CapPolicy, dmax: u32) -> (u64, Vec) { + struct Search { + t: u32, + q: u64, + cap: CapPolicy, + dmax: u32, + prefix: Vec, + best: Option<(u64, usize, Vec)>, + } + impl Search { + fn walk(&mut self, b: u32, cost: u64) { + if b == self.t { + let cand = (cost, self.prefix.len(), self.prefix.clone()); + if self.best.as_ref().is_none_or(|cur| cand < *cur) { + self.best = Some(cand); + } + return; + } + for d in 1..=self.dmax.min(b - self.t) { + self.prefix.push(d as u8); + let c = cost + oracle_layer_q(d, b - d, self.q, self.cap); + self.walk(b - d, c); + self.prefix.pop(); + } + } + } + let mut s = Search { + t, + q, + cap, + dmax, + prefix: Vec::new(), + best: None, + }; + s.walk(b0, 0); + let (cost, _, sched) = s.best.expect("at least the empty / all-ones composition"); + (cost, sched) +} + +#[test] +fn dp_is_optimal() { + let caps = [ + CapPolicy::Off, + CapPolicy::Auto, + CapPolicy::Fixed(2), + CapPolicy::Fixed(5), + ]; + let mut checked = 0u32; + for cap in caps { + for q in [1u64, 3, 110] { + for dmax in [1u32, 2, 3, FRI_SCHEDULE_DMAX] { + for b0 in 0..=16u32 { + for t in 0..=b0 { + let got = fri_schedule_with_cost(b0, t, q, cap, dmax); + let (cost, sched) = brute_force(b0, t, q, cap, dmax); + let ctx = format!("cap={cap:?} q={q} dmax={dmax} b0={b0} t={t}"); + assert_eq!(got.cost_q, cost, "{ctx}: cost"); + // The tie rule makes the optimum unique: the smallest + // (trees, schedule) among the cost-optimal ones. + assert_eq!(got.schedule, sched, "{ctx}: schedule"); + assert_eq!(got.trees as usize, sched.len(), "{ctx}: trees"); + assert_eq!( + fri_schedule_cost_q(b0, &got.schedule, q, cap), + Some(got.cost_q), + "{ctx}: cost of the schedule" + ); + let sum: u32 = got.schedule.iter().map(|&d| u32::from(d)).sum(); + assert_eq!(sum, b0 - t, "{ctx}: lands on the terminal"); + assert!( + got.schedule + .iter() + .all(|&d| (1..=dmax).contains(&u32::from(d))) + ); + checked += 1; + } + // Above the terminal: nothing to commit. + for t in b0 + 1..=b0 + 2 { + assert!(fri_schedule(b0, t, q, cap, dmax).is_empty()); + } + } + } + } + } + assert_eq!(checked, 4 * 3 * 4 * (17 * 18 / 2)); +} + +#[test] +fn dmax_one_is_the_legacy_schedule() { + for b0 in 0..=30u32 { + for t in 0..=31u32 { + for cap in [CapPolicy::Off, CapPolicy::Auto] { + assert_eq!(fri_schedule(b0, t, Q, cap, 1), legacy_fri_schedule(b0, t)); + // dmax = 0 is treated as 1. + assert_eq!(fri_schedule(b0, t, Q, cap, 0), legacy_fri_schedule(b0, t)); + } + } + } +} + +// --------------------------------------------------------------------------- +// U3: the legacy constructor is unchanged. +// --------------------------------------------------------------------------- + +/// `FriFoldLayout::new` as it was before the schedule existed (terminal.rs @ +/// 5d0b0a41a), copied verbatim: (total_folds, num_committed, terminal_len, +/// effective_k). +fn old_layout(lde_log: u32, blowup_log: u32, k: u32) -> (u32, usize, usize, u32) { + let terminal_log = (blowup_log + k).min(lde_log); + let total_folds = lde_log - terminal_log; + ( + total_folds, + total_folds.saturating_sub(1) as usize, + 1usize << terminal_log, + terminal_log - blowup_log, + ) +} + +fn dp_format(cap: CapPolicy) -> FriFormat { + FriFormat { + mode: FriMode::Dp, + one_row: false, + num_queries: Q, + cap, + schedule_override: None, + } +} + +#[test] +fn legacy_layout_equals_old_layout() { + // one_row = true is exercised through the schedule arithmetic only (the + // layout is still well defined); the prover refuses it (S2 not built). + let dp_formats: Vec = [false, true] + .into_iter() + .flat_map(|one_row| { + [CapPolicy::Off, CapPolicy::Auto].map(|cap| FriFormat { + one_row, + ..dp_format(cap) + }) + }) + .collect(); + let mut checked = 0u32; + for blowup_log in 1..=4u32 { + // The LDE is at least the blowup (trace length ≥ 1). + for lde_log in blowup_log..=30u32 { + for k in 0..=10u32 { + let ctx = format!("lde_log={lde_log} blowup_log={blowup_log} k={k}"); + let (total_folds, num_committed, terminal_len, effective_k) = + old_layout(lde_log, blowup_log, k); + let new = FriFoldLayout::new(lde_log, blowup_log, k); + assert_eq!(new.total_folds, total_folds, "{ctx}"); + assert_eq!(new.num_committed, num_committed, "{ctx}"); + assert_eq!(new.terminal_len, terminal_len, "{ctx}"); + assert_eq!(new.effective_k, effective_k, "{ctx}"); + assert_eq!(new.schedule, vec![1u8; num_committed], "{ctx}"); + assert!(!new.one_row, "{ctx}"); + assert!(new.is_legacy(), "{ctx}"); + assert_eq!(new.opened_values_per_query(), num_committed, "{ctx}"); + + // Pair mode ignores the query count, the cap policy and any + // schedule override. + for cap in [CapPolicy::Off, CapPolicy::Auto] { + let pair = FriFormat { + mode: FriMode::Pair, + schedule_override: FriScheduleOverride::new(&[3, 1]), + ..dp_format(cap) + }; + assert_eq!( + FriFoldLayout::for_format(lde_log, blowup_log, k, &pair), + Some(new.clone()), + "{ctx}" + ); + } + assert_eq!( + FriFoldLayout::from_schedule( + lde_log, + blowup_log, + k, + false, + new.schedule.clone() + ), + Some(new.clone()), + "{ctx}" + ); + + // Any format moves only the split of the folds into committed + // layers (and, off the legacy format, the encoding). + for fmt in &dp_formats { + let l = FriFoldLayout::for_format(lde_log, blowup_log, k, fmt) + .expect("the DP lands on the terminal"); + assert_eq!( + (l.total_folds, l.terminal_len, l.effective_k, l.one_row), + (total_folds, terminal_len, effective_k, fmt.one_row), + "{ctx} {fmt:?}" + ); + assert!( + !l.is_legacy(), + "{ctx} {fmt:?}: Dp is never the legacy encoding" + ); + assert_eq!(l.num_committed, l.schedule.len(), "{ctx} {fmt:?}"); + let covered: u32 = l.schedule.iter().map(|&d| u32::from(d)).sum(); + let expected = match (total_folds, fmt.one_row) { + (0, _) => 0, + (n, true) => n, + (n, false) => n - 1, + }; + assert_eq!(covered, expected, "{ctx} {fmt:?}"); + assert_eq!( + l.opened_values_per_query(), + l.schedule.iter().map(|&d| 1usize << d).sum::(), + "{ctx} {fmt:?}" + ); + for j in 0..l.num_committed { + let d = u32::from(l.schedule[j]); + assert_eq!( + l.layer_depth(lde_log, j) + d, + l.layer_log_len(lde_log, j), + "{ctx} {fmt:?} layer {j}" + ); + } + } + checked += 1; + } + } + } + assert_eq!(checked, 11 * (30 + 29 + 28 + 27)); +} + +#[test] +fn from_schedule_rejects_a_schedule_that_does_not_cover_the_folds() { + // lde_log 20, blowup 2, k 7: total_folds 11, row-pair chain covers 10 bits. + assert!(FriFoldLayout::from_schedule(20, 2, 7, false, vec![4, 4, 2]).is_some()); + assert!(FriFoldLayout::from_schedule(20, 2, 7, false, vec![4, 4, 3]).is_none()); + assert!(FriFoldLayout::from_schedule(20, 2, 7, false, vec![4, 4, 1]).is_none()); + // One-row: the chain covers all 11. + assert!(FriFoldLayout::from_schedule(20, 2, 7, true, vec![4, 4, 3]).is_some()); + assert!(FriFoldLayout::from_schedule(20, 2, 7, true, vec![4, 4, 2]).is_none()); + // Zero and over-DMAX exponents. + assert!(FriFoldLayout::from_schedule(20, 2, 7, false, vec![0, 5, 5]).is_none()); + assert!(FriFoldLayout::from_schedule(20, 2, 7, false, vec![7, 3]).is_none()); + // No fold: only the empty schedule. + assert!(FriFoldLayout::from_schedule(8, 2, 7, false, vec![]).is_some()); + assert!(FriFoldLayout::from_schedule(8, 2, 7, true, vec![1]).is_none()); + // One fold, row pair: no committed layer. + assert!(FriFoldLayout::from_schedule(10, 2, 7, false, vec![]).is_some()); + assert!(FriFoldLayout::from_schedule(10, 2, 7, false, vec![1]).is_none()); + assert!(FriFoldLayout::from_schedule(10, 2, 7, true, vec![1]).is_some()); +} + +// --------------------------------------------------------------------------- +// The layout from `ProofOptions` (what the prover and verifier build). +// --------------------------------------------------------------------------- + +fn options_with(format: ProofFormat) -> ProofOptions { + ProofOptions { + format, + ..ProofOptions::default_test_options() + } +} + +#[test] +fn layout_from_options() { + // Default format: today's layout. + let o = options_with(ProofFormat::DEFAULT); + let k = u32::from(o.fri_final_poly_log_degree); + assert_eq!( + FriFoldLayout::for_options(20, 1, &o, false), + Ok(FriFoldLayout::new(20, 1, k)) + ); + // Dp: the DP's schedule under the options' query count and cap. + let o = options_with(ProofFormat { + fri_mode: FriMode::Dp, + ..ProofFormat::DEFAULT + }); + let l = FriFoldLayout::for_options(20, 1, &o, false).unwrap(); + let t = (1 + k).min(20); + assert_eq!( + l.schedule, + fri_schedule( + 19, + t, + o.fri_number_of_queries as u64, + CapPolicy::Off, + FRI_SCHEDULE_DMAX + ) + ); + assert!(!l.is_legacy()); + // An override that fits is taken verbatim; one that does not is an error. + let span = 19 - t; + let mut fit = vec![1u8; span as usize - 3]; + fit.insert(0, 3); + let o = options_with(ProofFormat { + fri_mode: FriMode::Dp, + fri_schedule_override: FriScheduleOverride::new(&fit), + ..ProofFormat::DEFAULT + }); + assert_eq!( + FriFoldLayout::for_options(20, 1, &o, false) + .unwrap() + .schedule, + fit + ); + let o = options_with(ProofFormat { + fri_mode: FriMode::Dp, + fri_schedule_override: FriScheduleOverride::new(&[3, 1]), + ..ProofFormat::DEFAULT + }); + assert_eq!( + FriFoldLayout::for_options(20, 1, &o, false), + Err(FriFormatError::ScheduleOverrideMismatch) + ); + // An all-ones override under Dp keeps the GROUP encoding. + let o = options_with(ProofFormat { + fri_mode: FriMode::Dp, + fri_schedule_override: FriScheduleOverride::new(&vec![1u8; span as usize]), + ..ProofFormat::DEFAULT + }); + let l = FriFoldLayout::for_options(20, 1, &o, false).unwrap(); + assert_eq!(l.schedule, vec![1u8; span as usize]); + assert!(!l.is_legacy()); + // One row (S2): the chain starts at the LDE size, the encoding is the + // group one even at fri = pair, and the all-ones schedule covers every + // fold (no uncommitted fold 0). + for one_row in [OneRowMode::On, OneRowMode::Auto] { + let o = options_with(ProofFormat { + one_row, + ..ProofFormat::DEFAULT + }); + let l = FriFoldLayout::for_options(20, 1, &o, true).unwrap(); + assert!(l.one_row && !l.is_legacy()); + assert_eq!(l.schedule, vec![1u8; (20 - t) as usize]); + assert_eq!(l.num_committed as u32, l.total_folds); + assert_eq!(l.num_zetas(), l.num_committed); + assert_eq!( + l.layer_depth(20, 0), + 19, + "the input tree: 2^20 values in pairs" + ); + // The same options at a resolved row-pair layout: today's. + assert_eq!( + FriFoldLayout::for_options(20, 1, &o, false), + Ok(FriFoldLayout::new(20, 1, k)) + ); + } + // An override longer than the fixed capacity is refused at construction. + assert!(FriScheduleOverride::new(&[1u8; 33]).is_none()); + assert_eq!( + FriScheduleOverride::new(&[2, 1]).unwrap().as_slice(), + &[2, 1] + ); +} diff --git a/crypto/stark/src/tests/log_read_only_program_tests.rs b/crypto/stark/src/tests/log_read_only_program_tests.rs new file mode 100644 index 000000000..08e389bb7 --- /dev/null +++ b/crypto/stark/src/tests/log_read_only_program_tests.rs @@ -0,0 +1,151 @@ +//! `LogReadOnlyRAP` carries a constraint program. +//! +//! The CUDA composition arm evaluates `AIR::constraint_program()` once main +//! and aux are device-resident; `LogReadOnlyRAP` (the AIR of the checked-in +//! S3/S2 proof vectors) had none, so every device-proved vector test panicked +//! before it compared a byte. The program is captured from the SAME +//! `LogReadOnlyRAPConstraints` body the CPU folders run, so the device +//! composes the same polynomials and the vector bytes cannot move. +//! +//! These CPU tests pin that equality on random two-row frames three ways: +//! the prover folder (the CPU prover's hot path) == the captured program under +//! the generic interpreter == the lowered device program under its host model +//! (`eval_device_program`, the CPU model of the GPU kernel). + +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as E; +use math::field::goldilocks::GoldilocksField as F; + +use crate::constraint_ir::{DeviceProgram, eval_device_program, eval_program}; +use crate::examples::read_only_memory_logup::LogReadOnlyRAP; +use crate::frame::Frame; +use crate::proof::options::ProofOptions; +use crate::table::TableView; +use crate::traits::{AIR, TransitionEvaluationContext}; + +type Felt = FieldElement; +type Ext = FieldElement; + +struct SplitMix64(u64); +impl SplitMix64 { + fn next_u64(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + fn fp(&mut self) -> Felt { + Felt::from(self.next_u64()) + } + fn ext(&mut self) -> Ext { + Ext::from_raw([self.fp(), self.fp(), self.fp()]) + } +} + +fn limbs(x: &Ext) -> [u64; 3] { + let v = x.value(); + [v[0].canonical(), v[1].canonical(), v[2].canonical()] +} + +fn from_limbs(l: [u64; 3]) -> Ext { + Ext::from_raw([Felt::from(l[0]), Felt::from(l[1]), Felt::from(l[2])]) +} + +fn air() -> LogReadOnlyRAP { + LogReadOnlyRAP::::new(&ProofOptions::default_test_options()) +} + +#[test] +fn the_log_read_only_program_has_the_air_shape() { + let air = air(); + let prog = air.constraint_program(); + assert_eq!(prog.roots.len(), air.num_transition_constraints()); + assert_eq!(prog.num_base, air.num_base_transition_constraints()); + assert_eq!(prog.num_base, 2, "continuity and single-value are base"); + // Cached: a second call hands back the same program. + assert!(std::ptr::eq(prog, air.constraint_program())); +} + +#[test] +fn the_log_read_only_program_equals_the_prover_folder_and_the_device_model() { + let air = air(); + let prog = air.constraint_program(); + let dev = DeviceProgram::lower(prog); + let n = air.num_transition_constraints(); + let nb = air.num_base_transition_constraints(); + let (main_w, aux_w) = air.trace_layout(); + + let mut rng = SplitMix64(0x1F1C_D2D2_0000_0001); + for trial in 0..500 { + let main: Vec> = (0..2) + .map(|_| (0..main_w).map(|_| rng.fp()).collect()) + .collect(); + let aux: Vec> = (0..2) + .map(|_| (0..aux_w).map(|_| rng.ext()).collect()) + .collect(); + let rap = vec![rng.ext(), rng.ext()]; + let alphas: Vec = Vec::new(); + let offset = Ext::zero(); + + let steps: Vec> = main + .iter() + .zip(aux.iter()) + .map(|(m, a)| TableView::::new(vec![m.clone()], vec![a.clone()])) + .collect(); + let frame = Frame::::new(steps); + let ctx = + TransitionEvaluationContext::new_prover(frame.as_row_frame(), &rap, &alphas, &offset); + + // The CPU prover's path. + let mut folder_base = vec![Felt::zero(); nb]; + let mut folder_ext = vec![Ext::zero(); n]; + air.compute_transition_prover(&ctx, &mut folder_base, &mut folder_ext); + + // The captured program, generic interpreter. + let mut interp_base = vec![Felt::zero(); nb]; + let mut interp_ext = vec![Ext::zero(); n]; + eval_program(prog, &ctx, &mut interp_base, &mut interp_ext); + assert_eq!(folder_base, interp_base, "base constraints, trial {trial}"); + assert_eq!( + folder_ext[nb..], + interp_ext[nb..], + "ext constraints, trial {trial}" + ); + + // The lowered device program, host model of the GPU kernel. + let main_raw: Vec> = main + .iter() + .map(|r| r.iter().map(|x| x.canonical()).collect()) + .collect(); + let aux_raw: Vec> = + aux.iter().map(|r| r.iter().map(limbs).collect()).collect(); + let rap_raw: Vec<[u64; 3]> = rap.iter().map(limbs).collect(); + let mut base_dev = vec![0u64; nb]; + let mut ext_dev = vec![[0u64; 3]; n]; + eval_device_program( + &dev, + &main_raw, + &aux_raw, + &rap_raw, + &[], + limbs(&offset), + &mut base_dev, + &mut ext_dev, + ); + for c in 0..nb { + assert_eq!( + Felt::from(base_dev[c]), + folder_base[c], + "device base {c}, trial {trial}" + ); + } + for c in nb..n { + assert_eq!( + from_limbs(ext_dev[c]), + folder_ext[c], + "device ext {c}, trial {trial}" + ); + } + } +} diff --git a/crypto/stark/src/tests/merkle_cap_tests.rs b/crypto/stark/src/tests/merkle_cap_tests.rs new file mode 100644 index 000000000..d00d4ca70 --- /dev/null +++ b/crypto/stark/src/tests/merkle_cap_tests.rs @@ -0,0 +1,831 @@ +//! Merkle caps on univariate STARK proofs (lever S1). +//! +//! Every tree of a proof — main, precomputed, aux, composition, each committed +//! FRI layer — gets a height-`c` cap under a cap policy. The cap rides at the +//! end of the tree's first opening (the owner path); every path is cut to +//! `D − c` siblings. These tests pin: +//! - round trips at every policy, over the owned and the archived (rkyv) path; +//! - the default (`Off`) is byte-identical to a zero-height policy; +//! - the transcript does not move: an `Off` and an `Auto` proof of one witness +//! differ only in their Merkle paths; +//! - tampers of every tree class, of the owner split, and of the policy; +//! - load-bearing checks at the verifier level: an unreached cap node that only the +//! cap-to-root check rejects, and an internal node passed off as a leaf that +//! only the exact-length check rejects. + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use crypto::merkle_tree::cap::{CapPolicy, verify_cap}; +use crypto::merkle_tree::proof::verify_merkle_path_from_leaf_hash; +use crypto::merkle_tree::traits::IsMerkleTreeBackend; +use math::field::element::FieldElement; +use math::field::goldilocks::GoldilocksField; + +use crate::config::{Commitment, DefaultStarkHash, StarkHash}; +use crate::domain::new_verifier_domain; +use crate::examples::fibonacci_2_columns::compute_trace; +use crate::examples::fibonacci_rap::{FibonacciRAP, FibonacciRAPPublicInputs, fibonacci_rap_trace}; +use crate::examples::simple_addition::{ + SimpleAdditionAIR, SimpleAdditionPublicInputs, simple_addition_trace, +}; +use crate::examples::simple_fibonacci::FibonacciPublicInputs; +use crate::merkle_caps::StarkCaps; +use crate::proof::options::ProofOptions; +use crate::proof::stark::{MultiProof, StarkProof}; +use crate::proof::view::StarkProofView; +use crate::prover::{IsStarkProver, Prover}; +use crate::tests::opening_width_tests::FibonacciSplitAIR; +use crate::traits::AIR; +use crate::verifier::{IsStarkVerifier, Verifier}; + +type F = GoldilocksField; +type FE = FieldElement; +type PI = SimpleAdditionPublicInputs; +type Proof = StarkProof; +/// The leaf backend the default prover commits the trace trees with. +type Leaf = ::Batched; + +/// 1024 rows at blowup 2: trace trees 10 deep, 2 committed FRI layers. +const ROWS: usize = 1024; + +fn options(policy: CapPolicy, queries: usize, blowup: u8) -> ProofOptions { + let mut o = ProofOptions::default_test_options(); + o.blowup_factor = blowup; + o.fri_number_of_queries = queries; + // Grinding off: the nonce is then absent, and two proofs of one witness + // are comparable byte for byte. + o.grinding_factor = 0; + o.format.merkle_cap = policy; + o +} + +fn prove(opts: &ProofOptions) -> (SimpleAdditionAIR, Proof) { + let air = SimpleAdditionAIR::::new(opts); + let pub_inputs = SimpleAdditionPublicInputs { + a: FE::from(1u64), + b: FE::from(2u64), + }; + let mut trace = simple_addition_trace::(ROWS); + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .expect("proving must succeed"); + (air, proof) +} + +fn verifies(air: &SimpleAdditionAIR, proof: &Proof) -> bool { + Verifier::verify(proof, air, &mut DefaultTranscript::::new(&[])) +} + +/// The same proof over the wire: rkyv, then `multi_verify_archived` (the +/// read-in-place path host continuation verification uses). +fn verifies_archived(air: &SimpleAdditionAIR, proof: &Proof) -> bool { + let multi = MultiProof { + proofs: vec![proof.clone()], + }; + let bytes = rkyv::to_bytes::(&multi).unwrap(); + let archived = rkyv::access::< + crate::proof::stark::ArchivedMultiProof, + rkyv::rancor::Error, + >(&bytes) + .unwrap(); + let airs: Vec<&dyn AIR> = vec![air]; + Verifier::multi_verify_archived( + &airs, + archived, + &mut DefaultTranscript::::new(&[]), + &FE::zero(), + ) +} + +fn caps_of(air: &SimpleAdditionAIR, proof: &Proof) -> StarkCaps { + let o = air.options(); + StarkCaps::new( + o.format.merkle_cap, + o.fri_number_of_queries, + (o.blowup_factor as usize * proof.trace_length).trailing_zeros() as usize, + proof.fri_layers_merkle_roots.len(), + ) +} + +/// Every path of one tree: the owner carries `D − c + 2^c` nodes (the cap at +/// its end, hashing to `root`), every other opening `D − c`. +fn assert_tree_shape(paths: &[&Vec], root: &Commitment, depth: usize, c: usize) { + let owner_len = if c == 0 { depth } else { depth - c + (1 << c) }; + assert_eq!(paths[0].len(), owner_len, "owner path, D={depth} c={c}"); + for (q, p) in paths.iter().enumerate().skip(1) { + assert_eq!(p.len(), depth - c, "query {q}, D={depth} c={c}"); + } + if c > 0 { + assert!( + verify_cap::(&paths[0][depth - c..], root, c), + "the owner's cap must hash to the root" + ); + } +} + +fn assert_proof_shape(air: &SimpleAdditionAIR, proof: &Proof) { + let caps = caps_of(air, proof); + let main: Vec<_> = proof + .deep_poly_openings + .iter() + .map(|o| &o.main_trace_polys.proof.merkle_path) + .collect(); + assert_tree_shape( + &main, + &proof.lde_trace_main_merkle_root, + caps.trace_depth, + caps.trace, + ); + let comp: Vec<_> = proof + .deep_poly_openings + .iter() + .map(|o| &o.composition_poly.proof.merkle_path) + .collect(); + assert_tree_shape( + &comp, + &proof.composition_poly_root, + caps.trace_depth, + caps.trace, + ); + for (i, root) in proof.fri_layers_merkle_roots.iter().enumerate() { + let layer: Vec<_> = proof + .query_list + .iter() + .map(|q| &q.layers_auth_paths[i].merkle_path) + .collect(); + assert_tree_shape(&layer, root, caps.fri_depths[i], caps.fri[i]); + } +} + +// ------------------------------------------------------------------ round trips + +#[test] +fn every_policy_round_trips_owned_and_archived() { + for blowup in [2u8, 4] { + for (policy, queries) in [ + (CapPolicy::Fixed(1), 3), + (CapPolicy::Fixed(2), 3), + (CapPolicy::Fixed(3), 3), + (CapPolicy::Fixed(4), 3), + (CapPolicy::Auto, 3), + (CapPolicy::Auto, 8), + (CapPolicy::Auto, 30), + ] { + let (air, proof) = prove(&options(policy, queries, blowup)); + assert!( + proof.fri_layers_merkle_roots.len() >= 2, + "the FRI arm must commit layers" + ); + let caps = caps_of(&air, &proof); + if policy != CapPolicy::Auto || queries >= 4 { + assert!(caps.any(), "{policy} Q={queries}: some tree must be capped"); + } + assert_proof_shape(&air, &proof); + assert!( + verifies(&air, &proof), + "{policy} Q={queries} blowup {blowup}" + ); + assert!( + verifies_archived(&air, &proof), + "{policy} Q={queries} blowup {blowup}: archived" + ); + } + } +} + +/// A preprocessed table (precomputed + main trees) and a RAP table (main + aux +/// trees) round-trip under a cap, and a cap node flip in each of those trees is +/// rejected. +#[test] +fn preprocessed_and_aux_trees_are_capped() { + // Preprocessed: 1 precomputed column, 1 main column, 1024 rows. + let opts = options(CapPolicy::Fixed(3), 3, 2); + let mut trace = compute_trace([FE::one(), FE::one()], ROWS); + let reference = FibonacciSplitAIR::::honest(&opts, None); + let commitment = Prover::compute_precomputed_commitment_for_testing(&trace, &reference, 1) + .expect("precomputed commitment"); + let air = FibonacciSplitAIR::::preprocessed_declaring(&opts, None, 1, commitment); + let pi = FibonacciPublicInputs { + a0: FE::one(), + a1: FE::one(), + }; + let proof = + Prover::prove(&air, &mut trace, &pi, &mut DefaultTranscript::::new(&[])).expect("prove"); + let verify = |p: &StarkProof>| { + Verifier::verify(p, &air, &mut DefaultTranscript::::new(&[])) + }; + assert!(verify(&proof), "capped preprocessed proof"); + let depth = 10; + let pre = proof.deep_poly_openings[0] + .precomputed_trace_polys + .as_ref() + .expect("precomputed opening"); + assert_eq!(pre.proof.merkle_path.len(), depth - 3 + 8); + for k in 0..8 { + let mut bad = proof.clone(); + bad.deep_poly_openings[0] + .precomputed_trace_polys + .as_mut() + .unwrap() + .proof + .merkle_path[depth - 3 + k][0] ^= 1; + assert!(!verify(&bad), "precomputed cap node {k}"); + } + + // RAP: 2 main + 1 aux column, 16 steps (the AIR's constraints are fixed to + // 16 steps), capped at 3. + let opts = options(CapPolicy::Fixed(3), 3, 2); + let mut trace = fibonacci_rap_trace([FE::one(), FE::one()], 16); + let air = FibonacciRAP::::new(&opts); + let pi = FibonacciRAPPublicInputs { + steps: 16, + a0: FE::one(), + a1: FE::one(), + }; + let proof = + Prover::prove(&air, &mut trace, &pi, &mut DefaultTranscript::::new(&[])).expect("prove"); + let verify = |p: &StarkProof>| { + Verifier::verify(p, &air, &mut DefaultTranscript::::new(&[])) + }; + assert!(verify(&proof), "capped RAP proof"); + let depth = StarkCaps::trace_tree_depth((2 * proof.trace_length).trailing_zeros() as usize); + assert!(depth >= 3); + let aux = proof.deep_poly_openings[0] + .aux_trace_polys + .as_ref() + .expect("aux opening"); + assert_eq!(aux.proof.merkle_path.len(), depth - 3 + 8); + assert_eq!( + proof.deep_poly_openings[1] + .aux_trace_polys + .as_ref() + .unwrap() + .proof + .merkle_path + .len(), + depth - 3 + ); + for k in 0..8 { + let mut bad = proof.clone(); + bad.deep_poly_openings[0] + .aux_trace_polys + .as_mut() + .unwrap() + .proof + .merkle_path[depth - 3 + k][5] ^= 0x40; + assert!(!verify(&bad), "aux cap node {k}"); + } +} + +// ------------------------------------------------------------ default identity + +/// `Off`, `Fixed(0)` and a policy whose every height is 0 (`Auto` at 3 +/// queries) produce the same bytes: the default format is unchanged. +#[test] +fn a_zero_height_policy_is_byte_identical_to_off() { + let bytes = |policy| { + let (air, proof) = prove(&options(policy, 3, 2)); + assert!(!caps_of(&air, &proof).any()); + rkyv::to_bytes::(&proof) + .unwrap() + .to_vec() + }; + let off = bytes(CapPolicy::Off); + assert_eq!(off, bytes(CapPolicy::Fixed(0))); + assert_eq!(off, bytes(CapPolicy::Auto)); +} + +/// The transcript does not change under a cap. One witness +/// proved at `Off` and at `Auto` (grinding off) gives equal roots, OOD values, +/// FRI final coefficients, nonces and opened values; only the Merkle paths +/// differ, and each capped path is exactly its full path cut to `D − c`, with +/// the cap appended on the owner. +#[test] +fn the_transcript_is_the_same_with_and_without_a_cap() { + let (_, off) = prove(&options(CapPolicy::Off, 30, 2)); + let (air, on) = prove(&options(CapPolicy::Auto, 30, 2)); + let caps = caps_of(&air, &on); + assert_eq!(caps.trace, 3); + assert!(caps.fri.iter().all(|&c| c == 3)); + + assert_eq!( + off.lde_trace_main_merkle_root, + on.lde_trace_main_merkle_root + ); + assert_eq!(off.lde_trace_aux_merkle_root, on.lde_trace_aux_merkle_root); + assert_eq!( + off.lde_trace_precomputed_merkle_root, + on.lde_trace_precomputed_merkle_root + ); + assert_eq!(off.composition_poly_root, on.composition_poly_root); + assert_eq!(off.fri_layers_merkle_roots, on.fri_layers_merkle_roots); + assert_eq!(off.trace_ood_evaluations, on.trace_ood_evaluations); + assert_eq!( + off.trace_ood_next_evaluations, + on.trace_ood_next_evaluations + ); + assert_eq!( + off.composition_poly_parts_ood_evaluation, + on.composition_poly_parts_ood_evaluation + ); + assert_eq!(off.fri_final_poly_coeffs, on.fri_final_poly_coeffs); + assert_eq!(off.nonce, on.nonce); + assert_eq!(off.trace_length, on.trace_length); + + let cut = |full: &Vec, capped: &Vec, q: usize, d: usize, c: usize| { + assert_eq!(full.len(), d); + assert_eq!(&capped[..d - c], &full[..d - c], "query {q}: the siblings"); + let tail = if q == 0 { 1usize << c } else { 0 }; + assert_eq!(capped.len(), d - c + tail, "query {q}"); + }; + let d = caps.trace_depth; + for (q, (a, b)) in off + .deep_poly_openings + .iter() + .zip(&on.deep_poly_openings) + .enumerate() + { + assert_eq!( + a.main_trace_polys.evaluations, + b.main_trace_polys.evaluations + ); + assert_eq!( + a.main_trace_polys.evaluations_sym, + b.main_trace_polys.evaluations_sym + ); + assert_eq!( + a.composition_poly.evaluations, + b.composition_poly.evaluations + ); + assert_eq!( + a.composition_poly.evaluations_sym, + b.composition_poly.evaluations_sym + ); + cut( + &a.main_trace_polys.proof.merkle_path, + &b.main_trace_polys.proof.merkle_path, + q, + d, + 3, + ); + cut( + &a.composition_poly.proof.merkle_path, + &b.composition_poly.proof.merkle_path, + q, + d, + 3, + ); + } + for (q, (a, b)) in off.query_list.iter().zip(&on.query_list).enumerate() { + assert_eq!(a.layers_evaluations_sym, b.layers_evaluations_sym); + for i in 0..caps.fri.len() { + cut( + &a.layers_auth_paths[i].merkle_path, + &b.layers_auth_paths[i].merkle_path, + q, + caps.fri_depths[i], + caps.fri[i], + ); + } + } +} + +// --------------------------------------------------------------------- tampers + +type PathOf = fn(&mut Proof) -> &mut Vec; +type PathFn = dyn Fn(&mut Proof) -> &mut Vec; + +fn main_path(q: usize) -> impl Fn(&mut Proof) -> &mut Vec { + move |p| &mut p.deep_poly_openings[q].main_trace_polys.proof.merkle_path +} +fn comp_path(q: usize) -> impl Fn(&mut Proof) -> &mut Vec { + move |p| &mut p.deep_poly_openings[q].composition_poly.proof.merkle_path +} +fn fri_path(q: usize, layer: usize) -> impl Fn(&mut Proof) -> &mut Vec { + move |p| &mut p.query_list[q].layers_auth_paths[layer].merkle_path +} + +fn rejected_after( + air: &SimpleAdditionAIR, + honest: &Proof, + tamper: impl FnOnce(&mut Proof), +) -> bool { + let mut p = honest.clone(); + tamper(&mut p); + !verifies(air, &p) && !verifies_archived(air, &p) +} + +#[test] +fn every_cap_node_of_every_tree_class_is_bound() { + let (air, honest) = prove(&options(CapPolicy::Auto, 30, 2)); + assert!(verifies(&air, &honest)); + let caps = caps_of(&air, &honest); + let last = honest.fri_layers_merkle_roots.len() - 1; + let trees: Vec<(&str, Box, usize, usize)> = vec![ + ("main", Box::new(main_path(0)), caps.trace_depth, caps.trace), + ( + "composition", + Box::new(comp_path(0)), + caps.trace_depth, + caps.trace, + ), + ( + "FRI layer 0", + Box::new(fri_path(0, 0)), + caps.fri_depths[0], + caps.fri[0], + ), + ( + "last FRI layer", + Box::new(fri_path(0, last)), + caps.fri_depths[last], + caps.fri[last], + ), + ]; + for (what, path_of, d, c) in &trees { + assert_eq!(*c, 3, "{what}"); + for k in 0..(1usize << c) { + assert!( + rejected_after(&air, &honest, |p| path_of(p)[d - c + k][7] ^= 1), + "{what}: cap node {k} flipped" + ); + } + } +} + +#[test] +fn a_path_node_of_a_later_query_is_bound() { + let (air, honest) = prove(&options(CapPolicy::Auto, 30, 2)); + let paths: [(&str, PathOf); 3] = [ + ("main", |p| { + &mut p.deep_poly_openings[5].main_trace_polys.proof.merkle_path + }), + ("composition", |p| { + &mut p.deep_poly_openings[5].composition_poly.proof.merkle_path + }), + ("FRI layer 1", |p| { + &mut p.query_list[5].layers_auth_paths[1].merkle_path + }), + ]; + for (what, path_of) in paths { + let len = path_of(&mut honest.clone()).len(); + for k in 0..len { + assert!( + rejected_after(&air, &honest, |p| path_of(p)[k][0] ^= 0x10), + "{what}: query 5 node {k}" + ); + } + } +} + +#[test] +fn the_owner_split_is_exact() { + let (air, honest) = prove(&options(CapPolicy::Auto, 30, 2)); + // The owner path one node short (the last cap node dropped) or long. + assert!(rejected_after(&air, &honest, |p| { + main_path(0)(p).pop(); + })); + assert!(rejected_after(&air, &honest, |p| { + let path = main_path(0)(p); + path.push(path[0]); + })); + // A non-owner path carrying the cap too. + assert!(rejected_after(&air, &honest, |p| { + let cap: Vec = main_path(0)(p)[7..].to_vec(); + main_path(1)(p).extend(cap); + })); + // The cap moved from query 0 to query 1. + assert!(rejected_after(&air, &honest, |p| { + let cap: Vec = main_path(0)(p).split_off(7); + main_path(1)(p).extend(cap); + })); + // The same for a FRI layer. + assert!(rejected_after(&air, &honest, |p| { + let path = fri_path(0, 0); + let d = path(p).len() - 8; + let cap: Vec = path(p).split_off(d); + fri_path(1, 0)(p).extend(cap); + })); +} + +/// The cap height is a verifier constant: a proof made under one policy fails +/// under any other, in both directions. +#[test] +fn a_proof_made_under_one_policy_fails_under_another() { + let (_, fixed3) = prove(&options(CapPolicy::Fixed(3), 30, 2)); + let (air_off, off) = prove(&options(CapPolicy::Off, 30, 2)); + let air_at = |policy| SimpleAdditionAIR::::new(&options(policy, 30, 2)); + assert!(verifies(&air_at(CapPolicy::Fixed(3)), &fixed3)); + assert!(!verifies(&air_at(CapPolicy::Fixed(2)), &fixed3)); + assert!(!verifies(&air_at(CapPolicy::Fixed(4)), &fixed3)); + assert!(!verifies(&air_off, &fixed3)); + assert!(!verifies(&air_at(CapPolicy::Auto), &off)); + assert!(verifies(&air_off, &off)); +} + +// ---------------------------------------------------- M1 at the verifier level + +/// The transcript's index of query `q` of the main tree, recovered from its +/// capped opening (the only index whose fold lands on the cap), and the cap +/// node it reaches. +fn main_query_index(proof: &Proof, q: usize, d: usize, c: usize) -> (usize, usize) { + let owner = &proof.deep_poly_openings[0] + .main_trace_polys + .proof + .merkle_path; + let cap = &owner[d - c..]; + let opening = &proof.deep_poly_openings[q].main_trace_polys; + let siblings = &opening.proof.merkle_path[..d - c]; + let leaf = Leaf::hash_data_from_slices(&opening.evaluations, &opening.evaluations_sym); + let hits: Vec = (0..1usize << d) + .filter(|&i| { + verify_merkle_path_from_leaf_hash::(siblings, &cap[i >> (d - c)], i, leaf) + }) + .collect(); + assert_eq!( + hits.len(), + 1, + "query {q}: exactly one index folds onto the cap" + ); + (hits[0], hits[0] >> (d - c)) +} + +/// M1(b): with 3 queries and a height-3 cap, at least 5 of the 8 main-tree cap +/// nodes are reached by no query. Flipping one leaves every per-query check +/// green (each query still folds onto its own, unchanged, cap node), so only +/// the cap-to-root check rejects the proof. Deleting `verify_cap` from +/// `CappedRoot::from_owner` makes this test fail. +#[test] +fn an_unreached_cap_node_is_rejected_by_the_cap_to_root_check_alone() { + let (air, honest) = prove(&options(CapPolicy::Fixed(3), 3, 2)); + let (d, c) = (10, 3); + assert!(verifies(&air, &honest)); + let reached: Vec = (0..3) + .map(|q| main_query_index(&honest, q, d, c).1) + .collect(); + let unreached: Vec = (0..8).filter(|k| !reached.contains(k)).collect(); + assert!(unreached.len() >= 5, "3 queries reach at most 3 of 8 nodes"); + for k in unreached { + let mut bad = honest.clone(); + main_path(0)(&mut bad)[d - c + k][3] ^= 1; + // Precondition: the per-query folds are untouched by the flip. + for (q, &node) in reached.iter().enumerate() { + assert_eq!(main_query_index(&bad, q, d, c).1, node); + } + assert!(!verifies(&air, &bad), "unreached cap node {k}"); + assert!( + !verifies_archived(&air, &bad), + "unreached cap node {k}: archived" + ); + } +} + +/// M1(a): the verifier's own per-tree check (`table_tree_checks`) refuses the +/// real internal node one level above a queried leaf, presented as a leaf hash +/// with the path from that node up — which the length-agnostic fold accepts. +/// Only the exact-length check stands between the two; deleting it from the +/// cap primitive makes this test fail. Run at the default (`c = 0`) and +/// under a cap. +#[test] +fn an_internal_node_passed_as_a_leaf_is_rejected_by_the_length_check_alone() { + for policy in [CapPolicy::Off, CapPolicy::Fixed(3)] { + let (air, proof) = prove(&options(policy, 3, 2)); + let c = if policy == CapPolicy::Off { 0 } else { 3 }; + let d = 10; + let view = StarkProofView::Owned(&proof); + let domain = new_verifier_domain(&air, proof.trace_length); + let checks = Verifier::table_tree_checks(&air, view, &domain).expect("honest shape"); + for q in 1..3 { + let iota = if c == 0 { + // Uncapped: fold against the root directly. + let opening = &proof.deep_poly_openings[q].main_trace_polys; + let leaf = + Leaf::hash_data_from_slices(&opening.evaluations, &opening.evaluations_sym); + (0..1usize << d) + .find(|&i| { + verify_merkle_path_from_leaf_hash::( + &opening.proof.merkle_path, + &proof.lde_trace_main_merkle_root, + i, + leaf, + ) + }) + .expect("the honest index") + } else { + main_query_index(&proof, q, d, c).0 + }; + let opening = &proof.deep_poly_openings[q].main_trace_polys; + let path = &opening.proof.merkle_path; + let leaf = Leaf::hash_data_from_slices(&opening.evaluations, &opening.evaluations_sym); + // The real node one level up, and the position it sits at. + let node = if iota & 1 == 0 { + Leaf::hash_new_parent(&leaf, &path[0]) + } else { + Leaf::hash_new_parent(&path[0], &leaf) + }; + let forged = &path[1..]; + let target = if c == 0 { + proof.lde_trace_main_merkle_root + } else { + proof.deep_poly_openings[0] + .main_trace_polys + .proof + .merkle_path[d - c + (iota >> (d - c))] + }; + assert!( + verify_merkle_path_from_leaf_hash::(forged, &target, iota >> 1, node), + "{policy} q={q}: precondition, the fold alone accepts the forgery" + ); + assert!( + !checks.main.verify::(q, forged, iota >> 1, node), + "{policy} q={q}: an internal node passed for a leaf" + ); + // The honest opening passes the same check. + assert!(checks.main.verify::(q, path, iota, leaf)); + } + } +} + +// ------------------------------------------------------------- device trees + +/// A device-resident tree (a root-only host tree) whose cap has +/// no device read is a hard `Err` naming the tree — never a skipped cap, which +/// would ship full-length paths the verifier rejects with no pointer to the +/// cause. And a device read that fails is an `Err` too, not a panic. +#[test] +fn a_device_resident_tree_without_a_cap_read_is_an_error() { + use crate::prover::ProvingError; + use crypto::merkle_tree::merkle::MerkleTree; + type P = Prover; + let root_only = MerkleTree::::from_root([7u8; 32]); + match

>::tree_cap( + &root_only, + 10, + 3, + "main", + |_| None, + ) { + Err(ProvingError::DevicePath(msg)) => { + assert!( + msg.contains("main") && msg.contains("device-resident"), + "{msg}" + ) + } + other => panic!("expected a DevicePath error, got {other:?}"), + } + match

>::tree_cap( + &root_only, + 10, + 3, + "FRI layer 2", + |_| Some(Err("cudarc said no".to_string())), + ) { + Err(ProvingError::DevicePath(msg)) => { + assert!( + msg.contains("FRI layer 2") && msg.contains("cudarc said no"), + "{msg}" + ) + } + other => panic!("expected a DevicePath error, got {other:?}"), + } + // A host tree whose depth is not the format's is refused too. + let data: Vec> = (0..16u64) + .map(|i| vec![FE::from(i), FE::from(i + 1)]) + .collect(); + let host = MerkleTree::::build(&data).expect("tree"); + assert!( +

>::tree_cap(&host, 5, 2, "aux", |_| None) + .is_err() + ); + let cap = +

>::tree_cap(&host, 4, 2, "aux", |_| None) + .expect("a full host tree serves its cap"); + assert!(verify_cap::(&cap, &host.root, 2)); +} + +/// The device cap read on a real device (box only; `--features cuda -- --ignored`): a LogUp +/// table over the cubic extension, big enough that its main, aux, +/// composition and FRI trees are committed on the device (host trees +/// root-only), proved under `Auto` at 30 queries. The caps must come off the +/// device (`gpu_cap_read_calls` moves), the proof must verify owned and +/// archived, and against an `Off` proof of the same witness the transcript is +/// unchanged and every capped path is the full device-gathered path cut to +/// `D − c` (the cap on the owner). +#[cfg(feature = "cuda")] +#[test] +#[ignore = "requires a GPU; run with --features cuda -- --ignored"] +fn device_trees_serve_their_caps() { + // An `AirWithBuses` table: the device composition arm needs the AIR's + // constraint program, which the hand-written example AIRs do not supply + // (`LogReadOnlyRAP` here panicked in `constraint_program` on the box). + use crate::examples::bus_permutation::{bus_permutation_air, bus_permutation_trace}; + use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as E; + type Pi = (); + + let rows = 1usize << 14; + let prove_at = |policy| { + let opts = options(policy, 30, 2); + let mut trace = bus_permutation_trace(rows); + let air = bus_permutation_air(&opts); + let proof = Prover::prove(&air, &mut trace, &(), &mut DefaultTranscript::::new(&[])) + .expect("prove"); + (air, proof) + }; + + let (_, off) = prove_at(CapPolicy::Off); + let before = crate::gpu_lde::gpu_cap_read_calls(); + let (air, on) = prove_at(CapPolicy::Auto); + let reads = crate::gpu_lde::gpu_cap_read_calls() - before; + println!("CAPDEV device cap reads: {reads}"); + assert!( + reads > 0, + "no cap came off the device: the trees were host trees, the test proves nothing" + ); + assert!( + Verifier::verify(&on, &air, &mut DefaultTranscript::::new(&[])), + "a device-proved capped proof must verify" + ); + let multi = MultiProof { + proofs: vec![on.clone()], + }; + let bytes = rkyv::to_bytes::(&multi).unwrap(); + let archived = rkyv::access::< + crate::proof::stark::ArchivedMultiProof, + rkyv::rancor::Error, + >(&bytes) + .unwrap(); + let airs: Vec<&dyn AIR> = vec![&air]; + assert!(Verifier::multi_verify_archived( + &airs, + archived, + &mut DefaultTranscript::::new(&[]), + &FieldElement::::zero(), + )); + + assert_eq!( + off.lde_trace_main_merkle_root, + on.lde_trace_main_merkle_root + ); + assert_eq!(off.lde_trace_aux_merkle_root, on.lde_trace_aux_merkle_root); + assert_eq!(off.composition_poly_root, on.composition_poly_root); + assert_eq!(off.fri_layers_merkle_roots, on.fri_layers_merkle_roots); + assert_eq!(off.fri_final_poly_coeffs, on.fri_final_poly_coeffs); + let lde_log = (2 * on.trace_length).trailing_zeros() as usize; + let caps = StarkCaps::new( + CapPolicy::Auto, + 30, + lde_log, + on.fri_layers_merkle_roots.len(), + ); + assert_eq!(caps.trace, 3); + let check = |full: &Vec, capped: &Vec, q: usize, d: usize, c: usize| { + assert_eq!(full.len(), d, "query {q}: full path"); + assert_eq!(&capped[..d - c], &full[..d - c], "query {q}: siblings"); + assert_eq!( + capped.len(), + d - c + if q == 0 { 1 << c } else { 0 }, + "query {q}" + ); + }; + let d = caps.trace_depth; + for (q, (a, b)) in off + .deep_poly_openings + .iter() + .zip(&on.deep_poly_openings) + .enumerate() + { + check( + &a.main_trace_polys.proof.merkle_path, + &b.main_trace_polys.proof.merkle_path, + q, + d, + 3, + ); + check( + &a.composition_poly.proof.merkle_path, + &b.composition_poly.proof.merkle_path, + q, + d, + 3, + ); + let (aa, bb) = ( + a.aux_trace_polys.as_ref().unwrap(), + b.aux_trace_polys.as_ref().unwrap(), + ); + check(&aa.proof.merkle_path, &bb.proof.merkle_path, q, d, 3); + } + for (q, (a, b)) in off.query_list.iter().zip(&on.query_list).enumerate() { + for i in 0..caps.fri.len() { + check( + &a.layers_auth_paths[i].merkle_path, + &b.layers_auth_paths[i].merkle_path, + q, + caps.fri_depths[i], + caps.fri[i], + ); + } + } +} diff --git a/crypto/stark/src/tests/mod.rs b/crypto/stark/src/tests/mod.rs index f2520e2c4..de94682af 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -4,12 +4,19 @@ pub mod blake3_stark_roundtrip_tests; #[cfg(feature = "debug-checks")] pub mod bus_debug_tests; pub mod bus_tests; +pub mod cap_fri_matrix_tests; pub mod commitment_tests; pub mod constraint_index_tests; pub mod domain_cache_stats; +pub mod fri_group_tests; +pub mod fri_schedule_tests; pub mod fri_tests; pub mod grinding_tests; +pub mod log_read_only_program_tests; +pub mod merkle_cap_tests; +pub mod one_row_tests; pub mod opening_width_tests; +pub mod path_length_tests; pub mod proof_options_tests; pub mod prove_verify_roundtrip_tests; pub mod prover_tests; @@ -20,3 +27,9 @@ pub mod small_trace_tests; pub mod table_disk_spill_tests; pub mod terminal_tests; pub mod trace_test_helpers; +#[cfg(feature = "cuda")] +pub mod zf_fri_device_tests; +pub mod zf_fri_vectors; +pub mod zf_golden_tests; +#[cfg(feature = "cuda")] +pub mod zf_s2_device_tests; diff --git a/crypto/stark/src/tests/one_row_tests.rs b/crypto/stark/src/tests/one_row_tests.rs new file mode 100644 index 000000000..ab5f384cd --- /dev/null +++ b/crypto/stark/src/tests/one_row_tests.rs @@ -0,0 +1,981 @@ +//! S2 (one-row trace openings with a committed FRI input) on the CPU prover +//! and host verifier: the round trip U6 at one_row, the tamper +//! tests T4–T6, the load-bearing mutation M3, the transcript-order KAT, the +//! per-table `auto` rule, the preprocessed-root +//! miss (a hard error, never a recompute) and the cap × FRI × one-row matrix. + +use std::sync::Mutex; + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use crypto::fiat_shamir::is_transcript::IsTranscript; +use crypto::merkle_tree::cap::CapPolicy; +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; +use math::field::goldilocks::GoldilocksField; +use math::field::traits::IsFFTField; + +use crate::config::{Blake3StarkHash, KeccakStarkHash}; +use crate::examples::fibonacci_2_columns::compute_trace; +use crate::examples::simple_fibonacci::FibonacciPublicInputs; +use crate::fri::capture::{FriCapture, capture}; +use crate::fri::fri_functions::compute_coset_twiddles_inv; +use crate::fri::schedule::FRI_COST_WEIGHTS; +use crate::fri::terminal::FriFoldLayout; +use crate::fri::{commit_phase_with_layout, fold_times}; +use crate::leaf_layout::{ + LeafLayout, M3_PAIR_BOUND_AT_LDE, TableWidths, deep_point_xalu_rows, resolve_leaf_layout, + table_leaf_layout, table_openings_cost_q, +}; +use crate::proof::options::{FriMode, FriScheduleOverride, OneRowMode, ProofFormat, ProofOptions}; +use crate::proof::stark::MultiProof; +use crate::prover::{IsStarkProver, Prover, ProvingError}; +use crate::tests::opening_width_tests::FibonacciSplitAIR; +use crate::traits::AIR; +use crate::verifier::{IsStarkVerifier, Verifier}; + +use super::zf_golden_tests::{ + golden_options, prove_logup, prove_multi, prove_simple_addition, verify_logup, verify_multi, + verify_simple_addition, +}; + +type F = GoldilocksField; +type E = Degree3GoldilocksExtensionField; +type Felt = FieldElement; +type Ext = FieldElement; + +/// Serialises the tests that flip the process-global M3 switch (see +/// `leaf_layout::M3_PAIR_BOUND_AT_LDE`). +static M3_LOCK: Mutex<()> = Mutex::new(()); + +fn fmt(one_row: OneRowMode, fri_mode: FriMode, schedule: Option<&[u8]>) -> ProofFormat { + ProofFormat { + one_row, + fri_mode, + fri_schedule_override: schedule.map(|s| FriScheduleOverride::new(s).unwrap()), + ..ProofFormat::DEFAULT + } +} + +fn on(fri_mode: FriMode) -> ProofFormat { + fmt(OneRowMode::On, fri_mode, None) +} + +// --------------------------------------------------------------------------- +// The layout helper: one place a query becomes rows. +// --------------------------------------------------------------------------- + +#[test] +fn query_rows_bounds_and_depths() { + use math::fft::bit_reversing::reverse_index; + for lde_log in 1..=12u32 { + let n = 1usize << lde_log; + assert_eq!(LeafLayout::RowPair.query_bound(n as u64), (n / 2) as u64); + assert_eq!(LeafLayout::Row.query_bound(n as u64), n as u64); + assert_eq!( + LeafLayout::RowPair.tree_depth(lde_log as usize), + lde_log as usize - 1 + ); + assert_eq!( + LeafLayout::Row.tree_depth(lde_log as usize), + lde_log as usize + ); + for q in 0..n / 2 { + assert_eq!( + LeafLayout::RowPair.query_rows(q, n), + ( + reverse_index(2 * q, n as u64), + Some(reverse_index(2 * q + 1, n as u64)) + ) + ); + } + for r in 0..n { + assert_eq!( + LeafLayout::Row.query_rows(r, n), + (reverse_index(r, n as u64), None) + ); + } + } +} + +/// No stray `2·iota(+1)` row arithmetic outside the helper in +/// the opening code of the prover and the verifier (the legacy FRI +/// zero-fold terminal check, which indexes the TERMINAL codeword by the pair, +/// is the one named exception). +#[test] +fn every_opening_site_goes_through_query_rows() { + let prover = include_str!("../prover.rs"); + let verifier = include_str!("../verifier.rs"); + for (name, src) in [("prover.rs", prover), ("verifier.rs", verifier)] { + for (i, line) in src.lines().enumerate() { + let code = line.split("//").next().unwrap_or(""); + let pairish = code.contains("* 2 + 1") || code.contains("*2+1"); + let terminal = code.contains(".get(iota * 2 + 1)"); + let point_helper = code.contains("let raw = iota * 2"); + assert!( + !pairish || terminal || point_helper, + "{name}:{}: row-pair arithmetic outside LeafLayout::query_rows: {line}", + i + 1 + ); + } + } +} + +// --------------------------------------------------------------------------- +// U6: round trips at one_row, every fold count, pair and dp FRI. +// --------------------------------------------------------------------------- + +fn check_shape_simple( + proof: &crate::proof::stark::StarkProof< + F, + F, + crate::examples::simple_addition::SimpleAdditionPublicInputs, + >, + lde_log: u32, + layout: &FriFoldLayout, +) { + assert_eq!(proof.fri_layers_merkle_roots.len(), layout.num_committed); + for (q, dec) in proof.query_list.iter().zip(&proof.deep_poly_openings) { + assert!(dec.main_trace_polys.evaluations_sym.is_empty()); + assert!(dec.composition_poly.evaluations_sym.is_empty()); + assert_eq!( + dec.main_trace_polys.proof.merkle_path.len(), + lde_log as usize + ); + assert_eq!( + dec.composition_poly.proof.merkle_path.len(), + lde_log as usize + ); + assert_eq!( + q.layers_evaluations_sym.len(), + layout.opened_values_per_query() + ); + } +} + +#[test] +fn one_row_round_trips_at_every_fold_count() { + // k = 1: total_folds = log2(rows) + blowup_log − (blowup_log + 1). + for blowup in [2u8, 4] { + for log_rows in 1..=10u32 { + for mode in [FriMode::Pair, FriMode::Dp] { + let rows = 1usize << log_rows; + let o = golden_options(blowup, 1, 9, on(mode)); + let (air, proof) = prove_simple_addition::(rows, &o); + assert!( + verify_simple_addition::(&air, &proof), + "rows {rows} blowup {blowup} {mode:?}" + ); + let lde_log = log_rows + blowup.trailing_zeros(); + let l = + FriFoldLayout::for_options(lde_log, blowup.trailing_zeros(), &o, true).unwrap(); + check_shape_simple(&proof, lde_log, &l); + if l.total_folds > 0 { + // The input tree is layer 0: one more committed layer + // than the row-pair chain has under the pair schedule. + assert_eq!( + l.schedule.iter().map(|&d| u32::from(d)).sum::(), + l.total_folds + ); + } + } + } + } +} + +#[test] +fn one_row_round_trips_under_explicit_schedules() { + // rows 2^9, blowup 4, k 1: lde_log 11, chain from 11 to T = 3: 8 bits. + for sched in [ + &[1u8, 3, 4][..], + &[3, 1, 3, 1], + &[2, 1, 2, 2, 1], + &[1, 1, 1, 1, 1, 1, 1, 1], + &[6, 2], + &[1, 6, 1], + &[4, 4], + ] { + let o = golden_options(4, 1, 9, fmt(OneRowMode::On, FriMode::Dp, Some(sched))); + let (air, proof) = prove_simple_addition::(512, &o); + assert!( + verify_simple_addition::(&air, &proof), + "{sched:?}" + ); + assert_eq!(proof.fri_layers_merkle_roots.len(), sched.len()); + assert_eq!( + proof.query_list[0].layers_evaluations_sym.len(), + sched.iter().map(|&d| 1usize << d).sum::() + ); + } + // An override that fits the row-pair chain (7 bits) but not the one-row + // chain (8 bits) is a proving error under one row, never a fallback. + let o = golden_options(4, 1, 9, fmt(OneRowMode::On, FriMode::Dp, Some(&[3, 4]))); + let air = crate::examples::simple_addition::SimpleAdditionAIR::::new(&o); + let mut trace = crate::examples::simple_addition::simple_addition_trace::(512); + let pi = crate::examples::simple_addition::SimpleAdditionPublicInputs { + a: Felt::from(1u64), + b: Felt::from(2u64), + }; + assert!( + crate::prover::GenericProver::::prove( + &air, + &mut trace, + &pi, + &mut DefaultTranscript::::new(&[]), + ) + .is_err() + ); +} + +#[test] +fn one_row_round_trips_ext3_aux_and_multi_table() { + for (rows, blowup) in [(4usize, 2u8), (16, 2), (128, 4), (512, 2)] { + for format in [on(FriMode::Pair), on(FriMode::Dp)] { + let o = golden_options(blowup, 1, 7, format); + let (air, proof, _) = prove_logup::(rows, &o); + assert!(verify_logup::(&air, &proof), "rows {rows}"); + let lde_log = rows.trailing_zeros() + blowup.trailing_zeros(); + for dec in &proof.deep_poly_openings { + let aux = dec.aux_trace_polys.as_ref().expect("aux opening"); + assert!(aux.evaluations_sym.is_empty()); + assert_eq!(aux.proof.merkle_path.len(), lde_log as usize); + } + let (air, proof, _) = prove_logup::(rows, &o); + assert!( + verify_logup::(&air, &proof), + "keccak rows {rows}" + ); + } + } + for format in [ + on(FriMode::Pair), + on(FriMode::Dp), + fmt(OneRowMode::Auto, FriMode::Dp, None), + ] { + let o = golden_options(2, 1, 6, format); + let multi = prove_multi::(&o); + assert!(verify_multi::(&o, &multi), "{format:?}"); + } +} + +/// The archived (rkyv, read-in-place) verifier path verifies a one-row proof +/// too: the proof structs did not change, only the encoding of their vectors. +#[test] +fn one_row_verifies_archived() { + let o = golden_options(4, 2, 5, on(FriMode::Dp)); + let (air, proof) = prove_simple_addition::(256, &o); + let multi = MultiProof { + proofs: vec![proof.clone()], + }; + let bytes = rkyv::to_bytes::(&multi).unwrap(); + type Pi = crate::examples::simple_addition::SimpleAdditionPublicInputs; + let archived = rkyv::access::< + crate::proof::stark::ArchivedMultiProof, + rkyv::rancor::Error, + >(&bytes) + .unwrap(); + let airs: Vec<&dyn AIR> = vec![&air]; + assert!( + crate::verifier::GenericVerifier::::multi_verify_archived( + &airs, + archived, + &mut DefaultTranscript::::new(&[]), + &Felt::zero(), + ) + ); +} + +/// The layout is a verifier-side constant: a one-row proof does not verify +/// under row-pair options, nor a row-pair proof under one-row options. +#[test] +fn the_layout_is_a_verifier_constant() { + let one = golden_options(4, 1, 9, on(FriMode::Pair)); + let pair = golden_options(4, 1, 9, ProofFormat::DEFAULT); + let (one_air, one_proof) = prove_simple_addition::(1024, &one); + let (pair_air, pair_proof) = prove_simple_addition::(1024, &pair); + assert!(verify_simple_addition::( + &one_air, &one_proof + )); + assert!(verify_simple_addition::( + &pair_air, + &pair_proof + )); + assert!(!verify_simple_addition::( + &pair_air, &one_proof + )); + assert!(!verify_simple_addition::( + &one_air, + &pair_proof + )); +} + +// --------------------------------------------------------------------------- +// T4–T6: tamper tests on a one-row proof. +// --------------------------------------------------------------------------- + +#[test] +fn tampering_a_one_row_proof_is_rejected() { + let o = golden_options(4, 1, 5, fmt(OneRowMode::On, FriMode::Dp, Some(&[3, 2, 3]))); + let (air, honest, _) = prove_logup::(512, &o); + assert!(verify_logup::(&air, &honest)); + let bump = Ext::new([Felt::one(), Felt::zero(), Felt::zero()]); + let values = honest.query_list[0].layers_evaluations_sym.len(); + assert_eq!(values, 8 + 4 + 8); + + // T4: every value of query 0's input group (layer 0), the slot included — + // the input-slot check `group₀[slot] == DEEP(x_r)` and the group hash. + for i in 0..8 { + let mut p = honest.clone(); + p.query_list[0].layers_evaluations_sym[i] += bump; + assert!( + !verify_logup::(&air, &p), + "input group value {i}" + ); + } + // The input tree's root and a sibling of its path. + let mut p = honest.clone(); + p.fri_layers_merkle_roots[0][3] ^= 1; + assert!(!verify_logup::(&air, &p), "input root"); + let mut p = honest.clone(); + p.query_list[0].layers_auth_paths[0].merkle_path[0][0] ^= 1; + assert!(!verify_logup::(&air, &p), "input path"); + + // T5: a non-empty `evaluations_sym` under one row, for each tree — even + // one holding the honest value of the symmetric row. + let mut p = honest.clone(); + p.deep_poly_openings[0].main_trace_polys.evaluations_sym = + p.deep_poly_openings[0].main_trace_polys.evaluations.clone(); + assert!(!verify_logup::(&air, &p), "main sym"); + let mut p = honest.clone(); + p.deep_poly_openings[0].composition_poly.evaluations_sym = + p.deep_poly_openings[0].composition_poly.evaluations.clone(); + assert!( + !verify_logup::(&air, &p), + "composition sym" + ); + let mut p = honest.clone(); + let aux = p.deep_poly_openings[1].aux_trace_polys.as_mut().unwrap(); + aux.evaluations_sym = aux.evaluations.clone(); + assert!(!verify_logup::(&air, &p), "aux sym"); + + // T6: a trace value, an aux value and a composition value of one opening + // (each moves DEEP(x_r) and the leaf hash). + let mut p = honest.clone(); + p.deep_poly_openings[2].main_trace_polys.evaluations[0] += Felt::one(); + assert!(!verify_logup::(&air, &p), "main value"); + let mut p = honest.clone(); + p.deep_poly_openings[2] + .aux_trace_polys + .as_mut() + .unwrap() + .evaluations[0] += bump; + assert!(!verify_logup::(&air, &p), "aux value"); + let mut p = honest.clone(); + p.deep_poly_openings[2].composition_poly.evaluations[0] += bump; + assert!( + !verify_logup::(&air, &p), + "composition value" + ); + // A trace path one level short (the row-pair depth) and one long. + let mut p = honest.clone(); + p.deep_poly_openings[0] + .main_trace_polys + .proof + .merkle_path + .pop(); + assert!( + !verify_logup::(&air, &p), + "short trace path" + ); + let mut p = honest.clone(); + p.deep_poly_openings[0] + .main_trace_polys + .proof + .merkle_path + .push([0u8; 32]); + assert!( + !verify_logup::(&air, &p), + "long trace path" + ); + // The flat group vector one short / one long. + let mut p = honest.clone(); + p.query_list[0].layers_evaluations_sym.pop(); + assert!(!verify_logup::(&air, &p)); + let mut p = honest.clone(); + p.query_list[0].layers_evaluations_sym.push(Ext::zero()); + assert!(!verify_logup::(&air, &p)); + // A missing input layer. + let mut p = honest.clone(); + p.fri_layers_merkle_roots.remove(0); + assert!(!verify_logup::(&air, &p)); +} + +/// Zero folds under one row (`B ≤ T`): no layer, no challenge; the terminal +/// codeword IS the DEEP codeword and `terminal[r] == DEEP(x_r)` is the check. +#[test] +fn one_row_zero_fold_case() { + // rows 4, blowup 2, k 2: T = min(1 + 2, 3) = 3 = lde_log → no fold. + let o = golden_options(2, 2, 5, on(FriMode::Pair)); + let (air, proof) = prove_simple_addition::(4, &o); + assert!(verify_simple_addition::(&air, &proof)); + assert!(proof.fri_layers_merkle_roots.is_empty()); + assert_eq!(proof.fri_final_poly_coeffs.len(), 4); + let mut p = proof.clone(); + p.fri_final_poly_coeffs[1] += Felt::one(); + assert!(!verify_simple_addition::(&air, &p)); + let mut p = proof.clone(); + p.deep_poly_openings[0].main_trace_polys.evaluations[1] += Felt::one(); + assert!(!verify_simple_addition::(&air, &p)); +} + +// --------------------------------------------------------------------------- +// Soundness: r is uniform over ALL of D₀. M3 shows the test that says so +// is load-bearing. +// --------------------------------------------------------------------------- + +/// The query indexes the verifier draws for a one-row SimpleAddition proof of +/// `rows` rows at blowup 2 with `queries` queries (and whether it verifies). +fn one_row_iotas(rows: usize, queries: usize) -> (Vec, bool) { + let o = golden_options(2, 1, queries, on(FriMode::Pair)); + let (air, proof) = prove_simple_addition::(rows, &o); + let (ok, records) = capture(|| verify_simple_addition::(&air, &proof)); + let rec = FriCapture::::from_any(records[0].as_ref()).expect("one record"); + (rec.iotas.clone(), ok) +} + +/// The M3 shape: 4096 rows at blowup 2, an LDE of 8192 points no other +/// one-row test proves at (the mutation is keyed by it), and 64 queries: all +/// 64 indexes below `N / 2` has probability 2⁻⁶⁴ under the right bound; the +/// pair bound makes it certain. +const M3_ROWS: usize = 4096; +const M3_LDE: usize = 2 * M3_ROWS; + +fn upper_half_reached(iotas: &[usize], lde: usize) -> bool { + iotas.iter().any(|&r| r >= lde / 2) && iotas.iter().all(|&r| r < lde) +} + +#[test] +fn one_row_query_indexes_cover_the_whole_lde() { + let _g = M3_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let (iotas, ok) = one_row_iotas(M3_ROWS, 64); + assert!(ok); + assert!(upper_half_reached(&iotas, M3_LDE), "iotas {iotas:?}"); +} + +/// M3: sample r over N/2 under one row. Prover and verifier agree on the +/// mutated bound, so the proof still VERIFIES — the bias is invisible to +/// verification, and only the bound test catches it. +#[test] +fn m3_the_query_bound_test_is_load_bearing() { + let _g = M3_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + M3_PAIR_BOUND_AT_LDE.store(M3_LDE as u64, std::sync::atomic::Ordering::SeqCst); + let (iotas, ok) = one_row_iotas(M3_ROWS, 64); + M3_PAIR_BOUND_AT_LDE.store(0, std::sync::atomic::Ordering::SeqCst); + assert!(ok, "the mutated proof still verifies (both sides mutated)"); + assert!( + !upper_half_reached(&iotas, M3_LDE), + "under the mutation the bound test must fail" + ); +} + +// --------------------------------------------------------------------------- +// Soundness: the input root is absorbed before ζ₀ (transcript KAT). +// --------------------------------------------------------------------------- + +#[test] +fn input_root_is_absorbed_before_the_first_challenge() { + use crate::fri::group::roots_of_unity_table; + let o = Felt::from(3u64); + let lde_log = 10u32; + let n = 1usize << lde_log; + // A low-degree ext3 codeword (256 coefficients, blowup 4), bit-reversed. + let coeffs: Vec = (0..256u64) + .map(|i| Ext::new([Felt::from(i + 1), Felt::from(3 * i), Felt::from(7)])) + .collect(); + let poly = math::polynomial::Polynomial::new(&coeffs); + let mut cw = + math::polynomial::Polynomial::evaluate_offset_fft::(&poly, 4, Some(256), &o).unwrap(); + math::fft::bit_reversing::in_place_bit_reverse_permute(&mut cw); + // One row, schedule [2, 3, 3] from 10 to T = 2 + 0 = 2. + let layout = FriFoldLayout::from_schedule(lde_log, 2, 0, true, vec![2, 3, 3]).unwrap(); + let tw = compute_coset_twiddles_inv::(&o, n); + let mut t = DefaultTranscript::::new(&[9]); + let (_coeffs, layers) = commit_phase_with_layout::( + cw.clone(), + &mut t, + &o, + n, + 2, + 0, + &layout, + &tw, + ); + assert_eq!(layers.len(), 3); + assert_eq!( + layers[0].evaluation, cw, + "layer 0 is the DEEP codeword itself" + ); + + // The right order: root₀, then ζ₀. Folding layer 0 with that ζ₀ gives + // exactly the committed layer 1. + let mut right = DefaultTranscript::::new(&[9]); + right.append_bytes(&layers[0].merkle_tree.root); + let zeta0 = right.sample_field_element(); + let mut folded = cw.clone(); + let mut tw2 = tw.clone(); + fold_times(&mut folded, &zeta0, 2, &mut tw2); + assert_eq!(folded, layers[1].evaluation, "ζ₀ was drawn after root₀"); + + // The wrong order (ζ₀ before root₀) gives another challenge and another + // layer 1. + let mut wrong = DefaultTranscript::::new(&[9]); + let zeta_wrong = wrong.sample_field_element(); + assert_ne!(zeta_wrong, zeta0); + let mut folded = cw.clone(); + let mut tw2 = tw.clone(); + fold_times(&mut folded, &zeta_wrong, 2, &mut tw2); + assert_ne!(folded, layers[1].evaluation); + let _ = roots_of_unity_table::(1); +} + +/// M1 at the input tree: the input-slot check `group₀[slot] == DEEP(x_r)` is +/// what ties FRI to the trace openings under one row. A prover commits (and +/// folds) the input codeword `p₀ + c` — still low degree, so every layer and +/// the terminal are consistent — while DEEP(x_r) from the openings is `p₀`. +/// With the check the forgery is rejected; with it skipped (the mutation) it +/// is ACCEPTED. +#[test] +fn m1_the_input_slot_check_is_load_bearing() { + use crate::fri::group::{ + GROUP_MUTATION, GroupMutation, roots_of_unity_table, verify_query_groups, + }; + use crate::fri::query_phase_with_layout; + use crate::fri::terminal::terminal_codeword_from_coeffs; + use crate::merkle_caps::TreeCheck; + use math::fft::bit_reversing::{in_place_bit_reverse_permute, reverse_index}; + type H = KeccakStarkHash; + + let o = Felt::from(3u64); + let lde_log = 10u32; + let n = 1usize << lde_log; + let coeffs: Vec = (0..256u64) + .map(|i| Ext::new([Felt::from(i + 5), Felt::from(i * i), Felt::from(11)])) + .collect(); + let poly = math::polynomial::Polynomial::new(&coeffs); + let mut p0 = + math::polynomial::Polynomial::evaluate_offset_fft::(&poly, 4, Some(256), &o).unwrap(); + in_place_bit_reverse_permute(&mut p0); + let c = Ext::new([Felt::from(5u64), Felt::from(6u64), Felt::from(7u64)]); + let shifted: Vec = p0.iter().map(|v| v + &c).collect(); + + // One row, schedule [3, 2, 3] from 10 to T = 2 + 0. + let layout = FriFoldLayout::from_schedule(lde_log, 2, 0, true, vec![3, 2, 3]).unwrap(); + let tw = compute_coset_twiddles_inv::(&o, n); + let mut t = DefaultTranscript::::new(&[5]); + let (tcoeffs, layers) = + commit_phase_with_layout::(shifted, &mut t, &o, n, 2, 0, &layout, &tw); + let roots: Vec<[u8; 32]> = layers.iter().map(|l| l.merkle_tree.root).collect(); + // Replay: root₀ first, then (ζ, root) per later layer, then the final ζ. + let mut replay = DefaultTranscript::::new(&[5]); + let mut zetas = Vec::new(); + for (j, r) in roots.iter().enumerate() { + if j > 0 { + zetas.push(replay.sample_field_element()); + } + replay.append_bytes(r); + } + zetas.push(replay.sample_field_element()); + assert_eq!(zetas.len(), layout.num_zetas()); + let queries: Vec = (0..n).step_by(53).collect(); + let decs = query_phase_with_layout::(&layers, &queries, &layout); + let terminal = terminal_codeword_from_coeffs::( + &tcoeffs, + &o.pow(1u64 << layout.total_folds), + layout.terminal_len, + ); + let tables: Vec> = (0..=6) + .map(|d| roots_of_unity_table::(d).unwrap()) + .collect(); + let checks: Vec> = roots + .iter() + .enumerate() + .map(|(j, root)| { + TreeCheck::build::<::Batched>( + root, + layout.layer_depth(lde_log, j) as usize, + 0, + || None, + ) + .unwrap() + }) + .collect(); + let accepts = |deep: &[Ext]| { + queries.iter().zip(&decs).all(|(&r, dec)| { + let w = F::get_primitive_root_of_unity(u64::from(lde_log)).unwrap(); + let x_r = &o * w.pow(reverse_index(r, n as u64) as u64); + verify_query_groups::::Batched>( + &layout, + &checks, + 1, + |j| dec.layers_auth_paths[j].merkle_path.as_slice(), + &dec.layers_evaluations_sym, + &zetas, + r, + deep[r], + x_r.inv().unwrap(), + &terminal, + &tables, + ) + }) + }; + let shifted_again: Vec = p0.iter().map(|v| v + &c).collect(); + assert!(accepts(&shifted_again), "control: honest for p0 + c"); + assert!(!accepts(&p0), "the input-slot check must reject"); + GROUP_MUTATION.with(|m| m.set(GroupMutation::SkipSlotCheck)); + let mutated = accepts(&p0); + GROUP_MUTATION.with(|m| m.set(GroupMutation::None)); + assert!( + mutated, + "without the input-slot check the forgery is accepted (the check is load-bearing)" + ); +} + +// --------------------------------------------------------------------------- +// Preprocessed tables: one-row roots, and a miss is an error (never a recompute). +// --------------------------------------------------------------------------- + +#[test] +fn one_row_preprocessed_table_and_a_missing_root() { + let opts = golden_options(2, 1, 5, on(FriMode::Pair)); + let mut trace = compute_trace([Felt::one(), Felt::one()], 256); + let reference = FibonacciSplitAIR::::honest(&opts, None); + let pair_root = Prover::compute_precomputed_commitment_for_testing(&trace, &reference, 1) + .expect("row-pair root"); + let row_root = Prover::compute_precomputed_commitment_for_testing_with( + &trace, + &reference, + 1, + LeafLayout::Row, + ) + .expect("one-row root"); + assert_ne!( + pair_root, row_root, + "the two layouts commit different bytes" + ); + let pi = FibonacciPublicInputs { + a0: Felt::one(), + a1: Felt::one(), + }; + + // With both roots: proves and verifies, and the proof carries the ROW root. + let air = FibonacciSplitAIR::::preprocessed_declaring(&opts, None, 1, pair_root) + .with_one_row_commitment(row_root); + let proof = + Prover::prove(&air, &mut trace, &pi, &mut DefaultTranscript::::new(&[])).expect("prove"); + assert_eq!(proof.lde_trace_precomputed_merkle_root, Some(row_root)); + assert!(Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]) + )); + + // The same AIR without a one-row root: the prover refuses with an Err + // (no panic, no recompute) and the verifier rejects the honest proof. + let bare = FibonacciSplitAIR::::preprocessed_declaring(&opts, None, 1, pair_root); + let mut trace2 = compute_trace([Felt::one(), Felt::one()], 256); + match Prover::prove( + &bare, + &mut trace2, + &pi, + &mut DefaultTranscript::::new(&[]), + ) { + Err(ProvingError::PrecomputedCommitmentMissing(_)) => {} + other => panic!( + "expected PrecomputedCommitmentMissing, got {:?}", + other.map(|_| ()) + ), + } + assert!(!Verifier::verify( + &proof, + &bare, + &mut DefaultTranscript::::new(&[]) + )); + + // A wrong one-row root: the prover's rebuilt tree disagrees. + let wrong = FibonacciSplitAIR::::preprocessed_declaring(&opts, None, 1, pair_root) + .with_one_row_commitment(pair_root); + let mut trace3 = compute_trace([Felt::one(), Felt::one()], 256); + assert!(matches!( + Prover::prove( + &wrong, + &mut trace3, + &pi, + &mut DefaultTranscript::::new(&[]) + ), + Err(ProvingError::PrecomputedCommitmentMismatch) + )); +} + +// --------------------------------------------------------------------------- +// The per-table `auto` rule. +// --------------------------------------------------------------------------- + +fn opts_q(q: usize, one_row: OneRowMode, fri: FriMode, cap: CapPolicy) -> ProofOptions { + let mut o = golden_options(4, 7, q, fmt(one_row, fri, None)); + o.format.merkle_cap = cap; + o +} + +/// The rule is the cost comparison, strictly: one row iff cheaper. +#[test] +fn auto_is_the_strict_cost_comparison() { + for fri in [FriMode::Pair, FriMode::Dp] { + for cap in [CapPolicy::Off, CapPolicy::Auto] { + let o = opts_q(110, OneRowMode::Auto, fri, cap); + for lde_log in 4..=24u32 { + for main in [1u64, 4, 8, 30, 120, 400] { + for aux in [0u64, 3, 30, 120] { + let w = TableWidths { + precomputed: 0, + main, + aux, + composition: 6, + deep_point_rows: deep_point_xalu_rows(main + 2 * aux, 2, 2), + }; + let row = table_openings_cost_q(&w, &o, lde_log, 2, true); + let pair = table_openings_cost_q(&w, &o, lde_log, 2, false); + assert_eq!( + resolve_leaf_layout(&w, &o, lde_log, 2), + LeafLayout::from_one_row(row < pair), + "fri {fri:?} cap {cap:?} B {lde_log} main {main} aux {aux}" + ); + } + } + } + } + } + // Off and On ignore the costs. + let w = TableWidths { + precomputed: 0, + main: 1, + aux: 0, + composition: 3, + deep_point_rows: deep_point_xalu_rows(1, 1, 1), + }; + for lde_log in 4..=24 { + let off = opts_q(110, OneRowMode::Off, FriMode::Pair, CapPolicy::Off); + let on = opts_q(110, OneRowMode::On, FriMode::Pair, CapPolicy::Off); + assert_eq!( + resolve_leaf_layout(&w, &off, lde_log, 2), + LeafLayout::RowPair + ); + assert_eq!(resolve_leaf_layout(&w, &on, lde_log, 2), LeafLayout::Row); + } +} + +/// The DEEP term of the pinned cases: `E = 2` OOD rows (a current and a next +/// row; every case has aux columns, whose LogUp accumulators read the next +/// row), every column opened at the current row and the aux columns at the +/// next (an ASSUMED window: the real one is each AIR's +/// `trace_ood_next_row_columns`), `parts` composition parts. +fn pinned_deep_rows(pre: u64, main: u64, aux: u64, parts: u64) -> u64 { + deep_point_xalu_rows(pre + main + aux + aux, 2, parts) +} + +/// ⚠ A FORMAT PIN: `auto`'s choice for a set of production-like shapes (Q = +/// 110, blowup 4, k = 7, cap auto, fri dp). Wide tables go one-row, narrow +/// tall ones stay row pairs. Any change to the cost function or its weights +/// that moves one of these is a format change. The widths are illustrative +/// (MEMW 49 main / 13 aux; the others are round +/// numbers), not a census. With every emitted FRI row priced and DEEP at two +/// points vs one, two choices are one row only because of the DEEP term: the +/// MEMW-like case (one row by 5.5%, see `auto_choices_margins`) and the narrow short +/// preprocessed one (its LDE is already terminal, so no FRI layer separates +/// the layouts and the second DEEP point decides). +#[test] +fn auto_choices_are_pinned() { + let o = opts_q(110, OneRowMode::Auto, FriMode::Dp, CapPolicy::Auto); + // (name, B, precomputed, main, aux ext columns, composition parts, one row?) + let cases: &[(&str, u32, u64, u64, u64, u64, bool)] = &[ + ("wide keccak-like", 16, 0, 2600, 40, 2, true), + ("wide, short", 12, 0, 400, 20, 2, true), + ("narrow tall, preprocessed", 22, 12, 4, 2, 2, false), + ("narrow short, preprocessed", 7, 8, 1, 1, 2, true), + ("memw-like", 21, 0, 49, 13, 2, MEMW_LIKE_ONE_ROW), + ("cpu-like", 21, 0, 74, 20, 2, true), + ]; + let mut got = Vec::new(); + for &(name, b, pre, main, aux, parts, _) in cases { + got.push(( + name, + resolve_leaf_layout(&pinned_widths(pre, main, aux, parts), &o, b, 2).is_one_row(), + )); + } + let want: Vec<_> = cases.iter().map(|c| (c.0, c.6)).collect(); + assert_eq!(got, want); +} + +/// The MEMW-like case's pinned choice (see `auto_choices_are_pinned`). +const MEMW_LIKE_ONE_ROW: bool = true; + +fn pinned_widths(pre: u64, main: u64, aux: u64, parts: u64) -> TableWidths { + TableWidths { + precomputed: pre, + main, + aux: aux * 3, + composition: parts * 3, + deep_point_rows: pinned_deep_rows(pre, main, aux, parts), + } +} + +/// Prints each pinned case's two prices (`-- --nocapture`), and pins how much +/// of the one-row saving the DEEP term is at the two edge cases. +#[test] +fn auto_choices_margins() { + let o = opts_q(110, OneRowMode::Auto, FriMode::Dp, CapPolicy::Auto); + for (name, pre, main, aux, parts) in [ + ("memw-like", 0u64, 49u64, 13u64, 2u64), + ("cpu-like", 0, 74, 20, 2), + ] { + let w = pinned_widths(pre, main, aux, parts); + let row = table_openings_cost_q(&w, &o, 21, 2, true); + let pair = table_openings_cost_q(&w, &o, 21, 2, false); + let deep_point = 110 * w.deep_point_rows * FRI_COST_WEIGHTS.xalu; + println!( + " {name}: row {row} pair {pair} (x Q ns; one DEEP point {deep_point}; row/pair {:.4})", + row as f64 / pair as f64 + ); + assert!(row < pair, "{name} goes one row"); + // Without the DEEP point one row saves, the MEMW-like case would stay + // row pairs: the term decides it. + if name == "memw-like" { + assert!(row + deep_point >= pair, "{name}: the DEEP term decides"); + } + } +} + +/// DEEP costs two points under row pairs and one under one row, +/// each [`TableWidths::deep_point_rows`] XALU rows per query — and nothing +/// else in the price depends on it. +#[test] +fn the_deep_term_is_two_points_vs_one() { + for fri in [FriMode::Pair, FriMode::Dp] { + for cap in [CapPolicy::Off, CapPolicy::Auto] { + let o = opts_q(110, OneRowMode::Auto, fri, cap); + let base = pinned_widths(0, 49, 13, 2); + let none = TableWidths { + deep_point_rows: 0, + ..base + }; + let point = 110 * base.deep_point_rows * FRI_COST_WEIGHTS.xalu; + for lde_log in 8..=22u32 { + for (one_row, points) in [(true, 1u64), (false, 2)] { + assert_eq!( + table_openings_cost_q(&base, &o, lde_log, 2, one_row), + table_openings_cost_q(&none, &o, lde_log, 2, one_row) + points * point, + "fri {fri:?} cap {cap:?} B {lde_log} one_row {one_row}" + ); + } + } + } + } + // The formula: one XALU row per surviving opening, plus 4 per OOD row, + // one per part and 3. + assert_eq!(deep_point_xalu_rows(100, 2, 2), 100 + 8 + 2 + 3); +} + +/// `auto` resolves per table from the AIR, and the prover and the verifier +/// resolve identically (one function, `table_leaf_layout`); a multi-table +/// proof can mix layouts. +#[test] +fn auto_resolves_per_table_from_the_air() { + let o = golden_options(2, 1, 6, fmt(OneRowMode::Auto, FriMode::Dp, None)); + let air = crate::examples::simple_addition::SimpleAdditionAIR::::new(&o); + for log_rows in 1..=20 { + let rows = 1usize << log_rows; + let w = TableWidths::of(&air, rows); + assert_eq!(w.main, air.trace_layout().0 as u64); + assert_eq!( + table_leaf_layout(&air, rows), + resolve_leaf_layout(&w, &o, log_rows + 1, 1) + ); + } + // At the default format every AIR is row pairs, whatever its widths. + let d = golden_options(2, 1, 6, ProofFormat::DEFAULT); + let air = crate::examples::simple_addition::SimpleAdditionAIR::::new(&d); + assert_eq!(table_leaf_layout(&air, 1 << 20), LeafLayout::RowPair); +} + +// --------------------------------------------------------------------------- +// The format matrix: {cap off, auto} × {pair, dp} × {0, 1, auto}, Q ≥ 20. +// --------------------------------------------------------------------------- + +#[test] +fn cap_fri_one_row_matrix_round_trips() { + for cap in [CapPolicy::Off, CapPolicy::Auto] { + for fri in [FriMode::Pair, FriMode::Dp] { + for one_row in [OneRowMode::Off, OneRowMode::On, OneRowMode::Auto] { + let mut o = golden_options(4, 1, 24, fmt(one_row, fri, None)); + o.format.merkle_cap = cap; + let (air, proof, _) = prove_logup::(256, &o); + assert!( + verify_logup::(&air, &proof), + "cap {cap:?} fri {fri:?} one_row {one_row:?}" + ); + let (air, proof) = prove_simple_addition::(1024, &o); + assert!( + verify_simple_addition::(&air, &proof), + "simple cap {cap:?} fri {fri:?} one_row {one_row:?}" + ); + if cap == CapPolicy::Auto { + // Q = 24 ≥ 20: every tree deeper than 3 carries a cap of 3 + // at the end of query 0's path. + let lde_log = 12usize; + let layout = table_leaf_layout(&air, 1024); + let d = layout.tree_depth(lde_log); + assert_eq!( + proof.deep_poly_openings[0] + .main_trace_polys + .proof + .merkle_path + .len(), + d - 3 + 8 + ); + assert_eq!( + proof.deep_poly_openings[1] + .main_trace_polys + .proof + .merkle_path + .len(), + d - 3 + ); + } + } + } + } +} + +/// The FRI layout the verifier builds for a table resolves the SAME layout the +/// prover used, for the base-field and the extension-field AIRs alike. +#[test] +fn widths_of_an_extension_air() { + let o = golden_options(2, 1, 6, on(FriMode::Pair)); + let air = crate::examples::read_only_memory_logup::LogReadOnlyRAP::::new(&o); + let w = TableWidths::of(&air, 64); + assert_eq!(w.aux, 3 * air.num_auxiliary_rap_columns() as u64); + assert_eq!(w.main, air.trace_layout().0 as u64); + assert!(w.composition.is_multiple_of(3) && w.composition > 0); + // The DEEP term from the AIR's own OOD layout (the verifier's reading). + let e = air.context().transition_offsets.len() * air.step_size(); + let ood = crate::ood::OodLayout::new( + air.context().trace_columns, + e, + air.step_size(), + air.trace_ood_next_row_columns(), + ); + assert_eq!( + w.deep_point_rows, + deep_point_xalu_rows(ood.num_surviving() as u64, e as u64, w.composition / 3) + ); + assert!(w.deep_point_rows > ood.num_surviving() as u64); + let _ = ::TWO_ADICITY; +} diff --git a/crypto/stark/src/tests/opening_width_tests.rs b/crypto/stark/src/tests/opening_width_tests.rs index db5764220..8d86da29a 100644 --- a/crypto/stark/src/tests/opening_width_tests.rs +++ b/crypto/stark/src/tests/opening_width_tests.rs @@ -71,12 +71,15 @@ pub struct FibonacciSplitAIR { out: Option>, precomputed_columns: usize, precomputed_commitment: Commitment, + /// The one-row (S2) root of the same precomputed columns; `None` = the + /// AIR has none (the one-row prover must refuse, never recompute). + precomputed_commitment_row: Option, phantom: PhantomData, } impl FibonacciSplitAIR { /// The AIR as the verifier sees it: plain, non-preprocessed. - fn honest(proof_options: &ProofOptions, out: Option>) -> Self { + pub(crate) fn honest(proof_options: &ProofOptions, out: Option>) -> Self { let mut air = ::new(proof_options); air.out = out; air @@ -96,7 +99,7 @@ impl FibonacciSplitAIR { /// Handing the verifier a different count than the prover used is how the /// hook-free test below reaches the precomputed term of the guard: both /// sides still absorb the same commitment, so the transcripts agree. - fn preprocessed_declaring( + pub(crate) fn preprocessed_declaring( proof_options: &ProofOptions, out: Option>, precomputed_columns: usize, @@ -107,6 +110,12 @@ impl FibonacciSplitAIR { air.precomputed_commitment = commitment; air } + + /// This AIR with a one-row (S2) precomputed root as well. + pub(crate) fn with_one_row_commitment(mut self, commitment: Commitment) -> Self { + self.precomputed_commitment_row = Some(commitment); + self + } } impl AIR for FibonacciSplitAIR @@ -135,6 +144,7 @@ where out: None, precomputed_columns: 0, precomputed_commitment: [0u8; 32], + precomputed_commitment_row: None, phantom: PhantomData, } } @@ -213,6 +223,16 @@ where fn precomputed_commitment(&self) -> Commitment { self.precomputed_commitment } + + fn precomputed_commitment_for( + &self, + layout: crate::leaf_layout::LeafLayout, + ) -> Option { + match layout { + crate::leaf_layout::LeafLayout::RowPair => Some(self.precomputed_commitment), + crate::leaf_layout::LeafLayout::Row => self.precomputed_commitment_row, + } + } } fn pub_inputs() -> FibonacciPublicInputs { diff --git a/crypto/stark/src/tests/path_length_tests.rs b/crypto/stark/src/tests/path_length_tests.rs new file mode 100644 index 000000000..7634d3bff --- /dev/null +++ b/crypto/stark/src/tests/path_length_tests.rs @@ -0,0 +1,142 @@ +//! Exact authentication-path lengths at the default format. +//! +//! Every tree's depth is a verifier constant: `log2(lde) − 1` for the trace, +//! precomputed, aux and composition trees (a leaf is a row pair), and +//! `log2(lde) − i − 2` for committed FRI layer `i` (pair leaves over +//! `lde / 2^(i+1)` values). The verifier requires the exact length, so a leaf +//! hash is never compared with an internal node. These tests pin that honest proofs meet +//! the lengths exactly and that a path one node short or long is rejected, for +//! each tree class the verifier walks. + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use math::field::element::FieldElement; +use math::field::goldilocks::GoldilocksField; + +use crate::examples::simple_addition::{ + SimpleAdditionAIR, SimpleAdditionPublicInputs, simple_addition_trace, +}; +use crate::proof::options::ProofOptions; +use crate::proof::stark::StarkProof; +use crate::prover::{IsStarkProver, Prover}; +use crate::traits::AIR; +use crate::verifier::{IsStarkVerifier, Verifier}; + +type F = GoldilocksField; +type FE = FieldElement; +type PI = SimpleAdditionPublicInputs; + +/// 1024 rows at blowup 2: `lde = 2048`, so the trace trees are 10 deep and +/// FRI commits layers (final degree 2^7 < 1024). +const TRACE_ROWS: usize = 1024; +const LDE_LOG: usize = 11; + +fn prove() -> (SimpleAdditionAIR, StarkProof) { + let options = ProofOptions::default_test_options(); + let air = SimpleAdditionAIR::::new(&options); + let pub_inputs = SimpleAdditionPublicInputs { + a: FE::from(1u64), + b: FE::from(2u64), + }; + let mut trace = simple_addition_trace::(TRACE_ROWS); + let proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .expect("proving must succeed"); + (air, proof) +} + +fn verifies(air: &SimpleAdditionAIR, proof: &StarkProof) -> bool { + Verifier::verify(proof, air, &mut DefaultTranscript::::new(&[])) +} + +#[test] +fn honest_paths_have_exactly_the_verifier_depths_and_verify() { + let (air, proof) = prove(); + assert_eq!( + air.options().blowup_factor as usize * TRACE_ROWS, + 1 << LDE_LOG + ); + assert!( + !proof.fri_layers_merkle_roots.is_empty(), + "the trace must fold, or the FRI arm is vacuous" + ); + for opening in &proof.deep_poly_openings { + assert_eq!( + opening.main_trace_polys.proof.merkle_path.len(), + LDE_LOG - 1 + ); + assert_eq!( + opening.composition_poly.proof.merkle_path.len(), + LDE_LOG - 1 + ); + } + for query in &proof.query_list { + for (i, path) in query.layers_auth_paths.iter().enumerate() { + assert_eq!(path.merkle_path.len(), LDE_LOG - i - 2, "layer {i}"); + } + } + assert!(verifies(&air, &proof), "an honest proof must verify"); +} + +/// One node short, one node long: both rejected. +fn assert_both_lengths_rejected( + air: &SimpleAdditionAIR, + honest: &StarkProof, + what: &str, + path_of: impl Fn(&mut StarkProof) -> &mut Vec<[u8; 32]>, +) { + let mut short = honest.clone(); + let path = path_of(&mut short); + assert!(!path.is_empty(), "{what}: precondition, a non-empty path"); + path.pop(); + assert!( + !verifies(air, &short), + "{what}: a path one node short must be rejected" + ); + + let mut long = honest.clone(); + let path = path_of(&mut long); + let extra = path[0]; + path.push(extra); + assert!( + !verifies(air, &long), + "{what}: a path one node long must be rejected" + ); +} + +#[test] +fn a_main_trace_path_of_the_wrong_length_is_rejected() { + let (air, honest) = prove(); + assert_both_lengths_rejected(&air, &honest, "main, query 0", |p| { + &mut p.deep_poly_openings[0].main_trace_polys.proof.merkle_path + }); + let last = honest.deep_poly_openings.len() - 1; + assert_both_lengths_rejected(&air, &honest, "main, last query", move |p| { + &mut p.deep_poly_openings[last] + .main_trace_polys + .proof + .merkle_path + }); +} + +#[test] +fn a_composition_path_of_the_wrong_length_is_rejected() { + let (air, honest) = prove(); + assert_both_lengths_rejected(&air, &honest, "composition, query 0", |p| { + &mut p.deep_poly_openings[0].composition_poly.proof.merkle_path + }); +} + +#[test] +fn a_fri_layer_path_of_the_wrong_length_is_rejected() { + let (air, honest) = prove(); + let layers = honest.fri_layers_merkle_roots.len(); + for layer in [0, layers - 1] { + assert_both_lengths_rejected(&air, &honest, &format!("FRI layer {layer}"), move |p| { + &mut p.query_list[0].layers_auth_paths[layer].merkle_path + }); + } +} diff --git a/crypto/stark/src/tests/prover_tests.rs b/crypto/stark/src/tests/prover_tests.rs index 1fe37f8a2..4b5ebcc17 100644 --- a/crypto/stark/src/tests/prover_tests.rs +++ b/crypto/stark/src/tests/prover_tests.rs @@ -72,6 +72,7 @@ fn test_domain_constructor() { coset_offset, grinding_factor, fri_final_poly_log_degree: 7, + format: crate::proof::options::ProofFormat::DEFAULT, }; let domain = Domain::new( @@ -163,6 +164,7 @@ fn barycentric_trace_eval_matches_horner_trace_eval() { coset_offset, grinding_factor: 0, fri_final_poly_log_degree: 7, + format: crate::proof::options::ProofFormat::DEFAULT, }; let air = simple_fibonacci::FibonacciAIR::::new(&proof_options); @@ -235,6 +237,7 @@ fn test_decompose_and_extend_d2_matches_original() { coset_offset: 3, grinding_factor: 0, fri_final_poly_log_degree: 7, + format: crate::proof::options::ProofFormat::DEFAULT, }; // We need an AIR with composition_poly_degree_bound = 2 * trace_length. @@ -301,6 +304,7 @@ fn test_multi_prove_mixed_coset_offsets() { coset_offset: 3, grinding_factor: 1, fri_final_poly_log_degree: 7, + format: crate::proof::options::ProofFormat::DEFAULT, }; let proof_options_7 = ProofOptions { blowup_factor: 2, @@ -308,6 +312,7 @@ fn test_multi_prove_mixed_coset_offsets() { coset_offset: 7, grinding_factor: 1, fri_final_poly_log_degree: 7, + format: crate::proof::options::ProofFormat::DEFAULT, }; // Both AIRs have the same trace length and blowup, but different coset offsets. @@ -373,6 +378,7 @@ fn test_multi_prove_dedups_shared_domain_params() { coset_offset: 3, grinding_factor: 1, fri_final_poly_log_degree: 7, + format: crate::proof::options::ProofFormat::DEFAULT, }; let mut trace_1 = simple_fibonacci::fibonacci_trace([Felt::from(1), Felt::from(1)], 8); @@ -463,6 +469,7 @@ fn test_deep_poly_direct_2n_matches_interpolate_fft_extend() { coset_offset: 3, grinding_factor: 0, fri_final_poly_log_degree: 7, + format: crate::proof::options::ProofFormat::DEFAULT, }; let air = QuadraticAIR::::new(&proof_options); diff --git a/crypto/stark/src/tests/residency_mode_tests.rs b/crypto/stark/src/tests/residency_mode_tests.rs index 3d728b325..1c47ae062 100644 --- a/crypto/stark/src/tests/residency_mode_tests.rs +++ b/crypto/stark/src/tests/residency_mode_tests.rs @@ -34,7 +34,7 @@ type FE = FieldElement; /// The bus-balanced CPU/ADD/MUL instance from the completeness tests. Rebuilt /// per prove because `multi_prove` writes the LogUp aux columns into the caller's /// traces — and under `RecomputeLde` frees them again. -fn traces() -> (TraceTable, TraceTable, TraceTable) { +pub(super) fn traces() -> (TraceTable, TraceTable, TraceTable) { let cpu = TraceTable::from_columns_main( vec![ vec![ diff --git a/crypto/stark/src/tests/zf_fri_device_tests.rs b/crypto/stark/src/tests/zf_fri_device_tests.rs new file mode 100644 index 000000000..8dda1e81e --- /dev/null +++ b/crypto/stark/src/tests/zf_fri_device_tests.rs @@ -0,0 +1,167 @@ +//! S3 on the device: the device FRI commit and +//! query phases against the host CPU loop, under Keccak and Blake3 (the RPX +//! twins live in the prover crate's `tests::zf_rpx_device_tests`). +//! +//! Every `#[ignore]`d test here needs a GPU and a lowered +//! `LAMBDA_VM_GPU_LDE_THRESHOLD`; each one fails loudly when the device path +//! does not run (a declined commit is an `Err`, a vector proof must move the +//! device FRI counter), so none can pass by falling back to the host: +//! +//! ```text +//! LAMBDA_VM_GPU_LDE_THRESHOLD=2 cargo test -p stark --release --features cuda \ +//! --lib tests::zf_fri_device_tests::parity_ -- --ignored +//! LAMBDA_VM_GPU_LDE_THRESHOLD=1024 cargo test -p stark --release --features cuda \ +//! --lib tests::zf_fri_device_tests::proved_vectors_equal_the_cpu_bytes \ +//! -- --ignored --exact --test-threads=1 +//! ``` +//! +//! `dp_shapes_are_pinned` needs no GPU (it only computes the shape list). + +use crate::config::{Blake3StarkHash, KeccakStarkHash, StarkHash}; +use crate::fri::device_parity::{ + Case, dp_shapes, legacy_cases, production_cases, resident_cases, run_cases, sweep_cases, +}; + +/// The shapes the parity sweep covers: every distinct DP schedule for +/// `B ≤ 23` (T ∈ {4, 9, 10}, Q ∈ {3, 110}, cap off/auto) plus the extras. +/// Pinned so the box run's pre-registered count means something; a DP change +/// that moves this list is a format change and re-pins it deliberately. +#[test] +fn dp_shapes_are_pinned() { + let shapes = dp_shapes(); + let expected: &[&[u8]] = PINNED_SHAPES; + assert_eq!( + shapes, + expected.iter().map(|s| s.to_vec()).collect::>(), + "the DP's schedule set moved" + ); +} + +const PINNED_SHAPES: &[&[u8]] = &[ + // The DP's own (21, in first-appearance order). + &[1], + &[2], + &[3], + &[2, 2], + &[3, 2], + &[3, 3], + &[3, 2, 2], + &[3, 3, 2], + &[3, 3, 3], + &[3, 3, 2, 2], + &[3, 3, 3, 2], + &[3, 3, 3, 3], + &[3, 3, 3, 2, 2], + &[3, 3, 3, 3, 2], + &[3, 3, 3, 3, 3], + &[3, 3, 3, 3, 2, 2], + &[3, 3, 3, 3, 3, 2], + &[3, 3, 3, 3, 3, 3], + &[4, 3, 3], + &[4, 3, 3, 3], + &[4, 3], + // EXTRA_SHAPES (8). + &[4], + &[6], + &[1, 6], + &[6, 1], + &[3, 1, 3], + &[1, 3], + &[2, 5, 1], + &[1, 1, 1], +]; + +fn check(name: &str, cases: &[Case], resident: bool, seed: u64) { + if let Err(failures) = run_cases::(name, cases, resident, seed) { + panic!("{name}: {failures:#?}"); + } +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_every_dp_shape_keccak() { + check::("keccak", &sweep_cases(), false, 0x5a46_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_every_dp_shape_blake3() { + check::("blake3", &sweep_cases(), false, 0x5a46_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_production_sizes_keccak() { + check::("keccak", &production_cases(), false, 0x5a47_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_production_sizes_blake3() { + check::("blake3", &production_cases(), false, 0x5a47_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_resident_layers_keccak() { + check::("keccak", &resident_cases(), true, 0x5a48_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_resident_layers_blake3() { + check::("blake3", &resident_cases(), true, 0x5a48_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_legacy_encoding_keccak() { + check::("keccak", &legacy_cases(), false, 0x5a49_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_legacy_encoding_blake3() { + check::("blake3", &legacy_cases(), false, 0x5a49_0000); +} + +/// The (d) vector proofs (the README's (d): `pair`, `dp`, `dp_3_1_3`, and the +/// Merkle-capped `cap_pair`, `cap_dp` at Q = 20) proved on +/// the device path — LDE 4096, so `LAMBDA_VM_GPU_LDE_THRESHOLD` must be at +/// most 4096 — are byte-identical to the checked-in CPU-proved files (rkyv +/// bytes and the verifier-derived JSON), under Keccak and Blake3. The device +/// FRI counter must move once per proof, so a host fallback fails the test. +/// Run alone (`--exact --test-threads=1`): the counter is process-wide. +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD<=4096; run alone with --features cuda -- --ignored --exact --test-threads=1"] +fn proved_vectors_equal_the_cpu_bytes() { + use crate::fri::vectors::{check_or_write, proof_vectors}; + let before = crate::gpu_lde::gpu_fri_calls(); + let comp_before = crate::gpu_lde::gpu_composition_calls(); + let mut files = proof_vectors::("keccak"); + files.extend(proof_vectors::("blake3")); + let device_commits = crate::gpu_lde::gpu_fri_calls() - before; + let compositions = crate::gpu_lde::gpu_composition_calls() - comp_before; + println!( + "FRIDEV vector proofs: {} files, {device_commits} device FRI commits, {compositions} device compositions", + files.len() + ); + // Five (d) formats (pair, dp, dp_3_1_3 at Q = 3; cap_pair, cap_dp at + // Q = 20) x two hashes, two files and one FRI commit per proof. + assert_eq!(files.len(), 2 * 5 * 2); + assert_eq!( + device_commits, 10, + "every vector proof must take the device FRI commit (lower LAMBDA_VM_GPU_LDE_THRESHOLD)" + ); + // Every proof composes on the device (the AIR's constraint program); a + // host composition would not be counted here. + assert_eq!( + compositions, 10, + "every vector proof must compose on the device ({compositions} device compositions)" + ); + let bad = check_or_write(&files, false); + assert!( + bad.is_empty(), + "device-proved vectors differ from the checked-in CPU bytes: {bad:?}" + ); +} diff --git a/crypto/stark/src/tests/zf_fri_vectors.rs b/crypto/stark/src/tests/zf_fri_vectors.rs new file mode 100644 index 000000000..0561e63aa --- /dev/null +++ b/crypto/stark/src/tests/zf_fri_vectors.rs @@ -0,0 +1,46 @@ +//! The exported S3 and S2 vectors ((a)–(e) in the README) under Keccak and Blake3 are +//! current: regenerated in memory and byte-equal to the checked-in files in +//! `crypto/stark/tests/vectors/zf_fri/` (the RPX files: the prover crate's +//! `tests::zf_rpx_vectors`). Regenerate after a deliberate format change: +//! `cargo test -p stark --lib zf_fri_vectors::write_vectors -- --ignored`. + +use crate::config::{Blake3StarkHash, KeccakStarkHash}; +use crate::fri::vectors::{ + VectorFile, check_or_write, group_fold_json, leaf_digests_json, one_row_leaf_digests_json, + one_row_proof_vectors, proof_vectors, schedules_json, +}; + +fn all() -> Vec { + let mut v = vec![ + schedules_json(), + group_fold_json(), + leaf_digests_json::("keccak"), + leaf_digests_json::("blake3"), + ]; + v.extend(proof_vectors::("keccak")); + v.extend(proof_vectors::("blake3")); + // (e) S2. + v.push(one_row_leaf_digests_json::("keccak")); + v.push(one_row_leaf_digests_json::("blake3")); + v.extend(one_row_proof_vectors::("keccak")); + v.extend(one_row_proof_vectors::("blake3")); + v +} + +#[test] +fn vectors_are_current() { + let files = all(); + assert_eq!(files.len(), 4 + 2 * 5 * 2 + 2 + 2 * 2 * 2); + let bad = check_or_write(&files, false); + assert!( + bad.is_empty(), + "stale or missing vector files {bad:?}; regenerate with \ + `cargo test -p stark --lib zf_fri_vectors::write_vectors -- --ignored`" + ); +} + +#[test] +#[ignore = "writes crypto/stark/tests/vectors/zf_fri"] +fn write_vectors() { + assert!(check_or_write(&all(), true).is_empty()); +} diff --git a/crypto/stark/src/tests/zf_golden_tests.rs b/crypto/stark/src/tests/zf_golden_tests.rs new file mode 100644 index 000000000..97e9647d4 --- /dev/null +++ b/crypto/stark/src/tests/zf_golden_tests.rs @@ -0,0 +1,378 @@ +//! Default-format golden proofs: the bytes today's prover emits, +//! pinned, so a format lever that claims "the default is byte-identical" is +//! checked against the prover's own output rather than against a round trip +//! (a drifted prover still accepts its own proofs). +//! +//! Proof bytes are reproducible only without grinding (the nonce search is a +//! parallel `find_any`), so every case proves at `grinding_factor = 0`. Each +//! case pins the SHA3-256 of the proof's rkyv bytes (the wire format of +//! record) and, so that a failure says WHERE the drift is, separately the +//! digests of: the FRI layer roots, the terminal coefficients, the per-query FRI +//! decommitments, and the trace/composition openings (plus the layer count). +//! ζ and ι are not in a proof; a ζ drift moves every later layer root and the +//! terminal coefficients, an ι drift moves the decommitment and opening digests. +//! +//! Coverage: both byte hashes this crate owns (Keccak, Blake3; RPX is pinned +//! the same way in the prover crate, `tests::zf_rpx_golden_tests`), blowup 2 and +//! 4, a base-field AIR (`SimpleAddition`, E = F) and an extension-field AIR with +//! an aux trace (`LogReadOnlyRAP`, E = F³), `total_folds` ∈ {0, 1, 2, ≥ 3}, and +//! one multi-table bus proof (CPU/ADD/MUL, `multi_prove`). +//! +//! Generated at the default format BEFORE any S3 prover code existed (the +//! schedule DP alone changes no prover path). Regenerate only for a deliberate format change: +//! `cargo test -p stark --lib zf_golden_tests::print_goldens -- --ignored --nocapture`. + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; +use math::field::goldilocks::GoldilocksField; +use sha3::{Digest, Sha3_256}; + +use crate::config::{Blake3StarkHash, KeccakStarkHash, StarkHash}; +use crate::examples::multi_table_lookup::{ + new_add_air_with_lookup, new_cpu_air_with_lookup, new_mul_air_with_lookup, +}; +use crate::examples::read_only_memory_logup::{ + LogReadOnlyPublicInputs, LogReadOnlyRAP, read_only_logup_trace, +}; +use crate::examples::simple_addition::{ + SimpleAdditionAIR, SimpleAdditionPublicInputs, simple_addition_trace, +}; +use crate::proof::options::{ProofFormat, ProofOptions}; +use crate::proof::stark::{MultiProof, StarkProof}; +use crate::prover::{GenericProver, IsStarkProver}; +use crate::residency_mode::ResidencyMode; +use crate::trace::TraceTable; +use crate::traits::AIR; +use crate::verifier::{GenericVerifier, IsStarkVerifier}; + +type F = GoldilocksField; +type E = Degree3GoldilocksExtensionField; +type Felt = FieldElement; + +/// Test options at the DEFAULT format with grinding off. `k` is the terminal +/// log-degree, so `total_folds = log2(rows) − k` whenever that is ≥ 0. +pub(crate) fn golden_options( + blowup: u8, + k: u8, + queries: usize, + format: ProofFormat, +) -> ProofOptions { + ProofOptions { + blowup_factor: blowup, + fri_number_of_queries: queries, + coset_offset: 3, + grinding_factor: 0, + fri_final_poly_log_degree: k, + format, + } +} + +pub(crate) fn sha3_hex(bytes: &[u8]) -> String { + let d = Sha3_256::digest(bytes); + d.iter().map(|b| format!("{b:02x}")).collect() +} + +/// The pinned digests of one proof, as one line. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Fingerprint { + pub proof: String, + pub fri_roots: String, + pub num_fri_roots: usize, + pub coeffs: String, + pub queries: String, + pub openings: String, +} + +impl Fingerprint { + pub(crate) fn line(&self) -> String { + format!( + "proof {} roots[{}] {} coeffs {} queries {} openings {}", + self.proof, + self.num_fri_roots, + self.fri_roots, + self.coeffs, + self.queries, + self.openings + ) + } +} + +/// The [`Fingerprint`] of any `StarkProof` (a macro: the rkyv serializer +/// bounds of a generic `StarkProof` are not worth spelling out). +macro_rules! fingerprint { + ($proof:expr) => {{ + let proof = $proof; + let rk = |bytes: Result| { + $crate::tests::zf_golden_tests::sha3_hex(&bytes.expect("rkyv")) + }; + $crate::tests::zf_golden_tests::Fingerprint { + proof: rk(rkyv::to_bytes::(proof)), + fri_roots: $crate::tests::zf_golden_tests::sha3_hex( + &proof.fri_layers_merkle_roots.concat(), + ), + num_fri_roots: proof.fri_layers_merkle_roots.len(), + coeffs: rk(rkyv::to_bytes::( + &proof.fri_final_poly_coeffs, + )), + queries: rk(rkyv::to_bytes::(&proof.query_list)), + openings: rk(rkyv::to_bytes::( + &proof.deep_poly_openings, + )), + } + }}; +} +pub(crate) use fingerprint; + +// --------------------------------------------------------------------------- +// The cases +// --------------------------------------------------------------------------- + +/// `SimpleAddition` (E = F) under hash `H`. +pub(crate) fn prove_simple_addition( + rows: usize, + options: &ProofOptions, +) -> ( + SimpleAdditionAIR, + StarkProof>, +) { + let air = SimpleAdditionAIR::::new(options); + let pi = SimpleAdditionPublicInputs { + a: Felt::from(1u64), + b: Felt::from(2u64), + }; + let mut trace = simple_addition_trace::(rows); + let proof = GenericProver::::prove( + &air, + &mut trace, + &pi, + &mut DefaultTranscript::::new(&[]), + ) + .expect("proving must succeed"); + (air, proof) +} + +pub(crate) fn verify_simple_addition( + air: &SimpleAdditionAIR, + proof: &StarkProof>, +) -> bool { + GenericVerifier::::verify(proof, air, &mut DefaultTranscript::::new(&[])) +} + +/// A continuous read-only memory over addresses 1..=5, `rows` reads. +fn logup_reads(rows: usize) -> (Vec, Vec) { + let addr: Vec = (0..rows).map(|i| Felt::from((i % 5) as u64 + 1)).collect(); + let val: Vec = (0..rows) + .map(|i| Felt::from(((i % 5) as u64 + 1) * 10)) + .collect(); + (addr, val) +} + +/// `LogReadOnlyRAP` (E = F³, one aux column) under hash `H`. +/// An AIR, its proof and its public inputs. +pub(crate) type LogupCase = ( + LogReadOnlyRAP, + StarkProof>, + LogReadOnlyPublicInputs, +); + +pub(crate) fn prove_logup(rows: usize, options: &ProofOptions) -> LogupCase { + let (addr, val) = logup_reads(rows); + let mut trace: TraceTable = read_only_logup_trace(addr, val); + let cols = trace.columns_main(); + let pi = LogReadOnlyPublicInputs { + a0: cols[0][0], + v0: cols[1][0], + a_sorted_0: cols[2][0], + v_sorted_0: cols[3][0], + m0: cols[4][0], + }; + let air = LogReadOnlyRAP::::new(options); + let proof = GenericProver::::prove( + &air, + &mut trace, + &pi, + &mut DefaultTranscript::::new(&[]), + ) + .expect("proving must succeed"); + (air, proof, pi) +} + +pub(crate) fn verify_logup( + air: &LogReadOnlyRAP, + proof: &StarkProof>, +) -> bool { + GenericVerifier::::verify(proof, air, &mut DefaultTranscript::::new(&[])) +} + +/// The CPU/ADD/MUL bus instance (`residency_mode_tests`) under hash `H`. +pub(crate) fn prove_multi(options: &ProofOptions) -> MultiProof { + let (mut cpu_trace, mut add_trace, mut mul_trace) = super::residency_mode_tests::traces(); + let cpu_air = new_cpu_air_with_lookup(options); + let add_air = new_add_air_with_lookup(options); + let mul_air = new_mul_air_with_lookup(options); + let pairs: Vec<( + &dyn AIR, + _, + _, + )> = vec![ + (&cpu_air, &mut cpu_trace, &()), + (&add_air, &mut add_trace, &()), + (&mul_air, &mut mul_trace, &()), + ]; + GenericProver::::multi_prove( + pairs, + &mut DefaultTranscript::::new(&[]), + #[cfg(feature = "disk-spill")] + crate::storage_mode::StorageMode::Ram, + ResidencyMode::default(), + ) + .expect("proving must succeed") +} + +pub(crate) fn verify_multi( + options: &ProofOptions, + proof: &MultiProof, +) -> bool { + let cpu_air = new_cpu_air_with_lookup(options); + let add_air = new_add_air_with_lookup(options); + let mul_air = new_mul_air_with_lookup(options); + let airs: Vec<&dyn AIR> = + vec![&cpu_air, &add_air, &mul_air]; + GenericVerifier::::multi_verify( + &airs, + proof, + &mut DefaultTranscript::::new(&[]), + &FieldElement::zero(), + ) +} + +/// Every golden case: (name, fingerprint line) computed now. +fn compute_goldens() -> Vec<(String, String)> { + let mut out = Vec::new(); + let d = ProofFormat::DEFAULT; + // SimpleAddition, k = 2: total_folds = log2(rows) − 2. + for (hash, rows, blowup) in [ + ("keccak", 4usize, 2u8), // total_folds 0 + ("keccak", 8, 2), // 1 + ("keccak", 16, 4), // 2 + ("keccak", 256, 2), // 6 + ("blake3", 8, 4), // 1 + ("blake3", 64, 4), // 4 + ] { + let o = golden_options(blowup, 2, 5, d); + let (air, proof) = match hash { + "keccak" => prove_simple_addition::(rows, &o), + _ => prove_simple_addition::(rows, &o), + }; + let ok = match hash { + "keccak" => verify_simple_addition::(&air, &proof), + _ => verify_simple_addition::(&air, &proof), + }; + assert!(ok, "golden case must verify"); + out.push(( + format!("simple_addition/{hash}/rows{rows}/blowup{blowup}"), + fingerprint!(&proof).line(), + )); + } + // LogReadOnlyRAP (ext3 + aux), k = 1. + for (hash, rows, blowup) in [ + ("blake3", 16usize, 2u8), // total_folds 3 + ("blake3", 128, 4), // 6 + ("keccak", 32, 4), // 4 + ] { + let o = golden_options(blowup, 1, 7, d); + let (air, proof, _) = match hash { + "keccak" => prove_logup::(rows, &o), + _ => prove_logup::(rows, &o), + }; + let ok = match hash { + "keccak" => verify_logup::(&air, &proof), + _ => verify_logup::(&air, &proof), + }; + assert!(ok, "golden case must verify"); + out.push(( + format!("logup/{hash}/rows{rows}/blowup{blowup}"), + fingerprint!(&proof).line(), + )); + } + // Multi-table bus proof, k = 1 (CPU 8 rows, ADD/MUL 4 rows). + let o = golden_options(2, 1, 6, d); + let multi = prove_multi::(&o); + assert!(verify_multi::(&o, &multi)); + let bytes = rkyv::to_bytes::(&multi).expect("rkyv"); + let mut line = format!("multi {}", sha3_hex(&bytes)); + for (i, p) in multi.proofs.iter().enumerate() { + line.push_str(&format!(" | table{i} {}", fingerprint!(p).line())); + } + out.push(("multi/blake3/blowup2".to_string(), line)); + out +} + +/// Pinned at the default format (see the module docs). +const GOLDENS: &[(&str, &str)] = &[ + ( + "simple_addition/keccak/rows4/blowup2", + "proof d72bff5491a61ff5a58c4677dbcf1a2daa17953ea1b976113e21a24b54bec6b7 roots[0] a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a coeffs 283be3dea88b9f9fc3012a6ec6f4dd9452c63f49bf5030c0119426aeca2e2ead queries 52d34b9f6d30aaf0b5bc5a4c5cb5c99909fcc4018af897ee04b9ff4beb69bc0c openings bf85016f5d66788f79a6f55ec69d9829d8f2afad2a792e9432d2e2bc6e85a391", + ), + ( + "simple_addition/keccak/rows8/blowup2", + "proof df6437ee9bbbabb2d22dddfc8ac6888388328454cb6772bc52bd91d33e9c961e roots[0] a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a coeffs 1fa5e702cc4464b2fccd2c2ed05f9544aae1c1f81d6e63ec2907fa1225ee1309 queries 52d34b9f6d30aaf0b5bc5a4c5cb5c99909fcc4018af897ee04b9ff4beb69bc0c openings 02e305d0d828960f22c1c2d46dc01c2cdb04488a7cdaf66c97310061ffb34817", + ), + ( + "simple_addition/keccak/rows16/blowup4", + "proof f513e31eca509f0eb72429e007724d368fa3466007c9bbd3cb0c1a4e62d0231b roots[1] 1b8a5d9014a32fd18488405253b1b382fd94681299ce888d2054b413dc9ffd4e coeffs 6763e71de233097b3c8a2563c8fc958f7dd8258c015d4602ef1be5c5f3d7d1ed queries 3b62f8adbf53c30c0a06b8b8e04c1c655e776ea7193a6eac577e1612a564db09 openings d3e7a59c53cfba8ec96b4014ddeff9c395b6a2b12f0ad4e49139f824de65241c", + ), + ( + "simple_addition/keccak/rows256/blowup2", + "proof 0d21b0b5d2405473c93d17df09db5d3c771f87014e46e6aa26d9f2ee1f20caa8 roots[5] d274efdb442a2c9dec26ace00aadf90e47bb33c46cafc818d35aa2a44025ab82 coeffs 8ac737818bedf94a381b4b10f650dee2d300024d7a0bb9fa5f8200406c293971 queries 247e2e8c14f12cf87975bb5aabf89d7b4cb046e21d889bd736e3c9ab3b3891b3 openings cf8f7514ffd6b2df685f2cbcfaf5e5e37c60868b10c9c9186bd66b26267e2e7c", + ), + ( + "simple_addition/blake3/rows8/blowup4", + "proof d6ebfa4b14b5e9bd6239c7a31e17a146e6995ade0b3101847df515176fe563ba roots[0] a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a coeffs 307d4a0b258158554a0f243871f919fcf4031414b3ecbdd39f287d2d31cba197 queries 52d34b9f6d30aaf0b5bc5a4c5cb5c99909fcc4018af897ee04b9ff4beb69bc0c openings 4632d0d62386964b4e3566b3af2271c6b0800ffe89894d39ddf020eaa5258695", + ), + ( + "simple_addition/blake3/rows64/blowup4", + "proof 8caa0c1311ee2092d1a85c5e2fe6923466ebd8ed89edca123614d4127f1e674b roots[3] 9a7b1bf916dd5bdaee9ce8727a51131c055bcde12f780d48681cb6d51c543691 coeffs 9629f564cf5f72bc3b17b1c88d5864beb200da36ab5c0b2fea2f45905dc8b3f8 queries 0633449ec687a0c183ef547ee538d8620a51f3b3a0d42f570f9c50f5c3e13125 openings 24345624f78a948e8e006559792233857529c9612c603280be9e2779c50f7643", + ), + ( + "logup/blake3/rows16/blowup2", + "proof 51b186c5c4c958d4de0c1359e32d6568780c0631c9b5e7289aa3bd1363997e88 roots[2] d6bad767d9d70cbd82c59d6ee0d13f4415e7ac4046ea2d606ebc305a6bac61ce coeffs ba8e4c0e358be69f2d39a246318d31ccf796086042c469ac060702006e79d62f queries 818f9de07a7ea34f7b1794aa0a62a77fd4c5d700e349c33aba6be5033b93a9d4 openings a7045f424c3096a7554d78e6ff535700eaa8b6af269bcbd6939b56f93bf8391d", + ), + ( + "logup/blake3/rows128/blowup4", + "proof bea6e98f19c49e75401bcc9c21c73dbf3bf9576e5ce80a09a48f9dd5b70fa3ec roots[5] ceec0e7bc2ca1c16410c2222b96ce6d45d3e4573f54eb25378625bba375c4804 coeffs 17bb0241af6871c3359af6a56981dd842c667f6f1ca87227bda570f93c9a566b queries 1d84c98fdb64d057b0af98d2be55dd2c611cfc3cca5fea9abf1b5c1e86aaebc7 openings 4374ff4fab6a7bc892f8769661b6ad5abba2ac4979931c296f4b3d857b547369", + ), + ( + "logup/keccak/rows32/blowup4", + "proof 2caf234c6858994f7a1910bb95b465c9ab7b5fc675f489f3e3fdc818b5d54798 roots[3] 92a3704249af017157fb755e570d4b07f73c151392119f4a8ffd1d96e2511123 coeffs c85284e687629ec7d505bb0723f10324896d4e7ebd05eb6aa3dd57459b5af0f6 queries 67dbdc533f359c0b48f3b14e704e086ac17d785bec9cd53b9891147ce1f925a9 openings ede54d2b9998a60133ac0cf63c88d5b47a487082899becea17711c0e7db55283", + ), + ( + "multi/blake3/blowup2", + "multi eec51cb0e5701209e4709f89fb72b3af2dbc3038f2c80a185a2c74391ebffad5 | table0 proof 1079e69c4d47411814b05c5a7cc960d1c93846a3a873381c0184fa8273300cc3 roots[1] 1c7023dfeb09e6cc2ec141f6ec83e04f95e036ab1ae54a468248adde46a60d79 coeffs cb86ea5b8fe22227a96ad9d6ca4f68cb3bcd07825956d3ab804a98182304bfc8 queries 853284b820c6409aa20eab2a60863f4d1456deed856a8e9baadda3ecc754e7a3 openings e862a605d77e405c43605a4db1c05f49fefba0bda4d722d1dca33440ecf677ba | table1 proof 759ca54156cf6132fe61bd99f21528789bd60a21c190b3ffdc4caea20351f16d roots[0] a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a coeffs bd928b7e79613fa867a51d21ce6af7c18c3bd07aae44c0a83460486f50be9cfb queries c50b6659101c4ff74629092f4030534eec067bfaa650f3048807c4f2bba7ca72 openings 764b653d05d0cc14328bd94b8454420df2611a7d3465375b80235bfdb93f70ad | table2 proof b276a32877699f6e9e105ae05bd3bbfe1416d088187ceb22e710f968b9e5efd9 roots[0] a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a coeffs 1aef1aa9a22d64128d1469e69aa38886572f485071c6d6b046dd4f8bca60beb6 queries c50b6659101c4ff74629092f4030534eec067bfaa650f3048807c4f2bba7ca72 openings 9a7740f93e490a668f8f16dd9d4b00b79cab3e4561a85eecca03c8b5eafc13f6", + ), +]; + +#[test] +fn default_format_goldens_are_byte_identical() { + let got = compute_goldens(); + assert_eq!(got.len(), GOLDENS.len(), "one pin per case"); + for ((name, line), (pin_name, pin_line)) in got.iter().zip(GOLDENS) { + assert_eq!(name, pin_name); + assert_eq!( + line, pin_line, + "{name}: the default-format proof moved (a field whose digest differs is where)" + ); + } +} + +/// Prints `GOLDENS`. Run only for a deliberate format change. +#[test] +#[ignore = "generator for GOLDENS"] +fn print_goldens() { + println!("const GOLDENS: &[(&str, &str)] = &["); + for (name, line) in compute_goldens() { + println!(" (\n \"{name}\",\n \"{line}\",\n ),"); + } + println!("];"); +} diff --git a/crypto/stark/src/tests/zf_s2_device_tests.rs b/crypto/stark/src/tests/zf_s2_device_tests.rs new file mode 100644 index 000000000..97bbbfb26 --- /dev/null +++ b/crypto/stark/src/tests/zf_s2_device_tests.rs @@ -0,0 +1,134 @@ +//! S2 on the device: one-row trees and +//! openings, and the committed input tree from the DEEP codeword, against the +//! host CPU paths, under Keccak and Blake3 (the RPX twins live in the prover +//! crate's `tests::zf_rpx_device_tests`). +//! +//! Every test here needs a GPU and fails loudly when the device path does not +//! run (a declined commit is an `Err`, a vector proof must move the one-row +//! device counters), so none can pass by falling back to the host: +//! +//! ```text +//! cargo test -p stark --release --features cuda --lib \ +//! tests::zf_s2_device_tests::trees_ -- --ignored +//! LAMBDA_VM_GPU_LDE_THRESHOLD=2 cargo test -p stark --release --features cuda --lib \ +//! tests::zf_s2_device_tests::fri_ -- --ignored +//! LAMBDA_VM_GPU_LDE_THRESHOLD=1024 cargo test -p stark --release --features cuda --lib \ +//! tests::zf_s2_device_tests::proved_one_row_vectors_equal_the_cpu_bytes \ +//! -- --ignored --exact --test-threads=1 +//! ``` + +use crate::config::{Blake3StarkHash, KeccakStarkHash, StarkHash}; +use crate::fri::device_parity::{Case, one_row_cases, one_row_resident_cases, run_cases}; +use crate::s2_device_parity::run_tree_parity; + +fn trees(name: &str) { + if let Err(failures) = run_tree_parity::(name) { + panic!("{name}: {failures:#?}"); + } +} + +fn fri(name: &str, cases: &[Case], resident: bool, seed: u64) { + if let Err(failures) = run_cases::(name, cases, resident, seed) { + panic!("{name}: {failures:#?}"); + } +} + +/// The one-row case list is pinned by count, so the box run's pre-registered +/// `FRIDEV … cases equal` lines mean something (29 DP shapes + 6 pair-mode + 5 +/// production; 5 resident). +#[test] +fn one_row_case_lists_are_pinned() { + assert_eq!(one_row_cases().len(), 29 + 6 + 5); + assert_eq!(one_row_resident_cases().len(), 5); + for (lde_log, o) in one_row_cases().iter().chain(&one_row_resident_cases()) { + assert_eq!( + o.format.one_row, + crate::proof::options::OneRowMode::On, + "LDE 2^{lde_log}: a one-row case without one-row openings" + ); + } +} + +#[test] +#[ignore = "requires a GPU; run with --features cuda -- --ignored"] +fn trees_one_row_keccak() { + trees::("keccak"); +} + +#[test] +#[ignore = "requires a GPU; run with --features cuda -- --ignored"] +fn trees_one_row_blake3() { + trees::("blake3"); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn fri_one_row_keccak() { + fri::("keccak", &one_row_cases(), false, 0x5234_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn fri_one_row_blake3() { + fri::("blake3", &one_row_cases(), false, 0x5234_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn fri_one_row_resident_keccak() { + fri::("keccak", &one_row_resident_cases(), true, 0x5235_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn fri_one_row_resident_blake3() { + fri::("blake3", &one_row_resident_cases(), true, 0x5235_0000); +} + +/// The (e) vector proofs (the README's (e): `one_row_pair` and +/// `one_row_3_2_1_2`, LDE 4096, Q = 3, grinding 0) proved on the device path +/// are byte-identical to the checked-in CPU-proved files (rkyv bytes and the +/// verifier-derived JSON), under Keccak and Blake3. Each proof must take the +/// one-row device FRI commit once (the input tree off the DEEP codeword) and +/// build its main, aux and composition trees one-row on the device (at least +/// three one-row device trees per proof), so a host fallback fails the test. +/// Run alone (`--exact --test-threads=1`): the counters are process-wide. +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD<=4096; run alone with --features cuda -- --ignored --exact --test-threads=1"] +fn proved_one_row_vectors_equal_the_cpu_bytes() { + use crate::fri::vectors::{check_or_write, one_row_proof_vectors}; + let fri_before = crate::gpu_lde::gpu_one_row_fri_calls(); + let trees_before = crate::gpu_lde::gpu_one_row_trees(); + let comp_before = crate::gpu_lde::gpu_composition_calls(); + let mut files = one_row_proof_vectors::("keccak"); + files.extend(one_row_proof_vectors::("blake3")); + let fri_commits = crate::gpu_lde::gpu_one_row_fri_calls() - fri_before; + let trees = crate::gpu_lde::gpu_one_row_trees() - trees_before; + let compositions = crate::gpu_lde::gpu_composition_calls() - comp_before; + println!( + "S2DEV vector proofs: {} files, {fri_commits} one-row device FRI commits, {trees} one-row device trees, {compositions} device compositions", + files.len() + ); + // Two (e) formats x two hashes, two files per proof. + assert_eq!(files.len(), 2 * 2 * 2); + assert_eq!( + fri_commits, 4, + "every one-row vector proof must take the device FRI commit (lower LAMBDA_VM_GPU_LDE_THRESHOLD)" + ); + assert!( + trees >= 3 * 4, + "every one-row vector proof must build its main, aux and composition trees on the device \ + ({trees} one-row device trees for 4 proofs)" + ); + // Every proof composes on the device (the AIR's constraint program); a + // host composition would not be counted here. + assert_eq!( + compositions, 4, + "every one-row vector proof must compose on the device ({compositions} device compositions)" + ); + let bad = check_or_write(&files, false); + assert!( + bad.is_empty(), + "device-proved one-row vectors differ from the checked-in CPU bytes: {bad:?}" + ); +} diff --git a/crypto/stark/src/traits.rs b/crypto/stark/src/traits.rs index f28da26fb..fcb65fc11 100644 --- a/crypto/stark/src/traits.rs +++ b/crypto/stark/src/traits.rs @@ -210,6 +210,24 @@ pub trait AIR: Send + Sync { [0u8; 32] } + /// The hardcoded commitment to the precomputed columns under the trace + /// trees' leaf `layout` (S2). The root depends on the layout (a one-row + /// leaf hashes different bytes), so each layout has its own trust anchor. + /// + /// `None` = this AIR has no root for `layout`: the prover refuses to prove + /// and the verifier rejects (never a silent recompute, never + /// the other layout's root). The default serves today's layout only. + /// Only meaningful if `is_preprocessed()` returns true. + fn precomputed_commitment_for( + &self, + layout: crate::leaf_layout::LeafLayout, + ) -> Option { + match layout { + crate::leaf_layout::LeafLayout::RowPair => Some(self.precomputed_commitment()), + crate::leaf_layout::LeafLayout::Row => None, + } + } + /// The precomputed columns themselves, `0..num_precomputed_columns()`. /// /// Empty unless `is_preprocessed()`. The univariate path never needs these diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 891df1a91..c946c20aa 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -5,6 +5,8 @@ use super::{ proof::stark::StarkProof, traits::{AIR, TransitionEvaluationContext}, }; +use crate::leaf_layout::LeafLayout; +use crate::merkle_caps::{StarkCaps, TableTreeChecks, TreeCheck}; pub use crate::proof::view::PiDeserializer; use crate::{ config::Commitment, @@ -18,7 +20,7 @@ use crate::{ table::Table, }; use crypto::fiat_shamir::is_transcript::IsStarkTranscript; -use crypto::merkle_tree::proof::{verify_merkle_path, verify_merkle_path_from_leaf_hash}; +use crypto::merkle_tree::traits::IsMerkleTreeBackend; use crypto::merkle_tree::traits::IsStreamingLeafBackend; #[cfg(not(feature = "test_fiat_shamir"))] use log::error; @@ -147,17 +149,33 @@ pub trait IsStarkVerifier< PI: rkyv::Archive + Clone, ::Archived: rkyv::Deserialize, { + /// The query indexes: leaf indexes of the trace trees, uniform below + /// [`LeafLayout::query_bound`] — `lde / 2` (a row PAIR) today, `lde` under + /// one-row openings, where each index is one point of `D₀` (soundness: + /// sampling a pair and opening one of its points would bias `x₀`). fn sample_query_indexes( number_of_queries: usize, domain: &VerifierDomain, + leaf_layout: LeafLayout, transcript: &mut impl IsStarkTranscript, ) -> Vec { - let domain_size = domain.lde_length as u64; + let bound = leaf_layout.query_bound(domain.lde_length as u64); (0..number_of_queries) - .map(|_| (transcript.sample_u64(domain_size >> 1)) as usize) + .map(|_| (transcript.sample_u64(bound)) as usize) .collect::>() } + /// The trace-tree leaf layout of `air`'s proof over `trace_length` rows + /// (row pairs, or one row under S2): a verifier-side constant from the + /// AIR's options and widths and the trace length the FRI layout already + /// trusts ([`crate::leaf_layout::table_leaf_layout`]). + fn leaf_layout( + air: &dyn AIR, + trace_length: usize, + ) -> LeafLayout { + crate::leaf_layout::table_leaf_layout(air, trace_length) + } + /// The pruned-OOD layout for this AIR — the single place in the verifier that /// reads the shape metadata (`trace_columns`, `step_size`, the /// transition-offset count, and the next-row column set). Everything that used @@ -253,6 +271,16 @@ pub trait IsStarkVerifier< None => return false, }; let expected_aux = air.num_auxiliary_rap_columns(); + // The symmetric slot exists only for row pairs: a one-row leaf holds + // the queried row alone, so every `evaluations_sym` must be EMPTY (a + // non-empty one would be hashed into the leaf and read by nothing). + let one_row = Self::leaf_layout(air, proof.trace_length()).is_one_row(); + let sym = |n: usize| if one_row { 0 } else { n }; + let (sym_precomputed, sym_main, sym_aux) = ( + sym(expected_precomputed), + sym(expected_main), + sym(expected_aux), + ); if proof.deep_poly_openings_len() < num_queries { return false; @@ -272,11 +300,12 @@ pub trait IsStarkVerifier< let main = opening.main_trace_polys(); precomputed == expected_precomputed - && precomputed_sym == expected_precomputed + && precomputed_sym == sym_precomputed && main.evaluations().len() == expected_main - && main.evaluations_sym().len() == expected_main + && main.evaluations_sym().len() == sym_main && aux == expected_aux - && aux_sym == expected_aux + && aux_sym == sym_aux + && (!one_row || opening.composition_poly().evaluations_sym().is_empty()) }) } @@ -457,21 +486,32 @@ pub trait IsStarkVerifier< /// arithmetic as the CPU and GPU provers; drift between them would break all /// proofs. `VerifierDomain.lde_length` is the codeword size and /// `lde_length / trace_length` the blowup factor. + /// + /// The proof FORMAT (fold schedule, encoding) comes from `air.options()` — + /// a verifier-side constant, never read from the proof. `None` when the + /// format cannot be laid out for this table (a one-row mode, or a schedule + /// override that does not fit): the proof is then rejected. // `FriFoldLayout` is a crate-internal helper type returned from a default method // of this public trait; the exposure is intentional (internal helper). #[allow(private_interfaces)] fn fri_termination_params( air: &dyn AIR, domain: &VerifierDomain, - ) -> crate::fri::terminal::FriFoldLayout { - let k = air.options().fri_final_poly_log_degree as u32; + ) -> Option { let blowup_log = (domain.lde_length / domain.trace_length).trailing_zeros(); - crate::fri::terminal::FriFoldLayout::new(domain.lde_length.trailing_zeros(), blowup_log, k) + crate::fri::terminal::FriFoldLayout::for_options( + domain.lde_length.trailing_zeros(), + blowup_log, + air.options(), + Self::leaf_layout(air, domain.trace_length).is_one_row(), + ) + .ok() } /// Reconstructs the Deep composition polynomial evaluations at the challenge indices values using the provided /// openings of the trace polynomials and the composition polynomial parts. It then uses these to verify that the /// FRI decommitments are valid and correspond to the Deep composition polynomial. + #[allow(clippy::too_many_arguments)] fn step_3_verify_fri( air: &dyn AIR, proof: StarkProofView<'_, Field, FieldExtension, PI>, @@ -483,31 +523,41 @@ pub trait IsStarkVerifier< ood_full: &Table, next_row_cols: &[usize], step_size: usize, + // The per-tree Merkle checks (`table_tree_checks`); this step reads the + // committed FRI layers' ones. + checks: &TableTreeChecks<'_>, ) -> bool where FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { crate::profile_markers::step_marker::<{ crate::profile_markers::STEP_VERIFY_FRI }>(); + // ---- Reconstruct the FRI terminal codeword from the final-poly coeffs ---- + // The prover folds the deep composition codeword down to a terminal + // codeword of length `terminal_len = 2^(blowup_log + effective_k)` and sends + // the `2^effective_k` coefficients of the low-degree polynomial it encodes. + let Some(layout) = Self::fri_termination_params(air, domain) else { + return false; + }; + let num_committed = layout.num_committed; + // Row pairs: DEEP at `x` and `−x` per query. One row: DEEP at the one + // point `x_r` (the sym vector comes back empty). + let leaf_layout = LeafLayout::from_one_row(layout.one_row); let (deep_poly_evaluations, deep_poly_evaluations_sym) = - match Self::reconstruct_deep_composition_poly_evaluations_for_all_queries( + match Self::reconstruct_deep_composition_poly_evaluations_for_layout( challenges, domain, proof, ood_full, next_row_cols, step_size, + leaf_layout, ) { Some(pair) => pair, None => return false, }; - - // ---- Reconstruct the FRI terminal codeword from the final-poly coeffs ---- - // The prover folds the deep composition codeword down to a terminal - // codeword of length `terminal_len = 2^(blowup_log + effective_k)` and sends - // the `2^effective_k` coefficients of the low-degree polynomial it encodes. - let layout = Self::fri_termination_params(air, domain); - let num_committed = layout.num_committed; + #[cfg(any(test, feature = "test-utils"))] + crate::fri::capture::record_deep(&deep_poly_evaluations, &deep_poly_evaluations_sym); // Structural check: number of committed FRI layers must equal // `num_committed` (zero when no fold or a single final fold happened). @@ -527,14 +577,22 @@ pub trait IsStarkVerifier< // iterations and accept the query vacuously) or padded (making the loop // skip the terminal low-degree check), bypassing FRI entirely. This length // check is the only thing that pins them, so it must run before the loop. + // Opened values per query: one sibling per layer under the legacy + // encoding, every layer's full group otherwise (a format constant). + let values_per_query = layout.opened_values_per_query(); if (0..proof.query_list_len()).any(|i| { let decommitment = proof.query(i); decommitment.layers_auth_paths_len() != num_committed - || decommitment.layers_evaluations_sym().len() != num_committed + || decommitment.layers_evaluations_sym().len() != values_per_query }) { return false; } + // One check per committed layer, built with the same layer count. + if checks.fri.len() != num_committed { + return false; + } + let terminal_offset = domain.coset_offset.pow(1u64 << layout.total_folds); let terminal_codeword = crate::fri::terminal::terminal_codeword_from_coeffs::( @@ -547,13 +605,47 @@ pub trait IsStarkVerifier< let mut evaluation_point_inverse = challenges .iotas .iter() - .map(|iota| Self::query_challenge_to_evaluation_point(*iota, false, domain)) + .map(|iota| Self::query_point(leaf_layout, *iota, domain)) .collect::>>(); // Any zero evaluation point means a malformed query index, reject. if FieldElement::inplace_batch_inverse(&mut evaluation_point_inverse).is_err() { return false; } + if !layout.is_legacy() { + // Group encoding (S3): the ω_{2^d} tables once, then every query. + let mut roots_tables: Vec>> = Vec::new(); + for &d in &layout.schedule { + let d = d as usize; + if roots_tables.len() <= d { + roots_tables.resize(d + 1, Vec::new()); + } + if roots_tables[d].is_empty() { + match crate::fri::group::roots_of_unity_table::(d as u32) { + Some(t) => roots_tables[d] = t, + None => return false, + } + } + } + return (0..challenges.iotas.len()) + .zip(evaluation_point_inverse) + .all(|(i, eval)| { + Self::verify_query_groups( + &layout, + &checks.fri, + i, + &challenges.zetas, + challenges.iotas[i], + proof.query(i), + eval, + &deep_poly_evaluations[i], + deep_poly_evaluations_sym.get(i), + &terminal_codeword, + &roots_tables, + ) + }); + } + (0..challenges.iotas.len()) .zip(evaluation_point_inverse) .all(|(i, eval)| { @@ -566,10 +658,23 @@ pub trait IsStarkVerifier< &deep_poly_evaluations[i], &deep_poly_evaluations_sym[i], &terminal_codeword, + &checks.fri, + i, ) }) } + /// The LDE-coset point query `q` opens first under `leaf_layout`: the row + /// at bit-reversed position `2q` for row pairs (as + /// [`Self::query_challenge_to_evaluation_point`]), `q` for one row. + fn query_point( + leaf_layout: LeafLayout, + q: usize, + domain: &VerifierDomain, + ) -> FieldElement { + domain.lde_coset_element(leaf_layout.query_rows(q, domain.lde_length).0) + } + /// Returns the field element element of the domain `domain` corresponding to the given FRI query index challenge `iota`. /// Returns the LDE-coset element for FRI query challenge `iota`. The /// `sym` flag picks the symmetric counterpart (`iota*2+1`) instead of the @@ -587,9 +692,15 @@ pub trait IsStarkVerifier< /// (`2·iota`, `2·iota+1`) is committed as the single leaf at position `iota`, /// so one Merkle path authenticates both `evaluations` (the row) and /// `evaluations_sym` (its symmetric). Same layout used for trace and composition. + /// + /// `check` is the tree's [`TreeCheck`], built once per tree: it fixes the + /// exact path length (`log2(lde) − 1 − c`, a verifier constant) and, for a + /// capped tree, the authenticated cap the path folds onto. `query` is the + /// opening's position in proof order (query 0 is a capped tree's owner). fn verify_opening_pair( opening: PolynomialOpeningsView<'_, E>, - root: &Commitment, + check: &TreeCheck<'_>, + query: usize, iota: usize, ) -> bool where @@ -605,19 +716,15 @@ pub trait IsStarkVerifier< opening.evaluations(), opening.evaluations_sym(), ); - verify_merkle_path_from_leaf_hash::>( - opening.merkle_path(), - root, - iota, - leaf_hash, - ) + check.verify::>(query, opening.merkle_path(), iota, leaf_hash) } /// Verify opening Open(tⱼ(D_LDE), 𝜐) and Open(tⱼ(D_LDE), -𝜐) for all trace polynomials tⱼ, /// where 𝜐 and -𝜐 are the elements corresponding to the index challenge `iota`. fn verify_trace_openings( - proof: StarkProofView<'_, Field, FieldExtension, PI>, deep_poly_openings: DeepPolynomialOpeningView<'_, Field, FieldExtension>, + checks: &TableTreeChecks<'_>, + query: usize, iota: usize, ) -> bool where @@ -627,11 +734,13 @@ pub trait IsStarkVerifier< // Main trace (multiplicities for preprocessed, full trace for normal). let mut ok = Self::verify_opening_pair::( deep_poly_openings.main_trace_polys(), - proof.lde_trace_main_merkle_root(), + &checks.main, + query, iota, ); - // Precomputed trace (preprocessed tables only). Mismatched presence: + // Precomputed trace (preprocessed tables only). The check exists iff the + // proof carries a precomputed root (`table_tree_checks`). Mismatched presence: // `(Some(root), None)` and any `(None, Some(opening))` carrying at least // one column are rejected upstream by `trace_opening_widths_well_formed` // (which pins the precomputed opening width to the AIR — zero for a @@ -642,10 +751,12 @@ pub trait IsStarkVerifier< // only site that rejects that shape, and the check keeps the function // self-contained. ok &= match ( - proof.lde_trace_precomputed_merkle_root(), + checks.precomputed.as_ref(), deep_poly_openings.precomputed_trace_polys(), ) { - (Some(root), Some(opening)) => Self::verify_opening_pair::(opening, root, iota), + (Some(check), Some(opening)) => { + Self::verify_opening_pair::(opening, check, query, iota) + } (None, None) => true, _ => false, }; @@ -657,12 +768,9 @@ pub trait IsStarkVerifier< // aux tree got to choose them after seeing `z`/`alpha` // (`tests::aux_opening_width_tests`). The width is pinned upstream by // `trace_opening_widths_well_formed`; do not re-derive it from the proof. - ok &= match ( - proof.lde_trace_aux_merkle_root(), - deep_poly_openings.aux_trace_polys(), - ) { - (Some(root), Some(opening)) => { - Self::verify_opening_pair::(opening, root, iota) + ok &= match (checks.aux.as_ref(), deep_poly_openings.aux_trace_polys()) { + (Some(check), Some(opening)) => { + Self::verify_opening_pair::(opening, check, query, iota) } (None, None) => true, _ => false, @@ -675,8 +783,9 @@ pub trait IsStarkVerifier< /// polynomial, where 𝜐 and -𝜐 are the elements corresponding to the index challenge `iota`. fn verify_composition_poly_opening( deep_poly_openings: DeepPolynomialOpeningView<'_, Field, FieldExtension>, - composition_poly_merkle_root: &Commitment, - iota: &usize, + check: &TreeCheck<'_>, + query: usize, + iota: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -691,10 +800,10 @@ pub trait IsStarkVerifier< composition_poly.evaluations_sym(), ); - verify_merkle_path_from_leaf_hash::>( + check.verify::>( + query, composition_poly.merkle_path(), - composition_poly_merkle_root, - *iota, + iota, leaf_hash, ) } @@ -705,6 +814,7 @@ pub trait IsStarkVerifier< fn step_4_verify_trace_and_composition_openings( proof: StarkProofView<'_, Field, FieldExtension, PI>, challenges: &Challenges, + checks: &TableTreeChecks<'_>, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -715,19 +825,125 @@ pub trait IsStarkVerifier< >(); // `step_3_verify_fri` (which runs before this) already rejects proofs // whose `deep_poly_openings` is shorter than `challenges.iotas`. - challenges.iotas.iter().enumerate().all(|(i, iota_n)| { + challenges.iotas.iter().enumerate().all(|(i, &iota_n)| { let deep_poly_opening = proof.deep_poly_opening(i); - Self::verify_composition_poly_opening( - deep_poly_opening, - proof.composition_poly_root(), - iota_n, - ) && Self::verify_trace_openings(proof, deep_poly_opening, *iota_n) + Self::verify_composition_poly_opening(deep_poly_opening, &checks.composition, i, iota_n) + && Self::verify_trace_openings(deep_poly_opening, checks, i, iota_n) + }) + } + + /// The per-tree Merkle checks of one table's proof, built ONCE per tree + /// before any query is verified. + /// + /// Every depth and cap height is a verifier constant ([`StarkCaps`], from + /// the AIR's options and the LDE size): the trace, precomputed, aux and + /// composition trees are `log2(lde) − 1` deep, committed FRI layer `i` is + /// the fold layout's `layer_depth(i)` deep (`log2(lde) − i − 2` under the + /// all-ones schedule, the group tree's depth under any other). Every authentication path must be exactly + /// `depth − c` long, at `c = 0` too: a path of any other length would be + /// folded and compared with the root, letting an internal node pass as a leaf. + /// + /// A capped tree (`c > 0`) reads its owner opening — query 0's path — here, + /// splits off the cap and checks it hashes to the root. That read is safe + /// by construction: the caller runs this only after the + /// `query_list_len` / `trace_opening_widths_well_formed` count guards, and + /// every access below is a length-checked `get`, so a proof with no + /// openings, too few FRI layers, or a missing aux/precomputed opening + /// rejects (`None`) and never panics. At `c = 0` (the default format) no + /// opening is read at all. + /// + /// The FRI layer count is `fri_termination_params(..).num_committed`; a + /// proof with a different number of layer roots is rejected here as in + /// `step_3_verify_fri`. + fn table_tree_checks<'a>( + air: &dyn AIR, + proof: StarkProofView<'a, Field, FieldExtension, PI>, + domain: &VerifierDomain, + ) -> Option> + where + FieldElement: AsBytes + Sync + Send, + FieldElement: AsBytes + Sync + Send, + { + let options = air.options(); + // A format this verifier cannot lay out rejects here, as in step 3. + let layout = Self::fri_termination_params(air, domain)?; + let num_committed = layout.num_committed; + let lde_log = domain.lde_length.trailing_zeros() as usize; + // Every depth from the table's layouts (row pairs: `log2(lde) − 1`; + // one row: `log2(lde)`; a group tree per FRI layer under a fold + // schedule) — at the default exactly `StarkCaps::new`'s. + let caps = StarkCaps::from_layout( + options.format.merkle_cap, + options.fri_number_of_queries, + lde_log, + &layout, + ); + let fri_roots = proof.fri_layers_merkle_roots(); + if fri_roots.len() != num_committed { + return None; + } + // The owner opening: query 0, read only for a capped tree and only + // once it is known to exist. + let owner = || (proof.deep_poly_openings_len() > 0).then(|| proof.deep_poly_opening(0)); + let (d, c) = (caps.trace_depth, caps.trace); + + let main = TreeCheck::build::>( + proof.lde_trace_main_merkle_root(), + d, + c, + || owner().map(|o| o.main_trace_polys().merkle_path()), + )?; + let precomputed = match proof.lde_trace_precomputed_merkle_root() { + Some(root) => Some(TreeCheck::build::>(root, d, c, || { + owner()?.precomputed_trace_polys().map(|p| p.merkle_path()) + })?), + None => None, + }; + let aux = match proof.lde_trace_aux_merkle_root() { + Some(root) => Some(TreeCheck::build::>( + root, + d, + c, + || owner()?.aux_trace_polys().map(|p| p.merkle_path()), + )?), + None => None, + }; + let composition = TreeCheck::build::>( + proof.composition_poly_root(), + d, + c, + || owner().map(|o| o.composition_poly().merkle_path()), + )?; + let fri = fri_roots + .iter() + .enumerate() + .map(|(i, root)| { + TreeCheck::build::>( + root, + caps.fri_depths[i], + caps.fri[i], + || { + (proof.query_list_len() > 0) + .then(|| proof.query(0)) + .filter(|q| q.layers_auth_paths_len() > i) + .map(|q| q.layer_auth_path(i)) + }, + ) + }) + .collect::>>()?; + Some(TableTreeChecks { + main, + precomputed, + aux, + composition, + fri, }) } /// Verifies the openings of a fold polynomial of an inner layer of FRI. fn verify_fri_layer_openings( - merkle_root: &Commitment, + check: &TreeCheck<'_>, + query: usize, auth_path_sym: &[Commitment], evaluation: &FieldElement, evaluation_sym: &FieldElement, @@ -743,11 +959,80 @@ pub trait IsStarkVerifier< vec![evaluation.clone(), evaluation_sym.clone()] }; - verify_merkle_path::>( + check.verify::>( + query, auth_path_sym, - merkle_root, iota >> 1, - &evaluations, + as IsMerkleTreeBackend>::hash_data(&evaluations), + ) + } + + /// Verify a single FRI query under the group encoding (S3 and S2; any + /// format but the legacy one), then [`crate::fri::group::verify_query_groups`] + /// for the committed layers and the terminal check. + /// + /// * Row pairs (`p0_eval_sym = Some`): fold 0 from the DEEP pair as today; + /// the zero-fold case is the legacy one (no layer, no challenge, both + /// points checked against the terminal codeword). + /// * One row (`p0_eval_sym = None`, S2): layer 0 IS the committed DEEP + /// codeword, so the query's value there is `DEEP(x_r)` itself and the + /// layer-0 slot check is the input-slot check `group₀[slot] == DEEP(x_r)`. + /// With nothing to fold the terminal codeword is the DEEP + /// codeword and `terminal[r] == DEEP(x_r)` is the whole check. + // Crate-internal layout type on a default method, as `fri_termination_params`. + #[allow(clippy::too_many_arguments, private_interfaces)] + fn verify_query_groups( + layout: &crate::fri::terminal::FriFoldLayout, + // One per committed layer (`table_tree_checks`, at the layout's group + // tree depths), and this query's position in proof order (query 0 is + // every capped layer's owner). + fri_checks: &[TreeCheck<'_>], + query: usize, + zetas: &[FieldElement], + iota: usize, + fri_decommitment: FriDecommitmentView<'_, FieldExtension>, + evaluation_point_inv: FieldElement, + p0_eval: &FieldElement, + p0_eval_sym: Option<&FieldElement>, + terminal_codeword: &[FieldElement], + roots_tables: &[Vec>], + ) -> bool + where + FieldElement: AsBytes + Sync + Send, + FieldElement: AsBytes + Sync + Send, + { + // The encoding of the DEEP value(s) must match the layout: a + // one-row layout has no symmetric value, a row-pair one needs it. + let (v, y_inv) = match (layout.one_row, p0_eval_sym) { + (true, None) => (p0_eval.clone(), evaluation_point_inv), + (false, Some(p0_eval_sym)) => { + if zetas.is_empty() { + return terminal_codeword + .get(iota * 2) + .is_some_and(|t| p0_eval == t) + && terminal_codeword + .get(iota * 2 + 1) + .is_some_and(|t| p0_eval_sym == t); + } + // Fold 0 (binary, uncommitted) consumes the DEEP pair: p₁(𝜐²). + let v = (p0_eval + p0_eval_sym) + + &evaluation_point_inv * &zetas[0] * (p0_eval - p0_eval_sym); + (v, evaluation_point_inv.square()) + } + _ => return false, + }; + crate::fri::group::verify_query_groups::>( + layout, + fri_checks, + query, + |j| fri_decommitment.layer_auth_path(j), + fri_decommitment.layers_evaluations_sym(), + zetas, + iota, + v, + y_inv, + terminal_codeword, + roots_tables, ) } @@ -769,6 +1054,10 @@ pub trait IsStarkVerifier< deep_composition_evaluation: &FieldElement, deep_composition_evaluation_sym: &FieldElement, terminal_codeword: &[FieldElement], + // One per committed layer (`table_tree_checks`), and this query's + // position in proof order (query 0 is every capped layer's owner). + fri_checks: &[TreeCheck<'_>], + query: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -809,19 +1098,20 @@ pub trait IsStarkVerifier< // previous iteration), then obtain pᵢ₊₁(𝜐^(2ⁱ⁺¹)). When there are no // committed layers (`total_folds == 1`, a single final fold) this fold is // empty and `v`/`index` already hold the terminal-layer value/position. - let openings_ok = fri_layers_merkle_roots + let openings_ok = fri_checks .iter() .zip(fri_decommitment.layers_evaluations_sym()) .zip(evaluation_point_vec) .enumerate() .fold( true, - |result, (i, ((merkle_root, evaluation_sym), evaluation_point_inv))| { + |result, (i, ((check, evaluation_sym), evaluation_point_inv))| { // Verify opening Open(pᵢ(Dₖ), −𝜐^(2ⁱ)) and Open(pᵢ(Dₖ), 𝜐^(2ⁱ)). // `v` is pᵢ(𝜐^(2ⁱ)). // `evaluation_sym` is pᵢ(−𝜐^(2ⁱ)). let openings_ok = Self::verify_fri_layer_openings( - merkle_root, + check, + query, fri_decommitment.layer_auth_path(i), &v, evaluation_sym, @@ -951,6 +1241,31 @@ pub trait IsStarkVerifier< ood_full: &Table, next_row_cols: &[usize], step_size: usize, + ) -> Option> { + Self::reconstruct_deep_composition_poly_evaluations_for_layout( + challenges, + domain, + proof, + ood_full, + next_row_cols, + step_size, + LeafLayout::RowPair, + ) + } + + /// [`Self::reconstruct_deep_composition_poly_evaluations_for_all_queries`] + /// under a leaf layout: for row pairs, DEEP at each query's two points + /// (`x`, `−x`); for one row, DEEP at the query's one point `x_r` and an + /// EMPTY symmetric vector (the openings carry no symmetric row). + #[allow(clippy::too_many_arguments)] + fn reconstruct_deep_composition_poly_evaluations_for_layout( + challenges: &Challenges, + domain: &VerifierDomain, + proof: StarkProofView<'_, Field, FieldExtension, PI>, + ood_full: &Table, + next_row_cols: &[usize], + step_size: usize, + leaf_layout: LeafLayout, ) -> Option> { let num_queries = challenges.iotas.len(); @@ -982,6 +1297,34 @@ pub trait IsStarkVerifier< step_size, )?; + if leaf_layout.is_one_row() { + for (i, r) in challenges.iotas.iter().enumerate() { + let opening = proof.deep_poly_opening(i); + let lde_precomputed: &[FieldElement] = opening + .precomputed_trace_polys() + .map(|p| p.evaluations()) + .unwrap_or(&[]); + let lde_aux: &[FieldElement] = opening + .aux_trace_polys() + .map(|a| a.evaluations()) + .unwrap_or(&[]); + let point = Self::query_point(leaf_layout, *r, domain); + deep_poly_evaluations.push(Self::reconstruct_deep_composition_poly_evaluation_at( + &point, + primitive_root, + challenges, + &query_invariant_terms, + next_row_cols, + step_size, + lde_precomputed, + opening.main_trace_polys().evaluations(), + lde_aux, + opening.composition_poly().evaluations(), + )?); + } + return Some((deep_poly_evaluations, deep_poly_evaluations_sym)); + } + for (i, iota) in challenges.iotas.iter().enumerate() { let opening = proof.deep_poly_opening(i); @@ -1038,6 +1381,88 @@ pub trait IsStarkVerifier< Some((deep_poly_evaluations, deep_poly_evaluations_sym)) } + /// The deep composition polynomial at ONE point (one-row openings, S2): + /// the same terms as [`Self::reconstruct_deep_composition_poly_evaluation_pair`] + /// at `evaluation_point` alone, with the same panic guards (a malformed + /// width, a zero denominator → `None`). + #[allow(clippy::too_many_arguments)] + fn reconstruct_deep_composition_poly_evaluation_at( + evaluation_point: &FieldElement, + primitive_root: &FieldElement, + challenges: &Challenges, + query_invariant_terms: &QueryInvariantDeepTerms, + next_row_cols: &[usize], + step_size: usize, + lde_trace_precomputed_evaluations: &[FieldElement], + lde_trace_main_evaluations: &[FieldElement], + lde_trace_aux_evaluations: &[FieldElement], + lde_composition_poly_parts_evaluation: &[FieldElement], + ) -> Option> { + let height = query_invariant_terms.ood_row_sum.len(); + let width = query_invariant_terms.ood_width; + let trace_term_coeffs = &challenges.trace_term_coeffs; + let num_precomputed = lde_trace_precomputed_evaluations.len(); + let num_base = num_precomputed + lde_trace_main_evaluations.len(); + let base_at = |col: usize| -> &FieldElement { + if col < num_precomputed { + &lde_trace_precomputed_evaluations[col] + } else { + &lde_trace_main_evaluations[col - num_precomputed] + } + }; + if num_base + lde_trace_aux_evaluations.len() != width { + return None; + } + + let mut denoms = Vec::with_capacity(height); + let mut current_z = challenges.z.clone(); + for _ in 0..height { + denoms.push(evaluation_point - ¤t_z); + current_z = primitive_root * ¤t_z; + } + FieldElement::inplace_batch_inverse(&mut denoms).ok()?; + + let mut trace_term = FieldElement::::zero(); + for (row_idx, denom) in denoms.iter().enumerate() { + let ood_row_sum = &query_invariant_terms.ood_row_sum[row_idx]; + let mut base_row_sum = FieldElement::::zero(); + let mut add = |col_idx: usize, coeff: &FieldElement| { + if col_idx < num_base { + base_row_sum += base_at(col_idx) * coeff; + } else { + base_row_sum += coeff * &lde_trace_aux_evaluations[col_idx - num_base]; + } + }; + if row_idx < step_size { + for (col_idx, coeff_col) in trace_term_coeffs.iter().enumerate() { + add(col_idx, &coeff_col[row_idx]); + } + } else { + for &col_idx in next_row_cols { + add(col_idx, &trace_term_coeffs[col_idx][row_idx]); + } + } + trace_term += denom * &(&base_row_sum - ood_row_sum); + } + + let number_of_parts = query_invariant_terms.number_of_parts; + if lde_composition_poly_parts_evaluation.len() != number_of_parts { + return None; + } + let denom_composition = (evaluation_point - &query_invariant_terms.z_pow) + .inv() + .ok()?; + let mut h_sum = FieldElement::::zero(); + for (h, gamma) in lde_composition_poly_parts_evaluation + .iter() + .zip(&challenges.gammas) + { + h_sum += h * gamma; + } + let h_terms = (&h_sum - &query_invariant_terms.h_sum_zpow) * denom_composition; + Some(trace_term + h_terms) + } + /// Reconstructs the deep composition polynomial evaluation at a query's /// point and its symmetric counterpart together. Rewriting the per-element /// trace term `coeff*(base-ood)*denom` as `denom*(coeff*base - coeff*ood)` @@ -1320,7 +1745,16 @@ pub trait IsStarkVerifier< if air.is_preprocessed() { // Preprocessed table: VERIFY precomputed commitment matches hardcoded. // This is the critical soundness check - ensures prover used correct precomputed values. - let expected_precomputed = air.precomputed_commitment(); + // The root of THIS table's leaf layout (a verifier constant); + // a layout the AIR has no root for rejects (never a recompute). + let layout = Self::leaf_layout(*air, trace_length); + let Some(expected_precomputed) = air.precomputed_commitment_for(layout) else { + error!( + "Preprocessed table {idx}: no precomputed commitment for the {layout:?} \ + leaf layout" + ); + return false; + }; match proof.lde_trace_precomputed_merkle_root() { Some(actual) if *actual == expected_precomputed => { // OK - commitment matches hardcoded @@ -1594,24 +2028,34 @@ pub trait IsStarkVerifier< // <<<< Receive challenges: 𝛾ⱼ, 𝛾ⱼ' let gammas = deep_composition_coefficients; - // FRI commit phase + // FRI commit phase. Under one-row openings (S2) the first root is the + // input tree (the DEEP codeword itself), absorbed BEFORE any folding + // challenge; every other root follows its challenge as today. + let leaf_layout = Self::leaf_layout(air, trace_length); let merkle_roots = proof.fri_layers_merkle_roots(); - let mut zetas = merkle_roots - .iter() - .map(|root| { + let mut zetas = Vec::with_capacity(merkle_roots.len() + 1); + for (j, root) in merkle_roots.iter().enumerate() { + if !(leaf_layout.is_one_row() && j == 0) { // >>>> Send challenge 𝜁ₖ - let element = transcript.sample_field_element(); - // <<<< Receive commitment: [pₖ] (the first one is [p₀]) - transcript.append_bytes(root); - element - }) - .collect::>>(); + zetas.push(transcript.sample_field_element()); + } + // <<<< Receive commitment: [pₖ] (the first one is [p₀]) + transcript.append_bytes(root); + } // The prover only samples the final-fold challenge when the codeword // actually folds past the committed layers. For tiny traces (the clamp // case) no fold happens, so no challenge is drawn. This must mirror the // prover's `commit_phase_from_evaluations` exactly. - let total_folds = Self::fri_termination_params(air, domain).total_folds; + // `total_folds` does not depend on the format (only its split into + // committed layers does), so the replay reads it from today's layout; + // a format the verifier cannot lay out is rejected in step 3. + let total_folds = crate::fri::terminal::FriFoldLayout::new( + domain.lde_length.trailing_zeros(), + (domain.lde_length / domain.trace_length).trailing_zeros(), + u32::from(air.options().fri_final_poly_log_degree), + ) + .total_folds; // >>>> Send final-fold challenge 𝜁_final (only when folding occurs) if total_folds > 0 { @@ -1637,7 +2081,7 @@ pub trait IsStarkVerifier< // FRI query phase // <<<< Send challenges 𝜄ₛ (iota_s) let number_of_queries = air.options().fri_number_of_queries; - let iotas = Self::sample_query_indexes(number_of_queries, domain, transcript); + let iotas = Self::sample_query_indexes(number_of_queries, domain, leaf_layout, transcript); Challenges { z, @@ -1686,6 +2130,17 @@ pub trait IsStarkVerifier< return false; } + // The per-tree Merkle checks, built once per tree and only now: after + // the two count guards above, so a capped tree's owner opening (query + // 0) is known to exist before it is read. A capped + // tree's cap is authenticated against its root here; at the default + // format this reads no opening at all. + let Some(tree_checks) = Self::table_tree_checks(air, proof, &domain) else { + #[cfg(not(feature = "test_fiat_shamir"))] + error!("Merkle cap or path shape does not match the proof format"); + return false; + }; + // The pruned-OOD layout, read from the AIR once and shared by the round-4 // challenge replay, the block-shape guard, the single grid reconstruction, // and both verify steps below — one reconstruction instead of the previous @@ -1706,6 +2161,8 @@ pub trait IsStarkVerifier< rap_challenges, &layout, ); + #[cfg(any(test, feature = "test-utils"))] + crate::fri::capture::record_challenges(&challenges.zetas, &challenges.iotas); // verify grinding let grinding_factor = air.context().proof_options.grinding_factor; @@ -1792,6 +2249,7 @@ pub trait IsStarkVerifier< &ood_full, layout.next_row_cols(), layout.step_size(), + &tree_checks, ) { #[cfg(not(feature = "test_fiat_shamir"))] error!("FRI verification failed"); @@ -1809,7 +2267,7 @@ pub trait IsStarkVerifier< let timer4 = Instant::now(); #[allow(clippy::let_and_return)] - if !Self::step_4_verify_trace_and_composition_openings(proof, &challenges) { + if !Self::step_4_verify_trace_and_composition_openings(proof, &challenges, &tree_checks) { #[cfg(not(feature = "test_fiat_shamir"))] error!("DEEP Composition Polynomial verification failed"); return false; diff --git a/crypto/stark/tests/vectors/zf_fri/README.md b/crypto/stark/tests/vectors/zf_fri/README.md new file mode 100644 index 000000000..10539a060 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/README.md @@ -0,0 +1,143 @@ +# S3 and S2 FRI vectors (group-leaf FRI layers, one-row openings) + +Test vectors for the S3 proof-format lever (`LAMBDA_VM_ZF_FRI=dp`, +`ProofFormat.fri_mode = FriMode::Dp`): committed FRI layer `j` folds by +`2^{d_j}` and commits groups of `2^{d_j}` consecutive values per leaf. They are +the oracle for the device prover (group-leaf commits, multi-fold kernels, query +gathers) and the in-guest verifier (group folds, group-leaf walks, slot checks). + +Every file is generated by `crypto/stark/src/fri/vectors.rs` and checked by a +test that regenerates it in memory and requires it byte-equal to this copy: + +| files | test (fails if stale) | regenerate | +|---|---|---| +| `a_*`, `b_*`, `c_*_keccak`, `c_*_blake3`, `d_*_keccak_*`, `d_*_blake3_*`, `e_*_keccak*`, `e_*_blake3*` | `cargo test -p stark --lib zf_fri_vectors::vectors_are_current` | `cargo test -p stark --lib zf_fri_vectors::write_vectors -- --ignored` | +| `c_*_rpx`, `d_*_rpx_*`, `e_*_rpx*` | `cargo test -p lambda-vm-prover --lib tests::zf_rpx_vectors::rpx_vectors_are_current` | `cargo test -p lambda-vm-prover --lib tests::zf_rpx_vectors::write_vectors -- --ignored` | + +Regenerate only for a deliberate format change (the schedule DP, its weights, +the fold, the leaf encoding): a stale file means the format moved. + +## Conventions + +- Field: Goldilocks `p = 2^64 − 2^32 + 1`. An extension element is `[c0, c1, c2]` + (canonical `u64` limbs) of `Degree3GoldilocksExtensionField`. +- A layer of length `n = 2^b` on the coset `o·⟨ω_n⟩` is stored in + **bit-reversed order**: position `p` holds `f(o·ω_n^{br_b(p)})`, where + `ω_n = F::get_primitive_root_of_unity(b)` (the LDE domain's root). +- Binary fold (unchanged, `fri_functions::fold_evaluations_in_place`): for the + pair at positions `(2j, 2j+1)` = points `(x_j, −x_j)`, + `out[j] = (lo + hi) + x_j⁻¹·ζ·(lo − hi)` (no ½: the terminal polynomial + absorbs the `2^{total_folds}`). Then the coset squares: offset `o → o²`. +- **Group fold** of exponent `d` with challenge `ζ`: `d` binary folds with + `ζ, ζ², ζ⁴, …, ζ^{2^{d−1}}`. It equals `2^d·Σ_{i<2^d} ζ^i f_i(Y)` for + `f(X) = Σ X^i f_i(X^{2^d})`. +- **Group leaf** `g` of a layer with exponent `d` = the values at positions + `g·2^d .. (g+1)·2^d` (the fiber `x_g·⟨ω_{2^d}⟩`, bit-reversed), hashed as ONE + leaf with the configuration's `Batched` leaf backend over those `2^d` ext + values in position order (`hash_data_from_slices(group, [])`). Parents use + the configuration's parent hash (as every STARK tree). At `d = 1` this is + exactly today's pair leaf (the `StarkHash` two-element invariant). +- A query with pair index `ι` (sampled as today, `sample_u64(lde/2)`) sits at + position `p_1 = ι` of committed layer 1 (fold 0 is the uncommitted binary + fold of the DEEP pair `p₀(υ), p₀(−υ)`, `υ` = LDE point at position `2ι`). + At committed layer `j`: `leaf = p_j >> d_j`, `slot = p_j & (2^{d_j} − 1)`, + `p_{j+1} = p_j >> d_j`. The terminal position is `ι >> Σ d_j`. +- Checks per layer: the group hashes to the leaf and authenticates at `leaf` + with a path of exactly `layer_log_len − d_j` siblings; `group[slot] == v` + (the value the previous fold produced); `v ← group fold with ζ_{j+1}`, + using `x_g⁻¹ = y⁻¹·ω_{2^d}^{br_d(slot)}` (`y` = the query's point at this + layer). Finally `terminal[p] == v`. +- Transcript (unchanged in form): `γ` → per committed layer: sample `ζ_j`, + append `root_{j+1}` → sample the final `ζ` (if anything folds) → terminal + coefficients → grinding nonce → `ι`s. `zetas` has `layers + 1` entries. +- Proof encoding: under `dp` the flat `layers_evaluations_sym` of each query + carries every layer's FULL group (`Σ 2^{d_j}` values, the query's own value + included); under `pair` (today) one sibling per layer. + +## Files + +**(a) `a_schedules.json`** — the fold-schedule DP (`fri::schedule::fri_schedule`) +at terminal logs `T ∈ {4, 9, 10}`, queries `Q ∈ {3, 110}`, cap `off`/`auto`, +LDE log `B = 6..24`, chains `s3` (from `b0 = B − 1`, row-pair openings) and +`s2` (from `b0 = B`, for S2 later). `cost_q_ns` is `Q ×` the per-query +cost-law price (`weights_ns` in the file header). Production: +base legs `T = 9`, LFM proofs `T = 10`, `Q = 110`. + +**(b) `b_group_folds.json`** — the KAT codeword: `2^7` ext values on the coset +`3·⟨ω_128⟩` (bit-reversed). Value `i` = three consecutive SplitMix64 outputs +(each reduced mod p) from state `0x5a4646524933` (`fri::vectors::splitmix64`: +`s += 0x9e3779b97f4a7c15; z = s; z = (z ^ z>>30)·0xbf58476d1ce4e5b9; +z = (z ^ z>>27)·0x94d049bb133111eb; out = z ^ z>>31`). For `d = 1..6`: +`zeta` (SplitMix64 from state `0x5a4646524933 + d`) and `folded`, the codeword +after `d` folds (length `2^{7−d}`, bit-reversed on the coset `3^{2^d}·⟨ω⟩`). +The generator asserts the verifier's group fold of every group reproduces it. + +**(c) `c_leaf_digests_{keccak,blake3,rpx}.json`** — for `d = 1..6`: the leaf +digest of the KAT codeword's first group (values `0 .. 2^d`), and the root of +the whole KAT codeword committed as a group-leaf layer tree (`2^{7−d}` leaves). +Digests are the 32-byte node encoding, hex. + +**(d) `d_proof_{keccak,blake3,rpx}_{pair,dp,dp_3_1_3,cap_pair,cap_dp}.{json,rkyv}`** — one small +proof per format: `LogReadOnlyRAP` (one aux column), +`2^10` rows of reads `(i % 5 + 1, 10·(i % 5 + 1))`, blowup 4 (so `B = 12`), +`fri_final_poly_log_degree = 2` (`T = 4`), 3 queries (20 for the `cap_*` +formats), grinding 0, coset +offset 3, proved with `DefaultTranscript::new(&[])` by `GenericProver<…, H>`. +`.rkyv` is the proof's rkyv bytes (`StarkProof`, the wire format of record). +The JSON has the layout (`schedule`, `legacy_encoding`, `total_folds`, +`terminal_len`), the FRI `fri_roots`, all `zetas` and `terminal_coeffs`, and +per query `iota`, the DEEP pair (`deep` = p₀(υ), `deep_sym` = p₀(−υ)), +`terminal_position`, and per layer `position`, `leaf`, `slot`, the opened +`values` (the full group under dp; the single sibling under pair) and the +authentication `path_len`. Formats: `pair` (today, all-ones schedule), +`dp` (the DP's schedule at `Q = 3`, cap off: `[3, 2, 2]`), `dp_3_1_3` (an +explicit uneven schedule via the test hook `fri_schedule_override`: unequal +neighbouring exponents are what catch a fold-count off-by-one), and the +Merkle-cap pair: `cap_pair` (`LAMBDA_VM_ZF_CAP=auto`, today's +FRI) and `cap_dp` (`auto` cap and the DP's schedule), at `Q = 20` so that +`auto` caps every tree at height 3. Their JSON adds `merkle_cap`, +`trace_tree_depth`, `trace_cap`, `fri_tree_depths` and `fri_caps` (the +verifier's `StarkCaps`); each capped tree's `2^c` cap nodes ride at the end of +query 0's authentication path (the owner path, `D − c + 2^c` nodes; every +other query carries `D − c`). The cap changes no transcript value: `cap_dp`'s +roots and `zetas` equal `dp`'s. + +**(e) S2 — one-row openings with a committed FRI input.** + +`e_leaf_digests_{keccak,blake3,rpx}.json` — one-row trace-tree leaves: a KAT +base matrix (16 rows × 5 columns, SplitMix64 from `KAT_SEED + 100`, one output +per value, reduced mod p) and an ext3 matrix (16 rows × 2 columns, from +`KAT_SEED + 200`, three outputs per value), each read as bit-reversed LDE +columns and committed at `rows_per_leaf = 1` (leaf `i` = the row at +bit-reversed position `i`) and, for comparison, at `rows_per_leaf = 2` (today: +rows `2i`, `2i + 1`). Every leaf digest and both roots per layout. A leaf +hashes the row's values column by column (`leaves_bit_reversed_grouped`, the +same stream the verifier's `hash_data_from_slices(evaluations, [])` hashes). + +`e_proof_{keccak,blake3,rpx}_{one_row_pair,one_row_3_2_1_2}.{json,rkyv}` — the +(d) proof shape (same AIR, trace, blowup 4, `B = 12`, `T = 4`, `Q = 3`, +grinding 0) proved with `ProofFormat.one_row = On`: +- every trace, aux and composition tree commits ONE row per leaf and is + `B = 12` deep (`trace_tree_depth`); a query index `r` is uniform over the + whole LDE (`query_bound = 4096`, not 2048) and opens leaf `r` of every trace + tree (`trace_leaf`, `trace_path_len`); openings carry no symmetric row; +- `deep` is DEEP at the ONE point `x_r` = the LDE point at bit-reversed + position `r` (there is no `deep_sym`); +- FRI layer 0 is the INPUT tree: the DEEP codeword itself (`2^12` values, + bit-reversed), committed with groups of `2^{d_0}` values; its root is + `fri_roots[0]` and is absorbed BEFORE the first folding challenge. Transcript: + `γ` → append `root_0` → per later layer: sample `ζ`, append its root → sample + the final `ζ` → coefficients → nonce → `r`s. So `zetas` has one entry per + layer (layer `j` folds with `zetas[j]`), against `layers + 1` for row pairs; +- per layer `j`: `position = r >> Σ_{i> d_j`, + `slot = position & (2^{d_j} − 1)`; layer 0's slot check is the input-slot + check `group₀[slot] == deep`; the terminal position is `r >> Σ d_j`. +- Formats: `one_row_pair` (fri = pair: the all-ones schedule from `B`, eight + pair layers, group encoding), `one_row_3_2_1_2` (an explicit uneven schedule, + `Σ = 8 = B − T`). + +## Not here yet + +- A capped vector under one-row openings (the cap composes with one_row on + the host, `one_row_tests::cap_fri_one_row_matrix_round_trips`, but only the + row-pair `cap_pair` / `cap_dp` files are exported). diff --git a/crypto/stark/tests/vectors/zf_fri/a_schedules.json b/crypto/stark/tests/vectors/zf_fri/a_schedules.json new file mode 100644 index 000000000..38e9fdf84 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/a_schedules.json @@ -0,0 +1,463 @@ +{ + "generator": "stark::fri::vectors::schedules_json", + "weights_ns": {"compress": 2251, "select": 567, "unpack": 528, "hint": 460, "compare": 3789, "fold": 2610, "twiddle": 477, "xalu": 522, "balu": 477}, + "dmax": 6, + "rows": [ + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [1], "cost_q_ns": 85443}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 124764}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 124764}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 192378}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 192378}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 269196}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 269196}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 336810}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 336810}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 414258}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 414258}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 500910}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 500910}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 578358}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 578358}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 665640}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 665640}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 762126}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 762126}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 849408}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 849408}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 946524}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 946524}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 1052844}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 1052844}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1149960}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1149960}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 1256910}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 1256910}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 1373064}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 1373064}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1480014}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1480014}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1596798}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1596798}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 3, 2, 2], "cost_q_ns": 1722786}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 3, 2, 2], "cost_q_ns": 1722786}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3, 3, 2], "cost_q_ns": 1839570}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [1], "cost_q_ns": 85443}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 124764}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 124764}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 192378}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 192378}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 269196}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 269196}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 336810}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 336810}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 414258}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 414258}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 500910}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 500910}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 578358}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 578358}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 665640}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 665640}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 762126}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 762126}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 849408}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 849408}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 946524}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 946524}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 1052844}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 1052844}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1149960}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1149960}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 1256910}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 1256910}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 1373064}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 1373064}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1480014}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1480014}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1596798}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1596798}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 3, 2, 2], "cost_q_ns": 1722786}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 3, 2, 2], "cost_q_ns": 1722786}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3, 3, 2], "cost_q_ns": 1839570}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [1], "cost_q_ns": 3132910}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 4574680}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 4574680}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 7053860}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 7053860}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 9870520}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 9870520}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 12349700}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 12349700}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 15189460}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 15189460}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 18366700}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 18366700}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 21206460}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 21206460}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 24406800}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 24406800}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 27944620}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 27944620}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 31144960}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 31144960}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 34705880}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 34705880}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 38604280}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 38604280}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 42165200}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 42165200}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 46086700}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 46086700}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 50345680}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 50345680}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 54267180}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 54267180}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 58549260}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 58549260}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 3, 2, 2], "cost_q_ns": 63168820}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 3, 2, 2], "cost_q_ns": 63168820}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3, 3, 2], "cost_q_ns": 67450900}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [1], "cost_q_ns": 2569066}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 4010836}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 4010836}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 6490016}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 6490016}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 8742832}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 8742832}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 11222012}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 11222012}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 14061772}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 14061772}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 16675168}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 16675168}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 19514928}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 19514928}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 22715268}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 22715268}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 25689244}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 25689244}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 28889584}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 28889584}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 32450504}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 32450504}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 35785060}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 35785060}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 39345980}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 39345980}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 43267480}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 43267480}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 46962616}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 46962616}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 50884116}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 50884116}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 55166196}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 55166196}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 3, 2, 2], "cost_q_ns": 59221912}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 3, 2, 2], "cost_q_ns": 59221912}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3, 3, 2], "cost_q_ns": 63503992}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [1], "cost_q_ns": 134613}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 134613}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 173934}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 173934}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 241548}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 241548}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 367536}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 367536}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 435150}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 435150}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 512598}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 512598}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 648420}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 648420}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 725868}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 725868}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 813150}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 813150}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 949002}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 949002}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 1046088}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 1046088}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 1143204}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 1143204}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 1279056}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 1279056}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1395810}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1395810}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 1502760}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [1], "cost_q_ns": 134613}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 134613}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 173934}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 173934}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 241548}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 241548}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 367536}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 367536}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 435150}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 435150}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 512598}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 512598}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 648420}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 648420}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 725868}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 725868}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 813150}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 813150}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 949002}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 949002}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 1046088}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 1046088}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 1143204}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 1143204}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 1279056}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 1279056}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1395810}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1395810}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 1502760}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [1], "cost_q_ns": 4935810}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 4935810}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 6377580}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 6377580}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 8856760}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 8856760}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 13476320}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 13476320}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 15955500}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 15955500}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 18795260}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 18795260}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 23775400}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 23775400}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 26615160}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 26615160}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 29815500}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 29815500}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 34796740}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 34796740}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 38356560}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 38356560}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 41917480}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 41917480}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 46898720}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 46898720}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 51179700}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 51179700}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 55101200}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [1], "cost_q_ns": 4371966}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 4371966}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 5813736}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 5813736}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 8292916}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 8292916}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 12348632}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 12348632}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 14827812}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 14827812}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 17667572}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 17667572}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 22083868}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 22083868}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 24923628}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 24923628}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 28123968}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 28123968}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 2, 2], "cost_q_ns": 32900844}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 2, 2], "cost_q_ns": 32900844}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 36101184}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 36101184}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 39662104}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 39662104}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 44643344}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 44643344}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 48360480}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 48360480}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 52281980}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [1], "cost_q_ns": 144447}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 144447}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 183768}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 183768}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 251382}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 251382}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [2, 2], "cost_q_ns": 387204}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [2, 2], "cost_q_ns": 387204}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 454818}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 454818}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 532266}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 532266}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [4, 3], "cost_q_ns": 668118}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [4, 3], "cost_q_ns": 668118}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 755370}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 755370}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 842652}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 842652}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 978504}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 978504}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 1085424}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 1085424}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 1182540}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 1182540}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 1318392}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 1318392}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1444980}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [1], "cost_q_ns": 144447}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 144447}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 183768}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 183768}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 251382}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 251382}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [2, 2], "cost_q_ns": 387204}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [2, 2], "cost_q_ns": 387204}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 454818}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 454818}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 532266}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 532266}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [4, 3], "cost_q_ns": 668118}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [4, 3], "cost_q_ns": 668118}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 755370}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 755370}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 842652}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 842652}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 978504}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 978504}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 1085424}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 1085424}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 1182540}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 1182540}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 1318392}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 1318392}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1444980}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [1], "cost_q_ns": 5296390}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 5296390}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 6738160}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 6738160}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 9217340}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 9217340}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [2, 2], "cost_q_ns": 14197480}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [2, 2], "cost_q_ns": 14197480}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 16676660}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 16676660}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 19516420}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 19516420}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [4, 3], "cost_q_ns": 24497660}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [4, 3], "cost_q_ns": 24497660}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 27696900}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 27696900}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 30897240}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 30897240}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 35878480}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 35878480}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 39798880}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 39798880}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 43359800}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 43359800}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 48341040}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 48341040}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 52982600}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [], "cost_q_ns": 0}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [1], "cost_q_ns": 4732546}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 4732546}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 6174316}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 6174316}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 8653496}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 8653496}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [2, 2], "cost_q_ns": 13069792}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [2, 2], "cost_q_ns": 13069792}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 15548972}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 15548972}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 18388732}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 18388732}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 2, 2], "cost_q_ns": 23165608}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 2, 2], "cost_q_ns": 23165608}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 26005368}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 26005368}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 29205708}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 29205708}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 34186948}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 34186948}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 37543504}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 37543504}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 41104424}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 41104424}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 46085664}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 46085664}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 50163380} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/b_group_folds.json b/crypto/stark/tests/vectors/zf_fri/b_group_folds.json new file mode 100644 index 000000000..c596389b8 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/b_group_folds.json @@ -0,0 +1,14 @@ +{ + "generator": "stark::fri::vectors::group_fold_json", + "layer_log": 7, + "coset_offset": 3, + "codeword": [[114248373298330572,9950395923948974552,18292651019826168847],[11264014397209282797,17011101826315946882,7833410878953924418],[5067401236789812724,11181820620780326085,3913229743111987323],[6878663313357031824,4948368221433110479,4512378869763498949],[7300424359380325951,2629022478318430713,2352490085885205544],[3345914654228628712,17857800835092815061,4926885334099255041],[13961999760601160978,1108987075972034959,9069506549887126910],[3666212158788092907,15984312022772250051,13786698764199474274],[18233060593890754966,10696033068098814113,17116650682688893057],[1890999107425815346,13272904155828474695,15745756932851453243],[2002649893950066027,10673426297906541463,5573335164912747759],[14252812568771729760,8622463572814113680,16610631961562864340],[12657518525956593423,456049067459718558,6504017001053832],[7829250107108091587,10926092578370219313,15367852261670847390],[4283296287331528321,15615269286480435397,9412341781204946411],[17483792075638818644,17606390452909413141,16358299522213780360],[15538447972185760193,614856646744234365,14918316599210172173],[13828376987499133466,7148424706229729581,18158364113742349760],[13317215957353694181,9553347083671652872,5030147550646492824],[18336842288707666825,40487482472454760,5308286461165749931],[10834483197013613298,2298902883281893103,9942650691919595847],[11525892593642558393,9577750050426263731,2069904186241791718],[12406808437096063139,18073864409508715723,9230946790196975681],[3616632367654660274,10085665700693958697,16234982436400875270],[11917376668558775704,1901622792059622932,4968714500333547882],[4392135747981402671,5045545397172485329,15159213888871909626],[16061598631806973090,16901853911822821748,2267463094830397080],[4727640163684891461,9538126371028842686,9262479336654913939],[10304857519585917067,8918162236608948478,10808080315658145629],[13297830096390482799,4157291057334423461,15367988991022445219],[4469526150015381313,17786059701133525583,3006562354700561941],[9644214213943849393,657307829680962020,8280808660067407727],[6478239413655729580,18326407550008754007,11164300005736105706],[7471085016665548590,4020868342565639949,12446775137750043372],[11263844806142541136,8163614071841666446,504867900502982859],[2159692865036309446,5561796258936018645,4890282858963284249],[2258498590121909142,5188649223164923014,6006554213473999361],[16573169297305620351,7930659280834829778,3035332598973915968],[17592510683391522494,10588162808097688243,2750314556565551745],[1715278030362642140,17922857671664280100,17783735148214017483],[16877780672179297763,13713324251228354245,3672315061421186130],[11957769149749177020,7483809133384536686,13037449287285835030],[6060158857796037968,9236857535693839353,12225811918673374846],[6771836030305042448,5598588681544690427,736358855645767628],[14779764761098060676,9663318929005065160,7646712406485710697],[10352335144026281330,17034102847782780786,15015319655928055430],[1633346440070508358,18371146929211533278,13712502342528757365],[6697173780674409885,9465979684532956528,9986298627803261808],[1293703708762286999,15474728880582396103,11846671819987630109],[16286799938735909326,14556742360048391013,4621869127488820336],[3218948681922449986,221947035006398726,18253852596682421145],[5664709069538661127,4659564549683834986,9161648188075992511],[4114697847890739171,11027906093491171326,5104702896789353783],[5359187956347809939,4089103937352895916,2940850950576450587],[17399476639146650887,5016017632778311111,7271973075312826604],[8550063129321630329,5052407800723282653,9652683408301892866],[14047321464110593862,10155502225800659400,7644371686383087249],[2944575710255990900,8979321450340805175,1650833521640647570],[8476354023472637773,11675470599792868270,3889936884919286636],[4576427959904738712,13458256969793770315,13656500986899329602],[3314515815155456785,18041964041297850883,5747108416396853662],[16339498612485786868,4924956705534843858,18134066455875036650],[13147536215094553671,15997716004885695733,285745296538690591],[14255722797880109791,7210927213576476088,17587677203842504678],[11860626404695384951,8746280467222948941,2592292097704094105],[5717377429282337416,15899322474938656172,9164145311561944609],[8790455045054538216,8200372301439147858,4268466785537731197],[16115353753741389192,16439022717181043398,349125038085494325],[16242984037949079700,2000284139079405798,1102724556130734343],[17534186906134655633,12270638421670546342,2117549142856690160],[8558441615229845283,590680743110320488,846899180246940639],[8542947849373777247,8239462877891411699,13543070908409877225],[3806515466354321427,16677093996613821429,16921085645841743272],[4047188426270378454,10565945368133880359,17594120901052187483],[7520285678261358264,486417986208464051,211120145804645863],[2334355826641841630,10207668950452587969,9309038626488644891],[5508103946177442353,15433466943821065373,13938026508879038839],[10691340797952801443,13249035363727977704,16549098586767055074],[3189700400573926573,8505018310981494158,14989412664038149386],[1971071720876537511,8957196981073330551,3168195186127451837],[3370211075940519112,18221121112327787210,6388629410921300698],[13825718672353564576,2504067456039422864,14533168195148200715],[5098493520843201029,8869953106041465055,8122558809048000182],[18430351289255880067,8351103019978092501,11781980863743298682],[9373833362978281396,844921688433531872,18324413337107299243],[13485327368693733374,10691899904332665196,13229367546756152914],[1258912480864571676,2071340549663549730,6758277227815971576],[9549818200437251075,3405125360623851175,16942008793677491431],[13433015808893608304,9222727569003177390,14687583678862150856],[4894349561543165168,10011853121155194320,9907724893228480846],[12041518508022051244,14979367150570215901,3947232093288738331],[11640383997489081340,12156286191378664878,3386069373993561222],[14532281185964301339,17117192326887343454,12633381051772056545],[17985763565350791897,14532411921854015955,4139666522539931465],[10357060237166899055,10250685368132853801,4328663504089949328],[4264850046336035568,15127401253622843536,222690904877854589],[103201182986371004,7320118757475979880,984802483944950906],[4914621342385792092,4235556697395691596,2043652815146446309],[4532406341643755749,14046023185726959179,3407252364328999400],[15570319685166093974,11598076819033158060,10721222638522050524],[11649937100850667658,17866319465093320968,142299381593219063],[11567600299112655079,14716338034173449979,4226948256144437849],[17648997948271597061,4104103481584022969,9423885717708494854],[1800923050060833384,16555506371273926041,498679432809679599],[12532533693534476990,10541261484153048609,2029747857339337056],[17700837854248686544,8203001322382349030,15904685394162363036],[16467223518425325962,7471482565975648931,4501907999120368192],[13531965789846280913,3600794695377887032,7838235768256633532],[686248293220456248,4108049970362787107,6235191269358902257],[12274949027939456860,15170327482863637391,14551777247649584285],[4935455115743580892,17399757865092689306,12479638661441169770],[1141676659857718415,15295011150279022529,5183519654745430110],[14342582113481762098,9747082398264244127,7785589445197325825],[10731773423360016215,7909315320595870046,9263859799229246894],[16219555816209372043,14587867657169784888,13701890000248380413],[10735217865192115671,18281444572297642773,14564753480394576404],[4083176687777968338,9886723294778689857,2639615648938171605],[11804598711561790479,14108740349310438693,16344856194162855154],[12511529987086828495,15492173228579281126,7332520940589871870],[8790290147053500537,15807794493185089187,8793068148887118703],[3376003944858192249,13739302494003574116,62782776568845328],[16790760525790734333,16991944191494589086,14551119359498268396],[16553479249857101463,13158060559356436102,12798557079200239492],[9074427498021082773,2169770807462520334,3610202083964275218],[506760479544331739,11162253383698480455,836801623611651750],[5626003773148940461,13711907462553979908,3200744853949011781],[8991193990761346695,13002673796626326441,8147380006804403512],[15640945099500338290,13502092422384284689,2357524241674917606]], + "folds": [ + {"d": 1, "zeta": [5982986140143379172,8639098839961006196,9106571855708565181], "folded": [[13041342392770451398,1367539920642605247,12923439445535733123],[15111846465231705251,10869098118386980757,2110572119091924200],[1743889628524016675,9605940437084964891,5870648398677220037],[6785059710301850641,14432698623008569523,5117724888539462515],[6129637404252570793,15785473664722944987,15376383444291444540],[2830472080319199117,6916797001546272848,1139387193878398347],[854234618140711067,12865338332827840657,7683341066234270028],[8556763960483054921,14226068309370485405,8805068317203179907],[11652032324272791418,17067109057057488352,2155226401905577343],[7378977493906685189,7396070752922181160,14809630492126546402],[11221476835047076052,5552290674014561395,12839848385284537238],[17031632340357046723,18418693201929269666,13281360691649993105],[14947913625120789885,8869931497227681431,6516962090810790237],[4326895494534626222,210001341783279907,15836165149567730137],[4565156726448738349,1579099250129172856,11598873810743137497],[5081435166149576077,17735684136426917354,6036322550169833380],[3968018905876962370,9352856551674136594,256398433064191051],[5820988155798552494,16230330221979405516,5442468488737945834],[15049537498785237890,13862744868893397470,14510812512146738273],[2768819311689065150,11555171857964503456,9951892334960521639],[5497873220595924588,17485581362478882607,10739818946094945949],[12392623511920994647,10343193041454038320,11232463150930599175],[4625036141519292479,13478581817394022221,8433652285864766485],[8687067248403931348,14109209674648261023,8070097095869626139],[12511484557452774393,1615115631906611225,16808972079402084194],[12670112237463559315,16792075638721230118,4649929775577578039],[13963863236547405391,1737948292933472334,3462705389007925732],[13961623756411541908,17272945038285068896,13053563848099968430],[8542033251067556043,3943859879856553609,1365063928448331070],[2728051694180779145,10148083909313286564,3223039336597638137],[9969089259162820539,17201853852727321770,17280168509961302772],[3881414877058380274,16498435535752619897,15169721157991250091],[12282716117858167167,17479863209036482076,6900099660425552793],[5363085088357372880,12899192158122355397,13941244711242550925],[11954766328831456765,9114697862560243873,4708816652260351582],[4661049181802151049,4160451092099917117,15731571421871714693],[10394212062234425622,13543302894196970343,7268132449793459504],[14805027189806919077,6120844744139351106,8713141837178140203],[14364702613876332182,279691163524163244,3233426636605062982],[17638057925580708952,11880443875791088541,12663664333800944631],[2747387356761608808,6128024573264497456,1906647192604679604],[18142783074158655012,6930998950528696333,5966402208739676722],[5228353224625780153,11776267822025310956,7789010923833663159],[9198934511149137839,18297187364872279600,1100924458194065309],[15830556237832765901,15481187844668569608,5745954936691172836],[2463446672805614173,17654813086499958284,6627425475604431075],[9744974175376768951,6003907175064199467,3019863289756215214],[1105660295481585495,2846670207270484742,13893045392786235471],[8033640395713514621,2058287371987793338,16306955591035758260],[16718195256006779193,12845025146677090529,15507111131880041003],[17450061014428799367,8932849712635572444,1399273411695530237],[15314353830639146281,1617556098418450755,9680944244630765095],[7866028852159950947,5854581393547112260,1209047338584429762],[16498824236346505226,5525498508776325306,16356508675665315508],[2932115578077162425,14823813025467406358,8601995247003571190],[17627413824927498465,10363734279082421342,9882196638918242818],[8646412436728893485,4209536846449535861,5211710103494064584],[16883885278496934465,14398134661515437444,8320406202240514852],[1749674726038026642,10561302741404371186,1715480365650746116],[10098522374844915378,804272538400298735,11436350855199609294],[2242210078462043777,12856281862345183327,2934427225584611373],[8999664123015327012,3103813813738847199,6919160343947595269],[16973230894532108938,8182849463983648185,4979542969698307285],[1968658373866109019,4665310856168057167,8922308844881140926]]}, + {"d": 2, "zeta": [8277629894573820398,11987396219901710093,14286132552350819815], "folded": [[10659459074290418840,11404377731597685792,6412680166265662870],[18049568505121810974,10282830458869143188,10391546737972078780],[5253731277067850711,17621783193604886697,13132862192775322516],[11410018894194817656,16637267147743673871,15691689860434995800],[15506678364970685080,14861538350404551244,2837968110277602935],[2375804235161086170,8739888673911906050,4578176649759128810],[11442767291103846881,17342277446275794116,12662010138142028184],[14193068335114990813,13299033286367041493,8752101333196968920],[8620817047384791266,772804132056888022,3013625826044251668],[17723345225477209951,14603399632728913506,13406293657603104633],[4812604701894865410,3562163093929977386,15780981544278663169],[15273963609109427358,11988463685517847983,9902509355838126711],[4774731828063032978,17552776897472460989,2880706347154895482],[5738180859617894461,3267965271741550920,17196156652613304319],[6718748369866396894,13753766810220425783,14436843765103965982],[5096396145492219879,8332076396846918178,16515416299487658941],[12756397208828506185,14529843240352940945,1630979625933120288],[10236252437520398367,6121362081952229925,9675066952179694897],[12876998450208355897,5602539663494599642,16419361351446313034],[17633560925173793293,10491185305687269016,17609214207103544675],[9732322475272580838,2343921293067816168,11514857875151018473],[13301737884115171269,6768114510583834753,8437194746277735446],[7268976342321786428,6451174973850490262,17039742468036691953],[14197904117477439251,3256501541063215422,6078363490531782656],[18436670341339182065,4740965983665404202,13097850020662306658],[11643420520201444238,13894047914357275925,14847322032688363344],[6172254798990809302,9944297163286485901,16114164081446284441],[8126667779381531756,4335237177165305128,17759934364874626226],[2592542309881532259,3824543554027954290,7149789287312016189],[4722752886837845508,12270618432482714981,11136091373728017350],[15757453116814468536,4821251310523727862,11444859270510782763],[9887722687648353215,1752436496823926870,8599976724602573013]]}, + {"d": 3, "zeta": [1552802620964980016,1023686708859178672,17199325712939756230], "folded": [[14918904049632525799,12846102155294064811,12915689463213974410],[13752095238017906920,5561348862729856162,8163198169507984328],[11958998197578919994,1714262070066328704,13055202499057967831],[12579394395986170933,15453469291495055084,12511065676009529992],[5909815762699180536,325949289899580405,7602105144051417564],[8766981089440784624,12849391202710350407,10656754887032826357],[9043352336673692541,8082743422328089822,17620199666704121177],[1547345145325217649,17789026623191553517,13155450911809985342],[10770466821207224048,17638400349644439368,17680774930596894643],[3195520839440654360,358031439555466513,14635703796729568535],[13997904731947774862,2254971366004956008,4134927195987875200],[9792028107437876456,3202983791892939142,13683282596889965866],[5928029347378441967,7321247732850393678,12853557145597527353],[15655424873278218072,9543164650639120857,11400932202697208831],[1071396197694857509,11588664618481638829,6683877201390805789],[12171679035502060770,4545108039692003627,314780921239473147]]}, + {"d": 4, "zeta": [8615088116925916633,14390088107058038174,2021072648982338043], "folded": [[7052712868241756772,4553468042549522103,3196731637763074741],[16344025613025748553,452293191141481183,8204830158779577480],[18369870030235551668,5080227102498455366,5796020548436058409],[16981890415956580037,2793261484183937351,12700002093053910932],[9237817075302126092,16582740101697295386,3495992442642679478],[2649989405081990475,18422077614247848350,17579251581382283805],[11281358868900100483,6371375087829651837,15893368395499180654],[11309332093392525358,11076419593701924186,12957435685350473463]]}, + {"d": 5, "zeta": [12638890986933725064,6233988671304140119,10906738775467990202], "folded": [[14938993435336039587,8433402340287360947,1444885065781874699],[5846601836415060987,9442110707393776817,7424757494850568828],[6809934921619048346,230609055236823753,4480150124686349040],[313437947384391868,7391389585330885817,9866105847013411666]]}, + {"d": 6, "zeta": [16246432198723013017,7005472786384078398,3590918350879141987], "folded": [[11082178205324756851,2117653779716520051,16365930981660702508],[315019225942101281,11078153742178048043,5070729680691113956]]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/c_leaf_digests_blake3.json b/crypto/stark/tests/vectors/zf_fri/c_leaf_digests_blake3.json new file mode 100644 index 000000000..689daaff2 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/c_leaf_digests_blake3.json @@ -0,0 +1,13 @@ +{ + "generator": "stark::fri::vectors::leaf_digests_json", + "hash": "blake3", + "codeword": "b_group_folds.json codeword", + "leaves": [ + {"d": 1, "first_leaf": "3e94099701015ba4b517b2da241e742be08d6247bc8863a07484aa134dd3c0b2", "layer_root": "ab6a1860afd95f056e99f36162ee961aec77ee8e14c4206b024c02f63a5a7f1e"}, + {"d": 2, "first_leaf": "8a3a4601f36fa9c390e9d690a87c2c32ca8d039becd87497f2c7e6aeadda7a3f", "layer_root": "c5e6e011fd76656fa059479df53c33122b79894e95471ffab51dd6b6dedeb9a2"}, + {"d": 3, "first_leaf": "b1ea4061930ffd7af7ea20c263e8c41e3cfc46202480b54945b302759e4fb8a9", "layer_root": "1f638946ead0713ced39f29587cb25f308f43a2db7443fe4fc12ee5574e2e930"}, + {"d": 4, "first_leaf": "126e67d8deb4860e5a065a64b28319be8db0441a397dc716b2cc63b14dafacdc", "layer_root": "225d49f0dd2fc63dabf168f21825dd49dc08958ee89a3a1841db1dc2ab9e5fd7"}, + {"d": 5, "first_leaf": "a420dd3555ee2d35e54094b728abb93ef1e441426c0fdac244a88f0e733d7438", "layer_root": "f6c327d2325176f17f157c40f345251b788f37377c65ea3d5eaa3f72e2af68d3"}, + {"d": 6, "first_leaf": "3a1ad10b88c75a26d86a8898e00519c7bce95870d7d2f7be716d179393a124e7", "layer_root": "0f42251b7c5b0e388e761f3a35b319c455f9539adb59f7e725959b91ad0e3f5a"} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/c_leaf_digests_keccak.json b/crypto/stark/tests/vectors/zf_fri/c_leaf_digests_keccak.json new file mode 100644 index 000000000..6012eebbc --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/c_leaf_digests_keccak.json @@ -0,0 +1,13 @@ +{ + "generator": "stark::fri::vectors::leaf_digests_json", + "hash": "keccak", + "codeword": "b_group_folds.json codeword", + "leaves": [ + {"d": 1, "first_leaf": "b4356e59d9d0129ac1baeec1c40de2a0fcf45567e9e1e8a360d745f6778ce760", "layer_root": "99a27b756e7788b5f0fafd9ac8db8be0550e52b3acced0493e4afc8c9a8569c4"}, + {"d": 2, "first_leaf": "5e4e45941b9f874cd7a656bdc58ba55813f699836033cf2e05c2b45217e86ecb", "layer_root": "84735cf04432cd12c5f1b0d44b01765d65b6d1d442e4fa38d5e4567e57cbd40e"}, + {"d": 3, "first_leaf": "e592afb01c57ba10be32ea80ed280731d8aff2c07d13ca46f582d95ca0883710", "layer_root": "82365c8c49ea1f57c6ac51159d7f50ca46e05d5ac71bd04494bc86280ac19392"}, + {"d": 4, "first_leaf": "db81fea22e4d8a7a71f5a888328f5e1cd879b78b68c77b607beeaa32a26dec44", "layer_root": "4d720f49bb8997718342a6dd2c6d1c15a50165098e7a71b0fa797cb3b4a76bce"}, + {"d": 5, "first_leaf": "21c121a439c36321972e249c2aa67dfe06d681db973e1f9c264f1cfb71f2ff59", "layer_root": "12119631efb91a67936a88bc1d7d6ceff2a035b33cc82d63ee153b720ece6d63"}, + {"d": 6, "first_leaf": "3c89c987acd720a42d8ab1a61f200c6d5f57571fc5407adb81c9438e3c508ebd", "layer_root": "1be1ad81c0b977048465555718a8d8d5c5116339c1ccd09cf1de51ac4f82917c"} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/c_leaf_digests_rpx.json b/crypto/stark/tests/vectors/zf_fri/c_leaf_digests_rpx.json new file mode 100644 index 000000000..81af400ee --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/c_leaf_digests_rpx.json @@ -0,0 +1,13 @@ +{ + "generator": "stark::fri::vectors::leaf_digests_json", + "hash": "rpx", + "codeword": "b_group_folds.json codeword", + "leaves": [ + {"d": 1, "first_leaf": "c4e19fef0dcdc71c226bf8697094739acde92ea9d2259c1a3048ffcd9df670a1", "layer_root": "60d83fb6ab5a61b5009a1f668a918ba75fe21eb0a0b428055ccd2f827468ac24"}, + {"d": 2, "first_leaf": "6b783ab0c5d8a708a5834c5f6b3696c93447e470a702402f708d1dbc4e666de3", "layer_root": "e8b68715ea877da8ea07c99c43777b891b75105961e8fefde6b6ea9cb0292d82"}, + {"d": 3, "first_leaf": "5b3726fbd3b5ff5e7310f7e1e8023b595ca1e311d8bc1e398e5c236bd0d07fd7", "layer_root": "573e5bb92f63bf95684a5599c71f931371f096e829eaafe764ee2604037bfae2"}, + {"d": 4, "first_leaf": "4237eaa85f846fbe832443ca5ba31fccd041d1c2d1536f6726bc220c9583138a", "layer_root": "1d73919647290db8d8fa0031d93dbba43f40c097383c4adee0f1a59780004b0a"}, + {"d": 5, "first_leaf": "37fc7cc3a71608b4595d31c80f3914ba81ec8bdaec6c42f9bae72b6a3a9b972f", "layer_root": "c853b5880db08c3b1581c1f285fa1065beb62507d11e18db2167b17a29b60d49"}, + {"d": 6, "first_leaf": "6854823ddedf06afd546f97064760b19a811a8e4a9f026cabdfa1d17e1f971bb", "layer_root": "122803d141311339cbef2d4483906f0f548de2db1e58b6696aacbe21675a2595"} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_dp.json b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_dp.json new file mode 100644 index 000000000..1c15b0b37 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_dp.json @@ -0,0 +1,49 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "blake3", + "format": "cap_dp", + "proof_rkyv": "d_proof_blake3_cap_dp.rkyv", + "proof_rkyv_len": 41480, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 20, + "grinding_factor": 0, + "coset_offset": 3, + "merkle_cap": "auto", + "trace_tree_depth": 11, + "trace_cap": 3, + "fri_tree_depths": [8, 6, 4], + "fri_caps": [3, 3, 3], + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 2, 2], + "fri_roots": ["f5660c4333b6e611e901e87422b2c4270acfad24f631d9429831d76b54566517","0cd55ebeb840e8373096d7b45d7a99eb5f2ae89c0b700d618f5bc5e5cf7d8cac","eaed7db0665b821d2c690ad7c99f3fb3b28d4b7d3c6167be86e2033300fc3997"], + "zetas": [[10771210179622817679,127754635188287825,9592161990157892076],[2334636387541570725,4598538304975584359,12240732424901763132],[10166376440375277554,17046118386894069248,10115891851528829537],[1897382409627266702,6084356605560232122,6323818535469693028]], + "terminal_coeffs": [[5380735102582769720,14770520085343157731,17397790325610342738],[14177126935727096750,9878484623770692025,8126381307417814598],[13709110344626157024,14960543001611777495,13121995131109452668],[3543500174378306775,245990784589754978,17448449264639928647]], + "queries_detail": [ + {"iota": 975, "deep": [10762173397373278909,6238205991322615201,16902290430091080608], "deep_sym": [6360510169239840515,6098314158097471188,14374437857455028609], "terminal_position": 7, "layers": [{"layer": 0, "d": 3, "position": 975, "leaf": 121, "slot": 7, "values": [[6358380543480134188,15233297943481819331,7802884743754435287],[1561050269828331995,17056437354980486597,15953089478678981981],[9902832432796422005,16144145762744260912,2452053501664136327],[4059115819907980750,13622239393642520008,10742588081843022005],[15097201520147174657,13343912955236085051,15486897778505591961],[5267077696719908108,9464151356603282791,2320496645939400341],[7693143272069720023,17017576095617663956,6992176147234983386],[14242806506751524709,5590906401091982117,17893550137403079326]], "path_len": 13}, {"layer": 1, "d": 2, "position": 121, "leaf": 30, "slot": 1, "values": [[10075702410473013791,4716385726265313391,11609542590222699029],[7845467542947739937,12387174233603512715,18200019442985323599],[2909241347398984484,5399916910453173204,9233450494253184011],[4113688591137365584,8099000987494976281,456105366810755859]], "path_len": 11}, {"layer": 2, "d": 2, "position": 30, "leaf": 7, "slot": 2, "values": [[4720003309196990551,4178739593595040029,10411467881539262427],[17765747612192294497,14700500661535383226,13695608480138073067],[1009102207610472007,14305427383432249940,4131500240907956149],[1496298963912337677,3644564800854218514,5628674521221557415]], "path_len": 9}]}, + {"iota": 1979, "deep": [1491025643980379174,12685070184352261704,7728318385342818721], "deep_sym": [16450052277900778758,13025974084100681593,284476606938439535], "terminal_position": 15, "layers": [{"layer": 0, "d": 3, "position": 1979, "leaf": 247, "slot": 3, "values": [[17193942932342370980,12145038434561791480,11576353455053528850],[8379852527282925844,8601836539934055187,15168362594496179266],[2592359005135839197,5381418174777500780,3896499938254073228],[4822332081114310830,2565510977245490832,11907903767149013019],[13285130888391799040,1041585227093494597,2304343024112292721],[11261822772218442511,17634015746885931060,3497632743424289560],[10018483860357178719,5407737136598053306,11038589506904123227],[5386981123451134746,15435103548476985734,14493978820809476719]], "path_len": 5}, {"layer": 1, "d": 2, "position": 247, "leaf": 61, "slot": 3, "values": [[16716297285756313772,4980386954389515672,7587525751122803270],[14964533687760416685,13917074670739361960,17216570321581969515],[3750454710447327212,10631398170004764756,11015661283393845074],[10023782474286288464,1360821076844805368,12167231288721989489]], "path_len": 3}, {"layer": 2, "d": 2, "position": 61, "leaf": 15, "slot": 1, "values": [[6218711529479866098,15946762367783592827,15439960344592751968],[13215040631267974591,7742956115152970799,9370753578504439872],[8789152590505293503,2216348788422376564,6920254999236655340],[702787882218669028,13764292175754416497,529455276546629621]], "path_len": 1}]}, + {"iota": 196, "deep": [12996823082221702228,15546436838001982955,12765624607176451818], "deep_sym": [15258355026629750431,13813035249993486719,14742394712154322144], "terminal_position": 1, "layers": [{"layer": 0, "d": 3, "position": 196, "leaf": 24, "slot": 4, "values": [[5990450875556600463,11483765027548679992,15569727055914393856],[15481225486576456763,6990687647448126227,17377462139297815371],[17823918767605952564,15408822346409695669,7766449979365244357],[10521282185009747890,10208004641233698759,9793502955518714526],[15989513811586569514,8058942065900320453,2895357368330783138],[12375312625287684078,17725753712340770465,14836143340177261245],[6076100021308446097,6158543389568230761,12040165962685290590],[12787237553493716673,6399667255546886261,10645808993947995036]], "path_len": 5}, {"layer": 1, "d": 2, "position": 24, "leaf": 6, "slot": 0, "values": [[3955724155215551649,4903104037739106676,8341497941364977219],[1366207781672916929,4653995748612992444,1985491773491753288],[12110069387491220579,3462604155932552453,12998143760810181545],[12022604030106382896,9872253098559971229,8337931158086605882]], "path_len": 3}, {"layer": 2, "d": 2, "position": 6, "leaf": 1, "slot": 2, "values": [[13763808492525116750,17048718856988468120,4007208303874274964],[9747356233047801788,12353956971904007004,16715523138327251637],[17215889828796017128,10441867300762877602,5539458827215095394],[16874552336957454881,3298743664346751734,7613930184659830057]], "path_len": 1}]}, + {"iota": 1203, "deep": [15752534939543042330,15583402344956286664,18180929469912608205], "deep_sym": [6004675541515862623,15700646189296511672,11249127824744232770], "terminal_position": 9, "layers": [{"layer": 0, "d": 3, "position": 1203, "leaf": 150, "slot": 3, "values": [[10874562829332116737,16119632175137550595,5814025373320402574],[537192166862156226,1226209784871864696,10142087228779911497],[13928959866231520713,15456751127415083532,8693947028410900381],[3828009832111636345,5111308859100454226,13658206209512386784],[6969249891844518361,15061980941965197861,2988715140401985875],[4920000362291503630,4798949236132645012,14846074530401444401],[11403031433147724274,14457081909743702709,9196868723010592467],[8002035308229413454,14341041264576097070,4407237699945452429]], "path_len": 5}, {"layer": 1, "d": 2, "position": 150, "leaf": 37, "slot": 2, "values": [[3024954652785290495,9666301642784843613,6945943976099679195],[12418472477586847230,17196060582103289666,2393471149779994062],[4625263396386879378,12619942520976382752,7834949255201727970],[5650597075990240870,17238954492838515108,1158458673914035833]], "path_len": 3}, {"layer": 2, "d": 2, "position": 37, "leaf": 9, "slot": 1, "values": [[15079255711491555218,12300854889741332788,10589502824497300729],[11478058193422450763,12570729264925447255,11149230142755138274],[8007098169052196112,15752356817250027978,12944679080360575431],[17096915696030076215,2520825861781781734,8689682692440434782]], "path_len": 1}]}, + {"iota": 991, "deep": [8681993120970919677,2087072008066845857,11711806762509319738], "deep_sym": [7491938478046119788,14436381252314064679,2021960428839489614], "terminal_position": 7, "layers": [{"layer": 0, "d": 3, "position": 991, "leaf": 123, "slot": 7, "values": [[241286883786860593,7693402300417727673,16090435993444967711],[17935640868834497012,8620962675063390145,15113657813157556306],[15781246783767794142,4964978784804618058,15015775329041983920],[13016971764566756554,2018125833213814590,14318788237881126017],[10152329590681864501,3050038139514453021,1800033870197117892],[13712181320069854702,12383945862328316760,7877046573358953983],[16434444243828717809,5651549530951913294,4761546699101515128],[2254401359065396503,11395830875147205648,10277023618508617127]], "path_len": 5}, {"layer": 1, "d": 2, "position": 123, "leaf": 30, "slot": 3, "values": [[10075702410473013791,4716385726265313391,11609542590222699029],[7845467542947739937,12387174233603512715,18200019442985323599],[2909241347398984484,5399916910453173204,9233450494253184011],[4113688591137365584,8099000987494976281,456105366810755859]], "path_len": 3}, {"layer": 2, "d": 2, "position": 30, "leaf": 7, "slot": 2, "values": [[4720003309196990551,4178739593595040029,10411467881539262427],[17765747612192294497,14700500661535383226,13695608480138073067],[1009102207610472007,14305427383432249940,4131500240907956149],[1496298963912337677,3644564800854218514,5628674521221557415]], "path_len": 1}]}, + {"iota": 730, "deep": [3749017813721115261,14557058401905093868,6250134219782047991], "deep_sym": [10973933773174417008,12152588244373773982,7254759842174161858], "terminal_position": 5, "layers": [{"layer": 0, "d": 3, "position": 730, "leaf": 91, "slot": 2, "values": [[13625437253679645360,4781346700753413259,9739480830032267559],[10343180462388971404,14390237986214676488,1233992481629191112],[7094279693781689175,2117205439232100477,1821367394658403345],[10297720058835310480,1175273423588874691,13535140666215718820],[17216420973199492163,7368341788859375110,13031855082791043411],[14966444954846038540,15465064230605228385,11469894714541073913],[2621509644171994588,10617636738579191725,16179719487663085109],[6977255124091830661,18103993031165746594,17724514913003277706]], "path_len": 5}, {"layer": 1, "d": 2, "position": 91, "leaf": 22, "slot": 3, "values": [[5144674147078997594,5491908906114697312,3844920803894566132],[5020900329166536587,7144686597885242940,16786875069484486943],[12993512973289219318,7358410183833446838,13935406277168409330],[17784522352072343142,9563050577201338945,352692387285700454]], "path_len": 3}, {"layer": 2, "d": 2, "position": 22, "leaf": 5, "slot": 2, "values": [[6011936006421691404,10675680366181426359,16583790013325704227],[12401360982478626076,2729294664100958931,4483876789092781890],[7256982332670895249,18228230382919437261,7873183635821042676],[15333325283421136752,3228834115588111137,8250758386838693228]], "path_len": 1}]}, + {"iota": 1461, "deep": [5143682188678502351,16221457536374172264,13900721353390447660], "deep_sym": [753761722491209855,4901966673579435005,10628686203083643451], "terminal_position": 11, "layers": [{"layer": 0, "d": 3, "position": 1461, "leaf": 182, "slot": 5, "values": [[9078356149970439678,12742462076713984472,8334953287620001223],[6232363074738132968,9592569284571747908,17454801279480835743],[12665951647893564170,16631753895531546839,18037466198834859557],[9379129987218727754,2990857268642168597,3455448609799108188],[2306338561029461238,9231856230271642991,5092529416795310001],[11856654519610033437,18143211246415090160,2467191868091550359],[14215724434611581339,4721597587453395495,8407334378937024772],[174624353941079833,16048126312610653415,3173888900307413954]], "path_len": 5}, {"layer": 1, "d": 2, "position": 182, "leaf": 45, "slot": 2, "values": [[755410960438700629,2805302518756125053,17531434065929643718],[3071543226078084354,9909734270156800125,15970672385847214190],[10754777501829526062,8163465937849620718,17841447578808981118],[9283169039803580114,4638181989398948167,6627029688357333091]], "path_len": 3}, {"layer": 2, "d": 2, "position": 45, "leaf": 11, "slot": 1, "values": [[5980468832933785485,14242050719805404593,6439206589369129064],[15179786468695181372,4377693513116986124,1322832299243542576],[13360246428180627504,4483599704657039337,1136060911200590793],[16615211484878415091,13032718370113144279,16130260341719337467]], "path_len": 1}]}, + {"iota": 1956, "deep": [13012147546762863285,17378967107273509838,16785009961307145384], "deep_sym": [13570520044671590068,13173769417357444649,15377036488763418784], "terminal_position": 15, "layers": [{"layer": 0, "d": 3, "position": 1956, "leaf": 244, "slot": 4, "values": [[1139695083250640099,6281616195601480811,15831019522963135362],[18292945331627888905,3930898106053175575,14706941153128633931],[4811231360020697715,485782268072192698,165464771280611802],[4877247552542499443,3192777604604862923,6319355463364641086],[15125822918693992437,17537798162189493620,1629697386871100514],[13796269321411676444,1854432270020465144,6973461315370024637],[15795021451379651796,7736292815854993306,5175720845279510537],[3000276692915542192,11250424266363190971,590499797331551276]], "path_len": 5}, {"layer": 1, "d": 2, "position": 244, "leaf": 61, "slot": 0, "values": [[16716297285756313772,4980386954389515672,7587525751122803270],[14964533687760416685,13917074670739361960,17216570321581969515],[3750454710447327212,10631398170004764756,11015661283393845074],[10023782474286288464,1360821076844805368,12167231288721989489]], "path_len": 3}, {"layer": 2, "d": 2, "position": 61, "leaf": 15, "slot": 1, "values": [[6218711529479866098,15946762367783592827,15439960344592751968],[13215040631267974591,7742956115152970799,9370753578504439872],[8789152590505293503,2216348788422376564,6920254999236655340],[702787882218669028,13764292175754416497,529455276546629621]], "path_len": 1}]}, + {"iota": 1148, "deep": [559191688237983931,4204147968037837581,13155536057586080738], "deep_sym": [4401246871529010816,13593062267599263121,2999155291195094619], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1148, "leaf": 143, "slot": 4, "values": [[11039635021177097374,4477374690065111236,15665506636854122333],[6156104977645698326,6926748032535851306,11511066775905706022],[8580444569339206543,2026670230496760715,16952652587434159221],[14258287573194214551,3410911056137109245,17066913606006937522],[12939165465098362074,7509018497879618565,11429597422765499875],[7515506259952081009,5672153501166785178,12639132181321448068],[13980620889037711513,14136365014306635812,3631194797964167943],[9895737230979731583,14206155153721926339,7744139835103824986]], "path_len": 5}, {"layer": 1, "d": 2, "position": 143, "leaf": 35, "slot": 3, "values": [[1057482104613101351,12077553649510848318,12985024014551316629],[400287114595203204,11726952929487123048,12602932319028339522],[13399556945017055043,10426691419037754163,8459530740165622424],[15320314632502273335,16540722097958614548,2745741974161055219]], "path_len": 3}, {"layer": 2, "d": 2, "position": 35, "leaf": 8, "slot": 3, "values": [[2361770517565856606,18392804438032607863,14698819959800212628],[16937432840950086279,13504488964490434884,5737219975509076724],[16200627196689697959,5428662221761898729,14790338823710953763],[5136352044889977961,7465012862093234680,14298704360569780639]], "path_len": 1}]}, + {"iota": 1097, "deep": [9911322248523663148,7449350727945815226,7912850267400926375], "deep_sym": [4610277628482265293,7796822887877965851,16588608283361741697], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1097, "leaf": 137, "slot": 1, "values": [[16579933164913246961,14938927767270908477,12243013016946014882],[1211652563999613683,3322115237433417792,16033653635443512602],[12523441763998198184,12506679984508601708,2645930596723636519],[11351023897894651361,14829732713595927367,14888122988734751436],[6927428004267178384,5040625818333788974,1120890039336808382],[8903235146386526066,4628324794344608477,258520791444110126],[11261972428415464801,4725869900107112620,14674247944396481252],[14305402251855101376,10312372909744114657,3393540230751075403]], "path_len": 5}, {"layer": 1, "d": 2, "position": 137, "leaf": 34, "slot": 1, "values": [[12497579461873370340,17223221246127914665,1592668878267302670],[14187438249429787970,18179245312246498387,10554628983733290057],[2179539185730340905,2135162114646552181,7035059331050521551],[5465159271385692206,5923081554081629690,1937189932867117535]], "path_len": 3}, {"layer": 2, "d": 2, "position": 34, "leaf": 8, "slot": 2, "values": [[2361770517565856606,18392804438032607863,14698819959800212628],[16937432840950086279,13504488964490434884,5737219975509076724],[16200627196689697959,5428662221761898729,14790338823710953763],[5136352044889977961,7465012862093234680,14298704360569780639]], "path_len": 1}]}, + {"iota": 893, "deep": [14869975709152106679,1921980599777105357,12641264178656378660], "deep_sym": [4324020330371914544,15450910801133054761,11596191006253436286], "terminal_position": 6, "layers": [{"layer": 0, "d": 3, "position": 893, "leaf": 111, "slot": 5, "values": [[7364517996926882784,4423842349740890359,4094086245325002393],[78915192562221491,14174672544353666956,16649158135147014355],[17270498933821331212,17933816006483508343,9680389171947044434],[17637018130336130973,15277620556589607194,10715099103759978271],[7046591968540024718,11745653068892231709,4936447358022281253],[8846985800004343385,8976869429746572050,13004655854294770678],[135177480516526320,3881210905077568264,5572941719046141216],[14535766980758331393,15803350649698253723,12689112861429696910]], "path_len": 5}, {"layer": 1, "d": 2, "position": 111, "leaf": 27, "slot": 3, "values": [[6396069085956288173,15903209904786184991,6201948201142607279],[16683133169849730866,9587936157017377155,6431682643504102182],[4650709611847295533,4352442522449825086,12605080154257239873],[6067851984933547871,1248514420175494616,11918932032981867635]], "path_len": 3}, {"layer": 2, "d": 2, "position": 27, "leaf": 6, "slot": 3, "values": [[18054014190726286609,5230501828626636123,4663425874675860736],[3106421522097749857,13707080887749262922,9451679288545940912],[749343417051452695,17957036162478834582,2396924204100391866],[7003646066458667451,7182864086926233463,6926954008656726935]], "path_len": 1}]}, + {"iota": 1611, "deep": [10283771577448452308,6311775650455490315,10011119081764825290], "deep_sym": [2942709850112994258,15777379869206527462,16026469347544900843], "terminal_position": 12, "layers": [{"layer": 0, "d": 3, "position": 1611, "leaf": 201, "slot": 3, "values": [[15034096618106703532,16266067412950314637,6984842303440981460],[11574209943803058102,14368413300772335990,83483935827968805],[15002499834400737064,18125927580513201844,14083158362275701452],[2794306545247555542,12893554998288301299,6896983749182260428],[17387138325837804041,5534198072980561863,17681730334834591590],[1433277771010912265,7385939762528684302,5987338178976194148],[10817868126959669011,11169882412867946117,6174821245334077769],[6672868139619767727,15414026211813473718,12731116147464688792]], "path_len": 5}, {"layer": 1, "d": 2, "position": 201, "leaf": 50, "slot": 1, "values": [[4371265254077541125,15847370424923923597,9559539041216336520],[11627574753275232018,8594664059292053846,7068185601245487025],[8147614893575946269,4951488265475391522,2682227487549181930],[14284256686954366294,3438993162828147454,12704539693506731230]], "path_len": 3}, {"layer": 2, "d": 2, "position": 50, "leaf": 12, "slot": 2, "values": [[7732520432572237241,13165516863377634804,7705769214984174459],[9141973685089497517,2604347535429187405,11005539496063051989],[7573637287085966786,9264725481276609229,1916875510556010185],[5085817792127901346,9883478890715282684,12164815580791611471]], "path_len": 1}]}, + {"iota": 1315, "deep": [4868878336503280648,5502497972773445066,13818167565514677736], "deep_sym": [14213966109955828538,16788446689672845476,13145565271098668723], "terminal_position": 10, "layers": [{"layer": 0, "d": 3, "position": 1315, "leaf": 164, "slot": 3, "values": [[6376909581141271245,1241339294886889724,16399006528135619687],[17670280822265450577,11175947999744963222,6580869690978006091],[7637614296323359836,5078512359894114442,11077878261128887197],[6481499981089297500,12508414171445953510,221573720616415350],[2027678399645124930,17032205516310667683,8433264502054998557],[11784019934107793864,16587931275900712549,4206781342497882241],[3314194782870738512,5273174711638861445,9661562963755791982],[7181141736115860532,1833310872594558499,9936079683014406622]], "path_len": 5}, {"layer": 1, "d": 2, "position": 164, "leaf": 41, "slot": 0, "values": [[14261929297497037883,12784405026888296033,10988981792043533678],[1267904154492123740,15997048429724618067,9199126858793819891],[2141794565460227736,2222704669356598473,485299134004841347],[1392975148673254767,7775679193829347867,1952883468569488832]], "path_len": 3}, {"layer": 2, "d": 2, "position": 41, "leaf": 10, "slot": 1, "values": [[15744827006673985450,11711997518029820360,526067923678103240],[9361848992962370566,5820347163924234588,15609654412100488927],[12467756039166196733,1959095310809430573,10751774919655598094],[12317929042402698177,8299237154342900303,376015377231772288]], "path_len": 1}]}, + {"iota": 347, "deep": [5652495261376918233,11003491597239608592,8555109965912264684], "deep_sym": [11701311397388666708,14684683370759232903,4789515469099634998], "terminal_position": 2, "layers": [{"layer": 0, "d": 3, "position": 347, "leaf": 43, "slot": 3, "values": [[2279034670790862856,3918865091040307633,14883222214352393554],[9955061263535213790,1461571236494044152,11490165111605523433],[11370145577179544,7636842533978780720,12565305142199480505],[18246018928058218312,2899480678750643106,359559756082944587],[8261422293642933083,14348141907525272774,2827499312888751533],[15265874781544945251,15758119172686786126,14020258214772758245],[7692428087112738499,16089983222131207652,5190297970971948095],[6288015372171692919,3560619850227336675,3503535371271013628]], "path_len": 5}, {"layer": 1, "d": 2, "position": 43, "leaf": 10, "slot": 3, "values": [[8082651957698097349,11080401022154114235,13225618016723934942],[12967900839345856436,10245135173744964460,13882328420034803705],[7308376203592554799,2200255532820098891,3144650772598452114],[16979859653628826157,11957020649260092048,4154152077335082351]], "path_len": 3}, {"layer": 2, "d": 2, "position": 10, "leaf": 2, "slot": 2, "values": [[626631545137480777,14040093596076234059,11563086371519029480],[14166743346901113445,1584993346684759291,6073191583192600810],[17835536105487204093,4751491938112621960,5358807637082779310],[10015740991204856004,10917778466779632243,17376279851693605463]], "path_len": 1}]}, + {"iota": 7, "deep": [10729628599147983865,11262284762986532334,12067698941208999342], "deep_sym": [13793692426065840049,17804767925899894057,12522649863195261323], "terminal_position": 0, "layers": [{"layer": 0, "d": 3, "position": 7, "leaf": 0, "slot": 7, "values": [[15397680014036791351,1884054888443124705,10356418525983122034],[3574327614819495940,9311713712635394509,9592445444305087428],[3995997584691044082,2898147102620050906,1534897124393607006],[12010016124993134544,11373335453832742986,15988031694065613139],[8586139949312340825,7786086002367368963,15297348463510698442],[11932348748044977641,6067544001558133969,5132720332181670008],[7282236280819674652,15971785141886602896,15580216555727892132],[17515904616771614042,17626757208160268926,6170444558336859025]], "path_len": 5}, {"layer": 1, "d": 2, "position": 0, "leaf": 0, "slot": 0, "values": [[18419289037790398515,4119860990809229123,8532257483528117106],[2441301968378033486,11031829990918540139,9356755462377865367],[2444772718654314503,10338519488613625025,4057680024245573228],[9688440605635587702,993932466441680669,10433316942822491330]], "path_len": 3}, {"layer": 2, "d": 2, "position": 0, "leaf": 0, "slot": 0, "values": [[6889909335419354700,6796959293305346289,14415195110069695447],[18373991401276132678,11560808149746317105,10847324815709489832],[16208114084891951396,4178287177882041172,2800573986594485795],[4690326594433345688,7539141686653911826,11409842170078198974]], "path_len": 1}]}, + {"iota": 1833, "deep": [15091594336359056927,17731773756750016012,3951308870671530023], "deep_sym": [11288885646087766633,14279114384306327058,17497384530855239826], "terminal_position": 14, "layers": [{"layer": 0, "d": 3, "position": 1833, "leaf": 229, "slot": 1, "values": [[5835838119563522966,13527318661581226992,8988488912368153474],[7825002203746412915,7157868495818377610,391097759177990040],[15505006135794333734,11222088631013402988,7599709940706104976],[17400545490854520115,8691678530155684831,2366389859525368701],[13271538683612303742,9468247552162442421,9765400918464248667],[75309224696716840,15170015511120189248,3729324586155886010],[6124965313206069075,15411539721238865905,16260001499293568983],[3120690321051812524,4122134073004025220,4196588213607274638]], "path_len": 5}, {"layer": 1, "d": 2, "position": 229, "leaf": 57, "slot": 1, "values": [[16190254108336593334,153294746140690160,9788877368221835751],[5557446448754715381,12694267056985157970,2800513960783386007],[15681186669330942481,6671086479859882339,17225476729331239755],[17856735751311346283,17121906477052463042,17696748984568817182]], "path_len": 3}, {"layer": 2, "d": 2, "position": 57, "leaf": 14, "slot": 1, "values": [[984203213469929437,4224343177582388484,9572623288142859938],[16056684969230873251,10652539299959440313,12750365249972731935],[6580582599927055682,3943061636956573218,9060032440671433186],[17342194358321478128,894315335249473662,4301831733190433175]], "path_len": 1}]}, + {"iota": 1893, "deep": [9502080685171718273,11900261630676834135,11105421041413284470], "deep_sym": [7000977341138268486,6102080310951961489,13938957829736479714], "terminal_position": 14, "layers": [{"layer": 0, "d": 3, "position": 1893, "leaf": 236, "slot": 5, "values": [[5812351387423155422,8111386547126011924,6341489365517505745],[14564580661283786368,11910756739879885116,14124074572254428382],[11785855844471819792,6567606644225299130,8592427053884858150],[16039001077420822027,14876600592689496834,4177020490289287383],[12573539672230359958,10736543775311885948,8222790836481073619],[13429784704579008119,16778130495890976308,3410075892657910004],[1667848710464024897,3131118275609095446,13086150401349088028],[17315040022593921497,10611946554924693795,14092060639864510202]], "path_len": 5}, {"layer": 1, "d": 2, "position": 236, "leaf": 59, "slot": 0, "values": [[5034510331297990362,8916005402476503296,3316722436455100847],[4322052901144351236,5052993851804539011,7660734087728392519],[10109600378082773260,16588247839764178304,12656780741746346580],[10741868519284278895,5468237293959312321,8792679859332184847]], "path_len": 3}, {"layer": 2, "d": 2, "position": 59, "leaf": 14, "slot": 3, "values": [[984203213469929437,4224343177582388484,9572623288142859938],[16056684969230873251,10652539299959440313,12750365249972731935],[6580582599927055682,3943061636956573218,9060032440671433186],[17342194358321478128,894315335249473662,4301831733190433175]], "path_len": 1}]}, + {"iota": 1006, "deep": [278565245816208269,10403413944694705412,18373543770665442480], "deep_sym": [5782750051717883566,9869349874627987641,5162411754453845894], "terminal_position": 7, "layers": [{"layer": 0, "d": 3, "position": 1006, "leaf": 125, "slot": 6, "values": [[424531153078822075,14437669868520738150,6405119015988337644],[11781162730973500861,10727531823436307995,16141001035929866190],[16031527700303044792,10858195524075015505,5243643158770699539],[176124510599809593,14363838589718254776,15251157617241324835],[6771402597407673287,8335611809952380981,1074070984178883283],[3055397484829669688,4726642450162797422,1224456793439981816],[8394748372127108582,6409876248154059481,18188302789923254571],[5509435528613056072,579645458909143161,14213312078695342130]], "path_len": 5}, {"layer": 1, "d": 2, "position": 125, "leaf": 31, "slot": 1, "values": [[10361947016727152801,9865134566086214370,1855324780858693853],[378975246974756134,11395599957458128578,15469914222301991129],[9353484865522397870,949259615002670803,12259663185538090699],[15448519710699047426,10685872832601579041,2583763360548785823]], "path_len": 3}, {"layer": 2, "d": 2, "position": 31, "leaf": 7, "slot": 3, "values": [[4720003309196990551,4178739593595040029,10411467881539262427],[17765747612192294497,14700500661535383226,13695608480138073067],[1009102207610472007,14305427383432249940,4131500240907956149],[1496298963912337677,3644564800854218514,5628674521221557415]], "path_len": 1}]}, + {"iota": 1700, "deep": [1410836301490999973,1302595678191022288,179613372424907210], "deep_sym": [12728179003292154660,10911574655256566401,13521656637148951812], "terminal_position": 13, "layers": [{"layer": 0, "d": 3, "position": 1700, "leaf": 212, "slot": 4, "values": [[13559371558166416798,3015153850462235040,6169493881691645384],[13186179764720193276,10761686304300220634,6573602884002212893],[14971197062009843562,4024604816664565086,13338302287007886789],[17452603893950420306,13885944451748366985,2828010835887818953],[2716040662584989146,13533080688972263783,7945319097121604891],[16210715316971838963,13545031526832441319,17252233972962118392],[16894810944015893775,6179082506926514775,10260076086525229951],[6880444503718828723,7798036362196708901,5135439215824977126]], "path_len": 5}, {"layer": 1, "d": 2, "position": 212, "leaf": 53, "slot": 0, "values": [[11921998034465242009,15803801940516939286,5425014481155423121],[14339889849134608607,3519911207241293731,1012333810442413988],[8313961635410473445,4840142476262538653,10140604888347464359],[12239251471703462122,11212894982547623503,4964774848319983656]], "path_len": 3}, {"layer": 2, "d": 2, "position": 53, "leaf": 13, "slot": 1, "values": [[17097230081346561873,9416095663746874971,16909476699859173651],[5954831163834882122,10648165262835477953,16923660589352447836],[16560227121839913403,12539513539081302255,14451257014363123269],[17396501188837888843,6509268572137540082,14614123686429091845]], "path_len": 1}]}, + {"iota": 516, "deep": [5960966010798800123,2846205985179489762,6865158036389153804], "deep_sym": [16774012547545437690,10867764373143372640,12104783288707649791], "terminal_position": 4, "layers": [{"layer": 0, "d": 3, "position": 516, "leaf": 64, "slot": 4, "values": [[17750384411855288273,1741652024793611125,15489881843544541870],[9786778802115784024,14048855153868086834,10298495367053368114],[14192416002951717457,10195523734556928612,6591176491061048897],[15079918591651446981,15575218939349755148,15828430898221110407],[5072774290398093241,18130906298164818449,300258120769860962],[15503009350805253650,3867086856121895573,14538628658269385269],[3336112409534602303,17408497384008958408,3796420379159471748],[11021938962937693750,8369798889504894647,12334916565960248080]], "path_len": 5}, {"layer": 1, "d": 2, "position": 64, "leaf": 16, "slot": 0, "values": [[6139920174700566034,15308264411679642375,8162232957374836514],[4046575768078141090,4755179760145096589,2066516433627498940],[17931094972437772834,2869413196283560188,8007455432109443910],[17654138861816481141,10102921129471255319,16147744345843337964]], "path_len": 3}, {"layer": 2, "d": 2, "position": 16, "leaf": 4, "slot": 0, "values": [[2330219872305505679,2524558845414778148,13832449164595042239],[12707191272162393673,14207086513203844784,9008382796051335778],[7379323499145744541,74149321890311351,6771439343933968571],[2213691850474575489,841181090964414977,2633191934983112982]], "path_len": 1}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_dp.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_dp.rkyv new file mode 100644 index 000000000..017dfddb3 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_dp.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_pair.json b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_pair.json new file mode 100644 index 000000000..cf73a0ae8 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_pair.json @@ -0,0 +1,49 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "blake3", + "format": "cap_pair", + "proof_rkyv": "d_proof_blake3_cap_pair.rkyv", + "proof_rkyv_len": 51752, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 20, + "grinding_factor": 0, + "coset_offset": 3, + "merkle_cap": "auto", + "trace_tree_depth": 11, + "trace_cap": 3, + "fri_tree_depths": [10, 9, 8, 7, 6, 5, 4], + "fri_caps": [3, 3, 3, 3, 3, 3, 3], + "legacy_encoding": true, + "total_folds": 8, + "terminal_len": 16, + "schedule": [1, 1, 1, 1, 1, 1, 1], + "fri_roots": ["2fc983d7a9f8dba305332d7c27f44e2afc07aa1fe014ad8a85ca9bc36844a992","a01be93f245807d30fde826335bc7dd8bfbf9fb0545769f85da9a6ee2b564301","4cc8bdb5c5d436e8b5cc91aa230573d630e9eb8a908f2650f9d4598e3d731b65","82de5d8f879bb994fc9573a6c3706b71adaf8b236e17ff2047e44bfa64f1e480","779e1acea2c1b391312f39412312a9a6bcb8cf5a66e1954d2b9d340a74a74bd0","191d0d55f0bf47af108196ca3b2c067e667807e78ae6681a814b3be0a8f4e445","878efaffc3ca3b900bd232b64cdd6a142203e2ef0ff1cda3f52ec590b776f071"], + "zetas": [[10771210179622817679,127754635188287825,9592161990157892076],[339236561547217708,14515476371055385421,3041135081988152589],[7430745936816588155,8998042728974583901,11515773416551488605],[7956826836586454026,8667292109104632665,2851244499340860067],[16324173539864659489,11301157219502799655,18016560099956879839],[4272458413724263223,15273501817168123109,13432776003642703715],[18153136978195245525,4668271491129789573,15852649611975035906],[13206066232974685659,15811531208029248608,9742874826372310642]], + "terminal_coeffs": [[11908419985256297049,6320124696091700849,10477651950916658009],[16710003718284845920,14728440137509904251,12073313539240356766],[15142905694919717110,8656948196775444897,1363513317241862160],[2198207007945388790,2708142890943514224,17003186495140238478]], + "queries_detail": [ + {"iota": 1803, "deep": [15272426180920759111,5106447191221278975,14792296330971372023], "deep_sym": [10304415851256192438,7276545599604954905,12402529132092837573], "terminal_position": 14, "layers": [{"layer": 0, "d": 1, "position": 1803, "leaf": 901, "slot": 1, "values": [[3388282554933400969,3823175679916949076,2787870681482871753]], "path_len": 15}, {"layer": 1, "d": 1, "position": 901, "leaf": 450, "slot": 1, "values": [[17941501397892820289,9746070180589186316,4483120140038292319]], "path_len": 14}, {"layer": 2, "d": 1, "position": 450, "leaf": 225, "slot": 0, "values": [[1518365534971821388,8220153128022570539,11364526563819683345]], "path_len": 13}, {"layer": 3, "d": 1, "position": 225, "leaf": 112, "slot": 1, "values": [[10971480354833343982,3135816652628770915,6720283715471365573]], "path_len": 12}, {"layer": 4, "d": 1, "position": 112, "leaf": 56, "slot": 0, "values": [[5709537754561370510,10236832031319039769,1874314679153150939]], "path_len": 11}, {"layer": 5, "d": 1, "position": 56, "leaf": 28, "slot": 0, "values": [[2528382099678622252,12218130109821183716,1136296192569704372]], "path_len": 10}, {"layer": 6, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[7786343213267754545,6056155651923690370,13889151246865202821]], "path_len": 9}]}, + {"iota": 474, "deep": [4642716204571870719,13791353321977000304,9948795077040124575], "deep_sym": [10074490863165540107,1346332627183725457,15559140971681542809], "terminal_position": 3, "layers": [{"layer": 0, "d": 1, "position": 474, "leaf": 237, "slot": 0, "values": [[9803582068471756145,5326669840186105035,7793279955894935834]], "path_len": 7}, {"layer": 1, "d": 1, "position": 237, "leaf": 118, "slot": 1, "values": [[18172681946601424763,4149543359487769368,2150741210857753378]], "path_len": 6}, {"layer": 2, "d": 1, "position": 118, "leaf": 59, "slot": 0, "values": [[10054490575191079786,12193424298068301071,8417982262482120641]], "path_len": 5}, {"layer": 3, "d": 1, "position": 59, "leaf": 29, "slot": 1, "values": [[15947123418199701165,18407774728151935281,3292539258646734529]], "path_len": 4}, {"layer": 4, "d": 1, "position": 29, "leaf": 14, "slot": 1, "values": [[3117529940052833834,10473131964376682009,2083760568833245811]], "path_len": 3}, {"layer": 5, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[7713732352748329805,3614247649513246873,888672929281612740]], "path_len": 2}, {"layer": 6, "d": 1, "position": 7, "leaf": 3, "slot": 1, "values": [[12707951974262439387,10229714375229842447,6079425424868885692]], "path_len": 1}]}, + {"iota": 1018, "deep": [15882578000804364217,17570699945731153943,17271573467219472049], "deep_sym": [11776097457111120055,8466990234121688300,9890187330955688279], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 1018, "leaf": 509, "slot": 0, "values": [[11811277273608028663,11557005519804590428,6147111063572348711]], "path_len": 7}, {"layer": 1, "d": 1, "position": 509, "leaf": 254, "slot": 1, "values": [[7939867388228535545,10890533442334944369,9121900079366539214]], "path_len": 6}, {"layer": 2, "d": 1, "position": 254, "leaf": 127, "slot": 0, "values": [[432090123891713462,15713972828822391493,11186642764496342828]], "path_len": 5}, {"layer": 3, "d": 1, "position": 127, "leaf": 63, "slot": 1, "values": [[1293317382852890727,12037476111710244625,8463877166491912968]], "path_len": 4}, {"layer": 4, "d": 1, "position": 63, "leaf": 31, "slot": 1, "values": [[14282184254670867115,4596231514897671604,8263298406545493773]], "path_len": 3}, {"layer": 5, "d": 1, "position": 31, "leaf": 15, "slot": 1, "values": [[4047442787689190383,14125010312351736105,11271388519733766106]], "path_len": 2}, {"layer": 6, "d": 1, "position": 15, "leaf": 7, "slot": 1, "values": [[5728710831141537085,4396671778989160837,8614177465654515251]], "path_len": 1}]}, + {"iota": 1013, "deep": [16539758758549291992,4800579245526150018,16962061393147898641], "deep_sym": [3988521226579075591,6521018656605876229,8515646311306923282], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 1013, "leaf": 506, "slot": 1, "values": [[5710159636982614636,13770330871767670923,10497539398468115473]], "path_len": 7}, {"layer": 1, "d": 1, "position": 506, "leaf": 253, "slot": 0, "values": [[6144097270558028455,4496949226190935449,516924673664036100]], "path_len": 6}, {"layer": 2, "d": 1, "position": 253, "leaf": 126, "slot": 1, "values": [[15656447957273490722,18139085581736996306,16157908077113276289]], "path_len": 5}, {"layer": 3, "d": 1, "position": 126, "leaf": 63, "slot": 0, "values": [[2914172444221939731,10755256182453994313,12172760769612416178]], "path_len": 4}, {"layer": 4, "d": 1, "position": 63, "leaf": 31, "slot": 1, "values": [[14282184254670867115,4596231514897671604,8263298406545493773]], "path_len": 3}, {"layer": 5, "d": 1, "position": 31, "leaf": 15, "slot": 1, "values": [[4047442787689190383,14125010312351736105,11271388519733766106]], "path_len": 2}, {"layer": 6, "d": 1, "position": 15, "leaf": 7, "slot": 1, "values": [[5728710831141537085,4396671778989160837,8614177465654515251]], "path_len": 1}]}, + {"iota": 493, "deep": [17552019816042641949,18232228328537735996,17721947593967519347], "deep_sym": [11476893994278310325,10597338058207765344,1336796915425294807], "terminal_position": 3, "layers": [{"layer": 0, "d": 1, "position": 493, "leaf": 246, "slot": 1, "values": [[18246023938477235888,16844208616248807001,7488877263795102693]], "path_len": 7}, {"layer": 1, "d": 1, "position": 246, "leaf": 123, "slot": 0, "values": [[5457193399828553874,6465888369873413124,13473464106011068418]], "path_len": 6}, {"layer": 2, "d": 1, "position": 123, "leaf": 61, "slot": 1, "values": [[14501885818421213754,2404493437532211906,18327053541415153599]], "path_len": 5}, {"layer": 3, "d": 1, "position": 61, "leaf": 30, "slot": 1, "values": [[10253919672763263534,13085255767145830067,3363478501517189514]], "path_len": 4}, {"layer": 4, "d": 1, "position": 30, "leaf": 15, "slot": 0, "values": [[15108152339623035561,7941003559796812370,11287785174720768629]], "path_len": 3}, {"layer": 5, "d": 1, "position": 15, "leaf": 7, "slot": 1, "values": [[9325774833112564040,15477717807642519431,6899846380415881059]], "path_len": 2}, {"layer": 6, "d": 1, "position": 7, "leaf": 3, "slot": 1, "values": [[12707951974262439387,10229714375229842447,6079425424868885692]], "path_len": 1}]}, + {"iota": 1295, "deep": [2907714514381034844,1070164129957410707,17697561092214226008], "deep_sym": [10898207006015057880,4855160508758814138,8376350505293817877], "terminal_position": 10, "layers": [{"layer": 0, "d": 1, "position": 1295, "leaf": 647, "slot": 1, "values": [[18237373031964124495,12933656975114878376,8124190660344903209]], "path_len": 7}, {"layer": 1, "d": 1, "position": 647, "leaf": 323, "slot": 1, "values": [[15160245734528398038,2223725890827758023,1845689963201887565]], "path_len": 6}, {"layer": 2, "d": 1, "position": 323, "leaf": 161, "slot": 1, "values": [[3632524267806718007,17868686869941004595,2494655627500700339]], "path_len": 5}, {"layer": 3, "d": 1, "position": 161, "leaf": 80, "slot": 1, "values": [[2718920637662991361,14524410770564791347,12758797938498319947]], "path_len": 4}, {"layer": 4, "d": 1, "position": 80, "leaf": 40, "slot": 0, "values": [[4287120775860448141,16640217501487511425,11969890935976958528]], "path_len": 3}, {"layer": 5, "d": 1, "position": 40, "leaf": 20, "slot": 0, "values": [[17219987829003584375,1216251437672055770,14903611013931497919]], "path_len": 2}, {"layer": 6, "d": 1, "position": 20, "leaf": 10, "slot": 0, "values": [[14693608784504098272,9224706871872850546,6936163159577250052]], "path_len": 1}]}, + {"iota": 1692, "deep": [4675075042609955645,15761568838215639339,882232805073275850], "deep_sym": [333372144900086791,18010920076537972462,14717091746069690415], "terminal_position": 13, "layers": [{"layer": 0, "d": 1, "position": 1692, "leaf": 846, "slot": 0, "values": [[17431861332287145374,14475947496191718268,14296768809856075377]], "path_len": 7}, {"layer": 1, "d": 1, "position": 846, "leaf": 423, "slot": 0, "values": [[9642845874839210936,17308379870883033292,15084137183703271672]], "path_len": 6}, {"layer": 2, "d": 1, "position": 423, "leaf": 211, "slot": 1, "values": [[1604327021956893871,10821501388643107257,2099354348106496350]], "path_len": 5}, {"layer": 3, "d": 1, "position": 211, "leaf": 105, "slot": 1, "values": [[15537394326487831857,3289175237492248753,4061069590219069813]], "path_len": 4}, {"layer": 4, "d": 1, "position": 105, "leaf": 52, "slot": 1, "values": [[1158672035570015579,7704491263593106797,189694936280376845]], "path_len": 3}, {"layer": 5, "d": 1, "position": 52, "leaf": 26, "slot": 0, "values": [[2975349498257900987,8948160026774816179,8738746312419937635]], "path_len": 2}, {"layer": 6, "d": 1, "position": 26, "leaf": 13, "slot": 0, "values": [[12634357439633813059,12837117735719628106,9594443338063320145]], "path_len": 1}]}, + {"iota": 1926, "deep": [13364378999009406176,7107425205883074344,8183456523235029556], "deep_sym": [1636435348441089579,9805704461425670937,17419258600423616040], "terminal_position": 15, "layers": [{"layer": 0, "d": 1, "position": 1926, "leaf": 963, "slot": 0, "values": [[17308510152305227176,14537029458183837062,3374534082981073645]], "path_len": 7}, {"layer": 1, "d": 1, "position": 963, "leaf": 481, "slot": 1, "values": [[161046960075229799,15081533890008098182,8599524986623667909]], "path_len": 6}, {"layer": 2, "d": 1, "position": 481, "leaf": 240, "slot": 1, "values": [[13523182842471850292,10023096669923855615,13861557035808485449]], "path_len": 5}, {"layer": 3, "d": 1, "position": 240, "leaf": 120, "slot": 0, "values": [[67725569644445261,2660555931585193200,8656214761037478724]], "path_len": 4}, {"layer": 4, "d": 1, "position": 120, "leaf": 60, "slot": 0, "values": [[10483311268993759898,9100235776648696140,9649517420196481515]], "path_len": 3}, {"layer": 5, "d": 1, "position": 60, "leaf": 30, "slot": 0, "values": [[2870598012461783835,7827094732606362988,7524303457911972645]], "path_len": 2}, {"layer": 6, "d": 1, "position": 30, "leaf": 15, "slot": 0, "values": [[2109067029401775981,11881535591595910816,1594111885893068151]], "path_len": 1}]}, + {"iota": 618, "deep": [17392264061216667070,3799528080943413629,11407018515159519420], "deep_sym": [3459201148312316640,12192741989538265398,16701705627877756079], "terminal_position": 4, "layers": [{"layer": 0, "d": 1, "position": 618, "leaf": 309, "slot": 0, "values": [[6155497286543365943,6823874926095585913,8109198684617396853]], "path_len": 7}, {"layer": 1, "d": 1, "position": 309, "leaf": 154, "slot": 1, "values": [[9833782480490414257,17217623423597120964,8843356381310409960]], "path_len": 6}, {"layer": 2, "d": 1, "position": 154, "leaf": 77, "slot": 0, "values": [[2396282023618047124,5843578103453680384,12531555769585716035]], "path_len": 5}, {"layer": 3, "d": 1, "position": 77, "leaf": 38, "slot": 1, "values": [[2315015608088301356,11152573585322960408,1223035286229753124]], "path_len": 4}, {"layer": 4, "d": 1, "position": 38, "leaf": 19, "slot": 0, "values": [[9500029461340804850,2485568652985992693,10933239535849742952]], "path_len": 3}, {"layer": 5, "d": 1, "position": 19, "leaf": 9, "slot": 1, "values": [[3881399978602743849,14035452762582637444,9964587495007914030]], "path_len": 2}, {"layer": 6, "d": 1, "position": 9, "leaf": 4, "slot": 1, "values": [[18383902680402169760,11946899516366108295,4125966378742256522]], "path_len": 1}]}, + {"iota": 159, "deep": [10604883523135079191,13796358429189892774,13519594221824462179], "deep_sym": [6022801154996096846,1980184131037388799,13442421833478239751], "terminal_position": 1, "layers": [{"layer": 0, "d": 1, "position": 159, "leaf": 79, "slot": 1, "values": [[12670835830594729766,10775639007232550859,16555875757668836110]], "path_len": 7}, {"layer": 1, "d": 1, "position": 79, "leaf": 39, "slot": 1, "values": [[17928879845407862406,10337347125997172892,6291144022483388143]], "path_len": 6}, {"layer": 2, "d": 1, "position": 39, "leaf": 19, "slot": 1, "values": [[5290573282698993256,628791798829218958,8102047498213541200]], "path_len": 5}, {"layer": 3, "d": 1, "position": 19, "leaf": 9, "slot": 1, "values": [[14292313220916080740,6970106785239825796,6583204468013840496]], "path_len": 4}, {"layer": 4, "d": 1, "position": 9, "leaf": 4, "slot": 1, "values": [[7445378306973294798,9606155811494332299,13941390530992223098]], "path_len": 3}, {"layer": 5, "d": 1, "position": 4, "leaf": 2, "slot": 0, "values": [[9701830235220841955,11701359494893213657,2335524941435328481]], "path_len": 2}, {"layer": 6, "d": 1, "position": 2, "leaf": 1, "slot": 0, "values": [[9845217540333963803,15463176285714913034,4644416336041292088]], "path_len": 1}]}, + {"iota": 912, "deep": [12054840391066048689,3601865202668571108,3892056646934199431], "deep_sym": [9125201337264190457,16207456649903069037,12880097491155594601], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 912, "leaf": 456, "slot": 0, "values": [[2265230700843108553,17593609161732194274,15239094127825087222]], "path_len": 7}, {"layer": 1, "d": 1, "position": 456, "leaf": 228, "slot": 0, "values": [[16642757887568897673,12667640433037803234,3193412306162004453]], "path_len": 6}, {"layer": 2, "d": 1, "position": 228, "leaf": 114, "slot": 0, "values": [[13884150065805737304,15437553345903357492,13214756750175066224]], "path_len": 5}, {"layer": 3, "d": 1, "position": 114, "leaf": 57, "slot": 0, "values": [[11896301986580569790,4500452152629603038,11035825169199843994]], "path_len": 4}, {"layer": 4, "d": 1, "position": 57, "leaf": 28, "slot": 1, "values": [[1012511382225873659,15136067568327604338,60333472435469092]], "path_len": 3}, {"layer": 5, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[14606336359276572532,5481281866123149881,5272344691855823540]], "path_len": 2}, {"layer": 6, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[4057849095198852267,11892082119611012572,10198841277104067590]], "path_len": 1}]}, + {"iota": 28, "deep": [8259937475034448900,5883617888360704157,13149344576568937951], "deep_sym": [15432868084718982482,15561794222120679975,9170450966169669817], "terminal_position": 0, "layers": [{"layer": 0, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[9242926421875877150,16140238903834851021,13531888265072917881]], "path_len": 7}, {"layer": 1, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[13133029412005626438,798748005302048131,7332222029041754348]], "path_len": 6}, {"layer": 2, "d": 1, "position": 7, "leaf": 3, "slot": 1, "values": [[7796322037150240186,9577838651761349973,16298966752722802932]], "path_len": 5}, {"layer": 3, "d": 1, "position": 3, "leaf": 1, "slot": 1, "values": [[4038697707906694945,2235019174556978195,15759609321762126709]], "path_len": 4}, {"layer": 4, "d": 1, "position": 1, "leaf": 0, "slot": 1, "values": [[14091206233662491151,9403345652412347071,15921683191799735380]], "path_len": 3}, {"layer": 5, "d": 1, "position": 0, "leaf": 0, "slot": 0, "values": [[13879050027862793750,770925931399928084,9161288321845839824]], "path_len": 2}, {"layer": 6, "d": 1, "position": 0, "leaf": 0, "slot": 0, "values": [[8305487573804742677,4779453755826958897,5416814198222686333]], "path_len": 1}]}, + {"iota": 76, "deep": [10920452299577596995,3320775998846492791,6849000225488292243], "deep_sym": [11707565976056054812,18291447239090459750,10377317711770383279], "terminal_position": 0, "layers": [{"layer": 0, "d": 1, "position": 76, "leaf": 38, "slot": 0, "values": [[15441279527880237572,16195703840399892975,4758470874282941851]], "path_len": 7}, {"layer": 1, "d": 1, "position": 38, "leaf": 19, "slot": 0, "values": [[18288674810117416457,5942604063344825953,4067135455677183442]], "path_len": 6}, {"layer": 2, "d": 1, "position": 19, "leaf": 9, "slot": 1, "values": [[3243931976597857557,14219957437372274067,4958358972487943150]], "path_len": 5}, {"layer": 3, "d": 1, "position": 9, "leaf": 4, "slot": 1, "values": [[6405106249199677879,10131651475896546166,13979395597501489022]], "path_len": 4}, {"layer": 4, "d": 1, "position": 4, "leaf": 2, "slot": 0, "values": [[15443546284012971304,2993634536769373283,2481852760355668878]], "path_len": 3}, {"layer": 5, "d": 1, "position": 2, "leaf": 1, "slot": 0, "values": [[3684463403749072493,11162129148197513032,14132169035515477628]], "path_len": 2}, {"layer": 6, "d": 1, "position": 1, "leaf": 0, "slot": 1, "values": [[18015477955632229458,1590473352291154340,1284851958473884494]], "path_len": 1}]}, + {"iota": 1379, "deep": [2671572208018711864,17938506104726169260,699081865341519986], "deep_sym": [1467098100466440450,9829414836620083259,10592222250402520868], "terminal_position": 10, "layers": [{"layer": 0, "d": 1, "position": 1379, "leaf": 689, "slot": 1, "values": [[14575599383640259218,8800880267594736215,13241772630433177501]], "path_len": 7}, {"layer": 1, "d": 1, "position": 689, "leaf": 344, "slot": 1, "values": [[1068698510447171772,13785811637582432776,16021446183151759205]], "path_len": 6}, {"layer": 2, "d": 1, "position": 344, "leaf": 172, "slot": 0, "values": [[927602389973061550,5073606438314814579,9869150991189971161]], "path_len": 5}, {"layer": 3, "d": 1, "position": 172, "leaf": 86, "slot": 0, "values": [[2535000398961545073,13768963704791829580,7399445715925562080]], "path_len": 4}, {"layer": 4, "d": 1, "position": 86, "leaf": 43, "slot": 0, "values": [[7943116901400220395,6371902111188986077,18264207171394083113]], "path_len": 3}, {"layer": 5, "d": 1, "position": 43, "leaf": 21, "slot": 1, "values": [[1152140764835361081,13898656790604124651,211509517521939152]], "path_len": 2}, {"layer": 6, "d": 1, "position": 21, "leaf": 10, "slot": 1, "values": [[2898297057086247105,8938194155188493164,3922699838444447823]], "path_len": 1}]}, + {"iota": 432, "deep": [15168271348910525142,14863991650496712335,564399768222653450], "deep_sym": [14646540013786724593,627304134268139825,7693608058989799918], "terminal_position": 3, "layers": [{"layer": 0, "d": 1, "position": 432, "leaf": 216, "slot": 0, "values": [[16945056781763873103,4147654905260621537,13070939633098599226]], "path_len": 7}, {"layer": 1, "d": 1, "position": 216, "leaf": 108, "slot": 0, "values": [[8711713063120759004,17111841543407718986,7409132053022715737]], "path_len": 6}, {"layer": 2, "d": 1, "position": 108, "leaf": 54, "slot": 0, "values": [[335306033794264840,3472897435351186058,4033837016810566210]], "path_len": 5}, {"layer": 3, "d": 1, "position": 54, "leaf": 27, "slot": 0, "values": [[7651149011250772184,9302481777241286440,9218801175418545890]], "path_len": 4}, {"layer": 4, "d": 1, "position": 27, "leaf": 13, "slot": 1, "values": [[6273227611468200768,2351133109562315987,1227054803639332633]], "path_len": 3}, {"layer": 5, "d": 1, "position": 13, "leaf": 6, "slot": 1, "values": [[6241052350319128640,8850536336423185920,15942149741970688151]], "path_len": 2}, {"layer": 6, "d": 1, "position": 6, "leaf": 3, "slot": 0, "values": [[9090592503262467466,8815962496836678004,4005637083568909720]], "path_len": 1}]}, + {"iota": 1032, "deep": [17851351312830372904,17617180244584954975,6387515032357752484], "deep_sym": [16265563244185578973,3252040868362477919,11341903784051896692], "terminal_position": 8, "layers": [{"layer": 0, "d": 1, "position": 1032, "leaf": 516, "slot": 0, "values": [[14093802161954164449,2714211322275968535,16260835473628978242]], "path_len": 7}, {"layer": 1, "d": 1, "position": 516, "leaf": 258, "slot": 0, "values": [[18009724111979905813,17719079489240485487,11317947430945448557]], "path_len": 6}, {"layer": 2, "d": 1, "position": 258, "leaf": 129, "slot": 0, "values": [[12575320568206317040,12724817382033075473,17343707112876650515]], "path_len": 5}, {"layer": 3, "d": 1, "position": 129, "leaf": 64, "slot": 1, "values": [[2468983175696377286,8123992486804046219,14998166175444204309]], "path_len": 4}, {"layer": 4, "d": 1, "position": 64, "leaf": 32, "slot": 0, "values": [[7088268260308644145,12018111465420124058,11385959261406757704]], "path_len": 3}, {"layer": 5, "d": 1, "position": 32, "leaf": 16, "slot": 0, "values": [[4971290189600716302,5514894773276055215,7098701689719309312]], "path_len": 2}, {"layer": 6, "d": 1, "position": 16, "leaf": 8, "slot": 0, "values": [[7935000193983825264,5386745176623997326,2689120037326345474]], "path_len": 1}]}, + {"iota": 526, "deep": [7523797964145835221,2350256745822342772,7495064266374662697], "deep_sym": [12443822411346913512,588359790533263303,15431128093166141371], "terminal_position": 4, "layers": [{"layer": 0, "d": 1, "position": 526, "leaf": 263, "slot": 0, "values": [[2778619449088084568,6700130457648041594,9906573914237896132]], "path_len": 7}, {"layer": 1, "d": 1, "position": 263, "leaf": 131, "slot": 1, "values": [[16517878130852354667,8206981990154853455,13534764593092745799]], "path_len": 6}, {"layer": 2, "d": 1, "position": 131, "leaf": 65, "slot": 1, "values": [[13861028275067157678,7774492653685014689,10405536925574021168]], "path_len": 5}, {"layer": 3, "d": 1, "position": 65, "leaf": 32, "slot": 1, "values": [[7637396778244847013,7916728431030253874,4151452972168897929]], "path_len": 4}, {"layer": 4, "d": 1, "position": 32, "leaf": 16, "slot": 0, "values": [[11389250702043873935,10610216047882982229,14335557185298532304]], "path_len": 3}, {"layer": 5, "d": 1, "position": 16, "leaf": 8, "slot": 0, "values": [[1827481070999240844,7018596498770465949,12683874916808890167]], "path_len": 2}, {"layer": 6, "d": 1, "position": 8, "leaf": 4, "slot": 0, "values": [[17263492077473791592,13925874335233374149,14333243844514772893]], "path_len": 1}]}, + {"iota": 929, "deep": [7209442172898103651,5078432396478215691,3359681651317667922], "deep_sym": [278577782173243961,2648608839113898350,14362352946213059321], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 929, "leaf": 464, "slot": 1, "values": [[742727014119487962,9415944698655895699,15901341720432948056]], "path_len": 7}, {"layer": 1, "d": 1, "position": 464, "leaf": 232, "slot": 0, "values": [[11887246137253338911,17471030057314561898,834604778466298478]], "path_len": 6}, {"layer": 2, "d": 1, "position": 232, "leaf": 116, "slot": 0, "values": [[2389324468080173975,14177420689333058099,3158032004849467555]], "path_len": 5}, {"layer": 3, "d": 1, "position": 116, "leaf": 58, "slot": 0, "values": [[11619263787659055028,17318414171778481053,10437342890333779898]], "path_len": 4}, {"layer": 4, "d": 1, "position": 58, "leaf": 29, "slot": 0, "values": [[3959405585248829475,10255018570515570189,7622928700457320856]], "path_len": 3}, {"layer": 5, "d": 1, "position": 29, "leaf": 14, "slot": 1, "values": [[14583500637061974005,3413989937573586254,7710889154554862151]], "path_len": 2}, {"layer": 6, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[4057849095198852267,11892082119611012572,10198841277104067590]], "path_len": 1}]}, + {"iota": 147, "deep": [13192043606205898537,9469661584107672084,6315405556618292305], "deep_sym": [7642096387211884471,13644445525206900663,12919905892136611553], "terminal_position": 1, "layers": [{"layer": 0, "d": 1, "position": 147, "leaf": 73, "slot": 1, "values": [[5875338751136714530,7695228435662657364,5324885464763079482]], "path_len": 7}, {"layer": 1, "d": 1, "position": 73, "leaf": 36, "slot": 1, "values": [[12651956221626411650,9637806698413362635,7129211185802919037]], "path_len": 6}, {"layer": 2, "d": 1, "position": 36, "leaf": 18, "slot": 0, "values": [[6789928023159332903,8447447108763088136,17040077659500093070]], "path_len": 5}, {"layer": 3, "d": 1, "position": 18, "leaf": 9, "slot": 0, "values": [[15277897623289562052,8717440867787183431,5485634594337188074]], "path_len": 4}, {"layer": 4, "d": 1, "position": 9, "leaf": 4, "slot": 1, "values": [[7445378306973294798,9606155811494332299,13941390530992223098]], "path_len": 3}, {"layer": 5, "d": 1, "position": 4, "leaf": 2, "slot": 0, "values": [[9701830235220841955,11701359494893213657,2335524941435328481]], "path_len": 2}, {"layer": 6, "d": 1, "position": 2, "leaf": 1, "slot": 0, "values": [[9845217540333963803,15463176285714913034,4644416336041292088]], "path_len": 1}]}, + {"iota": 1839, "deep": [16378093384372275214,13015330254633996760,18205668585575265052], "deep_sym": [7837357816293187253,1010587672969474122,7673637645687776024], "terminal_position": 14, "layers": [{"layer": 0, "d": 1, "position": 1839, "leaf": 919, "slot": 1, "values": [[6124965313206069075,15411539721238865905,16260001499293568983]], "path_len": 7}, {"layer": 1, "d": 1, "position": 919, "leaf": 459, "slot": 1, "values": [[18150944016374367121,13957147611188270834,13296917984821951100]], "path_len": 6}, {"layer": 2, "d": 1, "position": 459, "leaf": 229, "slot": 1, "values": [[9313854673763300966,13537150988150843141,7627873754102692779]], "path_len": 5}, {"layer": 3, "d": 1, "position": 229, "leaf": 114, "slot": 1, "values": [[3800674278793119171,12770808243975540919,3800349180536817313]], "path_len": 4}, {"layer": 4, "d": 1, "position": 114, "leaf": 57, "slot": 0, "values": [[15928772226463361524,4352428081022084788,2641890282906734261]], "path_len": 3}, {"layer": 5, "d": 1, "position": 57, "leaf": 28, "slot": 1, "values": [[13689188977929574562,4258131260759228984,9939109160367792422]], "path_len": 2}, {"layer": 6, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[7786343213267754545,6056155651923690370,13889151246865202821]], "path_len": 1}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_pair.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_pair.rkyv new file mode 100644 index 000000000..f0b663c10 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_pair.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp.json b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp.json new file mode 100644 index 000000000..aae1dee82 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp.json @@ -0,0 +1,27 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "blake3", + "format": "dp", + "proof_rkyv": "d_proof_blake3_dp.rkyv", + "proof_rkyv_len": 8488, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 2, 2], + "fri_roots": ["f5660c4333b6e611e901e87422b2c4270acfad24f631d9429831d76b54566517","0cd55ebeb840e8373096d7b45d7a99eb5f2ae89c0b700d618f5bc5e5cf7d8cac","eaed7db0665b821d2c690ad7c99f3fb3b28d4b7d3c6167be86e2033300fc3997"], + "zetas": [[10771210179622817679,127754635188287825,9592161990157892076],[2334636387541570725,4598538304975584359,12240732424901763132],[10166376440375277554,17046118386894069248,10115891851528829537],[1897382409627266702,6084356605560232122,6323818535469693028]], + "terminal_coeffs": [[5380735102582769720,14770520085343157731,17397790325610342738],[14177126935727096750,9878484623770692025,8126381307417814598],[13709110344626157024,14960543001611777495,13121995131109452668],[3543500174378306775,245990784589754978,17448449264639928647]], + "queries_detail": [ + {"iota": 975, "deep": [10762173397373278909,6238205991322615201,16902290430091080608], "deep_sym": [6360510169239840515,6098314158097471188,14374437857455028609], "terminal_position": 7, "layers": [{"layer": 0, "d": 3, "position": 975, "leaf": 121, "slot": 7, "values": [[6358380543480134188,15233297943481819331,7802884743754435287],[1561050269828331995,17056437354980486597,15953089478678981981],[9902832432796422005,16144145762744260912,2452053501664136327],[4059115819907980750,13622239393642520008,10742588081843022005],[15097201520147174657,13343912955236085051,15486897778505591961],[5267077696719908108,9464151356603282791,2320496645939400341],[7693143272069720023,17017576095617663956,6992176147234983386],[14242806506751524709,5590906401091982117,17893550137403079326]], "path_len": 8}, {"layer": 1, "d": 2, "position": 121, "leaf": 30, "slot": 1, "values": [[10075702410473013791,4716385726265313391,11609542590222699029],[7845467542947739937,12387174233603512715,18200019442985323599],[2909241347398984484,5399916910453173204,9233450494253184011],[4113688591137365584,8099000987494976281,456105366810755859]], "path_len": 6}, {"layer": 2, "d": 2, "position": 30, "leaf": 7, "slot": 2, "values": [[4720003309196990551,4178739593595040029,10411467881539262427],[17765747612192294497,14700500661535383226,13695608480138073067],[1009102207610472007,14305427383432249940,4131500240907956149],[1496298963912337677,3644564800854218514,5628674521221557415]], "path_len": 4}]}, + {"iota": 1979, "deep": [1491025643980379174,12685070184352261704,7728318385342818721], "deep_sym": [16450052277900778758,13025974084100681593,284476606938439535], "terminal_position": 15, "layers": [{"layer": 0, "d": 3, "position": 1979, "leaf": 247, "slot": 3, "values": [[17193942932342370980,12145038434561791480,11576353455053528850],[8379852527282925844,8601836539934055187,15168362594496179266],[2592359005135839197,5381418174777500780,3896499938254073228],[4822332081114310830,2565510977245490832,11907903767149013019],[13285130888391799040,1041585227093494597,2304343024112292721],[11261822772218442511,17634015746885931060,3497632743424289560],[10018483860357178719,5407737136598053306,11038589506904123227],[5386981123451134746,15435103548476985734,14493978820809476719]], "path_len": 8}, {"layer": 1, "d": 2, "position": 247, "leaf": 61, "slot": 3, "values": [[16716297285756313772,4980386954389515672,7587525751122803270],[14964533687760416685,13917074670739361960,17216570321581969515],[3750454710447327212,10631398170004764756,11015661283393845074],[10023782474286288464,1360821076844805368,12167231288721989489]], "path_len": 6}, {"layer": 2, "d": 2, "position": 61, "leaf": 15, "slot": 1, "values": [[6218711529479866098,15946762367783592827,15439960344592751968],[13215040631267974591,7742956115152970799,9370753578504439872],[8789152590505293503,2216348788422376564,6920254999236655340],[702787882218669028,13764292175754416497,529455276546629621]], "path_len": 4}]}, + {"iota": 196, "deep": [12996823082221702228,15546436838001982955,12765624607176451818], "deep_sym": [15258355026629750431,13813035249993486719,14742394712154322144], "terminal_position": 1, "layers": [{"layer": 0, "d": 3, "position": 196, "leaf": 24, "slot": 4, "values": [[5990450875556600463,11483765027548679992,15569727055914393856],[15481225486576456763,6990687647448126227,17377462139297815371],[17823918767605952564,15408822346409695669,7766449979365244357],[10521282185009747890,10208004641233698759,9793502955518714526],[15989513811586569514,8058942065900320453,2895357368330783138],[12375312625287684078,17725753712340770465,14836143340177261245],[6076100021308446097,6158543389568230761,12040165962685290590],[12787237553493716673,6399667255546886261,10645808993947995036]], "path_len": 8}, {"layer": 1, "d": 2, "position": 24, "leaf": 6, "slot": 0, "values": [[3955724155215551649,4903104037739106676,8341497941364977219],[1366207781672916929,4653995748612992444,1985491773491753288],[12110069387491220579,3462604155932552453,12998143760810181545],[12022604030106382896,9872253098559971229,8337931158086605882]], "path_len": 6}, {"layer": 2, "d": 2, "position": 6, "leaf": 1, "slot": 2, "values": [[13763808492525116750,17048718856988468120,4007208303874274964],[9747356233047801788,12353956971904007004,16715523138327251637],[17215889828796017128,10441867300762877602,5539458827215095394],[16874552336957454881,3298743664346751734,7613930184659830057]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp.rkyv new file mode 100644 index 000000000..af336fd71 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp_3_1_3.json b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp_3_1_3.json new file mode 100644 index 000000000..5b2af63ec --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp_3_1_3.json @@ -0,0 +1,27 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "blake3", + "format": "dp_3_1_3", + "proof_rkyv": "d_proof_blake3_dp_3_1_3.rkyv", + "proof_rkyv_len": 8728, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 1, 3], + "fri_roots": ["f5660c4333b6e611e901e87422b2c4270acfad24f631d9429831d76b54566517","8f8497f1cc45d51572bb910d068306f0bfd0786175356863f7f078d4cdf4e6ba","559c0a403325bab03ff3d994f3f4bbb8bc5a5273ce96b19cfa3b3ce702096f79"], + "zetas": [[10771210179622817679,127754635188287825,9592161990157892076],[2334636387541570725,4598538304975584359,12240732424901763132],[1967510421138513926,14736776382609640613,1460789662965777522],[16830093636709684605,14842995202055722516,3914963795294019471]], + "terminal_coeffs": [[3331264495832828347,7185866664642756789,6843402643577021942],[10027383757451337894,9264328518027569750,618424366955010939],[8692021868639966632,14854503284949996751,16299654113095994525],[16358064282920315685,8517693515046071617,8244358201180006711]], + "queries_detail": [ + {"iota": 100, "deep": [15761165101399351880,9293892056917660698,13909387281546150815], "deep_sym": [18215445829741639441,18204034434822136671,3853351857208611712], "terminal_position": 0, "layers": [{"layer": 0, "d": 3, "position": 100, "leaf": 12, "slot": 4, "values": [[2664088888686076698,9363645576360161584,1094240937101656707],[2795005918076354239,7750515319260536800,16289535278568684750],[7828826467911276845,1956586071935190428,15259431314043971928],[7822913789832758503,9918120603487277767,12026915655054942547],[888266083852283926,5096366371443196859,15117111606629506813],[4632446112795977254,70591550309874064,2974240486951077444],[3263397032290487763,10334174333608144873,7959568761616464988],[3552368110883502870,10051530687251218433,1225942363435614861]], "path_len": 8}, {"layer": 1, "d": 1, "position": 12, "leaf": 6, "slot": 0, "values": [[15737476631448817853,12900508163676467004,1839840965848663120],[1924436232008221847,6241001949124084469,16747179164658090748]], "path_len": 7}, {"layer": 2, "d": 3, "position": 6, "leaf": 0, "slot": 6, "values": [[15641448562265420777,2088983744824930344,11939682928497748793],[736521859268441061,8342215319580479488,8897739578889100429],[2233749865829555840,7685479678190027881,7165376174653672864],[16112495396145500837,15368970251974031937,8711572005387377201],[10829235607656718845,15945783778988408022,6451476517960690463],[13004491108608034011,5760372999051763783,10788993322566782709],[867253151713547845,15943107042851967891,4251287764631266192],[15157411414371223400,10098736874879334226,12514777895591726498]], "path_len": 4}]}, + {"iota": 1086, "deep": [15608288023485154616,2200294644984832055,4541546713926039807], "deep_sym": [3079430625393683639,12119948572157099431,4967034397857673556], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1086, "leaf": 135, "slot": 6, "values": [[5064229977681024521,9535269840323459819,2145244677072055460],[125607644455879710,14506413514331503134,7702126688776868511],[12678478928085254970,4540996268990745755,14388757028488235130],[18401672303609607515,7662131759563109282,7660269693766476361],[12595928190669188977,1618490482153422770,8961555872666895518],[247968980481965489,17983378050067665214,4220288566350421147],[10455930571559153168,8083836495281389550,11606628425775345710],[6321202569249989985,13520813269599138260,9359837729431782857]], "path_len": 8}, {"layer": 1, "d": 1, "position": 135, "leaf": 67, "slot": 1, "values": [[9987387063165574134,10270232291887854585,7103905701655407856],[6084978948665007904,10991990918095191655,5571913575443446873]], "path_len": 7}, {"layer": 2, "d": 3, "position": 67, "leaf": 8, "slot": 3, "values": [[13495453314946811061,2343295106864069961,6620630672964439277],[10552127783637139629,3020177997249405451,7832607995418649242],[8174254051324615715,17948814487092268693,16221369621163145135],[15430193703939101357,9596189644948662926,6169730862756697992],[17705515929508746677,7532640930868421021,1201111313798040228],[4684151188798651217,1435893368118583475,17905397822166319127],[158248900632187591,7681052109159395530,11617423390658273269],[13135522550772402387,13249575094922739378,5732136554014601552]], "path_len": 4}]}, + {"iota": 53, "deep": [9529224066667257075,18203512621861259113,8451301895307292472], "deep_sym": [4759052315125095300,2509792134269440990,9502699147162584206], "terminal_position": 0, "layers": [{"layer": 0, "d": 3, "position": 53, "leaf": 6, "slot": 5, "values": [[6834949326104870826,11079296222041095752,10563898283978097910],[18016976558759513078,10014951360803540706,7578552812701626973],[7308646796660453164,16827564718672090573,3430734446912554275],[9366807165793692103,18212281866171309151,15746885233303123394],[12216624691318647156,17593898404433611439,4395950489549474014],[9621925250936315282,2015707641277960720,9413688144023119266],[13994221524705245228,2626300897160867166,5634372390616255857],[5369690143385904477,4543638345355920554,12365449364504512872]], "path_len": 8}, {"layer": 1, "d": 1, "position": 6, "leaf": 3, "slot": 0, "values": [[14698668472270914105,16176977995025063834,18253104268403247552],[5896354434492429616,9697870880870630454,4609762693976853786]], "path_len": 7}, {"layer": 2, "d": 3, "position": 3, "leaf": 0, "slot": 3, "values": [[15641448562265420777,2088983744824930344,11939682928497748793],[736521859268441061,8342215319580479488,8897739578889100429],[2233749865829555840,7685479678190027881,7165376174653672864],[16112495396145500837,15368970251974031937,8711572005387377201],[10829235607656718845,15945783778988408022,6451476517960690463],[13004491108608034011,5760372999051763783,10788993322566782709],[867253151713547845,15943107042851967891,4251287764631266192],[15157411414371223400,10098736874879334226,12514777895591726498]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp_3_1_3.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp_3_1_3.rkyv new file mode 100644 index 000000000..225fc63b3 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp_3_1_3.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_pair.json b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_pair.json new file mode 100644 index 000000000..6484a9238 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_pair.json @@ -0,0 +1,27 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "blake3", + "format": "pair", + "proof_rkyv": "d_proof_blake3_pair.rkyv", + "proof_rkyv_len": 11136, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "legacy_encoding": true, + "total_folds": 8, + "terminal_len": 16, + "schedule": [1, 1, 1, 1, 1, 1, 1], + "fri_roots": ["2fc983d7a9f8dba305332d7c27f44e2afc07aa1fe014ad8a85ca9bc36844a992","a01be93f245807d30fde826335bc7dd8bfbf9fb0545769f85da9a6ee2b564301","4cc8bdb5c5d436e8b5cc91aa230573d630e9eb8a908f2650f9d4598e3d731b65","82de5d8f879bb994fc9573a6c3706b71adaf8b236e17ff2047e44bfa64f1e480","779e1acea2c1b391312f39412312a9a6bcb8cf5a66e1954d2b9d340a74a74bd0","191d0d55f0bf47af108196ca3b2c067e667807e78ae6681a814b3be0a8f4e445","878efaffc3ca3b900bd232b64cdd6a142203e2ef0ff1cda3f52ec590b776f071"], + "zetas": [[10771210179622817679,127754635188287825,9592161990157892076],[339236561547217708,14515476371055385421,3041135081988152589],[7430745936816588155,8998042728974583901,11515773416551488605],[7956826836586454026,8667292109104632665,2851244499340860067],[16324173539864659489,11301157219502799655,18016560099956879839],[4272458413724263223,15273501817168123109,13432776003642703715],[18153136978195245525,4668271491129789573,15852649611975035906],[13206066232974685659,15811531208029248608,9742874826372310642]], + "terminal_coeffs": [[11908419985256297049,6320124696091700849,10477651950916658009],[16710003718284845920,14728440137509904251,12073313539240356766],[15142905694919717110,8656948196775444897,1363513317241862160],[2198207007945388790,2708142890943514224,17003186495140238478]], + "queries_detail": [ + {"iota": 1803, "deep": [15272426180920759111,5106447191221278975,14792296330971372023], "deep_sym": [10304415851256192438,7276545599604954905,12402529132092837573], "terminal_position": 14, "layers": [{"layer": 0, "d": 1, "position": 1803, "leaf": 901, "slot": 1, "values": [[3388282554933400969,3823175679916949076,2787870681482871753]], "path_len": 10}, {"layer": 1, "d": 1, "position": 901, "leaf": 450, "slot": 1, "values": [[17941501397892820289,9746070180589186316,4483120140038292319]], "path_len": 9}, {"layer": 2, "d": 1, "position": 450, "leaf": 225, "slot": 0, "values": [[1518365534971821388,8220153128022570539,11364526563819683345]], "path_len": 8}, {"layer": 3, "d": 1, "position": 225, "leaf": 112, "slot": 1, "values": [[10971480354833343982,3135816652628770915,6720283715471365573]], "path_len": 7}, {"layer": 4, "d": 1, "position": 112, "leaf": 56, "slot": 0, "values": [[5709537754561370510,10236832031319039769,1874314679153150939]], "path_len": 6}, {"layer": 5, "d": 1, "position": 56, "leaf": 28, "slot": 0, "values": [[2528382099678622252,12218130109821183716,1136296192569704372]], "path_len": 5}, {"layer": 6, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[7786343213267754545,6056155651923690370,13889151246865202821]], "path_len": 4}]}, + {"iota": 474, "deep": [4642716204571870719,13791353321977000304,9948795077040124575], "deep_sym": [10074490863165540107,1346332627183725457,15559140971681542809], "terminal_position": 3, "layers": [{"layer": 0, "d": 1, "position": 474, "leaf": 237, "slot": 0, "values": [[9803582068471756145,5326669840186105035,7793279955894935834]], "path_len": 10}, {"layer": 1, "d": 1, "position": 237, "leaf": 118, "slot": 1, "values": [[18172681946601424763,4149543359487769368,2150741210857753378]], "path_len": 9}, {"layer": 2, "d": 1, "position": 118, "leaf": 59, "slot": 0, "values": [[10054490575191079786,12193424298068301071,8417982262482120641]], "path_len": 8}, {"layer": 3, "d": 1, "position": 59, "leaf": 29, "slot": 1, "values": [[15947123418199701165,18407774728151935281,3292539258646734529]], "path_len": 7}, {"layer": 4, "d": 1, "position": 29, "leaf": 14, "slot": 1, "values": [[3117529940052833834,10473131964376682009,2083760568833245811]], "path_len": 6}, {"layer": 5, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[7713732352748329805,3614247649513246873,888672929281612740]], "path_len": 5}, {"layer": 6, "d": 1, "position": 7, "leaf": 3, "slot": 1, "values": [[12707951974262439387,10229714375229842447,6079425424868885692]], "path_len": 4}]}, + {"iota": 1018, "deep": [15882578000804364217,17570699945731153943,17271573467219472049], "deep_sym": [11776097457111120055,8466990234121688300,9890187330955688279], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 1018, "leaf": 509, "slot": 0, "values": [[11811277273608028663,11557005519804590428,6147111063572348711]], "path_len": 10}, {"layer": 1, "d": 1, "position": 509, "leaf": 254, "slot": 1, "values": [[7939867388228535545,10890533442334944369,9121900079366539214]], "path_len": 9}, {"layer": 2, "d": 1, "position": 254, "leaf": 127, "slot": 0, "values": [[432090123891713462,15713972828822391493,11186642764496342828]], "path_len": 8}, {"layer": 3, "d": 1, "position": 127, "leaf": 63, "slot": 1, "values": [[1293317382852890727,12037476111710244625,8463877166491912968]], "path_len": 7}, {"layer": 4, "d": 1, "position": 63, "leaf": 31, "slot": 1, "values": [[14282184254670867115,4596231514897671604,8263298406545493773]], "path_len": 6}, {"layer": 5, "d": 1, "position": 31, "leaf": 15, "slot": 1, "values": [[4047442787689190383,14125010312351736105,11271388519733766106]], "path_len": 5}, {"layer": 6, "d": 1, "position": 15, "leaf": 7, "slot": 1, "values": [[5728710831141537085,4396671778989160837,8614177465654515251]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_pair.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_pair.rkyv new file mode 100644 index 000000000..3aae6b0f8 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_blake3_pair.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_dp.json b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_dp.json new file mode 100644 index 000000000..e51187926 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_dp.json @@ -0,0 +1,49 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "keccak", + "format": "cap_dp", + "proof_rkyv": "d_proof_keccak_cap_dp.rkyv", + "proof_rkyv_len": 41480, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 20, + "grinding_factor": 0, + "coset_offset": 3, + "merkle_cap": "auto", + "trace_tree_depth": 11, + "trace_cap": 3, + "fri_tree_depths": [8, 6, 4], + "fri_caps": [3, 3, 3], + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 2, 2], + "fri_roots": ["24ad3d0e98f4bed6edf18ec793157a4b40d412b869a719de1c98a970fab00072","f9fe88a494b4bb9e5ca08fc1c1a7a3ef4624b75d0ad3812e39dc65d9020f91cf","7ecf9321963996332fa2eb0464dd78c32efb4185fbc866687520f36de7764131"], + "zetas": [[5019159632337129269,238091556992722228,5532889084085155677],[12296403571495774788,9626523507187974856,1515890197535251952],[9104931154505306807,6806930774857449431,13982536486847686418],[17943736705802395901,4283444887199783601,5105112647180456117]], + "terminal_coeffs": [[18046538310705593629,16035634115336395623,14269474772235161333],[13449754012599068599,8932449597508197521,3279495531022860796],[11948801525571458678,1807139812678879355,3178944376615033389],[11072843192958306056,3667138469373329065,14070513692562577743]], + "queries_detail": [ + {"iota": 1277, "deep": [112612903969624832,13540544077113206977,891744669294414204], "deep_sym": [10665692780904921752,13891743997545272459,5218021303841191956], "terminal_position": 9, "layers": [{"layer": 0, "d": 3, "position": 1277, "leaf": 159, "slot": 5, "values": [[2726840187197314970,4641373057133563422,18254905628294267124],[7726190489312153580,9907582621009652564,13704195924065075984],[4913854101341609490,7059003433635310965,6314417828660586086],[15329881112488297229,17154024704563340256,10996086559637584958],[8931011741374043492,14857842271836329185,1962274052252210912],[11335680486698327443,1217890136881310458,6960827381617415085],[11843024897136178316,4050095544328259531,1109189699536974526],[15465183123903289453,8756197396528546255,3770807126986676922]], "path_len": 13}, {"layer": 1, "d": 2, "position": 159, "leaf": 39, "slot": 3, "values": [[9337955700188682368,10005268201090501927,17075626829468745589],[12775344777395238092,12444988312381492194,18162313775388685340],[12834611725527989937,3931095319124210104,7011958104454824522],[18278843176886412077,2091177023787081796,10712499925409758781]], "path_len": 11}, {"layer": 2, "d": 2, "position": 39, "leaf": 9, "slot": 3, "values": [[12663636275089871938,1342734324200714786,13647156802297113741],[16459715330172958447,16246821789525783433,13803231028510688298],[4472259574895772221,15705768718567917064,4738154395575758232],[10184880754128237084,3408521813484574087,14812129773919197844]], "path_len": 9}]}, + {"iota": 1793, "deep": [8057175728474570347,4157164488656378128,15766577891820220836], "deep_sym": [6733030217476856996,3149008183846048310,5846868056871306014], "terminal_position": 14, "layers": [{"layer": 0, "d": 3, "position": 1793, "leaf": 224, "slot": 1, "values": [[5863889590658237167,8803207495494391631,488510412724115696],[7367902939689275966,5399515143439789253,13537028177165637670],[13057594490447211533,12028941489541574294,10245716700381823303],[3536160573392264847,13647402147340435120,9933763201558099138],[17705005489971962397,16100850492966888022,3356205035428066804],[4834413239841014089,10648175143241294336,14941339194282038433],[11268069224352915944,7397295511095760171,650865519941991105],[17810125080425025549,8252558882871738031,1603536863803495337]], "path_len": 5}, {"layer": 1, "d": 2, "position": 224, "leaf": 56, "slot": 0, "values": [[14576290996393278046,4099269296443923918,13962179114375143747],[8539012341704406611,14597685688217769420,16489000745330409389],[4928849784411569862,5656061150696874101,18052668495466081830],[10699077948197816254,7120867110842505641,2470038313983831606]], "path_len": 3}, {"layer": 2, "d": 2, "position": 56, "leaf": 14, "slot": 0, "values": [[7743892560805052942,10417724895478695146,9242061460595868046],[5215961958705134614,3646588380176163324,11215186743127464548],[18165082680919870330,16446510594026310692,9931249060720003450],[16761232818563256745,6094664995607304883,14831579372437454855]], "path_len": 1}]}, + {"iota": 1422, "deep": [4336444987633806031,42270359695066150,811124501724833250], "deep_sym": [6040322601513087150,2232031154133685564,13268270931765776955], "terminal_position": 11, "layers": [{"layer": 0, "d": 3, "position": 1422, "leaf": 177, "slot": 6, "values": [[5773020228763106950,12181432689259931341,2904380769668095371],[5378436167230488318,1136926564836430281,11025181981762941864],[9767397875213699870,16391873535337268069,9544088588384136146],[50456808154105685,7570275210936766391,3076092320148066703],[6519022523009943654,14501422860440411207,16766709789063948727],[1942043923567112082,9396051082847161748,4275006641168421309],[802135155222683305,8086721014210384187,5276472197522953276],[14963808776084644130,11822327586546991308,9902819457375193080]], "path_len": 5}, {"layer": 1, "d": 2, "position": 177, "leaf": 44, "slot": 1, "values": [[8007252311096823131,15451587500561065094,5200475833640745404],[12419273660987748398,619789569423171010,4299596803633862803],[14489492344890871493,14652990201720622453,5263973935492910147],[12959859661416681018,3696933911172366326,18087484035368403648]], "path_len": 3}, {"layer": 2, "d": 2, "position": 44, "leaf": 11, "slot": 0, "values": [[5556470237486890782,15738386834927433074,12010912686098111476],[1997630762550526785,7678738670208248417,194037932413528879],[7091997090193394797,1911130281305530368,8017953523793910594],[1805231709436512031,5522280617529416910,4194339594951184587]], "path_len": 1}]}, + {"iota": 375, "deep": [15896026706216286558,850738027208789055,10723639171146949965], "deep_sym": [3646273813276642761,2241354158260357600,16568479433952979865], "terminal_position": 2, "layers": [{"layer": 0, "d": 3, "position": 375, "leaf": 46, "slot": 7, "values": [[15387740746947457604,13589966986149538764,14032877163179148290],[13708225577102587055,13441481804739059493,8991395910890718293],[10414453853530795658,15349599265905617934,9809656972176258562],[13902200104895237219,8980123068515626615,17539094337944877822],[18430868960253363076,10447808943170909240,152854829625985981],[9586311282657541841,11963572983397487123,15423990981040471599],[8970700645449704597,438050523969751745,14841344594967333290],[54692778579574998,13160092994989063843,16827559681417886436]], "path_len": 5}, {"layer": 1, "d": 2, "position": 46, "leaf": 11, "slot": 2, "values": [[10482637989529479610,11046049345170984111,16611894623477967708],[17524538985625388400,3561755534548238727,12316349888672143316],[18242276635503340986,2708203015724362019,5591280696094092762],[6520070575385940768,12335650810165131772,14579374211771389454]], "path_len": 3}, {"layer": 2, "d": 2, "position": 11, "leaf": 2, "slot": 3, "values": [[7120644982042697520,4877211885227668924,17408917161652445778],[10118435938790577823,5444915436844861924,11542569338792872830],[157835802203199106,17140088994041704553,3978505728255765620],[14623016493704661158,10884525528551030981,1014977751818009979]], "path_len": 1}]}, + {"iota": 1948, "deep": [7087937129631102186,10324887181174666606,10384177212640173098], "deep_sym": [4839108783067615636,16148817134865123179,14412947888881988463], "terminal_position": 15, "layers": [{"layer": 0, "d": 3, "position": 1948, "leaf": 243, "slot": 4, "values": [[4384664434476341418,4109755903884089713,448848679048843676],[502577095427712111,1950110303512630307,2083947009798347444],[13138990615390871461,16250161617582919998,9900591626273979634],[8185137308944158747,11670104219697834910,4945466864899594101],[8246417461024835979,11435835163928684321,12443831336801837744],[14595002883778732256,3549608309749680403,9674226137969631683],[2538087213630520003,9729540931449938752,14042293170573545],[17478515448816995850,3847752760399403901,1279474917978074079]], "path_len": 5}, {"layer": 1, "d": 2, "position": 243, "leaf": 60, "slot": 3, "values": [[11375860614512743705,12159552691638095048,1748577297743065506],[10427635604250192507,12188377611915858990,3305939877387789858],[5427071017873339313,12464684671386940995,2097029895587251782],[5305312732620219072,1636602398709148096,9970069024630049051]], "path_len": 3}, {"layer": 2, "d": 2, "position": 60, "leaf": 15, "slot": 0, "values": [[16083923945252889347,14007201087796689962,9760068277015825398],[13908085417078852557,6940036306507102360,12381427201366074798],[17699679852100348611,14554545952620420176,15525485196160550265],[3320877808744335198,16258308082275368886,16540132530644722341]], "path_len": 1}]}, + {"iota": 1966, "deep": [2737464584824995267,14918683733418229385,7196299024600452041], "deep_sym": [18370036556571597981,1830788502375962850,10934794948443612497], "terminal_position": 15, "layers": [{"layer": 0, "d": 3, "position": 1966, "leaf": 245, "slot": 6, "values": [[13671960172398405357,17855080896113870507,9364618645616150458],[17414960211254073904,14038003153076967146,14805527776041656840],[6673455254303213924,17167822529254968482,6338703889954748273],[817936940740959714,13256059908467474472,4760748120011860765],[16094771459087701234,653221362292145817,15615540196276244089],[16454431268210406849,9591779889310584976,14000251994194405957],[16611363856511027300,5707903787206279761,3321883520519277902],[17140913678660095447,17794956949402229996,1697394557625431157]], "path_len": 5}, {"layer": 1, "d": 2, "position": 245, "leaf": 61, "slot": 1, "values": [[14919330226061001544,7003661483458825842,4258396156973505816],[1749975506220495240,14492956044198192519,2252471877392573853],[10278317655546946839,2718392541606326429,7276295292726873270],[6742827957320313011,989165657890668514,10537792207525539025]], "path_len": 3}, {"layer": 2, "d": 2, "position": 61, "leaf": 15, "slot": 1, "values": [[16083923945252889347,14007201087796689962,9760068277015825398],[13908085417078852557,6940036306507102360,12381427201366074798],[17699679852100348611,14554545952620420176,15525485196160550265],[3320877808744335198,16258308082275368886,16540132530644722341]], "path_len": 1}]}, + {"iota": 1057, "deep": [288064885883882665,10890893893344733623,4388390573256310456], "deep_sym": [77447489977792414,17008457381051219699,8203417707137847288], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1057, "leaf": 132, "slot": 1, "values": [[14602002417936541588,2060133397734822291,3872298068558041525],[16644565893843641095,6451828667327073839,5933463085769248929],[3093170179341351174,16475110652552847864,4116847028819369612],[7455182260930115447,9256567728239627584,10404107984679348218],[462894392829998510,12545489967056621090,9716019685493474827],[5141239682319935027,12773795560778526585,11627811492010847833],[16920552392970673286,12668306819870810712,13126806710171727182],[8142891815470815835,9396478658697097228,12628929348098678933]], "path_len": 5}, {"layer": 1, "d": 2, "position": 132, "leaf": 33, "slot": 0, "values": [[6762027819238736502,1994817665773640169,12308975809811156353],[430727574191951338,6595710101744551501,9974788267850124432],[10843514598889494219,10953618560938278742,4109801507007785601],[14297383862207660850,4695764767127474585,15066808880087009327]], "path_len": 3}, {"layer": 2, "d": 2, "position": 33, "leaf": 8, "slot": 1, "values": [[11758409758221297221,11034734048298619314,14549108557531584252],[8435201558561332969,8784175351312936877,16456903953760264513],[15792142755751630193,13249956046916067676,3979910583159107465],[10326060926810026901,6731236006246528468,9917621895543682894]], "path_len": 1}]}, + {"iota": 1649, "deep": [16113310391862898080,3750040675083148501,18062999865878766004], "deep_sym": [14118473159812533224,8837649250340416771,4486451966545117618], "terminal_position": 12, "layers": [{"layer": 0, "d": 3, "position": 1649, "leaf": 206, "slot": 1, "values": [[23773808765211251,2724987214266210270,10363574896205763514],[1063447907708073502,16993201402556864939,6216290352654158679],[13902131533558810019,8219699955683922369,5524029970275705845],[9663368849740022629,6328211828585780341,16135481082943213812],[13309480709205613563,13201964663308045097,15012894330470548828],[7952710722453649074,13361594031191679724,2551826062919144587],[2190871277041262405,14972906233227189104,13243835470767713833],[16257674481805524096,12933708557913758180,17717332288188416810]], "path_len": 5}, {"layer": 1, "d": 2, "position": 206, "leaf": 51, "slot": 2, "values": [[5012420639169771645,8546165005815674507,12115096682090388454],[1313249665614203061,11495183200823441072,7479279848115399142],[8563943629740366687,3146950596701191193,16306076291276600083],[11544006396789333386,7474851117767078503,14971448517887911011]], "path_len": 3}, {"layer": 2, "d": 2, "position": 51, "leaf": 12, "slot": 3, "values": [[1545572393754741841,11690420781243035314,9918423316728445502],[11627290908117424199,6051342473574127770,9134970903777327367],[8604029722356773913,15305165503694345878,7930243008777305646],[7998741184655164490,4603484846085525689,5714420271927771819]], "path_len": 1}]}, + {"iota": 1030, "deep": [14373800732940166725,18430465542586899752,3776694530884561141], "deep_sym": [7463758950313777458,5528294336202769938,10635152219191787810], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1030, "leaf": 128, "slot": 6, "values": [[5780659269726964115,15217834712966871504,15901731355578075927],[3191285795483457442,13926700417060571460,14330808147944740432],[13795075764902119338,6281162078196555170,6359184787760896428],[10558246965775800911,3982131670900263214,14050992306146354219],[2516726934071551986,3136147731612096946,14474217385552349651],[14293016964834423850,14204722566823234558,349167635719564975],[13167235163881118400,12264236181888999124,16205070808867739650],[11049080656783136961,10255632632760224697,17530910159094919662]], "path_len": 5}, {"layer": 1, "d": 2, "position": 128, "leaf": 32, "slot": 0, "values": [[1603596091347416547,1685572709416834966,14131289672992784683],[16630113299352830035,4130651607240740485,3953453261651720240],[12833920182305139813,5357683431524468303,17512826818408810918],[10196527414059609335,2811204653129765191,3987710025744114355]], "path_len": 3}, {"layer": 2, "d": 2, "position": 32, "leaf": 8, "slot": 0, "values": [[11758409758221297221,11034734048298619314,14549108557531584252],[8435201558561332969,8784175351312936877,16456903953760264513],[15792142755751630193,13249956046916067676,3979910583159107465],[10326060926810026901,6731236006246528468,9917621895543682894]], "path_len": 1}]}, + {"iota": 282, "deep": [4605030828346954542,10107141483819085453,16340363278551917239], "deep_sym": [3153592424307667625,10703831073067631050,3420493377982575506], "terminal_position": 2, "layers": [{"layer": 0, "d": 3, "position": 282, "leaf": 35, "slot": 2, "values": [[16351637314632867303,10550715376056936204,389777731531914906],[5423756822712339682,3207707814678981159,14554301294689068832],[9522977257558972583,1237680911860807845,5581983543124725091],[18155213215714573372,15753220935196394414,15413435704086206572],[17467711387504357770,12918792077398046747,13177021707668672818],[10384275476170871726,17189159971389164274,4738120100226236541],[2016889463784941813,15496084267640314674,4377476686201105459],[11044917781013069171,5759042811678180522,8832858058718560393]], "path_len": 5}, {"layer": 1, "d": 2, "position": 35, "leaf": 8, "slot": 3, "values": [[3773095292795911748,296981318361970991,3709827473597750432],[9579271102479213590,9301751476218455612,2459672023739543089],[16953705104003922674,10560831490120226302,12377739364455719677],[17933522243182263073,9435594104570083871,15300526250364715780]], "path_len": 3}, {"layer": 2, "d": 2, "position": 8, "leaf": 2, "slot": 0, "values": [[7120644982042697520,4877211885227668924,17408917161652445778],[10118435938790577823,5444915436844861924,11542569338792872830],[157835802203199106,17140088994041704553,3978505728255765620],[14623016493704661158,10884525528551030981,1014977751818009979]], "path_len": 1}]}, + {"iota": 1941, "deep": [9440125841541173544,15658990514951940362,9945303899609953144], "deep_sym": [2216207227382168010,1020647313760285429,17493691611512355197], "terminal_position": 15, "layers": [{"layer": 0, "d": 3, "position": 1941, "leaf": 242, "slot": 5, "values": [[1844732848412660250,4602035078406182240,18209111516444714084],[1788494049217962277,7927061198967915720,4534818919883673538],[16061940833143770472,10191170167737465651,16698057848562098452],[13677653368054703338,2015726882419666285,1181093536994502902],[13518204986550635413,17541492429993064125,12452400951551991409],[6813945670717812094,4826602370772664987,2554812707018042294],[6452100949112115734,2943030424053276291,18381591199498086949],[16888214740738548397,9551065487491227380,3842611139925254562]], "path_len": 5}, {"layer": 1, "d": 2, "position": 242, "leaf": 60, "slot": 2, "values": [[11375860614512743705,12159552691638095048,1748577297743065506],[10427635604250192507,12188377611915858990,3305939877387789858],[5427071017873339313,12464684671386940995,2097029895587251782],[5305312732620219072,1636602398709148096,9970069024630049051]], "path_len": 3}, {"layer": 2, "d": 2, "position": 60, "leaf": 15, "slot": 0, "values": [[16083923945252889347,14007201087796689962,9760068277015825398],[13908085417078852557,6940036306507102360,12381427201366074798],[17699679852100348611,14554545952620420176,15525485196160550265],[3320877808744335198,16258308082275368886,16540132530644722341]], "path_len": 1}]}, + {"iota": 728, "deep": [13153405618339387285,12087014183745369343,7112360601826025321], "deep_sym": [6731259745018423611,2389294347923787576,8183626695115640159], "terminal_position": 5, "layers": [{"layer": 0, "d": 3, "position": 728, "leaf": 91, "slot": 0, "values": [[4548103059600255688,13178230568439787764,3041699253933608412],[4457817450677572724,7643960018359720291,12243487772429543665],[12640410128497954957,17663777625894295479,16121535441378525388],[5867807913173865683,15979217689236923364,1430096963540407779],[14787506865643198218,5829300918005392054,7263572657707494447],[10220333170775435288,10906980106043563821,9691813031194362081],[9122624667692505876,8929466534834669539,1812126731322561044],[18283235548136170389,15471761571546707919,12216120490171808248]], "path_len": 5}, {"layer": 1, "d": 2, "position": 91, "leaf": 22, "slot": 3, "values": [[16496262089917335101,16848297095433487741,3667071413045078735],[4701350522645074997,6137785806070879134,6782802506336928605],[17016004655611056354,4792727985778759853,17352960297403153524],[3506807141796159313,3285735666660479462,10512393962352017174]], "path_len": 3}, {"layer": 2, "d": 2, "position": 22, "leaf": 5, "slot": 2, "values": [[7191562821665302979,16440975359403640357,805687569336425751],[99473289543227136,7658450705401826751,1775111614621733606],[3938272754229468231,13802545678870326785,10553018281064249974],[5837029391332631245,3021110496026535726,17844415609475040928]], "path_len": 1}]}, + {"iota": 1480, "deep": [9421862934060914690,1734270806357642127,233875232532598714], "deep_sym": [18117459245225845776,647221298613096286,10640037095774592376], "terminal_position": 11, "layers": [{"layer": 0, "d": 3, "position": 1480, "leaf": 185, "slot": 0, "values": [[13198700555948538666,664034775197733983,17957513155068905625],[2048503957988366328,9846960232985635947,13572178912083388628],[16233241902330938996,8784515565338695415,18070637485116033776],[18303603161390467571,17564789126088883463,9181958520784428546],[1097773023732610587,1700095744687457468,13923523307874957244],[6452537530029647754,760433398807241482,8147876834946939403],[2552498477488711372,6077095896422145430,3139202260933438747],[8831062329706968515,1468344329142906895,4596941720011238461]], "path_len": 5}, {"layer": 1, "d": 2, "position": 185, "leaf": 46, "slot": 1, "values": [[11479654887262673782,16879657354497066704,13493609808166566252],[9227553114284958970,268322353749174939,15955048230739207146],[15251812456265475774,4859854400533217022,17327573994810098785],[5937937289982444485,17224265450150211107,14220481255969979395]], "path_len": 3}, {"layer": 2, "d": 2, "position": 46, "leaf": 11, "slot": 2, "values": [[5556470237486890782,15738386834927433074,12010912686098111476],[1997630762550526785,7678738670208248417,194037932413528879],[7091997090193394797,1911130281305530368,8017953523793910594],[1805231709436512031,5522280617529416910,4194339594951184587]], "path_len": 1}]}, + {"iota": 290, "deep": [14021209778763882017,13506671047373952976,13392681072178172499], "deep_sym": [11086591086110335323,259671578182683537,18045322574659161142], "terminal_position": 2, "layers": [{"layer": 0, "d": 3, "position": 290, "leaf": 36, "slot": 2, "values": [[7261845420047701776,11246462243179677035,7702811478875914330],[2501725647556803489,1263325614023439244,618889748304473690],[14900655738542057542,17891011431003616839,17575788078977955023],[9759952812872127999,2959573722831254786,11962249271050766536],[8837517616108370065,8909991979831591490,14572195230786394145],[3413523119546850389,10719675103647747609,10771843755320173053],[16068304745558270030,15349103431007750612,15888609516370901269],[12163776512755042098,8738806965462894074,2933481754801473916]], "path_len": 5}, {"layer": 1, "d": 2, "position": 36, "leaf": 9, "slot": 0, "values": [[9665548463325373214,13945489773253237666,11177605171166059499],[5997090037924425191,17204975125146622508,503183837706051239],[2316026578557707879,16320620904918007181,17497979472925238826],[13879387828100917678,3163214737154467295,4711748701448570178]], "path_len": 3}, {"layer": 2, "d": 2, "position": 9, "leaf": 2, "slot": 1, "values": [[7120644982042697520,4877211885227668924,17408917161652445778],[10118435938790577823,5444915436844861924,11542569338792872830],[157835802203199106,17140088994041704553,3978505728255765620],[14623016493704661158,10884525528551030981,1014977751818009979]], "path_len": 1}]}, + {"iota": 661, "deep": [6854765970215394823,2556446177852765810,15642530413576195024], "deep_sym": [11839673598506880492,12298724903716565425,17867957635740925997], "terminal_position": 5, "layers": [{"layer": 0, "d": 3, "position": 661, "leaf": 82, "slot": 5, "values": [[7496922916330212072,12338475639414754372,17434511279595495729],[12245166466801611098,11133729606825314606,16531306043264518426],[564339237851429615,5203048796060508002,4430341536129038167],[5147401255457999382,9931580009307097849,8744002423214609744],[10491831961067514093,18103125205776967329,8175823406417691246],[11064366642819303520,13175449112774951959,15394722024297982774],[9273040857889525487,8156191332710451250,6058357687441996617],[15172984517813136604,572230973827870853,2578811246222104697]], "path_len": 5}, {"layer": 1, "d": 2, "position": 82, "leaf": 20, "slot": 2, "values": [[13594173089496239018,16757524473770480938,2194449842399469486],[7693636871554533860,18228379298759503779,7255998013993585144],[6003272190433350033,16403086524442062476,3157252260914019799],[6560541927535846002,8658991620623781167,52645927622895508]], "path_len": 3}, {"layer": 2, "d": 2, "position": 20, "leaf": 5, "slot": 0, "values": [[7191562821665302979,16440975359403640357,805687569336425751],[99473289543227136,7658450705401826751,1775111614621733606],[3938272754229468231,13802545678870326785,10553018281064249974],[5837029391332631245,3021110496026535726,17844415609475040928]], "path_len": 1}]}, + {"iota": 1763, "deep": [16676350833658941157,2584692057630204480,7582912906866648961], "deep_sym": [4095891502055755185,13892816713210296784,7051523027024881040], "terminal_position": 13, "layers": [{"layer": 0, "d": 3, "position": 1763, "leaf": 220, "slot": 3, "values": [[16326594147024038562,9586321346049704049,16274016159125623699],[9684549243598334032,17409814664793682726,12541896833276119385],[17641608400031098675,2253078537857552057,15121822299814424285],[7632477760011931609,17323557877642753933,6270578313288524675],[8888728255851738386,5448327313361066048,8657660858889133604],[8906225553342660315,4039383537123350997,4361862025659933030],[12059143965033852868,12077620965086179016,1717778924363437527],[8645643362090899969,2453455417848760733,6065353499642801339]], "path_len": 5}, {"layer": 1, "d": 2, "position": 220, "leaf": 55, "slot": 0, "values": [[11167732534386473640,3688647619183319927,6771087108562321391],[2756854838210769859,11731540172240008448,14994311570837748550],[12460925440629396509,8895390108817935301,4433031690687849429],[6736179714496515355,8860053820523315526,1155063141492321859]], "path_len": 3}, {"layer": 2, "d": 2, "position": 55, "leaf": 13, "slot": 3, "values": [[1950472547524004311,4558894411088780101,2566116983780004090],[9850772263745460440,12240919944799806857,9423785085799841200],[10115571518972942631,891027316670140863,3962163664572049993],[12458991981252355554,11912590518631871559,4112952399935969276]], "path_len": 1}]}, + {"iota": 1459, "deep": [8866747437831929030,10111830668002232419,9891926968670703598], "deep_sym": [14950462972542314436,11068150428568742217,4363920990391254045], "terminal_position": 11, "layers": [{"layer": 0, "d": 3, "position": 1459, "leaf": 182, "slot": 3, "values": [[5621156612807134869,3345956687826194262,13719133172692197722],[187232746531771179,17246540586741393957,2113581492916353628],[14537862810343895723,14215448634865111212,12991827929331658789],[13073895064759706803,17778673011867563448,452451749060463813],[12846679264035621120,12178187075908759317,12064065106831945187],[4579685478921958669,14774426740464025944,55055029294679824],[6099726242109184639,7488918690483691710,7049115936021752350],[2224517884119604090,17691735657694722276,9579643693807667617]], "path_len": 5}, {"layer": 1, "d": 2, "position": 182, "leaf": 45, "slot": 2, "values": [[8456382716454272480,16645421597608797660,12030709434883295243],[3372347214817990474,12018637425931002418,18138096918129559976],[11036828035571675075,10829007173575275474,15479641102643849163],[8865544767290468476,14597804511677549154,10856945379225943026]], "path_len": 3}, {"layer": 2, "d": 2, "position": 45, "leaf": 11, "slot": 1, "values": [[5556470237486890782,15738386834927433074,12010912686098111476],[1997630762550526785,7678738670208248417,194037932413528879],[7091997090193394797,1911130281305530368,8017953523793910594],[1805231709436512031,5522280617529416910,4194339594951184587]], "path_len": 1}]}, + {"iota": 114, "deep": [11751211446885482280,8012913465173039444,4107968611842971166], "deep_sym": [10488146978631146249,3467633423044049359,13479118611390879923], "terminal_position": 0, "layers": [{"layer": 0, "d": 3, "position": 114, "leaf": 14, "slot": 2, "values": [[5054091260429375997,2372563079791414841,15961965775627314875],[8208101533109755006,10960112208848981436,12546368827738177063],[17241176247833241026,11720761989984833555,8771774514348889502],[13410213695129425655,11767807066594055802,1657611495470542750],[4104538544930139773,8324811138881401032,10951935808416008988],[16337892790583063462,6895512917021839800,332909730861766138],[2519452891579026128,15222959152639598375,15658823423618261612],[1296293383294420664,16252829718733227685,4547694876070093619]], "path_len": 5}, {"layer": 1, "d": 2, "position": 14, "leaf": 3, "slot": 2, "values": [[15136756194127393062,4698044514178552355,12949198668179909142],[4091359350279459743,14319222623939706000,12219427317438862304],[4269573043818774567,16386562989658503068,5783719340279165],[4551029118358661196,3011374291792230268,2530559486119546592]], "path_len": 3}, {"layer": 2, "d": 2, "position": 3, "leaf": 0, "slot": 3, "values": [[1246705822755947889,8914108151194633966,4522266899253302396],[16201135612349464620,13859003806625765459,17984848598039352151],[6334738368622223838,6843337439306526304,18123371975397451841],[14227174061878079563,9112226576955262019,10815074815388628783]], "path_len": 1}]}, + {"iota": 544, "deep": [11057980555532760461,11586349582617303100,16497605342895638788], "deep_sym": [18061532605418420298,17071096387002494484,1394440083059761068], "terminal_position": 4, "layers": [{"layer": 0, "d": 3, "position": 544, "leaf": 68, "slot": 0, "values": [[10681629651480504680,6876253625368506226,10472659439291431599],[17785494285214182646,1804332925713511638,7153802427684694427],[1750264208377628993,820549178969237976,16390984533398973123],[10670530517867835066,12282871405054959092,12927285669478093049],[5546919256863979013,14443394136083095146,17564043265733484750],[14358571524397172434,7614147518147971039,4972517443731385986],[17394577959486964371,12156225653284998877,4867008229487794604],[10055672574215541953,9093457710633016265,15886120991533784050]], "path_len": 5}, {"layer": 1, "d": 2, "position": 68, "leaf": 17, "slot": 0, "values": [[17555740993009699460,13499094199330296378,12313673954514069309],[18266186885237466730,8332842579765698647,12229600321596010012],[7494822478802989443,1646007509889999451,14055340666938528550],[15299999973184419556,7901614965414837111,11004543585603055469]], "path_len": 3}, {"layer": 2, "d": 2, "position": 17, "leaf": 4, "slot": 1, "values": [[12308156264265450342,14529545468385582654,10306409771655370083],[5744127407541261138,609091809805764668,11357141328347006033],[9685222367075524996,4627985111548410376,17296675071807714573],[16230217316348512832,4224924582692012386,17776784353682712436]], "path_len": 1}]}, + {"iota": 1095, "deep": [13793916523207725911,4108451676966047413,1462634368654358881], "deep_sym": [7823026204548128064,13411246497390018434,15618839968645625409], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1095, "leaf": 136, "slot": 7, "values": [[11736401263141382284,10580128431826960097,5565915715799547472],[18003563539250073891,6570617972736388495,8214297766385341820],[1530694407674999126,16275703648402757513,16025522073522358616],[13906516806776928060,5099232614834454594,17068868816848297917],[47484327520214282,16540557461423049181,10993471889508224336],[16847149291275321641,17089824238747236703,17143855242932676043],[16202409188065483980,7855976757770989585,15943194525433908185],[5724927034599390525,383950499348991303,17140274434618449862]], "path_len": 5}, {"layer": 1, "d": 2, "position": 136, "leaf": 34, "slot": 0, "values": [[14341369317333296234,14417614635979218844,8846520178864173288],[1541780884121598895,9871339961103297156,10854594137206876826],[547616547450581964,11549891556971613671,7197857705597676318],[3423261488439137215,6479960370124087383,17627165654203595745]], "path_len": 3}, {"layer": 2, "d": 2, "position": 34, "leaf": 8, "slot": 2, "values": [[11758409758221297221,11034734048298619314,14549108557531584252],[8435201558561332969,8784175351312936877,16456903953760264513],[15792142755751630193,13249956046916067676,3979910583159107465],[10326060926810026901,6731236006246528468,9917621895543682894]], "path_len": 1}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_dp.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_dp.rkyv new file mode 100644 index 000000000..38608334e Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_dp.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_pair.json b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_pair.json new file mode 100644 index 000000000..d49fb6ee2 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_pair.json @@ -0,0 +1,49 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "keccak", + "format": "cap_pair", + "proof_rkyv": "d_proof_keccak_cap_pair.rkyv", + "proof_rkyv_len": 51752, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 20, + "grinding_factor": 0, + "coset_offset": 3, + "merkle_cap": "auto", + "trace_tree_depth": 11, + "trace_cap": 3, + "fri_tree_depths": [10, 9, 8, 7, 6, 5, 4], + "fri_caps": [3, 3, 3, 3, 3, 3, 3], + "legacy_encoding": true, + "total_folds": 8, + "terminal_len": 16, + "schedule": [1, 1, 1, 1, 1, 1, 1], + "fri_roots": ["ae4c32d62674232b0ba6d27505a79e0dc351cbef2c7bf981f7e6071e033b1452","fdab08009575f071a9cdb78934264925e0a3c15831bffb8e296c2591d89776f8","b4b063fcffce6bb444a2b9bf7c738bf8ef4465da63ce003dd7d5140381a04ac8","a7f6a30a8015cee5d8733324dd6d4be66fe4eebba7bfca7b22f8bbb5af303ef1","076d8140a23a10b4647daca20621eeb312110446791252fd479afe5d8bfbbde3","bcb71db9203da0dc14f0c83aaa660fb81bc4a5e75bb907a59cbe36950991d3a5","df52656e7ae59323992a70632af97caa3f59f00dd5e21b8777b2510bd3d222f8"], + "zetas": [[5019159632337129269,238091556992722228,5532889084085155677],[11401249367489891504,3463462679569597100,4274808243399237651],[8939167920209768920,2181998912923045116,13686372517593792052],[17506395411273156879,11867889290151972542,11407542419953424413],[12104105903477959461,15124137694392601173,12282310738917257185],[1926491111051272611,2535199797145028677,910132886595075560],[8723582983141910029,6422360606508862377,12863861130764823176],[7140187269295878849,18092345223848887536,1238341624802517565]], + "terminal_coeffs": [[9675119329879772776,14801841314017838067,18236548730038982274],[14126988372849377104,17362610904962048507,2997281616627556854],[12732028867745254654,13981984175972346630,2203858718614623478],[15602387493645224223,15059227250182045714,17573228172572152503]], + "queries_detail": [ + {"iota": 1377, "deep": [7455843768244639387,9743762440574029878,4101673830513504298], "deep_sym": [3511192201323992326,7425862771688426773,15803627959215119946], "terminal_position": 10, "layers": [{"layer": 0, "d": 1, "position": 1377, "leaf": 688, "slot": 1, "values": [[4904023103714634616,2269339872048973425,18202589099877576301]], "path_len": 15}, {"layer": 1, "d": 1, "position": 688, "leaf": 344, "slot": 0, "values": [[15552158325687108952,4967392357145231790,8955886657214556570]], "path_len": 14}, {"layer": 2, "d": 1, "position": 344, "leaf": 172, "slot": 0, "values": [[2612792798869544968,5468544103442595665,3584530796466538409]], "path_len": 13}, {"layer": 3, "d": 1, "position": 172, "leaf": 86, "slot": 0, "values": [[17892726695253162907,11675528495509671187,15693589065103820991]], "path_len": 12}, {"layer": 4, "d": 1, "position": 86, "leaf": 43, "slot": 0, "values": [[14820453473679253910,2296745045332094341,7018948464228315015]], "path_len": 11}, {"layer": 5, "d": 1, "position": 43, "leaf": 21, "slot": 1, "values": [[8878572242937089178,9511332576208914366,533065969276291189]], "path_len": 10}, {"layer": 6, "d": 1, "position": 21, "leaf": 10, "slot": 1, "values": [[329830151858172684,8816779408413419857,10745160516112719560]], "path_len": 9}]}, + {"iota": 1361, "deep": [4424386105649019747,2750120983721151361,6541960356959252561], "deep_sym": [2651307476190031573,2146706624393544526,7968881411570009805], "terminal_position": 10, "layers": [{"layer": 0, "d": 1, "position": 1361, "leaf": 680, "slot": 1, "values": [[10340133767590356363,16890795849631589605,4349300871495131212]], "path_len": 7}, {"layer": 1, "d": 1, "position": 680, "leaf": 340, "slot": 0, "values": [[16007861022951537862,1205624391150308005,9614344438830586704]], "path_len": 6}, {"layer": 2, "d": 1, "position": 340, "leaf": 170, "slot": 0, "values": [[5160827061158288284,12918447257018839138,7766567275162096996]], "path_len": 5}, {"layer": 3, "d": 1, "position": 170, "leaf": 85, "slot": 0, "values": [[6754672244016872340,5555744013098268221,7002442437311308123]], "path_len": 4}, {"layer": 4, "d": 1, "position": 85, "leaf": 42, "slot": 1, "values": [[4787433690686736449,16041398203469231226,7447353953408244230]], "path_len": 3}, {"layer": 5, "d": 1, "position": 42, "leaf": 21, "slot": 0, "values": [[7528210498357234213,5262471822230748745,1619393323132032449]], "path_len": 2}, {"layer": 6, "d": 1, "position": 21, "leaf": 10, "slot": 1, "values": [[329830151858172684,8816779408413419857,10745160516112719560]], "path_len": 1}]}, + {"iota": 1885, "deep": [16644821497740984244,7192193719577633592,567497027096456459], "deep_sym": [3328772084659598265,11972720802068272473,14505810264201907862], "terminal_position": 14, "layers": [{"layer": 0, "d": 1, "position": 1885, "leaf": 942, "slot": 1, "values": [[12364109274760885259,18414168185378424120,201814257603045233]], "path_len": 7}, {"layer": 1, "d": 1, "position": 942, "leaf": 471, "slot": 0, "values": [[7736303778366316429,1787490843314858765,7883070884957703536]], "path_len": 6}, {"layer": 2, "d": 1, "position": 471, "leaf": 235, "slot": 1, "values": [[15968265871928534159,10969654878936738885,1434479089693489220]], "path_len": 5}, {"layer": 3, "d": 1, "position": 235, "leaf": 117, "slot": 1, "values": [[14556752700637506643,2348388154040563259,6914534242512885631]], "path_len": 4}, {"layer": 4, "d": 1, "position": 117, "leaf": 58, "slot": 1, "values": [[5904884676539004901,13961094313008801215,10931051894898429435]], "path_len": 3}, {"layer": 5, "d": 1, "position": 58, "leaf": 29, "slot": 0, "values": [[6697622104848356345,9323096160160053871,1649543559850819773]], "path_len": 2}, {"layer": 6, "d": 1, "position": 29, "leaf": 14, "slot": 1, "values": [[2254094792868780472,5656680908759260325,8864344245400962516]], "path_len": 1}]}, + {"iota": 1744, "deep": [11452590330265941625,16465601636710775034,17872016418185674598], "deep_sym": [16550931405044858895,2842618607274450166,13945529987242443515], "terminal_position": 13, "layers": [{"layer": 0, "d": 1, "position": 1744, "leaf": 872, "slot": 0, "values": [[5840528715108031929,11251160953820502305,1847324356710588644]], "path_len": 7}, {"layer": 1, "d": 1, "position": 872, "leaf": 436, "slot": 0, "values": [[11441586800024912458,16897683070247569656,12936863636324987699]], "path_len": 6}, {"layer": 2, "d": 1, "position": 436, "leaf": 218, "slot": 0, "values": [[2931056859857396180,6542035642962382244,9584081401290887397]], "path_len": 5}, {"layer": 3, "d": 1, "position": 218, "leaf": 109, "slot": 0, "values": [[2750067571950691581,8917446378041792268,5959056670684938469]], "path_len": 4}, {"layer": 4, "d": 1, "position": 109, "leaf": 54, "slot": 1, "values": [[18190979118219497085,3777885519368171867,5693206000720286197]], "path_len": 3}, {"layer": 5, "d": 1, "position": 54, "leaf": 27, "slot": 0, "values": [[8706481269396914430,6674339489760451417,12658234950971102773]], "path_len": 2}, {"layer": 6, "d": 1, "position": 27, "leaf": 13, "slot": 1, "values": [[17485199390895953445,5012845572731004092,17089930098592877424]], "path_len": 1}]}, + {"iota": 210, "deep": [14307124538962133335,5367173567221739243,16181558281583666348], "deep_sym": [7477631851561761913,4032606894656421959,618632229077000541], "terminal_position": 1, "layers": [{"layer": 0, "d": 1, "position": 210, "leaf": 105, "slot": 0, "values": [[18137304087224859738,3711500441386140844,17907712343369021266]], "path_len": 7}, {"layer": 1, "d": 1, "position": 105, "leaf": 52, "slot": 1, "values": [[688129786007139312,16273523563578800001,6402715237464307222]], "path_len": 6}, {"layer": 2, "d": 1, "position": 52, "leaf": 26, "slot": 0, "values": [[15119722078793073875,9100397834406702866,8005263254883193710]], "path_len": 5}, {"layer": 3, "d": 1, "position": 26, "leaf": 13, "slot": 0, "values": [[12501950698698707958,9389678858044410090,2604431415729322136]], "path_len": 4}, {"layer": 4, "d": 1, "position": 13, "leaf": 6, "slot": 1, "values": [[12402545306693990231,6163162517672281727,1650688300675708627]], "path_len": 3}, {"layer": 5, "d": 1, "position": 6, "leaf": 3, "slot": 0, "values": [[4077053280000033629,46248355743088296,6931998335990998662]], "path_len": 2}, {"layer": 6, "d": 1, "position": 3, "leaf": 1, "slot": 1, "values": [[5059575420996256821,4190436121309789696,1054706479073574679]], "path_len": 1}]}, + {"iota": 284, "deep": [1593779888553658402,13684409011714935906,6988894231470032034], "deep_sym": [13681972226048710479,5087922577534541004,17738039277407157018], "terminal_position": 2, "layers": [{"layer": 0, "d": 1, "position": 284, "leaf": 142, "slot": 0, "values": [[10384275476170871726,17189159971389164274,4738120100226236541]], "path_len": 7}, {"layer": 1, "d": 1, "position": 142, "leaf": 71, "slot": 0, "values": [[5092224361370880150,5913027675357865702,3574493885316113157]], "path_len": 6}, {"layer": 2, "d": 1, "position": 71, "leaf": 35, "slot": 1, "values": [[3864201566255052169,10153589149463947626,15614707352052562337]], "path_len": 5}, {"layer": 3, "d": 1, "position": 35, "leaf": 17, "slot": 1, "values": [[13392845361354827382,7360065332806830307,13439209107509686589]], "path_len": 4}, {"layer": 4, "d": 1, "position": 17, "leaf": 8, "slot": 1, "values": [[3035440457560863603,17670638245452663662,3020459914118654763]], "path_len": 3}, {"layer": 5, "d": 1, "position": 8, "leaf": 4, "slot": 0, "values": [[5173582849907788165,17699447796082674486,18297160988347655497]], "path_len": 2}, {"layer": 6, "d": 1, "position": 4, "leaf": 2, "slot": 0, "values": [[7952914794860711365,1036989991297879170,13371197571151367681]], "path_len": 1}]}, + {"iota": 149, "deep": [573286711455923633,5855910878508172803,91428610298637857], "deep_sym": [2816550618177864130,6352906390970817013,7650975019200287906], "terminal_position": 1, "layers": [{"layer": 0, "d": 1, "position": 149, "leaf": 74, "slot": 1, "values": [[11233585344845411115,1959242180628250960,7104254617117422527]], "path_len": 7}, {"layer": 1, "d": 1, "position": 74, "leaf": 37, "slot": 0, "values": [[2484328013310643083,16375687057542596532,6992210163284325896]], "path_len": 6}, {"layer": 2, "d": 1, "position": 37, "leaf": 18, "slot": 1, "values": [[14240110153299640841,11864535742232373216,8820317589108825390]], "path_len": 5}, {"layer": 3, "d": 1, "position": 18, "leaf": 9, "slot": 0, "values": [[4219795048204406341,10081909315224922351,8536329541575263506]], "path_len": 4}, {"layer": 4, "d": 1, "position": 9, "leaf": 4, "slot": 1, "values": [[15818073039788991153,13592893004124091169,10505519961286136356]], "path_len": 3}, {"layer": 5, "d": 1, "position": 4, "leaf": 2, "slot": 0, "values": [[8969390140574186381,12119251962002215102,1342086715066738699]], "path_len": 2}, {"layer": 6, "d": 1, "position": 2, "leaf": 1, "slot": 0, "values": [[7282793269070874198,5967657170308489624,3490988850839770011]], "path_len": 1}]}, + {"iota": 281, "deep": [7039177929252956250,8051104077967999964,14732764528071854743], "deep_sym": [15313172973719342947,7854227470716335803,2735174912562240866], "terminal_position": 2, "layers": [{"layer": 0, "d": 1, "position": 281, "leaf": 140, "slot": 1, "values": [[16351637314632867303,10550715376056936204,389777731531914906]], "path_len": 7}, {"layer": 1, "d": 1, "position": 140, "leaf": 70, "slot": 0, "values": [[966404594873295819,15537343227084575831,7601432475360510279]], "path_len": 6}, {"layer": 2, "d": 1, "position": 70, "leaf": 35, "slot": 0, "values": [[7818355896976204163,14890152152511730349,12023921930726425947]], "path_len": 5}, {"layer": 3, "d": 1, "position": 35, "leaf": 17, "slot": 1, "values": [[13392845361354827382,7360065332806830307,13439209107509686589]], "path_len": 4}, {"layer": 4, "d": 1, "position": 17, "leaf": 8, "slot": 1, "values": [[3035440457560863603,17670638245452663662,3020459914118654763]], "path_len": 3}, {"layer": 5, "d": 1, "position": 8, "leaf": 4, "slot": 0, "values": [[5173582849907788165,17699447796082674486,18297160988347655497]], "path_len": 2}, {"layer": 6, "d": 1, "position": 4, "leaf": 2, "slot": 0, "values": [[7952914794860711365,1036989991297879170,13371197571151367681]], "path_len": 1}]}, + {"iota": 1972, "deep": [13972310113770535822,5552131090533568835,8748308349705042723], "deep_sym": [12904991763452026590,2045517226935572658,4927886687011235995], "terminal_position": 15, "layers": [{"layer": 0, "d": 1, "position": 1972, "leaf": 986, "slot": 0, "values": [[103123668545543070,621540249427176615,5870517844992589529]], "path_len": 7}, {"layer": 1, "d": 1, "position": 986, "leaf": 493, "slot": 0, "values": [[8303741178446572413,11118598719810183172,15827861596787744863]], "path_len": 6}, {"layer": 2, "d": 1, "position": 493, "leaf": 246, "slot": 1, "values": [[5268687713648529238,18141485527344485068,2311193777804771088]], "path_len": 5}, {"layer": 3, "d": 1, "position": 246, "leaf": 123, "slot": 0, "values": [[16925572875524888510,10859497315051053091,16558322538563635674]], "path_len": 4}, {"layer": 4, "d": 1, "position": 123, "leaf": 61, "slot": 1, "values": [[11539635759505059868,15834597176730063772,7414351026055757950]], "path_len": 3}, {"layer": 5, "d": 1, "position": 61, "leaf": 30, "slot": 1, "values": [[17088097359308541830,16348344533700096317,12062715773603923136]], "path_len": 2}, {"layer": 6, "d": 1, "position": 30, "leaf": 15, "slot": 0, "values": [[3243369265885820103,2781729126563128823,6192776688734542888]], "path_len": 1}]}, + {"iota": 525, "deep": [17094259693227384751,12469668007350821596,16165213632326518833], "deep_sym": [4824386483891070766,4297530548260959791,3323344396656680919], "terminal_position": 4, "layers": [{"layer": 0, "d": 1, "position": 525, "leaf": 262, "slot": 1, "values": [[637338097538477161,5920215863388397200,3152375791077944359]], "path_len": 7}, {"layer": 1, "d": 1, "position": 262, "leaf": 131, "slot": 0, "values": [[10024172190768150414,18093095236811875938,3690458710772253406]], "path_len": 6}, {"layer": 2, "d": 1, "position": 131, "leaf": 65, "slot": 1, "values": [[2283998104138345537,6246288698769373908,18207218848997438649]], "path_len": 5}, {"layer": 3, "d": 1, "position": 65, "leaf": 32, "slot": 1, "values": [[608973827866390398,3083718923908497492,4092593950150801479]], "path_len": 4}, {"layer": 4, "d": 1, "position": 32, "leaf": 16, "slot": 0, "values": [[5533161650558229887,6822717597707274535,10938914320750008584]], "path_len": 3}, {"layer": 5, "d": 1, "position": 16, "leaf": 8, "slot": 0, "values": [[16833823024521921629,5309715217326987516,15179334130005053137]], "path_len": 2}, {"layer": 6, "d": 1, "position": 8, "leaf": 4, "slot": 0, "values": [[16996637738703906945,8756168288594149033,9169518720773082560]], "path_len": 1}]}, + {"iota": 1610, "deep": [17498517330427110815,10324701463545824868,7265055644247617298], "deep_sym": [13004031424029418383,10647612169997720653,5854165218274484100], "terminal_position": 12, "layers": [{"layer": 0, "d": 1, "position": 1610, "leaf": 805, "slot": 0, "values": [[10812029475504267638,16720837822975707610,4489770116780798833]], "path_len": 7}, {"layer": 1, "d": 1, "position": 805, "leaf": 402, "slot": 1, "values": [[12805265948321776124,8836568045853207748,1905085313949096243]], "path_len": 6}, {"layer": 2, "d": 1, "position": 402, "leaf": 201, "slot": 0, "values": [[12693966393771267041,8348939428433425034,14079667903116984336]], "path_len": 5}, {"layer": 3, "d": 1, "position": 201, "leaf": 100, "slot": 1, "values": [[2226945069041744409,3011529255908565018,3820982554989890820]], "path_len": 4}, {"layer": 4, "d": 1, "position": 100, "leaf": 50, "slot": 0, "values": [[9043526195385487228,1384729029931888705,4930313006869208923]], "path_len": 3}, {"layer": 5, "d": 1, "position": 50, "leaf": 25, "slot": 0, "values": [[5834801524537936096,12279959006427881864,14659167700684975065]], "path_len": 2}, {"layer": 6, "d": 1, "position": 25, "leaf": 12, "slot": 1, "values": [[8161617527135559126,6297769416866581594,3173676877728375344]], "path_len": 1}]}, + {"iota": 742, "deep": [13303682450365250715,18180497031344687707,13387718062770299003], "deep_sym": [8019771175969538382,16505142083751995154,3756467025059499914], "terminal_position": 5, "layers": [{"layer": 0, "d": 1, "position": 742, "leaf": 371, "slot": 0, "values": [[7715330600617454806,263816730109342565,6982233398900496069]], "path_len": 7}, {"layer": 1, "d": 1, "position": 371, "leaf": 185, "slot": 1, "values": [[955661343416646194,3810245907731434521,18162941425162857639]], "path_len": 6}, {"layer": 2, "d": 1, "position": 185, "leaf": 92, "slot": 1, "values": [[11268010522668285064,3695687107814936352,17155735713986570760]], "path_len": 5}, {"layer": 3, "d": 1, "position": 92, "leaf": 46, "slot": 0, "values": [[10648387502898115835,13677350527157965004,13646005978346329332]], "path_len": 4}, {"layer": 4, "d": 1, "position": 46, "leaf": 23, "slot": 0, "values": [[2169193262335748111,4583523388905316755,11663405555188998011]], "path_len": 3}, {"layer": 5, "d": 1, "position": 23, "leaf": 11, "slot": 1, "values": [[2441722207610391689,3667110974786560880,16709615656122872703]], "path_len": 2}, {"layer": 6, "d": 1, "position": 11, "leaf": 5, "slot": 1, "values": [[8841621684899426418,6810113607590278534,4970473809851222151]], "path_len": 1}]}, + {"iota": 1559, "deep": [13187342739415041022,4483312787545931366,1258939318303884106], "deep_sym": [9013614428963284821,13873874726042830330,16401517306744501335], "terminal_position": 12, "layers": [{"layer": 0, "d": 1, "position": 1559, "leaf": 779, "slot": 1, "values": [[14279429615183321578,16868429224720157241,7715359029040314952]], "path_len": 7}, {"layer": 1, "d": 1, "position": 779, "leaf": 389, "slot": 1, "values": [[1300114829801264936,1368022253127134748,491228609246606049]], "path_len": 6}, {"layer": 2, "d": 1, "position": 389, "leaf": 194, "slot": 1, "values": [[12689291513986525901,17681993845909985885,7114229310549916761]], "path_len": 5}, {"layer": 3, "d": 1, "position": 194, "leaf": 97, "slot": 0, "values": [[9043297265158582254,5816561522348004322,12262412645634531093]], "path_len": 4}, {"layer": 4, "d": 1, "position": 97, "leaf": 48, "slot": 1, "values": [[12968352116263480832,10876007460350245110,8248432895412464350]], "path_len": 3}, {"layer": 5, "d": 1, "position": 48, "leaf": 24, "slot": 0, "values": [[2319650304318305615,1936107267691353544,1508962945379790603]], "path_len": 2}, {"layer": 6, "d": 1, "position": 24, "leaf": 12, "slot": 0, "values": [[5368639923100606835,8118461821476130869,15151552861996072850]], "path_len": 1}]}, + {"iota": 1876, "deep": [2188958272630719718,16739216132014462061,15969720682844330480], "deep_sym": [17625020173931802981,1432206676468061533,9266034828665035492], "terminal_position": 14, "layers": [{"layer": 0, "d": 1, "position": 1876, "leaf": 938, "slot": 0, "values": [[11088999406359806620,13387672636599280160,17389860177775088876]], "path_len": 7}, {"layer": 1, "d": 1, "position": 938, "leaf": 469, "slot": 0, "values": [[15424340190559890880,18056666780987163338,4717633278725151722]], "path_len": 6}, {"layer": 2, "d": 1, "position": 469, "leaf": 234, "slot": 1, "values": [[11159565469336764502,17022137387877916585,14189315808586600062]], "path_len": 5}, {"layer": 3, "d": 1, "position": 234, "leaf": 117, "slot": 0, "values": [[14827270393572676639,10638371894509469964,11247843226701229209]], "path_len": 4}, {"layer": 4, "d": 1, "position": 117, "leaf": 58, "slot": 1, "values": [[5904884676539004901,13961094313008801215,10931051894898429435]], "path_len": 3}, {"layer": 5, "d": 1, "position": 58, "leaf": 29, "slot": 0, "values": [[6697622104848356345,9323096160160053871,1649543559850819773]], "path_len": 2}, {"layer": 6, "d": 1, "position": 29, "leaf": 14, "slot": 1, "values": [[2254094792868780472,5656680908759260325,8864344245400962516]], "path_len": 1}]}, + {"iota": 902, "deep": [17300476179493052956,5814189479075607576,9616298647385021830], "deep_sym": [1907324078291216890,11818855514989027498,16227487509439123490], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 902, "leaf": 451, "slot": 0, "values": [[1388622092030634984,14956937837334565805,9515911371808271297]], "path_len": 7}, {"layer": 1, "d": 1, "position": 451, "leaf": 225, "slot": 1, "values": [[4488968937607451421,17097333351519751000,684929099341931785]], "path_len": 6}, {"layer": 2, "d": 1, "position": 225, "leaf": 112, "slot": 1, "values": [[10204113238986370967,15052308195105135188,7343687815021226533]], "path_len": 5}, {"layer": 3, "d": 1, "position": 112, "leaf": 56, "slot": 0, "values": [[3262852310309194876,9232449904487550350,2876133383225467975]], "path_len": 4}, {"layer": 4, "d": 1, "position": 56, "leaf": 28, "slot": 0, "values": [[1322899532053186713,3220390191406884778,2557975581575432098]], "path_len": 3}, {"layer": 5, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[15593906472738996186,8290296308377011327,15323376687127304814]], "path_len": 2}, {"layer": 6, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[12138138523651517612,14970545128614267257,13317972550568896498]], "path_len": 1}]}, + {"iota": 81, "deep": [7129660006418579837,5464112336437590821,16893227050504408018], "deep_sym": [10320409399989363056,2525461309991914337,5154789638308425332], "terminal_position": 0, "layers": [{"layer": 0, "d": 1, "position": 81, "leaf": 40, "slot": 1, "values": [[110229392569089403,12259696148007863211,3042644096845288748]], "path_len": 7}, {"layer": 1, "d": 1, "position": 40, "leaf": 20, "slot": 0, "values": [[8122893273598551502,14924910766491787263,6043356495912840217]], "path_len": 6}, {"layer": 2, "d": 1, "position": 20, "leaf": 10, "slot": 0, "values": [[1534937080669066257,13951054226642220235,1121681024521608867]], "path_len": 5}, {"layer": 3, "d": 1, "position": 10, "leaf": 5, "slot": 0, "values": [[17177952673650405035,1346144544956907988,9345414829703491079]], "path_len": 4}, {"layer": 4, "d": 1, "position": 5, "leaf": 2, "slot": 1, "values": [[210777378017248551,9127223126794160358,13638356578759501263]], "path_len": 3}, {"layer": 5, "d": 1, "position": 2, "leaf": 1, "slot": 0, "values": [[9219093612718247019,18210320365635570152,10887580690615904962]], "path_len": 2}, {"layer": 6, "d": 1, "position": 1, "leaf": 0, "slot": 1, "values": [[7953909299506274224,10647255018552488450,4686578840673990309]], "path_len": 1}]}, + {"iota": 526, "deep": [5717674392817305871,2534117920242909263,4358061539679503173], "deep_sym": [14563289310680755610,7841726989291354564,3706700768991955750], "terminal_position": 4, "layers": [{"layer": 0, "d": 1, "position": 526, "leaf": 263, "slot": 0, "values": [[5070813189403071397,17169903868570646682,7095625026664290666]], "path_len": 7}, {"layer": 1, "d": 1, "position": 263, "leaf": 131, "slot": 1, "values": [[16492098086237622833,9637699063752431944,3804902190530425641]], "path_len": 6}, {"layer": 2, "d": 1, "position": 131, "leaf": 65, "slot": 1, "values": [[2283998104138345537,6246288698769373908,18207218848997438649]], "path_len": 5}, {"layer": 3, "d": 1, "position": 65, "leaf": 32, "slot": 1, "values": [[608973827866390398,3083718923908497492,4092593950150801479]], "path_len": 4}, {"layer": 4, "d": 1, "position": 32, "leaf": 16, "slot": 0, "values": [[5533161650558229887,6822717597707274535,10938914320750008584]], "path_len": 3}, {"layer": 5, "d": 1, "position": 16, "leaf": 8, "slot": 0, "values": [[16833823024521921629,5309715217326987516,15179334130005053137]], "path_len": 2}, {"layer": 6, "d": 1, "position": 8, "leaf": 4, "slot": 0, "values": [[16996637738703906945,8756168288594149033,9169518720773082560]], "path_len": 1}]}, + {"iota": 1373, "deep": [12933527252844961314,13482483241538752965,12319601543423549241], "deep_sym": [7596263983079696846,9948320041500637225,10695456770784677056], "terminal_position": 10, "layers": [{"layer": 0, "d": 1, "position": 1373, "leaf": 686, "slot": 1, "values": [[2894892426971301864,18226940770443630715,2545440710177696719]], "path_len": 7}, {"layer": 1, "d": 1, "position": 686, "leaf": 343, "slot": 0, "values": [[6721789835500909538,12623796400853595278,17710357262450931617]], "path_len": 6}, {"layer": 2, "d": 1, "position": 343, "leaf": 171, "slot": 1, "values": [[4748393949877499829,18082733519044814220,4403730337012059288]], "path_len": 5}, {"layer": 3, "d": 1, "position": 171, "leaf": 85, "slot": 1, "values": [[13768096748957837555,16920999351439661381,15314456781383790071]], "path_len": 4}, {"layer": 4, "d": 1, "position": 85, "leaf": 42, "slot": 1, "values": [[4787433690686736449,16041398203469231226,7447353953408244230]], "path_len": 3}, {"layer": 5, "d": 1, "position": 42, "leaf": 21, "slot": 0, "values": [[7528210498357234213,5262471822230748745,1619393323132032449]], "path_len": 2}, {"layer": 6, "d": 1, "position": 21, "leaf": 10, "slot": 1, "values": [[329830151858172684,8816779408413419857,10745160516112719560]], "path_len": 1}]}, + {"iota": 290, "deep": [14021209778763882017,13506671047373952976,13392681072178172499], "deep_sym": [11086591086110335323,259671578182683537,18045322574659161142], "terminal_position": 2, "layers": [{"layer": 0, "d": 1, "position": 290, "leaf": 145, "slot": 0, "values": [[9759952812872127999,2959573722831254786,11962249271050766536]], "path_len": 7}, {"layer": 1, "d": 1, "position": 145, "leaf": 72, "slot": 1, "values": [[3160384506142212126,11129016019287199417,2497436678283294653]], "path_len": 6}, {"layer": 2, "d": 1, "position": 72, "leaf": 36, "slot": 0, "values": [[6632052039655398745,13544374544161250673,17324736033988297484]], "path_len": 5}, {"layer": 3, "d": 1, "position": 36, "leaf": 18, "slot": 0, "values": [[5636138478357741260,17945430522797296925,14722846344237536296]], "path_len": 4}, {"layer": 4, "d": 1, "position": 18, "leaf": 9, "slot": 0, "values": [[8112443770414550527,8363541473308682635,5818072564448079451]], "path_len": 3}, {"layer": 5, "d": 1, "position": 9, "leaf": 4, "slot": 1, "values": [[3973680665106131834,17762603807804556840,3909451547858769903]], "path_len": 2}, {"layer": 6, "d": 1, "position": 4, "leaf": 2, "slot": 0, "values": [[7952914794860711365,1036989991297879170,13371197571151367681]], "path_len": 1}]}, + {"iota": 1327, "deep": [4372229306796237183,4031141783655548937,5828194788736158720], "deep_sym": [2119907010727716769,8907448833006125972,8989670646794729154], "terminal_position": 10, "layers": [{"layer": 0, "d": 1, "position": 1327, "leaf": 663, "slot": 1, "values": [[9966484036733354412,13353043178793754619,15948320617124935973]], "path_len": 7}, {"layer": 1, "d": 1, "position": 663, "leaf": 331, "slot": 1, "values": [[12013446190091167856,12342764258757427021,4505252478189751377]], "path_len": 6}, {"layer": 2, "d": 1, "position": 331, "leaf": 165, "slot": 1, "values": [[9916273341643963839,15768417062469268654,2948508297908526741]], "path_len": 5}, {"layer": 3, "d": 1, "position": 165, "leaf": 82, "slot": 1, "values": [[5704875491456947424,5058415357100718396,10805003747125458482]], "path_len": 4}, {"layer": 4, "d": 1, "position": 82, "leaf": 41, "slot": 0, "values": [[17782440572252320235,8930515870775211256,12299752851325740463]], "path_len": 3}, {"layer": 5, "d": 1, "position": 41, "leaf": 20, "slot": 1, "values": [[5421279213360062408,4617814356361337913,11799045697810412882]], "path_len": 2}, {"layer": 6, "d": 1, "position": 20, "leaf": 10, "slot": 0, "values": [[5755772604009881655,14090912597746712379,5736120848822458413]], "path_len": 1}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_pair.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_pair.rkyv new file mode 100644 index 000000000..9a7335edf Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_pair.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp.json b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp.json new file mode 100644 index 000000000..039fee98f --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp.json @@ -0,0 +1,27 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "keccak", + "format": "dp", + "proof_rkyv": "d_proof_keccak_dp.rkyv", + "proof_rkyv_len": 8488, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 2, 2], + "fri_roots": ["24ad3d0e98f4bed6edf18ec793157a4b40d412b869a719de1c98a970fab00072","f9fe88a494b4bb9e5ca08fc1c1a7a3ef4624b75d0ad3812e39dc65d9020f91cf","7ecf9321963996332fa2eb0464dd78c32efb4185fbc866687520f36de7764131"], + "zetas": [[5019159632337129269,238091556992722228,5532889084085155677],[12296403571495774788,9626523507187974856,1515890197535251952],[9104931154505306807,6806930774857449431,13982536486847686418],[17943736705802395901,4283444887199783601,5105112647180456117]], + "terminal_coeffs": [[18046538310705593629,16035634115336395623,14269474772235161333],[13449754012599068599,8932449597508197521,3279495531022860796],[11948801525571458678,1807139812678879355,3178944376615033389],[11072843192958306056,3667138469373329065,14070513692562577743]], + "queries_detail": [ + {"iota": 1277, "deep": [112612903969624832,13540544077113206977,891744669294414204], "deep_sym": [10665692780904921752,13891743997545272459,5218021303841191956], "terminal_position": 9, "layers": [{"layer": 0, "d": 3, "position": 1277, "leaf": 159, "slot": 5, "values": [[2726840187197314970,4641373057133563422,18254905628294267124],[7726190489312153580,9907582621009652564,13704195924065075984],[4913854101341609490,7059003433635310965,6314417828660586086],[15329881112488297229,17154024704563340256,10996086559637584958],[8931011741374043492,14857842271836329185,1962274052252210912],[11335680486698327443,1217890136881310458,6960827381617415085],[11843024897136178316,4050095544328259531,1109189699536974526],[15465183123903289453,8756197396528546255,3770807126986676922]], "path_len": 8}, {"layer": 1, "d": 2, "position": 159, "leaf": 39, "slot": 3, "values": [[9337955700188682368,10005268201090501927,17075626829468745589],[12775344777395238092,12444988312381492194,18162313775388685340],[12834611725527989937,3931095319124210104,7011958104454824522],[18278843176886412077,2091177023787081796,10712499925409758781]], "path_len": 6}, {"layer": 2, "d": 2, "position": 39, "leaf": 9, "slot": 3, "values": [[12663636275089871938,1342734324200714786,13647156802297113741],[16459715330172958447,16246821789525783433,13803231028510688298],[4472259574895772221,15705768718567917064,4738154395575758232],[10184880754128237084,3408521813484574087,14812129773919197844]], "path_len": 4}]}, + {"iota": 1793, "deep": [8057175728474570347,4157164488656378128,15766577891820220836], "deep_sym": [6733030217476856996,3149008183846048310,5846868056871306014], "terminal_position": 14, "layers": [{"layer": 0, "d": 3, "position": 1793, "leaf": 224, "slot": 1, "values": [[5863889590658237167,8803207495494391631,488510412724115696],[7367902939689275966,5399515143439789253,13537028177165637670],[13057594490447211533,12028941489541574294,10245716700381823303],[3536160573392264847,13647402147340435120,9933763201558099138],[17705005489971962397,16100850492966888022,3356205035428066804],[4834413239841014089,10648175143241294336,14941339194282038433],[11268069224352915944,7397295511095760171,650865519941991105],[17810125080425025549,8252558882871738031,1603536863803495337]], "path_len": 8}, {"layer": 1, "d": 2, "position": 224, "leaf": 56, "slot": 0, "values": [[14576290996393278046,4099269296443923918,13962179114375143747],[8539012341704406611,14597685688217769420,16489000745330409389],[4928849784411569862,5656061150696874101,18052668495466081830],[10699077948197816254,7120867110842505641,2470038313983831606]], "path_len": 6}, {"layer": 2, "d": 2, "position": 56, "leaf": 14, "slot": 0, "values": [[7743892560805052942,10417724895478695146,9242061460595868046],[5215961958705134614,3646588380176163324,11215186743127464548],[18165082680919870330,16446510594026310692,9931249060720003450],[16761232818563256745,6094664995607304883,14831579372437454855]], "path_len": 4}]}, + {"iota": 1422, "deep": [4336444987633806031,42270359695066150,811124501724833250], "deep_sym": [6040322601513087150,2232031154133685564,13268270931765776955], "terminal_position": 11, "layers": [{"layer": 0, "d": 3, "position": 1422, "leaf": 177, "slot": 6, "values": [[5773020228763106950,12181432689259931341,2904380769668095371],[5378436167230488318,1136926564836430281,11025181981762941864],[9767397875213699870,16391873535337268069,9544088588384136146],[50456808154105685,7570275210936766391,3076092320148066703],[6519022523009943654,14501422860440411207,16766709789063948727],[1942043923567112082,9396051082847161748,4275006641168421309],[802135155222683305,8086721014210384187,5276472197522953276],[14963808776084644130,11822327586546991308,9902819457375193080]], "path_len": 8}, {"layer": 1, "d": 2, "position": 177, "leaf": 44, "slot": 1, "values": [[8007252311096823131,15451587500561065094,5200475833640745404],[12419273660987748398,619789569423171010,4299596803633862803],[14489492344890871493,14652990201720622453,5263973935492910147],[12959859661416681018,3696933911172366326,18087484035368403648]], "path_len": 6}, {"layer": 2, "d": 2, "position": 44, "leaf": 11, "slot": 0, "values": [[5556470237486890782,15738386834927433074,12010912686098111476],[1997630762550526785,7678738670208248417,194037932413528879],[7091997090193394797,1911130281305530368,8017953523793910594],[1805231709436512031,5522280617529416910,4194339594951184587]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp.rkyv new file mode 100644 index 000000000..000c26f6c Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp_3_1_3.json b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp_3_1_3.json new file mode 100644 index 000000000..fa967427f --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp_3_1_3.json @@ -0,0 +1,27 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "keccak", + "format": "dp_3_1_3", + "proof_rkyv": "d_proof_keccak_dp_3_1_3.rkyv", + "proof_rkyv_len": 8728, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 1, 3], + "fri_roots": ["24ad3d0e98f4bed6edf18ec793157a4b40d412b869a719de1c98a970fab00072","871e7c3df8ccc3c64730c863f6a2fa787fd06fae0f2a2e11ebf5abf3de3a2226","ade304313a4dfb3665231a85ee8c14f736c819109c36d1638f3fea9d06d68e1b"], + "zetas": [[5019159632337129269,238091556992722228,5532889084085155677],[12296403571495774788,9626523507187974856,1515890197535251952],[14523967509618755178,7981048790673426368,4165831416408514802],[15590192218200992447,15146425979047601999,2279642302822794473]], + "terminal_coeffs": [[1388079943033759100,11009361148285669739,18003680692828365043],[17237532129585750347,1517078761242656685,16199054381564524094],[1938844203267517687,9877447212410491634,12968975246239323922],[4666008817029420275,15603102928404590219,2901983394916486066]], + "queries_detail": [ + {"iota": 1101, "deep": [13444408416110140567,15620040802225407588,15223031624530673992], "deep_sym": [290005468330435364,4114526459923533080,2460530217353380230], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1101, "leaf": 137, "slot": 5, "values": [[7571539965246193760,12259342495260382898,7053829414421388370],[3226322966601049319,14317828094417592584,54137258033536103],[14021848107088228906,2919431011366834709,10068585320682235218],[2856107074513493232,15548865223592133260,2006005570476947405],[15658026536969269856,1554422422590567528,10731091312921677367],[16481563759951422033,12119679792493024127,5422020975449206720],[13826052861493771289,4614039571553346032,6654350834419205173],[1115307290735749016,14730692960078495598,8274405822262668945]], "path_len": 8}, {"layer": 1, "d": 1, "position": 137, "leaf": 68, "slot": 1, "values": [[14341369317333296234,14417614635979218844,8846520178864173288],[1541780884121598895,9871339961103297156,10854594137206876826]], "path_len": 7}, {"layer": 2, "d": 3, "position": 68, "leaf": 8, "slot": 4, "values": [[17340337315510687533,16466850183023390197,16170822732462346370],[13577065484001180074,10789052881678110757,1984289784499658738],[10322313144195499275,9573474260847586333,18360532062725495499],[13517606960732314903,5924632567214305012,6118505914147684536],[542347889760713356,9784404433359539161,7414969705090267463],[12066365013085694995,5956563561476960593,14866809534596581635],[4561576524370871036,10849591696019172330,6961009597333354300],[3866908127244773168,15669605755659572510,9331312028362224237]], "path_len": 4}]}, + {"iota": 685, "deep": [4925031604963438677,830417094021520731,15862979152598215734], "deep_sym": [6436035638622990139,2908039237793753059,14976610380852847077], "terminal_position": 5, "layers": [{"layer": 0, "d": 3, "position": 685, "leaf": 85, "slot": 5, "values": [[2711194398170199905,17428802182914895478,635293871302441912],[3653691656041130802,10425319869671607000,8515362419986619290],[9754395736388531158,5271124400817841404,13920649595609625458],[3615836389795967869,8175211513136783109,7424862389176434620],[5088724134008406528,2287923670496620078,11530128097074748850],[14785342658225808385,15047767963588960580,14000936891403725028],[12926575319413478177,154624349820291247,6857332647761830396],[5171891210798910511,5038297187202472504,2006656822716967144]], "path_len": 8}, {"layer": 1, "d": 1, "position": 85, "leaf": 42, "slot": 1, "values": [[13540904436659318567,17336738776988905048,17276842271454845243],[11189914825747541931,6819516493339643473,9190484781628012885]], "path_len": 7}, {"layer": 2, "d": 3, "position": 42, "leaf": 5, "slot": 2, "values": [[31446593612320238,17773679528145884633,5637457551076632612],[6257322271130649695,16400515433097762086,6346320073297491491],[15047501166121929445,1523283072139552980,16645225185759966672],[11124874806014753988,1879122302621705061,2303532870366114852],[17332404032257580601,17636788358461353225,1675268583132812138],[11429077391219906462,4620602044590711909,15393150759290484458],[13671100188568527934,12564621711691339017,7611452210500384787],[1397326939955468944,5653632621592529379,4992247048829424615]], "path_len": 4}]}, + {"iota": 1249, "deep": [9946431352694562883,2008734157674376412,16548416984122237506], "deep_sym": [16096242337339141933,11323512440171655616,10360796140662398409], "terminal_position": 9, "layers": [{"layer": 0, "d": 3, "position": 1249, "leaf": 156, "slot": 1, "values": [[6978142119154416518,8547442860041728708,14970652831834600826],[13824685731161489176,17885767568970612304,2534572619566806965],[10060589250654053507,6130325395888609220,6951027455020094700],[13364481942665285269,7033035359754839223,453294621666011599],[9238061526093088533,4650807553901898659,2478698084496860348],[18049237297669172639,7829189454966868378,9410917270666579147],[6566615427776827262,15491421233653898723,5991626663109222605],[3511981306102162279,2482433176886809990,15432684550774174243]], "path_len": 8}, {"layer": 1, "d": 1, "position": 156, "leaf": 78, "slot": 0, "values": [[9337955700188682368,10005268201090501927,17075626829468745589],[12775344777395238092,12444988312381492194,18162313775388685340]], "path_len": 7}, {"layer": 2, "d": 3, "position": 78, "leaf": 9, "slot": 6, "values": [[10776393641929202433,971877452076755765,14157900628235863657],[12650629188205370781,1368692753999156063,4809609548511919935],[6843606874063648474,4015927221886946244,11248907885089940182],[4735026784363667383,3345527530080963457,5763353545005297393],[6069373268525402579,13721627381486427074,16411807685985695596],[8299354347455469646,3094329301555192808,2288817730528401204],[8418242767604578714,4171510248854052332,14012470415711676305],[4277858526620869279,6369201857160850861,7422271882544911562]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp_3_1_3.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp_3_1_3.rkyv new file mode 100644 index 000000000..4f49e1749 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp_3_1_3.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_pair.json b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_pair.json new file mode 100644 index 000000000..062eaeca6 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_pair.json @@ -0,0 +1,27 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "keccak", + "format": "pair", + "proof_rkyv": "d_proof_keccak_pair.rkyv", + "proof_rkyv_len": 11136, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "legacy_encoding": true, + "total_folds": 8, + "terminal_len": 16, + "schedule": [1, 1, 1, 1, 1, 1, 1], + "fri_roots": ["ae4c32d62674232b0ba6d27505a79e0dc351cbef2c7bf981f7e6071e033b1452","fdab08009575f071a9cdb78934264925e0a3c15831bffb8e296c2591d89776f8","b4b063fcffce6bb444a2b9bf7c738bf8ef4465da63ce003dd7d5140381a04ac8","a7f6a30a8015cee5d8733324dd6d4be66fe4eebba7bfca7b22f8bbb5af303ef1","076d8140a23a10b4647daca20621eeb312110446791252fd479afe5d8bfbbde3","bcb71db9203da0dc14f0c83aaa660fb81bc4a5e75bb907a59cbe36950991d3a5","df52656e7ae59323992a70632af97caa3f59f00dd5e21b8777b2510bd3d222f8"], + "zetas": [[5019159632337129269,238091556992722228,5532889084085155677],[11401249367489891504,3463462679569597100,4274808243399237651],[8939167920209768920,2181998912923045116,13686372517593792052],[17506395411273156879,11867889290151972542,11407542419953424413],[12104105903477959461,15124137694392601173,12282310738917257185],[1926491111051272611,2535199797145028677,910132886595075560],[8723582983141910029,6422360606508862377,12863861130764823176],[7140187269295878849,18092345223848887536,1238341624802517565]], + "terminal_coeffs": [[9675119329879772776,14801841314017838067,18236548730038982274],[14126988372849377104,17362610904962048507,2997281616627556854],[12732028867745254654,13981984175972346630,2203858718614623478],[15602387493645224223,15059227250182045714,17573228172572152503]], + "queries_detail": [ + {"iota": 1377, "deep": [7455843768244639387,9743762440574029878,4101673830513504298], "deep_sym": [3511192201323992326,7425862771688426773,15803627959215119946], "terminal_position": 10, "layers": [{"layer": 0, "d": 1, "position": 1377, "leaf": 688, "slot": 1, "values": [[4904023103714634616,2269339872048973425,18202589099877576301]], "path_len": 10}, {"layer": 1, "d": 1, "position": 688, "leaf": 344, "slot": 0, "values": [[15552158325687108952,4967392357145231790,8955886657214556570]], "path_len": 9}, {"layer": 2, "d": 1, "position": 344, "leaf": 172, "slot": 0, "values": [[2612792798869544968,5468544103442595665,3584530796466538409]], "path_len": 8}, {"layer": 3, "d": 1, "position": 172, "leaf": 86, "slot": 0, "values": [[17892726695253162907,11675528495509671187,15693589065103820991]], "path_len": 7}, {"layer": 4, "d": 1, "position": 86, "leaf": 43, "slot": 0, "values": [[14820453473679253910,2296745045332094341,7018948464228315015]], "path_len": 6}, {"layer": 5, "d": 1, "position": 43, "leaf": 21, "slot": 1, "values": [[8878572242937089178,9511332576208914366,533065969276291189]], "path_len": 5}, {"layer": 6, "d": 1, "position": 21, "leaf": 10, "slot": 1, "values": [[329830151858172684,8816779408413419857,10745160516112719560]], "path_len": 4}]}, + {"iota": 1361, "deep": [4424386105649019747,2750120983721151361,6541960356959252561], "deep_sym": [2651307476190031573,2146706624393544526,7968881411570009805], "terminal_position": 10, "layers": [{"layer": 0, "d": 1, "position": 1361, "leaf": 680, "slot": 1, "values": [[10340133767590356363,16890795849631589605,4349300871495131212]], "path_len": 10}, {"layer": 1, "d": 1, "position": 680, "leaf": 340, "slot": 0, "values": [[16007861022951537862,1205624391150308005,9614344438830586704]], "path_len": 9}, {"layer": 2, "d": 1, "position": 340, "leaf": 170, "slot": 0, "values": [[5160827061158288284,12918447257018839138,7766567275162096996]], "path_len": 8}, {"layer": 3, "d": 1, "position": 170, "leaf": 85, "slot": 0, "values": [[6754672244016872340,5555744013098268221,7002442437311308123]], "path_len": 7}, {"layer": 4, "d": 1, "position": 85, "leaf": 42, "slot": 1, "values": [[4787433690686736449,16041398203469231226,7447353953408244230]], "path_len": 6}, {"layer": 5, "d": 1, "position": 42, "leaf": 21, "slot": 0, "values": [[7528210498357234213,5262471822230748745,1619393323132032449]], "path_len": 5}, {"layer": 6, "d": 1, "position": 21, "leaf": 10, "slot": 1, "values": [[329830151858172684,8816779408413419857,10745160516112719560]], "path_len": 4}]}, + {"iota": 1885, "deep": [16644821497740984244,7192193719577633592,567497027096456459], "deep_sym": [3328772084659598265,11972720802068272473,14505810264201907862], "terminal_position": 14, "layers": [{"layer": 0, "d": 1, "position": 1885, "leaf": 942, "slot": 1, "values": [[12364109274760885259,18414168185378424120,201814257603045233]], "path_len": 10}, {"layer": 1, "d": 1, "position": 942, "leaf": 471, "slot": 0, "values": [[7736303778366316429,1787490843314858765,7883070884957703536]], "path_len": 9}, {"layer": 2, "d": 1, "position": 471, "leaf": 235, "slot": 1, "values": [[15968265871928534159,10969654878936738885,1434479089693489220]], "path_len": 8}, {"layer": 3, "d": 1, "position": 235, "leaf": 117, "slot": 1, "values": [[14556752700637506643,2348388154040563259,6914534242512885631]], "path_len": 7}, {"layer": 4, "d": 1, "position": 117, "leaf": 58, "slot": 1, "values": [[5904884676539004901,13961094313008801215,10931051894898429435]], "path_len": 6}, {"layer": 5, "d": 1, "position": 58, "leaf": 29, "slot": 0, "values": [[6697622104848356345,9323096160160053871,1649543559850819773]], "path_len": 5}, {"layer": 6, "d": 1, "position": 29, "leaf": 14, "slot": 1, "values": [[2254094792868780472,5656680908759260325,8864344245400962516]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_pair.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_pair.rkyv new file mode 100644 index 000000000..df5753048 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_keccak_pair.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_dp.json b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_dp.json new file mode 100644 index 000000000..27ea5af30 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_dp.json @@ -0,0 +1,49 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "rpx", + "format": "cap_dp", + "proof_rkyv": "d_proof_rpx_cap_dp.rkyv", + "proof_rkyv_len": 41480, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 20, + "grinding_factor": 0, + "coset_offset": 3, + "merkle_cap": "auto", + "trace_tree_depth": 11, + "trace_cap": 3, + "fri_tree_depths": [8, 6, 4], + "fri_caps": [3, 3, 3], + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 2, 2], + "fri_roots": ["b8cd71d876dd084c3fba58b6a1b1788b09ee71b58bd373a3b0c32da0c88abac4","8abd4354863193215d65af7903d5f53018e4b2076a5d160d8197c0df719c33dc","5ba58234c6bd1059d39526597538aa208c0fce55ac284c9b1abcbfc4bc87fcb7"], + "zetas": [[4735330965523630181,1034630526833404286,12017969954712239940],[7889074366333103969,4290811767201827376,14455537773263474986],[16567739822379498242,5753162788299774204,5950576806486104926],[3148119476643166323,15342831354566172589,16163821384909909157]], + "terminal_coeffs": [[12646447477222048401,12937374675136009352,16549558038379651479],[2564516689604577222,14255657332782844950,7303342851315364550],[7904019038462202246,10880807545931735486,15264205294200432227],[13482626913175767796,15717304750858041741,4892518987751974292]], + "queries_detail": [ + {"iota": 1095, "deep": [15404367171170966026,18436452028196411499,5024906168965672354], "deep_sym": [16265809848131463264,11495087467666035873,7459148639182883977], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1095, "leaf": 136, "slot": 7, "values": [[2406839404446874184,14378864393598774400,9244658727562446653],[7310068857272658177,6690242714355677003,16014750232870151724],[16918409907732086886,2943891056625643727,17633501031830476314],[12754819164990375128,11705998079299817355,11740835587370543910],[2273041869480575555,1772390107814740151,15057029391149023100],[11526866775207687538,5051892843389067913,18351320525206389605],[12623868928666452372,4025975981633944670,16736429527117892317],[3360740073930066434,11793243838384529871,16278212037669344741]], "path_len": 13}, {"layer": 1, "d": 2, "position": 136, "leaf": 34, "slot": 0, "values": [[12541072756489582885,2370820915463632688,2423396583266204055],[14905864220509753211,15950579206739519424,15082498997069827244],[9459878356316854591,1270075853426673136,4472301856924467933],[14393624207528009097,9675335233598348594,16693550050532620583]], "path_len": 11}, {"layer": 2, "d": 2, "position": 34, "leaf": 8, "slot": 2, "values": [[16818112822979374356,10274787015391993318,15763705279771580830],[7191284429859224732,4809043185931564649,14671147736651195233],[2802946927799549417,8037886970914238609,9324105614581397069],[720806501774538325,13075390526153910697,6384491353149171884]], "path_len": 9}]}, + {"iota": 1336, "deep": [9782001122439711942,9555857402003070809,5596777784231506518], "deep_sym": [2420617880218855450,11218452639813192342,8178398275584052689], "terminal_position": 10, "layers": [{"layer": 0, "d": 3, "position": 1336, "leaf": 167, "slot": 0, "values": [[2041956337797731597,4112831764093230891,11432379156394499095],[8144473764935818890,6978818592127372038,13240419817221723331],[4510585753595495978,7259665793084123234,5963491368598554412],[12449613922291734239,18143330045847154993,11555008710041017422],[13423943835517857770,6610089783383402616,14732195742916433439],[3080128605461466890,10236270832537313262,8257530437238638301],[8362553719204526827,2153485092467020843,4632353622181731171],[11789751946472833749,6000336259605467995,973022182701680872]], "path_len": 5}, {"layer": 1, "d": 2, "position": 167, "leaf": 41, "slot": 3, "values": [[1806710437233960703,865918887752352625,6749716608005253549],[4630423819292356457,16185182827820005660,8859790871695731864],[642687296268297522,15932233592480046187,1035347428796527533],[1571642650814870704,5246859957097853911,1771889554939298762]], "path_len": 3}, {"layer": 2, "d": 2, "position": 41, "leaf": 10, "slot": 1, "values": [[16605815559724387167,7600882892259287443,6994922477906460043],[14201299358752116799,5717899003132557091,9025489995620184926],[15619332420018523266,1595985739793856288,4969668978550259454],[6667880876193245735,12155122404735786091,14288219524612442946]], "path_len": 1}]}, + {"iota": 396, "deep": [1195167017398997224,3684677677763618554,1602181459315078555], "deep_sym": [17092653866053864012,10319696574527941179,65705194922387228], "terminal_position": 3, "layers": [{"layer": 0, "d": 3, "position": 396, "leaf": 49, "slot": 4, "values": [[11065792751948336436,6945821278266161605,695451384357543318],[8408178489419937465,2838485655880223095,8492969326019934396],[265145725283343233,10180163860108398826,11843491620723569992],[15123282416936963659,17530049459658255167,8917537469248528646],[739600072118952183,889941540565197657,4394371451414923712],[15901449078141666097,9754018756689853016,12057470623441911843],[16131822616193081053,4052660922016275605,6765793682960198828],[9270165410843091007,10965847749993617830,14798583832773956809]], "path_len": 5}, {"layer": 1, "d": 2, "position": 49, "leaf": 12, "slot": 1, "values": [[3895026366660990149,2600803056632231256,4321642801809818286],[12836403324608808788,172846604945358907,7659991926477877030],[90956911091737129,4468391866397480991,9108435893769413449],[15315665520481292732,6371916974269842650,7526540926329622480]], "path_len": 3}, {"layer": 2, "d": 2, "position": 12, "leaf": 3, "slot": 0, "values": [[11401453414757530315,353357581965599259,15287411324344444169],[16460794964378913634,42195047452164617,805857385603866297],[8773174365564475115,10384145495722058796,3379422144880863992],[11394111484266117948,1699290548295696723,1801926384528077999]], "path_len": 1}]}, + {"iota": 464, "deep": [13315577128423449762,7252275752688912750,14779099846109420810], "deep_sym": [16753143537545762856,11973892546591768447,2290866283218361113], "terminal_position": 3, "layers": [{"layer": 0, "d": 3, "position": 464, "leaf": 58, "slot": 0, "values": [[11098928215659943301,10386996362178863142,1067926555475592947],[11535820862256123319,6058445108874161607,7587035286616875938],[4239169084106685387,14808332345509319384,4043547374147254999],[15526762616350004649,16244669597538508259,10278101662101015121],[18035363729350438005,12990733754043379238,8273599682664364735],[13621563928537378285,9657104990759654583,5190489215411308914],[10206630974422042578,733815345948759775,16325705866146830934],[15972587451892223286,5888285155558528998,8471935757408025741]], "path_len": 5}, {"layer": 1, "d": 2, "position": 58, "leaf": 14, "slot": 2, "values": [[6148049423775918455,14589867543249973290,3056806138608021102],[12775873754809006869,2779646161953595826,7537613261740435835],[14650203784677039238,2948257343798866283,2842367134151335097],[9538295325872013162,18409431940839324820,18140461979228732677]], "path_len": 3}, {"layer": 2, "d": 2, "position": 14, "leaf": 3, "slot": 2, "values": [[11401453414757530315,353357581965599259,15287411324344444169],[16460794964378913634,42195047452164617,805857385603866297],[8773174365564475115,10384145495722058796,3379422144880863992],[11394111484266117948,1699290548295696723,1801926384528077999]], "path_len": 1}]}, + {"iota": 772, "deep": [15575460507847184623,16637149665265295724,5265665106595265083], "deep_sym": [4196865330670975011,8511959952317818220,4486734497171560361], "terminal_position": 6, "layers": [{"layer": 0, "d": 3, "position": 772, "leaf": 96, "slot": 4, "values": [[2178356221295347642,1554040541670965436,12680044827998501479],[13087366873272454428,9093557071516503454,5636907849804883550],[14754354684262512615,5714243598007047435,13773896977707185793],[7705776266726168682,7258853463238437662,3752966942983826169],[1682970084775361702,9078299675771697083,15031900324424974662],[6155448351863329101,9050375465121490777,18189771998535169449],[18305285849261724818,13292852586514925182,4155242995629021457],[13189484825275779588,10465389948129098426,7519584792001068661]], "path_len": 5}, {"layer": 1, "d": 2, "position": 96, "leaf": 24, "slot": 0, "values": [[6688927116788273172,6301558512541473027,2984598334581139578],[10065444334709453301,1598632954335847018,7577084245944080724],[5802245441082115461,16683794194713280100,3262824299805359566],[6787132102759609571,15294244265503303220,18325636619723595824]], "path_len": 3}, {"layer": 2, "d": 2, "position": 24, "leaf": 6, "slot": 0, "values": [[18149104366334536105,18153758938222784692,13286440057442018956],[15775709367588068380,793261547229653412,4802668313348493294],[15397241215398330711,14917271561697322841,5468323818766131144],[17983203460117045278,11467095688805271950,3173804581379279974]], "path_len": 1}]}, + {"iota": 2024, "deep": [17862126616266007631,39298754798513079,16960535725074283747], "deep_sym": [14855693456863274664,9126881790095326223,12348213703764840419], "terminal_position": 15, "layers": [{"layer": 0, "d": 3, "position": 2024, "leaf": 253, "slot": 0, "values": [[248680714655802291,8287422482732093021,11512168046999728330],[1698550105716295035,4359650213628285538,15696348528790637165],[10225623802443553751,7412205301699440524,10936257831384578972],[5580328202384677588,3511693997310828484,4032541176270544725],[1502477949338677178,8937518693682245174,2377119977838417508],[16831417672874055750,8516735277199927274,7151182767156521586],[13172991599883336198,3614626671380230259,4994228220289388971],[5126695922719234861,6460266335430374719,13754234627137778040]], "path_len": 5}, {"layer": 1, "d": 2, "position": 253, "leaf": 63, "slot": 1, "values": [[12671436740885090106,4712797108775092170,16379995986181438746],[13487967099277239218,3917591648834860403,5185034935052652064],[7260034507286567562,15934756997500426999,8176082139629178738],[3799004422275886064,1804703660742419729,790511295811445306]], "path_len": 3}, {"layer": 2, "d": 2, "position": 63, "leaf": 15, "slot": 3, "values": [[13804487110218064471,11031393271821788048,12102032588536669622],[15933072575736243346,16812635495079777057,16914031897057335688],[10355406612524080793,5018917751865171386,5486395490041162930],[10611084620214441847,16088658798861853787,13676265492939844660]], "path_len": 1}]}, + {"iota": 1018, "deep": [9666497629706741416,2711014432294742997,5338993119085585124], "deep_sym": [17591789704411965224,8334765678444352322,12136050117161229927], "terminal_position": 7, "layers": [{"layer": 0, "d": 3, "position": 1018, "leaf": 127, "slot": 2, "values": [[15915610210880015156,654178370987434333,9001484692176855369],[16701406999377053209,14900983005331860275,15608324566254649573],[16800180945129655421,16239098360085105416,746862932917931344],[7135860444559045688,4368596734678544347,10009304364373728046],[7211051365822209500,7053194884746004028,9886256935976347858],[17030910034289132002,1075234149160835061,6072641001730603910],[9112216685035143354,8790352404567562935,1598562387253550849],[4263051900018733301,16370941081911055892,703039766805563670]], "path_len": 5}, {"layer": 1, "d": 2, "position": 127, "leaf": 31, "slot": 3, "values": [[893701164733485859,10783705128469438790,6430592977790142885],[9273314110616551886,3191912665801675716,4156207855013590725],[7766475431522936470,13966589519538816880,5903906557995018826],[17064858841826728263,13953088988948658971,4173474481815629242]], "path_len": 3}, {"layer": 2, "d": 2, "position": 31, "leaf": 7, "slot": 3, "values": [[1033049072983578823,10867488755001030385,3516800898677287794],[16923583511889176879,17224454678017056146,10546138564032550635],[244008804088196093,5255888736598227352,5009267593252073460],[7173875125936270454,543113462009931580,6960567853046668448]], "path_len": 1}]}, + {"iota": 434, "deep": [804929999337509138,1468933884987952182,7645286452403436813], "deep_sym": [12619978267720755695,13772173429398559183,11854116844548204019], "terminal_position": 3, "layers": [{"layer": 0, "d": 3, "position": 434, "leaf": 54, "slot": 2, "values": [[8986955191011758421,10726725355813200951,10473372829063026604],[9071506389801486070,8509869025281211871,2644477472432484436],[13964968611822928574,13285675260824773453,11748096510209525604],[2310215663300437810,8171542782131568893,9768443869295330051],[17498954884778899589,13217673593679621957,9626919390121158837],[5978707022097593593,16199170155804328509,15948541937847602294],[4647558760010050621,1464615084228594593,14590764956234869493],[3513037597053252531,16880048805136258473,3524654951292347810]], "path_len": 5}, {"layer": 1, "d": 2, "position": 54, "leaf": 13, "slot": 2, "values": [[4400625513865422810,6743756406446708109,16537227040912844421],[11049544044019279635,2177374879725984586,2701240540680480214],[10262723708506140141,9571236840615945782,17624993559481847384],[4590038678235841403,4251101814116480663,1187654653540111335]], "path_len": 3}, {"layer": 2, "d": 2, "position": 13, "leaf": 3, "slot": 1, "values": [[11401453414757530315,353357581965599259,15287411324344444169],[16460794964378913634,42195047452164617,805857385603866297],[8773174365564475115,10384145495722058796,3379422144880863992],[11394111484266117948,1699290548295696723,1801926384528077999]], "path_len": 1}]}, + {"iota": 1406, "deep": [13230221430615035516,10813863012752583972,2318008317718129139], "deep_sym": [6106910834220921197,6523099607877010761,6593979421588875795], "terminal_position": 10, "layers": [{"layer": 0, "d": 3, "position": 1406, "leaf": 175, "slot": 6, "values": [[323195911353260228,17263245316893654565,11463257750186360671],[7320445494663096340,14237138155561682560,3110675865172535763],[6713680053659494631,2032538538940427656,1057113045797355331],[8631725061358052832,423633208027496564,4662527007038932496],[18132757568820259228,2663534511082111563,8199429364722786367],[6933883104364360942,2929190753787481714,13046068366344658748],[14590650123346818081,6366762199596637381,12314754550654698852],[9574093927666969281,2563022925074734877,7105053068735183895]], "path_len": 5}, {"layer": 1, "d": 2, "position": 175, "leaf": 43, "slot": 3, "values": [[7377752767619274477,14960320299150954633,1656998823700909017],[13909380617930782423,1665610621370769977,4853928730766007756],[15420917467817398658,12095262020142939256,15586101709279441247],[545594005296785681,7981396123147073003,7300637563208043684]], "path_len": 3}, {"layer": 2, "d": 2, "position": 43, "leaf": 10, "slot": 3, "values": [[16605815559724387167,7600882892259287443,6994922477906460043],[14201299358752116799,5717899003132557091,9025489995620184926],[15619332420018523266,1595985739793856288,4969668978550259454],[6667880876193245735,12155122404735786091,14288219524612442946]], "path_len": 1}]}, + {"iota": 851, "deep": [5069211856273159308,15738204765827836414,7803687198427520729], "deep_sym": [3365555321677271314,7183365741688524927,7992791527747308157], "terminal_position": 6, "layers": [{"layer": 0, "d": 3, "position": 851, "leaf": 106, "slot": 3, "values": [[8185664597084646010,7199396461353948815,4290149667551161223],[10553626672500095754,16880963759787771784,5047385498810058389],[5128343943071886226,9397445922439232244,17537431850446381343],[12456949827564107349,1794586338037375500,10510426836892300002],[4392351958428368786,3828249484931527240,4409399853039764489],[17886905826175496683,11715164881135227845,717170915527280937],[4601525578578854078,12081089708058704727,3314088772912454648],[12218840982635270313,8071433161511740522,13958090671284912090]], "path_len": 5}, {"layer": 1, "d": 2, "position": 106, "leaf": 26, "slot": 2, "values": [[9301234740979162646,2753907425628220151,17889478520006217760],[6274786885341819980,6331824999788377843,3265708602123659790],[14009363662878866218,8048778356133517596,14181916097130251870],[11109783065294107,6529788703768046412,16921505187996595492]], "path_len": 3}, {"layer": 2, "d": 2, "position": 26, "leaf": 6, "slot": 2, "values": [[18149104366334536105,18153758938222784692,13286440057442018956],[15775709367588068380,793261547229653412,4802668313348493294],[15397241215398330711,14917271561697322841,5468323818766131144],[17983203460117045278,11467095688805271950,3173804581379279974]], "path_len": 1}]}, + {"iota": 1619, "deep": [1408276854512374808,8900300285643770305,6610355828502006211], "deep_sym": [14513375217811742076,16874657744221025801,1601542188964114380], "terminal_position": 12, "layers": [{"layer": 0, "d": 3, "position": 1619, "leaf": 202, "slot": 3, "values": [[16405036730644138202,13573262514280719051,8197665227336875941],[4810183087946276285,10961977203566478230,5769377055311518319],[1039569578885082534,10799768036753555823,5824281002988763926],[4663399330769730164,11219583712814900217,2591059508733142074],[3710383079295111760,7862687122903785212,8819873747000791656],[12731875014052926425,16826320984485484741,5314929152696311448],[6472125744179394964,9646881788806417866,18320151023544004885],[13413754152113967178,16805826508014926467,11562774648789987451]], "path_len": 5}, {"layer": 1, "d": 2, "position": 202, "leaf": 50, "slot": 2, "values": [[9746517607667315512,5356526412011744366,378649477001740238],[15819241902267462633,10827020772783032148,12295143323093252537],[16740218466659950335,11322925347085219479,12626480753167162603],[8928078663829289801,17970166156916115780,14458074476181583109]], "path_len": 3}, {"layer": 2, "d": 2, "position": 50, "leaf": 12, "slot": 2, "values": [[5464005601434038088,9623393316986421227,10756173414515652631],[15629554433951570575,5891404871893161211,7794506514771656936],[7015985412422436116,5314828133090116385,3339720643265786168],[7095733908393958914,18058755147947351090,12396916271309078570]], "path_len": 1}]}, + {"iota": 734, "deep": [6726635026919291185,6114134777381380747,3969234766899550501], "deep_sym": [11610741361144710308,8022256674750058278,4492303683849342962], "terminal_position": 5, "layers": [{"layer": 0, "d": 3, "position": 734, "leaf": 91, "slot": 6, "values": [[1776024911715557280,9535820208453597638,11375092966262227432],[4695891893436478717,14290340289102254735,10729924849860397325],[18204714348454572237,9512367534659701392,6022391662543991831],[10220654977012995561,9581140563703575642,537552342188447673],[17761397183816365641,2144580924868674917,16884137666549458392],[16018528100941444706,8139525193477316658,9580421776092478677],[1122027982641871908,15563930121846823177,4497287494922049376],[16980848242871090598,16990717267573275839,1474503480350023461]], "path_len": 5}, {"layer": 1, "d": 2, "position": 91, "leaf": 22, "slot": 3, "values": [[18181522475995947445,8128317690483459736,15340725558620520241],[17934575813731174648,3157137049601440032,9620131208779528051],[1301762668122567517,15591669752359556141,10373328281998565515],[17301133920439871298,14423637770918509274,11879195659894573040]], "path_len": 3}, {"layer": 2, "d": 2, "position": 22, "leaf": 5, "slot": 2, "values": [[6504414617537595573,6354919752630017286,15662046576375417059],[18190904080162926060,10799053964643538786,2642404754410762933],[7534862575300724521,10620461630712972371,7610325415054858437],[7083822503393396084,3502344503227274059,4024386299932845131]], "path_len": 1}]}, + {"iota": 1584, "deep": [145172723069761214,6132204174748908772,1251322044684973154], "deep_sym": [16371791428722529123,13517035650984980097,18308470790125148470], "terminal_position": 12, "layers": [{"layer": 0, "d": 3, "position": 1584, "leaf": 198, "slot": 0, "values": [[8084017772650914005,12302362108596853331,14892860500875362423],[16700768433721502832,174528624483998797,3598044338018230632],[17169989850796931758,8694474674286330181,5058940594902287935],[4759200886165615660,14047932516327578328,13086455261210927405],[17230655055940252911,10079960231862789693,1594365425215384720],[12450224610855411435,6793022022333426780,3810635609165506132],[7869220775064086658,3175255887204963156,13219361725689881080],[10624982050920163244,12102988984123124133,8621395626272779527]], "path_len": 5}, {"layer": 1, "d": 2, "position": 198, "leaf": 49, "slot": 2, "values": [[3935900619226349263,1242728730961115505,5259089442116211009],[3425851199438451350,2149634388463401711,2660624055263107619],[17389106764791954576,12349010740432980330,10945228020128263917],[15743952789761425276,1575275858179672379,4552729722665455521]], "path_len": 3}, {"layer": 2, "d": 2, "position": 49, "leaf": 12, "slot": 1, "values": [[5464005601434038088,9623393316986421227,10756173414515652631],[15629554433951570575,5891404871893161211,7794506514771656936],[7015985412422436116,5314828133090116385,3339720643265786168],[7095733908393958914,18058755147947351090,12396916271309078570]], "path_len": 1}]}, + {"iota": 1108, "deep": [8291018792785974071,13313098309166568094,8280431289888479898], "deep_sym": [14797191581087246444,943987639731003521,7102902153805509629], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1108, "leaf": 138, "slot": 4, "values": [[10747780665270753536,14925858981123565357,14776533201595099206],[17020063819487208823,16331022987088535628,398851099833709126],[3905999722443358219,13174069017335826404,615555513443939273],[7848731174735099969,16244771701549524850,3504209930058176715],[16177707419406544780,5336690175849378686,14295699186837538335],[14276398242879772669,10348977498094124152,380521701586907949],[15354946610280677012,14044707150946569948,17571854873593814494],[12389508910263015150,16025161443698577861,4003448375894115960]], "path_len": 5}, {"layer": 1, "d": 2, "position": 138, "leaf": 34, "slot": 2, "values": [[12541072756489582885,2370820915463632688,2423396583266204055],[14905864220509753211,15950579206739519424,15082498997069827244],[9459878356316854591,1270075853426673136,4472301856924467933],[14393624207528009097,9675335233598348594,16693550050532620583]], "path_len": 3}, {"layer": 2, "d": 2, "position": 34, "leaf": 8, "slot": 2, "values": [[16818112822979374356,10274787015391993318,15763705279771580830],[7191284429859224732,4809043185931564649,14671147736651195233],[2802946927799549417,8037886970914238609,9324105614581397069],[720806501774538325,13075390526153910697,6384491353149171884]], "path_len": 1}]}, + {"iota": 1115, "deep": [6618438615200267976,4919274826576345125,4625388421371444138], "deep_sym": [9246029957586231281,17197016168898005971,16581617646226338233], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1115, "leaf": 139, "slot": 3, "values": [[17001607609996340984,13454749039895100314,3715210367178856146],[6048616277769393765,1359761812099145862,8711385731818791115],[3932895076288661499,17258907193956398903,11400500372312523510],[12054074072422017896,3208762764990612220,5562583925758922572],[13314015859018059125,7621965631850760423,11054467258190560956],[18038525678620396892,3154653838113744463,15429304576725024961],[15961703551772895141,9711979530086833226,6731603843790161535],[11068648140989596682,4028169998411965337,14241442597618133873]], "path_len": 5}, {"layer": 1, "d": 2, "position": 139, "leaf": 34, "slot": 3, "values": [[12541072756489582885,2370820915463632688,2423396583266204055],[14905864220509753211,15950579206739519424,15082498997069827244],[9459878356316854591,1270075853426673136,4472301856924467933],[14393624207528009097,9675335233598348594,16693550050532620583]], "path_len": 3}, {"layer": 2, "d": 2, "position": 34, "leaf": 8, "slot": 2, "values": [[16818112822979374356,10274787015391993318,15763705279771580830],[7191284429859224732,4809043185931564649,14671147736651195233],[2802946927799549417,8037886970914238609,9324105614581397069],[720806501774538325,13075390526153910697,6384491353149171884]], "path_len": 1}]}, + {"iota": 1531, "deep": [9728795865480759962,10841781668352654296,10796525908360120488], "deep_sym": [14058290127965822562,13000425636675524513,14945972201157220666], "terminal_position": 11, "layers": [{"layer": 0, "d": 3, "position": 1531, "leaf": 191, "slot": 3, "values": [[948368160510490102,8544727733740695269,10749830773933086533],[10329255878178745701,16086143103855673178,14825016990397793196],[9788573768211855305,1974783517146606653,7321026158536110064],[18390256767309975591,14744604900512807878,5586461636310614090],[17243629565902491525,5069704867446951940,15263602100351989401],[12555155414279618008,13856182192732480001,6153453073174628912],[348446857428337296,6537149763573446604,3301475865544646631],[8211285100903112410,13242877615395271090,5151960547612505286]], "path_len": 5}, {"layer": 1, "d": 2, "position": 191, "leaf": 47, "slot": 3, "values": [[9004309183751256646,15053749661805259059,13402872281440665070],[11923832368540281318,9591949487276575382,3941126143583950984],[10524734310662209668,18323881505728001248,6372778122146690452],[1004243276605533431,3591193302962306504,1633219252869578500]], "path_len": 3}, {"layer": 2, "d": 2, "position": 47, "leaf": 11, "slot": 3, "values": [[13252749493197899059,7111335394328122694,16103957492148269244],[15670868136619302923,10379423668602200154,74901909926248704],[10169946223356276752,13455008041475535374,5339865456753800954],[591266454968139898,799088598745050263,7430921739329049493]], "path_len": 1}]}, + {"iota": 460, "deep": [3946463641621599873,5430918586547480310,12399672165657981355], "deep_sym": [817130378730303002,18226081220269208290,10040121201250819828], "terminal_position": 3, "layers": [{"layer": 0, "d": 3, "position": 460, "leaf": 57, "slot": 4, "values": [[15528130404920832646,5215522714446081493,12271422738351742797],[6863031435719765099,8964286762487153093,3348744363473493650],[2929074367356164008,2538292072132929796,7267988012732332696],[16186424673451190780,12421741952053121662,7403524188509867498],[9913385481138587183,566253416801528561,4689471667889074830],[5238292704539165876,5207577595457270053,15319933152776760516],[988335866269748584,12704285650474491375,13454789435901030277],[5181234302079931035,16724170191637402584,9857139164589527058]], "path_len": 5}, {"layer": 1, "d": 2, "position": 57, "leaf": 14, "slot": 1, "values": [[6148049423775918455,14589867543249973290,3056806138608021102],[12775873754809006869,2779646161953595826,7537613261740435835],[14650203784677039238,2948257343798866283,2842367134151335097],[9538295325872013162,18409431940839324820,18140461979228732677]], "path_len": 3}, {"layer": 2, "d": 2, "position": 14, "leaf": 3, "slot": 2, "values": [[11401453414757530315,353357581965599259,15287411324344444169],[16460794964378913634,42195047452164617,805857385603866297],[8773174365564475115,10384145495722058796,3379422144880863992],[11394111484266117948,1699290548295696723,1801926384528077999]], "path_len": 1}]}, + {"iota": 21, "deep": [16751469336709747935,13034541952351682476,362160150163509523], "deep_sym": [13613547504591209906,17994713379913002098,12951980278075054529], "terminal_position": 0, "layers": [{"layer": 0, "d": 3, "position": 21, "leaf": 2, "slot": 5, "values": [[12229915140224745050,11914636115907519215,15454813039040787602],[11004608921862797938,2786446545964014409,11215296566347994755],[14247823219081093028,7678319371580752545,4364550269713641097],[4950501297074424090,3777394133863232678,3952696221507450235],[17403688435427697952,14643967294446729293,938567040701683587],[1347531407523399298,12307613284931727666,2603176576665318112],[17569334922626660479,15457741679174967682,2884291902000028503],[2613590051935242029,9966289619120623030,13359190776901017615]], "path_len": 5}, {"layer": 1, "d": 2, "position": 2, "leaf": 0, "slot": 2, "values": [[14271557498503267540,5604443783523955218,2419070109913866744],[5057225870813679079,6878920305747299227,7904171198740585785],[11637960830172701537,9246719637937614202,1888778944505873394],[14752417623798547784,12864596472359031008,17470784308582478716]], "path_len": 3}, {"layer": 2, "d": 2, "position": 0, "leaf": 0, "slot": 0, "values": [[16711837865871043867,7230876273111376341,3914710442871488734],[9427846744532214811,5523266006034183126,10995783862421668556],[6894989026912389225,5249821206546415796,2490306299428115481],[2435848346614272276,13191746087021246298,17137928531725839726]], "path_len": 1}]}, + {"iota": 1190, "deep": [10564639282863785564,8648928983583492162,1854090593019502747], "deep_sym": [14736828121689239151,13061678655855235343,595509014249069299], "terminal_position": 9, "layers": [{"layer": 0, "d": 3, "position": 1190, "leaf": 148, "slot": 6, "values": [[1912108142230074757,18120753521788756304,4744797074824315095],[7622973140663785730,3218517023600596379,12933172925186170580],[6856954486901287670,10911063608587332337,10495005156059272273],[3630161301090547043,9976280925750132817,14959174963198538503],[15849533635970265205,7931782409217351652,1711881977559092105],[4244786941653413432,611712464118926980,10550338065376625928],[4261656037681919894,13899927091162554149,4496214128402436523],[4758719564465786030,6242494762166384866,6922579272962812326]], "path_len": 5}, {"layer": 1, "d": 2, "position": 148, "leaf": 37, "slot": 0, "values": [[1720604404904246207,4456310064256824594,16023405838743839253],[12095426019459384804,3545515663528391901,13847932260916760557],[14481945053830228832,10032951144866827271,18307948857941228918],[9568183540619999479,6075525606092921498,9812470079212029532]], "path_len": 3}, {"layer": 2, "d": 2, "position": 37, "leaf": 9, "slot": 1, "values": [[4938759970149737351,5713750335509713787,7704473464848629755],[12178860564334633133,4505589151486207207,11019837447191491161],[759847948334892703,2198096190256262147,2258375761380806592],[8544214346083083966,2594628239276646728,10573244010893722073]], "path_len": 1}]}, + {"iota": 540, "deep": [13790780674944227860,7489226687845769437,18266880323214702999], "deep_sym": [4078467403041968014,11722414815152663777,6384647940485904195], "terminal_position": 4, "layers": [{"layer": 0, "d": 3, "position": 540, "leaf": 67, "slot": 4, "values": [[16038763184741478660,4860630134492478394,5827712014215397494],[8602567303568281597,6070847271420277083,4594582598770358286],[5742595377141191612,11862514134763543219,7170856629784425895],[3215088790510035073,3705899893413116883,13687575627121706169],[11324792041998818675,16480222931634912753,9694814253569073593],[7554074958443565868,10066082608075392955,3858077481277024958],[12771829728786218223,15277647901280526420,2963318663981532012],[10528238661142981081,7489180081743875754,5379717373430647217]], "path_len": 5}, {"layer": 1, "d": 2, "position": 67, "leaf": 16, "slot": 3, "values": [[7108793073240269897,13667909718173120897,14121100863859163201],[17341774714088985140,15724372809747486471,226187607144172421],[9549280667125660671,2332354502197878932,9627352183216333391],[4789373132786799347,10303671649960281535,1512145240231698617]], "path_len": 3}, {"layer": 2, "d": 2, "position": 16, "leaf": 4, "slot": 0, "values": [[13859184395281262686,3799190906283224991,18307316685430313213],[10009945053196490616,8817414741194159404,9457261446137636070],[8676620017703582792,17038654695824559897,8064727157295768002],[641207936648328848,6763031934725076030,4948104159318021490]], "path_len": 1}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_dp.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_dp.rkyv new file mode 100644 index 000000000..6ec585737 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_dp.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_pair.json b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_pair.json new file mode 100644 index 000000000..e003a144d --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_pair.json @@ -0,0 +1,49 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "rpx", + "format": "cap_pair", + "proof_rkyv": "d_proof_rpx_cap_pair.rkyv", + "proof_rkyv_len": 51752, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 20, + "grinding_factor": 0, + "coset_offset": 3, + "merkle_cap": "auto", + "trace_tree_depth": 11, + "trace_cap": 3, + "fri_tree_depths": [10, 9, 8, 7, 6, 5, 4], + "fri_caps": [3, 3, 3, 3, 3, 3, 3], + "legacy_encoding": true, + "total_folds": 8, + "terminal_len": 16, + "schedule": [1, 1, 1, 1, 1, 1, 1], + "fri_roots": ["a5cb9815a33628e2d9aee1104d2210e8066854dd38976a0143c7b0638504f9f3","5af88af0782998624ef05869a90b8e35e82a85d7da5c899ce3a0d8d1872a1ab0","97ae9eedee1a1b29b7eb30478eb3b8f118b84f3bd43e6352fbdac68314723730","2f2da5b7622cc6eb49677363305e2626fd79387dc9acaae9202b655cb84f1f8a","9fef7049e45b8f097dee76ba5feadc10268bb51f63e9336dfafc3ac54e3e3e38","c261958da4b13c4ce5dcf9cb19d42cf09808cb932dea3b9e69bdb7577f308167","2aa133f65dd602f13ab5b2961d2b96cc3f6daf6f106e9783939c69716b9d2ab9"], + "zetas": [[4735330965523630181,1034630526833404286,12017969954712239940],[16665743570319646148,16897879252278531211,10291861723093761662],[10619368815145924427,1493089516910409884,14431758427697423319],[9552622148858278301,6488516694070886107,4893272118711353122],[2783515638190829017,9945112524553572548,15631202162117197821],[17815262798501899446,18394714429174366312,8636369618480887460],[11856812424473920575,10766055906630249609,2957922871886357218],[8171264462115707646,14626134561370321125,16568024845886241762]], + "terminal_coeffs": [[3714161951696662500,2595793324825979078,3565379477475041685],[4424026265791135346,6558459514194683116,753777937513084834],[7727091673734829526,10561609288187203284,15868472042909273283],[7864830287677944053,6520068215425390864,4795411284951093801]], + "queries_detail": [ + {"iota": 907, "deep": [3040718383397280274,11956941222209451830,13732184933327600162], "deep_sym": [14770993481378414249,10902020514396191223,13023915065759495237], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 907, "leaf": 453, "slot": 1, "values": [[10738449088863093738,17001367698800175667,872083450534898496]], "path_len": 15}, {"layer": 1, "d": 1, "position": 453, "leaf": 226, "slot": 1, "values": [[8949331791643690447,11945875389884147400,4863937416666523377]], "path_len": 14}, {"layer": 2, "d": 1, "position": 226, "leaf": 113, "slot": 0, "values": [[6531312029425646452,8614572489917572820,5793377902239563396]], "path_len": 13}, {"layer": 3, "d": 1, "position": 113, "leaf": 56, "slot": 1, "values": [[13245888542464671401,9490131273601039377,5473879260557931677]], "path_len": 12}, {"layer": 4, "d": 1, "position": 56, "leaf": 28, "slot": 0, "values": [[967364531071989975,2293195037664380504,8237422110493151214]], "path_len": 11}, {"layer": 5, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[3068323341190833160,2495604132745403317,440675983381132721]], "path_len": 10}, {"layer": 6, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[6235735872747817365,1215148853071865642,1291260655626560448]], "path_len": 9}]}, + {"iota": 327, "deep": [6648971487643812783,3504019026315364339,5553986533175031714], "deep_sym": [4847162544338375188,3760822623165876653,18385909206353757824], "terminal_position": 2, "layers": [{"layer": 0, "d": 1, "position": 327, "leaf": 163, "slot": 1, "values": [[3102706233933768041,2061658593148860966,2937765886239263664]], "path_len": 7}, {"layer": 1, "d": 1, "position": 163, "leaf": 81, "slot": 1, "values": [[5351203088071617904,5190285090595573022,679443460868787999]], "path_len": 6}, {"layer": 2, "d": 1, "position": 81, "leaf": 40, "slot": 1, "values": [[6813072834546256573,17560727769766864490,11355003854934910309]], "path_len": 5}, {"layer": 3, "d": 1, "position": 40, "leaf": 20, "slot": 0, "values": [[4276057153471798645,16589259977369031850,10213981092871167197]], "path_len": 4}, {"layer": 4, "d": 1, "position": 20, "leaf": 10, "slot": 0, "values": [[6724049492496023179,14516868345363052182,5463760551208548401]], "path_len": 3}, {"layer": 5, "d": 1, "position": 10, "leaf": 5, "slot": 0, "values": [[872892273747685357,17097460053185137664,7011811299561387579]], "path_len": 2}, {"layer": 6, "d": 1, "position": 5, "leaf": 2, "slot": 1, "values": [[6972245892337738186,7308248010937532343,15369312713036268331]], "path_len": 1}]}, + {"iota": 1055, "deep": [817615283715329005,16733328116567315164,13240493922581948560], "deep_sym": [1731008489953378402,3915464684156498806,11643985226885705726], "terminal_position": 8, "layers": [{"layer": 0, "d": 1, "position": 1055, "leaf": 527, "slot": 1, "values": [[7812814834329037929,5936428735545781918,6769737470751296857]], "path_len": 7}, {"layer": 1, "d": 1, "position": 527, "leaf": 263, "slot": 1, "values": [[17350970259606761374,14937434176606905201,2436331477479557398]], "path_len": 6}, {"layer": 2, "d": 1, "position": 263, "leaf": 131, "slot": 1, "values": [[11807655768007569086,16744789612728260734,4620195842063357801]], "path_len": 5}, {"layer": 3, "d": 1, "position": 131, "leaf": 65, "slot": 1, "values": [[13354553806338021504,6368498475783127995,1444223788932639250]], "path_len": 4}, {"layer": 4, "d": 1, "position": 65, "leaf": 32, "slot": 1, "values": [[2595125021894609724,3173778131451323870,2008467966233623536]], "path_len": 3}, {"layer": 5, "d": 1, "position": 32, "leaf": 16, "slot": 0, "values": [[734813548736948502,6040967620827257108,7735869121954590664]], "path_len": 2}, {"layer": 6, "d": 1, "position": 16, "leaf": 8, "slot": 0, "values": [[6896385518190873683,9696907498203743737,11255624105341837683]], "path_len": 1}]}, + {"iota": 490, "deep": [5333555309022376619,6497848330298835332,16563034812639328791], "deep_sym": [1540291644563635092,1259808496858708141,6436795295029442319], "terminal_position": 3, "layers": [{"layer": 0, "d": 1, "position": 490, "leaf": 245, "slot": 0, "values": [[16165997071554694371,7809042381953747683,14845781732875929587]], "path_len": 7}, {"layer": 1, "d": 1, "position": 245, "leaf": 122, "slot": 1, "values": [[4202361345781088339,2870932090735936211,12730858377474507808]], "path_len": 6}, {"layer": 2, "d": 1, "position": 122, "leaf": 61, "slot": 0, "values": [[4487121043982363903,7222683238575461495,4659152637789648093]], "path_len": 5}, {"layer": 3, "d": 1, "position": 61, "leaf": 30, "slot": 1, "values": [[9216116296820106680,16579878421713900505,13631642496076283964]], "path_len": 4}, {"layer": 4, "d": 1, "position": 30, "leaf": 15, "slot": 0, "values": [[8296754296575280656,4566791758227172626,8560225543529906602]], "path_len": 3}, {"layer": 5, "d": 1, "position": 15, "leaf": 7, "slot": 1, "values": [[16151268240327736551,6959880367891302202,12923693381147279704]], "path_len": 2}, {"layer": 6, "d": 1, "position": 7, "leaf": 3, "slot": 1, "values": [[2041773572074858086,8235519873000800075,6801272137943839603]], "path_len": 1}]}, + {"iota": 1293, "deep": [13202019215294755661,792069985274517970,1837919879426916066], "deep_sym": [11004593316219637619,11457286750131782961,4287723567360687560], "terminal_position": 10, "layers": [{"layer": 0, "d": 1, "position": 1293, "leaf": 646, "slot": 1, "values": [[11542857627666095499,5331048150248723898,10911724239825767773]], "path_len": 7}, {"layer": 1, "d": 1, "position": 646, "leaf": 323, "slot": 0, "values": [[4284204476542245753,10807736373362441887,8541842606839334792]], "path_len": 6}, {"layer": 2, "d": 1, "position": 323, "leaf": 161, "slot": 1, "values": [[6049964432242287280,10740098497842851829,11762819817772120687]], "path_len": 5}, {"layer": 3, "d": 1, "position": 161, "leaf": 80, "slot": 1, "values": [[3659481731940555489,7978114488151616121,2434855029340771432]], "path_len": 4}, {"layer": 4, "d": 1, "position": 80, "leaf": 40, "slot": 0, "values": [[3062799608155256757,10298993203696925632,4135505462794541032]], "path_len": 3}, {"layer": 5, "d": 1, "position": 40, "leaf": 20, "slot": 0, "values": [[208230508203853506,15333826890764774037,18057329976005815313]], "path_len": 2}, {"layer": 6, "d": 1, "position": 20, "leaf": 10, "slot": 0, "values": [[8675393632150954143,13193766732232908346,11058925871258175295]], "path_len": 1}]}, + {"iota": 1232, "deep": [5100849131875730764,13311523601572949287,17754933639302584926], "deep_sym": [9125629300697698907,6092338751183093721,4621944178520612389], "terminal_position": 9, "layers": [{"layer": 0, "d": 1, "position": 1232, "leaf": 616, "slot": 0, "values": [[16018690189368362260,420945639044652702,6800220696365653435]], "path_len": 7}, {"layer": 1, "d": 1, "position": 616, "leaf": 308, "slot": 0, "values": [[423722432265633255,17388955584979232176,11808809124110976521]], "path_len": 6}, {"layer": 2, "d": 1, "position": 308, "leaf": 154, "slot": 0, "values": [[6224050008368218659,7793526065195312542,7754584313398499093]], "path_len": 5}, {"layer": 3, "d": 1, "position": 154, "leaf": 77, "slot": 0, "values": [[5353496693137660222,1400459084141815741,12865696614160780188]], "path_len": 4}, {"layer": 4, "d": 1, "position": 77, "leaf": 38, "slot": 1, "values": [[10248192692058468149,13403323599212523055,1065945654017210721]], "path_len": 3}, {"layer": 5, "d": 1, "position": 38, "leaf": 19, "slot": 0, "values": [[15747202659571070477,11582933239615491642,9692129310833850457]], "path_len": 2}, {"layer": 6, "d": 1, "position": 19, "leaf": 9, "slot": 1, "values": [[7595395067547975575,4679686797858417144,7459990344248030932]], "path_len": 1}]}, + {"iota": 906, "deep": [16987545712744995801,9134272306172516667,6154404976421516758], "deep_sym": [1567212379715838741,18001184530175578021,18020286302555685985], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 906, "leaf": 453, "slot": 0, "values": [[208147472821821449,11920382692700401347,10451650462477063379]], "path_len": 7}, {"layer": 1, "d": 1, "position": 453, "leaf": 226, "slot": 1, "values": [[8949331791643690447,11945875389884147400,4863937416666523377]], "path_len": 6}, {"layer": 2, "d": 1, "position": 226, "leaf": 113, "slot": 0, "values": [[6531312029425646452,8614572489917572820,5793377902239563396]], "path_len": 5}, {"layer": 3, "d": 1, "position": 113, "leaf": 56, "slot": 1, "values": [[13245888542464671401,9490131273601039377,5473879260557931677]], "path_len": 4}, {"layer": 4, "d": 1, "position": 56, "leaf": 28, "slot": 0, "values": [[967364531071989975,2293195037664380504,8237422110493151214]], "path_len": 3}, {"layer": 5, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[3068323341190833160,2495604132745403317,440675983381132721]], "path_len": 2}, {"layer": 6, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[6235735872747817365,1215148853071865642,1291260655626560448]], "path_len": 1}]}, + {"iota": 1445, "deep": [8238996569358525839,7400933341107165980,13539245176576736813], "deep_sym": [16723622915527146625,4997619439840450854,16643934399247561766], "terminal_position": 11, "layers": [{"layer": 0, "d": 1, "position": 1445, "leaf": 722, "slot": 1, "values": [[748244684854860367,15581535348713203250,14488124003887067430]], "path_len": 7}, {"layer": 1, "d": 1, "position": 722, "leaf": 361, "slot": 0, "values": [[12029438690276759030,14110935376777698328,2444834449659203005]], "path_len": 6}, {"layer": 2, "d": 1, "position": 361, "leaf": 180, "slot": 1, "values": [[1344770080555719020,4450866369841668538,8535818524384001641]], "path_len": 5}, {"layer": 3, "d": 1, "position": 180, "leaf": 90, "slot": 0, "values": [[1627076855653398376,3342203343274309383,17248029193346284789]], "path_len": 4}, {"layer": 4, "d": 1, "position": 90, "leaf": 45, "slot": 0, "values": [[9174377671867584593,4419776172399299719,8816228065868848859]], "path_len": 3}, {"layer": 5, "d": 1, "position": 45, "leaf": 22, "slot": 1, "values": [[11828230151300071203,11852837209955541876,18387241096789405769]], "path_len": 2}, {"layer": 6, "d": 1, "position": 22, "leaf": 11, "slot": 0, "values": [[4497056867330769201,5556595583679787664,7599353405711249964]], "path_len": 1}]}, + {"iota": 1900, "deep": [7431512126338372394,12901884170471295656,13229107773857763089], "deep_sym": [4305251512907028158,11796135359754093295,10529446823649361544], "terminal_position": 14, "layers": [{"layer": 0, "d": 1, "position": 1900, "leaf": 950, "slot": 0, "values": [[10597607009932306054,2041180774756384802,15073779384058885182]], "path_len": 7}, {"layer": 1, "d": 1, "position": 950, "leaf": 475, "slot": 0, "values": [[7594923001698684643,17769333214797688037,9184058274024548434]], "path_len": 6}, {"layer": 2, "d": 1, "position": 475, "leaf": 237, "slot": 1, "values": [[6570785028429573968,7305051340909591677,17087044582461523085]], "path_len": 5}, {"layer": 3, "d": 1, "position": 237, "leaf": 118, "slot": 1, "values": [[15847483128935587866,9558607112153220780,8383580498378797538]], "path_len": 4}, {"layer": 4, "d": 1, "position": 118, "leaf": 59, "slot": 0, "values": [[13707884290003644050,6351837470214772186,5687335870977301326]], "path_len": 3}, {"layer": 5, "d": 1, "position": 59, "leaf": 29, "slot": 1, "values": [[2614229400037681378,4408538218630961289,11581585520222664939]], "path_len": 2}, {"layer": 6, "d": 1, "position": 29, "leaf": 14, "slot": 1, "values": [[3713361366251623804,5215610397924481652,3743305604543961985]], "path_len": 1}]}, + {"iota": 716, "deep": [3889282557667010008,16658559808392896075,4479839851402389919], "deep_sym": [16572265183460544525,3676808044400424795,6222338751362850681], "terminal_position": 5, "layers": [{"layer": 0, "d": 1, "position": 716, "leaf": 358, "slot": 0, "values": [[12116550705477708982,16724229524281302287,5666602575093991096]], "path_len": 7}, {"layer": 1, "d": 1, "position": 358, "leaf": 179, "slot": 0, "values": [[8804090231151493253,18093309713086984478,15588806257211634066]], "path_len": 6}, {"layer": 2, "d": 1, "position": 179, "leaf": 89, "slot": 1, "values": [[10899689051865181242,10286430280999561071,10374738856475973108]], "path_len": 5}, {"layer": 3, "d": 1, "position": 89, "leaf": 44, "slot": 1, "values": [[15859176201743508538,9966478424929065552,17216980535016118127]], "path_len": 4}, {"layer": 4, "d": 1, "position": 44, "leaf": 22, "slot": 0, "values": [[11294734992695880275,17781633920126111425,6521795554173378949]], "path_len": 3}, {"layer": 5, "d": 1, "position": 22, "leaf": 11, "slot": 0, "values": [[12562602703896107334,12802966902777675319,12362156114430019523]], "path_len": 2}, {"layer": 6, "d": 1, "position": 11, "leaf": 5, "slot": 1, "values": [[7956105874520444819,17097517896355943076,9000902050689723599]], "path_len": 1}]}, + {"iota": 1338, "deep": [12916742567028440091,13262759860843631686,6490219764970839891], "deep_sym": [4390440514850516016,11679559744418381099,6473519317075923322], "terminal_position": 10, "layers": [{"layer": 0, "d": 1, "position": 1338, "leaf": 669, "slot": 0, "values": [[12449613922291734239,18143330045847154993,11555008710041017422]], "path_len": 7}, {"layer": 1, "d": 1, "position": 669, "leaf": 334, "slot": 1, "values": [[15840889759225825365,16154054991495270570,8346715740435189744]], "path_len": 6}, {"layer": 2, "d": 1, "position": 334, "leaf": 167, "slot": 0, "values": [[15446200573881484727,493204182891896018,5570847731959122629]], "path_len": 5}, {"layer": 3, "d": 1, "position": 167, "leaf": 83, "slot": 1, "values": [[11306093278808513099,5005574932225287058,3712866700044915855]], "path_len": 4}, {"layer": 4, "d": 1, "position": 83, "leaf": 41, "slot": 1, "values": [[9898632816009731525,11460701178569989294,5910734408848020819]], "path_len": 3}, {"layer": 5, "d": 1, "position": 41, "leaf": 20, "slot": 1, "values": [[13823564168872677235,12873871450576728205,10000039197109786678]], "path_len": 2}, {"layer": 6, "d": 1, "position": 20, "leaf": 10, "slot": 0, "values": [[8675393632150954143,13193766732232908346,11058925871258175295]], "path_len": 1}]}, + {"iota": 316, "deep": [9809389796479279253,2609724777592635053,6358204434308916543], "deep_sym": [2090293030578564485,17046018365802373505,710220348793320602], "terminal_position": 2, "layers": [{"layer": 0, "d": 1, "position": 316, "leaf": 158, "slot": 0, "values": [[14340795179817377614,13720843975494266939,16347076499677306763]], "path_len": 7}, {"layer": 1, "d": 1, "position": 158, "leaf": 79, "slot": 0, "values": [[13483727959324373428,17135481261001400981,5488797648533180440]], "path_len": 6}, {"layer": 2, "d": 1, "position": 79, "leaf": 39, "slot": 1, "values": [[6265626914156287390,16951504734271861328,13384183869976926336]], "path_len": 5}, {"layer": 3, "d": 1, "position": 39, "leaf": 19, "slot": 1, "values": [[2974214004291265886,10871188085049143848,7536558763607470101]], "path_len": 4}, {"layer": 4, "d": 1, "position": 19, "leaf": 9, "slot": 1, "values": [[1709687672422415859,7196711959144469574,4032055591279181426]], "path_len": 3}, {"layer": 5, "d": 1, "position": 9, "leaf": 4, "slot": 1, "values": [[2292456446077332702,16811939064872409390,9436216556421672528]], "path_len": 2}, {"layer": 6, "d": 1, "position": 4, "leaf": 2, "slot": 0, "values": [[8757596275494264689,4441538513452859255,12082179960950577135]], "path_len": 1}]}, + {"iota": 242, "deep": [18073784541193312396,15542186237964955282,555832555083137815], "deep_sym": [14622721919178152000,11799583864430949889,14403748362265045306], "terminal_position": 1, "layers": [{"layer": 0, "d": 1, "position": 242, "leaf": 121, "slot": 0, "values": [[7347252276027670505,14027197946541438400,14584497917100139601]], "path_len": 7}, {"layer": 1, "d": 1, "position": 121, "leaf": 60, "slot": 1, "values": [[8302613772554768521,13092101283703885902,15794297093428991085]], "path_len": 6}, {"layer": 2, "d": 1, "position": 60, "leaf": 30, "slot": 0, "values": [[12473978045343060063,10453235614622229329,3311550628499108388]], "path_len": 5}, {"layer": 3, "d": 1, "position": 30, "leaf": 15, "slot": 0, "values": [[15262068716790978691,9813059787786197463,17791554441717694526]], "path_len": 4}, {"layer": 4, "d": 1, "position": 15, "leaf": 7, "slot": 1, "values": [[9382801202838763478,521562019286824498,8986832934377767675]], "path_len": 3}, {"layer": 5, "d": 1, "position": 7, "leaf": 3, "slot": 1, "values": [[8128307510882286524,10821318725892370323,13877665066000822352]], "path_len": 2}, {"layer": 6, "d": 1, "position": 3, "leaf": 1, "slot": 1, "values": [[4491081093077012764,4892525095907921320,4263981023199701481]], "path_len": 1}]}, + {"iota": 948, "deep": [12443299642485287037,11141614935052770991,3956088967515912652], "deep_sym": [11552668708672716993,7022530386125334965,11013481679675578715], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 948, "leaf": 474, "slot": 0, "values": [[3313509122448078552,6828550099476726627,16726730109080172301]], "path_len": 7}, {"layer": 1, "d": 1, "position": 474, "leaf": 237, "slot": 0, "values": [[6469422069387281069,5851964510015292197,6102876639432844707]], "path_len": 6}, {"layer": 2, "d": 1, "position": 237, "leaf": 118, "slot": 1, "values": [[8504627830733515497,3203577139713973580,17102368772612150880]], "path_len": 5}, {"layer": 3, "d": 1, "position": 118, "leaf": 59, "slot": 0, "values": [[16130295155283771788,17235604904183598091,4187471025053003634]], "path_len": 4}, {"layer": 4, "d": 1, "position": 59, "leaf": 29, "slot": 1, "values": [[3962084612706899730,4714968921900557097,709166559014599792]], "path_len": 3}, {"layer": 5, "d": 1, "position": 29, "leaf": 14, "slot": 1, "values": [[8060973025226066530,6562800565017872823,17062166073441934934]], "path_len": 2}, {"layer": 6, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[6235735872747817365,1215148853071865642,1291260655626560448]], "path_len": 1}]}, + {"iota": 955, "deep": [9748633164615405044,15200968922465510431,4328658426123658909], "deep_sym": [1010012317306963274,17571607453822902460,6536507881715918866], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 955, "leaf": 477, "slot": 1, "values": [[1238885797271121897,17869821964957541322,51871560878855088]], "path_len": 7}, {"layer": 1, "d": 1, "position": 477, "leaf": 238, "slot": 1, "values": [[11770897055806471365,12170177732376960747,365830011230340707]], "path_len": 6}, {"layer": 2, "d": 1, "position": 238, "leaf": 119, "slot": 0, "values": [[5243897450647853820,3709982785023675266,15992418649835130718]], "path_len": 5}, {"layer": 3, "d": 1, "position": 119, "leaf": 59, "slot": 1, "values": [[331311268110073093,7642796023034543418,16183959937845547380]], "path_len": 4}, {"layer": 4, "d": 1, "position": 59, "leaf": 29, "slot": 1, "values": [[3962084612706899730,4714968921900557097,709166559014599792]], "path_len": 3}, {"layer": 5, "d": 1, "position": 29, "leaf": 14, "slot": 1, "values": [[8060973025226066530,6562800565017872823,17062166073441934934]], "path_len": 2}, {"layer": 6, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[6235735872747817365,1215148853071865642,1291260655626560448]], "path_len": 1}]}, + {"iota": 425, "deep": [5220382834730846224,11698870976303831066,12172341881198518479], "deep_sym": [10761357836357599593,17991906410856106278,11940257533055125249], "terminal_position": 3, "layers": [{"layer": 0, "d": 1, "position": 425, "leaf": 212, "slot": 1, "values": [[18297288576489106507,17090009343077141516,1372897608266065964]], "path_len": 7}, {"layer": 1, "d": 1, "position": 212, "leaf": 106, "slot": 0, "values": [[13641345983585634201,12358551939098946232,12614312160614023414]], "path_len": 6}, {"layer": 2, "d": 1, "position": 106, "leaf": 53, "slot": 0, "values": [[13491220669646918991,3519918468530596688,1717073988549802829]], "path_len": 5}, {"layer": 3, "d": 1, "position": 53, "leaf": 26, "slot": 1, "values": [[12453130284583355935,1672709573116503708,6358973158760603860]], "path_len": 4}, {"layer": 4, "d": 1, "position": 26, "leaf": 13, "slot": 0, "values": [[13023521510587738275,6244946759545711611,2933419860181065892]], "path_len": 3}, {"layer": 5, "d": 1, "position": 13, "leaf": 6, "slot": 1, "values": [[8746702812610050947,17055089583592083208,10622082825270590110]], "path_len": 2}, {"layer": 6, "d": 1, "position": 6, "leaf": 3, "slot": 0, "values": [[3528697806224570571,16712965396148344850,11633688770159473649]], "path_len": 1}]}, + {"iota": 1024, "deep": [8467191801933543488,2838244879671958413,253003732133238029], "deep_sym": [8674660436931916443,2580563650129586025,4381248104485517596], "terminal_position": 8, "layers": [{"layer": 0, "d": 1, "position": 1024, "leaf": 512, "slot": 0, "values": [[552255252847067932,7290890199123029178,15598943983249374509]], "path_len": 7}, {"layer": 1, "d": 1, "position": 512, "leaf": 256, "slot": 0, "values": [[16489740470231076529,12150094973064418068,3809331140355786413]], "path_len": 6}, {"layer": 2, "d": 1, "position": 256, "leaf": 128, "slot": 0, "values": [[6692803711311527429,9433701761439709710,11396497374487473788]], "path_len": 5}, {"layer": 3, "d": 1, "position": 128, "leaf": 64, "slot": 0, "values": [[2851110493503553692,17864469541465912512,14423609184050031580]], "path_len": 4}, {"layer": 4, "d": 1, "position": 64, "leaf": 32, "slot": 0, "values": [[1878384626655112620,13903070236496566397,8873806899472591798]], "path_len": 3}, {"layer": 5, "d": 1, "position": 32, "leaf": 16, "slot": 0, "values": [[734813548736948502,6040967620827257108,7735869121954590664]], "path_len": 2}, {"layer": 6, "d": 1, "position": 16, "leaf": 8, "slot": 0, "values": [[6896385518190873683,9696907498203743737,11255624105341837683]], "path_len": 1}]}, + {"iota": 1865, "deep": [4212103210510884997,7035972176942140753,17306526335631249262], "deep_sym": [5983969155960921170,7716709446605112430,3507511199173005913], "terminal_position": 14, "layers": [{"layer": 0, "d": 1, "position": 1865, "leaf": 932, "slot": 1, "values": [[5072085790887243848,7949766786871583993,15324686885973668545]], "path_len": 7}, {"layer": 1, "d": 1, "position": 932, "leaf": 466, "slot": 0, "values": [[8531984238557370083,9886895154897844227,7323311592408787298]], "path_len": 6}, {"layer": 2, "d": 1, "position": 466, "leaf": 233, "slot": 0, "values": [[2262375630065310858,8475167051901754791,4082565711443850205]], "path_len": 5}, {"layer": 3, "d": 1, "position": 233, "leaf": 116, "slot": 1, "values": [[12487752698509265952,9985264885850011293,7003863281393010368]], "path_len": 4}, {"layer": 4, "d": 1, "position": 116, "leaf": 58, "slot": 0, "values": [[16471209112474711913,15774800313780824510,5119409826517069109]], "path_len": 3}, {"layer": 5, "d": 1, "position": 58, "leaf": 29, "slot": 0, "values": [[11193987865309744766,16476775623635910867,11903106571698127077]], "path_len": 2}, {"layer": 6, "d": 1, "position": 29, "leaf": 14, "slot": 1, "values": [[3713361366251623804,5215610397924481652,3743305604543961985]], "path_len": 1}]}, + {"iota": 520, "deep": [3002569401286487231,2687094515823569584,10351005430430287996], "deep_sym": [5657188868044694823,1228961647863731570,2179735891322572965], "terminal_position": 4, "layers": [{"layer": 0, "d": 1, "position": 520, "leaf": 260, "slot": 0, "values": [[6120686149557313247,9185726802812345144,11610026896189521524]], "path_len": 7}, {"layer": 1, "d": 1, "position": 260, "leaf": 130, "slot": 0, "values": [[17876923936705498700,16905214363602848937,13102287876002453738]], "path_len": 6}, {"layer": 2, "d": 1, "position": 130, "leaf": 65, "slot": 0, "values": [[1998095284741463700,706089076349940846,6176734019944784667]], "path_len": 5}, {"layer": 3, "d": 1, "position": 65, "leaf": 32, "slot": 1, "values": [[15588686557797525038,6358534243003599342,16114922279370500069]], "path_len": 4}, {"layer": 4, "d": 1, "position": 32, "leaf": 16, "slot": 0, "values": [[357439278952587411,537701478644505544,1654234603253554058]], "path_len": 3}, {"layer": 5, "d": 1, "position": 16, "leaf": 8, "slot": 0, "values": [[9198749334315201944,7219853247166450147,146514115183690041]], "path_len": 2}, {"layer": 6, "d": 1, "position": 8, "leaf": 4, "slot": 0, "values": [[5723305765232985900,4398833039615894329,17495563933725145151]], "path_len": 1}]}, + {"iota": 517, "deep": [14456110041334564788,13307720518286312411,14751228950079222761], "deep_sym": [13824372239230008108,620968933146506970,13902742496082168769], "terminal_position": 4, "layers": [{"layer": 0, "d": 1, "position": 517, "leaf": 258, "slot": 1, "values": [[3023625249063251227,17529527820976626583,16522680750687216278]], "path_len": 7}, {"layer": 1, "d": 1, "position": 258, "leaf": 129, "slot": 0, "values": [[3498053157125553740,17589795835218554013,18370974423581492306]], "path_len": 6}, {"layer": 2, "d": 1, "position": 129, "leaf": 64, "slot": 1, "values": [[753327007082907383,14955370069137255706,10625147434646339336]], "path_len": 5}, {"layer": 3, "d": 1, "position": 64, "leaf": 32, "slot": 0, "values": [[7989894481772335406,16901511694245139737,2822325316258093644]], "path_len": 4}, {"layer": 4, "d": 1, "position": 32, "leaf": 16, "slot": 0, "values": [[357439278952587411,537701478644505544,1654234603253554058]], "path_len": 3}, {"layer": 5, "d": 1, "position": 16, "leaf": 8, "slot": 0, "values": [[9198749334315201944,7219853247166450147,146514115183690041]], "path_len": 2}, {"layer": 6, "d": 1, "position": 8, "leaf": 4, "slot": 0, "values": [[5723305765232985900,4398833039615894329,17495563933725145151]], "path_len": 1}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_pair.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_pair.rkyv new file mode 100644 index 000000000..dadf62917 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_pair.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp.json b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp.json new file mode 100644 index 000000000..93eb41b19 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp.json @@ -0,0 +1,27 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "rpx", + "format": "dp", + "proof_rkyv": "d_proof_rpx_dp.rkyv", + "proof_rkyv_len": 8488, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 2, 2], + "fri_roots": ["b8cd71d876dd084c3fba58b6a1b1788b09ee71b58bd373a3b0c32da0c88abac4","8abd4354863193215d65af7903d5f53018e4b2076a5d160d8197c0df719c33dc","5ba58234c6bd1059d39526597538aa208c0fce55ac284c9b1abcbfc4bc87fcb7"], + "zetas": [[4735330965523630181,1034630526833404286,12017969954712239940],[7889074366333103969,4290811767201827376,14455537773263474986],[16567739822379498242,5753162788299774204,5950576806486104926],[3148119476643166323,15342831354566172589,16163821384909909157]], + "terminal_coeffs": [[12646447477222048401,12937374675136009352,16549558038379651479],[2564516689604577222,14255657332782844950,7303342851315364550],[7904019038462202246,10880807545931735486,15264205294200432227],[13482626913175767796,15717304750858041741,4892518987751974292]], + "queries_detail": [ + {"iota": 1095, "deep": [15404367171170966026,18436452028196411499,5024906168965672354], "deep_sym": [16265809848131463264,11495087467666035873,7459148639182883977], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1095, "leaf": 136, "slot": 7, "values": [[2406839404446874184,14378864393598774400,9244658727562446653],[7310068857272658177,6690242714355677003,16014750232870151724],[16918409907732086886,2943891056625643727,17633501031830476314],[12754819164990375128,11705998079299817355,11740835587370543910],[2273041869480575555,1772390107814740151,15057029391149023100],[11526866775207687538,5051892843389067913,18351320525206389605],[12623868928666452372,4025975981633944670,16736429527117892317],[3360740073930066434,11793243838384529871,16278212037669344741]], "path_len": 8}, {"layer": 1, "d": 2, "position": 136, "leaf": 34, "slot": 0, "values": [[12541072756489582885,2370820915463632688,2423396583266204055],[14905864220509753211,15950579206739519424,15082498997069827244],[9459878356316854591,1270075853426673136,4472301856924467933],[14393624207528009097,9675335233598348594,16693550050532620583]], "path_len": 6}, {"layer": 2, "d": 2, "position": 34, "leaf": 8, "slot": 2, "values": [[16818112822979374356,10274787015391993318,15763705279771580830],[7191284429859224732,4809043185931564649,14671147736651195233],[2802946927799549417,8037886970914238609,9324105614581397069],[720806501774538325,13075390526153910697,6384491353149171884]], "path_len": 4}]}, + {"iota": 1336, "deep": [9782001122439711942,9555857402003070809,5596777784231506518], "deep_sym": [2420617880218855450,11218452639813192342,8178398275584052689], "terminal_position": 10, "layers": [{"layer": 0, "d": 3, "position": 1336, "leaf": 167, "slot": 0, "values": [[2041956337797731597,4112831764093230891,11432379156394499095],[8144473764935818890,6978818592127372038,13240419817221723331],[4510585753595495978,7259665793084123234,5963491368598554412],[12449613922291734239,18143330045847154993,11555008710041017422],[13423943835517857770,6610089783383402616,14732195742916433439],[3080128605461466890,10236270832537313262,8257530437238638301],[8362553719204526827,2153485092467020843,4632353622181731171],[11789751946472833749,6000336259605467995,973022182701680872]], "path_len": 8}, {"layer": 1, "d": 2, "position": 167, "leaf": 41, "slot": 3, "values": [[1806710437233960703,865918887752352625,6749716608005253549],[4630423819292356457,16185182827820005660,8859790871695731864],[642687296268297522,15932233592480046187,1035347428796527533],[1571642650814870704,5246859957097853911,1771889554939298762]], "path_len": 6}, {"layer": 2, "d": 2, "position": 41, "leaf": 10, "slot": 1, "values": [[16605815559724387167,7600882892259287443,6994922477906460043],[14201299358752116799,5717899003132557091,9025489995620184926],[15619332420018523266,1595985739793856288,4969668978550259454],[6667880876193245735,12155122404735786091,14288219524612442946]], "path_len": 4}]}, + {"iota": 396, "deep": [1195167017398997224,3684677677763618554,1602181459315078555], "deep_sym": [17092653866053864012,10319696574527941179,65705194922387228], "terminal_position": 3, "layers": [{"layer": 0, "d": 3, "position": 396, "leaf": 49, "slot": 4, "values": [[11065792751948336436,6945821278266161605,695451384357543318],[8408178489419937465,2838485655880223095,8492969326019934396],[265145725283343233,10180163860108398826,11843491620723569992],[15123282416936963659,17530049459658255167,8917537469248528646],[739600072118952183,889941540565197657,4394371451414923712],[15901449078141666097,9754018756689853016,12057470623441911843],[16131822616193081053,4052660922016275605,6765793682960198828],[9270165410843091007,10965847749993617830,14798583832773956809]], "path_len": 8}, {"layer": 1, "d": 2, "position": 49, "leaf": 12, "slot": 1, "values": [[3895026366660990149,2600803056632231256,4321642801809818286],[12836403324608808788,172846604945358907,7659991926477877030],[90956911091737129,4468391866397480991,9108435893769413449],[15315665520481292732,6371916974269842650,7526540926329622480]], "path_len": 6}, {"layer": 2, "d": 2, "position": 12, "leaf": 3, "slot": 0, "values": [[11401453414757530315,353357581965599259,15287411324344444169],[16460794964378913634,42195047452164617,805857385603866297],[8773174365564475115,10384145495722058796,3379422144880863992],[11394111484266117948,1699290548295696723,1801926384528077999]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp.rkyv new file mode 100644 index 000000000..ce6c014db Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp_3_1_3.json b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp_3_1_3.json new file mode 100644 index 000000000..3a6256410 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp_3_1_3.json @@ -0,0 +1,27 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "rpx", + "format": "dp_3_1_3", + "proof_rkyv": "d_proof_rpx_dp_3_1_3.rkyv", + "proof_rkyv_len": 8728, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 1, 3], + "fri_roots": ["b8cd71d876dd084c3fba58b6a1b1788b09ee71b58bd373a3b0c32da0c88abac4","4e3dc7f5aa11f888402fd6360c83de194bdb5f026d1e18197efa932467225837","c697839c9a24596be7c0905e049be88be18edded924fafa042fa6c04aff41539"], + "zetas": [[4735330965523630181,1034630526833404286,12017969954712239940],[7889074366333103969,4290811767201827376,14455537773263474986],[7770302924502205803,17599204623846053939,9971594608079091649],[17586252287694023458,7259590326536071696,12289082985476495883]], + "terminal_coeffs": [[6545599878510160802,16048981941637217531,11388709074500405832],[14061425267190733018,15002089986646168363,58669014784357566],[2413176590794729875,18383285226207373965,3934831234105776640],[2976777462567838906,972764554709234443,8590380189090386920]], + "queries_detail": [ + {"iota": 1053, "deep": [10250707639143879807,8370264329266959220,3378560773770382438], "deep_sym": [9932815673506821976,2964873329768126194,1815148923933943615], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 1053, "leaf": 131, "slot": 5, "values": [[8206566942588720809,17872933398873708985,7115180618693784557],[2678263910421709698,8346434880095868658,6107710828755343595],[16155660615844807708,15139547910119346356,10456400642597445409],[6862225755362317604,8468614376293384197,4418093324826000636],[3436445875439159867,9162846014864517992,17418766279869328368],[4884572017146877890,13142482661507438973,8397595055179396044],[7812814834329037929,5936428735545781918,6769737470751296857],[5571634424827863459,16952340246853650937,3860405205017447442]], "path_len": 8}, {"layer": 1, "d": 1, "position": 131, "leaf": 65, "slot": 1, "values": [[13279332016024174713,10180249446915101956,14037762681904687442],[629567766354029343,2428986310818076928,3442626561722631918]], "path_len": 7}, {"layer": 2, "d": 3, "position": 65, "leaf": 8, "slot": 1, "values": [[16234327972113548284,7197673276934127279,8012846128365704540],[15705483126643608833,9148138369265927751,4497445583271196018],[1116871917513432828,7754822209782634050,13152119519303694369],[8099659913026938434,5657455326385259056,15838886403916165803],[8176479475952277130,2228622485820353743,2845101904427108813],[10292683523489835292,10247814894946347631,2644590758149436851],[14525911450532087857,17942252987826072121,6801450836348820530],[10671464302529786982,7450404385797873183,9037411470647583838]], "path_len": 4}]}, + {"iota": 567, "deep": [18047729633876989466,99374521245134966,3360754488684044331], "deep_sym": [17516618015903139100,2951436536334618682,17980489157745383386], "terminal_position": 4, "layers": [{"layer": 0, "d": 3, "position": 567, "leaf": 70, "slot": 7, "values": [[6245239563054698545,10953784134743681631,309117201705107312],[10147311537216613353,7920342408185388334,2703788232899065126],[3635161026520856384,6954959520249125012,13529324898537327155],[15764061797087379466,9370872654131914299,6335165267128049318],[10994028427916486458,13040415860252061487,6960268057679038591],[10815095027707255667,18279067243625263691,11050218513605064717],[6181460592069893286,7846926160755689348,13639874225392218244],[7537553748832786394,11392017698094394877,103694717963243848]], "path_len": 8}, {"layer": 1, "d": 1, "position": 70, "leaf": 35, "slot": 0, "values": [[10356989277157925581,7734667612467985055,16277606814930031695],[11515250190753871632,14360030790213387188,16280773358479821956]], "path_len": 7}, {"layer": 2, "d": 3, "position": 35, "leaf": 4, "slot": 3, "values": [[7986871652212847729,3216947561173799887,17389602311097823189],[2840685283826001252,3345163626551462189,757499744147822538],[15701632955855902379,16763676821054664820,13807560596838550641],[2742910096593592820,8353559143048342553,9954138356989099170],[9847969385057154419,12680646407006050555,18056330072996240036],[14446742634554659401,12873011047125567117,32390744878969203],[13758404333410532093,17090097282290779556,16627657403379939801],[8324004754530806517,15962588376622656722,6193420264960613083]], "path_len": 4}]}, + {"iota": 1526, "deep": [2567025427785041582,8000187002850677269,3827534577909862613], "deep_sym": [16629321827697767665,13697610440565804273,59291125760329351], "terminal_position": 11, "layers": [{"layer": 0, "d": 3, "position": 1526, "leaf": 190, "slot": 6, "values": [[18367964984216933062,5660898099715863344,12091825699828272785],[4064797189050673755,8758312521479053195,17586433145799905667],[11042109590396741557,13453998188520539681,4026691058011471979],[6920737729535120217,5791562277528914497,1738885481870857403],[17162616217057319708,14564053023082606491,7572870302558855093],[12956855366818227103,17813714031214072642,12230382703550138851],[757677278244590014,1785069472838724489,3241296500485712017],[3086114952743418053,15349864503276037083,11277820017340792738]], "path_len": 8}, {"layer": 1, "d": 1, "position": 190, "leaf": 95, "slot": 0, "values": [[10524734310662209668,18323881505728001248,6372778122146690452],[1004243276605533431,3591193302962306504,1633219252869578500]], "path_len": 7}, {"layer": 2, "d": 3, "position": 95, "leaf": 11, "slot": 7, "values": [[5240087236059509009,1172869790317804758,5089515052485497818],[10355844810202830688,17925465335816866521,8351483564904242640],[4079809391339080498,15892637900298544667,7778229385415383189],[17801563449072120160,11734094917552760905,11750172894724870474],[16550070760801625399,11956680567406663251,11737023057057024488],[440034897161641969,8320818327047863761,5881652704612506137],[8980928091881739289,807661344907923994,2585567908456372771],[13123983852650476319,5128554601153007764,13742590666612112409]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp_3_1_3.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp_3_1_3.rkyv new file mode 100644 index 000000000..e57464d91 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp_3_1_3.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_pair.json b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_pair.json new file mode 100644 index 000000000..af3751548 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_pair.json @@ -0,0 +1,27 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "rpx", + "format": "pair", + "proof_rkyv": "d_proof_rpx_pair.rkyv", + "proof_rkyv_len": 11136, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "legacy_encoding": true, + "total_folds": 8, + "terminal_len": 16, + "schedule": [1, 1, 1, 1, 1, 1, 1], + "fri_roots": ["a5cb9815a33628e2d9aee1104d2210e8066854dd38976a0143c7b0638504f9f3","5af88af0782998624ef05869a90b8e35e82a85d7da5c899ce3a0d8d1872a1ab0","97ae9eedee1a1b29b7eb30478eb3b8f118b84f3bd43e6352fbdac68314723730","2f2da5b7622cc6eb49677363305e2626fd79387dc9acaae9202b655cb84f1f8a","9fef7049e45b8f097dee76ba5feadc10268bb51f63e9336dfafc3ac54e3e3e38","c261958da4b13c4ce5dcf9cb19d42cf09808cb932dea3b9e69bdb7577f308167","2aa133f65dd602f13ab5b2961d2b96cc3f6daf6f106e9783939c69716b9d2ab9"], + "zetas": [[4735330965523630181,1034630526833404286,12017969954712239940],[16665743570319646148,16897879252278531211,10291861723093761662],[10619368815145924427,1493089516910409884,14431758427697423319],[9552622148858278301,6488516694070886107,4893272118711353122],[2783515638190829017,9945112524553572548,15631202162117197821],[17815262798501899446,18394714429174366312,8636369618480887460],[11856812424473920575,10766055906630249609,2957922871886357218],[8171264462115707646,14626134561370321125,16568024845886241762]], + "terminal_coeffs": [[3714161951696662500,2595793324825979078,3565379477475041685],[4424026265791135346,6558459514194683116,753777937513084834],[7727091673734829526,10561609288187203284,15868472042909273283],[7864830287677944053,6520068215425390864,4795411284951093801]], + "queries_detail": [ + {"iota": 907, "deep": [3040718383397280274,11956941222209451830,13732184933327600162], "deep_sym": [14770993481378414249,10902020514396191223,13023915065759495237], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 907, "leaf": 453, "slot": 1, "values": [[10738449088863093738,17001367698800175667,872083450534898496]], "path_len": 10}, {"layer": 1, "d": 1, "position": 453, "leaf": 226, "slot": 1, "values": [[8949331791643690447,11945875389884147400,4863937416666523377]], "path_len": 9}, {"layer": 2, "d": 1, "position": 226, "leaf": 113, "slot": 0, "values": [[6531312029425646452,8614572489917572820,5793377902239563396]], "path_len": 8}, {"layer": 3, "d": 1, "position": 113, "leaf": 56, "slot": 1, "values": [[13245888542464671401,9490131273601039377,5473879260557931677]], "path_len": 7}, {"layer": 4, "d": 1, "position": 56, "leaf": 28, "slot": 0, "values": [[967364531071989975,2293195037664380504,8237422110493151214]], "path_len": 6}, {"layer": 5, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[3068323341190833160,2495604132745403317,440675983381132721]], "path_len": 5}, {"layer": 6, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[6235735872747817365,1215148853071865642,1291260655626560448]], "path_len": 4}]}, + {"iota": 327, "deep": [6648971487643812783,3504019026315364339,5553986533175031714], "deep_sym": [4847162544338375188,3760822623165876653,18385909206353757824], "terminal_position": 2, "layers": [{"layer": 0, "d": 1, "position": 327, "leaf": 163, "slot": 1, "values": [[3102706233933768041,2061658593148860966,2937765886239263664]], "path_len": 10}, {"layer": 1, "d": 1, "position": 163, "leaf": 81, "slot": 1, "values": [[5351203088071617904,5190285090595573022,679443460868787999]], "path_len": 9}, {"layer": 2, "d": 1, "position": 81, "leaf": 40, "slot": 1, "values": [[6813072834546256573,17560727769766864490,11355003854934910309]], "path_len": 8}, {"layer": 3, "d": 1, "position": 40, "leaf": 20, "slot": 0, "values": [[4276057153471798645,16589259977369031850,10213981092871167197]], "path_len": 7}, {"layer": 4, "d": 1, "position": 20, "leaf": 10, "slot": 0, "values": [[6724049492496023179,14516868345363052182,5463760551208548401]], "path_len": 6}, {"layer": 5, "d": 1, "position": 10, "leaf": 5, "slot": 0, "values": [[872892273747685357,17097460053185137664,7011811299561387579]], "path_len": 5}, {"layer": 6, "d": 1, "position": 5, "leaf": 2, "slot": 1, "values": [[6972245892337738186,7308248010937532343,15369312713036268331]], "path_len": 4}]}, + {"iota": 1055, "deep": [817615283715329005,16733328116567315164,13240493922581948560], "deep_sym": [1731008489953378402,3915464684156498806,11643985226885705726], "terminal_position": 8, "layers": [{"layer": 0, "d": 1, "position": 1055, "leaf": 527, "slot": 1, "values": [[7812814834329037929,5936428735545781918,6769737470751296857]], "path_len": 10}, {"layer": 1, "d": 1, "position": 527, "leaf": 263, "slot": 1, "values": [[17350970259606761374,14937434176606905201,2436331477479557398]], "path_len": 9}, {"layer": 2, "d": 1, "position": 263, "leaf": 131, "slot": 1, "values": [[11807655768007569086,16744789612728260734,4620195842063357801]], "path_len": 8}, {"layer": 3, "d": 1, "position": 131, "leaf": 65, "slot": 1, "values": [[13354553806338021504,6368498475783127995,1444223788932639250]], "path_len": 7}, {"layer": 4, "d": 1, "position": 65, "leaf": 32, "slot": 1, "values": [[2595125021894609724,3173778131451323870,2008467966233623536]], "path_len": 6}, {"layer": 5, "d": 1, "position": 32, "leaf": 16, "slot": 0, "values": [[734813548736948502,6040967620827257108,7735869121954590664]], "path_len": 5}, {"layer": 6, "d": 1, "position": 16, "leaf": 8, "slot": 0, "values": [[6896385518190873683,9696907498203743737,11255624105341837683]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_pair.rkyv b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_pair.rkyv new file mode 100644 index 000000000..1bd3d5358 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/d_proof_rpx_pair.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/e_leaf_digests_blake3.json b/crypto/stark/tests/vectors/zf_fri/e_leaf_digests_blake3.json new file mode 100644 index 000000000..da5dd975f --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/e_leaf_digests_blake3.json @@ -0,0 +1,11 @@ +{ + "generator": "stark::fri::vectors::one_row_leaf_digests_json", + "hash": "blake3", + "rows": 16, + "base_columns": [[14950669930584181769,11380843527670038249,14170810701887864585,12657459883858543732,11080332778200492175,4152169804438290561,12191777403133591725,10801486430904554830,4417364748388562854,4379087181347593436,2580857809684985080,7673975303685132775,13322131507302669334,9040154351058314042,14264724532560863887,13962096292449051454],[18243414257841422358,1304221489434139653,4524329391722882702,18400865441867651612,8077364911250023428,594594441077591684,2534844611324517698,1969697784587826307,13838274770865440410,17810497879044384847,17948609656845876769,2245783734148948709,3359004654947870420,7611350254961757910,2256523342594777630,5184348059537790602],[3333165186168681317,10992969103574531539,10875599246434246438,4202797048359915902,13708589652114080127,8862588509040537726,5062794908899136299,16177654723492523013,782810894950674176,16085627345094361018,11968464090099871210,10878072172278744852,2776239942643392900,1706434847502238813,1553514265852765581,10755221291880268160],[15185130643288894846,2969650219458132482,10730508005208922807,6538486738868699860,13637771772236929810,2559123444577356896,18126217652353331113,5721278983068996567,9421049141588406289,3056349041078578205,1927015489752515349,16193479730068331852,7962887402148557259,18122082188664764562,4432334024656166286,14439109430197722085],[12869403534454369847,444100742738500988,5149525751578300798,16074201275155691844,18094321804223741766,9704695991314911754,11529325637956947874,5462031299392823211,12784861903617806249,15621907776666625844,6514538806212006718,16075501809475733688,11901509892253068338,3954885611778170505,14288373624468033718,5293929189132021115]], + "ext_columns": [[[207727902132756252,11173563745377630981,7306909256194215961],[3383316449733693245,14112212308402603625,16675907919222413400],[11091225657268605779,2260900423939991720,9458175385801186643],[6403564405749070118,14462018993348769223,1663236480835094319],[10760925658949673415,6256953096125034850,6374165608116273133],[7525097355787171930,8065360669614765403,15671833331641072930],[8387767360670315620,12721973472388740613,12037449270884550397],[12780009495799128023,18254530395830801598,17124580712984908689],[7170091433976859457,2918423366040466885,12162269374600581905],[16638539687531051970,2633731385302464777,9274096096546535786],[15186392598723191854,4370449889476143518,10080202853130152767],[10247876770105988134,17464801317635882529,17998396633050378591],[13977131749530808397,7521738060361358462,1158021110493825475],[17395948259724017503,18208524233454958027,9357130278945496078],[12514637307887469569,8173084814001755783,16874068347906640087],[13076889950263576212,9681825774613785687,14728844907461535493]],[[1703295679615235702,13608405329556208281,13586959445987754067],[13509429031623984718,13028166630131220703,12842497139504455345],[12483125829912424503,359627891118073558,14115869743926542122],[206993782868978585,1945608048083412892,16924920981352735495],[10460072476710726221,6746467623189781681,718200883831581176],[17283619850490311477,15509599890076648547,9393695392791290257],[14045616078604790859,3033230427237039184,13069887780656089759],[12119710066391128062,9603138251095584760,12526281507633415864],[11646594443330237117,10431672579314833108,2414794606147947405],[7226819219904749,13787705273014176717,2174618065661578531],[11184428261634409861,17041311285406150036,9151670609840952406],[4504922362140931424,3374239225141597561,8705297669257511518],[12618315602974981246,12028487155674968288,14243761148199342949],[11397300516573109633,2515281435755958282,9596751895950036808],[1977998875513216148,3757846768502258754,9275101095842896217],[12145146597496230120,1463483070281991503,5369856519452762238]]], + "layouts": [ + {"layout": "row", "rows_per_leaf": 1, "base_leaves": ["c2ae1f207f4add26960a874712a5161f3e7a0a8a02b2fd6b613b97b4a0fc8c24","3f627fcbdd872e6d88c03d9f395575fa3b3eea394010620ff65d9722689a598e","bb3ed2b53c1f771e3bd9c5acddf0b1c8755b058e41d9b649e8ff0021d366dd1c","808da1a70572374969157c4afda12b3f52583dd427dafc3741b4d9ba07eece1b","d01b246953e878de6a9b410436ebf1e76ee3b22e1c03524c587dae6687b0170f","15af76f38031f18d8b1c8361710081971d319539f9bbd471451b83b88d2fff1f","a1ec624c309ec5cbc49834b1564c4682c3bc9dc541145eff3b86894b72efe181","a31686ba3cc471d239920544d9d0a0530aa44307dcea70f7153305fc89f92a12","f0249e04f045a3e8532fd6fed462d1b3fcf83d3701a44b91abbb44642f1ae604","ecb609906aed5a3016b0a9387d37a4e62030c174507ca681c26eaa62fce2d186","cb46989ec3547d9848b76ac33ba51159d15c5419932deb3cf42d8ea3e8bffb3c","804bbf310e4150eab3986958be7207e67c99883e0334da005535cd8cd7e12734","02c125aa450ca69c2af813165a1fecf7d2be257d03e07004be0007feb7709a6b","9d3756f8ecb33d589f6f47ec016b3b84f5c306d320fba9dc05ea3884800c4974","2458aff19dd89b72a29ec102231dbf0cdab82928a9b35ac70a6642d253292429","591ef32c4b694c118ae2515d4a4c6bc0130e337be7262235fc7e8909a1265de4"], "base_root": "b460868e5b8e9f7e1bcc9d9761ba1f985ff2ab56d2b86d938eafef633ef0736a", "ext_leaves": ["5cb7148f5b3bd7a78c4e7f145222de7f889406c0ba7e472760fcbbb527c953c1","055ba11f6b9a6b6a8f87f6d9e114e1b8568c6c806372f25961b10a7b61f2623a","d058badb4b77e7678d127b38fab95f08b6c9a69e91457598a7befaeab56f45d7","10ea36f5886e3bbbb44dd85e688a3d4df098e9855824191429beeed571c3ecc3","8f82f9e303ce6654b137e7c4eea8923c345eb60f829ea086fbdb448d0f895f8a","224fbba90b02b3f4ba70f6b525fe0e125496f29b1bd1219fa6389b01d76eb0ab","a80cc0208b85255bb3fc7db907f7c099717c8359cc6d7447507415b26d925ccf","ba058dbd45ff6a0c56d6863f0db9ee3027f614ab272f29af577144d432ae9034","0c5f1927bbf3ebb9de0d8f7d86930e7a9ea113463d9857069a1900835ab24b8a","8a9c5cfe653b949eddb01b2a3aef15e60c7f4a3305721dd399007ca53cbf2a39","4ccfa171a65d37af70f753f8890a89b71697b0a4c7e12dfe629639579674b47a","48add12b795515f30db197468f3e933158f3da39442c92e1314e16840eb5d886","1ba6c68722fb7b8d37a119c6f89142e284bb1a64ceedff9f93e3563a799721e4","c06fda11a91ced90b28bee4c98ae0d17986eba3e06aacd3404014fc763963d26","09670b114cce12e38257d0d91ecf952f43b89531cf2465e4ada56974a1294646","eed259595561389703fafee890c5efa3fd55f12a5f6d7e56f840595afda51652"], "ext_root": "624475b85934f5abe03978164a25aa1a14c518fcd393ed0fc11f407b956ea4ad"}, + {"layout": "row_pair", "rows_per_leaf": 2, "base_leaves": ["9f7e2bcfc6268ad6b644aa70a6dd99fbceb797191a821d5e3e6b3ab62ca41c02","bf01cfebfb776782174a75e80826318dad32a6480378f84747861930f066e585","5f2a6e2e9899df9318108f0a23ed4f71fe0655479c19ee044e725c621d1e5782","fce457696f147a49d959e7031fbc58febd75380bc25f10c53f574b92f171fb38","01bf0eeb8d9551c210e0a09fd46312785ba0391a41c568ad21b76f55ce051d3d","0ebe04379d102820eeace432f6195c2d1110318cacf8e93ce57de2b8677220ac","0c036dff3607011f632ac774c4df64387cd28f88797c1ea9e9473e814d9d4c21","966ff57fe712ae13733cd13031a88e692b70ba8a5f897d25c39f060aefc87f53"], "base_root": "8bb46242b852482ed7068636fcee4b72b98aa1bed07ab497f772872f4f3a7daf", "ext_leaves": ["e413ef1157c371bea24606d9acff1976e6a84f7161a435010a774fa80ca62424","7aa5945d700777d3764a218cfb5c9f74e8268694e3bad8707f25ccb740c4a03e","213996c3e0303bffb30e1d3f7849cfec2ee2c04cae38f598c6ad4e8f31a54961","4b75d22f0b7abd02ccb5038f19c60e265e1029f8ea0dd577b4b3014f96c60d72","533e2b13491d03acdc84546116e5d8ff773c072682c2b86f360b4ab6c7874176","3b76337a827ab8a20edcf5770025783e0e30a47dec1595cf4fadc7811e37caf0","fb0d3a529500753ec91fadf14f843a828ad56531e1935147e385b0e96c3e2f42","95e6d95c63db1bcd7a6df701931f28e13916b9451908a29db4d7837508e611f9"], "ext_root": "a7a60a908813bc33dc23a595b9f9a3e8c476f4fce108b35caad8e3c730e7fc31"} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/e_leaf_digests_keccak.json b/crypto/stark/tests/vectors/zf_fri/e_leaf_digests_keccak.json new file mode 100644 index 000000000..d7a4b101f --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/e_leaf_digests_keccak.json @@ -0,0 +1,11 @@ +{ + "generator": "stark::fri::vectors::one_row_leaf_digests_json", + "hash": "keccak", + "rows": 16, + "base_columns": [[14950669930584181769,11380843527670038249,14170810701887864585,12657459883858543732,11080332778200492175,4152169804438290561,12191777403133591725,10801486430904554830,4417364748388562854,4379087181347593436,2580857809684985080,7673975303685132775,13322131507302669334,9040154351058314042,14264724532560863887,13962096292449051454],[18243414257841422358,1304221489434139653,4524329391722882702,18400865441867651612,8077364911250023428,594594441077591684,2534844611324517698,1969697784587826307,13838274770865440410,17810497879044384847,17948609656845876769,2245783734148948709,3359004654947870420,7611350254961757910,2256523342594777630,5184348059537790602],[3333165186168681317,10992969103574531539,10875599246434246438,4202797048359915902,13708589652114080127,8862588509040537726,5062794908899136299,16177654723492523013,782810894950674176,16085627345094361018,11968464090099871210,10878072172278744852,2776239942643392900,1706434847502238813,1553514265852765581,10755221291880268160],[15185130643288894846,2969650219458132482,10730508005208922807,6538486738868699860,13637771772236929810,2559123444577356896,18126217652353331113,5721278983068996567,9421049141588406289,3056349041078578205,1927015489752515349,16193479730068331852,7962887402148557259,18122082188664764562,4432334024656166286,14439109430197722085],[12869403534454369847,444100742738500988,5149525751578300798,16074201275155691844,18094321804223741766,9704695991314911754,11529325637956947874,5462031299392823211,12784861903617806249,15621907776666625844,6514538806212006718,16075501809475733688,11901509892253068338,3954885611778170505,14288373624468033718,5293929189132021115]], + "ext_columns": [[[207727902132756252,11173563745377630981,7306909256194215961],[3383316449733693245,14112212308402603625,16675907919222413400],[11091225657268605779,2260900423939991720,9458175385801186643],[6403564405749070118,14462018993348769223,1663236480835094319],[10760925658949673415,6256953096125034850,6374165608116273133],[7525097355787171930,8065360669614765403,15671833331641072930],[8387767360670315620,12721973472388740613,12037449270884550397],[12780009495799128023,18254530395830801598,17124580712984908689],[7170091433976859457,2918423366040466885,12162269374600581905],[16638539687531051970,2633731385302464777,9274096096546535786],[15186392598723191854,4370449889476143518,10080202853130152767],[10247876770105988134,17464801317635882529,17998396633050378591],[13977131749530808397,7521738060361358462,1158021110493825475],[17395948259724017503,18208524233454958027,9357130278945496078],[12514637307887469569,8173084814001755783,16874068347906640087],[13076889950263576212,9681825774613785687,14728844907461535493]],[[1703295679615235702,13608405329556208281,13586959445987754067],[13509429031623984718,13028166630131220703,12842497139504455345],[12483125829912424503,359627891118073558,14115869743926542122],[206993782868978585,1945608048083412892,16924920981352735495],[10460072476710726221,6746467623189781681,718200883831581176],[17283619850490311477,15509599890076648547,9393695392791290257],[14045616078604790859,3033230427237039184,13069887780656089759],[12119710066391128062,9603138251095584760,12526281507633415864],[11646594443330237117,10431672579314833108,2414794606147947405],[7226819219904749,13787705273014176717,2174618065661578531],[11184428261634409861,17041311285406150036,9151670609840952406],[4504922362140931424,3374239225141597561,8705297669257511518],[12618315602974981246,12028487155674968288,14243761148199342949],[11397300516573109633,2515281435755958282,9596751895950036808],[1977998875513216148,3757846768502258754,9275101095842896217],[12145146597496230120,1463483070281991503,5369856519452762238]]], + "layouts": [ + {"layout": "row", "rows_per_leaf": 1, "base_leaves": ["b145d5dfcfba3e9fbc6fc0f2b22f1f09cface989a60271c974c7527775d70966","d174efcf6b40ca002c952a477dd2dd314560bf6a67f399383d25896fea933476","f3c366529bba2c38ca7ae96915d677b148fd17e4e8384fcedd0b1b9b8fc00669","d62a245394266645006b118ea9512ef37a8b5ee505b261b700282d01d705479d","c7ca499150cab681ff24b6c26b5a581c88c81e80fcbddd96ed610c4e6efca518","18f9a71059056718abe5ce1c90837753e3fa546b3b902d6a4926e474b8606a51","e5e7234ad258993e0afd4ae7b3e65825bcc87bef7136545e398b20407da7df78","4f8c5ea8ceb6d90b8cfdcafd2cc7a03ef6a67ff3cb14a3a1aa926aeb75e42c95","02cd5ceb9d233c6737c77ff6cb74102447cf6d3bbfd55b1891a8bcef4a362823","8dc989a34870bc7dbe1e590062db5a176ee4aa8c0ac1d33f864cbe68df135c9f","3be46427672143f60911dc082387e7c47a9e7b0b96447c74d949e6ecd24dd71d","b921b40b675814f03cfc6e4b6c432a0d71595341774fc9201c87e11d821b3be7","5a2ebfc42f679e888bcc9c841d4aaebeeed53d01dab0c4edc461579b1d9e9690","f08ffa382d4ee6de4b2b0d1d44febc30d214c2a60ff801e462ca64f324142b18","5a7540037162d28a86465459c79a644708307875c7c2932855dfd23729447fd1","185933343be20010c846441b1a7a195f3486e35572294e35ce86793d43ed097b"], "base_root": "3c3af9866450b1fc3d15b65113f318ecf11cd7edff613bf7bcfbca08fec9a4a9", "ext_leaves": ["148a2ad5111bf100ce8db96c8fe59d5b92f379a1db44af511db1146957e9f371","c0a40f4420e1e64c87dad9812ff3178934daccfb3d918606e8c84a33c82904f8","f274452120f914f4e92ae73d567e4a57d5eeea968fd544166e98b20de91c6a7a","9fa6d9e3a8bec3c2c55df2883be794c76ba185a1986a1103e9f66cc79840a296","e3aca190939b9c1df8aec098f803fc9b6d2047d4c1f7c6dca7c6c3dba1aab124","61afea46b802e6e048a3a5a9b6b3fb631bb24871ce510fb411a4c60d6cd214c3","7c40f52149bb6e91631efcc3b719a55ecb234bcf1401e5f4ba3974db13dde47b","46a994cf8c9d16d65eb5d200157b3271019a1d7673b630ed6efd86e7ae1b256e","ba7b579ba82456a7be452a5c0003a10542ffd41e27c3e13ac1d0ab1ff18968bc","1e2dcbcfa40a77880dfe33836b0627a5654d4ccca4972deb4919a760406d4e62","9b2b4f60a42eb415ae22044c5ec829e4ef3d11860e8001cf656733ee863e2988","ac706956eb2e8e10329cac607f0fd73dacbe265adf48f37a3869fcdc86df0659","6b9deb036a2ff21a5cffea8094a086aac4d1d8d49df630bc90ea452348a63847","9195181ef2a3ead35296e1accf80c54586517b0001e3419cdfc5761d4ca3d0db","e03605e10a09a8cc5c6c2c08f2dad3a9d10f3e636b203c67d1cc863692de183a","4335332d20d3ef2a29aab8786890d3f73f218dc8ff00788316b4c60e4474f0ec"], "ext_root": "eec6dc06c211f42f6f29af14bbc2ad32e51d4ff0c679d6dd47a53682310ccb5e"}, + {"layout": "row_pair", "rows_per_leaf": 2, "base_leaves": ["a36317970aaf00a83fe6135cfe78616cf49bd3722b64b69b708c5f2d6abf88ab","45865e8afc7ac83bbf0735c2ce56982e5af254ed5259e92588bc51c4cc4b3cdf","07ed9afd31cc009980d8b13bae81eb47bc3da4e385f2c27ec1fa767b25e8afbb","2be6e6fb26e8d9d1673ad2ca4a6ceaa750fe2e18a15f30009d796cac2303fe20","29cdb49d1e49b05bfa2cd22da89b5797227600d68009ecaa30d82ff93e61a996","f398e56c618e09676a414306a23c602ef6017aa0a3d3fc12f35c8323f6ea1f73","2becd8729d468c25db0dce34bb5646b5bdd23d63fca063bb3edfe330bfc20dba","19a0abb354cfbcd2dc44abb85aefa038969b9286e3699b5a1ff90bb5d1d6e120"], "base_root": "d147ee4422cef57eb47727eafd4d78486626c66279439806f708264f4be74534", "ext_leaves": ["e8efb646b7299d1862c3ebdfc2819842509758766679b687b01ee66aec2c5b37","3038c9b8d756d3c7517c535ed23821e40a2e5615378c9025a5f5fc15acf98d9f","8b89774cf9b005e98093d0a75735b7751787812f0a2cde4d649636fcd529dc81","f1a5c88fe103293b22937693d160520e6499db51725d8bdf133975067f9781ab","ab33959ac5998722fb8f51d00727592c409eabcb2db5280b2e7a3861ec2fecc6","79dd017b1547ea8140601a45064c6f3f2e485ac4966bee5a03fba69f82f4216a","af6d4e37be309fd771da76b81ecb6ce3dd1a5bd25dc051fef98aabb9dc649f35","3811a544683432e9cbf78da718ddc0e09608b81fb9ea1e44666dc9a38593a2be"], "ext_root": "90c35a8d73cbccd81153f8086d835f7a6521fee7f66800a48fb4d63495a4b5bf"} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/e_leaf_digests_rpx.json b/crypto/stark/tests/vectors/zf_fri/e_leaf_digests_rpx.json new file mode 100644 index 000000000..0e1709d4d --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/e_leaf_digests_rpx.json @@ -0,0 +1,11 @@ +{ + "generator": "stark::fri::vectors::one_row_leaf_digests_json", + "hash": "rpx", + "rows": 16, + "base_columns": [[14950669930584181769,11380843527670038249,14170810701887864585,12657459883858543732,11080332778200492175,4152169804438290561,12191777403133591725,10801486430904554830,4417364748388562854,4379087181347593436,2580857809684985080,7673975303685132775,13322131507302669334,9040154351058314042,14264724532560863887,13962096292449051454],[18243414257841422358,1304221489434139653,4524329391722882702,18400865441867651612,8077364911250023428,594594441077591684,2534844611324517698,1969697784587826307,13838274770865440410,17810497879044384847,17948609656845876769,2245783734148948709,3359004654947870420,7611350254961757910,2256523342594777630,5184348059537790602],[3333165186168681317,10992969103574531539,10875599246434246438,4202797048359915902,13708589652114080127,8862588509040537726,5062794908899136299,16177654723492523013,782810894950674176,16085627345094361018,11968464090099871210,10878072172278744852,2776239942643392900,1706434847502238813,1553514265852765581,10755221291880268160],[15185130643288894846,2969650219458132482,10730508005208922807,6538486738868699860,13637771772236929810,2559123444577356896,18126217652353331113,5721278983068996567,9421049141588406289,3056349041078578205,1927015489752515349,16193479730068331852,7962887402148557259,18122082188664764562,4432334024656166286,14439109430197722085],[12869403534454369847,444100742738500988,5149525751578300798,16074201275155691844,18094321804223741766,9704695991314911754,11529325637956947874,5462031299392823211,12784861903617806249,15621907776666625844,6514538806212006718,16075501809475733688,11901509892253068338,3954885611778170505,14288373624468033718,5293929189132021115]], + "ext_columns": [[[207727902132756252,11173563745377630981,7306909256194215961],[3383316449733693245,14112212308402603625,16675907919222413400],[11091225657268605779,2260900423939991720,9458175385801186643],[6403564405749070118,14462018993348769223,1663236480835094319],[10760925658949673415,6256953096125034850,6374165608116273133],[7525097355787171930,8065360669614765403,15671833331641072930],[8387767360670315620,12721973472388740613,12037449270884550397],[12780009495799128023,18254530395830801598,17124580712984908689],[7170091433976859457,2918423366040466885,12162269374600581905],[16638539687531051970,2633731385302464777,9274096096546535786],[15186392598723191854,4370449889476143518,10080202853130152767],[10247876770105988134,17464801317635882529,17998396633050378591],[13977131749530808397,7521738060361358462,1158021110493825475],[17395948259724017503,18208524233454958027,9357130278945496078],[12514637307887469569,8173084814001755783,16874068347906640087],[13076889950263576212,9681825774613785687,14728844907461535493]],[[1703295679615235702,13608405329556208281,13586959445987754067],[13509429031623984718,13028166630131220703,12842497139504455345],[12483125829912424503,359627891118073558,14115869743926542122],[206993782868978585,1945608048083412892,16924920981352735495],[10460072476710726221,6746467623189781681,718200883831581176],[17283619850490311477,15509599890076648547,9393695392791290257],[14045616078604790859,3033230427237039184,13069887780656089759],[12119710066391128062,9603138251095584760,12526281507633415864],[11646594443330237117,10431672579314833108,2414794606147947405],[7226819219904749,13787705273014176717,2174618065661578531],[11184428261634409861,17041311285406150036,9151670609840952406],[4504922362140931424,3374239225141597561,8705297669257511518],[12618315602974981246,12028487155674968288,14243761148199342949],[11397300516573109633,2515281435755958282,9596751895950036808],[1977998875513216148,3757846768502258754,9275101095842896217],[12145146597496230120,1463483070281991503,5369856519452762238]]], + "layouts": [ + {"layout": "row", "rows_per_leaf": 1, "base_leaves": ["7a656aa379ece6ab0d5023d5fcd1c5017daaa9516e633411dccf7ee7866c6af0","232b79335b3332a541edf79e0df1cec14debacb48c087cde110e0c5a51cf8651","479953e1ea3119218d15dbf0e79b26f6a877763d3010b0898519cf62ff281d67","ea4f255dfe92205d9e211022f0427898debdc7d6f26c765d52af22a36c719e62","df4ca202bd7a3ac32af9b915695123eb863377f28c3030f64059913358266b4a","9680d16e917410c10be2fbf05c40e8321bc2bb94edf7feb9d6445b3fb77e7fb3","8f586b1d423e031a3361f5ef3e5228d17bbce14ce417fe384e4bd9a87532db18","74c1164da23920abdae2e977117a40f897b8fcfb194e65af9043c67428d0190e","e083f6db031e946a2bacb0e144955efe90fa1753b9c4e51b7cb5cb6d78ba979a","a95e210e54add5c9bd71881fec66ccecd55a5e1a62c77c63ab19c87c9141e65f","e96fb82d9895224c11bba9a827e202218a1a6e057c41c2bd77e67a11fde061ce","601a1ceb3e882454002ff63c661ca542180888d618c3e6c2fa97697d7b9b1f80","8e45674da156a10920975d5b5d37e8a5ab3e4dbfeb21655ccfb88c48ff7ffae3","8bd8e04ab8499789bf74604dc68a89e629fa4b243f2d570d0d8ddb146c54fceb","db1d1debba5659edb8b922fbd6632df4c0b54811fd25b78bdcc68d1322289db6","fb688db3bb190dfb5e6f1f8d62ba967ad8feb333940b910e117010dc7252eb29"], "base_root": "650703b5195d641807f1115b2941afc8e6badd197b4720f7e5cc726f80f0e94b", "ext_leaves": ["e419504fcdaac941688c9b129a10ef93e071239523bfa6c5a15bb7cddd89fdd7","6e7c6e1dc231c3071d4bcfa74715fd4d15050c985255964ccc9d7ca89700c975","789bf008db122ef16929cbff7f511cecf808a7fc4845e748544362d4d83270ec","091ecc0d98d7284afc23ee5a52e0bd539be406a5e46ac80120e2d13a39aba975","79c18d4b78159536942793f03b88fbd0f93ec7842705a0d3bdeae4bdc43fbdfa","ecdba206bca9c613692ac36df568afd4af0dddbdc9742749eeab253fb130a836","208c517175582dae45602ed704f994d80a2cc6abbd60a1a9f726d82f2c73088a","5a89d4ac4fd8d53f9f1393c60fa534d80d86a41a07236fe31e34b1c68dbc0e6e","cd8f955ad87fb3e020db7bd504e83e85cc191c07decc1870326130fa581a6a35","ecf26ee0dd73b146ca161c832bc372dbc141e5e25b5d2061bbfc14240603bf99","4fa40c537344477c2efeecb2ae31e6a7a5f99db28d225f5b7dee4fcec5f17ee9","b172009224988abff945032a0662c4d9e8e238162f44b5a320cacb7097b0436c","fc0da71090b7ee174679277c91bd812dd9c397dbbb712ac208c439414ada87d3","85e8820f2db905bc31195a9b7ed4a1df71c480b3551645e0416fa4d9c73170ec","4cfaacbdae71da58fe2e60a023c725fab89d4a6964181f7188502c7f7f0169e6","d17030d4f63c802ce32188cbfaed596a3e94c01524da10afaaab38f022618444"], "ext_root": "469ec5bc2e06bd3ad746e058f0f735191581b91b5797082ebdcc520fd32cfece"}, + {"layout": "row_pair", "rows_per_leaf": 2, "base_leaves": ["f379cb35b3ab7aa83b17d05c7d78222697c27af406322bca15d11095f7318624","098fa8a23134de8b9d29b5aa7549f82226a280ba82fc5018c00df22ea2b19a87","5542d40fd569abae399fe4bafe1136422ad298740727a4e34bfaeebfa71aa6a4","d22ff0692ced201a9b9a5a27d7145b9bd37681b73e2c3052e4209c11f9650f88","9bf40a6d5e9eadd8bceee2b929ef60ee6b308b7104dcac4a98d33c5543839e19","c23ad117cd81006a27ffd1106761d2362c43ce5f7f59dd3bf94e26c2f8c36b6b","50ddaa72336de98afa901d94e8f018ffe47d9324bc657504a5a108d233e3d081","c69ad71b949c67c8f904107de5f2f0e5ed17f6eb963f5e271675cbc86b81013a"], "base_root": "5672122216f801a68d91b66ce4ed4116409df94443816318e495ed50d8ca5c9b", "ext_leaves": ["75f0801e118d4e5da1ee9fb58d326c8b8d8bfa5c7bc8e46847b4604c6e06a70d","bd71b862db923fd053d5f0919533edf3d988c080028df66b5d00d62227aa9833","68714ecd51fa4ceec480c49176202da940b6edf3ddb9cc0233ac39e0218de228","0fd68f5c137f1eff95fa07414cacc2a55a8de82f93aa07374f33c6c79b030403","e74ab3f854ffcd180d98c2abe8f295cd41b16c977d59b1118ee7757173bebbd9","e2cc8e459b6afa6c681abd691f9b458ad49bd599220aa645e4ae0dbb0e9fc79d","6b0465b2b2058547912be3efc8a7fda4154b32d1c1416ed6117c99f9cef23aaa","41708a29c8127bb725993061c29acdca304ad24618b18260cba34c2e42d7ee42"], "ext_root": "239707df45a54d232d9a70936d777527fd096305a0871e83292c5f3b2b5101bc"} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_3_2_1_2.json b/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_3_2_1_2.json new file mode 100644 index 000000000..366f49906 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_3_2_1_2.json @@ -0,0 +1,30 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "blake3", + "format": "one_row_3_2_1_2", + "proof_rkyv": "e_proof_blake3_one_row_3_2_1_2.rkyv", + "proof_rkyv_len": 9432, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "one_row": true, + "query_bound": 4096, + "trace_tree_depth": 12, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 2, 1, 2], + "fri_roots": ["631d2cc3b49a8af4dfe3f98daf6aed283292da1952fa3955b5f7f08b0addbdcd","bdd4fec81d28bac7cd36e7ee047215e8e4e103a6bbaf6283b7451634d9ad7b13","da5a923543dd35c5ca8583a9e7ab6628943515c416c5b40f7d4cc001287c0f21","57dcb115bfa2c9d5d0b40f9d0095f35f4748f9f7b775fe1c691ec543de2f9991"], + "zetas": [[15303203179608116932,15332629808348820381,2738315253515061193],[363681095822826847,9352367609937442414,5055796092274921848],[415111747713879034,9715309884249926995,17241085662786188492],[11355540028731819133,14059481269354791517,9429735590016017728]], + "terminal_coeffs": [[10406129371342019884,6637860243315701565,17413526420005229991],[10816053847836238497,6095471629599537880,14691285080173723159],[5086529536450569695,3715400737876031162,8188649170801691210],[2322353628246006421,538626804931903626,11216069688036902183]], + "queries_detail": [ + {"iota": 1456, "trace_leaf": 1456, "trace_path_len": 12, "deep": [194573393572430413,575541586100696177,17185197193417606866], "terminal_position": 5, "layers": [{"layer": 0, "d": 3, "position": 1456, "leaf": 182, "slot": 0, "values": [[194573393572430413,575541586100696177,17185197193417606866],[15167111713629212848,3371841055974538903,389590522308985887],[9856468522289727427,1141024469855737413,8823427854447807630],[10019267653830064531,7797126114468478428,3383543947467966788],[16420762772661263866,17352027881895391520,1058807058912222348],[16025551977927421603,5271612813891409186,3946984757956381153],[167401832213904072,11333883202427868807,9026386292192864724],[13734568471847334025,15888679784224105042,16695142320317115780]], "path_len": 9}, {"layer": 1, "d": 2, "position": 182, "leaf": 45, "slot": 2, "values": [[7985111730242223468,13043427922133413739,15165458788387177067],[11920096695007353835,4322196391270192330,6331521415760836415],[17528524193106433492,16056325209359722539,13396748850679192187],[11059400933671702759,3929660671090461634,17528699381798378854]], "path_len": 7}, {"layer": 2, "d": 1, "position": 45, "leaf": 22, "slot": 1, "values": [[11036601194838964653,13091553256446016225,17725342463852698195],[17165721511809460325,16005781762499650030,6120337115418879894]], "path_len": 6}, {"layer": 3, "d": 2, "position": 22, "leaf": 5, "slot": 2, "values": [[10080803378345683631,13581445870922208388,112183069305103648],[274796791223342208,11333819441498540010,18272391722137453186],[935894502491640394,8069316797606646829,2723518402895760156],[5241309797487768752,1497453635557166066,4753616983058012740]], "path_len": 4}]}, + {"iota": 2121, "trace_leaf": 2121, "trace_path_len": 12, "deep": [5932154655850286336,14843004070290831321,5490619261809314267], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 2121, "leaf": 265, "slot": 1, "values": [[9752620111750347501,17004819712705738208,10174864346111711421],[5932154655850286336,14843004070290831321,5490619261809314267],[12457881330515312702,9438882645062572601,2477304330124838740],[13342612810388937410,1955529733589402555,17637786938873078095],[11463360995704355283,5397845986356162493,5459989775793774225],[17202488491312627995,4756487032691328612,7189238154931068032],[12446893681390783201,13814247764991316483,10749934184693828068],[17656899509184221667,3230105340003095516,1646950555390068296]], "path_len": 9}, {"layer": 1, "d": 2, "position": 265, "leaf": 66, "slot": 1, "values": [[143943179255856116,4798827311101200581,11778189320573623738],[11960653567770059154,15137154786077885916,3444065176719318769],[15345592422504941107,13385472108954667751,16279989344517567620],[294297353456909017,2378799116308021259,5821554510368416071]], "path_len": 7}, {"layer": 2, "d": 1, "position": 66, "leaf": 33, "slot": 0, "values": [[4366185996301223320,18130458512453329325,12321842910238819391],[13152781073289974129,16384943335603405623,14986113949777996567]], "path_len": 6}, {"layer": 3, "d": 2, "position": 33, "leaf": 8, "slot": 1, "values": [[15524180633480904874,14071768287211384567,1927601050817459981],[13921811725908965900,9784889492536125216,6245093131103517056],[12348990429102907453,7378250459311877760,16884998755649302425],[3694751754196132007,6208476045974134093,12303339167180175644]], "path_len": 4}]}, + {"iota": 1748, "trace_leaf": 1748, "trace_path_len": 12, "deep": [9397218555520598655,18184148177222589316,16018724279546536410], "terminal_position": 6, "layers": [{"layer": 0, "d": 3, "position": 1748, "leaf": 218, "slot": 4, "values": [[17724856226765146592,10082092572657001926,13645259543255264275],[863625578904618075,10400287656113463467,16528818973977431892],[11599916912452455840,352475967223050044,13498709060109220207],[18319345150264424015,15100223399046302655,14937551297828451863],[9397218555520598655,18184148177222589316,16018724279546536410],[14436493135967899386,7768390391746153895,1993950119548277638],[6389622230992030034,9873739093623680361,12974578811051650909],[12122986716699513979,3853691390967758280,3045013771093848114]], "path_len": 9}, {"layer": 1, "d": 2, "position": 218, "leaf": 54, "slot": 2, "values": [[6707930231302068305,6289296625208072513,6228121725933507311],[3402815304253234050,7931680117825231650,5904690421620213191],[3811509250261400570,10486550803692321562,6467245992250408341],[18434400026770783222,3810674857839593215,178699979830025126]], "path_len": 7}, {"layer": 2, "d": 1, "position": 54, "leaf": 27, "slot": 0, "values": [[11497692889911500389,4956481265465714259,11985015358430959406],[2966973332759365868,15603791491812407637,10069156031838418994]], "path_len": 6}, {"layer": 3, "d": 2, "position": 27, "leaf": 6, "slot": 3, "values": [[3392282136864393874,10616718363163724573,17205136376367452557],[16827435575822246951,2730373153072397240,9528833467876665279],[16245708365531335642,8964017129754350926,7380809409241229694],[1307506453050297819,16732696313432695426,12187551015507047031]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_3_2_1_2.rkyv b/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_3_2_1_2.rkyv new file mode 100644 index 000000000..3d83f778c Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_3_2_1_2.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_pair.json b/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_pair.json new file mode 100644 index 000000000..b737d89d5 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_pair.json @@ -0,0 +1,30 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "blake3", + "format": "one_row_pair", + "proof_rkyv": "e_proof_blake3_one_row_pair.rkyv", + "proof_rkyv_len": 12872, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "one_row": true, + "query_bound": 4096, + "trace_tree_depth": 12, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [1, 1, 1, 1, 1, 1, 1, 1], + "fri_roots": ["ffcda5bca5d29a901db3846a0a5fa7a118262e25e4728bb756f5c3e049d81721","10eb5cc9eeee08e57157087e948c84c60527a46ceeb0d2089ddf045d779a2413","af88804bc30f486661c073c2e6ff7a5a8d21afe8a0770f1158bb1b25d181b7f8","42cee7233e12c7eb7632517a3f13a5cb95766d730eecbdd40639ed8967584469","5ed6e25331051300afac1ab13117757d4e3527a7bf5ee9b7c76446fb48f7bde0","5ab17f3d3712ad338e104eb211a723c069b954ac84e050cc42c484cc749d914e","4142f22a168743d89b8a6bb8e5faf9ebd05d4942673a1cbffe2cc0275fcec5a8","1943156b542514107b16253cbb66bb7c511207bff9832d934520c4f998e343fe"], + "zetas": [[12610513987238684980,2656594610096053963,9788033839815437623],[8202322326541763249,4638671715876570408,1548089539524959966],[5418912639886591044,14436703782498228938,13150451438376090423],[16129413131115373670,12116921447145899673,1789434332460341265],[5229606843698452721,2538028647533910313,2087788943472497651],[7882549724414645682,15998438433459111387,17172254213527133673],[15797561344767407336,7451312555724701235,9353079321439324766],[2630306677520305007,18443316671340290298,698233072451755557]], + "terminal_coeffs": [[8166688057727980294,3527098508508475499,3579219994840971435],[13088957070833237586,2843452289971455650,13931698865628860263],[10280665272663538462,16957920478250150732,16525456308843497239],[3490947975253976115,15530748444489164043,8363654267677576632]], + "queries_detail": [ + {"iota": 908, "trace_leaf": 908, "trace_path_len": 12, "deep": [8337040516664561595,3591184463121466475,5880410691370975250], "terminal_position": 3, "layers": [{"layer": 0, "d": 1, "position": 908, "leaf": 454, "slot": 0, "values": [[8337040516664561595,3591184463121466475,5880410691370975250],[9942079076094822757,15207253271776161812,17271810815973175383]], "path_len": 11}, {"layer": 1, "d": 1, "position": 454, "leaf": 227, "slot": 0, "values": [[6500024495234768669,14790536704385357656,10970653425198349506],[3037118830635648665,12049339759137376056,2058818805291034312]], "path_len": 10}, {"layer": 2, "d": 1, "position": 227, "leaf": 113, "slot": 1, "values": [[12998320690946467439,3143873445886610368,8364194405026736828],[12303592634730705070,7969752057750737167,7711769752711670497]], "path_len": 9}, {"layer": 3, "d": 1, "position": 113, "leaf": 56, "slot": 1, "values": [[6149215606200672108,12230310026488736926,6564511547928486060],[14404622591513458299,7898366117446141210,15470602245685106535]], "path_len": 8}, {"layer": 4, "d": 1, "position": 56, "leaf": 28, "slot": 0, "values": [[9258841562081245909,5303464181200090505,15395838452030486667],[17983690631426950677,9281380336221551905,4689865579398814961]], "path_len": 7}, {"layer": 5, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[16752007877985850286,14449418999911030214,18103323226226493927],[15685253434469102082,1229693862459337190,2229360339491735221]], "path_len": 6}, {"layer": 6, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[13212989330237734024,11511715167835460658,3579953756865186267],[11683874874595907750,17023168257014162671,7683877784938300551]], "path_len": 5}, {"layer": 7, "d": 1, "position": 7, "leaf": 3, "slot": 1, "values": [[3673055120577650390,17761862347260528145,15879867737729884886],[15701614389839158101,16196947250955156894,9204580243531569325]], "path_len": 4}]}, + {"iota": 1667, "trace_leaf": 1667, "trace_path_len": 12, "deep": [13636529597871698334,13678607474726528147,9848862240480814905], "terminal_position": 6, "layers": [{"layer": 0, "d": 1, "position": 1667, "leaf": 833, "slot": 1, "values": [[10220852481278625157,14071873960699589269,828872414573626613],[13636529597871698334,13678607474726528147,9848862240480814905]], "path_len": 11}, {"layer": 1, "d": 1, "position": 833, "leaf": 416, "slot": 1, "values": [[16761011709352981912,10851157093617305415,3734351154823449094],[2178317076330816469,2215320630266717566,10026952272033988103]], "path_len": 10}, {"layer": 2, "d": 1, "position": 416, "leaf": 208, "slot": 0, "values": [[11153389165091966496,8933652262252062319,4639213090021931600],[9036890528007069229,9889438455510066330,7755295326886188573]], "path_len": 9}, {"layer": 3, "d": 1, "position": 208, "leaf": 104, "slot": 0, "values": [[15798430107321119679,7399920888497789797,3851469018050691970],[12265152089375660269,12464326470257268915,13750970462859745799]], "path_len": 8}, {"layer": 4, "d": 1, "position": 104, "leaf": 52, "slot": 0, "values": [[10197137544449174329,2666341861104716732,8994168763272311938],[2467333217759550862,15961397925655894924,3556723982947593317]], "path_len": 7}, {"layer": 5, "d": 1, "position": 52, "leaf": 26, "slot": 0, "values": [[13808654807413576808,12906200930280508316,18118829275719952794],[11445909634129708166,5679871913112149183,1062655856487114970]], "path_len": 6}, {"layer": 6, "d": 1, "position": 26, "leaf": 13, "slot": 0, "values": [[16433333226992445962,4282547907874224073,5296795991017901170],[3650660750394594762,12394385974057356737,16977602210444761993]], "path_len": 5}, {"layer": 7, "d": 1, "position": 13, "leaf": 6, "slot": 1, "values": [[14131047591148505700,8704142041558711804,15425037308105431711],[12808742107704184070,12949984306744884736,12983161641959877635]], "path_len": 4}]}, + {"iota": 2556, "trace_leaf": 2556, "trace_path_len": 12, "deep": [14961774084741758894,17724548465625463449,407937579721329606], "terminal_position": 9, "layers": [{"layer": 0, "d": 1, "position": 2556, "leaf": 1278, "slot": 0, "values": [[14961774084741758894,17724548465625463449,407937579721329606],[10956977985548048024,1607572610044263675,9234012651162083785]], "path_len": 11}, {"layer": 1, "d": 1, "position": 1278, "leaf": 639, "slot": 0, "values": [[12579967482476121137,8013459107863289407,903732426692410311],[10959159311742709132,3796355782169593955,16416941109827413153]], "path_len": 10}, {"layer": 2, "d": 1, "position": 639, "leaf": 319, "slot": 1, "values": [[14145955780063915793,223976980533745232,4941025582364757750],[979968522591148177,17376083620876130881,8934103765040324984]], "path_len": 9}, {"layer": 3, "d": 1, "position": 319, "leaf": 159, "slot": 1, "values": [[93790095226020304,5037292919527109604,5200685977454171447],[1267192859330294723,15876287822629290895,5670415768498196749]], "path_len": 8}, {"layer": 4, "d": 1, "position": 159, "leaf": 79, "slot": 1, "values": [[17463990687529665122,194968129408922552,2783831484230524712],[5763012266564981842,7685162751697513212,18392797312610438655]], "path_len": 7}, {"layer": 5, "d": 1, "position": 79, "leaf": 39, "slot": 1, "values": [[6387031204678005052,5691431782676540765,17705074806211579330],[4488774873052044926,1720374853552626477,12277685967489572963]], "path_len": 6}, {"layer": 6, "d": 1, "position": 39, "leaf": 19, "slot": 1, "values": [[11996208840794354830,17068718129454278781,14889985865285000741],[12959561901970052932,6028487744399307154,12374012229986672980]], "path_len": 5}, {"layer": 7, "d": 1, "position": 19, "leaf": 9, "slot": 1, "values": [[15922681132257262015,4190699033194233110,8485117899075106073],[9473060730087051793,12084821640297482348,1315857464550192679]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_pair.rkyv b/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_pair.rkyv new file mode 100644 index 000000000..2f9915aac Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_pair.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_3_2_1_2.json b/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_3_2_1_2.json new file mode 100644 index 000000000..287ca4ba2 --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_3_2_1_2.json @@ -0,0 +1,30 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "keccak", + "format": "one_row_3_2_1_2", + "proof_rkyv": "e_proof_keccak_one_row_3_2_1_2.rkyv", + "proof_rkyv_len": 9432, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "one_row": true, + "query_bound": 4096, + "trace_tree_depth": 12, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 2, 1, 2], + "fri_roots": ["0573d33cd3831d78041a924b5fb8e99c6e7869182059338fc15f7a4733ee0a85","90290b9eb0e2d969efcdf2aaf485507286ffb07d43f6820471f7f23cd188a131","006f08a7f15cb5500f5cfd9b0a63cff4740c9bfc5568651611a59584e2131c4c","5969363fdc7f452e555c8fe81f1067568f2db56604cd9252b1863428573ed9ef"], + "zetas": [[614569478871447995,15598463788603497943,12360548214323292500],[2749684798805164550,11842624514060754293,11317727074124763797],[11378158352117685781,13883522662539884430,10300089410576297656],[18245032832565177967,14493753044579952825,7681180882173639262]], + "terminal_coeffs": [[16982082107160915382,2282929924407988814,10086391487892448182],[15345308450015855078,1512152681275637452,17871467084486542926],[15729379371492426567,14327971657269957480,15709819461529248088],[2510735358381904920,10777919307354146356,849522161725247283]], + "queries_detail": [ + {"iota": 3747, "trace_leaf": 3747, "trace_path_len": 12, "deep": [13162161543905520345,16222340621118398767,14812861061492486552], "terminal_position": 14, "layers": [{"layer": 0, "d": 3, "position": 3747, "leaf": 468, "slot": 3, "values": [[11939368898252254820,7966298589702067435,13923464541084449620],[16519000565047420203,14800180017840624901,2473854278768394879],[18353344654355725185,7598655595952838514,485174952313354076],[13162161543905520345,16222340621118398767,14812861061492486552],[516438490002139474,1312461157064834419,11778937658407907710],[7512883462459603712,11575140191108900564,9299557270393767791],[13927402591382951840,14922292783994652432,2270976895176414228],[13126156821235988227,13558411403710322322,15584318315517571146]], "path_len": 9}, {"layer": 1, "d": 2, "position": 468, "leaf": 117, "slot": 0, "values": [[9741722024427321673,15417388380850289838,8511550521789513588],[17090311002612650370,17871609573955562961,15251904324037649227],[3481455838415491242,12644378374962812916,4629625775108086156],[2520177331257733270,5248631378196257018,17601467731237825492]], "path_len": 7}, {"layer": 2, "d": 1, "position": 117, "leaf": 58, "slot": 1, "values": [[1171841683027868450,3516707735676916944,8408424371334170472],[13055852338994778977,6161935832821634597,17625442510433625962]], "path_len": 6}, {"layer": 3, "d": 2, "position": 58, "leaf": 14, "slot": 2, "values": [[2041002906107072756,9720053546030065860,555422539991461512],[8649597796105717676,9887359070425505677,7903904779785967548],[12514932492420530080,11822836074630855773,12630954109616877078],[13178656397698138192,14330519614295969207,1587797339945367521]], "path_len": 4}]}, + {"iota": 3932, "trace_leaf": 3932, "trace_path_len": 12, "deep": [11997983309011693619,14832433174498138681,3972042518256438394], "terminal_position": 15, "layers": [{"layer": 0, "d": 3, "position": 3932, "leaf": 491, "slot": 4, "values": [[593841392894287482,3244515950377860767,17235984492213527070],[13357798639288218697,1603406640572299408,1355476450824903574],[14365125491994659199,17578081687479202067,2333721695909416367],[14797892649259416114,17329545340641227275,9090376908372138884],[11997983309011693619,14832433174498138681,3972042518256438394],[2404507422347028884,3461603417214790720,13793889970733275211],[16958760744710710146,8223175518268331369,6936434935236651275],[11435013313213504764,13514836260626656026,3988205428095959867]], "path_len": 9}, {"layer": 1, "d": 2, "position": 491, "leaf": 122, "slot": 3, "values": [[6441239995333214949,2008557044640809954,6302267122927451198],[5755366049690739100,5940433195631773921,10073298633835659656],[10355852173748934975,375915313542881553,9614581595079369722],[9838682547908915799,13076759558720065724,15972429628786102473]], "path_len": 7}, {"layer": 2, "d": 1, "position": 122, "leaf": 61, "slot": 0, "values": [[9046291822940486809,15904882642864702632,9243802697598860090],[637086985604128903,11984196675483089673,10105544773484042559]], "path_len": 6}, {"layer": 3, "d": 2, "position": 61, "leaf": 15, "slot": 1, "values": [[13705054946293609652,10520758388559636598,1843839319895042140],[8984595321379749268,957464648666588723,5697566634900201733],[6979165281995072482,14556780583710113004,18196894460862786007],[5854409060872630881,11874995298834561657,18087563126396428879]], "path_len": 4}]}, + {"iota": 2157, "trace_leaf": 2157, "trace_path_len": 12, "deep": [17241446171198887870,9089374642387650352,4362693261445459131], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 2157, "leaf": 269, "slot": 5, "values": [[18072602033057449462,791493680086261727,5006389479070283833],[13982827640424845255,1912086835758784968,9468131248488756799],[15669337530775031072,10768501467625042840,11838115817859132235],[9770512334600625737,12989387495815999473,1158167208066671977],[17046020249111576087,1346189118933924257,14161816367472112208],[17241446171198887870,9089374642387650352,4362693261445459131],[8007161180953444347,4632969092460641420,18079743432785932568],[18364944787622309257,6409634255995638389,17237001282452441669]], "path_len": 9}, {"layer": 1, "d": 2, "position": 269, "leaf": 67, "slot": 1, "values": [[659414522409282892,14050932206177175844,18208880827946597537],[4678902988385856119,18195843974585314494,806802659805655072],[10558643965950283455,12223164169372974730,8716989064208992085],[14026387295497206822,2716121227615181969,6807619021643285971]], "path_len": 7}, {"layer": 2, "d": 1, "position": 67, "leaf": 33, "slot": 1, "values": [[9471244475049593593,17798865937770683050,14180813714250709557],[14814757386327982232,16484305865918755940,12556180948754871812]], "path_len": 6}, {"layer": 3, "d": 2, "position": 33, "leaf": 8, "slot": 1, "values": [[10573339342448139864,13245993176403407621,8769913239944605419],[16805842258116935004,8741300544834758532,7091541834846627245],[6988904484043719279,18021125333806579078,8922319143029332726],[7354216556376030521,16944997308615623648,6846330711616610055]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_3_2_1_2.rkyv b/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_3_2_1_2.rkyv new file mode 100644 index 000000000..df7b44bd1 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_3_2_1_2.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_pair.json b/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_pair.json new file mode 100644 index 000000000..cc2aa5ecb --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_pair.json @@ -0,0 +1,30 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "keccak", + "format": "one_row_pair", + "proof_rkyv": "e_proof_keccak_one_row_pair.rkyv", + "proof_rkyv_len": 12872, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "one_row": true, + "query_bound": 4096, + "trace_tree_depth": 12, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [1, 1, 1, 1, 1, 1, 1, 1], + "fri_roots": ["eb1586bcc3bde284f3670ea0fc65169877abd7992d5dcffac57e32cdfc0ba5f2","30b219ab4e8860d4f3c872e81f11240bc60e921a204b888f53f4348dd589c2ed","ec7372f78de6460bb087801ee033a25fb3c016625d1acb946c1e96e0a55d29a8","8f88290226e193b7acc7e388a2a127e9d692f577fb235912df6eb2ed3d726bf5","87f44cf42799f46c75c2cec1cf06b6e9af17948837dd4cfa03de3cce38ebcb96","90a1b58155d4e93a0beec06f1b1fa52e9fe4935083e0ae7314cc9f3be42a4af4","6e7269bd4c2931aee21f1ecd16dbce5485e7d78a83fbd62f34b7f12e0d014bd3","6de72339fcca67b8bbd95f83392ba7e72e6e417d8f2d76cf219d2e4c0229561b"], + "zetas": [[5395733614478478870,11359234539595361029,9468765475367811309],[4691404022056334253,17758983664188144681,8299164536713330578],[5571467312536115048,5474225967440588377,2237621988735074941],[16344818043268592932,10747181205106830409,3900555077883816039],[13194324152105116145,9291931726475171977,5358827732935644598],[3745110952868976217,4893045201053783273,1326274809970111783],[15086693329382369694,10223940530923563648,9474084692470291838],[2414133768868227156,15636351245895875877,1237127212595351297]], + "terminal_coeffs": [[6924181524295252559,1631579396016903923,6572868464790892487],[15080983657215378178,5352747742150251534,1033168116145250917],[9161197465518332209,1467001063992449915,13256635128719367890],[11794544518733838288,17573472610644282767,9381019061455135878]], + "queries_detail": [ + {"iota": 762, "trace_leaf": 762, "trace_path_len": 12, "deep": [10094068370361461326,18298575760673302723,167760047989915880], "terminal_position": 2, "layers": [{"layer": 0, "d": 1, "position": 762, "leaf": 381, "slot": 0, "values": [[10094068370361461326,18298575760673302723,167760047989915880],[5459334766786537305,7232075403155012048,2842130299185158477]], "path_len": 11}, {"layer": 1, "d": 1, "position": 381, "leaf": 190, "slot": 1, "values": [[6567901125757655267,17890067315322448035,14925681233931409419],[18235131754497127385,4333965804878517668,15984067725454874720]], "path_len": 10}, {"layer": 2, "d": 1, "position": 190, "leaf": 95, "slot": 0, "values": [[14137450043918885600,2486776962123393416,10502354082803621117],[15828920042687943811,16961409959142313627,2460745610550574849]], "path_len": 9}, {"layer": 3, "d": 1, "position": 95, "leaf": 47, "slot": 1, "values": [[10227001240940270478,17070186034540605583,17207116355067825115],[10861036229411731283,2596952122795351504,8962616386048239227]], "path_len": 8}, {"layer": 4, "d": 1, "position": 47, "leaf": 23, "slot": 1, "values": [[14899232665400030951,11822327205407897399,12852772973679258007],[67353967484070222,7344793933182310052,13538204803111544355]], "path_len": 7}, {"layer": 5, "d": 1, "position": 23, "leaf": 11, "slot": 1, "values": [[1048029576799362121,15752469893941610794,15074550518954149367],[4813813954182030731,14827624934013545034,14558705574433931634]], "path_len": 6}, {"layer": 6, "d": 1, "position": 11, "leaf": 5, "slot": 1, "values": [[2992753296415936463,1047880712958114015,588465930372032317],[2822506618795001014,3554454054060556307,3722117492988577995]], "path_len": 5}, {"layer": 7, "d": 1, "position": 5, "leaf": 2, "slot": 1, "values": [[14523993385875431820,10849282782304498204,17992197523634908884],[7510878449777876572,17576379251232901406,3916410497425810328]], "path_len": 4}]}, + {"iota": 1814, "trace_leaf": 1814, "trace_path_len": 12, "deep": [3486173205532075028,13501479726213628195,7711397338049492637], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 1814, "leaf": 907, "slot": 0, "values": [[3486173205532075028,13501479726213628195,7711397338049492637],[2724410617343204033,17041886471397262471,10048053216853072532]], "path_len": 11}, {"layer": 1, "d": 1, "position": 907, "leaf": 453, "slot": 1, "values": [[15218919491708099717,6265300493536016072,17374044107043655971],[15414362704001559067,5910203742828375847,18174508871703012260]], "path_len": 10}, {"layer": 2, "d": 1, "position": 453, "leaf": 226, "slot": 1, "values": [[8297598642705710651,8896841861319899434,4271968726705068454],[14232960524765109061,1751219308888705154,1611213564303633947]], "path_len": 9}, {"layer": 3, "d": 1, "position": 226, "leaf": 113, "slot": 0, "values": [[10187672769979218181,6883190422014197230,17630315930747985708],[11543340710353323292,2579691248118751998,10788465591631670544]], "path_len": 8}, {"layer": 4, "d": 1, "position": 113, "leaf": 56, "slot": 1, "values": [[3255185536445046756,801363096168136444,2144739588122306839],[2881737247016614189,16267857870605242701,2892508963994520231]], "path_len": 7}, {"layer": 5, "d": 1, "position": 56, "leaf": 28, "slot": 0, "values": [[10005708081878913971,16883002466271960497,15819714690417286842],[8979443613673943889,446485870763451531,7396295827609540705]], "path_len": 6}, {"layer": 6, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[11530783784575839535,16830773339017628756,13263531435704047809],[3000166235818018639,10723029199016018551,4802476952484817780]], "path_len": 5}, {"layer": 7, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[15108810641842900353,9017572136933373745,827650886629197094],[8879386830232170817,4440284036099816188,756978902737227389]], "path_len": 4}]}, + {"iota": 1574, "trace_leaf": 1574, "trace_path_len": 12, "deep": [2436382877645926339,12231969268009871513,13861361779801063289], "terminal_position": 6, "layers": [{"layer": 0, "d": 1, "position": 1574, "leaf": 787, "slot": 0, "values": [[2436382877645926339,12231969268009871513,13861361779801063289],[2290948958860766437,9846444826915849467,11266926617187786854]], "path_len": 11}, {"layer": 1, "d": 1, "position": 787, "leaf": 393, "slot": 1, "values": [[3052277439643912161,790057607007591807,1104643025786218165],[13383466910132863239,8751924119524353407,1366177720789238310]], "path_len": 10}, {"layer": 2, "d": 1, "position": 393, "leaf": 196, "slot": 1, "values": [[9149083653236277356,15817005062217202304,1581999755255560507],[18317007025956390544,15382505965948419620,1288943076662677473]], "path_len": 9}, {"layer": 3, "d": 1, "position": 196, "leaf": 98, "slot": 0, "values": [[2435561994978144773,9972848500245771018,15897565491963037741],[16346129647770001039,4519376622261929280,16372045926229684526]], "path_len": 8}, {"layer": 4, "d": 1, "position": 98, "leaf": 49, "slot": 0, "values": [[2655294817086169804,3003897080814048378,15077680701138897251],[13861152057247201528,1734797488345216608,10118591602100077410]], "path_len": 7}, {"layer": 5, "d": 1, "position": 49, "leaf": 24, "slot": 1, "values": [[9208453110434430430,10253020393070553579,3519126129019616401],[8333278291328356074,2249190254359800375,15583977317131110273]], "path_len": 6}, {"layer": 6, "d": 1, "position": 24, "leaf": 12, "slot": 0, "values": [[6821195066155593029,7962639569178694042,7256273041640925963],[6951321742118564316,3964363899377279367,6170583504292385774]], "path_len": 5}, {"layer": 7, "d": 1, "position": 12, "leaf": 6, "slot": 0, "values": [[527725121383626666,16173092967399922496,4843509717089046499],[12008023089913322763,2738286001211741840,9628318339839965077]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_pair.rkyv b/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_pair.rkyv new file mode 100644 index 000000000..917d6a823 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_pair.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_3_2_1_2.json b/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_3_2_1_2.json new file mode 100644 index 000000000..d9db0388e --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_3_2_1_2.json @@ -0,0 +1,30 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "rpx", + "format": "one_row_3_2_1_2", + "proof_rkyv": "e_proof_rpx_one_row_3_2_1_2.rkyv", + "proof_rkyv_len": 9432, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "one_row": true, + "query_bound": 4096, + "trace_tree_depth": 12, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [3, 2, 1, 2], + "fri_roots": ["c8c6a3857a1e9d7c2f61fd46746e2bb3535cdb9f63b6e22d3ec7f9a5e383a2f2","95f4386479d6d55922c93cfdea9675fb5f65b0c2321b74dffb788cf03ae2c53c","ebdfff240e34a91a0648ee7466adb199067ae4715f31db95ed0776b6213f786f","9ba65129b5413142af80b633ecb5a1d37f6c8816c451709ae71b0674281831a5"], + "zetas": [[2072553990002038672,3152733128493477078,11602711029815742146],[15459197400690133497,11842684412984680192,16710139345478133626],[13216241016891166733,2969942746573330626,6504928145990413379],[2081893305540362911,6699593400215758319,13627062838661458060]], + "terminal_coeffs": [[8917127793609234202,3818436251448018868,9153923638574958298],[41372134856129893,14445791458964858478,6916624050008194800],[1369532866486613187,17310202425868976552,5527516346235608556],[113871941917206458,5117289324887501201,17934737032884348866]], + "queries_detail": [ + {"iota": 2205, "trace_leaf": 2205, "trace_path_len": 12, "deep": [6857190562724640788,4007496433083125706,17921103436030748622], "terminal_position": 8, "layers": [{"layer": 0, "d": 3, "position": 2205, "leaf": 275, "slot": 5, "values": [[4602638682387835976,8451276510867598838,14669621850686772327],[8046089631358719438,6000422636414950401,17791408447453155495],[13224227945885332137,4022497221569521121,14132968543576776612],[8366093996352563224,10586245183137297300,16590897440396536726],[10577642219811204609,8132865527707081930,3794842370168730599],[6857190562724640788,4007496433083125706,17921103436030748622],[17771010997934343328,9579289656828118766,11375662375469437872],[11752688824513319943,11813227599571567056,203875144659324433]], "path_len": 9}, {"layer": 1, "d": 2, "position": 275, "leaf": 68, "slot": 3, "values": [[14027603398985142442,14931974495511084269,13169342190365263967],[10729570682734190201,13360886974794249550,12166794907646215730],[17505870873946782225,1272114538253616029,7221576044859230217],[14680968125359200025,1834120669802456377,7634397345903233497]], "path_len": 7}, {"layer": 2, "d": 1, "position": 68, "leaf": 34, "slot": 0, "values": [[1755336120330909687,4738723403799425508,16526304371652197035],[6198070429371798043,9875400842722205776,4978023505035734507]], "path_len": 6}, {"layer": 3, "d": 2, "position": 34, "leaf": 8, "slot": 2, "values": [[12432865959921923654,8411549371093338152,4907757842896396886],[16829062543972548747,4595223385839320908,17306133878441923256],[9516230404474158645,184282845086571435,16960709705922000024],[3121559606125253722,16789510687716757680,2036697317195379873]], "path_len": 4}]}, + {"iota": 752, "trace_leaf": 752, "trace_path_len": 12, "deep": [1916903000875823615,16736707246874285829,9564656102773943099], "terminal_position": 2, "layers": [{"layer": 0, "d": 3, "position": 752, "leaf": 94, "slot": 0, "values": [[1916903000875823615,16736707246874285829,9564656102773943099],[11453173613332389795,4145797392594904289,7876548709028745774],[4615854952402447642,14543475126162204942,12910463926362207121],[7593405292236047148,8724292599272739129,17322028385419128472],[12625625798216679042,18063690514277702530,14755773946776919650],[4912295306283908672,14347175193378890942,7951297300128137473],[12063826769130634695,7054630009345082543,10321569615007428609],[14137661524220596099,3152399428801735017,10004540932738062053]], "path_len": 9}, {"layer": 1, "d": 2, "position": 94, "leaf": 23, "slot": 2, "values": [[8811625971723314382,7552532437654286077,16441080950187543807],[17484288545086663,16256622829149716703,8461021173188657701],[6252273343596476642,11321822468352368566,9211443333124516620],[8039005244383157612,6950435551192179736,13659146476691124801]], "path_len": 7}, {"layer": 2, "d": 1, "position": 23, "leaf": 11, "slot": 1, "values": [[18056204561276561229,14835128819825911563,5046426107340521641],[5893548925016901766,4760873787913692676,18439055921004270113]], "path_len": 6}, {"layer": 3, "d": 2, "position": 11, "leaf": 2, "slot": 3, "values": [[10904235002716272775,9975472978635657080,14964204635917748194],[5305060826180553417,12937376069285077497,3287539991567464608],[8719348652353813625,994707129833621565,11914955711098119318],[17889548190955188583,5660707692435915686,3267735454667947369]], "path_len": 4}]}, + {"iota": 760, "trace_leaf": 760, "trace_path_len": 12, "deep": [18267711020647082997,13248031096375226280,188428981296028522], "terminal_position": 2, "layers": [{"layer": 0, "d": 3, "position": 760, "leaf": 95, "slot": 0, "values": [[18267711020647082997,13248031096375226280,188428981296028522],[12182114973803986625,9335268974613975205,10075971023962334808],[12163018125168083220,2817955096360236833,15999670448454868863],[8495492306395569540,4095933714799065998,13368466763250358722],[8006825421091369573,5803517742005105201,1788791879629939833],[1820353059050006357,5346090292370034386,2193165956274050915],[802007586120352552,16206387393817464715,4632934878557695217],[7783716627999292698,9167226174152632427,17535031471772053546]], "path_len": 9}, {"layer": 1, "d": 2, "position": 95, "leaf": 23, "slot": 3, "values": [[8811625971723314382,7552532437654286077,16441080950187543807],[17484288545086663,16256622829149716703,8461021173188657701],[6252273343596476642,11321822468352368566,9211443333124516620],[8039005244383157612,6950435551192179736,13659146476691124801]], "path_len": 7}, {"layer": 2, "d": 1, "position": 23, "leaf": 11, "slot": 1, "values": [[18056204561276561229,14835128819825911563,5046426107340521641],[5893548925016901766,4760873787913692676,18439055921004270113]], "path_len": 6}, {"layer": 3, "d": 2, "position": 11, "leaf": 2, "slot": 3, "values": [[10904235002716272775,9975472978635657080,14964204635917748194],[5305060826180553417,12937376069285077497,3287539991567464608],[8719348652353813625,994707129833621565,11914955711098119318],[17889548190955188583,5660707692435915686,3267735454667947369]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_3_2_1_2.rkyv b/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_3_2_1_2.rkyv new file mode 100644 index 000000000..b73dc5fb5 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_3_2_1_2.rkyv differ diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_pair.json b/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_pair.json new file mode 100644 index 000000000..859468b1a --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_pair.json @@ -0,0 +1,30 @@ +{ + "generator": "stark::fri::vectors::proof_vectors", + "hash": "rpx", + "format": "one_row_pair", + "proof_rkyv": "e_proof_rpx_one_row_pair.rkyv", + "proof_rkyv_len": 12872, + "air": "LogReadOnlyRAP, reads (i % 5 + 1, 10·(i % 5 + 1))", + "trace_rows": 1024, + "lde_log": 12, + "blowup": 4, + "fri_final_poly_log_degree": 2, + "queries": 3, + "grinding_factor": 0, + "coset_offset": 3, + "one_row": true, + "query_bound": 4096, + "trace_tree_depth": 12, + "legacy_encoding": false, + "total_folds": 8, + "terminal_len": 16, + "schedule": [1, 1, 1, 1, 1, 1, 1, 1], + "fri_roots": ["77e5d86a0702fe0c50672508977a685b2bfdae2cd941fad392c8294731c9e422","7258024a9819630328acea419c9a638b25bdbdee4bd724583300d7d1848f5519","7ab08ab352939e6e9b968fce340beb441ed26f4d1f179b03e74183a4503d1239","0d41af1406e7f515bad385e1fdcc4d28446fe84ab66f3fec873810cbec793c33","81e8a52594b1153a62d1ef60f5b10785044c11977864f8598b7bb7e677a383f5","458ef527b1574a69063b821685e35402eb11527965e9a8afbfe27d1c055a706d","ed0dac83bdc4727544d623f801c76a700690e70738888ddd5b58ce4ec98aad16","b87b6976a6466e4ba2a542e3e401ab0dc8ad488df17e4e95f35413220c779215"], + "zetas": [[11356657239044866106,13376194269418604399,16611853721418243315],[10366391614236701735,449823887536073129,4380920291293489693],[11922106070388417666,2861436225089580234,7941726727098846953],[6173349043357729189,13221589175675104657,12541630541605756582],[9571725825048407501,8781332429625970728,14051607987466410322],[271455075676084364,8661011714467229856,11921036807597295366],[15420589626999451879,2381820146514785423,464796940378704146],[9630091406616888336,14072038474898281586,2889648399077434883]], + "terminal_coeffs": [[9252102389862037098,11485521080712742978,15330746307211460791],[2408142744451238759,16015006922876515991,8289145647860378560],[3822746191229327094,13873058238083039505,4675575401224124965],[17237282230694182751,10651742490541135044,7065525042114859165]], + "queries_detail": [ + {"iota": 1490, "trace_leaf": 1490, "trace_path_len": 12, "deep": [2932413113042791435,11894382985923903979,9485622643104290083], "terminal_position": 5, "layers": [{"layer": 0, "d": 1, "position": 1490, "leaf": 745, "slot": 0, "values": [[2932413113042791435,11894382985923903979,9485622643104290083],[5617561700813657648,9150526953144641040,5053509220889776834]], "path_len": 11}, {"layer": 1, "d": 1, "position": 745, "leaf": 372, "slot": 1, "values": [[17441972696003475220,9388283442433602320,4390345575578824170],[5133627800531223425,11914988783658563617,14937632697871883584]], "path_len": 10}, {"layer": 2, "d": 1, "position": 372, "leaf": 186, "slot": 0, "values": [[8133041217017086758,1820646995207011242,11484845985140404156],[915012823390639608,17635150337718406583,5780426687318592651]], "path_len": 9}, {"layer": 3, "d": 1, "position": 186, "leaf": 93, "slot": 0, "values": [[10340667171407563529,1547279552216815127,6915765815699207391],[3154596510699264083,818314717512897513,17101621809342283826]], "path_len": 8}, {"layer": 4, "d": 1, "position": 93, "leaf": 46, "slot": 1, "values": [[1886552692492201400,5423010926098300279,15295999817653895971],[1768335035148138120,7519399748343183899,9919504600048365073]], "path_len": 7}, {"layer": 5, "d": 1, "position": 46, "leaf": 23, "slot": 0, "values": [[11863182194811651752,13735783487106290491,14167096383978964230],[17804173782191017059,1101561508558957260,5395509579711619792]], "path_len": 6}, {"layer": 6, "d": 1, "position": 23, "leaf": 11, "slot": 1, "values": [[14734565326035031259,17231263762901376046,10981121081937478029],[1400117789794109009,2476100258917768647,7408475937295308469]], "path_len": 5}, {"layer": 7, "d": 1, "position": 11, "leaf": 5, "slot": 1, "values": [[8182145593033563858,4641852677457838421,8561738423708568284],[17635728374138200680,13543955077887316863,1304709511706523848]], "path_len": 4}]}, + {"iota": 1846, "trace_leaf": 1846, "trace_path_len": 12, "deep": [2284487697263572951,17954063421123594597,12734531888209557865], "terminal_position": 7, "layers": [{"layer": 0, "d": 1, "position": 1846, "leaf": 923, "slot": 0, "values": [[2284487697263572951,17954063421123594597,12734531888209557865],[470910654823987329,4406533214267543541,17643437692058643171]], "path_len": 11}, {"layer": 1, "d": 1, "position": 923, "leaf": 461, "slot": 1, "values": [[13111132197997166221,11524508225265192094,12264127282465523178],[12430851011513078654,15525325890315442870,9377900677550954376]], "path_len": 10}, {"layer": 2, "d": 1, "position": 461, "leaf": 230, "slot": 1, "values": [[12783785382459763502,8900401989734896668,2244166906061745250],[10849887253553603615,4793736872544089110,6359342210468001257]], "path_len": 9}, {"layer": 3, "d": 1, "position": 230, "leaf": 115, "slot": 0, "values": [[8588935302892386269,8551761774913690890,4520450724077434230],[18374304396057544181,18384245929379916003,7814889479906253757]], "path_len": 8}, {"layer": 4, "d": 1, "position": 115, "leaf": 57, "slot": 1, "values": [[5267360283434351791,15392776900246357898,4943372451034957132],[15726203054000898387,8814074561425959376,6200283473451399643]], "path_len": 7}, {"layer": 5, "d": 1, "position": 57, "leaf": 28, "slot": 1, "values": [[8964694237736672537,2508030093710076938,2895555912020756696],[1316945196546810175,9540203727253325223,3428063813032906148]], "path_len": 6}, {"layer": 6, "d": 1, "position": 28, "leaf": 14, "slot": 0, "values": [[5827834535601424539,2126802115856515298,18004136256491770090],[9108720695214937294,5361393747591213366,673049126834026586]], "path_len": 5}, {"layer": 7, "d": 1, "position": 14, "leaf": 7, "slot": 0, "values": [[5890571997869690666,10800464999013389735,9378580896967811195],[704334176429400473,7326528124909318680,5250527536912250307]], "path_len": 4}]}, + {"iota": 3542, "trace_leaf": 3542, "trace_path_len": 12, "deep": [11685830694059955100,12253131036674908176,5644896282847145797], "terminal_position": 13, "layers": [{"layer": 0, "d": 1, "position": 3542, "leaf": 1771, "slot": 0, "values": [[11685830694059955100,12253131036674908176,5644896282847145797],[6796601152078455260,4829064821059167185,8422718924143860404]], "path_len": 11}, {"layer": 1, "d": 1, "position": 1771, "leaf": 885, "slot": 1, "values": [[10348593929766088319,600276997320945892,11551193361309397901],[4497425865068046782,7711618918678573010,3670607890048107603]], "path_len": 10}, {"layer": 2, "d": 1, "position": 885, "leaf": 442, "slot": 1, "values": [[4549713374957844263,14882952107271375459,15888844724609212573],[11867586896494500539,5382822032643071953,2547573499666355760]], "path_len": 9}, {"layer": 3, "d": 1, "position": 442, "leaf": 221, "slot": 0, "values": [[179249684856242416,8237726877005160673,11377156407621955226],[16338343630712151739,4547151014003636050,545748730564614338]], "path_len": 8}, {"layer": 4, "d": 1, "position": 221, "leaf": 110, "slot": 1, "values": [[17445710063408886615,3577256811508087571,13704116923286529205],[16582893718205853404,6964390291711440128,11944101285969035096]], "path_len": 7}, {"layer": 5, "d": 1, "position": 110, "leaf": 55, "slot": 0, "values": [[13599868760913890866,8792505734133014168,15701834533643325861],[17796919121337590328,5161833031535532699,595353643473481312]], "path_len": 6}, {"layer": 6, "d": 1, "position": 55, "leaf": 27, "slot": 1, "values": [[10120435360324233608,4278971934281013570,4137924372760735235],[16590085914178287331,864859356312877269,16418927990479178093]], "path_len": 5}, {"layer": 7, "d": 1, "position": 27, "leaf": 13, "slot": 1, "values": [[16002614920667576628,9559377662140985339,2909311776212759241],[4909556401234084390,18251274463717352852,444518797285025356]], "path_len": 4}]} + ] +} diff --git a/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_pair.rkyv b/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_pair.rkyv new file mode 100644 index 000000000..666fd43a2 Binary files /dev/null and b/crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_pair.rkyv differ diff --git a/prover/src/bin/compute_static_commitments.rs b/prover/src/bin/compute_static_commitments.rs index 3f7bc9fa7..515c92aba 100644 --- a/prover/src/bin/compute_static_commitments.rs +++ b/prover/src/bin/compute_static_commitments.rs @@ -10,6 +10,12 @@ //! Run with: //! cargo run --bin compute_static_commitments --release //! +//! `--layout row` prints the ONE-ROW (S2) twins instead — the same columns +//! committed with one LDE row per leaf — for `STATIC_BLOWUP_FACTORS_ONE_ROW`; +//! they are pasted into the `*_one_row` match bodies next to each constant +//! and pinned by the one-row drift tests. `--layout pair` (the default) is +//! the output above, unchanged. +//! //! ⚠ On a hash-pin change run this FIRST and paste before `compute_lfm_registry`: //! the registry embeds these constants (slots 13 and 14 of every entry, and //! `program_id` folds them), so a registry generated before the paste carries @@ -21,8 +27,11 @@ //! appropriate to bless new bytes. A hash-pin change is one such time, and it //! regenerates all four families together (`prover/src/hash_pin.rs`). -use lambda_vm_prover::tables::{STATIC_BLOWUP_FACTORS, bitwise, keccak_rc, page}; +use lambda_vm_prover::tables::{ + STATIC_BLOWUP_FACTORS, STATIC_BLOWUP_FACTORS_ONE_ROW, bitwise, keccak_rc, page, +}; use stark::config::Commitment; +use stark::leaf_layout::LeafLayout; use stark::proof::options::GoldilocksCubicProofOptions; fn format_commitment(commitment: &Commitment) -> String { @@ -41,17 +50,40 @@ fn format_commitment(commitment: &Commitment) -> String { out } +/// `--layout pair|row` (default `pair`). Anything else aborts: a typo must +/// not print the other layout's constants under this one's name. +fn layout_arg() -> LeafLayout { + let args: Vec = std::env::args().skip(1).collect(); + match args.as_slice() { + [] => LeafLayout::RowPair, + [flag, value] if flag == "--layout" => match value.as_str() { + "pair" => LeafLayout::RowPair, + "row" => LeafLayout::Row, + other => panic!("--layout must be `pair` or `row`, got `{other}`"), + }, + other => panic!("usage: compute_static_commitments [--layout pair|row], got {other:?}"), + } +} + fn main() { + let layout = layout_arg(); + let blowups = match layout { + LeafLayout::RowPair => STATIC_BLOWUP_FACTORS, + LeafLayout::Row => STATIC_BLOWUP_FACTORS_ONE_ROW, + }; + println!("// leaf layout: {layout:?}"); + // The one-row twins go into the `*_one_row` functions beside each constant. + let suffix = if layout.is_one_row() { "_one_row" } else { "" }; println!( - "// Paste these match arms into the `static_commitment` match bodies\n\ + "// Paste these match arms into the `static_commitment{suffix}` match bodies\n\ // in `prover/src/tables/{{bitwise,keccak_rc}}.rs` and the\n\ - // `static_zero_page_commitment` / `static_private_page_commitment`\n\ + // `static_zero_page_commitment{suffix}` / `static_private_page_commitment{suffix}`\n\ // match bodies in `prover/src/tables/page.rs`.\n" ); let zero_page_config = page::PageConfig::zero_init(0); - for &blowup in STATIC_BLOWUP_FACTORS { + for &blowup in blowups { let options = match GoldilocksCubicProofOptions::with_blowup(blowup) { Ok(o) => o, Err(e) => { @@ -60,10 +92,11 @@ fn main() { } }; - let bitwise = bitwise::compute_preprocessed_commitment(&options); - let keccak_rc = keccak_rc::compute_preprocessed_commitment(&options); - let zero_page = page::compute_precomputed_commitment(&zero_page_config, &options); - let private_page = page::compute_offset_only_commitment(&options); + let bitwise = bitwise::compute_preprocessed_commitment_with(&options, layout); + let keccak_rc = keccak_rc::compute_preprocessed_commitment_with(&options, layout); + let zero_page = + page::compute_precomputed_commitment_with(&zero_page_config, &options, layout); + let private_page = page::compute_offset_only_commitment_with(&options, layout); println!( "// blowup_factor = {blowup}\n\ diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index fc411f89e..516ad44fb 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -250,8 +250,8 @@ pub(crate) fn global_memory_air( // `address_lo = page_base_lo + OFFSET` is prover-chosen and the genesis // token can name an arbitrary address. GLOBAL_MEMORY's OFFSET column is // identical to PAGE's, so the same commitment serves both. - return air.with_preprocessed_columns( - page::private_page_preprocessed_commitment(opts), + return air.with_lazy_preprocessed_columns( + page::private_page_lazy_commitment(opts), page::NUM_PREPROCESSED_COLS_PRIVATE, Arc::new(|| vec![page::offset_column()]), ); @@ -261,18 +261,27 @@ pub(crate) fn global_memory_air( // compares these instead. They are PAGE's — GLOBAL_MEMORY's preprocessed // prefix is the same OFFSET and INIT, which is why the same commitment // serves both. - let commitment = match preprocessed { - Some(commitment) => LazyCommitment::ready(commitment), - None => { - let config = config.clone(); - let options = opts.clone(); - LazyCommitment::deferred(move || { - if config.init_values.is_some() { - page::compute_precomputed_commitment(&config, &options) - } else { - page::zero_init_preprocessed_commitment(&options) - } - }) + // Both leaf layouts (S2): a zero-init page's one-row root is the static + // twin, a data page's is computed on first use; a supplied root is a + // row-pair root and never stands in for the other layout. + let commitment = if config.init_values.is_some() { + page::data_page_lazy_commitment(config, opts, preprocessed) + } else { + match preprocessed { + Some(c) => page::zero_init_lazy_commitment_from(c, opts), + None => { + let options = opts.clone(); + LazyCommitment::deferred(move || page::zero_init_preprocessed_commitment(&options)) + .with_one_row({ + let options = opts.clone(); + move || { + page::zero_init_preprocessed_commitment_for( + &options, + stark::leaf_layout::LeafLayout::Row, + ) + } + }) + } } }; let config = config.clone(); diff --git a/prover/src/lfm/airs.rs b/prover/src/lfm/airs.rs index e7ebc3ee2..73b51eabd 100644 --- a/prover/src/lfm/airs.rs +++ b/prover/src/lfm/airs.rs @@ -900,6 +900,33 @@ impl LfmAirs { } } + /// This set with every preprocessed chip's ONE-ROW (S2) root attached: + /// what `precomputed_commitment_for(Row)` returns when the STARK prover or + /// verifier resolves that chip to one row. Without it a one-row chip is a + /// hard miss, never a recompute. `KECCAK_RND` has no preprocessed columns. + pub fn with_one_row_roots(mut self, one_row: &super::registry::LfmOneRowRoots) -> Self { + let r = &one_row.roots; + self.const_ = self.const_.with_one_row_commitment(r[0]); + self.balu = self.balu.with_one_row_commitment(r[1]); + self.xalu = self.xalu.with_one_row_commitment(r[2]); + self.select = self.select.with_one_row_commitment(r[3]); + self.bitdec = self.bitdec.with_one_row_commitment(r[4]); + self.hash = self.hash.with_one_row_commitment(r[5]); + self.keccak = self.keccak.with_one_row_commitment(r[6]); + self.lanes = self.lanes.with_one_row_commitment(r[7]); + self.hint = self.hint.with_one_row_commitment(r[8]); + self.public = self.public.with_one_row_commitment(r[9]); + self.range = self.range.with_one_row_commitment(r[10]); + self.blake3 = std::mem::take(&mut self.blake3) + .into_iter() + .enumerate() + .map(|(i, air)| air.with_one_row_commitment(one_row.blake3_chunk_roots.get(i).copied())) + .collect(); + self.keccak_rc = self.keccak_rc.with_one_row_commitment(r[13]); + self.bitwise = self.bitwise.with_one_row_commitment(r[14]); + self + } + /// Number of `KECCAK_RND` instances this set was built with. pub fn keccak_rnd_chunks(&self) -> usize { self.keccak_rnd.len() diff --git a/prover/src/lfm/commit.rs b/prover/src/lfm/commit.rs index 1c673b442..223f00058 100644 --- a/prover/src/lfm/commit.rs +++ b/prover/src/lfm/commit.rs @@ -8,8 +8,9 @@ //! keygen in this framework). use math::polynomial::Polynomial; -use stark::commitment::{ROWS_PER_LEAF, commit_bit_reversed_with}; +use stark::commitment::commit_bit_reversed_with; use stark::config::Commitment; +use stark::leaf_layout::LeafLayout; use stark::proof::options::ProofOptions; use stark::prover::evaluate_polynomial_on_lde_domain; @@ -87,8 +88,15 @@ pub fn lde_columns(columns: &[Vec], options: &ProofOptions) -> Vec> columns.iter().map(expand).collect() } -/// Commits an already-expanded LDE column matrix. +/// Commits an already-expanded LDE column matrix with today's row-pair +/// leaves. pub fn commit_lde_columns(lde_columns: &[Vec]) -> Commitment { + commit_lde_columns_with(lde_columns, LeafLayout::RowPair) +} + +/// [`commit_lde_columns`] under an explicit trace-tree leaf layout (S2: a +/// one-row table's preprocessed root is this at [`LeafLayout::Row`]). +pub fn commit_lde_columns_with(lde_columns: &[Vec], layout: LeafLayout) -> Commitment { // ★ Under the block path's PIN, not `stark`'s default aliases. These commit // the production tables whose roots `lfm_program_id` names, so the hash that // BUILDS them and the hash the program identity CLAIMS have to be the same @@ -97,7 +105,7 @@ pub fn commit_lde_columns(lde_columns: &[Vec]) -> Commitment { let (_, root) = commit_bit_reversed_with::< GoldilocksField, ::Batched, - >(lde_columns, ROWS_PER_LEAF) + >(lde_columns, layout.rows_per_leaf()) .expect("Merkle build failed for LFM column group"); root } @@ -107,6 +115,15 @@ pub fn commit_columns(columns: &[Vec], options: &ProofOptions) -> Commitment commit_lde_columns(&lde_columns(columns, options)) } +/// [`commit_columns`] under an explicit leaf layout. +pub fn commit_columns_with( + columns: &[Vec], + options: &ProofOptions, + layout: LeafLayout, +) -> Commitment { + commit_lde_columns_with(&lde_columns(columns, options), layout) +} + /// A [`ColumnGroup`]'s data, column-major (the commit pipeline's input shape). /// /// A strided gather: the group is row-major, so column `c` is read with stride @@ -196,14 +213,28 @@ pub fn commit_group_device_or_host( label: &str, group: &ColumnGroup, options: &ProofOptions, +) -> Commitment { + commit_group_device_or_host_with(label, group, options, LeafLayout::RowPair) +} + +/// [`commit_group_device_or_host`] under an explicit leaf layout. The device +/// commit (`gpu_lde::try_commit_row_major_with`) builds the tree with +/// `layout.rows_per_leaf()` rows per leaf, so a one-row root (S2) takes the +/// device like a row-pair one. +pub fn commit_group_device_or_host_with( + label: &str, + group: &ColumnGroup, + options: &ProofOptions, + layout: LeafLayout, ) -> Commitment { #[cfg(feature = "cuda")] if device_artifacts() && group.padded_rows > 0 && group.width > 0 { - let set = stark::device_set::commit_device_set( + let set = stark::device_set::commit_device_set_rpl( group.padded_rows, group.width, options.blowup_factor as usize, true, + layout.rows_per_leaf(), ); DEVICE_PEAK_BYTES.fetch_max(set.total(), std::sync::atomic::Ordering::Relaxed); // ⛔ ROUND-3 TREE PROBE (diagnostic, OFF by default). The card permit is @@ -219,7 +250,7 @@ pub fn commit_group_device_or_host( // would otherwise not have. The measurement cannot perturb what it // measures. let probe_t = super::tree_probe::enabled().then(std::time::Instant::now); - let committed = stark::gpu_lde::try_commit_row_major::< + let committed = stark::gpu_lde::try_commit_row_major_with::< GoldilocksField, ::Batched, >( @@ -229,6 +260,7 @@ pub fn commit_group_device_or_host( group.width, options.blowup_factor as usize, &FE::from(options.coset_offset), + layout.rows_per_leaf(), ); if let Some(t) = probe_t { super::tree_probe::note_device_commit(t.elapsed().as_nanos() as u64); @@ -240,7 +272,7 @@ pub fn commit_group_device_or_host( } let _ = label; HOST_GROUPS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - commit_lde_columns(&lde_columns(&group_columns(group), options)) + commit_lde_columns_with(&lde_columns(&group_columns(group), options), layout) } /// Commits one instruction column group. @@ -314,6 +346,48 @@ mod device_parity { } } + /// S2: the one-row artifact root on the device equals the + /// host one-row root at the same production shapes, and differs from the + /// row-pair root (a device that ignored the layout would equal it). The + /// device one-row tree counter must move once per group, so a host + /// fallback fails this test instead of comparing host with host. + #[test] + fn the_one_row_device_commit_matches_the_host_commit_above_the_floor() { + let options = GoldilocksCubicProofOptions::with_blowup(4).expect("options"); + assert!( + device_artifacts(), + "LFM_DEVICE_ARTIFACTS=0: this test would compare a host root with a host root" + ); + let before = stark::gpu_lde::gpu_one_row_trees(); + let shapes = [(4_096usize, 1usize), (8_192, 20), (4_096, 134)]; + for (rows, width) in shapes { + let g = group(rows, width); + let lde = lde_columns(&group_columns(&g), &options); + let host = commit_lde_columns_with(&lde, LeafLayout::Row); + let pair = commit_lde_columns_with(&lde, LeafLayout::RowPair); + let device = commit_group_device_or_host_with( + "device_parity_one_row", + &g, + &options, + LeafLayout::Row, + ); + assert_eq!( + device, host, + "{rows}x{width}: the one-row device root differs from the host one-row root" + ); + assert_ne!( + device, pair, + "{rows}x{width}: the one-row root equals the row-pair root" + ); + } + let moved = stark::gpu_lde::gpu_one_row_trees() - before; + assert!( + moved >= shapes.len() as u64, + "only {moved} one-row device trees for {} groups: the device declined (host fallback)", + shapes.len() + ); + } + /// And the control: `LFM_DEVICE_ARTIFACTS=0` must reach the host pass. Read /// once per process, so this asserts the knob's VALUE agrees with the branch /// rather than flipping it mid-run. diff --git a/prover/src/lfm/constraint_tests.rs b/prover/src/lfm/constraint_tests.rs index c19f43349..bda7e7414 100644 --- a/prover/src/lfm/constraint_tests.rs +++ b/prover/src/lfm/constraint_tests.rs @@ -939,7 +939,10 @@ pub(super) fn open_sub_proof( // single-table case (no per-table domain separator). let mut transcript = crate::hash_pin::block_transcript(&[]); if air.is_preprocessed() { - transcript.append_bytes(&air.precomputed_commitment()); + transcript.append_bytes(&super::epoch_verify_tests::layout_precomputed_commitment( + air, + view.trace_length(), + )); } transcript.append_bytes(view.lde_trace_main_merkle_root()); let rap_challenges: Vec = if air.has_aux_trace() { diff --git a/prover/src/lfm/epoch.rs b/prover/src/lfm/epoch.rs index 890ac1cc7..4263fe96b 100644 --- a/prover/src/lfm/epoch.rs +++ b/prover/src/lfm/epoch.rs @@ -299,10 +299,12 @@ impl TableChallengeShape { } /// Bits one query index carries — `sample_u64(lde_length >> 1)` - /// (`verifier.rs:138-141`), so one bit narrower than the domain, which is - /// exactly the Merkle depth the walk consumes. + /// (`verifier.rs:138-141`), so one bit narrower than the domain, for row + /// pairs; `sample_u64(lde_length)`, the whole domain, under one-row leaves + /// (S2, `LeafLayout::query_bound`). Either way exactly the Merkle depth the + /// walk consumes — the FRI shape's [`FriShape::index_bits`], one definition. pub fn index_bits(&self) -> usize { - self.log2_lde_length() as usize - 1 + self.fri.index_bits() } fn check(&self) { @@ -705,6 +707,27 @@ pub(super) fn nonce_halves(b: &mut LfmBuilder, nonce: Felt) -> [Felt; 2] { super::transcript_replay::felt_be_halves(b, nonce) } +// The transcript-order mutation (tamper T6, in-guest): a test build can +// replay a one-row table with a ζ drawn BEFORE the input root and watch the +// challenge differential go red. Production has no switch. +#[cfg(test)] +thread_local! { + pub(super) static ZETA_BEFORE_INPUT_ROOT: core::cell::Cell = + const { core::cell::Cell::new(false) }; +} + +#[inline] +fn zeta_before_input_root() -> bool { + #[cfg(test)] + { + ZETA_BEFORE_INPUT_ROOT.with(|c| c.get()) + } + #[cfg(not(test))] + { + false + } +} + /// Replay one table's rounds 2 to 4 against a FORKED transcript. /// /// `t` must be the fork ([`fork_table`]), not the shared transcript. Returns @@ -785,11 +808,20 @@ pub fn emit_table_challenges( // ---- Round 4: γ, the interleaved FRI commit phase, then the queries. let gamma = t.sample_ext(b); - let mut zetas = Vec::with_capacity(shape.fri.num_committed() + 1); - for root in absorbs.fri_roots { + let mut zetas = Vec::with_capacity(shape.fri.num_zetas()); + for (j, root) in absorbs.fri_roots.iter().enumerate() { // Sample FIRST, absorb SECOND — a ζ drawn after its own layer root is a // challenge the prover answers rather than one that binds them. - zetas.push(t.sample_ext(b)); + // + // ★ Except the one-row INPUT tree (S2): root 0 is + // the DEEP codeword itself, committed BEFORE any folding challenge — + // absorbed right after γ, with no ζ ahead of it. A ζ drawn before it + // would let the prover pick the codeword after seeing λ₁; the host + // replay (`verifier.rs`, `replay_rounds_after_round_1`) + // is the same loop. + if !(shape.fri.one_row() && j == 0) || zeta_before_input_root() { + zetas.push(t.sample_ext(b)); + } root.absorb(b, t); } if shape.fri.total_folds() > 0 { diff --git a/prover/src/lfm/epoch_tests.rs b/prover/src/lfm/epoch_tests.rs index 18e0fca6e..6b2819d98 100644 --- a/prover/src/lfm/epoch_tests.rs +++ b/prover/src/lfm/epoch_tests.rs @@ -92,7 +92,11 @@ fn host_table( let trace_length = view.trace_length(); let log2_trace_length = trace_length.trailing_zeros(); let log2_blowup = (opts.blowup_factor as usize).trailing_zeros(); - let fri = FriShape::from_options(opts, log2_trace_length + log2_blowup); + let fri = FriShape::for_layout( + opts, + log2_trace_length + log2_blowup, + stark::leaf_layout::table_leaf_layout(air, trace_length), + ); let ood_c = view.trace_ood_evaluations(); let ood_n = view.trace_ood_next_evaluations(); @@ -119,7 +123,9 @@ fn host_table( HostTable { shape, - precomputed_root: air.is_preprocessed().then(|| air.precomputed_commitment()), + precomputed_root: air.is_preprocessed().then(|| { + super::epoch_verify_tests::layout_precomputed_commitment(air, view.trace_length()) + }), main_root: *view.lde_trace_main_merkle_root(), aux_root: view.lde_trace_aux_merkle_root().copied(), contribution: view.bus_table_contribution(), @@ -338,6 +344,84 @@ fn the_challenge_replay_matches_production() { } } +/// ★ S2 (one-row leaves): the in-machine replay of a +/// one-row table reproduces production's challenges — the input root absorbed +/// right after `γ` with NO challenge ahead of it, one `ζ` per committed layer +/// (layer `j` folds with `ζ_j`), and the query indices sampled over the WHOLE +/// LDE (`log2(lde)` bits, the upper half reached). Swept over folding counts 0 +/// (the zero-fold case: no input tree at all), 1+ layers. The transcript order +/// is load-bearing: the replay with a `ζ` drawn BEFORE the input root (the test +/// mutation) diverges from production wherever an input tree exists. +#[test] +fn the_one_row_challenge_replay_matches_production() { + for (boundaries, fri) in [ + (4usize, stark::proof::options::FriMode::Pair), + (512, stark::proof::options::FriMode::Pair), + (2048, stark::proof::options::FriMode::Pair), + (2048, stark::proof::options::FriMode::Dp), + ] { + let mut opts = + stark::proof::options::GoldilocksCubicProofOptions::with_blowup(2).expect("blowup 2"); + opts.format.one_row = stark::proof::options::OneRowMode::On; + opts.format.fri_mode = fri; + let (air, proof) = super::fri_tests::folding_fixture_with(boundaries, opts); + let h = host_table(&*air, &proof); + let label = format!("{boundaries} boundaries, fri={fri:?}"); + assert!(h.shape.fri.one_row(), "{label}"); + assert_eq!(h.shape.index_bits(), h.shape.log2_lde_length() as usize); + assert_eq!( + h.zetas.len(), + h.shape.fri.num_committed(), + "{label}: one challenge per committed layer (the input tree has none)" + ); + assert_eq!(h.zetas.len(), h.shape.fri.num_zetas()); + + let (beta, z, gamma, zetas, iotas) = run(&h); + assert_eq!(beta, h.beta, "{label}: beta"); + assert_eq!(z, h.z, "{label}: z"); + assert_eq!(gamma, h.gamma, "{label}: gamma"); + assert_eq!(zetas, h.zetas, "{label}: the FRI zetas"); + let want: Vec = h.iotas.iter().map(|i| *i as u64).collect(); + assert_eq!(iotas, want, "{label}: the query indices"); + let lde = 1u64 << h.shape.log2_lde_length(); + if h.iotas.len() >= 8 { + assert!( + want.iter().any(|&r| r >= lde / 2), + "{label}: one-row indices range over the whole LDE" + ); + } + + if h.shape.fri.num_committed() > 0 { + super::epoch::ZETA_BEFORE_INPUT_ROOT.with(|c| c.set(true)); + let mutated = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let program = challenge_program(&h); + let arenas = challenge_arenas(&h); + execute(&program, &arenas, &crate::hash_pin::BLOCK_HASHER) + .ok() + .map(|exec| { + (0..h.zetas.len()) + .map(|k| word_as_ext(&exec.public_words[3 + k].1).expect("ext")) + .collect::>() + }) + })); + super::epoch::ZETA_BEFORE_INPUT_ROOT.with(|c| c.set(false)); + let mutated = mutated.expect("the mutated replay still emits"); + assert_ne!( + mutated.as_ref(), + Some(&h.zetas), + "{label}: a ζ drawn before the input root must move the challenges" + ); + } + println!( + "{label}: {} layers, {} zetas, {} queries over 2^{} — replay == production", + h.shape.fri.num_committed(), + h.zetas.len(), + h.iotas.len(), + h.shape.log2_lde_length() + ); + } +} + /// ★ Two defects the differential above CANNOT see, pinned so they are not /// mistaken for coverage. /// @@ -571,25 +655,39 @@ pub(super) fn prep_source_census(e: &RealEpoch) -> (usize, usize, usize) { /// really buys is the failure mode — a preprocessed AIR whose root matches /// nothing known is a root the machine has no binding for, and this panics /// rather than hinting it. +/// +/// `layout` is the table's resolved trace-tree leaf layout (S2): every +/// candidate is recomputed AT that layout, so a one-row table's root is matched +/// against the one-row candidates only (a row-pair root never stands in). fn prep_source( root: Commitment, opts: &crate::ProofOptions, elf: &executor::elf::Elf, register_init: &[u32], reg_fini: &[u32], + layout: stark::leaf_layout::LeafLayout, ) -> PrepSource { use crate::tables::{bitwise, decode, keccak_rc, page, register}; - if root == bitwise::preprocessed_commitment(opts) - || root == keccak_rc::preprocessed_commitment(opts) - || root == page::zero_init_preprocessed_commitment(opts) + if Some(root) == bitwise::preprocessed_commitment_for(opts, layout) + || Some(root) == keccak_rc::preprocessed_commitment_for(opts, layout) + || Some(root) == page::zero_init_preprocessed_commitment_for(opts, layout) { return PrepSource::Constant(root); } - if root == register::compute_precomputed_commitment_with_fini(opts, register_init, reg_fini) { + if root + == register::compute_precomputed_commitment_with_fini_layout( + opts, + register_init, + reg_fini, + layout, + ) + { return PrepSource::Register(root); } - if root == decode::commitment_from_elf(elf, opts).expect("the DECODE commitment must compute") { + let instructions = + decode::instructions_from_elf(elf).expect("the DECODE commitment must compute"); + if root == decode::compute_precomputed_commitment_with(&instructions, opts, layout) { return PrepSource::ElfDependent(root); } panic!( @@ -1087,21 +1185,40 @@ fn harvest_real_epoch( // ---- Phase A, transcribed from `multi_verify_views:1160-1227`. let mut transcript = seed(); let mut phase_a = Vec::new(); + // S2: the REGISTER table's leaf layout (the in-circuit register commitment + // is emitted at its `rows_per_leaf`) and the DECODE root Phase A absorbs + // (the attestation folds that very root) — both at the table's resolved + // layout, which is today's row pair at the default format. + let mut register_layout = stark::leaf_layout::LeafLayout::RowPair; + let mut absorbed_decode_root = decode_root; for (idx, air) in refs.iter().enumerate() { let v = view.get(idx); if air.is_preprocessed() { - let prep = air.precomputed_commitment(); + let layout = stark::leaf_layout::table_leaf_layout(*air, v.trace_length()); + let prep = air + .precomputed_commitment_for(layout) + .unwrap_or_else(|| panic!("table {idx}: no precomputed root at {layout:?}")); transcript.append_bytes(&prep); transcript.append_bytes(v.lde_trace_main_merkle_root()); - phase_a.push(( - Some(prep_source(prep, opts, elf, ®ister_init, ®_fini)), - *v.lde_trace_main_merkle_root(), - )); + let source = prep_source(prep, opts, elf, ®ister_init, ®_fini, layout); + match source { + PrepSource::Register(_) => register_layout = layout, + PrepSource::ElfDependent(root) => absorbed_decode_root = root, + PrepSource::Constant(_) => {} + } + phase_a.push((Some(source), *v.lde_trace_main_merkle_root())); } else { transcript.append_bytes(v.lde_trace_main_merkle_root()); phase_a.push((None, *v.lde_trace_main_merkle_root())); } } + if opts.format.one_row == stark::proof::options::OneRowMode::Off { + assert_eq!( + absorbed_decode_root, decode_root, + "at the default format Phase A absorbs today's DECODE root" + ); + assert_eq!(register_layout, stark::leaf_layout::LeafLayout::RowPair); + } let needs_lookup_challenges = refs.iter().any(|a| a.has_aux_trace()); assert!(needs_lookup_challenges, "an epoch uses LogUp"); let lookup_challenges: Vec = (0..stark::lookup::LOGUP_NUM_CHALLENGES) @@ -1185,11 +1302,17 @@ fn harvest_real_epoch( reg_shape: super::programs::RegisterDerivationShape { blowup: opts.blowup_factor as usize, coset_offset: opts.coset_offset, + // The REGISTER table's own leaf layout: 2 at the default format. + rows_per_leaf: register_layout.rows_per_leaf(), }, + // The attestation folds the DECODE root Phase A absorbed — the + // row-pair `decode_root` at the default format (asserted below), the + // DECODE table's one-row root when S2 resolves it to one row (the + // attestation id moves with the knob). expected_program_id: crate::recursion::program_id_from_digest( &crate::statement::elf_digest(&elf_bytes), elf.entry_point, - &decode_root, + &absorbed_decode_root, &[], ), tables, @@ -1332,6 +1455,7 @@ pub(super) fn from_proof_gate_options() -> crate::ProofOptions { coset_offset: 3, grinding_factor: 1, fri_final_poly_log_degree: 7, + format: stark::proof::options::ProofFormat::DEFAULT, } } @@ -1530,14 +1654,20 @@ pub(super) fn host_table_forked( ood_current_dims: (ood_c.width(), ood_c.height()), ood_next_dims: (ood_n.width(), ood_n.height()), num_parts: view.composition_poly_parts_ood_evaluation().len(), - fri: FriShape::from_options(opts, log2_trace_length + log2_blowup), + fri: FriShape::for_layout( + opts, + log2_trace_length + log2_blowup, + stark::leaf_layout::table_leaf_layout(air, view.trace_length()), + ), grinding_factor: opts.grinding_factor, num_queries: opts.fri_number_of_queries, }; HostTable { shape, - precomputed_root: air.is_preprocessed().then(|| air.precomputed_commitment()), + precomputed_root: air.is_preprocessed().then(|| { + super::epoch_verify_tests::layout_precomputed_commitment(air, view.trace_length()) + }), main_root: *view.lde_trace_main_merkle_root(), aux_root: view.lde_trace_aux_merkle_root().copied(), contribution: view.bus_table_contribution(), @@ -2200,6 +2330,7 @@ pub(super) fn epoch_arena_words(e: &RealEpoch, with_legs: bool) -> Vec usize { self.num_queries * self.fri.query_words(digest_words) } + + /// Arena words this sub-proof's Merkle caps occupy, once per sub-proof: + /// the committed matrices' caps (group order), then the committed FRI + /// layers' (layer order). Zero at the default format. + pub fn cap_words(&self, digest_words: usize) -> usize { + self.sub.cap_words(digest_words) + self.fri.cap_words(digest_words) + } + + fn check_caps(&self) { + assert_eq!( + self.sub.trace_cap, + self.fri + .format + .merkle_cap + .height(self.num_queries, self.sub.merkle_depth), + "the trace trees' cap is the format's, at their depth and query count" + ); + } } /// The two arenas one sub-proof's query verification reads, in declaration @@ -156,14 +180,22 @@ pub struct TableQueryArenas { /// Per query, per committed FRI layer: the symmetric evaluation then the /// sibling digests. pub fri: ArenaId, + /// The sub-proof's Merkle caps ([`TableVerifyShape::cap_words`]), declared + /// only when the format caps some tree — so the default format's arena + /// schema, program and program id are today's. + pub caps: Option, } /// Declare the query arenas for one sub-proof. pub fn declare_table_arenas(b: &mut LfmBuilder, shape: &TableVerifyShape) -> TableQueryArenas { let digest_words = super::edsl::digest_words(b) as usize; + let openings = b.declare_arena(shape.opening_words(digest_words) as u32); + let fri = b.declare_arena(shape.fri_words(digest_words) as u32); + let cap_words = shape.cap_words(digest_words); TableQueryArenas { - openings: b.declare_arena(shape.opening_words(digest_words) as u32), - fri: b.declare_arena(shape.fri_words(digest_words) as u32), + openings, + fri, + caps: (cap_words > 0).then(|| b.declare_arena(cap_words as u32)), } } @@ -172,7 +204,8 @@ pub struct TableInputs<'a> { /// The precomputed-columns root, when the AIR is preprocessed. /// /// Production never reads this from the proof: it takes - /// `air.precomputed_commitment()`, absorbs THAT, and rejects a proof whose + /// `air.precomputed_commitment_for(layout)` (the root of the table's leaf + /// layout), absorbs THAT, and rejects a proof whose /// copy disagrees (`verifier.rs:1184-1209`). So the cells here are the ones /// Phase A absorbed, and the equality production checks explicitly is, in /// this machine, the absence of a second value. @@ -304,15 +337,44 @@ pub fn emit_table_verification( ); // ---- the FRI commitments, likewise from the transcript's own cells. - let fri = FriCommitments { - layers: absorbs - .fri_roots - .iter() - .map(|r| LayerCommitment::from_lanes(r.lanes.clone())) - .collect(), - zetas: challenges.zetas.clone(), - coeffs: absorbs.fri_coeffs.to_vec(), - }; + let layers = absorbs + .fri_roots + .iter() + .map(|r| LayerCommitment::from_lanes(r.lanes.clone())) + .collect(); + let mut fri = FriCommitments::new( + b, + shape.fri, + layers, + challenges.zetas.clone(), + absorbs.fri_coeffs.to_vec(), + ); + + // ---- the Merkle caps, once per tree, against the SAME root cells the + // transcript absorbed: the matrices in group order, + // then the FRI layers. Every opening below is checked against these cells. + let digest_words = super::edsl::digest_words(b) as usize; + assert_eq!( + arenas.caps.is_some(), + shape.cap_words(digest_words) > 0, + "a caps arena exists exactly when the format caps some tree" + ); + if let Some(caps) = arenas.caps { + let mut at = 0u32; + for c in &mut commitments { + at = c.hint_cap(b, caps, at, shape.sub.trace_cap); + } + assert_eq!(at as usize, shape.sub.cap_words(digest_words)); + let mut fri_at = at; + for (i, layer) in fri.layers.iter_mut().enumerate() { + fri_at = layer.hint_cap(b, caps, fri_at, shape.fri.layer_cap(i)); + } + assert_eq!( + fri_at as usize, + shape.cap_words(digest_words), + "the caps arena is filled exactly" + ); + } // ---- (4) per query: authenticate, fold DEEP, then fold FRI. let stride = shape @@ -324,14 +386,14 @@ pub fn emit_table_verification( let openings: Vec = groups .iter() .map(|g| { - let values = (0..g.num_values()) + let values = (0..shape.sub.group_values(g)) .map(|_| { let c = b.hint_word(arenas.openings, cursor); cursor += 1; c }) .collect(); - let siblings = (0..shape.sub.merkle_depth) + let siblings = (0..shape.sub.path_len()) .map(|_| { // The stride follows the DIGEST's width, not a literal. let d = super::edsl::hint_digest(b, arenas.openings, cursor); @@ -363,8 +425,8 @@ pub fn emit_table_verification( shape.fri, &fri, &FriQuery { - p0: out.deep.0, - p0_sym: out.deep.1, + p0: out.deep, + p0_sym: out.deep_sym, point: out.point, point_sym: out.point_sym, bits: &out.bits, @@ -422,7 +484,7 @@ pub fn leaf_permutations(shape: &SubProofShape) -> usize { shape .groups() .iter() - .map(|g| super::keccak_host::num_blocks(g.leaf_bytes())) + .map(|g| super::keccak_host::num_blocks(g.leaf_bytes_at(shape.rows_per_leaf()))) .sum() } @@ -471,10 +533,16 @@ pub const LFM_HASH_RATE_FELTS: usize = super::hash::HASH_DIGEST_FELTS; /// block at the candidate's rate 4. pub const FRI_LEAF_FELTS: usize = 6; -/// Felts one query's opening of a group covers, the felt-side counterpart of -/// [`super::sub_proof::GroupShape::leaf_bytes`]. +/// Felts one query's row-pair opening of a group covers, the felt-side +/// counterpart of [`super::sub_proof::GroupShape::leaf_bytes`]. pub fn group_leaf_felts(g: &super::sub_proof::GroupShape) -> usize { - g.num_values() * if g.is_ext { 3 } else { 1 } + group_leaf_felts_at(g, super::sub_proof::ROWS_PER_LEAF) +} + +/// [`group_leaf_felts`] at `rows_per_leaf` rows per leaf (1 under S2's +/// one-row leaves) — what the closed forms price, at the sub-proof's layout. +pub fn group_leaf_felts_at(g: &super::sub_proof::GroupShape, rows_per_leaf: usize) -> usize { + g.values_at(rows_per_leaf) * if g.is_ext { 3 } else { 1 } } /// Permutations a sponge of `rate_felts` spends absorbing `felts`, under keccak's @@ -504,7 +572,7 @@ pub fn leaf_permutations_at_rate(shape: &SubProofShape, rate_felts: usize) -> us shape .groups() .iter() - .map(|g| blocks_at_rate(group_leaf_felts(g), rate_felts)) + .map(|g| blocks_at_rate(group_leaf_felts_at(g, shape.rows_per_leaf()), rate_felts)) .sum() } @@ -523,7 +591,11 @@ pub fn leaf_permutations_at_rate(shape: &SubProofShape, rate_felts: usize) -> us /// take two blocks. The premise is gone rather than re-asserted; this function /// is what replaced it. pub fn fri_leaf_permutations_at_rate(fri: &FriShape, rate_felts: usize) -> usize { - fri.num_committed() * blocks_at_rate(FRI_LEAF_FELTS, rate_felts) + // Per layer: the pair's six felts under `pair`, a `2^d`-value group's + // `3·2^d` under a fold schedule (`FriShape::layer_leaf_felts`). + (0..fri.num_committed()) + .map(|j| blocks_at_rate(fri.layer_leaf_felts(j), rate_felts)) + .sum() } /// [`query_permutations`] at an arbitrary sponge rate. @@ -544,7 +616,7 @@ pub fn query_permutations_at_rate(shape: &TableVerifyShape, rate_felts: usize) - let groups = shape.sub.groups().len(); let per_query = leaf_permutations_at_rate(&shape.sub, rate_felts) + fri_leaf_permutations_at_rate(&shape.fri, rate_felts) - + groups * shape.sub.merkle_depth + + groups * shape.sub.path_len() + shape.fri.path_steps_per_query(); shape.num_queries * per_query } @@ -585,18 +657,37 @@ pub fn blocks_for(felts: usize, hash: WrapHash) -> usize { /// absorptions move. pub fn query_permutations_for(shape: &TableVerifyShape, hash: WrapHash) -> usize { let groups = shape.sub.groups().len(); + let rows = shape.sub.rows_per_leaf(); let leaves: usize = shape .sub .groups() .iter() - .map(|g| blocks_for(group_leaf_felts(g), hash)) + .map(|g| blocks_for(group_leaf_felts_at(g, rows), hash)) .sum(); - let fri_leaves = shape.fri.num_committed() * blocks_for(FRI_LEAF_FELTS, hash); + // Per committed layer: a pair leaf (six felts), or a `2^d`-value group. + let fri_leaves = shape.fri.leaf_permutations_per_query(hash); let per_query = - leaves + fri_leaves + groups * shape.sub.merkle_depth + shape.fri.path_steps_per_query(); + leaves + fri_leaves + groups * shape.sub.path_len() + shape.fri.path_steps_per_query(); shape.num_queries * per_query } +/// Permutations one sub-proof's Merkle cap checks cost, ONCE per sub-proof +/// (not per query): every capped tree hashes its `2^c` cap up to its root, +/// `2^c − 1` parents. Zero at the +/// default format. +pub fn cap_permutations(shape: &TableVerifyShape) -> usize { + shape.sub.cap_permutations() + shape.fri.cap_permutations() +} + +/// Every permutation one sub-proof's verification legs cost: +/// [`query_permutations_for`] (per query, with the capped path lengths) plus +/// [`cap_permutations`] (once). This is the closed form the emitted legs are +/// pinned against at every format; at the default it IS +/// [`query_permutations_for`]. +pub fn table_permutations_for(shape: &TableVerifyShape, hash: WrapHash) -> usize { + query_permutations_for(shape, hash) + cap_permutations(shape) +} + /// Keccak permutations one sub-proof's whole query verification costs, from /// shape alone. /// @@ -608,7 +699,7 @@ pub fn query_permutations_for(shape: &TableVerifyShape, hash: WrapHash) -> usize pub fn query_permutations(shape: &TableVerifyShape) -> usize { let groups = shape.sub.groups().len(); let per_query = leaf_permutations(&shape.sub) - + groups * shape.sub.merkle_depth + + groups * shape.sub.path_len() + shape.fri.permutations_per_query(); shape.num_queries * per_query } diff --git a/prover/src/lfm/epoch_verify_tests.rs b/prover/src/lfm/epoch_verify_tests.rs index dd014e77b..153f1a49d 100644 --- a/prover/src/lfm/epoch_verify_tests.rs +++ b/prover/src/lfm/epoch_verify_tests.rs @@ -67,8 +67,13 @@ pub(super) struct TableLegs { pub(super) analysis: Analysis, /// `[query][group]` — the row pair in leaf order, then the path. openings: Vec, Vec)>>, - /// `[query][layer]` — `(pᵢ(−υ^(2ⁱ)), path)`. - fri_openings: Vec)>>, + /// `[query][layer]` — `(opened values, path)`: the sibling `pᵢ(−υ^(2ⁱ))` + /// under `pair`, the whole `2^{d_j}` group under a fold schedule. + fri_openings: Vec, Vec)>>, + /// Every capped tree's cap, split off query 0's (owner) path, in the caps + /// arena's order: the committed matrices in group order, then the capped + /// FRI layers. Empty at the default format. + caps: Vec, /// Production's OWN boundary-constraint list for this AIR, kept so /// [`the_boundary_terms_are_program_shape`] can compare the program-shape /// rule against the call rather than against a belief about it. @@ -170,12 +175,21 @@ pub(super) fn build_table_legs( "the next-row block covers every evaluation point past the first step" ); + // The table's leaf layout (S2): the host prover's and verifier's own + // per-table resolution, so `auto` mixes layouts across a proof's tables. + let leaf_layout = stark::leaf_layout::table_leaf_layout(air, trace_length); + let merkle_depth = leaf_layout.tree_depth(log2_lde_length as usize); let sub = SubProofShape { deep, trace_groups, - merkle_depth: log2_lde_length as usize - 1, + merkle_depth, log2_lde_length, coset_offset: FE::from(opts.coset_offset), + trace_cap: opts + .format + .merkle_cap + .height(opts.fri_number_of_queries, merkle_depth), + layout: leaf_layout, }; let has_aux_trace = air.has_aux_trace(); let verify = TableVerifyShape { @@ -184,7 +198,7 @@ pub(super) fn build_table_legs( num_composition_parts: claimed_parts.len(), boundary: boundary_terms(has_aux_trace, num_total_cols), }, - fri: FriShape::from_options(opts, log2_lde_length), + fri: FriShape::for_layout(opts, log2_lde_length, leaf_layout), main_width, num_alpha_powers: if has_aux_trace { artifact.shape.max_bus_elements as usize @@ -195,6 +209,41 @@ pub(super) fn build_table_legs( sub, }; + // ---- the cap heights: the in-guest shapes' against the host's own + // `StarkCaps` (the prover's and the verifier's), so the two sides derive + // every tree's height and depth from one function. + let host_caps = stark::merkle_caps::StarkCaps::for_options( + opts, + log2_lde_length as usize, + leaf_layout.is_one_row(), + ) + .expect("a format the host lays out"); + assert_eq!(host_caps.trace_depth, verify.sub.merkle_depth); + assert_eq!( + host_caps.trace, verify.sub.trace_cap, + "the trace trees' cap" + ); + assert_eq!(host_caps.fri.len(), verify.fri.num_committed()); + for (i, (&d, &c)) in host_caps.fri_depths.iter().zip(&host_caps.fri).enumerate() { + assert_eq!(d, verify.fri.layer_depth(i), "FRI layer {i}'s tree depth"); + assert_eq!(c, verify.fri.layer_cap(i), "FRI layer {i}'s cap"); + } + + // ---- the owner split: query 0 of a capped tree carries the cap at the end + // of its path; the arenas take the `D − c` siblings, the caps arena the cap. + let mut trace_caps: Vec> = Vec::new(); + let mut split = |q: usize, path: &[Commitment], depth: usize, c: usize| -> Vec { + if c == 0 || q != 0 { + assert_eq!(path.len(), depth - c, "query {q}: a path to the cap"); + return path.to_vec(); + } + let (siblings, cap) = crypto::merkle_tree::cap::split_owner_path(path, depth, c) + .expect("the owner path is D − c + 2^c long"); + trace_caps.push(cap.to_vec()); + siblings.to_vec() + }; + let (depth, c_trace) = (verify.sub.merkle_depth, verify.sub.trace_cap); + // ---- the openings, per query, in the emitter's group order. let openings = (0..view.deep_poly_openings_len()) .map(|q| { @@ -210,7 +259,7 @@ pub(super) fn build_table_legs( .chain(p.evaluations_sym()) .map(|v| base_word(*v)) .collect(), - p.merkle_path().to_vec(), + split(q, p.merkle_path(), depth, c_trace), )); } let m = o.main_trace_polys(); @@ -220,7 +269,7 @@ pub(super) fn build_table_legs( .chain(m.evaluations_sym()) .map(|v| base_word(*v)) .collect(), - m.merkle_path().to_vec(), + split(q, m.merkle_path(), depth, c_trace), )); if aux_width > 0 { let a = o.aux_trace_polys().expect("an aux opening"); @@ -230,7 +279,7 @@ pub(super) fn build_table_legs( .chain(a.evaluations_sym()) .map(ext_word) .collect(), - a.merkle_path().to_vec(), + split(q, a.merkle_path(), depth, c_trace), )); } let c = o.composition_poly(); @@ -240,22 +289,13 @@ pub(super) fn build_table_legs( .chain(c.evaluations_sym()) .map(ext_word) .collect(), - c.merkle_path().to_vec(), + split(q, c.merkle_path(), depth, c_trace), )); groups }) .collect(); - let fri_openings = (0..view.query_list_len()) - .map(|q| { - let d = view.query(q); - d.layers_evaluations_sym() - .iter() - .enumerate() - .map(|(i, sym)| (*sym, d.layer_auth_path(i).to_vec())) - .collect() - }) - .collect(); + let (fri_openings, fri_caps) = fri_layer_openings(view, verify.fri); // Production's own boundary list, for the premise check only. It takes the // bus public inputs, which are PROOF data — which is exactly why the emitted @@ -283,18 +323,93 @@ pub(super) fn build_table_legs( }) .collect(); + let caps: Vec = trace_caps.into_iter().flatten().chain(fri_caps).collect(); + assert_eq!( + caps.len() * super::proof_arena::words_per_root(), + verify.cap_words(super::proof_arena::words_per_root()), + "every capped tree's cap, and nothing else" + ); + TableLegs { verify, analysis: analyze(&artifact), openings, fri_openings, + caps, production_boundary, has_aux_trace, num_precomputed_cols: num_precomputed, - precomputed_commitment: air.is_preprocessed().then(|| air.precomputed_commitment()), + precomputed_commitment: air + .is_preprocessed() + .then(|| layout_precomputed_commitment(air, trace_length)), } } +/// The precomputed-columns commitment the host verifier takes for `air` over +/// a trace of `trace_length` rows: `precomputed_commitment_for` the table's +/// resolved leaf layout (S2 — a layout with no root is a hard +/// error, never the other layout's root). At row pairs it IS +/// `air.precomputed_commitment()`. +pub(super) fn layout_precomputed_commitment( + air: &dyn AIR, + trace_length: usize, +) -> Commitment { + let layout = stark::leaf_layout::table_leaf_layout(air, trace_length); + air.precomputed_commitment_for(layout) + .unwrap_or_else(|| panic!("no precomputed commitment at {layout:?}")) +} + +/// Every query's FRI layer openings, per layer `(opened values, path)`, and +/// the capped layers' caps (layer order) split off query 0's owner paths. +/// +/// The proof's flat `layers_evaluations_sym` is one sibling per layer under +/// `pair` and every layer's full group (`2^{d_j}` values, position order) +/// under a fold schedule; `FriShape::layer_values` says which. +/// Each path is cut at its layer's cap: query 0 of a capped layer carries +/// `D − c + 2^c` nodes, every other query `D − c`. +#[allow(clippy::type_complexity)] +pub(super) fn fri_layer_openings( + view: StarkProofView<'_, Gl, Ext3, PI>, + fri: FriShape, +) -> (Vec, Vec)>>, Vec) +where + PI: rkyv::Archive, + ::Archived: rkyv::Deserialize, +{ + let mut caps: Vec = Vec::new(); + let openings = (0..view.query_list_len()) + .map(|q| { + let d = view.query(q); + let flat = d.layers_evaluations_sym(); + let per_query: usize = (0..fri.num_committed()).map(|j| fri.layer_values(j)).sum(); + assert_eq!( + flat.len(), + per_query, + "query {q}: the opened values per query" + ); + let mut offset = 0usize; + (0..fri.num_committed()) + .map(|i| { + let values = flat[offset..offset + fri.layer_values(i)].to_vec(); + offset += fri.layer_values(i); + let path = d.layer_auth_path(i); + let (depth, c) = (fri.layer_depth(i), fri.layer_cap(i)); + if c == 0 || q != 0 { + assert_eq!(path.len(), depth - c, "query {q} FRI layer {i}"); + return (values, path.to_vec()); + } + let (siblings, cap) = + crypto::merkle_tree::cap::split_owner_path(path, depth, c) + .expect("the owner path is D − c + 2^c long"); + caps.extend_from_slice(cap); + (values, siblings.to_vec()) + }) + .collect() + }) + .collect(); + (openings, caps) +} + impl TableLegs { /// Per query, per group: the row-pair values then the sibling digests. /// @@ -319,12 +434,30 @@ impl TableLegs { out } + /// The sub-proof's Merkle caps, once — `None` at the default format, where + /// the emitter declares no caps arena + /// (`epoch_verify::declare_table_arenas`). + pub(super) fn caps_arena(&self) -> Option> { + let words = self.verify.cap_words(super::proof_arena::words_per_root()); + if words == 0 { + assert!(self.caps.is_empty()); + return None; + } + let out = super::proof_arena::commitments_to_arena(&self.caps); + assert_eq!( + out.len(), + words, + "the caps arena is what the shape declares" + ); + Some(out) + } + /// Per query, per committed layer: the symmetric evaluation then its path. pub(super) fn fri_arena(&self) -> Vec { let mut out = Vec::new(); for query in &self.fri_openings { - for (sym, path) in query { - out.push(ext_word(sym)); + for (values, path) in query { + out.extend(values.iter().map(ext_word)); out.extend(super::proof_arena::commitments_to_arena(path)); } } @@ -1384,6 +1517,7 @@ fn the_candidate_rate_model_is_derived_not_remembered() { final_poly_log_degree: 3, coset_offset: 3, num_queries: 73, + format: stark::proof::options::ProofFormat::DEFAULT, }; assert!(fri.num_committed() > 0, "the shape must exercise the term"); @@ -1410,3 +1544,194 @@ fn the_candidate_rate_model_is_derived_not_remembered() { assert_eq!(fri_leaf_permutations_at_rate(&terminal, rate), 0); } } + +/// Queries the knob-on twin proves at: enough openings that `auto` caps every +/// tall tree at height 3 (from 20 openings on). +const PROCESS_FORMAT_QUERIES: usize = 24; + +/// ★ The KNOB-ON TWIN of [`the_assembled_epoch_verifier_runs`] (box only): a +/// real continuation epoch proved at the PROCESS format — `ZfFormat::global()`, +/// i.e. `LAMBDA_VM_ZF_CAP` / `LAMBDA_VM_ZF_FRI` — at the MIN preset with +/// [`PROCESS_FORMAT_QUERIES`] queries, verified by the assembled machine. +/// +/// Asserts, per format: the program executes (every cap authenticated once per +/// tree, every opening checked against it, every FRI group folded to the +/// terminal); the legs' emitted permutations equal the closed form +/// `Σ table_permutations_for` (per-query paths cut at each tree's cap plus +/// `2^c − 1` once per capped tree; group leaves and group paths under +/// `fri = dp`); a moved cap word does not execute. Prints the census to +/// compare across arms (instructions, permutations, `Select`s, cells per +/// chip). At the default format it is the MIN-preset run at 24 queries. +#[test] +#[ignore = "a real epoch proof at 24 queries and its assembled verifier: box only"] +fn the_assembled_epoch_verifier_runs_at_the_process_format() { + let mut opts = super::proof_fixture::fixture_options(); + opts.fri_number_of_queries = PROCESS_FORMAT_QUERIES; + assembled_twin_at_the_process_format(opts); +} + +/// ★ [`the_assembled_epoch_verifier_runs_at_the_process_format`] at BLOWUP 4 +/// — the S2 (one-row) twin, box only. One-row static roots exist at blowup 4 +/// only (`STATIC_BLOWUP_FACTORS_ONE_ROW`; a missing twin is a +/// proving error), so the MIN preset's blowup 2 cannot prove a one-row +/// BITWISE; this arm keeps every other MIN-preset option and lifts the blowup +/// to 4 for every format, so its knob-off and knob-on runs are one A/B. Under +/// `one_row = auto` the epoch's tables resolve their layouts one by one +/// (printed per leg), so the assembled machine verifies a MIXED-layout proof; +/// the REGISTER root is derived in-machine at that table's own layout. +#[test] +#[ignore = "a real epoch proof at blowup 4, 24 queries, and its assembled verifier: box only"] +fn the_assembled_epoch_verifier_runs_at_blowup_4_at_the_process_format() { + let mut opts = super::proof_fixture::fixture_options(); + opts.fri_number_of_queries = PROCESS_FORMAT_QUERIES; + opts.blowup_factor = 4; + assembled_twin_at_the_process_format(opts); +} + +/// The body of the assembled-verifier twins: `base` with the process format +/// stamped on, proved, harvested and verified by the assembled machine. +fn assembled_twin_at_the_process_format(base: crate::ProofOptions) { + let format = crate::zf_format::ZfFormat::global(); + let opts = format.options(base); + let e = super::epoch_tests::real_epoch_with(opts.clone()); + let program = super::epoch_tests::epoch_program(&e, true); + let arenas = super::epoch_tests::epoch_arena_words(&e, true); + execute(&program, &arenas, &crate::hash_pin::BLOCK_HASHER) + .expect("the assembled verifier must execute at the process format"); + + let spine = super::epoch_tests::epoch_program(&e, false); + let perms = |p: &_| super::machine_tests::wrap_hash_instrs(p); + let selects = |p: &super::compiler::LfmProgram| { + p.instrs + .iter() + .filter(|i| matches!(i, super::instr::Instr::Select { .. })) + .count() + }; + let hash = super::edsl::WrapHash::production(); + let emitted = perms(&program) - perms(&spine); + let predicted: usize = e + .legs + .iter() + .map(|l| super::epoch_verify::table_permutations_for(&l.verify, hash)) + .sum(); + let cap_perms: usize = e + .legs + .iter() + .map(|l| super::epoch_verify::cap_permutations(&l.verify)) + .sum(); + println!( + "\n★ ASSEMBLED EPOCH VERIFIER AT THE PROCESS FORMAT\n {}\n opts: blowup {}, \ + {} queries, grinding {}, k {}\n sub-proofs {} | legs: {} instructions, \ + {} permutations ({} of them cap roots), {} selects | whole: {} instructions, \ + {} permutations", + format.banner(), + opts.blowup_factor, + opts.fri_number_of_queries, + opts.grinding_factor, + opts.fri_final_poly_log_degree, + e.legs.len(), + program.instrs.len() - spine.instrs.len(), + emitted, + cap_perms, + selects(&program) - selects(&spine), + program.instrs.len(), + perms(&program), + ); + for (i, l) in e.legs.iter().enumerate() { + let f = l.verify.fri; + println!( + " leg {i:>2}: log2(lde) {:>2} layout {:?} trace cap {} FRI schedule {:?} \ + depths {:?} caps {:?} {} permutations", + l.verify.sub.log2_lde_length, + l.verify.sub.layout, + l.verify.sub.trace_cap, + f.schedule(), + (0..f.num_committed()) + .map(|j| f.layer_depth(j)) + .collect::>(), + (0..f.num_committed()) + .map(|j| f.layer_cap(j)) + .collect::>(), + super::epoch_verify::table_permutations_for(&l.verify, hash), + ); + } + for c in super::airs::lfm_chip_census(&program) { + println!( + " CENSUS {:<14} real {:>10} padded {:>10} cells {:>12}", + c.name, + c.real_rows, + c.rows, + c.main_cells() + ); + } + assert_eq!( + emitted, predicted, + "the legs' emitted permutations must equal the closed form at the process format" + ); + println!(" emitted permutations == closed form: {emitted}"); + // One parseable line for the box wrapper's cross-arm comparison. + println!( + "ZFTWIN legs_permutations={emitted} cap_root_permutations={cap_perms} \ + legs_instructions={} legs_selects={} whole_instructions={}", + program.instrs.len() - spine.instrs.len(), + selects(&program) - selects(&spine), + program.instrs.len(), + ); + // S2: how many legs verify one-row tables (0 at `one_row = 0`, every leg + // at `1`, the AIR widths' choice at `auto`), and the blowup of the arm. + let one_row_legs = e + .legs + .iter() + .filter(|l| l.verify.sub.layout.is_one_row()) + .count(); + println!( + "ZFS2TWIN blowup={} legs={} one_row_legs={one_row_legs} row_pair_legs={}", + opts.blowup_factor, + e.legs.len(), + e.legs.len() - one_row_legs, + ); + match opts.format.one_row { + stark::proof::options::OneRowMode::Off => assert_eq!(one_row_legs, 0), + stark::proof::options::OneRowMode::On => assert_eq!(one_row_legs, e.legs.len()), + stark::proof::options::OneRowMode::Auto => {} + } + // A one-row leg's input-tree group value (query 0, layer 0, value 0) moved + // must not execute — the input group is authenticated and slot-checked. + // The FRI arena is found by content rather than by a hand-counted offset. + if let Some((k, words)) = e + .legs + .iter() + .enumerate() + .find(|(_, l)| l.verify.sub.layout.is_one_row() && l.verify.fri.num_committed() > 0) + .map(|(k, l)| (k, l.fri_arena())) + { + let at = arenas + .iter() + .position(|a| *a == words) + .expect("the one-row leg's FRI arena is among the program's arenas"); + let mut bad = arenas.clone(); + bad[at][0][0] += FE::one(); + execute(&program, &bad, &crate::hash_pin::BLOCK_HASHER) + .expect_err("a moved input-tree value must not execute"); + println!(" leg {k}: a moved one-row input-tree value is refused"); + } + + // A moved cap word must not execute (only when the format caps a tree). + // The caps arena is found by content rather than by a hand-counted offset. + if let Some((k, words)) = e + .legs + .iter() + .enumerate() + .find_map(|(k, l)| l.caps_arena().map(|w| (k, w))) + { + let at = arenas + .iter() + .position(|a| *a == words) + .expect("the caps arena is among the program's arenas"); + let mut bad = arenas.clone(); + bad[at][0][0] += FE::one(); + execute(&program, &bad, &crate::hash_pin::BLOCK_HASHER) + .expect_err("a moved cap word must not execute"); + println!(" leg {k}: a moved cap word is refused"); + } +} diff --git a/prover/src/lfm/fri.rs b/prover/src/lfm/fri.rs index d47a524e7..a8b6df958 100644 --- a/prover/src/lfm/fri.rs +++ b/prover/src/lfm/fri.rs @@ -32,13 +32,16 @@ //! also mirrors the CPU layout only — `fri/mod.rs` has cuda fast paths that //! claim the same layout, unverified here and never run by the machine. -use stark::proof::options::ProofOptions; +use stark::fri::schedule::FriFormat; +use stark::leaf_layout::LeafLayout; +use stark::proof::options::{FriMode, OneRowMode, ProofFormat, ProofOptions}; use crate::tables::types::FE; -use super::builder::{Bit, Ext, Felt, LfmBuilder}; +use super::builder::{Bit, Cell, Ext, Felt, LfmBuilder}; use super::edsl::{self, WrapDigest}; use super::instr::ArenaId; +use super::merkle_cap::CapCells; use super::sub_proof::{self, GroupShape}; /// The compile-time shape of one sub-proof's FRI verification. @@ -58,6 +61,15 @@ pub struct FriShape { pub coset_offset: u64, /// Queries the sub-proof carries. pub num_queries: usize, + /// The inner proof's FORMAT: its Merkle cap + /// policy caps every committed layer tree. A verifier constant, taken from + /// the inner proof's options — never from the proof. + /// + /// `format.one_row` is the table's RESOLVED leaf layout (S2): `Off` (row + /// pairs) or `On` (one row), never `Auto` — `auto` is resolved per table + /// from the AIR's widths ([`Self::for_layout`]) before a shape exists, and + /// [`Self::check`] refuses an unresolved one. + pub format: ProofFormat, } impl FriShape { @@ -66,16 +78,64 @@ impl FriShape { /// Every FRI-relevant parameter comes from `options` — including the coset /// offset, which discharges the plumbing half of the `coset_offset != 3` /// deferral recorded in `others/lfm-assembly-obligations.md`. + /// + /// # Panics + /// + /// On `one_row = auto`: the layout of an `auto` table is resolved from its + /// AIR's committed widths (`stark::leaf_layout::table_leaf_layout`), which + /// the options alone do not carry — use [`Self::for_layout`] with the + /// table's resolved layout. `Off` and `On` resolve themselves. pub fn from_options(options: &ProofOptions, log2_lde_length: u32) -> Self { + let layout = match options.format.one_row { + OneRowMode::Off => LeafLayout::RowPair, + OneRowMode::On => LeafLayout::Row, + OneRowMode::Auto => panic!( + "one_row = auto resolves per table from the AIR's widths: build the \ + FRI shape with FriShape::for_layout(options, lde, table_leaf_layout(air, n))" + ), + }; + Self::for_layout(options, log2_lde_length, layout) + } + + /// The shape of a table proved under `options` whose trace trees use the + /// RESOLVED leaf `layout` (the table's `stark::leaf_layout::table_leaf_layout` + /// — what the host prover and verifier lay the proof out with). The + /// resolved layout is stored in `format.one_row` (`Off` / `On`). + pub fn for_layout(options: &ProofOptions, log2_lde_length: u32, layout: LeafLayout) -> Self { + let mut format = options.format; + format.one_row = if layout.is_one_row() { + OneRowMode::On + } else { + OneRowMode::Off + }; Self { log2_lde_length, blowup_log: (options.blowup_factor as u32).trailing_zeros(), final_poly_log_degree: options.fri_final_poly_log_degree as u32, coset_offset: options.coset_offset, num_queries: options.fri_number_of_queries, + format, } } + /// Whether the table's trace trees hold one row per leaf (S2): the DEEP + /// codeword is then committed as FRI layer 0 (the input tree), the query + /// index has `log2(lde)` bits and no fold precedes layer 0. + pub fn one_row(self) -> bool { + match self.format.one_row { + OneRowMode::Off => false, + OneRowMode::On => true, + OneRowMode::Auto => { + panic!("a FRI shape carries a RESOLVED layout, never one_row = auto") + } + } + } + + /// The trace trees' leaf layout this shape verifies. + pub fn leaf_layout(self) -> LeafLayout { + LeafLayout::from_one_row(self.one_row()) + } + /// `log2` of the terminal codeword length, clamped to the full LDE for /// traces too small to fold that far (`terminal.rs:46`'s `.min(lde_log)`). pub fn terminal_log(self) -> u32 { @@ -87,15 +147,111 @@ impl FriShape { self.log2_lde_length - self.terminal_log() } + /// Whether the proof uses today's FRI encoding: pair layers, one sibling + /// value per committed layer (`fri = pair` with row-pair openings). + /// Decided by the FORMAT, never by the schedule's values: a `dp` schedule + /// of all ones still uses the group encoding, and so does every one-row + /// table (`FriFormat::is_legacy`: its layer 0 is the committed DEEP + /// codeword, opened as a full group). Every non-legacy path below is the + /// group path; the legacy emission is today's, instruction for instruction. + pub fn is_legacy(self) -> bool { + self.format.fri_mode == FriMode::Pair && !self.one_row() + } + + /// The host's own FRI format for this shape (the fold-schedule DP's + /// inputs): the mode, the query count (every FRI tree is opened once per + /// query), the cap policy and the test-only schedule override. + fn fri_format(self) -> FriFormat { + FriFormat { + mode: self.format.fri_mode, + one_row: self.one_row(), + num_queries: self.num_queries as u64, + cap: self.format.merkle_cap, + schedule_override: self.format.fri_schedule_override, + } + } + + /// ★ The committed layers' fold exponents, first committed layer first — + /// the SAME function the host prover and verifier lay out with + /// (`stark::fri::schedule::FriFormat::schedule`: the all-ones schedule + /// under `pair`, the cost-law DP under `dp`). A format + /// constant: nothing here reads a proof. + /// + /// ⚠ `num_queries` is a DP input (and a cap-policy input): a program that + /// verifies a SUBSET of a proof's queries has a different `dp` schedule + /// and `auto` caps than the proof unless the query count is kept. + pub fn schedule(self) -> Vec { + self.fri_format() + .schedule(self.log2_lde_length, self.terminal_log()) + } + /// Committed (Merkle-rooted) layers — one root, one auth path per query, - /// and one Merkle walk to emit, each. + /// and one Merkle walk to emit, each: the schedule's length. /// - /// **`total_folds − 1`, not `total_folds`.** The final fold is performed - /// and never committed (`fri/mod.rs:114-118`), so a query folds once more - /// than it authenticates. This off-by-one is the readiest way to build a - /// verifier that looks right and checks one layer too few. + /// **`total_folds − 1` under `pair`, not `total_folds`.** The final fold is + /// performed and never committed (`fri/mod.rs:114-118`), so a query folds + /// once more than it authenticates. This off-by-one is the readiest way to + /// build a verifier that looks right and checks one layer too few. Under + /// one-row leaves the chain starts at the DEEP codeword itself (layer 0 = + /// the input tree), so the pair schedule is `total_folds` ones. pub fn num_committed(self) -> usize { - self.total_folds().saturating_sub(1) as usize + self.schedule().len() + } + + /// Folding challenges the proof draws (`FriFoldLayout::num_zetas`): + /// one per committed layer plus the final fold's for row pairs (fold 0 + /// consumes the first), one per committed layer under one row (layer 0 is + /// committed before any challenge); none when nothing folds. + pub fn num_zetas(self) -> usize { + if self.total_folds() == 0 { + 0 + } else { + self.num_committed() + usize::from(!self.one_row()) + } + } + + /// Index of committed layer `j`'s challenge in the ζ list: `j + 1` for row + /// pairs (`ζ₀` drove the uncommitted fold 0), `j` under one row. + pub fn layer_zeta_index(self, layer: usize) -> usize { + layer + usize::from(!self.one_row()) + } + + /// Fold exponent `d_j` of committed layer `j`: a leaf groups `2^{d_j}` + /// consecutive values (1 = today's pair). + pub fn layer_fold(self, layer: usize) -> u32 { + u32::from(self.schedule()[layer]) + } + + /// Index bits consumed before committed layer `j`: `G_j = Σ_{i usize { + self.schedule()[..layer].iter().map(|&d| d as usize).sum() + } + + /// Opened values one query's opening of committed layer `j` carries: the + /// sibling alone under `pair`, the whole `2^{d_j}` group otherwise + /// (the query's own value included). + pub fn layer_values(self, layer: usize) -> usize { + if self.is_legacy() { + 1 + } else { + 1usize << self.layer_fold(layer) + } + } + + /// Felts committed layer `j`'s leaf hashes: `2^{d_j}` extension values of + /// three felts — six (the pair) under `pair`. + pub fn layer_leaf_felts(self, layer: usize) -> usize { + 3 << self.layer_fold(layer) + } + + /// Leaf permutations one query costs across every committed layer under + /// `hash`'s own block rule (`epoch_verify::blocks_for`). + pub fn leaf_permutations_per_query(self, hash: super::edsl::WrapHash) -> usize { + (0..self.num_committed()) + .map(|j| super::epoch_verify::blocks_for(self.layer_leaf_felts(j), hash)) + .sum() } /// Folds a query performs: `num_committed + 1` whenever anything folds at @@ -120,13 +276,52 @@ impl FriShape { 1usize << self.effective_k() } - /// Merkle path length for committed layer `i`: that layer's codeword is - /// `2^(n−i−1)` long and its leaves are pairs, so the tree has `2^(n−i−2)` - /// leaves. + /// Tree depth of committed layer `j`: the layer is `2^(n − 1 − G_j)` + /// values long and its leaves group `2^{d_j}`, so the tree has + /// `2^(n − 1 − G_j − d_j)` leaves — `n − j − 2` under `pair`. + pub fn layer_depth(self, layer: usize) -> usize { + let schedule = self.schedule(); + assert!( + layer < schedule.len(), + "layer index must be below num_committed" + ); + let consumed: usize = schedule[..=layer].iter().map(|&d| d as usize).sum(); + self.index_bits() - consumed + } + + /// Merkle-cap height of committed layer `i`'s tree under the format's cap + /// policy: every layer tree is opened once per query (`0` = uncapped). + /// The same function the host prover and verifier use + /// (`stark::merkle_caps::StarkCaps`), at the same depth. + pub fn layer_cap(self, layer: usize) -> usize { + self.format + .merkle_cap + .height(self.num_queries, self.layer_depth(layer)) + } + + /// Merkle path length a query's opening of committed layer `i` carries: + /// the tree's depth less its cap height (the owner path's cap is split off + /// into the caps arena). pub fn layer_path_len(self, layer: usize) -> usize { - (self.log2_lde_length as usize) - .checked_sub(layer + 2) - .expect("layer index must be below num_committed") + self.layer_depth(layer) - self.layer_cap(layer) + } + + /// Arena words the committed layers' caps occupy, once per sub-proof. + pub fn cap_words(self, digest_words: usize) -> usize { + (0..self.num_committed()) + .map(|i| match self.layer_cap(i) { + 0 => 0, + c => (1usize << c) * digest_words, + }) + .sum() + } + + /// Permutations the committed layers' cap checks cost, once per + /// sub-proof: `2^c − 1` parents per capped layer. + pub fn cap_permutations(self) -> usize { + (0..self.num_committed()) + .map(|i| super::merkle_cap::cap_root_permutations(self.layer_cap(i))) + .sum() } /// Merkle path steps one query walks across every committed layer. @@ -136,15 +331,19 @@ impl FriShape { .sum() } - /// Keccak permutations one query costs: one leaf hash per committed layer - /// (a 48-byte pair, one rate block) plus one per path step (64 bytes, one - /// rate block). + /// Permutations one query costs under the production wrap hash: every + /// committed layer's leaf (a 48-byte pair is one block under every hash; + /// a `2^d` group is `⌈3·2^d / 8⌉` at the rate-8 algebraic sponge) plus one + /// per path step (a parent is one compression under every hash). pub fn permutations_per_query(self) -> usize { - self.num_committed() + self.path_steps_per_query() + self.leaf_permutations_per_query(super::edsl::WrapHash::production()) + + self.path_steps_per_query() } - /// Index bits a query carries — `log2(lde) − 1`, which is both the TRACE - /// trees' Merkle depth and the bit width of `iota`. + /// Index bits a query carries — `log2(lde) − 1` for row pairs (the pair + /// index `iota`), `log2(lde)` under one-row leaves (`r` over the whole + /// LDE) — which is both the TRACE trees' Merkle depth and the bit + /// width of the index. /// /// The FRI layers consume SUFFIXES of this one decomposition rather than /// decompositions of their own, which is what makes the emitted walks @@ -152,20 +351,26 @@ impl FriShape { /// as its leaf-ordering parity and `bits[i+1..]` as its walk, and /// `bits[i+1..].len() = n − i − 2 = layer_path_len(i)` exactly — the layer /// tree's depth is not a separate fact to keep in sync, it is what is left - /// of the index after the folds already performed. + /// of the index after the folds already performed. (Under a Merkle cap the + /// top `layer_cap(i)` of those bits pick the cap node instead of being + /// walked; the split is the cap's own, [`CapCells::verify_path`].) pub fn index_bits(self) -> usize { - self.log2_lde_length as usize - 1 + self.leaf_layout().tree_depth(self.log2_lde_length as usize) } - /// Arena words one query's FRI opening occupies: per committed layer the - /// symmetric evaluation (one word) and its path (`digest_words` per level). + /// Arena words one query's FRI opening occupies: per committed layer its + /// opened values ([`Self::layer_values`]: the symmetric evaluation, or the + /// whole group) and its path (`digest_words` per level). /// /// `digest_words` is the BUILDER's digest width on the machine side /// (`edsl::digest_words(b)`) and `proof_arena::words_per_root()` on the /// host side — see `SubProofShape::query_words` for why it is an argument. pub fn query_words(self, digest_words: usize) -> usize { // The path stride is the DIGEST's width, not a literal two. - self.num_committed() + digest_words * self.path_steps_per_query() + let values: usize = (0..self.num_committed()) + .map(|j| self.layer_values(j)) + .sum(); + values + digest_words * self.path_steps_per_query() } /// Keccak permutations the whole sub-proof's FRI costs. @@ -175,6 +380,31 @@ impl FriShape { /// Invariants a caller cannot assemble their way out of. pub fn check(self) { + assert!( + self.format.one_row != OneRowMode::Auto, + "a FRI shape carries a RESOLVED layout (FriShape::for_layout), never one_row = auto" + ); + // The schedule covers exactly the committed folds (`FriFoldLayout`'s + // constructor invariant, which refuses a proof otherwise). + let schedule = self.schedule(); + let covered: u32 = schedule.iter().map(|&d| u32::from(d)).sum(); + assert!( + schedule + .iter() + .all(|&d| (1..=stark::fri::schedule::FRI_SCHEDULE_DMAX).contains(&u32::from(d))), + "every fold exponent is in 1..=DMAX: {schedule:?}" + ); + // Row pairs: fold 0 is binary and uncommitted. One row: every fold is + // a committed layer's (layer 0 is the DEEP codeword). + let committed_folds = if self.one_row() { + self.total_folds() + } else { + self.total_folds().saturating_sub(1) + }; + assert_eq!( + covered, committed_folds, + "the schedule {schedule:?} must cover the committed folds" + ); assert!( self.blowup_log >= 1, "a blowup of 1 is not a low-degree extension" @@ -271,6 +501,9 @@ pub struct LayerCommitment { /// of four felts. `edsl::assert_digest_eq_lanes` zips a digest against these /// and asserts the widths agree, so it works at either width unchanged. pub root_lanes: Vec<[Felt; 4]>, + /// The layer tree's authenticated Merkle cap, when the format caps it + /// (see [`super::sub_proof::GroupCommitment::cap`]). `None` = today. + pub cap: Option, } impl LayerCommitment { @@ -286,7 +519,10 @@ impl LayerCommitment { b.unpack(w) }) .collect(); - LayerCommitment { root_lanes } + LayerCommitment { + root_lanes, + cap: None, + } } /// A layer commitment over lanes the caller already holds. @@ -297,8 +533,73 @@ impl LayerCommitment { /// [`super::sub_proof::GroupCommitment::from_lanes`] for the same argument at /// the trace trees. pub fn from_lanes(root_lanes: Vec<[Felt; 4]>) -> Self { - LayerCommitment { root_lanes } + LayerCommitment { + root_lanes, + cap: None, + } } + + /// Hint this layer tree's height-`c` cap out of `arena` at `base` and + /// authenticate it against the root lanes, once per tree (see + /// [`super::sub_proof::GroupCommitment::hint_cap`]). Returns the next free + /// word; `c = 0` hints nothing. + pub fn hint_cap(&mut self, b: &mut LfmBuilder, arena: ArenaId, base: u32, c: usize) -> u32 { + if c == 0 { + return base; + } + let (cap, next) = + super::merkle_cap::hint_and_authenticate(b, arena, base, c, &self.root_lanes); + self.cap = Some(cap); + next + } + + /// Authenticate an opened leaf of this layer at the tree's WHOLE leaf + /// index: against the cap when capped, else against the root lanes (the + /// uncapped emission is today's, instruction for instruction). + fn authenticate( + &self, + b: &mut LfmBuilder, + leaf: WrapDigest, + index_bits: &[Bit], + siblings: &[WrapDigest], + ) { + match &self.cap { + None => { + let root = edsl::wrap_merkle_walk(b, leaf, index_bits, siblings); + edsl::assert_digest_eq_lanes(b, root, &self.root_lanes); + } + Some(cap) => cap.verify_path(b, leaf, index_bits, siblings), + } + } + + fn cap_height(&self) -> usize { + self.cap.as_ref().map_or(0, CapCells::height) + } +} + +/// Hint and authenticate every capped committed layer's cap, in layer order, +/// out of `arena` from word 0 — once per sub-proof. The words it reads are +/// exactly [`FriShape::cap_words`]. +pub fn hint_layer_caps( + b: &mut LfmBuilder, + shape: FriShape, + arena: ArenaId, + layers: &mut [LayerCommitment], +) { + assert_eq!( + layers.len(), + shape.num_committed(), + "one commitment per layer" + ); + let mut at = 0u32; + for (i, layer) in layers.iter_mut().enumerate() { + at = layer.hint_cap(b, arena, at, shape.layer_cap(i)); + } + assert_eq!( + at as usize, + shape.cap_words(edsl::digest_words(b) as usize), + "the FRI caps fill exactly what the shape declares" + ); } /// A sub-proof's FRI data that does not depend on the query. @@ -308,10 +609,54 @@ pub struct FriCommitments { /// The folding challenges `ζ₀ .. ζ_C` — `num_committed + 1` of them, or /// none when nothing folds. The asymmetry is the whole off-by-one of this /// leg: the first fold consumes the DEEP pair and is not committed, so - /// folds exceed layers by one (`fri/mod.rs:114-118`). + /// folds exceed layers by one (`fri/mod.rs:114-118`). Under one-row + /// leaves there is no such fold: `num_committed` challenges + /// ([`FriShape::num_zetas`]). pub zetas: Vec, /// The terminal polynomial's `2^effective_k` coefficients, low-to-high. pub coeffs: Vec, + /// Under the group encoding (S3, and every one-row table): per committed + /// layer `j`, the challenges its `d_j` binary folds use — `ζ, ζ², …, + /// ζ^{2^{d_j−1}}` for `ζ = ζ_{j+1}` (row pairs) or `ζ_j` (one row, + /// [`FriShape::layer_zeta_index`]) — squared ONCE per + /// sub-proof, not per query. Empty under the legacy encoding, where each + /// layer folds once with `ζ_{j+1}` itself. + pub zeta_powers: Vec>, +} + +impl FriCommitments { + /// The commitments of one sub-proof's FRI, with the group encoding's + /// challenge powers hoisted ([`Self::zeta_powers`]; nothing is emitted + /// under `pair`, so today's program is unchanged). + pub fn new( + b: &mut LfmBuilder, + shape: FriShape, + layers: Vec, + zetas: Vec, + coeffs: Vec, + ) -> Self { + let zeta_powers = if shape.is_legacy() || zetas.is_empty() { + Vec::new() + } else { + (0..shape.num_committed()) + .map(|j| { + let mut z = zetas[shape.layer_zeta_index(j)]; + let mut powers = vec![z]; + for _ in 1..shape.layer_fold(j) { + z = b.emul(z, z); + powers.push(z); + } + powers + }) + .collect() + }; + FriCommitments { + layers, + zetas, + coeffs, + zeta_powers, + } + } } /// One query's opening of one committed layer. @@ -320,11 +665,16 @@ pub struct FriCommitments { /// [`super::sub_proof::GroupOpening`], the values are the caller's, so what the /// walk authenticates is what the fold consumes. pub struct LayerOpening { - /// `pᵢ(−υ^(2ⁱ))` — the conjugate the prover supplies. Its partner - /// `pᵢ(υ^(2ⁱ))` is not in the proof at all: the verifier computed it as the - /// previous fold's output, which is why a FRI layer opening is one value and - /// not two. - pub sym: Ext, + /// Under `pair`: ONE value, `pᵢ(−υ^(2ⁱ))` — the conjugate the prover + /// supplies. Its partner `pᵢ(υ^(2ⁱ))` is not in the proof at all: the + /// verifier computed it as the previous fold's output, which is why a pair + /// layer opening is one value and not two. + /// + /// Under the group encoding: the whole group of `2^{d_j}` values in + /// position (bit-reversed) order, the query's own value at its slot + /// included — the leaf is hashed straight from them and the + /// slot check `values[slot] == v` ties them to the previous fold. + pub values: Vec, /// Sibling digests, LEAF LEVEL FIRST. pub siblings: Vec, } @@ -335,20 +685,30 @@ pub struct LayerOpening { /// re-derivation. [`super::sub_proof::QueryOutput`] is exactly this shape's /// supplier. pub struct FriQuery<'a> { - /// `p₀(υ)` — the DEEP reconstruction at the query point. + /// `p₀(υ)` — the DEEP reconstruction at the query point (`DEEP(x_r)` under + /// one-row leaves). pub p0: Ext, - /// `p₀(−υ)`. - pub p0_sym: Ext, - /// `υ`. Not Merkle-checked here and not hinted: it is the point the - /// authenticated opening was folded at. + /// `p₀(−υ)` for a row-pair shape; `None` under one-row leaves, which open + /// one point. + pub p0_sym: Option, + /// `υ` (or `x_r`). Not Merkle-checked here and not hinted: it is the point + /// the authenticated opening was folded at. pub point: Felt, - /// `−υ`, needed only by the zero-fold shape. - pub point_sym: Felt, + /// `−υ`, needed only by the row-pair zero-fold shape; `None` under one row. + pub point_sym: Option, /// The query index low-to-high, `shape.index_bits()` of them — the cells /// the trace walk consumed. pub bits: &'a [Bit], } +impl FriQuery<'_> { + /// `p₀(−υ)` — a row-pair shape's. + fn p0_sym(&self) -> Ext { + self.p0_sym + .expect("a row-pair FRI query carries the symmetric DEEP value") + } +} + /// The arenas one sub-proof's FRI verification reads, in declaration order. pub struct FriArenas { /// Two words per committed layer root, in fold order. @@ -360,6 +720,9 @@ pub struct FriArenas { /// Per query, per committed layer: the symmetric evaluation, then the /// sibling digests (two words per level). pub queries: ArenaId, + /// Per capped committed layer, its `2^c` cap digests — declared only when + /// the format caps some layer ([`FriShape::cap_words`] `> 0`). + pub caps: Option, } /// Declare the FRI arenas and hoist everything a query does not depend on. @@ -371,21 +734,26 @@ pub fn declare_fri( shape.check(); assert!(num_queries > 0, "a proof carries at least one query"); let c = shape.num_committed(); - let num_zetas = if shape.total_folds() > 0 { c + 1 } else { 0 }; + let num_zetas = shape.num_zetas(); let roots = b.declare_arena(edsl::digest_words(b) * c as u32); let zetas = b.declare_arena(num_zetas as u32); let coeffs = b.declare_arena(shape.num_terminal_coeffs() as u32); let queries = b.declare_arena((num_queries * shape.query_words(edsl::digest_words(b) as usize)) as u32); + let cap_words = shape.cap_words(edsl::digest_words(b) as usize); + let caps = (cap_words > 0).then(|| b.declare_arena(cap_words as u32)); - let layers = (0..c) + let mut layers: Vec = (0..c) .map(|i| LayerCommitment::hint(b, roots, edsl::digest_words(b) * i as u32)) .collect(); - let zeta_cells = (0..num_zetas as u32) + if let Some(caps) = caps { + hint_layer_caps(b, shape, caps, &mut layers); + } + let zeta_cells: Vec = (0..num_zetas as u32) .map(|i| b.hint_word(zetas, i).as_ext()) .collect(); - let coeff_cells = (0..shape.num_terminal_coeffs() as u32) + let coeff_cells: Vec = (0..shape.num_terminal_coeffs() as u32) .map(|i| b.hint_word(coeffs, i).as_ext()) .collect(); @@ -395,12 +763,9 @@ pub fn declare_fri( zetas, coeffs, queries, + caps, }, - FriCommitments { - layers, - zetas: zeta_cells, - coeffs: coeff_cells, - }, + FriCommitments::new(b, shape, layers, zeta_cells, coeff_cells), ) } @@ -430,8 +795,13 @@ pub fn hint_layer_openings_from( let mut cursor = (query * stride) as u32; let openings: Vec = (0..shape.num_committed()) .map(|layer| { - let sym = b.hint_word(arena, cursor).as_ext(); - cursor += 1; + let values: Vec = (0..shape.layer_values(layer)) + .map(|_| { + let v = b.hint_word(arena, cursor).as_ext(); + cursor += 1; + v + }) + .collect(); let siblings: Vec = (0..shape.layer_path_len(layer)) .map(|_| { // The stride follows the DIGEST's width, not a literal. @@ -440,7 +810,7 @@ pub fn hint_layer_openings_from( d }) .collect(); - LayerOpening { sym, siblings } + LayerOpening { values, siblings } }) .collect(); assert_eq!( @@ -528,31 +898,51 @@ pub fn emit_query_fri( q.bits.len(), shape.index_bits(), "the FRI leg reads suffixes of the trace walk's own decomposition, so \ - it needs all log2(lde) − 1 index bits" + it needs all of its index bits (log2(lde) − 1 for row pairs, log2(lde) \ + for one row)" ); assert_eq!(fri.layers.len(), c, "one commitment per committed layer"); assert_eq!(openings.len(), c, "one opening per committed layer"); + for (i, layer) in fri.layers.iter().enumerate() { + assert_eq!( + layer.cap_height(), + shape.layer_cap(i), + "layer {i} is capped at the shape's height" + ); + } assert_eq!( fri.coeffs.len(), shape.num_terminal_coeffs(), "the terminal polynomial carries 2^effective_k coefficients" ); + assert_eq!( + q.p0_sym.is_none(), + shape.one_row(), + "a one-row query opens ONE point, a row-pair query two" + ); + assert_eq!(q.point_sym.is_none(), shape.one_row()); + if shape.total_folds() == 0 { assert!( fri.zetas.is_empty(), "a codeword that never folds draws no folding challenge" ); + // One row: the terminal codeword IS the DEEP codeword and + // `terminal[r] == DEEP(x_r)` is the whole check (host + // `verify_query_groups`); row pairs check both points. let at = emit_terminal_eval(b, fri, q.point); b.assert_eq_ext(at, q.p0); - let at_sym = emit_terminal_eval(b, fri, q.point_sym); - b.assert_eq_ext(at_sym, q.p0_sym); + if let (Some(p0_sym), Some(point_sym)) = (q.p0_sym, q.point_sym) { + let at_sym = emit_terminal_eval(b, fri, point_sym); + b.assert_eq_ext(at_sym, p0_sym); + } return q.p0; } assert_eq!( fri.zetas.len(), - c + 1, - "folds exceed committed layers by one" + shape.num_zetas(), + "folds exceed committed layers by one (row pairs), equal them (one row)" ); // `υ⁻¹`, once. Production batch-inverts across queries and REJECTS on a @@ -562,34 +952,99 @@ pub fn emit_query_fri( let one = b.felt_const(FE::one()); let inv = b.div(one, q.point); + if shape.one_row() { + // ★ S2: layer 0 IS the committed DEEP + // codeword, so no fold precedes it. The query's value there is + // `DEEP(x_r)` itself and the point's inverse is `x_r⁻¹`; the layer-0 + // slot check of `emit_group_layer` is then the INPUT-SLOT check + // `group₀[slot] == DEEP(x_r)` — the only thing tying the FRI chain to + // the authenticated trace openings (host `verify_query_groups`, M1). + assert_eq!( + fri.zeta_powers.len(), + c, + "the challenge powers are hoisted once per committed layer" + ); + let mut v = q.p0; + let mut y_inv = inv; + for (j, opening) in openings.iter().enumerate() { + (v, y_inv) = emit_group_layer( + b, + shape, + j, + &fri.layers[j], + &fri.zeta_powers[j], + v, + y_inv, + opening, + q.bits, + ); + } + return emit_terminal_check(b, shape, fri, q.point, v); + } + // Fold 0 consumes the DEEP pair and authenticates nothing: there is no // layer under it, which is why `zetas` is one longer than `layers`. - let mut v = edsl::fri_fold(b, q.p0, q.p0_sym, fri.zetas[0], inv); + let mut v = edsl::fri_fold(b, q.p0, q.p0_sym(), fri.zetas[0], inv); // The point chain is one squaring per layer and nothing else — no bit // reversal, no domain lookup, no coset offset past the first point // (spec §6). And no parity branch, because the sign the odd slot introduces // into `x⁻¹` is the same sign it introduces into `v − sym`, so the two // cancel (spec §3). Parity is consulted ONLY for the leaf byte order below. - let mut inv_pow = inv; - for (i, opening) in openings.iter().enumerate() { - // `if index % 2 == 1 { [sym, v] } else { [v, sym] }` (`verifier.rs:637`) - // — the even codeword slot leads. `select(bit, l, r)` returns `(l, r)` - // at 0 and `(r, l)` at 1, so this IS that conditional. - let (first, second) = b.select(q.bits[i], v.as_cell(), opening.sym.as_cell()); - let leaf = sub_proof::emit_leaf_hash(b, FRI_LEAF_GROUP, &[first, second]); - let root = edsl::wrap_merkle_walk(b, leaf, &q.bits[i + 1..], &opening.siblings); - edsl::assert_digest_eq_lanes(b, root, &fri.layers[i].root_lanes); - - // `evaluation_point_vec[i] = υ^(−2^(i+1))` — `inv.square()` then one - // squaring per layer (`verifier.rs:692-697`). - inv_pow = b.mul(inv_pow, inv_pow); - v = edsl::fri_fold(b, v, opening.sym, fri.zetas[i + 1], inv_pow); - } - - // `x = υ^(2^total_folds)`: where the fold chain has arrived, and the - // terminal codeword's point at position `iota >> C`. See the doc comment. - let mut x = q.point; + if shape.is_legacy() { + let mut inv_pow = inv; + for (i, opening) in openings.iter().enumerate() { + (v, inv_pow) = emit_pair_layer( + b, + i, + &fri.layers[i], + fri.zetas[i + 1], + v, + inv_pow, + opening, + q.bits, + ); + } + } else { + // The group encoding (S3): committed layer `j` opens a whole coset of + // `2^{d_j}` values. `y⁻¹` at committed layer 0 is `υ^{−2}`, and each + // layer hands the next its own point (`x_g^{2^d}`). + assert_eq!( + fri.zeta_powers.len(), + c, + "the challenge powers are hoisted once per committed layer" + ); + let mut y_inv = b.mul(inv, inv); + for (j, opening) in openings.iter().enumerate() { + (v, y_inv) = emit_group_layer( + b, + shape, + j, + &fri.layers[j], + &fri.zeta_powers[j], + v, + y_inv, + opening, + q.bits, + ); + } + } + + emit_terminal_check(b, shape, fri, q.point, v) +} + +/// `x = υ^(2^total_folds)`: where the fold chain has arrived, and the terminal +/// codeword's point at position `iota >> C` (`r >> total_folds` under one row +/// — the same point, since `x_r` IS the query point at layer 0). See +/// [`emit_query_fri`]'s doc comment. Asserts `P(x) == v` and returns `v`. +fn emit_terminal_check( + b: &mut LfmBuilder, + shape: FriShape, + fri: &FriCommitments, + point: Felt, + v: Ext, +) -> Ext { + let mut x = point; for _ in 0..shape.total_folds() { x = b.mul(x, x); } @@ -598,6 +1053,227 @@ pub fn emit_query_fri( v } +/// One committed layer under TODAY's pair encoding (`FriShape::is_legacy`): +/// the opening carries the sibling value only. With the query's value `v` at +/// this layer and `x⁻¹` of its point one layer up (`inv_pow`), order the pair +/// by the parity bit `bits[layer]`, hash it as the leaf, authenticate it at the +/// tree's leaf index `bits[layer + 1..]`, square the point and fold with +/// `zeta`. Returns `(v, inv_pow)` at the next layer. The rows it emits are +/// `stark::fri::schedule::fri_pair_layer_rows` (pinned in `fri_group_tests`). +#[allow(clippy::too_many_arguments)] +pub fn emit_pair_layer( + b: &mut LfmBuilder, + layer: usize, + commitment: &LayerCommitment, + zeta: Ext, + v: Ext, + inv_pow: Felt, + opening: &LayerOpening, + bits: &[Bit], +) -> (Ext, Felt) { + assert_eq!(opening.values.len(), 1, "a pair layer opens its sibling"); + let sym = opening.values[0]; + // `if index % 2 == 1 { [sym, v] } else { [v, sym] }` (`verifier.rs:637`) + // — the even codeword slot leads. `select(bit, l, r)` returns `(l, r)` + // at 0 and `(r, l)` at 1, so this IS that conditional. + let (first, second) = b.select(bits[layer], v.as_cell(), sym.as_cell()); + let leaf = sub_proof::emit_leaf_hash(b, FRI_LEAF_GROUP, &[first, second]); + // `bits[layer+1..]` is this layer tree's whole leaf index; a cap walks its + // low bits and muxes the top ones. + commitment.authenticate(b, leaf, &bits[layer + 1..], &opening.siblings); + + // `evaluation_point_vec[i] = υ^(−2^(i+1))` — `inv.square()` then one + // squaring per layer (`verifier.rs:692-697`). + let inv_pow = b.mul(inv_pow, inv_pow); + (edsl::fri_fold(b, v, sym, zeta, inv_pow), inv_pow) +} + +/// The program constants of one group fold of exponent `d`, in +/// the host verifier's own terms (`fri::group::group_fold`, whose table is +/// `ω_{2^d}^t` for `ω_{2^d} = get_primitive_root_of_unity(d)`): +/// +/// - `slot[ℓ] = ω_{2^d}^{2^{d−1−ℓ}}`, so `x_g⁻¹ = y⁻¹·Π_ℓ slot[ℓ]^{s_ℓ} +/// = y⁻¹·ω_{2^d}^{br_d(s)}` for the slot `s` (bits `s_ℓ`, low first); +/// - `kappa[ℓ][j] = ω_{2^d}^{−2^ℓ·br_{d−ℓ−1}(j)}`: fold level `ℓ`'s pair `j` +/// sits at `(X, −X)` with `X⁻¹ = x_g^{−2^ℓ}·kappa[ℓ][j]` (`kappa[ℓ][0] = 1`). +fn group_fold_constants(d: u32) -> (Vec, Vec>) { + use math::fft::bit_reversing::reverse_index; + use math::field::traits::IsFFTField; + + let n = 1usize << d; + let w = ::get_primitive_root_of_unity( + u64::from(d), + ) + .expect("2^d divides the two-adicity for d <= DMAX"); + let pow = |e: usize| w.pow(e as u64); + let slot = (0..d as usize) + .map(|l| pow(1 << (d as usize - 1 - l))) + .collect(); + let kappa = (0..d as usize) + .map(|l| { + let half = n >> (l + 1); + (0..half) + .map(|j| { + let br = if half > 1 { + reverse_index(j, half as u64) + } else { + 0 + }; + pow((n - (br << l)) % n) + }) + .collect() + }) + .collect(); + (slot, kappa) +} + +// The load-bearing test of the slot check (the in-guest M1): a test build can +// emit without it and watch a moved `p₀` execute. Production has no switch. +#[cfg(test)] +thread_local! { + pub(super) static SKIP_SLOT_CHECK: core::cell::Cell = + const { core::cell::Cell::new(false) }; +} + +#[inline] +fn skip_slot_check() -> bool { + #[cfg(test)] + { + SKIP_SLOT_CHECK.with(|c| c.get()) + } + #[cfg(not(test))] + { + false + } +} + +/// `values[slot]` for the slot's bits, LOW first: a balanced mux of +/// `2^d − 1` `Select`s over ext cells, pairs `(2t, 2t + 1)` level by level. +fn emit_value_mux(b: &mut LfmBuilder, values: &[Ext], slot_bits: &[Bit]) -> Ext { + assert_eq!( + values.len(), + 1usize << slot_bits.len(), + "one mux level per slot bit" + ); + let mut level: Vec = values.iter().map(|v| v.as_cell()).collect(); + for bit in slot_bits { + let mut next = Vec::with_capacity(level.len() / 2); + for pair in level.chunks_exact(2) { + next.push(b.select(*bit, pair[0], pair[1]).0); + } + level = next; + } + level[0].as_ext() +} + +/// ★ One committed layer under the group encoding (S3). +/// +/// With `d = d_j`, `G = G_j`, the query's bits `bits` (low first, all +/// `index_bits`), its value `v` at this layer (the previous fold's output) and +/// the inverse `y⁻¹` of its point here: +/// +/// 1. **slot check** — `values[bits[G..G+d]] == v`, a `2^d − 1`-select mux and +/// an `assert_eq_ext`: the round-consistency check tying the opened group +/// to the value the previous fold produced (M1 on the host); +/// 2. **the group is the leaf** — hashed in full, position order (a +/// `GroupShape` of `2^{d−1}` ext columns covers `2^d` values), and +/// authenticated at the tree's leaf index `bits[G+d..]` against the +/// layer's root or cap; +/// 3. **the group fold** with `ζ, ζ², …, ζ^{2^{d−1}}`: `x_g⁻¹ = y⁻¹·ω_{2^d}^{br_d(s)}` +/// (`d` selects of constants and `d` base muls), then `d` levels of +/// `fri_fold` over the pairs, the level's `x_g^{−2^ℓ}` squared once per +/// level. A level with more than two pairs folds `ζ^{2^ℓ}·x_g^{−2^ℓ}` into +/// the challenge once (one `emul_base`) and multiplies each pair by its +/// constant inside the fold; a level with one or two pairs multiplies the +/// point instead (at most one base mul). After `d` levels the point is +/// `x_g^{−2^d}`, the NEXT layer's `y⁻¹`. +/// +/// Returns `(v, y⁻¹)` at the next layer. +#[allow(clippy::too_many_arguments)] +pub fn emit_group_layer( + b: &mut LfmBuilder, + shape: FriShape, + layer: usize, + commitment: &LayerCommitment, + zeta_powers: &[Ext], + v: Ext, + y_inv: Felt, + opening: &LayerOpening, + bits: &[Bit], +) -> (Ext, Felt) { + let d = shape.layer_fold(layer); + let g = shape.layer_bit_offset(layer); + let n = 1usize << d; + assert_eq!(opening.values.len(), n, "a group layer opens 2^d values"); + assert_eq!( + zeta_powers.len(), + d as usize, + "one challenge power per fold level" + ); + assert_eq!( + bits.len() - (g + d as usize), + shape.layer_depth(layer), + "the tree's leaf index is what is left of the query after the slot" + ); + let slot_bits = &bits[g..g + d as usize]; + + // (1) the slot check. + let v_slot = emit_value_mux(b, &opening.values, slot_bits); + if !skip_slot_check() { + b.assert_eq_ext(v_slot, v); + } + + // (2) the group is the leaf. + let cells: Vec = opening.values.iter().map(|x| x.as_cell()).collect(); + let leaf = sub_proof::emit_leaf_hash( + b, + GroupShape { + num_columns: n / 2, + is_ext: true, + }, + &cells, + ); + commitment.authenticate(b, leaf, &bits[g + d as usize..], &opening.siblings); + + // (3) the group fold. + let (slot_factors, kappa) = group_fold_constants(d); + let mut xinv = y_inv; + for (bit, factor) in slot_bits.iter().zip(&slot_factors) { + let one = b.felt_const(FE::one()); + let f = b.felt_const(*factor); + let (chosen, _) = b.select(*bit, one.as_cell(), f.as_cell()); + xinv = b.mul(xinv, Felt(chosen.0)); + } + let mut vals = opening.values.clone(); + for (l, zeta) in zeta_powers.iter().enumerate() { + let half = vals.len() / 2; + let scaled = (half > 2).then(|| b.emul_base(*zeta, xinv)); + let mut next = Vec::with_capacity(half); + for j in 0..half { + let (lo, hi) = (vals[2 * j], vals[2 * j + 1]); + let folded = match scaled { + Some(zx) => { + let k = b.felt_const(kappa[l][j]); + edsl::fri_fold(b, lo, hi, zx, k) + } + None => { + let x = if j == 0 { + xinv + } else { + let k = b.felt_const(kappa[l][j]); + b.mul(xinv, k) + }; + edsl::fri_fold(b, lo, hi, *zeta, x) + } + }; + next.push(folded); + } + vals = next; + xinv = b.mul(xinv, xinv); + } + (vals[0], xinv) +} + /// A whole sub-proof, both legs: every query's openings authenticated and folded /// to `p₀` ([`super::sub_proof::emit_sub_proof_with_bits`]), then that `p₀` /// folded down FRI's layers to the terminal check. @@ -626,6 +1302,11 @@ pub fn emit_sub_proof_with_fri( shape.num_queries, num_queries, "the query count is one shape, declared once" ); + assert_eq!( + sub.layout, + shape.leaf_layout(), + "both legs verify one table at one leaf layout" + ); let (sub_arenas, queries) = super::sub_proof::emit_sub_proof_with_bits(b, sub, num_queries); let (fri_arenas, fri) = declare_fri(b, shape, num_queries); @@ -640,8 +1321,8 @@ pub fn emit_sub_proof_with_fri( shape, &fri, &FriQuery { - p0: out.deep.0, - p0_sym: out.deep.1, + p0: out.deep, + p0_sym: out.deep_sym, point: out.point, point_sym: out.point_sym, bits: &out.bits, diff --git a/prover/src/lfm/fri_group_tests.rs b/prover/src/lfm/fri_group_tests.rs new file mode 100644 index 000000000..ef68ca2b1 --- /dev/null +++ b/prover/src/lfm/fri_group_tests.rs @@ -0,0 +1,767 @@ +//! S3 in the in-guest FRI verifier: the shape from the shared schedule (G1) +//! and the group-layer emitter (G2). +//! +//! Checked against the host's own artefacts, never against a second model: +//! - the in-guest shape (schedule, layer depths, caps) against the host's +//! `StarkCaps::for_options` / `FriFormat::schedule` over a sweep of shapes; +//! - the emitted verifier against the host's checked-in RPX vectors +//! (`crypto/stark/tests/vectors/zf_fri/d_proof_rpx_*`: pair, dp, the uneven +//! `[3, 1, 3]` override, and the two capped Q = 20 formats), +//! executed, with its permutation count equal to the closed form; +//! - tampers of every value a group opening carries, and the slot check shown +//! load-bearing (a moved `p₀` executes when, and only when, it is skipped); +//! - the {cap off, auto} × {pair, dp, uneven dp} round-trip matrix on a real +//! laptop-scale proof, both legs as one program; +//! - every row the emitter emits per FRI layer (group and +//! pair, capped and uncapped) equals the DP's model (`stark::fri::schedule`), +//! and a DEEP point's rows equal the S2 `auto` rule's DEEP term. + +use crypto::merkle_tree::cap::CapPolicy; +use serde_json::Value; +use stark::examples::read_only_memory_logup::LogReadOnlyPublicInputs; +use stark::fri::schedule::{ + FRI_COST_WEIGHTS, FriLayerRows, fri_group_layer_rows, fri_layer_cost_q, fri_pair_layer_cost_q, + fri_pair_layer_rows, +}; +use stark::leaf_layout::deep_point_xalu_rows; +use stark::merkle_caps::StarkCaps; +use stark::proof::options::{FriMode, FriScheduleOverride, ProofFormat, ProofOptions}; +use stark::proof::stark::StarkProof; +use stark::proof::view::StarkProofView; + +use crate::tables::types::{FE, FEE, GoldilocksExtension, GoldilocksField}; + +use super::builder::Felt; +use super::builder::LfmBuilder; +use super::compiler::{LfmProgram, compile}; +use super::deep::{DeepInvariants, DeepOpening, DeepShape, emit_deep_point}; +use super::executor::execute; +use super::fri::{FriShape, LayerCommitment, LayerOpening, emit_group_layer, emit_pair_layer}; +use super::fri_tests::{folding_fixture_with, fri_only_program, host_fri_from, permutations}; +use super::instr::Instr; +use super::word::{LfmWord, base_word, ext_word, word_as_ext}; + +type Gl = GoldilocksField; +type Ext3 = GoldilocksExtension; +type VectorProof = StarkProof>; + +// ============================================================================= +// G1 — the in-guest shape IS the host's layout +// ============================================================================= + +/// ★ One schedule, one depth, one cap per layer, on both sides: the in-guest +/// `FriShape` (the emitter's program shape) against the host's `StarkCaps` — +/// the function the prover embeds caps with and the verifier checks them with, +/// itself built on the host's `FriFoldLayout` — over every LDE size of +/// interest, both terminals in production (T = 9 base legs, T = 10 LFM +/// proofs), both FRI modes and both cap policies. +#[test] +fn the_in_guest_fri_shape_is_the_hosts_layout() { + let mut checked = 0usize; + for (blowup, k) in [(4u8, 7u8), (4, 8), (2, 7)] { + for queries in [3usize, 24, 110] { + for cap in [CapPolicy::Off, CapPolicy::Auto] { + for fri in [FriMode::Pair, FriMode::Dp] { + let blowup_log = (blowup as u32).trailing_zeros(); + for lde_log in (blowup_log + 1)..=25 { + let opts = ProofOptions { + blowup_factor: blowup, + fri_number_of_queries: queries, + coset_offset: 3, + grinding_factor: 0, + fri_final_poly_log_degree: k, + format: ProofFormat { + merkle_cap: cap, + fri_mode: fri, + ..ProofFormat::DEFAULT + }, + }; + let shape = FriShape::from_options(&opts, lde_log); + shape.check(); + let host = StarkCaps::for_options(&opts, lde_log as usize, false) + .expect("a row-pair format lays out"); + let depths: Vec = (0..shape.num_committed()) + .map(|j| shape.layer_depth(j)) + .collect(); + let caps: Vec = (0..shape.num_committed()) + .map(|j| shape.layer_cap(j)) + .collect(); + assert_eq!(depths, host.fri_depths, "{opts:?} lde {lde_log}"); + assert_eq!(caps, host.fri, "{opts:?} lde {lde_log}"); + assert_eq!(shape.index_bits(), host.trace_depth); + assert_eq!(shape.is_legacy(), fri == FriMode::Pair); + checked += 1; + } + } + } + } + } + println!("{checked} shapes: in-guest schedule, depths and caps == the host's"); +} + +// ============================================================================= +// G2 — the emitted verifier on the host's RPX vectors +// ============================================================================= + +fn ext_of(v: &Value) -> FEE { + let limbs: Vec = v + .as_array() + .expect("an ext value is three limbs") + .iter() + .map(|x| x.as_u64().expect("a canonical limb")) + .collect(); + assert_eq!(limbs.len(), 3); + FEE::new([FE::from(limbs[0]), FE::from(limbs[1]), FE::from(limbs[2])]) +} + +/// One checked-in RPX (d) vector: its JSON, its proof, and the in-guest shape +/// the emitter builds for it from the vector's FORMAT (the host generator's +/// own `proof_formats`, never re-spelled here). +struct Vector { + name: &'static str, + json: Value, + proof: VectorProof, + shape: FriShape, +} + +fn rpx_vectors() -> Vec { + stark::fri::vectors::proof_formats() + .into_iter() + .map(|(name, format, queries)| { + let dir = stark::fri::vectors::vectors_dir(); + let stem = format!("d_proof_rpx_{name}"); + let json: Value = serde_json::from_slice( + &std::fs::read(dir.join(format!("{stem}.json"))).expect("the vector JSON"), + ) + .expect("valid JSON"); + let bytes = std::fs::read(dir.join(format!("{stem}.rkyv"))).expect("the vector proof"); + let proof: VectorProof = + rkyv::from_bytes::(&bytes).expect("rkyv"); + let opts = stark::fri::vectors::proof_options(format, queries); + let lde_log = json["lde_log"].as_u64().expect("lde_log") as u32; + let shape = FriShape::from_options(&opts, lde_log); + Vector { + name, + json, + proof, + shape, + } + }) + .collect() +} + +impl Vector { + /// The arenas [`fri_only_program`] declares: `(ι, p₀(υ), p₀(−υ))` per + /// query, then the roots, the ζs, the terminal coefficients, the per-query + /// layer openings and (when capped) the caps. + fn arenas(&self) -> Vec> { + let queries = self.json["queries_detail"].as_array().expect("queries"); + let mut deep = Vec::new(); + for q in queries { + deep.push(base_word(FE::from(q["iota"].as_u64().expect("iota")))); + deep.push(ext_word(&ext_of(&q["deep"]))); + deep.push(ext_word(&ext_of(&q["deep_sym"]))); + } + let view = StarkProofView::Owned(&self.proof); + let (openings, caps) = super::epoch_verify_tests::fri_layer_openings(view, self.shape); + let mut per_query = Vec::new(); + for query in &openings { + for (values, path) in query { + per_query.extend(values.iter().map(ext_word)); + per_query.extend(super::proof_arena::commitments_to_arena(path)); + } + } + let zetas: Vec = self.json["zetas"] + .as_array() + .expect("zetas") + .iter() + .map(|z| ext_word(&ext_of(z))) + .collect(); + let mut out = vec![ + deep, + super::proof_arena::commitments_to_arena(&self.proof.fri_layers_merkle_roots), + zetas, + self.proof + .fri_final_poly_coeffs + .iter() + .map(ext_word) + .collect(), + per_query, + ]; + if self.shape.cap_words(super::proof_arena::words_per_root()) > 0 { + out.push(super::proof_arena::commitments_to_arena(&caps)); + } + out + } + + fn program(&self) -> LfmProgram { + fri_only_program(self.shape, self.shape.num_queries) + } +} + +/// ★ The emitted FRI verifier accepts every RPX (d) vector — today's pair +/// proof, the DP schedule, the uneven `[3, 1, 3]` override (the only shape +/// that catches a fold-count off-by-one) and both capped Q = 20 +/// formats — with the vector's schedule, depths and caps derived by the +/// emitter's own shape, and the permutation count exactly the closed form. +#[test] +fn the_emitted_fri_verifier_accepts_every_rpx_vector() { + for v in rpx_vectors() { + let s = v.shape; + s.check(); + let schedule: Vec = v.json["schedule"] + .as_array() + .expect("schedule") + .iter() + .map(|d| d.as_u64().expect("d") as u8) + .collect(); + assert_eq!(s.schedule(), schedule, "{}: the schedule", v.name); + assert_eq!( + s.is_legacy(), + v.json["legacy_encoding"] + .as_bool() + .expect("legacy_encoding"), + "{}", + v.name + ); + if let Some(caps) = v.json.get("fri_caps") { + let want: Vec = caps + .as_array() + .expect("fri_caps") + .iter() + .map(|c| c.as_u64().expect("c") as usize) + .collect(); + let depths: Vec = v.json["fri_tree_depths"] + .as_array() + .expect("depths") + .iter() + .map(|c| c.as_u64().expect("d") as usize) + .collect(); + assert_eq!( + (0..s.num_committed()) + .map(|j| s.layer_cap(j)) + .collect::>(), + want, + "{}: caps", + v.name + ); + assert_eq!( + (0..s.num_committed()) + .map(|j| s.layer_depth(j)) + .collect::>(), + depths, + "{}: depths", + v.name + ); + } + let program = v.program(); + let exec = execute(&program, &v.arenas(), &crate::hash_pin::BLOCK_HASHER) + .unwrap_or_else(|e| panic!("{}: the honest vector must execute: {e:?}", v.name)); + assert_eq!(exec.public_words.len(), s.num_queries); + let closed = s.num_queries * s.permutations_per_query() + s.cap_permutations(); + assert_eq!( + permutations(&program), + closed, + "{}: emitted permutations against the closed form", + v.name + ); + println!( + "{:<9} Q={:<2} schedule {:?} caps {:?}: {} permutations, {} instructions", + v.name, + s.num_queries, + s.schedule(), + (0..s.num_committed()) + .map(|j| s.layer_cap(j)) + .collect::>(), + closed, + program.instrs.len() + ); + } +} + +/// ★ Every value a group opening carries is bound: the slot value, a non-slot +/// value, the last value of the group, a sibling, a cap word, a folding +/// challenge, a terminal coefficient, and the DEEP value the first slot check +/// compares against. Run on the uneven override and on the capped DP vector. +#[test] +fn no_tampered_group_opening_value_can_pass() { + for v in rpx_vectors() { + if !matches!(v.name, "dp_3_1_3" | "cap_dp") { + continue; + } + let program = v.program(); + let honest = v.arenas(); + execute(&program, &honest, &crate::hash_pin::BLOCK_HASHER).expect("honest"); + let q0 = &v.json["queries_detail"][0]["layers"][0]; + let slot = q0["slot"].as_u64().expect("slot") as usize; + let d0 = 1usize << v.shape.layer_fold(0); + let other = (slot + 1) % d0; + // Arenas: deep, roots, zetas, coeffs, queries[, caps]. + let mut bump: Vec<(String, usize, usize)> = vec![ + ("p0 (the DEEP value)".into(), 0, 1), + ("zeta_1".into(), 2, 1), + ("terminal coefficient 0".into(), 3, 0), + (format!("query 0 layer 0 slot value (slot {slot})"), 4, slot), + (format!("query 0 layer 0 non-slot value {other}"), 4, other), + ("query 0 layer 0 last group value".into(), 4, d0 - 1), + ("query 0 layer 0 first sibling".into(), 4, d0), + ]; + if honest.len() == 6 { + bump.push(("cap word 0".into(), 5, 0)); + bump.push(("last cap word".into(), 5, honest[5].len() - 1)); + } + for (label, arena, word) in bump { + let mut bad = honest.clone(); + bad[arena][word][0] += FE::one(); + execute(&program, &bad, &crate::hash_pin::BLOCK_HASHER).expect_err(&format!( + "{}: moving {label} must make the program unexecutable", + v.name + )); + } + } +} + +/// ★ The slot check is LOAD-BEARING (the in-guest M1). Under the group +/// encoding the value the first fold produces from the DEEP pair meets the +/// committed layers ONLY at the slot check: the leaf hashes the group, the +/// walk authenticates it, the group fold reads it. So a moved `p₀(υ)` is +/// refused with the check and ACCEPTED without it — which is exactly a +/// verifier that would accept FRI for a different codeword than the trace +/// openings commit to. +#[test] +fn the_slot_check_is_load_bearing() { + let v = rpx_vectors() + .into_iter() + .find(|v| v.name == "cap_dp") + .expect("the capped dp vector"); + let honest = v.arenas(); + let mut moved = honest.clone(); + moved[0][1][0] += FE::one(); + + let with = v.program(); + execute(&with, &honest, &crate::hash_pin::BLOCK_HASHER).expect("honest"); + execute(&with, &moved, &crate::hash_pin::BLOCK_HASHER) + .expect_err("a moved p0 must be refused by the slot check"); + + super::fri::SKIP_SLOT_CHECK.with(|c| c.set(true)); + let without = v.program(); + super::fri::SKIP_SLOT_CHECK.with(|c| c.set(false)); + execute(&without, &moved, &crate::hash_pin::BLOCK_HASHER) + .expect("WITHOUT the slot check a moved p0 is accepted — the check is the only binding"); +} + +// ============================================================================= +// The {cap} × {fri} round-trip matrix, both legs, on a real proof +// ============================================================================= + +/// ★ The cap × FRI matrix on a real laptop-scale proof (L2G_MEMORY, 2048 +/// rows, blowup 2, `k = 2` so the committed chain covers 11 → 3, Q = 24): +/// {cap off, auto} × {pair, dp, dp `[3, 1, 4]`}. Per cell the FRI leg alone +/// and both legs as one program execute over every query, reach the terminal +/// codeword production computed, and emit exactly the closed form. +#[test] +fn the_cap_and_fri_matrix_round_trips_in_guest() { + use super::epoch_verify::{blocks_for, group_leaf_felts}; + + let hash = super::edsl::WrapHash::production(); + for cap in [CapPolicy::Off, CapPolicy::Auto] { + for (label, fri, over) in [ + ("pair", FriMode::Pair, None), + ("dp", FriMode::Dp, None), + ("dp [3,1,4]", FriMode::Dp, Some(&[3u8, 1, 4][..])), + ] { + let mut opts = stark::proof::options::GoldilocksCubicProofOptions::with_blowup(2) + .expect("blowup 2"); + opts.fri_number_of_queries = 24; + opts.grinding_factor = 0; + opts.fri_final_poly_log_degree = 2; + opts.format = ProofFormat { + merkle_cap: cap, + fri_mode: fri, + fri_schedule_override: over.and_then(FriScheduleOverride::new), + ..ProofFormat::DEFAULT + }; + let (air, proof) = folding_fixture_with(2048, opts); + let h = host_fri_from(&*air, &proof); + let s = h.shape; + let all: Vec = (0..h.trace.iotas.len()).collect(); + let codeword = h.terminal_codeword(); + let position = |iota: usize| iota >> (s.total_folds() - 1); + + // The FRI leg alone. + let program = fri_only_program(s, all.len()); + let exec = execute( + &program, + &h.all_arenas(&all), + &crate::hash_pin::BLOCK_HASHER, + ) + .unwrap_or_else(|e| panic!("cap={cap} fri={label}: FRI leg: {e:?}")); + for (k, &q) in all.iter().enumerate() { + let v = word_as_ext(&exec.public_words[k].1).expect("ext"); + assert_eq!( + v, + codeword[position(h.trace.iotas[q])], + "cap={cap} fri={label}" + ); + } + assert_eq!( + permutations(&program), + all.len() * s.permutations_per_query() + s.cap_permutations(), + "cap={cap} fri={label}: FRI leg closed form" + ); + + // Both legs as one program. + let mut b = LfmBuilder::new().with_wrap_hash(hash); + let (_, _, terminal) = + super::fri::emit_sub_proof_with_fri(&mut b, &h.trace.shape, s, all.len()); + for t in &terminal { + b.public(t.as_cell()); + } + let joined = compile(b.finish()); + let mut arenas = h.trace.arenas(&all); + arenas.extend(h.fri_arenas(&all)); + let exec = execute(&joined, &arenas, &crate::hash_pin::BLOCK_HASHER) + .unwrap_or_else(|e| panic!("cap={cap} fri={label}: joined: {e:?}")); + for (k, &q) in all.iter().enumerate() { + let v = word_as_ext(&exec.public_words[k].1).expect("ext"); + assert_eq!(v, codeword[position(h.trace.iotas[q])]); + } + let sub = &h.trace.shape; + let leaves: usize = sub + .groups() + .iter() + .map(|g| blocks_for(group_leaf_felts(g), hash)) + .sum(); + let closed = all.len() + * (leaves + sub.groups().len() * sub.path_len() + s.permutations_per_query()) + + sub.cap_permutations() + + s.cap_permutations(); + assert_eq!( + permutations(&joined), + closed, + "cap={cap} fri={label}: both legs' closed form" + ); + println!( + "cap={cap:<4} fri={label:<10} schedule {:?} FRI caps {:?} trace cap {}: FRI leg \ + {} perms, both legs {} perms / {} instructions", + s.schedule(), + (0..s.num_committed()) + .map(|j| s.layer_cap(j)) + .collect::>(), + sub.trace_cap, + permutations(&program), + closed, + joined.instrs.len(), + ); + } + } +} + +// ============================================================================= +// Every emitted row per FRI layer and per DEEP point, against +// the host's cost model (`stark::fri::schedule`, `stark::leaf_layout`) +// ============================================================================= + +/// The kinds of every instruction a program emits: `(selects, XALU, BALU, +/// hashes, unpacks, packs, hints, other)`. +fn count_kinds(instrs: &[Instr]) -> [usize; 8] { + let mut k = [0usize; 8]; + for i in instrs { + let slot = match i { + Instr::Select { .. } => 0, + Instr::ExtAlu { .. } => 1, + Instr::BaseAlu { .. } => 2, + Instr::Hash { .. } => 3, + Instr::Unpack { .. } => 4, + Instr::Pack { .. } => 5, + Instr::Hint { .. } => 6, + _ => 7, + }; + k[slot] += 1; + } + k +} + +/// The rows `emit(times)` adds per repetition: the program is built at +/// `times = 1` and `times = 2` over the same hinted inputs, and the difference +/// is one repetition's rows — interned program constants and one-time setup +/// (the index decomposition, the root's unpack, the cap's hints and root +/// check) fall out of it. Asserts the repetition emits nothing but the priced +/// row kinds. +fn rows_of_one(emit: &dyn Fn(usize) -> LfmProgram) -> FriLayerRows { + let (once, twice) = (emit(1), emit(2)); + let (a, b) = (count_kinds(&once.instrs), count_kinds(&twice.instrs)); + let d: Vec = (0..8).map(|i| (b[i] - a[i]) as u64).collect(); + assert_eq!(d[7], 0, "a repetition emits only priced row kinds"); + assert_eq!( + (twice.instrs.len() - once.instrs.len()) as u64, + d.iter().sum::(), + "every instruction is counted" + ); + FriLayerRows { + selects: d[0], + xalu: d[1], + balu: d[2], + hashes: d[3], + unpacks: d[4], + packs: d[5], + hints: d[6], + } +} + +/// Tree depth of the measured layers. +const MEASURED_DEPTH: usize = 2; + +/// A program emitting `times` openings of one committed FRI layer (group +/// layer of exponent `d`, or today's pair layer when `d == 0`) over a +/// `MEASURED_DEPTH`-level tree capped at `c`, every shared input hinted before +/// the first opening. Every opening's values and siblings are hinted in the +/// loop (as `hint_layer_openings` does), so they count. +fn fri_layer_program(d: u32, c: usize, times: usize) -> LfmProgram { + let pair = d == 0; + let fold = if pair { 1 } else { d }; + let shape = FriShape { + log2_lde_length: fold + MEASURED_DEPTH as u32 + 1, + blowup_log: 1, + final_poly_log_degree: 1, + coset_offset: 3, + num_queries: 1, + format: if pair { + ProofFormat::DEFAULT + } else { + ProofFormat { + fri_mode: FriMode::Dp, + fri_schedule_override: FriScheduleOverride::new(&[d as u8]), + ..ProofFormat::DEFAULT + } + }, + }; + shape.check(); + assert_eq!(shape.is_legacy(), pair); + assert_eq!(shape.schedule(), vec![fold as u8]); + assert_eq!(shape.layer_depth(0), MEASURED_DEPTH); + + let n = if pair { 1 } else { 1usize << d }; + let num_siblings = MEASURED_DEPTH - c; + let mut b = LfmBuilder::new().with_wrap_hash(super::edsl::WrapHash::production()); + assert_eq!( + super::edsl::digest_words(&b), + 1, + "the model prices the production one-cell digest" + ); + let arena = b.declare_arena((4 + fold as usize + (1 << c) + times * (n + num_siblings)) as u32); + let root = b.hint_word(arena, 0); + let mut commitment = LayerCommitment::from_lanes(vec![b.unpack(root)]); + let v = b.hint_word(arena, 1).as_ext(); + let y_inv = b.hint_felt(arena, 2); + let index = b.hint_felt(arena, 3); + let bits = b.bit_dec(index, shape.index_bits()); + let zetas: Vec<_> = (0..fold) + .map(|i| b.hint_word(arena, 4 + i).as_ext()) + .collect(); + let mut at = commitment.hint_cap(&mut b, arena, 4 + fold, c); + for _ in 0..times { + let values = (0..n) + .map(|_| { + at += 1; + b.hint_word(arena, at - 1).as_ext() + }) + .collect(); + let siblings = (0..num_siblings) + .map(|_| { + at += 1; + super::edsl::WrapDigest::from_cell(b.hint_word(arena, at - 1)) + }) + .collect(); + let opening = LayerOpening { values, siblings }; + if pair { + emit_pair_layer(&mut b, 0, &commitment, zetas[0], v, y_inv, &opening, &bits); + } else { + emit_group_layer( + &mut b, + shape, + 0, + &commitment, + &zetas, + v, + y_inv, + &opening, + &bits, + ); + } + } + compile(b.finish()) +} + +/// ★ The rows one query's opening of a committed FRI layer +/// emits in-guest EQUAL the host model's, kind by kind and in total, for group +/// layers `d = 1..=6` and today's pair layer, uncapped and capped (`c = 1, 2` +/// on a two-level tree): +/// +/// ```text +/// group d, depth D, cap c (stark::fri::schedule::fri_group_layer_rows): +/// selects (2^d − 1) slot mux + (D − c) walk + (2^c − 1) cap mux + d x_g +/// XALU 5·(2^d − 1) folds + 2 slot assert + max(0, d − 2) scaling +/// BALU d twiddles + d x_g + [d ≥ 2] scaling + 8 root compare +/// hashes leaf(d) + (D − c) +/// unpacks 2^d values + 1 walked digest + [c ≥ 1] cap node +/// packs ⌈3·2^d / 4⌉ leaf words +/// hints 2^d values + (D − c) siblings +/// pair, depth D, cap c (fri_pair_layer_rows): +/// selects 1 + (D − c) + (2^c − 1), XALU 5, BALU 1 + 8, hashes 1 + (D − c), +/// unpacks 2 + 1 + [c ≥ 1], packs 2, hints 1 + (D − c) +/// ``` +/// +/// The DP prices exactly these rows: `fri_layer_cost_q` is the uncapped rows +/// minus the cap's gain and the `c` sibling hints it removes, checked here +/// against the capped rows priced directly plus the cap's per-tree cost. A +/// change to the emitter that is not also a change to the model fails here. +#[test] +fn every_emitted_fri_row_is_priced() { + let w = FRI_COST_WEIGHTS; + println!("\n layer c | sel XALU BALU hash unpack pack hint | ns/query"); + for c in 0..=2usize { + for d in 0..=6u32 { + let got = rows_of_one(&|times| fri_layer_program(d, c, times)); + let (label, model) = if d == 0 { + ( + "pair".to_string(), + fri_pair_layer_rows(MEASURED_DEPTH as u32, c as u32), + ) + } else { + ( + format!("d={d}"), + fri_group_layer_rows(d, MEASURED_DEPTH as u32, c as u32), + ) + }; + assert_eq!(got, model, "{label} c={c}: emitted rows == model rows"); + println!( + " {label:>5} {c} | {:>3} {:>4} {:>4} {:>4} {:>6} {:>4} {:>4} | {:>8}", + got.selects, + got.xalu, + got.balu, + got.hashes, + got.unpacks, + got.packs, + got.hints, + got.price(&w) + ); + } + } + + // The DP's per-layer price is these rows: uncapped exactly; capped, the + // capped rows plus the cap's once-per-tree cost (`cap_gain`'s per-tree + // term: 2^c − 1 compressions, 2^c hints, one compare). + let depth = 10u32; + for q in [1u64, 20, 110] { + for cap in [ + CapPolicy::Off, + CapPolicy::Fixed(1), + CapPolicy::Fixed(3), + CapPolicy::Auto, + ] { + let c = cap.height(q as usize, depth as usize) as u32; + let per_tree = if c == 0 { + 0 + } else { + ((1u64 << c) - 1) * w.cap.compress + (1u64 << c) * w.cap.hint + w.cap.compare + }; + for d in 1..=6u32 { + let direct = q * fri_group_layer_rows(d, depth, c).price(&w) + per_tree; + assert_eq!( + fri_layer_cost_q(&w, d, depth, q, cap), + direct, + "group d={d} q={q} cap={cap:?}" + ); + } + let direct = q * fri_pair_layer_rows(depth, c).price(&w) + per_tree; + assert_eq!( + fri_pair_layer_cost_q(&w, depth, q, cap), + direct, + "pair q={q} cap={cap:?}" + ); + } + } +} + +/// A program emitting `times` DEEP points of `shape` over hinted openings and +/// hinted invariants (the invariants hinted before the first point). +fn deep_point_program(shape: &DeepShape, times: usize) -> LfmProgram { + let e = shape.num_eval_points; + let cols = shape.num_total_cols; + let parts = shape.num_composition_parts; + let mut b = LfmBuilder::new().with_wrap_hash(super::edsl::WrapHash::production()); + let arena = b.declare_arena((4 * e + 4 + times * (1 + cols + parts)) as u32); + let mut at = 0u32; + let mut next = |b: &mut LfmBuilder| { + at += 1; + b.hint_word(arena, at - 1).as_ext() + }; + let gamma = next(&mut b); + let inv = DeepInvariants { + ood_row_sum: (0..e).map(|_| next(&mut b)).collect(), + h_sum_zpow: next(&mut b), + z_pow: next(&mut b), + row_points: (0..e).map(|_| next(&mut b)).collect(), + gamma_pow_surviving: next(&mut b), + gamma_pow_block: (0..e).map(|_| next(&mut b)).collect(), + gamma_stride: (0..e).map(|_| next(&mut b)).collect(), + }; + for _ in 0..times { + let point = Felt(next(&mut b).as_cell().0); + let opening = DeepOpening { + point, + trace: (0..cols).map(|_| next(&mut b)).collect(), + parts: (0..parts).map(|_| next(&mut b)).collect(), + }; + emit_deep_point(&mut b, shape, gamma, &inv, &opening); + } + compile(b.finish()) +} + +/// ★ The XALU rows of ONE in-guest DEEP point EQUAL the S2 `auto` +/// rule's DEEP term (`stark::leaf_layout::deep_point_xalu_rows`: +/// `num_surviving + 4·E + P + 3`), over shapes with and without a next row, +/// a widened step, and one or many composition parts. DEEP emits no other +/// row kind; the point's hinted inputs here stand in for the cells the trace +/// walk already authenticated. +#[test] +fn the_deep_point_rows_are_the_auto_rules_deep_term() { + let shapes = [ + // (step, offsets, cols, next-row cols, parts) + (1usize, 1usize, 7usize, vec![], 1usize), + (1, 2, 5, vec![1, 3], 2), + (1, 2, 40, vec![0, 5, 39], 3), + (2, 2, 6, vec![2], 2), + (1, 3, 9, vec![0, 8], 4), + ]; + for (step, offsets, cols, next_cols, parts) in shapes { + let shape = DeepShape { + step_size: step, + num_eval_points: offsets * step, + num_total_cols: cols, + next_row_cols: next_cols.clone(), + num_composition_parts: parts, + log2_trace_length: 8, + }; + let got = rows_of_one(&|times| deep_point_program(&shape, times)); + let want = deep_point_xalu_rows( + shape.num_surviving() as u64, + shape.num_eval_points as u64, + parts as u64, + ); + let ctx = + format!("step {step} offsets {offsets} cols {cols} next {next_cols:?} parts {parts}"); + assert_eq!(got.xalu, want, "{ctx}: DEEP XALU rows"); + assert_eq!( + (got.selects, got.balu, got.hashes, got.unpacks, got.packs), + (0, 0, 0, 0, 0), + "{ctx}: DEEP emits only XALU rows" + ); + assert_eq!( + got.hints as usize, + 1 + cols + parts, + "{ctx}: the stand-in hints" + ); + } +} diff --git a/prover/src/lfm/fri_tests.rs b/prover/src/lfm/fri_tests.rs index 711e4b26d..b68c59479 100644 --- a/prover/src/lfm/fri_tests.rs +++ b/prover/src/lfm/fri_tests.rs @@ -33,6 +33,7 @@ //! challenges to a transcript — they arrive as arena values, and tying them to a //! replay is assembly's obligation. +use crypto::merkle_tree::cap::CapPolicy; use math::field::traits::IsPrimeField; use math::polynomial::Polynomial; use stark::config::Commitment; @@ -76,6 +77,17 @@ fn embed(x: &FE) -> FEE { pub(super) fn folding_fixture( num_boundaries: usize, blowup: usize, +) -> (BoxedAir, MultiProof) { + let opts = stark::proof::options::GoldilocksCubicProofOptions::with_blowup(blowup as u8) + .expect("a power-of-two blowup is valid"); + folding_fixture_with(num_boundaries, opts) +} + +/// [`folding_fixture`] under explicit proof options — the format axis (a +/// Merkle cap, a FRI fold schedule) and the query count a cap needs. +pub(super) fn folding_fixture_with( + num_boundaries: usize, + opts: stark::proof::options::ProofOptions, ) -> (BoxedAir, MultiProof) { use crate::tables::local_to_global::{ CellBoundary, FiniClaim, InitClaim, generate_local_to_global_trace, @@ -87,8 +99,6 @@ pub(super) fn folding_fixture( "the trace is padded to a power of two, so a non-power-of-two row count \ would not be the shape asked for" ); - let opts = stark::proof::options::GoldilocksCubicProofOptions::with_blowup(blowup as u8) - .expect("a power-of-two blowup is valid"); let air = crate::continuation::l2g_memory_air(&opts, EPOCH_TEST_LABEL); let boundaries: Vec = (0..num_boundaries as u64) @@ -120,19 +130,24 @@ pub(super) fn folding_fixture( } /// Everything the FRI leg reads about one real sub-proof. -struct HostFri { - shape: FriShape, +pub(super) struct HostFri { + pub(super) shape: FriShape, /// The trace-side host fixture over the SAME proof: the openings, the roots, /// and production's own DEEP answers, which are this leg's `p₀`. - trace: HostSubProof, + pub(super) trace: HostSubProof, /// One root per committed layer, in fold order. - layer_roots: Vec, + pub(super) layer_roots: Vec, /// `ζ₀ .. ζ_C` from the verifier's replay. - zetas: Vec, + pub(super) zetas: Vec, /// The terminal polynomial's coefficients, low-to-high. - coeffs: Vec, - /// `[query][layer]` — `(pᵢ(−υ^(2ⁱ)), path)`. - openings: Vec)>>, + pub(super) coeffs: Vec, + /// `[query][layer]` — `(opened values, path)`: the sibling `pᵢ(−υ^(2ⁱ))` + /// under `pair`, the whole group under a fold schedule. Paths are cut at + /// each layer's cap (query 0's cap split off into [`Self::caps`]). + pub(super) openings: Vec, Vec)>>, + /// Every capped layer's cap, in layer order — the caps arena. Empty at + /// the default format. + pub(super) caps: Vec, } /// Build the FRI host fixture for a real proof of `num_boundaries` rows. @@ -143,7 +158,7 @@ fn host_fri(num_boundaries: usize, blowup: usize) -> HostFri { /// [`host_fri`] for a proof the caller already holds — needed where the test /// also wants the AIR's verifier domain. -fn host_fri_from( +pub(super) fn host_fri_from( air: &dyn AIR, proof: &MultiProof, ) -> HostFri { @@ -152,19 +167,12 @@ fn host_fri_from( let trace = build_host_sub_proof(air, proof); let view = StarkProofView::Owned(&proof.proofs[0]); let opts = air.options(); - let shape = FriShape::from_options(opts, trace.shape.log2_lde_length); + let shape = FriShape::for_layout(opts, trace.shape.log2_lde_length, trace.shape.layout); shape.check(); - let openings = (0..view.query_list_len()) - .map(|q| { - let d = view.query(q); - d.layers_evaluations_sym() - .iter() - .enumerate() - .map(|(i, sym)| (*sym, d.layer_auth_path(i).to_vec())) - .collect() - }) - .collect(); + // Per layer the opened values (the sibling, or the whole group) and the + // path cut at the layer's cap; query 0's caps go to the caps arena. + let (openings, caps) = super::epoch_verify_tests::fri_layer_openings(view, shape); HostFri { shape, @@ -172,27 +180,33 @@ fn host_fri_from( zetas: trace.zetas.clone(), coeffs: view.fri_final_poly_coeffs().to_vec(), openings, + caps, trace, } } impl HostFri { /// The arenas the FRI-only program declares, for the given queries. - fn fri_arenas(&self, queries: &[usize]) -> Vec> { - vec![ + pub(super) fn fri_arenas(&self, queries: &[usize]) -> Vec> { + let mut out = vec![ super::proof_arena::commitments_to_arena(&self.layer_roots), self.zetas.iter().map(ext_word).collect(), self.coeffs.iter().map(ext_word).collect(), self.query_arena(queries), - ] + ]; + // Declared by `declare_fri` only when the format caps some layer. + if self.shape.cap_words(super::proof_arena::words_per_root()) > 0 { + out.push(super::proof_arena::commitments_to_arena(&self.caps)); + } + out } /// Per query, per layer: the symmetric evaluation then its path. - fn query_arena(&self, queries: &[usize]) -> Vec { + pub(super) fn query_arena(&self, queries: &[usize]) -> Vec { let mut out = Vec::new(); for &q in queries { - for (sym, path) in &self.openings[q] { - out.push(ext_word(sym)); + for (values, path) in &self.openings[q] { + out.extend(values.iter().map(ext_word)); out.extend(super::proof_arena::commitments_to_arena(path)); } } @@ -209,7 +223,7 @@ impl HostFri { /// evaluation against — and the mirror itself is checked, because the same /// codeword must reproduce the values the PROVER folded to, which no reading /// of these three lines could fake. - fn terminal_codeword(&self) -> Vec { + pub(super) fn terminal_codeword(&self) -> Vec { use math::fft::bit_reversing::in_place_bit_reverse_permute; let coset_offset = FE::from(self.shape.coset_offset); @@ -347,21 +361,34 @@ fn the_fri_leaf_is_byte_identical_to_productions_own_backends() { /// /// Arena order: the per-query `(index, p₀, p₀ˢ)` block, then the four /// [`FriArenas`]. -fn fri_only_program(shape: FriShape, num_queries: usize) -> LfmProgram { +pub(super) fn fri_only_program(shape: FriShape, num_queries: usize) -> LfmProgram { let mut b = LfmBuilder::new().with_wrap_hash(super::edsl::WrapHash::production()); - let q = b.declare_arena(3 * num_queries as u32); + // Per query `(index, p₀, p₀ˢ)`, or `(r, DEEP(x_r))` under one-row leaves. + let per = fri_deep_words(shape) as u32; + let q = b.declare_arena(per * num_queries as u32); let (arenas, fri) = declare_fri(&mut b, shape, num_queries); for i in 0..num_queries { - let index = b.hint_felt(q, 3 * i as u32); - let p0 = b.hint_word(q, 3 * i as u32 + 1).as_ext(); - let p0_sym = b.hint_word(q, 3 * i as u32 + 2).as_ext(); + let index = b.hint_felt(q, per * i as u32); + let p0 = b.hint_word(q, per * i as u32 + 1).as_ext(); let bits = b.bit_dec(index, shape.index_bits()); - let (point, point_sym) = super::sub_proof::emit_points_from_bits( - &mut b, - shape.log2_lde_length, - FE::from(shape.coset_offset), - &bits, - ); + let (p0_sym, point, point_sym) = if shape.one_row() { + let point = super::sub_proof::emit_point_from_row_bits( + &mut b, + shape.log2_lde_length, + FE::from(shape.coset_offset), + &bits, + ); + (None, point, None) + } else { + let p0_sym = b.hint_word(q, per * i as u32 + 2).as_ext(); + let (point, point_sym) = super::sub_proof::emit_points_from_bits( + &mut b, + shape.log2_lde_length, + FE::from(shape.coset_offset), + &bits, + ); + (Some(p0_sym), point, Some(point_sym)) + }; let openings = hint_layer_openings(&mut b, shape, &arenas, i); let v = emit_query_fri( &mut b, @@ -383,20 +410,31 @@ fn fri_only_program(shape: FriShape, num_queries: usize) -> LfmProgram { program } +/// Words per query of [`fri_only_program`]'s DEEP arena: `(index, p₀, p₀ˢ)` +/// for row pairs, `(r, DEEP(x_r))` under one-row leaves. +pub(super) fn fri_deep_words(shape: FriShape) -> usize { + if shape.one_row() { 2 } else { 3 } +} + impl HostFri { - /// The `(index, p₀, p₀ˢ)` arena [`fri_only_program`] reads. - fn deep_arena(&self, queries: &[usize]) -> Vec { + /// The DEEP arena [`fri_only_program`] reads: `(index, p₀, p₀ˢ)` per + /// query, or `(r, DEEP(x_r))` under one-row leaves. + pub(super) fn deep_arena(&self, queries: &[usize]) -> Vec { let mut out = Vec::new(); for &q in queries { out.push(base_word(FE::from(self.trace.iotas[q] as u64))); - out.push(ext_word(&self.trace.expected[q].0)); - out.push(ext_word(&self.trace.expected[q].1)); + if self.shape.one_row() { + out.push(ext_word(&self.trace.expected_at_r[q])); + } else { + out.push(ext_word(&self.trace.expected[q].0)); + out.push(ext_word(&self.trace.expected[q].1)); + } } out } /// Every arena [`fri_only_program`] declares, in order. - fn all_arenas(&self, queries: &[usize]) -> Vec> { + pub(super) fn all_arenas(&self, queries: &[usize]) -> Vec> { let mut all = vec![self.deep_arena(queries)]; all.extend(self.fri_arenas(queries)); all @@ -736,14 +774,17 @@ fn the_two_legs_verify_one_real_folding_proof_as_one_program() { ); } -fn permutations(program: &LfmProgram) -> usize { +pub(super) fn permutations(program: &LfmProgram) -> usize { // The CONFIGURED wrap hash's compressions. Filtering `KeccakF` here read // zero the moment production moved to BLAKE3, turning a cost measurement // into a failed assertion about a count nobody had re-derived. super::machine_tests::wrap_hash_instrs(program) } -fn count_matching bool>(program: &LfmProgram, f: F) -> usize { +pub(super) fn count_matching bool>( + program: &LfmProgram, + f: F, +) -> usize { program.instrs.iter().filter(|i| f(i)).count() } @@ -826,6 +867,7 @@ fn the_emitted_permutation_count_meets_the_pinned_prediction() { final_poly_log_degree: 7, coset_offset: 3, num_queries: queries, + format: stark::proof::options::ProofFormat::DEFAULT, }; shape.check(); let per = marginal_fri(shape); @@ -1273,3 +1315,178 @@ fn the_fri_leg_proves_and_verifies() { h.shape.num_committed(), ); } + +// ============================================================================= +// Merkle caps in the FRI leg (S1) +// ============================================================================= + +/// The folding fixture's options under a cap policy: blowup 2, `queries` +/// queries (a cap needs openings: `auto` caps at 3 from 20 on), no grinding. +fn capped_options(policy: CapPolicy, queries: usize) -> stark::proof::options::ProofOptions { + let mut o = stark::proof::options::GoldilocksCubicProofOptions::with_blowup(2) + .expect("blowup 2 is valid"); + o.fri_number_of_queries = queries; + o.grinding_factor = 0; + o.format.merkle_cap = policy; + o +} + +/// 2048 rows at blowup 2: LDE 2^12, trace trees 11 deep, three committed FRI +/// layers 10, 9 and 8 deep — every tree tall enough for a height-3 cap. +const CAPPED_ROWS: usize = 2048; + +fn capped_host(policy: CapPolicy, queries: usize) -> HostFri { + let (air, proof) = folding_fixture_with(CAPPED_ROWS, capped_options(policy, queries)); + host_fri_from(&*air, &proof) +} + +/// ★ The FRI leg verifies every query of a real CAPPED folding proof, and its +/// permutation count is the capped closed form exactly: per query one leaf and +/// `depth − c` parents per layer, plus `2^c − 1` parents per capped layer ONCE +/// (the cap hashed up to its root). +#[test] +fn the_fri_emitter_verifies_a_capped_folding_proof() { + for policy in [CapPolicy::Fixed(1), CapPolicy::Fixed(2), CapPolicy::Auto] { + let h = capped_host(policy, 24); + assert_eq!(h.shape.num_committed(), 3); + for i in 0..3 { + let want = if policy == CapPolicy::Fixed(1) { + 1 + } else if policy == CapPolicy::Fixed(2) { + 2 + } else { + 3 + }; + assert_eq!(h.shape.layer_cap(i), want, "{policy}: layer {i}"); + } + let all: Vec = (0..h.trace.iotas.len()).collect(); + let program = fri_only_program(h.shape, all.len()); + let exec = execute( + &program, + &h.all_arenas(&all), + &crate::hash_pin::BLOCK_HASHER, + ) + .expect("an honest capped FRI decommitment must execute"); + + let codeword = h.terminal_codeword(); + let c = h.shape.num_committed(); + for (k, &q) in all.iter().enumerate() { + let v = word_as_ext(&exec.public_words[k].1).expect("ext"); + assert_eq!(v, codeword[h.trace.iotas[q] >> c], "{policy} query {q}"); + } + let emitted = permutations(&program); + let closed = all.len() * h.shape.permutations_per_query() + h.shape.cap_permutations(); + assert_eq!( + emitted, closed, + "{policy}: emitted permutations against the capped closed form" + ); + // And the saving against the uncapped shape is the cap's own formula: + // per tree `Q·c − (2^c − 1)`. + let uncapped = FriShape { + format: stark::proof::options::ProofFormat::DEFAULT, + ..h.shape + }; + let saved: usize = (0..c) + .map(|i| { + let cap = h.shape.layer_cap(i); + all.len() * cap - ((1usize << cap) - 1) + }) + .sum(); + assert_eq!( + all.len() * uncapped.permutations_per_query() - saved, + emitted, + "{policy}: the cap saves Q·c − (2^c − 1) per layer tree" + ); + println!( + "{policy}: {} queries, caps {:?}: {emitted} permutations (uncapped {})", + all.len(), + (0..c).map(|i| h.shape.layer_cap(i)).collect::>(), + all.len() * uncapped.permutations_per_query(), + ); + } +} + +/// ★ Every cap word of every capped FRI layer is bound — including the ones no +/// query reaches, which only the once-per-tree cap-to-root check can reject +/// (in-guest). One query at a height-3 cap reaches one of +/// eight nodes per layer, so seven words per layer are rejected by that check +/// alone. +#[test] +fn every_fri_cap_word_is_bound_even_the_unreached_ones() { + let h = capped_host(CapPolicy::Fixed(3), 24); + let queries = vec![0usize]; + let shape = FriShape { + num_queries: 1, + ..h.shape + }; + let program = fri_only_program(shape, 1); + let honest = h.all_arenas(&queries); + execute(&program, &honest, &crate::hash_pin::BLOCK_HASHER).expect("honest"); + // Arena order: deep, roots, zetas, coeffs, queries, caps. + let caps = honest.len() - 1; + assert_eq!( + honest[caps].len(), + 3 * 8 * super::proof_arena::words_per_root(), + "three layers, eight cap digests each" + ); + for w in 0..honest[caps].len() { + let mut bad = honest.clone(); + bad[caps][w][0] += FE::one(); + execute(&program, &bad, &crate::hash_pin::BLOCK_HASHER) + .expect_err(&format!("cap word {w} moved must not execute")); + } +} + +/// ★ Both legs as one program over a CAPPED folding proof: the four trace +/// trees' caps and the three FRI layers' caps authenticated once, every opening +/// checked against them, and the permutation count the capped closed form. +#[test] +fn the_two_legs_verify_one_capped_folding_proof_as_one_program() { + use super::epoch_verify::{blocks_for, group_leaf_felts}; + + let h = capped_host(CapPolicy::Fixed(3), 24); + assert_eq!(h.trace.shape.trace_cap, 3); + let queries: Vec = (0..6).collect(); + let shape = FriShape { + num_queries: queries.len(), + ..h.shape + }; + let mut b = LfmBuilder::new().with_wrap_hash(super::edsl::WrapHash::production()); + let (_, _, terminal) = + super::fri::emit_sub_proof_with_fri(&mut b, &h.trace.shape, shape, queries.len()); + for v in &terminal { + b.public(v.as_cell()); + } + let program = compile(b.finish()); + validate(&program).expect("the joined capped program is admissible"); + + let mut arenas = h.trace.arenas(&queries); + arenas.extend(h.fri_arenas(&queries)); + let exec = execute(&program, &arenas, &crate::hash_pin::BLOCK_HASHER) + .expect("the honest capped proof must authenticate, fold and reach the terminal"); + let codeword = h.terminal_codeword(); + for (k, &q) in queries.iter().enumerate() { + let v = word_as_ext(&exec.public_words[k].1).expect("ext"); + assert_eq!(v, codeword[h.trace.iotas[q] >> h.shape.num_committed()]); + } + + let sub = &h.trace.shape; + let hash = super::edsl::WrapHash::production(); + let leaves: usize = sub + .groups() + .iter() + .map(|g| blocks_for(group_leaf_felts(g), hash)) + .sum(); + let closed = queries.len() + * (leaves + sub.groups().len() * sub.path_len() + shape.permutations_per_query()) + + sub.cap_permutations() + + shape.cap_permutations(); + assert_eq!(permutations(&program), closed, "the capped closed form"); + + // A trace-tree cap word moved: the caps arena of the TRACE leg is the + // sixth arena (uniforms, ood, parts, roots, queries, caps). + let mut bad = arenas.clone(); + bad[5][0][0] += FE::one(); + execute(&program, &bad, &crate::hash_pin::BLOCK_HASHER) + .expect_err("a moved trace-tree cap word must not execute"); +} diff --git a/prover/src/lfm/join_tests.rs b/prover/src/lfm/join_tests.rs index c9e859811..0c28dbae5 100644 --- a/prover/src/lfm/join_tests.rs +++ b/prover/src/lfm/join_tests.rs @@ -70,6 +70,9 @@ pub(super) struct HostSubProof { claimed_parts: Vec, /// One root per group, in `SubProofShape::groups` order. roots: Vec, + /// Every group's Merkle cap in group order, split off query 0's path; empty + /// when the format caps nothing. + trace_caps: Vec, /// `[query][group]`. openings: Vec>, pub(super) iotas: Vec, @@ -79,7 +82,12 @@ pub(super) struct HostSubProof { /// trace leg does not. pub(super) zetas: Vec, /// The production reconstruction's answer per query, `(regular, sym)`. + /// Row-pair shapes only (empty under one-row leaves). pub(super) expected: Vec<(FEE, FEE)>, + /// Under one-row leaves (S2): production's DEEP at the ONE point `x_r` + /// per query (`reconstruct_deep_composition_poly_evaluation_at`). Empty + /// for row pairs. + pub(super) expected_at_r: Vec, /// The same, asked of production with the PRECOMPUTED and MAIN slices /// swapped — the alternative column order a fixture without a precomputed /// group cannot distinguish. Empty when there is no precomputed group, or @@ -87,8 +95,9 @@ pub(super) struct HostSubProof { /// well-formed reading). expected_base_swapped: Vec<(FEE, FEE)>, /// Production's query points, kept so the machine's derivation can be - /// checked against them rather than against a local formula. - points: Vec<(FE, FE)>, + /// checked against them rather than against a local formula: `(υ, −υ)` + /// for row pairs, `(x_r, None)` under one-row leaves. + pub(super) points: Vec<(FE, Option)>, } fn host_sub_proof() -> &'static HostSubProof { @@ -134,12 +143,42 @@ pub(super) fn build_host_sub_proof( let blowup = air.options().blowup_factor as usize; let lde_length = view.trace_length() * blowup; + // The table's leaf layout — the host prover's and verifier's own + // resolution (S2: `auto` per table from the AIR's widths). + let leaf_layout = stark::leaf_layout::table_leaf_layout(air, view.trace_length()); + let merkle_depth = leaf_layout.tree_depth(lde_length.trailing_zeros() as usize); + let opts = air.options(); + let trace_cap = opts + .format + .merkle_cap + .height(opts.fri_number_of_queries, merkle_depth); let shape = SubProofShape { deep: deep.clone(), trace_groups, - merkle_depth: lde_length.trailing_zeros() as usize - 1, + merkle_depth, log2_lde_length: lde_length.trailing_zeros(), coset_offset: FE::from(air.options().coset_offset), + trace_cap, + layout: leaf_layout, + }; + // Query 0 of a capped tree is its owner: its path carries the cap after + // the `D − c` siblings. The query arena takes the siblings, the caps + // arena the caps (group order). + let mut trace_caps: Vec = Vec::new(); + let mut split = |q: usize, path: &[Commitment]| -> Vec { + if trace_cap == 0 || q != 0 { + assert_eq!( + path.len(), + merkle_depth - trace_cap, + "query {q}: a path to the cap" + ); + return path.to_vec(); + } + let (siblings, cap) = + crypto::merkle_tree::cap::split_owner_path(path, merkle_depth, trace_cap) + .expect("the owner path is D − c + 2^c long"); + trace_caps.extend_from_slice(cap); + siblings.to_vec() }; let mut roots = vec![]; @@ -173,6 +212,7 @@ pub(super) fn build_host_sub_proof( num_precomputed > 0 && main_width - num_precomputed == num_precomputed; let mut openings = Vec::new(); let mut expected = Vec::new(); + let mut expected_at_r = Vec::new(); let mut expected_base_swapped = Vec::new(); let mut points = Vec::new(); for (q, iota) in sp.challenges.iotas.iter().enumerate() { @@ -187,7 +227,7 @@ pub(super) fn build_host_sub_proof( .chain(p.evaluations_sym()) .map(|v| base_word(*v)) .collect(), - siblings: p.merkle_path().to_vec(), + siblings: split(q, p.merkle_path()), }); } let m = o.main_trace_polys(); @@ -198,7 +238,7 @@ pub(super) fn build_host_sub_proof( .chain(m.evaluations_sym()) .map(|v| base_word(*v)) .collect(), - siblings: m.merkle_path().to_vec(), + siblings: split(q, m.merkle_path()), }); if aux_width > 0 { let a = o.aux_trace_polys().expect("aux opening"); @@ -209,7 +249,7 @@ pub(super) fn build_host_sub_proof( .chain(a.evaluations_sym()) .map(ext_word) .collect(), - siblings: a.merkle_path().to_vec(), + siblings: split(q, a.merkle_path()), }); } let c = o.composition_poly(); @@ -220,10 +260,35 @@ pub(super) fn build_host_sub_proof( .chain(c.evaluations_sym()) .map(ext_word) .collect(), - siblings: c.merkle_path().to_vec(), + siblings: split(q, c.merkle_path()), }); openings.push(groups); + if leaf_layout.is_one_row() { + // S2: one point, `x_r`, and DEEP there alone — production's own + // one-row functions (`query_point`, `…_evaluation_at`). + let point = V::query_point(leaf_layout, *iota, &domain); + let empty_base: &[FE] = &[]; + let want = V::reconstruct_deep_composition_poly_evaluation_at( + &point, + &generator, + &sp.challenges, + &invariants, + layout.next_row_cols(), + layout.step_size(), + o.precomputed_trace_polys() + .map(|p| p.evaluations()) + .unwrap_or(empty_base), + m.evaluations(), + o.aux_trace_polys().map(|a| a.evaluations()).unwrap_or(&[]), + c.evaluations(), + ) + .expect("a real one-row proof reconstructs"); + expected_at_r.push(want); + points.push((point, None)); + continue; + } + let point = V::query_challenge_to_evaluation_point(*iota, false, &domain); let point_sym = V::query_challenge_to_evaluation_point(*iota, true, &domain); let empty_base: &[FE] = &[]; @@ -276,7 +341,7 @@ pub(super) fn build_host_sub_proof( .expect("the swapped reading is well formed, so it reconstructs"); expected_base_swapped.push(swapped); } - points.push((point, point_sym)); + points.push((point, Some(point_sym))); } let ood: Vec = (0..deep.num_eval_points) @@ -290,10 +355,12 @@ pub(super) fn build_host_sub_proof( ood, claimed_parts: sp.claimed_parts.clone(), roots, + trace_caps, openings, iotas: sp.challenges.iotas.clone(), zetas: sp.challenges.zetas.clone(), expected, + expected_at_r, expected_base_swapped, points, } @@ -302,13 +369,18 @@ pub(super) fn build_host_sub_proof( impl HostSubProof { /// The arenas [`emit_sub_proof`] declares, in its declaration order. pub(super) fn arenas(&self, queries: &[usize]) -> Vec> { - vec![ + let mut out = vec![ vec![ext_word(&self.gamma), ext_word(&self.zeta)], self.ood.iter().map(ext_word).collect(), self.claimed_parts.iter().map(ext_word).collect(), super::proof_arena::commitments_to_arena(&self.roots), self.query_arena(queries), - ] + ]; + // The caps arena, declared by the emitter only when the shape caps. + if self.shape.trace_cap > 0 { + out.push(super::proof_arena::commitments_to_arena(&self.trace_caps)); + } + out } /// Per query: the index, then per group the row-pair values and the @@ -406,7 +478,8 @@ fn the_join_premises_hold_on_a_real_proof() { query_challenge_to_evaluation_point(iota, false)" ); assert_eq!( - exec.public_words[1].1[0], h.points[q].1, + exec.public_words[1].1[0], + h.points[q].1.expect("a row-pair fixture"), "query {q}: the machine's symmetric point must be \ query_challenge_to_evaluation_point(iota, true)" ); @@ -565,6 +638,8 @@ fn shape_for( merkle_depth: (log2_trace_length + log2_blowup) as usize - 1, log2_lde_length: log2_trace_length + log2_blowup, coset_offset: FE::from(3u64), + trace_cap: 0, + layout: stark::leaf_layout::LeafLayout::RowPair, } } @@ -1313,7 +1388,10 @@ fn the_controls_show_what_the_join_denies() { let program = compile(control_program_source(&h.shape, Control::HintedPoint)); validate(&program).expect("admissible"); let mut arenas = h.arenas(&[q]); - arenas.push(vec![base_word(h.points[q].0), base_word(h.points[q].1)]); + arenas.push(vec![ + base_word(h.points[q].0), + base_word(h.points[q].1.expect("a row-pair fixture")), + ]); let clean = execute(&program, &arenas, &crate::hash_pin::BLOCK_HASHER).expect("honest"); assert_eq!( word_as_ext(&clean.public_words[0].1).expect("ext"), @@ -1321,7 +1399,10 @@ fn the_controls_show_what_the_join_denies() { ); let mut attacked = arenas.clone(); - attacked[5] = vec![base_word(h.points[other].0), base_word(h.points[other].1)]; + attacked[5] = vec![ + base_word(h.points[other].0), + base_word(h.points[other].1.expect("a row-pair fixture")), + ]; let forged = execute(&program, &attacked, &crate::hash_pin::BLOCK_HASHER).expect( "HintedPoint: a hinted point is not tied to the authenticated index, \ which is what this control permits", @@ -1739,6 +1820,7 @@ fn the_exposed_bits_are_the_cells_the_walk_consumed() { // ==================== FRI slice 1: the fold layout ==================== use super::fri::FriShape; +use stark::proof::options::ProofFormat; /// ★ The shape mirror against production's observable BEHAVIOUR on the real /// proof — the vector lengths the verifier structurally enforces. @@ -1848,6 +1930,7 @@ fn the_fold_layout_is_right_off_productions_constants() { final_poly_log_degree: k, coset_offset: 3, num_queries: 1, + format: ProofFormat::DEFAULT, }; shape.check(); let got = ( @@ -1898,6 +1981,7 @@ fn the_fri_sizing_prediction() { final_poly_log_degree: 7, coset_offset: 3, num_queries: queries, + format: ProofFormat::DEFAULT, }; shape.check(); println!( diff --git a/prover/src/lfm/logup_tests.rs b/prover/src/lfm/logup_tests.rs index 58e672ee9..e7e63d9b9 100644 --- a/prover/src/lfm/logup_tests.rs +++ b/prover/src/lfm/logup_tests.rs @@ -1393,7 +1393,8 @@ fn a_zero_row_fixed_table_carries_some_zero_not_none() { num_contributing_tables: contributions.len(), num_output_bytes: public_output.len(), }; - let (z, alpha) = crate::replay_transcript_phase_a_view(&refs, view, &mut seed()); + let (z, alpha) = crate::replay_transcript_phase_a_view(&refs, view, &mut seed()) + .expect("every preprocessed table has a root for its layout"); let n_tables = contributions.len() as u32; let n_bytes = public_output.len() as u32; diff --git a/prover/src/lfm/machine_tests.rs b/prover/src/lfm/machine_tests.rs index 7a9830eec..eb09f09ac 100644 --- a/prover/src/lfm/machine_tests.rs +++ b/prover/src/lfm/machine_tests.rs @@ -4222,6 +4222,7 @@ fn derivation_shape(blowup: usize) -> RegisterDerivationShape { RegisterDerivationShape { blowup, coset_offset: PRODUCTION_COSET_OFFSET, + rows_per_leaf: stark::commitment::ROWS_PER_LEAF, } } @@ -4615,6 +4616,7 @@ fn the_register_derivation_proves_and_verifies() { let shape = RegisterDerivationShape { blowup: inner.blowup_factor as usize, coset_offset: inner.coset_offset, + rows_per_leaf: stark::commitment::ROWS_PER_LEAF, }; assert_eq!( shape, diff --git a/prover/src/lfm/merkle_cap.rs b/prover/src/lfm/merkle_cap.rs new file mode 100644 index 000000000..c71521e72 --- /dev/null +++ b/prover/src/lfm/merkle_cap.rs @@ -0,0 +1,142 @@ +//! ★ One tree's authenticated Merkle cap, in-guest — the one gadget the WHIR +//! chain verifier (W1) and the STARK sub-proof verifier (S1) share. +//! +//! A tree of depth `D` committed with a height-`c` cap is authenticated in two +//! places, and the in-guest verifier makes the dangerous state of each +//! unconstructible rather than checked: +//! +//! - **Once per tree**, [`CapCells::authenticate`] hashes the `2^c` hinted cap +//! digests up to their root and asserts it equals the tree's root lanes. It +//! is the ONLY constructor, so every [`CapCells`] value is a cap that hashes +//! to its root — and a tree has exactly one: the cells checked against the +//! root and the cells the mux reads are the same cells. +//! - **Per opening**, [`CapCells::verify_path`] is the ONE entry point. It takes +//! the opened leaf, the tree's WHOLE leaf index (low bit first, one bit per +//! level) and the path to the cap, walks the low `D − c` bits, picks +//! `cap[index >> (D − c)]` with the top `c` bits and asserts the two digests +//! equal. The split point is computed here from the index's own length and +//! the cap's height; the mux is private, so no caller can feed it a constant, +//! a hinted bit or a sub-slice of its own choosing. +//! +//! The mux is a balanced tree of `2^c − 1` `Select`s per digest cell: the LFM +//! has no load at a computed address, which is why the cap height is priced by +//! the cost law and stays at most 3. +//! +//! ⚠ What a caller still owes: `index_bits` must be the tree's own leaf index +//! as the TRANSCRIPT produced it — the query's bits, or a suffix of them for a +//! tree whose leaves cover several positions (a FRI layer). Those bits reach +//! every caller as cells of the one `sample_u64_pow2` decomposition; nothing +//! here can tell a transcript bit from a hinted one. + +use super::builder::{Bit, Felt, LfmBuilder}; +use super::edsl::{self, WrapDigest}; +use super::instr::ArenaId; + +/// One tree's authenticated Merkle cap. +pub struct CapCells { + cap: Vec, + height: usize, +} + +impl CapCells { + /// Authenticate a hinted cap against a tree's root lanes, once per tree. + /// + /// `cap` must be `2^c` digests, `c ≥ 1`: a tree at `c = 0` has no cap and + /// is checked against its root. `root_lanes` holds one entry per digest + /// cell (one for an algebraic root, two for a byte digest), as + /// [`edsl::assert_digest_eq_lanes`] takes them. + pub fn authenticate(b: &mut LfmBuilder, cap: &[WrapDigest], root_lanes: &[[Felt; 4]]) -> Self { + assert!( + cap.len() >= 2 && cap.len().is_power_of_two(), + "a cap is 2^c digests with c >= 1, got {}", + cap.len() + ); + let root = edsl::wrap_merkle_tree_root(b, cap); + edsl::assert_digest_eq_lanes(b, root, root_lanes); + Self { + cap: cap.to_vec(), + height: cap.len().trailing_zeros() as usize, + } + } + + /// The cap height `c`. + pub fn height(&self) -> usize { + self.height + } + + /// ★ Authenticate one opening against this cap, as a REFUSAL: `leaf` is the + /// opened leaf's digest, `index_bits` the tree's WHOLE leaf index (low + /// first, `D` bits) and `siblings` the path to the cap (`D − c` digests, + /// leaf level first). The low `D − c` bits are walked, the top `c` pick the + /// cap node, and the walked digest must equal it. + pub fn verify_path( + &self, + b: &mut LfmBuilder, + leaf: WrapDigest, + index_bits: &[Bit], + siblings: &[WrapDigest], + ) { + assert_eq!( + siblings.len() + self.height, + index_bits.len(), + "a path to the cap: one sibling per level below it" + ); + let (walk_bits, top_bits) = index_bits.split_at(siblings.len()); + let walked = edsl::wrap_merkle_walk(b, leaf, walk_bits, siblings); + let node = self.select(b, top_bits); + for (x, y) in walked.iter().zip(node.iter()) { + edsl::assert_word_eq(b, *x, *y); + } + } + + /// `cap[index >> (depth − c)]` from the index's top `c` bits, LOW first: + /// a balanced mux, `2^c − 1` `Select` rows a digest cell. Pairs are + /// `(2t, 2t + 1)` because the bits arrive low first (the slot mux's + /// reason, `whir_chain::emit_slot_mux`). + fn select(&self, b: &mut LfmBuilder, top_bits: &[Bit]) -> WrapDigest { + assert_eq!(top_bits.len(), self.height, "one mux level per cap level"); + let mut level: Vec = self.cap.clone(); + for bit in top_bits { + level = level + .chunks_exact(2) + .map(|pair| { + let cells: Vec<_> = pair[0] + .iter() + .zip(pair[1].iter()) + .map(|(l, r)| b.select(*bit, *l, *r).0) + .collect(); + WrapDigest::from_cells(&cells) + }) + .collect(); + } + level[0] + } +} + +/// Hint a height-`c` cap — `2^c` digests at [`edsl::digest_words`] words each — +/// out of `arena` from word `base`, and authenticate it against `root_lanes`. +/// Returns the cells and the next free word. +pub fn hint_and_authenticate( + b: &mut LfmBuilder, + arena: ArenaId, + base: u32, + c: usize, + root_lanes: &[[Felt; 4]], +) -> (CapCells, u32) { + let dw = edsl::digest_words(b); + let mut cursor = base; + let cap: Vec = (0..1usize << c) + .map(|_| { + let d = edsl::hint_digest(b, arena, cursor); + cursor += dw; + d + }) + .collect(); + (CapCells::authenticate(b, &cap, root_lanes), cursor) +} + +/// Permutations one tree's cap check costs: the cap hashed up to its root, +/// `2^c − 1` parents. Nothing at `c = 0`. +pub const fn cap_root_permutations(c: usize) -> usize { + (1usize << c) - 1 +} diff --git a/prover/src/lfm/mod.rs b/prover/src/lfm/mod.rs index fedba4a1e..afcb9f5e6 100644 --- a/prover/src/lfm/mod.rs +++ b/prover/src/lfm/mod.rs @@ -46,6 +46,7 @@ pub mod keccak_host; pub mod layout; pub mod lde; pub mod logup; +pub mod merkle_cap; pub mod per_table_aggregator; pub mod poseidon; pub mod preprocessed; @@ -147,6 +148,8 @@ mod exec_identity_tests; #[cfg(test)] mod framework_probe; #[cfg(test)] +mod fri_group_tests; +#[cfg(test)] mod fri_tests; #[cfg(test)] mod join_tests; @@ -161,6 +164,10 @@ mod logup_tests; #[cfg(test)] mod machine_tests; #[cfg(test)] +mod one_row_guest_tests; +#[cfg(test)] +mod one_row_tests; +#[cfg(test)] mod per_table_aggregator_tests; #[cfg(test)] mod per_table_census_tests; diff --git a/prover/src/lfm/one_row_guest_tests.rs b/prover/src/lfm/one_row_guest_tests.rs new file mode 100644 index 000000000..4826044de --- /dev/null +++ b/prover/src/lfm/one_row_guest_tests.rs @@ -0,0 +1,711 @@ +//! S2 in the in-guest (LFM) STARK verifier (G3): one-row trace leaves, DEEP +//! at ONE point, the committed FRI input and +//! its input-slot check, index bits over the whole LDE, no `−υ` point. +//! +//! Checked against the host's own artefacts, never against a second model: +//! - the in-guest one-row shape (index bits, schedule, layer depths, caps, +//! challenge count) against the host's `StarkCaps::for_options(.., true)`; +//! - the emitted FRI verifier against the host's checked-in RPX (e) proofs +//! (`crypto/stark/tests/vectors/zf_fri/e_proof_rpx_*`), executed, with its +//! permutation count equal to the closed form; +//! - the in-guest one-row (and row-pair) trace leaf against the (e) leaf +//! digests; +//! - tampers of every value the input tree's opening carries, and the +//! input-slot check shown load-bearing (a moved `DEEP(x_r)` executes when, +//! and only when, the slot check is skipped); +//! - both legs as one program on real laptop-scale proofs at `one_row` ∈ +//! {1, auto} × cap {off, auto} × fri {pair, dp}, and one program verifying a +//! one-row table and a row-pair table side by side (mixed layouts). + +use crypto::merkle_tree::cap::CapPolicy; +use math::fft::bit_reversing::reverse_index; +use serde_json::Value; +use stark::config::Commitment; +use stark::examples::read_only_memory_logup::LogReadOnlyPublicInputs; +use stark::leaf_layout::LeafLayout; +use stark::merkle_caps::StarkCaps; +use stark::proof::options::{FriMode, FriScheduleOverride, OneRowMode, ProofFormat, ProofOptions}; +use stark::proof::stark::StarkProof; +use stark::proof::view::StarkProofView; + +use crate::tables::types::{FE, FEE, GoldilocksExtension, GoldilocksField}; + +use super::builder::LfmBuilder; +use super::compiler::{LfmProgram, compile}; +use super::epoch_verify::{blocks_for, group_leaf_felts_at}; +use super::executor::execute; +use super::fri::FriShape; +use super::fri_tests::{ + HostFri, folding_fixture_with, fri_only_program, host_fri_from, permutations, +}; +use super::sub_proof::{GroupShape, emit_leaf_hash_rows}; +use super::word::{LfmWord, base_word, ext_word, word_as_ext}; + +type Gl = GoldilocksField; +type Ext3 = GoldilocksExtension; +type VectorProof = StarkProof>; + +// ============================================================================= +// The shape — the in-guest one-row layout IS the host's +// ============================================================================= + +/// ★ One index width, one schedule, one depth and one cap per layer on both +/// sides under one-row leaves: the in-guest `FriShape` (resolved to one row) +/// against the host's `StarkCaps` at `one_row = true` — built on the host's +/// `FriFoldLayout` — over every LDE size of interest, both production +/// terminals, both FRI modes and both cap policies. Also: the index is +/// `log2(lde)` bits wide (not `log2(lde) − 1`), the chain covers EVERY fold +/// (layer 0 is the DEEP codeword), and a proof draws one challenge per +/// committed layer (none before the input tree). +#[test] +fn the_in_guest_one_row_shape_is_the_hosts_layout() { + let mut checked = 0usize; + for (blowup, k) in [(4u8, 7u8), (4, 8), (2, 7)] { + for queries in [3usize, 24, 110] { + for cap in [CapPolicy::Off, CapPolicy::Auto] { + for fri in [FriMode::Pair, FriMode::Dp] { + let blowup_log = (blowup as u32).trailing_zeros(); + for lde_log in (blowup_log + 1)..=25 { + let opts = ProofOptions { + blowup_factor: blowup, + fri_number_of_queries: queries, + coset_offset: 3, + grinding_factor: 0, + fri_final_poly_log_degree: k, + format: ProofFormat { + merkle_cap: cap, + fri_mode: fri, + one_row: OneRowMode::On, + ..ProofFormat::DEFAULT + }, + }; + let shape = FriShape::from_options(&opts, lde_log); + shape.check(); + assert!(shape.one_row()); + assert!(!shape.is_legacy(), "a one-row table is never legacy"); + let host = StarkCaps::for_options(&opts, lde_log as usize, true) + .expect("a one-row format lays out"); + assert_eq!(shape.index_bits(), lde_log as usize); + assert_eq!(shape.index_bits(), host.trace_depth); + let depths: Vec = (0..shape.num_committed()) + .map(|j| shape.layer_depth(j)) + .collect(); + let caps: Vec = (0..shape.num_committed()) + .map(|j| shape.layer_cap(j)) + .collect(); + assert_eq!(depths, host.fri_depths, "{opts:?} lde {lde_log}"); + assert_eq!(caps, host.fri, "{opts:?} lde {lde_log}"); + let covered: u32 = shape.schedule().iter().map(|&d| u32::from(d)).sum(); + assert_eq!(covered, shape.total_folds(), "every fold is committed"); + assert_eq!( + shape.num_zetas(), + if shape.total_folds() > 0 { + shape.num_committed() + } else { + 0 + } + ); + checked += 1; + } + } + } + } + } + println!( + "{checked} one-row shapes: in-guest index bits, schedule, depths and caps == the host's" + ); +} + +/// `one_row = auto` has no layout of its own: a shape is built at the table's +/// RESOLVED layout (the AIR's widths decide), and asking the options alone is +/// refused rather than guessed. +#[test] +#[should_panic(expected = "one_row = auto resolves per table")] +fn an_unresolved_auto_layout_is_refused() { + let mut opts = + stark::proof::options::GoldilocksCubicProofOptions::with_blowup(4).expect("blowup 4"); + opts.format.one_row = OneRowMode::Auto; + let _ = FriShape::from_options(&opts, 12); +} + +// ============================================================================= +// (e) — the emitted FRI verifier on the host's one-row RPX proofs +// ============================================================================= + +fn ext_of(v: &Value) -> FEE { + let limbs: Vec = v + .as_array() + .expect("an ext value is three limbs") + .iter() + .map(|x| x.as_u64().expect("a canonical limb")) + .collect(); + assert_eq!(limbs.len(), 3); + FEE::new([FE::from(limbs[0]), FE::from(limbs[1]), FE::from(limbs[2])]) +} + +fn commitment_of_hex(s: &str) -> Commitment { + assert_eq!(s.len(), 64, "a 32-byte digest"); + let mut out = [0u8; 32]; + for (i, byte) in out.iter_mut().enumerate() { + *byte = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).expect("hex"); + } + out +} + +/// One checked-in RPX (e) proof: its JSON, its proof, and the in-guest shape +/// built from the vector's FORMAT (the host generator's own +/// `one_row_proof_formats`, never re-spelled here). +struct OneRowVector { + name: &'static str, + json: Value, + proof: VectorProof, + shape: FriShape, +} + +fn one_row_rpx_vectors() -> Vec { + stark::fri::vectors::one_row_proof_formats() + .into_iter() + .map(|(name, format)| { + let dir = stark::fri::vectors::vectors_dir(); + let stem = format!("e_proof_rpx_{name}"); + let json: Value = serde_json::from_slice( + &std::fs::read(dir.join(format!("{stem}.json"))).expect("the vector JSON"), + ) + .expect("valid JSON"); + let bytes = std::fs::read(dir.join(format!("{stem}.rkyv"))).expect("the vector proof"); + let proof: VectorProof = + rkyv::from_bytes::(&bytes).expect("rkyv"); + let queries = json["queries"].as_u64().expect("queries") as usize; + let opts = stark::fri::vectors::proof_options(format, queries); + let lde_log = json["lde_log"].as_u64().expect("lde_log") as u32; + let shape = FriShape::from_options(&opts, lde_log); + OneRowVector { + name, + json, + proof, + shape, + } + }) + .collect() +} + +impl OneRowVector { + /// The arenas [`fri_only_program`] declares for a one-row shape: + /// `(r, DEEP(x_r))` per query, then the roots (the input tree's first), + /// the ζs (one per layer), the terminal coefficients and the per-query + /// layer openings (every layer a full group — layer 0 the input group). + fn arenas(&self) -> Vec> { + let queries = self.json["queries_detail"].as_array().expect("queries"); + let mut deep = Vec::new(); + for q in queries { + deep.push(base_word(FE::from(q["iota"].as_u64().expect("r")))); + deep.push(ext_word(&ext_of(&q["deep"]))); + } + let view = StarkProofView::Owned(&self.proof); + let (openings, caps) = super::epoch_verify_tests::fri_layer_openings(view, self.shape); + let mut per_query = Vec::new(); + for query in &openings { + for (values, path) in query { + per_query.extend(values.iter().map(ext_word)); + per_query.extend(super::proof_arena::commitments_to_arena(path)); + } + } + let zetas: Vec = self.json["zetas"] + .as_array() + .expect("zetas") + .iter() + .map(|z| ext_word(&ext_of(z))) + .collect(); + let mut out = vec![ + deep, + super::proof_arena::commitments_to_arena(&self.proof.fri_layers_merkle_roots), + zetas, + self.proof + .fri_final_poly_coeffs + .iter() + .map(ext_word) + .collect(), + per_query, + ]; + if self.shape.cap_words(super::proof_arena::words_per_root()) > 0 { + out.push(super::proof_arena::commitments_to_arena(&caps)); + } + out + } + + fn program(&self) -> LfmProgram { + fri_only_program(self.shape, self.shape.num_queries) + } +} + +/// ★ The emitted FRI verifier accepts every one-row RPX (e) proof — the pair +/// schedule from the input tree and the uneven `[3, 2, 1, 2]` override — with +/// the shape's index width, schedule, challenge count and layer depths equal +/// to the vector's, and the permutation count exactly the closed form. +#[test] +fn the_emitted_fri_verifier_accepts_every_one_row_rpx_vector() { + let vectors = one_row_rpx_vectors(); + assert_eq!(vectors.len(), 2, "one_row_pair and one_row_3_2_1_2"); + for v in vectors { + let s = v.shape; + s.check(); + assert!(v.json["one_row"].as_bool().expect("one_row")); + assert!(s.one_row()); + let schedule: Vec = v.json["schedule"] + .as_array() + .expect("schedule") + .iter() + .map(|d| d.as_u64().expect("d") as u8) + .collect(); + assert_eq!(s.schedule(), schedule, "{}: the schedule", v.name); + assert_eq!( + s.is_legacy(), + v.json["legacy_encoding"].as_bool().expect("legacy"), + "{}", + v.name + ); + assert_eq!( + s.index_bits() as u64, + v.json["trace_tree_depth"].as_u64().expect("depth"), + "{}: r has log2(lde) bits", + v.name + ); + assert_eq!( + 1u64 << s.index_bits(), + v.json["query_bound"].as_u64().expect("bound"), + "{}: r ranges over the whole LDE", + v.name + ); + assert_eq!( + s.num_zetas(), + v.json["zetas"].as_array().expect("zetas").len(), + "{}: one challenge per committed layer, none before the input tree", + v.name + ); + assert_eq!(s.num_committed(), v.proof.fri_layers_merkle_roots.len()); + for (qi, q) in v.json["queries_detail"] + .as_array() + .expect("queries") + .iter() + .enumerate() + { + for (j, layer) in q["layers"].as_array().expect("layers").iter().enumerate() { + assert_eq!( + s.layer_path_len(j) as u64, + layer["path_len"].as_u64().expect("path_len"), + "{} query {qi} layer {j}", + v.name + ); + } + } + + let program = v.program(); + let exec = execute(&program, &v.arenas(), &crate::hash_pin::BLOCK_HASHER) + .unwrap_or_else(|e| panic!("{}: the honest vector must execute: {e:?}", v.name)); + assert_eq!(exec.public_words.len(), s.num_queries); + let closed = s.num_queries * s.permutations_per_query() + s.cap_permutations(); + assert_eq!( + permutations(&program), + closed, + "{}: emitted permutations against the closed form", + v.name + ); + println!( + "{:<16} Q={} index bits {} schedule {:?} zetas {}: {} permutations, {} instructions", + v.name, + s.num_queries, + s.index_bits(), + s.schedule(), + s.num_zetas(), + closed, + program.instrs.len() + ); + } +} + +/// ★ Every value the INPUT tree's opening carries is bound, and so is the DEEP +/// value it is checked against: `DEEP(x_r)`, the input root, `ζ₀` (layer 0's +/// challenge under one row), a terminal coefficient, the input group's slot +/// value, a non-slot value, its last value and its first sibling. Run on both +/// (e) formats. +#[test] +fn no_tampered_input_tree_value_can_pass() { + for v in one_row_rpx_vectors() { + let program = v.program(); + let honest = v.arenas(); + execute(&program, &honest, &crate::hash_pin::BLOCK_HASHER).expect("honest"); + let q0 = &v.json["queries_detail"][0]["layers"][0]; + let slot = q0["slot"].as_u64().expect("slot") as usize; + assert_eq!( + ext_of(&q0["values"][slot]), + ext_of(&v.json["queries_detail"][0]["deep"]), + "{}: the input group's slot holds DEEP(x_r) (the host's input-slot check)", + v.name + ); + let d0 = 1usize << v.shape.layer_fold(0); + let other = (slot + 1) % d0; + // Arenas: deep (r, DEEP) per query, roots, zetas, coeffs, queries. + let bump: Vec<(String, usize, usize)> = vec![ + ("DEEP(x_r)".into(), 0, 1), + ("the input root".into(), 1, 0), + ("zeta_0 (layer 0's challenge)".into(), 2, 0), + ("terminal coefficient 0".into(), 3, 0), + (format!("input group slot value (slot {slot})"), 4, slot), + (format!("input group non-slot value {other}"), 4, other), + ("input group last value".into(), 4, d0 - 1), + ("input group first sibling".into(), 4, d0), + ]; + for (label, arena, word) in bump { + let mut bad = honest.clone(); + bad[arena][word][0] += FE::one(); + execute(&program, &bad, &crate::hash_pin::BLOCK_HASHER).expect_err(&format!( + "{}: moving {label} must make the program unexecutable", + v.name + )); + } + } +} + +/// ★ The INPUT-SLOT check is LOAD-BEARING (the in-guest M1 at the input +/// tree). Under one-row leaves `DEEP(x_r)` meets the committed FRI +/// chain ONLY at `group₀[slot] == DEEP(x_r)`: the input leaf hashes the group, +/// the walk authenticates it, the group fold reads it — none reads `DEEP(x_r)`. +/// So a moved `DEEP(x_r)` is refused with the check and ACCEPTED without it, +/// which is exactly a verifier that would run FRI on a codeword the trace +/// openings do not commit to. +#[test] +fn the_input_slot_check_is_load_bearing() { + for v in one_row_rpx_vectors() { + let honest = v.arenas(); + let mut moved = honest.clone(); + moved[0][1][0] += FE::one(); + + let with = v.program(); + execute(&with, &honest, &crate::hash_pin::BLOCK_HASHER).expect("honest"); + execute(&with, &moved, &crate::hash_pin::BLOCK_HASHER) + .expect_err("a moved DEEP(x_r) must be refused by the input-slot check"); + + super::fri::SKIP_SLOT_CHECK.with(|c| c.set(true)); + let without = v.program(); + super::fri::SKIP_SLOT_CHECK.with(|c| c.set(false)); + execute(&without, &moved, &crate::hash_pin::BLOCK_HASHER).unwrap_or_else(|e| { + panic!( + "{}: WITHOUT the slot check a moved DEEP(x_r) is accepted — the input-slot \ + check is the only binding: {e:?}", + v.name + ) + }); + } +} + +// ============================================================================= +// (e) — the one-row trace leaf +// ============================================================================= + +/// ★ The in-guest trace leaf at `rows_per_leaf = 1` (and at 2, today's) is +/// the host's: every leaf of the (e) KAT matrices (16 rows × 5 base +/// columns, 16 rows × 2 ext3 columns, read as bit-reversed LDE columns) under +/// the production hash. One row: leaf `i` = the row at bit-reversed position +/// `i`, columns in order. Row pair: rows `2i` then `2i + 1`. +#[test] +fn the_one_row_trace_leaf_is_the_hosts() { + let dir = stark::fri::vectors::vectors_dir(); + let json: Value = serde_json::from_slice( + &std::fs::read(dir.join("e_leaf_digests_rpx.json")).expect("the (e) leaf digests"), + ) + .expect("valid JSON"); + let rows = json["rows"].as_u64().expect("rows") as usize; + let base: Vec> = json["base_columns"] + .as_array() + .expect("base") + .iter() + .map(|c| { + c.as_array() + .expect("a column") + .iter() + .map(|x| FE::from(x.as_u64().expect("a felt"))) + .collect() + }) + .collect(); + let ext: Vec> = json["ext_columns"] + .as_array() + .expect("ext") + .iter() + .map(|c| c.as_array().expect("a column").iter().map(ext_of).collect()) + .collect(); + + let mut checked = 0usize; + for layout in json["layouts"].as_array().expect("layouts") { + let rows_per_leaf = layout["rows_per_leaf"].as_u64().expect("rows") as usize; + for (is_ext, key, width) in [ + (false, "base_leaves", base.len()), + (true, "ext_leaves", ext.len()), + ] { + let shape = GroupShape { + num_columns: width, + is_ext, + }; + let mut b = LfmBuilder::new().with_wrap_hash(super::edsl::WrapHash::production()); + let arena = b.declare_arena(shape.values_at(rows_per_leaf) as u32); + let cells: Vec<_> = (0..shape.values_at(rows_per_leaf) as u32) + .map(|i| b.hint_word(arena, i)) + .collect(); + let leaf = emit_leaf_hash_rows(&mut b, shape, rows_per_leaf, &cells); + for cell in leaf.cells() { + b.public(*cell); + } + let program = compile(b.finish()); + + let leaves = layout[key].as_array().expect("leaves"); + assert_eq!(leaves.len(), rows / rows_per_leaf); + for (i, want) in leaves.iter().enumerate() { + let mut words = Vec::new(); + for k in 0..rows_per_leaf { + let row = reverse_index(rows_per_leaf * i + k, rows as u64); + if is_ext { + words.extend(ext.iter().map(|c| ext_word(&c[row]))); + } else { + words.extend(base.iter().map(|c| base_word(c[row]))); + } + } + let exec = execute(&program, &[words], &crate::hash_pin::BLOCK_HASHER) + .expect("the leaf hash executes"); + let got: Vec = exec.public_words.iter().map(|(_, w)| *w).collect(); + assert_eq!( + got, + super::proof_arena::commitment_words(&commitment_of_hex( + want.as_str().expect("hex") + )), + "rows_per_leaf {rows_per_leaf} {key} leaf {i}" + ); + checked += 1; + } + } + } + assert_eq!(checked, 16 * 2 + 8 * 2, "every leaf of both layouts"); + println!("{checked} in-guest trace leaves == the host's (e) digests (rpx)"); +} + +// ============================================================================= +// Round trips on real proofs — both legs as one program +// ============================================================================= + +fn opts_with(one_row: OneRowMode, cap: CapPolicy, fri: FriMode) -> ProofOptions { + let mut opts = + stark::proof::options::GoldilocksCubicProofOptions::with_blowup(2).expect("blowup 2"); + opts.fri_number_of_queries = 24; + opts.grinding_factor = 0; + opts.fri_final_poly_log_degree = 2; + opts.format = ProofFormat { + merkle_cap: cap, + fri_mode: fri, + one_row, + ..ProofFormat::DEFAULT + }; + opts +} + +/// The terminal-codeword position a query arrives at. +fn terminal_position(s: FriShape, index: usize) -> usize { + if s.one_row() { + index >> s.total_folds() + } else { + index >> (s.total_folds() - 1) + } +} + +/// Both legs of one real sub-proof emitted into `b`, returning the program's +/// arenas for it (the trace leg's then the FRI leg's) and the closed-form +/// permutation count of the two legs. +fn emit_both_legs(b: &mut LfmBuilder, h: &HostFri) -> (Vec>, usize) { + let s = h.shape; + let all: Vec = (0..h.trace.iotas.len()).collect(); + let (_, _, terminal) = super::fri::emit_sub_proof_with_fri(b, &h.trace.shape, s, all.len()); + for t in &terminal { + b.public(t.as_cell()); + } + let mut arenas = h.trace.arenas(&all); + arenas.extend(h.fri_arenas(&all)); + + let hash = super::edsl::WrapHash::production(); + let sub = &h.trace.shape; + let leaves: usize = sub + .groups() + .iter() + .map(|g| blocks_for(group_leaf_felts_at(g, sub.rows_per_leaf()), hash)) + .sum(); + let closed = all.len() + * (leaves + sub.groups().len() * sub.path_len() + s.permutations_per_query()) + + sub.cap_permutations() + + s.cap_permutations(); + (arenas, closed) +} + +/// ★ The in-guest round trip at `one_row` ∈ {1, auto} × cap {off, auto} × fri +/// {pair, dp} on a real laptop-scale proof (L2G_MEMORY, 2048 rows, blowup 2, +/// `k = 2`, Q = 24): the FRI leg alone and both legs as one program execute +/// over every query, reach the terminal codeword production computed, and emit +/// exactly the closed form. Under `auto` the table's layout is the host's own +/// resolution (printed). At `one_row = 1` a moved one-row trace opening value +/// is refused (the join still binds the fold to the leaf). +#[test] +fn one_row_round_trips_in_guest() { + let mut layouts = Vec::new(); + for one_row in [OneRowMode::On, OneRowMode::Auto] { + for cap in [CapPolicy::Off, CapPolicy::Auto] { + for fri in [FriMode::Pair, FriMode::Dp] { + let label = format!("one_row={one_row} cap={cap} fri={fri:?}"); + let (air, proof) = folding_fixture_with(2048, opts_with(one_row, cap, fri)); + let h = host_fri_from(&*air, &proof); + let s = h.shape; + assert_eq!(h.trace.shape.layout, s.leaf_layout()); + if one_row == OneRowMode::On { + assert!(s.one_row(), "{label}"); + } + layouts.push((label.clone(), s.leaf_layout())); + let all: Vec = (0..h.trace.iotas.len()).collect(); + let codeword = h.terminal_codeword(); + + // The FRI leg alone. + let program = fri_only_program(s, all.len()); + let exec = execute( + &program, + &h.all_arenas(&all), + &crate::hash_pin::BLOCK_HASHER, + ) + .unwrap_or_else(|e| panic!("{label}: FRI leg: {e:?}")); + for (k, &q) in all.iter().enumerate() { + let v = word_as_ext(&exec.public_words[k].1).expect("ext"); + assert_eq!( + v, + codeword[terminal_position(s, h.trace.iotas[q])], + "{label}" + ); + } + assert_eq!( + permutations(&program), + all.len() * s.permutations_per_query() + s.cap_permutations(), + "{label}: FRI leg closed form" + ); + + // Both legs as one program. + let mut b = LfmBuilder::new().with_wrap_hash(super::edsl::WrapHash::production()); + let (arenas, closed) = emit_both_legs(&mut b, &h); + let joined = compile(b.finish()); + let exec = execute(&joined, &arenas, &crate::hash_pin::BLOCK_HASHER) + .unwrap_or_else(|e| panic!("{label}: joined: {e:?}")); + for (k, &q) in all.iter().enumerate() { + let v = word_as_ext(&exec.public_words[k].1).expect("ext"); + assert_eq!( + v, + codeword[terminal_position(s, h.trace.iotas[q])], + "{label}" + ); + } + assert_eq!( + permutations(&joined), + closed, + "{label}: both legs' closed form" + ); + + if s.one_row() { + // A one-row trace opening value (query 0, main column 0 — + // right after the index word) must not execute. + let mut bad = arenas.clone(); + bad[4][1][0] += FE::one(); + execute(&joined, &bad, &crate::hash_pin::BLOCK_HASHER).expect_err(&format!( + "{label}: a moved one-row trace value must be refused" + )); + // The upper half of the LDE is reached: r is not a pair index. + let lde = 1usize << s.log2_lde_length; + assert!( + h.trace.iotas.iter().any(|&r| r >= lde / 2), + "{label}: 24 one-row indices over the whole LDE reach its upper half" + ); + } + println!( + "{label:<34} layout {:?} index bits {} schedule {:?} FRI caps {:?} trace cap \ + {}: FRI leg {} perms, both legs {} perms / {} instructions", + s.leaf_layout(), + s.index_bits(), + s.schedule(), + (0..s.num_committed()) + .map(|j| s.layer_cap(j)) + .collect::>(), + h.trace.shape.trace_cap, + permutations(&program), + closed, + joined.instrs.len(), + ); + } + } + } + assert_eq!(layouts.len(), 8); +} + +/// ★ Mixed layouts in ONE program: a one-row table (L2G_MEMORY at 2048 rows, +/// `one_row = 1`, `fri = dp`, cap auto) and a row-pair table (L2G_MEMORY at +/// 1024 rows, today's format) verified side by side, both legs each — the +/// shape of an `auto` epoch whose tables resolve differently. Each table's +/// layout is its own verifier constant; the program executes, every terminal +/// is production's, and the permutations are the sum of the two closed forms. +#[test] +fn a_one_row_and_a_row_pair_table_verify_in_one_program() { + let (air_a, proof_a) = folding_fixture_with( + 2048, + opts_with(OneRowMode::On, CapPolicy::Auto, FriMode::Dp), + ); + let (air_b, proof_b) = folding_fixture_with( + 1024, + opts_with(OneRowMode::Off, CapPolicy::Off, FriMode::Pair), + ); + let a = host_fri_from(&*air_a, &proof_a); + let b_host = host_fri_from(&*air_b, &proof_b); + assert_eq!(a.shape.leaf_layout(), LeafLayout::Row); + assert_eq!(b_host.shape.leaf_layout(), LeafLayout::RowPair); + + let mut b = LfmBuilder::new().with_wrap_hash(super::edsl::WrapHash::production()); + let (mut arenas, closed_a) = emit_both_legs(&mut b, &a); + let (arenas_b, closed_b) = emit_both_legs(&mut b, &b_host); + arenas.extend(arenas_b); + let program = compile(b.finish()); + let exec = execute(&program, &arenas, &crate::hash_pin::BLOCK_HASHER) + .expect("a one-row and a row-pair table verify in one program"); + let mut k = 0usize; + for h in [&a, &b_host] { + let codeword = h.terminal_codeword(); + for &iota in &h.trace.iotas { + let v = word_as_ext(&exec.public_words[k].1).expect("ext"); + assert_eq!(v, codeword[terminal_position(h.shape, iota)]); + k += 1; + } + } + assert_eq!(permutations(&program), closed_a + closed_b); + println!( + "mixed program: one-row table {} perms + row-pair table {} perms = {} ({} instructions)", + closed_a, + closed_b, + closed_a + closed_b, + program.instrs.len() + ); +} + +/// The one-row query index needs a schedule override that the DP never +/// picks to exercise unequal neighbouring exponents from the INPUT tree +/// (the fold-count off-by-one check at layer 0): `[3, 1, 3, 2]` over the 9 committed folds of a +/// 2048-row, blowup-2, `k = 2` one-row table (`12 → 3`, every fold committed) +/// — both legs, executed. +#[test] +fn an_uneven_one_row_schedule_round_trips_in_guest() { + let mut opts = opts_with(OneRowMode::On, CapPolicy::Off, FriMode::Dp); + opts.format.fri_schedule_override = FriScheduleOverride::new(&[3, 1, 3, 2]); + let (air, proof) = folding_fixture_with(2048, opts); + let h = host_fri_from(&*air, &proof); + assert_eq!(h.shape.schedule(), vec![3, 1, 3, 2]); + let mut b = LfmBuilder::new().with_wrap_hash(super::edsl::WrapHash::production()); + let (arenas, closed) = emit_both_legs(&mut b, &h); + let program = compile(b.finish()); + execute(&program, &arenas, &crate::hash_pin::BLOCK_HASHER) + .expect("the uneven one-row schedule verifies"); + assert_eq!(permutations(&program), closed); +} diff --git a/prover/src/lfm/one_row_tests.rs b/prover/src/lfm/one_row_tests.rs new file mode 100644 index 000000000..c5d81566f --- /dev/null +++ b/prover/src/lfm/one_row_tests.rs @@ -0,0 +1,213 @@ +//! S2 (one-row openings) on the LFM side, host only: the commit helpers at +//! both leaf layouts, the registry +//! policy (a one-row format never reads `LFM_REGISTRY`), the one-row roots of +//! a program's artifacts, and the in-circuit register commitment against its +//! host twin at BOTH layouts. Execute-only and artifact builds; nothing here +//! proves. + +use stark::leaf_layout::LeafLayout; +use stark::proof::options::{GoldilocksCubicProofOptions, OneRowMode, ProofOptions}; + +use crate::tables::types::{FE, GoldilocksField}; + +use super::commit::{ + commit_columns_with, commit_lde_columns, commit_lde_columns_with, group_columns, +}; +use super::programs::{RegisterDerivationShape, register_derivation_program}; +use super::registry::{ + LfmProgramKind, PROGRAM_GROUP_SLOTS, REGISTRY_READS, build_artifacts, program_groups, resolve, + resolve_artifacts, +}; +use super::validator::validate; +use super::word::LfmWord; + +fn options(blowup: u8, one_row: OneRowMode) -> ProofOptions { + let mut o = GoldilocksCubicProofOptions::with_blowup(blowup).expect("options"); + o.format.one_row = one_row; + o +} + +fn splitmix(state: &mut u64) -> u64 { + *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = *state; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) +} + +/// `lfm::commit` at one row is `stark::commitment` at `rows_per_leaf = 1`, +/// under the block pin; at row pairs it is today's helper. +#[test] +fn the_commit_helpers_follow_the_layout() { + let mut st = 11u64; + let cols: Vec> = (0..3) + .map(|_| (0..64).map(|_| FE::from(splitmix(&mut st))).collect()) + .collect(); + type B = + ::Batched; + let (_, row) = + stark::commitment::commit_bit_reversed_with::(&cols, 1).expect("tree"); + let (_, pair) = + stark::commitment::commit_bit_reversed_with::(&cols, 2).expect("tree"); + assert_eq!(commit_lde_columns_with(&cols, LeafLayout::Row), row); + assert_eq!(commit_lde_columns_with(&cols, LeafLayout::RowPair), pair); + assert_eq!( + commit_lde_columns(&cols), + pair, + "today's helper is the row-pair one" + ); + assert_ne!(row, pair); +} + +/// ★ The registry policy: `LFM_REGISTRY` stays row-pair only. +/// At the default format `resolve_artifacts` IS the registry row; under a +/// one-row format (`On` or `Auto`) it never reads the registry and builds the +/// program's artifacts at run time — with the SAME row-pair roots and program +/// id (so the identity is unchanged) plus the one-row roots. +#[test] +fn a_one_row_format_never_reads_the_registry() { + let kind = LfmProgramKind::TrivialV0; + let reads = || REGISTRY_READS.with(|c| c.get()); + + let before = reads(); + let default = resolve_artifacts(kind, &options(2, OneRowMode::Off)).expect("registered"); + assert_eq!(reads(), before + 1, "the default format reads the registry"); + assert_eq!(default, resolve(kind, 2).expect("row").artifacts()); + assert!(default.one_row_roots.is_none()); + + for mode in [OneRowMode::On, OneRowMode::Auto] { + let before = reads(); + let built = resolve_artifacts(kind, &options(2, mode)).expect("built"); + assert_eq!(reads(), before, "{mode:?}: LFM_REGISTRY must not be read"); + assert_eq!( + built.roots, default.roots, + "{mode:?}: row-pair roots unchanged" + ); + assert_eq!( + built.program_id, default.program_id, + "{mode:?}: identity unchanged" + ); + let one_row = built.one_row_roots.as_ref().expect("one-row roots built"); + for slot in 0..=10 { + let root = one_row.roots[slot].expect("every committed group has a one-row root"); + assert_ne!(root, built.roots[slot], "slot {slot}: layouts differ"); + } + // Blowup 2 has no one-row static twin: the hosted KECCAK_RC and + // BITWISE roots are hard misses, not recomputes. + assert_eq!(one_row.roots[13], None); + assert_eq!(one_row.roots[14], None); + } +} + +/// The one-row roots are each group's own one-row commitment, and the hosted +/// static tables take their shipped twins (blowup 4, the knob's blowup). +#[test] +fn artifacts_carry_each_groups_one_row_root() { + let program = LfmProgramKind::TrivialV0.program(); + let opts = options(4, OneRowMode::On); + let artifacts = build_artifacts(&program, &opts); + let one_row = artifacts.one_row_roots.as_ref().expect("one-row roots"); + let groups = program_groups(&program); + for (slot, group) in groups.iter().enumerate().take(PROGRAM_GROUP_SLOTS) { + assert_eq!( + one_row.roots[slot], + Some(commit_columns_with( + &group_columns(group), + &opts, + LeafLayout::Row + )), + "slot {slot}" + ); + } + assert_eq!( + one_row.roots[13], + crate::tables::keccak_rc::preprocessed_commitment_for(&opts, LeafLayout::Row) + ); + assert!(one_row.roots[13].is_some() && one_row.roots[14].is_some()); + assert_eq!( + one_row.roots[12], None, + "KECCAK_RND has no preprocessed columns" + ); + assert_eq!( + one_row.roots[super::airs::BLAKE3_SLOT], + one_row.blake3_chunk_roots.first().copied() + ); + // The default format builds none, and its artifacts are unchanged. + let default = build_artifacts(&program, &options(4, OneRowMode::Off)); + assert!(default.one_row_roots.is_none()); + assert_eq!(default.roots, artifacts.roots); + assert_eq!(default.program_id, artifacts.program_id); +} + +fn register_file(seed: u64) -> Vec { + let mut st = seed; + (0..crate::tables::register::NUM_REGISTER_ADDRESSES) + .map(|_| (splitmix(&mut st) >> 32) as u32) + .collect() +} + +fn digest_bytes(public: &[(u32, LfmWord)]) -> [u8; 32] { + use math::field::traits::IsPrimeField; + if public.len() == 1 { + return super::algebraic_commit::digest_to_commitment(&public[0].1); + } + assert_eq!( + public.len(), + 2, + "a digest is one algebraic word or two byte words" + ); + let mut out = [0u8; 32]; + for h in 0..8 { + let lane = public[h / 4].1[h % 4]; + let half = GoldilocksField::canonical(lane.value()) as u32; + out[4 * h..4 * h + 4].copy_from_slice(&half.to_le_bytes()); + } + out +} + +/// ★ The in-circuit register commitment against its host twin at BOTH leaf +/// layouts. A mismatch would show only as a runtime +/// `DivByZero` deep in a node, so each layout gets its own root equality. One +/// emitter, two constants (`RegisterDerivationShape::rows_per_leaf`). +#[test] +fn the_register_derivation_matches_its_host_twin_at_both_layouts() { + for blowup in [2usize, 4] { + let opts = GoldilocksCubicProofOptions::with_blowup(blowup as u8).expect("options"); + for layout in [LeafLayout::RowPair, LeafLayout::Row] { + let shape = RegisterDerivationShape { + blowup, + coset_offset: opts.coset_offset, + rows_per_leaf: layout.rows_per_leaf(), + }; + assert_eq!(shape.leaves(), 128 * blowup / layout.rows_per_leaf()); + let program = register_derivation_program(shape); + validate(&program).expect("admission"); + let (init, fini) = (register_file(1), register_file(2)); + let column = |v: &[u32]| { + v.iter() + .map(|&x| super::word::base_word(FE::from(x as u64))) + .collect::>() + }; + let arenas = vec![column(&init), column(&fini)]; + let exec = super::executor::execute(&program, &arenas, &crate::hash_pin::BLOCK_HASHER) + .unwrap_or_else(|e| panic!("blowup {blowup} {layout:?}: {e:?}")); + let host = crate::tables::register::compute_precomputed_commitment_with_fini_layout( + &opts, &init, &fini, layout, + ); + assert_eq!( + digest_bytes(&exec.public_words), + host, + "blowup {blowup} {layout:?}: the emitted root must equal the host twin's" + ); + if layout == LeafLayout::RowPair { + assert_eq!( + host, + crate::tables::register::compute_precomputed_commitment_with_fini( + &opts, &init, &fini + ), + "row pairs are today's commitment" + ); + } + } + } +} diff --git a/prover/src/lfm/per_table_aggregator.rs b/prover/src/lfm/per_table_aggregator.rs index 32aa63654..de66b36fc 100644 --- a/prover/src/lfm/per_table_aggregator.rs +++ b/prover/src/lfm/per_table_aggregator.rs @@ -79,7 +79,8 @@ pub struct ChildTable<'a> { /// The preprocessed-columns commitment, when the AIR is preprocessed. /// /// An AIR-SET constant at emit time, exactly as production takes it - /// (`air.precomputed_commitment()`, never the proof's copy). Interning it + /// (`air.precomputed_commitment_for(layout)` at the table's leaf layout, + /// never the proof's copy). Interning it /// here is what makes production's explicit proof-copy-equals-AIR-copy check /// the ABSENCE of a second value in this machine rather than a comparison. pub precomputed_root: Option<&'a Commitment>, diff --git a/prover/src/lfm/per_table_aggregator_tests.rs b/prover/src/lfm/per_table_aggregator_tests.rs index dd0419339..0c9c18e5e 100644 --- a/prover/src/lfm/per_table_aggregator_tests.rs +++ b/prover/src/lfm/per_table_aggregator_tests.rs @@ -138,7 +138,10 @@ pub(super) fn real_global( for (idx, air) in refs.iter().enumerate() { let v = view.get(idx); if air.is_preprocessed() { - transcript.append_bytes(&air.precomputed_commitment()); + transcript.append_bytes(&super::epoch_verify_tests::layout_precomputed_commitment( + *air, + v.trace_length(), + )); } transcript.append_bytes(v.lde_trace_main_merkle_root()); } @@ -457,6 +460,7 @@ pub(super) fn global_arena_words(g: &RealGlobal) -> Vec> { } arenas.push(leg.opening_arena()); arenas.push(leg.fri_arena()); + arenas.extend(leg.caps_arena()); } arenas } @@ -1085,7 +1089,7 @@ pub(super) fn real_child_timed( ); let verify_secs = t_verify.elapsed().as_secs_f64(); - let airs = super::airs::LfmAirs::new_chunked( + let mut airs = super::airs::LfmAirs::new_chunked( &artifacts.roots, &artifacts.blake3_chunk_roots, &opts, @@ -1093,6 +1097,11 @@ pub(super) fn real_child_timed( artifacts.hasher, artifacts.chip_set, ); + // S2: the one-row preprocessed roots, exactly as `verify_against_artifacts` + // attaches them — a one-row chip's Phase A root and leg compare use them. + if let Some(one_row) = &artifacts.one_row_roots { + airs = airs.with_one_row_roots(one_row); + } let refs = airs.air_refs(); let view = MultiProofView::Owned(&proved.proof); assert_eq!(refs.len(), view.len(), "one AIR per sub-proof"); @@ -1114,7 +1123,10 @@ pub(super) fn real_child_timed( for (idx, air) in refs.iter().enumerate() { let v = view.get(idx); if air.is_preprocessed() { - transcript.append_bytes(&air.precomputed_commitment()); + transcript.append_bytes(&super::epoch_verify_tests::layout_precomputed_commitment( + *air, + v.trace_length(), + )); } transcript.append_bytes(v.lde_trace_main_merkle_root()); } @@ -1216,6 +1228,7 @@ pub(super) fn child_arena_words(c: &RealChild) -> Vec> { } arenas.push(leg.opening_arena()); arenas.push(leg.fri_arena()); + arenas.extend(leg.caps_arena()); } arenas } @@ -1443,6 +1456,21 @@ fn the_leaf_node_verifies_and_binds_two_wraps() { } let label_refs: Vec<&[u64]> = labels.iter().map(|l| &l[..]).collect(); let label_range = (labels[0][0], labels[FAN_IN - 1][0]); + // S2: how many of each wrap's sub-proofs the node verifies at one-row + // leaves (0 at the default format, all at `one_row = 1`, the AIR widths' + // choice at `auto`). One parseable line per child for the box wrapper. + for (k, c) in children.iter().enumerate() { + let one_row = c + .legs + .iter() + .filter(|l| l.verify.sub.layout.is_one_row()) + .count(); + println!( + "ZFS2NODE child={k} {} sub_proofs={} one_row_legs={one_row}", + crate::zf_format::ZfFormat::global().banner(), + c.legs.len() + ); + } println!( " {FAN_IN} epoch wraps proved in {:.1}s, {} published words each, \ {} sub-proofs each\n RSS high-water AFTER the wrap proves: {:?} GiB", @@ -2678,7 +2706,8 @@ fn the_production_leaf_node_measures() { ); let inputs = EpochInputs::from_env(); - let inner = crate::recursion::Preset::Blowup4.options(); + // ★ The production format sites (the process's `ZfFormat` stamped on). + let inner = super::proof::block_base_options(); let wrap_opts = super::proof::aggregation_wrap_options(); println!( "★ PRODUCTION LEAF NODE: FAN-IN {fan_in} · guest {}, {} input bytes, \ @@ -5926,7 +5955,8 @@ fn the_production_tree_composes_to_a_root() { }; let inputs = EpochInputs::from_env(); - let inner = crate::recursion::Preset::Blowup4.options(); + // ★ The production format sites (the process's `ZfFormat` stamped on). + let inner = super::proof::block_base_options(); let wrap_opts = super::proof::aggregation_wrap_options(); let ceiling = cgroup_limit_gib(); println!( @@ -7835,7 +7865,8 @@ fn the_whir_production_tree_composes_to_a_root() { ); let inputs = EpochInputs::from_env(); - let inner = crate::recursion::Preset::Blowup4.options(); + // ★ The production format sites (the process's `ZfFormat` stamped on). + let inner = super::proof::block_base_options(); let wrap_opts = super::proof::aggregation_wrap_options(); let ceiling = cgroup_limit_gib(); println!( diff --git a/prover/src/lfm/per_table_census_tests.rs b/prover/src/lfm/per_table_census_tests.rs index 4c6f98102..7a422284f 100644 --- a/prover/src/lfm/per_table_census_tests.rs +++ b/prover/src/lfm/per_table_census_tests.rs @@ -93,8 +93,7 @@ use super::deep::DeepShape; use super::edsl::WrapHash; use super::epoch::{RootCells, TableAbsorbs, TableChallengeShape, fork_table}; use super::epoch_verify::{ - FRI_LEAF_FELTS, TableVerifyShape, blocks_for, boundary_terms, group_leaf_felts, - query_permutations_for, + TableVerifyShape, blocks_for, boundary_terms, group_leaf_felts, query_permutations_for, }; use super::fri::FriShape; use super::hash::HasherKind; @@ -403,15 +402,23 @@ fn table_shape( num_composition_parts: num_parts, log2_trace_length, }; + // The table's leaf layout (S2), resolved as the host prover resolves it. + let leaf_layout = stark::leaf_layout::table_leaf_layout(air, trace_length); + let merkle_depth = leaf_layout.tree_depth(log2_lde_length as usize); let sub = SubProofShape { deep, trace_groups, - merkle_depth: log2_lde_length as usize - 1, + merkle_depth, log2_lde_length, coset_offset: FE::from(opts.coset_offset), + trace_cap: opts + .format + .merkle_cap + .height(opts.fri_number_of_queries, merkle_depth), + layout: leaf_layout, }; let has_aux_trace = air.has_aux_trace(); - let fri = FriShape::from_options(opts, log2_lde_length); + let fri = FriShape::for_layout(opts, log2_lde_length, leaf_layout); TableShape { name, @@ -516,8 +523,9 @@ fn bill(tables: &[TableShape], hash: WrapHash, hash_chip: &str) -> (Bill, usize) .iter() .map(|g| blocks_for(group_leaf_felts(g), hash)) .sum(); - let fri_leaves = t.verify.fri.num_committed() * blocks_for(FRI_LEAF_FELTS, hash); - let parents = groups.len() * t.verify.sub.merkle_depth; + let fri_leaves = t.verify.fri.leaf_permutations_per_query(hash); + // Paths stop at the trees' cap (`merkle_depth − trace_cap`). + let parents = groups.len() * t.verify.sub.path_len(); let fri_paths = t.verify.fri.path_steps_per_query(); b.trace_leaves += leaves; @@ -793,6 +801,7 @@ fn wrap_options() -> ProofOptions { coset_offset: 3, grinding_factor: 20, fri_final_poly_log_degree: 7, + format: stark::proof::options::ProofFormat::DEFAULT, } } diff --git a/prover/src/lfm/programs.rs b/prover/src/lfm/programs.rs index abdc8a69a..c47941cd3 100644 --- a/prover/src/lfm/programs.rs +++ b/prover/src/lfm/programs.rs @@ -1189,6 +1189,11 @@ pub struct RegisterDerivationShape { pub blowup: usize, /// The inner proof's coset offset (`ProofOptions::coset_offset`). pub coset_offset: u64, + /// Rows per Merkle leaf of the inner REGISTER tree: the inner table's leaf + /// layout (`stark::leaf_layout::LeafLayout::rows_per_leaf`) — 2 today, 1 + /// under one-row openings (S2). One emitter, two constants; the host twin + /// is `register::compute_precomputed_commitment_with_fini_layout`. + pub rows_per_leaf: usize, } impl RegisterDerivationShape { @@ -1202,9 +1207,9 @@ impl RegisterDerivationShape { self.num_rows() * self.blowup } - /// Merkle leaves — one per row PAIR (`ROWS_PER_LEAF = 2`). + /// Merkle leaves — one per `rows_per_leaf` rows (a row PAIR today). pub fn leaves(self) -> usize { - self.lde_rows() / stark::commitment::ROWS_PER_LEAF + self.lde_rows() / self.rows_per_leaf } /// Permutations the tree costs: one per leaf plus one per internal node. @@ -1329,7 +1334,6 @@ pub fn emit_register_commitment( use super::lde::coset_lde; use crate::tables::register::{NUM_PREPROCESSED_COLS_WITH_FINI, NUM_REGISTER_ADDRESSES}; use math::fft::bit_reversing::reverse_index; - use stark::commitment::ROWS_PER_LEAF; assert_eq!( NUM_PREPROCESSED_COLS_WITH_FINI, 3, @@ -1384,14 +1388,20 @@ pub fn emit_register_commitment( let init_lde = coset_lde(b, &init_col, shape.blowup, coset_offset); let fini_lde = coset_lde(b, &fini_col, shape.blowup, coset_offset); - // Leaf `i` hashes the bit-reversed rows `2i` and `2i+1`, each written - // column by column in big-endian — `keccak_leaves_bit_reversed_grouped`. + // Leaf `i` hashes the bit-reversed rows `R·i .. R·i + R − 1` (`R` = + // `shape.rows_per_leaf`: the pair `2i`, `2i+1` today), each written column + // by column in big-endian — `keccak_leaves_bit_reversed_grouped`. + let rows_per_leaf = shape.rows_per_leaf; + assert!( + rows_per_leaf == 1 || rows_per_leaf == 2, + "a REGISTER leaf holds one row or a row pair" + ); let lde_rows = shape.lde_rows(); let leaves: Vec<_> = (0..shape.leaves()) .map(|leaf| { - let mut values = Vec::with_capacity(ROWS_PER_LEAF * NUM_PREPROCESSED_COLS_WITH_FINI); - for k in 0..ROWS_PER_LEAF { - let row = reverse_index(ROWS_PER_LEAF * leaf + k, lde_rows as u64); + let mut values = Vec::with_capacity(rows_per_leaf * NUM_PREPROCESSED_COLS_WITH_FINI); + for k in 0..rows_per_leaf { + let row = reverse_index(rows_per_leaf * leaf + k, lde_rows as u64); values.extend([offset_lde[row], init_lde[row], fini_lde[row]]); } edsl::wrap_leaf_hash(b, &values) diff --git a/prover/src/lfm/proof.rs b/prover/src/lfm/proof.rs index 98f473a0f..c54a62928 100644 --- a/prover/src/lfm/proof.rs +++ b/prover/src/lfm/proof.rs @@ -26,7 +26,7 @@ use super::airs::{BLAKE3_SLOT, ChipSet, LfmAirs, NUM_LFM_CHIPS}; use super::compiler::LfmProgram; use super::executor::{LfmExecError, LfmExecution, execute}; use super::hash::HasherKind; -use super::registry::{LfmArtifacts, LfmProgramKind, LfmRegistryError, resolve}; +use super::registry::{LfmArtifacts, LfmProgramKind, LfmRegistryError}; use super::statement::absorb_lfm_statement; use super::trace::{LfmTraces, build_traces_with_hasher}; use super::word::LfmWord; @@ -285,7 +285,7 @@ pub(crate) fn prove_traces_with_hasher( // must be free to overlap another proof's device phase, which is the entire // point of the lever. let _card = super::device_permit::hold_labeled("multi_prove"); - let airs = LfmAirs::new_chunked( + let mut airs = LfmAirs::new_chunked( &artifacts.roots, &artifacts.blake3_chunk_roots, options, @@ -293,6 +293,11 @@ pub(crate) fn prove_traces_with_hasher( hasher, artifacts.chip_set, ); + // One-row chips (S2) take their roots from the artifacts; without them a + // chip resolved to one row is refused by `multi_prove`, never recomputed. + if let Some(one_row) = &artifacts.one_row_roots { + airs = airs.with_one_row_roots(one_row); + } let mut transcript = crate::hash_pin::block_transcript(&[]); absorb_lfm_statement( &mut transcript, @@ -339,9 +344,9 @@ pub fn lfm_verify( claimed_public: &[(u32, LfmWord)], options: &ProofOptions, ) -> Result { - let entry = resolve(kind, options.blowup_factor)?; + let artifacts = super::registry::resolve_artifacts(kind, options)?; Ok(verify_against_artifacts( - &entry.artifacts(), + &artifacts, proof, claimed_public, options, @@ -363,7 +368,8 @@ pub fn verify_against_artifacts( claimed_public: &[(u32, LfmWord)], options: &ProofOptions, ) -> bool { - verify_against_chunked( + verify_against_chunked_with( + artifacts.one_row_roots.as_ref(), &artifacts.roots, &artifacts.blake3_chunk_roots, &artifacts.program_id, @@ -444,6 +450,36 @@ pub fn verify_against_chunked( options: &ProofOptions, hasher: HasherKind, chip_set: ChipSet, +) -> bool { + verify_against_chunked_with( + None, + roots, + blake3_roots, + program_id, + keccak_rnd_chunks, + proof, + claimed_public, + options, + hasher, + chip_set, + ) +} + +/// [`verify_against_chunked`] with the program's one-row (S2) roots, when it +/// has them (`None` = row-pair roots only: a chip resolved to one row then +/// rejects). +#[allow(clippy::too_many_arguments)] +fn verify_against_chunked_with( + one_row_roots: Option<&super::registry::LfmOneRowRoots>, + roots: &[Commitment; NUM_LFM_CHIPS], + blake3_roots: &[Commitment], + program_id: &Commitment, + keccak_rnd_chunks: usize, + proof: &MultiProof, + claimed_public: &[(u32, LfmWord)], + options: &ProofOptions, + hasher: HasherKind, + chip_set: ChipSet, ) -> bool { // The chunk count and the mask must agree, and BOTH come from the resolved // registry entry rather than the proof — so this rejects a malformed entry, @@ -462,7 +498,7 @@ pub fn verify_against_chunked( return false; } - let airs = LfmAirs::new_chunked( + let mut airs = LfmAirs::new_chunked( roots, blake3_roots, options, @@ -470,6 +506,9 @@ pub fn verify_against_chunked( hasher, chip_set, ); + if let Some(one_row) = one_row_roots { + airs = airs.with_one_row_roots(one_row); + } let refs = airs.air_refs(); let mut transcript = crate::hash_pin::block_transcript(&[]); @@ -484,7 +523,9 @@ pub fn verify_against_chunked( // LogUp challenges; the expected balance is the LfmPublic sum recomputed // from the claimed words (all other LFM buses balance to zero internally). let mut replay = transcript.clone(); - let (z, alpha) = crate::replay_transcript_phase_a_view(&refs, view, &mut replay); + let Some((z, alpha)) = crate::replay_transcript_phase_a_view(&refs, view, &mut replay) else { + return false; + }; let Some(expected) = expected_public_balance(claimed_public, &z, &alpha) else { return false; }; @@ -546,9 +587,28 @@ fn expected_public_balance( /// coefficients it merely absorbs. Inner epochs are NOT touched by this /// choice: the wrap PROGRAM is a function of the inner proof's options, so /// this constructor moves no program identity. +/// +/// ★ A PRODUCTION FORMAT SITE: the process's [`ZfFormat`](crate::zf_format::ZfFormat) +/// is stamped on here (`LAMBDA_VM_ZF_CAP`, `_FRI`, `_ONE_ROW`), so every LFM +/// proof — wraps, nodes, the root — and every emitter that derives its shape +/// from these options sees one format. Unset knobs give +/// [`ZfFormat::DEFAULT`](crate::zf_format::ZfFormat::DEFAULT), the measured +/// configuration; every knob at its off spelling gives the legacy options. pub fn aggregation_wrap_options() -> ProofOptions { let mut opts = stark::proof::options::GoldilocksCubicProofOptions::with_blowup(4) .expect("blowup=4 is valid"); opts.fri_final_poly_log_degree = 8; - opts + crate::zf_format::ZfFormat::global().options(opts) +} + +/// The STARK block's base-epoch options: the blowup-4 preset the production +/// tree proves its epochs under, with the process's +/// [`ZfFormat`](crate::zf_format::ZfFormat) stamped on — a PRODUCTION FORMAT +/// SITE, like [`aggregation_wrap_options`]. +/// +/// Not [`crate::recursion::Preset::options`] itself: that value also fixes +/// the RV64 recursion guest's verifier, which stays on the LEGACY format +/// (its presets name it). +pub fn block_base_options() -> ProofOptions { + crate::zf_format::ZfFormat::global().options(crate::recursion::Preset::Blowup4.options()) } diff --git a/prover/src/lfm/registry.rs b/prover/src/lfm/registry.rs index 8130d0c5c..9835f8bab 100644 --- a/prover/src/lfm/registry.rs +++ b/prover/src/lfm/registry.rs @@ -43,6 +43,52 @@ pub enum LfmProgramKind { StatementReplayV0, } +impl LfmProgramKind { + /// The fixture program this kind names, built from code — what + /// `compute_lfm_registry` blesses into the row. + pub fn program(self) -> LfmProgram { + use super::programs::{ + KECCAK_SPONGE_LEN, fri_toy_program, keccak_chain_program, keccak_sponge_program, + statement_replay_program, transcript_replay_program, trivial_program, + }; + match self { + Self::TrivialV0 => trivial_program(), + Self::FriToyV0 => fri_toy_program(), + Self::KeccakChainV0 => keccak_chain_program(), + Self::KeccakSpongeV0 => keccak_sponge_program(KECCAK_SPONGE_LEN), + Self::TranscriptReplayV0 => transcript_replay_program(), + Self::StatementReplayV0 => statement_replay_program(), + } + } +} + +/// ★ The artifacts a fixture program is verified against under `options`. +/// +/// The registry policy: `LFM_REGISTRY` is blessed at +/// today's leaf layout and STAYS row-pair only. At the default format this is +/// [`resolve`] — the registry row, no fallback. Under a one-row format (`On` +/// or `Auto`) the registry is NOT read: the program is rebuilt from code and +/// its artifacts computed at run time (row-pair AND one-row roots, as +/// `compute_lfm_registry` would), which is what the registry pins anyway — +/// `registry_drift_*` hold the two equal at the default. +pub fn resolve_artifacts( + kind: LfmProgramKind, + options: &ProofOptions, +) -> Result { + if options.format.one_row == stark::proof::options::OneRowMode::Off { + return Ok(resolve(kind, options.blowup_factor)?.artifacts()); + } + Ok(build_artifacts(&kind.program(), options)) +} + +#[cfg(test)] +thread_local! { + /// Reads of `LFM_REGISTRY` on this thread (test builds only), for the + /// registry-policy test. + pub(crate) static REGISTRY_READS: core::cell::Cell = + const { core::cell::Cell::new(0) }; +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum LfmRegistryError { UnknownProgram { @@ -113,6 +159,9 @@ impl LfmRegistryEntry { hasher: self.hasher, chip_set: self.chip_set, program_id: self.program_id, + // The registry is ROW-PAIR ONLY: a one-row + // format never reads it — `resolve_artifacts` builds at run time. + one_row_roots: None, } } } @@ -151,6 +200,27 @@ pub struct LfmArtifacts { /// compiled groups at bless time. See [`ChipSet`]. pub chip_set: ChipSet, pub program_id: Commitment, + /// The ONE-ROW (S2) preprocessed roots of the same groups, built only when + /// the options' format has one-row openings on (`On` or `Auto`: which chips + /// `Auto` resolves to one row is decided later, per chip, by the STARK + /// prover and verifier, so every chip gets one). `None` at the default. + /// + /// NOT folded into `program_id`: the identity stays the row-pair roots' + /// (the one-row roots are a deterministic function of the same columns), + /// so a program keeps one id across layouts on the LFM side. + pub one_row_roots: Option, +} + +/// The one-row preprocessed roots of an [`LfmArtifacts`] (see its field). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LfmOneRowRoots { + /// Per chip slot, as `LfmArtifacts::roots`; `None` = no one-row root (a + /// static table with no one-row twin at this blowup — a hard miss if the + /// chip resolves to one row). Slot 12 (`KECCAK_RND`) has no + /// preprocessed columns and stays `None`. + pub roots: [Option; NUM_LFM_CHIPS], + /// One per `LFM_BLAKE3` chunk, as `LfmArtifacts::blake3_chunk_roots`. + pub blake3_chunk_roots: Vec, } impl LfmArtifacts { @@ -515,6 +585,8 @@ pub fn build_artifacts_with_hasher( &blake3_chunk_roots, &blake3_chunk_log_heights, ); + let one_row_roots = (options.format.one_row != stark::proof::options::OneRowMode::Off) + .then(|| build_one_row_roots(program, options, &groups)); LfmArtifacts { roots, log_heights, @@ -524,6 +596,36 @@ pub fn build_artifacts_with_hasher( hasher, chip_set, program_id, + one_row_roots, + } +} + +/// The one-row roots of every committed group (host pass: the device commit +/// builds row-pair leaves only), plus the static tables' one-row twins. +fn build_one_row_roots( + program: &LfmProgram, + options: &ProofOptions, + groups: &[&ColumnGroup; 11], +) -> LfmOneRowRoots { + use stark::leaf_layout::LeafLayout::Row; + let mut roots: [Option; NUM_LFM_CHIPS] = [None; NUM_LFM_CHIPS]; + let commits = map_maybe_parallel(groups, |g| { + super::commit::commit_group_device_or_host_with(PREP_GROUP_LABEL, g, options, Row) + }); + for (slot, root) in commits.into_iter().enumerate() { + roots[slot] = Some(root); + } + let chunks: Vec = (0..blake3_chunk_rows(program).len()).collect(); + let blake3_chunk_roots = map_maybe_parallel(&chunks, |c| { + let group = program.blake3_chunk_group(*c); + super::commit::commit_group_device_or_host_with(BLAKE3_CHUNK_LABEL, &group, options, Row) + }); + roots[BLAKE3_SLOT] = blake3_chunk_roots.first().copied(); + roots[13] = keccak_rc::preprocessed_commitment_for(options, Row); + roots[14] = bitwise::preprocessed_commitment_for(options, Row); + LfmOneRowRoots { + roots, + blake3_chunk_roots, } } @@ -547,6 +649,8 @@ pub fn resolve( kind: LfmProgramKind, blowup_factor: u8, ) -> Result<&'static LfmRegistryEntry, LfmRegistryError> { + #[cfg(test)] + REGISTRY_READS.with(|c| c.set(c.get() + 1)); let mut matches = LFM_REGISTRY .iter() .filter(|e| e.kind == kind && e.blowup_factor == blowup_factor); diff --git a/prover/src/lfm/sub_proof.rs b/prover/src/lfm/sub_proof.rs index d11cce3c6..c5c7bbffc 100644 --- a/prover/src/lfm/sub_proof.rs +++ b/prover/src/lfm/sub_proof.rs @@ -50,18 +50,33 @@ //! `Mul` per bit against program constants, via [`super::edsl::pow_bits`]. The //! symmetric point is `−υ`: `br(2·iota+1) = br(2·iota) + L/2` and `g^{L/2} = //! −1`, so it costs one subtraction rather than a second derivation. +//! +//! # One-row leaves (S2) +//! +//! Under [`SubProofShape::layout`] = `LeafLayout::Row` every committed matrix +//! holds ONE row per leaf: a query index `r` has `log2(lde)` bits (uniform over +//! the whole LDE, not a pair index), every tree is `log2(lde)` deep, a group's +//! opening is `num_columns` cells (no symmetric row), the point is +//! `x_r = offset · g^{br(r)}` alone ([`emit_point_from_row_bits`]), and DEEP +//! is evaluated ONCE. The FRI leg then starts at the committed DEEP codeword +//! (the input tree) with the input-slot check `group₀[slot] == DEEP(x_r)` +//! (`super::fri::emit_query_fri`). use math::field::traits::IsFFTField; use crate::tables::types::{FE, GoldilocksField}; +use stark::leaf_layout::LeafLayout; + use super::builder::{Bit, Cell, Ext, Felt, LfmBuilder}; use super::deep::{DeepInvariants, DeepOpening, DeepShape, emit_deep_point}; use super::edsl::{self, WrapDigest}; +use super::merkle_cap::CapCells; -/// Rows a Merkle leaf covers — `crypto/stark`'s `ROWS_PER_LEAF`, mirrored here -/// because it fixes program shape: a leaf holds a row PAIR, which is why one -/// path authenticates both of a query's two points. +/// Rows a Merkle leaf covers at today's layout — `crypto/stark`'s +/// `ROWS_PER_LEAF`, mirrored here because it fixes program shape: a leaf holds +/// a row PAIR, which is why one path authenticates both of a query's two +/// points. One-row leaves (S2) are [`SubProofShape::layout`]'s other value. pub const ROWS_PER_LEAF: usize = 2; /// The compile-time shape of one committed matrix of a sub-proof. @@ -79,14 +94,27 @@ pub struct GroupShape { } impl GroupShape { - /// Cells one query's opening of this group occupies — both points. + /// Cells one query's opening of this group occupies under row-pair + /// leaves — both points. [`Self::values_at`] is the layout-generic form. pub fn num_values(&self) -> usize { - ROWS_PER_LEAF * self.num_columns + self.values_at(ROWS_PER_LEAF) } - /// Bytes the leaf hash covers. + /// Cells one query's opening of this group occupies when a leaf holds + /// `rows_per_leaf` rows: `2·num_columns` for row pairs, `num_columns` + /// under one-row leaves (S2). + pub fn values_at(&self, rows_per_leaf: usize) -> usize { + rows_per_leaf * self.num_columns + } + + /// Bytes the row-pair leaf hash covers. pub fn leaf_bytes(&self) -> usize { - self.num_values() * if self.is_ext { 24 } else { 8 } + self.leaf_bytes_at(ROWS_PER_LEAF) + } + + /// Bytes the leaf hash covers at `rows_per_leaf` rows per leaf. + pub fn leaf_bytes_at(&self, rows_per_leaf: usize) -> usize { + self.values_at(rows_per_leaf) * if self.is_ext { 24 } else { 8 } } } @@ -104,17 +132,43 @@ pub struct SubProofShape { /// aux. Absent groups are omitted, exactly as the proof omits them. Their /// widths must sum to `deep.num_total_cols`. pub trace_groups: Vec, - /// Merkle depth — `log2(lde_length) − 1`, since a leaf is a row pair. All - /// four trees commit over the same LDE domain, so one depth serves them - /// all and one index addresses them all. + /// Merkle depth — `log2(lde_length) − 1` when a leaf is a row pair, + /// `log2(lde_length)` under one-row leaves ([`Self::layout`]). All four + /// trees commit over the same LDE domain at the same layout, so one depth + /// serves them all and one index addresses them all. pub merkle_depth: usize, /// `log2` of the LDE domain — `log2_trace_length + log2(blowup)`. pub log2_lde_length: u32, /// The LDE coset offset, `ProofOptions::coset_offset`. pub coset_offset: FE, + /// The Merkle-cap height of every committed matrix's tree (they share a + /// depth and an opening count, so one height): `0` = uncapped, today's + /// format. With `c > 0` each tree's `2^c` cap digests are hinted ONCE per + /// sub-proof and authenticated against the root ([`CapCells`]), and every + /// query's path stops `c` levels short. A verifier + /// constant: `CapPolicy::height(num_queries, merkle_depth)` of the inner + /// proof's options, never read from the proof. + pub trace_cap: usize, + /// The trace trees' leaf layout (S2): today's row + /// pairs, or one row per leaf. A verifier constant — the table's + /// `stark::leaf_layout::table_leaf_layout`, resolved from the AIR's + /// widths and the trace length, never read from the proof. Under + /// [`LeafLayout::Row`] a query opens ONE row per tree, its index ranges + /// over the whole LDE, and DEEP is evaluated at the one point `x_r`. + pub layout: LeafLayout, } impl SubProofShape { + /// Rows one leaf of every committed matrix holds (2, or 1 under S2). + pub fn rows_per_leaf(&self) -> usize { + self.layout.rows_per_leaf() + } + + /// Cells one query's opening of `g` occupies at this shape's layout. + pub fn group_values(&self, g: &GroupShape) -> usize { + g.values_at(self.rows_per_leaf()) + } + /// The composition-parts group. Its width is the part count and its /// elements are extension, both of which are already DEEP shape. pub fn parts_group(&self) -> GroupShape { @@ -153,25 +207,55 @@ impl SubProofShape { /// values and the paths. An arena that still carried an index would be /// offering the prover a second one. pub fn opening_words(&self, digest_words: usize) -> usize { - let values: usize = self.groups().iter().map(GroupShape::num_values).sum(); - let siblings = digest_words * self.merkle_depth * self.groups().len(); + let values: usize = self.groups().iter().map(|g| self.group_values(g)).sum(); + let siblings = digest_words * self.path_len() * self.groups().len(); values + siblings } + /// Siblings one query's path carries per group: the tree's depth less its + /// cap height (the owner path's cap is split off into the caps arena). + pub fn path_len(&self) -> usize { + self.merkle_depth - self.trace_cap + } + + /// Arena words the committed matrices' caps occupy, once per sub-proof: + /// `2^c` digests per group when capped, nothing otherwise. + pub fn cap_words(&self, digest_words: usize) -> usize { + if self.trace_cap == 0 { + 0 + } else { + self.groups().len() * (1usize << self.trace_cap) * digest_words + } + } + + /// Permutations the committed matrices' cap checks cost, once per + /// sub-proof: `2^c − 1` parents per group (nothing uncapped). + pub fn cap_permutations(&self) -> usize { + self.groups().len() * super::merkle_cap::cap_root_permutations(self.trace_cap) + } + /// Checked invariants of a shape, so a caller cannot assemble one whose /// groups do not cover the fold. fn check(&self) { + assert!( + self.trace_cap <= self.merkle_depth, + "a cap is at most the tree: height {} over depth {}", + self.trace_cap, + self.merkle_depth + ); let width: usize = self.trace_groups.iter().map(|g| g.num_columns).sum(); assert_eq!( width, self.deep.num_total_cols, "the trace groups must cover exactly the DEEP column set" ); - assert!( - self.merkle_depth + 1 == self.log2_lde_length as usize, - "a leaf is a row pair, so the tree is one level shallower than the \ - LDE domain: depth {} against log2(lde) {}", + assert_eq!( + self.merkle_depth, + self.layout.tree_depth(self.log2_lde_length as usize), + "a row-pair tree is one level shallower than the LDE domain, a \ + one-row tree is as deep as it: depth {} against log2(lde) {} at {:?}", self.merkle_depth, - self.log2_lde_length + self.log2_lde_length, + self.layout ); // ⚠ NO `merkle_depth >= 1`. A ONE-PAIR domain — a one-row trace at blowup // 2 — has a single leaf, so the tree has no levels and the LEAF HASH IS @@ -209,6 +293,10 @@ pub struct GroupCommitment { /// and asserts the widths agree, so it works at either width unchanged. pub root_lanes: Vec<[Felt; 4]>, pub shape: GroupShape, + /// The tree's authenticated Merkle cap, when the format caps it: every + /// query's opening is then checked against THESE cells + /// ([`CapCells::verify_path`]) instead of the root lanes. `None` = today. + pub cap: Option, } impl GroupCommitment { @@ -229,7 +317,11 @@ impl GroupCommitment { b.unpack(w) }) .collect(); - GroupCommitment { root_lanes, shape } + GroupCommitment { + root_lanes, + shape, + cap: None, + } } /// A commitment over lanes the caller already holds — the assembled @@ -244,7 +336,36 @@ impl GroupCommitment { /// join, and it takes lanes rather than words precisely so there is nothing /// left to hint. pub fn from_lanes(root_lanes: Vec<[Felt; 4]>, shape: GroupShape) -> Self { - GroupCommitment { root_lanes, shape } + GroupCommitment { + root_lanes, + shape, + cap: None, + } + } + + /// Hint this tree's height-`c` cap out of `arena` at `base`, authenticate + /// it against the root lanes (once per tree), and check every later + /// opening against it. Returns the next free word. `c = 0` hints nothing + /// and leaves the root check in place. + pub fn hint_cap( + &mut self, + b: &mut LfmBuilder, + arena: super::instr::ArenaId, + base: u32, + c: usize, + ) -> u32 { + if c == 0 { + return base; + } + let (cap, next) = + super::merkle_cap::hint_and_authenticate(b, arena, base, c, &self.root_lanes); + self.cap = Some(cap); + next + } + + /// The cap height openings of this tree are checked at (0 = the root). + pub fn cap_height(&self) -> usize { + self.cap.as_ref().map_or(0, CapCells::height) } } @@ -277,13 +398,27 @@ pub struct GroupOpening { /// caller that authenticated an extension group WITHOUT folding it would owe /// that check itself. pub fn emit_leaf_hash(b: &mut LfmBuilder, shape: GroupShape, values: &[Cell]) -> WrapDigest { + emit_leaf_hash_rows(b, shape, ROWS_PER_LEAF, values) +} + +/// [`emit_leaf_hash`] for a leaf of `rows_per_leaf` rows: the same stream +/// (every value's felts in the order given — row-major across the leaf's +/// rows, `hash_data_from_slices(evaluations, evaluations_sym)` on the host), +/// sized by the layout. At `rows_per_leaf = 2` it IS [`emit_leaf_hash`], +/// instruction for instruction. +pub fn emit_leaf_hash_rows( + b: &mut LfmBuilder, + shape: GroupShape, + rows_per_leaf: usize, + values: &[Cell], +) -> WrapDigest { use super::keccak_host::BYTES_PER_HALF; use super::transcript_replay::felt_be_halves; assert_eq!( values.len(), - shape.num_values(), - "a leaf covers the whole row pair" + shape.values_at(rows_per_leaf), + "a leaf covers the whole row pair (or the one row)" ); if !shape.is_ext { let felts: Vec = values.iter().map(|c| Felt(c.addr())).collect(); @@ -313,7 +448,7 @@ pub fn emit_leaf_hash(b: &mut LfmBuilder, shape: GroupShape, values: &[Cell]) -> } } let len_bytes = BYTES_PER_HALF * stream.len(); - debug_assert_eq!(len_bytes, shape.leaf_bytes()); + debug_assert_eq!(len_bytes, shape.leaf_bytes_at(rows_per_leaf)); edsl::wrap_hash_bytes(b, byte_hash, &stream, len_bytes) } @@ -328,25 +463,71 @@ pub fn emit_group_authentication( commitment: &GroupCommitment, opening: &GroupOpening, bits: &[Bit], +) { + emit_group_authentication_at(b, commitment, ROWS_PER_LEAF, opening, bits); +} + +/// [`emit_group_authentication`] for a leaf of `rows_per_leaf` rows (the +/// sub-proof's [`SubProofShape::rows_per_leaf`]). +pub fn emit_group_authentication_at( + b: &mut LfmBuilder, + commitment: &GroupCommitment, + rows_per_leaf: usize, + opening: &GroupOpening, + bits: &[Bit], ) { assert_eq!( - opening.siblings.len(), + opening.siblings.len() + commitment.cap_height(), bits.len(), - "one sibling per level, and every group walks the same index" + "one sibling per level below the cap, and every group walks the same index" ); - let leaf = emit_leaf_hash(b, commitment.shape, &opening.values); - let root = edsl::wrap_merkle_walk(b, leaf, bits, &opening.siblings); - edsl::assert_digest_eq_lanes(b, root, &commitment.root_lanes); + let leaf = emit_leaf_hash_rows(b, commitment.shape, rows_per_leaf, &opening.values); + match &commitment.cap { + None => { + let root = edsl::wrap_merkle_walk(b, leaf, bits, &opening.siblings); + edsl::assert_digest_eq_lanes(b, root, &commitment.root_lanes); + } + // The whole index goes in; the cap splits it (walk low, mux top). + Some(cap) => cap.verify_path(b, leaf, bits, &opening.siblings), + } } -/// The LDE-domain constants the point derivation multiplies together: -/// `factors[i] = g^{2^{depth-1-i}}`, matching index bit `i`'s weight after the -/// bit reversal. -fn point_factors(log2_lde_length: u32) -> Vec { +/// The LDE-domain constants the point derivation multiplies together for an +/// index of `nbits` bits: `factors[i] = g^{2^{nbits-1-i}}`, matching index bit +/// `i`'s weight after the bit reversal. +/// +/// Row pairs: the index `ι` has `nbits = log2(lde) − 1` bits and the point is +/// at bit-reversed position `2ι`, so bit `i` of `ι` is bit `i + 1` of `2ι`, +/// weight `2^{log2(lde)−2−i} = 2^{nbits−1−i}`. One row: the index `r` has +/// `nbits = log2(lde)` bits and the point is at position `r` itself, weight +/// `2^{log2(lde)−1−i} = 2^{nbits−1−i}`. One formula, keyed on the bit count. +fn point_factors(log2_lde_length: u32, nbits: usize) -> Vec { let g = ::get_primitive_root_of_unity(log2_lde_length as u64) .expect("a power-of-two LDE length has a root of unity"); - let depth = log2_lde_length as usize - 1; - (0..depth).map(|i| g.pow(1u64 << (depth - 1 - i))).collect() + (0..nbits).map(|i| g.pow(1u64 << (nbits - 1 - i))).collect() +} + +/// `x_r` — the LDE point at bit-reversed position `r` — from the ONE-ROW +/// query index bits (`log2(lde)` of them, S2; `r` uniform over the whole LDE). +/// The one-row counterpart of [`emit_points_from_bits`]: no symmetric point, +/// because a one-row leaf holds one point. +pub fn emit_point_from_row_bits( + b: &mut LfmBuilder, + log2_lde_length: u32, + coset_offset: FE, + bits: &[Bit], +) -> Felt { + assert_eq!( + bits.len(), + log2_lde_length as usize, + "a one-row index ranges over the whole LDE domain" + ); + edsl::pow_bits( + b, + bits, + &point_factors(log2_lde_length, bits.len()), + coset_offset, + ) } /// `(υ, −υ)` from the query index bits, for the LDE domain given by its size and @@ -370,14 +551,24 @@ pub fn emit_points_from_bits( log2_lde_length as usize - 1, "a leaf is a row pair, so the index is one bit narrower than the domain" ); - let point = edsl::pow_bits(b, bits, &point_factors(log2_lde_length), coset_offset); + let point = edsl::pow_bits( + b, + bits, + &point_factors(log2_lde_length, bits.len()), + coset_offset, + ); let zero = b.felt_const(FE::zero()); (point, b.sub(zero, point)) } -/// `(υ, −υ)` from the query index bits. +/// `(υ, −υ)` from the query index bits (row-pair shapes). pub fn emit_query_points(b: &mut LfmBuilder, shape: &SubProofShape, bits: &[Bit]) -> (Felt, Felt) { assert_eq!(bits.len(), shape.merkle_depth); + assert_eq!( + shape.layout, + LeafLayout::RowPair, + "a one-row query has one point (emit_point_from_row_bits)" + ); emit_points_from_bits(b, shape.log2_lde_length, shape.coset_offset, bits) } @@ -398,13 +589,22 @@ pub fn emit_query( index: Felt, openings: &[GroupOpening], ) -> (Ext, Ext) { - emit_query_with_bits(b, shape, gamma, inv, commitments, index, openings).deep + let out = emit_query_with_bits(b, shape, gamma, inv, commitments, index, openings); + ( + out.deep, + out.deep_sym + .expect("emit_query returns the DEEP pair: a row-pair shape"), + ) } /// What one query contributes when the caller needs more than the DEEP pair. pub struct QueryOutput { - /// `(DEEP(υ), DEEP(−υ))`. - pub deep: (Ext, Ext), + /// `DEEP(υ)` — or, under one-row leaves, `DEEP(x_r)`, the ONE point the + /// query opens. + pub deep: Ext, + /// `DEEP(−υ)` for a row-pair shape; `None` under one-row leaves (S2), + /// where the query opens no symmetric row and DEEP runs once. + pub deep_sym: Option, /// The query index decomposed low-to-high — the SAME cells the Merkle walk /// consumed and the query points were derived from. /// @@ -432,8 +632,9 @@ pub struct QueryOutput { pub point: Felt, /// `−υ`, likewise. The zero-fold FRI shape checks the terminal polynomial /// at both points (production's `zetas.is_empty()` branch tests - /// `terminal[2·iota]` AND `terminal[2·iota+1]`). - pub point_sym: Felt, + /// `terminal[2·iota]` AND `terminal[2·iota+1]`). `None` under one-row + /// leaves: there is no second point. + pub point_sym: Option, } /// [`emit_query`], additionally returning the index bits — see [`QueryOutput`]. @@ -484,6 +685,11 @@ pub fn emit_query_from_bits( assert_eq!(openings.len(), groups.len(), "one opening per group"); for (c, g) in commitments.iter().zip(&groups) { assert_eq!(c.shape, *g, "commitment shapes must match the sub-proof"); + assert_eq!( + c.cap_height(), + shape.trace_cap, + "every committed matrix is capped at the shape's height" + ); } assert_eq!( bits.len(), @@ -491,8 +697,13 @@ pub fn emit_query_from_bits( "a query index is exactly the tree's depth in bits" ); + let rows = shape.rows_per_leaf(); for (commitment, opening) in commitments.iter().zip(openings) { - emit_group_authentication(b, commitment, opening, &bits); + emit_group_authentication_at(b, commitment, rows, opening, &bits); + } + + if shape.layout.is_one_row() { + return emit_one_row_deep(b, shape, gamma, inv, openings, &groups, bits); } let (point, point_sym) = emit_query_points(b, shape, &bits); @@ -529,13 +740,47 @@ pub fn emit_query_from_bits( parts: parts_sym, }; QueryOutput { - deep: ( - emit_deep_point(b, &shape.deep, gamma, inv, ®ular), - emit_deep_point(b, &shape.deep, gamma, inv, &symmetric), - ), + deep: emit_deep_point(b, &shape.deep, gamma, inv, ®ular), + deep_sym: Some(emit_deep_point(b, &shape.deep, gamma, inv, &symmetric)), + bits, + point, + point_sym: Some(point_sym), + } +} + +/// The one-row half of [`emit_query_from_bits`] (S2), after +/// every group was authenticated at leaf `r`: `x_r` from the SAME bits, then +/// DEEP ONCE, over the authenticated cells — column `c` is `values[c]` (a +/// one-row leaf holds no symmetric row, so there is no `values[w + c]`). +fn emit_one_row_deep( + b: &mut LfmBuilder, + shape: &SubProofShape, + gamma: Ext, + inv: &DeepInvariants, + openings: &[GroupOpening], + groups: &[GroupShape], + bits: Vec, +) -> QueryOutput { + let point = emit_point_from_row_bits(b, shape.log2_lde_length, shape.coset_offset, &bits); + let mut trace = Vec::with_capacity(shape.deep.num_total_cols); + for (opening, g) in openings.iter().zip(groups).take(shape.trace_groups.len()) { + assert_eq!(opening.values.len(), g.num_columns, "one row per leaf"); + trace.extend(opening.values.iter().map(|v| v.as_ext())); + } + let parts_opening = openings.last().expect("the parts group is always present"); + let parts: Vec = parts_opening.values.iter().map(|v| v.as_ext()).collect(); + assert_eq!(parts.len(), shape.deep.num_composition_parts); + let at = DeepOpening { + point, + trace, + parts, + }; + QueryOutput { + deep: emit_deep_point(b, &shape.deep, gamma, inv, &at), + deep_sym: None, bits, point, - point_sym, + point_sym: None, } } @@ -560,21 +805,35 @@ pub struct SubProofArenas { /// Per query, in order: the index, then per group the row-pair values /// followed by the sibling digests (two words per level). pub queries: super::instr::ArenaId, + /// Per group, its `2^c` cap digests — declared only when the shape caps + /// the trees ([`SubProofShape::trace_cap`] `> 0`). + pub caps: Option, } /// Emit a whole sub-proof's query verification: the invariants once, then every /// query authenticated and folded. /// -/// Returns `(DEEP(υ), DEEP(−υ))` per query. The invariant hoist is the reason a -/// 219-query proof is affordable, and it is production's own hoist — the OOD -/// row sums and the block scalars do not depend on the query. +/// Returns `(DEEP(υ), DEEP(−υ))` per query (a row-pair shape). The invariant +/// hoist is the reason a 219-query proof is affordable, and it is production's +/// own hoist — the OOD row sums and the block scalars do not depend on the query. pub fn emit_sub_proof( b: &mut LfmBuilder, shape: &SubProofShape, num_queries: usize, ) -> (SubProofArenas, Vec<(Ext, Ext)>) { let (arenas, out) = emit_sub_proof_with_bits(b, shape, num_queries); - (arenas, out.into_iter().map(|q| q.deep).collect()) + ( + arenas, + out.into_iter() + .map(|q| { + ( + q.deep, + q.deep_sym + .expect("emit_sub_proof returns DEEP pairs: a row-pair shape"), + ) + }) + .collect(), + ) } /// [`emit_sub_proof`], additionally returning each query's index bits — see @@ -596,12 +855,15 @@ pub fn emit_sub_proof_with_bits( let roots = b.declare_arena(edsl::digest_words(b) * groups.len() as u32); let queries = b.declare_arena((num_queries * shape.query_words(edsl::digest_words(b) as usize)) as u32); + let cap_words = shape.cap_words(edsl::digest_words(b) as usize); + let caps = (cap_words > 0).then(|| b.declare_arena(cap_words as u32)); let arenas = SubProofArenas { uniforms, ood, parts, roots, queries, + caps, }; let gamma = b.hint_word(uniforms, 0).as_ext(); @@ -623,11 +885,18 @@ pub fn emit_sub_proof_with_bits( .map(|j| b.hint_word(parts, j).as_ext()) .collect(); - let commitments: Vec = groups + let mut commitments: Vec = groups .iter() .enumerate() .map(|(i, g)| GroupCommitment::hint(b, roots, edsl::digest_words(b) * i as u32, *g)) .collect(); + if let Some(caps) = caps { + let mut at = 0u32; + for c in &mut commitments { + at = c.hint_cap(b, caps, at, shape.trace_cap); + } + assert_eq!(at as usize, cap_words, "the caps arena is filled exactly"); + } let inv = emit_deep_invariants(b, &shape.deep, gamma, zeta, &ood_steps, &claimed_parts); @@ -639,14 +908,14 @@ pub fn emit_sub_proof_with_bits( let openings: Vec = groups .iter() .map(|g| { - let values: Vec = (0..g.num_values()) + let values: Vec = (0..shape.group_values(g)) .map(|_| { let c = b.hint_word(queries, cursor); cursor += 1; c }) .collect(); - let siblings: Vec = (0..shape.merkle_depth) + let siblings: Vec = (0..shape.path_len()) .map(|_| { // The stride follows the DIGEST's width, not a literal. let d = edsl::hint_digest(b, queries, cursor); diff --git a/prover/src/lfm/whir_chain.rs b/prover/src/lfm/whir_chain.rs index 6155d4ac0..e40bcd666 100644 --- a/prover/src/lfm/whir_chain.rs +++ b/prover/src/lfm/whir_chain.rs @@ -82,7 +82,8 @@ use super::builder::{Bit, Cell, Ext, Felt, LfmBuilder}; use super::edsl::WrapDigest; use super::whir_fold::{emit_fold_coset, fold_coset_rows}; use super::whir_open::{ - BlockValues, emit_verify_opening, verify_opening_perms, verify_opening_rows, + BlockValues, CapCells, TreeAuth, cap_check_perms, cap_check_rows, verify_opening_perms_capped, + verify_opening_rows_capped, }; use super::whir_poly::{ emit_eq_eval, emit_sumcheck_round, eq_eval_rows_again, sumcheck_round_rows, @@ -127,6 +128,12 @@ pub struct ChainRoundWires<'a> { /// Per query: the successor block holding the folded value. Empty on the /// last round. pub next: &'a [QueryOpening<'a>], + /// The current tree's Merkle cap, when this round OWNS it: round 0 with + /// `caps[0] > 0`. Empty otherwise (W1; a later round's current tree was + /// authenticated as the round before's successor). + pub current_cap: &'a [WrapDigest], + /// The successor tree's Merkle cap, when it has one (`caps[r + 1] > 0`). + pub next_cap: &'a [WrapDigest], } /// The shape of one chain: everything the closed forms below are a function of. @@ -143,6 +150,10 @@ pub struct ChainShape { pub num_vars: usize, pub num_queries: usize, pub grind: (usize, usize, usize), + /// Each tree's Merkle cap height (W1): tree `r` is round `r`'s current + /// tree. From the same `ChainConfig::tree_caps` the host prover and + /// verifier use; all zero at the default format. + pub caps: Vec, } impl ChainShape { @@ -154,11 +165,14 @@ impl ChainShape { domain_log.push(d); d -= k; } + let caps = config.tree_caps(num_vars); + debug_assert_eq!(caps.len(), schedule.len(), "one cap height per tree"); Self { schedule, domain_log, num_vars, num_queries: config.num_queries, + caps, grind: ( config.grind.folding as usize, config.grind.ood as usize, @@ -183,6 +197,23 @@ impl ChainShape { (r + 1 < self.rounds()).then(|| self.current_depth(r + 1)) } + /// Round `r`'s current tree's cap height. ⚠ [`current_depth`](Self::current_depth) + /// stays the index-bit count; the sibling count is + /// [`current_path`](Self::current_path). + pub fn current_cap(&self, r: usize) -> usize { + self.caps[r] + } + + /// Siblings on a path to round `r`'s current tree's cap. + pub fn current_path(&self, r: usize) -> usize { + self.current_depth(r) - self.caps[r] + } + + /// The successor tree's cap height at round `r`. + pub fn next_cap(&self, r: usize) -> Option { + (r + 1 < self.rounds()).then(|| self.caps[r + 1]) + } + /// Felts in round `r`'s current block: one per value in round 0, where the /// codeword is still base-field, and three after. pub fn current_felts(&self, r: usize) -> usize { @@ -208,12 +239,24 @@ impl ChainShape { pub fn chain_opening_perms(shape: &ChainShape) -> usize { let mut per_query = 0; for r in 0..shape.rounds() { - per_query += verify_opening_perms(shape.current_felts(r), shape.current_depth(r)); + per_query += verify_opening_perms_capped( + shape.current_felts(r), + shape.current_depth(r), + shape.caps[r], + ); if let Some(depth) = shape.next_depth(r) { - per_query += verify_opening_perms(3 << shape.schedule[r + 1], depth); + per_query += + verify_opening_perms_capped(3 << shape.schedule[r + 1], depth, shape.caps[r + 1]); } } - shape.num_queries * per_query + shape.num_queries * per_query + chain_cap_perms(shape) +} + +/// PERMUTATIONS the chain's cap checks cost: each capped tree's cap hashed up +/// to its root once, `2^c − 1` parents. Zero at the default. Part of +/// [`chain_opening_perms`], stated apart so the per-tree term is visible. +pub fn chain_cap_perms(shape: &ChainShape) -> usize { + shape.caps.iter().map(|&c| cap_check_perms(c)).sum() } /// INSTRUCTIONS one chain's query phase costs: per round, per query, the two @@ -230,14 +273,18 @@ pub fn chain_query_rows(shape: &ChainShape) -> usize { let depth = shape.current_depth(r); // The index draw, the current opening, and the fold. let mut q = 1 - + verify_opening_rows(felts, unpacks, depth) + + verify_opening_rows_capped(felts, unpacks, depth, shape.caps[r]) + fold_coset_rows(1usize << shape.schedule[r], depth); match shape.next_depth(r) { Some(next_depth) => { let next_block = 1usize << shape.schedule[r + 1]; // The successor opening, the slot mux, and `folded == claimed`. - q += verify_opening_rows(3 * next_block, next_block, next_depth) - + (next_block - 1) + q += verify_opening_rows_capped( + 3 * next_block, + next_block, + next_depth, + shape.caps[r + 1], + ) + (next_block - 1) + 2; } // `folded == final_value`. @@ -291,6 +338,9 @@ pub fn chain_fixed_rows(shape: &ChainShape) -> usize { rows += eq_eval_rows_again(shape.num_vars - shape.bound(r)) + 1; } rows += 1 + 1 + 2; + // W1: each capped tree's cap check, once (its hinted words are arena + // words, counted with the arena like every other hint). + rows += shape.caps.iter().map(|&c| cap_check_rows(c)).sum::(); rows } @@ -490,7 +540,9 @@ pub fn emit_verify_weighted( let mut claim = y; let mut alphas: Vec = Vec::with_capacity(shape.num_vars); - let mut current_root = *root_lanes; + // Tree 0: its cap (when it has one) is authenticated against the root + // here, once; every later tree where its root is absorbed. + let mut current_tree = tree_auth(b, shape.caps[0], rounds[0].current_cap, root_lanes); let mut current_domain = domain.clone(); // Each out-of-domain claim: its batching weight, its point, and how many // variables were bound when it entered. @@ -525,7 +577,7 @@ pub fn emit_verify_weighted( } let bound = alphas.len() + k; - let next_root_lanes = match (round.next_root, round.ood_value, shape.next_depth(r)) { + let next_tree = match (round.next_root, round.ood_value, shape.next_depth(r)) { (Some(next_root), Some(y0), Some(_)) => { let lanes = b.unpack(next_root); transcript.absorb_felts(b, &lanes); @@ -552,9 +604,10 @@ pub fn emit_verify_weighted( ood.push((gamma, ood_point, bound)); emit_grind_check(b, transcript, grind_query as u8, round.nonces.query); - Some(lanes) + Some(tree_auth(b, shape.caps[r + 1], round.next_cap, &lanes)) } (None, None, None) => { + assert!(round.next_cap.is_empty(), "the last round has no successor"); transcript.absorb_ext(b, final_value); emit_grind_check(b, transcript, grind_query as u8, round.nonces.query); None @@ -573,13 +626,13 @@ pub fn emit_verify_weighted( r, ¤t_domain, &point, - ¤t_root, - next_root_lanes.as_ref(), + ¤t_tree, + next_tree.as_ref(), final_value, ); - if let Some(lanes) = next_root_lanes { - current_root = lanes; + if let Some(tree) = next_tree { + current_tree = tree; } alphas.extend(point); current_domain = next_domain; @@ -596,6 +649,29 @@ pub fn emit_verify_weighted( emit_final_check(b, claim, weight, final_value, one); } +/// How a tree's openings are checked: against its root lanes when its cap +/// height is 0 (today's emission, unchanged), else against its cap, hinted as +/// `cap` and authenticated against the root lanes HERE — the one place a +/// tree's [`CapCells`] are made. +fn tree_auth( + b: &mut LfmBuilder, + cap_height: usize, + cap: &[WrapDigest], + root_lanes: &[Felt; 4], +) -> TreeAuth { + if cap_height == 0 { + assert!(cap.is_empty(), "an uncapped tree carries no cap wires"); + TreeAuth::Root(*root_lanes) + } else { + assert_eq!(cap.len(), 1usize << cap_height, "a cap is 2^c digests"); + TreeAuth::Cap(CapCells::authenticate( + b, + cap, + std::slice::from_ref(root_lanes), + )) + } +} + /// ★ `require_out_of_domain` (`whir_chain.rs:88-101`), emitted as a REFUSAL. /// /// The host rejects `z0` whose `2^log_size`-th power is one, because such a @@ -647,29 +723,30 @@ fn emit_query_phase( r: usize, current_domain: &Domain, alphas: &[Ext], - current_root: &[Felt; 4], - next_root: Option<&[Felt; 4]>, + current_tree: &TreeAuth, + next_tree: Option<&TreeAuth>, final_value: Ext, ) { let depth = shape.current_depth(r); + debug_assert_eq!(current_tree.cap_height(), shape.caps[r]); assert_eq!(round.current.len(), shape.num_queries); let queries: Vec> = (0..shape.num_queries) .map(|_| transcript.sample_u64_pow2(b, depth)) .collect(); - match (next_root, shape.next_depth(r)) { - (Some(next_lanes), Some(next_depth)) => { + match (next_tree, shape.next_depth(r)) { + (Some(next_tree), Some(next_depth)) => { let next_block = 1usize << shape.schedule[r + 1]; assert_eq!(round.next.len(), shape.num_queries); for (q, bits) in queries.iter().enumerate() { let current = &round.current[q]; let next = &round.next[q]; - emit_verify_opening(b, current.values, bits, current.siblings, current_root); + current_tree.verify_opening(b, current.values, bits, current.siblings); // `leaf_and_slot`: the low `next_depth` bits index the successor // leaf and the high ones choose the slot inside it. Both bounds // are powers of two, so this is a partition of the bits. let (leaf_bits, slot_bits) = bits.split_at(next_depth); - emit_verify_opening(b, next.values, leaf_bits, next.siblings, next_lanes); + next_tree.verify_opening(b, next.values, leaf_bits, next.siblings); let folded = emit_fold_coset(b, &block_ext(current.values), current_domain, bits, alphas); @@ -681,7 +758,7 @@ fn emit_query_phase( _ => { for (q, bits) in queries.iter().enumerate() { let current = &round.current[q]; - emit_verify_opening(b, current.values, bits, current.siblings, current_root); + current_tree.verify_opening(b, current.values, bits, current.siblings); let folded = emit_fold_coset(b, &block_ext(current.values), current_domain, bits, alphas); b.assert_eq_ext(folded, final_value); @@ -783,6 +860,10 @@ pub struct RoundStorage { roots: Vec>, oods: Vec>, nonces: Vec, + /// Per round: the current tree's cap when the round owns it (round 0), + /// and the successor's cap. Empty where the tree is uncapped. + current_caps: Vec>, + next_caps: Vec>, } impl RoundStorage { @@ -806,6 +887,8 @@ impl RoundStorage { let mut roots: Vec> = Vec::new(); let mut oods: Vec> = Vec::new(); let mut nonces: Vec = Vec::new(); + let mut current_caps: Vec> = Vec::new(); + let mut next_caps: Vec> = Vec::new(); let mut at = base; for r in 0..shape.rounds() { @@ -825,7 +908,16 @@ impl RoundStorage { let query = b.hint_felt(arena, at + 2); at += 3; - let depth = shape.current_depth(r); + // W1: tree 0's cap, right after round 0's nonces (the words the + // owner path carried after its siblings). + let current_cap: Vec = if r == 0 { + (0..cap_words(shape.caps[0])) + .map(|_| WrapDigest::from_cell(next_word(b, &mut at))) + .collect() + } else { + Vec::new() + }; + let depth = shape.current_path(r); let block = 1usize << k; // ★ ROUND 0's current codeword is BASE on the host // (`whir_chain.rs:983`), so its block hashes ONE felt a value and @@ -855,10 +947,15 @@ impl RoundStorage { }) .collect(); - let (next_root, ood_value, next) = match shape.next_depth(r) { + let (next_root, ood_value, next, next_cap) = match shape.next_depth(r) { Some(next_depth) => { let nr = next_word(b, &mut at); let ov = next_word(b, &mut at).as_ext(); + // W1: the successor's cap, after its root and ood value. + let next_cap: Vec = (0..cap_words(shape.caps[r + 1])) + .map(|_| WrapDigest::from_cell(next_word(b, &mut at))) + .collect(); + let next_depth = next_depth - shape.caps[r + 1]; let next_block = 1usize << shape.schedule[r + 1]; let next: Vec<(Vec, Vec)> = (0..shape.num_queries) .map(|_| { @@ -871,9 +968,9 @@ impl RoundStorage { (values, path) }) .collect(); - (Some(nr), Some(ov), next) + (Some(nr), Some(ov), next, next_cap) } - None => (None, None, Vec::new()), + None => (None, None, Vec::new(), Vec::new()), }; sumchecks.push(sumcheck); @@ -886,6 +983,8 @@ impl RoundStorage { ood: ood_nonce, query, }); + current_caps.push(current_cap); + next_caps.push(next_cap); } assert_eq!( at - base, @@ -901,6 +1000,8 @@ impl RoundStorage { roots, oods, nonces, + current_caps, + next_caps, } } @@ -951,6 +1052,8 @@ impl RoundStorage { nonces: self.nonces[r], current: ¤t[r], next: &next[r], + current_cap: &self.current_caps[r], + next_cap: &self.next_caps[r], }) .collect() } @@ -959,21 +1062,31 @@ impl RoundStorage { pub fn round_words(shape: &ChainShape, r: usize) -> u32 { let k = shape.schedule[r]; // The sumcheck's two evaluations a round, three nonces, and per query - // the current block plus its path. + // the current block plus its path (to the cap, when the tree has one). let mut n = (2 * k + 3) as u32; - let depth = shape.current_depth(r); + let depth = shape.current_path(r); let block = 1usize << k; n += (shape.num_queries * (block + depth)) as u32; + if r == 0 { + // W1: tree 0's cap words. + n += cap_words(shape.caps[0]) as u32; + } if let Some(next_depth) = shape.next_depth(r) { - // The successor root, its out-of-domain value, and per query its - // block and path. - n += 2; + // The successor root, its out-of-domain value, its cap, and per query + // its block and path. + n += 2 + cap_words(shape.caps[r + 1]) as u32; let next_block = 1usize << shape.schedule[r + 1]; - n += (shape.num_queries * (next_block + next_depth)) as u32; + n += (shape.num_queries * (next_block + next_depth - shape.caps[r + 1])) as u32; } n } +/// Words a tree's cap occupies in the arena: `2^c` one-word digests, none at +/// `c = 0` (an uncapped tree is checked against its root). +fn cap_words(cap: usize) -> usize { + if cap == 0 { 0 } else { 1usize << cap } +} + /// One chain's round wires, in the order [`RoundStorage::hint`] reads them. /// /// Split out of [`chain_arena`] for the same reason [`RoundStorage`] was split @@ -994,7 +1107,10 @@ pub fn push_round_words( for nonce in [round.nonces.folding, round.nonces.ood, round.nonces.query] { words.push([FE::from(nonce), FE::zero(), FE::zero(), FE::zero()]); } - push_openings(words, round, true); + // W1: round 0 owns tree 0, whose cap rides at the end of its first + // current path; it goes to the arena here and the path goes without it. + let current_cap = if r == 0 { cap_words(shape.caps[0]) } else { 0 }; + push_openings(words, round, true, current_cap); if shape.next_depth(r).is_some() { words.push(commitment_to_digest( round.next_root.as_ref().expect("a successor root"), @@ -1002,50 +1118,68 @@ pub fn push_round_words( words.push(ext_word( round.ood_value.as_ref().expect("an out-of-domain value"), )); - push_openings(words, round, false); + push_openings(words, round, false, cap_words(shape.caps[r + 1])); } } } /// One round's query openings, current or successor, block then path. +/// +/// `owner_cap` is the number of cap words the side's FIRST opening carries at +/// the end of its path (the owner-path encoding, W1): they are written first, +/// ahead of every block, and the path is written without them. Zero at the +/// default. A malformed path is not repaired here: its words are written as +/// they are, and the arena's length (a verifier constant) refuses it. fn push_openings( words: &mut Vec, round: &ChainRound, current: bool, + owner_cap: usize, ) { + fn side( + words: &mut Vec, + openings: &[multilinear::whir_commit::CosetOpening], + owner_cap: usize, + value_word: impl Fn(&math::field::element::FieldElement) -> LfmWord, + ) where + V: math::field::traits::IsField, + { + let owner_split = openings + .first() + .map_or(0, |o| o.proof.merkle_path.len().saturating_sub(owner_cap)); + if let Some(owner) = openings.first() { + for node in &owner.proof.merkle_path[owner_split..] { + words.push(commitment_to_digest(node)); + } + } + for (i, opening) in openings.iter().enumerate() { + for v in &opening.values { + words.push(value_word(v)); + } + let path = if i == 0 { + &opening.proof.merkle_path[..owner_split] + } else { + &opening.proof.merkle_path[..] + }; + for node in path { + words.push(commitment_to_digest(node)); + } + } + } match &round.openings { RoundOpenings::Base(p) => { if current { - for opening in &p.current { - // A base value arrives as `(v, 0, 0, 0)`. - for v in &opening.values { - words.push([*v, FE::zero(), FE::zero(), FE::zero()]); - } - for node in &opening.proof.merkle_path { - words.push(commitment_to_digest(node)); - } - } + // A base value arrives as `(v, 0, 0, 0)`. + side(words, &p.current, owner_cap, |v| { + [*v, FE::zero(), FE::zero(), FE::zero()] + }); } else { - for opening in &p.next { - for v in &opening.values { - words.push(ext_word(v)); - } - for node in &opening.proof.merkle_path { - words.push(commitment_to_digest(node)); - } - } + side(words, &p.next, owner_cap, ext_word); } } RoundOpenings::Extension(p) => { - let side = if current { &p.current } else { &p.next }; - for opening in side { - for v in &opening.values { - words.push(ext_word(v)); - } - for node in &opening.proof.merkle_path { - words.push(commitment_to_digest(node)); - } - } + let openings = if current { &p.current } else { &p.next }; + side(words, openings, owner_cap, ext_word); } } } diff --git a/prover/src/lfm/whir_chain_tests.rs b/prover/src/lfm/whir_chain_tests.rs index 0d929582f..ba5854d12 100644 --- a/prover/src/lfm/whir_chain_tests.rs +++ b/prover/src/lfm/whir_chain_tests.rs @@ -32,7 +32,8 @@ use math::traits::AsBytes; use multilinear::mle::Mle; use multilinear::whir::Domain; use multilinear::whir_chain::{ - ChainConfig, ChainProof, GrindBits, RoundOpenings, commit, prove, verify, + CapPolicy, ChainConfig, ChainFormat, ChainProof, FirstFold, GrindBits, RoundOpenings, + WhirFolds, commit, prove, verify, }; use multilinear::whir_hash::RpxWhir; @@ -44,9 +45,9 @@ use super::compiler::{LfmProgram, compile}; use super::executor::execute; use super::validator::validate; use super::whir_chain::{ - ChainShape, RoundStorage, chain_grind_perms, chain_hash_schedule, chain_opening_perms, - chain_perms, chain_rows, chain_schedule_perms, chain_schedule_rows, chain_shape_rows, - emit_verify_weighted, push_round_words, round_words, + ChainShape, RoundStorage, chain_cap_perms, chain_grind_perms, chain_hash_schedule, + chain_opening_perms, chain_perms, chain_rows, chain_schedule_perms, chain_schedule_rows, + chain_shape_rows, emit_verify_weighted, push_round_words, round_words, }; use super::whir_poly::{emit_eq_eval, eq_eval_rows_again}; use super::whir_transcript::{SpongeEntry, SpongeHash, WhirTranscript}; @@ -260,11 +261,31 @@ fn point(num_vars: usize, seed: u64) -> Vec { } fn config(num_queries: usize, grind: u8) -> ChainConfig { + config_with(num_queries, grind, CapPolicy::Off) +} + +/// [`config`] under a Merkle cap policy (W1). +fn config_with(num_queries: usize, grind: u8, cap: CapPolicy) -> ChainConfig { ChainConfig { log_blowup: 2, log_folding: 4, num_queries, grind: GrindBits::uniform(grind), + format: multilinear::whir_chain::ChainFormat { + cap, + ..multilinear::whir_chain::ChainFormat::DEFAULT + }, + } +} + +/// [`config`] under a first fold of `k0` (W2's `first5`/`first6` at `k0` = 5, 6). +fn first_fold_config(num_queries: usize, grind: u8, k0: usize) -> ChainConfig { + ChainConfig { + format: ChainFormat { + folds: WhirFolds::First(FirstFold::new(k0).expect("a tested first fold")), + ..ChainFormat::DEFAULT + }, + ..config(num_queries, grind) } } @@ -291,17 +312,26 @@ struct Fixture { /// replay reproduces that hash and no other, so a fixture on the default /// transcript would be a fixture of a different protocol. fn fixture(num_vars: usize, num_queries: usize, grind: u8) -> Fixture { - let cfg = config(num_queries, grind); + fixture_with(&config(num_queries, grind), num_vars) +} + +/// [`fixture`] under a Merkle cap policy (W1). +fn fixture_capped(num_vars: usize, num_queries: usize, grind: u8, cap: CapPolicy) -> Fixture { + fixture_with(&config_with(num_queries, grind, cap), num_vars) +} + +/// [`fixture`] under any config — the knob-on shapes use it. +fn fixture_with(cfg: &ChainConfig, num_vars: usize) -> Fixture { + let num_queries = cfg.num_queries; let f = pseudo_mle(num_vars, 11); let z = point(num_vars, 0); // `evaluate_in`, not `evaluate`: the claimed point is in the cubic // extension, which is where every WHIR challenge lives. let y = f.evaluate_in::(&z).expect("f takes its own point"); - let (commitment, domain) = - commit::(&f, &cfg, true).expect("the polynomial commits"); + let (commitment, domain) = commit::(&f, cfg, true).expect("the polynomial commits"); let mut proving = HostTranscript::new(&[]); - let proof = prove::(&f, &z, &commitment, &domain, &cfg, &mut proving) + let proof = prove::(&f, &z, &commitment, &domain, cfg, &mut proving) .expect("the chain proves"); let mut recorded = Recording::new(); @@ -311,7 +341,7 @@ fn fixture(num_vars: usize, num_queries: usize, grind: u8) -> Fixture { &z, y, &domain, - &cfg, + cfg, &mut recorded, ) .expect("the control proof must verify"); @@ -320,7 +350,7 @@ fn fixture(num_vars: usize, num_queries: usize, grind: u8) -> Fixture { // challenge per sumcheck round, `z0` and `gamma` on every round but the // last, and `Q` bounded draws a round. Derived from the schedule, not read // off the recorder. - let shape = ChainShape::new(&cfg, num_vars); + let shape = ChainShape::new(cfg, num_vars); let rounds = shape.rounds(); assert_eq!( recorded.sampled.len(), @@ -734,6 +764,36 @@ fn the_refusals_a_real_proof_cannot_reach() { const COST_SHAPES: [(usize, usize, u8); 5] = [(6, 3, 0), (6, 5, 0), (5, 3, 0), (6, 3, 8), (9, 3, 8)]; +/// ★ The knob-on shapes (W2): `(num_vars, num_queries, grind, k0)`. +/// +/// `S = 9` under `first6` is `[6, 3]` and `S = 11` under `first5` is +/// `[5, 4, 2]`, each at grind 0 and 8 for the reason +/// [`COST_SHAPES`] gives. `S = 6` under `first6` is the one-round chain whose +/// only block is 64 base values, and `S = 7` is `[6, 1]`, a 64-wide base block +/// folded into a 2-wide extension tail. +const KNOB_COST_SHAPES: [(usize, usize, u8, usize); 6] = [ + (9, 3, 0, 6), + (9, 3, 8, 6), + (11, 3, 0, 5), + (11, 3, 8, 5), + (6, 3, 8, 6), + (7, 3, 8, 6), +]; + +/// Every shape the cost forms are gated at: [`COST_SHAPES`] under today's +/// schedule, then [`KNOB_COST_SHAPES`] under their first folds. +fn cost_configs() -> Vec<(ChainConfig, usize)> { + COST_SHAPES + .iter() + .map(|&(n, q, g)| (config(q, g), n)) + .chain( + KNOB_COST_SHAPES + .iter() + .map(|&(n, q, g, k0)| (first_fold_config(q, g, k0), n)), + ) + .collect() +} + pub(super) fn count_rows(program: &LfmProgram, want: fn(&super::instr::Instr) -> bool) -> usize { program.instrs.iter().filter(|instr| want(instr)).count() } @@ -781,8 +841,9 @@ fn chain_plumbing(shape: &ChainShape) -> usize { /// hash, which is a running quantity and not a shape. #[test] fn the_schedule_is_the_host_transcripts() { - for (num_vars, num_queries, grind) in COST_SHAPES { - let f = fixture(num_vars, num_queries, grind); + for (cfg, num_vars) in cost_configs() { + let (num_queries, grind) = (cfg.num_queries, cfg.grind.query); + let f = fixture_with(&cfg, num_vars); let host = f.recorded.duplex.borrow().hashes.clone(); let mine = chain_hash_schedule(&f.shape, SpongeEntry::fresh()); @@ -792,8 +853,9 @@ fn the_schedule_is_the_host_transcripts() { .count(); let states = mine.len() - squeezes; println!( - "schedule S={num_vars} Q={num_queries} grind={grind}: {squeezes} squeezes, \ + "schedule S={num_vars} Q={num_queries} grind={grind} {:?}: {squeezes} squeezes, \ {states} state reads, {} rows, {} permutations", + f.shape.schedule, chain_schedule_rows(&f.shape, SpongeEntry::fresh()), chain_schedule_perms(&f.shape, SpongeEntry::fresh()), ); @@ -839,8 +901,9 @@ fn the_schedule_is_the_host_transcripts() { /// own — and only the first of them was pinned before this test. #[test] fn the_chain_emits_its_closed_form() { - for (num_vars, num_queries, grind) in COST_SHAPES { - let f = fixture(num_vars, num_queries, grind); + for (cfg, num_vars) in cost_configs() { + let (num_queries, grind) = (cfg.num_queries, cfg.grind.query); + let f = fixture_with(&cfg, num_vars); let program = chain_program(&f.shape); let entry = SpongeEntry::fresh(); @@ -859,10 +922,11 @@ fn the_chain_emits_its_closed_form() { let predicted_perms = chain_perms(&f.shape, entry); println!( - "chain S={num_vars} Q={num_queries} grind={grind}: {measured} rows \ + "chain S={num_vars} Q={num_queries} grind={grind} {:?}: {measured} rows \ ({} shape + {} schedule predicted {predicted}); {perms} permutations \ ({} openings + {} grind + {} schedule predicted {predicted_perms}); \ {consts} constants, {hints} hints, {} instructions", + f.shape.schedule, chain_shape_rows(&f.shape), chain_schedule_rows(&f.shape, entry), chain_opening_perms(&f.shape), @@ -1394,3 +1458,571 @@ fn the_single_chain_term_prices_a_stack_of_at_most_sixty_four_pages() { // The block carries three. assert_eq!(polys_at(3), 1); } + +// ============================================================================= +// W1: the chain under a Merkle cap +// ============================================================================= + +/// The policies the capped gates run under. At `Q = 3` `Auto` caps tree 0 not +/// at all (3 openings) and every later tree at 2 (6 openings) — a chain with an +/// uncapped owner round and capped successors, the mixed case. +const CAP_POLICIES: [CapPolicy; 3] = [CapPolicy::Fixed(1), CapPolicy::Fixed(2), CapPolicy::Auto]; + +/// ★ The capped chain executes on a proof the host accepts, at every policy, +/// one- and three-round shapes, and `Q` large enough for `Auto` to cap the +/// first tree at 3. +#[test] +fn a_capped_chain_executes_on_a_proof_the_host_accepts() { + for (num_vars, num_queries) in [(6usize, 3usize), (5, 3), (9, 3), (6, 25), (9, 25)] { + for cap in CAP_POLICIES { + let f = fixture_capped(num_vars, num_queries, 0, cap); + assert_eq!( + f.shape.caps, + config_with(num_queries, 0, cap).tree_caps(num_vars), + "the shape's caps are the host's" + ); + assert!( + f.shape.caps.iter().any(|&c| c > 0), + "{cap}: something is capped" + ); + let program = chain_program(&f.shape); + let arena = chain_arena(&f, &f.proof); + execute(&program, &[arena], &crate::hash_pin::BLOCK_HASHER).unwrap_or_else(|e| { + panic!( + "S={num_vars} Q={num_queries} {cap} caps {:?}: the machine refused an \ + accepted proof: {e:?}", + f.shape.caps + ) + }); + } + } +} + +/// ★ GATE TWO under the cap: emitted rows and permutations against the closed +/// forms, which now carry the cap terms. +#[test] +fn a_capped_chain_emits_its_closed_form() { + for (num_vars, num_queries, grind) in COST_SHAPES.into_iter().chain([(9, 25, 0)]) { + for cap in [CapPolicy::Fixed(2), CapPolicy::Fixed(3), CapPolicy::Auto] { + let shape = ChainShape::new(&config_with(num_queries, grind, cap), num_vars); + let program = chain_program(&shape); + let entry = SpongeEntry::fresh(); + assert_eq!( + hint_rows(&program), + Layout::new(&shape).total as usize, + "every arena word, cap words included, is hinted exactly once" + ); + let measured = program.instrs.len() - const_rows(&program) - chain_plumbing(&shape); + let tag = format!( + "S={num_vars} Q={num_queries} grind={grind} {cap} {:?}", + shape.caps + ); + assert_eq!(measured, chain_rows(&shape, entry), "{tag}: rows"); + assert_eq!( + perm_rows(&program), + chain_perms(&shape, entry), + "{tag}: permutations" + ); + } + } +} + +/// ★ The transcript does not move with the cap: the host's hash schedule on a +/// capped proof is the same form the default follows. +#[test] +fn the_schedule_is_the_host_transcripts_under_the_cap() { + for (num_vars, num_queries, grind) in COST_SHAPES { + let f = fixture_capped(num_vars, num_queries, grind, CapPolicy::Auto); + let host = f.recorded.duplex.borrow().hashes.clone(); + assert_eq!( + chain_hash_schedule(&f.shape, SpongeEntry::fresh()), + host, + "S={num_vars} Q={num_queries} grind={grind}" + ); + } +} + +/// ★ The tamper arm under the cap: a cap node of tree 0 that NO query reaches +/// (so only the in-guest cap-to-root check can refuse it), +/// a reached one, and a successor tree's cap node. Each: the host rejects it +/// and the machine has no execution. +#[test] +fn a_tampered_capped_chain_cannot_execute() { + // S = 6, k = 4: schedule [4, 2], trees of depth 4 and 2. Fixed(3): caps + // [3, 2] — tree 0 has eight cap nodes and three queries, so at least five + // are unreached. + let cap = CapPolicy::Fixed(3); + let f = fixture_capped(6, 3, 0, cap); + assert_eq!(f.shape.caps, vec![3, 2]); + let program = chain_program(&f.shape); + assert!( + execute( + &program, + &[chain_arena(&f, &f.proof)], + &crate::hash_pin::BLOCK_HASHER + ) + .is_ok(), + "the untouched proof must execute, or the arm proves nothing" + ); + let cfg = config_with(3, 0, cap); + let host_rejects = |proof: &ChainProof| -> bool { + verify::( + proof, + &f.root_bytes, + &f.z, + f.y, + &f.domain, + &cfg, + &mut Recording::new(), + ) + .is_err() + }; + + // Round 0's query positions, as the host drew them: the leaf is the + // position itself, its cap node the top three of its four bits. + let reached: Vec = f.recorded.drawn_u64[..3].iter().map(|q| q >> 1).collect(); + let unreached = (0..8u64).find(|j| !reached.contains(j)).unwrap() as usize; + let reached = reached[0] as usize; + let depth0 = f.shape.current_depth(0); + + let mut sites: Vec<(String, ChainProof)> = Vec::new(); + for (name, j) in [("unreached", unreached), ("reached", reached)] { + let mut forged = f.proof.clone(); + match &mut forged.rounds[0].openings { + RoundOpenings::Base(p) => p.current[0].proof.merkle_path[depth0 - 3 + j][9] ^= 1, + RoundOpenings::Extension(_) => unreachable!("round 0 is base"), + } + sites.push((format!("tree-0 cap node {j} ({name})"), forged)); + } + let mut forged = f.proof.clone(); + match &mut forged.rounds[0].openings { + RoundOpenings::Base(p) => { + let path = &mut p.next[0].proof.merkle_path; + let last = path.len() - 1; + path[last][0] ^= 1; + } + RoundOpenings::Extension(_) => unreachable!("round 0 is base"), + } + sites.push(("tree-1 cap node 3".to_string(), forged)); + + for (name, forged) in &sites { + assert!( + host_rejects(forged), + "{name}: the host must reject the forgery" + ); + assert!( + execute( + &program, + &[chain_arena(&f, forged)], + &crate::hash_pin::BLOCK_HASHER + ) + .is_err(), + "{name}: the machine must refuse the forgery" + ); + } +} + +/// ★ The production chain under `Auto`, evaluated — the knob-on twin of +/// [`the_production_chain_costs_what_the_census_quotes`] and +/// [`the_production_shape_reproduces_the_campaigns_permutation_count`]. +/// +/// Hand derivation: trees of depth 23, 19, 15, 11, 7, 3, 2 +/// opened 112, then 224 times each, capped 3, 3, 3, 3, 3, 3, 2. Openings save +/// `112·3 + 5·224·3 + 224·2 = 4,144` parents; the caps cost `6·7 + 3 = 45`: +/// 22,512 → 18,413 opening permutations, 22,828 → 18,729 in all. Rows: `+2` +/// an opening at `c = 3` (`7 − 6 + 1`), `0` at `c = 2` (`3 − 4 + 1`), so +/// `2·(112 + 5·224) = 2,464`, plus the cap checks `6·16 + 12 = 108`: +/// 184,673 → 187,245 shape rows, 185,509 → 188,081 in all (the schedule does +/// not move). +#[test] +fn the_production_chain_under_the_auto_cap_costs_its_hand_derivation() { + let shape = ChainShape::new(&config_with(112, 20, CapPolicy::Auto), 25); + assert_eq!(shape.caps, vec![3, 3, 3, 3, 3, 3, 2]); + let entry = SpongeEntry::fresh(); + assert_eq!(chain_cap_perms(&shape), 45, "cap permutations"); + assert_eq!(chain_opening_perms(&shape), 18_413, "opening permutations"); + assert_eq!( + chain_schedule_rows(&shape, entry), + 836, + "schedule rows unmoved" + ); + assert_eq!( + chain_schedule_perms(&shape, entry), + 276, + "schedule perms unmoved" + ); + assert_eq!(chain_grind_perms(&shape), 40); + assert_eq!(chain_shape_rows(&shape), 187_245, "shape rows"); + assert_eq!(chain_rows(&shape, entry), 188_081, "rows a chain"); + assert_eq!(chain_perms(&shape, entry), 18_729, "permutations a chain"); + println!( + "production chain S=25 k=4 Q=112 grind=20 cap=auto: {} rows, {} permutations", + chain_rows(&shape, entry), + chain_perms(&shape, entry) + ); +} + +/// ★ The production chain under `Auto`, EMITTED — the knob-on twin of +/// [`the_production_chain_emits_its_closed_form`]. Ignored for the same +/// reason; laptop-safe. +#[test] +#[ignore = "builds a production-shape chain program; run it when the census needs the number"] +fn the_production_chain_emits_its_closed_form_under_the_auto_cap() { + let shape = ChainShape::new(&config_with(112, 20, CapPolicy::Auto), 25); + let program = chain_program(&shape); + let entry = SpongeEntry::fresh(); + let consts = const_rows(&program); + let measured = program.instrs.len() - consts - chain_plumbing(&shape); + let selects = count_rows(&program, |i| { + matches!(i, super::instr::Instr::Select { .. }) + }); + let unpacks = count_rows(&program, |i| { + matches!(i, super::instr::Instr::Unpack { .. }) + }); + println!( + "PRODUCTION chain cap=auto S=25 k=4 Q=112 grind=20: {measured} rows against {} \ + predicted; {} permutations against {} predicted; {selects} Select, {unpacks} Unpack, \ + {consts} constants, {} hints, {} instructions whole", + chain_rows(&shape, entry), + perm_rows(&program), + chain_perms(&shape, entry), + hint_rows(&program), + program.instrs.len(), + ); + assert_eq!(hint_rows(&program), Layout::new(&shape).total as usize); + assert_eq!(measured, chain_rows(&shape, entry)); + assert_eq!(perm_rows(&program), chain_perms(&shape, entry)); +} + +/// ⛔ `PREPARED_LEG_ROWS` is a ROUTING constant and stays fixed +/// across formats. Under the `Auto` cap a chain costs slightly more rows (+2 an +/// opening at `c = 3`, plus the cap checks), so the constant under-states the +/// 24-variable chain it was read from — by less than 2%, and it still covers +/// the block's 20-variable stack. +#[test] +fn the_genesis_threshold_budget_stays_within_two_percent_under_the_auto_cap() { + let auto = |vars| { + chain_shape_rows(&ChainShape::new( + &config_with(112, 20, CapPolicy::Auto), + vars, + )) + }; + let (at_20, at_24) = (auto(20), auto(24)); + let budget = crate::continuation::PREPARED_LEG_ROWS; + println!( + "GENESIS BUDGET cap=auto: {budget} rows against {at_20} at 20 variables, {at_24} at 24" + ); + assert!( + budget >= at_20, + "the budget must still cover the 20-variable stack" + ); + assert!( + (budget as f64) >= 0.98 * at_24 as f64 && budget <= at_24 + at_24 / 50, + "the budget must stay within 2% of the 24-variable chain it stands for" + ); +} + +// --------------------------------------------------------------------------- +// W2: the first-fold schedules (`LAMBDA_VM_ZF_WHIR_FOLDS=first5 | first6`). +// --------------------------------------------------------------------------- + +/// ★ A first-fold chain executes on a proof the host accepts — the stream +/// comparison of [`the_chain_executes_on_a_proof_the_host_accepts`], with the +/// round-0 block 32 or 64 base values wide. +#[test] +fn a_first_fold_chain_executes_on_a_proof_the_host_accepts() { + for (num_vars, num_queries, grind, k0) in KNOB_COST_SHAPES { + let cfg = first_fold_config(num_queries, grind, k0); + let f = fixture_with(&cfg, num_vars); + assert_eq!(f.shape.schedule, cfg.schedule(num_vars)); + assert_eq!(f.shape.schedule[0], k0.min(num_vars)); + assert_eq!(f.shape.current_felts(0), 1 << k0.min(num_vars)); + let program = chain_program(&f.shape); + execute( + &program, + &[chain_arena(&f, &f.proof)], + &crate::hash_pin::BLOCK_HASHER, + ) + .unwrap_or_else(|e| { + panic!( + "S={num_vars} first{k0} {:?}: the machine refused an accepted proof: {e:?}", + f.shape.schedule + ) + }); + } +} + +/// ★ The tamper arm on the wide base block: the LAST value of round 0's +/// 64-value block and one of its Merkle siblings. The host must reject each +/// forgery (so the refusal is of something invalid) and the machine must +/// refuse it. +#[test] +fn a_tampered_first_fold_chain_cannot_execute() { + let grind = 8u8; + let cfg = first_fold_config(3, grind, 6); + let f = fixture_with(&cfg, 9); + assert_eq!(f.shape.schedule, vec![6, 3]); + let program = chain_program(&f.shape); + assert!( + execute( + &program, + &[chain_arena(&f, &f.proof)], + &crate::hash_pin::BLOCK_HASHER + ) + .is_ok(), + "the untouched proof must execute, or the arm below proves nothing" + ); + + let host_rejects = |proof: &ChainProof| -> bool { + let mut t = Recording::new(); + verify::(proof, &f.root_bytes, &f.z, f.y, &f.domain, &cfg, &mut t) + .is_err() + }; + + let mut sites: Vec<(&str, ChainProof)> = Vec::new(); + let mut forged = f.proof.clone(); + match &mut forged.rounds[0].openings { + RoundOpenings::Base(p) => { + assert_eq!(p.current[0].values.len(), 64, "round 0 opens 64 values"); + p.current[0].values[63] += FE::one(); + } + RoundOpenings::Extension(_) => panic!("round 0 is base"), + } + sites.push(("the last value of a 64-wide base block", forged)); + + let mut forged = f.proof.clone(); + match &mut forged.rounds[0].openings { + RoundOpenings::Base(p) => p.current[0].proof.merkle_path[0][0] ^= 1, + RoundOpenings::Extension(_) => panic!("round 0 is base"), + } + sites.push(("a round-0 Merkle sibling", forged)); + + let mut forged = f.proof.clone(); + match &mut forged.rounds[0].openings { + RoundOpenings::Base(p) => p.next[0].values[0] += FEE::one(), + RoundOpenings::Extension(_) => panic!("round 0 is base"), + } + sites.push(( + "the successor block round 0 checks its fold against", + forged, + )); + + for (name, forged) in &sites { + assert!( + host_rejects(forged), + "{name}: the host must reject the forgery" + ); + assert!( + execute( + &program, + &[chain_arena(&f, forged)], + &crate::hash_pin::BLOCK_HASHER + ) + .is_err(), + "{name}: the machine must refuse the forgery" + ); + } +} + +/// ★ The knob-on production pins, at `S = 25, Q = 112`, 20-bit grinds. +/// +/// Derived by hand first, off `verify_weighted`'s round structure, as +/// [`the_production_shape_reproduces_the_campaigns_permutation_count`] did: +/// +/// - `first6` `[6,4,4,4,4,3]`: domains 27/21/17/13/9/5; current depths +/// 21+17+13+9+5+2 = 67, successor depths 17+13+9+5+2 = 46, so 113 parents; +/// current leaves 8 (64 BASE felts) + 4×6 + 3 (the 24-felt tail) = 35 and +/// successor leaves 4×6 + 3 = 27, so 62 leaf blocks. 175 a query, 19,600 a +/// chain. +/// - `first5` `[5,4,4,4,4,4]`: domains 27/22/18/14/10/6; current depths +/// 22+18+14+10+6+2 = 72, successor 18+14+10+6+2 = 50, so 122 parents; leaves +/// 4 (32 base felts) + 5×6 + 5×6 = 64. 186 a query, 20,832 a chain. +/// +/// The whole-chain figures (grind + schedule terms) come from an independent +/// Python re-implementation of these forms, which reproduces today's +/// 22,828 / 185,509: first6 +/// 19,877 permutations and 201,318 rows, first5 21,109 and 189,028. R = 6 +/// under both, so `3R − 1 = 17` grinds, 34 permutations. +#[test] +fn the_first_fold_production_chains_cost_what_the_design_derived() { + let entry = SpongeEntry::fresh(); + for (k0, schedule, parents, per_query, perms, rows) in [ + (6, vec![6, 4, 4, 4, 4, 3], 113, 175, 19_877, 201_318), + (5, vec![5, 4, 4, 4, 4, 4], 122, 186, 21_109, 189_028), + ] { + let shape = ChainShape::new(&first_fold_config(112, 20, k0), 25); + assert_eq!(shape.schedule, schedule, "first{k0}"); + let got_parents: usize = (0..shape.rounds()) + .map(|r| shape.current_depth(r) + shape.next_depth(r).unwrap_or(0)) + .sum(); + assert_eq!(got_parents, parents, "first{k0}: Merkle parents a query"); + assert_eq!( + shape.current_felts(0), + 1 << k0, + "first{k0}: round 0 is base" + ); + let opening = chain_opening_perms(&shape); + assert_eq!(opening, per_query * 112, "first{k0}: opening permutations"); + assert_eq!(chain_grind_perms(&shape), 34, "first{k0}: 17 grinds"); + println!( + "production chain S=25 first{k0} Q=112 grind=20: {opening} opening permutations, \ + {} permutations, {} rows ({} schedule perms, {} schedule rows)", + chain_perms(&shape, entry), + chain_rows(&shape, entry), + chain_schedule_perms(&shape, entry), + chain_schedule_rows(&shape, entry), + ); + assert_eq!( + chain_perms(&shape, entry), + perms, + "first{k0}: permutations a chain" + ); + assert_eq!(chain_rows(&shape, entry), rows, "first{k0}: rows a chain"); + } +} + +/// ★ THE PRODUCTION DEFAULT CHAIN — what `chain_config` builds with +/// no knob set — is `first6` under the `Auto` cap, at the legacy security +/// parameters (blowup 2^2, Q = 112, 20-bit grinds). The legacy chain keeps its +/// own pins above (`the_production_chain_costs…`, 185,509 / 22,828); these are +/// the default's, the two levers measured together on the WHIR pipeline's +/// block (−9.10 s, ABBA): W2's six rounds and W1's cap. +#[test] +fn the_production_default_chain_is_first6_under_the_auto_cap() { + let production = crate::multilinear_prove::chain_config_under( + &crate::zf_format::ZfFormat::DEFAULT, + &[(1, 25)], + ); + let want = ChainConfig { + format: ChainFormat { + cap: CapPolicy::Auto, + folds: WhirFolds::First(FirstFold::new(6).expect("6")), + }, + ..config(112, 20) + }; + assert_eq!(production, want, "the production default's chain config"); + let shape = ChainShape::new(&production, 25); + assert_eq!(shape.schedule, vec![6, 4, 4, 4, 4, 3], "first6 at 25"); + let entry = SpongeEntry::fresh(); + println!( + "production DEFAULT chain S=25 first6 cap=auto Q=112 grind=20: caps {:?}, {} opening \ + permutations, {} cap permutations, {} grind permutations, {} permutations, {} rows \ + ({} shape rows)", + shape.caps, + chain_opening_perms(&shape), + chain_cap_perms(&shape), + chain_grind_perms(&shape), + chain_perms(&shape, entry), + chain_rows(&shape, entry), + chain_shape_rows(&shape), + ); + assert_eq!(chain_grind_perms(&shape), 34, "17 grinds"); + assert_eq!(chain_opening_perms(&shape), 16_166, "opening permutations"); + assert_eq!(chain_cap_perms(&shape), 38, "cap permutations"); + assert_eq!(chain_shape_rows(&shape), 202_690, "shape rows"); + assert_eq!(shape.caps, DEFAULT_CHAIN_CAPS, "the auto caps per tree"); + assert_eq!( + chain_perms(&shape, entry), + DEFAULT_CHAIN_PERMS, + "permutations a chain" + ); + assert_eq!( + chain_rows(&shape, entry), + DEFAULT_CHAIN_ROWS, + "rows a chain" + ); + // Both levers pay: fewer permutations than either alone. + const { assert!(DEFAULT_CHAIN_PERMS < 18_729 && DEFAULT_CHAIN_PERMS < 19_877) }; +} + +/// The production default chain's pins, derived by the closed +/// forms and checked against the EMITTED program by +/// [`the_production_default_chain_emits_its_closed_form`]. +const DEFAULT_CHAIN_CAPS: &[usize] = &[3, 3, 3, 3, 3, 2]; +const DEFAULT_CHAIN_PERMS: usize = 16_443; +const DEFAULT_CHAIN_ROWS: usize = 203_426; + +/// ★ The production default chain, EMITTED (the F1 of the test above). +/// `#[ignore]`d like its siblings: a production-shape program; laptop-safe. +#[test] +#[ignore = "builds a production-shape chain program; run with -- --ignored"] +fn the_production_default_chain_emits_its_closed_form() { + let production = crate::multilinear_prove::chain_config_under( + &crate::zf_format::ZfFormat::DEFAULT, + &[(1, 25)], + ); + let shape = ChainShape::new(&production, 25); + let entry = SpongeEntry::fresh(); + let program = chain_program(&shape); + let consts = const_rows(&program); + let hints = hint_rows(&program); + assert_eq!( + hints, + Layout::new(&shape).total as usize, + "every arena word hinted once" + ); + let measured = program.instrs.len() - consts - chain_plumbing(&shape); + let perms = perm_rows(&program); + println!( + "PRODUCTION DEFAULT chain S=25 first6 cap=auto Q=112 grind=20: {measured} rows against {} \ + predicted; {perms} permutations against {} predicted; {consts} constants, {hints} hints, \ + {} instructions", + chain_rows(&shape, entry), + chain_perms(&shape, entry), + program.instrs.len(), + ); + assert_eq!(measured, chain_rows(&shape, entry), "rows"); + assert_eq!(perms, chain_perms(&shape, entry), "permutations"); + assert_eq!(measured, DEFAULT_CHAIN_ROWS); + assert_eq!(perms, DEFAULT_CHAIN_PERMS); +} + +/// ★ The knob-on production chains EMIT their closed forms — the F1 of +/// [`the_production_chain_emits_its_closed_form`] under `first5` and `first6`. +/// `#[ignore]`d for the same reason (a production-shape program). +#[test] +#[ignore = "builds two production-shape chain programs; run with -- --ignored"] +fn the_first_fold_production_chains_emit_their_closed_forms() { + let entry = SpongeEntry::fresh(); + for k0 in [5, 6] { + let shape = ChainShape::new(&first_fold_config(112, 20, k0), 25); + let program = chain_program(&shape); + let consts = const_rows(&program); + let hints = hint_rows(&program); + assert_eq!(hints, Layout::new(&shape).total as usize, "first{k0}"); + let measured = program.instrs.len() - consts - chain_plumbing(&shape); + let perms = perm_rows(&program); + println!( + "PRODUCTION chain S=25 first{k0} Q=112 grind=20: {measured} rows against {} \ + predicted; {perms} permutations against {}; {consts} constants, {} instructions", + chain_rows(&shape, entry), + chain_perms(&shape, entry), + program.instrs.len(), + ); + assert_eq!(measured, chain_rows(&shape, entry), "first{k0}: rows"); + assert_eq!(perms, chain_perms(&shape, entry), "first{k0}: permutations"); + } +} + +/// ⛔ `PREPARED_LEG_ROWS` stays FIXED under the fold knob, and +/// this is what makes that safe: under each first fold the constant still +/// covers the block's 20-variable stack, so no page is left sparse that the +/// opening could carry. The default band above is untouched; its upper side +/// (the 24-variable chain) is a statement about where the constant was read +/// from, at the default schedule only. +#[test] +fn the_genesis_threshold_budget_still_covers_the_stack_under_each_first_fold() { + let budget = crate::continuation::PREPARED_LEG_ROWS; + for (k0, at_20_design) in [(5, 137_321), (6, 155_889)] { + let at_20 = chain_shape_rows(&ChainShape::new(&first_fold_config(112, 20, k0), 20)); + println!("GENESIS BUDGET first{k0}: {budget} rows against a chain of {at_20} at 20"); + assert_eq!( + at_20, at_20_design, + "first{k0}: the 20-variable stack's rows (design/WHIR.md §4.8)" + ); + assert!( + budget >= at_20, + "first{k0}: the threshold charges {budget} rows for a stack that costs {at_20}" + ); + } +} diff --git a/prover/src/lfm/whir_epoch_program_tests.rs b/prover/src/lfm/whir_epoch_program_tests.rs index 6c35bcab4..d70a6f9c2 100644 --- a/prover/src/lfm/whir_epoch_program_tests.rs +++ b/prover/src/lfm/whir_epoch_program_tests.rs @@ -148,7 +148,25 @@ fn the_production_epoch_recount() { assert_eq!(shapes.len(), 34, "epoch 0 is 34 tables (sh1)"); let sizes = epoch_groups(shapes.len()); assert_eq!(sizes, vec![33, 1], "the bookend is committed alone"); - let config = chain_config(&shapes); + // ⚠ AT THE LEGACY WHIR FORMAT, named. This recount is of sh1's measured + // epoch, and sh1 ran before the default flip (uniform folds, no cap): its + // "rounds 56" is 8 chains x 7 rounds. The production default (first6, cap + // auto) proves this epoch in 8 x 6 = 48 rounds — asserted + // below so the flip is a stated fact here, not a silent re-pin of a record. + let config = + crate::multilinear_prove::chain_config_under(&crate::zf_format::ZfFormat::LEGACY, &shapes); + { + let production = crate::multilinear_prove::chain_config_under( + &crate::zf_format::ZfFormat::DEFAULT, + &shapes, + ); + assert_eq!( + production.format, + crate::zf_format::ZfFormat::DEFAULT.chain_format() + ); + assert_eq!(production.num_queries, 112, "the flip keeps Q"); + assert_eq!(ChainShape::new(&production, 25).rounds(), 6, "first6 at 25"); + } let (layouts, _domains) = stacks(&shapes, &sizes, &config).expect("the epoch's stacks build"); // ⚠ ASSERTED BEFORE ANYTHING IS COUNTED. These four are sh1's own printed @@ -888,6 +906,7 @@ fn walk_config() -> multilinear::whir_chain::ChainConfig { log_folding: 2, num_queries: 3, grind: multilinear::whir_chain::GrindBits::default(), + format: multilinear::whir_chain::ChainFormat::DEFAULT, } } diff --git a/prover/src/lfm/whir_epoch_tests.rs b/prover/src/lfm/whir_epoch_tests.rs index e6b0de6b0..ef9087202 100644 --- a/prover/src/lfm/whir_epoch_tests.rs +++ b/prover/src/lfm/whir_epoch_tests.rs @@ -475,7 +475,7 @@ mod tests { /// state the type system forbids is a check that cannot fail. /// /// ⚠ The refusal is NOT a shape guard. `DecodePrepared::agrees_with` - /// compares only `log_blowup` and `log_folding`, which two programs at the + /// compares only `log_blowup`, `log_folding` and the fold schedule, which two programs at the /// same options share, so a wrong-program prepared sails past it and is /// caught by the derived roots block the transcript absorbs — the same /// cryptographic mechanism as the hash agreement. The reason is printed so diff --git a/prover/src/lfm/whir_fold_tests.rs b/prover/src/lfm/whir_fold_tests.rs index c72caf402..9a06307a0 100644 --- a/prover/src/lfm/whir_fold_tests.rs +++ b/prover/src/lfm/whir_fold_tests.rs @@ -85,7 +85,18 @@ fn const_rows(program: &LfmProgram) -> usize { /// that moves with the index width, so pinning it needs two widths at the SAME /// block: `(8, 4, 4)` and `(8, 4, 6)` are that pair, and a form that folded the /// index term into the block term would fit one and miss the other. -const SHAPES: &[(usize, usize, usize)] = &[(5, 1, 4), (6, 2, 4), (8, 4, 4), (10, 4, 6), (8, 4, 6)]; +/// +/// The last two are W2's first folds (`first5`, `first6`): blocks of 32 and 64, +/// on the chain relation, the widest folds the stack runs (`MAX_FOLD`). +const SHAPES: &[(usize, usize, usize)] = &[ + (5, 1, 4), + (6, 2, 4), + (8, 4, 4), + (10, 4, 6), + (8, 4, 6), + (12, 5, 7), + (13, 6, 7), +]; /// ★ F1 for the fold: every row named, with the interned constants counted /// separately and pinned in their own right. diff --git a/prover/src/lfm/whir_open.rs b/prover/src/lfm/whir_open.rs index 248d04efb..3a07aa65a 100644 --- a/prover/src/lfm/whir_open.rs +++ b/prover/src/lfm/whir_open.rs @@ -143,10 +143,28 @@ pub const fn block_leaf_rows(felts: usize, unpacks: usize) -> usize { /// once by the caller and shared across every query against that root, so they /// are not charged here. pub const fn verify_opening_rows(felts: usize, unpacks: usize, depth: usize) -> usize { + verify_opening_rows_capped(felts, unpacks, depth, 0) +} + +/// [`verify_opening_rows`] against a tree capped at height `cap` +/// ([`CapCells`]): the walk stops `cap` levels short (`2·cap` rows fewer), +/// the cap mux picks the node with `2^cap − 1` `Select` rows, and the +/// comparison is of two VARIABLE cells, so it unpacks both (one `Unpack` more +/// than against the hoisted root lanes). At `cap = 0` it is the root form. +pub const fn verify_opening_rows_capped( + felts: usize, + unpacks: usize, + depth: usize, + cap: usize, +) -> usize { let leaf = block_leaf_rows(felts, unpacks); - let walk = 2 * depth; + let walk = 2 * (depth - cap); let compare = 1 + 2 * FELTS_PER_WORD; - leaf + walk + compare + if cap == 0 { + leaf + walk + compare + } else { + leaf + walk + ((1usize << cap) - 1) + 1 + compare + } } /// PERMUTATIONS one query's opening costs: the leaf's blocks plus one parent a @@ -154,7 +172,32 @@ pub const fn verify_opening_rows(felts: usize, unpacks: usize, depth: usize) -> /// function of the block's felts and the tree's depth alone — no row /// bookkeeping enters it. pub const fn verify_opening_perms(felts: usize, depth: usize) -> usize { - felts.div_ceil(RATE_FELTS) + depth + verify_opening_perms_capped(felts, depth, 0) +} + +/// [`verify_opening_perms`] against a tree capped at height `cap`: `cap` +/// parents fewer. The cap's own `2^cap − 1` parents are paid once per TREE, +/// by [`cap_check_perms`]. +pub const fn verify_opening_perms_capped(felts: usize, depth: usize, cap: usize) -> usize { + felts.div_ceil(RATE_FELTS) + depth - cap +} + +/// PERMUTATIONS one tree's cap check costs: the cap hashed up to its root, +/// `2^cap − 1` parents. Nothing at `cap = 0`. +pub const fn cap_check_perms(cap: usize) -> usize { + (1usize << cap) - 1 +} + +/// INSTRUCTIONS one tree's cap check costs beyond its hinted words: the +/// `2^cap − 1` parents (one `compress` each) and the root comparison (one +/// `Unpack` and four lowered asserts). Nothing at `cap = 0`: an uncapped tree +/// is compared against its root lanes query by query. +pub const fn cap_check_rows(cap: usize) -> usize { + if cap == 0 { + 0 + } else { + cap_check_perms(cap) + 1 + 2 * FELTS_PER_WORD + } } /// ★ The block's Merkle leaf: `sponge_leaf` over its felts. @@ -207,3 +250,53 @@ pub fn emit_verify_opening( let walked = edsl::wrap_merkle_walk(b, leaf, index_bits, siblings); edsl::assert_digest_eq_lanes(b, walked, std::slice::from_ref(root_lanes)); } + +/// ★ One tree's authenticated Merkle cap — the gadget the STARK verifier +/// shares ([`super::merkle_cap`]): its only constructor checks the cap against +/// the tree's root, and its one entry point walks, muxes and compares. +pub use super::merkle_cap::CapCells; + +/// How one tree's openings are authenticated in-guest: against its root +/// lanes (no cap — today's emission, instruction for instruction), or +/// against its authenticated [`CapCells`]. +pub enum TreeAuth { + Root([Felt; 4]), + Cap(CapCells), +} + +impl TreeAuth { + /// The cap height the openings are cut to (0 for a root). + pub fn cap_height(&self) -> usize { + match self { + TreeAuth::Root(_) => 0, + TreeAuth::Cap(cap) => cap.height(), + } + } + + /// ★ `whir_commit::verify_opening_capped`, emitted as a refusal. + /// + /// `index_bits` is the WHOLE leaf index, low first, one bit per tree + /// level; `siblings` is the path to the cap, `index_bits.len() − c` long. + /// The low bits are walked and the top `c` pick the cap node. With a + /// [`TreeAuth::Root`] this is [`emit_verify_opening`] exactly. + pub fn verify_opening( + &self, + b: &mut LfmBuilder, + values: BlockValues<'_>, + index_bits: &[Bit], + siblings: &[WrapDigest], + ) { + match self { + TreeAuth::Root(lanes) => emit_verify_opening(b, values, index_bits, siblings, lanes), + TreeAuth::Cap(cap) => { + assert_eq!( + siblings.len() + cap.height(), + index_bits.len(), + "a path to the cap: one sibling per level below it" + ); + let leaf = emit_block_leaf(b, values); + cap.verify_path(b, leaf, index_bits, siblings); + } + } + } +} diff --git a/prover/src/lfm/whir_open_tests.rs b/prover/src/lfm/whir_open_tests.rs index d58aaae9f..fa6804649 100644 --- a/prover/src/lfm/whir_open_tests.rs +++ b/prover/src/lfm/whir_open_tests.rs @@ -18,7 +18,9 @@ use super::edsl::WrapDigest; use super::executor::execute; use super::validator::validate; use super::whir_open::{ - BlockValues, emit_verify_opening, verify_opening_perms, verify_opening_rows, + BlockValues, CapCells, TreeAuth, cap_check_perms, cap_check_rows, emit_verify_opening, + verify_opening_perms, verify_opening_perms_capped, verify_opening_rows, + verify_opening_rows_capped, }; use super::word::{LfmWord, ext_word}; @@ -292,7 +294,7 @@ fn the_opening_accepts_what_the_host_accepts() { for index in [0usize, 1, num_leaves / 2, num_leaves - 1] { let opening = commitment.open(index).expect("the block opens"); assert!( - verify_opening::(&commitment.root(), index, &opening), + verify_opening::(&commitment.root(), depth, index, &opening), "{}: the host must accept its own opening at {index}", shape.name ); @@ -316,7 +318,7 @@ fn the_opening_accepts_what_the_host_accepts() { for index in [0usize, 1, num_leaves - 1] { let opening = commitment.open(index).expect("the block opens"); assert!( - verify_opening::(&commitment.root(), index, &opening), + verify_opening::(&commitment.root(), depth, index, &opening), "{}: the host must accept its own opening at {index}", shape.name ); @@ -363,7 +365,7 @@ fn a_tampered_opening_cannot_execute() { let honest_root = commitment_to_digest(&root); assert!( - verify_opening::(&root, index, &opening), + verify_opening::(&root, depth, index, &opening), "the control opening must authenticate" ); assert!( @@ -385,7 +387,7 @@ fn a_tampered_opening_cannot_execute() { let mut forged = opening.clone(); forged.values[block / 2] += FEE::one(); assert!( - !verify_opening::(&root, index, &forged), + !verify_opening::(&root, depth, index, &forged), "the host must reject a corrupted value" ); let values: Vec = forged.values.iter().map(ext_word).collect(); @@ -403,7 +405,7 @@ fn a_tampered_opening_cannot_execute() { let mut forged = opening.clone(); forged.proof.merkle_path[0][0] ^= 1; assert!( - !verify_opening::(&root, index, &forged), + !verify_opening::(&root, depth, index, &forged), "the host must reject a corrupted sibling" ); assert!( @@ -425,7 +427,7 @@ fn a_tampered_opening_cannot_execute() { let mut wrong_root = root; wrong_root[0] ^= 1; assert!( - !verify_opening::(&wrong_root, index, &opening), + !verify_opening::(&wrong_root, depth, index, &opening), "the host must reject a wrong root" ); assert!( @@ -448,7 +450,7 @@ fn a_tampered_opening_cannot_execute() { // and fail only here. let elsewhere = (index + 1) % num_leaves; assert!( - !verify_opening::(&root, elsewhere, &opening), + !verify_opening::(&root, depth, elsewhere, &opening), "the host must reject an opening claimed at the wrong index" ); assert!( @@ -530,3 +532,236 @@ fn the_leaf_pins_a_hinted_base_value_to_its_low_lane() { failure that happens to also stop the program: got {refusal:?}" ); } + +// ============================================================================= +// W1: openings against a Merkle cap +// ============================================================================= + +/// A tree of 64 leaves (depth 6) over ext blocks of two, capped at `c`. +fn capped_commitment() -> CodewordCommitment { + ext_commitment( + &Shape { + log_domain: 7, + log_folding: 1, + name: "block 2, 6 levels", + }, + 0xCA9, + ) +} + +/// Arena: the cap (`2^c` words), the root, then per opening its block, its +/// `depth − c` siblings and its index. +fn capped_arena( + commitment: &CodewordCommitment, + c: usize, + openings: &[(usize, CosetOpening)], + cap: &[[u8; 32]], +) -> Vec { + let mut words: Vec = cap.iter().map(commitment_to_digest).collect(); + words.push(commitment_to_digest(&commitment.root())); + let depth = commitment.depth(); + for (index, opening) in openings { + words.extend(opening.values.iter().map(ext_word)); + words.extend( + opening.proof.merkle_path[..depth - c] + .iter() + .map(commitment_to_digest), + ); + words.push([FE::from(*index as u64), FE::zero(), FE::zero(), FE::zero()]); + } + words +} + +/// The program [`capped_arena`] feeds: one cap check, then `n` openings through +/// [`TreeAuth::verify_opening`]. Returns the program and the rows of its cap +/// check alone (measured by building the same prefix twice). +fn capped_program(depth: usize, c: usize, n: usize) -> LfmProgram { + let block = 2usize; + let mut b = LfmBuilder::new().with_wrap_hash(super::edsl::WrapHash::production()); + let per = block + (depth - c) + 1; + let arena = b.declare_arena(((1 << c) + 1 + n * per) as u32); + let cap: Vec = (0..1u32 << c) + .map(|i| WrapDigest::from_cell(b.hint_word(arena, i))) + .collect(); + let root = b.hint_word(arena, 1 << c); + let root_lanes = b.unpack(root); + let tree = TreeAuth::Cap(CapCells::authenticate( + &mut b, + &cap, + std::slice::from_ref(&root_lanes), + )); + for q in 0..n { + let at = ((1 << c) + 1 + q * per) as u32; + let values: Vec = (0..block) + .map(|i| b.hint_word(arena, at + i as u32).as_ext()) + .collect(); + let siblings: Vec = (0..depth - c) + .map(|i| WrapDigest::from_cell(b.hint_word(arena, at + (block + i) as u32))) + .collect(); + let index = b.hint_felt(arena, at + (block + depth - c) as u32); + let bits = b.bit_dec(index, depth); + tree.verify_opening(&mut b, BlockValues::Ext(&values), &bits, &siblings); + } + b.public(root); + let program = compile(b.finish()); + validate(&program).expect("the capped opening leg must be admissible"); + program +} + +/// The host's owner encoding: every path cut to `depth − c`, the first one +/// carrying the cap. Returns the openings and the cap. +#[allow(clippy::type_complexity)] +fn open_capped( + commitment: &CodewordCommitment, + c: usize, + indices: &[usize], +) -> (Vec<(usize, CosetOpening)>, Vec<[u8; 32]>) { + let depth = commitment.depth(); + let openings = commitment + .open_many_capped(indices, c, true) + .expect("the blocks open"); + let cap = openings[0].proof.merkle_path[depth - c..].to_vec(); + (indices.iter().copied().zip(openings).collect(), cap) +} + +/// ★ The cap mux selects the right node for EVERY index: all 64 leaves of a +/// depth-6 tree opened against a height-3 cap, so all eight top-bit patterns +/// are exercised. A mux level fed a constant (or the wrong bit) picks the +/// wrong node for half the indices and this refuses to execute. And an +/// opening claimed under another top-bit pattern — right leaf, right path, +/// wrong subtree — is refused, host and machine. +#[test] +fn the_cap_mux_selects_every_index() { + let commitment = capped_commitment(); + let depth = commitment.depth(); + assert_eq!(depth, 6); + for c in 1..=3usize { + let all: Vec = (0..64).collect(); + let (openings, cap) = open_capped(&commitment, c, &all); + let program = capped_program(depth, c, all.len()); + execute( + &program, + &[capped_arena(&commitment, c, &openings, &cap)], + &crate::hash_pin::BLOCK_HASHER, + ) + .unwrap_or_else(|e| panic!("c={c}: the machine refused honest capped openings: {e:?}")); + + // Right leaf and path, claimed in another subtree. + let (index, opening) = &openings[5]; + let elsewhere = index ^ (1 << (depth - 1)); + let root = commitment.root(); + let (check, _) = crypto::merkle_tree::cap::CappedRoot::from_owner::< + ::Backend, + >(&root, &openings[0].1.proof.merkle_path, depth, c) + .expect("the owner's cap authenticates"); + let siblings = &opening.proof.merkle_path[..depth - c]; + assert!( + multilinear::whir_commit::verify_opening_capped::( + &check, *index, opening, siblings + ) + ); + assert!( + !multilinear::whir_commit::verify_opening_capped::( + &check, elsewhere, opening, siblings + ), + "c={c}: the host must refuse the wrong subtree" + ); + let program = capped_program(depth, c, 1); + let forged = vec![(elsewhere, opening.clone())]; + assert!( + execute( + &program, + &[capped_arena(&commitment, c, &forged, &cap)], + &crate::hash_pin::BLOCK_HASHER + ) + .is_err(), + "c={c}: the machine must refuse the wrong subtree" + ); + } +} + +/// ★ In-guest: a cap word NO opening reaches, tampered. The +/// walk and the mux of every opening are unaffected, so only the cap-to-root +/// check can refuse it — and it does. A cap word an opening does reach is +/// refused too. +#[test] +fn a_tampered_cap_word_cannot_execute() { + let commitment = capped_commitment(); + let depth = commitment.depth(); + let c = 3; + // Two openings, both under cap node 0 (indices < 8). + let (openings, cap) = open_capped(&commitment, c, &[1, 6]); + let program = capped_program(depth, c, 2); + let honest = capped_arena(&commitment, c, &openings, &cap); + assert!( + execute( + &program, + std::slice::from_ref(&honest), + &crate::hash_pin::BLOCK_HASHER + ) + .is_ok(), + "the untouched arena must execute, or the arm proves nothing" + ); + for node in [5usize, 0] { + let mut forged = honest.clone(); + forged[node][1] += FE::one(); + assert!( + execute(&program, &[forged], &crate::hash_pin::BLOCK_HASHER).is_err(), + "cap word {node} tampered: the machine must refuse" + ); + } +} + +/// ★ F1 for the capped opening and the cap check: the emitted rows and +/// permutations against [`verify_opening_rows_capped`], +/// [`verify_opening_perms_capped`], [`cap_check_rows`] and +/// [`cap_check_perms`], at every cap height of a depth-6 tree. +#[test] +fn the_capped_opening_emits_its_closed_form() { + let depth = 6; + let (block, felts, unpacks) = (2usize, 6usize, 2usize); + for c in 1..=depth { + let one = capped_program(depth, c, 1); + let two = capped_program(depth, c, 2); + // The second opening's own rows: the difference, less its plumbing + // (its hints and its `BitDec`). + let per_opening_plumbing = block + (depth - c) + 1 + 1; + let opening_rows = (two.instrs.len() - const_rows(&two)) + - (one.instrs.len() - const_rows(&one)) + - per_opening_plumbing; + assert_eq!( + opening_rows, + verify_opening_rows_capped(felts, unpacks, depth, c), + "c={c}: rows a capped opening" + ); + assert_eq!( + perm_rows(&two) - perm_rows(&one), + verify_opening_perms_capped(felts, depth, c), + "c={c}: permutations a capped opening" + ); + // The one-opening program: plumbing (cap hints, root hint, its + // `Unpack`, the public) + the cap check + one opening. + let fixed_plumbing = (1 << c) + 1 + 1 + 1; + let check_rows = one.instrs.len() + - const_rows(&one) + - fixed_plumbing + - per_opening_plumbing + - opening_rows; + assert_eq!(check_rows, cap_check_rows(c), "c={c}: rows the cap check"); + assert_eq!( + perm_rows(&one) - verify_opening_perms_capped(felts, depth, c), + cap_check_perms(c), + "c={c}: permutations the cap check" + ); + } + // At c = 0 the capped forms are the root forms. + assert_eq!( + verify_opening_rows_capped(felts, unpacks, depth, 0), + verify_opening_rows(felts, unpacks, depth) + ); + assert_eq!( + verify_opening_perms_capped(felts, depth, 0), + verify_opening_perms(felts, depth) + ); + assert_eq!((cap_check_rows(0), cap_check_perms(0)), (0, 0)); +} diff --git a/prover/src/lfm/whir_stacked_tests.rs b/prover/src/lfm/whir_stacked_tests.rs index 8e445024a..a4160a5be 100644 --- a/prover/src/lfm/whir_stacked_tests.rs +++ b/prover/src/lfm/whir_stacked_tests.rs @@ -342,6 +342,7 @@ fn group_config(group: &Group) -> ChainConfig { log_folding: 2, num_queries: group.num_queries, grind: GrindBits::uniform(group.grind), + format: multilinear::whir_chain::ChainFormat::DEFAULT, } } diff --git a/prover/src/lfm/whir_statement.rs b/prover/src/lfm/whir_statement.rs index e6fc767bc..fd28a4b2e 100644 --- a/prover/src/lfm/whir_statement.rs +++ b/prover/src/lfm/whir_statement.rs @@ -99,11 +99,21 @@ pub struct GlobalStatement<'a> { fn push_config(bytes: &mut Vec, config: &ChainConfig) { let &ChainConfig { log_blowup, - log_folding, + // ★ Written as `fold_word()`: `log_folding` itself (4u64) under the + // default schedule — today's bytes — and a tagged word that binds the + // fold schedule otherwise (W2). Same length either way: 245 bytes. + log_folding: _, num_queries, grind, + // ⚠ NOT absorbed: the rest of the format (the cap policy) is a set of + // verifier-side constants, like the STARK cap. Absorbing it would move + // this statement's bytes, and every WHIR transcript KAT, at the + // default. A change to a lever's effect on the statement + // decides that here, explicitly. The fold schedule is absorbed through + // the word above, whose default value is today's. + format: _, } = config; - for value in [log_blowup as u64, log_folding as u64, num_queries as u64] { + for value in [log_blowup as u64, config.fold_word(), num_queries as u64] { bytes.extend_from_slice(&value.to_le_bytes()); } bytes.extend_from_slice(&[grind.folding, grind.ood, grind.query]); diff --git a/prover/src/lfm/whir_statement_tests.rs b/prover/src/lfm/whir_statement_tests.rs index 4e57469d6..3738bdc3e 100644 --- a/prover/src/lfm/whir_statement_tests.rs +++ b/prover/src/lfm/whir_statement_tests.rs @@ -13,7 +13,7 @@ use crypto::fiat_shamir::default_transcript::DefaultTranscript; use crypto::fiat_shamir::is_transcript::IsTranscript; use crypto::fiat_shamir::transcript_hash::RpxTranscriptHash; -use multilinear::whir_chain::{ChainConfig, GrindBits}; +use multilinear::whir_chain::{ChainConfig, FirstFold, GrindBits, WhirFolds}; use crate::TableCounts; use crate::statement::statement_padding; @@ -89,6 +89,27 @@ fn host_epoch_challenge( table_counts: &TableCounts, table_num_vars: &[u8], root_bytes: &[u8; 32], +) -> FEE { + host_epoch_challenge_under( + &config(), + elf, + label, + public_output, + table_counts, + table_num_vars, + root_bytes, + ) +} + +/// [`host_epoch_challenge`] at a given config. +fn host_epoch_challenge_under( + config: &ChainConfig, + elf: &[u8; 32], + label: u64, + public_output: &[u8], + table_counts: &TableCounts, + table_num_vars: &[u8], + root_bytes: &[u8; 32], ) -> FEE { let mut transcript = HostTranscript::new(&[]); crate::multilinear_continuation::absorb_epoch( @@ -98,7 +119,7 @@ fn host_epoch_challenge( table_counts, label, table_num_vars, - &config(), + config, ); transcript.append_bytes(root_bytes); transcript.sample_field_element() @@ -113,6 +134,27 @@ fn machine_epoch_challenge( table_counts: &TableCounts, table_num_vars: &[u8], root_word: LfmWord, +) -> (FEE, StatementCost, usize, usize) { + machine_epoch_challenge_under( + &config(), + elf, + label, + public_output, + table_counts, + table_num_vars, + root_word, + ) +} + +/// [`machine_epoch_challenge`] at a given config. +fn machine_epoch_challenge_under( + config: &ChainConfig, + elf: &[u8; 32], + label: u64, + public_output: &[u8], + table_counts: &TableCounts, + table_num_vars: &[u8], + root_word: LfmWord, ) -> (FEE, StatementCost, usize, usize) { let mut b = LfmBuilder::new().with_wrap_hash(super::edsl::WrapHash::production()); let arena = b.declare_arena(1); @@ -126,7 +168,7 @@ fn machine_epoch_challenge( public_output, table_counts, table_num_vars, - config: &config(), + config, }, ); @@ -650,3 +692,174 @@ fn the_global_statement_draws_the_challenge_the_host_draws() { ); assert_eq!(cost.operations(), 0, "a statement emits no operation row"); } + +// --------------------------------------------------------------- +// W2: the fold schedule's statement word. +// --------------------------------------------------------------- + +fn first_fold(k0: usize, tallest: usize) -> ChainConfig { + ChainConfig::with_security_folds( + 2, + 4, + WhirFolds::First(FirstFold::new(k0).unwrap()), + tallest, + 128, + GrindBits::uniform(20), + ) +} + +/// The byte offset of the fold word in the epoch statement: the three config +/// words and the 3-byte grind trailer end the stream. +fn fold_word_range(len: usize) -> std::ops::Range { + let words_start = len - 3 - 3 * 8; + words_start + 8..words_start + 16 +} + +/// ★ The statement keeps its length and moves only in the fold word, and the +/// machine draws the host's challenge under each accepted schedule. +#[test] +fn a_first_fold_statement_moves_only_its_fold_word() { + let elf = digest(0x21); + let table_counts = counts(); + let table_num_vars: Vec = (0..34).map(|i| 12 + (i as u8) % 9).collect(); + let (root_bytes, root_word) = root(0x6b); + let statement = |config: &ChainConfig| { + epoch_statement_bytes(&EpochStatement { + elf_digest: &elf, + epoch_label: 3, + public_output: &[], + table_counts: &table_counts, + table_num_vars: &table_num_vars, + config, + }) + }; + let today = statement(&config()); + let range = fold_word_range(today.len()); + assert_eq!( + &today[range.clone()], + &4u64.to_le_bytes(), + "the default word is 4u64" + ); + for k0 in [5, 6] { + let arm = first_fold(k0, 25); + assert_eq!(arm.num_queries, config().num_queries, "Q stays 112"); + let bytes = statement(&arm); + assert_eq!( + bytes.len(), + today.len(), + "first{k0}: the statement keeps its length" + ); + assert_eq!(&bytes[range.clone()], &arm.fold_word().to_le_bytes()); + for (i, (a, b)) in today.iter().zip(&bytes).enumerate() { + if !range.contains(&i) { + assert_eq!(a, b, "first{k0}: byte {i} moved outside the fold word"); + } + } + assert_ne!(bytes, today); + + let want = host_epoch_challenge_under( + &arm, + &elf, + 3, + &[], + &table_counts, + &table_num_vars, + &root_bytes, + ); + let (got, _, _, _) = machine_epoch_challenge_under( + &arm, + &elf, + 3, + &[], + &table_counts, + &table_num_vars, + root_word, + ); + assert_eq!( + got, want, + "first{k0}: the machine must draw the host's challenge" + ); + assert_ne!( + want, + host_epoch_challenge(&elf, 3, &[], &table_counts, &table_num_vars, &root_bytes), + "first{k0}: the schedule must move the challenge" + ); + } +} + +/// ★ MUTATION GATE: the word is what binds the schedule. +/// +/// At `num_vars <= 4` a `first6` chain and a `uniform4` chain have the SAME +/// schedule (one round of everything) and the same Q, so every other byte of +/// the statement and every round-count check agree: a proof at one would pass +/// the other's shape checks. Only the fold word tells them apart. Stop +/// absorbing it (write `log_folding` back) and the two challenges below become +/// equal, and this test fails. +#[test] +fn the_fold_word_alone_separates_two_schedules_that_agree() { + let today = ChainConfig::with_security(2, 4, 4, 128, GrindBits::uniform(20)); + let arm = first_fold(6, 4); + for n in 0..=4 { + assert_eq!(today.schedule(n), arm.schedule(n), "n={n}"); + } + assert_eq!( + ( + today.log_blowup, + today.log_folding, + today.num_queries, + today.grind + ), + (arm.log_blowup, arm.log_folding, arm.num_queries, arm.grind), + "the two configs must differ ONLY in the fold schedule" + ); + assert_ne!(today.format.folds, arm.format.folds); + + let elf = digest(0x31); + let table_counts = counts(); + let table_num_vars = [4u8, 3, 4]; + let (root_bytes, _) = root(0x77); + let challenge = |config: &ChainConfig| { + host_epoch_challenge_under( + config, + &elf, + 1, + &[], + &table_counts, + &table_num_vars, + &root_bytes, + ) + }; + assert_ne!(challenge(&today), challenge(&arm)); + + let global = |config: &ChainConfig| { + let mut t = HostTranscript::new(&[]); + crate::multilinear_continuation::absorb_global( + &mut t, + &elf, + 2, + 0, + &[0x1000], + &table_num_vars, + config, + ); + t.sample_field_element() + }; + assert_ne!(global(&today), global(&arm)); + + // And the monolithic statement, the third host site. + let monolithic = |config: &ChainConfig| { + let mut t = HostTranscript::new(&[]); + crate::multilinear_prove::absorb( + &mut t, + &elf, + &[], + &table_counts, + 0, + &[], + &table_num_vars, + config, + ); + t.sample_field_element() + }; + assert_ne!(monolithic(&today), monolithic(&arm)); +} diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 49c6f23e8..66db8cf3c 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -34,6 +34,7 @@ pub mod test_utils; pub mod tests; pub mod whir_hash_knob; pub mod whir_identity; +pub mod zf_format; // The lib's test harness runs the allocator the shipped binary runs // (`bin/cli/src/main.rs` installs the same one), so every host-memory number a @@ -53,7 +54,6 @@ use crypto::fiat_shamir::is_transcript::IsTranscript; use executor::elf::Elf; use executor::vm::execution::Executor; use math::field::element::FieldElement; -use stark::lookup::LazyCommitment; use stark::prover::IsStarkProver; #[cfg(feature = "disk-spill")] use stark::storage_mode::StorageMode; @@ -1030,11 +1030,13 @@ impl VmAirs { // own preprocessed commitment first. Box::new(create_bitwise_air(proof_options)) } else { - Box::new(create_bitwise_air(proof_options).with_preprocessed_columns( - bitwise::preprocessed_commitment(proof_options), - bitwise::NUM_PRECOMPUTED_COLS, - Arc::new(bitwise::preprocessed_columns), - )) + Box::new( + create_bitwise_air(proof_options).with_lazy_preprocessed_columns( + bitwise::lazy_commitment(proof_options), + bitwise::NUM_PRECOMPUTED_COLS, + Arc::new(bitwise::preprocessed_columns), + ), + ) }; let lts: Vec<_> = (0..table_counts.lt) .map(|i| { @@ -1073,16 +1075,10 @@ impl VmAirs { // Deferred: the commitment is an LDE and a Merkle tree over the // program's whole instruction table, and only the univariate path // compares it — the multilinear one checks the columns instead. - let decode_root = match decode_commitment { - Some(commitment) => LazyCommitment::ready(commitment), - None => { - let instructions = instructions.clone(); - let options = proof_options.clone(); - LazyCommitment::deferred(move || { - decode::compute_precomputed_commitment(&instructions, &options) - }) - } - }; + // Both leaf layouts (S2): the one-row root is computed on first + // use, never taken from a supplied row-pair root. + let decode_root = + decode::lazy_commitment(instructions.clone(), proof_options, decode_commitment); Box::new( create_decode_air(proof_options).with_lazy_preprocessed_columns( decode_root, @@ -1133,8 +1129,8 @@ impl VmAirs { // without the generator it cannot tell a real preprocessed table from a // forged one. The univariate path ignores the extra argument. let keccak_rc: VmAir = Box::new( - create_keccak_rc_air(proof_options).with_preprocessed_columns( - tables::keccak_rc::preprocessed_commitment(proof_options), + create_keccak_rc_air(proof_options).with_lazy_preprocessed_columns( + tables::keccak_rc::lazy_commitment(proof_options), tables::keccak_rc::NUM_PRECOMPUTED_COLS, Arc::new(tables::keccak_rc::preprocessed_columns), ), @@ -1170,11 +1166,12 @@ impl VmAirs { // epoch and through `verify_epochs`. The univariate path is // unaffected either way — it compares the root and never calls // `precomputed_columns()`. + let root = register::lazy_commitment_with_fini(proof_options, commitment, init, fini); let init = init.to_vec(); let fini = fini.to_vec(); Box::new( - create_register_air(proof_options).with_preprocessed_columns( - commitment, + create_register_air(proof_options).with_lazy_preprocessed_columns( + root, register::NUM_PREPROCESSED_COLS_WITH_FINI, Arc::new(move || register::preprocessed_columns_with_fini(&init, &fini)), ), @@ -1183,9 +1180,9 @@ impl VmAirs { let register_init = register_init .map(<[u32]>::to_vec) .unwrap_or_else(|| register::register_init_from_entry_point(elf.entry_point)); - let commitment = register::preprocessed_commitment(proof_options, ®ister_init); + let commitment = register::lazy_commitment(proof_options, ®ister_init); Box::new( - create_register_air(proof_options).with_preprocessed_columns( + create_register_air(proof_options).with_lazy_preprocessed_columns( commitment, register::NUM_PREPROCESSED_COLS, Arc::new(move || register::preprocessed_columns(®ister_init)), @@ -1219,16 +1216,16 @@ impl VmAirs { // Committing OFFSET alone publishes nothing: it is the dense // `0..page_size-1` enumeration, byte-identical for every page // regardless of program or input. - Box::new(air.with_preprocessed_columns( - page::private_page_preprocessed_commitment(proof_options), + Box::new(air.with_lazy_preprocessed_columns( + page::private_page_lazy_commitment(proof_options), page::NUM_PREPROCESSED_COLS_PRIVATE, Arc::new(|| vec![page::offset_column()]), )) } else if config.init_values.is_none() { // Zero-init pages: the shared commitment computed once above. let config = config.clone(); - Box::new(air.with_preprocessed_columns( - zero_init_commitment, + Box::new(air.with_lazy_preprocessed_columns( + page::zero_init_lazy_commitment_from(zero_init_commitment, proof_options), page::NUM_PREPROCESSED_COLS, Arc::new(move || page::preprocessed_columns(&config)), )) @@ -1238,18 +1235,13 @@ impl VmAirs { // (recursion guest); otherwise recompute from the ELF. // Deferred when it has to be computed: two dozen pages of // LDE and Merkle that only the univariate path compares. - let commitment = page_commitments + let supplied = page_commitments .unwrap_or(&[]) .iter() .find(|(pb, _)| *pb == config.page_base) - .map(|(_, c)| LazyCommitment::ready(*c)) - .unwrap_or_else(|| { - let config = config.clone(); - let options = proof_options.clone(); - LazyCommitment::deferred(move || { - page::compute_precomputed_commitment(&config, &options) - }) - }); + .map(|(_, c)| *c); + let commitment = + page::data_page_lazy_commitment(config, proof_options, supplied); let config = config.clone(); Box::new(air.with_lazy_preprocessed_columns( commitment, @@ -1387,20 +1379,35 @@ pub(crate) fn compute_commit_bus_offset( /// Generic over the transcript for the same reason as `absorb_lfm_statement`: /// the replay is `append_bytes` plus `sample_field_element`, both on /// `IsTranscript`, so it is the same replay under any sponge. +/// +/// ★ The preprocessed root absorbed is the one of the table's LEAF LAYOUT +/// (S2), resolved exactly as the STARK prover and verifier resolve it — +/// `stark::leaf_layout::table_leaf_layout(air, proof.trace_length())`, then +/// `air.precomputed_commitment_for(layout)` — because that is the root the +/// prover absorbed before sampling `z` and `α`. Absorbing the row-pair root +/// for a one-row table replays a different transcript: the recovered `z`, `α` +/// differ from the prover's, so every expected bus balance that depends on +/// them (the LFM public words, the VM commit bus) is wrong and an honest proof +/// is rejected. At the default format every layout is row pairs and this is +/// the row-pair root, byte for byte what was absorbed before. +/// +/// `None` = a preprocessed table has no root for its layout (never recomputed): the +/// caller rejects, exactly as the STARK verifier would. pub(crate) fn replay_transcript_phase_a_view<'p>( airs: &[&dyn AIR], proofs: impl ProofViewSource<'p, F, E, ()>, transcript: &mut impl IsTranscript, -) -> (FieldElement, FieldElement) { +) -> Option<(FieldElement, FieldElement)> { for (air, proof) in airs.iter().zip(proofs.view_iter()) { if air.is_preprocessed() { - transcript.append_bytes(&air.precomputed_commitment()); + let layout = stark::leaf_layout::table_leaf_layout(*air, proof.trace_length()); + transcript.append_bytes(&air.precomputed_commitment_for(layout)?); } transcript.append_bytes(proof.lde_trace_main_merkle_root()); } let z: FieldElement = transcript.sample_field_element(); let alpha: FieldElement = transcript.sample_field_element(); - (z, alpha) + Some((z, alpha)) } /// Computes the expected COMMIT bus balance for a proof view slice (owned or @@ -1415,7 +1422,7 @@ pub(crate) fn compute_expected_commit_bus_balance_view<'p>( // TYPE rather than the same type over a different digest. transcript: &mut impl crypto::fiat_shamir::is_transcript::IsTranscript, ) -> Option> { - let (z, alpha) = replay_transcript_phase_a_view(airs, proofs, transcript); + let (z, alpha) = replay_transcript_phase_a_view(airs, proofs, transcript)?; compute_commit_bus_offset(public_output_bytes, start_index, &z, &alpha) } diff --git a/prover/src/multilinear_continuation.rs b/prover/src/multilinear_continuation.rs index 7517704c4..3274bdf17 100644 --- a/prover/src/multilinear_continuation.rs +++ b/prover/src/multilinear_continuation.rs @@ -25,7 +25,7 @@ use crypto::fiat_shamir::is_transcript::IsTranscript; use executor::elf::Elf; use math::field::element::FieldElement; use multilinear::mle::Mle; -use multilinear::whir_chain::ChainConfig; +use multilinear::whir_chain::{ChainConfig, WhirFolds}; use stark::config::Commitment; use stark::multilinear_table::{ self, CommittedTable, CommittedTables, MultiProof, TableLayout, TableStatement, @@ -171,6 +171,9 @@ where /// The parameters it was committed under — see [`Self::agrees_with`]. log_blowup: usize, log_folding: usize, + /// The fold schedule (W2): its first round sets the leaf width tree 0 was + /// built at. + folds: WhirFolds, } impl DecodePrepared @@ -219,15 +222,20 @@ where /// exactly as long as that holds, and it is ASSERTED per epoch rather than /// assumed, because the day a blowup becomes shape-dependent this is the /// line that says so instead of a proof nobody can verify. + /// + /// ★ AND THE FOLD SCHEDULE (W2). `commit_stacked` blocks tree 0's leaves at + /// `config.schedule(n)[0]`, which under a first fold is `k0`, not + /// `log_folding`. The schedule is a function of `(log_folding, folds, n)` + /// and `n` is the commitment's own, so equal policies mean an equal first + /// fold; a commitment built at `first6` has 64-wide leaves that a + /// `uniform4` epoch would open as 16-wide ones. pub(crate) fn agrees_with(&self, config: &ChainConfig) -> Result<(), Error> { - if (config.log_blowup, config.log_folding) != (self.log_blowup, self.log_folding) { - return Err(Error::Prover(format!( - "the pinned DECODE commitment was built at blowup {} / folding {}, \ - and this epoch argues at blowup {} / folding {}", - self.log_blowup, self.log_folding, config.log_blowup, config.log_folding, - ))); - } - Ok(()) + committed_under( + "the pinned DECODE commitment was built", + "this epoch argues", + (self.log_blowup, self.log_folding, self.folds), + config, + ) } /// What the verifier settles the opening against. @@ -299,6 +307,9 @@ where /// The parameters it was committed under — see [`Self::agrees_with`]. log_blowup: usize, log_folding: usize, + /// The fold schedule (W2): its first round sets the leaf width tree 0 was + /// built at. + folds: WhirFolds, } impl GenesisPrepared @@ -349,15 +360,15 @@ where /// [`DecodePrepared::agrees_with`] stops being vacuous for DECODE too. /// `StackedCommitment::commit` reads these two and never `num_queries`, so /// they are the whole of what a cached commitment must agree on. + /// The fold schedule is part of it for the reason + /// [`DecodePrepared::agrees_with`] gives. pub(crate) fn agrees_with(&self, config: &ChainConfig) -> Result<(), Error> { - if (config.log_blowup, config.log_folding) != (self.log_blowup, self.log_folding) { - return Err(Error::Prover(format!( - "the genesis stack was committed at blowup {} / folding {}, and this \ - cross-epoch proof argues at blowup {} / folding {}", - self.log_blowup, self.log_folding, config.log_blowup, config.log_folding, - ))); - } - Ok(()) + committed_under( + "the genesis stack was committed", + "this cross-epoch proof argues", + (self.log_blowup, self.log_folding, self.folds), + config, + ) } /// What an EMITTER needs to build the prepared leg, taken from the very @@ -379,6 +390,7 @@ where domain: self.commitment.domain().clone(), log_blowup: self.log_blowup, log_folding: self.log_folding, + folds: self.folds, } } } @@ -418,6 +430,8 @@ pub struct GlobalPrepared { pub domain: multilinear::whir::Domain, pub log_blowup: usize, pub log_folding: usize, + /// The fold schedule it was committed under (W2). + pub folds: WhirFolds, } impl GlobalPrepared { @@ -435,15 +449,35 @@ impl GlobalPrepared { /// The same assertion [`GenesisPrepared::agrees_with`] makes, for a consumer /// that holds the published form rather than the commitment. pub fn agrees_with(&self, config: &ChainConfig) -> Result<(), Error> { - if (config.log_blowup, config.log_folding) != (self.log_blowup, self.log_folding) { - return Err(Error::Prover(format!( - "the genesis stack was committed at blowup {} / folding {}, and this \ - program is emitted against blowup {} / folding {}", - self.log_blowup, self.log_folding, config.log_blowup, config.log_folding, - ))); - } - Ok(()) + committed_under( + "the genesis stack was committed", + "this program is emitted against", + (self.log_blowup, self.log_folding, self.folds), + config, + ) + } +} + +/// The one comparison every `agrees_with` makes: a commitment built once and +/// reused must have been built under the blowup, the fold width AND the fold +/// schedule the proof argues at — the three things `StackedCommitment::commit` +/// reads (it never reads `num_queries`). +fn committed_under( + built: &str, + argues: &str, + (log_blowup, log_folding, folds): (usize, usize, WhirFolds), + config: &ChainConfig, +) -> Result<(), Error> { + if (config.log_blowup, config.log_folding, config.format.folds) + != (log_blowup, log_folding, folds) + { + return Err(Error::Prover(format!( + "{built} at blowup {log_blowup} / folding {log_folding} / folds {folds:?}, and \ + {argues} at blowup {} / folding {} / folds {:?}", + config.log_blowup, config.log_folding, config.format.folds, + ))); } + Ok(()) } /// The genesis stack for a page family, or `None` when nothing is dense enough @@ -547,6 +581,7 @@ where commitment, log_blowup: config.log_blowup, log_folding: config.log_folding, + folds: config.format.folds, })) } @@ -641,6 +676,7 @@ where commitment, log_blowup: config.log_blowup, log_folding: config.log_folding, + folds: config.format.folds, }) } @@ -757,11 +793,17 @@ pub(crate) fn absorb_epoch( let &ChainConfig { log_blowup, - log_folding, + // ★ Absorbed as `fold_word()`: `log_folding` itself (4u64) at the + // default fold schedule, a tagged word binding the schedule otherwise. + log_folding: _, num_queries, grind, + // ⚠ Format, NOT absorbed except the fold schedule, through the word + // above: verifier-side constants (see `lfm::whir_statement::push_config`, + // the emitter's twin of this). + format: _, } = config; - for value in [log_blowup as u64, log_folding as u64, num_queries as u64] { + for value in [log_blowup as u64, config.fold_word(), num_queries as u64] { t.append_bytes(&value.to_le_bytes()); len += size_of_val(&value); } @@ -919,11 +961,17 @@ pub(crate) fn absorb_global( len += table_num_vars.len(); let &ChainConfig { log_blowup, - log_folding, + // ★ Absorbed as `fold_word()`: `log_folding` itself (4u64) at the + // default fold schedule, a tagged word binding the schedule otherwise. + log_folding: _, num_queries, grind, + // ⚠ Format, NOT absorbed except the fold schedule, through the word + // above: verifier-side constants (see `lfm::whir_statement::push_config`, + // the emitter's twin of this). + format: _, } = config; - for value in [log_blowup as u64, log_folding as u64, num_queries as u64] { + for value in [log_blowup as u64, config.fold_word(), num_queries as u64] { t.append_bytes(&value.to_le_bytes()); len += size_of_val(&value); } diff --git a/prover/src/multilinear_prove.rs b/prover/src/multilinear_prove.rs index 8bf7cd71d..25f90f2df 100644 --- a/prover/src/multilinear_prove.rs +++ b/prover/src/multilinear_prove.rs @@ -84,13 +84,38 @@ pub struct MultilinearVmProof { /// The query count comes from the tallest stacked polynomial in the proof, so /// one config covers every table: a taller stack means more rounds, and more /// rounds is what the union bound charges for. +/// +/// ★ A PRODUCTION FORMAT SITE: the process's +/// [`ZfFormat`](crate::zf_format::ZfFormat) WHIR fields (`LAMBDA_VM_ZF_WHIR_CAP`, +/// `_WHIR_FOLDS`) are stamped on here. Unset knobs give +/// [`ZfFormat::DEFAULT`](crate::zf_format::ZfFormat::DEFAULT)'s WHIR fields +/// (`whir_cap=auto`, `whir_folds=first6`); both knobs at their off spellings +/// give the legacy config. pub fn chain_config(shapes: &[Shape]) -> ChainConfig { + chain_config_under(crate::zf_format::ZfFormat::global(), shapes) +} + +/// [`chain_config`] under an explicit format, so a test can build a knob-on +/// production config without setting the environment. +/// +/// The query count is charged the fold schedule's worst round count +/// (`with_security_folds`): the schedule is part of the security accounting, +/// not a label stamped on afterwards. +pub fn chain_config_under(format: &crate::zf_format::ZfFormat, shapes: &[Shape]) -> ChainConfig { let tallest = shapes .iter() .map(|&(width, num_vars)| multilinear::constraint_argument::one_stack(num_vars, width)) .max() .unwrap_or(1); - ChainConfig::with_security(2, 4, tallest, 128, GrindBits::uniform(20)) + let config = ChainConfig::with_security_folds( + 2, + crate::zf_format::PRODUCTION_WHIR_LOG_FOLDING, + format.whir_folds, + tallest, + 128, + GrindBits::uniform(20), + ); + format.chain(config) } /// Binds the statement into the transcript before any challenge is drawn. @@ -153,11 +178,17 @@ pub(crate) fn absorb( // not only in the code. let &ChainConfig { log_blowup, - log_folding, + // ★ Absorbed as `fold_word()`: `log_folding` itself (4u64) at the + // default fold schedule, a tagged word binding the schedule otherwise. + log_folding: _, num_queries, grind, + // ⚠ Format, NOT absorbed except the fold schedule, through the word + // above: verifier-side constants (see `lfm::whir_statement::push_config`, + // the emitter's twin of this). + format: _, } = config; - for value in [log_blowup as u64, log_folding as u64, num_queries as u64] { + for value in [log_blowup as u64, config.fold_word(), num_queries as u64] { t.append_bytes(&value.to_le_bytes()); len += size_of_val(&value); } diff --git a/prover/src/recursion.rs b/prover/src/recursion.rs index d929f6f49..ab7065a36 100644 --- a/prover/src/recursion.rs +++ b/prover/src/recursion.rs @@ -42,6 +42,9 @@ pub const MIN_PROOF_OPTIONS: ProofOptions = ProofOptions { coset_offset: 3, grinding_factor: 1, fri_final_poly_log_degree: 7, + // The RV64 guest verifies the LEGACY format, named here rather + // than inherited from a default. + format: stark::proof::options::ProofFormat::LEGACY, }; /// The recursion verifier's build presets. Each fixes the guest's @@ -73,8 +76,14 @@ impl Preset { ]; /// The fixed `ProofOptions` this preset's guest verifies with. + /// + /// ★ Always the LEGACY proof format ([`ProofFormat::LEGACY`](stark::proof::options::ProofFormat::LEGACY)), + /// stamped explicitly: the RV64 guest's archived verifier is + /// not threaded with the ZF format levers, so its presets name the format + /// it was built for instead of inheriting the process's production format + /// ([`crate::zf_format::ZfFormat::DEFAULT`]). pub fn options(&self) -> ProofOptions { - match self { + let mut options = match self { Preset::Min => MIN_PROOF_OPTIONS, Preset::Blowup2 => crate::GoldilocksCubicProofOptions::with_blowup(2) .expect("blowup=2 is always valid"), @@ -82,7 +91,9 @@ impl Preset { .expect("blowup=4 is always valid"), Preset::Blowup8 => crate::GoldilocksCubicProofOptions::with_blowup(8) .expect("blowup=8 is always valid"), - } + }; + options.format = stark::proof::options::ProofFormat::LEGACY; + options } /// Artifact stem under `executor/program_artifacts/recursion/` @@ -265,6 +276,23 @@ pub fn program_id_from_elf( )) } +/// The RV64 recursion guest verifies the LEGACY proof format only: its +/// presets fix the options at build time and +/// name the legacy format, and the archived verifier it runs is not threaded +/// with the ZF format levers. Any other format — including the production +/// default [`crate::zf_format::ZfFormat::DEFAULT`] — must never reach it, so +/// both guest entry points refuse one up front instead of verifying a proof +/// under a format the guest was not built for. +fn require_legacy_format(proof_options: &ProofOptions) -> Result<(), Error> { + if proof_options.has_legacy_format() { + Ok(()) + } else { + Err(Error::Execution(String::from( + "the recursion guest verifies legacy-format proofs only (every ZF format lever off)", + ))) + } +} + /// Verify the guest's private-input blob ([`encode_guest_input`]) in place and, /// on success, produce the attestation bytes the recursion guest commits: /// `program_id(elf, roots) || inner_public_output`. `Ok(None)` means the @@ -279,6 +307,7 @@ pub fn verify_and_attest_blob( blob: &[u8], proof_options: &ProofOptions, ) -> Result>, Error> { + require_legacy_format(proof_options)?; let verification = crate::verify_recursion_blob(blob, proof_options)?; if !verification.ok { return Ok(None); @@ -314,6 +343,8 @@ pub fn verify_continuation_and_attest( ) -> Result>, Error> { use rkyv::rancor::Error as RkyvError; + require_legacy_format(proof_options)?; + let archive_bytes = crate::recursion_archive_bytes(blob).ok_or_else(|| { Error::Execution(String::from( "continuation recursion blob: bad magic or version", diff --git a/prover/src/tables/bitwise.rs b/prover/src/tables/bitwise.rs index 37cf591fd..da9d946fc 100644 --- a/prover/src/tables/bitwise.rs +++ b/prover/src/tables/bitwise.rs @@ -27,6 +27,7 @@ use math::polynomial::Polynomial; use stark::config::Commitment; +use stark::leaf_layout::LeafLayout; use stark::lookup::{BusInteraction, BusValue, Multiplicity, Packing}; use stark::proof::options::ProofOptions; use stark::prover::evaluate_polynomial_on_lde_domain; @@ -215,6 +216,23 @@ fn static_commitment(blowup_factor: u8) -> Option { } } +/// The ONE-ROW (S2) twin of [`static_commitment`]: the same columns committed +/// with one LDE row per leaf ([`LeafLayout::Row`]), per `blowup_factor` in +/// [`crate::tables::STATIC_BLOWUP_FACTORS_ONE_ROW`], generated by +/// `compute_static_commitments --layout row` and pinned by the one-row drift +/// test. The same regeneration rules as [`static_commitment`]. A blowup with +/// no arm here is a hard miss under one row: no recompute. +pub(crate) fn static_commitment_one_row(blowup_factor: u8) -> Option { + match blowup_factor { + 4 => Some([ + 0x34, 0x22, 0x21, 0x59, 0xc3, 0xe7, 0x92, 0x18, 0xb5, 0xf0, 0x3b, 0xd8, 0x73, 0x37, + 0xd8, 0x33, 0x62, 0xbb, 0xea, 0xe4, 0x1f, 0x1e, 0x0a, 0x15, 0x59, 0x19, 0x8c, 0xf2, + 0x13, 0xd4, 0x82, 0x09, + ]), + _ => None, + } +} + /// The precomputed columns themselves, one per column, `NUM_ROWS` tall. /// /// The multilinear path checks a proof's claimed openings against these instead @@ -392,6 +410,16 @@ where /// shortcut is used when applicable. #[doc(hidden)] pub fn compute_preprocessed_commitment(options: &ProofOptions) -> Commitment { + compute_preprocessed_commitment_with(options, LeafLayout::RowPair) +} + +/// [`compute_preprocessed_commitment`] under an explicit trace-tree leaf +/// layout (the generator and the one-row drift test; S2). +#[doc(hidden)] +pub fn compute_preprocessed_commitment_with( + options: &ProofOptions, + layout: LeafLayout, +) -> Commitment { let columns = preprocessed_columns(); // Interpolate each column to a polynomial (parallel) @@ -441,7 +469,7 @@ pub fn compute_preprocessed_commitment(options: &ProofOptions) -> Commitment { // the hash that path commits under — on a branch that pins an algebraic // hash, a root left on the alias would be the one BLAKE3 artifact in an RPO // proof, and it would fail as a root nothing reconstructs. - crate::lfm::commit::commit_lde_columns(&lde_columns) + crate::lfm::commit::commit_lde_columns_with(&lde_columns, layout) } /// Returns the preprocessed commitment for the bitwise table. @@ -466,6 +494,31 @@ pub fn preprocessed_commitment(options: &ProofOptions) -> Commitment { compute_preprocessed_commitment(options) } +/// The preprocessed commitment under the table's resolved leaf `layout`: +/// today's [`preprocessed_commitment`] for row pairs; for one row the static +/// twin ([`static_commitment_one_row`]) at coset 3, and `None` otherwise — a +/// hard miss the prover refuses and the verifier rejects, never a +/// recompute of a 2^20-row table behind the operator's back. +pub fn preprocessed_commitment_for( + options: &ProofOptions, + layout: LeafLayout, +) -> Option { + match layout { + LeafLayout::RowPair => Some(preprocessed_commitment(options)), + LeafLayout::Row => (options.coset_offset == 3) + .then(|| static_commitment_one_row(options.blowup_factor)) + .flatten(), + } +} + +/// The AIR's commitment source for both leaf layouts: today's root now, the +/// one-row twin on demand ([`preprocessed_commitment_for`]). +pub fn lazy_commitment(options: &ProofOptions) -> stark::lookup::LazyCommitment { + let o = options.clone(); + stark::lookup::LazyCommitment::ready(preprocessed_commitment(options)) + .with_one_row(move || preprocessed_commitment_for(&o, LeafLayout::Row)) +} + // ========================================================================= // Trace generation // ========================================================================= diff --git a/prover/src/tables/decode.rs b/prover/src/tables/decode.rs index 09a8c5eb3..1ababdf93 100644 --- a/prover/src/tables/decode.rs +++ b/prover/src/tables/decode.rs @@ -36,6 +36,7 @@ use executor::vm::instruction::decoding::{Instruction, InstructionError}; use executor::vm::memory::U64HashMap; use math::polynomial::Polynomial; use stark::config::Commitment; +use stark::leaf_layout::LeafLayout; use stark::lookup::{BusInteraction, BusValue, Multiplicity, Packing}; use stark::proof::options::ProofOptions; use stark::prover::evaluate_polynomial_on_lde_domain; @@ -312,6 +313,17 @@ pub fn preprocessed_columns_from_elf(elf: &Elf) -> Result>, Instruct pub fn compute_precomputed_commitment( instructions: &U64HashMap, options: &ProofOptions, +) -> Commitment { + compute_precomputed_commitment_with(instructions, options, LeafLayout::RowPair) +} + +/// [`compute_precomputed_commitment`] under an explicit trace-tree leaf layout +/// (S2). DECODE is program-dependent, so a one-row DECODE root is computed at +/// run time, like the row-pair one. +pub fn compute_precomputed_commitment_with( + instructions: &U64HashMap, + options: &ProofOptions, + layout: LeafLayout, ) -> Commitment { let columns = preprocessed_columns(instructions); let num_rows = columns[0].len(); @@ -341,7 +353,35 @@ pub fn compute_precomputed_commitment( // commitment the prover recomputes and compares against, so building it with // a different hash than the path commits under fails at prove time with // `PrecomputedCommitmentMismatch` — which is exactly how it was found. - crate::lfm::commit::commit_lde_columns(&lde_columns) + crate::lfm::commit::commit_lde_columns_with(&lde_columns, layout) +} + +/// DECODE's commitment source for both leaf layouts: the row-pair root +/// `supplied` by the caller (the recursion guest's) or computed on first use, +/// and the one-row root computed on first use (program-dependent: no static +/// twin; a supplied root never stands in for the other layout). +pub fn lazy_commitment( + instructions: std::sync::Arc>, + options: &ProofOptions, + supplied: Option, +) -> stark::lookup::LazyCommitment { + let base = match supplied { + Some(c) => stark::lookup::LazyCommitment::ready(c), + None => { + let (instructions, options) = (instructions.clone(), options.clone()); + stark::lookup::LazyCommitment::deferred(move || { + compute_precomputed_commitment(&instructions, &options) + }) + } + }; + let options = options.clone(); + base.with_one_row(move || { + Some(compute_precomputed_commitment_with( + &instructions, + &options, + LeafLayout::Row, + )) + }) } // ========================================================================= diff --git a/prover/src/tables/keccak_rc.rs b/prover/src/tables/keccak_rc.rs index f97c1e286..a036aee04 100644 --- a/prover/src/tables/keccak_rc.rs +++ b/prover/src/tables/keccak_rc.rs @@ -10,6 +10,7 @@ use math::polynomial::Polynomial; use stark::config::Commitment; +use stark::leaf_layout::LeafLayout; use stark::lookup::{BusInteraction, BusValue, Multiplicity, Packing}; use stark::proof::options::ProofOptions; use stark::prover::evaluate_polynomial_on_lde_domain; @@ -114,6 +115,23 @@ fn static_commitment(blowup_factor: u8) -> Option { } } +/// The ONE-ROW (S2) twin of [`static_commitment`]: the same columns committed with one +/// LDE row per leaf ([`LeafLayout::Row`]), per `blowup_factor` in +/// [`crate::tables::STATIC_BLOWUP_FACTORS_ONE_ROW`], generated by +/// `compute_static_commitments --layout row` and pinned by the one-row drift +/// test. The same regeneration rules as [`static_commitment`]. A blowup with no arm here +/// is a hard miss under one row: no recompute. +pub(crate) fn static_commitment_one_row(blowup_factor: u8) -> Option { + match blowup_factor { + 4 => Some([ + 0xe6, 0x9e, 0xfc, 0xec, 0x0d, 0x6f, 0x04, 0x22, 0xfc, 0xfe, 0x7c, 0x8b, 0x44, 0xcd, + 0x6d, 0x60, 0x27, 0x6f, 0x3b, 0x9a, 0x78, 0xf6, 0x89, 0x7b, 0x26, 0x40, 0x4b, 0x2a, + 0x65, 0x6d, 0x48, 0x79, + ]), + _ => None, + } +} + /// The precomputed columns themselves, one per column, `NUM_ROWS` tall. /// /// The multilinear path checks a proof's claimed openings against these instead @@ -137,6 +155,16 @@ pub fn preprocessed_columns() -> Vec> { /// shortcut is used when applicable. #[doc(hidden)] pub fn compute_preprocessed_commitment(options: &ProofOptions) -> Commitment { + compute_preprocessed_commitment_with(options, LeafLayout::RowPair) +} + +/// [`compute_preprocessed_commitment`] under an explicit trace-tree leaf +/// layout (the generator and the one-row drift test; S2). +#[doc(hidden)] +pub fn compute_preprocessed_commitment_with( + options: &ProofOptions, + layout: LeafLayout, +) -> Commitment { let columns = preprocessed_columns(); // Interpolate each column to a polynomial @@ -165,7 +193,7 @@ pub fn compute_preprocessed_commitment(options: &ProofOptions) -> Commitment { // the hash that path commits under — on a branch that pins an algebraic // hash, a root left on the alias would be the one BLAKE3 artifact in an RPO // proof, and it would fail as a root nothing reconstructs. - crate::lfm::commit::commit_lde_columns(&lde_columns) + crate::lfm::commit::commit_lde_columns_with(&lde_columns, layout) } /// Returns the preprocessed commitment for the keccak_rc table. @@ -191,6 +219,30 @@ pub fn preprocessed_commitment(options: &ProofOptions) -> Commitment { compute_preprocessed_commitment(options) } +/// The preprocessed commitment under the table's resolved leaf `layout`: +/// today's [`preprocessed_commitment`] for row pairs; for one row the static +/// twin ([`static_commitment_one_row`]) at coset 3, and `None` otherwise (a +/// hard miss, never a recompute). +pub fn preprocessed_commitment_for( + options: &ProofOptions, + layout: LeafLayout, +) -> Option { + match layout { + LeafLayout::RowPair => Some(preprocessed_commitment(options)), + LeafLayout::Row => (options.coset_offset == 3) + .then(|| static_commitment_one_row(options.blowup_factor)) + .flatten(), + } +} + +/// The AIR's commitment source for both leaf layouts: today's root now, the +/// one-row twin on demand ([`preprocessed_commitment_for`]). +pub fn lazy_commitment(options: &ProofOptions) -> stark::lookup::LazyCommitment { + let o = options.clone(); + stark::lookup::LazyCommitment::ready(preprocessed_commitment(options)) + .with_one_row(move || preprocessed_commitment_for(&o, LeafLayout::Row)) +} + // ========================================================================= // Trace generation // ========================================================================= diff --git a/prover/src/tables/mod.rs b/prover/src/tables/mod.rs index 910388111..8835e39d0 100644 --- a/prover/src/tables/mod.rs +++ b/prover/src/tables/mod.rs @@ -61,6 +61,14 @@ pub use types::BusId; /// silently skip a test. pub const STATIC_BLOWUP_FACTORS: &[u8] = &[2, 4, 8]; +/// Blowup factors for which the ONE-ROW (S2) twins of those static +/// commitments ship (`static_commitment_one_row` and the page twins), emitted +/// by `compute_static_commitments --layout row` and pinned by the one-row drift +/// tests. Only the blowup the knob is measured at (4 for +/// the base and for the LFM chips): under one row any other blowup is a hard +/// miss, never a recompute. +pub const STATIC_BLOWUP_FACTORS_ONE_ROW: &[u8] = &[4]; + /// Per-table maximum rows, sized so each chunk uses roughly the same memory. /// /// Effective width = main_cols + 3 × bus_interactions (extension field = 3× cost). diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index fadf1c5ee..ac53351d3 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -34,6 +34,7 @@ use std::collections::HashMap; use math::polynomial::Polynomial; use stark::config::Commitment; +use stark::leaf_layout::LeafLayout; use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; use stark::proof::options::ProofOptions; use stark::prover::evaluate_polynomial_on_lde_domain; @@ -428,6 +429,23 @@ pub(crate) fn static_zero_page_commitment(blowup_factor: u8) -> Option Option { + match blowup_factor { + 4 => Some([ + 0x19, 0x19, 0x77, 0x25, 0x76, 0x36, 0xfc, 0x2e, 0xa9, 0xaf, 0xb5, 0x0a, 0x11, 0x93, + 0xe7, 0x8f, 0xe2, 0x58, 0x38, 0x7a, 0x36, 0x4d, 0xf5, 0xad, 0x72, 0x91, 0x2d, 0x43, + 0xee, 0xca, 0xfa, 0xe4, + ]), + _ => None, + } +} + /// Static OFFSET-only commitments for private-input pages, per `blowup_factor`. /// /// Same provenance, regeneration rules and drift-test protection as @@ -454,6 +472,23 @@ pub(crate) fn static_private_page_commitment(blowup_factor: u8) -> Option Option { + match blowup_factor { + 4 => Some([ + 0x59, 0x6a, 0x3c, 0xc9, 0x79, 0x61, 0x9d, 0x33, 0xa2, 0xcc, 0xfc, 0xba, 0xc3, 0xd9, + 0x6f, 0x84, 0xb0, 0x4a, 0x48, 0x88, 0x87, 0x5d, 0x37, 0x18, 0xa3, 0xfb, 0xd7, 0xce, + 0x2f, 0x5d, 0x96, 0x39, + ]), + _ => None, + } +} + /// Computes the Merkle root commitment over the LDE of PAGE precomputed columns. /// /// The commitment covers OFFSET (0..page_size-1) and INIT (from config). @@ -463,7 +498,23 @@ pub(crate) fn static_private_page_commitment(blowup_factor: u8) -> Option Commitment { - commit_preprocessed_columns(&preprocessed_columns(config), DEFAULT_PAGE_SIZE, options) + compute_precomputed_commitment_with(config, options, LeafLayout::RowPair) +} + +/// [`compute_precomputed_commitment`] under an explicit trace-tree leaf layout +/// (S2). ELF data pages have no static root, so a one-row data page computes +/// this at run time. +pub fn compute_precomputed_commitment_with( + config: &PageConfig, + options: &ProofOptions, + layout: LeafLayout, +) -> Commitment { + commit_preprocessed_columns( + &preprocessed_columns(config), + DEFAULT_PAGE_SIZE, + options, + layout, + ) } /// The precomputed columns themselves, `DEFAULT_PAGE_SIZE` tall. @@ -514,6 +565,7 @@ fn commit_preprocessed_columns( columns: &[Vec], num_rows: usize, options: &ProofOptions, + layout: LeafLayout, ) -> Commitment { let polys: Vec> = columns .iter() @@ -539,7 +591,7 @@ fn commit_preprocessed_columns( // the hash that path commits under — on a branch that pins an algebraic // hash, a root left on the alias would be the one BLAKE3 artifact in an RPO // proof, and it would fail as a root nothing reconstructs. - crate::lfm::commit::commit_lde_columns(&lde_columns) + crate::lfm::commit::commit_lde_columns_with(&lde_columns, layout) } /// Commitment over the OFFSET column **alone** — the preprocessed anchor for @@ -556,12 +608,20 @@ fn commit_preprocessed_columns( /// Memory-bus address is `page_base_lo + OFFSET`, so a free OFFSET names an /// arbitrary address and forges that address's memory history. pub fn compute_offset_only_commitment(options: &ProofOptions) -> Commitment { + compute_offset_only_commitment_with(options, LeafLayout::RowPair) +} + +/// [`compute_offset_only_commitment`] under an explicit leaf layout (S2). +pub fn compute_offset_only_commitment_with( + options: &ProofOptions, + layout: LeafLayout, +) -> Commitment { let num_rows = DEFAULT_PAGE_SIZE; let mut offset_col = crate::tables::types::zeroed_fe_vec(num_rows); for (i, cell) in offset_col.iter_mut().enumerate() { *cell = FE::from(i as u64); } - commit_preprocessed_columns(&[offset_col], num_rows, options) + commit_preprocessed_columns(&[offset_col], num_rows, options, layout) } /// Returns the zero-init PAGE preprocessed commitment. @@ -612,6 +672,85 @@ pub fn private_page_preprocessed_commitment(options: &ProofOptions) -> Commitmen compute_offset_only_commitment(options) } +/// The zero-init PAGE commitment under the table's resolved leaf `layout`: +/// today's [`zero_init_preprocessed_commitment`] for row pairs; for one row the +/// static twin at coset 3, and `None` otherwise (a hard miss, never a recompute). +pub fn zero_init_preprocessed_commitment_for( + options: &ProofOptions, + layout: LeafLayout, +) -> Option { + match layout { + LeafLayout::RowPair => Some(zero_init_preprocessed_commitment(options)), + LeafLayout::Row => (options.coset_offset == 3) + .then(|| static_zero_page_commitment_one_row(options.blowup_factor)) + .flatten(), + } +} + +/// The private-input PAGE commitment under the table's resolved leaf `layout` +/// (see [`zero_init_preprocessed_commitment_for`]). +pub fn private_page_preprocessed_commitment_for( + options: &ProofOptions, + layout: LeafLayout, +) -> Option { + match layout { + LeafLayout::RowPair => Some(private_page_preprocessed_commitment(options)), + LeafLayout::Row => (options.coset_offset == 3) + .then(|| static_private_page_commitment_one_row(options.blowup_factor)) + .flatten(), + } +} + +/// The zero-init page's commitment source for both leaf layouts. +pub fn zero_init_lazy_commitment(options: &ProofOptions) -> stark::lookup::LazyCommitment { + zero_init_lazy_commitment_from(zero_init_preprocessed_commitment(options), options) +} + +/// [`zero_init_lazy_commitment`] with the row-pair root already in hand. +pub fn zero_init_lazy_commitment_from( + row_pair: Commitment, + options: &ProofOptions, +) -> stark::lookup::LazyCommitment { + let o = options.clone(); + stark::lookup::LazyCommitment::ready(row_pair) + .with_one_row(move || zero_init_preprocessed_commitment_for(&o, LeafLayout::Row)) +} + +/// The private-input page's (OFFSET-only) commitment source for both layouts. +pub fn private_page_lazy_commitment(options: &ProofOptions) -> stark::lookup::LazyCommitment { + let o = options.clone(); + stark::lookup::LazyCommitment::ready(private_page_preprocessed_commitment(options)) + .with_one_row(move || private_page_preprocessed_commitment_for(&o, LeafLayout::Row)) +} + +/// An ELF data page's commitment source: the row-pair root `supplied` by the +/// caller or computed on first use, and the one-row root computed on first use +/// (program-dependent, so there is no static twin; a supplied root is a +/// row-pair root and never stands in for the other layout). +pub fn data_page_lazy_commitment( + config: &PageConfig, + options: &ProofOptions, + supplied: Option, +) -> stark::lookup::LazyCommitment { + let base = match supplied { + Some(c) => stark::lookup::LazyCommitment::ready(c), + None => { + let (config, options) = (config.clone(), options.clone()); + stark::lookup::LazyCommitment::deferred(move || { + compute_precomputed_commitment(&config, &options) + }) + } + }; + let (config, options) = (config.clone(), options.clone()); + base.with_one_row(move || { + Some(compute_precomputed_commitment_with( + &config, + &options, + LeafLayout::Row, + )) + }) +} + // ========================================================================= // Bus interactions // ========================================================================= diff --git a/prover/src/tables/register.rs b/prover/src/tables/register.rs index 4f324653c..20af0a7ba 100644 --- a/prover/src/tables/register.rs +++ b/prover/src/tables/register.rs @@ -22,6 +22,7 @@ use std::collections::HashMap; use math::polynomial::Polynomial; use stark::config::Commitment; +use stark::leaf_layout::LeafLayout; use stark::lookup::{BusInteraction, BusValue, Multiplicity, Packing}; use stark::proof::options::ProofOptions; use stark::prover::evaluate_polynomial_on_lde_domain; @@ -307,7 +308,17 @@ pub fn fini_from_final_state(final_state: &FinalRegisterStateMap, init: &[u32]) /// OFFSET encodes the Word address (0..63 for x0-x31, 508 for x254, 510-511 for x255). /// INIT holds the initial value (SP=STACK_TOP, PC=entry_point, rest=0). pub fn compute_precomputed_commitment(options: &ProofOptions, init: &[u32]) -> Commitment { - commit_register_columns(options, preprocessed_columns(init)) + compute_precomputed_commitment_with(options, init, LeafLayout::RowPair) +} + +/// [`compute_precomputed_commitment`] under an explicit trace-tree leaf +/// layout (S2; program-dependent, so computed at run time either way). +pub fn compute_precomputed_commitment_with( + options: &ProofOptions, + init: &[u32], + layout: LeafLayout, +) -> Commitment { + commit_register_columns(options, preprocessed_columns(init), layout) } /// The precomputed columns themselves: OFFSET and INIT, padded to a power of @@ -401,13 +412,29 @@ pub fn compute_precomputed_commitment_with_fini( init: &[u32], fini: &[u32], ) -> Commitment { - commit_register_columns(options, preprocessed_columns_with_fini(init, fini)) + compute_precomputed_commitment_with_fini_layout(options, init, fini, LeafLayout::RowPair) +} + +/// [`compute_precomputed_commitment_with_fini`] under an explicit leaf layout +/// (S2) — the host twin of the in-circuit register commitment +/// (`lfm::programs::emit_register_commitment` at the same `rows_per_leaf`). +pub fn compute_precomputed_commitment_with_fini_layout( + options: &ProofOptions, + init: &[u32], + fini: &[u32], + layout: LeafLayout, +) -> Commitment { + commit_register_columns(options, preprocessed_columns_with_fini(init, fini), layout) } /// LDE + bit-reverse + Merkle-commit the given preprocessed columns (in column /// order). Shared by the monolithic (OFFSET, INIT) and continuation /// (OFFSET, INIT, FINI) preprocessed commitments. -fn commit_register_columns(options: &ProofOptions, columns: Vec>) -> Commitment { +fn commit_register_columns( + options: &ProofOptions, + columns: Vec>, + layout: LeafLayout, +) -> Commitment { let num_rows = NUM_REGISTER_ADDRESSES.next_power_of_two(); let polys: Vec> = columns .iter() @@ -432,7 +459,7 @@ fn commit_register_columns(options: &ProofOptions, columns: Vec>) -> Com // commitment the prover recomputes and compares against, so building it with // a different hash than the path commits under fails at prove time with // `PrecomputedCommitmentMismatch` — which is exactly how it was found. - crate::lfm::commit::commit_lde_columns(&lde_columns) + crate::lfm::commit::commit_lde_columns_with(&lde_columns, layout) } /// Returns the preprocessed commitment for the REGISTER table. @@ -442,6 +469,41 @@ pub fn preprocessed_commitment(options: &ProofOptions, init: &[u32]) -> Commitme compute_precomputed_commitment(options, init) } +/// REGISTER's (OFFSET, INIT) commitment source for both leaf layouts, both +/// computed (program-dependent), the one-row one on first use. +pub fn lazy_commitment(options: &ProofOptions, init: &[u32]) -> stark::lookup::LazyCommitment { + let (o, init) = (options.clone(), init.to_vec()); + stark::lookup::LazyCommitment::ready(preprocessed_commitment(options, &init)).with_one_row( + move || { + Some(compute_precomputed_commitment_with( + &o, + &init, + LeafLayout::Row, + )) + }, + ) +} + +/// The continuation variant (OFFSET, INIT, FINI): the row-pair root the caller +/// holds, and the one-row root computed from the same `init`/`fini` on first +/// use. +pub fn lazy_commitment_with_fini( + options: &ProofOptions, + row_pair: Commitment, + init: &[u32], + fini: &[u32], +) -> stark::lookup::LazyCommitment { + let (o, init, fini) = (options.clone(), init.to_vec(), fini.to_vec()); + stark::lookup::LazyCommitment::ready(row_pair).with_one_row(move || { + Some(compute_precomputed_commitment_with_fini_layout( + &o, + &init, + &fini, + LeafLayout::Row, + )) + }) +} + // ========================================================================= // Bus interactions // ========================================================================= diff --git a/prover/src/test_utils.rs b/prover/src/test_utils.rs index e3d7eca0b..d834060fc 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -26,7 +26,7 @@ use stark::domain::Domain; use stark::lookup::{ AirWithBuses, AuxiliaryTraceBuildData, BusInteraction, BusValue, NullBoundaryConstraintBuilder, }; -use stark::proof::options::ProofOptions; +use stark::proof::options::{ProofFormat, ProofOptions}; use stark::proof::stark::MultiProof; use stark::prover::{IsStarkProver, ProvingError}; #[cfg(feature = "disk-spill")] @@ -629,7 +629,12 @@ pub fn generate_minimal_bitwise_trace(ops: &[BitwiseOperation]) -> TraceTable AirProtoKey { + // ⛔ No `..`: every field of `ProofOptions` is part of the key. The format + // was once missing (it was added to `ProofOptions` after this key was + // written), and the first AIR built in a process then fixed the format of + // every later AIR of that name — a verifier asked for the default format + // verified a capped proof with capped AIRs and accepted it. + let ProofOptions { + blowup_factor, + fri_number_of_queries, + coset_offset, + grinding_factor, + fri_final_poly_log_degree, + format, + } = o; ( name.to_string(), - o.blowup_factor, - o.fri_number_of_queries, - o.coset_offset, - o.grinding_factor, - o.fri_final_poly_log_degree, + *blowup_factor, + *fri_number_of_queries, + *coset_offset, + *grinding_factor, + *fri_final_poly_log_degree, + *format, ) } diff --git a/prover/src/tests/decode_prepared_tests.rs b/prover/src/tests/decode_prepared_tests.rs index 815f9cb12..21c9f4454 100644 --- a/prover/src/tests/decode_prepared_tests.rs +++ b/prover/src/tests/decode_prepared_tests.rs @@ -40,6 +40,7 @@ fn config() -> ChainConfig { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: multilinear::whir_chain::ChainFormat::DEFAULT, } } @@ -228,3 +229,51 @@ fn decode_is_found_by_name_and_only_once() { a choice rather than a fact" ); } + +/// ★ W2: a commitment reused across epochs must have been built under the +/// epoch's fold schedule. At `first6` tree 0's leaves are 64 values wide, so a +/// `uniform4` epoch would open them as 16-wide ones: `agrees_with` refuses +/// before any opening is attempted, and the roots differ besides. +#[test] +fn a_decode_commitment_refuses_another_fold_schedule() { + use multilinear::whir_chain::{FirstFold, WhirFolds}; + + let first6 = ChainConfig { + format: multilinear::whir_chain::ChainFormat { + folds: WhirFolds::First(FirstFold::new(6).unwrap()), + ..multilinear::whir_chain::ChainFormat::DEFAULT + }, + ..config() + }; + let instrs = program(200, 7); + let at_first6 = + decode_prepared_from_columns::([1; 32], preprocessed_columns(&instrs), &first6) + .expect("prepared at first6"); + let at_uniform = prepared::(&instrs, 1); + + at_first6 + .agrees_with(&first6) + .expect("same schedule: accepted"); + at_uniform + .agrees_with(&config()) + .expect("same schedule: accepted"); + let err = at_first6 + .agrees_with(&config()) + .expect_err("a first6 commitment under a uniform epoch must be refused"); + assert!(format!("{err:?}").contains("folds"), "{err:?}"); + at_uniform + .agrees_with(&first6) + .expect_err("a uniform commitment under a first6 epoch must be refused"); + // Q does not enter: a config differing only in num_queries still agrees. + at_first6 + .agrees_with(&ChainConfig { + num_queries: first6.num_queries + 1, + ..first6 + }) + .expect("num_queries is not part of a commitment"); + + assert_ne!( + at_first6.roots, at_uniform.roots, + "the leaf width moves the roots" + ); +} diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index d5747c2b6..58690e97d 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -130,3 +130,17 @@ pub mod whir_byte_gate; pub mod whir_hash_tests; #[cfg(test)] pub mod whir_identity_tests; +#[cfg(test)] +pub mod zf_air_cache_tests; +#[cfg(test)] +pub mod zf_lfm_bytes_tests; +#[cfg(all(test, feature = "cuda"))] +pub mod zf_rpx_device_tests; +#[cfg(test)] +pub mod zf_rpx_golden_tests; +#[cfg(test)] +pub mod zf_rpx_vectors; +#[cfg(test)] +pub mod zf_vm_dp_tests; +#[cfg(test)] +pub mod zf_vm_one_row_tests; diff --git a/prover/src/tests/multilinear_bench_tests.rs b/prover/src/tests/multilinear_bench_tests.rs index 6098585d3..88f329bec 100644 --- a/prover/src/tests/multilinear_bench_tests.rs +++ b/prover/src/tests/multilinear_bench_tests.rs @@ -1165,6 +1165,21 @@ fn check_transcript_pins( // variable: `MaxRowsConfig::default` is what chunked the epochs whose // transcript this is, and it reaches the posture through this function. let max_rows_log2 = crate::tables::max_rows_log2_override(); + // ★ The bases were MEASURED at the legacy WHIR format (uniform + // folds, no cap). A run at any other WHIR format — the production default + // included — is a different measurement: it SKIPS and says so, like a run + // at another table cap. Re-pinning at the default needs a box measurement. + let whir_format = crate::zf_format::ZfFormat::global().chain_format(); + if whir_format != multilinear::whir_chain::ChainFormat::DEFAULT { + println!( + "{:<12} transcript pin SKIPPED - WHIR format {:?} (the bases were measured at the \ + legacy format {:?}; set LAMBDA_VM_ZF_WHIR_CAP=off LAMBDA_VM_ZF_WHIR_FOLDS=uniform4)", + "WHIR", + whir_format, + multilinear::whir_chain::ChainFormat::DEFAULT, + ); + return; + } if !pin_applies(&sha, elf.len(), epoch_size_log2, max_rows_log2) { // Never silent. A skipped assert that prints nothing is // indistinguishable from one that passed, which is the failure this @@ -1434,7 +1449,13 @@ fn the_pinned_pair_is_the_measurement() { // tallest stacked polynomial exactly — and the query count is 112 for every // height the block's cross-epoch tables can reach. The RUNTIME pin does not // rely on that: it evaluates the terms at the run's own config. - let config = crate::multilinear_prove::chain_config(&[(1, 21)]); + // ⚠ AT THE LEGACY WHIR FORMAT, named: the bases and lb17/lb18 + // were measured before the default flip, and the runtime pin skips any + // other format. + let config = crate::multilinear_prove::chain_config_under( + &crate::zf_format::ZfFormat::LEGACY, + &[(1, 21)], + ); assert_eq!( (config.log_folding, config.num_queries), (4, 112), @@ -1538,7 +1559,12 @@ fn the_genesis_stack_is_the_schedule_the_shape_implies() { // polynomial exactly. Stated here because the literal triple at the end of // this test is only the block's numbers at THIS posture; the runtime pin // evaluates the same form at the run's own config and does not rely on it. - let config = crate::multilinear_prove::chain_config(&[(1, 21)]); + // ⚠ THE LEGACY WHIR FORMAT: lb17/lb18 ran before the flip; + // under first6 the 21-variable stack is five rounds, not six. + let config = crate::multilinear_prove::chain_config_under( + &crate::zf_format::ZfFormat::LEGACY, + &[(1, 21)], + ); assert_eq!( (config.log_blowup, config.log_folding, config.num_queries), (2, 4, 112), @@ -1684,9 +1710,25 @@ fn the_prepared_opening_is_the_schedule_the_shape_implies() { columns, "one placement per column, which is what the opening's wrapper absorbs" ); + // ★ The DECODE group is committed under the PROCESS format + // (`decode_prepared_config` → `chain_config`), so with no knob set this is + // the production default's first6 schedule, [6,4,4,4,4,1] — pre-flip it was + // uniform4's [4,4,4,4,4,3]. Both are six rounds over 23 folded variables, + // so every count below is the same at either format. + assert_eq!( + config.format, + crate::zf_format::ZfFormat::global().chain_format() + ); + let want: Vec = if crate::zf_format::ZfFormat::global().whir_folds + == multilinear::whir_chain::WhirFolds::Uniform + { + vec![4, 4, 4, 4, 4, 3] + } else { + vec![6, 4, 4, 4, 4, 1] + }; assert_eq!( config.schedule(layout.n_stack()), - vec![4, 4, 4, 4, 4, 3], + want, "the fold schedule the chain runs" ); assert_eq!( @@ -1725,7 +1767,10 @@ fn pinned_stack() -> transcript_pin::Stack { columns: 6, num_vars: crate::continuation::PAGE_NUM_VARS, }), - config: crate::multilinear_prove::chain_config(&[(1, 21)]), + config: crate::multilinear_prove::chain_config_under( + &crate::zf_format::ZfFormat::LEGACY, + &[(1, 21)], + ), } } @@ -1859,7 +1904,10 @@ fn the_pinned_constants_differ_by_owed() { pinned_stack(), transcript_pin::Stack { shape: None, - config: crate::multilinear_prove::chain_config(&[(1, 21)]), + config: crate::multilinear_prove::chain_config_under( + &crate::zf_format::ZfFormat::LEGACY, + &[(1, 21)], + ), }, ] { let (pa, ps, pt) = transcript_pin::prove(shape, &stack); @@ -2348,12 +2396,15 @@ fn check_device_pins( .iter() .map(|b| format!("{b:02x}")) .collect(); - if !pin_applies( - &sha, - elf.len(), - epoch_size_log2, - crate::tables::max_rows_log2_override(), - ) { + if crate::zf_format::ZfFormat::global().chain_format() + != multilinear::whir_chain::ChainFormat::DEFAULT + || !pin_applies( + &sha, + elf.len(), + epoch_size_log2, + crate::tables::max_rows_log2_override(), + ) + { println!( "{:<12} device pin SKIPPED - see the transcript pin's line", "WHIR" diff --git a/prover/src/tests/multilinear_prove_tests.rs b/prover/src/tests/multilinear_prove_tests.rs index 73c4c74c2..4b2f2d81f 100644 --- a/prover/src/tests/multilinear_prove_tests.rs +++ b/prover/src/tests/multilinear_prove_tests.rs @@ -113,6 +113,7 @@ fn a_forged_preprocessed_column_is_rejected() { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: multilinear::whir_chain::ChainFormat::DEFAULT, }; let air = create_keccak_rc_air(&ProofOptions::default_test_options()); let width = air.trace_layout().0; diff --git a/prover/src/tests/multilinear_table_tests.rs b/prover/src/tests/multilinear_table_tests.rs index 9b86539a9..0f0694d9b 100644 --- a/prover/src/tests/multilinear_table_tests.rs +++ b/prover/src/tests/multilinear_table_tests.rs @@ -52,6 +52,7 @@ fn config() -> ChainConfig { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: multilinear::whir_chain::ChainFormat::DEFAULT, } } diff --git a/prover/src/tests/statement_alignment_tests.rs b/prover/src/tests/statement_alignment_tests.rs index 58ee2a006..beabe47eb 100644 --- a/prover/src/tests/statement_alignment_tests.rs +++ b/prover/src/tests/statement_alignment_tests.rs @@ -320,6 +320,7 @@ fn config() -> ChainConfig { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: multilinear::whir_chain::ChainFormat::DEFAULT, } } diff --git a/prover/src/tests/static_commitments_tests.rs b/prover/src/tests/static_commitments_tests.rs index 7b3d38e12..5e051217a 100644 --- a/prover/src/tests/static_commitments_tests.rs +++ b/prover/src/tests/static_commitments_tests.rs @@ -296,3 +296,196 @@ fn bitwise_non_three_coset_recomputes_and_differs_from_static() { ); } } + +// ========================================================================= +// One-row (S2) twins: a missing twin is a hard miss, never a recompute +// ========================================================================= +// +// Each static table ships a SECOND match table for the one-row leaf layout +// (`*_one_row`), generated by `compute_static_commitments --layout row` for +// `STATIC_BLOWUP_FACTORS_ONE_ROW`. The row-pair tests above are untouched; +// these pin the twins the same way, and pin that a missing twin is a hard +// miss (`None`, the prover's `PrecomputedCommitmentMissing`), never a +// recompute. + +use stark::leaf_layout::LeafLayout; + +use crate::tables::STATIC_BLOWUP_FACTORS_ONE_ROW; + +#[test] +fn bitwise_one_row_static_matches_recompute() { + for &blowup in STATIC_BLOWUP_FACTORS_ONE_ROW { + let options = options_for(blowup); + let recomputed = bitwise::compute_preprocessed_commitment_with(&options, LeafLayout::Row); + assert_eq!( + bitwise::static_commitment_one_row(blowup), + Some(recomputed), + "bitwise one-row commitment drifted for blowup={blowup}; regenerate via \ + `cargo run --bin compute_static_commitments --release -- --layout row`", + ); + assert_eq!( + bitwise::preprocessed_commitment_for(&options, LeafLayout::Row), + Some(recomputed) + ); + assert_ne!( + recomputed, + bitwise::preprocessed_commitment(&options), + "the two layouts commit different bytes" + ); + } +} + +#[test] +fn keccak_rc_one_row_static_matches_recompute() { + for &blowup in STATIC_BLOWUP_FACTORS_ONE_ROW { + let options = options_for(blowup); + let recomputed = keccak_rc::compute_preprocessed_commitment_with(&options, LeafLayout::Row); + assert_eq!( + keccak_rc::static_commitment_one_row(blowup), + Some(recomputed), + "keccak_rc one-row commitment drifted for blowup={blowup}" + ); + assert_eq!( + keccak_rc::preprocessed_commitment_for(&options, LeafLayout::Row), + Some(recomputed) + ); + assert_ne!(recomputed, keccak_rc::preprocessed_commitment(&options)); + } +} + +#[test] +fn pages_one_row_static_match_recompute() { + let zero_page_config = page::PageConfig::zero_init(0); + for &blowup in STATIC_BLOWUP_FACTORS_ONE_ROW { + let options = options_for(blowup); + let zero = + page::compute_precomputed_commitment_with(&zero_page_config, &options, LeafLayout::Row); + assert_eq!( + page::static_zero_page_commitment_one_row(blowup), + Some(zero) + ); + assert_eq!( + page::zero_init_preprocessed_commitment_for(&options, LeafLayout::Row), + Some(zero) + ); + assert_ne!(zero, page::zero_init_preprocessed_commitment(&options)); + let private = page::compute_offset_only_commitment_with(&options, LeafLayout::Row); + assert_eq!( + page::static_private_page_commitment_one_row(blowup), + Some(private) + ); + assert_eq!( + page::private_page_preprocessed_commitment_for(&options, LeafLayout::Row), + Some(private) + ); + assert_ne!(private, zero, "OFFSET alone vs OFFSET+INIT"); + } +} + +/// Under one row, a blowup with no twin and a non-3 coset are +/// HARD MISSES — `None`, never the recompute the row-pair wrappers fall back +/// to (which would silently rebuild a 2^20-row BITWISE LDE and tree). The +/// row-pair layout keeps today's answers. +#[test] +fn a_missing_one_row_twin_is_a_hard_miss() { + for blowup in [2u8, 8, NON_STATIC_BLOWUP] { + assert!(!STATIC_BLOWUP_FACTORS_ONE_ROW.contains(&blowup)); + let options = options_for(blowup); + assert_eq!(bitwise::static_commitment_one_row(blowup), None); + assert_eq!( + bitwise::preprocessed_commitment_for(&options, LeafLayout::Row), + None + ); + assert_eq!( + keccak_rc::preprocessed_commitment_for(&options, LeafLayout::Row), + None + ); + assert_eq!( + page::zero_init_preprocessed_commitment_for(&options, LeafLayout::Row), + None + ); + assert_eq!( + page::private_page_preprocessed_commitment_for(&options, LeafLayout::Row), + None + ); + } + for &blowup in STATIC_BLOWUP_FACTORS_ONE_ROW { + let options = options_with_coset(blowup, NON_STANDARD_COSET); + assert_eq!( + bitwise::preprocessed_commitment_for(&options, LeafLayout::Row), + None + ); + assert_eq!( + keccak_rc::preprocessed_commitment_for(&options, LeafLayout::Row), + None + ); + assert_eq!( + page::zero_init_preprocessed_commitment_for(&options, LeafLayout::Row), + None + ); + assert_eq!( + page::private_page_preprocessed_commitment_for(&options, LeafLayout::Row), + None + ); + } + // Row pairs: unchanged (the static root at a shipped blowup). + let options = options_for(2); + assert_eq!( + keccak_rc::preprocessed_commitment_for(&options, LeafLayout::RowPair), + Some(keccak_rc::preprocessed_commitment(&options)) + ); +} + +/// The lazy commitment sources the AIRs are built with serve both layouts: +/// today's root for row pairs (unchanged) and the twin for one row. +#[test] +fn the_air_commitment_sources_serve_both_layouts() { + let options = options_for(4); + let k = keccak_rc::lazy_commitment(&options); + assert_eq!( + k.get_for(LeafLayout::RowPair), + Some(keccak_rc::preprocessed_commitment(&options)) + ); + assert_eq!( + k.get_for(LeafLayout::Row), + keccak_rc::static_commitment_one_row(4) + ); + let p = page::private_page_lazy_commitment(&options); + assert_eq!( + p.get_for(LeafLayout::Row), + page::static_private_page_commitment_one_row(4) + ); + // A data page: computed on demand, per layout. + let mut config = page::PageConfig::zero_init(0x1000); + config.init_values = Some((0..64u8).collect()); + let d = page::data_page_lazy_commitment(&config, &options, None); + assert_eq!( + d.get_for(LeafLayout::Row), + Some(page::compute_precomputed_commitment_with( + &config, + &options, + LeafLayout::Row + )) + ); + assert_eq!( + d.get_for(LeafLayout::RowPair), + Some(page::compute_precomputed_commitment(&config, &options)) + ); + // A SUPPLIED row-pair root is never handed out for the other layout. + let supplied = page::data_page_lazy_commitment(&config, &options, Some([7u8; 32])); + assert_eq!(supplied.get_for(LeafLayout::RowPair), Some([7u8; 32])); + assert_ne!(supplied.get_for(LeafLayout::Row), Some([7u8; 32])); + // REGISTER (program-dependent): both computed. + let init: Vec = (0..crate::tables::register::NUM_REGISTER_ADDRESSES as u32).collect(); + let r = crate::tables::register::lazy_commitment(&options, &init); + assert_eq!( + r.get_for(LeafLayout::Row), + Some( + crate::tables::register::compute_precomputed_commitment_with( + &options, + &init, + LeafLayout::Row + ) + ) + ); +} diff --git a/prover/src/tests/transcript_counts.rs b/prover/src/tests/transcript_counts.rs index 6166cce19..07677dafa 100644 --- a/prover/src/tests/transcript_counts.rs +++ b/prover/src/tests/transcript_counts.rs @@ -159,6 +159,11 @@ fn drive_table(s: &mut Sim, t: &TableTranscriptShape) { } } +/// Today's uniform fold schedule, spelled independently of `ChainConfig`: `k` +/// per round, the remainder last. The LEGACY format's schedule; the closed +/// form below drives the config's own schedule, and +/// `the_uniform_schedule_is_the_legacy_configs` ties the two at the legacy +/// format. fn schedule(num_vars: usize, k: usize) -> Vec { let mut out = Vec::new(); let mut left = num_vars; @@ -170,8 +175,10 @@ fn schedule(num_vars: usize, k: usize) -> Vec { out } -fn drive_chain(s: &mut Sim, n_stack: usize, k: usize, queries: usize) { - let sch = schedule(n_stack, k); +/// One chain's transcript over the fold schedule `sch` — the config's own +/// (`ChainConfig::schedule`), so a non-uniform first fold (`whir_folds=first6`, +/// the production default) is priced as it is proved. +fn drive_chain(s: &mut Sim, sch: &[usize], queries: usize) { let rounds = sch.len(); for (r, &kr) in sch.iter().enumerate() { s.state(); // check_grind(folding) @@ -208,7 +215,7 @@ pub fn transcript_counts( statement_absorbs: &[u64], tables: &[TableTranscriptShape], groups: &[GroupTranscriptShape], - log_folding: usize, + chain: &multilinear::whir_chain::ChainConfig, queries: usize, owed_probe: bool, ) -> TranscriptCounts { @@ -251,7 +258,7 @@ pub fn transcript_counts( } s.sample_ext(); // the batching challenge for _ in 0..g.num_polys { - drive_chain(&mut s, g.n_stack, log_folding, queries); + drive_chain(&mut s, &chain.schedule(g.n_stack), queries); } } s.c @@ -423,7 +430,7 @@ fn continuation_transcript_counts( &epoch_statement_absorbs(EPOCH_TAG, public_output.len(), shapes.len()), &tables, &groups, - config.log_folding, + &config, config.num_queries, true, ); @@ -437,7 +444,7 @@ fn continuation_transcript_counts( let roots: usize = groups.iter().map(|g| g.num_polys).sum(); let chain_rounds: usize = groups .iter() - .map(|g| g.num_polys * g.n_stack.div_ceil(config.log_folding)) + .map(|g| g.num_polys * config.rounds(g.n_stack)) .sum(); println!( "epoch {:>2}: transcript_absorbs {:>8} transcript_squeezes {:>7} | absorb_calls {:>9} bytes {:>11} states {:>6}", @@ -530,7 +537,7 @@ fn continuation_transcript_counts( &global_statement_absorbs(page_bases.len(), gshapes.len()), >ables, &ggroups, - gconfig.log_folding, + &gconfig, gconfig.num_queries, false, ); @@ -556,7 +563,7 @@ fn continuation_transcript_counts( let groots: usize = ggroups.iter().map(|g| g.num_polys).sum(); let chain_rounds: usize = ggroups .iter() - .map(|g| g.num_polys * g.n_stack.div_ceil(gconfig.log_folding)) + .map(|g| g.num_polys * gconfig.rounds(g.n_stack)) .sum(); println!( " tables {} sum_m {} gkr_rounds {} sum_n {} cols {} factors {} n*deg {} roots {} chain_rounds {} Q {}", @@ -728,3 +735,31 @@ fn whir_transcript_counts_for_the_block() { c.finalizes() ); } + +/// The closed form drives `ChainConfig::schedule`; at the LEGACY format that +/// is exactly the uniform schedule spelled independently above, at every +/// height a chain reaches. Under the production default (first6) the two +/// differ, which is why the form no longer takes a fold width. +#[test] +fn the_uniform_schedule_is_the_legacy_configs() { + let legacy = crate::multilinear_prove::chain_config_under( + &crate::zf_format::ZfFormat::LEGACY, + &[(1, 25)], + ); + for n in 1..=32 { + assert_eq!( + schedule(n, legacy.log_folding), + legacy.schedule(n), + "n = {n}" + ); + } + let production = crate::multilinear_prove::chain_config_under( + &crate::zf_format::ZfFormat::DEFAULT, + &[(1, 25)], + ); + assert_ne!( + schedule(25, production.log_folding), + production.schedule(25), + "first6 is not the uniform walk" + ); +} diff --git a/prover/src/tests/whir_byte_gate.rs b/prover/src/tests/whir_byte_gate.rs index 4c7668a8d..4ab028240 100644 --- a/prover/src/tests/whir_byte_gate.rs +++ b/prover/src/tests/whir_byte_gate.rs @@ -200,6 +200,7 @@ fn the_whir_identity_line_over_a_canonically_sorted_eq_trace() { log_folding: 2, num_queries: 3, grind: GrindBits::default(), + format: multilinear::whir_chain::ChainFormat::DEFAULT, }; let options = ProofOptions::default_test_options(); diff --git a/prover/src/tests/whir_hash_tests.rs b/prover/src/tests/whir_hash_tests.rs index c6856b5a0..486489b8f 100644 --- a/prover/src/tests/whir_hash_tests.rs +++ b/prover/src/tests/whir_hash_tests.rs @@ -51,6 +51,7 @@ fn config() -> ChainConfig { log_folding: 2, num_queries: 3, grind: GrindBits::uniform(4), + format: multilinear::whir_chain::ChainFormat::DEFAULT, } } diff --git a/prover/src/tests/whir_identity_tests.rs b/prover/src/tests/whir_identity_tests.rs index f778d3bff..ae08c1da1 100644 --- a/prover/src/tests/whir_identity_tests.rs +++ b/prover/src/tests/whir_identity_tests.rs @@ -84,6 +84,7 @@ fn config() -> ChainConfig { log_folding: 2, num_queries: 3, grind: GrindBits::uniform(4), + format: multilinear::whir_chain::ChainFormat::DEFAULT, } } diff --git a/prover/src/tests/zf_air_cache_tests.rs b/prover/src/tests/zf_air_cache_tests.rs new file mode 100644 index 000000000..cdba728e9 --- /dev/null +++ b/prover/src/tests/zf_air_cache_tests.rs @@ -0,0 +1,84 @@ +//! The AIR prototype cache (`test_utils::build_air`) must key the proof +//! FORMAT: an AIR built for one format and asked for under another is a +//! different verifier. +//! +//! A key of `(name, blowup, queries, coset, grinding, final degree)` alone — +//! every `ProofOptions` field except `format` — lets the first AIR built in a +//! process fix the format of every later AIR with the same name and +//! parameters, whatever format the caller asks for. Two test failures come +//! from exactly that: +//! +//! - `merkle_cap_vm` (`LAMBDA_VM_ZF_CAP=auto`): the capped prove caches capped +//! AIRs, so `verify_with_options(.., &default, ..)` verifies the capped proof +//! with those capped AIRs and accepts it. +//! - `zf_vm_dp_tests`: in a fresh process the dp prove caches dp AIRs and the +//! "default" verifier accepts the dp proof; in the lib suite an earlier test +//! has cached default AIRs, so the dp prove proves at `pair` and the +//! non-vacuity assertion fires. +//! +//! The options used here carry a query count no other test uses, so these +//! keys are this test's alone however the suite interleaves. + +use stark::proof::options::{CapPolicy, FriMode, ProofFormat, ProofOptions}; +use stark::traits::AIR; + +use crate::test_utils::{create_cpu_air, create_halt_air}; + +/// A query count no other test builds AIRs with. +const PRIVATE_QUERIES: usize = 47; + +fn base() -> ProofOptions { + ProofOptions { + fri_number_of_queries: PRIVATE_QUERIES, + ..ProofOptions::default_test_options() + } +} + +fn formats() -> Vec { + vec![ + ProofFormat { + merkle_cap: CapPolicy::Auto, + ..ProofFormat::DEFAULT + }, + ProofFormat { + fri_mode: FriMode::Dp, + ..ProofFormat::DEFAULT + }, + ProofFormat { + merkle_cap: CapPolicy::Fixed(2), + fri_mode: FriMode::Dp, + ..ProofFormat::DEFAULT + }, + ProofFormat::DEFAULT, + ] +} + +/// Every format asked for is the format handed back, in both build orders +/// (non-default first, then default; and the reverse through a second AIR), +/// and asking again (a cache hit) changes nothing. +#[test] +fn the_air_prototype_cache_keys_the_proof_format() { + let options = |format: ProofFormat| ProofOptions { format, ..base() }; + // HALT: non-default formats first, the default last. + for _round in 0..2 { + for format in formats() { + let air = create_halt_air(&options(format)); + assert_eq!( + air.options().format, + format, + "HALT built for {format:?} carries another format" + ); + } + } + // CPU (a constraint-bearing AIR): the default first, then the rest. + for _round in 0..2 { + for format in formats().into_iter().rev() { + let air = create_cpu_air(&options(format)); + assert_eq!( + air.options().format, + format, + "CPU built for {format:?} carries another format" + ); + } + } +} diff --git a/prover/src/tests/zf_lfm_bytes_tests.rs b/prover/src/tests/zf_lfm_bytes_tests.rs new file mode 100644 index 000000000..f9767e349 --- /dev/null +++ b/prover/src/tests/zf_lfm_bytes_tests.rs @@ -0,0 +1,230 @@ +//! Device parity at the proof level, on an LFM machine proof. +//! +//! An RV64 VM proof is not a usable cross-build oracle: it is not a function +//! of the ELF and the format alone. Six base-table builders dedup through a std +//! `HashMap` (`RandomState`) and lay rows out in iteration order, so the main +//! roots — and with them the whole transcript — change from process to process +//! (the same build differs from itself). A cross-build `cmp` of such bytes +//! means nothing. +//! +//! This test proves an LFM machine program instead: its trace is a function of +//! the program and the arenas, the proof is made at grinding 0 (no host nonce +//! search), and each format is proved TWICE in the same process with the two +//! byte strings asserted equal (the in-run determinism control), so a +//! cross-build `cmp` of the written files means "the device proof is the CPU +//! proof". The program (`TrivialV0`) has public outputs, so the statement the +//! transcript absorbs — and the LogUp balance through `z`, `α` — depends on the +//! proof actually being the one the verifier replays. +//! +//! Files: `$ZF_S2_PROOF_DIR/{cpu,cuda}_{format}.rkyv` for four formats: +//! - `legacy`: every lever off; +//! - `one_row_1`: legacy + one row on every chip; +//! - `production`: the STARK levers of the prover's default format +//! ([`crate::zf_format::ZfFormat::DEFAULT`]: cap auto, fri dp, one row off), +//! asserted equal to what that default stamps so the two cannot drift; +//! - `all_levers`: every STARK lever on (cap auto, fri dp, one_row auto). +//! +//! Under cuda the `one_row_1` proof must build one-row trees on the device and +//! take the one-row device FRI commit (a silent host fallback would still give +//! equal bytes, so the counters are what make the comparison mean "device"); +//! run with `LAMBDA_VM_GPU_LDE_THRESHOLD` low enough that the LFM tables cross +//! it (the box line sets 1024). + +use stark::proof::options::{FriMode, OneRowMode, ProofFormat, ProofOptions}; + +use crate::lfm::proof::{lfm_prove, verify_against_artifacts}; +use crate::lfm::registry::{LfmProgramKind, build_artifacts}; +use crate::lfm::word::LfmWord; +use crate::tables::types::FE; + +/// The four formats compared across builds. +fn formats() -> [(&'static str, ProofFormat); 4] { + let legacy = ProofFormat { + merkle_cap: crypto::merkle_tree::cap::CapPolicy::Off, + fri_mode: FriMode::Pair, + one_row: OneRowMode::Off, + fri_schedule_override: None, + }; + let production = ProofFormat { + merkle_cap: crypto::merkle_tree::cap::CapPolicy::Auto, + fri_mode: FriMode::Dp, + one_row: OneRowMode::Off, + fri_schedule_override: None, + }; + assert_eq!( + production, + crate::zf_format::ZfFormat::DEFAULT.proof_format(), + "the `production` arm must be the STARK part of the prover's default format" + ); + [ + ("legacy", legacy), + ( + "one_row_1", + ProofFormat { + one_row: OneRowMode::On, + ..legacy + }, + ), + ("production", production), + ( + "all_levers", + ProofFormat { + one_row: OneRowMode::Auto, + ..production + }, + ), + ] +} + +fn options(format: ProofFormat) -> ProofOptions { + // Blowup 4 (the blowup the one-row static twins ship for), 128-bit + // queries with NO grinding: the proof is then a function of the program, + // the arenas and the format. + let mut o = stark::proof::options::GoldilocksCubicProofOptions::with_params(4, 128, 0) + .expect("valid options"); + assert_eq!(o.grinding_factor, 0); + o.format = format; + o +} + +fn arenas() -> Vec> { + vec![ + (0..4u64) + .map(|i| core::array::from_fn(|j| FE::from(1_000 * (i + 1) + j as u64))) + .collect(), + ] +} + +#[test] +#[ignore = "box: set ZF_S2_PROOF_DIR, run twice in a CPU build and twice in a cuda build, then cmp the files"] +fn lfm_proof_bytes_for_the_device_comparison() { + let dir = std::env::var("ZF_S2_PROOF_DIR").expect("set ZF_S2_PROOF_DIR"); + std::fs::create_dir_all(&dir).expect("create ZF_S2_PROOF_DIR"); + let build = if cfg!(feature = "cuda") { + "cuda" + } else { + "cpu" + }; + let kind = LfmProgramKind::TrivialV0; + let program = kind.program(); + let arenas = arenas(); + for (name, format) in formats() { + let o = options(format); + let artifacts = build_artifacts(&program, &o); + match format.one_row { + OneRowMode::On => assert!(artifacts.one_row_roots.is_some(), "{name}: one-row roots"), + OneRowMode::Off => assert!( + artifacts.one_row_roots.is_none(), + "{name}: no one-row roots" + ), + _ => {} + } + #[cfg(feature = "cuda")] + let (trees0, fri0, all_fri0) = ( + stark::gpu_lde::gpu_one_row_trees(), + stark::gpu_lde::gpu_one_row_fri_calls(), + stark::gpu_lde::gpu_fri_calls(), + ); + let mut runs: Vec> = Vec::with_capacity(2); + let mut last = None; + for _ in 0..2 { + let proved = lfm_prove(&program, &artifacts, &arenas, &o) + .unwrap_or_else(|e| panic!("{name}: the LFM program must prove: {e:?}")); + assert!( + !proved.public_words.is_empty(), + "{name}: the program publishes words" + ); + runs.push( + rkyv::to_bytes::(&proved.proof) + .expect("rkyv") + .to_vec(), + ); + last = Some(proved); + } + #[cfg(feature = "cuda")] + let (trees, fri, all_fri) = ( + stark::gpu_lde::gpu_one_row_trees() - trees0, + stark::gpu_lde::gpu_one_row_fri_calls() - fri0, + stark::gpu_lde::gpu_fri_calls() - all_fri0, + ); + let proved = last.expect("proved twice"); + assert_eq!( + runs[0], runs[1], + "{name}: the same LFM proof, proved twice in one process, must be byte-identical \ + (otherwise a cross-build cmp is not an oracle)" + ); + let bytes = &runs[0]; + let path = std::path::Path::new(&dir).join(format!("{build}_{name}.rkyv")); + std::fs::write(&path, bytes).expect("write the proof bytes"); + // After the file is written, so a verify failure still leaves the + // bytes for the cross-build cmp. The balance depends on z, α through + // the published words, so this is the Phase-A replay too. + assert!( + verify_against_artifacts(&artifacts, &proved.proof, &proved.public_words, &o), + "{name}: an honest LFM proof must verify" + ); + + let tables: Vec = proved + .proof + .proofs + .iter() + .map(|p| { + let one_row = p.deep_poly_openings[0] + .main_trace_polys + .evaluations_sym + .is_empty(); + format!("{}{}", p.trace_length, if one_row { "r" } else { "p" }) + }) + .collect(); + let one_row_tables = tables.iter().filter(|t| t.ends_with('r')).count(); + println!( + "ZF LFMBYTES {build} {name}: {} bytes, twice equal, {one_row_tables} of {} tables one-row \ + [rows: {}] -> {}", + bytes.len(), + tables.len(), + tables.join(" "), + path.display() + ); + if format.one_row == OneRowMode::On { + assert_eq!(one_row_tables, tables.len(), "{name}: every chip one-row"); + } + if format.one_row == OneRowMode::Off { + assert_eq!(one_row_tables, 0, "{name}: no chip one-row"); + } + #[cfg(feature = "cuda")] + { + println!( + "ZF LFM DEVICE {name}: {all_fri} device FRI commits, {trees} one-row device trees, \ + {fri} one-row device FRI commits (two proofs)" + ); + // The two large chips (2^16 and 2^20 rows, LDE >= 2^18) are above + // the default device floor in every format. + assert!( + all_fri > 0, + "{name}: no FRI commit reached the device: the proof would be a host proof" + ); + if format.one_row == OneRowMode::On { + assert!( + trees > 0 && fri > 0, + "{name}: no one-row tree or FRI commit reached the device \ + ({trees} trees, {fri} FRI commits): the proof would be a host proof \ + (lower LAMBDA_VM_GPU_LDE_THRESHOLD)" + ); + } + if format.one_row == OneRowMode::Off { + assert_eq!( + trees + fri, + 0, + "{name}: no one-row device work without one row" + ); + } + println!( + "ZF LFM DEVMEM {name}: largest one-row tree {} B; device fallbacks {}; \ + reserved high water {} B", + stark::gpu_lde::gpu_one_row_tree_peak_bytes(), + math_cuda::device::device_fallbacks(), + math_cuda::device::reserved_high_water() + ); + } + } +} diff --git a/prover/src/tests/zf_rpx_device_tests.rs b/prover/src/tests/zf_rpx_device_tests.rs new file mode 100644 index 000000000..10467e071 --- /dev/null +++ b/prover/src/tests/zf_rpx_device_tests.rs @@ -0,0 +1,176 @@ +//! S3 on the device under the production RPX pin: the RPX +//! twins of the stark crate's `tests::zf_fri_device_tests` (which cover Keccak +//! and Blake3; the stark crate cannot name `RpxStarkHash`). +//! +//! Every `#[ignore]`d test needs a GPU and a lowered +//! `LAMBDA_VM_GPU_LDE_THRESHOLD`, and fails when the device path does not run: +//! +//! ```text +//! LAMBDA_VM_GPU_LDE_THRESHOLD=2 cargo test --release -p lambda-vm-prover --features cuda \ +//! --lib tests::zf_rpx_device_tests::parity_ -- --ignored +//! LAMBDA_VM_GPU_LDE_THRESHOLD=1024 cargo test --release -p lambda-vm-prover --features cuda \ +//! --lib tests::zf_rpx_device_tests::proved_rpx_vectors_equal_the_cpu_bytes \ +//! -- --ignored --exact --test-threads=1 +//! ``` +//! +//! S2 on the device: `trees_one_row_rpx` (default threshold), +//! `fri_one_row_*` (threshold 2), `proved_rpx_one_row_vectors_equal_the_cpu_bytes` +//! (threshold 1024, alone), the RPX twins of `stark`'s `tests::zf_s2_device_tests`. + +use stark::fri::device_parity::{ + Case, legacy_cases, production_cases, resident_cases, run_cases, sweep_cases, +}; + +use crate::lfm::algebraic_commit::RpxStarkHash; + +fn check(cases: &[Case], resident: bool, seed: u64) { + if let Err(failures) = run_cases::("rpx", cases, resident, seed) { + panic!("rpx: {failures:#?}"); + } +} + +/// Every distinct DP schedule for B ≤ 23 plus the extra shapes (the list is +/// pinned by the stark crate's `dp_shapes_are_pinned`: 29 shapes). +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_every_dp_shape_rpx() { + let cases = sweep_cases(); + assert_eq!(cases.len(), 29); + check(&cases, false, 0x5a46_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_production_sizes_rpx() { + check(&production_cases(), false, 0x5a47_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_resident_layers_rpx() { + check(&resident_cases(), true, 0x5a48_0000); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn parity_legacy_encoding_rpx() { + check(&legacy_cases(), false, 0x5a49_0000); +} + +/// The RPX (d) vector proofs (`pair`, `dp`, `dp_3_1_3`, `cap_pair`, `cap_dp`; +/// LDE 4096) proved on +/// the device path are byte-identical to the checked-in CPU-proved files. The +/// device FRI counter must move once per proof. Run alone: the counter is +/// process-wide. +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD<=4096; run alone with --features cuda -- --ignored --exact --test-threads=1"] +fn proved_rpx_vectors_equal_the_cpu_bytes() { + use stark::fri::vectors::{check_or_write, proof_vectors}; + let before = stark::gpu_lde::gpu_fri_calls(); + let comp_before = stark::gpu_lde::gpu_composition_calls(); + let files = proof_vectors::("rpx"); + let device_commits = stark::gpu_lde::gpu_fri_calls() - before; + let compositions = stark::gpu_lde::gpu_composition_calls() - comp_before; + println!( + "FRIDEV rpx vector proofs: {} files, {device_commits} device FRI commits, {compositions} device compositions", + files.len() + ); + // Five (d) formats (pair, dp, dp_3_1_3 at Q = 3; cap_pair, cap_dp at + // Q = 20), two files and one FRI commit per proof. + assert_eq!(files.len(), 5 * 2); + assert_eq!( + device_commits, 5, + "every vector proof must take the device FRI commit (lower LAMBDA_VM_GPU_LDE_THRESHOLD)" + ); + // Every proof composes on the device (the AIR's constraint program); a + // host composition would not be counted here. + assert_eq!( + compositions, 5, + "every RPX vector proof must compose on the device ({compositions} device compositions)" + ); + let bad = check_or_write(&files, false); + assert!( + bad.is_empty(), + "device-proved RPX vectors differ from the checked-in CPU bytes: {bad:?}" + ); +} + +// --------------------------------------------------------------------------- +// S2 on the device under the RPX pin. +// --------------------------------------------------------------------------- + +/// One-row main / preprocessed split / aux (host and resident) / composition +/// trees and their device openings against the host (the RPX twin of +/// `stark`'s `trees_one_row_*`). +#[test] +#[ignore = "requires a GPU; run with --features cuda -- --ignored"] +fn trees_one_row_rpx() { + if let Err(failures) = stark::s2_device_parity::run_tree_parity::("rpx") { + panic!("rpx: {failures:#?}"); + } +} + +/// The one-row FRI commit (input tree from the codeword, then the group chain) +/// and query phases against the host CPU loop. +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn fri_one_row_rpx() { + check( + &stark::fri::device_parity::one_row_cases(), + false, + 0x5234_0000, + ); +} + +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD=2; run with --features cuda -- --ignored"] +fn fri_one_row_resident_rpx() { + check( + &stark::fri::device_parity::one_row_resident_cases(), + true, + 0x5235_0000, + ); +} + +/// The RPX (e) vector proofs (`one_row_pair`, `one_row_3_2_1_2`; LDE 4096) +/// proved on the device path are byte-identical to the checked-in CPU-proved +/// files. Each proof must take the one-row device FRI commit and build at +/// least its main, aux and composition trees one-row on the device. Run alone: +/// the counters are process-wide. +#[test] +#[ignore = "requires a GPU and LAMBDA_VM_GPU_LDE_THRESHOLD<=4096; run alone with --features cuda -- --ignored --exact --test-threads=1"] +fn proved_rpx_one_row_vectors_equal_the_cpu_bytes() { + use stark::fri::vectors::{check_or_write, one_row_proof_vectors}; + let fri_before = stark::gpu_lde::gpu_one_row_fri_calls(); + let trees_before = stark::gpu_lde::gpu_one_row_trees(); + let comp_before = stark::gpu_lde::gpu_composition_calls(); + let files = one_row_proof_vectors::("rpx"); + let fri_commits = stark::gpu_lde::gpu_one_row_fri_calls() - fri_before; + let trees = stark::gpu_lde::gpu_one_row_trees() - trees_before; + let compositions = stark::gpu_lde::gpu_composition_calls() - comp_before; + println!( + "S2DEV rpx vector proofs: {} files, {fri_commits} one-row device FRI commits, {trees} one-row device trees, {compositions} device compositions", + files.len() + ); + assert_eq!(files.len(), 2 * 2); + assert_eq!( + fri_commits, 2, + "every one-row vector proof must take the device FRI commit (lower LAMBDA_VM_GPU_LDE_THRESHOLD)" + ); + assert!( + trees >= 3 * 2, + "every one-row vector proof must build its main, aux and composition trees on the device \ + ({trees} one-row device trees for 2 proofs)" + ); + // Every proof composes on the device (the AIR's constraint program); a + // host composition would not be counted here. + assert_eq!( + compositions, 2, + "every one-row RPX vector proof must compose on the device ({compositions} device compositions)" + ); + let bad = check_or_write(&files, false); + assert!( + bad.is_empty(), + "device-proved one-row RPX vectors differ from the checked-in CPU bytes: {bad:?}" + ); +} diff --git a/prover/src/tests/zf_rpx_golden_tests.rs b/prover/src/tests/zf_rpx_golden_tests.rs new file mode 100644 index 000000000..e750ce706 --- /dev/null +++ b/prover/src/tests/zf_rpx_golden_tests.rs @@ -0,0 +1,445 @@ +//! Golden proofs under the production RPX pin, in TWO formats: +//! +//! - the LEGACY format (every ZF lever off; `ProofFormat::LEGACY`, the stark +//! crate's default): the RPX half of `stark::tests::zf_golden_tests` (which +//! covers Keccak and Blake3 and cannot name `RpxStarkHash`, a prover-crate +//! type). It keeps the legacy bytes pinned after the default flip, so +//! the rollback arm (every knob off) is still checked against bytes, not +//! against a round trip; +//! - the PRODUCTION default (`ZfFormat::DEFAULT.proof_format()`): +//! the bytes every production site stamps. Regenerate only for a +//! deliberate format change. +//! +//! Each case proves a small in-repo AIR at `grinding_factor = 0` (so the bytes +//! are reproducible) and pins the SHA-256 of the proof's rkyv bytes plus, so a +//! failure says where the drift is, the digests of its FRI layer roots, terminal +//! coefficients, FRI decommitments and trace/composition openings. The legacy +//! pins were generated at the (then default) legacy format before any S3 prover +//! code existed; regenerate either set only for a deliberate format change: +//! `cargo test -p lambda-vm-prover --lib tests::zf_rpx_golden_tests::print_goldens -- --ignored --nocapture`. + +use crypto::fiat_shamir::default_transcript::DefaultTranscript; +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; +use math::field::goldilocks::GoldilocksField; +use sha2::{Digest, Sha256}; +use stark::examples::bus_permutation::{bus_permutation_air, bus_permutation_trace}; +use stark::examples::read_only_memory_logup::{ + LogReadOnlyPublicInputs, LogReadOnlyRAP, read_only_logup_trace, +}; +use stark::examples::simple_addition::{ + SimpleAdditionAIR, SimpleAdditionPublicInputs, simple_addition_trace, +}; +use stark::proof::options::{ProofFormat, ProofOptions}; +use stark::proof::stark::StarkProof; +use stark::prover::{GenericProver, IsStarkProver}; +use stark::trace::TraceTable; +use stark::traits::AIR; +use stark::verifier::{GenericVerifier, IsStarkVerifier}; + +use crate::lfm::algebraic_commit::RpxStarkHash; + +type F = GoldilocksField; +type E = Degree3GoldilocksExtensionField; +type Felt = FieldElement; + +pub(crate) fn options(blowup: u8, k: u8, queries: usize, format: ProofFormat) -> ProofOptions { + ProofOptions { + blowup_factor: blowup, + fri_number_of_queries: queries, + coset_offset: 3, + grinding_factor: 0, + fri_final_poly_log_degree: k, + format, + } +} + +fn hex(bytes: &[u8]) -> String { + Sha256::digest(bytes) + .iter() + .map(|b| format!("{b:02x}")) + .collect() +} + +macro_rules! fingerprint { + ($proof:expr) => {{ + let proof = $proof; + let rk = + |bytes: Result| hex(&bytes.expect("rkyv")); + format!( + "proof {} roots[{}] {} coeffs {} queries {} openings {}", + rk(rkyv::to_bytes::(proof)), + proof.fri_layers_merkle_roots.len(), + hex(&proof.fri_layers_merkle_roots.concat()), + rk(rkyv::to_bytes::( + &proof.fri_final_poly_coeffs + )), + rk(rkyv::to_bytes::(&proof.query_list)), + rk(rkyv::to_bytes::( + &proof.deep_poly_openings + )), + ) + }}; +} + +pub(crate) fn prove_simple_addition( + rows: usize, + o: &ProofOptions, +) -> ( + SimpleAdditionAIR, + StarkProof>, +) { + let air = SimpleAdditionAIR::::new(o); + let pi = SimpleAdditionPublicInputs { + a: Felt::from(1u64), + b: Felt::from(2u64), + }; + let mut trace = simple_addition_trace::(rows); + let proof = GenericProver::::prove( + &air, + &mut trace, + &pi, + &mut DefaultTranscript::::new(&[]), + ) + .expect("proving must succeed"); + (air, proof) +} + +pub(crate) fn verify_simple_addition( + air: &SimpleAdditionAIR, + proof: &StarkProof>, +) -> bool { + GenericVerifier::::verify( + proof, + air, + &mut DefaultTranscript::::new(&[]), + ) +} + +pub(crate) fn prove_logup( + rows: usize, + o: &ProofOptions, +) -> ( + LogReadOnlyRAP, + StarkProof>, +) { + let addr: Vec = (0..rows).map(|i| Felt::from((i % 5) as u64 + 1)).collect(); + let val: Vec = (0..rows) + .map(|i| Felt::from(((i % 5) as u64 + 1) * 10)) + .collect(); + let mut trace: TraceTable = read_only_logup_trace(addr, val); + let cols = trace.columns_main(); + let pi = LogReadOnlyPublicInputs { + a0: cols[0][0], + v0: cols[1][0], + a_sorted_0: cols[2][0], + v_sorted_0: cols[3][0], + m0: cols[4][0], + }; + let air = LogReadOnlyRAP::::new(o); + let proof = GenericProver::::prove( + &air, + &mut trace, + &pi, + &mut DefaultTranscript::::new(&[]), + ) + .expect("proving must succeed"); + (air, proof) +} + +pub(crate) fn verify_logup( + air: &LogReadOnlyRAP, + proof: &StarkProof>, +) -> bool { + GenericVerifier::::verify( + proof, + air, + &mut DefaultTranscript::::new(&[]), + ) +} + +fn compute_goldens() -> Vec<(String, String)> { + compute_goldens_at(ProofFormat::LEGACY) +} + +/// The production default's univariate format, the one the goldens below pin. +fn production_format() -> ProofFormat { + crate::zf_format::ZfFormat::DEFAULT.proof_format() +} + +fn compute_goldens_at(d: ProofFormat) -> Vec<(String, String)> { + let mut out = Vec::new(); + for (rows, blowup) in [(16usize, 2u8), (64, 4)] { + let o = options(blowup, 2, 5, d); + let (air, proof) = prove_simple_addition(rows, &o); + assert!(verify_simple_addition(&air, &proof)); + out.push(( + format!("simple_addition/rpx/rows{rows}/blowup{blowup}"), + fingerprint!(&proof), + )); + } + for (rows, blowup) in [(32usize, 4u8), (128, 2)] { + let o = options(blowup, 1, 7, d); + let (air, proof) = prove_logup(rows, &o); + assert!(verify_logup(&air, &proof)); + out.push(( + format!("logup/rpx/rows{rows}/blowup{blowup}"), + fingerprint!(&proof), + )); + } + out +} + +/// PRODUCTION-default pins, generated by `print_goldens` at the default flip. +const PRODUCTION_GOLDENS: &[(&str, &str)] = &[ + ( + "simple_addition/rpx/rows16/blowup2", + "proof ee8ca7ebe2cd632fd40d3242450377f17f966c85d35ad69dc11d918a61a12fa9 roots[1] 19764f49df000e57080b4eada26d3d1d3b4d8a7356fe4fa0a779458ffcc0cc94 coeffs c71ca99567bf64cd75e4d2ca5a68533bd196fe44545180370ac90b29cd062b9b queries 4fb6a8d93089bd818a6a5a8b0026fe133491446029263b8d89cf3feec24f254a openings f5700d0bf2e5a02d28b973177bd7828d215bbabaa9c4c2a9c5ac59fb8475f293", + ), + ( + "simple_addition/rpx/rows64/blowup4", + "proof 961d5e394cf9913b261fd255b1b2ebd9d9304560a302cd25c6126a62c99728e4 roots[1] e2a8a7ce17b0c97ec91d741f84349494d7344943a84b0feb3a0fb93a43576846 coeffs bb0d9b495382e02c0b4ac8d0d3fca25bccacc363b6433ec1459c3ba4e26f81d0 queries a636ad70b084ad76ec0dcb2c9d904fe582d12e7379f0bb80541b81abf3febdc0 openings 090f06c93b8a9220d6f7d6dbb63302e708a513be939d85d12fd64ceda96da3a7", + ), + ( + "logup/rpx/rows32/blowup4", + "proof 67c9013a35ae703146e41c999cf082433031067576be62f0dd75b8adfac1fcbf roots[1] 26365ee78c3be44e7d96f1e77a2afcc1747e3736153693877bfc4f9dd6a88dff coeffs 7d98df3343a174fe6add0b9188e592bb5ad84b5797385185ff216f887bc5499d queries 57e99bd603476d6df18dedefa00dc727af256bd36ec91b0bc00773a77e6e739a openings dcb750dd32ae77ecc0b1928369db0bc1ee0ed3f1cad5bb0c4f5b7c258a3cf264", + ), + ( + "logup/rpx/rows128/blowup2", + "proof 978ebf4ab14b4f86c378642e53c3c9ab2cdae7732b2fdd09bd07ec90549ba8fb roots[2] 25c71fa19482dd430d9ce9b413bddf5f905f77a11ba5c9c38f32f64b9cb3738e coeffs 7d4fb0cbc585c68b5422bc19757d3d2151a7dde0f5edd29e312deaecb21428df queries 9281910f6451083a9a3ffc0c12ff31d952e3f718cd6a53022fe38c60d7b791b3 openings d0c9d124a01ace817bf58f0ecb4acb9c51b5ffecca32c04402af9de972856f82", + ), +]; + +/// LEGACY-format pins (every lever off), generated before any S3 prover code. +const GOLDENS: &[(&str, &str)] = &[ + ( + "simple_addition/rpx/rows16/blowup2", + "proof 76e4be044a53802e20b5a79c6fddea37893d4c7ba575d5b1b2d6007d6e741191 roots[1] 19764f49df000e57080b4eada26d3d1d3b4d8a7356fe4fa0a779458ffcc0cc94 coeffs c71ca99567bf64cd75e4d2ca5a68533bd196fe44545180370ac90b29cd062b9b queries 1db5f91a7ffd2fa4786315d948666c225859f74b79f97b0eee756531f12c4e29 openings a7254eb12c3eb00518026d8245f76c9c7283090720cf1aacf491af1edbb1a4df", + ), + ( + "simple_addition/rpx/rows64/blowup4", + "proof 54f04bbe46b0330480daf71af2fcafa1fc00dbf698fa538e9168d3c17ae253a9 roots[3] 136c65bbe688080ed90357acda8a3896f8fb8e93e6ab897590684c3d2e5e745d coeffs 51ee895a33735700296d7a776ff1ca28bf7892e7052c28e52a843ebecea5aa2e queries 45c4ec6e74ea93b1a8c98a912b1790936b6856f0884fcf92fcbb6d9be8872b2f openings dc4496a5b2413db451381eddc74765987fb5dd745f6adbcf6ace3814be57717f", + ), + ( + "logup/rpx/rows32/blowup4", + "proof 891ec879640f6a01244829495336a41dfb587214304e4420e2c47f619c3f4d03 roots[3] ec74768e299f79c15f92be8adaa27c38719ee5811fcbf07220163aa5a6d7bacc coeffs afb0be8e7a6b95223d78e5997d681828de2fcefe22704303fc5876b1e3a07fe2 queries cc4f54c61d2c1ad5bbf965c1eb622faec313ed0e8987babea88d9c2ac3cabe87 openings 720412c24f099a3897074effe7f9248cfbd0d370d7ca7cad49d7c794b2ffaf0a", + ), + ( + "logup/rpx/rows128/blowup2", + "proof 16cdff91c22119e7a5bd35be33d0a0e33c09413aba833c1ef4ba48b64bc03b0c roots[5] a928041f346311a1bd49ef81f791370075006cdce88efc45ed5c1608071e5d72 coeffs 709758625cbc3ce3eb8b0f6859198181e95484b5183965163762a3ac4a29852d queries b70a32fb34b697580ddfc50e9a7ac5b29271336301926b0bf62b75242d6c02bf openings 8832140f1efd004ebcd1b70f50a5680abb6d1ede4ca2120d3a826edf6fdef692", + ), +]; + +/// The LEGACY-format RPX goldens: the legacy bytes, unmoved by the +/// default flip. +#[test] +fn legacy_format_rpx_goldens_are_byte_identical() { + let got = compute_goldens(); + assert_eq!(got.len(), GOLDENS.len(), "one pin per case"); + for ((name, line), (pin_name, pin_line)) in got.iter().zip(GOLDENS) { + assert_eq!(name, pin_name); + assert_eq!( + line, pin_line, + "{name}: the legacy-format RPX proof moved (a field whose digest differs is where)" + ); + } +} + +/// The PRODUCTION-default RPX goldens (cap auto, `fri=dp`, and +/// whatever `ZfFormat::DEFAULT` stamps). A move here is a production format +/// change. +#[test] +fn production_format_rpx_goldens_are_byte_identical() { + let f = production_format(); + assert!( + !f.is_legacy(), + "the production default is not the legacy format" + ); + let got = compute_goldens_at(f); + assert_eq!(got.len(), PRODUCTION_GOLDENS.len(), "one pin per case"); + for ((name, line), (pin_name, pin_line)) in got.iter().zip(PRODUCTION_GOLDENS) { + assert_eq!(name, pin_name); + assert_eq!( + line, pin_line, + "{name}: the production-default RPX proof moved (a field whose digest differs is where)" + ); + } + // The two formats' proofs differ: the production pins are not the legacy + // ones under another name. + for ((_, a), (_, b)) in GOLDENS.iter().zip(PRODUCTION_GOLDENS) { + assert_ne!(a, b); + } +} + +#[test] +#[ignore = "generator for GOLDENS (legacy) and PRODUCTION_GOLDENS"] +fn print_goldens() { + for (name, line) in compute_goldens() { + println!("GOLDEN (\"{name}\", \"{line}\"),"); + } + println!("production format: {:?}", production_format()); + for (name, line) in compute_goldens_at(production_format()) { + println!("PRODUCTION GOLDEN (\"{name}\", \"{line}\"),"); + } +} + +// --------------------------------------------------------------------------- +// S3 (fri = dp) round trips under the production RPX pin: group leaves are +// hashed by the algebraic `Batched` sponge over 3·2^d felts, so the RPX leaf +// path of the group encoding is exercised here (the stark crate's S3 tests +// cover Keccak and Blake3). +// --------------------------------------------------------------------------- + +fn dp(schedule: Option<&[u8]>) -> ProofFormat { + ProofFormat { + fri_mode: stark::proof::options::FriMode::Dp, + fri_schedule_override: schedule + .map(|s| stark::proof::options::FriScheduleOverride::new(s).expect("fits")), + ..ProofFormat::LEGACY + } +} + +#[test] +fn rpx_dp_round_trips() { + // SimpleAddition 2^9 rows, blowup 4, k 1: the chain covers 10 → 3. + for sched in [None, Some(&[3u8, 1, 3][..]), Some(&[1, 6][..])] { + let o = options(4, 1, 9, dp(sched)); + let (air, proof) = prove_simple_addition(512, &o); + assert!(verify_simple_addition(&air, &proof), "{sched:?}"); + if let Some(s) = sched { + assert_eq!(proof.fri_layers_merkle_roots.len(), s.len()); + let values: usize = s.iter().map(|&d| 1usize << d).sum(); + assert_eq!(proof.query_list[0].layers_evaluations_sym.len(), values); + } + } + // LogReadOnlyRAP (ext3 + aux) 2^7 rows, blowup 4, k 1: 8 → 3. + for sched in [None, Some(&[2u8, 3][..])] { + let o = options(4, 1, 7, dp(sched)); + let (air, proof) = prove_logup(128, &o); + assert!(verify_logup(&air, &proof), "{sched:?}"); + // A tampered group value is rejected. + let mut bad = proof.clone(); + bad.query_list[0].layers_evaluations_sym[0] += FieldElement::::one(); + assert!(!verify_logup(&air, &bad)); + } +} + +/// Under RPX, the group path at an all-ones schedule commits +/// the same layer roots, terminal polynomial and paths as the legacy pair path +/// (the `Batched`/`Pair` two-element invariant, as a tested fact for the +/// algebraic backend). +#[test] +fn rpx_group_path_at_all_ones_equals_legacy() { + // LogReadOnlyRAP 2^7 rows, blowup 4, k 1: 5 committed binary layers. + let legacy = prove_logup(128, &options(4, 1, 7, ProofFormat::LEGACY)).1; + let group = prove_logup(128, &options(4, 1, 7, dp(Some(&[1, 1, 1, 1, 1])))).1; + assert_eq!(legacy.fri_layers_merkle_roots.len(), 5); + assert_eq!( + legacy.fri_layers_merkle_roots, + group.fri_layers_merkle_roots + ); + assert_eq!(legacy.fri_final_poly_coeffs, group.fri_final_poly_coeffs); + for (l, g) in legacy.query_list.iter().zip(&group.query_list) { + for j in 0..5 { + assert_eq!( + l.layers_auth_paths[j].merkle_path, + g.layers_auth_paths[j].merkle_path + ); + assert!( + g.layers_evaluations_sym[2 * j..2 * j + 2].contains(&l.layers_evaluations_sym[j]) + ); + } + } +} + +/// The production format sites at the PROCESS format (`ZfFormat::global()`): +/// a small ext3 STARK proved and host-verified under RPX with +/// `block_base_options()` (STARK base epochs) and `aggregation_wrap_options()` +/// (every LFM proof). Without a knob it proves at the production default +/// (`ZfFormat::DEFAULT`: cap auto, `fri=dp`, group layers); the knobs select +/// the arms — `LAMBDA_VM_ZF_FRI=pair` (legacy pair layers), +/// `LAMBDA_VM_ZF_ONE_ROW=1|auto` (both sites stamp the one-row mode; a table +/// resolved to one row opens no symmetric rows and commits the FRI input). +/// Every arm proves. +#[test] +fn production_sites_prove_at_the_process_format() { + use stark::proof::options::{FriMode, OneRowMode}; + let knob = |name: &str| { + std::env::var(name) + .ok() + .map(|v| v.trim().to_ascii_lowercase()) + }; + // An unset knob is the production default's value. + let default = crate::zf_format::ZfFormat::DEFAULT; + let want = match knob(crate::zf_format::ENV_FRI).as_deref() { + Some("dp") => FriMode::Dp, + Some("pair") => FriMode::Pair, + None => default.fri, + Some(other) => panic!("unexpected {}={other}", crate::zf_format::ENV_FRI), + }; + let want_one_row = match knob(crate::zf_format::ENV_ONE_ROW).as_deref() { + Some("1") => OneRowMode::On, + Some("auto") => OneRowMode::Auto, + Some("0") => OneRowMode::Off, + None => default.one_row, + Some(other) => panic!("unexpected {}={other}", crate::zf_format::ENV_ONE_ROW), + }; + assert_eq!(crate::zf_format::ZfFormat::global().fri, want); + assert_eq!(crate::zf_format::ZfFormat::global().one_row, want_one_row); + for (site, o) in [ + ( + "block_base_options", + crate::lfm::proof::block_base_options(), + ), + ( + "aggregation_wrap_options", + crate::lfm::proof::aggregation_wrap_options(), + ), + ] { + assert_eq!(o.format.fri_mode, want, "{site}"); + assert_eq!(o.format.one_row, want_one_row, "{site}"); + // 2^12 rows: LDE 2^14, so both terminals (T = 9, 10) leave committed + // layers. At that size a `cuda` build commits on the device, whose + // composition arm needs the AIR's constraint program — so an + // `AirWithBuses` table, as in production (`LogReadOnlyRAP` panicked in + // `constraint_program` there). + let air = bus_permutation_air(&o); + let mut trace = bus_permutation_trace(1 << 12); + let proof = GenericProver::::prove( + &air, + &mut trace, + &(), + &mut DefaultTranscript::::new(&[]), + ) + .unwrap_or_else(|e| panic!("{site}: proving must succeed: {e:?}")); + assert!( + GenericVerifier::::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + ), + "{site}: must verify" + ); + let layers = proof.fri_layers_merkle_roots.len(); + assert!(layers > 0, "{site}: committed layers"); + let values = proof.query_list[0].layers_evaluations_sym.len(); + let one_row = stark::leaf_layout::table_leaf_layout(&air, 1 << 12).is_one_row(); + if want_one_row == OneRowMode::On { + assert!(one_row, "{site}: one_row = 1 puts every table on one row"); + } + let sym = &proof.deep_poly_openings[0].main_trace_polys.evaluations_sym; + assert_eq!( + sym.is_empty(), + one_row, + "{site}: symmetric rows iff row pairs" + ); + println!( + "ZF SITE {site}: fri={want} one_row={want_one_row} resolved_one_row={one_row} layers={layers} values={values}" + ); + if want == FriMode::Dp || one_row { + assert!(values > layers, "{site}: group encoding"); + } else { + assert_eq!(values, layers, "{site}: legacy encoding"); + } + } +} diff --git a/prover/src/tests/zf_rpx_vectors.rs b/prover/src/tests/zf_rpx_vectors.rs new file mode 100644 index 000000000..a5e3b3c2e --- /dev/null +++ b/prover/src/tests/zf_rpx_vectors.rs @@ -0,0 +1,40 @@ +//! The exported S3 and S2 vectors ((c), (d), (e) in the README) under the production RPX pin, +//! written next to the Keccak/Blake3 ones in +//! `crypto/stark/tests/vectors/zf_fri/` (the stark crate cannot name +//! `RpxStarkHash`). Regenerated in memory and required byte-equal to the +//! checked-in files; regenerate after a deliberate format change: +//! `cargo test -p lambda-vm-prover --lib tests::zf_rpx_vectors::write_vectors -- --ignored`. + +use stark::fri::vectors::{ + VectorFile, check_or_write, leaf_digests_json, one_row_leaf_digests_json, + one_row_proof_vectors, proof_vectors, +}; + +use crate::lfm::algebraic_commit::RpxStarkHash; + +fn all() -> Vec { + let mut v = vec![leaf_digests_json::("rpx")]; + v.extend(proof_vectors::("rpx")); + // (e) S2. + v.push(one_row_leaf_digests_json::("rpx")); + v.extend(one_row_proof_vectors::("rpx")); + v +} + +#[test] +fn rpx_vectors_are_current() { + let files = all(); + assert_eq!(files.len(), 1 + 5 * 2 + 1 + 2 * 2); + let bad = check_or_write(&files, false); + assert!( + bad.is_empty(), + "stale or missing vector files {bad:?}; regenerate with \ + `cargo test -p lambda-vm-prover --lib tests::zf_rpx_vectors::write_vectors -- --ignored`" + ); +} + +#[test] +#[ignore = "writes crypto/stark/tests/vectors/zf_fri"] +fn write_vectors() { + assert!(check_or_write(&all(), true).is_empty()); +} diff --git a/prover/src/tests/zf_vm_dp_tests.rs b/prover/src/tests/zf_vm_dp_tests.rs new file mode 100644 index 000000000..1feb57414 --- /dev/null +++ b/prover/src/tests/zf_vm_dp_tests.rs @@ -0,0 +1,99 @@ +//! S3 end to end on the production VM path: a real multi-table VM proof +//! (every table the program touches, the preprocessed ones included, under the +//! RPX block pin, host CPU FRI) proved and verified at `fri = dp`. +//! +//! Proves a full VM trace (the 2^20-row BITWISE table among them), so it runs +//! in the box lib suite, not on the laptop — like its default-format sibling +//! `skip_empty_tables_tests::dropping_a_used_table_through_the_real_verifier_is_rejected`. + +use stark::proof::options::{FriMode, ProofFormat, ProofOptions}; + +#[test] +fn a_vm_proof_round_trips_at_fri_dp() { + let elf_bytes = crate::test_utils::asm_elf_bytes("test_mul_8"); + let default = ProofOptions::default_test_options(); + let dp = ProofOptions { + format: ProofFormat { + fri_mode: FriMode::Dp, + ..ProofFormat::DEFAULT + }, + ..default.clone() + }; + let vm_proof = crate::prove_with_options(&elf_bytes, &dp, &Default::default()) + .expect("the fixture must prove at fri = dp"); + assert!( + crate::verify_with_options(&vm_proof, &elf_bytes, &dp, None, None) + .expect("honest verify must not error"), + "an honest dp VM proof must verify" + ); + // Non-vacuity: some table folds a committed layer by more than 2, i.e. + // carries more opened values per query than committed layers. + assert!( + vm_proof.proof.proofs.iter().any(|p| { + let layers = p.fri_layers_merkle_roots.len(); + layers > 0 && p.query_list[0].layers_evaluations_sym.len() > 2 * layers + }), + "no table used a group of more than two values" + ); + // The format is a verifier constant: the default verifier rejects it. + assert!( + !crate::verify_with_options(&vm_proof, &elf_bytes, &default, None, None).unwrap_or(false), + "a dp proof must not verify under the default format" + ); + // A tampered FRI group value is rejected. + let mut bad = vm_proof.clone(); + let table = bad + .proof + .proofs + .iter() + .position(|p| !p.fri_layers_merkle_roots.is_empty()) + .expect("a table with committed layers"); + bad.proof.proofs[table].query_list[0].layers_evaluations_sym[0] += + math::field::element::FieldElement::< + math::field::extensions_goldilocks::Degree3GoldilocksExtensionField, + >::one(); + assert!( + !crate::verify_with_options(&bad, &elf_bytes, &dp, None, None).unwrap_or(false), + "a tampered group value must be rejected" + ); +} + +/// The same VM proof on the device path (a cuda build, the default device +/// thresholds): every table whose LDE the device admits commits its FRI +/// layers with the device group loop, and the proof still verifies. The device +/// FRI counter must move — under `dp` every device FRI commit is a group +/// commit, so a host-only run fails here. Run with `--test-threads=1`: the +/// counter is process-wide. +#[cfg(feature = "cuda")] +#[test] +fn a_vm_proof_round_trips_at_fri_dp_on_the_device() { + let elf_bytes = crate::test_utils::asm_elf_bytes("test_mul_8"); + let dp = ProofOptions { + format: ProofFormat { + fri_mode: FriMode::Dp, + ..ProofFormat::DEFAULT + }, + ..ProofOptions::default_test_options() + }; + let before = stark::gpu_lde::gpu_fri_calls(); + let vm_proof = crate::prove_with_options(&elf_bytes, &dp, &Default::default()) + .expect("the fixture must prove at fri = dp on the device path"); + let device_commits = stark::gpu_lde::gpu_fri_calls() - before; + println!("FRIDEV VM dp proof: {device_commits} device FRI commits"); + assert!( + device_commits > 0, + "no table took the device FRI commit at fri = dp" + ); + assert!( + crate::verify_with_options(&vm_proof, &elf_bytes, &dp, None, None) + .expect("honest verify must not error"), + "a device-proved dp VM proof must verify" + ); + assert!( + vm_proof.proof.proofs.iter().any(|p| { + let layers = p.fri_layers_merkle_roots.len(); + layers > 0 && p.query_list[0].layers_evaluations_sym.len() > 2 * layers + }), + "no table used a group of more than two values" + ); +} diff --git a/prover/src/tests/zf_vm_one_row_tests.rs b/prover/src/tests/zf_vm_one_row_tests.rs new file mode 100644 index 000000000..459fd70b5 --- /dev/null +++ b/prover/src/tests/zf_vm_one_row_tests.rs @@ -0,0 +1,282 @@ +//! S2 end to end on the production paths (box lib suite: each proves a full +//! VM trace with the 2^20-row BITWISE table, or an LFM machine proof). +//! +//! - A real multi-table VM proof (RPX block pin, host CPU paths) at +//! `one_row = 1` and at `one_row = auto` with `fri = dp`, blowup 4 (the +//! blowup the one-row static twins ship for). +//! - The hard miss at the VM level: at blowup 2 there is no one-row twin, so +//! `one_row = 1` is a proving ERROR naming the missing root — never a silent +//! recompute, never a proof. +//! - An LFM machine proof (`TrivialV0`) at `one_row = 1`, blowup 4, verified +//! through `lfm_verify`, i.e. through the registry policy (built at run time, +//! `LFM_REGISTRY` not read). +//! - A regression: the Phase-A replay that recovers `z`, `α` for +//! the expected bus balances absorbs each preprocessed table's root AT ITS +//! LEAF LAYOUT — an LFM proof at the wrap's options under `one_row = auto` +//! (mixed layouts, one-row preprocessed chips, published words) and a VM +//! proof with public output at `one_row = 1`. + +use stark::proof::options::{FriMode, OneRowMode, ProofFormat, ProofOptions}; + +fn opts(blowup: u8, one_row: OneRowMode, fri_mode: FriMode) -> ProofOptions { + let mut o = ProofOptions::default_test_options(); + o.blowup_factor = blowup; + o.format = ProofFormat { + one_row, + fri_mode, + ..ProofFormat::DEFAULT + }; + o +} + +#[test] +fn a_vm_proof_round_trips_at_one_row() { + let elf_bytes = crate::test_utils::asm_elf_bytes("test_mul_8"); + let one_row = opts(4, OneRowMode::On, FriMode::Pair); + let vm_proof = crate::prove_with_options(&elf_bytes, &one_row, &Default::default()) + .expect("the fixture must prove at one_row = 1"); + assert!( + crate::verify_with_options(&vm_proof, &elf_bytes, &one_row, None, None) + .expect("honest verify must not error"), + "an honest one-row VM proof must verify" + ); + // Every table is one-row: no symmetric rows anywhere, and the input tree + // is FRI layer 0 wherever anything folds. + for p in &vm_proof.proof.proofs { + for o in &p.deep_poly_openings { + assert!(o.main_trace_polys.evaluations_sym.is_empty()); + assert!(o.composition_poly.evaluations_sym.is_empty()); + } + } + println!( + "ZF S2 VM one_row=1: {} tables, proof tables with a precomputed root: {}", + vm_proof.proof.proofs.len(), + vm_proof + .proof + .proofs + .iter() + .filter(|p| p.lde_trace_precomputed_merkle_root.is_some()) + .count() + ); + // The layout is a verifier constant: the row-pair verifier rejects it. + let default = opts(4, OneRowMode::Off, FriMode::Pair); + assert!( + !crate::verify_with_options(&vm_proof, &elf_bytes, &default, None, None).unwrap_or(false), + "a one-row proof must not verify under the default format" + ); + // A tampered input-group value is rejected. + let mut bad = vm_proof.clone(); + let table = bad + .proof + .proofs + .iter() + .position(|p| !p.fri_layers_merkle_roots.is_empty()) + .expect("a table with committed layers"); + bad.proof.proofs[table].query_list[0].layers_evaluations_sym[0] += + math::field::element::FieldElement::< + math::field::extensions_goldilocks::Degree3GoldilocksExtensionField, + >::one(); + assert!( + !crate::verify_with_options(&bad, &elf_bytes, &one_row, None, None).unwrap_or(false), + "a tampered input-group value must be rejected" + ); +} + +#[test] +fn a_vm_proof_round_trips_at_one_row_auto_with_dp() { + let elf_bytes = crate::test_utils::asm_elf_bytes("test_mul_8"); + let auto = opts(4, OneRowMode::Auto, FriMode::Dp); + let vm_proof = crate::prove_with_options(&elf_bytes, &auto, &Default::default()) + .expect("the fixture must prove at one_row = auto, fri = dp"); + assert!( + crate::verify_with_options(&vm_proof, &elf_bytes, &auto, None, None) + .expect("honest verify must not error"), + "an honest auto/dp VM proof must verify" + ); + let one_row_tables = vm_proof + .proof + .proofs + .iter() + .filter(|p| { + p.deep_poly_openings[0] + .composition_poly + .evaluations_sym + .is_empty() + }) + .count(); + println!( + "ZF S2 VM one_row=auto fri=dp: {one_row_tables} of {} tables one-row", + vm_proof.proof.proofs.len() + ); +} + +/// No one-row static twin at blowup 2 ⇒ a proving error naming +/// the missing root. +#[test] +fn a_missing_one_row_twin_is_a_vm_proving_error() { + let elf_bytes = crate::test_utils::asm_elf_bytes("test_mul_8"); + let one_row = opts(2, OneRowMode::On, FriMode::Pair); + let err = crate::prove_with_options(&elf_bytes, &one_row, &Default::default()) + .expect_err("no one-row twin at blowup 2: proving must fail"); + let msg = format!("{err:?}"); + assert!( + msg.contains("PrecomputedCommitmentMissing"), + "the error must name the missing one-row root: {msg}" + ); +} + +#[test] +fn an_lfm_proof_round_trips_at_one_row() { + use crate::lfm::proof::{lfm_prove, lfm_verify}; + use crate::lfm::registry::{LfmProgramKind, build_artifacts}; + use crate::tables::types::FE; + let mut o = stark::proof::options::GoldilocksCubicProofOptions::with_blowup(4).unwrap(); + o.format.one_row = OneRowMode::On; + let program = LfmProgramKind::TrivialV0.program(); + let artifacts = build_artifacts(&program, &o); + assert!(artifacts.one_row_roots.is_some()); + let arenas: Vec> = vec![ + (0..4u64) + .map(|i| core::array::from_fn(|j| FE::from(1_000 * (i + 1) + j as u64))) + .collect(), + ]; + let proved = lfm_prove(&program, &artifacts, &arenas, &o).expect("one-row LFM prove"); + for p in &proved.proof.proofs { + assert!( + p.deep_poly_openings[0] + .main_trace_polys + .evaluations_sym + .is_empty() + ); + } + assert!( + lfm_verify( + LfmProgramKind::TrivialV0, + &proved.proof, + &proved.public_words, + &o + ) + .expect("built at run time under one row"), + "an honest one-row LFM proof must verify" + ); +} + +/// ★ REGRESSION: one-row PREPROCESSED tables and the Phase-A +/// replay. The prover absorbs each preprocessed table's root OF ITS LEAF +/// LAYOUT before sampling the shared LogUp `z`, `α`; the verify paths recover +/// `z`, `α` with `crate::replay_transcript_phase_a_view`, which absorbed the +/// ROW-PAIR root unconditionally. For a one-row preprocessed table the replay +/// then diverges, and every expected balance that depends on `z`, `α` is +/// wrong: an honest proof is rejected. The balance depends on them only when +/// something is published — the LFM public words, the VM commit bus — which +/// is why a VM proof without public output (`test_mul_8`) verified anyway. +/// +/// At the wrap's options (blowup 4, terminal 2^8, 128-bit queries) under +/// `one_row = auto`, as the block tree proves its LFM wraps: layouts MIX +/// within one proof and at least one preprocessed chip goes one-row (asserted, +/// so this keeps exercising the bug). Verified through +/// `verify_against_artifacts` — the call the tree harness makes before +/// harvesting a child (`per_table_aggregator_tests::real_child_timed`) — and +/// through `lfm_verify`. +#[test] +fn an_lfm_proof_at_the_wrap_options_round_trips_at_one_row_auto() { + use crate::lfm::proof::{lfm_prove, lfm_verify, verify_against_artifacts}; + use crate::lfm::registry::{LfmProgramKind, build_artifacts}; + use crate::tables::types::FE; + use stark::leaf_layout::table_leaf_layout; + let mut o = stark::proof::options::GoldilocksCubicProofOptions::with_blowup(4).unwrap(); + o.fri_final_poly_log_degree = 8; + o.format.one_row = OneRowMode::Auto; + let program = LfmProgramKind::TrivialV0.program(); + let artifacts = build_artifacts(&program, &o); + let arenas: Vec> = vec![ + (0..4u64) + .map(|i| core::array::from_fn(|j| FE::from(1_000 * (i + 1) + j as u64))) + .collect(), + ]; + let proved = lfm_prove(&program, &artifacts, &arenas, &o).expect("auto LFM prove"); + assert!( + !proved.public_words.is_empty(), + "the balance must depend on z and alpha: the program publishes" + ); + + let mut airs = crate::lfm::airs::LfmAirs::new_chunked( + &artifacts.roots, + &artifacts.blake3_chunk_roots, + &o, + artifacts.keccak_rnd_chunks, + artifacts.hasher, + artifacts.chip_set, + ); + airs = airs.with_one_row_roots(artifacts.one_row_roots.as_ref().expect("built")); + let refs = airs.air_refs(); + let (mut prep_rows, mut rows, mut pairs) = (0usize, 0usize, 0usize); + for (air, p) in refs.iter().zip(&proved.proof.proofs) { + let layout = table_leaf_layout(*air, p.trace_length); + println!( + "ZF FIX-S2 layout {:<12} 2^{:<2} {layout:?}", + air.name(), + p.trace_length.trailing_zeros() + ); + if layout.is_one_row() { + rows += 1; + prep_rows += usize::from(air.is_preprocessed()); + } else { + pairs += 1; + } + } + println!( + "ZF FIX-S2 LFM one_row=auto: {rows} one-row, {pairs} row-pair, {prep_rows} one-row preprocessed" + ); + assert!( + prep_rows >= 1 && pairs >= 1, + "the fixture must mix layouts with a one-row preprocessed chip \ + ({prep_rows} one-row preprocessed, {pairs} row-pair)" + ); + + assert!( + verify_against_artifacts(&artifacts, &proved.proof, &proved.public_words, &o), + "an honest one-row-auto LFM proof must verify (the tree harness's call)" + ); + assert!( + lfm_verify( + LfmProgramKind::TrivialV0, + &proved.proof, + &proved.public_words, + &o + ) + .expect("built at run time under one row"), + "an honest one-row-auto LFM proof must verify through lfm_verify" + ); + // Still bound to the claimed words: one moved public word rejects. + let mut wrong = proved.public_words.clone(); + wrong[0].1[0] += FE::from(1u64); + assert!( + !verify_against_artifacts(&artifacts, &proved.proof, &wrong, &o), + "a moved public word must be rejected" + ); +} + +/// The VM half of the same regression: a VM proof WITH public output (the +/// commit bus's expected balance depends on the replayed `z`, `α`) at +/// `one_row = 1`, where every preprocessed VM table (BITWISE, DECODE, the +/// pages, REGISTER) is one-row. +#[test] +fn a_vm_proof_with_public_output_round_trips_at_one_row() { + let elf_bytes = crate::test_utils::asm_elf_bytes("test_commit_4"); + let one_row = opts(4, OneRowMode::On, FriMode::Pair); + let vm_proof = crate::prove_with_options(&elf_bytes, &one_row, &Default::default()) + .expect("test_commit_4 must prove at one_row = 1"); + assert_eq!(vm_proof.public_output, vec![0xAA, 0xBB, 0xCC, 0xDD]); + assert!( + crate::verify_with_options(&vm_proof, &elf_bytes, &one_row, None, None) + .expect("honest verify must not error"), + "an honest one-row VM proof with public output must verify" + ); + let mut wrong = vm_proof.clone(); + wrong.public_output[0] ^= 1; + assert!( + !crate::verify_with_options(&wrong, &elf_bytes, &one_row, None, None).unwrap_or(false), + "a moved public output byte must be rejected" + ); +} diff --git a/prover/src/zf_format.rs b/prover/src/zf_format.rs new file mode 100644 index 000000000..05842565d --- /dev/null +++ b/prover/src/zf_format.rs @@ -0,0 +1,911 @@ +//! ★ The proof FORMAT this process proves under — the ZF proof-format levers. +//! +//! ```text +//! LAMBDA_VM_ZF_CAP off | auto | 0..=16 Merkle cap, every univariate STARK tree (S1) +//! LAMBDA_VM_ZF_WHIR_CAP off | auto | 0..=16 Merkle cap, every WHIR chain tree (W1) +//! LAMBDA_VM_ZF_FRI pair | dp FRI fold schedule (S3) +//! LAMBDA_VM_ZF_ONE_ROW 0 | 1 | auto one-row trace openings (S2) +//! LAMBDA_VM_ZF_WHIR_FOLDS uniform4 | first5 | first6 WHIR first-round fold (W2) +//! ``` +//! +//! ★ Every unset knob is [`ZfFormat::DEFAULT`], the MEASURED configuration: +//! `cap=auto whir_cap=auto fri=dp one_row=0 whir_folds=first6`. +//! Each lever was measured net positive on block runs before it became the +//! default. Every knob keeps its OFF spelling (`cap=off`, `whir_cap=off`, +//! `fri=pair`, `one_row=0`, `whir_folds=uniform4`), so setting all five to off +//! reproduces [`ZfFormat::LEGACY`] — the format before any lever, byte for byte — +//! for rollback and for A/B arms. The crypto crates' own defaults +//! (`stark::proof::options::ProofFormat::DEFAULT`, +//! `multilinear::whir_chain::ChainFormat::DEFAULT`) stay the legacy format: a +//! library value built without a format is the legacy one, and the production +//! format reaches the proofs only through the three sites below. +//! +//! # Where the format goes +//! +//! Parsed ONCE per process ([`ZfFormat::global`]) and read only where a +//! production format value is built — [`crate::lfm::proof::aggregation_wrap_options`] +//! (every LFM proof: wraps, nodes, the root), [`crate::multilinear_prove::chain_config`] +//! (the WHIR base proofs) and [`crate::lfm::proof::block_base_options`] (the +//! STARK block's base epochs). From there it travels inside the option types +//! the crypto crates already take — `stark::ProofOptions` and +//! `multilinear::ChainConfig` — which never read the environment themselves. +//! Host verification reads nothing global: it uses the options it is given. +//! Tests build those option values explicitly; none sets the environment. +//! +//! # Three rules, as in `whir_hash_knob` +//! +//! **An unknown value ABORTS.** A typo that fell back to the default would +//! produce a valid default-format proof labelled as the lever — a measurement +//! that looks like arm B and is arm A. +//! +//! **A lever this build does not implement ABORTS too.** The fields exist +//! before the levers do (so the option structs and this banner are stable +//! before the levers land), and a knob set on a build that only parses +//! it would print a non-default format and prove the default one. Each +//! `*_IMPLEMENTED` constant is flipped when its lever is real. +//! +//! **The banner prints on every setting, including the default**: +//! `ZF FORMAT: cap=auto whir_cap=auto fri=dp one_row=0 whir_folds=first6`. +//! Its absence in a log is then a fact about the run, not an ambiguity. + +use std::sync::OnceLock; + +use multilinear::whir_chain::{ChainConfig, ChainFormat, FirstFold, WhirFolds}; +use stark::proof::options::{CapPolicy, FriMode, OneRowMode, ProofFormat, ProofOptions}; + +/// The knob names, in banner order. +pub const ENV_CAP: &str = "LAMBDA_VM_ZF_CAP"; +pub const ENV_WHIR_CAP: &str = "LAMBDA_VM_ZF_WHIR_CAP"; +pub const ENV_FRI: &str = "LAMBDA_VM_ZF_FRI"; +pub const ENV_ONE_ROW: &str = "LAMBDA_VM_ZF_ONE_ROW"; +pub const ENV_WHIR_FOLDS: &str = "LAMBDA_VM_ZF_WHIR_FOLDS"; + +/// The uniform WHIR schedule's fold, as production configures it +/// (`multilinear_prove::chain_config`); the banner spells the default +/// `uniform4` after it. +pub const PRODUCTION_WHIR_LOG_FOLDING: usize = 4; + +/// One process's proof format. [`ZfFormat::default`] is [`ZfFormat::DEFAULT`], +/// the measured configuration; [`ZfFormat::LEGACY`] is every lever off. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ZfFormat { + /// S1: the cap on every univariate STARK tree. + pub cap: CapPolicy, + /// W1: the cap on every WHIR chain tree. + pub whir_cap: CapPolicy, + /// S3: the FRI fold schedule. + pub fri: FriMode, + /// S2: one-row trace openings. + pub one_row: OneRowMode, + /// W2: the WHIR per-round fold schedule. + pub whir_folds: WhirFolds, +} + +/// The first-round WHIR fold of the default format (`whir_folds=first6`). +const DEFAULT_WHIR_FIRST_FOLD: FirstFold = match FirstFold::new(6) { + Some(k0) => k0, + None => panic!("6 is a legal first fold"), +}; + +impl Default for ZfFormat { + fn default() -> Self { + Self::DEFAULT + } +} + +impl ZfFormat { + /// ★ The production format when no knob is set: the MEASURED + /// configuration. S1 `cap=auto` (STARK block −15.35 s), + /// S1+S3 `fri=dp` (−28.55 s), W1 `whir_cap=auto` and W2 `whir_folds=first6` + /// (WHIR block −9.10 s together), each measured net positive in an ABBA + /// block run. `one_row` stays off: in ABBA block runs it costs +3.2 s on + /// the WHIR pipeline (the prover-side cost of one-row LFM proofs) and saves + /// 8.0 s and 8 GiB of host memory on the STARK pipeline, so it is a knob + /// (`LAMBDA_VM_ZF_ONE_ROW=auto`), recommended for the STARK pipeline. + /// Security parameters (queries, grinding, blowup) are the + /// legacy ones: no lever touches them. + pub const DEFAULT: Self = Self { + cap: CapPolicy::Auto, + whir_cap: CapPolicy::Auto, + fri: FriMode::Dp, + one_row: OneRowMode::Off, + whir_folds: WhirFolds::First(DEFAULT_WHIR_FIRST_FOLD), + }; + + /// The legacy format: every lever off. What all five knobs at their + /// OFF spellings select, what the crypto crates' own defaults are, and the + /// only format the RV64 recursion guest verifies. + pub const LEGACY: Self = Self { + cap: CapPolicy::Off, + whir_cap: CapPolicy::Off, + fri: FriMode::Pair, + one_row: OneRowMode::Off, + whir_folds: WhirFolds::Uniform, + }; + + /// True when every lever is off: the format proves exactly what the + /// prover proved before any lever existed. + pub fn is_legacy(&self) -> bool { + self.cap.is_off() + && self.whir_cap.is_off() + && self.fri == FriMode::Pair + && self.one_row == OneRowMode::Off + && self.whir_folds == WhirFolds::Uniform + } + + /// Parse the five knobs through `lookup` (the process environment in + /// production, a map in tests). An unset knob is [`Self::DEFAULT`]'s value; a set one + /// must be one of the accepted spellings (surrounding whitespace and case + /// are ignored, as for `LAMBDA_VM_WHIR_HASH`). + pub fn from_lookup(lookup: impl Fn(&str) -> Option) -> Result { + let mut format = Self::DEFAULT; + let get = |name: &str| lookup(name).map(|raw| raw.trim().to_ascii_lowercase()); + if let Some(v) = get(ENV_CAP) { + format.cap = parse_cap(ENV_CAP, &v)?; + } + if let Some(v) = get(ENV_WHIR_CAP) { + format.whir_cap = parse_cap(ENV_WHIR_CAP, &v)?; + } + if let Some(v) = get(ENV_FRI) { + format.fri = v + .parse() + .map_err(|()| format!("{ENV_FRI}={v:?}: expected `pair` or `dp`"))?; + } + if let Some(v) = get(ENV_ONE_ROW) { + format.one_row = v + .parse() + .map_err(|()| format!("{ENV_ONE_ROW}={v:?}: expected `0`, `1` or `auto`"))?; + } + if let Some(v) = get(ENV_WHIR_FOLDS) { + format.whir_folds = parse_whir_folds(&v)?; + } + Ok(format) + } + + /// [`from_lookup`](Self::from_lookup) over the process environment. A + /// variable that is set but not valid Unicode is an error, not "unset". + pub fn from_env() -> Result { + let non_unicode = std::cell::Cell::new(None); + let format = Self::from_lookup(|name| match std::env::var(name) { + Ok(v) => Some(v), + Err(std::env::VarError::NotPresent) => None, + Err(std::env::VarError::NotUnicode(_)) => { + non_unicode.set(Some(name.to_string())); + None + } + })?; + match non_unicode.into_inner() { + Some(name) => Err(format!("{name} is set but not valid Unicode")), + None => Ok(format), + } + } + + /// The knobs set to a non-default value whose lever this build does not + /// implement yet. Selecting one must fail: see the module header. + pub fn unimplemented_levers(&self) -> Vec<&'static str> { + let mut out = Vec::new(); + if !self.cap.is_off() && !stark::proof::options::MERKLE_CAP_IMPLEMENTED { + out.push(ENV_CAP); + } + if !self.whir_cap.is_off() && !multilinear::whir_chain::WHIR_CAP_IMPLEMENTED { + out.push(ENV_WHIR_CAP); + } + if self.fri != FriMode::Pair && !stark::proof::options::FRI_MODE_IMPLEMENTED { + out.push(ENV_FRI); + } + if self.one_row != OneRowMode::Off && !stark::proof::options::ONE_ROW_IMPLEMENTED { + out.push(ENV_ONE_ROW); + } + if self.whir_folds != WhirFolds::Uniform && !multilinear::whir_chain::WHIR_FOLDS_IMPLEMENTED + { + out.push(ENV_WHIR_FOLDS); + } + out + } + + /// ★ The format for this process, read once and cached. + /// + /// Prints the banner on the first call. Aborts on an unrecognised value + /// or a lever this build does not implement — see the module header. + pub fn global() -> &'static Self { + static FORMAT: OnceLock = OnceLock::new(); + FORMAT.get_or_init(|| { + let format = Self::from_env().unwrap_or_else(|e| { + // eprintln then abort rather than a panic: a configuration + // error at startup, and the operator needs the accepted + // values, not a backtrace through the prover. + eprintln!("ZF FORMAT: {e}"); + std::process::abort() + }); + let missing = format.unimplemented_levers(); + if !missing.is_empty() { + eprintln!( + "ZF FORMAT: {} set to a non-default value, but this build does not \ + implement that lever yet ({})", + missing.join(", "), + format.banner() + ); + std::process::abort() + } + // Always, including the default — see the module header. + println!("{}", format.banner()); + println!("{}", format.whir_schedule_line()); + format + }) + } + + /// `ZF FORMAT: cap=… whir_cap=… fri=… one_row=… whir_folds=…`, each value + /// in the spelling its knob accepts. + pub fn banner(&self) -> String { + format!( + "ZF FORMAT: cap={} whir_cap={} fri={} one_row={} whir_folds={}", + self.cap, + self.whir_cap, + self.fri, + self.one_row, + whir_folds_name(&self.whir_folds) + ) + } + + /// `ZF WHIR SCHEDULES: whir_folds=… n=20:[…] … n=25:[…]` — the fold + /// schedule the WHIR base chains run at the production stack heights, so a + /// log states the rounds it proved and not only the knob's name. Printed + /// under the banner, on every setting. + pub fn whir_schedule_line(&self) -> String { + let config = crate::multilinear_prove::chain_config_under(self, &[(1, 25)]); + let schedules = (20..=25) + .map(|n| format!("n={n}:{:?}", config.schedule(n)).replace(' ', "")) + .collect::>() + .join(" "); + format!( + "ZF WHIR SCHEDULES: whir_folds={} q={} {schedules}", + whir_folds_name(&self.whir_folds), + config.num_queries + ) + } + + /// The univariate part: what `stark::ProofOptions` carries. + pub fn proof_format(&self) -> ProofFormat { + ProofFormat { + merkle_cap: self.cap, + fri_mode: self.fri, + one_row: self.one_row, + // A test hook only; no knob sets it. + fri_schedule_override: None, + } + } + + /// The WHIR part: what `multilinear::ChainConfig` carries. + pub fn chain_format(&self) -> ChainFormat { + ChainFormat { + cap: self.whir_cap, + folds: self.whir_folds, + } + } + + /// Stamp this format's univariate fields onto `options`. + pub fn apply_to_options(&self, options: &mut ProofOptions) { + options.format = self.proof_format(); + } + + /// `options` with this format's univariate fields. + pub fn options(&self, mut options: ProofOptions) -> ProofOptions { + self.apply_to_options(&mut options); + options + } + + /// Stamp this format's WHIR fields onto `config`. + pub fn apply_to_chain(&self, config: &mut ChainConfig) { + config.format = self.chain_format(); + } + + /// `config` with this format's WHIR fields. + pub fn chain(&self, mut config: ChainConfig) -> ChainConfig { + self.apply_to_chain(&mut config); + config + } +} + +fn parse_cap(name: &str, v: &str) -> Result { + v.parse().map_err(|e| format!("{name}={v:?}: {e}")) +} + +/// The first-round folds the knob accepts: the two arms that are built. +/// +/// ⚠ Not `first1..=first4`: a first fold narrower than the uniform one adds +/// rounds at some heights (Q would rise and the arms stop being comparable), +/// and `first4` IS `uniform4` under another statement word. Not `dp`: only +/// the first fold is a lever. Widening this list is a format decision, not a parser one. +pub const WHIR_FIRST_FOLDS: [usize; 2] = [5, 6]; + +/// `uniform4` | `first5` | `first6`. +fn parse_whir_folds(v: &str) -> Result { + if v == format!("uniform{PRODUCTION_WHIR_LOG_FOLDING}") { + return Ok(WhirFolds::Uniform); + } + WHIR_FIRST_FOLDS + .iter() + .find(|&&k| v == format!("first{k}")) + .and_then(|&k| FirstFold::new(k)) + .map(WhirFolds::First) + .ok_or_else(|| { + format!( + "{ENV_WHIR_FOLDS}={v:?}: expected `uniform{PRODUCTION_WHIR_LOG_FOLDING}`, {}", + WHIR_FIRST_FOLDS + .iter() + .map(|k| format!("`first{k}`")) + .collect::>() + .join(" or ") + ) + }) +} + +fn whir_folds_name(folds: &WhirFolds) -> String { + match folds { + WhirFolds::Uniform => format!("uniform{PRODUCTION_WHIR_LOG_FOLDING}"), + WhirFolds::First(k0) => format!("first{}", k0.get()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + fn parse(pairs: &[(&str, &str)]) -> Result { + let map: HashMap = pairs + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + ZfFormat::from_lookup(|k| map.get(k).cloned()) + } + + /// ★ With no knob set the process proves the MEASURED + /// configuration. + #[test] + fn nothing_set_is_the_measured_default() { + let f = parse(&[]).unwrap(); + assert_eq!(f, ZfFormat::DEFAULT); + assert_eq!(f, ZfFormat::default()); + assert_eq!( + f, + ZfFormat { + cap: CapPolicy::Auto, + whir_cap: CapPolicy::Auto, + fri: FriMode::Dp, + one_row: OneRowMode::Off, + whir_folds: WhirFolds::First(FirstFold::new(6).unwrap()), + } + ); + assert_eq!( + f.banner(), + "ZF FORMAT: cap=auto whir_cap=auto fri=dp one_row=0 whir_folds=first6" + ); + assert!(!f.is_legacy()); + assert!(f.unimplemented_levers().is_empty()); + } + + /// Every knob keeps its OFF spelling, and all five at off are the legacy + /// format (every lever off): the rollback and A/B arm. + #[test] + fn the_off_spellings_parse_to_the_legacy_format() { + let f = parse(&[ + (ENV_CAP, "off"), + (ENV_WHIR_CAP, "0"), + (ENV_FRI, "pair"), + (ENV_ONE_ROW, "0"), + (ENV_WHIR_FOLDS, "uniform4"), + ]) + .unwrap(); + assert_eq!(f, ZfFormat::LEGACY); + assert!(f.is_legacy()); + assert_eq!( + f.banner(), + "ZF FORMAT: cap=off whir_cap=off fri=pair one_row=0 whir_folds=uniform4" + ); + assert!(f.unimplemented_levers().is_empty()); + assert!(f.proof_format().is_legacy()); + assert_eq!(f.proof_format(), ProofFormat::LEGACY); + assert_eq!(f.chain_format(), ChainFormat::DEFAULT); + } + + /// One knob at its off spelling turns off that lever ONLY; the others keep + /// the default's value. + #[test] + fn one_off_knob_turns_off_one_lever() { + for (name, v, want) in [ + ( + ENV_CAP, + "off", + ZfFormat { + cap: CapPolicy::Off, + ..ZfFormat::DEFAULT + }, + ), + ( + ENV_WHIR_CAP, + "off", + ZfFormat { + whir_cap: CapPolicy::Off, + ..ZfFormat::DEFAULT + }, + ), + ( + ENV_FRI, + "pair", + ZfFormat { + fri: FriMode::Pair, + ..ZfFormat::DEFAULT + }, + ), + ( + ENV_WHIR_FOLDS, + "uniform4", + ZfFormat { + whir_folds: WhirFolds::Uniform, + ..ZfFormat::DEFAULT + }, + ), + ] { + let f = parse(&[(name, v)]).unwrap(); + assert_eq!(f, want, "{name}={v}"); + assert!(!f.is_legacy(), "{name}={v}"); + } + } + + #[test] + fn every_accepted_spelling_parses() { + for (v, want) in [ + ("off", CapPolicy::Off), + ("auto", CapPolicy::Auto), + ("0", CapPolicy::Off), + ("3", CapPolicy::Fixed(3)), + ("16", CapPolicy::Fixed(16)), + (" AUTO ", CapPolicy::Auto), + ] { + assert_eq!(parse(&[(ENV_CAP, v)]).unwrap().cap, want, "{v:?}"); + assert_eq!(parse(&[(ENV_WHIR_CAP, v)]).unwrap().whir_cap, want, "{v:?}"); + } + assert_eq!(parse(&[(ENV_FRI, "dp")]).unwrap().fri, FriMode::Dp); + assert_eq!( + parse(&[(ENV_ONE_ROW, "1")]).unwrap().one_row, + OneRowMode::On + ); + assert_eq!( + parse(&[(ENV_ONE_ROW, "auto")]).unwrap().one_row, + OneRowMode::Auto + ); + for k in [5, 6] { + assert_eq!( + parse(&[(ENV_WHIR_FOLDS, &format!("first{k}"))]) + .unwrap() + .whir_folds, + WhirFolds::First(FirstFold::new(k).unwrap()) + ); + } + assert_eq!( + parse(&[(ENV_WHIR_FOLDS, " FIRST6 ")]).unwrap().whir_folds, + WhirFolds::First(FirstFold::new(6).unwrap()) + ); + } + + #[test] + fn every_bad_spelling_is_refused_and_names_its_knob() { + for (name, v) in [ + (ENV_CAP, ""), + (ENV_CAP, "17"), + (ENV_CAP, "on"), + (ENV_CAP, "-1"), + (ENV_CAP, "3.0"), + (ENV_WHIR_CAP, "yes"), + (ENV_FRI, "binary"), + (ENV_FRI, ""), + (ENV_ONE_ROW, "2"), + (ENV_ONE_ROW, "on"), + (ENV_WHIR_FOLDS, "uniform"), + (ENV_WHIR_FOLDS, "uniform3"), + (ENV_WHIR_FOLDS, ""), + (ENV_WHIR_FOLDS, "4,4,4"), + (ENV_WHIR_FOLDS, "dp"), + (ENV_WHIR_FOLDS, "first"), + (ENV_WHIR_FOLDS, "first4"), + (ENV_WHIR_FOLDS, "first3"), + (ENV_WHIR_FOLDS, "first7"), + (ENV_WHIR_FOLDS, "first0"), + (ENV_WHIR_FOLDS, "first 6"), + (ENV_WHIR_FOLDS, "first06"), + (ENV_WHIR_FOLDS, "list:6,4"), + ] { + let err = parse(&[(name, v)]).expect_err(&format!("{name}={v:?} must be refused")); + assert!(err.contains(name), "{err}"); + } + } + + #[test] + fn the_banner_round_trips_through_the_knobs() { + let f = ZfFormat { + cap: CapPolicy::Auto, + whir_cap: CapPolicy::Fixed(2), + fri: FriMode::Dp, + one_row: OneRowMode::Auto, + whir_folds: WhirFolds::First(FirstFold::new(6).unwrap()), + }; + assert_eq!( + f.banner(), + "ZF FORMAT: cap=auto whir_cap=2 fri=dp one_row=auto whir_folds=first6" + ); + // Every banner value is a spelling its knob accepts, back to the same + // format. + let banner = f.banner(); + let fields: HashMap<&str, &str> = banner + .trim_start_matches("ZF FORMAT: ") + .split(' ') + .map(|kv| kv.split_once('=').unwrap()) + .collect(); + let back = parse(&[ + (ENV_CAP, fields["cap"]), + (ENV_WHIR_CAP, fields["whir_cap"]), + (ENV_FRI, fields["fri"]), + (ENV_ONE_ROW, fields["one_row"]), + (ENV_WHIR_FOLDS, fields["whir_folds"]), + ]) + .unwrap(); + assert_eq!(back, f); + } + + #[test] + fn a_lever_this_build_lacks_is_reported() { + // Wave A implements none of the levers; the list names each knob set. + let f = parse(&[(ENV_CAP, "auto"), (ENV_FRI, "dp")]).unwrap(); + let missing = f.unimplemented_levers(); + // S3 is implemented on the host (stark::proof::options::FRI_MODE_IMPLEMENTED). + const { assert!(stark::proof::options::FRI_MODE_IMPLEMENTED) }; + assert!(!missing.contains(&ENV_FRI), "fri=dp is selectable"); + if !stark::proof::options::MERKLE_CAP_IMPLEMENTED { + assert!(missing.contains(&ENV_CAP)); + } + if !stark::proof::options::FRI_MODE_IMPLEMENTED { + assert!(missing.contains(&ENV_FRI)); + } + assert!( + !missing.contains(&ENV_ONE_ROW), + "an unset knob is never reported" + ); + } + + #[test] + fn the_one_row_knob_is_selectable() { + // S2 is implemented on the host CPU paths: `LAMBDA_VM_ZF_ONE_ROW` no + // longer aborts, and every spelling reaches the options unchanged. + const { assert!(stark::proof::options::ONE_ROW_IMPLEMENTED) }; + for (v, want) in [ + ("1", OneRowMode::On), + ("auto", OneRowMode::Auto), + ("0", OneRowMode::Off), + ] { + let f = parse(&[(ENV_ONE_ROW, v)]).unwrap(); + assert!(f.unimplemented_levers().is_empty(), "{v}"); + let base = crate::GoldilocksCubicProofOptions::with_blowup(4).unwrap(); + assert_eq!(f.options(base).format.one_row, want, "{v}"); + } + assert_eq!( + parse(&[(ENV_ONE_ROW, "auto"), (ENV_FRI, "dp")]) + .unwrap() + .banner(), + "ZF FORMAT: cap=auto whir_cap=auto fri=dp one_row=auto whir_folds=first6" + ); + } + + #[test] + fn the_merkle_cap_knob_is_selectable() { + // The STARK cap is real on host and device, so `LAMBDA_VM_ZF_CAP` does + // not abort; every spelling reaches the options unchanged. + const { assert!(stark::proof::options::MERKLE_CAP_IMPLEMENTED) }; + for (v, want) in [ + ("auto", CapPolicy::Auto), + ("3", CapPolicy::Fixed(3)), + ("off", CapPolicy::Off), + ] { + let f = parse(&[(ENV_CAP, v)]).unwrap(); + assert!(!f.unimplemented_levers().contains(&ENV_CAP), "{v}"); + let base = crate::GoldilocksCubicProofOptions::with_blowup(4).unwrap(); + assert_eq!(f.options(base).format.merkle_cap, want, "{v}"); + } + } + + #[test] + fn the_whir_cap_is_implemented_and_selectable() { + const { assert!(multilinear::whir_chain::WHIR_CAP_IMPLEMENTED) }; + for v in ["auto", "3"] { + let f = parse(&[(ENV_WHIR_CAP, v)]).unwrap(); + assert!( + !f.unimplemented_levers().contains(&ENV_WHIR_CAP), + "LAMBDA_VM_ZF_WHIR_CAP={v} must be selectable" + ); + } + } + + #[test] + fn the_whir_fold_lever_is_selectable() { + const { assert!(multilinear::whir_chain::WHIR_FOLDS_IMPLEMENTED) }; + for v in ["first5", "first6"] { + let f = parse(&[(ENV_WHIR_FOLDS, v)]).unwrap(); + assert!(f.unimplemented_levers().is_empty(), "{v}"); + } + } + + #[test] + fn apply_stamps_only_the_format_fields() { + let base = crate::GoldilocksCubicProofOptions::with_blowup(4).unwrap(); + assert!(base.has_default_format()); + let f = ZfFormat { + cap: CapPolicy::Auto, + fri: FriMode::Dp, + one_row: OneRowMode::On, + ..ZfFormat::DEFAULT + }; + let o = f.options(base.clone()); + assert_eq!(o.format.merkle_cap, CapPolicy::Auto); + assert_eq!(o.format.fri_mode, FriMode::Dp); + assert_eq!(o.format.one_row, OneRowMode::On); + assert!(!o.has_default_format()); + assert_eq!(o.blowup_factor, base.blowup_factor); + assert_eq!(o.fri_number_of_queries, base.fri_number_of_queries); + assert_eq!(o.grinding_factor, base.grinding_factor); + assert_eq!(o.coset_offset, base.coset_offset); + assert_eq!(o.fri_final_poly_log_degree, base.fri_final_poly_log_degree); + // The legacy format leaves options untouched. + let d = ZfFormat::LEGACY.options(base.clone()); + assert!(d.has_default_format()); + assert!(d.has_legacy_format()); + // The production default stamps cap=auto and fri=dp, nothing else. + let p = ZfFormat::DEFAULT.options(base.clone()); + assert_eq!(p.format.merkle_cap, CapPolicy::Auto); + assert_eq!(p.format.fri_mode, FriMode::Dp); + assert_eq!(p.format.one_row, ZfFormat::DEFAULT.one_row); + assert_eq!(p.format.fri_schedule_override, None); + assert!(!p.has_legacy_format()); + assert_eq!( + ( + p.blowup_factor, + p.fri_number_of_queries, + p.grinding_factor, + p.coset_offset, + p.fri_final_poly_log_degree + ), + ( + base.blowup_factor, + base.fri_number_of_queries, + base.grinding_factor, + base.coset_offset, + base.fri_final_poly_log_degree + ), + "no security parameter moves with the format" + ); + + let chain = crate::multilinear_prove::chain_config_under(&ZfFormat::LEGACY, &[(8, 20)]); + let c = ZfFormat { + whir_cap: CapPolicy::Fixed(3), + whir_folds: WhirFolds::First(FirstFold::new(5).unwrap()), + ..ZfFormat::LEGACY + } + .chain(chain); + assert_eq!(c.format.cap, CapPolicy::Fixed(3)); + assert_eq!(c.format.folds, WhirFolds::First(FirstFold::new(5).unwrap())); + assert_eq!( + (c.log_blowup, c.log_folding, c.num_queries, c.grind), + ( + chain.log_blowup, + chain.log_folding, + chain.num_queries, + chain.grind + ) + ); + } + + #[test] + fn the_schedule_line_states_the_rounds() { + assert_eq!( + ZfFormat::LEGACY.whir_schedule_line(), + "ZF WHIR SCHEDULES: whir_folds=uniform4 q=112 n=20:[4,4,4,4,4] \ + n=21:[4,4,4,4,4,1] n=22:[4,4,4,4,4,2] n=23:[4,4,4,4,4,3] \ + n=24:[4,4,4,4,4,4] n=25:[4,4,4,4,4,4,1]" + ); + let first6 = ZfFormat { + whir_folds: WhirFolds::First(FirstFold::new(6).unwrap()), + ..ZfFormat::LEGACY + }; + // The production default runs the first6 schedules. + assert_eq!( + ZfFormat::DEFAULT.whir_schedule_line(), + first6.whir_schedule_line() + ); + assert_eq!( + first6.whir_schedule_line(), + "ZF WHIR SCHEDULES: whir_folds=first6 q=112 n=20:[6,4,4,4,2] \ + n=21:[6,4,4,4,3] n=22:[6,4,4,4,4] n=23:[6,4,4,4,4,1] \ + n=24:[6,4,4,4,4,2] n=25:[6,4,4,4,4,3]" + ); + } + + /// The production WHIR config under each accepted knob value: the format + /// is carried, Q is charged the schedule's rounds, and at the block's + /// tallest stack (25) every arm keeps today's Q = 112. + #[test] + fn the_production_chain_config_under_each_arm() { + use crate::multilinear_prove::chain_config_under; + let today = chain_config_under(&ZfFormat::LEGACY, &[(1, 25)]); + assert_eq!(today.format, ChainFormat::DEFAULT); + assert_eq!((today.rounds(25), today.num_queries), (7, 112)); + // The production config (no knob set) is the measured default's: + // cap auto, first6 — six rounds at 25, Q unchanged at 112. + let production = crate::multilinear_prove::chain_config(&[(1, 25)]); + assert_eq!( + production, + chain_config_under(&ZfFormat::DEFAULT, &[(1, 25)]) + ); + assert_eq!(production.format, ZfFormat::DEFAULT.chain_format()); + assert_eq!((production.rounds(25), production.num_queries), (6, 112)); + assert_eq!(production.schedule(25), vec![6, 4, 4, 4, 4, 3]); + assert_eq!( + ( + production.log_blowup, + production.log_folding, + production.grind + ), + (today.log_blowup, today.log_folding, today.grind), + "no security parameter moves with the format" + ); + for (name, rounds25) in [("first5", 6), ("first6", 6)] { + let f = parse(&[(ENV_WHIR_FOLDS, name)]).unwrap(); + let c = chain_config_under(&f, &[(1, 25)]); + assert_eq!(c.format.folds, f.whir_folds); + assert_eq!((c.rounds(25), c.num_queries), (rounds25, 112), "{name}"); + assert_eq!( + (c.log_blowup, c.log_folding, c.grind), + (today.log_blowup, today.log_folding, today.grind) + ); + assert_ne!(c.fold_word(), today.fold_word()); + } + } + + #[test] + fn production_sites_build_the_default_format_when_nothing_is_set() { + // No test sets a ZF knob, so the process format is the default and the + // production constructors must stamp the MEASURED configuration. + assert_eq!(*ZfFormat::global(), ZfFormat::DEFAULT); + let want = ZfFormat::DEFAULT.proof_format(); + for (site, o) in [ + ( + "aggregation_wrap_options", + crate::lfm::proof::aggregation_wrap_options(), + ), + ( + "block_base_options", + crate::lfm::proof::block_base_options(), + ), + ] { + assert_eq!(o.format, want, "{site}"); + assert!(!o.has_legacy_format(), "{site}"); + } + // Security parameters are the legacy presets'. + let base = crate::lfm::proof::block_base_options(); + let preset = crate::recursion::Preset::Blowup4.options(); + assert_eq!( + ( + base.blowup_factor, + base.fri_number_of_queries, + base.grinding_factor, + base.coset_offset, + base.fri_final_poly_log_degree + ), + ( + preset.blowup_factor, + preset.fri_number_of_queries, + preset.grinding_factor, + preset.coset_offset, + preset.fri_final_poly_log_degree + ) + ); + let chain = crate::multilinear_prove::chain_config(&[(8, 20)]); + assert_eq!(chain.format, ZfFormat::DEFAULT.chain_format()); + assert_eq!(chain.log_folding, PRODUCTION_WHIR_LOG_FOLDING); + } + + /// ★ The RV64 guest verifier stays on + /// the LEGACY format after the default flip. Its presets NAME the legacy + /// format (not the process default), and both guest entries refuse every + /// other format — the production default included. + #[test] + fn the_recursion_guest_stays_on_the_legacy_format() { + for preset in crate::recursion::Preset::ALL { + let o = preset.options(); + assert_eq!(o.format, ProofFormat::LEGACY, "{}", preset.name()); + assert!(o.has_legacy_format(), "{}", preset.name()); + } + assert_eq!( + crate::recursion::MIN_PROOF_OPTIONS.format, + ProofFormat::LEGACY + ); + // The production default is NOT legacy, so the presets cannot have + // inherited their format from it. + assert!(!ZfFormat::DEFAULT.is_legacy()); + assert!(!ZfFormat::DEFAULT.proof_format().is_legacy()); + + let base = crate::recursion::Preset::Blowup4.options(); + let refused = |opts: &ProofOptions| { + for result in [ + crate::recursion::verify_and_attest_blob(&[], opts), + crate::recursion::verify_continuation_and_attest(&[], opts), + ] { + let err = result.expect_err("a non-legacy format must be refused"); + assert!(format!("{err:?}").contains("legacy-format"), "{err:?}"); + } + }; + for f in [ + ZfFormat::DEFAULT, + ZfFormat { + cap: CapPolicy::Auto, + ..ZfFormat::LEGACY + }, + ZfFormat { + fri: FriMode::Dp, + ..ZfFormat::LEGACY + }, + ZfFormat { + one_row: OneRowMode::On, + ..ZfFormat::LEGACY + }, + ZfFormat { + one_row: OneRowMode::Auto, + ..ZfFormat::LEGACY + }, + ] { + refused(&f.options(base.clone())); + } + // The production STARK base options are refused whenever the process + // format is not legacy (always, with no knob set). + if !ZfFormat::global().proof_format().is_legacy() { + refused(&crate::lfm::proof::block_base_options()); + } + // The legacy format gets past the guard (and fails on the empty blob). + for opts in [base.clone(), ZfFormat::LEGACY.options(base.clone())] { + for result in [ + crate::recursion::verify_and_attest_blob(&[], &opts), + crate::recursion::verify_continuation_and_attest(&[], &opts), + ] { + if let Err(err) = result { + assert!(!format!("{err:?}").contains("legacy-format"), "{err:?}"); + } + } + } + } + + #[test] + fn the_serialized_options_bytes_ignore_the_format_fields() { + // The format fields are skipped by serde and rkyv, so a + // serialized `ProofOptions` has the same bytes whatever the format, + // and deserializes to the default format. + let base = crate::GoldilocksCubicProofOptions::with_blowup(4).unwrap(); + let capped = ZfFormat { + cap: CapPolicy::Auto, + fri: FriMode::Dp, + one_row: OneRowMode::Auto, + ..ZfFormat::DEFAULT + } + .options(base.clone()); + let a = rkyv::to_bytes::(&base).unwrap(); + let b = rkyv::to_bytes::(&capped).unwrap(); + assert_eq!(a.as_slice(), b.as_slice()); + let back: ProofOptions = rkyv::from_bytes::(&b).unwrap(); + assert!(back.has_default_format()); + assert_eq!(back.fri_number_of_queries, base.fri_number_of_queries); + + let ja = serde_json::to_string(&base).unwrap(); + let jb = serde_json::to_string(&capped).unwrap(); + assert_eq!(ja, jb); + assert!(!ja.contains("merkle_cap") && !ja.contains("fri_mode") && !ja.contains("one_row")); + let back: ProofOptions = serde_json::from_str(&jb).unwrap(); + assert!(back.has_default_format()); + } +} diff --git a/prover/tests/merkle_cap_vm.rs b/prover/tests/merkle_cap_vm.rs new file mode 100644 index 000000000..b9cbee11e --- /dev/null +++ b/prover/tests/merkle_cap_vm.rs @@ -0,0 +1,121 @@ +//! A real VM proof under the Merkle cap policy the PROCESS FORMAT names +//! (`LAMBDA_VM_ZF_CAP`): every production table — the +//! preprocessed ones (precomputed + main trees), the LogUp aux trees, the +//! composition trees and every committed FRI layer — capped, proved and +//! verified through the public `prove_with_options_and_inputs` / +//! `verify_with_options` entry points. +//! +//! Knob-on only, hence `#[ignore]`: at the default format it would prove +//! nothing new, so it refuses to run unless `LAMBDA_VM_ZF_CAP` selects a cap. +//! +//! ```text +//! LAMBDA_VM_ZF_CAP=auto cargo test --release -p lambda-vm-prover --test merkle_cap_vm -- --ignored --nocapture +//! LAMBDA_VM_ZF_CAP=auto cargo test --release -p lambda-vm-prover --features cuda --test merkle_cap_vm -- --ignored --nocapture --test-threads=1 +//! ``` +//! +//! Under `cuda` the fixture is big enough that its tables commit on the device, +//! and the caps must come off the resident trees (`gpu_cap_read_calls`). + +use lambda_vm_prover::test_utils::asm_elf_bytes; +use lambda_vm_prover::zf_format::ZfFormat; +use lambda_vm_prover::{ + GoldilocksCubicProofOptions, MaxRowsConfig, prove_with_options_and_inputs, verify_with_options, +}; +use stark::merkle_caps::StarkCaps; + +/// CPU: a fixture that touches every instruction class (many tables). Device: +/// the fixture the cuda integration tests use, whose tables cross the GPU LDE +/// threshold. +#[cfg(not(feature = "cuda"))] +const FIXTURE: &str = "all_instructions_64"; +#[cfg(feature = "cuda")] +const FIXTURE: &str = "fib_iterative_1M"; + +#[test] +#[ignore = "knob-on: run with LAMBDA_VM_ZF_CAP=auto (or a height) and -- --ignored"] +fn a_vm_proof_round_trips_under_the_process_cap_policy() { + let format = ZfFormat::from_env().expect("a valid ZF format"); + assert!( + !format.cap.is_off(), + "LAMBDA_VM_ZF_CAP is unset or off: this test only means something with a cap" + ); + println!("{}", format.banner()); + let base = GoldilocksCubicProofOptions::with_blowup(2).expect("blowup 2"); + let capped = format.options(base.clone()); + let mut default = base; + default.format = Default::default(); + assert!(default.has_default_format()); + + let elf = asm_elf_bytes(FIXTURE); + #[cfg(feature = "cuda")] + let before = stark::gpu_lde::gpu_cap_read_calls(); + let proof = prove_with_options_and_inputs(&elf, &[], &capped, &MaxRowsConfig::default()) + .expect("prove under the cap policy"); + #[cfg(feature = "cuda")] + { + let reads = stark::gpu_lde::gpu_cap_read_calls() - before; + println!("CAPVM device cap reads: {reads}"); + assert!( + reads > 0, + "no cap came off the device: {FIXTURE} proved on host trees" + ); + } + + assert!( + verify_with_options(&proof, &elf, &capped, None, None).expect("verify"), + "a capped VM proof must verify under its own policy" + ); + // Non-vacuity: the policy engaged. Every table's main-tree paths have the + // lengths the verifier's `StarkCaps` gives its shape, and at least one + // table is really capped — so the default verifier below meets paths that + // differ from the ones it expects, and its refusal means something. + let mut capped_tables = 0usize; + for (t, p) in proof.proof.proofs.iter().enumerate() { + let lde_log = (p.trace_length * usize::from(capped.blowup_factor)).trailing_zeros(); + let caps = StarkCaps::new( + format.cap, + capped.fri_number_of_queries, + lde_log as usize, + p.fri_layers_merkle_roots.len(), + ); + let path = |q: usize| { + p.deep_poly_openings[q] + .main_trace_polys + .proof + .merkle_path + .len() + }; + assert_eq!( + path(1), + caps.trace_depth - caps.trace, + "table {t}: a non-owner main path is not cut to the cap" + ); + if caps.trace > 0 { + assert_eq!( + path(0), + caps.trace_depth - caps.trace + (1 << caps.trace), + "table {t}: the owner path does not carry the cap" + ); + capped_tables += 1; + } + } + println!( + "CAPVM capped tables: {capped_tables} of {}", + proof.proof.proofs.len() + ); + assert!( + capped_tables > 0, + "no table was capped: the policy did not engage and the cross-format check below is vacuous" + ); + // The cap height is a verifier constant: the default verifier must refuse + // the capped proof (full-length paths expected), without panicking. This + // held only once the AIR prototype cache keyed the proof format: before, + // the prove above cached capped AIRs and `&default` got them back. + assert!( + !matches!( + verify_with_options(&proof, &elf, &default, None, None), + Ok(true) + ), + "a capped proof accepted by the default-format verifier" + ); +} diff --git a/prover/tests/whir_transcript_configuration.rs b/prover/tests/whir_transcript_configuration.rs index 647a41ddd..55a3e5d89 100644 --- a/prover/tests/whir_transcript_configuration.rs +++ b/prover/tests/whir_transcript_configuration.rs @@ -73,6 +73,7 @@ fn config() -> ChainConfig { log_folding: 2, num_queries: 3, grind: GrindBits::uniform(4), + format: multilinear::whir_chain::ChainFormat::DEFAULT, } }