From ab7208ff1d271764b31702ac844884782396aeec Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:16:48 -0300 Subject: [PATCH 01/73] feat(crypto): Merkle cap primitive and the cap-height policy A tree of depth D can publish its cap, the 2^c nodes c levels below the root, once, and cut every authentication path to D - c siblings. The verifier folds each path onto cap[index >> (D - c)] and checks once per tree that the cap hashes to the committed root. The root stays the commitment, so no transcript changes. New merkle_tree::cap module (design/CAP.md sections 1-2): - MerkleTree::depth / MerkleTree::cap (heap slice, disk-spill safe; None for a root-only tree or c > depth: fail closed, never clamp). - Proof::truncate_to_cap, cap_root, verify_cap, and verify_merkle_path_to_cap_from_leaf_hash, which checks the exact path length, index < 2^D and the cap length, then runs the unchanged verify_merkle_path_from_leaf_hash against the cap node. - The owner-path wire encoding (split_owner_path / embed_cap): the cap rides at the end of the tree's first opening, so c = 0 moves no byte. - CappedRoot: one per tree; its only capped constructor authenticates the cap against the root before handing it out. - CapPolicy { Off, Auto, Fixed(c) } with the integer cost-law weights (AUTO_WEIGHTS) that give c = 3 at >= 20 openings, 2 at 4-19, 0 below, clamped to the depth; FromStr/Display for the knob spellings. Nothing calls it yet. Tests: the cap is the heap slice and hashes to the root for 1..1024 leaves and every height; every leaf verifies against its cap node, and at c = 0 agrees with the full-path check; tamper tests (cap byte, path node, swapped cap nodes, foreign cap, flipped index bit, path length +-1, cap on the wrong opening, wrong height); mutation-style tests that run the same property against copies without the length check, with the wrong cap index, and without the cap-to-root check, and show each copy fails; the Auto heights and weights pinned. --- crypto/crypto/src/merkle_tree/cap.rs | 1104 +++++++++++++++++++++++ crypto/crypto/src/merkle_tree/merkle.rs | 33 + crypto/crypto/src/merkle_tree/mod.rs | 1 + 3 files changed, 1138 insertions(+) create mode 100644 crypto/crypto/src/merkle_tree/cap.rs diff --git a/crypto/crypto/src/merkle_tree/cap.rs b/crypto/crypto/src/merkle_tree/cap.rs new file mode 100644 index 000000000..9ff4cdc15 --- /dev/null +++ b/crypto/crypto/src/merkle_tree/cap.rs @@ -0,0 +1,1104 @@ +//! 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. `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; + } + 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. + 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 => { + // argmax, ties to the smaller height; gain(·, 0) = 0. + 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 + } + } + } +} + +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)); + } + + // ------------------------------------------------------------ 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)); + } + + #[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 (CAP.md §2). + 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; From f82e42bce75dda899fc5e8a54ab1fc52a506a057 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:17:48 -0300 Subject: [PATCH 02/73] fix(stark): require exact authentication-path lengths in the verifier The host verifier folded an authentication path of any length and compared the result with the root. A path one node short compares an internal node with the root. No exploit is known: it needs a leaf hash equal to an internal node, a cross-function collision under the algebraic backend (design/CAP.md section 9.4). But every tree depth is a verifier constant, so it is now enforced: - trace, precomputed, aux and composition trees: log2(lde) - 1 (row-pair leaves; 0 for a two-point LDE, where the leaf hash is the root); - committed FRI layer i: log2(lde) - i - 2. Every opening now goes through CappedRoot::uncapped(root, depth), the cap primitive at c = 0: the same fold as before, plus the exact-length and index < 2^depth checks. Honest proofs already meet these lengths, so they verify unchanged. Only malformed proofs see a difference. No proof byte, root or transcript moves. The archived (guest) path shares these functions, so it is hardened too. Tests (tests::path_length_tests, small AIR, 1024 rows): honest proofs carry exactly the verifier depths for main, composition and every FRI layer, and verify. A path one node short or long is rejected for main (first and last query), composition, and the first and last FRI layer. With honest data, the old code also rejected these lengths, because a forgery needs a collision. The check is shown load-bearing at the primitive level (merkle_tree::cap mutation test with an identity leaf hash). --- crypto/stark/src/tests/mod.rs | 1 + crypto/stark/src/tests/path_length_tests.rs | 143 ++++++++++++++++++++ crypto/stark/src/verifier.rs | 63 ++++++--- 3 files changed, 191 insertions(+), 16 deletions(-) create mode 100644 crypto/stark/src/tests/path_length_tests.rs diff --git a/crypto/stark/src/tests/mod.rs b/crypto/stark/src/tests/mod.rs index f2520e2c4..a757e909a 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -10,6 +10,7 @@ pub mod domain_cache_stats; pub mod fri_tests; pub mod grinding_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; 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..88927ee3f --- /dev/null +++ b/crypto/stark/src/tests/path_length_tests.rs @@ -0,0 +1,143 @@ +//! 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 used to fold a path of any length and +//! compare the result with the root; it now requires the exact length +//! (design/CAP.md §9.4, commit C1b). 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/verifier.rs b/crypto/stark/src/verifier.rs index 891df1a91..ad093e182 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -18,7 +18,8 @@ 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::cap::CappedRoot; +use crypto::merkle_tree::traits::IsMerkleTreeBackend; use crypto::merkle_tree::traits::IsStreamingLeafBackend; #[cfg(not(feature = "test_fiat_shamir"))] use log::error; @@ -535,6 +536,10 @@ pub trait IsStarkVerifier< return false; } + // `log2` of the LDE size: every tree's depth is a function of it (a + // verifier constant, never read from the proof). + let lde_log = domain.lde_length.trailing_zeros() as usize; + let terminal_offset = domain.coset_offset.pow(1u64 << layout.total_folds); let terminal_codeword = crate::fri::terminal::terminal_codeword_from_coeffs::( @@ -566,6 +571,7 @@ pub trait IsStarkVerifier< &deep_poly_evaluations[i], &deep_poly_evaluations_sym[i], &terminal_codeword, + lde_log, ) }) } @@ -587,10 +593,14 @@ 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. + /// + /// The path must be exactly `depth` siblings long (`log2(lde) − 1`, a + /// verifier constant): see [`trace_tree_depth`](Self::trace_tree_depth). fn verify_opening_pair( opening: PolynomialOpeningsView<'_, E>, root: &Commitment, iota: usize, + depth: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -605,9 +615,8 @@ pub trait IsStarkVerifier< opening.evaluations(), opening.evaluations_sym(), ); - verify_merkle_path_from_leaf_hash::>( + CappedRoot::uncapped(root, depth).verify::>( opening.merkle_path(), - root, iota, leaf_hash, ) @@ -619,6 +628,7 @@ pub trait IsStarkVerifier< proof: StarkProofView<'_, Field, FieldExtension, PI>, deep_poly_openings: DeepPolynomialOpeningView<'_, Field, FieldExtension>, iota: usize, + depth: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -629,6 +639,7 @@ pub trait IsStarkVerifier< deep_poly_openings.main_trace_polys(), proof.lde_trace_main_merkle_root(), iota, + depth, ); // Precomputed trace (preprocessed tables only). Mismatched presence: @@ -645,7 +656,9 @@ pub trait IsStarkVerifier< proof.lde_trace_precomputed_merkle_root(), deep_poly_openings.precomputed_trace_polys(), ) { - (Some(root), Some(opening)) => Self::verify_opening_pair::(opening, root, iota), + (Some(root), Some(opening)) => { + Self::verify_opening_pair::(opening, root, iota, depth) + } (None, None) => true, _ => false, }; @@ -662,7 +675,7 @@ pub trait IsStarkVerifier< deep_poly_openings.aux_trace_polys(), ) { (Some(root), Some(opening)) => { - Self::verify_opening_pair::(opening, root, iota) + Self::verify_opening_pair::(opening, root, iota, depth) } (None, None) => true, _ => false, @@ -677,6 +690,7 @@ pub trait IsStarkVerifier< deep_poly_openings: DeepPolynomialOpeningView<'_, Field, FieldExtension>, composition_poly_merkle_root: &Commitment, iota: &usize, + depth: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -691,12 +705,8 @@ pub trait IsStarkVerifier< composition_poly.evaluations_sym(), ); - verify_merkle_path_from_leaf_hash::>( - composition_poly.merkle_path(), - composition_poly_merkle_root, - *iota, - leaf_hash, - ) + CappedRoot::uncapped(composition_poly_merkle_root, depth) + .verify::>(composition_poly.merkle_path(), *iota, leaf_hash) } /// Verifies the validity of the purported values of the trace polynomials and the composition polynomial @@ -705,6 +715,7 @@ pub trait IsStarkVerifier< fn step_4_verify_trace_and_composition_openings( proof: StarkProofView<'_, Field, FieldExtension, PI>, challenges: &Challenges, + domain: &VerifierDomain, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -715,16 +726,32 @@ pub trait IsStarkVerifier< >(); // `step_3_verify_fri` (which runs before this) already rejects proofs // whose `deep_poly_openings` is shorter than `challenges.iotas`. + let depth = Self::trace_tree_depth(domain); 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) + depth, + ) && Self::verify_trace_openings(proof, deep_poly_opening, *iota_n, depth) }) } + /// 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). Every authentication + /// path into these trees must be exactly this long. + /// + /// Before this was checked, a path of any length was folded and compared + /// with the root; a short one compares an internal node with the root. No + /// exploit was shown (it needs a leaf hash equal to an internal node, a + /// cross-function collision under the algebraic backend), but the length + /// is a verifier constant, so it is now enforced (design/CAP.md §9.4). + fn trace_tree_depth(domain: &VerifierDomain) -> usize { + (domain.lde_length.trailing_zeros() as usize).saturating_sub(1) + } + /// Verifies the openings of a fold polynomial of an inner layer of FRI. fn verify_fri_layer_openings( merkle_root: &Commitment, @@ -732,6 +759,7 @@ pub trait IsStarkVerifier< evaluation: &FieldElement, evaluation_sym: &FieldElement, iota: usize, + depth: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -743,11 +771,10 @@ pub trait IsStarkVerifier< vec![evaluation.clone(), evaluation_sym.clone()] }; - verify_merkle_path::>( + CappedRoot::uncapped(merkle_root, depth).verify::>( auth_path_sym, - merkle_root, iota >> 1, - &evaluations, + as IsMerkleTreeBackend>::hash_data(&evaluations), ) } @@ -769,6 +796,7 @@ pub trait IsStarkVerifier< deep_composition_evaluation: &FieldElement, deep_composition_evaluation_sym: &FieldElement, terminal_codeword: &[FieldElement], + lde_log: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -826,6 +854,9 @@ pub trait IsStarkVerifier< &v, evaluation_sym, index, + // Layer `i` holds `lde / 2^(i+1)` values in pair + // leaves: `log2(lde) − i − 2` levels. + lde_log.saturating_sub(i + 2), ); // Update `v` with next value pᵢ₊₁(𝜐^(2ⁱ⁺¹)). @@ -1809,7 +1840,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, &domain) { #[cfg(not(feature = "test_fiat_shamir"))] error!("DEEP Composition Polynomial verification failed"); return false; From 77ea1ab890c3b17026a6068a268dd6d8cc764830 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:30:14 -0300 Subject: [PATCH 03/73] feat(prover): ZfFormat, the one proof-format config, and its option fields The ZF campaign's levers get one config and one banner before any lever lands, so the option structs stay stable while the lanes fill them in. prover/src/zf_format.rs: ZfFormat { cap, whir_cap, fri, one_row, whir_folds }, parsed from LAMBDA_VM_ZF_CAP / _WHIR_CAP / _FRI / _ONE_ROW / _WHIR_FOLDS through from_lookup (tests pass a map; none sets the env). ZfFormat::global() reads it once and prints "ZF FORMAT: cap=off whir_cap=off fri=pair one_row=0 whir_folds=uniform4" on every setting, including the default. An unknown value aborts. So does a knob set to a lever this build does not implement yet, because otherwise a run could print a non-default format and prove the default one. Each lane flips its *_IMPLEMENTED constant; all are false here. The format travels in the crypto crates' own option types (RULINGS 9): - stark::ProofOptions.format: ProofFormat { merkle_cap, fri_mode, one_row }. FriMode and OneRowMode are defined next to it. - multilinear::ChainConfig.format: ChainFormat { cap, folds } with WhirFolds { Uniform, Dp, List(FoldList) }. Each is grouped in one field, so a literal names the format in one line and a lever added later touches only its format struct. Literal sites get format: ...::DEFAULT (mechanical). RULINGS 10, what I found: repo-wide, nothing serializes a ProofOptions into pinned bytes. The one by-value holder, AirContext, derives no serde or rkyv, and no rkyv/bincode/serde_json call takes options. ChainConfig has no serde or rkyv derive. To make this hold by construction, ProofOptions.format is #[serde(skip)] and #[rkyv(with = Skip)] (default on deserialize), so serialized options keep today's bytes whatever the format; a test pins that for rkyv and serde_json. The WHIR statement absorbs (push_config, multilinear absorb x3) bind format: _ and do not absorb it: absorbing it would move every transcript at the default. Production format sites: aggregation_wrap_options (every LFM proof), chain_config (WHIR base), and the new block_base_options (STARK base epochs: the Blowup4 preset plus the format), used by the three production drivers in per_table_aggregator_tests. With nothing set, each builds today's value (tested). RULINGS 11: both RV64 recursion-guest entries (verify_and_attest_blob, verify_continuation_and_attest) refuse options with a non-default format, returning an error rather than panicking. No lever does anything yet. Defaults are today's bytes. --- crypto/multilinear/src/constraint_argument.rs | 3 + crypto/multilinear/src/stacked_eval.rs | 1 + crypto/multilinear/src/whir_chain.rs | 91 +++ .../tests/host_fallback_counter.rs | 1 + crypto/stark/benches/profile_prover.rs | 1 + crypto/stark/benches/prover_benchmark.rs | 1 + crypto/stark/src/multilinear_air.rs | 1 + crypto/stark/src/multilinear_table.rs | 1 + crypto/stark/src/proof/options.rs | 149 +++++ crypto/stark/src/tests/prover_tests.rs | 7 + prover/src/lfm/epoch_tests.rs | 1 + prover/src/lfm/per_table_aggregator_tests.rs | 9 +- prover/src/lfm/per_table_census_tests.rs | 1 + prover/src/lfm/proof.rs | 18 +- prover/src/lfm/whir_chain_tests.rs | 1 + prover/src/lfm/whir_epoch_program_tests.rs | 1 + prover/src/lfm/whir_stacked_tests.rs | 1 + prover/src/lfm/whir_statement.rs | 6 + prover/src/lib.rs | 1 + prover/src/multilinear_continuation.rs | 6 + prover/src/multilinear_prove.rs | 16 +- prover/src/recursion.rs | 19 + prover/src/tests/decode_prepared_tests.rs | 1 + prover/src/tests/multilinear_prove_tests.rs | 1 + prover/src/tests/multilinear_table_tests.rs | 1 + prover/src/tests/statement_alignment_tests.rs | 1 + prover/src/tests/whir_byte_gate.rs | 1 + prover/src/tests/whir_hash_tests.rs | 1 + prover/src/tests/whir_identity_tests.rs | 1 + prover/src/zf_format.rs | 553 ++++++++++++++++++ prover/tests/whir_transcript_configuration.rs | 1 + 31 files changed, 892 insertions(+), 5 deletions(-) create mode 100644 prover/src/zf_format.rs 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/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_chain.rs b/crypto/multilinear/src/whir_chain.rs index d614b92e1..667e4ffc8 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, @@ -176,6 +177,12 @@ impl GrindBits { } /// Blowup, fold factor, query count and proof of work. +/// +/// `format` is the proof FORMAT ([`ChainFormat`], the ZF campaign's W1 and W2 +/// levers); its default is today's format. Like the rest of the config it is +/// a verifier-side constant, never read from a proof. It is NOT absorbed into +/// the statement (`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,6 +193,88 @@ 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 lane flips its own flag in the commit that +/// makes the lever real. +pub const WHIR_CAP_IMPLEMENTED: bool = false; + +/// The longest explicit fold list [`WhirFolds::List`] holds. +pub const MAX_FOLD_ROUNDS: usize = 32; + +/// The per-round fold schedule of a chain (W2). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum WhirFolds { + /// `log_folding` variables every round, the remainder last. Today's format. + #[default] + Uniform, + /// A schedule chosen per chain by the verifier-side DP. + Dp, + /// An explicit schedule, round by round. + List(FoldList), +} + +/// See [`WHIR_CAP_IMPLEMENTED`]. +pub const WHIR_FOLDS_IMPLEMENTED: bool = false; + +/// An explicit fold schedule: `1 ..= MAX_FOLD_ROUNDS` rounds of `1 ..= 16` +/// variables each. `Copy`, so [`ChainConfig`] stays `Copy`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct FoldList { + len: u8, + folds: [u8; MAX_FOLD_ROUNDS], +} + +impl FoldList { + /// `None` when empty, longer than [`MAX_FOLD_ROUNDS`], or a fold outside + /// `1..=16`. + pub fn new(folds: &[u8]) -> Option { + if folds.is_empty() + || folds.len() > MAX_FOLD_ROUNDS + || folds.iter().any(|&k| !(1..=16).contains(&k)) + { + return None; + } + let mut out = [0u8; MAX_FOLD_ROUNDS]; + out[..folds.len()].copy_from_slice(folds); + Some(Self { + len: folds.len() as u8, + folds: out, + }) + } + + pub fn as_slice(&self) -> &[u8] { + &self.folds[..self.len as usize] + } } impl ChainConfig { @@ -224,6 +313,7 @@ impl ChainConfig { log_folding, num_queries, grind, + format: ChainFormat::DEFAULT, } } @@ -1229,6 +1319,7 @@ mod tests { log_folding, num_queries: 3, grind: GrindBits::default(), + format: ChainFormat::DEFAULT, } } 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/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/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..280649f17 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 campaign's levers). +/// Its default is today's format, 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,140 @@ 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`] = today. 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, +} + +impl ProofFormat { + /// Today's format: every lever off. + pub const DEFAULT: Self = Self { + merkle_cap: CapPolicy::Off, + fri_mode: FriMode::Pair, + one_row: OneRowMode::Off, + }; + + /// True when this is today's format (`Fixed(0)` counts as `Off`). + pub fn is_default(&self) -> bool { + self.merkle_cap.is_off() + && self.fri_mode == FriMode::Pair + && self.one_row == OneRowMode::Off + } +} + +/// 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 while the campaign lands it — must not be selectable, or a run +/// could print a non-default format and prove the default one. Each lane +/// flips its own flag in the commit that makes the lever real. +pub const MERKLE_CAP_IMPLEMENTED: bool = false; + +/// See [`MERKLE_CAP_IMPLEMENTED`]. +pub const FRI_MODE_IMPLEMENTED: bool = false; + +/// See [`MERKLE_CAP_IMPLEMENTED`]. +pub const ONE_ROW_IMPLEMENTED: bool = false; + impl ProofOptions { + /// True when every format field is at its default: the proof this + /// produces is today's format, byte for byte. + pub fn has_default_format(&self) -> bool { + self.format.is_default() + } + /// Default proof options used for testing purposes. /// These options should never be used in production. pub fn default_test_options() -> Self { @@ -70,6 +217,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 +278,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/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/prover/src/lfm/epoch_tests.rs b/prover/src/lfm/epoch_tests.rs index 18e0fca6e..a18887c19 100644 --- a/prover/src/lfm/epoch_tests.rs +++ b/prover/src/lfm/epoch_tests.rs @@ -1332,6 +1332,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, } } diff --git a/prover/src/lfm/per_table_aggregator_tests.rs b/prover/src/lfm/per_table_aggregator_tests.rs index dd0419339..4d067b4aa 100644 --- a/prover/src/lfm/per_table_aggregator_tests.rs +++ b/prover/src/lfm/per_table_aggregator_tests.rs @@ -2678,7 +2678,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 +5927,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 +7837,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..a67c609be 100644 --- a/prover/src/lfm/per_table_census_tests.rs +++ b/prover/src/lfm/per_table_census_tests.rs @@ -793,6 +793,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/proof.rs b/prover/src/lfm/proof.rs index 98f473a0f..9aa96df42 100644 --- a/prover/src/lfm/proof.rs +++ b/prover/src/lfm/proof.rs @@ -546,9 +546,25 @@ 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 today's 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 default-format only. +pub fn block_base_options() -> ProofOptions { + crate::zf_format::ZfFormat::global().options(crate::recursion::Preset::Blowup4.options()) } diff --git a/prover/src/lfm/whir_chain_tests.rs b/prover/src/lfm/whir_chain_tests.rs index 0d929582f..5000399d5 100644 --- a/prover/src/lfm/whir_chain_tests.rs +++ b/prover/src/lfm/whir_chain_tests.rs @@ -265,6 +265,7 @@ fn config(num_queries: usize, grind: u8) -> ChainConfig { log_folding: 4, num_queries, grind: GrindBits::uniform(grind), + format: multilinear::whir_chain::ChainFormat::DEFAULT, } } diff --git a/prover/src/lfm/whir_epoch_program_tests.rs b/prover/src/lfm/whir_epoch_program_tests.rs index 6c35bcab4..fc83127a7 100644 --- a/prover/src/lfm/whir_epoch_program_tests.rs +++ b/prover/src/lfm/whir_epoch_program_tests.rs @@ -888,6 +888,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_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..cb7319ee8 100644 --- a/prover/src/lfm/whir_statement.rs +++ b/prover/src/lfm/whir_statement.rs @@ -102,6 +102,12 @@ fn push_config(bytes: &mut Vec, config: &ChainConfig) { log_folding, num_queries, grind, + // ⚠ NOT absorbed: the format (cap policy, fold schedule) 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 lane that changes a lever's effect on the statement + // decides that here, explicitly. + format: _, } = config; for value in [log_blowup as u64, log_folding as u64, num_queries as u64] { bytes.extend_from_slice(&value.to_le_bytes()); diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 49c6f23e8..9466d3fea 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 diff --git a/prover/src/multilinear_continuation.rs b/prover/src/multilinear_continuation.rs index 7517704c4..2a5c1622d 100644 --- a/prover/src/multilinear_continuation.rs +++ b/prover/src/multilinear_continuation.rs @@ -760,6 +760,9 @@ pub(crate) fn absorb_epoch( log_folding, num_queries, grind, + // ⚠ Format, NOT absorbed: 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] { t.append_bytes(&value.to_le_bytes()); @@ -922,6 +925,9 @@ pub(crate) fn absorb_global( log_folding, num_queries, grind, + // ⚠ Format, NOT absorbed: 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] { t.append_bytes(&value.to_le_bytes()); diff --git a/prover/src/multilinear_prove.rs b/prover/src/multilinear_prove.rs index 8bf7cd71d..920cd0347 100644 --- a/prover/src/multilinear_prove.rs +++ b/prover/src/multilinear_prove.rs @@ -84,13 +84,24 @@ 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 today's config. pub fn chain_config(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( + 2, + crate::zf_format::PRODUCTION_WHIR_LOG_FOLDING, + tallest, + 128, + GrindBits::uniform(20), + ); + crate::zf_format::ZfFormat::global().chain(config) } /// Binds the statement into the transcript before any challenge is drawn. @@ -156,6 +167,9 @@ pub(crate) fn absorb( log_folding, num_queries, grind, + // ⚠ Format, NOT absorbed: 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] { t.append_bytes(&value.to_le_bytes()); diff --git a/prover/src/recursion.rs b/prover/src/recursion.rs index d929f6f49..1c2a23108 100644 --- a/prover/src/recursion.rs +++ b/prover/src/recursion.rs @@ -42,6 +42,7 @@ pub const MIN_PROOF_OPTIONS: ProofOptions = ProofOptions { coset_offset: 3, grinding_factor: 1, fri_final_poly_log_degree: 7, + format: stark::proof::options::ProofFormat::DEFAULT, }; /// The recursion verifier's build presets. Each fixes the guest's @@ -265,6 +266,21 @@ pub fn program_id_from_elf( )) } +/// The RV64 recursion guest verifies today's proof format only: its presets +/// fix the options at build time, and the archived verifier it runs is not +/// threaded with the ZF format levers. A non-default format 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_default_format(proof_options: &ProofOptions) -> Result<(), Error> { + if proof_options.has_default_format() { + Ok(()) + } else { + Err(Error::Execution(String::from( + "the recursion guest verifies default-format proofs only (ZF format levers 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 +295,7 @@ pub fn verify_and_attest_blob( blob: &[u8], proof_options: &ProofOptions, ) -> Result>, Error> { + require_default_format(proof_options)?; let verification = crate::verify_recursion_blob(blob, proof_options)?; if !verification.ok { return Ok(None); @@ -314,6 +331,8 @@ pub fn verify_continuation_and_attest( ) -> Result>, Error> { use rkyv::rancor::Error as RkyvError; + require_default_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/tests/decode_prepared_tests.rs b/prover/src/tests/decode_prepared_tests.rs index 815f9cb12..fa8e3ecc9 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, } } 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/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/zf_format.rs b/prover/src/zf_format.rs new file mode 100644 index 000000000..10e6b2f7e --- /dev/null +++ b/prover/src/zf_format.rs @@ -0,0 +1,553 @@ +//! ★ The proof FORMAT this process proves under — the ZF campaign's 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 | dp | k,k,… WHIR per-round fold schedule (W2) +//! ``` +//! +//! Every unset knob is today's format, so an unconfigured run proves exactly +//! what it proved before this module existed. +//! +//! # 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 +//! while the campaign lands them), and a knob set on a build that only parses +//! it would print a non-default format and prove the default one. Each lane +//! flips its `*_IMPLEMENTED` constant when its lever is real. +//! +//! **The banner prints on every setting, including the default**: +//! `ZF FORMAT: cap=off whir_cap=off fri=pair one_row=0 whir_folds=uniform4`. +//! 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, FoldList, 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. Every field's default is today's format. +#[derive(Clone, Copy, Debug, Default, 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, +} + +impl ZfFormat { + /// Today's format: every lever off. + pub const DEFAULT: Self = Self { + cap: CapPolicy::Off, + whir_cap: CapPolicy::Off, + fri: FriMode::Pair, + one_row: OneRowMode::Off, + whir_folds: WhirFolds::Uniform, + }; + + /// Parse the five knobs through `lookup` (the process environment in + /// production, a map in tests). An unset knob is the default; 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()); + 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) + ) + } + + /// 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, + } + } + + /// 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}")) +} + +/// `uniform4` | `dp` | a comma-separated list of folds (`4,4,4,1`). +fn parse_whir_folds(v: &str) -> Result { + let err = || { + format!( + "{ENV_WHIR_FOLDS}={v:?}: expected `uniform{PRODUCTION_WHIR_LOG_FOLDING}`, `dp`, or a \ + comma-separated list of 1..=16, at most {} rounds", + multilinear::whir_chain::MAX_FOLD_ROUNDS + ) + }; + if v == format!("uniform{PRODUCTION_WHIR_LOG_FOLDING}") { + return Ok(WhirFolds::Uniform); + } + if v == "dp" { + return Ok(WhirFolds::Dp); + } + let folds = v + .split(',') + .map(|k| { + let k = k.trim(); + if k.is_empty() || !k.bytes().all(|b| b.is_ascii_digit()) { + return Err(err()); + } + k.parse::().map_err(|_| err()) + }) + .collect::, String>>()?; + FoldList::new(&folds).map(WhirFolds::List).ok_or_else(err) +} + +fn whir_folds_name(folds: &WhirFolds) -> String { + match folds { + WhirFolds::Uniform => format!("uniform{PRODUCTION_WHIR_LOG_FOLDING}"), + WhirFolds::Dp => "dp".to_string(), + WhirFolds::List(list) => list + .as_slice() + .iter() + .map(u8::to_string) + .collect::>() + .join(","), + } +} + +#[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()) + } + + #[test] + fn nothing_set_is_todays_format() { + let f = parse(&[]).unwrap(); + assert_eq!(f, ZfFormat::DEFAULT); + assert_eq!(f, ZfFormat::default()); + 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()); + } + + #[test] + fn the_default_spellings_parse_to_the_default() { + 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::DEFAULT); + assert!(f.unimplemented_levers().is_empty()); + } + + #[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 + ); + assert_eq!( + parse(&[(ENV_WHIR_FOLDS, "dp")]).unwrap().whir_folds, + WhirFolds::Dp + ); + assert_eq!( + parse(&[(ENV_WHIR_FOLDS, "4,4,4,4,4,4,1")]) + .unwrap() + .whir_folds, + WhirFolds::List(FoldList::new(&[4, 4, 4, 4, 4, 4, 1]).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"), + (ENV_WHIR_FOLDS, "4,0"), + (ENV_WHIR_FOLDS, "4,17"), + (ENV_WHIR_FOLDS, "+4"), + ] { + let err = parse(&[(name, v)]).expect_err(&format!("{name}={v:?} must be refused")); + assert!(err.contains(name), "{err}"); + } + let too_long = vec!["1"; multilinear::whir_chain::MAX_FOLD_ROUNDS + 1].join(","); + assert!(parse(&[(ENV_WHIR_FOLDS, &too_long)]).is_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::List(FoldList::new(&[4, 4, 3]).unwrap()), + }; + assert_eq!( + f.banner(), + "ZF FORMAT: cap=auto whir_cap=2 fri=dp one_row=auto whir_folds=4,4,3" + ); + // 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(); + 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 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 default format leaves options untouched. + let d = ZfFormat::DEFAULT.options(base.clone()); + assert!(d.has_default_format()); + + let chain = crate::multilinear_prove::chain_config(&[(8, 20)]); + let c = ZfFormat { + whir_cap: CapPolicy::Fixed(3), + whir_folds: WhirFolds::Dp, + ..ZfFormat::DEFAULT + } + .chain(chain); + assert_eq!(c.format.cap, CapPolicy::Fixed(3)); + assert_eq!(c.format.folds, WhirFolds::Dp); + 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 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 produce today's values. + assert_eq!(*ZfFormat::global(), ZfFormat::DEFAULT); + assert!(crate::lfm::proof::aggregation_wrap_options().has_default_format()); + assert!(crate::lfm::proof::block_base_options().has_default_format()); + let chain = crate::multilinear_prove::chain_config(&[(8, 20)]); + assert_eq!(chain.format, ChainFormat::DEFAULT); + assert_eq!(chain.log_folding, PRODUCTION_WHIR_LOG_FOLDING); + } + + #[test] + fn the_recursion_guest_entries_refuse_a_non_default_format() { + // RULINGS 11: the RV64 guest verifier stays default-only. + let base = crate::recursion::Preset::Blowup4.options(); + for f in [ + ZfFormat { + cap: CapPolicy::Auto, + ..ZfFormat::DEFAULT + }, + ZfFormat { + fri: FriMode::Dp, + ..ZfFormat::DEFAULT + }, + ZfFormat { + one_row: OneRowMode::On, + ..ZfFormat::DEFAULT + }, + ] { + let opts = f.options(base.clone()); + for result in [ + crate::recursion::verify_and_attest_blob(&[], &opts), + crate::recursion::verify_continuation_and_attest(&[], &opts), + ] { + let err = result.expect_err("a non-default format must be refused"); + assert!(format!("{err:?}").contains("default-format"), "{err:?}"); + } + } + // The default format gets past the guard (and fails on the empty blob). + for result in [ + crate::recursion::verify_and_attest_blob(&[], &base), + crate::recursion::verify_continuation_and_attest(&[], &base), + ] { + if let Err(err) = result { + assert!(!format!("{err:?}").contains("default-format"), "{err:?}"); + } + } + } + + #[test] + fn the_serialized_options_bytes_ignore_the_format_fields() { + // RULINGS 10: 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/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, } } From 72304a52791a6a3e0b844d9962dd57e9e03d2cbc Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:16:44 -0300 Subject: [PATCH 04/73] feat(stark/fri): the verifier-side FRI fold-schedule DP and a scheduled FriFoldLayout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `fri::schedule`: the integer dynamic program of FRI.md §2.1 that picks the fold exponent of each committed FRI layer from public shape constants only (first committed size, terminal size, query count, the cap-height function as a parameter, DMAX = 6). Cost is kept in units of 1/Q so the format function is u64-only, with the (cost, trees) lexicographic tie rule (smallest d first). Also the FriMode / OneRowMode enums and FriFormat, the verifier-side constants a layout is built from. `FriFoldLayout` gains `schedule` (per committed layer) and `one_row`; `num_committed = schedule.len()`. `FriFoldLayout::new` is now `for_format(.., FriFormat::LEGACY)`, i.e. the all-ones schedule through the general constructor, and produces the same total_folds / num_committed / terminal_len / effective_k as before. The struct is no longer Copy (it owns a Vec); no call site copied it. No caller uses a non-legacy format yet, so no behaviour changes; ProofOptions and every serialized type are untouched. Tests (fri_schedule_tests): the FRI.md §2.2 table pinned for T = 9 and 10 under three cap functions (off, the design model's, CAP.md §2 Auto, implemented locally until the cap primitive lands); brute-force optimality of the DP for b0 <= 16; legacy_layout_equals_old_layout against a verbatim copy of the old constructor over B <= 30, blowup_log 1..4, k 0..10. --- crypto/stark/src/fri/mod.rs | 1 + crypto/stark/src/fri/schedule.rs | 273 ++++++ crypto/stark/src/fri/terminal.rs | 80 +- crypto/stark/src/tests/fri_schedule_tests.rs | 831 +++++++++++++++++++ crypto/stark/src/tests/mod.rs | 1 + 5 files changed, 1182 insertions(+), 4 deletions(-) create mode 100644 crypto/stark/src/fri/schedule.rs create mode 100644 crypto/stark/src/tests/fri_schedule_tests.rs diff --git a/crypto/stark/src/fri/mod.rs b/crypto/stark/src/fri/mod.rs index 0458b9b93..e6af9f024 100644 --- a/crypto/stark/src/fri/mod.rs +++ b/crypto/stark/src/fri/mod.rs @@ -1,6 +1,7 @@ pub mod fri_commitment; pub mod fri_decommit; pub(crate) mod fri_functions; +pub mod schedule; pub(crate) mod terminal; use crypto::fiat_shamir::is_transcript::IsStarkTranscript; diff --git a/crypto/stark/src/fri/schedule.rs b/crypto/stark/src/fri/schedule.rs new file mode 100644 index 000000000..5ed0ba55d --- /dev/null +++ b/crypto/stark/src/fri/schedule.rs @@ -0,0 +1,273 @@ +//! 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 +//! (`u64`), 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; +//! * the cap-height function `depth ↦ c` of the active Merkle-cap policy (the +//! caller closes over the opening count; `c ≡ 0` when caps are off); +//! * `dmax` — the largest fold exponent the program may choose. +//! +//! Cost model (per query, in units of `1/num_queries` of an in-guest hash +//! permutation, so every term is an integer): a layer of fold exponent `d` +//! whose tree has `depth` levels costs +//! +//! ```text +//! Q·leaf(d) + Q·(depth − c) + (2^c − 1), leaf(d) = max(1, ⌈3·2^d / 8⌉), c = cap(depth) +//! ``` +//! +//! i.e. the leaf absorption of `2^d` cubic-extension values at an 8-felt rate, +//! the authentication walk down to the cap, and the cap-to-root reduction +//! amortised over the `Q` queries. + +/// 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; + +/// The FRI layer format (`LAMBDA_VM_ZF_FRI`). `Pair` is today's all-ones +/// schedule; `Dp` is the schedule [`fri_schedule`] picks. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum FriMode { + #[default] + Pair, + Dp, +} + +/// The trace-opening layout (`LAMBDA_VM_ZF_ONE_ROW`). `Off` is today's row-pair +/// leaves with an uncommitted binary fold 0; `On` opens one row and commits the +/// DEEP codeword as FRI layer 0; `Auto` decides per table. A layout is built +/// from the RESOLVED per-table choice (a `bool`), never from `Auto`. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum OneRowMode { + #[default] + Off, + On, + Auto, +} + +/// A cap-height function that caps nothing (`c ≡ 0`). +pub fn no_cap(_depth: u32) -> u32 { + 0 +} + +/// 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) + } +} + +/// 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 authentication cost of a tree of `depth` levels: +/// `Q·(depth − c) + 2^c − 1`, `c = cap_height(depth)` clamped to `depth`. +pub fn fri_path_cost_q(depth: u32, num_queries: u64, cap_height: &dyn Fn(u32) -> u32) -> u64 { + // Clamped so that a policy returning more than the tree has can never make + // the walk negative (and `2^c` never overflows). + let c = cap_height(depth).min(depth).min(63); + num_queries + .saturating_mul(u64::from(depth - c)) + .saturating_add((1u64 << c) - 1) +} + +/// `Q ×` the per-query cost of one committed layer of fold exponent `d` whose +/// tree has `depth` levels (the layer is `2^{depth + d}` values long). +fn layer_cost_q(d: u32, depth: u32, num_queries: u64, cap_height: &dyn Fn(u32) -> u32) -> u64 { + num_queries + .saturating_mul(fri_leaf_blocks(d)) + .saturating_add(fri_path_cost_q(depth, num_queries, cap_height)) +} + +/// `Q ×` the per-query cost of an arbitrary schedule starting at `b0`, or +/// `None` if a fold exponent is zero or the schedule folds past zero bits. +/// (The model's own number for "today" is this at the all-ones schedule.) +pub fn fri_schedule_cost_q( + b0: u32, + schedule: &[u8], + num_queries: u64, + cap_height: &dyn Fn(u32) -> u32, +) -> 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, num_queries, cap_height)); + } + Some(cost) +} + +/// The optimum [`fri_schedule`] 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 fold schedule and its cost: the dynamic program of FRI.md §2.1. +/// +/// ```text +/// best(T) = (0, 0, []) +/// best(b > T) = min over d ∈ [1, min(dmax, b − T)] of +/// (Q·leaf(d) + path_q(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. +pub fn fri_schedule_with_cost( + b0: u32, + terminal_log: u32, + num_queries: u64, + cap_height: &dyn Fn(u32) -> u32, + dmax: u32, +) -> FriScheduleChoice { + if b0 <= terminal_log { + return FriScheduleChoice { + cost_q: 0, + trees: 0, + schedule: Vec::new(), + }; + } + let dmax = dmax.max(1); + 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, num_queries, cap_height).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 fold schedule of FRI.md §2.1 (see [`fri_schedule_with_cost`]). +pub fn fri_schedule( + b0: u32, + terminal_log: u32, + num_queries: u64, + cap_height: &dyn Fn(u32) -> u32, + dmax: u32, +) -> Vec { + fri_schedule_with_cost(b0, terminal_log, num_queries, cap_height, 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] +} + +/// Everything the fold layout needs to know about the proof format. +/// +/// All fields are verifier-side constants; none is ever read from a proof. +#[derive(Clone, Copy)] +pub struct FriFormat<'a> { + pub mode: FriMode, + /// The resolved one-row choice for this table. + pub one_row: bool, + /// FRI query count (the DP's opening count per tree). + pub num_queries: u64, + /// The active cap policy's height function for FRI-layer trees. + pub cap_height: &'a dyn Fn(u32) -> u32, +} + +impl FriFormat<'static> { + /// Today's format: pair layers, row-pair openings. The query count and cap + /// function are unused by the all-ones schedule. + pub const LEGACY: Self = Self { + mode: FriMode::Pair, + one_row: false, + num_queries: 0, + cap_height: &no_cap, + }; +} + +impl FriFormat<'_> { + /// The committed-layer fold schedule for an LDE of `2^lde_log` folding to a + /// terminal of `2^terminal_log`. + pub fn schedule(&self, lde_log: u32, terminal_log: u32) -> Vec { + let b0 = fri_chain_start(lde_log, self.one_row); + match self.mode { + FriMode::Pair => legacy_fri_schedule(b0, terminal_log), + FriMode::Dp => fri_schedule( + b0, + terminal_log, + self.num_queries, + self.cap_height, + FRI_SCHEDULE_DMAX, + ), + } + } +} + +impl std::fmt::Debug for FriFormat<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FriFormat") + .field("mode", &self.mode) + .field("one_row", &self.one_row) + .field("num_queries", &self.num_queries) + .finish_non_exhaustive() + } +} diff --git a/crypto/stark/src/fri/terminal.rs b/crypto/stark/src/fri/terminal.rs index 716fbcf3d..bb03445f7 100644 --- a/crypto/stark/src/fri/terminal.rs +++ b/crypto/stark/src/fri/terminal.rs @@ -9,6 +9,8 @@ use math::field::element::FieldElement; use math::field::traits::{IsFFTField, IsField, IsSubFieldOf}; use math::polynomial::Polynomial; +use crate::fri::schedule::{FRI_SCHEDULE_DMAX, FriFormat}; + /// The FRI early-termination fold layout. /// /// Derived identically by the CPU prover (`commit_phase_from_evaluations`), the @@ -16,11 +18,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 +35,18 @@ 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, } 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 +57,73 @@ 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 { + Self::for_format(lde_log, blowup_log, k, &FriFormat::LEGACY) + } + + /// 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. + pub(crate) fn for_format(lde_log: u32, blowup_log: u32, k: u32, fmt: &FriFormat<'_>) -> Self { + let terminal_log = (blowup_log + k).min(lde_log); + let schedule = fmt.schedule(lde_log, terminal_log); + let layout = Self::assemble(lde_log, blowup_log, terminal_log, fmt.one_row, schedule); + // Holds by construction: both schedules land exactly on the terminal. + debug_assert!(layout.schedule_is_consistent()); + layout + } + + /// 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`). + #[allow(dead_code)] // first caller arrives with the S3 prover/verifier. + 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 layout = Self::assemble(lde_log, blowup_log, terminal_log, one_row, schedule); + layout.schedule_is_consistent().then_some(layout) + } + + 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, } } + + /// 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/tests/fri_schedule_tests.rs b/crypto/stark/src/tests/fri_schedule_tests.rs new file mode 100644 index 000000000..42281b094 --- /dev/null +++ b/crypto/stark/src/tests/fri_schedule_tests.rs @@ -0,0 +1,831 @@ +//! Tests for the FRI fold schedule (`crate::fri::schedule`) and the generalised +//! `FriFoldLayout` (FRI.md §10 U1–U3). + +use crate::fri::schedule::{ + FRI_SCHEDULE_DMAX, FriFormat, FriMode, fri_chain_start, fri_leaf_blocks, fri_path_cost_q, + fri_schedule, fri_schedule_cost_q, fri_schedule_with_cost, legacy_fri_schedule, no_cap, +}; +use crate::fri::terminal::FriFoldLayout; + +const Q: u64 = 110; + +// --------------------------------------------------------------------------- +// Cap-height functions (the DP takes the active cap policy as a parameter). +// --------------------------------------------------------------------------- + +/// The cap rule FRI.md §2.2's table was computed with (PLAN §4): +/// `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 +} + +/// CAP.md §2 `AUTO_WEIGHTS` (ns): compress, select, unpack, hint, compare. +const AUTO_WEIGHTS: (i64, i64, i64, i64, i64) = (2251, 567, 528, 460, 3789); + +/// CAP.md §2 `CapPolicy::Auto.height(openings, depth)`, the policy adopted by +/// RULINGS.md 1. Implemented locally because the cap primitive commit (lane +/// I-CAP-S) is not yet on this branch; on rebase this becomes a call to +/// `CapPolicy::Auto.height` and `cap_auto_heights_match_cap_md` pins that the +/// two agree. +fn cap_auto_height(openings: u64, depth: u32) -> u32 { + let (wc, ws, wu, wh, wq) = AUTO_WEIGHTS; + let o = openings as i64; + let gain = |c: u32| -> i64 { + if c == 0 { + return 0; + } + let p = 1i64 << c; + o * (i64::from(c) * (wc + ws) - (p - 1) * ws - wu) - ((p - 1) * wc + p * wh + wq) + }; + let (mut best, mut best_c) = (0i64, 0u32); + for c in 0..=depth.min(16) { + let g = gain(c); + if g > best { + (best, best_c) = (g, c); + } + } + best_c +} + +/// Every FRI tree is opened once per query, so the FRI cap function is +/// `Auto.height(Q, ·)`. +fn cap_auto(depth: u32) -> u32 { + cap_auto_height(Q, depth) +} + +#[test] +fn cap_auto_heights_match_cap_md() { + // CAP.md §11 "CapPolicy pins", 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!(cap_auto_height(openings, 20), want, "openings {openings}"); + } + // Clamped to depth. + for depth in 0..8 { + assert_eq!(cap_auto_height(110, depth), depth.min(3), "depth {depth}"); + } + // The design model's rule reaches 7 at Q = 110 (FRI.md §2.2 used it). + assert_eq!(cap_design_model(20), 7); + assert_eq!(cap_design_model(5), 5); +} + +// --------------------------------------------------------------------------- +// Cost-model primitives. +// --------------------------------------------------------------------------- + +#[test] +fn leaf_blocks_and_path_cost() { + // ⌈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}"); + } + assert_eq!(fri_path_cost_q(9, Q, &no_cap), 990); + // depth 9, c = 7: Q·2 + 127. + assert_eq!(fri_path_cost_q(9, Q, &cap_design_model), 347); + // A policy asking for more than the tree has is clamped to the depth. + assert_eq!(fri_path_cost_q(2, Q, &|_| 40), 3); + assert_eq!(fri_path_cost_q(0, Q, &|_| 40), 0); +} + +#[test] +fn schedule_cost_rejects_malformed_schedules() { + assert_eq!(fri_schedule_cost_q(10, &[], Q, &no_cap), Some(0)); + assert_eq!(fri_schedule_cost_q(10, &[0, 1], Q, &no_cap), None); + assert_eq!(fri_schedule_cost_q(3, &[2, 2], Q, &no_cap), None); + assert!(fri_schedule_cost_q(4, &[2, 2], Q, &no_cap).is_some()); +} + +// --------------------------------------------------------------------------- +// U1: the §2.2 table, pinned. The schedule is a format constant. +// --------------------------------------------------------------------------- + +/// (B, today, S3 from B−1, S2+S3 from B); each entry = (cost·Q, schedule). +/// Generated by an independent Python reproduction of FRI.md §2.1 in exact +/// integer units (lane I-FRI-H scratch), and cross-checked against +/// `lanes/D-FRI/model_output.txt` 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!( + fri_schedule_cost_q(b0, today, Q, cap), + Some(today_q), + "{ctx} today cost" + ); + // S3: the DP from B − 1. + let got = fri_schedule_with_cost(b0, terminal_log, Q, cap, FRI_SCHEDULE_DMAX); + 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 = fri_schedule_with_cost(b0, terminal_log, Q, cap, FRI_SCHEDULE_DMAX); + assert_eq!(got.schedule, s2, "{ctx} S2+S3 schedule"); + assert_eq!(got.cost_q, s2_q, "{ctx} S2+S3 cost"); + assert_eq!( + fri_schedule(b0, terminal_log, Q, cap, FRI_SCHEDULE_DMAX), + s2 + ); + } +} + +#[test] +fn schedule_pinned_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 pins to the printed FRI.md §2.2 table (costs there are +/// per query, i.e. cost·Q / 110, rounded to two decimals). +#[test] +fn schedule_pins_match_fri_md_table() { + let per_query = |cost_q: u64| (cost_q as f64 / Q as f64 * 100.0).round() / 100.0; + let s3 = |b0: u32, t: u32, cap: &dyn Fn(u32) -> u32| { + fri_schedule_with_cost(b0, t, Q, cap, FRI_SCHEDULE_DMAX) + }; + // Base legs, T = 9, B = 21. + let today = legacy_fri_schedule(20, 9); + assert_eq!( + per_query(fri_schedule_cost_q(20, &today, Q, &no_cap).unwrap()), + 165.0 + ); + assert_eq!( + per_query(fri_schedule_cost_q(20, &today, Q, &cap_design_model).unwrap()), + 100.70 + ); + let c = s3(20, 9, &no_cap); + assert_eq!((per_query(c.cost_q), c.schedule), (52.0, vec![4, 4, 3])); + let c = s3(20, 9, &cap_design_model); + assert_eq!((per_query(c.cost_q), c.schedule), (34.46, vec![4, 4, 3])); + let c = s3(21, 9, &cap_design_model); + assert_eq!((per_query(c.cost_q), c.schedule), (39.46, vec![4, 4, 4])); + // B = 19, T = 9: the schedule depends on the cap policy (FRI.md §12.2). + assert_eq!(s3(18, 9, &no_cap).schedule, vec![5, 4]); + assert_eq!(s3(18, 9, &cap_design_model).schedule, vec![3, 3, 3]); + // LFM proofs, T = 10: S3+cap at B = 21 is [4,3,3] 33.46, at B = 22 [4,4,3] 37.46. + let c = s3(20, 10, &cap_design_model); + assert_eq!((per_query(c.cost_q), c.schedule), (33.46, vec![4, 3, 3])); + let c = s3(21, 10, &cap_design_model); + assert_eq!((per_query(c.cost_q), c.schedule), (37.46, vec![4, 4, 3])); +} + +// --------------------------------------------------------------------------- +// U2: brute-force optimality for b₀ ≤ 16. +// --------------------------------------------------------------------------- + +/// Independent oracle for one layer's cost (FRI.md §2.1, written out again). +fn oracle_layer_q(d: u32, depth: u32, q: u64, cap: &dyn Fn(u32) -> u32) -> u64 { + let leaf = (3u64 << d).div_ceil(8); + let c = cap(depth).min(depth); + q * leaf.max(1) + q * u64::from(depth - c) + (1u64 << c) - 1 +} + +/// 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: &dyn Fn(u32) -> u32, dmax: u32) -> (u64, Vec) { + struct Search<'a> { + t: u32, + q: u64, + cap: &'a dyn Fn(u32) -> u32, + 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 depth_mod_3 = |depth: u32| depth % 3; // an arbitrary, non-monotone policy + let caps: [(&str, &dyn Fn(u32) -> u32); 4] = [ + ("off", &no_cap), + ("model", &cap_design_model), + ("auto", &cap_auto), + ("depth%3", &depth_mod_3), + ]; + let mut checked = 0u32; + for (cap_name, 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_name} 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 [&no_cap as &dyn Fn(u32) -> u32, &cap_auto, &cap_design_model] { + 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, + ) +} + +#[test] +fn legacy_layout_equals_old_layout() { + let dp_formats: Vec> = [false, true] + .into_iter() + .flat_map(|one_row| { + [&no_cap as &'static dyn Fn(u32) -> u32, &cap_auto].map(|cap_height| FriFormat { + mode: FriMode::Dp, + one_row, + num_queries: Q, + cap_height, + }) + }) + .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}"); + + // Pair mode ignores the query count and the cap policy. + for cap_height in [&no_cap as &dyn Fn(u32) -> u32, &cap_auto] { + let pair = FriFormat { + mode: FriMode::Pair, + one_row: false, + num_queries: Q, + cap_height, + }; + assert_eq!( + FriFoldLayout::for_format(lde_log, blowup_log, k, &pair), + new, + "{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. + for fmt in &dp_formats { + let l = FriFoldLayout::for_format(lde_log, blowup_log, k, fmt); + 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_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!( + FriFoldLayout::from_schedule( + lde_log, + blowup_log, + k, + fmt.one_row, + l.schedule.clone() + ), + Some(l), + "{ctx} {fmt:?}" + ); + } + 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()); +} diff --git a/crypto/stark/src/tests/mod.rs b/crypto/stark/src/tests/mod.rs index a757e909a..bb15f76b5 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -7,6 +7,7 @@ pub mod bus_tests; pub mod commitment_tests; pub mod constraint_index_tests; pub mod domain_cache_stats; +pub mod fri_schedule_tests; pub mod fri_tests; pub mod grinding_tests; pub mod opening_width_tests; From db7c054c68046d4cb0299c68e61ec6b16ac87e1d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:44:20 -0300 Subject: [PATCH 05/73] feat(multilinear): the WHIR first-fold schedule (W2), its Q rule and fold word WhirFolds becomes { Uniform, First(FirstFold) }: the first round folds k0 variables (all of them when the chain has fewer) and every later round is today's walk, log_folding with the remainder last. A config serves chains of every height, so a per-round list would have to say what a shorter chain does with it; the lever design/WHIR.md measured is the first fold alone. The C2 skeleton's Dp and List variants are removed (RULINGS 15: no DP, and the knob is uniform4 | first5 | first6). FirstFold holds 1..=MAX_FOLD (6), the widest fold the stack is tested at; nothing else is constructible. - schedule(): Uniform runs today's body verbatim (tested against a copy of it for n <= 40, k 1..=6). - with_security_folds(): Q is charged the worst round count of any chain of <= tallest variables under the schedule. Uniform gives exactly today's config (tested on the grid); first5/first6 never add a round at any height, so Q never rises, and it is 112 at 25 (6 rounds instead of 7). - fold_word(): the statement word. Uniform = log_folding (4u64 at the default, today's bytes); First(k0) = 1<<63 | log_folding<<52 | 1<<48 | k0 (design/WHIR.md's prefix encoding, one entry). Not absorbed yet. - zf_format: LAMBDA_VM_ZF_WHIR_FOLDS accepts uniform4 | first5 | first6 and refuses dp, lists and other first. WHIR_FOLDS_IMPLEMENTED stays false. Host prove/verify at k0 = 5, 6 (n = 3..11), a tampered 64-wide base block rejected, and a first6 proof refused under uniform4 (and back). --- crypto/multilinear/src/whir_chain.rs | 449 ++++++++++++++++++++++++--- prover/src/zf_format.rs | 98 +++--- 2 files changed, 462 insertions(+), 85 deletions(-) diff --git a/crypto/multilinear/src/whir_chain.rs b/crypto/multilinear/src/whir_chain.rs index 667e4ffc8..40cd030be 100644 --- a/crypto/multilinear/src/whir_chain.rs +++ b/crypto/multilinear/src/whir_chain.rs @@ -228,55 +228,63 @@ impl ChainFormat { /// makes the lever real. pub const WHIR_CAP_IMPLEMENTED: bool = false; -/// The longest explicit fold list [`WhirFolds::List`] holds. -pub const MAX_FOLD_ROUNDS: usize = 32; +/// 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 (design/WHIR.md §3.2), 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 design/WHIR.md measured 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 (RULINGS 15). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub enum WhirFolds { /// `log_folding` variables every round, the remainder last. Today's format. #[default] Uniform, - /// A schedule chosen per chain by the verifier-side DP. - Dp, - /// An explicit schedule, round by round. - List(FoldList), + /// 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`]. pub const WHIR_FOLDS_IMPLEMENTED: bool = false; -/// An explicit fold schedule: `1 ..= MAX_FOLD_ROUNDS` rounds of `1 ..= 16` -/// variables each. `Copy`, so [`ChainConfig`] stays `Copy`. +/// 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 FoldList { - len: u8, - folds: [u8; MAX_FOLD_ROUNDS], -} +pub struct FirstFold(u8); -impl FoldList { - /// `None` when empty, longer than [`MAX_FOLD_ROUNDS`], or a fold outside - /// `1..=16`. - pub fn new(folds: &[u8]) -> Option { - if folds.is_empty() - || folds.len() > MAX_FOLD_ROUNDS - || folds.iter().any(|&k| !(1..=16).contains(&k)) - { - return None; +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 } - let mut out = [0u8; MAX_FOLD_ROUNDS]; - out[..folds.len()].copy_from_slice(folds); - Some(Self { - len: folds.len() as u8, - folds: out, - }) } - pub fn as_slice(&self) -> &[u8] { - &self.folds[..self.len as usize] + 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 @@ -299,29 +307,107 @@ 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, - format: ChainFormat::DEFAULT, + /// 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`: + /// design/WHIR.md §4.3's 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 + } } } /// 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); @@ -1429,6 +1515,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()); + } + + /// design/WHIR.md §3.2's 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/prover/src/zf_format.rs b/prover/src/zf_format.rs index 10e6b2f7e..291eaaaa2 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -5,7 +5,7 @@ //! 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 | dp | k,k,… WHIR per-round fold schedule (W2) +//! LAMBDA_VM_ZF_WHIR_FOLDS uniform4 | first5 | first6 WHIR first-round fold (W2) //! ``` //! //! Every unset knob is today's format, so an unconfigured run proves exactly @@ -41,7 +41,7 @@ use std::sync::OnceLock; -use multilinear::whir_chain::{ChainConfig, ChainFormat, FoldList, WhirFolds}; +use multilinear::whir_chain::{ChainConfig, ChainFormat, FirstFold, WhirFolds}; use stark::proof::options::{CapPolicy, FriMode, OneRowMode, ProofFormat, ProofOptions}; /// The knob names, in banner order. @@ -238,44 +238,40 @@ fn parse_cap(name: &str, v: &str) -> Result { v.parse().map_err(|e| format!("{name}={v:?}: {e}")) } -/// `uniform4` | `dp` | a comma-separated list of folds (`4,4,4,1`). +/// The first-round folds the knob accepts: the two arms RULINGS 15 builds. +/// +/// ⚠ 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`: RULINGS +/// 15, no DP. 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 { - let err = || { - format!( - "{ENV_WHIR_FOLDS}={v:?}: expected `uniform{PRODUCTION_WHIR_LOG_FOLDING}`, `dp`, or a \ - comma-separated list of 1..=16, at most {} rounds", - multilinear::whir_chain::MAX_FOLD_ROUNDS - ) - }; if v == format!("uniform{PRODUCTION_WHIR_LOG_FOLDING}") { return Ok(WhirFolds::Uniform); } - if v == "dp" { - return Ok(WhirFolds::Dp); - } - let folds = v - .split(',') - .map(|k| { - let k = k.trim(); - if k.is_empty() || !k.bytes().all(|b| b.is_ascii_digit()) { - return Err(err()); - } - k.parse::().map_err(|_| err()) + 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 ") + ) }) - .collect::, String>>()?; - FoldList::new(&folds).map(WhirFolds::List).ok_or_else(err) } fn whir_folds_name(folds: &WhirFolds) -> String { match folds { WhirFolds::Uniform => format!("uniform{PRODUCTION_WHIR_LOG_FOLDING}"), - WhirFolds::Dp => "dp".to_string(), - WhirFolds::List(list) => list - .as_slice() - .iter() - .map(u8::to_string) - .collect::>() - .join(","), + WhirFolds::First(k0) => format!("first{}", k0.get()), } } @@ -340,15 +336,17 @@ mod tests { 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, "dp")]).unwrap().whir_folds, - WhirFolds::Dp - ); - assert_eq!( - parse(&[(ENV_WHIR_FOLDS, "4,4,4,4,4,4,1")]) - .unwrap() - .whir_folds, - WhirFolds::List(FoldList::new(&[4, 4, 4, 4, 4, 4, 1]).unwrap()) + parse(&[(ENV_WHIR_FOLDS, " FIRST6 ")]).unwrap().whir_folds, + WhirFolds::First(FirstFold::new(6).unwrap()) ); } @@ -368,16 +366,20 @@ mod tests { (ENV_WHIR_FOLDS, "uniform"), (ENV_WHIR_FOLDS, "uniform3"), (ENV_WHIR_FOLDS, ""), - (ENV_WHIR_FOLDS, "4,,4"), - (ENV_WHIR_FOLDS, "4,0"), - (ENV_WHIR_FOLDS, "4,17"), - (ENV_WHIR_FOLDS, "+4"), + (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}"); } - let too_long = vec!["1"; multilinear::whir_chain::MAX_FOLD_ROUNDS + 1].join(","); - assert!(parse(&[(ENV_WHIR_FOLDS, &too_long)]).is_err()); } #[test] @@ -387,11 +389,11 @@ mod tests { whir_cap: CapPolicy::Fixed(2), fri: FriMode::Dp, one_row: OneRowMode::Auto, - whir_folds: WhirFolds::List(FoldList::new(&[4, 4, 3]).unwrap()), + 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=4,4,3" + "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. @@ -456,12 +458,12 @@ mod tests { let chain = crate::multilinear_prove::chain_config(&[(8, 20)]); let c = ZfFormat { whir_cap: CapPolicy::Fixed(3), - whir_folds: WhirFolds::Dp, + whir_folds: WhirFolds::First(FirstFold::new(5).unwrap()), ..ZfFormat::DEFAULT } .chain(chain); assert_eq!(c.format.cap, CapPolicy::Fixed(3)); - assert_eq!(c.format.folds, WhirFolds::Dp); + assert_eq!(c.format.folds, WhirFolds::First(FirstFold::new(5).unwrap())); assert_eq!( (c.log_blowup, c.log_folding, c.num_queries, c.grind), ( From dc2d8720d4a5bbf882c6005f4b380d9e2ac1b949 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:44:34 -0300 Subject: [PATCH 06/73] fix(crypto): the cap review fixes on the primitive (REVIEW-CAP S5, M1) S5: CapPolicy::Auto is now RULINGS 1's table stated directly (3 from 20 openings, 2 from 4, else 0, clamped to the depth), with the thresholds as named format constants. No arithmetic runs on the policy path, so no verifier can disagree on an overflow. cap_gain stays (the FRI schedule DP prices with the same weights) but is bounded: a height past MAX_CAP_HEIGHT returns i128::MIN instead of shifting, and every term fits i128 for any usize opening count, on 32-bit wasm too. Pinned: the table is the cost-law argmax for every opening count up to 10^6 and at the usize extremes. The depth-clamped table and a depth-bounded argmax differ at exactly one point (4 openings, depth 1: table 1, argmax 0); a test pins that single difference. M1: two fixtures that only one check rejects, on the real keccak backend: - the real internal node above leaf 0 / leaf 2^D-1 presented as a leaf hash with a path one sibling short: the length-agnostic fold accepts it, only siblings.len() == D - c rejects it (every c < D, c = 0 is C1b); - an unreached cap node flipped with 3 queries at c = 3: every per-query check passes, only the cap-to-root check rejects it. Checked by hand: deleting the length check or the verify_cap call in from_owner makes the matching test fail. --- crypto/crypto/src/merkle_tree/cap.rs | 195 +++++++++++++++++++++++++-- 1 file changed, 185 insertions(+), 10 deletions(-) diff --git a/crypto/crypto/src/merkle_tree/cap.rs b/crypto/crypto/src/merkle_tree/cap.rs index 9ff4cdc15..43c21daf0 100644 --- a/crypto/crypto/src/merkle_tree/cap.rs +++ b/crypto/crypto/src/merkle_tree/cap.rs @@ -359,7 +359,13 @@ pub const AUTO_WEIGHTS: CapWeights = CapWeights { /// 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. `gain(o, 0) = 0`; for `c ≥ 1`: +/// 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 ) @@ -374,6 +380,9 @@ pub fn cap_gain(weights: &CapWeights, openings: usize, cap_height: usize) -> i12 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; @@ -393,6 +402,14 @@ impl CapPolicy { /// 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 RULINGS 1's 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; @@ -402,20 +419,27 @@ impl CapPolicy { Self::Off => 0, Self::Fixed(c) => (c as usize).min(limit), Self::Auto => { - // argmax, ties to the smaller height; gain(·, 0) = 0. - 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 + 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 +/// (RULINGS 1). ⚠ 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`] (RULINGS 1). ⚠ 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. @@ -990,6 +1014,96 @@ mod tests { assert!(!rejects_forged_cap(mutant)); } + // ------------------------------------------- the only-rejecting-check fixtures + // + // REVIEW-CAP M1: 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 C1b 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] @@ -1020,6 +1134,67 @@ mod tests { 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 + } + + /// REVIEW-CAP S5: `Auto` is RULINGS 1's 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 (RULINGS 1) 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 { From 177ec2c90510a1722c926ac1e2c789340286cc99 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:48:04 -0300 Subject: [PATCH 07/73] feat(prover): the statement binds the WHIR fold schedule; reused commitments agree on it The three host absorbs (monolithic, epoch, global) and the LFM emitter's push_config write ChainConfig::fold_word() where they wrote log_folding. At the default schedule that is log_folding itself, 4u64, so every default statement, transcript KAT and byte gate keeps its bytes; under first5/first6 it is a tagged word, so the 245-byte statement keeps its length and moves in those 8 bytes only. The schedule of every chain is f(word, num_vars) and the heights are already bound, so the word binds every schedule, including the heights (num_vars <= 4) where first6 and uniform4 give the same schedule and the same Q and only the word tells two proofs apart. agrees_with (DecodePrepared, GenesisPrepared, GlobalPrepared) now compares (log_blowup, log_folding, folds): commit_stacked blocks tree 0 at the schedule's first fold, so a first6 commitment has 64-wide leaves that a uniform4 epoch would open as 16-wide ones. One helper, committed_under, makes the comparison for all three. Tests: a_first_fold_statement_moves_only_its_fold_word (length kept, only the fold word moves, the machine draws the host's challenge under first5 and first6); the_fold_word_alone_separates_two_schedules_that_agree (mutation gate: two configs equal in every field and schedule but the policy draw different challenges at all three host sites; with fold_word() forced to log_folding it and the test above FAIL, checked by hand); a_decode_commitment_refuses_another_fold_schedule. --- crypto/multilinear/src/whir_chain.rs | 8 +- prover/src/lfm/whir_epoch_tests.rs | 2 +- prover/src/lfm/whir_statement.rs | 12 +- prover/src/lfm/whir_statement_tests.rs | 219 +++++++++++++++++++++- prover/src/multilinear_continuation.rs | 108 +++++++---- prover/src/multilinear_prove.rs | 11 +- prover/src/tests/decode_prepared_tests.rs | 48 +++++ 7 files changed, 360 insertions(+), 48 deletions(-) diff --git a/crypto/multilinear/src/whir_chain.rs b/crypto/multilinear/src/whir_chain.rs index 40cd030be..0cd42c853 100644 --- a/crypto/multilinear/src/whir_chain.rs +++ b/crypto/multilinear/src/whir_chain.rs @@ -180,9 +180,11 @@ impl GrindBits { /// /// `format` is the proof FORMAT ([`ChainFormat`], the ZF campaign's W1 and W2 /// levers); its default is today's format. Like the rest of the config it is -/// a verifier-side constant, never read from a proof. It is NOT absorbed into -/// the statement (`push_config` binds it as `_`): absorbing it would move -/// every transcript at the default. +/// 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. 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_statement.rs b/prover/src/lfm/whir_statement.rs index cb7319ee8..6560bbdb1 100644 --- a/prover/src/lfm/whir_statement.rs +++ b/prover/src/lfm/whir_statement.rs @@ -99,17 +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 format (cap policy, fold schedule) is a set of + // ⚠ 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 lane that changes a lever's effect on the statement - // decides that here, explicitly. + // 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/multilinear_continuation.rs b/prover/src/multilinear_continuation.rs index 2a5c1622d..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,14 +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: verifier-side constants (see - // `lfm::whir_statement::push_config`, the emitter's twin of this). + // ⚠ 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); } @@ -922,14 +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: verifier-side constants (see - // `lfm::whir_statement::push_config`, the emitter's twin of this). + // ⚠ 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 920cd0347..6d0893dd8 100644 --- a/prover/src/multilinear_prove.rs +++ b/prover/src/multilinear_prove.rs @@ -164,14 +164,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: verifier-side constants (see - // `lfm::whir_statement::push_config`, the emitter's twin of this). + // ⚠ 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/tests/decode_prepared_tests.rs b/prover/src/tests/decode_prepared_tests.rs index fa8e3ecc9..21c9f4454 100644 --- a/prover/src/tests/decode_prepared_tests.rs +++ b/prover/src/tests/decode_prepared_tests.rs @@ -229,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" + ); +} From 92ba48e3ab075165aa07c099f380c7180670196f Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:49:39 -0300 Subject: [PATCH 08/73] feat(prover): LAMBDA_VM_ZF_WHIR_FOLDS reaches the WHIR base config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chain_config now builds through chain_config_under(format, shapes), which calls ChainConfig::with_security_folds with the format's fold schedule, so Q is charged the schedule's own worst round count rather than the uniform one with the format stamped on afterwards. At the default this is exactly the previous config (tested: chain_config_under(DEFAULT) == chain_config); under first5/first6 Q stays 112 at the block's tallest stack (25), with 6 rounds instead of 7. decode_prepared_config and the five continuation call sites go through chain_config and inherit the schedule. ZfFormat::global() prints a second line under the banner, on every setting: "ZF WHIR SCHEDULES: whir_folds=… q=… n=20:[…] … n=25:[…]", the schedules the base chains run at the production heights, so a log states the rounds it proved and not only the knob's name. WHIR_FOLDS_IMPLEMENTED stays false until the GPU and in-guest gates land. --- prover/src/multilinear_prove.rs | 15 +++++++-- prover/src/zf_format.rs | 60 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/prover/src/multilinear_prove.rs b/prover/src/multilinear_prove.rs index 6d0893dd8..b71301230 100644 --- a/prover/src/multilinear_prove.rs +++ b/prover/src/multilinear_prove.rs @@ -89,19 +89,30 @@ pub struct MultilinearVmProof { /// [`ZfFormat`](crate::zf_format::ZfFormat) WHIR fields (`LAMBDA_VM_ZF_WHIR_CAP`, /// `_WHIR_FOLDS`) are stamped on here. Unset knobs give today's 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); - let config = ChainConfig::with_security( + let config = ChainConfig::with_security_folds( 2, crate::zf_format::PRODUCTION_WHIR_LOG_FOLDING, + format.whir_folds, tallest, 128, GrindBits::uniform(20), ); - crate::zf_format::ZfFormat::global().chain(config) + format.chain(config) } /// Binds the statement into the transcript before any challenge is drawn. diff --git a/prover/src/zf_format.rs b/prover/src/zf_format.rs index 291eaaaa2..9b0faa689 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -177,6 +177,7 @@ impl ZfFormat { } // Always, including the default — see the module header. println!("{}", format.banner()); + println!("{}", format.whir_schedule_line()); format }) } @@ -194,6 +195,23 @@ impl ZfFormat { ) } + /// `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 { @@ -475,6 +493,48 @@ mod tests { ); } + #[test] + fn the_schedule_line_states_the_rounds() { + assert_eq!( + ZfFormat::DEFAULT.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::DEFAULT + }; + 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::DEFAULT, &[(1, 25)]); + assert_eq!(today, crate::multilinear_prove::chain_config(&[(1, 25)])); + assert_eq!((today.rounds(25), today.num_queries), (7, 112)); + 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 From 50ec5016f5be4d49b2b603c79498946b4f75a39d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:50:43 -0300 Subject: [PATCH 09/73] feat(stark/fri): cost-law schedule objective, options-driven FRI layout, schedule override RULINGS 13 / REVIEW-FRI F2: the fold-schedule DP now minimises the same cost-law objective as the cap policy, per query per committed layer: leaf blocks and walk levels priced with the cap policy's AUTO_WEIGHTS (compress, select), minus the tree's cap gain; plus the slot mux (2^d - 1 selects), the group fold (2^d - 1 binary folds at 5 XALU rows, edsl::fri_fold) and the twiddle chain (d BALU muls). XALU and BALU rows are priced from the node cost law at their committed widths (18 and 10 cells: 522 and 477 ns). FRI_COST_WEIGHTS is a format constant, pinned. The generic DP (fri_schedule_by) keeps the design model's permutation objective as a second instance, still pinned against the FRI.md 2.2 table, so the DP machinery stays checked against an independent model. U1 is re-pinned from the Rust DP (T = 9 and 10, B = 6..24, cap Off and Auto, S3 and S2 chains); at T = 9 under Auto it matches REVIEW-FRI F2's independent cost-law column at every B it lists. U2 brute-forces the new objective (4 cap policies x 3 query counts x 4 dmax, b0 <= 16). RULINGS 18: FriMode / OneRowMode now come from stark::proof::options (the local enums are gone); the cap input is a CapPolicy, and the test- local Auto cap is CapPolicy::Auto.height. FriFoldLayout::for_options builds the layout from ProofOptions (the format is a verifier-side constant) and records the encoding: legacy (pair leaves, one sibling per layer) exactly for fri=pair with row-pair openings, decided by the format, not the schedule's values. A one-row mode other than Off is an error (S2 is not built), never a silent row-pair proof. REVIEW-FRI F1.3: ProofFormat.fri_schedule_override, a test hook that replaces the DP's schedule under fri=dp so round trips can use schedules the DP never picks. No knob sets it (ZfFormat leaves it None); a schedule that does not cover the table's folds is an error; is_default() requires it None. ProofFormat is not serialized (skipped by serde and rkyv), so no pinned byte moves. No prover or verifier path uses the new layout yet; defaults unchanged. --- crypto/stark/src/fri/schedule.rs | 303 ++++++---- crypto/stark/src/fri/terminal.rs | 90 ++- crypto/stark/src/proof/options.rs | 47 ++ crypto/stark/src/tests/fri_schedule_tests.rs | 584 +++++++++++++++---- prover/src/zf_format.rs | 2 + 5 files changed, 796 insertions(+), 230 deletions(-) diff --git a/crypto/stark/src/fri/schedule.rs b/crypto/stark/src/fri/schedule.rs index 5ed0ba55d..32dc652b0 100644 --- a/crypto/stark/src/fri/schedule.rs +++ b/crypto/stark/src/fri/schedule.rs @@ -8,29 +8,43 @@ //! //! 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 -//! (`u64`), with a fixed tie rule, and its inputs are all public: +//! 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; -//! * the cap-height function `depth ↦ c` of the active Merkle-cap policy (the -//! caller closes over the opening count; `c ≡ 0` when caps are off); +//! * `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. //! -//! Cost model (per query, in units of `1/num_queries` of an in-guest hash -//! permutation, so every term is an integer): a layer of fold exponent `d` -//! whose tree has `depth` levels costs +//! # The objective (RULINGS 13): the cost law, not permutations +//! +//! 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: //! //! ```text -//! Q·leaf(d) + Q·(depth − c) + (2^c − 1), leaf(d) = max(1, ⌈3·2^d / 8⌉), c = cap(depth) +//! 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^d − 1)·fold the group fold: 2^d − 1 binary folds +//! + d·twiddle the twiddle chain: one base mul per fold level +//! − cap_gain(Q, c(depth)) / Q what the tree's cap saves, per query (0 without a cap) //! ``` //! -//! i.e. the leaf absorption of `2^d` cubic-extension values at an 8-felt rate, -//! the authentication walk down to the cap, and the cap-to-root reduction -//! amortised over the `Q` queries. +//! `leaf(d) = max(1, ⌈3·2^d / 8⌉)` (an ext3 group at the RPX rate of 8 felts). +//! The per-operation row counts are the in-guest emitter's +//! (`prover/src/lfm/edsl.rs::fri_fold` = 5 `XALU` rows, a `Select` = 1 +//! `SELECT` row, a base `mul` = 1 `BALU` row) — the in-guest lane pins +//! "emitted rows == these rows" against its emitter. 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, OneRowMode, ProofOptions}; /// Largest fold exponent the schedule may choose (a 64-value group leaf). pub const FRI_SCHEDULE_DMAX: u32 = 6; @@ -42,32 +56,46 @@ pub const FRI_LEAF_RATE_FELTS: u64 = 8; /// Extension degree of the FRI codeword values. pub const FRI_EXTENSION_DEGREE: u64 = 3; -/// The FRI layer format (`LAMBDA_VM_ZF_FRI`). `Pair` is today's all-ones -/// schedule; `Dp` is the schedule [`fri_schedule`] picks. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] -pub enum FriMode { - #[default] - Pair, - Dp, -} +/// `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; -/// The trace-opening layout (`LAMBDA_VM_ZF_ONE_ROW`). `Off` is today's row-pair -/// leaves with an uncommitted binary fold 0; `On` opens one row and commits the -/// DEEP codeword as FRI layer 0; `Auto` decides per table. A layout is built -/// from the RESOLVED per-table choice (a `bool`), never from `Auto`. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] -pub enum OneRowMode { - #[default] - Off, - On, - Auto, -} +/// `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; + +/// 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; -/// A cap-height function that caps nothing (`c ≡ 0`). -pub fn no_cap(_depth: u32) -> u32 { - 0 +/// 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. + pub fold: u64, + /// One step of the twiddle chain in-guest. + pub twiddle: 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, +}; + /// 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, @@ -87,33 +115,40 @@ pub fn fri_leaf_blocks(d: u32) -> u64 { felts.div_ceil(FRI_LEAF_RATE_FELTS).max(1) } -/// `Q ×` the per-query authentication cost of a tree of `depth` levels: -/// `Q·(depth − c) + 2^c − 1`, `c = cap_height(depth)` clamped to `depth`. -pub fn fri_path_cost_q(depth: u32, num_queries: u64, cap_height: &dyn Fn(u32) -> u32) -> u64 { - // Clamped so that a policy returning more than the tree has can never make - // the walk negative (and `2^c` never overflows). - let c = cap_height(depth).min(depth).min(63); - num_queries - .saturating_mul(u64::from(depth - c)) - .saturating_add((1u64 << c) - 1) -} - -/// `Q ×` the per-query cost of one committed layer of fold exponent `d` whose -/// tree has `depth` levels (the layer is `2^{depth + d}` values long). -fn layer_cost_q(d: u32, depth: u32, num_queries: u64, cap_height: &dyn Fn(u32) -> u32) -> u64 { - num_queries - .saturating_mul(fri_leaf_blocks(d)) - .saturating_add(fri_path_cost_q(depth, num_queries, cap_height)) +/// `Q ×` the per-query cost-law price (ns) of one committed 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`. See the module docs. +pub fn fri_layer_cost_q( + weights: &FriCostWeights, + d: u32, + depth: u32, + num_queries: u64, + cap: CapPolicy, +) -> u64 { + // i128 throughout, d clamped to 64 so 2^d fits; the result is clamped into + // u64 (it is non-negative — a cap never saves more than the walk it + // shortens — but the clamp keeps that a non-assumption). + let w = |x: u64| x as i128; + let d = d.min(64); + let q = num_queries as i128; + let group = (1i128 << d) - 1; + let per_query = w(fri_leaf_blocks(d)) * w(weights.cap.compress) + + i128::from(depth) * (w(weights.cap.compress) + w(weights.cap.select)) + + group * (w(FRI_SLOT_SELECT_ROWS) * w(weights.cap.select) + w(weights.fold)) + + i128::from(d) * w(weights.twiddle); + let queries = usize::try_from(num_queries).unwrap_or(usize::MAX); + let c = cap.height(queries, depth as usize); + let total = q.saturating_mul(per_query) - cap_gain(&weights.cap, queries, c); + u64::try_from(total.max(0)).unwrap_or(u64::MAX) } -/// `Q ×` the per-query cost of an arbitrary schedule starting at `b0`, or -/// `None` if a fold exponent is zero or the schedule folds past zero bits. -/// (The model's own number for "today" is this at the all-ones schedule.) -pub fn fri_schedule_cost_q( +/// `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], - num_queries: u64, - cap_height: &dyn Fn(u32) -> u32, + layer_cost_q: &dyn Fn(u32, u32) -> u64, ) -> Option { let mut b = b0; let mut cost = 0u64; @@ -123,12 +158,25 @@ pub fn fri_schedule_cost_q( return None; } b = b.checked_sub(d)?; - cost = cost.saturating_add(layer_cost_q(d, b, num_queries, cap_height)); + cost = cost.saturating_add(layer_cost_q(d, b)); } Some(cost) } -/// The optimum [`fri_schedule`] picks, with its cost. +/// [`fri_schedule_cost_by`] under the production objective +/// ([`FRI_COST_WEIGHTS`], [`fri_layer_cost_q`]). +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). @@ -139,25 +187,25 @@ pub struct FriScheduleChoice { pub schedule: Vec, } -/// The fold schedule and its cost: the dynamic program of FRI.md §2.1. +/// 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 -/// (Q·leaf(d) + path_q(b − d) + best(b − d).cost, best(b − d).trees + 1, [d] ++ best(b − d).sched) +/// (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. -pub fn fri_schedule_with_cost( +/// 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, - num_queries: u64, - cap_height: &dyn Fn(u32) -> u32, dmax: u32, + layer_cost_q: &dyn Fn(u32, u32) -> u64, ) -> FriScheduleChoice { if b0 <= terminal_log { return FriScheduleChoice { @@ -166,7 +214,7 @@ pub fn fri_schedule_with_cost( schedule: Vec::new(), }; } - let dmax = dmax.max(1); + 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 @@ -178,7 +226,7 @@ pub fn fri_schedule_with_cost( 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, num_queries, cap_height).saturating_add(rest_cost); + 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)) { @@ -203,15 +251,29 @@ pub fn fri_schedule_with_cost( } } -/// The fold schedule of FRI.md §2.1 (see [`fri_schedule_with_cost`]). +/// 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_height: &dyn Fn(u32) -> u32, + cap: CapPolicy, dmax: u32, ) -> Vec { - fri_schedule_with_cost(b0, terminal_log, num_queries, cap_height, dmax).schedule + fri_schedule_with_cost(b0, terminal_log, num_queries, cap, dmax).schedule } /// Today's schedule: every committed layer folds by 2. @@ -219,55 +281,98 @@ pub fn legacy_fri_schedule(b0: u32, terminal_log: u32) -> Vec { vec![1; b0.saturating_sub(terminal_log) as usize] } -/// Everything the fold layout needs to know about the proof format. -/// -/// All fields are verifier-side constants; none is ever read from a proof. -#[derive(Clone, Copy)] -pub struct FriFormat<'a> { +/// Why a proof format cannot be laid out for a table. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FriFormatError { + /// `one_row` is not `Off`: one-row openings (S2) are not implemented on + /// this build. Refused rather than silently proving the row-pair layout. + OneRowNotImplemented, + /// 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::OneRowNotImplemented => { + f.write_str("one-row openings (LAMBDA_VM_ZF_ONE_ROW) are not implemented") + } + 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. + /// The RESOLVED one-row choice for this table (never `Auto`). pub one_row: bool, - /// FRI query count (the DP's opening count per tree). + /// FRI query count (the opening count of every FRI tree). pub num_queries: u64, - /// The active cap policy's height function for FRI-layer trees. - pub cap_height: &'a dyn Fn(u32) -> u32, + /// The active Merkle-cap policy (an input of the DP, RULINGS 7). + pub cap: CapPolicy, + /// An explicit schedule that replaces the DP's under [`FriMode::Dp`]. + pub schedule_override: Option, } -impl FriFormat<'static> { +impl FriFormat { /// Today's format: pair layers, row-pair openings. The query count and cap - /// function are unused by the all-ones schedule. + /// policy are unused by the all-ones schedule. pub const LEGACY: Self = Self { mode: FriMode::Pair, one_row: false, num_queries: 0, - cap_height: &no_cap, + cap: CapPolicy::Off, + schedule_override: None, }; -} -impl FriFormat<'_> { + /// The format of a table proved under `options`. + /// + /// Errors on a one-row mode other than `Off` (not implemented here: the + /// per-table `Auto` resolution and the one-row layout arrive with S2). + pub fn from_options(options: &ProofOptions) -> Result { + if options.format.one_row != OneRowMode::Off { + return Err(FriFormatError::OneRowNotImplemented); + } + Ok(Self { + mode: options.format.fri_mode, + one_row: false, + 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 (FRI.md §3.4). 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 + } + /// The committed-layer fold schedule for an LDE of `2^lde_log` folding to a - /// terminal of `2^terminal_log`. + /// 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 { - FriMode::Pair => legacy_fri_schedule(b0, terminal_log), - FriMode::Dp => fri_schedule( + 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_height, + self.cap, FRI_SCHEDULE_DMAX, ), } } } - -impl std::fmt::Debug for FriFormat<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("FriFormat") - .field("mode", &self.mode) - .field("one_row", &self.one_row) - .field("num_queries", &self.num_queries) - .finish_non_exhaustive() - } -} diff --git a/crypto/stark/src/fri/terminal.rs b/crypto/stark/src/fri/terminal.rs index bb03445f7..346d32af8 100644 --- a/crypto/stark/src/fri/terminal.rs +++ b/crypto/stark/src/fri/terminal.rs @@ -9,7 +9,8 @@ use math::field::element::FieldElement; use math::field::traits::{IsFFTField, IsField, IsSubFieldOf}; use math::polynomial::Polynomial; -use crate::fri::schedule::{FRI_SCHEDULE_DMAX, FriFormat}; +use crate::fri::schedule::{FRI_SCHEDULE_DMAX, FriFormat, FriFormatError}; +use crate::proof::options::ProofOptions; /// The FRI early-termination fold layout. /// @@ -43,8 +44,17 @@ pub(crate) struct FriFoldLayout { /// 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, } +// The format-aware constructors' first callers are the S3 prover and verifier +// (the next commit); until then only the tests use them. +#[allow(dead_code)] impl FriFoldLayout { /// Today's layout, derived from the LDE codeword size. /// @@ -61,25 +71,55 @@ impl FriFoldLayout { /// 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 { - Self::for_format(lde_log, blowup_log, k, &FriFormat::LEGACY) + 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. - pub(crate) fn for_format(lde_log: u32, blowup_log: u32, k: u32, fmt: &FriFormat<'_>) -> Self { + /// + /// `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 layout = Self::assemble(lde_log, blowup_log, terminal_log, fmt.one_row, schedule); - // Holds by construction: both schedules land exactly on the terminal. - debug_assert!(layout.schedule_is_consistent()); - layout + 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`: what the prover and the host + /// verifier both build. The format comes from `options` — a verifier-side + /// constant — never from a proof. + pub(crate) fn for_options( + lde_log: u32, + blowup_log: u32, + options: &ProofOptions, + ) -> Result { + let fmt = FriFormat::from_options(options)?; + 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`). - #[allow(dead_code)] // first caller arrives with the S3 prover/verifier. + /// `1..=FRI_SCHEDULE_DMAX`). The encoding is the group encoding unless + /// the schedule is today's (row pair, all ones), where it is legacy. pub(crate) fn from_schedule( lde_log: u32, blowup_log: u32, @@ -88,10 +128,39 @@ impl FriFoldLayout { schedule: Vec, ) -> Option { let terminal_log = (blowup_log + k).min(lde_log); - let layout = Self::assemble(lde_log, blowup_log, terminal_log, one_row, schedule); + 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`]). Every device FRI arm is gated on this. + 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]) + } + + /// 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, @@ -107,6 +176,7 @@ impl FriFoldLayout { effective_k: terminal_log - blowup_log, schedule, one_row, + legacy_encoding: !one_row, } } diff --git a/crypto/stark/src/proof/options.rs b/crypto/stark/src/proof/options.rs index 280649f17..4a5c56d4f 100644 --- a/crypto/stark/src/proof/options.rs +++ b/crypto/stark/src/proof/options.rs @@ -92,6 +92,17 @@ pub struct ProofFormat { 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 { @@ -100,6 +111,7 @@ impl ProofFormat { merkle_cap: CapPolicy::Off, fri_mode: FriMode::Pair, one_row: OneRowMode::Off, + fri_schedule_override: None, }; /// True when this is today's format (`Fixed(0)` counts as `Off`). @@ -107,6 +119,41 @@ impl ProofFormat { 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] } } diff --git a/crypto/stark/src/tests/fri_schedule_tests.rs b/crypto/stark/src/tests/fri_schedule_tests.rs index 42281b094..a37e143bd 100644 --- a/crypto/stark/src/tests/fri_schedule_tests.rs +++ b/crypto/stark/src/tests/fri_schedule_tests.rs @@ -1,18 +1,37 @@ //! Tests for the FRI fold schedule (`crate::fri::schedule`) and the generalised //! `FriFoldLayout` (FRI.md §10 U1–U3). +//! +//! Two objectives appear here. The PRODUCTION one is the cost law (RULINGS 13, +//! `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 +//! (FRI.md §2.1, the §2.2 table) is kept as a second instance of the generic DP +//! (`fri_schedule_by`), pinned against the design document: it shows the DP +//! machinery reproduces an independent model exactly, and documents how far +//! the two objectives' schedules differ. use crate::fri::schedule::{ - FRI_SCHEDULE_DMAX, FriFormat, FriMode, fri_chain_start, fri_leaf_blocks, fri_path_cost_q, - fri_schedule, fri_schedule_cost_q, fri_schedule_with_cost, legacy_fri_schedule, no_cap, + BALU_ROW_NS, FRI_COST_WEIGHTS, FRI_FOLD_XALU_ROWS, FRI_SCHEDULE_DMAX, FriFormat, + FriFormatError, XALU_ROW_NS, fri_chain_start, fri_layer_cost_q, fri_leaf_blocks, 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 (the DP takes the active cap policy as a parameter). +// Cap-height functions for the permutation objective. // --------------------------------------------------------------------------- +/// A cap-height function that caps nothing. +fn no_cap(_depth: u32) -> u32 { + 0 +} + /// The cap rule FRI.md §2.2's table was computed with (PLAN §4): /// `c = argmax_{0 ≤ c ≤ depth} (Q·c − (2^c − 1))`, ties to the smaller `c`. fn cap_design_model(depth: u32) -> u32 { @@ -26,84 +45,128 @@ fn cap_design_model(depth: u32) -> u32 { best_c } -/// CAP.md §2 `AUTO_WEIGHTS` (ns): compress, select, unpack, hint, compare. -const AUTO_WEIGHTS: (i64, i64, i64, i64, i64) = (2251, 567, 528, 460, 3789); - -/// CAP.md §2 `CapPolicy::Auto.height(openings, depth)`, the policy adopted by -/// RULINGS.md 1. Implemented locally because the cap primitive commit (lane -/// I-CAP-S) is not yet on this branch; on rebase this becomes a call to -/// `CapPolicy::Auto.height` and `cap_auto_heights_match_cap_md` pins that the -/// two agree. -fn cap_auto_height(openings: u64, depth: u32) -> u32 { - let (wc, ws, wu, wh, wq) = AUTO_WEIGHTS; - let o = openings as i64; - let gain = |c: u32| -> i64 { - if c == 0 { - return 0; - } - let p = 1i64 << c; - o * (i64::from(c) * (wc + ws) - (p - 1) * ws - wu) - ((p - 1) * wc + p * wh + wq) - }; - let (mut best, mut best_c) = (0i64, 0u32); - for c in 0..=depth.min(16) { - let g = gain(c); - if g > best { - (best, best_c) = (g, c); - } - } - best_c -} - -/// Every FRI tree is opened once per query, so the FRI cap function is -/// `Auto.height(Q, ·)`. +/// The adopted policy (RULINGS 1): every FRI tree is opened once per query. fn cap_auto(depth: u32) -> u32 { - cap_auto_height(Q, depth) + CapPolicy::Auto.height(Q as usize, depth as usize) as u32 } #[test] fn cap_auto_heights_match_cap_md() { // CAP.md §11 "CapPolicy pins", 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!(cap_auto_height(openings, 20), want, "openings {openings}"); + assert_eq!( + CapPolicy::Auto.height(openings, 20), + want, + "openings {openings}" + ); } // Clamped to depth. for depth in 0..8 { - assert_eq!(cap_auto_height(110, depth), depth.min(3), "depth {depth}"); + assert_eq!(cap_auto(depth), depth.min(3), "depth {depth}"); } // The design model's rule reaches 7 at Q = 110 (FRI.md §2.2 used it). assert_eq!(cap_design_model(20), 7); assert_eq!(cap_design_model(5), 5); } +/// FRI.md §2.1'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_and_path_cost() { +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}"); } - assert_eq!(fri_path_cost_q(9, Q, &no_cap), 990); - // depth 9, c = 7: Q·2 + 127. - assert_eq!(fri_path_cost_q(9, Q, &cap_design_model), 347); - // A policy asking for more than the tree has is clamped to the depth. - assert_eq!(fri_path_cost_q(2, Q, &|_| 40), 3); - assert_eq!(fri_path_cost_q(0, Q, &|_| 40), 0); +} + +/// The objective's weights are a format constant (RULINGS 13): the cap +/// policy's weights plus the in-guest fold and twiddle rows. +#[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) + ); + // 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. +#[test] +fn layer_cost_by_hand() { + // d = 3, depth 10, no cap: leaf 3·2251 + 10·(2251+567) + 7·(567+2610) + 3·477. + let per_query = 3 * 2251 + 10 * (2251 + 567) + 7 * (567 + 2610) + 3 * 477; + 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. + 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 + ); + // The legacy layer (d = 1) prices one leaf block, one select, one fold, + // one twiddle. + assert_eq!( + fri_layer_cost_q(&FRI_COST_WEIGHTS, 1, 0, 1, CapPolicy::Off), + 2251 + 567 + 2610 + 477 + ); } #[test] fn schedule_cost_rejects_malformed_schedules() { - assert_eq!(fri_schedule_cost_q(10, &[], Q, &no_cap), Some(0)); - assert_eq!(fri_schedule_cost_q(10, &[0, 1], Q, &no_cap), None); - assert_eq!(fri_schedule_cost_q(3, &[2, 2], Q, &no_cap), None); - assert!(fri_schedule_cost_q(4, &[2, 2], Q, &no_cap).is_some()); + 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()); } // --------------------------------------------------------------------------- -// U1: the §2.2 table, pinned. The schedule is a format constant. +// The design model (permutation objective): the FRI.md §2.2 table, reproduced. // --------------------------------------------------------------------------- /// (B, today, S3 from B−1, S2+S3 from B); each entry = (cost·Q, schedule). @@ -522,30 +585,22 @@ fn check_pin(name: &str, terminal_log: u32, cap: &dyn Fn(u32) -> u32, rows: &[Ro // 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!( - fri_schedule_cost_q(b0, today, Q, cap), - Some(today_q), - "{ctx} today cost" - ); + assert_eq!(perm_cost(b0, today, cap), Some(today_q), "{ctx} today cost"); // S3: the DP from B − 1. - let got = fri_schedule_with_cost(b0, terminal_log, Q, cap, FRI_SCHEDULE_DMAX); + 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 = fri_schedule_with_cost(b0, terminal_log, Q, cap, FRI_SCHEDULE_DMAX); + 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"); - assert_eq!( - fri_schedule(b0, terminal_log, Q, cap, FRI_SCHEDULE_DMAX), - s2 - ); } } #[test] -fn schedule_pinned_table() { +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); @@ -554,63 +609,246 @@ fn schedule_pinned_table() { check_pin("T10 cap auto", 10, &cap_auto, PIN_T10_CAP_AUTO); } -/// Spot checks tying the pins to the printed FRI.md §2.2 table (costs there are -/// per query, i.e. cost·Q / 110, rounded to two decimals). +/// Spot checks tying the design-model pins to the printed FRI.md §2.2 table +/// (costs there are per query, i.e. cost·Q / 110, rounded to two decimals). #[test] -fn schedule_pins_match_fri_md_table() { +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 s3 = |b0: u32, t: u32, cap: &dyn Fn(u32) -> u32| { - fri_schedule_with_cost(b0, t, Q, cap, FRI_SCHEDULE_DMAX) - }; - // Base legs, T = 9, B = 21. let today = legacy_fri_schedule(20, 9); + assert_eq!(per_query(perm_cost(20, &today, &no_cap).unwrap()), 165.0); assert_eq!( - per_query(fri_schedule_cost_q(20, &today, Q, &no_cap).unwrap()), - 165.0 - ); - assert_eq!( - per_query(fri_schedule_cost_q(20, &today, Q, &cap_design_model).unwrap()), + per_query(perm_cost(20, &today, &cap_design_model).unwrap()), 100.70 ); - let c = s3(20, 9, &no_cap); + 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 = s3(20, 9, &cap_design_model); + 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 = s3(21, 9, &cap_design_model); + let c = perm_schedule(21, 9, &cap_design_model); assert_eq!((per_query(c.cost_q), c.schedule), (39.46, vec![4, 4, 4])); - // B = 19, T = 9: the schedule depends on the cap policy (FRI.md §12.2). - assert_eq!(s3(18, 9, &no_cap).schedule, vec![5, 4]); - assert_eq!(s3(18, 9, &cap_design_model).schedule, vec![3, 3, 3]); - // LFM proofs, T = 10: S3+cap at B = 21 is [4,3,3] 33.46, at B = 22 [4,4,3] 37.46. - let c = s3(20, 10, &cap_design_model); + 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 = s3(21, 10, &cap_design_model); + let c = perm_schedule(21, 10, &cap_design_model); assert_eq!((per_query(c.cost_q), c.schedule), (37.46, vec![4, 4, 3])); } // --------------------------------------------------------------------------- -// U2: brute-force optimality for b₀ ≤ 16. +// 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. // --------------------------------------------------------------------------- -/// Independent oracle for one layer's cost (FRI.md §2.1, written out again). -fn oracle_layer_q(d: u32, depth: u32, q: u64, cap: &dyn Fn(u32) -> u32) -> u64 { - let leaf = (3u64 << d).div_ceil(8); - let c = cap(depth).min(depth); - q * leaf.max(1) + q * u64::from(depth - c) + (1u64 << c) - 1 +/// (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 at the commit that introduced the cost-law objective. Independent +/// cross-check: design/REVIEW-FRI.md F2's cost-law column (its own model, +/// ASSUMED widths, cap = ruling 1) gives [2,2] / [3,3,3] / [3,3,3,2] / +/// [3,3,3,3,2] at B = 14 / 19 / 21 / 24, T = 9 — exactly the Auto rows here. +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], &[4, 3]), + (17, &[4, 3], &[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], &[4]), + (15, &[4], &[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 g = (1u64 << d) - 1; + Q * (leaf * 2251 + depth * (2251 + 567) + g * (567 + 2610) + d * 477) + }; + 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 with the cap's gain recomputed from its terms). +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 (fold, tw) = (2610i128, 477i128); + let leaf = i128::from((3u64 << d).div_ceil(8).max(1) as u32); + let g = (1i128 << d) - 1; + let per_query = + leaf * wc + i128::from(depth) * (wc + ws) + g * (ws + fold) + i128::from(d) * tw; + let c = cap.height(q as usize, depth as usize) as i128; + let gain = if c == 0 { + 0 + } else { + let n = 1i128 << c; + q as i128 * (c * (wc + ws) - (n - 1) * ws - wu) - ((n - 1) * wc + n * wh + wq) + }; + (q as i128 * per_query - gain) 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: &dyn Fn(u32) -> u32, dmax: u32) -> (u64, Vec) { - struct Search<'a> { +fn brute_force(b0: u32, t: u32, q: u64, cap: CapPolicy, dmax: u32) -> (u64, Vec) { + struct Search { t: u32, q: u64, - cap: &'a dyn Fn(u32) -> u32, + cap: CapPolicy, dmax: u32, prefix: Vec, best: Option<(u64, usize, Vec)>, } - impl Search<'_> { + impl Search { fn walk(&mut self, b: u32, cost: u64) { if b == self.t { let cand = (cost, self.prefix.len(), self.prefix.clone()); @@ -642,22 +880,21 @@ fn brute_force(b0: u32, t: u32, q: u64, cap: &dyn Fn(u32) -> u32, dmax: u32) -> #[test] fn dp_is_optimal() { - let depth_mod_3 = |depth: u32| depth % 3; // an arbitrary, non-monotone policy - let caps: [(&str, &dyn Fn(u32) -> u32); 4] = [ - ("off", &no_cap), - ("model", &cap_design_model), - ("auto", &cap_auto), - ("depth%3", &depth_mod_3), + let caps = [ + CapPolicy::Off, + CapPolicy::Auto, + CapPolicy::Fixed(2), + CapPolicy::Fixed(5), ]; let mut checked = 0u32; - for (cap_name, cap) in caps { + 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_name} q={q} dmax={dmax} b0={b0} t={t}"); + 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. @@ -692,7 +929,7 @@ fn dp_is_optimal() { fn dmax_one_is_the_legacy_schedule() { for b0 in 0..=30u32 { for t in 0..=31u32 { - for cap in [&no_cap as &dyn Fn(u32) -> u32, &cap_auto, &cap_design_model] { + 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)); @@ -719,16 +956,26 @@ fn old_layout(lde_log: u32, blowup_log: u32, k: u32) -> (u32, usize, usize, u32) ) } +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() { - let dp_formats: Vec> = [false, true] + // 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| { - [&no_cap as &'static dyn Fn(u32) -> u32, &cap_auto].map(|cap_height| FriFormat { - mode: FriMode::Dp, + [CapPolicy::Off, CapPolicy::Auto].map(|cap| FriFormat { one_row, - num_queries: Q, - cap_height, + ..dp_format(cap) }) }) .collect(); @@ -747,18 +994,20 @@ fn legacy_layout_equals_old_layout() { 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 and the cap policy. - for cap_height in [&no_cap as &dyn Fn(u32) -> u32, &cap_auto] { + // 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, - one_row: false, - num_queries: Q, - cap_height, + schedule_override: FriScheduleOverride::new(&[3, 1]), + ..dp_format(cap) }; assert_eq!( FriFoldLayout::for_format(lde_log, blowup_log, k, &pair), - new, + Some(new.clone()), "{ctx}" ); } @@ -774,14 +1023,20 @@ fn legacy_layout_equals_old_layout() { "{ctx}" ); - // Any format moves only the split of the folds into committed layers. + // 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); + 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) { @@ -791,16 +1046,18 @@ fn legacy_layout_equals_old_layout() { }; assert_eq!(covered, expected, "{ctx} {fmt:?}"); assert_eq!( - FriFoldLayout::from_schedule( - lde_log, - blowup_log, - k, - fmt.one_row, - l.schedule.clone() - ), - Some(l), + 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; } @@ -829,3 +1086,88 @@ fn from_schedule_rejects_a_schedule_that_does_not_cover_the_folds() { 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), + 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).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).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), + 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).unwrap(); + assert_eq!(l.schedule, vec![1u8; span as usize]); + assert!(!l.is_legacy()); + // One-row is refused until S2 exists. + for one_row in [OneRowMode::On, OneRowMode::Auto] { + let o = options_with(ProofFormat { + one_row, + ..ProofFormat::DEFAULT + }); + assert_eq!( + FriFoldLayout::for_options(20, 1, &o), + Err(FriFormatError::OneRowNotImplemented) + ); + } + // 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/prover/src/zf_format.rs b/prover/src/zf_format.rs index 10e6b2f7e..f364b371f 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -200,6 +200,8 @@ impl ZfFormat { 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, } } From 321e4860a72abc613224dad9e73756a01aa7126d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:51:09 -0300 Subject: [PATCH 10/73] test(math-cuda): device commit and fold parity at k = 6 W2's first6 schedule folds 6 variables in round 0, so tree 0's leaves are 64 base felts and the first fold runs six levels in one residency. GPU parity covered k <= 5. - whir_commit every_shape (both hashes): parity at (14, 2, 6), (12, 2, 6) and (7, 1, 6) (four leaves). Root, codeword, and openings against the host pipeline, through commit_codeword_to_host, which has no host fallback. - whir_fold: the_device_folds_six_levels_as_the_host_does, six levels on the base codeword at 2^16, 2^14 and 2^8, against fold_codeword_k_on_host. It calls math_cuda::whir::fold_codeword_base directly, because whir::fold_codeword_k falls back to the host silently (size threshold, kill switch) and a comparison through it can be the host against itself. Box only: the laptop has no CUDA (clippy with stub cubins is green). --- crypto/math-cuda/tests/whir_commit.rs | 11 +++++-- crypto/math-cuda/tests/whir_fold.rs | 47 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/crypto/math-cuda/tests/whir_commit.rs b/crypto/math-cuda/tests/whir_commit.rs index ac20f5b91..911ab71b6 100644 --- a/crypto/math-cuda/tests/whir_commit.rs +++ b/crypto/math-cuda/tests/whir_commit.rs @@ -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..3c3dc1739 100644 --- a/crypto/math-cuda/tests/whir_fold.rs +++ b/crypto/math-cuda/tests/whir_fold.rs @@ -92,3 +92,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); + } +} From 299f379301a6e1716dad7a5cfa7faa52c39502b7 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:54:06 -0300 Subject: [PATCH 11/73] feat(multilinear): the Merkle cap on WHIR chains, host prover and verifier (W1, C6) Every WHIR commitment tree can now be opened under a Merkle cap: each authentication path stops c levels below the root, and the tree's cap (its 2^c nodes at that height) rides once, at the end of the tree's first opening in proof order (the owner-path encoding, design/CAP.md section 3). No struct changes, nothing new absorbed: the root is still the commitment. At the default (CapPolicy::Off) every height is 0 and the proof bytes are today's. - ChainConfig::tree_caps: one height per tree from config.format.cap, CapPolicy::height(openings, depth) with depth = D_t - k_t and openings Q for tree 0, 2Q for every later tree (the last included). Shared by the prover, the host verifier and (next commit) the LFM ChainShape. - CodewordCommitment::open_many_capped(indices, c, owner): paths cut to depth - c, the owner's first path carrying the cap. Host trees read MerkleTree::cap; device codewords read the cap in the SAME with_tree rebuild as the paths (DeviceCodeword::paths_and_cap, math-cuda + the multilinear gpu.rs wrapper), so the cap costs no extra tree build and retention/eviction are untouched. open_many is open_many_capped(.., 0, _). - whir_round::prove takes RoundCaps; round 0 owns tree 0, every round owns its successor. final_openings likewise (a one-round chain's tree 0 is owned by the final openings). - Verifier: TreeCheck { Owner, Checked }. A tree is authenticated ONCE, by CappedRoot::from_owner on its owner opening; round t opens tree t against the check round t-1 returned and never re-reads a cap (a cap on round t's first current opening fails its exact length). The checks are built after the opening-count guards, from .first(), so a short proof is refused, never a panic (REVIEW-CAP M2). - Default-path hardening, the WHIR analogue of C1b (RULINGS 3/16): whir_commit::verify_opening now takes the tree depth and requires an exact-length path (CappedRoot::uncapped). Honest proofs are unaffected; only malformed proofs see a difference. New verify_opening_capped. - New errors: CapRejected (verifier), CapEmbedFailed (prover). Tests (laptop, multilinear --lib whir_*): capped chains round-trip at Fixed(1..3) and Auto, Q 3 and 25, one-round, multi-round, remainder, base and extension rounds, keccak and RPX, with every path length pinned (depth - c, owner + 2^c); Off and Fixed(0) give identical rkyv bytes; transcript invariance Off vs Fixed(3)/Auto (REVIEW-CAP S2); tamper arm (tree-0 cap, tree-t cap in rounds[t-1].next[0], a second cap on round t's current[0], owner path +-1, non-owner path +-1, cap moved to query 1, a sibling, a proof read under another policy); M1(b) an unreached cap node that every per-query check accepts, refused as CapRejected only by the cap-to-root check (both trees of a round); M1(a) a keccak leaf forged from an internal node (8 base values = 64 bytes = a parent input) that the raw fold accepts, refused by the exact length alone; M2 an empty capped round refused without a panic; tree_caps pinned at production (Auto [3,3,3,3,3,3,2]). --- crypto/math-cuda/src/whir.rs | 31 ++ crypto/math-cuda/tests/whir_commit.rs | 2 +- crypto/math-cuda/tests/whir_fold.rs | 1 + crypto/math-cuda/tests/whir_tree_cache.rs | 2 +- crypto/multilinear/src/gpu.rs | 51 +++ crypto/multilinear/src/lib.rs | 10 + crypto/multilinear/src/whir_cap_tests.rs | 402 +++++++++++++++++ crypto/multilinear/src/whir_chain.rs | 134 ++++-- crypto/multilinear/src/whir_commit.rs | 163 ++++++- crypto/multilinear/src/whir_eval.rs | 2 +- crypto/multilinear/src/whir_round.rs | 499 ++++++++++++++++++++-- prover/src/lfm/whir_open_tests.rs | 14 +- 12 files changed, 1225 insertions(+), 86 deletions(-) create mode 100644 crypto/multilinear/src/whir_cap_tests.rs 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/whir_commit.rs b/crypto/math-cuda/tests/whir_commit.rs index ac20f5b91..7dc6435db 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 ); diff --git a/crypto/math-cuda/tests/whir_fold.rs b/crypto/math-cuda/tests/whir_fold.rs index 2a3decdcc..67dcb6471 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 ), 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/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/whir_cap_tests.rs b/crypto/multilinear/src/whir_cap_tests.rs new file mode 100644 index 000000000..cbc483c93 --- /dev/null +++ b/crypto/multilinear/src/whir_cap_tests.rs @@ -0,0 +1,402 @@ +//! W1 — the Merkle cap on WHIR chains (design/CAP.md §5), 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 internal-node leaf of REVIEW-CAP M1) 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])); + } + } +} + +/// REVIEW-CAP S2: 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 667e4ffc8..83eb3b750 100644 --- a/crypto/multilinear/src/whir_chain.rs +++ b/crypto/multilinear/src/whir_chain.rs @@ -56,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. @@ -317,6 +317,34 @@ impl ChainConfig { } } + /// 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, + /// design/CAP.md §5.1). + /// + /// 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. pub fn schedule(&self, num_vars: usize) -> Vec { let step = self.log_folding.max(1); @@ -798,6 +826,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); @@ -901,26 +931,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 { @@ -1016,6 +1061,7 @@ where fn final_openings( current: &CodewordCommitment, config: &RoundConfig, + caps: RoundCaps, transcript: &mut T, ) -> Result, Error> where @@ -1033,7 +1079,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(), }) } @@ -1073,9 +1119,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, @@ -1102,7 +1148,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` @@ -1159,11 +1212,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, @@ -1180,8 +1234,8 @@ where &round_config, transcript, )?, - } - current_root = *next_root; + }; + current = TreeCheck::Checked(next_check); } (None, None, None) => { transcript.append_field_element(&proof.final_value); @@ -1189,7 +1243,7 @@ where match &round.openings { RoundOpenings::Base(openings) => verify_final::( openings, - ¤t_root, + current, ¤t_domain, &group.point, &round_config, @@ -1198,7 +1252,7 @@ where )?, RoundOpenings::Extension(openings) => verify_final::( openings, - ¤t_root, + current, ¤t_domain, &group.point, &round_config, @@ -1243,9 +1297,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, @@ -1267,10 +1321,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 (REVIEW-CAP M2). 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 { diff --git a/crypto/multilinear/src/whir_commit.rs b/crypto/multilinear/src/whir_commit.rs index b1a72e7fa..580bb54d5 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 (design/CAP.md §9.4). +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..770130411 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 (design/CAP.md §5.3, §9.3): 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 (design/REVIEW-CAP.md M2). +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:?}")); + } + } + + /// REVIEW-CAP M1(b): 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) + )); + } + + /// REVIEW-CAP M1(a), the WHIR analogue of C1b: 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/prover/src/lfm/whir_open_tests.rs b/prover/src/lfm/whir_open_tests.rs index d58aaae9f..7ac5a7138 100644 --- a/prover/src/lfm/whir_open_tests.rs +++ b/prover/src/lfm/whir_open_tests.rs @@ -292,7 +292,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 +316,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 +363,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 +385,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 +403,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 +425,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 +448,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!( From 93447d9720b5e8a663761757f200ffa345ceceb2 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:54:27 -0300 Subject: [PATCH 12/73] test(lfm): the in-guest WHIR verifier at first folds of 5 and 6 No emitter change: ChainShape builds from config.schedule, so the closed forms, the arena layout, the query phase and the slot mux follow the schedule. What was missing is gates at k = 5 and 6. - whir_fold_tests SHAPES gain (12, 5, 7) and (13, 6, 7): blocks of 32 and 64, closed form, interned constants by value, and the fold against the host over extension and base blocks. - whir_chain_tests: KNOB_COST_SHAPES (S = 9 first6 [6,3], S = 11 first5 [5,4,2] at grind 0 and 8; S = 6 [6] and S = 7 [6,1] under first6) join the schedule gate (the emitter's hash schedule is the host transcript's) and the closed-form gate, through a cost_configs() list that keeps COST_SHAPES at the default schedule. A first-fold chain executes on a proof the host accepts; the tamper arm refuses the last value of a 64-wide base block, a round-0 sibling and the successor block, each rejected by the host too. - Knob-on production pins at S = 25, Q = 112, grind 20, derived by hand (parents, leaf blocks) and equal to design/WHIR.md's independent model: first6 19,600 opening / 19,877 chain permutations / 201,318 rows; first5 20,832 / 21,109 / 189,028. The ignored F1 at the production shape emits both programs and matches (run on the laptop: 0.08 s, 153 MB). - PREPARED_LEG_ROWS stays fixed (RULINGS 15); a knob-on test asserts it still covers the 20-variable stack: 137,321 rows under first5, 155,889 under first6, against 175,066. Default pins unchanged (22,512 / 22,828 / 185,509 and the band test). --- prover/src/lfm/whir_chain_tests.rs | 285 +++++++++++++++++++++++++++-- prover/src/lfm/whir_fold_tests.rs | 13 +- 2 files changed, 284 insertions(+), 14 deletions(-) diff --git a/prover/src/lfm/whir_chain_tests.rs b/prover/src/lfm/whir_chain_tests.rs index 5000399d5..dafa60e73 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, + ChainConfig, ChainFormat, ChainProof, FirstFold, GrindBits, RoundOpenings, WhirFolds, commit, + prove, verify, }; use multilinear::whir_hash::RpxWhir; @@ -269,6 +270,17 @@ fn config(num_queries: usize, grind: u8) -> ChainConfig { } } +/// [`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) + } +} + /// Everything one run needs on both sides. struct Fixture { proof: ChainProof, @@ -292,17 +304,21 @@ 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 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(); @@ -312,7 +328,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"); @@ -321,7 +337,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(), @@ -735,6 +751,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]` (design/WHIR.md §4.8), 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() } @@ -782,8 +828,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()); @@ -793,8 +840,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()), ); @@ -840,8 +888,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(); @@ -860,10 +909,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), @@ -1395,3 +1445,212 @@ fn the_single_chain_term_prices_a_stack_of_at_most_sixty_four_pages() { // The block carries three. assert_eq!(polys_at(3), 1); } + +// --------------------------------------------------------------------------- +// 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) are design/WHIR.md §4.8's, +/// from D-WHIR's independent Python re-implementation of these forms +/// (`whir_model.py`), 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 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 (RULINGS 15), 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_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. From 4d67789701b9148a3d580a5116480c8a6775a835 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:54:34 -0300 Subject: [PATCH 13/73] feat(stark): Merkle caps on the host path (C3, design/CAP.md section 4) Every tree of a univariate STARK proof (main, precomputed, aux, composition and each committed FRI layer) now honours ProofOptions.format.merkle_cap. - merkle_caps.rs (new): StarkCaps, the one place the heights are computed from public shape (policy, query count, log2(lde), committed layer count; trace trees log2(lde)-1 deep, FRI layer i log2(lde)-i-2); TreeCheck, the verifier's per-tree check (built once, then used for every query); TableTreeChecks. - Prover: a post-pass in round 4 after the openings. Per capped tree it reads the cap from the host tree and embeds it on the owner path (query 0), cutting every path to D - c. Round 4 now returns a Result. A device-resident tree (root-only host tree) is a hard DevicePath error that names the tree until the device read lands (REVIEW-CAP S6); a host tree whose depth is not the format's is refused. - Verifier: table_tree_checks builds every tree's check once, after the query-count and opening-width guards and with length-checked access only, so a malformed proof rejects and never panics (REVIEW-CAP M2). At c = 0 it reads no opening at all and is exactly the C1b exact-length check. Every opening (trace, precomputed, aux, composition, FRI layer) goes through its tree's check with its query position; query 0 of a capped tree uses the owner siblings split off once. Nothing is absorbed, so the transcript is unchanged, and at the default every height is 0: no path is cut, no cap is appended, the bytes are the same. MERKLE_CAP_IMPLEMENTED stays false until the device arm (C4) is in. Tests (tests::merkle_cap_tests, small AIRs, laptop): - round trips at Fixed(1..=4) and Auto, 3/8/30 queries, blowup 2 and 4, owned and archived (rkyv, multi_verify_archived), with the path shapes pinned (owner D-c+2^c, others D-c, the cap hashes to the root); - a preprocessed table and a RAP (aux) table capped, every cap node bound; - Off == Fixed(0) == Auto-at-3-queries, byte for byte; - REVIEW-CAP S2: Off vs Auto at grinding 0 give equal roots, OOD values, final coefficients, nonce and opened values; only paths differ, each the full path cut to D - c (+ the cap on the owner); - tampers: every cap node of main/composition/first and last FRI layer, every node of a later query's path, the owner one node short/long, the cap on a non-owner, the cap moved to query 1, and a proof made under one policy verified under another (both directions); - REVIEW-CAP M1 at the verifier level: an unreached cap node (3 queries, c = 3) that only the cap-to-root check rejects, and the real internal node above a queried leaf passed as a leaf hash, which the verifier's own TreeCheck refuses and the length-agnostic fold accepts. Deleting verify_cap or the length check from the primitive fails both (checked by hand); - S6: a root-only tree with no device read is an Err naming the tree. --- crypto/stark/src/lib.rs | 1 + crypto/stark/src/merkle_caps.rs | 156 ++++ crypto/stark/src/prover.rs | 189 ++++- crypto/stark/src/tests/merkle_cap_tests.rs | 707 ++++++++++++++++++ crypto/stark/src/tests/mod.rs | 1 + crypto/stark/src/tests/opening_width_tests.rs | 4 +- crypto/stark/src/verifier.rs | 231 ++++-- 7 files changed, 1218 insertions(+), 71 deletions(-) create mode 100644 crypto/stark/src/merkle_caps.rs create mode 100644 crypto/stark/src/tests/merkle_cap_tests.rs diff --git a/crypto/stark/src/lib.rs b/crypto/stark/src/lib.rs index 8888b30a6..1154742cb 100644 --- a/crypto/stark/src/lib.rs +++ b/crypto/stark/src/lib.rs @@ -24,6 +24,7 @@ pub mod instruments; #[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; diff --git a/crypto/stark/src/merkle_caps.rs b/crypto/stark/src/merkle_caps.rs new file mode 100644 index 000000000..7492894ad --- /dev/null +++ b/crypto/stark/src/merkle_caps.rs @@ -0,0 +1,156 @@ +//! Merkle caps of a univariate STARK proof (design/CAP.md §4, 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 C1b +//! exact-length check, so the default format verifies the bytes it did. + +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. Every tree + /// is opened `num_queries` times. + 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, + } + } + + /// 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/prover.rs b/crypto/stark/src/prover.rs index a457f0995..3afea142d 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -2755,7 +2755,7 @@ pub trait IsStarkProver< round_3_result: &Round3, z: &FieldElement, transcript: &mut (impl IsStarkTranscript + Clone), - ) -> Round4 + ) -> Result, ProvingError> where FieldElement: AsBytes, FieldElement: AsBytes, @@ -2922,15 +2922,36 @@ pub trait IsStarkProver< let number_of_queries = air.options().fri_number_of_queries; let iotas = Self::sample_query_indexes(number_of_queries, domain, transcript); - let query_list = fri::query_phase::(&fri_layers, &iotas); + let mut query_list = fri::query_phase::(&fri_layers, &iotas); let fri_layers_merkle_roots: Vec<_> = fri_layers .iter() .map(|layer| layer.merkle_tree.root) .collect(); - let deep_poly_openings = + let mut deep_poly_openings = Self::open_deep_composition_poly(domain, round_1_result, round_2_result, &iotas); + + // Merkle caps (design/CAP.md §4.2): 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. + let caps = crate::merkle_caps::StarkCaps::new( + air.options().format.merkle_cap, + number_of_queries, + domain_size.trailing_zeros() as usize, + 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,12 +2960,170 @@ 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 (design/CAP.md §3–§4.2). + /// + /// 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}")) + }) + } + + let (depth, c) = (caps.trace_depth, caps.trace); + if c > 0 { + let main_cap = Self::tree_cap(&round_1_result.main.tree, depth, c, "main", |_| None)?; + 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() { + 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() { + let cap = Self::tree_cap(&aux.tree, depth, c, "aux", |_| None)?; + embed( + deep_poly_openings + .iter_mut() + .map(|o| o.aux_trace_polys.as_mut().map(|p| &mut p.proof.merkle_path)), + depth, + &cap, + "aux", + )?; + } + let cap = Self::tree_cap( + &round_2_result.composition_poly_merkle_tree, + depth, + c, + "composition", + |_| None, + )?; + 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}"); + let cap = Self::tree_cap(&layer.merkle_tree, depth, c, &what, |_| None)?; + 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 + /// (REVIEW-CAP S6). + 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" + ))), } } @@ -5314,7 +5493,7 @@ pub trait IsStarkProver< &round_3_result, &z, transcript, - ); + )?; #[cfg(feature = "instruments")] { 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..c9825607a --- /dev/null +++ b/crypto/stark/src/tests/merkle_cap_tests.rs @@ -0,0 +1,707 @@ +//! Merkle caps on univariate STARK proofs (design/CAP.md §4, lever S1, commit C3). +//! +//! 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 (REVIEW-CAP S2); +//! - tampers of every tree class, of the owner split, and of the policy; +//! - REVIEW-CAP M1 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)); +} + +/// REVIEW-CAP S2: 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`, C1b) 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 + +/// REVIEW-CAP S6: 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)); +} diff --git a/crypto/stark/src/tests/mod.rs b/crypto/stark/src/tests/mod.rs index a757e909a..fc784e3f1 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -9,6 +9,7 @@ pub mod constraint_index_tests; pub mod domain_cache_stats; pub mod fri_tests; pub mod grinding_tests; +pub mod merkle_cap_tests; pub mod opening_width_tests; pub mod path_length_tests; pub mod proof_options_tests; diff --git a/crypto/stark/src/tests/opening_width_tests.rs b/crypto/stark/src/tests/opening_width_tests.rs index db5764220..ca3dbbd33 100644 --- a/crypto/stark/src/tests/opening_width_tests.rs +++ b/crypto/stark/src/tests/opening_width_tests.rs @@ -76,7 +76,7 @@ pub struct FibonacciSplitAIR { 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 +96,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, diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index ad093e182..491718c65 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -5,6 +5,7 @@ use super::{ proof::stark::StarkProof, traits::{AIR, TransitionEvaluationContext}, }; +use crate::merkle_caps::{StarkCaps, TableTreeChecks, TreeCheck}; pub use crate::proof::view::PiDeserializer; use crate::{ config::Commitment, @@ -18,7 +19,6 @@ use crate::{ table::Table, }; use crypto::fiat_shamir::is_transcript::IsStarkTranscript; -use crypto::merkle_tree::cap::CappedRoot; use crypto::merkle_tree::traits::IsMerkleTreeBackend; use crypto::merkle_tree::traits::IsStreamingLeafBackend; #[cfg(not(feature = "test_fiat_shamir"))] @@ -473,6 +473,7 @@ pub trait IsStarkVerifier< /// 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>, @@ -484,6 +485,9 @@ 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, @@ -536,9 +540,10 @@ pub trait IsStarkVerifier< return false; } - // `log2` of the LDE size: every tree's depth is a function of it (a - // verifier constant, never read from the proof). - let lde_log = domain.lde_length.trailing_zeros() as usize; + // 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 = @@ -571,7 +576,8 @@ pub trait IsStarkVerifier< &deep_poly_evaluations[i], &deep_poly_evaluations_sym[i], &terminal_codeword, - lde_log, + &checks.fri, + i, ) }) } @@ -594,13 +600,15 @@ pub trait IsStarkVerifier< /// so one Merkle path authenticates both `evaluations` (the row) and /// `evaluations_sym` (its symmetric). Same layout used for trace and composition. /// - /// The path must be exactly `depth` siblings long (`log2(lde) − 1`, a - /// verifier constant): see [`trace_tree_depth`](Self::trace_tree_depth). + /// `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, - depth: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -615,20 +623,16 @@ pub trait IsStarkVerifier< opening.evaluations(), opening.evaluations_sym(), ); - CappedRoot::uncapped(root, depth).verify::>( - opening.merkle_path(), - 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, - depth: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -637,12 +641,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, - depth, ); - // 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 @@ -653,11 +658,11 @@ 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, depth) + (Some(check), Some(opening)) => { + Self::verify_opening_pair::(opening, check, query, iota) } (None, None) => true, _ => false, @@ -670,12 +675,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, depth) + 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, @@ -688,9 +690,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, - depth: usize, + check: &TreeCheck<'_>, + query: usize, + iota: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -705,8 +707,12 @@ pub trait IsStarkVerifier< composition_poly.evaluations_sym(), ); - CappedRoot::uncapped(composition_poly_merkle_root, depth) - .verify::>(composition_poly.merkle_path(), *iota, leaf_hash) + check.verify::>( + query, + composition_poly.merkle_path(), + iota, + leaf_hash, + ) } /// Verifies the validity of the purported values of the trace polynomials and the composition polynomial @@ -715,7 +721,7 @@ pub trait IsStarkVerifier< fn step_4_verify_trace_and_composition_openings( proof: StarkProofView<'_, Field, FieldExtension, PI>, challenges: &Challenges, - domain: &VerifierDomain, + checks: &TableTreeChecks<'_>, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -726,40 +732,123 @@ pub trait IsStarkVerifier< >(); // `step_3_verify_fri` (which runs before this) already rejects proofs // whose `deep_poly_openings` is shorter than `challenges.iotas`. - let depth = Self::trace_tree_depth(domain); - 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, - depth, - ) && Self::verify_trace_openings(proof, deep_poly_opening, *iota_n, depth) + Self::verify_composition_poly_opening(deep_poly_opening, &checks.composition, i, iota_n) + && Self::verify_trace_openings(deep_poly_opening, checks, i, iota_n) }) } - /// 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). Every authentication - /// path into these trees must be exactly this long. + /// The per-tree Merkle checks of one table's proof, built ONCE per tree + /// before any query is verified (design/CAP.md §4.3). + /// + /// 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 + /// `log2(lde) − i − 2` deep. Every authentication path must be exactly + /// `depth − c` long (C1b at `c = 0`: before that a path of any length was + /// folded and compared with the root, design/CAP.md §9.4). /// - /// Before this was checked, a path of any length was folded and compared - /// with the root; a short one compares an internal node with the root. No - /// exploit was shown (it needs a leaf hash equal to an internal node, a - /// cross-function collision under the algebraic backend), but the length - /// is a verifier constant, so it is now enforced (design/CAP.md §9.4). - fn trace_tree_depth(domain: &VerifierDomain) -> usize { - (domain.lde_length.trailing_zeros() as usize).saturating_sub(1) + /// 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 (REVIEW-CAP M2): 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(); + let num_committed = Self::fri_termination_params(air, domain).num_committed; + let lde_log = domain.lde_length.trailing_zeros() as usize; + let caps = StarkCaps::new( + options.format.merkle_cap, + options.fri_number_of_queries, + lde_log, + num_committed, + ); + 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, iota: usize, - depth: usize, ) -> bool where FieldElement: AsBytes + Sync + Send, @@ -771,7 +860,8 @@ pub trait IsStarkVerifier< vec![evaluation.clone(), evaluation_sym.clone()] }; - CappedRoot::uncapped(merkle_root, depth).verify::>( + check.verify::>( + query, auth_path_sym, iota >> 1, as IsMerkleTreeBackend>::hash_data(&evaluations), @@ -796,7 +886,10 @@ pub trait IsStarkVerifier< deep_composition_evaluation: &FieldElement, deep_composition_evaluation_sym: &FieldElement, terminal_codeword: &[FieldElement], - lde_log: usize, + // 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, @@ -837,26 +930,24 @@ 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, index, - // Layer `i` holds `lde / 2^(i+1)` values in pair - // leaves: `log2(lde) − i − 2` levels. - lde_log.saturating_sub(i + 2), ); // Update `v` with next value pᵢ₊₁(𝜐^(2ⁱ⁺¹)). @@ -1717,6 +1808,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 (REVIEW-CAP M2). 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 @@ -1823,6 +1925,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"); @@ -1840,7 +1943,7 @@ pub trait IsStarkVerifier< let timer4 = Instant::now(); #[allow(clippy::let_and_return)] - if !Self::step_4_verify_trace_and_composition_openings(proof, &challenges, &domain) { + 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; From f7dee29b011f72a148483bdc2c7bc1cb81de2830 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:54:58 -0300 Subject: [PATCH 14/73] test(math-cuda,multilinear): device parity for the WHIR Merkle cap (W1, C7) The device side of W1 landed with the host commit (the Codeword::Device arm of open_many_capped must compile): DeviceCodeword::paths_and_cap reads the cap as the heap slice [2^c - 1, 2^(c+1) - 1) of the node buffer the paths are gathered from, inside ONE with_tree rebuild. These are its box gates; the laptop has no CUDA, so they only compile here. - math-cuda/tests/whir_cap.rs: for k = 1..5 under keccak and RPX, every cap height up to min(depth, 6): the device paths equal the host tree's full paths, the height-0 cap is the root, and the device cap equals the cap the host owner encoding appends. In three leaf-layer regimes: served from the retained layer (0 extra leaf passes), rehashed at another blocking (1), and after the allocator's evictor reclaimed the layer. Each call is exactly one tree build (tree_builds + 1): the cap costs no extra rebuild. - multilinear/tests/whir_cap_device.rs (cuda-gated): a 2^16-variable chain whose codeword stays on the card (asserted) proves the same rkyv bytes as the chain over a host-held codeword at Off, Auto and Fixed(5), both hashes, and the host verifier accepts it; tree 0's owner path length is pinned. --- crypto/math-cuda/tests/whir_cap.rs | 188 ++++++++++++++++++++ crypto/multilinear/tests/whir_cap_device.rs | 126 +++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 crypto/math-cuda/tests/whir_cap.rs create mode 100644 crypto/multilinear/tests/whir_cap_device.rs diff --git a/crypto/math-cuda/tests/whir_cap.rs b/crypto/math-cuda/tests/whir_cap.rs new file mode 100644 index 000000000..9ad332424 --- /dev/null +++ b/crypto/math-cuda/tests/whir_cap.rs @@ -0,0 +1,188 @@ +//! 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)`. +fn assert_matches_host( + name: &str, + device: &math_cuda::whir::DeviceCodeword, + host: &CodewordCommitment, + k: usize, + positions: &[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) { + 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. +#[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 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, "the layer was evicted"); + drop(got); + drop(hog); + + 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, |d| d >= 1); +} 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); + } +} From 2d1c56234dd741a177885652dfe663eaedd249c0 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:55:32 -0300 Subject: [PATCH 15/73] feat(multilinear): WHIR_FOLDS_IMPLEMENTED = true LAMBDA_VM_ZF_WHIR_FOLDS=first5 | first6 is now selectable: host chain, statement word, agrees_with, the production config, the GPU parity cases at k = 6 and the in-guest gates at k = 5 and 6 are in. The GPU parity tests run on the box (no CUDA on the laptop); the knob-on block proofs are the box request that follows. --- crypto/multilinear/src/whir_chain.rs | 6 ++++-- prover/src/zf_format.rs | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/crypto/multilinear/src/whir_chain.rs b/crypto/multilinear/src/whir_chain.rs index 0cd42c853..e112ebdfa 100644 --- a/crypto/multilinear/src/whir_chain.rs +++ b/crypto/multilinear/src/whir_chain.rs @@ -259,8 +259,10 @@ pub enum WhirFolds { First(FirstFold), } -/// See [`WHIR_CAP_IMPLEMENTED`]. -pub const WHIR_FOLDS_IMPLEMENTED: bool = false; +/// 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 diff --git a/prover/src/zf_format.rs b/prover/src/zf_format.rs index 9b0faa689..f40b12eba 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -449,6 +449,15 @@ mod tests { ); } + #[test] + fn the_whir_fold_lever_is_selectable() { + 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(); From 88db27b4bc576544958db4fc9c3a946020f5a797 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:57:30 -0300 Subject: [PATCH 16/73] test(stark,prover): default-format golden proofs (H0) REVIEW-FRI F1: nothing proved the default FRI format byte-identical. A round trip cannot (a drifted prover accepts its own proofs), and proof bytes are not reproducible under grinding (parallel nonce search). These goldens prove at grinding_factor = 0, where the bytes ARE reproducible (checked: two runs, identical), and pin, per case, the digest of the proof's rkyv bytes plus separately its FRI layer roots, terminal coefficients, FRI decommitments and trace/composition openings, so a failure names the field that drifted. Generated before any S3 prover code, on the schedule-DP commits (which change no prover path): - stark::tests::zf_golden_tests (SHA3-256): Keccak and Blake3; SimpleAddition (E = F) and LogReadOnlyRAP (E = F^3, aux); blowup 2 and 4; total_folds 0, 1, 2, 3, 4, 6; one CPU/ADD/MUL multi_prove bus proof. - prover tests::zf_rpx_golden_tests (SHA-256): the same AIRs under the production RPX pin (RpxStarkHash), which the stark crate cannot name. Shown able to fail: swapping the pair order of the FRI layer leaves in the CPU prover turns default_format_goldens_are_byte_identical red. sha3 becomes a stark dev-dependency (the version crypto already links). --- Cargo.lock | 1 + crypto/stark/Cargo.toml | 3 + crypto/stark/src/tests/mod.rs | 1 + .../stark/src/tests/residency_mode_tests.rs | 2 +- crypto/stark/src/tests/zf_golden_tests.rs | 378 ++++++++++++++++++ prover/src/tests/mod.rs | 2 + prover/src/tests/zf_rpx_golden_tests.rs | 214 ++++++++++ 7 files changed, 600 insertions(+), 1 deletion(-) create mode 100644 crypto/stark/src/tests/zf_golden_tests.rs create mode 100644 prover/src/tests/zf_rpx_golden_tests.rs 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/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/src/tests/mod.rs b/crypto/stark/src/tests/mod.rs index bb15f76b5..556b25ea8 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -22,3 +22,4 @@ pub mod small_trace_tests; pub mod table_disk_spill_tests; pub mod terminal_tests; pub mod trace_test_helpers; +pub mod zf_golden_tests; 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_golden_tests.rs b/crypto/stark/src/tests/zf_golden_tests.rs new file mode 100644 index 000000000..0d11ea857 --- /dev/null +++ b/crypto/stark/src/tests/zf_golden_tests.rs @@ -0,0 +1,378 @@ +//! Default-format golden proofs (REVIEW-FRI F1): 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 (commit +//! "H0" of lane I-FRI-H, on `zf/cap-stark` @ 77ea1ab89 + the schedule DP, which +//! 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| { + sha3_hex(&bytes.expect("rkyv")) + }; + Fingerprint { + proof: rk(rkyv::to_bytes::(proof)), + fri_roots: 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, + )), + } + }}; +} +#[allow(unused_imports)] // for the prover-free S3 tests in this crate +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`. +pub(crate) fn prove_logup( + rows: usize, + options: &ProofOptions, +) -> ( + LogReadOnlyRAP, + StarkProof>, + LogReadOnlyPublicInputs, +) { + 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/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index d5747c2b6..b63e9d403 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -130,3 +130,5 @@ pub mod whir_byte_gate; pub mod whir_hash_tests; #[cfg(test)] pub mod whir_identity_tests; +#[cfg(test)] +pub mod zf_rpx_golden_tests; 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..0c0fd6f54 --- /dev/null +++ b/prover/src/tests/zf_rpx_golden_tests.rs @@ -0,0 +1,214 @@ +//! Default-format golden proofs under the production RPX pin (REVIEW-FRI F1): +//! the RPX half of `stark::tests::zf_golden_tests` (which covers Keccak and +//! Blake3 and cannot name `RpxStarkHash`, a prover-crate type). +//! +//! 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. Generated at +//! the default format before any S3 prover code existed; regenerate 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::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)> { + let d = ProofFormat::DEFAULT; + 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 +} + +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", + ), +]; + +#[test] +fn default_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 default-format RPX proof moved (a field whose digest differs is where)" + ); + } +} + +#[test] +#[ignore = "generator for GOLDENS"] +fn print_goldens() { + for (name, line) in compute_goldens() { + println!("GOLDEN (\"{name}\", \"{line}\"),"); + } +} From ac73346b457f0102201ec7beed48e0d115de3b8f Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 17:59:09 -0300 Subject: [PATCH 17/73] test(prover): the fold lever's selectability as a const assertion (clippy) assertions_on_constants: WHIR_FOLDS_IMPLEMENTED is a const, so the check is a const block. make fmt and make lint green. --- prover/src/zf_format.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prover/src/zf_format.rs b/prover/src/zf_format.rs index f40b12eba..7038f88b4 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -451,7 +451,7 @@ mod tests { #[test] fn the_whir_fold_lever_is_selectable() { - assert!(multilinear::whir_chain::WHIR_FOLDS_IMPLEMENTED); + 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}"); From 1293f5a09199e4b0a99f9af5bd2040ccb5cffa0a Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 18:03:55 -0300 Subject: [PATCH 18/73] feat(stark,math-cuda): Merkle caps off device-resident trees (C4) The R4 cap post-pass now reads a device-resident tree's cap instead of refusing it: math_cuda::merkle::read_cap_dev is one D2H of the heap slice [(2^c-1)*32, (2^{c+1}-1)*32) (the device heap has the host layout, so these are the nodes MerkleTree::cap returns), and gpu_lde::read_cap_dev wraps it with shape checks that fail closed with a message, never a panic. The device arms: main and aux (gpu_main/gpu_aux trees, the table's bound stream), composition (gpu_composition_tree) and each FRI layer (gpu_tree, a fresh backend stream as the device FRI query phase uses). The precomputed tree is always a full host tree. No kernel, no commit-phase change: paths are still gathered in full and cut on the host (the merkle_gather parity is untouched). New counter gpu_cap_read_calls. MERKLE_CAP_IMPLEMENTED is now true (C3 + C4 are both in), so LAMBDA_VM_ZF_CAP no longer aborts. The in-guest LFM verifier (C5) does not verify caps yet: a recursion run that wraps a capped proof fails there, so the knob is for STARK-level tests until C5. Tests (box only; the laptop has no CUDA, cuda clippy is the laptop gate): - math-cuda tests/merkle_cap.rs: keccak trees 2^1..2^8, 2^12, 2^18, 2^22 leaves, every c <= min(D, 6): the device read equals the host cap and the heap slice, c = 0 the root; RPX trees equal the device's own heap slice; a kept composition tree (GpuMerkleTree) serves its cap and root; - stark tests::merkle_cap_tests::device_trees_serve_their_caps (ignored, cuda): a 2^14-row cubic LogUp table proved at Auto/30 queries takes caps off the device (counter moves), verifies owned and archived, and matches an Off proof of the same witness with every path cut to D - c; - zf_format::the_merkle_cap_knob_is_selectable. --- crypto/math-cuda/src/merkle.rs | 41 ++++++ crypto/math-cuda/tests/merkle_cap.rs | 143 +++++++++++++++++++++ crypto/stark/src/gpu_lde.rs | 64 +++++++++ crypto/stark/src/proof/options.rs | 8 +- crypto/stark/src/prover.rs | 53 +++++++- crypto/stark/src/tests/merkle_cap_tests.rs | 135 +++++++++++++++++++ prover/src/zf_format.rs | 17 +++ 7 files changed, 456 insertions(+), 5 deletions(-) create mode 100644 crypto/math-cuda/tests/merkle_cap.rs diff --git a/crypto/math-cuda/src/merkle.rs b/crypto/math-cuda/src/merkle.rs index 8510200ce..a7161a152 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)` (design/CAP.md §1.3). 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 diff --git a/crypto/math-cuda/tests/merkle_cap.rs b/crypto/math-cuda/tests/merkle_cap.rs new file mode 100644 index 000000000..f2fa2d320 --- /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 +//! (design/CAP.md §4.2) 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/stark/src/gpu_lde.rs b/crypto/stark/src/gpu_lde.rs index 41d48d36b..1847e6804 100644 --- a/crypto/stark/src/gpu_lde.rs +++ b/crypto/stark/src/gpu_lde.rs @@ -534,6 +534,7 @@ 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); @@ -610,6 +611,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 @@ -3517,6 +3527,60 @@ pub(crate) fn gather_proofs_dev( Some(proofs) } +/// Read the height-`cap_height` Merkle cap of a device-resident tree +/// (design/CAP.md §4.2): 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 diff --git a/crypto/stark/src/proof/options.rs b/crypto/stark/src/proof/options.rs index 280649f17..0f2455995 100644 --- a/crypto/stark/src/proof/options.rs +++ b/crypto/stark/src/proof/options.rs @@ -193,7 +193,13 @@ impl FromStr for OneRowMode { /// stable while the campaign lands it — must not be selectable, or a run /// could print a non-default format and prove the default one. Each lane /// flips its own flag in the commit that makes the lever real. -pub const MERKLE_CAP_IMPLEMENTED: bool = false; +/// +/// The Merkle cap is real on the host and device STARK provers and the host +/// verifier (design/CAP.md C3 + C4). ⚠ NOT yet in the LFM in-guest verifier +/// (C5): a recursion run that wraps a capped proof fails closed there, so +/// `LAMBDA_VM_ZF_CAP` is for STARK-level tests and measurements until C5 +/// lands. +pub const MERKLE_CAP_IMPLEMENTED: bool = true; /// See [`MERKLE_CAP_IMPLEMENTED`]. pub const FRI_MODE_IMPLEMENTED: bool = false; diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 3afea142d..283864088 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -3011,9 +3011,32 @@ pub trait IsStarkProver< }) } + // 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 { - let main_cap = Self::tree_cap(&round_1_result.main.tree, depth, c, "main", |_| None)?; + #[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() @@ -3023,6 +3046,8 @@ pub trait IsStarkProver< "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| { @@ -3036,7 +3061,13 @@ pub trait IsStarkProver< )?; } if let Some(aux) = round_1_result.aux.as_ref() { - let cap = Self::tree_cap(&aux.tree, depth, c, "aux", |_| None)?; + #[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() @@ -3046,12 +3077,18 @@ pub trait IsStarkProver< "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", - |_| None, + comp_dev, )?; embed( deep_poly_openings @@ -3069,7 +3106,15 @@ pub trait IsStarkProver< continue; } let what = format!("FRI layer {i}"); - let cap = Self::tree_cap(&layer.merkle_tree, depth, c, &what, |_| None)?; + // 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() diff --git a/crypto/stark/src/tests/merkle_cap_tests.rs b/crypto/stark/src/tests/merkle_cap_tests.rs index c9825607a..3d14a0ea9 100644 --- a/crypto/stark/src/tests/merkle_cap_tests.rs +++ b/crypto/stark/src/tests/merkle_cap_tests.rs @@ -705,3 +705,138 @@ fn a_device_resident_tree_without_a_cap_read_is_an_error() { .expect("a full host tree serves its cap"); assert!(verify_cap::(&cap, &host.root, 2)); } + +/// C4 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() { + use crate::examples::read_only_memory_logup::{ + LogReadOnlyPublicInputs, LogReadOnlyRAP, read_only_logup_trace, + }; + use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField as E; + type Pi = LogReadOnlyPublicInputs; + + let rows = 1usize << 14; + let addresses: Vec = (0..rows as u64) + .map(|i| FE::from((i * 7919) % 4099 + 1)) + .collect(); + let values: Vec = addresses.iter().map(|a| *a * FE::from(10u64)).collect(); + let prove_at = |policy| { + let opts = options(policy, 30, 2); + let mut trace = read_only_logup_trace::(addresses.clone(), values.clone()); + let cols = trace.columns_main(); + let pi = Pi { + 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(&opts); + let proof = Prover::prove(&air, &mut trace, &pi, &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/prover/src/zf_format.rs b/prover/src/zf_format.rs index 10e6b2f7e..817712269 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -429,6 +429,23 @@ mod tests { ); } + #[test] + fn the_merkle_cap_knob_is_selectable() { + // C3 + C4 made the STARK cap real, so `LAMBDA_VM_ZF_CAP` no longer + // aborts; 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 apply_stamps_only_the_format_fields() { let base = crate::GoldilocksCubicProofOptions::with_blowup(4).unwrap(); From e6ea35953f10143337d664943aac5af1ae34d889 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 18:05:52 -0300 Subject: [PATCH 19/73] test(prover): a real VM proof under the process cap policy prover/tests/merkle_cap_vm.rs proves an ELF through prove_with_options_and_inputs with the options the process format names (ZfFormat::from_env, so LAMBDA_VM_ZF_CAP), verifies it under the same options, and checks the default-format verifier refuses it (Ok(false) or Err, never a panic or an accept). Every production table is capped: preprocessed precomputed + main trees, LogUp aux trees, composition trees, FRI layers. CPU fixture all_instructions_64; under cuda fib_iterative_1M, whose tables commit on the device, and the caps must come off the resident trees (gpu_cap_read_calls moves). Knob-on only: #[ignore], and it refuses to run with the cap off. Box only (it proves a real trace). --- prover/tests/merkle_cap_vm.rs | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 prover/tests/merkle_cap_vm.rs diff --git a/prover/tests/merkle_cap_vm.rs b/prover/tests/merkle_cap_vm.rs new file mode 100644 index 000000000..577531b4d --- /dev/null +++ b/prover/tests/merkle_cap_vm.rs @@ -0,0 +1,76 @@ +//! A real VM proof under the Merkle cap policy the PROCESS FORMAT names +//! (`LAMBDA_VM_ZF_CAP`, design/CAP.md §4): 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, +}; + +/// 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" + ); + // The cap height is a verifier constant: the default verifier must refuse + // the capped proof (full-length paths expected), without panicking. + assert!( + !matches!( + verify_with_options(&proof, &elf, &default, None, None), + Ok(true) + ), + "a capped proof accepted by the default-format verifier" + ); +} From 713e18b350def72d8b644b2de25f517f2908f208 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 18:07:53 -0300 Subject: [PATCH 20/73] feat(lfm): the WHIR Merkle cap in the in-guest chain verifier and its cost model (W1, C8) The level-0 WHIR wrap now verifies capped chains (design/CAP.md 6.2). Everything is derived from ChainShape.caps = ChainConfig::tree_caps, the same heights the host prover and verifier use; at the default every height is 0 and the emitted program, the arena and every pin are today's (no new arena, no new word, the root path instruction for instruction). - whir_open: CapCells, whose only constructor authenticate() hashes the hinted cap to its root (2^c - 1 compressions) and asserts it equals the tree's root lanes, once per tree. TreeAuth { Root, Cap }: every opening goes through TreeAuth::verify_opening with the WHOLE index; it walks the low bits and a private mux (2^c - 1 Selects, pairs (2t, 2t+1), low bit first) consumes exactly the top c, then compares two variable cells. So the cap the mux reads is the cap the root check read (REVIEW-CAP (e)), and no caller splits the index for the mux ((d), S1 in its WHIR form). Closed forms: verify_opening_{rows,perms}_capped, cap_check_{rows,perms}. - whir_chain: ChainShape.caps, current_path (the sibling count; current_depth stays the index-bit count, the two meanings the map flagged). Tree 0's cap is authenticated at the top of emit_verify_weighted, each successor's where its root is unpacked, and carried to the next round with it. Arena: tree 0's 2^c words right after round 0's nonces, tree r+1's right after round r's successor root and ood value, paths depth - c (round_words, RoundStorage::hint, push_round_words split the owner path). Cost model: chain_opening_perms carries depth - c per opening plus chain_cap_perms; chain_query_rows the capped opening rows; chain_fixed_rows the per-tree cap checks. Hints stay arena words (the chain's plumbing). Pins that move only with the knob on (all default pins unchanged): production chain S=25 k=4 Q=112 grind=20 at Auto, caps [3,3,3,3,3,3,2]: opening perms 22,512 -> 18,413 (-4,144 + 45), perms 22,828 -> 18,729, shape rows 184,673 -> 187,245, rows 185,509 -> 188,081 (hand-derived in the test doc, then run). Emitted at the production shape (ignored, laptop-safe): 188,081 rows / 18,729 perms == the forms; 37,968 Select (+5,152 a chain). PREPARED_LEG_ROWS stays fixed (RULINGS 4): under Auto it is within 2% of the 24-variable chain and still covers the 20-variable stack (tested). Tests (laptop): capped chains execute on host-accepted proofs at Fixed(1), Fixed(2), Auto, Q 3 and 25, one and three rounds; emitted rows and perms == the forms at Fixed(2), Fixed(3), Auto; the host transcript schedule is unchanged under the cap; tamper: an UNREACHED tree-0 cap node (positions from the host's own draws: only the cap-to-root check can refuse it), a reached one, and a successor's cap node, each rejected by the host and with no execution; the cap mux selects every index (all 64 leaves of a depth-6 tree, c = 1..3) and refuses the right leaf claimed in another subtree; an unreached tampered cap word cannot execute; capped opening and cap check closed forms at every height of a depth-6 tree. --- prover/src/lfm/whir_chain.rs | 248 +++++++++++++++++++------ prover/src/lfm/whir_chain_tests.rs | 288 ++++++++++++++++++++++++++++- prover/src/lfm/whir_open.rs | 163 +++++++++++++++- prover/src/lfm/whir_open_tests.rs | 233 ++++++++++++++++++++++- 4 files changed, 863 insertions(+), 69 deletions(-) diff --git a/prover/src/lfm/whir_chain.rs b/prover/src/lfm/whir_chain.rs index 6155d4ac0..98b25958f 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,25 @@ 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, 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 +719,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 +754,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 +856,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 +883,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 +904,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 +943,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 +964,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 +979,8 @@ impl RoundStorage { ood: ood_nonce, query, }); + current_caps.push(current_cap); + next_caps.push(next_cap); } assert_eq!( at - base, @@ -901,6 +996,8 @@ impl RoundStorage { roots, oods, nonces, + current_caps, + next_caps, } } @@ -951,6 +1048,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 +1058,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 +1103,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 +1114,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 5000399d5..1733dd656 100644 --- a/prover/src/lfm/whir_chain_tests.rs +++ b/prover/src/lfm/whir_chain_tests.rs @@ -32,7 +32,7 @@ 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, ChainProof, GrindBits, RoundOpenings, commit, prove, verify, }; use multilinear::whir_hash::RpxWhir; @@ -44,9 +44,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,12 +260,20 @@ 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::DEFAULT, + format: multilinear::whir_chain::ChainFormat { + cap, + ..multilinear::whir_chain::ChainFormat::DEFAULT + }, } } @@ -292,7 +300,12 @@ 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(num_vars, num_queries, grind, CapPolicy::Off) +} + +/// [`fixture`] under a Merkle cap policy (W1). +fn fixture_with(num_vars: usize, num_queries: usize, grind: u8, cap: CapPolicy) -> Fixture { + let cfg = config_with(num_queries, grind, cap); let f = pseudo_mle(num_vars, 11); let z = point(num_vars, 0); // `evaluate_in`, not `evaluate`: the claimed point is in the cubic @@ -1395,3 +1408,266 @@ 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_with(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_with(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 — REVIEW-CAP M1(b)), +/// 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_with(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 (design/CAP.md §10): 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)); +} + +/// ⛔ RULINGS 4: `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" + ); +} diff --git a/prover/src/lfm/whir_open.rs b/prover/src/lfm/whir_open.rs index 248d04efb..39d2a6789 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,117 @@ 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 (W1, design/CAP.md §6.2, §9.2). +/// +/// The ONLY constructor, [`CapCells::authenticate`], hashes the hinted cap up +/// to its root and asserts that root equals the tree's root lanes. Every +/// opening of the tree then reads THESE cells through +/// [`TreeAuth::verify_opening`] — so the cells checked against the root and +/// the cells the mux picks from are the same cells, and a tree has one cap +/// (REVIEW-CAP (e)). +/// +/// The mux is private to this module and consumes exactly the top `c` of the +/// index bits it is handed, the rest being walked (REVIEW-CAP (d)): a caller +/// passes the whole index, never a split of it. +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 ([`TreeAuth::Root`]). + 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, std::slice::from_ref(root_lanes)); + Self { + cap: cap.to_vec(), + height: cap.len().trailing_zeros() as usize, + } + } + + pub fn height(&self) -> usize { + self.height + } + + /// `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] + } +} + +/// 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 (walk_bits, top_bits) = index_bits.split_at(siblings.len()); + let leaf = emit_block_leaf(b, values); + let walked = edsl::wrap_merkle_walk(b, leaf, walk_bits, siblings); + let node = cap.select(b, top_bits); + for (x, y) in walked.iter().zip(node.iter()) { + edsl::assert_word_eq(b, *x, *y); + } + } + } + } +} diff --git a/prover/src/lfm/whir_open_tests.rs b/prover/src/lfm/whir_open_tests.rs index 7ac5a7138..927c5d233 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}; @@ -530,3 +532,232 @@ 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, &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" + ); + } +} + +/// ★ REVIEW-CAP M1(b) 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)); +} From d281c3b867f966dbb92f14a47b96ddc8ec86c92b Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 18:07:53 -0300 Subject: [PATCH 21/73] feat(multilinear): W1 is implemented; LAMBDA_VM_ZF_WHIR_CAP is selectable WHIR_CAP_IMPLEMENTED flips to true now that the cap is in the host prover and verifier (C6), on the device (C7) and in the in-guest verifier and its cost model (C8). ZfFormat no longer aborts on LAMBDA_VM_ZF_WHIR_CAP=auto or a fixed height; the default (off) is unchanged. A zf_format test pins that the knob is selectable. --- crypto/multilinear/src/whir_chain.rs | 6 +++++- prover/src/zf_format.rs | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/crypto/multilinear/src/whir_chain.rs b/crypto/multilinear/src/whir_chain.rs index 83eb3b750..f7855a4a4 100644 --- a/crypto/multilinear/src/whir_chain.rs +++ b/crypto/multilinear/src/whir_chain.rs @@ -226,7 +226,11 @@ impl ChainFormat { /// parsed must not be selectable (see `stark::proof::options:: /// MERKLE_CAP_IMPLEMENTED`). Each lane flips its own flag in the commit that /// makes the lever real. -pub const WHIR_CAP_IMPLEMENTED: bool = false; +/// +/// 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 longest explicit fold list [`WhirFolds::List`] holds. pub const MAX_FOLD_ROUNDS: usize = 32; diff --git a/prover/src/zf_format.rs b/prover/src/zf_format.rs index 10e6b2f7e..f2d1b69ce 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -429,6 +429,18 @@ mod tests { ); } + #[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 apply_stamps_only_the_format_fields() { let base = crate::GoldilocksCubicProofOptions::with_blowup(4).unwrap(); From 5a61661573520693646c588cc8cdb7697e3dc5b0 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 18:14:01 -0300 Subject: [PATCH 22/73] feat(stark/fri): S3 group-leaf FRI layers on the CPU prover and host verifier (H2) Under LAMBDA_VM_ZF_FRI=dp (ProofFormat.fri_mode = Dp) committed FRI layer j folds by 2^{d_j}, d_j from the verifier-side schedule DP (FRI.md 1-3, with REVIEW-FRI F5/F6 applied). The legacy format (fri = pair) runs today's code, byte for byte: the H0 goldens are unchanged. Prover (fri/mod.rs): commit_phase_with_layout. Per committed layer: sample zeta, fold d_{j-1} times with zeta, zeta^2, ... (d_{-1} = 1: fold 0 is the binary fold of the DEEP pair; F6's fold-count fix), commit the result, append the root; the final zeta folds d_last times into the terminal. The fold is the unchanged binary fold. Group trees hash each 2^d-value group with H::Batched and build parents with H::Pair, as today's layer trees (built with Pair, verified with Batched). query_phase_with_layout opens the full group (the query's own value included, FRI.md 3.4) and the path of leaf p >> d. Proof structs are unchanged: the flat layers_evaluations_sym carries every layer's group under a non-legacy format (its length a verifier constant). Verifier: fri_termination_params builds the layout from the AIR's options (never the proof); a format it cannot lay out is rejected. The group checks live in fri::group::verify_query_groups: per layer the group is hashed in full and authenticated at the exact depth, the slot check group[p & (2^d - 1)] == v, and the group fold (d binary levels on the fiber, x_g^-1 from the query point and the slot). The structural check pins the value count per query before any loop. The legacy/group encoding is decided by the format, not the schedule's values (F5's per-table predicate reduces to the format until S2). Device: every device FRI arm (DEEP-to-FRI on device, the device commit, the device query gather) runs only for the legacy encoding; a dp table takes the CPU FRI loop (DEEP may still run on the device, its values are format-independent). One-row modes are refused (Err), not proved. Round 4 now returns Result: an unsupported format is a ProvingError. Tests (tests::fri_group_tests, prover tests::zf_rpx_golden_tests): - U4 group_fold_equals_d_binary_folds (d = 1..6, every group and slot, and 2^d * sum zeta^i f_i from the polynomial); - U5 group_leaf_is_a_coset (b <= 10); - U6 round trips at dp: every fold count 0..9 at blowup 2 and 4; explicit schedules [1,3,3] [3,1,3] [2,1,2,2] [1]*7 [6,1] [1,6] [4,3]; ext3 with aux; a multi-table bus proof; Keccak, Blake3 and RPX; a non-covering override is a proving error; - the format is a verifier constant (dp proof rejected under pair and vice versa); - F1.2 generic_path_at_all_ones_equals_legacy (Keccak and RPX): same roots, terminal, openings, paths; each group is the legacy pair; - T1-T3: every group value of a query (slot and non-slot), a path sibling, a root, values one short / long, a short path, a missing layer; - M1 the slot check and M2 the group authentication are load-bearing: a p0 + c FRI forgery / a foreign root is ACCEPTED with the check switched off (test-only thread-local mutation) and rejected with it. Shown able to fail: folding d_j instead of d_{j-1} (F6's bug) turns 8 S3 tests red while the goldens and the all-ones differential stay green. --- crypto/stark/src/fri/group.rs | 224 ++++++++ crypto/stark/src/fri/mod.rs | 195 ++++++- crypto/stark/src/fri/terminal.rs | 4 +- crypto/stark/src/prover.rs | 81 +-- crypto/stark/src/tests/fri_group_tests.rs | 606 ++++++++++++++++++++++ crypto/stark/src/tests/mod.rs | 1 + crypto/stark/src/tests/zf_golden_tests.rs | 19 +- crypto/stark/src/verifier.rs | 118 ++++- prover/src/tests/zf_rpx_golden_tests.rs | 69 +++ 9 files changed, 1247 insertions(+), 70 deletions(-) create mode 100644 crypto/stark/src/fri/group.rs create mode 100644 crypto/stark/src/tests/fri_group_tests.rs diff --git a/crypto/stark/src/fri/group.rs b/crypto/stark/src/fri/group.rs new file mode 100644 index 000000000..f00cce848 --- /dev/null +++ b/crypto/stark/src/fri/group.rs @@ -0,0 +1,224 @@ +//! Group-leaf FRI layers (S3): a committed layer of fold exponent `d` groups +//! `2^d` consecutive bit-reversed evaluations per leaf (FRI.md §1). +//! +//! # 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::cap::CappedRoot; +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; + +/// 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` against `roots[j]` (path `paths(j)`, exact depth), the +/// slot check `group[p & (2^{d_j} − 1)] == v` holds, and `v` becomes the group +/// fold with `zetas[j + 1]`; finally `terminal[p] == v`. +/// +/// * `v` / `y_inv`: the query's value at committed layer 0 and the inverse of +/// its point there (fold 0 already applied by the caller); +/// * `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, + lde_log: u32, + roots: &[Commitment], + 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 roots.len() != layout.num_committed + || values.len() != layout.opened_values_per_query() + || zetas.len() != layout.num_committed + 1 + { + return false; + } + 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, &[]); + let depth = layout.layer_depth(lde_log, j) as usize; + if !CappedRoot::uncapped(&roots[j], depth).verify::(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 + 1], &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 e6af9f024..219328308 100644 --- a/crypto/stark/src/fri/mod.rs +++ b/crypto/stark/src/fri/mod.rs @@ -1,6 +1,7 @@ pub mod fri_commitment; pub mod fri_decommit; pub(crate) mod fri_functions; +pub(crate) mod group; pub mod schedule; pub(crate) mod terminal; @@ -11,10 +12,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,6 +43,62 @@ pub fn commit_phase_from_evaluations< 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, + 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). +/// +/// 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`). +/// +/// Every device FRI arm is taken only for the legacy encoding: a group-encoded +/// layout always runs this CPU loop. +#[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, >( mut evals: Vec>, transcript: &mut T, @@ -46,6 +106,7 @@ pub fn commit_phase_from_evaluations< domain_size: usize, blowup_log: u32, final_poly_log_degree: u32, + layout: &FriFoldLayout, inv_twiddles: &[FieldElement], ) -> (Vec>, Vec>>) where @@ -60,7 +121,7 @@ where // error restores state and lets the CPU loop below run as if the GPU // had never been tried. #[cfg(feature = "cuda")] - { + if layout.is_legacy() { // 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 @@ -84,12 +145,16 @@ where // 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() ); + // One-row layouts (S2) commit the DEEP codeword itself as layer 0; they are + // refused before a layout is built (`FriFormat::from_options`). + debug_assert!(!layout.one_row, "one-row FRI layouts are not implemented"); let num_committed = layout.num_committed; // Inverse twiddle factors for evaluation-form folding: per-layer working @@ -97,37 +162,44 @@ 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`. + let mut pending: u32 = 1; + // Commit `num_committed` folded layers to the transcript. - for _ in 0..num_committed { + for &d in &layout.schedule { // <<<< 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). 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(), @@ -158,6 +230,43 @@ 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]`. +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. /// @@ -219,3 +328,47 @@ 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, FRI.md §3.4) and the path of +/// `leaf = p >> d_j`, then moves to `p >> d_j`. Host layers only: a group +/// layout never takes the device commit. +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); + } + 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/terminal.rs b/crypto/stark/src/fri/terminal.rs index 346d32af8..e2703a6ed 100644 --- a/crypto/stark/src/fri/terminal.rs +++ b/crypto/stark/src/fri/terminal.rs @@ -52,9 +52,6 @@ pub(crate) struct FriFoldLayout { pub(crate) legacy_encoding: bool, } -// The format-aware constructors' first callers are the S3 prover and verifier -// (the next commit); until then only the tests use them. -#[allow(dead_code)] impl FriFoldLayout { /// Today's layout, derived from the LDE codeword size. /// @@ -120,6 +117,7 @@ impl FriFoldLayout { /// 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, diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index a457f0995..16a841f21 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -2755,11 +2755,22 @@ 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 fri_layout = crate::fri::terminal::FriFoldLayout::for_options( + domain.lde_roots_of_unity_coset.len().trailing_zeros(), + domain.blowup_factor.trailing_zeros(), + air.options(), + ) + .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 +2811,39 @@ pub trait IsStarkProver< let __ps_df = crate::prove_split::mark(); #[cfg(feature = "instruments")] let t_sub = Instant::now(); + // Device FRI implements the legacy encoding only: any other format + // takes the host arm below (which may still compute DEEP on device). #[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 = if !fri_layout.is_legacy() { + None + } else { + 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(), + ) + }) + }; #[cfg(not(feature = "cuda"))] #[allow(clippy::type_complexity)] let precomputed_fri: Option<( @@ -2875,13 +2892,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")] @@ -2922,7 +2940,8 @@ pub trait IsStarkProver< let number_of_queries = air.options().fri_number_of_queries; let iotas = Self::sample_query_indexes(number_of_queries, domain, transcript); - let query_list = fri::query_phase::(&fri_layers, &iotas); + let query_list = + fri::query_phase_with_layout::(&fri_layers, &iotas, &fri_layout); let fri_layers_merkle_roots: Vec<_> = fri_layers .iter() @@ -2939,13 +2958,13 @@ 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, - } + }) } fn sample_query_indexes( @@ -5314,7 +5333,7 @@ pub trait IsStarkProver< &round_3_result, &z, transcript, - ); + )?; #[cfg(feature = "instruments")] { 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..247206623 --- /dev/null +++ b/crypto/stark/src/tests/fri_group_tests.rs @@ -0,0 +1,606 @@ +//! S3 (group-leaf FRI layers) on the CPU prover and host verifier: FRI.md §10 +//! 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 (REVIEW-FRI F1.2). + +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::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(); + 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, + run.lde_log, + &run.roots, + |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).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).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 + )); +} + +// --------------------------------------------------------------------------- +// F1.2: 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/mod.rs b/crypto/stark/src/tests/mod.rs index 556b25ea8..fbe1f06e6 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -7,6 +7,7 @@ pub mod bus_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; diff --git a/crypto/stark/src/tests/zf_golden_tests.rs b/crypto/stark/src/tests/zf_golden_tests.rs index 0d11ea857..c5200bae6 100644 --- a/crypto/stark/src/tests/zf_golden_tests.rs +++ b/crypto/stark/src/tests/zf_golden_tests.rs @@ -105,11 +105,13 @@ macro_rules! fingerprint { ($proof:expr) => {{ let proof = $proof; let rk = |bytes: Result| { - sha3_hex(&bytes.expect("rkyv")) + $crate::tests::zf_golden_tests::sha3_hex(&bytes.expect("rkyv")) }; - Fingerprint { + $crate::tests::zf_golden_tests::Fingerprint { proof: rk(rkyv::to_bytes::(proof)), - fri_roots: sha3_hex(&proof.fri_layers_merkle_roots.concat()), + 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, @@ -121,7 +123,6 @@ macro_rules! fingerprint { } }}; } -#[allow(unused_imports)] // for the prover-free S3 tests in this crate pub(crate) use fingerprint; // --------------------------------------------------------------------------- @@ -169,14 +170,14 @@ fn logup_reads(rows: usize) -> (Vec, Vec) { } /// `LogReadOnlyRAP` (E = F³, one aux column) under hash `H`. -pub(crate) fn prove_logup( - rows: usize, - options: &ProofOptions, -) -> ( +/// 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(); diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index ad093e182..65ca3322b 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -458,16 +458,25 @@ 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(), + ) + .ok() } /// Reconstructs the Deep composition polynomial evaluations at the challenge indices values using the provided @@ -507,7 +516,9 @@ pub trait IsStarkVerifier< // 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 Some(layout) = Self::fri_termination_params(air, domain) else { + return false; + }; let num_committed = layout.num_committed; // Structural check: number of committed FRI layers must equal @@ -528,10 +539,13 @@ 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; } @@ -559,6 +573,40 @@ pub trait IsStarkVerifier< 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( + proof, + &layout, + &challenges.zetas, + challenges.iotas[i], + proof.query(i), + eval, + &deep_poly_evaluations[i], + &deep_poly_evaluations_sym[i], + &terminal_codeword, + lde_log as u32, + &roots_tables, + ) + }); + } + (0..challenges.iotas.len()) .zip(evaluation_point_inverse) .all(|(i, eval)| { @@ -778,6 +826,56 @@ pub trait IsStarkVerifier< ) } + /// Verify a single FRI query under the group encoding (S3; any format but + /// the legacy one): fold 0 from the DEEP pair as today, then + /// [`crate::fri::group::verify_query_groups`] for the committed layers and + /// the terminal check. The zero-fold case is the legacy one (no layer, no + /// challenge). + // Crate-internal layout type on a default method, as `fri_termination_params`. + #[allow(clippy::too_many_arguments, private_interfaces)] + fn verify_query_groups( + proof: StarkProofView<'_, Field, FieldExtension, PI>, + layout: &crate::fri::terminal::FriFoldLayout, + zetas: &[FieldElement], + iota: usize, + fri_decommitment: FriDecommitmentView<'_, FieldExtension>, + evaluation_point_inv: FieldElement, + p0_eval: &FieldElement, + p0_eval_sym: &FieldElement, + terminal_codeword: &[FieldElement], + lde_log: u32, + roots_tables: &[Vec>], + ) -> bool + where + FieldElement: AsBytes + Sync + Send, + FieldElement: AsBytes + Sync + Send, + { + 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); + crate::fri::group::verify_query_groups::>( + layout, + lde_log, + proof.fri_layers_merkle_roots(), + |j| fri_decommitment.layer_auth_path(j), + fri_decommitment.layers_evaluations_sym(), + zetas, + iota, + v, + evaluation_point_inv.square(), + terminal_codeword, + roots_tables, + ) + } + /// Verify a single FRI query /// `zetas`: the vector of all challenges sent by the verifier to the prover at the commit /// phase to fold polynomials. @@ -1642,7 +1740,15 @@ pub trait IsStarkVerifier< // 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 { diff --git a/prover/src/tests/zf_rpx_golden_tests.rs b/prover/src/tests/zf_rpx_golden_tests.rs index 0c0fd6f54..bd8825067 100644 --- a/prover/src/tests/zf_rpx_golden_tests.rs +++ b/prover/src/tests/zf_rpx_golden_tests.rs @@ -212,3 +212,72 @@ fn print_goldens() { println!("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::DEFAULT + } +} + +#[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)); + } +} + +/// REVIEW-FRI F1.2 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::DEFAULT)).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]) + ); + } + } +} From 1696bbbb6b9a79f2b82c5651fecbd71042100c24 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 18:24:34 -0300 Subject: [PATCH 23/73] test(stark,prover): the S3 test vectors, and a VM proof at fri = dp (H3) FRI.md 10, "Vectors the host lane exports" (a)-(d), checked in under crypto/stark/tests/vectors/zf_fri/ with a README (conventions: field and limbs, bit-reversed coset layers, the binary and group folds, group-leaf hashing, query/leaf/slot arithmetic, transcript, proof encoding): (a) a_schedules.json: the DP's schedules and cost-law costs, T in {4, 9, 10}, Q in {3, 110}, cap off/auto, B = 6..24, S3 and S2 chains; (b) b_group_folds.json: a SplitMix64 KAT codeword (2^7 ext3 values, the generator documented) folded d = 1..6 times; the generator asserts the verifier's group fold of every group reproduces the prover's; (c) c_leaf_digests_{keccak,blake3,rpx}.json: the first group's leaf digest and the whole group-leaf layer root, d = 1..6; (d) d_proof_{keccak,blake3,rpx}_{pair,dp,dp_3_1_3}.{rkyv,json}: a LogReadOnlyRAP proof (B = 12, blowup 4, k = 2, Q = 3, grinding 0) per format, with the layout, roots, every zeta, the terminal coefficients, and per query iota, the DEEP pair and per layer the position, leaf, slot, opened values and path length. The generators live in stark::fri::vectors (test / test-utils only, so the prover crate generates the RPX files with the same code). zeta, iota and the DEEP values come from the host verifier itself, through a test-only thread-local capture (stark::fri::capture). The tests zf_fri_vectors::vectors_are_current (stark) and tests::zf_rpx_vectors::rpx_vectors_are_current (prover) regenerate every file in memory and require it byte-equal to the checked-in copy. prover tests::zf_vm_dp_tests::a_vm_proof_round_trips_at_fri_dp: a real multi-table VM proof (test_mul_8, the preprocessed tables included, RPX, CPU FRI) proved and host-verified at fri = dp, rejected by the default-format verifier and after a group value is tampered. It builds a full VM trace, so it is a box test (lib suite), not run on the laptop. The ZF FORMAT banner's fri field (fri=pair|dp) already exists (C2). --- crypto/stark/src/fri/capture.rs | 75 +++ crypto/stark/src/fri/mod.rs | 9 +- crypto/stark/src/fri/vectors.rs | 431 ++++++++++++++++ crypto/stark/src/tests/mod.rs | 1 + crypto/stark/src/tests/zf_fri_vectors.rs | 40 ++ crypto/stark/src/verifier.rs | 4 + crypto/stark/tests/vectors/zf_fri/README.md | 100 ++++ .../tests/vectors/zf_fri/a_schedules.json | 463 ++++++++++++++++++ .../tests/vectors/zf_fri/b_group_folds.json | 14 + .../vectors/zf_fri/c_leaf_digests_blake3.json | 13 + .../vectors/zf_fri/c_leaf_digests_keccak.json | 13 + .../vectors/zf_fri/c_leaf_digests_rpx.json | 13 + .../vectors/zf_fri/d_proof_blake3_dp.json | 27 + .../vectors/zf_fri/d_proof_blake3_dp.rkyv | Bin 0 -> 8488 bytes .../zf_fri/d_proof_blake3_dp_3_1_3.json | 27 + .../zf_fri/d_proof_blake3_dp_3_1_3.rkyv | Bin 0 -> 8728 bytes .../vectors/zf_fri/d_proof_blake3_pair.json | 27 + .../vectors/zf_fri/d_proof_blake3_pair.rkyv | Bin 0 -> 11136 bytes .../vectors/zf_fri/d_proof_keccak_dp.json | 27 + .../vectors/zf_fri/d_proof_keccak_dp.rkyv | Bin 0 -> 8488 bytes .../zf_fri/d_proof_keccak_dp_3_1_3.json | 27 + .../zf_fri/d_proof_keccak_dp_3_1_3.rkyv | Bin 0 -> 8728 bytes .../vectors/zf_fri/d_proof_keccak_pair.json | 27 + .../vectors/zf_fri/d_proof_keccak_pair.rkyv | Bin 0 -> 11136 bytes .../tests/vectors/zf_fri/d_proof_rpx_dp.json | 27 + .../tests/vectors/zf_fri/d_proof_rpx_dp.rkyv | Bin 0 -> 8488 bytes .../vectors/zf_fri/d_proof_rpx_dp_3_1_3.json | 27 + .../vectors/zf_fri/d_proof_rpx_dp_3_1_3.rkyv | Bin 0 -> 8728 bytes .../vectors/zf_fri/d_proof_rpx_pair.json | 27 + .../vectors/zf_fri/d_proof_rpx_pair.rkyv | Bin 0 -> 11136 bytes prover/src/tests/mod.rs | 4 + prover/src/tests/zf_rpx_vectors.rs | 34 ++ prover/src/tests/zf_vm_dp_tests.rs | 59 +++ 33 files changed, 1515 insertions(+), 1 deletion(-) create mode 100644 crypto/stark/src/fri/capture.rs create mode 100644 crypto/stark/src/fri/vectors.rs create mode 100644 crypto/stark/src/tests/zf_fri_vectors.rs create mode 100644 crypto/stark/tests/vectors/zf_fri/README.md create mode 100644 crypto/stark/tests/vectors/zf_fri/a_schedules.json create mode 100644 crypto/stark/tests/vectors/zf_fri/b_group_folds.json create mode 100644 crypto/stark/tests/vectors/zf_fri/c_leaf_digests_blake3.json create mode 100644 crypto/stark/tests/vectors/zf_fri/c_leaf_digests_keccak.json create mode 100644 crypto/stark/tests/vectors/zf_fri/c_leaf_digests_rpx.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp_3_1_3.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_blake3_dp_3_1_3.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_blake3_pair.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_blake3_pair.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp_3_1_3.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_keccak_dp_3_1_3.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_keccak_pair.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_keccak_pair.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp_3_1_3.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_rpx_dp_3_1_3.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_rpx_pair.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_rpx_pair.rkyv create mode 100644 prover/src/tests/zf_rpx_vectors.rs create mode 100644 prover/src/tests/zf_vm_dp_tests.rs diff --git a/crypto/stark/src/fri/capture.rs b/crypto/stark/src/fri/capture.rs new file mode 100644 index 000000000..5ba960f38 --- /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`, FRI.md §10 (d)): a vector +//! carries a proof AND the ζ, ι and DEEP values a correct verifier derives +//! from it, so the device and in-guest lanes 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/mod.rs b/crypto/stark/src/fri/mod.rs index 219328308..05af45940 100644 --- a/crypto/stark/src/fri/mod.rs +++ b/crypto/stark/src/fri/mod.rs @@ -1,9 +1,13 @@ +#[cfg(any(test, feature = "test-utils"))] +pub mod capture; 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; @@ -250,7 +254,10 @@ pub(crate) fn fold_times, E: IsField>( } /// A group-leaf layer tree: leaf `g` = `H::Batched` over `evals[g·n .. (g+1)·n]`. -fn group_tree(evals: &[FieldElement], n: usize) -> Option>> +pub(crate) fn group_tree( + evals: &[FieldElement], + n: usize, +) -> Option>> where E: IsField + 'static + Send + Sync, FieldElement: AsBytes + Sync + Send, diff --git a/crypto/stark/src/fri/vectors.rs b/crypto/stark/src/fri/vectors.rs new file mode 100644 index 000000000..1802ca384 --- /dev/null +++ b/crypto/stark/src/fri/vectors.rs @@ -0,0 +1,431 @@ +//! The S3 test vectors the host lane exports (FRI.md §10, "Vectors the host +//! lane exports" (a)–(d)) for the device and in-guest lanes, 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 lane 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. +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\": {}}},", + w.cap.compress, w.cap.select, w.cap.unpack, w.cap.hint, w.cap.compare, w.fold, w.twiddle + ); + 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, Q = 3, grinding 0, coset offset 3. +pub const PROOF_ROWS: usize = 1 << 10; + +pub fn proof_options(format: ProofFormat) -> ProofOptions { + ProofOptions { + blowup_factor: 4, + fri_number_of_queries: 3, + coset_offset: 3, + grinding_factor: 0, + fri_final_poly_log_degree: 2, + format, + } +} + +/// The formats of (d): `pair` (today), `dp` (the DP's schedule) and +/// `dp_3_1_3` (an explicit uneven schedule, to catch fold-count bugs). +pub fn proof_formats() -> Vec<(&'static str, ProofFormat)> { + let dp = ProofFormat { + fri_mode: FriMode::Dp, + ..ProofFormat::DEFAULT + }; + vec![ + ("pair", ProofFormat::DEFAULT), + ("dp", dp), + ( + "dp_3_1_3", + ProofFormat { + fri_schedule_override: FriScheduleOverride::new(&[3, 1, 3]), + ..dp + }, + ), + ] +} + +fn logup_case( + format: ProofFormat, +) -> ( + 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)), + 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) in proof_formats() { + let (air, mut trace, pi) = logup_case(format); + 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 (d) 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()).expect("layout"); + let stem = format!("d_proof_{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\": 3,\n \"grinding_factor\": 0,\n \"coset_offset\": 3," + ); + 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 + }; + } + 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/tests/mod.rs b/crypto/stark/src/tests/mod.rs index fbe1f06e6..9fd672ef4 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -23,4 +23,5 @@ pub mod small_trace_tests; pub mod table_disk_spill_tests; pub mod terminal_tests; pub mod trace_test_helpers; +pub mod zf_fri_vectors; pub mod zf_golden_tests; 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..d3d01f05f --- /dev/null +++ b/crypto/stark/src/tests/zf_fri_vectors.rs @@ -0,0 +1,40 @@ +//! The exported S3 vectors (FRI.md §10 (a)–(d)) 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, 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")); + v +} + +#[test] +fn vectors_are_current() { + let files = all(); + assert_eq!(files.len(), 4 + 2 * 3 * 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/verifier.rs b/crypto/stark/src/verifier.rs index 65ca3322b..d93891336 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -511,6 +511,8 @@ pub trait IsStarkVerifier< Some(pair) => pair, None => return false, }; + #[cfg(any(test, feature = "test-utils"))] + crate::fri::capture::record_deep(&deep_poly_evaluations, &deep_poly_evaluations_sym); // ---- Reconstruct the FRI terminal codeword from the final-poly coeffs ---- // The prover folds the deep composition codeword down to a terminal @@ -1843,6 +1845,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; 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..4cbde2e1b --- /dev/null +++ b/crypto/stark/tests/vectors/zf_fri/README.md @@ -0,0 +1,100 @@ +# S3 FRI vectors (group-leaf FRI layers) + +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 lane (group-leaf commits, multi-fold kernels, query +gathers) and the in-guest lane (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_*` | `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_*` | `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 (RULINGS 13; `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}.{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, 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). + +## Not here yet + +- (e) S2 one-row leaf digests and the input-tree root (H6, after S2). +- A vector with a Merkle cap (`Q ≥ 20` so `cap = auto` caps; REVIEW-FRI F9): + the cap is not implemented on this branch. 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..e6605b3c0 --- /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}, + "dmax": 6, + "rows": [ + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [1], "cost_q_ns": 51531}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 78777}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 78777}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 125085}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 125085}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 174462}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 174462}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 220770}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 220770}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 275532}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 275532}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 333363}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 333363}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 388125}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 388125}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 451341}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 451341}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 517626}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 517626}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 580842}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 580842}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 652512}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 652512}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 727251}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 727251}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 798921}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 798921}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 879045}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 879045}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 962238}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 962238}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1042362}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1042362}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1130940}, + {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1130940}, + {"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": 1222587}, + {"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": 1222587}, + {"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": 1311165}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [1], "cost_q_ns": 51531}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 78777}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 78777}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 125085}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 125085}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 174462}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 174462}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 220770}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 220770}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 275532}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 275532}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 333363}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 333363}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 388125}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 388125}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 451341}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 451341}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 517626}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 517626}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 580842}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 580842}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 652512}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 652512}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 727251}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 727251}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 798921}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 798921}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 879045}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 879045}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 962238}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 962238}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1042362}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1042362}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1130940}, + {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1130940}, + {"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": 1222587}, + {"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": 1222587}, + {"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": 1311165}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [1], "cost_q_ns": 1889470}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 2888490}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 2888490}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 4586450}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 4586450}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 6396940}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 6396940}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 8094900}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 8094900}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 10102840}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 10102840}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 12223310}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 12223310}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 14231250}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 14231250}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 16549170}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 16549170}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 18979620}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 18979620}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 21297540}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 21297540}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 23925440}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 23925440}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 26665870}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 26665870}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 29293770}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 29293770}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 32231650}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 32231650}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 35282060}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 35282060}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 38219940}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 38219940}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 41467800}, + {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 41467800}, + {"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": 44828190}, + {"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": 44828190}, + {"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": 48076050}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [1], "cost_q_ns": 1477426}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 2476446}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 2476446}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 4174406}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 4174406}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 5572852}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 5572852}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 7270812}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 7270812}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 9278752}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 9278752}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 10987178}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 10987178}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 12995118}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 12995118}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 15313038}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 15313038}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 17331444}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 17331444}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 19649364}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 19649364}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 22277264}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 22277264}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 24605650}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 24605650}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 27233550}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 27233550}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 30171430}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 30171430}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 32809796}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 32809796}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 35747676}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 35747676}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 38995536}, + {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 38995536}, + {"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": 41943882}, + {"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": 41943882}, + {"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": 45191742}, + {"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": 93801}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 93801}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 121047}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 121047}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 167355}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 167355}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 259002}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 259002}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 305310}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 305310}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 360072}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 360072}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [4, 3], "cost_q_ns": 458010}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [4, 3], "cost_q_ns": 458010}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 514935}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 514935}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 578151}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 578151}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 676089}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 676089}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 749922}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 749922}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 821592}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 821592}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 919530}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 919530}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1010271}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1010271}, + {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 1090395}, + {"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": 93801}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 93801}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 121047}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 121047}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 167355}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 167355}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 259002}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 259002}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 305310}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 305310}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 360072}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 360072}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [4, 3], "cost_q_ns": 458010}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [4, 3], "cost_q_ns": 458010}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 514935}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 514935}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 578151}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 578151}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 676089}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 676089}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 749922}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 749922}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 821592}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 821592}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 919530}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 919530}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1010271}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1010271}, + {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 1090395}, + {"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": 3439370}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 3439370}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 4438390}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 4438390}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 6136350}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 6136350}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 9496740}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 9496740}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 11194700}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 11194700}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 13202640}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 13202640}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [4, 3], "cost_q_ns": 16793700}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [4, 3], "cost_q_ns": 16793700}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 18880950}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 18880950}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 21198870}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 21198870}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 24789930}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 24789930}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 27497140}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 27497140}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 30125040}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 30125040}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 33716100}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 33716100}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 37043270}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 37043270}, + {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 39981150}, + {"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": 3027326}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 3027326}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 4026346}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 4026346}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 5724306}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 5724306}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 8672652}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 8672652}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 10370612}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 10370612}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 12378552}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 12378552}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 15636878}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 15636878}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 17644818}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 17644818}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 19962738}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 19962738}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 2, 2], "cost_q_ns": 23531044}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 2, 2], "cost_q_ns": 23531044}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 25848964}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 25848964}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 28476864}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 28476864}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 32067924}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 32067924}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 34983050}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 34983050}, + {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 37920930}, + {"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": 102255}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 102255}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 129501}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 129501}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 175809}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 175809}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [4], "cost_q_ns": 273747}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [4], "cost_q_ns": 273747}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 322218}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 322218}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 376980}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 376980}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [4, 3], "cost_q_ns": 474918}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [4, 3], "cost_q_ns": 474918}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 540297}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 540297}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 603513}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 603513}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 701451}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 701451}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 783738}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 783738}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 855408}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 855408}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 953346}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 953346}, + {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1052541}, + {"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": 102255}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 102255}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 129501}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 129501}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 175809}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 175809}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [4], "cost_q_ns": 273747}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [4], "cost_q_ns": 273747}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 322218}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 322218}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 376980}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 376980}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [4, 3], "cost_q_ns": 474918}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [4, 3], "cost_q_ns": 474918}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 540297}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 540297}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 603513}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 603513}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 701451}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 701451}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 783738}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 783738}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 855408}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 855408}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 953346}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 953346}, + {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1052541}, + {"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": 3749350}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 3749350}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 4748370}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 4748370}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 6446330}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 6446330}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [4], "cost_q_ns": 10037390}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [4], "cost_q_ns": 10037390}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 11814660}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 11814660}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 13822600}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 13822600}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [4, 3], "cost_q_ns": 17413660}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [4, 3], "cost_q_ns": 17413660}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 19810890}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 19810890}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 22128810}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 22128810}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 25719870}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 25719870}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 28737060}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 28737060}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 31364960}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 31364960}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 34956020}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 34956020}, + {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 38593170}, + {"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": 3337306}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 3337306}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 4336326}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 4336326}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 6034286}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 6034286}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [2, 2], "cost_q_ns": 9292612}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [2, 2], "cost_q_ns": 9292612}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 10990572}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 10990572}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 12998512}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 12998512}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 2, 2], "cost_q_ns": 16566818}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 2, 2], "cost_q_ns": 16566818}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 18574758}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 18574758}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 20892678}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 20892678}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 24483738}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 24483738}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 27088884}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 27088884}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 29716784}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 29716784}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 33307844}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 33307844}, + {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 36532950} + ] +} 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_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 0000000000000000000000000000000000000000..af336fd71ec41bf6ca778b76387d8c8a91e6fb97 GIT binary patch literal 8488 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

heCoVVI}*h&RDuuH5$! z#*f`T<>0p(G^9)>Sy^DE$nWlQXVxO`%cHVR2||a|!Xdu-fqcKf?uR%smX2M# zCuC)j(fdfn0)*mfl(Pe|o|dW~3F8Zj`+l_jPy4%OBaRc^8Q?hHis_JbVD%&xmu{T_ zBR2WXarw`kBcZ$HMW{&gP;kR+_*93|0Z$}Q)b|kg+r?z?yrUuHn;^amaHY4hYr#9+ z(ny#8M5Z|IyAPEJKAbx)SsI0gydpNw%&`wG1QO0TKkY%txVt)d?QBd+w%}A0$ym*1!b*R#UVSZj)>&Cd zPECj({&dv|gDTXY_-i~-bAI((iJFFuC8}X+l&7Wk7NWLA=>pB~47` zi&MJjRxF7*X*cV6_M0;JYJd)P2|9Z@Y%KLwjaegmePvt4+T!Qjd|R-_|?Zx}*uq#FuU0{9PSuB->wX6|`g{zEJb@W4U_vI-mL>%R#X z>`T6t@WPlcEO!6zc9R!=wPU{IZ?EIoU-;F2j7Dv*oP4)V&P2pIG9eH@*J`rC)*^hl zJ;;~h0Taas@(2kWPSj+Q|uO@c+z+~)Et(y>n zJDR=yvOXdl%j4JDDtdCnc&(YeXK7PhQE=`1-AGjv>yI;P;(!uzu;@j>J?k;ulI z?RXQa9*h#U9MFk4LPMka&6dFGsbJh5jZQox2V5k|Gy&D8RW4VNUh|1+UYsp9XhJ$R zgfB7QtEqgyR(PK`so}=JmhGe=aRIvu&?H3vEQ3}`s*sZ)4;)7}Ww}q;q;&CjI51HE zh5)xI<8R2nkR;B_kRb@Sz%mq5sW9i~VXL|P{YZbe*;!V-r<{%DtTYw_v|W@4 zgCL~WTFgl6kptOKB=+4V?E4^3l&@4E(n>*w)rOCBE+7adTXWjEmJy3r#+a#}UM#?8 zEGlsgNOw;gK2mS2!ApEwy0}r2$(l#L6rrjTb=U#bV6VswqU~3&ZV`i9Q0{3Waxk5@ zO;3|h(G*}F`XVPb7TKi1k>Jp>mhi|Xy0h;CG*K{-M~%C%D7iG`1sn|?uH!@!%X>L` zDZJl@LQtW6P&hR^q|mX-+rZ-86GyXP4~PN|EqsqSQ$!XXo=Bv`BmFcH$lNXm_){p1 zHYjYR!W={eeJGJ>kwNU=<3=l;&ZM{2t(~RjtvWqJ(i?g=_63HN1=xPYw3?5%_CxWb zI3;Yh)~R(h162ldl&>i9?!k>!d(X~MwNBiLvom-@F)zTpKR*WFTVIbQEW8QXTVd5> zf6HNi^zCfpP&Jo0y2VUJYnP$IWDzYjm?Fn3f&pfrBhB9x_B^WU7iVL*v*{>nF6jI% z_{4QrB>?It@RwP(#B@!&et5%KkbV;1pDw(aomDFu+Zj%Ph62fX8*)|^7i+IgS@w}Q z!no%LcX`R*pQM>-S;IdzlT7H)7Qr%w{aBAh(oc7jkM!T{Uvz~Jvgr5&M=*wCW|7{8 zNTx~CzX=glWKcKFrx;w~dnAB^urvhm9si~$Cf3f9PSu^&9>VPTSyp~w-vEs?}WPH1B(Uhsa zlqifCC4}mH^z3)Q4MZWO5%cz&+(rFrHLtZE+kNkv+H1?)Q{RyGt~mkVG#+n$qAcMT zIC-_qFCW10nHAk%6!QL5T1&#}17~ceQQh=mlVNpgT|@|~jU<1lfaN-D{kzEy<_Nfr zFP8yvgyg-hu|E$Ps`wtEQ;^lP10Kfq6;(Ogaw+IG_6~&zY657_q`|dN97>*)ehw>0!xsunb1aM{9mX=(l{NVl|6x`M znL4kEcKO`^E|te>i&&KT952Te^U~Uzx?`7POva~%=tOop2zdPHUnd?VbDXQ=vQdeU zo%YCI3g`Jk)|BLLdX4TW-JWKjVfMS`h2lh-HnCa-82?acsnEx9MREKghRhz@UqD*G z=2CGHv?HJv{V4@LrS6n|@o-J}+iLjw8g}|(#COqHxxC(YL!C2yo7OyJlruV4cp-!7H9W-0&uBt;YgxEw>T$}&*&&HU<-BC8cdI9x8!U{u-vfCw8@ z8hO{d8d&(7=`B@H-vu7p`;5^~j%EqS)#k;TQP5>nhn#%YRg$gc>3Gp?!Wiknbu4f$ zrKUcVC!yQHnK!Dh~kzB1^T^g!lxY=2J{HT^}9N!VHLnDyzqWU?N zjT2aV2S%f}A@=8m66cId4wmyW6YKYyXB^Rz=})FlOctNj+3!2Cy%BF6c&gSdxwQ4h zzFzRgq05%&m(1a8aUkG+ZsXa@bmvWSG-yjrdzfFP>d({fcsE|itryn}jmy5*%bL8! zgUFyQ1YksO7X5Ban&f)dbCB9^Od8A6&Jgcdtt`vnwmU9E`-hzXhMm202+XmWg?{n- zOK386iy7Jd)yI4371Ex5f+NG<;u#4AABwLU{jZSK?njNa{ILQ^L%Pw8_*K$*gG!4c zsI(m#@W;Qt%UtpsGti_%t?D9W?=li(BLLWu(BE{$0NY}Op5z;bHx*VZ1ZoQ!uIf9; z*IC}V&hr=+%153hKueWT`>GsYPfTTb9%m}dW|J9ev-Ox?T2lN>2aG@pa|8kO#^D&0 zuq!+rX!f0aEx(E#SXZRBLcQ8GE-yGWQ2Nk4^$2cL5)1tfs!0~q!OCm&6S&quFaZYK zjggsm@KFqXMbkp3kbj7~g75|ibt2T?I`G`f7L9!m1J1jR(4|JJuLBNvVmoD)Y0TEa zyInHO?PosDIhz+EbU`*VUJuU*HuVI5j60_kD$}j(ewKvWg=`E)6zqMvwiLYUv;2bK zbe38MPE^)Lhu1c2+7TyGOFFXD54JWPwtkXF6ilG;`ItT9);I;{L@y4%b2)W!{_X|4tUjK*X*+y|?gE&ovdc)CwubVZTbb`E6dN{F<7jPSIgrlp- zjex|0cI@?;MY5yBn#6LBODzj|c-uSPOB%TE)qoE1JqAN1`}y=N?V7b#OXx>OailJh zwYs_`mafB*KmVv&uu`5{cBzx$O(1EQo7`l5Y+H|qaq;~CJeb7uE0zJOXlO^!*DQz@ z*V&lh(m}b)t&(EKLHb9h~ z0^d!igvw_sT;W7>(k6J(FJ+MX^Ox7T>1ANQQs%_Q*W!emvkIC+M zY)DKNfi!PAH=*$Rb>qOGCtDH&w(Wt{+3mD{|5p1dkoOS}l!aM3jNX6zBzgf_gRX3~{0U$zX4BTs)vJX+s8 z_+|MSfnwbB5uFh}|IR>)ilZ(3jvmN!;9EawY}Z^_&hHh){#c`YNPD9ht2^r= z@SpNOvsTYgc&m~8HH(Cmu-2Kcf2Mq}jG;f0I~kg`7GTN3Up>&_T{?B(}WZ%sg(w#D{qNvkro=JYC`AQPs8Dn3jnaPkVY zml>J*IbUsoJIC&A8e|!&PZqLZBU6xWQ(o^%6Z`JDe3!t#QX^V~CWD^R)2PqVNEs?z zk*6VFM524;wj0fhTsqrLV-B_t{m?>E zM_TbB6z%f~M(m2~CZ#KNu&ymH*uVlvT z?lPU=rAo{Dl%4&N)!e#DTUhVLN8nA$#DOETonYo%wI%5{-i;1cknfd4eot*Uku;3A zZ+EfN7%1F#xdzAoOegC0epslP-5}I=B*q?U{odradBxSE;kyrW4W5?L3IU2Yh ziRod?e}ZS+j9D0(&K23b#AOsMXf#$Fkmo;!7?moXRpv-nO7(@PJ0>*4ZaPRPi0+ma zAXiI!T%JEyDz2b(7Z^~5tY{vsD^H3CjFH!NxVj*Ho}eoEd59nu0`;p7F!fW#q^a58 zeb`Nm?F0|zskWW`3vU6xTZNE{Rg$S2CXBSBXRbs1t+)6GBC1MFACHR08{hww|Lyhv zLU9q=wa`VbweI2D-Es0WnModRrtzN}mzkV6<#i1RG2W9Q@Q?UaAY|I08h?EDW-c;Q$7jf=nj@Gt(i|KOMI|GI&G88P)T-)lU|tH1GH?eza(_7{Hj z4}7^Vc-2$z5AJ#4SAWdgf7e6*5B{%(ybO3k5Au82f3<_;|Fen zSDw10Bne5P1}?~PxIHB+pfw(=*$71p8lFJS@2!|mPD<|wSHqeYxhl)#!P{i*1pmW@ zZ>;~-|60F$ZC}@?vhjbngRW<*A|10mwR2;`1-}Jat^TVf_5-sMf;VL&16a75EO;6J u`aODH`1QUCbMx=~uk}Lq, 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 0000000000000000000000000000000000000000..225fc63b3fc669b63c81b28b2281452c8aa291fb GIT binary patch literal 8728 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, 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 0000000000000000000000000000000000000000..3aae6b0f8e05c5b917673d8be9e0c195f1a81d63 GIT binary patch literal 11136 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 MoytFX;=}WQ0Q5_-#{d8T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..000c26f6cd56c989c4c542149b985689896d0766 GIT binary patch literal 8488 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, 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 0000000000000000000000000000000000000000..4f49e1749297c810bf173ab77b394c0a8eddb93d GIT binary patch literal 8728 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 AGXMYp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..df57530482d52e502037285af561118db0a0ca89 GIT binary patch literal 11136 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 W@qaO_|LgV^$6KAs-#qc^`M&@KpwU49 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ce6c014db790ad63fb096727c6c8dab63a2c2c1b GIT binary patch literal 8488 zcmchcRZv__ySDM5gIjPXKyXcP%LI2PxDFHCAq?)pf(9qJy99y;cM0z9?lOPXA2|5l zgT3pkw`#AGuB#vEzSpYm)l%dF7D4(jN_Rluj+6Jwdjq~1bBvu+cr+(l+C!w9+Jk+w zsUbja?SMcljp^hPOpQhIap@G@$vtdo-qSYA3)jLI#09>QVo3$em$~?c{4%Xf_@kq+ z)PV@#f|IKG*%VDv9heeR{jsY+G2Wm)uk~hrDBdBaj|jafM4PAP-O1Rnro=X(;t$*_ z)}f27YG`l%2ODr=4Igo>6hNhAdSsAgBW%*=1Uwu$_zt*-@%9X-@vER2nEax3*GBot zRJKVI3)E0C?}B#x&_7P02^9*BjsX26aT5jSu<~zz`(dWBdMGJuar;Rx*PBlTzw-AU zLp!%_fYFY|8X+UC7%PNf&u8Ke&y};Eg9;4IZP1G2d_6y1oA8jFwhejdE!NI9qLqcWfyR?skHKjX59EGN>W zRhL!i7LIacUmHH*m{ZaDHnTUZsVl5SLM@0Rj!NIG+#P=ElAUOy>Xo%V0fuj4+p>Ks z*Me@YA9r^Pmi9zEy|y!d5oHACuzCrj-1pbbtzFH*5A`vAPJlaZ-|~>jw4sK+uQ~o&2Y@ zrzS4Kt~7q_lKhOkb<8!xr(;d~^xEuwDV{E1urBitQj-X1-etBoPpg82tJYlk|6@2t)P zZPU`7Co$hZxkRI$Gh?9}k51d1WE{D0qMKy+YtDpJ_gVkDVkbu`qT{0p<)k0U;ejsq z2AP;z80*#kBGJ1eEP406Dd$F3sWC}2l4G$V`iyqp&AfWCEX?QAZPAJm@gte++d>XH z$W4;20&7u7zxKv6A7uy~3dp?yA4-KTI>|eHg!8PwCp%cx!7ReAJfoC@GzmKlz+S|(bq^5mzKSWjOK?+nGtTV6on842rsU1 z4!-ipwOhMt^zIi8_q`B@E|5XWNMQ2LOQ~l4F+=Iu-5W^dzaIPGVtkU;GFxN*Y=Q1^ zK2V$hOUlbkSUQB(f7DTTK zuSNu(M!ddBu??l}&kh>k_vJmbWN20$NRJx&j_Y+VT-|DoDxo@B4@F9-&vbTU42{5ZGNKj?HflX0r84#_|C(dMxIXC0^qqOXe`+ z)-00!dm|_5QOZMejIT9XNIQG#E@lFfmL2)wD#I~xtlwigWz&ParaLB%vn4w20z+NH zDeBwmzFQ3>5qY&@6gVkejylx(#)}tRXM|FRn{^+ONSnb+^!7cJJbe=r!`gXdFaOjW zPBTHX%(ocEFL~c0u>%)|7$eh~!#ZN*UeJe7l(RY2C6O|*y-Ihx4)bmrwT!)9x*2nJ zi!xEbLs2I?i^@~6qBKNuVYqGEPWQvK?qZ2th}q$w;+P0=NrJEt(EmhA&R(1^6MNB5 z{%U#{OE@%*x$VNHt%pN7 z$m+P|mG0~hewOca6ijK{N~Scei(Fq%(6VR7aRdUA<{S0iuXQU3Q}_^MDpM@=L3L8S zehMe=9l|tgg2}Ngyw2eX2cuHGN+c!~z(tRtr*N2F!3i_YUH+ zc4Q-y17L1ca@`Q6gRY(j>>=8js)~EC6HFkHOew0okd#VmCPvx zgxG%B5QDQ`vQfa#5y=!Ww|@nq1+cBs4bPe;2bN61-rfA$@qQl)ug_T09FoP#$9Nnn zh%VFJ=C@4Ev~QpWMCcS58U+nSNF*&Kk1YCc+nd|5o*f4B`-jiD9v3WqX~kz+C;Y!2 z?-=ni_HJGpq|62l%T;x_0)%?*9#S{?>0wJ=yUq7V_DBOiT0?%#8g}A^%anjx>zSmX z5;J2-!p4?UN^$QZO`#BZe>2T@1RTXRQ$F&$CheI*D3d|m90uVnp^$ z&`aOcs(cwK*0(AL3m(OYi;^~8IlWRzTS;Omk)4;j@ZdCSny&!vBZE?vSy78#FP5GM zi^9l$0#rDa+!rQ|;7^~U+^Qvx0{c6SqTV1R5*Z92F-z4tZp9fn(>8Q?T7`SVO&?w0 z7>Qt6fP(1pf78@Ui_8La6hArk-l*M+di%+p4d5Yk&QQ9-V8-Fm6FdobA$}jZmSnId z=T*ZA)dP_hvw@4t3&dqr<1fxXb_;tlIbLgh@miqKCy_uYy!~l9DGXa9ds41aUHbi~ z-Z!+M=Hdh=wXSNXScJszv*?MY9*GX&%u1CGQ)UW=8ioz`%m)Zi$H3ALqqceVQx@EA zIc6sR@bp~wxed{2H_CbFQ!9ocF|lM7F)*^EurgSBnlkNX2(_RdG?5y)Di^%OU;79` zk|9}d%kab0v%j>d+H<|03St+u;J}NC8G$9V3KV!UOi#iLa-b6k|3yw6-!X~JeA$_5 zgjwQQvp^t?tR%TaKdLZ(o=LBY@CQn0QiFnaRGlOE)&w~>j{}$KXE-l;X0{VG2tG!v zu?4S7ZEV~5jcr;cgm({{f4m_gw5U9oIs!-B;tAVULg?MZLBgzCmN0hU*Zpl_jbPZ1 zqy47s`G6emA~K1oO4*hM6nh?s;!2flfew5yY~h>xGnA`a&+vDjk7_O|!-h{swyl!4 zQQZ58nf01vsN|KG+u9*;`MAaa!huCm-k&s#7s7&ulZ7?W$)gh*PzMzP!orE}=-sQ^@(rGHQwpjFa2KR{+2Ms|Vev*F1tu*I_DoYNR>(WMPH&e)N z`O#K1^Ph=iFkHDzeNsi{pNSakL!dAFMNPzt{{`U-=H=R+irC;ZK7h~5ub z1v}aIl`CT+pv&9WA|K9?j0S$=Ik$UWuR#82B}kxZ=ZMyj)oP=+eXQ|{JiQm>=uFxu zmn~h;x_eSH64Vc-fm{*hwW#GUk`JM9wJtBmldj)qp9=Apl-MBq9d7807+M>v3g1Sg z;F`nHALC@=bjVx3G4^Jdq?LfBm|4_r+Jh7RNE1H6R%G#|S^0rHmnC9yt}7edlmylq z#Z=ZO%5}6H@aUs+Cw`YsLfs}ZYq*Vok~`&)GQcotjHZ=3>}|J!(oqCp*@=f~BoFUJ zyk|w*Pe8UwiEDFbG4+a8+$4#L>ihgFSXBNHtPrKnWzu2*!*KaTJ?9cpScjHW#dpIwUYfTmFA55Z-p>8K- z^u$ko<989PP4=|fSJg|@d|z3ij#dQ{KbJTVr6DTI<}B6LVD?eI8pOvA(ao8kMw4>{L{keS;-m5 z^z)dfUXo(^0nGMs8S+>Ab3+(N1`sxZ9A*@CD`0zj?ZX9IO4n-!Yvt~6{&teXn1Rwe zV^4m&+yzr!R#X#*1~Edo4gso%KDiFp)%K&Jvug(6nq8J_!s)_O*qw}1u<{6#{T?yS z9H{YFdKia4%UMww-|cn%!387Ko4j?@j_5t^$x116vL7F z(uGR7z;xeOy3CIG36{xacP~}I6HVFZEgWO(91_2_s}P}UL4f91Aut^X%ZgY2$6rLq zKcDPdRXU@dW)|Kv9)G(Jr_9h-j0*r4AhI}4F-sL9+E$Tr4 zfa{uEc6$W#WX*F%9Eg8mjl^{xe_V9%ENM7f!Bk~6*IBr2i5S{&HhiZ%c^zVElwXC; z*@u07Sd7`IDFjDEkv;8SjN;N%G1P1K%xik~X)e>PHA8y8Vl`qGEHLqu3_SrQHK!PD zza-P9b&j0-;R8=NGnhA^1BD(mRqWRvdJwE3xJsC2IWoVmzB4As{^*gZVVX3Q;SIPj zuKlHW)kLW%-6t7#9~Z(sTm=fh^%B8|f!4{391H|FKWGmwVh13r(;rQ@DH9?^8kN6t zSi>2-M))>N~%Q1U*c0nOjU8C(`N6upc zgXz~}%6_>}l{KPsqZXC*phOW-tsLnyS&hgj|M|wElhHF`P)6eWlUe)Hhxk<+bgNiL zB;Y%bUPStYeDluVUyx0;aN&&-$?86Ann>)2YM8N@gBu9itFQ{R;qIbL&#`JBPXoav zG#(wlfsZ_gr+Ep>rgR}KaqL<7;~mz7ERwEtx+}WY%6T%v7|O&1KAOKu1aY-mM(MhW zrwO}OgQQvUhn1#jK2TzgO?;-kL48Ff_wy!g-oU>WlUOxN+OBp}H~X9sF&CeQY)U!f zt!X50Y@HWh0!LXSS(ZM%iVT@V>Q00^fl2sCtA?3WP7ge?NPh}bYC~}PH~u@Gg^5@KBBzOq6PL8GU zQemqt(1XXr=~fN^i9DWUVXEkCP6i#;e*A%Obl&9<*Z*qsyN+Wbfm~{TdzPh>0fL6m zO{w??I)Ise*xR&$twK&4c|lQw4#v}9s(gRv?@elU3rsv`DQ6Plg1Ek^H^TJSGg3=p zD1|}2=RGW}rr2Lb{PGm^8TGg=BVkBOa?alBN+Te--ZmXcks@}3Lo`J(K-=*$XV}q?sZo!2Ui6TW z?pBAMaVC*GA>$HOnkv<0TZMbSPsGfK=ob#M;9-m00J}L9WHI9f{H3NrUX|vQbCslt zN5}53iQI8scHK_maOR~;Kj;T)jdGT9Ye);kQ8@d11ux-TG{1&qK<--4TEiX+J{`#u zFvmm=#IcPtty{@ZR`k~@$?h>+*EBAUY+lRudH~frW&%yaloi9povY06k$Z+tXIL4o zGz{;&OP}V(PCni3QMY^#Ewf-Q&LKvk-D`1dojG|0Vi;rhehc-}Cg0BZxAR}_X+jXB z7h3^HG=_j~d)MAK#LobBZr;ReY71oh1=zsC2Bs93Z;2fz%lCmk(LQMB4S3!fm&YB* z@dFR(-m)fn@8CGtUdUVtsqp@2*g}}+G?<;U2nq_| zPz1he^8;jjyYB-XG>@0vt2P~a9V5f^DOE@}pFdi#33x62GTh+xb$%*c@^rQy(sRd& zs4Lcp4Fs_X(Q)C^Wj3$cN_C4psaJ`#+qqrDzTW&P&;#s|te{1B$pazH(6ZyWX}xFmim-&SiO9ZR8K;09+rr zV_!4q#MB781QplsC9IL5=yPbZ2xAMViGO<-7Sk9RfMc>WWW1f%30 zyNTAFmfzXZo{)8k7_+#H#s&IVSf;PiXOtX!pYUOb_uiTA^cB^n!(9LfiJfRDY&nIF zxfpvffWL^qh4=M4ibl16JOBA(_=v+=Z#%vYzDmNPMGbZ%X%Om5Cf_W;;jr3(c#`LZsrny2}?-SQhuVM_EHwKEwfB>)Y4ua4{BLn&l<0D?0j;h={&^=+2V^6iNd z2II%qJ}&U*82T2-OyA18tohr2x*Up+DK{jp5s(t;4qT!>atpA3EtZ$Enc0_*H7}lI ziU-=^vp7zg#5#OFfTLp*8msNZ795s&6L>2}u(r@YLVecAfJ{m*?wf^Q=Bg`Kv~D^y zjMZJOH5sqKvP48mGqbu5h@c(RNI2L$JIDo8J|0cQz1y6;aicCe^%&7MY-6%PgF_)2qQimCj!_sz@H2#8rPXHX zu9c5U60sMCuLro^k039`^wDv;${M{1Q;;rHuw#krSwR< zm?1Q8J1Hk3qmUiN5ue$~B-;IGOMx_=L0{R7*wAOds8%5nV`lfJn>}_4A^zszqqZ04 zayG+|OxDEYngG3#^pqZ>eQ!%s%oXv%ELz-mmBBW0vj!mAp8zcy;#0KXo`U6CaiNd> zp$GK+0iVeaBgq@>Hl%*^izGa@9R(iLxQINi*=oZn195Z=aD1FpnJKGAzz!>xfN?n> zZNg#%j_NxM!W!tb_TXUP;atX8UqLKsv70*ysgs?E{BmZ`8Hn&BYC=;dYyQw^$D@9h zx3m+!dXG)!X|gNDZWWpHbr)yUK@hjy2g_|R@Zo3;_e@c*4oXc4mwequZb#=?=*>I1 zZ-Kwujk`F4oLHx0RT8{Kvu>j|R=M{}s2bM^hQoYZk^He`eIZ#iO#G2jcH>n&G_MS$ z2rhyXiwe~00i{E9)j}0bT+lT{Q5mJy?tzjZd9R}%)t)-#wsQ5~d60MlX$_BrmwK=6 z{N7ndV~$%fqU~ND!rx7x&^74Jh-}l`SSB)B_Tambp}mdux-+>-z#vUsQZRvU0h!#g zvt_JSVC#i$NJzD{K;~X^{Kl)98fB<`8>L6$*@GfPqeFv);yi(;5@8dpuA4$}zgy*J zzNs9g5y-?c%!xCuC;8Azj&@dmBq*{l|9wSktRhh}^PF!enKmm69SOVuaKCB(1fybL zyu~#GQ-^smdusuIDPM&|7(79(4t>k}!Yyk2|B6@Un$9CKm|8mO1`yU?-O;au)a zDa|v7;+^XtjGMq3MX)iUr?Ht5^eJGZfp+q6PG9?Cva*$9RzzRKKdLd!fS{U-*~<1b z)7jLD>LAw8ng`G}!sJV9|y$ zY2Q>73kWb$Z~zu3H+uu?I|^c8!O*LXkj%zRCj&@bFo((aOb*RnPg%r>4QQD$Vbf0s z>GqZFo_@_Y<&IqIQOKsJKllBc3&ZlKi;Prd zbIP0RVZ~XygAlPUd~D(R+r(_ZOjAdJ@sDgcLSx01Fm~S1ZBODRynIoH5bNop6W?K~uTWe= z%6)gUd+lS25k2^-Ddaxn-e5LR7KGHu_4O`XP(@-my<-X*e1?UL3p&Iukp>KAo#R zAh2^I%<13#GurchTIuXB!#%UzKREiCU;J0j|N0|7```S7pR51t0`@s#^f}*4dz6=d z+k3Gy{)5?{`Nbddd|vRPC-WcN_RKH-SU3Nvhw&f$UkiB-c839O; zK0h<(G~vsA-2c}9mF2(sU;JOnm;NcO{Ll7;Tg?mFomn>_b{K6Mz=MQT2?%5X*x|*I zx{lQUx<{G$9RKn?+MfC4ya{XdZ~mA2LiWSI{9=bZzrPp$-}L;`{-Oi*+3-&udanO} DnH0P1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e57464d91a1f51a867850b4a41fa32d0331e4335 GIT binary patch literal 8728 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1bd3d5358ffdf73c4ca7ca22d75f20f0f1418ee0 GIT binary patch literal 11136 zcmd6tRZyKv@ZfRhKyY_=3-0dj?(VL^CBY$RaCeu3g#`ByTu*R!clKW!c)0iF*49>S z?YvC=s(YrVXR4;Z{-!HD9fV+{CMl1we9%f@1GXSLQGTf6sPE?WsdKuDpdM%d zJA_12M|%Q!V|Zp_qfB3!Qi@S)N%!6Ic#3P{Fb-0Cl+MQ{-m~8mT4KA{N=4{5OygHO zjjuzcQuYpMOv9Eh&E_w7|)%;nm( zQ0;(q%btj*Pa{*rTCW116JqQ-@V$i{3YoZfzZO|&t{;mFTHPD!7YA^uV%GN`Q2*r6 z3p3f%+`y)x5Mc_UJ_wwC!0=)lVI_5VEe2{Qv*9fe27(g_{U(3RG!{!XAGd6?pZMKO z`bb1wI*bU}17a?a1s;m1fd|0h5F}xu@+~`n+(?%60#!%#zLXV@rehXz(rhf&tfMNB zq3*$0tFLmW*@BA=)|rJv-Cye7eL3@{O*d6M)3zx`jQhAp7e2qF|2bMXOR7Y`qB;@NI=u!2ez-Art@&+hQ>#M`eZD9GJok`h}~&o95d zazBbY!*G@kH=4|Nz?Wmw)v6W95^P16&wgVt2XW5Z+>lC0pE@SwQ0s*1m=>CxW#eL( zP1m+nOSPD-8_hmIlaeJ#07H>NYMAxC-r0G~8a_*JuvjKeH07?hc#qVN)r+&J$Q`RUnZ84%%>WgNm z6g3kn(a^mj%e&e8c9w~JvGAh(QHC^9l)1-Z&|t8tu;hTTXcWxKrxX2hywVH1zf{;e+j;?C-9t= z#iSWeYCr_AA}eDL!4rv35G9Wy5bDV>rE|pecNkqQ0-!kb@dKdm;%a8LIj5wsWsRFp zQ7I<}?mh!qlJJOmo=hAGx&mW}BRs?yo9N|6@??a% zbG>YG@e_{hug*rH>qXY`N8rO2FwZm~iuz01O9zWH(Z0%wtnWn}0{uWnM&$^q7my7w z9g_Ic#HI}?otLr&bR3zIFpc73;*cu%pgR=8Au6qIZ!V@Lql$0|Rz#>~X1F6$>vXW* zBtJTipp{YGECvV_j7qM!K4%15Gs!+K+~b3LmpmTWUM_w0EzRWbD$rxOPnJGr*gY}+ zKk^D)Ve+9dpno8F$f?#ww1h>&U0YP|BKJst+SY$ck>u)!B!13ppzkiSRT(}PW$XKy zR9P--8xEX{MjRcx&7l?jax~0%eok$6x^bv&bK2HF?)Kz?V5fS6b?^C`DRWXE2qb}X zl5$$p%W1$1>H(dE_p6}fHx9Veo8DxMDcpZBa{Nt^xt3FvTjjW~>9tPp99V2(RdhYw zbkwPWo-E6dHhiY1oFT5WlFRKT=dl`J-7L(L1g+J*S2d5yUb=HNjD1ZExn#ucd36ym z!H|CFRj=MTullqwzHnAZr|=npqKWzJU}Dfbb!Gmk;a-+%(xeifyo^A9AoU?^lk_sJ zl>32bozU@QobjAdDPl_dODDkPZu&+=M}v{(R+hHXw!U%UqK=lW=a4l%ZraCx@sC}5 zel<&k7?^uV{-OGg;}r(YE?=9>l}@*0c1GxDXsK7h6q#Mr;a)*iM;Emri%cI=i7wVU zewIiSQ;21eIz-DQ;-fz;{(s_fOZz${twl?2MlbB(_>m8LS>)0TI3|uHsK(H6CESv$ zSx4n=7|ZpDygkGkB0+OZ<&9XIC1mY34?BUmXu#l7Ut9-k-)6C#XY<7Z-$EXCcG zzDr%V{-MR2jA>V)`1QvE-;+-8N9^Xdhrs+6bcBE1NdfRtj#!%PS|ppx7D(j4^fbB> z*`gjUhE$pqAtk>)_fKa&VK*m6?D6MHE)JS|RpW1AVTlj?HfTLjI1`om~> zlE1Q*koYp&+4m=qDHhPvF2=bft7?`fTvK_$?+ zorMV=o5$Kq6lUnISq!6k5#*pD4AY@klX(nSLd6(Ok2P%!TuVG5T`39*8*~oryUwfkiY!W^T3)|Wvjhd8~fl5Ouh8l=w1QhP66aqbID1V9( zS6dx3^O<%WFpiF~k$5b&nt&_mRK^Po8ltiOvTwtL zQJT1{brd_Rl`tSwUY$)e`zrAP{51AZlVx7Cw9fP6Wa$D;Lq+h|X;n*hL{fLICoSEB}XI^-8s_=&njA60%P9+8{Lul;x$5`sZ%hODw)2)< zO%HzJLhDoy6za&ni&s*B0$+hm5M@^OZoR*3$_(FbX)zp#J;MC4<#3lp)QS6|859ie;exKUxt5UlBd56_bz|_%dl^}_Qo)8obd8j4*fpL2cmKdnfbIihtdMdlkx)*cOR7~8y535mGZZtWVZCVvqKcu z8+(tkQziQYX5korAlXv)ss!)GQ71~PA1G3XccXaITmEh%S67emTyLD2QVy)kRz5Uh zM)Ffb^fztblbxcpj@hdZCMo=1WuN`{SZuj`QC2G}1$MqNsRagbf;2q%kheB;OUc)QE9$ zb*My9X@r#Te8q6v4k`EC+f+1QzV_D*C3yt#cV=wI-D5e-(8T|Wo0ipaV%)4vPNI)) zLR1Z&H&i-or}jC2J}87(?0k@WlF+FRRwSrmN;Z|b&Dzp?Q$PoRbPNBKIH7!N-< zFDLBw6a^y;@&O{0dZE1qHgCs7i56$`celK6TT;L;kjbQ`!S3MbC;}qqG-DLk*!3Fj zJt){2)4gZEAY2r6_GMBe8m;cul~J44D=Z_~3;#QNDn4)UI`MzS&Ac#!aKDc8R(5zV zQO2-=UwJ@ww${_`Zs}8MnhI%k#xGFbNRDt6D6bO{hVzD(s+SaTG#u`eixbBc;w_U6 z$&YOnd7WTV%s@CUU#ruJpJ>mIvk4(>a@K?ptqLFoX9(UPAjh9ELQmh;BUB@G;wXd;v;ISrvy{}neCl<4^H zXCSv_LEmnc^lo+folOfQ^idK#4$u3)g56=??b+&IcQSa%M;(3qOw}vxnwN@-QLQ=f z4YnQf^769umtWS<$4_*e6bOA&R&WgqTnA|ZG}4H@VF2#C?sZonIVKfI2ow_ueY2)| zZZ4f(?24T$U39%>77J?mXD!lJ&2(@b32F zzv3pi7&)i96r3bEwT4r-SfzC!!9E=AWeV*5hEhbg5mPKEah+p7;|q_jbH9-aVn=(L z@V9zXPfeev+`Q+$-O*7Y=mqt>!|a;;-*gBH}ERWZ=;VjIvbx9)y*~JK0G_h zY>Co;hf~}{#fVr8NJ;*sf`l|;ZWz6MWf;?0>472S{tFu1+;zeYyd2udv|%x43CT2^qEygF;XB(8r(Xmqkz~7t0oc&`;y+ z;HR=ID<(@6du29~4l7Ok3sIF9h^NhO1dN`Uy$x zD+|Y`I(c!;-{5q4jdu9+n67%}<_u{+zW$tph~h^dNm4m@*hGW*Lu zyYU|jB0qLAbx{ZztVdmj|A8X2^wJXU;2=?R`h2jjTGq5k0D*HVU{gZUFtG-bCQFju z-YwR`D0atl)g?~r7x0CnM*Zmbz(dJ7)ZifX`oJ!NF!^lW=@x7yKrgqOKHmwcB=@qy z95fPQ+1Br)Ql(_eOUL0LwZ-{mh(9$L601mw&~R`u6O~m({+pw|>Fp)`G?q~R+BS~* zu@=!RlVL366m~Eb;n}o_ukSBz9tbNc-Zlg{J{cfUFdxUacrVb1u>51zE@nRlO4#Lg z?}5Un5Ebxb)~l(Cf?O3sUl=-JyfX&Q?~xWl<1u||;^w1AA`y41tjt$ruH5y-+S^zH zN`pD}YOtrN50Eyl(o#P_w|GWTp;s=8>BPCo1YV!Ed-m$QbQ_YbCBk2LvGVO{?3A6= z7VW_$Ag%^n2NNU)^@m^-r-5{o9w72Mf~f9RY?ox^Z<^km7weV@J8r~L)vLdACwSew zZ_c*V;6eSZIgQu8U;PwhV{8Ig@NXwmp7g0jxlI_b0+FmS; z6krx54Brbnh+VpGQLM*PoY&N&UR<~&NB(eqn4JL2;L_iko6mL9MmBo{)}bAk_bu^z zsteGpv;Gu?8rg$JW4JXgI-m_^w*I7^ixFU7{gMn@zviSxRHli1CFZ^oqv=+8yCeP( zX=8gH>hqv_1z7{hX22dMIHU|Ui&JrYJBUnTe9kxRwYS_A zG%l_`1XqsL)v0&xSqiQm%5&1#tyVheu<>I03B#XDKOiYkv3nsQT`Ds~Yq1p<@w1TL z0wZTiPVypl|uK8V7(Xb}|qs+(S) zcq2^x0MXEoJZfAt#NWzFWowId3K4o+hguI)JR&uS5a?E4KEi zCfi5b;s*ijxLn2iGp*|Q0;lf3-lN4waav{F5EUPtX%z-Z>z~EgABXz6cGfGgz|?Bf zh~qlDTU?>wb(0Pff<88Wc3^#kDpS6eZ~@S7k>*uQ^F(-aknomVe^+Upj*d#uer86#|DmZ|kWv zWT4G?{Q7Bez>kfC%Ta59M3hR6Gl9}$aWh&D4{wBiLL3Eb273XPA0a$8I&pcm4VU`U za~{_isUK_8jgBSq1PH8`Q@*S#r{pu0(SxA^jVt2-tKVeBc%zDAd__cB)Q7UmA~lfi zy*`>b0ybcZ^CjW|!eNl_Yj`e$H0CnqNSvaJAjS z9~c**i0T?d&mPY8nL1Jm8Ep5nbI7b0`{lz7T z*wZHtG#|Kz!E-QYOH`0ppH!P6)bpp#>jJvOB_Cs?U7VpV2X@>+;sn14y1KYVRvLr* z-6K0fZzvGz8nLTeeSK`ZU`AnfwA&aTS$OI*%lQ16=PLzV?h50)24|c;g4VK;T!eT1 zyPH3o5<3zlNp#V{V_vGvuP0RBPM_mIIVWwL0cOFXh9E|Tg+F=Qd-a{0j> zA-sVaoS*ype9apdY0~Kk1WN$)J7TJ#K#DsRC1Y9VzV?GA=7jv;0Uzc|rL}2tKS_S` zw-AwJ#cy@;Vc5W1&i3boOy2SuqNGdzu$k`#_MBe}7UB2XdzfKmGRHk?>jH-9f#|>| zmZ{oK0s{haB=dkY&M)H8ZG2fhJ2~Ohzq&(~`l@b%EB%=fcB_mz{es}jih`05Hn!vM z|JV7ZO%-NdL%qFyPEB5rpQEX&j>1rl{@H~=juUS}IPB<|3%S#pfP?%mtj`jFq|{%e z-%prA^to3HKc%afIJubY*=QZL6!H-X2WjUxpX{o)b%a*(@Dwp&&%Q7^6q!mde&C^p|1_n zGJ;wiBl`McJWpYb;Xhz3%b7u<(K7~AQLB)WLk2U|q`dP?32xA&i6Z1B%;VVzqH!!M zv}^%C9Xcj)VgG6StQ)yIb}*^Pf(bxx$*0`IH-wQRi=hbh6UZ}fA2Fi|+9qh^Th26YCS?*?V}YGx*z z1OM5h?@18&OK|gSJL3bHRjT;%omPn9l$*xj6;6}U7| zk$BpmfU6uK+ObEKk-nK2Q!%gzv)GKriY#Mv@sU=3X~anb;^wCT`^t$L z=_1VV2ukFH2+M9DiUn3%;|mz*G4!jXb~6YY@_}M_O4VOYs{O?+6aAq3!wG>B6Df#tDXw;SDu8>YTuYQKjE zYpf;Dqw3UnhPyqfW)3!U=9`*;Oc}#Hy#p&A(P*AUKkJ=TqOYY>UjqY6E9*^LKmM)D zirA-wc)Lw*vMS@(n#Ll@#fNZ7kKY+xHS}5tVe$$|m!fR;Eok*0nX%&BXZI|2 z$*Nc1;uVi3C9A8mkObh-BxGpA3uI*i)aBP3R?yrfzPlXnhCN1gyM>Z0qr zfBHQ`<)AOe+5D@8^RA>ReYG~UCvt@VmcZDq;nr6@R8VWi|J;GNT*B!K{l`qpGV&kW zqZTt-V*qQCPFI!5z@SffCK37+qAzD~qaxXMDMyKj^Vk^%-K>pnme&uZKH;G~)M*qN zwKjfh7%M6OKAfq($xOC*Mb}_eZ#bP`GZz3)qn0o7CLJ27FG0q*rO3nBAW=$;rlr$> zp%*Mo_zS*?t0zS z|M~p4ljCuMohk%R4J=;UW^_Ssh(jrxX{@kN-KdFKxmYA5OTd+r>r4+!8uFET^uR2ojqH?)|je;Yus1Ks7dm8J@8Al=92?t_TT_rO#-f9 zh|I5B3@}GAv|Lrn`PeUOjm|(GEEfPO^0DEfF)_QauJjrPa{Yjp2w^P= zAFlFvB`9&u)jf7zi~I>g!sugOK9LMyb>nrx9)?%jl1jP$kbzdKFXeL_YJB?>i08u8 z(@}^I*FYIOR}4f;GtL!xA-0NbP2h%GENThCI#LBR8f=m{t-C*k^7%xLe}4JC%=)A8 zvw!CEOu#Pq4jmdw%)aU^Wmn^}iyDw&!?FBT?f4r-Nw%!bAr+rEMQIZz{KQv!A&YhQ zBW@n@S0ToGFI4Ks(4qp!1d5}7=YAY* zI#NxzcfzXxS zN?w{Gbu|V}jTafa>2+RbY%KhEDfjnqSrS2|w+}vnyR*=L-GAeuj1oJ^d!$?8c^Hfi zR=T=gGdc@wl1UaWPjJ51P~ZbP(T03zY7SyvE%ruSH`Q8h`HKTmz!(`uZaMC6a%>GL zN3tD|{F-f7GP}wZog%A|*u3MJl1NF5{k{6k_+gJMX-hRI0XTi-5$b~h$;wGfOvl6r ziLx@$Yo18#j%U~E-&)%Vp*Uj3Z-mo-WZRs~4}vr@qzLm=4jpP~B{3Li$OSJ`6dBXv zh^+UGk?Bjz;blNTlNxts%D&{WrQ?7Kh{6&{|C1-yqCZVXKVdT(T&1Qt#(+@?7FQ50 zGbbI1_TD!=-bE*gS3TqTegQ5t+Xbkk#v#n}Ewva}2}@O*$Xl_kMFNJv(ORHizJ1gew0W}4-*9HTW|5XM8@o*YyEO}^>mEiDj+Dyn;wrxr zo_@@zYohWO0{RR?IoMjw7OtiM_c7~t3ALHkC6=!km%30>X5c0<%ydZE%;s3bipbe^ zdnIV+c)=)4%(WU0ZsuUe}GDlX(iAqDB|ABj8{EEWIj{`0ve_r_)6L~T#C zs()D{ETBK}M2ozWJS|oz1}Zi8Yv*L3Sm-1|@LIQ?;@9zi&P1Ryz}_{~hldcSQg#?! zbU`iqL@IL;>^h0Zlj-4l`N#(kMft&>5p(Z{?~}P0zLnbHSqFzo`i~aRiECttgzBzk zdaJ(LbeFIiv%9Vyh}X7QZi&oPdr+R`;mJ~a!RmwkksjbTmOzry+@HE^C`e9gAsXz! z$t!bf5vv3;?BL9x{%6`#+-_;uxoJQ8i4L3qo+1Rudi}5~5gzH_Iqej7q$E856|}%! z1Ua5gw%%Ne^J&r99YIKO5RX^9xIXB>NZV&;geu3Pbv50XLrEj|AsZ$f{M#zrWAi;P zx-k(C2rTDL^%2+hj5Z6(d75)qf0P;>vENO&98KMlVl?xBejY!dW-XvXFTX}Wl87;3 z4a&tyWOjm63L^;h_2W<^vt51=fr%I1K14)aS~Qhg9};OX>jfPyi{@|huZoppa)6q% z<&2V&mU-fv)ra^jl=H)NWcx6hJKRs3oZ|m=|Md+iY#{U}1HLx)CY0HvJtqDvX*{tw zuRE=`3~W=l&eKZfPt)LWYSzFx44_di^Lac+Ido&~S5jc2)6u6lhp1oi$4i_MkPDWq z?hmw-;Gh*L(rHvmSIVwwleS9S74nE7thfM{(*b1eTq&E95xt&#;;Pd%2`fv~r9N2h z2D7z+$CV0B3QK)pW?KJ>DE&d`FR}U;4Z7F;kz#9aPDV+I!owm z-_j8N9-H##K#P*aK$-YckI+;Nz=?GFja8c};5fjAu6SBZ?PwQd|NIOw5$LC)Mx6}d8zvQ)N}mhWS5zK6A6r|k7u%sCQX?rcjigpcW& z3E4lyxS7dkhC0x@`m@xmg#i7RRDNSbPCQ4DdCHu?u#OJ>s>Og2+3REP4+rY2+;d0p zW+gj-xxN`M1GAV$l>|O<6Kqs%9bKb;?bzTrs{X(lsn-r(_&w-DhGRcNT=8FG)4fUK?9w-Y!@YCNf4& z`%EK=jNk2f%4gaGwI1}q^1hw-ST*{5`(iS|K)1k2^9n?1EwxVq=~W>n(=dD}vZi9w zlISp(k>q7asr@S8AlVgLR%7$eI6Qs-`P@iPAtv$HNquaJ(RKgBQ)}B|^{^ zn- zh=ldikBSu}sY^R>M0|V;X*Am-lxQsy92#o(GelAwta4G>+^((;FQqemR=q&ijQ|9< zkvrPd)@j~_`>UwjPC1IeLu47*OtPNr!5R4`vybGBUBlC8E*9+{Kdwa84MmWCytHS#mx+T93BQ{b*zREiQ{ zVeo}9dvvFq&0c|D_=lS>iwKDCtVSqxCiaX&5?DPXrHQ(_iGe%tEppwUJ!nE{dX;ot zitIOa^smN-N3iq12{Gc^JGAn}Uxs*N=YMg+8^7zXUH;X7f7ibI7r#yOf82q;jhK9! z@7*5B^}|exd&+Nh z2Jdq1f3fi!zw6gM{L`PwfAQHHzv~OX)hWE6kM_U#|JEOtxB1@f1-;chy!#XNF9v%n zcu@A(BFvDMwt>NSAFxRGP{(a(OA&@UxPrBZcG&Y$>;>2So!W+kP@gT0dJ3(TkNc@s zpmiHrmM18}fSaCe9{atn;=k4(HT!qJ-u2)6_x0WVhdlLO>xyD;(OZB8OotrsBqmX1 zXJ-Z2<3y9VjWvinAd#N?=XqGa@%#TJ?)ks+_c{-j Vec { + let mut v = vec![leaf_digests_json::("rpx")]; + v.extend(proof_vectors::("rpx")); + v +} + +#[test] +fn rpx_vectors_are_current() { + let files = all(); + assert_eq!(files.len(), 1 + 3 * 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..e4b4ff7e9 --- /dev/null +++ b/prover/src/tests/zf_vm_dp_tests.rs @@ -0,0 +1,59 @@ +//! 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" + ); +} From b04e895b7b80ca4a6cc19e4e17320364939f88f7 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 18:24:42 -0300 Subject: [PATCH 24/73] feat(stark): FRI_MODE_IMPLEMENTED = true (S3 on the host CPU path) LAMBDA_VM_ZF_FRI=dp is now selectable: ZfFormat no longer aborts on it. Implemented: the CPU prover (group-leaf layer commits, scheduled folds, group openings) and the host verifier, owned and archived views. On a cuda build every device FRI arm runs only for fri = pair; a dp table takes the CPU FRI loop (DEEP may still run on the device). Not implemented: device group-leaf FRI (I-FRI-D); the in-guest LFM verifier of a dp proof (I-FRI-G: lfm::fri::FriShape still derives the legacy layout, so emitting a wrap or node over a dp proof fails its committed-layer assert); the RV64 recursion guest (default-only by RULINGS 11, it refuses a non-default format). So a block run at LAMBDA_VM_ZF_FRI=dp proves and host-verifies its STARK proofs but cannot recurse over them yet. The zf_format lever test now pins that fri=dp is not reported as unimplemented. --- crypto/stark/src/proof/options.rs | 16 ++++++++++++++-- prover/src/zf_format.rs | 3 +++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crypto/stark/src/proof/options.rs b/crypto/stark/src/proof/options.rs index 4a5c56d4f..f1edfbef5 100644 --- a/crypto/stark/src/proof/options.rs +++ b/crypto/stark/src/proof/options.rs @@ -242,8 +242,20 @@ impl FromStr for OneRowMode { /// flips its own flag in the commit that makes the lever real. pub const MERKLE_CAP_IMPLEMENTED: bool = false; -/// See [`MERKLE_CAP_IMPLEMENTED`]. -pub const FRI_MODE_IMPLEMENTED: bool = false; +/// `FriMode::Dp` (S3) is implemented on the HOST paths only: +/// - 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 every device FRI arm (DEEP→FRI on device, the device +/// layer commit, the device query gather) is taken only for `Pair`; a `Dp` +/// table runs the CPU FRI loop (DEEP may still run on the device). +/// +/// NOT implemented: device group-leaf FRI (lane I-FRI-D), the in-guest (LFM) +/// verifier of a `Dp` proof (lane I-FRI-G: `lfm::fri::FriShape` still derives +/// the legacy layout, so an LFM wrap or node over a `Dp` proof fails at emit +/// time), and the RV64 recursion guest (default-only by RULINGS 11; it refuses +/// a non-default format). A block run under `LAMBDA_VM_ZF_FRI=dp` therefore +/// proves and host-verifies its STARK proofs but cannot recurse over them yet. +pub const FRI_MODE_IMPLEMENTED: bool = true; /// See [`MERKLE_CAP_IMPLEMENTED`]. pub const ONE_ROW_IMPLEMENTED: bool = false; diff --git a/prover/src/zf_format.rs b/prover/src/zf_format.rs index f364b371f..c098b7c78 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -419,6 +419,9 @@ mod tests { // 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)); } From 6092c77dd017aee565ff4288b473ae1a1bd34c35 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 18:26:50 -0300 Subject: [PATCH 25/73] test(prover): the production format sites prove at the process format production_sites_prove_at_the_process_format proves and host-verifies a small ext3 STARK under RPX with block_base_options() and aggregation_wrap_options(), the two univariate production format sites, and checks the encoding the process format implies. Without a knob it pins today's legacy encoding; under LAMBDA_VM_ZF_FRI=dp (the box's knob-on line) it asserts both sites stamp FriMode::Dp and the proofs carry group layers. Checked on the laptop both ways (the dp run prints "ZF FORMAT: ... fri=dp ..."). --- prover/src/tests/zf_rpx_golden_tests.rs | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/prover/src/tests/zf_rpx_golden_tests.rs b/prover/src/tests/zf_rpx_golden_tests.rs index bd8825067..5d2803023 100644 --- a/prover/src/tests/zf_rpx_golden_tests.rs +++ b/prover/src/tests/zf_rpx_golden_tests.rs @@ -281,3 +281,42 @@ fn rpx_group_path_at_all_ones_equals_legacy() { } } } + +/// 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). Meant for a knob-on run, `LAMBDA_VM_ZF_FRI=dp` (then it +/// asserts both sites stamp `Dp` and the proofs use group layers); without the +/// knob it proves the same at the default format. Either way it proves. +#[test] +fn production_sites_prove_at_the_process_format() { + let knob = std::env::var(crate::zf_format::ENV_FRI).ok(); + let want = match knob.as_deref().map(str::trim) { + Some("dp") => stark::proof::options::FriMode::Dp, + _ => stark::proof::options::FriMode::Pair, + }; + assert_eq!(crate::zf_format::ZfFormat::global().fri, want); + 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}"); + // 2^12 rows: LDE 2^14, so both terminals (T = 9, 10) leave committed layers. + let (air, proof) = prove_logup(1 << 12, &o); + assert!(verify_logup(&air, &proof), "{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(); + if want == stark::proof::options::FriMode::Dp { + assert!(values > layers, "{site}: group encoding"); + } else { + assert_eq!(values, layers, "{site}: legacy encoding"); + } + } +} From 1a936325ba7e01c68bcadfc7b6574d8a75fe1636 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:11:55 -0300 Subject: [PATCH 26/73] feat(stark): Merkle caps on group-leaf FRI layers (cap x fri=dp) Under a fold schedule (fri=dp) a committed FRI layer is a group tree whose depth is the fold layout's, not log2(lde) - j - 2. StarkCaps took the FRI depths from the pair layout, and the group-path verifier authenticated every layer with an uncapped CappedRoot, so LAMBDA_VM_ZF_CAP with LAMBDA_VM_ZF_FRI=dp failed closed (M-MERGE-B note 2, REVIEW-FRI F9). - StarkCaps::from_layout / for_options: FRI depths from FriFoldLayout; the prover (round-4 cap post-pass) and the verifier (table_tree_checks) both build from the layout they already hold. StarkCaps::new stays the pair-layout form for existing callers. - fri::group::verify_query_groups authenticates layer j with the tree's TreeCheck (exact length D - c, query 0 the cap's owner, cap-to-root once per tree) instead of an uncapped root check. - Default format unchanged: at cap=off every height is 0 and TreeCheck is the C1b exact-length check the group path already ran. Tests (cap_fri_matrix_tests): {off, fixed(2), auto} x {pair, dp, dp [3,1,3]} at Q=24 round-trips owned and archived with the layout-depth capped path shape; every cap node of a capped group layer is bound; an unreached cap node is rejected by the cap-to-root check alone; a proof made under one (cap, fri) cell fails under the others. --- crypto/stark/src/fri/group.rs | 17 +- crypto/stark/src/merkle_caps.rs | 56 +++- crypto/stark/src/prover.rs | 14 +- .../stark/src/tests/cap_fri_matrix_tests.rs | 270 ++++++++++++++++++ crypto/stark/src/tests/fri_group_tests.rs | 15 +- crypto/stark/src/tests/mod.rs | 1 + crypto/stark/src/verifier.rs | 28 +- 7 files changed, 374 insertions(+), 27 deletions(-) create mode 100644 crypto/stark/src/tests/cap_fri_matrix_tests.rs diff --git a/crypto/stark/src/fri/group.rs b/crypto/stark/src/fri/group.rs index f00cce848..42b8f8974 100644 --- a/crypto/stark/src/fri/group.rs +++ b/crypto/stark/src/fri/group.rs @@ -31,7 +31,6 @@ //! 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::cap::CappedRoot; use crypto::merkle_tree::traits::IsStreamingLeafBackend; use math::fft::bit_reversing::reverse_index; use math::field::element::FieldElement; @@ -40,6 +39,7 @@ 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, @@ -140,7 +140,9 @@ where /// 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` against `roots[j]` (path `paths(j)`, exact depth), the +/// `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 `zetas[j + 1]`; finally `terminal[p] == v`. /// @@ -154,8 +156,8 @@ where #[allow(clippy::too_many_arguments)] pub(crate) fn verify_query_groups<'p, F, E, B>( layout: &FriFoldLayout, - lde_log: u32, - roots: &[Commitment], + checks: &[TreeCheck<'_>], + query: usize, paths: impl Fn(usize) -> &'p [Commitment], values: &[FieldElement], zetas: &[FieldElement], @@ -171,7 +173,7 @@ where FieldElement: AsBytes + Sync + Send, B: IsStreamingLeafBackend, { - if roots.len() != layout.num_committed + if checks.len() != layout.num_committed || values.len() != layout.opened_values_per_query() || zetas.len() != layout.num_committed + 1 { @@ -194,10 +196,7 @@ where } // (1) the group is the leaf, authenticated with the exact depth. let leaf_hash = B::hash_data_from_slices(group, &[]); - let depth = layout.layer_depth(lde_log, j) as usize; - if !CappedRoot::uncapped(&roots[j], depth).verify::(paths(j), leaf, leaf_hash) - && !mutated(2) - { + if !checks[j].verify::(query, paths(j), leaf, leaf_hash) && !mutated(2) { ok = false; } // (3) fold: x_g⁻¹ = y⁻¹ · ω_{2^d}^{br_d(slot)}. diff --git a/crypto/stark/src/merkle_caps.rs b/crypto/stark/src/merkle_caps.rs index 7492894ad..a21ccc84f 100644 --- a/crypto/stark/src/merkle_caps.rs +++ b/crypto/stark/src/merkle_caps.rs @@ -50,8 +50,10 @@ impl StarkCaps { } /// The heights for a proof with `num_queries` queries over an LDE of - /// `2^lde_log` points and `num_committed` committed FRI layers. Every tree - /// is opened `num_queries` times. + /// `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, @@ -74,6 +76,56 @@ impl StarkCaps { } } + /// The heights of a table proved under `options` over an LDE of + /// `2^lde_log` points: the committed FRI layers are the ones the proof + /// format's fold layout commits (`FriFoldLayout::for_options`, the same + /// call the prover and the verifier make), 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. `Err` for a format that cannot be laid out. + pub fn for_options( + options: &crate::proof::options::ProofOptions, + lde_log: usize, + ) -> 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)?; + 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). + /// Committed layer `j` is `layout.layer_depth(lde_log, j)` deep: `log2(lde) + /// − j − 2` under the all-ones 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 { + let trace_depth = Self::trace_tree_depth(lde_log); + let fri_depths: Vec = (0..layout.num_committed) + .map(|j| layout.layer_depth(lde_log as u32, j) as usize) + .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, + } + } + /// True when some tree has a cap (`c > 0`). pub fn any(&self) -> bool { self.trace > 0 || self.fri.iter().any(|&c| c > 0) diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index b8fadf9b4..c2605b9f6 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -2955,12 +2955,22 @@ pub trait IsStarkProver< // 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. - let caps = crate::merkle_caps::StarkCaps::new( + // + // The FRI layer depths are the layout's (a group tree under a fold + // schedule), so a capped `fri = dp` 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_layers.len(), + &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, 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..907b7ba01 --- /dev/null +++ b/crypto/stark/src/tests/cap_fri_matrix_tests.rs @@ -0,0 +1,270 @@ +//! Merkle caps (S1) composed with group-leaf FRI layers (S3) on the host path: +//! REVIEW-FRI F9's 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 (REVIEW-CAP M1(b) 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 (RULINGS 1). +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) + .expect("a row-pair format") + .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).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"); + } + } +} + +/// REVIEW-CAP M1(b) 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 index 247206623..046de2eba 100644 --- a/crypto/stark/src/tests/fri_group_tests.rs +++ b/crypto/stark/src/tests/fri_group_tests.rs @@ -21,6 +21,7 @@ use crate::fri::group::{ }; 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; @@ -234,6 +235,16 @@ fn fri_accepts(run: &FriRun, deep: &[Ext], o: &Felt) -> bool { 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) @@ -244,8 +255,8 @@ fn fri_accepts(run: &FriRun, deep: &[Ext], o: &Felt) -> bool { let v = (p0 + p0s) + &x_inv * &run.zetas[0] * (p0 - p0s); verify_query_groups::>( &run.layout, - run.lde_log, - &run.roots, + &checks, + 0, |j| dec.layers_auth_paths[j].merkle_path.as_slice(), &dec.layers_evaluations_sym, &run.zetas, diff --git a/crypto/stark/src/tests/mod.rs b/crypto/stark/src/tests/mod.rs index 5c44cc5d4..a454041d9 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -4,6 +4,7 @@ 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; diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 596e59e5d..6db005b6a 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -561,10 +561,6 @@ pub trait IsStarkVerifier< return false; } - // `log2` of the LDE size: every tree's depth is a function of it (a - // verifier constant, never read from the proof). - let lde_log = domain.lde_length.trailing_zeros() as usize; - let terminal_offset = domain.coset_offset.pow(1u64 << layout.total_folds); let terminal_codeword = crate::fri::terminal::terminal_codeword_from_coeffs::( @@ -605,6 +601,8 @@ pub trait IsStarkVerifier< Self::verify_query_groups( proof, &layout, + &checks.fri, + i, &challenges.zetas, challenges.iotas[i], proof.query(i), @@ -612,7 +610,6 @@ pub trait IsStarkVerifier< &deep_poly_evaluations[i], &deep_poly_evaluations_sym[i], &terminal_codeword, - lde_log as u32, &roots_tables, ) }); @@ -799,7 +796,8 @@ pub trait IsStarkVerifier< /// 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 - /// `log2(lde) − i − 2` deep. Every authentication path must be exactly + /// 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 (C1b at `c = 0`: before that a path of any length was /// folded and compared with the root, design/CAP.md §9.4). /// @@ -826,13 +824,15 @@ pub trait IsStarkVerifier< { let options = air.options(); // A format this verifier cannot lay out rejects here, as in step 3. - let num_committed = Self::fri_termination_params(air, domain)?.num_committed; + let layout = Self::fri_termination_params(air, domain)?; + let num_committed = layout.num_committed; let lde_log = domain.lde_length.trailing_zeros() as usize; - let caps = StarkCaps::new( + // FRI layer depths from the layout: a group tree under a fold schedule. + let caps = StarkCaps::from_layout( options.format.merkle_cap, options.fri_number_of_queries, lde_log, - num_committed, + &layout, ); let fri_roots = proof.fri_layers_merkle_roots(); if fri_roots.len() != num_committed { @@ -933,6 +933,11 @@ pub trait IsStarkVerifier< fn verify_query_groups( proof: StarkProofView<'_, Field, FieldExtension, PI>, 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>, @@ -940,7 +945,6 @@ pub trait IsStarkVerifier< p0_eval: &FieldElement, p0_eval_sym: &FieldElement, terminal_codeword: &[FieldElement], - lde_log: u32, roots_tables: &[Vec>], ) -> bool where @@ -960,8 +964,8 @@ pub trait IsStarkVerifier< (p0_eval + p0_eval_sym) + &evaluation_point_inv * &zetas[0] * (p0_eval - p0_eval_sym); crate::fri::group::verify_query_groups::>( layout, - lde_log, - proof.fri_layers_merkle_roots(), + fri_checks, + query, |j| fri_decommitment.layer_auth_path(j), fri_decommitment.layers_evaluations_sym(), zetas, From 3a9512c02acaf81fbe7b75c898a774ec5f7a4066 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:14:34 -0300 Subject: [PATCH 27/73] test(stark,prover): S3 proof vectors with a Merkle cap at Q = 20 (F9) The (d) proof vectors ran at Q = 3, where the auto cap policy caps nothing (RULINGS 1: height 0 below 4 openings), so no vector exercised S1 with or without S3. Two formats are added for Keccak, Blake3 and RPX at Q = 20: cap_pair (cap=auto, fri=pair) and cap_dp (cap=auto, fri=dp), every tree capped at height 3. Their JSON also records the verifier's StarkCaps (trace/FRI depths and heights). The existing pair/dp/dp_3_1_3 files are byte-identical (proof_options takes the query count; the new JSON lines are written for capped formats only). cap_dp's FRI roots and zetas equal dp's: the cap moves no transcript value. --- crypto/stark/src/fri/vectors.rs | 61 ++++++++++++++---- crypto/stark/src/tests/zf_fri_vectors.rs | 2 +- crypto/stark/tests/vectors/zf_fri/README.md | 17 +++-- .../vectors/zf_fri/d_proof_blake3_cap_dp.json | 49 ++++++++++++++ .../vectors/zf_fri/d_proof_blake3_cap_dp.rkyv | Bin 0 -> 41480 bytes .../zf_fri/d_proof_blake3_cap_pair.json | 49 ++++++++++++++ .../zf_fri/d_proof_blake3_cap_pair.rkyv | Bin 0 -> 51752 bytes .../vectors/zf_fri/d_proof_keccak_cap_dp.json | 49 ++++++++++++++ .../vectors/zf_fri/d_proof_keccak_cap_dp.rkyv | Bin 0 -> 41480 bytes .../zf_fri/d_proof_keccak_cap_pair.json | 49 ++++++++++++++ .../zf_fri/d_proof_keccak_cap_pair.rkyv | Bin 0 -> 51752 bytes .../vectors/zf_fri/d_proof_rpx_cap_dp.json | 49 ++++++++++++++ .../vectors/zf_fri/d_proof_rpx_cap_dp.rkyv | Bin 0 -> 41480 bytes .../vectors/zf_fri/d_proof_rpx_cap_pair.json | 49 ++++++++++++++ .../vectors/zf_fri/d_proof_rpx_cap_pair.rkyv | Bin 0 -> 51752 bytes prover/src/tests/zf_rpx_vectors.rs | 2 +- 16 files changed, 357 insertions(+), 19 deletions(-) create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_dp.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_dp.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_pair.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_blake3_cap_pair.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_dp.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_dp.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_pair.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_keccak_cap_pair.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_dp.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_dp.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_pair.json create mode 100644 crypto/stark/tests/vectors/zf_fri/d_proof_rpx_cap_pair.rkyv diff --git a/crypto/stark/src/fri/vectors.rs b/crypto/stark/src/fri/vectors.rs index 1802ca384..3669c57cd 100644 --- a/crypto/stark/src/fri/vectors.rs +++ b/crypto/stark/src/fri/vectors.rs @@ -263,13 +263,19 @@ pub fn leaf_digests_json(hash_name: &str) -> VectorFile { // --------------------------------------------------------------------------- /// The (d) proof shape: `LogReadOnlyRAP` (ext3, one aux column), 2^10 rows, -/// blowup 4 (B = 12), k = 2, Q = 3, grinding 0, coset offset 3. +/// 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; -pub fn proof_options(format: ProofFormat) -> ProofOptions { +/// The query count of the capped (d) formats: the `auto` cap policy caps a +/// tree opened at least 20 times at height 3 (RULINGS 1), so a Q = 3 proof +/// carries no cap at all (REVIEW-FRI F9). +pub const CAPPED_QUERIES: usize = 20; + +pub fn proof_options(format: ProofFormat, queries: usize) -> ProofOptions { ProofOptions { blowup_factor: 4, - fri_number_of_queries: 3, + fri_number_of_queries: queries, coset_offset: 3, grinding_factor: 0, fri_final_poly_log_degree: 2, @@ -277,28 +283,46 @@ pub fn proof_options(format: ProofFormat) -> ProofOptions { } } -/// The formats of (d): `pair` (today), `dp` (the DP's schedule) and -/// `dp_3_1_3` (an explicit uneven schedule, to catch fold-count bugs). -pub fn proof_formats() -> Vec<(&'static str, ProofFormat)> { +/// 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 of REVIEW-FRI F9. +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), - ("dp", dp), + ("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, @@ -319,7 +343,7 @@ fn logup_case( m0: cols[4][0], }; ( - LogReadOnlyRAP::::new(&proof_options(format)), + LogReadOnlyRAP::::new(&proof_options(format, queries)), trace, pi, ) @@ -331,8 +355,8 @@ fn logup_case( /// opened values. pub fn proof_vectors(hash_name: &str) -> Vec { let mut out = Vec::new(); - for (fmt_name, format) in proof_formats() { - let (air, mut trace, pi) = logup_case(format); + for (fmt_name, format, queries) in proof_formats() { + let (air, mut trace, pi) = logup_case(format, queries); let proof = GenericProver::::prove( &air, &mut trace, @@ -362,8 +386,21 @@ pub fn proof_vectors(hash_name: &str) -> Vec { ); 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\": 3,\n \"grinding_factor\": 0,\n \"coset_offset\": 3," + " \"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) + .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 + ); + } let _ = writeln!( s, " \"legacy_encoding\": {},\n \"total_folds\": {},\n \"terminal_len\": {},\n \"schedule\": {:?},", diff --git a/crypto/stark/src/tests/zf_fri_vectors.rs b/crypto/stark/src/tests/zf_fri_vectors.rs index d3d01f05f..69827daf0 100644 --- a/crypto/stark/src/tests/zf_fri_vectors.rs +++ b/crypto/stark/src/tests/zf_fri_vectors.rs @@ -24,7 +24,7 @@ fn all() -> Vec { #[test] fn vectors_are_current() { let files = all(); - assert_eq!(files.len(), 4 + 2 * 3 * 2); + assert_eq!(files.len(), 4 + 2 * 5 * 2); let bad = check_or_write(&files, false); assert!( bad.is_empty(), diff --git a/crypto/stark/tests/vectors/zf_fri/README.md b/crypto/stark/tests/vectors/zf_fri/README.md index 4cbde2e1b..101da609e 100644 --- a/crypto/stark/tests/vectors/zf_fri/README.md +++ b/crypto/stark/tests/vectors/zf_fri/README.md @@ -77,10 +77,11 @@ 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}.{json,rkyv}`** — one small +**(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, grinding 0, coset +`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`, @@ -91,10 +92,16 @@ per query `iota`, the DEEP pair (`deep` = p₀(υ), `deep_sym` = p₀(−υ)), 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). +neighbouring exponents are what catch a fold-count off-by-one), and the +Merkle-cap pair (REVIEW-FRI F9): `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. ## Not here yet - (e) S2 one-row leaf digests and the input-tree root (H6, after S2). -- A vector with a Merkle cap (`Q ≥ 20` so `cap = auto` caps; REVIEW-FRI F9): - the cap is not implemented on this branch. 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 0000000000000000000000000000000000000000..017dfddb3d7e1584fafd95f36b70cb4226a2c8c3 GIT binary patch literal 41480 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, 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 0000000000000000000000000000000000000000..f0b663c10ad7279f2ba266703f7a0b6a4619e6ba GIT binary patch literal 51752 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, 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 0000000000000000000000000000000000000000..38608334e73a3bd92b8777346aed1b7675348541 GIT binary patch literal 41480 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 CHv#Mb literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9a7335edffc1486d4dd95e3cc1b1a162c641159b GIT binary patch literal 51752 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

NhD>l1zr)hp7B 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 Qj$@z)1@H`%JAvo_59#Wb{Qv*} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6ec58573742b1a956365391a0f5820637041e3d7 GIT binary patch literal 41480 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 aAMo<~d*%OIt=HVE2h7Vi-tx%H_x}$%$S6|) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dadf629177264deef1bb73999f35d9ec93ce0f36 GIT binary patch literal 51752 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 Vec { #[test] fn rpx_vectors_are_current() { let files = all(); - assert_eq!(files.len(), 1 + 3 * 2); + assert_eq!(files.len(), 1 + 5 * 2); let bad = check_or_write(&files, false); assert!( bad.is_empty(), From dc64ae3af33e373b0b6dc3c18f961ad82b523e28 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:20:42 -0300 Subject: [PATCH 28/73] fix(stark): drop the group-path verifier's now-unused proof parameter verify_query_groups read the layer roots off the proof; it authenticates with the per-tree checks since the previous commit, so the parameter was unused (a -D warnings lint failure). --- crypto/stark/src/verifier.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 6db005b6a..8ce6841bf 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -599,7 +599,6 @@ pub trait IsStarkVerifier< .zip(evaluation_point_inverse) .all(|(i, eval)| { Self::verify_query_groups( - proof, &layout, &checks.fri, i, @@ -931,7 +930,6 @@ pub trait IsStarkVerifier< // Crate-internal layout type on a default method, as `fri_termination_params`. #[allow(clippy::too_many_arguments, private_interfaces)] fn verify_query_groups( - proof: StarkProofView<'_, Field, FieldExtension, PI>, 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 From 942201d7559f367efbb43406e90bcb3c53e83c24 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:21:35 -0300 Subject: [PATCH 29/73] feat(math-cuda): S3 group-leaf FRI layers on the device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the device half of higher-arity committed FRI layers (S3, design/FRI.md §5): - `{keccak,blake3,rpx}_fri_group_leaves_ext3`: leaf g hashes the 2^d consecutive ext3 values from g*2^d of an interleaved eval vector, the host `Batched` leaf over the group (at d = 1 exactly the pair-leaf kernels' byte/felt stream). Registered in the backend. - `FriCommitState::fold_and_commit_group(zeta_powers, group_log, want_host)`: zeta_powers.len() binary folds (the existing fri_fold_ext3 and twiddle update), then a group-leaf commit of the last output. The fold count and the group size are separate parameters: layer j is reached by d_{j-1} folds and grouped by d_j. Intermediate codewords are released. - `FriCommitState::fold_to_host`: the uncommitted folds into the terminal. - `build_fri_group_tree_from_evals_ext3`: a parity harness over the same kernels. The legacy `fold_and_commit_layer` and pair-leaf kernels are unchanged. tests/fri_group_tree.rs (GPU): the group trees equal the host trees node for node (Keccak, Blake3, d = 1..6); the KAT codeword's first leaf and layer root equal the checked-in vectors (c) under Keccak, Blake3 and RPX; the device folds equal vector (b); a d = 1 group tree equals the pair-leaf tree under all three hashes. --- crypto/math-cuda/kernels/blake3.cu | 23 ++ crypto/math-cuda/kernels/keccak.cu | 31 +++ crypto/math-cuda/kernels/rpx.cu | 23 ++ crypto/math-cuda/src/device.rs | 9 + crypto/math-cuda/src/fri.rs | 242 ++++++++++++++++ crypto/math-cuda/tests/fri_group_tree.rs | 336 +++++++++++++++++++++++ 6 files changed, 664 insertions(+) create mode 100644 crypto/math-cuda/tests/fri_group_tree.rs diff --git a/crypto/math-cuda/kernels/blake3.cu b/crypto/math-cuda/kernels/blake3.cu index 3b30e25f6..efdb476ad 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 diff --git a/crypto/math-cuda/kernels/keccak.cu b/crypto/math-cuda/kernels/keccak.cu index 300bc5a7a..35666cc06 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. // diff --git a/crypto/math-cuda/kernels/rpx.cu b/crypto/math-cuda/kernels/rpx.cu index d9bfb5587..b2e92b533 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, diff --git a/crypto/math-cuda/src/device.rs b/crypto/math-cuda/src/device.rs index 9d010151a..38776ac5f 100644 --- a/crypto/math-cuda/src/device.rs +++ b/crypto/math-cuda/src/device.rs @@ -213,6 +213,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, @@ -232,6 +234,8 @@ pub struct Backend { 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, @@ -254,6 +258,8 @@ pub struct Backend { 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, @@ -879,6 +885,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")?, @@ -892,6 +899,7 @@ impl Backend { 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")?, @@ -912,6 +920,7 @@ impl Backend { 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..24f78d9e4 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` (FRI.md §3.1). 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/tests/fri_group_tree.rs b/crypto/math-cuda/tests/fri_group_tree.rs new file mode 100644 index 000000000..1a846d3a4 --- /dev/null +++ b/crypto/math-cuda/tests/fri_group_tree.rs @@ -0,0 +1,336 @@ +//! S3 group-leaf FRI layers on the device (FRI.md §5, lane I-FRI-D). +//! +//! - 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" + ); + } + } +} From 67890ae932dd7c9ba723fef00fd97bd17fc06b6b Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:21:51 -0300 Subject: [PATCH 30/73] feat(stark): the device FRI arms run the S3 group encoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lifts I-FRI-H's legacy-only gate on the device FRI arms (design/FRI.md §5, D1): - `try_fri_commit_gpu` / `try_fri_commit_gpu_from_dev` take the proof format's `FriFoldLayout` (built once by the prover, as the verifier builds it) instead of re-deriving today's. The legacy encoding runs today's loop unchanged (debug-asserted equal to `FriFoldLayout::new`); the group encoding runs `fri_commit_gpu_drive_groups`, the device twin of `commit_phase_with_layout`'s pending-fold loop: sample zeta, fold d_{j-1} times with zeta, zeta^2, ... (squared on the host as `fold_times` does), commit groups of 2^{d_j}, append the root; the final folds go to the host for the terminal coefficients. One-row layouts are declined before any sampling (not implemented on the device). - `try_fri_query_phase_gpu_groups`: per layer, paths at leaf = p >> d_j gathered on device, the whole group read from the host evals or gathered off the resident evals (`gather_ext3_at` over the group positions). - The prover's device DEEP->FRI arm and `commit_phase_with_layout`'s device arm now gate on `one_row` only; `query_phase_with_layout` tries the device group gather before the host walk (`query_phase_groups_host`). - `commit_phase_cpu_with_layout`: the CPU loop split out, unchanged, as the parity reference. Default format: the pair loop and its bytes are untouched; the new `parity_legacy_encoding_*` and the device-proved `pair` vector pin it. Tests (fri::device_parity + tests::zf_fri_device_tests, cuda, GPU, ignored, box only): device vs host CPU loop at the 29 pinned shapes (every distinct DP schedule for B <= 23 at T 4/9/10, Q 3/110, cap off/auto, plus uneven and d = 6 extras), at production sizes (B 14/19/21/23 T = 9, B 22 T = 10, pair B 21), with device-only layers, and the pair encoding: coefficients, roots, layer evals, transcript, every opened value and path. And the (d) vector proofs proved on the device equal the checked-in CPU bytes (Keccak, Blake3), with the device FRI counter required to move once per proof. `dp_shapes_are_pinned` runs anywhere. --- crypto/stark/src/fri/device_parity.rs | 367 ++++++++++++++++++ crypto/stark/src/fri/mod.rs | 71 +++- crypto/stark/src/fri/terminal.rs | 3 +- crypto/stark/src/gpu_lde.rs | 359 ++++++++++++++++- crypto/stark/src/proof/options.rs | 12 +- crypto/stark/src/prover.rs | 8 +- crypto/stark/src/tests/mod.rs | 2 + crypto/stark/src/tests/zf_fri_device_tests.rs | 156 ++++++++ 8 files changed, 948 insertions(+), 30 deletions(-) create mode 100644 crypto/stark/src/fri/device_parity.rs create mode 100644 crypto/stark/src/tests/zf_fri_device_tests.rs diff --git a/crypto/stark/src/fri/device_parity.rs b/crypto/stark/src/fri/device_parity.rs new file mode 100644 index 000000000..63816069d --- /dev/null +++ b/crypto/stark/src/fri/device_parity.rs @@ -0,0 +1,367 @@ +//! 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, 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]] = &[ + &[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. +/// +/// `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 layout = FriFoldLayout::for_options(lde_log, blowup_log, options) + .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 {}, 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 pair indices and both ends of the range. + let half = n / 2; + let mut iotas: Vec = (0..40) + .map(|_| (splitmix64(&mut rng) % half as u64) as usize) + .collect(); + iotas.push(0); + iotas.push(half - 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 +} + +/// 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/mod.rs b/crypto/stark/src/fri/mod.rs index 05af45940..dd7401f61 100644 --- a/crypto/stark/src/fri/mod.rs +++ b/crypto/stark/src/fri/mod.rs @@ -1,5 +1,7 @@ #[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; @@ -95,8 +97,10 @@ where /// families share, which today's layer trees already rely on (built with /// `H::Pair`, verified with `H::Batched`). /// -/// Every device FRI arm is taken only for the legacy encoding: a group-encoded -/// layout always runs this CPU loop. +/// 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 are not +/// implemented on the device and always take the CPU loop +/// ([`commit_phase_cpu_with_layout`]). #[allow(clippy::type_complexity, clippy::too_many_arguments)] pub(crate) fn commit_phase_with_layout< F: IsFFTField + IsSubFieldOf + 'static, @@ -104,7 +108,7 @@ pub(crate) fn commit_phase_with_layout< T: IsStarkTranscript + Clone, H: StarkHash, >( - mut evals: Vec>, + evals: Vec>, transcript: &mut T, coset_offset: &FieldElement, domain_size: usize, @@ -125,7 +129,7 @@ where // error restores state and lets the CPU loop below run as if the GPU // had never been tried. #[cfg(feature = "cuda")] - if layout.is_legacy() { + if !layout.one_row { // 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 @@ -139,12 +143,47 @@ where 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. @@ -340,8 +379,9 @@ where /// [`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, FRI.md §3.4) and the path of -/// `leaf = p >> d_j`, then moves to `p >> d_j`. Host layers only: a group -/// layout never takes the device commit. +/// `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], @@ -353,6 +393,25 @@ where 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() diff --git a/crypto/stark/src/fri/terminal.rs b/crypto/stark/src/fri/terminal.rs index e2703a6ed..d59d28898 100644 --- a/crypto/stark/src/fri/terminal.rs +++ b/crypto/stark/src/fri/terminal.rs @@ -132,7 +132,8 @@ impl FriFoldLayout { } /// Whether this layout uses today's FRI encoding (see - /// [`Self::legacy_encoding`]). Every device FRI arm is gated on this. + /// [`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 } diff --git a/crypto/stark/src/gpu_lde.rs b/crypto/stark/src/gpu_lde.rs index 1847e6804..0171e6fc7 100644 --- a/crypto/stark/src/gpu_lde.rs +++ b/crypto/stark/src/gpu_lde.rs @@ -3672,7 +3672,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, @@ -3680,8 +3680,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, @@ -3740,7 +3812,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, @@ -3748,19 +3819,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>)> @@ -3811,15 +3884,23 @@ 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 are +/// not implemented on the device and return `None` before any sampling. +#[allow(clippy::type_complexity, clippy::too_many_arguments)] fn fri_commit_gpu_drive( mut state: math_cuda::fri::FriCommitState, transcript: &mut T, @@ -3827,6 +3908,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 @@ -3852,12 +3934,18 @@ 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.one_row + || 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 @@ -3866,6 +3954,25 @@ where if layout.total_folds == 0 || layout.terminal_len < 2 { return None; } + 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); @@ -3944,6 +4051,106 @@ 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. +/// 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`. + let mut pending: u32 = 1; + for &d in &layout.schedule { + // <<<< Receive challenge zeta_j + let zeta: FieldElement = transcript.sample_field_element(); + let powers = zeta_powers_raw(&zeta, pending); + 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); + 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 @@ -4049,6 +4256,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 diff --git a/crypto/stark/src/proof/options.rs b/crypto/stark/src/proof/options.rs index 5e12f728f..c51537f9b 100644 --- a/crypto/stark/src/proof/options.rs +++ b/crypto/stark/src/proof/options.rs @@ -248,14 +248,16 @@ impl FromStr for OneRowMode { /// lands. pub const MERKLE_CAP_IMPLEMENTED: bool = true; -/// `FriMode::Dp` (S3) is implemented on the HOST paths only: +/// `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 every device FRI arm (DEEP→FRI on device, the device -/// layer commit, the device query gather) is taken only for `Pair`; a `Dp` -/// table runs the CPU FRI loop (DEEP may still run on the device). +/// - 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`). /// -/// NOT implemented: device group-leaf FRI (lane I-FRI-D), the in-guest (LFM) +/// NOT implemented: the in-guest (LFM) /// verifier of a `Dp` proof (lane I-FRI-G: `lfm::fri::FriShape` still derives /// the legacy layout, so an LFM wrap or node over a `Dp` proof fails at emit /// time), and the RV64 recursion guest (default-only by RULINGS 11; it refuses diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index b8fadf9b4..6ba0a73bf 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -2811,10 +2811,11 @@ pub trait IsStarkProver< let __ps_df = crate::prove_split::mark(); #[cfg(feature = "instruments")] let t_sub = Instant::now(); - // Device FRI implements the legacy encoding only: any other format - // takes the host arm below (which may still compute DEEP on device). + // Device FRI implements the pair and group (S3) encodings; a one-row + // layout (not implemented on the device) takes the host arm below + // (which may still compute DEEP on device). #[cfg(feature = "cuda")] - let precomputed_fri = if !fri_layout.is_legacy() { + let precomputed_fri = if fri_layout.one_row { None } else { Self::try_compute_deep_dev( @@ -2839,6 +2840,7 @@ pub trait IsStarkProver< &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(), ) diff --git a/crypto/stark/src/tests/mod.rs b/crypto/stark/src/tests/mod.rs index 5c44cc5d4..aef984f28 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -24,5 +24,7 @@ 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; 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..4485c42e0 --- /dev/null +++ b/crypto/stark/src/tests/zf_fri_device_tests.rs @@ -0,0 +1,156 @@ +//! S3 on the device (FRI.md §5, lane I-FRI-D, D1): 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 (22). + &[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], + &[4, 3, 3], + &[4, 3, 3, 3], + &[4], + // EXTRA_SHAPES (7). + &[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 (FRI.md §10 (d): `pair`, `dp`, `dp_3_1_3`) 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 mut files = proof_vectors::("keccak"); + files.extend(proof_vectors::("blake3")); + let device_commits = crate::gpu_lde::gpu_fri_calls() - before; + println!( + "FRIDEV vector proofs: {} files, {device_commits} device FRI commits", + files.len() + ); + assert_eq!(files.len(), 2 * 3 * 2); + assert_eq!( + device_commits, 6, + "every vector proof must take the device FRI commit (lower LAMBDA_VM_GPU_LDE_THRESHOLD)" + ); + let bad = check_or_write(&files, false); + assert!( + bad.is_empty(), + "device-proved vectors differ from the checked-in CPU bytes: {bad:?}" + ); +} From d9d67698365dfa7519bb03980313011a1edf3593 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:21:52 -0300 Subject: [PATCH 31/73] test(prover): S3 device parity under RPX and a VM dp proof on the device - tests::zf_rpx_device_tests (cuda, GPU, ignored): the stark crate's device-vs-host FRI parity cases under the production RPX pin (29 shapes, production sizes, device-only layers, pair encoding), and the RPX (d) vector proofs proved on the device equal the checked-in CPU bytes (device FRI counter must move 3 times). - zf_vm_dp_tests::a_vm_proof_round_trips_at_fri_dp_on_the_device (cuda): the test_mul_8 VM proof at fri = dp takes the device group FRI commit (counter must move) and verifies. --- prover/src/tests/mod.rs | 2 + prover/src/tests/zf_rpx_device_tests.rs | 81 +++++++++++++++++++++++++ prover/src/tests/zf_vm_dp_tests.rs | 40 ++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 prover/src/tests/zf_rpx_device_tests.rs diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index 8d5e7bb0c..ff43bc4e8 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -130,6 +130,8 @@ pub mod whir_byte_gate; pub mod whir_hash_tests; #[cfg(test)] pub mod whir_identity_tests; +#[cfg(all(test, feature = "cuda"))] +pub mod zf_rpx_device_tests; #[cfg(test)] pub mod zf_rpx_golden_tests; #[cfg(test)] 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..5041b5df3 --- /dev/null +++ b/prover/src/tests/zf_rpx_device_tests.rs @@ -0,0 +1,81 @@ +//! S3 on the device under the production RPX pin (lane I-FRI-D, D1): 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 +//! ``` + +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`, 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 files = proof_vectors::("rpx"); + let device_commits = stark::gpu_lde::gpu_fri_calls() - before; + println!( + "FRIDEV rpx vector proofs: {} files, {device_commits} device FRI commits", + files.len() + ); + assert_eq!(files.len(), 3 * 2); + assert_eq!( + device_commits, 3, + "every vector proof must take the device FRI commit (lower LAMBDA_VM_GPU_LDE_THRESHOLD)" + ); + let bad = check_or_write(&files, false); + assert!( + bad.is_empty(), + "device-proved RPX vectors differ from the checked-in CPU bytes: {bad:?}" + ); +} diff --git a/prover/src/tests/zf_vm_dp_tests.rs b/prover/src/tests/zf_vm_dp_tests.rs index e4b4ff7e9..1feb57414 100644 --- a/prover/src/tests/zf_vm_dp_tests.rs +++ b/prover/src/tests/zf_vm_dp_tests.rs @@ -57,3 +57,43 @@ fn a_vm_proof_round_trips_at_fri_dp() { "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" + ); +} From f004a5a37bb8122fcad6f7b94e34447e6b0267fc Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:24:04 -0300 Subject: [PATCH 32/73] feat(stark): S2 one-row openings with a committed FRI input on the CPU prover and host verifier (H4) Behind LAMBDA_VM_ZF_ONE_ROW (ProofFormat.one_row = Off | On | Auto), the default unchanged byte for byte (zf_golden_tests green): - leaf_layout.rs (new): LeafLayout {RowPair, Row}, the ONE query-index-to-rows helper (query_rows, REVIEW-FRI F7) with the query bound (lde/2 vs lde) and tree depth; the per-table `auto` rule (RULINGS 6, REVIEW-FRI F5): table_openings_cost_q prices every trace tree (leaf + walk - cap gain) and the FRI chain (the schedule objective of RULINGS 13, plus fold 0 for row pairs) under both layouts from the AIR's committed widths; one row iff strictly cheaper. table_leaf_layout(air, trace_length) is what the prover and the verifier both call. The in-guest DEEP arithmetic is not priced (it only ever favours row pairs). - Prover: rows_per_leaf through every CPU commit (main, precomputed split, aux, composition); the DEEP codeword committed as FRI layer 0 with group leaves and its root absorbed BEFORE the first challenge (pending fold 0); query indexes uniform over all of D0 (bound N); one-row openings at every site (evaluations_sym empty). Device arms are row-pair only: a one-row table never goes device-only, keeps its aux build on the host, and skips the fused main/split/aux/composition device commits and device openings (asserted); DEEP->FRI on device was already legacy-only. - Verifier: the layout resolved per table; widths check requires empty sym slots under one row; replay absorbs the input root with no challenge before it; DEEP at ONE point; the group FRI loop starts at layer 0 with the input-slot check group0[slot] == DEEP(x_r); zero folds check terminal[r] == DEEP(x_r). verify_query_groups now authenticates through the per-tree TreeChecks and StarkCaps::with_depths takes the layout's depths, so caps compose with dp and one_row (REVIEW-FRI F9). - AIR::precomputed_commitment_for(layout) (default: row pairs only) and LazyCommitment::with_one_row; a table with no root for its layout is a ProvingError::PrecomputedCommitmentMissing and a verifier reject (RULINGS 14). The precomputed-tree cache is keyed by (root, rows_per_leaf). - FriFormat::from_options / FriFoldLayout::for_options take the resolved one_row (FriFormatError::OneRowNotImplemented removed); num_zetas. - LFM FriShape::from_options refuses a one-row inner format (G3 not built). Tests (one_row_tests): U6 at one_row x {pair, dp} x fold counts 0..9 x blowup 2/4, explicit uneven schedules, ext3+aux, multi-table incl. auto, archived path; T4-T6 tampers; M3 (query bound) load-bearing; the input-root transcript KAT; preprocessed one-row root and RULINGS 14 miss; the auto rule (strict comparison + pinned choices); the cap x fri x one_row matrix at Q=24. --- crypto/stark/src/fri/group.rs | 36 +- crypto/stark/src/fri/mod.rs | 30 +- crypto/stark/src/fri/schedule.rs | 27 +- crypto/stark/src/fri/terminal.rs | 29 +- crypto/stark/src/fri/vectors.rs | 2 +- crypto/stark/src/leaf_layout.rs | 298 +++++++ crypto/stark/src/lib.rs | 1 + crypto/stark/src/lookup.rs | 63 ++ crypto/stark/src/merkle_caps.rs | 26 + crypto/stark/src/prover.rs | 423 +++++++--- crypto/stark/src/tests/fri_group_tests.rs | 24 +- crypto/stark/src/tests/fri_schedule_tests.rs | 34 +- crypto/stark/src/tests/mod.rs | 1 + crypto/stark/src/tests/one_row_tests.rs | 782 ++++++++++++++++++ crypto/stark/src/tests/opening_width_tests.rs | 20 + crypto/stark/src/traits.rs | 18 + crypto/stark/src/verifier.rs | 324 ++++++-- prover/src/lfm/fri.rs | 14 + 18 files changed, 1920 insertions(+), 232 deletions(-) create mode 100644 crypto/stark/src/leaf_layout.rs create mode 100644 crypto/stark/src/tests/one_row_tests.rs diff --git a/crypto/stark/src/fri/group.rs b/crypto/stark/src/fri/group.rs index f00cce848..fae8e4aef 100644 --- a/crypto/stark/src/fri/group.rs +++ b/crypto/stark/src/fri/group.rs @@ -31,7 +31,6 @@ //! 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::cap::CappedRoot; use crypto::merkle_tree::traits::IsStreamingLeafBackend; use math::fft::bit_reversing::reverse_index; use math::field::element::FieldElement; @@ -40,6 +39,7 @@ 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, @@ -140,12 +140,22 @@ where /// 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` against `roots[j]` (path `paths(j)`, exact depth), the -/// slot check `group[p & (2^{d_j} − 1)] == v` holds, and `v` becomes the group -/// fold with `zetas[j + 1]`; finally `terminal[p] == v`. +/// `leaf = p >> d_j` by `fri_checks[j]` (path `paths(j)`; the exact depth and +/// any cap are the check's, built once per tree from the verifier's +/// constants), 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 (fold 0 already applied by the caller); +/// 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 @@ -154,8 +164,8 @@ where #[allow(clippy::too_many_arguments)] pub(crate) fn verify_query_groups<'p, F, E, B>( layout: &FriFoldLayout, - lde_log: u32, - roots: &[Commitment], + fri_checks: &[TreeCheck<'_>], + query: usize, paths: impl Fn(usize) -> &'p [Commitment], values: &[FieldElement], zetas: &[FieldElement], @@ -171,12 +181,13 @@ where FieldElement: AsBytes + Sync + Send, B: IsStreamingLeafBackend, { - if roots.len() != layout.num_committed + if fri_checks.len() != layout.num_committed || values.len() != layout.opened_values_per_query() - || zetas.len() != layout.num_committed + 1 + || 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; @@ -194,10 +205,7 @@ where } // (1) the group is the leaf, authenticated with the exact depth. let leaf_hash = B::hash_data_from_slices(group, &[]); - let depth = layout.layer_depth(lde_log, j) as usize; - if !CappedRoot::uncapped(&roots[j], depth).verify::(paths(j), leaf, leaf_hash) - && !mutated(2) - { + if !fri_checks[j].verify::(query, paths(j), leaf, leaf_hash) && !mutated(2) { ok = false; } // (3) fold: x_g⁻¹ = y⁻¹ · ω_{2^d}^{br_d(slot)}. @@ -213,7 +221,7 @@ where 0 }; let x_g_inv = &y_inv * &table[br_slot]; - v = group_fold::(group, &zetas[j + 1], &x_g_inv, table); + v = group_fold::(group, &zetas[j + zeta_offset], &x_g_inv, table); for _ in 0..d { y_inv = y_inv.square(); } diff --git a/crypto/stark/src/fri/mod.rs b/crypto/stark/src/fri/mod.rs index 05af45940..71955ea45 100644 --- a/crypto/stark/src/fri/mod.rs +++ b/crypto/stark/src/fri/mod.rs @@ -88,6 +88,12 @@ where /// `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 @@ -156,9 +162,6 @@ where layout.total_folds, evals.len().trailing_zeros() - layout.terminal_len.trailing_zeros() ); - // One-row layouts (S2) commit the DEEP codeword itself as layer 0; they are - // refused before a layout is built (`FriFormat::from_options`). - debug_assert!(!layout.one_row, "one-row FRI layouts are not implemented"); let num_committed = layout.num_committed; // Inverse twiddle factors for evaluation-form folding: per-layer working @@ -167,16 +170,22 @@ where 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`. - let mut pending: u32 = 1; + // 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 (FRI.md §7.3; 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 &d in &layout.schedule { - // <<<< Receive challenge 𝜁ₖ - let zeta = transcript.sample_field_element(); + if pending > 0 { + // <<<< Receive challenge 𝜁ₖ + let zeta = transcript.sample_field_element(); - // Fold `pending` times with 𝜁, 𝜁², … (evaluation form, no FFT). - fold_times(&mut evals, &zeta, pending, &mut inv_twiddles); + // Fold `pending` times with 𝜁, 𝜁², … (evaluation form, no FFT). + fold_times(&mut evals, &zeta, pending, &mut inv_twiddles); + } let merkle_tree = if layout.is_legacy() { // Build the Merkle tree from consecutive pairs. @@ -199,7 +208,8 @@ where } // The final folds to reach the terminal codeword (size terminal_len), - // unless already there (total_folds == 0 means initial_len == 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(); diff --git a/crypto/stark/src/fri/schedule.rs b/crypto/stark/src/fri/schedule.rs index 32dc652b0..2a6ce3022 100644 --- a/crypto/stark/src/fri/schedule.rs +++ b/crypto/stark/src/fri/schedule.rs @@ -44,7 +44,7 @@ use crypto::merkle_tree::cap::{AUTO_WEIGHTS, CapPolicy, CapWeights, cap_gain}; -use crate::proof::options::{FriMode, FriScheduleOverride, OneRowMode, ProofOptions}; +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; @@ -284,9 +284,6 @@ pub fn legacy_fri_schedule(b0: u32, terminal_log: u32) -> Vec { /// Why a proof format cannot be laid out for a table. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FriFormatError { - /// `one_row` is not `Off`: one-row openings (S2) are not implemented on - /// this build. Refused rather than silently proving the row-pair layout. - OneRowNotImplemented, /// The schedule override does not cover this table's committed folds /// exactly, or has an exponent outside `1..=FRI_SCHEDULE_DMAX`. ScheduleOverrideMismatch, @@ -295,9 +292,6 @@ pub enum FriFormatError { impl core::fmt::Display for FriFormatError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::OneRowNotImplemented => { - f.write_str("one-row openings (LAMBDA_VM_ZF_ONE_ROW) are not implemented") - } Self::ScheduleOverrideMismatch => { f.write_str("the FRI schedule override does not cover this table's committed folds") } @@ -332,21 +326,18 @@ impl FriFormat { schedule_override: None, }; - /// The format of a table proved under `options`. - /// - /// Errors on a one-row mode other than `Off` (not implemented here: the - /// per-table `Auto` resolution and the one-row layout arrive with S2). - pub fn from_options(options: &ProofOptions) -> Result { - if options.format.one_row != OneRowMode::Off { - return Err(FriFormatError::OneRowNotImplemented); - } - Ok(Self { + /// 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: false, + 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 diff --git a/crypto/stark/src/fri/terminal.rs b/crypto/stark/src/fri/terminal.rs index e2703a6ed..dd0247afe 100644 --- a/crypto/stark/src/fri/terminal.rs +++ b/crypto/stark/src/fri/terminal.rs @@ -95,15 +95,17 @@ impl FriFoldLayout { } /// The layout of a table proved under `options` over an LDE of - /// `2^lde_log` with blowup `2^blowup_log`: what the prover and the host - /// verifier both build. The format comes from `options` — a verifier-side - /// constant — never from a proof. + /// `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)?; + let fmt = FriFormat::from_options(options, one_row); Self::for_format( lde_log, blowup_log, @@ -149,6 +151,25 @@ impl FriFoldLayout { 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 { diff --git a/crypto/stark/src/fri/vectors.rs b/crypto/stark/src/fri/vectors.rs index 1802ca384..174d3ab2c 100644 --- a/crypto/stark/src/fri/vectors.rs +++ b/crypto/stark/src/fri/vectors.rs @@ -353,7 +353,7 @@ pub fn proof_vectors(hash_name: &str) -> Vec { .expect("rkyv") .to_vec(); let lde_log = PROOF_ROWS.trailing_zeros() + 2; - let layout = FriFoldLayout::for_options(lde_log, 2, air.options()).expect("layout"); + let layout = FriFoldLayout::for_options(lde_log, 2, air.options(), false).expect("layout"); let stem = format!("d_proof_{hash_name}_{fmt_name}"); let mut s = format!( diff --git a/crypto/stark/src/leaf_layout.rs b/crypto/stark/src/leaf_layout.rs new file mode 100644 index 000000000..7bb30fea8 --- /dev/null +++ b/crypto/stark/src/leaf_layout.rs @@ -0,0 +1,298 @@ +//! The trace-tree leaf layout of one table's proof (S2, design/FRI.md §7). +//! +//! 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 +//! (REVIEW-FRI F7): 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, fri_schedule_cost_q}; +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 + /// (FRI.md §7.7 (i): 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_UNDER_ONE_ROW.load(core::sync::atomic::Ordering::SeqCst) => { + 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 (REVIEW-FRI F7). + 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 (FRI.md §10), test builds only: sample one-row query indexes +/// over the row-pair bound `N / 2`. 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); the tests that set it hold +/// `one_row_tests::M3_LOCK`, and every other proof stays valid while it is set. +#[cfg(test)] +pub(crate) static M3_PAIR_BOUND_UNDER_ONE_ROW: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + +/// 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, +} + +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::(); + Self { + precomputed: precomputed as u64, + main: main as u64, + aux: (aux as u64).saturating_mul(ext), + composition: (parts as u64).saturating_mul(ext), + } + } +} + +/// 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`, and the uncommitted fold 0 costs one fold +/// and one twiddle step. One row: every leaf holds one row and is `lde_log` +/// deep, and the FRI chain (layer 0 = the committed DEEP codeword) starts at +/// `lde_log`. The in-guest DEEP arithmetic (two points vs one) is NOT priced: +/// it is not a term of the shared objective, and leaving it out only ever +/// favours today's layout. +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 b0 = crate::fri::schedule::fri_chain_start(lde_log, one_row); + let schedule = fmt.schedule(lde_log, terminal_log); + let chain = fri_schedule_cost_q(b0, &schedule, q, cap).unwrap_or(u64::MAX); + let fold0 = if !one_row && lde_log > terminal_log { + q.saturating_mul(FRI_COST_WEIGHTS.fold + FRI_COST_WEIGHTS.twiddle) + } else { + 0 + }; + trees.saturating_add(chain).saturating_add(fold0) +} + +/// RULINGS 6's `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 1154742cb..2efb21306 100644 --- a/crypto/stark/src/lib.rs +++ b/crypto/stark/src/lib.rs @@ -21,6 +21,7 @@ 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; diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index daba6fb0d..fb8239c88 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, RULINGS 14). #[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< @@ -1552,6 +1603,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 index 7492894ad..a4bc68ba9 100644 --- a/crypto/stark/src/merkle_caps.rs +++ b/crypto/stark/src/merkle_caps.rs @@ -74,6 +74,32 @@ impl StarkCaps { } } + /// 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) diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index b8fadf9b4..5bf22abe2 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 (RULINGS 14): 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 (FRI.md §7.5.5). +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,20 @@ 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. The device arms build + /// row-pair leaves only, so a one-row table (S2) always takes the CPU arm + /// (device one-row trees are lane I-FRI-D's D2). + #[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,8 +1642,9 @@ 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() { + if precomputed.is_none() && !residency.recomputes_main_lde() && !layout.is_one_row() { let (trace_slice, num_cols) = trace.main_data_row_major(); let n = if num_cols > 0 { trace_slice.len() / num_cols @@ -1622,6 +1702,7 @@ pub trait IsStarkProver< #[cfg(feature = "cuda")] if let Some((expected_precomputed_root, num_precomputed)) = precomputed && !residency.recomputes_main_lde() + && !layout.is_one_row() { let (trace_slice, num_cols) = trace.main_data_row_major(); let n = if num_cols > 0 { @@ -1635,7 +1716,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")] @@ -1680,6 +1764,7 @@ pub trait IsStarkProver< if cache_ok { precomputed_tree_cache_put::>( expected_precomputed_root, + rows_per_leaf, Arc::clone(&tree), ); } @@ -1727,8 +1812,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 +1832,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 +1844,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 +1861,7 @@ pub trait IsStarkProver< if cache_ok { precomputed_tree_cache_put::>( expected_precomputed_root, + rows_per_leaf, Arc::clone(&tree), ); } @@ -1778,11 +1869,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 +2614,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 device composition trees are + // row-pair only, so a one-row table commits on the host. + 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 @@ -2533,6 +2629,7 @@ pub trait IsStarkProver< match round_1_result .lde_trace .gpu_composition_parts() + .filter(|_| !leaf_layout.is_one_row()) .and_then(|h| { crate::gpu_lde::try_build_comp_poly_tree_gpu_from_dev::< FieldExtension, @@ -2540,10 +2637,14 @@ pub trait IsStarkProver< >(h) }) .or_else(|| { - crate::gpu_lde::try_build_comp_poly_tree_gpu::< - FieldExtension, - H::Batched, - >(&lde_composition_poly_parts_evaluations) + (!leaf_layout.is_one_row()) + .then(|| { + crate::gpu_lde::try_build_comp_poly_tree_gpu::< + FieldExtension, + H::Batched, + >(&lde_composition_poly_parts_evaluations) + }) + .flatten() }) { 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); @@ -2764,10 +2865,13 @@ pub trait IsStarkProver< // 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}")))?; @@ -2938,7 +3042,7 @@ 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 mut query_list = fri::query_phase_with_layout::(&fri_layers, &iotas, &fri_layout); @@ -2948,18 +3052,24 @@ pub trait IsStarkProver< .map(|layer| layer.merkle_tree.root) .collect(); - let mut 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 (design/CAP.md §4.2): 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. - let caps = crate::merkle_caps::StarkCaps::new( + let lde_log = domain_size.trailing_zeros(); + let caps = crate::merkle_caps::StarkCaps::with_depths( air.options().format.merkle_cap, number_of_queries, - domain_size.trailing_zeros() as usize, - fri_layers.len(), + leaf_layout.tree_depth(lde_log as usize), + fri_layout.layer_depths(lde_log), ); if caps.any() { Self::embed_stark_caps( @@ -3191,14 +3301,17 @@ pub trait IsStarkProver< } } + /// 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::>() } @@ -3481,6 +3594,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, @@ -3489,28 +3603,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(), } } @@ -3529,29 +3654,13 @@ pub trait IsStarkProver< 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 { + // Device composition trees exist for row-pair tables only. + 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, + LeafLayout::RowPair, + ) } /// Computes values and validity proofs of the evaluations of trace polynomials at @@ -3562,6 +3671,7 @@ pub trait IsStarkProver< domain: &Domain, tree: &MerkleTree>, challenge: usize, + leaf_layout: LeafLayout, gather: G, ) -> PolynomialOpenings where @@ -3569,16 +3679,18 @@ 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(), } } @@ -3599,11 +3711,13 @@ pub trait IsStarkProver< FieldElement: AsBytes + Sync + Send, G: Fn(usize) -> Vec>, { - let domain_size = domain.lde_roots_of_unity_coset.len() as u64; + // Device trees exist for row-pair tables only. + let (row, sym) = + LeafLayout::RowPair.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(), } } @@ -3696,6 +3810,7 @@ pub trait IsStarkProver< ncols: usize, col_range: std::ops::Range, what: &str, + leaf_layout: LeafLayout, gather: G, ) -> PolynomialOpenings where @@ -3703,6 +3818,12 @@ pub trait IsStarkProver< FieldElement: AsBytes + Sync + Send, G: Fn(usize) -> Vec>, { + // Device trees and gathers are row-pair only: a one-row table never + // has them (its commits took the CPU arms), so this is the host walk. + assert!( + !leaf_layout.is_one_row() || dev_proofs.is_none(), + "R4 {what} opening: a one-row table has a device-resident tree" + ); let Some(proofs) = dev_proofs else { assert!( !lde_trace.host_trace_empty(), @@ -3717,7 +3838,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 { @@ -3739,8 +3860,8 @@ pub trait IsStarkProver< // 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 (r_even, r_odd) = LeafLayout::RowPair.query_rows(challenge, domain_size as usize); + let r_odd = r_odd.expect("a row pair has a symmetric row"); assert_eq!( even, gather(r_even), @@ -3755,12 +3876,14 @@ pub trait IsStarkProver< 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, @@ -3785,12 +3908,15 @@ pub trait IsStarkProver< 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, - ] + let (row, sym) = LeafLayout::RowPair.query_rows(c, domain_size as usize); + [row as u32, sym.unwrap_or(row) as u32] }) .collect(); + // Every device arm below is row-pair only: a one-row table (S2) has no + // device-resident tree (its commits took the CPU arms) and opens on + // the host. Filtering here keeps it that way even if one appeared. + #[cfg(feature = "cuda")] + let device_ok = !leaf_layout.is_one_row(); // R4 trace proofs from the resident device trees, gathered in one batch // over all query positions instead of walking the host trees (byte @@ -3806,6 +3932,7 @@ pub trait IsStarkProver< #[cfg(feature = "cuda")] let main_dev_proofs: Option>> = lde_trace .gpu_main() + .filter(|_| device_ok) .and_then(|h| h.tree.as_ref()) .map(|tree| { let stream = lde_trace @@ -3821,6 +3948,7 @@ pub trait IsStarkProver< let aux_dev_proofs: Option>> = round_1_result .aux .as_ref() + .filter(|_| device_ok) .and_then(|_aux| lde_trace.gpu_aux().and_then(|h| h.tree.as_ref())) .map(|tree| { let stream = lde_trace @@ -3834,8 +3962,11 @@ pub trait IsStarkProver< // Composition tree: openings open a single position `index` (row pair // 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| { + let comp_dev_proofs: Option>> = round_2_result + .gpu_composition_tree + .as_ref() + .filter(|_| device_ok) + .map(|tree| { let stream = lde_trace .bound_stream() .expect("bound stream for device-resident composition-tree opening"); @@ -3948,13 +4079,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 { @@ -3971,12 +4103,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) }) } @@ -4001,8 +4134,9 @@ pub trait IsStarkProver< // 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) = + LeafLayout::RowPair.query_rows(*index, domain_size as usize); + let r_odd = r_odd.expect("a row pair has a symmetric row"); assert_eq!( even, lde_trace.gather_main_row_range( @@ -4031,14 +4165,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) }) }); @@ -4099,6 +4233,7 @@ pub trait IsStarkProver< &round_2_result.composition_poly_merkle_tree, composition_parts, *index, + leaf_layout, ), } } @@ -4108,6 +4243,7 @@ pub trait IsStarkProver< &round_2_result.composition_poly_merkle_tree, composition_parts, *index, + leaf_layout, ) } }; @@ -4126,12 +4262,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) }) } @@ -4226,6 +4363,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); @@ -4363,14 +4507,27 @@ 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 (RULINGS 14). + 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 never goes device-only: its trees are host + // trees (the device arms build row pairs only). #[cfg(feature = "cuda")] - let device_only = Self::device_only_for(*air, domain); + let device_only = Self::device_only_for(*air, domain) && !layout.is_one_row(); Self::commit_main_trace( air.name(), @@ -4378,6 +4535,7 @@ pub trait IsStarkProver< domain, twiddles, precomputed, + layout, #[cfg(feature = "cuda")] device_only, #[cfg(feature = "disk-spill")] @@ -4455,6 +4613,16 @@ pub trait IsStarkProver< } } + // One-row tables (S2) commit every tree on the host (the device arms + // build row-pair leaves only), so their aux build stays host-side too: + // a resident aux would leave no host aux trace for the CPU commit. + #[cfg(feature = "cuda")] + for ((_, trace, _), layout) in air_trace_pairs.iter_mut().zip(&leaf_layouts) { + if layout.is_one_row() { + trace.set_resident_aux_ok(false); + } + } + // `RecomputeLde` already forced the main commit onto the host path; // keeping the aux build there too makes the mode wholly host-side, which // is what its aux release at the end of each fused task acts on. @@ -4612,9 +4780,11 @@ 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(); + && gpu_main_cells[idx].lock().unwrap().is_some() + && !layout.is_one_row(); // Resident GPU path: aux columns already on device (from // the resident LogUp aux build) — LDE straight from device @@ -4624,7 +4794,7 @@ pub trait IsStarkProver< // a clean error (falling through as-is would commit a // zero aux trace). #[cfg(feature = "cuda")] - if trace.aux_resident().is_some() { + if trace.aux_resident().is_some() && !layout.is_one_row() { #[cfg(feature = "instruments")] let t_sub = Instant::now(); let num_cols = trace.aux_resident().map_or(0, |ra| ra.num_aux_cols); @@ -4686,9 +4856,10 @@ 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. Row-pair + // leaves only, so never for a one-row table. #[cfg(feature = "cuda")] - { + if !layout.is_one_row() { let (trace_slice, num_cols) = trace.aux_data_row_major(); let n = if num_cols > 0 { trace_slice.len() / num_cols @@ -4747,9 +4918,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); @@ -5796,10 +5970,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")) @@ -5808,7 +5982,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 } @@ -5897,6 +6071,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/tests/fri_group_tests.rs b/crypto/stark/src/tests/fri_group_tests.rs index 247206623..9a1d4ee01 100644 --- a/crypto/stark/src/tests/fri_group_tests.rs +++ b/crypto/stark/src/tests/fri_group_tests.rs @@ -21,6 +21,7 @@ use crate::fri::group::{ }; 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; @@ -242,10 +243,24 @@ fn fri_accepts(run: &FriRun, deep: &[Ext], o: &Felt) -> bool { 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); + let checks: Vec> = run + .roots + .iter() + .enumerate() + .map(|(j, root)| { + TreeCheck::build::>( + root, + run.layout.layer_depth(run.lde_log, j) as usize, + 0, + || None, + ) + .unwrap() + }) + .collect(); verify_query_groups::>( &run.layout, - run.lde_log, - &run.roots, + &checks, + 1, |j| dec.layers_auth_paths[j].merkle_path.as_slice(), &dec.layers_evaluations_sym, &run.zetas, @@ -365,14 +380,15 @@ fn dp_round_trips_at_every_fold_count() { 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).unwrap(); + 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).unwrap(); + let l = FriFoldLayout::for_options(12, 2, &o, false).unwrap(); assert!( l.schedule.iter().any(|&d| d > 1), "schedule {:?}", diff --git a/crypto/stark/src/tests/fri_schedule_tests.rs b/crypto/stark/src/tests/fri_schedule_tests.rs index a37e143bd..721b0010f 100644 --- a/crypto/stark/src/tests/fri_schedule_tests.rs +++ b/crypto/stark/src/tests/fri_schedule_tests.rs @@ -1104,7 +1104,7 @@ fn layout_from_options() { let o = options_with(ProofFormat::DEFAULT); let k = u32::from(o.fri_final_poly_log_degree); assert_eq!( - FriFoldLayout::for_options(20, 1, &o), + 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. @@ -1112,7 +1112,7 @@ fn layout_from_options() { fri_mode: FriMode::Dp, ..ProofFormat::DEFAULT }); - let l = FriFoldLayout::for_options(20, 1, &o).unwrap(); + let l = FriFoldLayout::for_options(20, 1, &o, false).unwrap(); let t = (1 + k).min(20); assert_eq!( l.schedule, @@ -1134,14 +1134,19 @@ fn layout_from_options() { fri_schedule_override: FriScheduleOverride::new(&fit), ..ProofFormat::DEFAULT }); - assert_eq!(FriFoldLayout::for_options(20, 1, &o).unwrap().schedule, fit); + 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), + FriFoldLayout::for_options(20, 1, &o, false), Err(FriFormatError::ScheduleOverrideMismatch) ); // An all-ones override under Dp keeps the GROUP encoding. @@ -1150,18 +1155,31 @@ fn layout_from_options() { fri_schedule_override: FriScheduleOverride::new(&vec![1u8; span as usize]), ..ProofFormat::DEFAULT }); - let l = FriFoldLayout::for_options(20, 1, &o).unwrap(); + 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 is refused until S2 exists. + // 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), - Err(FriFormatError::OneRowNotImplemented) + FriFoldLayout::for_options(20, 1, &o, false), + Ok(FriFoldLayout::new(20, 1, k)) ); } // An override longer than the fixed capacity is refused at construction. diff --git a/crypto/stark/src/tests/mod.rs b/crypto/stark/src/tests/mod.rs index 5c44cc5d4..e3f815a6f 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -12,6 +12,7 @@ pub mod fri_schedule_tests; pub mod fri_tests; pub mod grinding_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; 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..29fb2576b --- /dev/null +++ b/crypto/stark/src/tests/one_row_tests.rs @@ -0,0 +1,782 @@ +//! S2 (one-row trace openings with a committed FRI input) on the CPU prover +//! and host verifier: design/FRI.md §7 and §10 — U6 at one_row, the tamper +//! tests T4–T6, the load-bearing mutation M3, the transcript-order KAT, the +//! per-table `auto` rule (RULINGS 6, REVIEW-FRI F5), the preprocessed-root +//! miss (RULINGS 14) and the cap × FRI × one-row matrix (REVIEW-FRI F9). + +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::terminal::FriFoldLayout; +use crate::fri::{commit_phase_with_layout, fold_times}; +use crate::leaf_layout::{ + LeafLayout, M3_PAIR_BOUND_UNDER_ONE_ROW, TableWidths, 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_UNDER_ONE_ROW`). +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 (REVIEW-FRI F7): 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) + ); + } + } +} + +/// REVIEW-FRI F7: 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)); +} + +// --------------------------------------------------------------------------- +// FRI.md §7.7 (i): 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 the proof 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) +} + +/// With 64 queries over an LDE of 64 points, all 64 indexes below `N / 2` +/// has probability 2⁻⁶⁴ under the right bound; the pair bound makes it +/// certain. +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(32, 64); + assert!(ok); + assert!(upper_half_reached(&iotas, 64), "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_UNDER_ONE_ROW.store(true, std::sync::atomic::Ordering::SeqCst); + let (iotas, ok) = one_row_iotas(32, 64); + M3_PAIR_BOUND_UNDER_ONE_ROW.store(false, std::sync::atomic::Ordering::SeqCst); + assert!(ok, "the mutated proof still verifies (both sides mutated)"); + assert!( + !upper_half_reached(&iotas, 64), + "under the mutation the bound test must fail" + ); +} + +// --------------------------------------------------------------------------- +// FRI.md §7.7 (ii): 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); +} + +// --------------------------------------------------------------------------- +// Preprocessed tables: one-row roots, and RULINGS 14 (a miss is an error). +// --------------------------------------------------------------------------- + +#[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) + )); +} + +// --------------------------------------------------------------------------- +// RULINGS 6 / REVIEW-FRI F5: 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, + }; + 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, + }; + 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); + } +} + +/// ⚠ 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 as REVIEW-FRI §C reads them; the others are round +/// numbers), not a census: at generation the MEMW-like and CPU-like cases sat +/// within 0.5% and 2% of the threshold (row 49,988,402 vs pair 49,741,452; +/// row 51,474,062 vs pair 52,465,162, ×Q ns), so they pin the rule's edge. +#[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, false), + ("memw-like", 21, 0, 49, 13, 2, false), + ("cpu-like", 21, 0, 74, 20, 2, true), + ]; + let mut got = Vec::new(); + for &(name, b, pre, main, aux, parts, _) in cases { + let w = TableWidths { + precomputed: pre, + main, + aux: aux * 3, + composition: parts * 3, + }; + got.push((name, resolve_leaf_layout(&w, &o, b, 2).is_one_row())); + } + let want: Vec<_> = cases.iter().map(|c| (c.0, c.6)).collect(); + assert_eq!(got, want); +} + +/// `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); +} + +// --------------------------------------------------------------------------- +// REVIEW-FRI F9: {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 % 3 == 0 && w.composition > 0); + let _ = ::TWO_ADICITY; +} diff --git a/crypto/stark/src/tests/opening_width_tests.rs b/crypto/stark/src/tests/opening_width_tests.rs index ca3dbbd33..f50717f12 100644 --- a/crypto/stark/src/tests/opening_width_tests.rs +++ b/crypto/stark/src/tests/opening_width_tests.rs @@ -71,6 +71,9 @@ 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, RULINGS 14). + precomputed_commitment_row: Option, phantom: PhantomData, } @@ -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/traits.rs b/crypto/stark/src/traits.rs index f28da26fb..9b77884ff 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 (RULINGS 14 — 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 596e59e5d..51a3138f6 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -5,6 +5,7 @@ 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::{ @@ -148,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₀` (FRI.md §7.7 (i): + /// 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 @@ -254,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; @@ -273,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()) }) } @@ -475,6 +503,7 @@ pub trait IsStarkVerifier< domain.lde_length.trailing_zeros(), blowup_log, air.options(), + Self::leaf_layout(air, domain.trace_length).is_one_row(), ) .ok() } @@ -503,14 +532,26 @@ pub trait IsStarkVerifier< 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, @@ -518,15 +559,6 @@ pub trait IsStarkVerifier< #[cfg(any(test, feature = "test-utils"))] crate::fri::capture::record_deep(&deep_poly_evaluations, &deep_poly_evaluations_sym); - // ---- 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; - // Structural check: number of committed FRI layers must equal // `num_committed` (zero when no fold or a single final fold happened). if proof.fri_layers_merkle_roots().len() != num_committed { @@ -577,7 +609,7 @@ 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() { @@ -599,20 +631,21 @@ pub trait IsStarkVerifier< } } } + let _ = lde_log; return (0..challenges.iotas.len()) .zip(evaluation_point_inverse) .all(|(i, eval)| { Self::verify_query_groups( - proof, &layout, &challenges.zetas, challenges.iotas[i], proof.query(i), eval, &deep_poly_evaluations[i], - &deep_poly_evaluations_sym[i], + deep_poly_evaluations_sym.get(i), &terminal_codeword, - lde_log as u32, + &checks.fri, + i, &roots_tables, ) }); @@ -636,6 +669,17 @@ pub trait IsStarkVerifier< }) } + /// 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 @@ -826,13 +870,17 @@ pub trait IsStarkVerifier< { let options = air.options(); // A format this verifier cannot lay out rejects here, as in step 3. - let num_committed = Self::fri_termination_params(air, domain)?.num_committed; + let fri_layout = Self::fri_termination_params(air, domain)?; + let num_committed = fri_layout.num_committed; let lde_log = domain.lde_length.trailing_zeros() as usize; - let caps = StarkCaps::new( + // Every depth from the table's layout (row pairs: `log2(lde) − 1`; one + // row: `log2(lde)`) and the FRI schedule's per-layer depths — at the + // default exactly `StarkCaps::new`'s. + let caps = StarkCaps::with_depths( options.format.merkle_cap, options.fri_number_of_queries, - lde_log, - num_committed, + LeafLayout::from_one_row(fri_layout.one_row).tree_depth(lde_log), + fri_layout.layer_depths(lde_log as u32), ); let fri_roots = proof.fri_layers_merkle_roots(); if fri_roots.len() != num_committed { @@ -923,51 +971,67 @@ pub trait IsStarkVerifier< ) } - /// Verify a single FRI query under the group encoding (S3; any format but - /// the legacy one): fold 0 from the DEEP pair as today, then - /// [`crate::fri::group::verify_query_groups`] for the committed layers and - /// the terminal check. The zero-fold case is the legacy one (no layer, no - /// challenge). + /// 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)` + /// (FRI.md §7.4). 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( - proof: StarkProofView<'_, Field, FieldExtension, PI>, layout: &crate::fri::terminal::FriFoldLayout, zetas: &[FieldElement], iota: usize, fri_decommitment: FriDecommitmentView<'_, FieldExtension>, evaluation_point_inv: FieldElement, p0_eval: &FieldElement, - p0_eval_sym: &FieldElement, + p0_eval_sym: Option<&FieldElement>, terminal_codeword: &[FieldElement], - lde_log: u32, + fri_checks: &[TreeCheck<'_>], + query: usize, roots_tables: &[Vec>], ) -> bool where FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { - 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); + // 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, - lde_log, - proof.fri_layers_merkle_roots(), + fri_checks, + query, |j| fri_decommitment.layer_auth_path(j), fri_decommitment.layers_evaluations_sym(), zetas, iota, v, - evaluation_point_inv.square(), + y_inv, terminal_codeword, roots_tables, ) @@ -1178,6 +1242,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(); @@ -1209,6 +1298,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); @@ -1265,6 +1382,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)` @@ -1547,7 +1746,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 (RULINGS 14). + 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 @@ -1821,18 +2029,20 @@ 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 @@ -1872,7 +2082,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, diff --git a/prover/src/lfm/fri.rs b/prover/src/lfm/fri.rs index d47a524e7..5881fa55d 100644 --- a/prover/src/lfm/fri.rs +++ b/prover/src/lfm/fri.rs @@ -66,7 +66,21 @@ 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 a one-row inner format (`LAMBDA_VM_ZF_ONE_ROW` ≠ 0, S2): the + /// in-guest verifier of one-row openings and the committed FRI input is + /// lane I-FRI-G's G3 and does not exist yet, so an emitter built for the + /// row-pair layout must never be handed one — it would emit a verifier of + /// the wrong protocol. Emit time, not a proof outcome. pub fn from_options(options: &ProofOptions, log2_lde_length: u32) -> Self { + assert!( + options.format.one_row == stark::proof::options::OneRowMode::Off, + "the in-guest STARK verifier does not implement one-row openings (S2, lane G3); \ + inner format one_row = {}", + options.format.one_row + ); Self { log2_lde_length, blowup_log: (options.blowup_factor as u32).trailing_zeros(), From c8ffb4b5cd95c4de447f3823cd0d10a308701cde Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:25:37 -0300 Subject: [PATCH 33/73] refactor(prover): one in-guest Merkle cap gadget for the WHIR and STARK verifiers CapCells (the authenticated cap: its only constructor hashes the hinted cap to the tree's root) moves from lfm/whir_open.rs to lfm/merkle_cap.rs so the STARK sub-proof verifier (C5) uses the same gadget instead of a second one (I-GUEST brief). Its per-opening check becomes one public entry point, CapCells::verify_path(leaf, whole leaf index, path to the cap): it walks the low D - c bits, muxes the top c with the private select and compares (REVIEW-CAP S1: the split point is computed inside; no caller reaches the mux). authenticate takes the root as one lane array per digest cell, so a byte-digest (two-cell) root works too. The WHIR emission is instruction-for-instruction unchanged (TreeAuth::Cap calls verify_path in the same order): the production chain pins read 185,509 / 22,828 (off) and 188,081 / 18,729 with 219,768 instructions (cap=auto), as before. --- prover/src/lfm/merkle_cap.rs | 143 ++++++++++++++++++++++++++++++ prover/src/lfm/mod.rs | 1 + prover/src/lfm/whir_chain.rs | 6 +- prover/src/lfm/whir_open.rs | 78 ++-------------- prover/src/lfm/whir_open_tests.rs | 6 +- 5 files changed, 161 insertions(+), 73 deletions(-) create mode 100644 prover/src/lfm/merkle_cap.rs diff --git a/prover/src/lfm/merkle_cap.rs b/prover/src/lfm/merkle_cap.rs new file mode 100644 index 000000000..c5d7612e6 --- /dev/null +++ b/prover/src/lfm/merkle_cap.rs @@ -0,0 +1,143 @@ +//! ★ One tree's authenticated Merkle cap, in-guest — the one gadget the WHIR +//! chain verifier (W1) and the STARK sub-proof verifier (S1) share +//! (design/CAP.md §6.1, §6.2, §9.2; REVIEW-CAP S1). +//! +//! 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 (REVIEW-CAP (e)). +//! - **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 (REVIEW-CAP (d)). +//! +//! 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 (RULINGS 1). +//! +//! ⚠ 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..a4512811f 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; diff --git a/prover/src/lfm/whir_chain.rs b/prover/src/lfm/whir_chain.rs index 98b25958f..e40bcd666 100644 --- a/prover/src/lfm/whir_chain.rs +++ b/prover/src/lfm/whir_chain.rs @@ -664,7 +664,11 @@ fn tree_auth( TreeAuth::Root(*root_lanes) } else { assert_eq!(cap.len(), 1usize << cap_height, "a cap is 2^c digests"); - TreeAuth::Cap(CapCells::authenticate(b, cap, root_lanes)) + TreeAuth::Cap(CapCells::authenticate( + b, + cap, + std::slice::from_ref(root_lanes), + )) } } diff --git a/prover/src/lfm/whir_open.rs b/prover/src/lfm/whir_open.rs index 39d2a6789..3a07aa65a 100644 --- a/prover/src/lfm/whir_open.rs +++ b/prover/src/lfm/whir_open.rs @@ -251,69 +251,10 @@ pub fn emit_verify_opening( edsl::assert_digest_eq_lanes(b, walked, std::slice::from_ref(root_lanes)); } -/// ★ One tree's authenticated Merkle cap (W1, design/CAP.md §6.2, §9.2). -/// -/// The ONLY constructor, [`CapCells::authenticate`], hashes the hinted cap up -/// to its root and asserts that root equals the tree's root lanes. Every -/// opening of the tree then reads THESE cells through -/// [`TreeAuth::verify_opening`] — so the cells checked against the root and -/// the cells the mux picks from are the same cells, and a tree has one cap -/// (REVIEW-CAP (e)). -/// -/// The mux is private to this module and consumes exactly the top `c` of the -/// index bits it is handed, the rest being walked (REVIEW-CAP (d)): a caller -/// passes the whole index, never a split of it. -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 ([`TreeAuth::Root`]). - 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, std::slice::from_ref(root_lanes)); - Self { - cap: cap.to_vec(), - height: cap.len().trailing_zeros() as usize, - } - } - - pub fn height(&self) -> usize { - self.height - } - - /// `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] - } -} +/// ★ 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 @@ -328,7 +269,7 @@ impl TreeAuth { pub fn cap_height(&self) -> usize { match self { TreeAuth::Root(_) => 0, - TreeAuth::Cap(cap) => cap.height, + TreeAuth::Cap(cap) => cap.height(), } } @@ -349,17 +290,12 @@ impl TreeAuth { TreeAuth::Root(lanes) => emit_verify_opening(b, values, index_bits, siblings, lanes), TreeAuth::Cap(cap) => { assert_eq!( - siblings.len() + cap.height, + siblings.len() + cap.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 leaf = emit_block_leaf(b, values); - let walked = edsl::wrap_merkle_walk(b, leaf, walk_bits, siblings); - let node = cap.select(b, top_bits); - for (x, y) in walked.iter().zip(node.iter()) { - edsl::assert_word_eq(b, *x, *y); - } + 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 927c5d233..a50a7e332 100644 --- a/prover/src/lfm/whir_open_tests.rs +++ b/prover/src/lfm/whir_open_tests.rs @@ -585,7 +585,11 @@ fn capped_program(depth: usize, c: usize, n: usize) -> LfmProgram { .collect(); let root = b.hint_word(arena, 1 << c); let root_lanes = b.unpack(root); - let tree = TreeAuth::Cap(CapCells::authenticate(&mut b, &cap, &root_lanes)); + 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) From 87176ab829231d7f7cc50b89bfc07d06133fd04c Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:25:51 -0300 Subject: [PATCH 34/73] feat(prover): Merkle caps in the in-guest STARK verifier (C5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LFM STARK verifier (level-0 STARK wraps and every node verifying an LFM proof) now verifies proofs made under a Merkle cap policy (design/CAP.md §6.1). With the knob off nothing is emitted differently: no caps arena is declared, every path is the full depth, every opening is compared with its root lanes exactly as before. - Shapes: SubProofShape.trace_cap (the four trace/composition trees share a depth and opening count); FriShape carries the inner proof's ProofFormat and derives layer_depth / layer_cap / layer_path_len from it. Every height is CapPolicy::height(Q, depth) of the inner options, a verifier constant; the host serializer asserts it equals the host's StarkCaps::for_options. - Arenas: a third per-sub-proof arena, caps, declared only when some tree is capped (TableQueryArenas.caps; FriArenas.caps and SubProofArenas.caps in the isolation drivers): the matrices' caps in group order, then the FRI layers'. Query strides use the capped path lengths. - Emission: each capped tree's cap is hinted once and authenticated against the SAME root cells the transcript absorbed (CapCells, one per tree); every trace opening and every FRI layer opening is checked through CapCells::verify_path with the tree's whole leaf index (bits, and bits[i+1..] for FRI layer i). - Closed forms: per-query parents use the capped path lengths; cap_permutations / table_permutations_for add the 2^c - 1 cap-root parents once per capped tree (census bill updated to match). - Host serializers split query 0's owner path into (siblings, cap): epoch_verify_tests::build_table_legs (+ caps_arena, pushed by epoch_tests and both per_table_aggregator_tests drivers), join_tests HostSubProof, fri_tests HostFri. Tests (laptop, fri_tests): a real capped folding proof (L2G, 2048 rows, Q = 24) verified by the FRI leg at fixed(1), fixed(2) and auto with emitted permutations equal to the capped closed form and the saving Q*c - (2^c - 1) per layer tree; every FRI cap word bound, including the seven unreached per layer that only the cap-to-root check rejects; trace + FRI legs as one program over the capped proof, closed form exact, a moved trace cap word refused. Box twin: epoch_verify_tests:: the_assembled_epoch_verifier_runs_at_the_process_format (ignored). --- prover/src/lfm/epoch_tests.rs | 1 + prover/src/lfm/epoch_verify.rs | 84 ++++++- prover/src/lfm/epoch_verify_tests.rs | 212 +++++++++++++++++- prover/src/lfm/fri.rs | 156 ++++++++++++- prover/src/lfm/fri_tests.rs | 223 ++++++++++++++++++- prover/src/lfm/join_tests.rs | 53 ++++- prover/src/lfm/per_table_aggregator_tests.rs | 2 + prover/src/lfm/per_table_census_tests.rs | 7 +- prover/src/lfm/sub_proof.rs | 116 +++++++++- 9 files changed, 809 insertions(+), 45 deletions(-) diff --git a/prover/src/lfm/epoch_tests.rs b/prover/src/lfm/epoch_tests.rs index a18887c19..34028fffd 100644 --- a/prover/src/lfm/epoch_tests.rs +++ b/prover/src/lfm/epoch_tests.rs @@ -2201,6 +2201,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 +175,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)), } } @@ -304,7 +331,7 @@ pub fn emit_table_verification( ); // ---- the FRI commitments, likewise from the transcript's own cells. - let fri = FriCommitments { + let mut fri = FriCommitments { layers: absorbs .fri_roots .iter() @@ -314,6 +341,32 @@ pub fn emit_table_verification( coeffs: absorbs.fri_coeffs.to_vec(), }; + // ---- the Merkle caps, once per tree, against the SAME root cells the + // transcript absorbed (design/CAP.md §6.1): 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 .sub @@ -331,7 +384,7 @@ pub fn emit_table_verification( 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); @@ -544,7 +597,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 } @@ -593,10 +646,27 @@ pub fn query_permutations_for(shape: &TableVerifyShape, hash: WrapHash) -> usize .sum(); let fri_leaves = shape.fri.num_committed() * blocks_for(FRI_LEAF_FELTS, 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 (design/CAP.md §6.1 `cap_permutations`). 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 +678,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..11e383b99 100644 --- a/prover/src/lfm/epoch_verify_tests.rs +++ b/prover/src/lfm/epoch_verify_tests.rs @@ -69,6 +69,10 @@ pub(super) struct TableLegs { openings: Vec, Vec)>>, /// `[query][layer]` — `(pᵢ(−υ^(2ⁱ)), path)`. fri_openings: 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 +174,17 @@ pub(super) fn build_table_legs( "the next-row block covers every evaluation point past the first step" ); + let merkle_depth = log2_lde_length as usize - 1; 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), }; let has_aux_trace = air.has_aux_trace(); let verify = TableVerifyShape { @@ -195,6 +204,37 @@ 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) + .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 +250,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 +260,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 +270,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,19 +280,33 @@ 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 = verify.fri; + let mut fri_caps: Vec = Vec::new(); 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())) + .map(|(i, sym)| { + 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 (*sym, 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"); + fri_caps.extend_from_slice(cap); + (*sym, siblings.to_vec()) + }) .collect() }) .collect(); @@ -283,11 +337,19 @@ 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, @@ -319,6 +381,24 @@ 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(); @@ -1384,6 +1464,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 +1491,122 @@ 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 (RULINGS 1: 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 the lead +/// compares 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 format = crate::zf_format::ZfFormat::global(); + let mut opts = super::proof_fixture::fixture_options(); + opts.fri_number_of_queries = PROCESS_FORMAT_QUERIES; + let opts = format.options(opts); + 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} trace cap {} FRI depths {:?} caps {:?} \ + {} permutations", + l.verify.sub.log2_lde_length, + l.verify.sub.trace_cap, + (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}"); + + // 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..40cf13932 100644 --- a/prover/src/lfm/fri.rs +++ b/prover/src/lfm/fri.rs @@ -32,13 +32,14 @@ //! 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::proof::options::{FriMode, OneRowMode, ProofFormat, ProofOptions}; use crate::tables::types::FE; use super::builder::{Bit, 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 +59,10 @@ pub struct FriShape { pub coset_offset: u64, /// Queries the sub-proof carries. pub num_queries: usize, + /// The inner proof's FORMAT (design/CAP.md, design/FRI.md): its Merkle cap + /// policy caps every committed layer tree. A verifier constant, taken from + /// the inner proof's options — never from the proof. + pub format: ProofFormat, } impl FriShape { @@ -73,6 +78,7 @@ impl FriShape { 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: options.format, } } @@ -120,15 +126,50 @@ impl FriShape { 1usize << self.effective_k() } - /// Merkle path length for committed layer `i`: that layer's codeword is + /// Tree depth of 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. - pub fn layer_path_len(self, layer: usize) -> usize { + pub fn layer_depth(self, layer: usize) -> usize { (self.log2_lde_length as usize) .checked_sub(layer + 2) .expect("layer index must be below num_committed") } + /// 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.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. pub fn path_steps_per_query(self) -> usize { (0..self.num_committed()) @@ -152,7 +193,9 @@ 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 } @@ -175,6 +218,13 @@ impl FriShape { /// Invariants a caller cannot assemble their way out of. pub fn check(self) { + assert!( + self.format.fri_mode == FriMode::Pair && self.format.one_row == OneRowMode::Off, + "the in-guest FRI verifier implements pair layers with row-pair openings \ + only: {:?} / {:?}", + self.format.fri_mode, + self.format.one_row + ); assert!( self.blowup_log >= 1, "a blowup of 1 is not a low-degree extension" @@ -271,6 +321,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 +339,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 +353,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. @@ -360,6 +481,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. @@ -378,10 +502,15 @@ pub fn declare_fri( 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(); + if let Some(caps) = caps { + hint_layer_caps(b, shape, caps, &mut layers); + } let zeta_cells = (0..num_zetas as u32) .map(|i| b.hint_word(zetas, i).as_ext()) .collect(); @@ -395,6 +524,7 @@ pub fn declare_fri( zetas, coeffs, queries, + caps, }, FriCommitments { layers, @@ -532,6 +662,13 @@ pub fn emit_query_fri( ); 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(), @@ -578,8 +715,9 @@ pub fn emit_query_fri( // 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); + // `bits[i+1..]` is this layer tree's whole leaf index; a cap walks its + // low bits and muxes the top ones. + fri.layers[i].authenticate(b, leaf, &q.bits[i + 1..], &opening.siblings); // `evaluation_point_vec[i] = υ^(−2^(i+1))` — `inv.square()` then one // squaring per layer (`verifier.rs:692-697`). diff --git a/prover/src/lfm/fri_tests.rs b/prover/src/lfm/fri_tests.rs index 711e4b26d..bb0d92190 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) @@ -131,8 +141,12 @@ struct HostFri { zetas: Vec, /// The terminal polynomial's coefficients, low-to-high. coeffs: Vec, - /// `[query][layer]` — `(pᵢ(−υ^(2ⁱ)), path)`. + /// `[query][layer]` — `(pᵢ(−υ^(2ⁱ)), path)`. Paths are cut at each layer's + /// cap (query 0's cap split off into [`Self::caps`]). openings: Vec)>>, + /// Every capped layer's cap, in layer order — the caps arena. Empty at + /// the default format. + caps: Vec, } /// Build the FRI host fixture for a real proof of `num_boundaries` rows. @@ -155,13 +169,28 @@ fn host_fri_from( let shape = FriShape::from_options(opts, trace.shape.log2_lde_length); shape.check(); + // Query 0 of a capped layer is its owner: the cap rides after the + // `D − c` siblings and goes to the caps arena. + let mut caps = Vec::new(); 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())) + .map(|(i, sym)| { + let path = d.layer_auth_path(i); + let (depth, c) = (shape.layer_depth(i), shape.layer_cap(i)); + if c == 0 || q != 0 { + assert_eq!(path.len(), depth - c, "query {q} layer {i}"); + return (*sym, 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); + (*sym, siblings.to_vec()) + }) .collect() }) .collect(); @@ -172,6 +201,7 @@ fn host_fri_from( zetas: trace.zetas.clone(), coeffs: view.fri_final_poly_coeffs().to_vec(), openings, + caps, trace, } } @@ -179,12 +209,17 @@ fn host_fri_from( impl HostFri { /// The arenas the FRI-only program declares, for the given queries. fn fri_arenas(&self, queries: &[usize]) -> Vec> { - 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. @@ -826,6 +861,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 +1309,178 @@ fn the_fri_leg_proves_and_verifies() { h.shape.num_committed(), ); } + +// ============================================================================= +// Merkle caps in the FRI leg (S1, design/CAP.md §6.1, C5) +// ============================================================================= + +/// 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 +/// (REVIEW-CAP M1(b) 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..3942d305d 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, @@ -134,12 +137,38 @@ pub(super) fn build_host_sub_proof( let blowup = air.options().blowup_factor as usize; let lde_length = view.trace_length() * blowup; + let merkle_depth = lde_length.trailing_zeros() as usize - 1; + 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, + }; + // 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![]; @@ -187,7 +216,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 +227,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 +238,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,7 +249,7 @@ 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); @@ -290,6 +319,7 @@ 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(), @@ -302,13 +332,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 @@ -565,6 +600,7 @@ 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, } } @@ -1739,6 +1775,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 +1885,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 +1936,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/per_table_aggregator_tests.rs b/prover/src/lfm/per_table_aggregator_tests.rs index 4d067b4aa..084afd679 100644 --- a/prover/src/lfm/per_table_aggregator_tests.rs +++ b/prover/src/lfm/per_table_aggregator_tests.rs @@ -457,6 +457,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 } @@ -1216,6 +1217,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 } diff --git a/prover/src/lfm/per_table_census_tests.rs b/prover/src/lfm/per_table_census_tests.rs index a67c609be..1af37c087 100644 --- a/prover/src/lfm/per_table_census_tests.rs +++ b/prover/src/lfm/per_table_census_tests.rs @@ -409,6 +409,10 @@ fn table_shape( merkle_depth: log2_lde_length as usize - 1, log2_lde_length, coset_offset: FE::from(opts.coset_offset), + trace_cap: opts + .format + .merkle_cap + .height(opts.fri_number_of_queries, log2_lde_length as usize - 1), }; let has_aux_trace = air.has_aux_trace(); let fri = FriShape::from_options(opts, log2_lde_length); @@ -517,7 +521,8 @@ fn bill(tables: &[TableShape], hash: WrapHash, hash_chip: &str) -> (Bill, usize) .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; + // 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; diff --git a/prover/src/lfm/sub_proof.rs b/prover/src/lfm/sub_proof.rs index d11cce3c6..3c1740d29 100644 --- a/prover/src/lfm/sub_proof.rs +++ b/prover/src/lfm/sub_proof.rs @@ -58,6 +58,7 @@ use crate::tables::types::{FE, GoldilocksField}; 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 @@ -112,6 +113,14 @@ pub struct SubProofShape { 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 (design/CAP.md §6.1). A verifier + /// constant: `CapPolicy::height(num_queries, merkle_depth)` of the inner + /// proof's options, never read from the proof. + pub trace_cap: usize, } impl SubProofShape { @@ -154,13 +163,41 @@ impl SubProofShape { /// 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 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, @@ -209,6 +246,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 +270,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 +289,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) } } @@ -330,13 +404,19 @@ pub fn emit_group_authentication( 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); + 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: @@ -484,6 +564,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(), @@ -560,6 +645,9 @@ 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 @@ -596,12 +684,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 +714,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); @@ -646,7 +744,7 @@ pub fn emit_sub_proof_with_bits( 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); From 45108449cfdbe0d6673bb941c64674d070396fea Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:39:43 -0300 Subject: [PATCH 35/73] feat(prover): S3 group-leaf FRI layers in the in-guest verifier (G1, G2) G1 (shape and cost model). FriShape derives its committed layers from the same schedule function the host lays out with (FriFormat::schedule: all ones under fri=pair, the RULINGS-13 cost-law DP under fri=dp, or the test override): num_committed, layer_fold, layer_bit_offset (G_j), layer_depth (b0 - G_j - d_j), layer_values (1, or the whole 2^d group), layer_leaf_felts (3 * 2^d), leaf_permutations_per_query, query_words. Closed forms (query_permutations_for, fri_leaf_permutations_at_rate, the census bill) use the per-layer leaves. The transcript replay needs no change: it already draws num_committed + 1 zetas and absorbs num_committed roots. G2 (emitter). Under the group encoding, committed layer j: 1. slot check: values[bits[G..G+d]] == v (a 2^d - 1 select mux, then assert_eq_ext) - the only place the previous fold's value meets the layer; 2. the group is the leaf: hashed in position order as a GroupShape of 2^(d-1) ext columns (REVIEW-FRI F6), authenticated at bits[G+d..] against the layer's root or cap; 3. group fold: x_g^-1 = y^-1 * w_(2^d)^br(slot) (d constant selects + d muls), d levels of fri_fold with zeta^(2^l) hoisted once per sub-proof (FriCommitments::new), each level's point squared once; the last square is the next layer's y^-1. The legacy (pair) branch is today's emission unchanged; FriCommitments::new emits nothing under pair. Host serializers take the flat layers_evaluations_sym per layer (fri_layer_openings, shared). Tests (fri_group_tests, laptop): - the in-guest schedule, depths and caps equal the host's StarkCaps over 840 shapes (T = 9/10, pair/dp, off/auto, Q 3/24/110); - the emitted FRI verifier executes every RPX (d) vector (pair, dp, dp_3_1_3, cap_pair, cap_dp) with permutations == closed form; - tampers of the slot value, a non-slot value, a sibling, a cap word, a zeta, a coefficient and p0 are refused; the slot check is load-bearing (a moved p0 executes only with the check skipped, a test-only switch); - F9 matrix {off, auto} x {pair, dp, dp [3,1,4]} on a real L2G proof (Q = 24): FRI leg and both legs as one program, terminal values equal production's codeword, permutations == closed form; - RULINGS 13: per group layer the slot mux (2^d - 1 selects), the fold (5 XALU per binary fold) and the twiddle chain (d BALU) equal the DP's terms; the emitter also emits rows the DP does not price (x_g: d selects + d muls; level scaling; the slot assert; unpacks and hints), pinned and printed with the schedules the DP would pick if they were priced. --- prover/src/lfm/epoch_verify.rs | 30 +- prover/src/lfm/epoch_verify_tests.rs | 90 ++- prover/src/lfm/fri.rs | 468 +++++++++++++-- prover/src/lfm/fri_group_tests.rs | 692 +++++++++++++++++++++++ prover/src/lfm/fri_tests.rs | 74 +-- prover/src/lfm/mod.rs | 2 + prover/src/lfm/per_table_census_tests.rs | 5 +- 7 files changed, 1217 insertions(+), 144 deletions(-) create mode 100644 prover/src/lfm/fri_group_tests.rs diff --git a/prover/src/lfm/epoch_verify.rs b/prover/src/lfm/epoch_verify.rs index a01794d2e..9f2bacec3 100644 --- a/prover/src/lfm/epoch_verify.rs +++ b/prover/src/lfm/epoch_verify.rs @@ -331,15 +331,18 @@ pub fn emit_table_verification( ); // ---- the FRI commitments, likewise from the transcript's own cells. - let mut 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 (design/CAP.md §6.1): the matrices in group order, @@ -576,7 +579,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. @@ -644,7 +651,8 @@ pub fn query_permutations_for(shape: &TableVerifyShape, hash: WrapHash) -> usize .iter() .map(|g| blocks_for(group_leaf_felts(g), 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.path_len() + shape.fri.path_steps_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 11e383b99..6e74b0b84 100644 --- a/prover/src/lfm/epoch_verify_tests.rs +++ b/prover/src/lfm/epoch_verify_tests.rs @@ -67,8 +67,9 @@ 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. @@ -286,30 +287,7 @@ pub(super) fn build_table_legs( }) .collect(); - let fri = verify.fri; - let mut fri_caps: Vec = Vec::new(); - let fri_openings = (0..view.query_list_len()) - .map(|q| { - let d = view.query(q); - d.layers_evaluations_sym() - .iter() - .enumerate() - .map(|(i, sym)| { - 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 (*sym, 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"); - fri_caps.extend_from_slice(cap); - (*sym, siblings.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 @@ -357,6 +335,57 @@ pub(super) fn build_table_legs( } } +/// 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 (FRI.md §3.4); `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. /// @@ -403,8 +432,8 @@ impl TableLegs { 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)); } } @@ -1563,10 +1592,11 @@ fn the_assembled_epoch_verifier_runs_at_the_process_format() { for (i, l) in e.legs.iter().enumerate() { let f = l.verify.fri; println!( - " leg {i:>2}: log2(lde) {:>2} trace cap {} FRI depths {:?} caps {:?} \ - {} permutations", + " leg {i:>2}: log2(lde) {:>2} trace cap {} FRI schedule {:?} depths {:?} \ + caps {:?} {} permutations", l.verify.sub.log2_lde_length, l.verify.sub.trace_cap, + f.schedule(), (0..f.num_committed()) .map(|j| f.layer_depth(j)) .collect::>(), diff --git a/prover/src/lfm/fri.rs b/prover/src/lfm/fri.rs index 40cf13932..f27f6e534 100644 --- a/prover/src/lfm/fri.rs +++ b/prover/src/lfm/fri.rs @@ -32,11 +32,12 @@ //! 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::fri::schedule::FriFormat; 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; @@ -93,15 +94,90 @@ 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`). Decided by the FORMAT, never + /// by the schedule's values: a `dp` schedule of all ones still uses the + /// group encoding (`FriFormat::is_legacy`). Every non-legacy path below is + /// the S3 group path; the legacy emission is today's, instruction for + /// instruction. + pub fn is_legacy(self) -> bool { + self.format.fri_mode == FriMode::Pair + } + + /// 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: false, + 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 RULINGS-13 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. pub fn num_committed(self) -> usize { - self.total_folds().saturating_sub(1) as usize + self.schedule().len() + } + + /// 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 + /// (FRI.md §3.4 — 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 @@ -126,13 +202,17 @@ impl FriShape { 1usize << self.effective_k() } - /// Tree depth of 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 { - (self.log2_lde_length as usize) - .checked_sub(layer + 2) - .expect("layer index must be below num_committed") + 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 @@ -177,11 +257,13 @@ 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 @@ -200,15 +282,19 @@ impl FriShape { self.log2_lde_length as usize - 1 } - /// 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. @@ -219,12 +305,27 @@ impl FriShape { /// Invariants a caller cannot assemble their way out of. pub fn check(self) { assert!( - self.format.fri_mode == FriMode::Pair && self.format.one_row == OneRowMode::Off, - "the in-guest FRI verifier implements pair layers with row-pair openings \ - only: {:?} / {:?}", - self.format.fri_mode, + self.format.one_row == OneRowMode::Off, + "the in-guest FRI verifier implements row-pair openings only (one-row \ + openings, S2, are a later in-guest unit): {:?}", self.format.one_row ); + // 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:?}" + ); + assert_eq!( + covered, + self.total_folds().saturating_sub(1), + "the schedule {schedule:?} must cover the committed folds (fold 0 is binary \ + and uncommitted)" + ); assert!( self.blowup_log >= 1, "a blowup of 1 is not a low-degree extension" @@ -433,6 +534,46 @@ pub struct FriCommitments { pub zetas: Vec, /// The terminal polynomial's `2^effective_k` coefficients, low-to-high. pub coeffs: Vec, + /// Under the group encoding (S3): per committed layer `j`, the challenges + /// its `d_j` binary folds use — `ζ_{j+1}, ζ_{j+1}², …, ζ_{j+1}^{2^{d_j−1}}` + /// (FRI.md §1.2) — squared ONCE per sub-proof, not per query. Empty under + /// `pair`, 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[j + 1]; + 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. @@ -441,11 +582,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 (FRI.md §3.4) — 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, } @@ -511,10 +657,10 @@ pub fn declare_fri( if let Some(caps) = caps { hint_layer_caps(b, shape, caps, &mut layers); } - let zeta_cells = (0..num_zetas as u32) + 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(); @@ -526,11 +672,7 @@ pub fn declare_fri( queries, caps, }, - FriCommitments { - layers, - zetas: zeta_cells, - coeffs: coeff_cells, - }, + FriCommitments::new(b, shape, layers, zeta_cells, coeff_cells), ) } @@ -560,8 +702,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. @@ -570,7 +717,7 @@ pub fn hint_layer_openings_from( d }) .collect(); - LayerOpening { sym, siblings } + LayerOpening { values, siblings } }) .collect(); assert_eq!( @@ -708,21 +855,48 @@ pub fn emit_query_fri( // (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]); - // `bits[i+1..]` is this layer tree's whole leaf index; a cap walks its - // low bits and muxes the top ones. - fri.layers[i].authenticate(b, leaf, &q.bits[i + 1..], &opening.siblings); - - // `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); + if shape.is_legacy() { + let mut inv_pow = inv; + for (i, opening) in openings.iter().enumerate() { + 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(q.bits[i], v.as_cell(), sym.as_cell()); + let leaf = sub_proof::emit_leaf_hash(b, FRI_LEAF_GROUP, &[first, second]); + // `bits[i+1..]` is this layer tree's whole leaf index; a cap walks its + // low bits and muxes the top ones. + fri.layers[i].authenticate(b, leaf, &q.bits[i + 1..], &opening.siblings); + + // `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, sym, fri.zetas[i + 1], inv_pow); + } + } 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}`, FRI.md §1.1). + 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, + ); + } } // `x = υ^(2^total_folds)`: where the fold chain has arrived, and the @@ -736,6 +910,192 @@ pub fn emit_query_fri( v } +/// The program constants of one group fold of exponent `d` (FRI.md §1.3), 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; FRI.md §1.3, §3.2, §6). +/// +/// 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; REVIEW-FRI +/// F6), 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. diff --git a/prover/src/lfm/fri_group_tests.rs b/prover/src/lfm/fri_group_tests.rs new file mode 100644 index 000000000..8b41013f5 --- /dev/null +++ b/prover/src/lfm/fri_group_tests.rs @@ -0,0 +1,692 @@ +//! S3 in the in-guest FRI verifier: the shape from the shared schedule (G1) +//! and the group-layer emitter (G2), design/FRI.md §6, §11. +//! +//! 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 I-FRI-H'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 of REVIEW-FRI F9), +//! 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 (F9), both legs as one program; +//! - RULINGS 13: the rows the emitter emits per group layer, against the DP's +//! cost-model terms (`stark::fri::schedule`), with every unmodelled row named. + +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, FRI_FOLD_XALU_ROWS, FRI_SLOT_SELECT_ROWS, FRI_TWIDDLE_BALU_ROWS, + fri_leaf_blocks, fri_schedule_by, +}; +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::LfmBuilder; +use super::compiler::{LfmProgram, compile}; +use super::executor::execute; +use super::fri::{FriShape, LayerCommitment, LayerOpening, emit_group_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) + .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 I-FRI-H'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, REVIEW-FRI F6) and both capped Q = 20 +/// formats (F9) — 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"); +} + +// ============================================================================= +// F9 — the {cap} × {fri} round-trip matrix, both legs, on a real proof +// ============================================================================= + +/// ★ REVIEW-FRI F9's 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(), + ); + } + } +} + +// ============================================================================= +// RULINGS 13 — the emitted rows per group layer against the DP's cost terms +// ============================================================================= + +/// Rows one group layer of fold exponent `d` emits, by kind, measured on the +/// emitter itself: the layer is emitted TWICE in one builder over hinted +/// inputs and the second emission is counted, so interned program constants +/// (paid once per program) are out of the figure. The tree is two levels +/// deep and uncapped, which isolates the model's path term. +struct LayerRows { + selects: usize, + xalu: usize, + balu: usize, + hashes: usize, + unpacks: usize, + hints: usize, + total: usize, +} + +fn measure_group_layer(d: u32) -> LayerRows { + let once = group_layer_program(d, 1); + let twice = group_layer_program(d, 2); + let (a, b) = (count_kinds(&once.instrs), count_kinds(&twice.instrs)); + LayerRows { + selects: b.0 - a.0, + xalu: b.1 - a.1, + balu: b.2 - a.2, + hashes: b.3 - a.3, + unpacks: b.4 - a.4, + hints: b.5 - a.5, + total: twice.instrs.len() - once.instrs.len(), + } +} + +/// A program emitting `times` group layers of exponent `d` over hinted +/// inputs that are all hinted BEFORE the first emission, so the difference +/// between `times = 2` and `times = 1` is exactly one layer's rows. One +/// committed layer over a two-level tree: `n − 1 = d + 2` index bits and a +/// terminal at `2^2` (blowup `2^1`, `k = 1`). +fn group_layer_program(d: u32, times: usize) -> LfmProgram { + let shape = FriShape { + log2_lde_length: d + 3, + blowup_log: 1, + final_poly_log_degree: 1, + coset_offset: 3, + num_queries: 1, + format: ProofFormat { + fri_mode: FriMode::Dp, + fri_schedule_override: FriScheduleOverride::new(&[d as u8]), + ..ProofFormat::DEFAULT + }, + }; + shape.check(); + assert_eq!(shape.schedule(), vec![d as u8]); + assert_eq!(shape.layer_depth(0), 2); + + let n = 1usize << d; + let mut b = LfmBuilder::new().with_wrap_hash(super::edsl::WrapHash::production()); + let arena = b.declare_arena((4 + d as usize + times * (n + 2)) as u32); + let root = b.hint_word(arena, 0); + let 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..d).map(|i| b.hint_word(arena, 4 + i).as_ext()).collect(); + let mut at = 4 + d; + let openings: Vec = (0..times) + .map(|_| { + let values = (0..n) + .map(|_| { + at += 1; + b.hint_word(arena, at - 1).as_ext() + }) + .collect(); + let siblings = (0..2) + .map(|_| { + at += 1; + super::edsl::WrapDigest::from_cell(b.hint_word(arena, at - 1)) + }) + .collect(); + LayerOpening { values, siblings } + }) + .collect(); + for opening in &openings { + emit_group_layer( + &mut b, + shape, + 0, + &commitment, + &zetas, + v, + y_inv, + opening, + &bits, + ); + } + compile(b.finish()) +} + +/// `(selects, XALU, BALU, hashes, unpacks, hints)` over an instruction list. +fn count_kinds(instrs: &[Instr]) -> (usize, usize, usize, usize, usize, usize) { + let mut k = (0, 0, 0, 0, 0, 0); + for i in instrs { + match i { + Instr::Select { .. } => k.0 += 1, + Instr::ExtAlu { .. } => k.1 += 1, + Instr::BaseAlu { .. } => k.2 += 1, + Instr::Hash { .. } => k.3 += 1, + Instr::Unpack { .. } => k.4 += 1, + Instr::Hint { .. } => k.5 += 1, + _ => {} + } + } + k +} + +/// ★ RULINGS 13: the rows the emitter emits per group layer, against the +/// DP's cost-model terms (I-FRI-H's weights, `stark::fri::schedule`): +/// +/// ```text +/// model, per query per committed layer of exponent d over a depth-D tree: +/// leaf(d)·compress + D·(compress + select) + (2^d − 1)·select +/// + (2^d − 1)·fold(5 XALU) + d·twiddle(1 BALU) +/// ``` +/// +/// The three terms the ruling names — the slot mux, the group fold and the +/// twiddle chain — each MATCH the emitter row for row (and so do the leaf and +/// the walk). The emitter ALSO emits rows the model does not price, and this +/// test pins them rather than hiding them, because the schedule is a format +/// constant and a change of weights is the lead's ruling (RULINGS 13): +/// +/// - `x_g⁻¹ = y⁻¹·ω^{br(slot)}`: `d` selects of constants and `d` base muls; +/// - fold-level scaling: one `emul_base` per level with more than two pairs +/// (`max(0, d − 2)` XALU) and one base mul on the level with two pairs +/// (`[d ≥ 2]` BALU); +/// - the slot check's `assert_eq_ext`: 2 XALU; +/// - the per-opening root (or cap node) compare: 8 BALU rows (four lowered +/// asserts) and one unpack — which today's pair layer pays as well; +/// - the group's `2^d` unpacks (the leaf reads three lanes of each value) and +/// `2^d` value hints, plus the walked root's one unpack and the path hints. +/// +/// At `d = 1` the model is today's pair layer exactly (1 select, 5 XALU, +/// 1 BALU); the group encoding at `d = 1` pays the extras on top. +#[test] +fn the_group_layer_rows_against_the_dp_cost_model() { + let w = FRI_COST_WEIGHTS; + let depth = 2usize; + println!( + "\n d | model sel/XALU/BALU/hash | emitted sel/XALU/BALU/hash | unmodelled \ + sel/XALU/BALU unpack hint | model ns unmodelled ns" + ); + for d in 1..=6u32 { + let r = measure_group_layer(d); + let n = 1usize << d; + // The model's rows (the ruling's terms plus the leaf and the walk). + let m_sel = (n - 1) * FRI_SLOT_SELECT_ROWS as usize + depth; + let m_xalu = (n - 1) * FRI_FOLD_XALU_ROWS as usize; + let m_balu = d as usize * FRI_TWIDDLE_BALU_ROWS as usize; + let m_hash = fri_leaf_blocks(d) as usize + depth; + // What the emitter adds on top, by construction (see the doc). + let x_sel = d as usize; + let x_xalu = 2 + (d as usize).saturating_sub(2); + // + the per-opening root compare: four lowered base asserts (a `sub` + // and a `div` each), today's pair layer pays it too. + let x_balu = d as usize + usize::from(d >= 2) + 8; + assert_eq!( + r.hashes, m_hash, + "d={d}: leaf blocks + one compression per level" + ); + assert_eq!(r.selects, m_sel + x_sel, "d={d}: selects"); + assert_eq!(r.xalu, m_xalu + x_xalu, "d={d}: XALU rows"); + assert_eq!(r.balu, m_balu + x_balu, "d={d}: BALU rows"); + assert_eq!( + r.unpacks, + n + 1, + "d={d}: the group's unpacks and the walked root's" + ); + assert_eq!(r.hints, n + depth, "d={d}: the group's values and its path"); + let model_ns = m_sel as u64 * w.cap.select + + (n as u64 - 1) * w.fold + + d as u64 * w.twiddle + + m_hash as u64 * w.cap.compress; + let unmodelled_ns = x_sel as u64 * w.cap.select + + x_xalu as u64 * XALU_NS + + x_balu as u64 * BALU_NS + + (n as u64 + 1) * w.cap.unpack + + n as u64 * w.cap.hint; + println!( + " {d} | {m_sel:>3}/{m_xalu:>4}/{m_balu:>2}/{m_hash:>2} | \ + {:>3}/{:>4}/{:>2}/{:>2} | {x_sel:>3}/{x_xalu:>4}/{x_balu:>2} \ + {:>4} {:>4} | {model_ns:>8} {unmodelled_ns:>8} ({} instructions)", + r.selects, + r.xalu, + r.balu, + r.hashes, + n + 1, + n, + r.total, + ); + } + + // What the unmodelled rows would do to the schedule, for the lead: the DP + // re-run with them priced (hint words priced at the cap policy's hint + // weight), at the production terminals and Q = 110 under cap = auto. + // Printed, not asserted: changing the objective is a format change. + let cap = CapPolicy::Auto; + let q = 110u64; + let with_extras = |d: u32, depth: u32| -> u64 { + let base = stark::fri::schedule::fri_layer_cost_q(&w, d, depth, q, cap); + let n = 1u64 << d; + let extra = u64::from(d) * w.cap.select + + (2 + u64::from(d.saturating_sub(2))) * XALU_NS + + (u64::from(d) + u64::from(d >= 2) + 8) * BALU_NS + + (n + 1) * w.cap.unpack + + n * w.cap.hint; + base + q * extra + }; + println!("\n schedules at Q = 110, cap = auto: the ruled objective vs the emitted rows"); + for t in [9u32, 10] { + for b0 in [13u32, 18, 20, 21, 23] { + let ruled = fri_schedule_by(b0, t, 6, &|d, depth| { + stark::fri::schedule::fri_layer_cost_q(&w, d, depth, q, cap) + }); + let emitted = fri_schedule_by(b0, t, 6, &with_extras); + println!( + " T={t} b0={b0}: ruled {:?} (ns·Q {}) | with the emitted rows {:?} \ + (ns·Q {})", + ruled.schedule, ruled.cost_q, emitted.schedule, emitted.cost_q + ); + } + } +} + +/// Cost-law prices of an `XALU` and a `BALU` row, the schedule module's. +const XALU_NS: u64 = stark::fri::schedule::XALU_ROW_NS; +const BALU_NS: u64 = stark::fri::schedule::BALU_ROW_NS; diff --git a/prover/src/lfm/fri_tests.rs b/prover/src/lfm/fri_tests.rs index bb0d92190..6a1193543 100644 --- a/prover/src/lfm/fri_tests.rs +++ b/prover/src/lfm/fri_tests.rs @@ -130,23 +130,24 @@ pub(super) fn folding_fixture_with( } /// 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)`. Paths are cut at each layer's - /// cap (query 0's cap split off into [`Self::caps`]). - 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. - caps: Vec, + pub(super) caps: Vec, } /// Build the FRI host fixture for a real proof of `num_boundaries` rows. @@ -157,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 { @@ -169,31 +170,9 @@ fn host_fri_from( let shape = FriShape::from_options(opts, trace.shape.log2_lde_length); shape.check(); - // Query 0 of a capped layer is its owner: the cap rides after the - // `D − c` siblings and goes to the caps arena. - let mut caps = Vec::new(); - let openings = (0..view.query_list_len()) - .map(|q| { - let d = view.query(q); - d.layers_evaluations_sym() - .iter() - .enumerate() - .map(|(i, sym)| { - let path = d.layer_auth_path(i); - let (depth, c) = (shape.layer_depth(i), shape.layer_cap(i)); - if c == 0 || q != 0 { - assert_eq!(path.len(), depth - c, "query {q} layer {i}"); - return (*sym, 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); - (*sym, siblings.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, @@ -208,7 +187,7 @@ fn host_fri_from( impl HostFri { /// The arenas the FRI-only program declares, for the given queries. - fn fri_arenas(&self, queries: &[usize]) -> 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(), @@ -223,11 +202,11 @@ impl HostFri { } /// 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)); } } @@ -244,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); @@ -382,7 +361,7 @@ 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); let (arenas, fri) = declare_fri(&mut b, shape, num_queries); @@ -420,7 +399,7 @@ fn fri_only_program(shape: FriShape, num_queries: usize) -> LfmProgram { impl HostFri { /// The `(index, p₀, p₀ˢ)` arena [`fri_only_program`] reads. - fn deep_arena(&self, queries: &[usize]) -> Vec { + 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))); @@ -431,7 +410,7 @@ impl HostFri { } /// 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 @@ -771,14 +750,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() } diff --git a/prover/src/lfm/mod.rs b/prover/src/lfm/mod.rs index a4512811f..8d315a117 100644 --- a/prover/src/lfm/mod.rs +++ b/prover/src/lfm/mod.rs @@ -148,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; diff --git a/prover/src/lfm/per_table_census_tests.rs b/prover/src/lfm/per_table_census_tests.rs index 1af37c087..b92101ae1 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; @@ -520,7 +519,7 @@ 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 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(); From 59639d3c3b46c0003c31f7395fa663fdd8a98686 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:41:47 -0300 Subject: [PATCH 36/73] docs(stark): the Merkle cap and fri=dp are in the in-guest verifier MERKLE_CAP_IMPLEMENTED and FRI_MODE_IMPLEMENTED documented the LFM in-guest verifier as missing (C5, G1/G2); it now verifies capped and group-leaf proofs. Device group-leaf FRI (I-FRI-D) and the RV64 guest (default-only, RULINGS 11) remain as stated. Flags unchanged. --- crypto/stark/src/proof/options.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/crypto/stark/src/proof/options.rs b/crypto/stark/src/proof/options.rs index 5e12f728f..d8a450218 100644 --- a/crypto/stark/src/proof/options.rs +++ b/crypto/stark/src/proof/options.rs @@ -241,11 +241,11 @@ impl FromStr for OneRowMode { /// could print a non-default format and prove the default one. Each lane /// flips its own flag in the commit that makes the lever real. /// -/// The Merkle cap is real on the host and device STARK provers and the host -/// verifier (design/CAP.md C3 + C4). ⚠ NOT yet in the LFM in-guest verifier -/// (C5): a recursion run that wraps a capped proof fails closed there, so -/// `LAMBDA_VM_ZF_CAP` is for STARK-level tests and measurements until C5 -/// lands. +/// The Merkle cap is real on the host and device STARK provers, the host +/// verifier (design/CAP.md C3 + C4) and the LFM in-guest STARK verifier (C5: +/// `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 +/// default-only (RULINGS 11). pub const MERKLE_CAP_IMPLEMENTED: bool = true; /// `FriMode::Dp` (S3) is implemented on the HOST paths only: @@ -255,12 +255,13 @@ pub const MERKLE_CAP_IMPLEMENTED: bool = true; /// layer commit, the device query gather) is taken only for `Pair`; a `Dp` /// table runs the CPU FRI loop (DEEP may still run on the device). /// -/// NOT implemented: device group-leaf FRI (lane I-FRI-D), the in-guest (LFM) -/// verifier of a `Dp` proof (lane I-FRI-G: `lfm::fri::FriShape` still derives -/// the legacy layout, so an LFM wrap or node over a `Dp` proof fails at emit -/// time), and the RV64 recursion guest (default-only by RULINGS 11; it refuses -/// a non-default format). A block run under `LAMBDA_VM_ZF_FRI=dp` therefore -/// proves and host-verifies its STARK proofs but cannot recurse over them yet. +/// - 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: device group-leaf FRI (lane I-FRI-D) and the RV64 +/// recursion guest (default-only by RULINGS 11; it refuses a non-default +/// format). pub const FRI_MODE_IMPLEMENTED: bool = true; /// See [`MERKLE_CAP_IMPLEMENTED`]. From bbe1f3caffa62faa9dc0dbdbbcde0426884e7d84 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:45:11 -0300 Subject: [PATCH 37/73] test(prover): a parseable census line from the knob-on assembled verifier twin ZFTWIN legs_permutations / cap_root_permutations / legs_instructions / legs_selects / whole_instructions, so the box wrapper compares the four format arms (off, cap=auto, fri=dp, both) without scraping prose. --- prover/src/lfm/epoch_verify_tests.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/prover/src/lfm/epoch_verify_tests.rs b/prover/src/lfm/epoch_verify_tests.rs index 6e74b0b84..44bef98ed 100644 --- a/prover/src/lfm/epoch_verify_tests.rs +++ b/prover/src/lfm/epoch_verify_tests.rs @@ -1620,6 +1620,14 @@ fn the_assembled_epoch_verifier_runs_at_the_process_format() { "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(), + ); // 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. From c63780e312f5605d39712b273702a698aecbe595 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:45:17 -0300 Subject: [PATCH 38/73] test(stark): key the M3 query-bound mutation by the LDE size The M3 switch was a process-global bool: while the M3 test held it, every concurrently running one-row test sampled over N/2 (query_rows_bounds_and_depths failed once on the laptop), and a proof straddling the flip could have seen different bounds on its prover and verifier sides. It now mutates only an LDE of exactly the stored size (8192 points, 4096 rows at blowup 2), a shape no other one-row test proves at; the bound test and M3 share that shape and a lock. --- crypto/stark/src/leaf_layout.rs | 19 ++++++++++------- crypto/stark/src/tests/one_row_tests.rs | 28 ++++++++++++++----------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/crypto/stark/src/leaf_layout.rs b/crypto/stark/src/leaf_layout.rs index 7bb30fea8..389b55816 100644 --- a/crypto/stark/src/leaf_layout.rs +++ b/crypto/stark/src/leaf_layout.rs @@ -66,7 +66,9 @@ impl LeafLayout { match self { Self::RowPair => lde_len >> 1, #[cfg(test)] - Self::Row if M3_PAIR_BOUND_UNDER_ONE_ROW.load(core::sync::atomic::Ordering::SeqCst) => { + Self::Row + if M3_PAIR_BOUND_AT_LDE.load(core::sync::atomic::Ordering::SeqCst) == lde_len => + { lde_len >> 1 } Self::Row => lde_len, @@ -99,14 +101,15 @@ impl LeafLayout { } /// Mutation M3 (FRI.md §10), test builds only: sample one-row query indexes -/// over the row-pair bound `N / 2`. 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); the tests that set it hold -/// `one_row_tests::M3_LOCK`, and every other proof stays valid while it is set. +/// 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_UNDER_ONE_ROW: core::sync::atomic::AtomicBool = - core::sync::atomic::AtomicBool::new(false); +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. diff --git a/crypto/stark/src/tests/one_row_tests.rs b/crypto/stark/src/tests/one_row_tests.rs index 29fb2576b..8a7b31578 100644 --- a/crypto/stark/src/tests/one_row_tests.rs +++ b/crypto/stark/src/tests/one_row_tests.rs @@ -22,7 +22,7 @@ use crate::fri::fri_functions::compute_coset_twiddles_inv; use crate::fri::terminal::FriFoldLayout; use crate::fri::{commit_phase_with_layout, fold_times}; use crate::leaf_layout::{ - LeafLayout, M3_PAIR_BOUND_UNDER_ONE_ROW, TableWidths, resolve_leaf_layout, table_leaf_layout, + LeafLayout, M3_PAIR_BOUND_AT_LDE, TableWidths, resolve_leaf_layout, table_leaf_layout, table_openings_cost_q, }; use crate::proof::options::{FriMode, FriScheduleOverride, OneRowMode, ProofFormat, ProofOptions}; @@ -43,7 +43,7 @@ type Felt = FieldElement; type Ext = FieldElement; /// Serialises the tests that flip the process-global M3 switch (see -/// `leaf_layout::M3_PAIR_BOUND_UNDER_ONE_ROW`). +/// `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 { @@ -432,7 +432,7 @@ fn one_row_zero_fold_case() { // --------------------------------------------------------------------------- /// The query indexes the verifier draws for a one-row SimpleAddition proof of -/// `rows` rows at blowup 2 with `queries` queries (and the proof verifies). +/// `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); @@ -441,9 +441,13 @@ fn one_row_iotas(rows: usize, queries: usize) -> (Vec, bool) { (rec.iotas.clone(), ok) } -/// With 64 queries over an LDE of 64 points, all 64 indexes below `N / 2` -/// has probability 2⁻⁶⁴ under the right bound; the pair bound makes it -/// certain. +/// 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) } @@ -451,9 +455,9 @@ fn upper_half_reached(iotas: &[usize], lde: usize) -> bool { #[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(32, 64); + let (iotas, ok) = one_row_iotas(M3_ROWS, 64); assert!(ok); - assert!(upper_half_reached(&iotas, 64), "iotas {iotas:?}"); + assert!(upper_half_reached(&iotas, M3_LDE), "iotas {iotas:?}"); } /// M3: sample r over N/2 under one row. Prover and verifier agree on the @@ -462,12 +466,12 @@ fn one_row_query_indexes_cover_the_whole_lde() { #[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_UNDER_ONE_ROW.store(true, std::sync::atomic::Ordering::SeqCst); - let (iotas, ok) = one_row_iotas(32, 64); - M3_PAIR_BOUND_UNDER_ONE_ROW.store(false, std::sync::atomic::Ordering::SeqCst); + 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, 64), + !upper_half_reached(&iotas, M3_LDE), "under the mutation the bound test must fail" ); } From 1adbebef6aadcc18595705026b90f3ce104b3677 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:45:17 -0300 Subject: [PATCH 39/73] feat(prover): the preprocessed roots of one-row tables (H5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit design/FRI.md §7.5 + REVIEW-FRI F8, RULINGS 14. Every default root is unchanged (the row-pair paths are today's code; the one-row sources are only asked when a table resolves to one row). - Static tables: a ONE-ROW twin match table per static root (bitwise/keccak_rc static_commitment_one_row, page zero-init and private twins) for STATIC_BLOWUP_FACTORS_ONE_ROW = [4], generated by `compute_static_commitments --layout row` (the default output unchanged) and pinned by one-row drift tests. `*_preprocessed_commitment_for(options, layout)`: row pairs = today's wrapper; one row = the twin at coset 3, else None — a hard miss, never a recompute (RULINGS 14: the prover's PrecomputedCommitmentMissing, the verifier's reject). - Runtime roots parameterised by the leaf layout: lfm::commit (commit_lde_columns_with, commit_columns_with, commit_group_device_or_host_with — the device arm is row-pair only, so a one-row root is always the host pass, F8.1), decode, register (incl. the with-fini continuation root), page data/offset commits. - Every VM AIR (lib.rs VmAirs, continuation's global-memory AIRs) is built with a LazyCommitment serving both layouts; a SUPPLIED root (recursion guest, continuation genesis) is row-pair and never stands in for one row — the one-row root is computed from the data on first use. - The in-circuit register commitment (programs::emit_register_commitment) takes rows_per_leaf from RegisterDerivationShape (one emitter, two constants); its host twin is compute_precomputed_commitment_with_fini_layout. - LFM: LfmArtifacts.one_row_roots (built when one_row != Off: every group's one-row root on the host, plus the static twins; NOT folded into program_id), LfmAirs::with_one_row_roots, attached on the prove and verify paths. Registry policy (F8.4): LFM_REGISTRY stays row-pair only; registry::resolve_artifacts reads it at the default and, under a one-row format, rebuilds the fixture from code instead (LfmProgramKind::program). - stark: AirWithBuses::with_one_row_commitment. Tests: laptop — static one-row drift (bitwise, keccak_rc, pages) and the hard-miss cases; the AIR commitment sources per layout; lfm::one_row_tests (commit helpers per layout, the registry policy via a test-only read counter, artifacts' one-row roots, the register derivation executed in-circuit == its host twin at BOTH layouts, blowup 2 and 4). Box — tests::zf_vm_one_row_tests: a VM proof at one_row=1 and at auto+dp (blowup 4), the blowup-2 missing-twin proving error, an LFM TrivialV0 proof at one_row=1 verified through lfm_verify. --- crypto/stark/src/lookup.rs | 11 + prover/src/bin/compute_static_commitments.rs | 49 ++++- prover/src/continuation.rs | 37 ++-- prover/src/lfm/airs.rs | 27 +++ prover/src/lfm/commit.rs | 40 +++- prover/src/lfm/epoch_tests.rs | 1 + prover/src/lfm/machine_tests.rs | 2 + prover/src/lfm/mod.rs | 2 + prover/src/lfm/one_row_tests.rs | 213 +++++++++++++++++++ prover/src/lfm/programs.rs | 26 ++- prover/src/lfm/proof.rs | 51 ++++- prover/src/lfm/registry.rs | 104 +++++++++ prover/src/lib.rs | 61 +++--- prover/src/tables/bitwise.rs | 55 ++++- prover/src/tables/decode.rs | 42 +++- prover/src/tables/keccak_rc.rs | 54 ++++- prover/src/tables/mod.rs | 8 + prover/src/tables/page.rs | 145 ++++++++++++- prover/src/tables/register.rs | 70 +++++- prover/src/tests/mod.rs | 2 + prover/src/tests/static_commitments_tests.rs | 193 +++++++++++++++++ prover/src/tests/zf_vm_one_row_tests.rs | 157 ++++++++++++++ 22 files changed, 1264 insertions(+), 86 deletions(-) create mode 100644 prover/src/lfm/one_row_tests.rs create mode 100644 prover/src/tests/zf_vm_one_row_tests.rs diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index fb8239c88..a748aba3a 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -1151,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. /// 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..78fdcfd69 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 (RULINGS 14). `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..364033748 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,9 +213,22 @@ 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 builds row-pair leaves only (`gpu_lde::try_commit_row_major`), so a +/// one-row root (S2) is always the host pass (REVIEW-FRI F8.1: gated, until +/// the device lane makes it layout-aware). +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 { + if device_artifacts() && group.padded_rows > 0 && group.width > 0 && !layout.is_one_row() { let set = stark::device_set::commit_device_set( group.padded_rows, group.width, @@ -240,7 +270,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. diff --git a/prover/src/lfm/epoch_tests.rs b/prover/src/lfm/epoch_tests.rs index a18887c19..e5a1df17a 100644 --- a/prover/src/lfm/epoch_tests.rs +++ b/prover/src/lfm/epoch_tests.rs @@ -1185,6 +1185,7 @@ fn harvest_real_epoch( reg_shape: super::programs::RegisterDerivationShape { blowup: opts.blowup_factor as usize, coset_offset: opts.coset_offset, + rows_per_leaf: stark::commitment::ROWS_PER_LEAF, }, expected_program_id: crate::recursion::program_id_from_digest( &crate::statement::elf_digest(&elf_bytes), 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/mod.rs b/prover/src/lfm/mod.rs index fedba4a1e..693ddd7c2 100644 --- a/prover/src/lfm/mod.rs +++ b/prover/src/lfm/mod.rs @@ -161,6 +161,8 @@ mod logup_tests; #[cfg(test)] mod machine_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_tests.rs b/prover/src/lfm/one_row_tests.rs new file mode 100644 index 000000000..e145eccfa --- /dev/null +++ b/prover/src/lfm/one_row_tests.rs @@ -0,0 +1,213 @@ +//! S2 (one-row openings) on the LFM side, host only (design/FRI.md §7.5.2–4, +//! REVIEW-FRI F8): 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 (FRI.md §7.5.4): `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 (RULINGS 14), 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 (FRI.md §7.5.3). 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/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 9aa96df42..7f0811d6e 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` (RULINGS 14). + 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, RULINGS 14). +#[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(&[]); diff --git a/prover/src/lfm/registry.rs b/prover/src/lfm/registry.rs index 8130d0c5c..c88b1cce4 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 (design/FRI.md §7.5.4): `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 (design/FRI.md §7.5.4): 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, RULINGS 14). 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/lib.rs b/prover/src/lib.rs index 9466d3fea..2da4ca4f7 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -54,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; @@ -1031,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| { @@ -1074,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, @@ -1134,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), ), @@ -1171,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)), ), @@ -1184,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)), @@ -1220,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)), )) @@ -1239,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, diff --git a/prover/src/tables/bitwise.rs b/prover/src/tables/bitwise.rs index 37cf591fd..78e1c2b38 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 (RULINGS 14): 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 (RULINGS 14), 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..7fd256a17 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 (RULINGS 14): 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, RULINGS 14). +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..5d57eeb87 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 (design/FRI.md §7.5: 4 for +/// the base and for the LFM chips): under one row any other blowup is a hard +/// miss (RULINGS 14), 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..2016b87d8 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, RULINGS 14). +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/tests/mod.rs b/prover/src/tests/mod.rs index 8d5e7bb0c..ffe121932 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -136,3 +136,5 @@ pub mod zf_rpx_golden_tests; 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/static_commitments_tests.rs b/prover/src/tests/static_commitments_tests.rs index 7b3d38e12..4169a70c6 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: design/FRI.md §7.5.1, RULINGS 14 +// ========================================================================= +// +// 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"); + } +} + +/// RULINGS 14: 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/zf_vm_one_row_tests.rs b/prover/src/tests/zf_vm_one_row_tests.rs new file mode 100644 index 000000000..b8a5eaebe --- /dev/null +++ b/prover/src/tests/zf_vm_one_row_tests.rs @@ -0,0 +1,157 @@ +//! 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). +//! - RULINGS 14 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). + +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() + ); +} + +/// RULINGS 14: 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" + ); +} From dfc498b788ab74988487501e971b7d3173b15d5d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:45:17 -0300 Subject: [PATCH 40/73] test(stark,prover): the S2 test vectors (H6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit design/FRI.md §10 (e), for the device (D2) and in-guest (G3) lanes, under Keccak, Blake3 and RPX: - e_leaf_digests_*: one-row trace-tree leaves (KAT base and ext3 matrices, every leaf and the root at rows_per_leaf 1 and 2); - e_proof_*_{one_row_pair,one_row_3_2_1_2}.{json,rkyv}: the (d) proof shape with one-row openings — trace leaf r over the whole LDE, DEEP at one point, the input tree as FRI layer 0 (root absorbed before the first zeta; one zeta per layer), per-layer position/leaf/slot/values/path_len. The (d) files are byte-identical (proof_vectors now shares proof_files with (e); the row-pair JSON schema is unchanged). README documents (e). vectors_are_current / rpx_vectors_are_current regenerate and compare all. --- crypto/stark/src/fri/vectors.rs | 155 +++++++++++++++++- crypto/stark/src/tests/zf_fri_vectors.rs | 12 +- crypto/stark/tests/vectors/zf_fri/README.md | 46 +++++- .../vectors/zf_fri/e_leaf_digests_blake3.json | 11 ++ .../vectors/zf_fri/e_leaf_digests_keccak.json | 11 ++ .../vectors/zf_fri/e_leaf_digests_rpx.json | 11 ++ .../e_proof_blake3_one_row_3_2_1_2.json | 30 ++++ .../e_proof_blake3_one_row_3_2_1_2.rkyv | Bin 0 -> 9432 bytes .../zf_fri/e_proof_blake3_one_row_pair.json | 30 ++++ .../zf_fri/e_proof_blake3_one_row_pair.rkyv | Bin 0 -> 12872 bytes .../e_proof_keccak_one_row_3_2_1_2.json | 30 ++++ .../e_proof_keccak_one_row_3_2_1_2.rkyv | Bin 0 -> 9432 bytes .../zf_fri/e_proof_keccak_one_row_pair.json | 30 ++++ .../zf_fri/e_proof_keccak_one_row_pair.rkyv | Bin 0 -> 12872 bytes .../zf_fri/e_proof_rpx_one_row_3_2_1_2.json | 30 ++++ .../zf_fri/e_proof_rpx_one_row_3_2_1_2.rkyv | Bin 0 -> 9432 bytes .../zf_fri/e_proof_rpx_one_row_pair.json | 30 ++++ .../zf_fri/e_proof_rpx_one_row_pair.rkyv | Bin 0 -> 12872 bytes prover/src/tests/zf_rpx_vectors.rs | 12 +- 19 files changed, 417 insertions(+), 21 deletions(-) create mode 100644 crypto/stark/tests/vectors/zf_fri/e_leaf_digests_blake3.json create mode 100644 crypto/stark/tests/vectors/zf_fri/e_leaf_digests_keccak.json create mode 100644 crypto/stark/tests/vectors/zf_fri/e_leaf_digests_rpx.json create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_3_2_1_2.json create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_3_2_1_2.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_pair.json create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_blake3_one_row_pair.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_3_2_1_2.json create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_3_2_1_2.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_pair.json create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_keccak_one_row_pair.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_3_2_1_2.json create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_3_2_1_2.rkyv create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_pair.json create mode 100644 crypto/stark/tests/vectors/zf_fri/e_proof_rpx_one_row_pair.rkyv diff --git a/crypto/stark/src/fri/vectors.rs b/crypto/stark/src/fri/vectors.rs index 174d3ab2c..547fd7a47 100644 --- a/crypto/stark/src/fri/vectors.rs +++ b/crypto/stark/src/fri/vectors.rs @@ -332,7 +332,126 @@ fn logup_case( pub fn proof_vectors(hash_name: &str) -> Vec { let mut out = Vec::new(); for (fmt_name, format) in proof_formats() { + out.extend(proof_files::(hash_name, fmt_name, format, "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 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, "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, + prefix: &str, +) -> Vec { + let mut out = Vec::new(); + { let (air, mut trace, pi) = logup_case(format); + let one_row = crate::leaf_layout::table_leaf_layout(&air, PROOF_ROWS).is_one_row(); let proof = GenericProver::::prove( &air, &mut trace, @@ -347,14 +466,15 @@ pub fn proof_vectors(hash_name: &str) -> Vec { &mut DefaultTranscript::::new(&[]), ) }); - assert!(ok, "the (d) proof must verify"); + 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(), false).expect("layout"); - let stem = format!("d_proof_{hash_name}_{fmt_name}"); + 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", @@ -364,6 +484,13 @@ pub fn proof_vectors(hash_name: &str) -> Vec { 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\": 3,\n \"grinding_factor\": 0,\n \"coset_offset\": 3," ); + 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\": {:?},", @@ -409,12 +536,22 @@ pub fn proof_vectors(hash_name: &str) -> Vec { index >> d }; } - 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(", ") - )); + 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"); diff --git a/crypto/stark/src/tests/zf_fri_vectors.rs b/crypto/stark/src/tests/zf_fri_vectors.rs index d3d01f05f..9310974fd 100644 --- a/crypto/stark/src/tests/zf_fri_vectors.rs +++ b/crypto/stark/src/tests/zf_fri_vectors.rs @@ -1,4 +1,4 @@ -//! The exported S3 vectors (FRI.md §10 (a)–(d)) under Keccak and Blake3 are +//! The exported S3 and S2 vectors (FRI.md §10 (a)–(e)) 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: @@ -6,7 +6,8 @@ use crate::config::{Blake3StarkHash, KeccakStarkHash}; use crate::fri::vectors::{ - VectorFile, check_or_write, group_fold_json, leaf_digests_json, proof_vectors, schedules_json, + 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 { @@ -18,13 +19,18 @@ fn all() -> Vec { ]; 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 * 3 * 2); + assert_eq!(files.len(), 4 + 2 * 3 * 2 + 2 + 2 * 2 * 2); let bad = check_or_write(&files, false); assert!( bad.is_empty(), diff --git a/crypto/stark/tests/vectors/zf_fri/README.md b/crypto/stark/tests/vectors/zf_fri/README.md index 4cbde2e1b..c6853c300 100644 --- a/crypto/stark/tests/vectors/zf_fri/README.md +++ b/crypto/stark/tests/vectors/zf_fri/README.md @@ -1,4 +1,4 @@ -# S3 FRI vectors (group-leaf FRI layers) +# 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 @@ -11,8 +11,8 @@ 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_*` | `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_*` | `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` | +| `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. @@ -93,8 +93,42 @@ authentication `path_len`. Formats: `pair` (today, all-ones schedule), explicit uneven schedule via the test hook `fri_schedule_override`: unequal neighbouring exponents are what catch a fold-count off-by-one). +**(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 -- (e) S2 one-row leaf digests and the input-tree root (H6, after S2). -- A vector with a Merkle cap (`Q ≥ 20` so `cap = auto` caps; REVIEW-FRI F9): - the cap is not implemented on this branch. +- A vector with a Merkle cap (`Q ≥ 20` so `cap = auto` caps; REVIEW-FRI F9). + The cap now composes with dp and one_row on the host (`one_row_tests:: + cap_fri_one_row_matrix_round_trips`), but no capped vector is exported. 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 0000000000000000000000000000000000000000..3d83f778c5161b5d983825fd0bb11201296589c5 GIT binary patch literal 9432 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2f9915aac0468e880df4e5ff796e9b506cb671b2 GIT binary patch literal 12872 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 sSN$k@`k(c``r~xY|6$Nix4l2#-xvSS(fq&FuQ, 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 0000000000000000000000000000000000000000..df7b44bd10fce4da050ab906f227fc4a07140410 GIT binary patch literal 9432 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, 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 0000000000000000000000000000000000000000..917d6a8230cfe6ef41055a9614f2e3ecbc49c335 GIT binary patch literal 12872 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 J, 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 0000000000000000000000000000000000000000..b73dc5fb5a309bdcdd3761183d6fd7dfd2e66a67 GIT binary patch literal 9432 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, 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 0000000000000000000000000000000000000000..666fd43a2bb97c7aca8d8a39e08de8a16225a3e4 GIT binary patch literal 12872 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 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 + 3 * 2); + assert_eq!(files.len(), 1 + 3 * 2 + 1 + 2 * 2); let bad = check_or_write(&files, false); assert!( bad.is_empty(), From 525bf0511029d2b36aa71212dfadd2273728d565 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:46:55 -0300 Subject: [PATCH 41/73] feat(stark): ONE_ROW_IMPLEMENTED = true (S2 on the host CPU paths) LAMBDA_VM_ZF_ONE_ROW = 1 | auto is now selectable. The flag's doc lists what is and is not implemented: CPU prover + host verifier + the preprocessed roots (one-row static twins at blowup 4 only; elsewhere a one-row table with a static root is a hard proving error, RULINGS 14); on cuda a one-row table runs every commit/opening on the host; NOT device one-row trees/openings (I-FRI-D D2), NOT the in-guest verifier (I-FRI-G G3, refused at emit time), NOT the RV64 guest (RULINGS 11). So a block run under the knob proves and host-verifies but cannot recurse over one-row STARK proofs yet. zf_format::the_one_row_knob_is_selectable pins it; production_sites_prove_at_the_process_format now also checks the one-row knob (1: every table one-row, symmetric rows iff row pairs, group encoding). --- crypto/stark/src/proof/options.rs | 25 ++++++++++++-- prover/src/tests/zf_rpx_golden_tests.rs | 44 ++++++++++++++++++++----- prover/src/zf_format.rs | 23 +++++++++++++ 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/crypto/stark/src/proof/options.rs b/crypto/stark/src/proof/options.rs index 5e12f728f..1199e2c54 100644 --- a/crypto/stark/src/proof/options.rs +++ b/crypto/stark/src/proof/options.rs @@ -263,8 +263,29 @@ pub const MERKLE_CAP_IMPLEMENTED: bool = true; /// proves and host-verifies its STARK proofs but cannot recurse over them yet. pub const FRI_MODE_IMPLEMENTED: bool = true; -/// See [`MERKLE_CAP_IMPLEMENTED`]. -pub const ONE_ROW_IMPLEMENTED: bool = false; +/// `OneRowMode::{On, Auto}` (S2) is implemented on the HOST CPU paths only: +/// - 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`, RULINGS 6); +/// - 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 (RULINGS 14) +/// — e.g. `one_row = 1` at blowup 2, 8 or 16 fails on BITWISE; +/// - on a `cuda` build a one-row table takes the CPU arm of every commit and +/// opening (never device-only, host aux build) — correct, not fast. +/// +/// NOT implemented: device one-row trees, openings and the device input tree +/// (lane I-FRI-D, D2 — a one-row table on a cuda build runs on the host), the +/// in-guest (LFM) verifier of a one-row proof (lane I-FRI-G, G3: an emitter +/// asked for one refuses at emit time, `lfm::fri::FriShape::from_options`), +/// and the RV64 recursion guest (default-only, RULINGS 11). 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 its default: the proof this diff --git a/prover/src/tests/zf_rpx_golden_tests.rs b/prover/src/tests/zf_rpx_golden_tests.rs index 5d2803023..9fa517602 100644 --- a/prover/src/tests/zf_rpx_golden_tests.rs +++ b/prover/src/tests/zf_rpx_golden_tests.rs @@ -285,17 +285,31 @@ fn rpx_group_path_at_all_ones_equals_legacy() { /// 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). Meant for a knob-on run, `LAMBDA_VM_ZF_FRI=dp` (then it -/// asserts both sites stamp `Dp` and the proofs use group layers); without the -/// knob it proves the same at the default format. Either way it proves. +/// (every LFM proof). Meant for knob-on runs — `LAMBDA_VM_ZF_FRI=dp` (both +/// sites stamp `Dp` and the proofs use group layers) and +/// `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); +/// without a knob it proves the same at the default format. Either way it +/// proves. #[test] fn production_sites_prove_at_the_process_format() { - let knob = std::env::var(crate::zf_format::ENV_FRI).ok(); - let want = match knob.as_deref().map(str::trim) { - Some("dp") => stark::proof::options::FriMode::Dp, - _ => stark::proof::options::FriMode::Pair, + use stark::proof::options::{FriMode, OneRowMode}; + let knob = |name: &str| { + std::env::var(name) + .ok() + .map(|v| v.trim().to_ascii_lowercase()) + }; + let want = match knob(crate::zf_format::ENV_FRI).as_deref() { + Some("dp") => FriMode::Dp, + _ => FriMode::Pair, + }; + let want_one_row = match knob(crate::zf_format::ENV_ONE_ROW).as_deref() { + Some("1") => OneRowMode::On, + Some("auto") => OneRowMode::Auto, + _ => OneRowMode::Off, }; 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", @@ -307,13 +321,27 @@ fn production_sites_prove_at_the_process_format() { ), ] { 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. let (air, proof) = prove_logup(1 << 12, &o); assert!(verify_logup(&air, &proof), "{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(); - if want == stark::proof::options::FriMode::Dp { + 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/zf_format.rs b/prover/src/zf_format.rs index fb648c39a..8cfda686d 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -454,6 +454,29 @@ mod tests { ); } + #[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=off whir_cap=off fri=dp one_row=auto whir_folds=uniform4" + ); + } + #[test] fn the_merkle_cap_knob_is_selectable() { // C3 + C4 made the STARK cap real, so `LAMBDA_VM_ZF_CAP` no longer From 9f5b8f7cc59ca9562c8b477dd69d82f7045cb1db Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:48:48 -0300 Subject: [PATCH 42/73] style(stark): clippy manual_is_multiple_of in one_row_tests --- crypto/stark/src/tests/one_row_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/stark/src/tests/one_row_tests.rs b/crypto/stark/src/tests/one_row_tests.rs index 8a7b31578..17b4e8ee7 100644 --- a/crypto/stark/src/tests/one_row_tests.rs +++ b/crypto/stark/src/tests/one_row_tests.rs @@ -781,6 +781,6 @@ fn widths_of_an_extension_air() { 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 % 3 == 0 && w.composition > 0); + assert!(w.composition.is_multiple_of(3) && w.composition > 0); let _ = ::TWO_ADICITY; } From f7a8c5914b7efe8e0f1bb2bc160d332ecabb8079 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:50:26 -0300 Subject: [PATCH 43/73] =?UTF-8?q?test(stark):=20M1=20at=20the=20input=20tr?= =?UTF-8?q?ee=20=E2=80=94=20the=20input-slot=20check=20is=20load-bearing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A prover that commits and folds p0 + c (low degree, so every layer and the terminal agree) while the trace openings give DEEP(x_r) = p0 is rejected by group0[slot] == DEEP(x_r) and ACCEPTED with the check skipped. --- crypto/stark/src/tests/one_row_tests.rs | 102 ++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/crypto/stark/src/tests/one_row_tests.rs b/crypto/stark/src/tests/one_row_tests.rs index 17b4e8ee7..21cd493cb 100644 --- a/crypto/stark/src/tests/one_row_tests.rs +++ b/crypto/stark/src/tests/one_row_tests.rs @@ -536,6 +536,108 @@ fn input_root_is_absorbed_before_the_first_challenge() { 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].clone(), + 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 RULINGS 14 (a miss is an error). // --------------------------------------------------------------------------- From 25ec130e2a735c51178a7c8baafc679e80c1e1bd Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 19:56:18 -0300 Subject: [PATCH 44/73] style(stark): clippy clone_on_copy in the input-slot M1 test --- crypto/stark/src/tests/one_row_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/stark/src/tests/one_row_tests.rs b/crypto/stark/src/tests/one_row_tests.rs index 21cd493cb..c385fc543 100644 --- a/crypto/stark/src/tests/one_row_tests.rs +++ b/crypto/stark/src/tests/one_row_tests.rs @@ -619,7 +619,7 @@ fn m1_the_input_slot_check_is_load_bearing() { &dec.layers_evaluations_sym, &zetas, r, - deep[r].clone(), + deep[r], x_r.inv().unwrap(), &terminal, &tables, From 730257a4a257fea50a3d5cd5e8ec406ec08dfa6b Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 20:16:38 -0300 Subject: [PATCH 45/73] style: rustfmt the candidate-c merge resolutions make fmt over the two lines the I-S2-H merge adapted (vectors.rs and epoch_verify_tests.rs). Formatting only. --- crypto/stark/src/fri/vectors.rs | 12 +++++++++--- prover/src/lfm/epoch_verify_tests.rs | 5 +++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/crypto/stark/src/fri/vectors.rs b/crypto/stark/src/fri/vectors.rs index 48e7394e4..8534fe7fc 100644 --- a/crypto/stark/src/fri/vectors.rs +++ b/crypto/stark/src/fri/vectors.rs @@ -356,7 +356,9 @@ fn logup_case( 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.extend(proof_files::( + hash_name, fmt_name, format, queries, "d_proof", + )); } out } @@ -514,8 +516,12 @@ fn proof_files( // 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 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\": {:?},", diff --git a/prover/src/lfm/epoch_verify_tests.rs b/prover/src/lfm/epoch_verify_tests.rs index 16e9fab2d..cc04ca14c 100644 --- a/prover/src/lfm/epoch_verify_tests.rs +++ b/prover/src/lfm/epoch_verify_tests.rs @@ -208,8 +208,9 @@ pub(super) fn build_table_legs( // ---- 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, false) - .expect("a format the host lays out"); + let host_caps = + stark::merkle_caps::StarkCaps::for_options(opts, log2_lde_length as usize, false) + .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, From 23ee332e547aa5478c4fc9b1e8e138e942cf7550 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 20:18:29 -0300 Subject: [PATCH 46/73] test(stark,prover): the device (d) vector tests count I-GUEST's capped formats A semantic merge conflict with no textual one: I-FRI-D's proved_vectors_equal_the_cpu_bytes and proved_rpx_vectors_equal_the_cpu_bytes assert proof_vectors() returns the three Q = 3 (d) formats (12 / 6 files, 6 / 3 device FRI commits), but I-GUEST added cap_pair and cap_dp at Q = 20 to proof_formats(), so on the merged tree they return five formats. Both tests would have failed on the box at the file-count assert. The counts now follow the union: 2*5*2 files and 10 device FRI commits (Keccak + Blake3), 5*2 files and 5 commits (RPX). The test's claim is unchanged (every vector proof takes the device FRI commit and equals the checked-in CPU bytes); it now also covers the capped proofs on the device (cap x dp with device-resident group trees). Both tests are ignored and cuda-only: laptop result = compiles (cargo check --features cuda --tests). --- crypto/stark/src/tests/zf_fri_device_tests.rs | 9 ++++++--- prover/src/tests/zf_rpx_device_tests.rs | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crypto/stark/src/tests/zf_fri_device_tests.rs b/crypto/stark/src/tests/zf_fri_device_tests.rs index 4485c42e0..480176893 100644 --- a/crypto/stark/src/tests/zf_fri_device_tests.rs +++ b/crypto/stark/src/tests/zf_fri_device_tests.rs @@ -125,7 +125,8 @@ fn parity_legacy_encoding_blake3() { check::("blake3", &legacy_cases(), false, 0x5a49_0000); } -/// The (d) vector proofs (FRI.md §10 (d): `pair`, `dp`, `dp_3_1_3`) proved on +/// The (d) vector proofs (FRI.md §10 (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 @@ -143,9 +144,11 @@ fn proved_vectors_equal_the_cpu_bytes() { "FRIDEV vector proofs: {} files, {device_commits} device FRI commits", files.len() ); - assert_eq!(files.len(), 2 * 3 * 2); + // 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, 6, + device_commits, 10, "every vector proof must take the device FRI commit (lower LAMBDA_VM_GPU_LDE_THRESHOLD)" ); let bad = check_or_write(&files, false); diff --git a/prover/src/tests/zf_rpx_device_tests.rs b/prover/src/tests/zf_rpx_device_tests.rs index 5041b5df3..0498d4eed 100644 --- a/prover/src/tests/zf_rpx_device_tests.rs +++ b/prover/src/tests/zf_rpx_device_tests.rs @@ -53,7 +53,8 @@ fn parity_legacy_encoding_rpx() { check(&legacy_cases(), false, 0x5a49_0000); } -/// The RPX (d) vector proofs (`pair`, `dp`, `dp_3_1_3`, LDE 4096) proved on +/// 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. @@ -68,9 +69,11 @@ fn proved_rpx_vectors_equal_the_cpu_bytes() { "FRIDEV rpx vector proofs: {} files, {device_commits} device FRI commits", files.len() ); - assert_eq!(files.len(), 3 * 2); + // 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, 3, + device_commits, 5, "every vector proof must take the device FRI commit (lower LAMBDA_VM_GPU_LDE_THRESHOLD)" ); let bad = check_or_write(&files, false); From 8d74043d646e858d6fd63418515caf765d781506 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 20:26:25 -0300 Subject: [PATCH 47/73] fix(prover): key the AIR prototype cache on the proof format `build_air` caches one pre-captured AIR prototype per (name, options) and hands clones out. Its key listed every `ProofOptions` field except `format`, which the ZF levers added after the key was written. The AIR carries its options and the prover and the verifier both read the proof format from `air.options()`, so the first AIR built in a process fixed the format of every later AIR of that name, whatever format the caller asked for. Two candidate-b gate reds were this alone: - merkle_cap_vm at LAMBDA_VM_ZF_CAP=auto: the capped prove cached capped AIRs, and `verify_with_options(.., &default, ..)` got them back, verified the capped proof as a capped proof and accepted it. - zf_vm_dp_tests: in a fresh process the dp prove cached dp AIRs and the "default" verifier accepted the dp proof; in the lib suite an earlier test had cached default AIRs, so the dp prove silently proved at `pair`. The key is now the whole `ProofOptions`, destructured without `..`, so a field added later does not compile until it is keyed. The new `zf_air_cache_tests` fails on the old key (HALT built for fri=dp carried the default format) and passes on the new one. --- prover/src/test_utils.rs | 35 +++++++--- prover/src/tests/mod.rs | 2 + prover/src/tests/zf_air_cache_tests.rs | 88 ++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 prover/src/tests/zf_air_cache_tests.rs 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/mod.rs b/prover/src/tests/mod.rs index 8d5e7bb0c..684e36cf2 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -131,6 +131,8 @@ 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_rpx_golden_tests; #[cfg(test)] pub mod zf_rpx_vectors; 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..b3e617252 --- /dev/null +++ b/prover/src/tests/zf_air_cache_tests.rs @@ -0,0 +1,88 @@ +//! 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. +//! +//! Before this test the key was `(name, blowup, queries, coset, grinding, +//! final degree)` — every `ProofOptions` field except `format`, which the ZF +//! campaign added later. The first AIR built in a process then fixed the +//! format of every later AIR with the same name and parameters, whatever +//! format the caller asked for. Two gate reds on candidate-b were this and +//! nothing else: +//! +//! - `merkle_cap_vm` (`LAMBDA_VM_ZF_CAP=auto`): the capped prove cached capped +//! AIRs, so `verify_with_options(.., &default, ..)` verified the capped proof +//! with those capped AIRs and accepted it. +//! - `zf_vm_dp_tests`: in a fresh process the dp prove cached dp AIRs and the +//! "default" verifier accepted the dp proof; in the lib suite an earlier test +//! had cached default AIRs, so the dp prove proved at `pair` and the +//! non-vacuity assertion fired. +//! +//! 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" + ); + } + } +} From f4d57bdc75119b07e98d667c9196f5d5c70f725c Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 20:28:40 -0300 Subject: [PATCH 48/73] test(prover): merkle_cap_vm asserts the cap policy engaged The cross-format check (a default-format verifier must refuse the capped VM proof) only means something if some tree was really capped. The test now recomputes every table's `StarkCaps` from its public shape and asserts the main-tree paths have exactly the lengths the policy gives (non-owner D - c, owner D - c + 2^c), and that at least one table is capped. Prints `CAPVM capped tables: n of m`. The accept the candidate-b gate saw came from the AIR prototype cache (previous commit), not from this test; this closes the vacuity hole the brief asks every cross-format test to close. --- prover/tests/merkle_cap_vm.rs | 41 ++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/prover/tests/merkle_cap_vm.rs b/prover/tests/merkle_cap_vm.rs index 577531b4d..c201bd521 100644 --- a/prover/tests/merkle_cap_vm.rs +++ b/prover/tests/merkle_cap_vm.rs @@ -18,6 +18,7 @@ use lambda_vm_prover::test_utils::asm_elf_bytes; use lambda_vm_prover::zf_format::ZfFormat; +use stark::merkle_caps::StarkCaps; use lambda_vm_prover::{ GoldilocksCubicProofOptions, MaxRowsConfig, prove_with_options_and_inputs, verify_with_options, }; @@ -64,8 +65,46 @@ fn a_vm_proof_round_trips_under_the_process_cap_policy() { 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. + // 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), From c9b6b1df2d1411dfb4f373a826e915b63c65a2e6 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 20:28:40 -0300 Subject: [PATCH 49/73] test(stark,prover): device-sized ZF tests prove an AirWithBuses table On a cuda build a table whose LDE crosses the GPU threshold is composed on the device, from the AIR's captured constraint IR (`AIR::constraint_program`). `AirWithBuses` - the type of every production table - supplies it; the hand-written example AIRs do not, and the trait default panics by design. `device_trees_serve_their_caps` (2^14 rows) and `production_sites_prove_at_the_process_format` (2^12 rows, LDE 2^14) proved `LogReadOnlyRAP` and panicked with "constraint_program is not available for this AIR" on the box - at the default format too (extra20 had no knob), so a test bug, not a format bug; the GPU composition call is unchanged since the campaign base. New example `stark::examples::bus_permutation`: a self-balancing four-column LogUp `AirWithBuses` (two buses, one committed term column), with CPU tests that it round-trips, has a constraint program, and is rejected when unbalanced. Both device-sized tests now prove it. The RPX goldens still use `LogReadOnlyRAP` and are unchanged. --- crypto/stark/src/examples/bus_permutation.rs | 112 +++++++++++++++++++ crypto/stark/src/examples/mod.rs | 1 + crypto/stark/src/tests/merkle_cap_tests.rs | 27 ++--- prover/src/tests/zf_rpx_golden_tests.rs | 26 ++++- 4 files changed, 144 insertions(+), 22 deletions(-) create mode 100644 crypto/stark/src/examples/bus_permutation.rs diff --git a/crypto/stark/src/examples/bus_permutation.rs b/crypto/stark/src/examples/bus_permutation.rs new file mode 100644 index 000000000..057e553e2 --- /dev/null +++ b/crypto/stark/src/examples/bus_permutation.rs @@ -0,0 +1,112 @@ +//! 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/tests/merkle_cap_tests.rs b/crypto/stark/src/tests/merkle_cap_tests.rs index 3d14a0ea9..73ab1ebcb 100644 --- a/crypto/stark/src/tests/merkle_cap_tests.rs +++ b/crypto/stark/src/tests/merkle_cap_tests.rs @@ -718,30 +718,19 @@ fn a_device_resident_tree_without_a_cap_read_is_an_error() { #[test] #[ignore = "requires a GPU; run with --features cuda -- --ignored"] fn device_trees_serve_their_caps() { - use crate::examples::read_only_memory_logup::{ - LogReadOnlyPublicInputs, LogReadOnlyRAP, read_only_logup_trace, - }; + // 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 = LogReadOnlyPublicInputs; + type Pi = (); let rows = 1usize << 14; - let addresses: Vec = (0..rows as u64) - .map(|i| FE::from((i * 7919) % 4099 + 1)) - .collect(); - let values: Vec = addresses.iter().map(|a| *a * FE::from(10u64)).collect(); let prove_at = |policy| { let opts = options(policy, 30, 2); - let mut trace = read_only_logup_trace::(addresses.clone(), values.clone()); - let cols = trace.columns_main(); - let pi = Pi { - 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(&opts); - let proof = Prover::prove(&air, &mut trace, &pi, &mut DefaultTranscript::::new(&[])) + 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) }; diff --git a/prover/src/tests/zf_rpx_golden_tests.rs b/prover/src/tests/zf_rpx_golden_tests.rs index 5d2803023..8a1ea9bbd 100644 --- a/prover/src/tests/zf_rpx_golden_tests.rs +++ b/prover/src/tests/zf_rpx_golden_tests.rs @@ -15,6 +15,7 @@ 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, }; @@ -307,9 +308,28 @@ fn production_sites_prove_at_the_process_format() { ), ] { assert_eq!(o.format.fri_mode, want, "{site}"); - // 2^12 rows: LDE 2^14, so both terminals (T = 9, 10) leave committed layers. - let (air, proof) = prove_logup(1 << 12, &o); - assert!(verify_logup(&air, &proof), "{site}: must verify"); + // 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(); From eb6fe2b711fcbcfdb37664ecc65512d248cc2784 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 20:28:40 -0300 Subject: [PATCH 50/73] test(math-cuda): evict before every cap height in the WHIR eviction test `paths_and_cap_after_the_retained_layer_is_evicted` evicted the retained leaf layer once and then demanded a leaf pass at every cap height. The first rebuild re-captures the layer - by design: the evictor keeps the codeword's registry entry because the slot "may refill" (math-cuda src/whir.rs) - so the second height was served from it, and the box read "rpx evicted k=4 c=1: unexpected leaf-pass count 2 -> 2" (a delta of 0). The test now evicts before every height, expects exactly one leaf pass per height, asserts the layer is back before each eviction and after the last rebuild (pinning the re-capture), and counts one eviction per height. --- crypto/math-cuda/tests/whir_cap.rs | 59 ++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/crypto/math-cuda/tests/whir_cap.rs b/crypto/math-cuda/tests/whir_cap.rs index 9ad332424..70aaebbc6 100644 --- a/crypto/math-cuda/tests/whir_cap.rs +++ b/crypto/math-cuda/tests/whir_cap.rs @@ -56,19 +56,22 @@ fn nodes(bytes: &[u8]) -> Vec<[u8; 32]> { } /// The device result against the host tree at blocking `k`, every cap height -/// up to `min(depth, 6)`. +/// 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 @@ -144,14 +147,14 @@ fn paths_and_cap_are_the_host_trees_served_or_rehashed() { 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); + 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); + assert_matches_host(name, &device, &other, k_other, &positions, |_| {}, |d| d == 1); } } run::("keccak"); @@ -160,6 +163,13 @@ fn paths_and_cap_are_the_host_trees_served_or_rehashed() { /// 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(); @@ -169,20 +179,39 @@ fn paths_and_cap_after_the_retained_layer_is_evicted() { let layer_bytes = device.retained_leaf_bytes(); assert!(layer_bytes > 0, "precondition: the commit retained a layer"); - 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, "the layer was evicted"); - drop(got); - drop(hog); + 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, |d| d >= 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" + ); } From 737415ee7fdb7d01c38cb21ca3f8c102f88c8fca Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 20:31:02 -0300 Subject: [PATCH 51/73] style: rustfmt the I-FIX-B test edits --- crypto/math-cuda/tests/whir_cap.rs | 30 +++++++++++++++++--- crypto/stark/src/examples/bus_permutation.rs | 17 +++++++---- prover/src/tests/zf_air_cache_tests.rs | 5 +--- prover/tests/merkle_cap_vm.rs | 10 +++++-- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/crypto/math-cuda/tests/whir_cap.rs b/crypto/math-cuda/tests/whir_cap.rs index 70aaebbc6..330ffda26 100644 --- a/crypto/math-cuda/tests/whir_cap.rs +++ b/crypto/math-cuda/tests/whir_cap.rs @@ -147,14 +147,30 @@ fn paths_and_cap_are_the_host_trees_served_or_rehashed() { 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); + 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); + assert_matches_host( + name, + &device, + &other, + k_other, + &positions, + |_| {}, + |d| d == 1, + ); } } run::("keccak"); @@ -194,7 +210,11 @@ fn paths_and_cap_after_the_retained_layer_is_evicted() { 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"); + assert_eq!( + device.retained_leaf_bytes(), + 0, + "c={c}: the layer was evicted" + ); drop(got); drop(hog); evictions += 1; @@ -203,7 +223,9 @@ fn paths_and_cap_after_the_retained_layer_is_evicted() { 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_matches_host("rpx evicted", &device, &host, k, &positions, evict, |d| { + d == 1 + }); assert_eq!( evictions, host.depth().min(6) + 1, diff --git a/crypto/stark/src/examples/bus_permutation.rs b/crypto/stark/src/examples/bus_permutation.rs index 057e553e2..c4f54291f 100644 --- a/crypto/stark/src/examples/bus_permutation.rs +++ b/crypto/stark/src/examples/bus_permutation.rs @@ -31,7 +31,8 @@ type E = Degree3GoldilocksExtensionField; type FE = FieldElement; /// The AIR: no table constraints of its own, the LogUp ones from the framework. -pub type BusPermutationAir = AirWithBuses; +pub type BusPermutationAir = + AirWithBuses; const BUS_AB: u64 = 1; const BUS_CD: u64 = 2; @@ -58,7 +59,10 @@ pub fn bus_permutation_air(options: &ProofOptions) -> BusPermutationAir { /// 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"); + 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) @@ -103,10 +107,11 @@ mod tests { 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(&[])), - }; + 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/prover/src/tests/zf_air_cache_tests.rs b/prover/src/tests/zf_air_cache_tests.rs index b3e617252..cf3d8ba88 100644 --- a/prover/src/tests/zf_air_cache_tests.rs +++ b/prover/src/tests/zf_air_cache_tests.rs @@ -59,10 +59,7 @@ fn formats() -> Vec { /// 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() - }; + let options = |format: ProofFormat| ProofOptions { format, ..base() }; // HALT: non-default formats first, the default last. for _round in 0..2 { for format in formats() { diff --git a/prover/tests/merkle_cap_vm.rs b/prover/tests/merkle_cap_vm.rs index c201bd521..8a5c47293 100644 --- a/prover/tests/merkle_cap_vm.rs +++ b/prover/tests/merkle_cap_vm.rs @@ -18,10 +18,10 @@ use lambda_vm_prover::test_utils::asm_elf_bytes; use lambda_vm_prover::zf_format::ZfFormat; -use stark::merkle_caps::StarkCaps; 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 @@ -78,7 +78,13 @@ fn a_vm_proof_round_trips_under_the_process_cap_policy() { 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(); + 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, From 21b1b77914f56e076d869bac4582800195daee47 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 20:44:06 -0300 Subject: [PATCH 52/73] style: rustfmt the merged test module list (zf_air_cache_tests before zf_rpx_device_tests) --- prover/src/tests/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index 829937f79..5791b4904 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -130,10 +130,10 @@ pub mod whir_byte_gate; pub mod whir_hash_tests; #[cfg(test)] pub mod whir_identity_tests; -#[cfg(all(test, feature = "cuda"))] -pub mod zf_rpx_device_tests; #[cfg(test)] pub mod zf_air_cache_tests; +#[cfg(all(test, feature = "cuda"))] +pub mod zf_rpx_device_tests; #[cfg(test)] pub mod zf_rpx_golden_tests; #[cfg(test)] From 130ddb13532aeaa36eaf582ef188414ef48deac3 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 20:55:52 -0300 Subject: [PATCH 53/73] feat(prover): one-row openings (S2) in the in-guest STARK verifier (G3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LFM STARK verifier now verifies tables proved with one-row trace leaves and a committed FRI input (design/FRI.md §7.2-§7.4), at the table's resolved leaf layout (stark::leaf_layout::table_leaf_layout; `auto` mixes layouts across a proof's tables): - sub_proof: SubProofShape.layout; one-row leaves (`num_columns` cells, no symmetric row) hashed by emit_leaf_hash_rows and walked at depth log2(lde); the point x_r from log2(lde) index bits (emit_point_from_row_bits); DEEP evaluated once. QueryOutput and FriQuery carry the symmetric value/point as Option. - fri: FriShape stores the resolved layout in format.one_row (for_layout; from_options refuses an unresolved auto). Under one row the chain starts at the DEEP codeword (index bits log2(lde), every fold committed, num_zetas = num_committed, layer j folds with zeta_j) and layer 0's slot check is the input-slot check group0[slot] == DEEP(x_r); with zero folds terminal(x_r) == DEEP(x_r). - epoch: the transcript replay absorbs the input root right after gamma with no challenge ahead of it; query indexes use the FRI shape's bits. - epoch_verify: per-group value counts and the closed forms at the sub-proof's rows per leaf. - Test harnesses (host serializers, shape builders, the real-epoch and real-child harvests): per-table layouts, precomputed roots taken with precomputed_commitment_for(layout), the REGISTER derivation at that table's rows_per_leaf, the attestation's DECODE root as absorbed, and one-row roots attached to the LFM AIRs as verify_against_artifacts does. Plus a blowup-4 knob-on twin of the assembled verifier (one-row static roots exist at blowup 4 only) that prints per-leg layouts. The default format emits today's program instruction for instruction: every new branch is taken only under a one-row layout. --- prover/src/lfm/constraint_tests.rs | 5 +- prover/src/lfm/epoch.rs | 44 +++- prover/src/lfm/epoch_tests.rs | 160 ++++++++++-- prover/src/lfm/epoch_verify.rs | 30 ++- prover/src/lfm/epoch_verify_tests.rs | 106 +++++++- prover/src/lfm/fri.rs | 251 ++++++++++++++---- prover/src/lfm/fri_tests.rs | 52 +++- prover/src/lfm/join_tests.rs | 59 ++++- prover/src/lfm/per_table_aggregator_tests.rs | 17 +- prover/src/lfm/per_table_census_tests.rs | 10 +- prover/src/lfm/sub_proof.rs | 259 +++++++++++++++---- 11 files changed, 824 insertions(+), 169 deletions(-) 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..972658ccd 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 (FRI.md §10 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, design/FRI.md §7.3): 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 λ₁ (FRI.md §7.7 + // (ii)); 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 045807dd2..07d9f6607 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, design/FRI.md §7.2–§7.3): 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,12 +1302,17 @@ fn harvest_real_epoch( reg_shape: super::programs::RegisterDerivationShape { blowup: opts.blowup_factor as usize, coset_offset: opts.coset_offset, - rows_per_leaf: stark::commitment::ROWS_PER_LEAF, + // 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, FRI.md §7.5.2). expected_program_id: crate::recursion::program_id_from_digest( &crate::statement::elf_digest(&elf_bytes), elf.entry_point, - &decode_root, + &absorbed_decode_root, &[], ), tables, @@ -1532,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(), diff --git a/prover/src/lfm/epoch_verify.rs b/prover/src/lfm/epoch_verify.rs index 9f2bacec3..18565fe96 100644 --- a/prover/src/lfm/epoch_verify.rs +++ b/prover/src/lfm/epoch_verify.rs @@ -103,6 +103,11 @@ impl TableVerifyShape { self.fri.index_bits(), "the FRI layers consume suffixes of the trace walk's decomposition" ); + assert_eq!( + self.sub.layout, + self.fri.leaf_layout(), + "the trace trees and the FRI chain verify one table at one leaf layout" + ); assert_eq!( self.fri.num_queries, self.num_queries, "the query count is one shape, declared once" @@ -380,7 +385,7 @@ 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; @@ -419,8 +424,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, @@ -478,7 +483,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() } @@ -527,10 +532,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 @@ -560,7 +571,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() } @@ -645,11 +656,12 @@ 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(); // Per committed layer: a pair leaf (six felts), or a `2^d`-value group. let fri_leaves = shape.fri.leaf_permutations_per_query(hash); diff --git a/prover/src/lfm/epoch_verify_tests.rs b/prover/src/lfm/epoch_verify_tests.rs index cc04ca14c..009d8d0a1 100644 --- a/prover/src/lfm/epoch_verify_tests.rs +++ b/prover/src/lfm/epoch_verify_tests.rs @@ -175,7 +175,10 @@ pub(super) fn build_table_legs( "the next-row block covers every evaluation point past the first step" ); - let merkle_depth = log2_lde_length as usize - 1; + // 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, @@ -186,6 +189,7 @@ pub(super) fn build_table_legs( .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 { @@ -194,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 @@ -208,9 +212,12 @@ pub(super) fn build_table_legs( // ---- 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, false) - .expect("a format the host lays out"); + 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, @@ -332,10 +339,26 @@ pub(super) fn build_table_legs( 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, RULINGS 14 — 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. /// @@ -1542,10 +1565,34 @@ const PROCESS_FORMAT_QUERIES: usize = 24; #[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 format = crate::zf_format::ZfFormat::global(); let mut opts = super::proof_fixture::fixture_options(); opts.fri_number_of_queries = PROCESS_FORMAT_QUERIES; - let opts = format.options(opts); + 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`, RULINGS 14: 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); @@ -1593,9 +1640,10 @@ fn the_assembled_epoch_verifier_runs_at_the_process_format() { for (i, l) in e.legs.iter().enumerate() { let f = l.verify.fri; println!( - " leg {i:>2}: log2(lde) {:>2} trace cap {} FRI schedule {:?} depths {:?} \ - caps {:?} {} permutations", + " 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()) @@ -1629,6 +1677,44 @@ fn the_assembled_epoch_verifier_runs_at_the_process_format() { 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. diff --git a/prover/src/lfm/fri.rs b/prover/src/lfm/fri.rs index 194c28b83..41c0ee903 100644 --- a/prover/src/lfm/fri.rs +++ b/prover/src/lfm/fri.rs @@ -33,6 +33,7 @@ //! claim the same layout, unverified here and never run by the machine. use stark::fri::schedule::FriFormat; +use stark::leaf_layout::LeafLayout; use stark::proof::options::{FriMode, OneRowMode, ProofFormat, ProofOptions}; use crate::tables::types::FE; @@ -63,6 +64,11 @@ pub struct FriShape { /// The inner proof's FORMAT (design/CAP.md, design/FRI.md): 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, } @@ -75,28 +81,61 @@ impl FriShape { /// /// # Panics /// - /// On a one-row inner format (`LAMBDA_VM_ZF_ONE_ROW` ≠ 0, S2): the - /// in-guest verifier of one-row openings and the committed FRI input is - /// lane I-FRI-G's G3 and does not exist yet, so an emitter built for the - /// row-pair layout must never be handed one — it would emit a verifier of - /// the wrong protocol. Emit time, not a proof outcome. + /// 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 { - assert!( - options.format.one_row == stark::proof::options::OneRowMode::Off, - "the in-guest STARK verifier does not implement one-row openings (S2, lane G3); \ - inner format one_row = {}", - options.format.one_row - ); + 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: options.format, + 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 { @@ -109,13 +148,14 @@ impl FriShape { } /// Whether the proof uses today's FRI encoding: pair layers, one sibling - /// value per committed layer (`fri = pair`). Decided by the FORMAT, never - /// by the schedule's values: a `dp` schedule of all ones still uses the - /// group encoding (`FriFormat::is_legacy`). Every non-legacy path below is - /// the S3 group path; the legacy emission is today's, instruction for - /// instruction. + /// 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.format.fri_mode == FriMode::Pair && !self.one_row() } /// The host's own FRI format for this shape (the fold-schedule DP's @@ -124,7 +164,7 @@ impl FriShape { fn fri_format(self) -> FriFormat { FriFormat { mode: self.format.fri_mode, - one_row: false, + one_row: self.one_row(), num_queries: self.num_queries as u64, cap: self.format.merkle_cap, schedule_override: self.format.fri_schedule_override, @@ -151,11 +191,31 @@ impl FriShape { /// **`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. + /// 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.schedule().len() } + /// Folding challenges the proof draws (FRI.md §7.3, `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 { @@ -280,8 +340,10 @@ impl FriShape { + 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, + /// FRI.md §7.2) — 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 @@ -293,7 +355,7 @@ impl FriShape { /// 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 its @@ -319,10 +381,8 @@ impl FriShape { /// Invariants a caller cannot assemble their way out of. pub fn check(self) { assert!( - self.format.one_row == OneRowMode::Off, - "the in-guest FRI verifier implements row-pair openings only (one-row \ - openings, S2, are a later in-guest unit): {:?}", - self.format.one_row + 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). @@ -334,11 +394,16 @@ impl FriShape { .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, - self.total_folds().saturating_sub(1), - "the schedule {schedule:?} must cover the committed folds (fold 0 is binary \ - and uncommitted)" + covered, committed_folds, + "the schedule {schedule:?} must cover the committed folds" ); assert!( self.blowup_log >= 1, @@ -544,14 +609,18 @@ 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): per committed layer `j`, the challenges - /// its `d_j` binary folds use — `ζ_{j+1}, ζ_{j+1}², …, ζ_{j+1}^{2^{d_j−1}}` - /// (FRI.md §1.2) — squared ONCE per sub-proof, not per query. Empty under - /// `pair`, where each layer folds once with `ζ_{j+1}` itself. + /// 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`]) (FRI.md §1.2) — 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>, } @@ -571,7 +640,7 @@ impl FriCommitments { } else { (0..shape.num_committed()) .map(|j| { - let mut z = zetas[j + 1]; + 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); @@ -616,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. @@ -655,7 +734,7 @@ 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); @@ -819,7 +898,8 @@ 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"); @@ -836,21 +916,33 @@ pub fn emit_query_fri( "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 @@ -860,9 +952,39 @@ pub fn emit_query_fri( let one = b.felt_const(FE::one()); let inv = b.div(one, q.point); + if shape.one_row() { + // ★ S2 (design/FRI.md §7.3-§7.4): 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 @@ -913,9 +1035,21 @@ pub fn emit_query_fri( } } - // `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; + 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); } @@ -1138,6 +1272,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); @@ -1152,8 +1291,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_tests.rs b/prover/src/lfm/fri_tests.rs index 6a1193543..e405825ba 100644 --- a/prover/src/lfm/fri_tests.rs +++ b/prover/src/lfm/fri_tests.rs @@ -167,7 +167,7 @@ pub(super) 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(); // Per layer the opened values (the sibling, or the whole group) and the @@ -363,19 +363,32 @@ fn the_fri_leaf_is_byte_identical_to_productions_own_backends() { /// [`FriArenas`]. 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, @@ -397,14 +410,25 @@ pub(super) fn fri_only_program(shape: FriShape, num_queries: usize) -> LfmProgra 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. + /// 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 } diff --git a/prover/src/lfm/join_tests.rs b/prover/src/lfm/join_tests.rs index 3942d305d..0c28dbae5 100644 --- a/prover/src/lfm/join_tests.rs +++ b/prover/src/lfm/join_tests.rs @@ -82,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 @@ -90,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 { @@ -137,7 +143,10 @@ pub(super) fn build_host_sub_proof( let blowup = air.options().blowup_factor as usize; let lde_length = view.trace_length() * blowup; - let merkle_depth = lde_length.trailing_zeros() as usize - 1; + // 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 @@ -150,6 +159,7 @@ pub(super) fn build_host_sub_proof( 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 @@ -202,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() { @@ -253,6 +264,31 @@ pub(super) fn build_host_sub_proof( }); 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] = &[]; @@ -305,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) @@ -324,6 +360,7 @@ pub(super) fn build_host_sub_proof( iotas: sp.challenges.iotas.clone(), zetas: sp.challenges.zetas.clone(), expected, + expected_at_r, expected_base_swapped, points, } @@ -441,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)" ); @@ -601,6 +639,7 @@ fn shape_for( log2_lde_length: log2_trace_length + log2_blowup, coset_offset: FE::from(3u64), trace_cap: 0, + layout: stark::leaf_layout::LeafLayout::RowPair, } } @@ -1349,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"), @@ -1357,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", diff --git a/prover/src/lfm/per_table_aggregator_tests.rs b/prover/src/lfm/per_table_aggregator_tests.rs index 084afd679..6977f2dc7 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()); } @@ -1086,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, @@ -1094,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"); @@ -1115,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()); } diff --git a/prover/src/lfm/per_table_census_tests.rs b/prover/src/lfm/per_table_census_tests.rs index b92101ae1..7a422284f 100644 --- a/prover/src/lfm/per_table_census_tests.rs +++ b/prover/src/lfm/per_table_census_tests.rs @@ -402,19 +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, log2_lde_length as usize - 1), + .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, diff --git a/prover/src/lfm/sub_proof.rs b/prover/src/lfm/sub_proof.rs index 3c1740d29..791c56564 100644 --- a/prover/src/lfm/sub_proof.rs +++ b/prover/src/lfm/sub_proof.rs @@ -50,19 +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, design/FRI.md §7) +//! +//! 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. @@ -80,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) + } + + /// 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, design/FRI.md §7.4). + pub fn values_at(&self, rows_per_leaf: usize) -> usize { + rows_per_leaf * self.num_columns } - /// Bytes the leaf hash covers. + /// 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 } } } @@ -105,9 +132,10 @@ 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, @@ -121,9 +149,26 @@ pub struct SubProofShape { /// 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, design/FRI.md §7): 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 { @@ -162,7 +207,7 @@ 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 values: usize = self.groups().iter().map(|g| self.group_values(g)).sum(); let siblings = digest_words * self.path_len() * self.groups().len(); values + siblings } @@ -203,12 +248,14 @@ impl SubProofShape { 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 @@ -351,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(); @@ -387,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) } @@ -402,13 +463,25 @@ 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() + commitment.cap_height(), bits.len(), "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 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); @@ -419,14 +492,42 @@ pub fn emit_group_authentication( } } -/// 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 @@ -450,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) } @@ -478,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. /// @@ -512,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`]. @@ -576,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); @@ -614,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, + point_sym: Some(point_sym), + } +} + +/// The one-row half of [`emit_query_from_bits`] (S2, design/FRI.md §7.4), 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: None, } } @@ -653,16 +813,27 @@ pub struct SubProofArenas { /// 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 @@ -737,7 +908,7 @@ 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; From a08a0bcaccbcbea87b77242e20155e5e4bfb8123 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 20:56:00 -0300 Subject: [PATCH 54/73] test(prover): S2 in the in-guest verifier against the host's (e) vectors and real proofs lfm::one_row_guest_tests: - the in-guest one-row shape (index bits, schedule, layer depths, caps, challenge count) equals the host's StarkCaps at one_row = true over 840 shapes; an unresolved `auto` is refused; - the emitted FRI verifier executes both RPX (e) proofs (one_row_pair, one_row_3_2_1_2) with emitted permutations == the closed form; - every value of the input tree's opening, DEEP(x_r), the input root, zeta_0 and a terminal coefficient is bound (tamper -> no execution); - the input-slot check is load-bearing: a moved DEEP(x_r) executes only when the slot check is skipped; - the in-guest trace leaf equals the (e) leaf digests at rows_per_leaf 1 and 2 (48 leaves, rpx); - both legs as one program on real L2G_MEMORY proofs at one_row {1, auto} x cap {off, auto} x fri {pair, dp}, an uneven one-row schedule [3,1,3,2], and a one-row and a row-pair table verified in one program (mixed layouts), each at the closed form. --- prover/src/lfm/mod.rs | 2 + prover/src/lfm/one_row_guest_tests.rs | 711 ++++++++++++++++++++++++++ 2 files changed, 713 insertions(+) create mode 100644 prover/src/lfm/one_row_guest_tests.rs diff --git a/prover/src/lfm/mod.rs b/prover/src/lfm/mod.rs index e3011c7aa..afcb9f5e6 100644 --- a/prover/src/lfm/mod.rs +++ b/prover/src/lfm/mod.rs @@ -164,6 +164,8 @@ 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; 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..ac036401b --- /dev/null +++ b/prover/src/lfm/one_row_guest_tests.rs @@ -0,0 +1,711 @@ +//! S2 in the in-guest (LFM) STARK verifier (G3, design/FRI.md §7.2–§7.4, +//! §11): 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 I-S2-H'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 I-S2-H'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, +/// FRI.md §7.7). 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 I-S2-H's (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 +/// (REVIEW-FRI F6 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); +} From e72d3e64d9ffcb8e06d3dc80ed225f40d0b6c912 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 21:01:55 -0300 Subject: [PATCH 55/73] test(prover): the leaf node prints how many of each wrap's sub-proofs it verifies at one row A parseable ZFS2NODE line per child (the process format's banner, the sub-proof count and the one-row leg count) so the box run of the leaf node under LAMBDA_VM_ZF_ONE_ROW shows that the node verified one-row LFM proofs rather than only that it executed. --- prover/src/lfm/per_table_aggregator_tests.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/prover/src/lfm/per_table_aggregator_tests.rs b/prover/src/lfm/per_table_aggregator_tests.rs index 6977f2dc7..0c9c18e5e 100644 --- a/prover/src/lfm/per_table_aggregator_tests.rs +++ b/prover/src/lfm/per_table_aggregator_tests.rs @@ -1456,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", From 7f4e3d924cd65127b49065407a0891b6f4a58a40 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 21:02:55 -0300 Subject: [PATCH 56/73] feat(math-cuda): one-row (rows_per_leaf = 1) device leaves for S2 trees S2 commits every trace tree with one bit-reversed row per leaf. The row-pair row-major kernels read rows brev(2i), brev(2i+1) and cannot be reused at another width (I-FRI-D note 1), so each hash gets its own one-row kernel ({keccak256,blake3,rpx}_leaves_base_row_major_row_range: row brev(i), a column range). - lde.rs: launch_row_major_leaves dispatches by rows_per_leaf (2 launches exactly the kernels it did before); coset_lde_row_major_inner and the split trees take rows_per_leaf; *_rpl public variants, the old names stay as the row-pair wrappers. row_major_leaves is a host-matrix parity harness. - merkle.rs / blake3.rs / rpx.rs: composition trees take rows_per_leaf; one row uses the existing per-row ext3 kernels (same arguments). - host KATs: the blake3 and rpx one-row kernels replayed thread by thread against the CPU one-row leaf spec, every column range, plus a control that a one-row leaf is not the row-pair leaf. The default (rows_per_leaf = 2) launches the same kernels with the same arguments. --- crypto/math-cuda/kernels/blake3.cu | 26 ++ crypto/math-cuda/kernels/keccak.cu | 40 ++ crypto/math-cuda/kernels/rpx.cu | 27 ++ crypto/math-cuda/src/blake3.rs | 36 +- crypto/math-cuda/src/device.rs | 12 + crypto/math-cuda/src/lde.rs | 418 ++++++++++++++---- crypto/math-cuda/src/merkle.rs | 55 ++- crypto/math-cuda/src/rpx.rs | 36 +- .../tests/host_kat/blake3_host_kat.cpp | 50 +++ .../math-cuda/tests/host_kat/rpx_host_kat.cpp | 30 ++ 10 files changed, 638 insertions(+), 92 deletions(-) diff --git a/crypto/math-cuda/kernels/blake3.cu b/crypto/math-cuda/kernels/blake3.cu index efdb476ad..680a42afe 100644 --- a/crypto/math-cuda/kernels/blake3.cu +++ b/crypto/math-cuda/kernels/blake3.cu @@ -560,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 35666cc06..c372348cf 100644 --- a/crypto/math-cuda/kernels/keccak.cu +++ b/crypto/math-cuda/kernels/keccak.cu @@ -683,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 +// (I-FRI-D note 1), 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 b2e92b533..6c3bbff14 100644 --- a/crypto/math-cuda/kernels/rpx.cu +++ b/crypto/math-cuda/kernels/rpx.cu @@ -769,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 38776ac5f..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, @@ -229,6 +231,8 @@ 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, @@ -253,6 +257,8 @@ 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, @@ -876,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")?, @@ -893,6 +901,8 @@ 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")?, @@ -914,6 +924,8 @@ 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")?, 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 a7161a152..2326e587c 100644 --- a/crypto/math-cuda/src/merkle.rs +++ b/crypto/math-cuda/src/merkle.rs @@ -488,7 +488,12 @@ pub fn read_cap_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; @@ -502,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()?; @@ -536,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) @@ -569,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()?; @@ -588,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) @@ -623,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/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"); From 1564fda1239ba79656d082056719a2574166b31e Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 21:03:04 -0300 Subject: [PATCH 57/73] =?UTF-8?q?feat(stark):=20S2=20on=20the=20device=20(?= =?UTF-8?q?D2)=20=E2=80=94=20one-row=20trees,=20openings=20and=20the=20inp?= =?UTF-8?q?ut=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lifts I-S2-H's CPU-only gating for one-row tables: every device arm now follows the table's leaf layout (table_leaf_layout, per table under auto, so one proof may mix device row-pair and device one-row tables). - gpu_lde: the fused main commit, the preprocessed split, the aux commits (host input and resident) and both composition-tree entries take rows_per_leaf; LFM artifact commit via try_commit_row_major_with. Counters gpu_one_row_trees / gpu_one_row_tree_peak_bytes / gpu_one_row_fri_calls (tests assert on them so a host fallback fails). - FRI: the group drive commits layer 0 (the input tree) from the codeword with zero folds and NO challenge before it under one row (the CPU loop's pending = 0); the one_row declines in fri_commit_gpu_drive, fri/mod.rs and the DEEP->FRI arm are gone, so the tree is built off the resident DEEP codeword. - prover: device openings at row r (device_query_rows / device_rows by layout, the host cross-checks by layout); one-row tables may be device-only and keep the resident aux build. - device_set: tree_bytes_for(lde, rows_per_leaf) — a one-row tree is (2*lde-1)*32 bytes; commit/table sets and the VRAM gate estimates take the layout; FRI admission uses the one-row bound under one row. - lfm/commit.rs: REVIEW-FRI F8.1 lifted (device one-row artifact roots) with a cuda parity test against the host one-row root. Default format: rows_per_leaf = 2 everywhere, the same kernels and the same admission numbers (device_set test pins table_device_set_rpl(_, 2) == table_device_set). --- crypto/stark/src/device_set.rs | 91 +++++++++++- crypto/stark/src/fri/mod.rs | 43 +++--- crypto/stark/src/gpu_lde.rs | 212 ++++++++++++++++++++------ crypto/stark/src/proof/options.rs | 16 +- crypto/stark/src/prover.rs | 238 ++++++++++++++++-------------- prover/src/lfm/commit.rs | 56 ++++++- 6 files changed, 466 insertions(+), 190 deletions(-) diff --git a/crypto/stark/src/device_set.rs b/crypto/stark/src/device_set.rs index 81771fbdd..020a4538b 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 (lane I-S2-D): 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, FRI.md §7.6) — 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/fri/mod.rs b/crypto/stark/src/fri/mod.rs index 0d360857b..67e94c0bf 100644 --- a/crypto/stark/src/fri/mod.rs +++ b/crypto/stark/src/fri/mod.rs @@ -104,9 +104,10 @@ where /// `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 are not -/// implemented on the device and always take the CPU loop -/// ([`commit_phase_cpu_with_layout`]). +/// 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, @@ -134,26 +135,24 @@ 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")] - if !layout.one_row { - // 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, - layout, - 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, diff --git a/crypto/stark/src/gpu_lde.rs b/crypto/stark/src/gpu_lde.rs index 0171e6fc7..dc6679897 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 @@ -540,6 +541,9 @@ pub fn reset_all_gpu_call_counters() { 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(); } @@ -1322,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, @@ -1345,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) } @@ -1360,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, @@ -1391,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) }; @@ -1400,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::(), @@ -1413,6 +1439,7 @@ where blowup_factor, &weights_u64, retain_host_lde, + rows_per_leaf, ) { Ok(v) => v, Err(e) => { @@ -1488,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, @@ -1519,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) }; @@ -1528,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::(), @@ -1541,6 +1570,7 @@ where split_col, build_precomputed, want_host, + rows_per_leaf, ) { Ok(v) => v, Err(e) => { @@ -1583,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], @@ -1591,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, @@ -1620,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) }; @@ -1629,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, @@ -1641,6 +1674,7 @@ where blowup_factor, &weights_u64, retain_host_lde, + rows_per_leaf, ) { Ok(v) => v, Err(e) => { @@ -1728,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 // ============================================================================ @@ -1849,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, @@ -1865,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; } @@ -1891,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)", @@ -1907,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)) } @@ -1918,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, @@ -1930,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; @@ -1941,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)", @@ -1966,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)) } @@ -2922,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, @@ -2946,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, @@ -2962,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 @@ -3777,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; } @@ -3855,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; } @@ -3898,8 +4003,9 @@ where /// /// 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 are -/// not implemented on the device and return `None` before any sampling. +/// 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, @@ -3937,8 +4043,7 @@ where // 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.one_row - || layout.terminal_len == 0 + if layout.terminal_len == 0 || n0 .trailing_zeros() .checked_sub(layout.terminal_len.trailing_zeros()) @@ -3954,6 +4059,9 @@ 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, @@ -4075,6 +4183,12 @@ fn zeta_powers_raw(zeta: &FieldElement, n: u32) -> Vec<[u64; 3]> /// `ζ, ζ², …` (`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, I-FRI-D's group kernels), its root appended with NO challenge +/// sampled before it (FRI.md §7.3, 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)] @@ -4096,12 +4210,17 @@ where { 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`. - let mut pending: u32 = 1; + // 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 { - // <<<< Receive challenge zeta_j - let zeta: FieldElement = transcript.sample_field_element(); - let powers = zeta_powers_raw(&zeta, pending); + 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, @@ -4148,6 +4267,9 @@ where } 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)) } @@ -4423,6 +4545,7 @@ mod admission_box_tests { blowup, &weights, true, + 2, ); panic!( "the over-budget commit returned {} instead of aborting", @@ -4490,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/proof/options.rs b/crypto/stark/src/proof/options.rs index e6e1ca1b9..59b2e6234 100644 --- a/crypto/stark/src/proof/options.rs +++ b/crypto/stark/src/proof/options.rs @@ -265,7 +265,8 @@ pub const MERKLE_CAP_IMPLEMENTED: bool = true; /// refuses a non-default format). pub const FRI_MODE_IMPLEMENTED: bool = true; -/// `OneRowMode::{On, Auto}` (S2) is implemented on the HOST CPU paths only: +/// `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 @@ -277,12 +278,15 @@ pub const FRI_MODE_IMPLEMENTED: bool = true; /// 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 (RULINGS 14) /// — e.g. `one_row = 1` at blowup 2, 8 or 16 fails on BITWISE; -/// - on a `cuda` build a one-row table takes the CPU arm of every commit and -/// opening (never device-only, host aux build) — correct, not fast. +/// - the device (lane I-S2-D, D2): 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: device one-row trees, openings and the device input tree -/// (lane I-FRI-D, D2 — a one-row table on a cuda build runs on the host), the -/// in-guest (LFM) verifier of a one-row proof (lane I-FRI-G, G3: an emitter +/// NOT implemented: the in-guest (LFM) verifier of a one-row proof (lane I-FRI-G, G3: an emitter /// asked for one refuses at emit time, `lfm::fri::FriShape::from_options`), /// and the RV64 recursion guest (default-only, RULINGS 11). A block run under /// `LAMBDA_VM_ZF_ONE_ROW` therefore proves and host-verifies its STARK and diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 8dcdd9cbf..3c8fa6002 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -1615,9 +1615,10 @@ pub trait IsStarkProver< /// 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. The device arms build - /// row-pair leaves only, so a one-row table (S2) always takes the CPU arm - /// (device one-row trees are lane I-FRI-D's D2). + /// `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, @@ -1644,7 +1645,7 @@ pub trait IsStarkProver< // 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() && !layout.is_one_row() { + if precomputed.is_none() && !residency.recomputes_main_lde() { let (trace_slice, num_cols) = trace.main_data_row_major(); let n = if num_cols > 0 { trace_slice.len() / num_cols @@ -1668,6 +1669,7 @@ pub trait IsStarkProver< domain.blowup_factor, &twiddles.coset_weights, !device_only, + rows_per_leaf, ) { #[cfg(feature = "instruments")] @@ -1702,7 +1704,6 @@ pub trait IsStarkProver< #[cfg(feature = "cuda")] if let Some((expected_precomputed_root, num_precomputed)) = precomputed && !residency.recomputes_main_lde() - && !layout.is_one_row() { let (trace_slice, num_cols) = trace.main_data_row_major(); let n = if num_cols > 0 { @@ -1740,6 +1741,7 @@ pub trait IsStarkProver< num_precomputed, cached_pre.is_none(), !device_only, + rows_per_leaf, ) { #[cfg(feature = "instruments")] @@ -2614,8 +2616,8 @@ 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 device composition trees are - // row-pair only, so a one-row table commits on the host. + // 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 @@ -2629,22 +2631,20 @@ pub trait IsStarkProver< match round_1_result .lde_trace .gpu_composition_parts() - .filter(|_| !leaf_layout.is_one_row()) .and_then(|h| { crate::gpu_lde::try_build_comp_poly_tree_gpu_from_dev::< FieldExtension, H::Batched, - >(h) + >(h, leaf_layout.rows_per_leaf()) }) .or_else(|| { - (!leaf_layout.is_one_row()) - .then(|| { - crate::gpu_lde::try_build_comp_poly_tree_gpu::< - FieldExtension, - H::Batched, - >(&lde_composition_poly_parts_evaluations) - }) - .flatten() + crate::gpu_lde::try_build_comp_poly_tree_gpu::< + FieldExtension, + H::Batched, + >( + &lde_composition_poly_parts_evaluations, + leaf_layout.rows_per_leaf(), + ) }) { Some((host_tree, dev_tree)) => { let root = host_tree.root; @@ -2915,13 +2915,11 @@ 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; a one-row - // layout (not implemented on the device) takes the host arm below - // (which may still compute DEEP on device). + // 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 = if fri_layout.one_row { - None - } else { + let precomputed_fri = { Self::try_compute_deep_dev( &round_1_result.lde_trace, composition_parts, @@ -3656,23 +3654,24 @@ 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, { - // Device composition trees exist for row-pair tables only. Self::composition_opening_from_proof( proof, lde_composition_poly_evaluations, index, - LeafLayout::RowPair, + leaf_layout, ) } @@ -3709,14 +3708,16 @@ pub trait IsStarkProver< /// 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 @@ -3724,9 +3725,7 @@ pub trait IsStarkProver< FieldElement: AsBytes + Sync + Send, G: Fn(usize) -> Vec>, { - // Device trees exist for row-pair tables only. - let (row, sym) = - LeafLayout::RowPair.query_rows(challenge, domain.lde_roots_of_unity_coset.len()); + let (row, sym) = leaf_layout.query_rows(challenge, domain.lde_roots_of_unity_coset.len()); PolynomialOpenings { proof, evaluations: gather(row), @@ -3751,17 +3750,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_row_pair( + 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_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 @@ -3831,12 +3850,6 @@ pub trait IsStarkProver< FieldElement: AsBytes + Sync + Send, G: Fn(usize) -> Vec>, { - // Device trees and gathers are row-pair only: a one-row table never - // has them (its commits took the CPU arms), so this is the host walk. - assert!( - !leaf_layout.is_one_row() || dev_proofs.is_none(), - "R4 {what} opening: a one-row table has a device-resident tree" - ); let Some(proofs) = dev_proofs else { assert!( !lde_trace.host_trace_empty(), @@ -3861,10 +3874,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 @@ -3872,9 +3891,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, r_odd) = LeafLayout::RowPair.query_rows(challenge, domain_size as usize); - let r_odd = r_odd.expect("a row pair has a symmetric row"); + 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), @@ -3882,7 +3900,7 @@ pub trait IsStarkProver< ); assert_eq!( odd, - gather(r_odd), + r_odd.map(&gather).unwrap_or_default(), "device {what}-row gather mismatch (odd), query {qi}" ); } @@ -3911,25 +3929,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| { - let (row, sym) = LeafLayout::RowPair.query_rows(c, domain_size as usize); - [row as u32, sym.unwrap_or(row) as u32] - }) - .collect(); - // Every device arm below is row-pair only: a one-row table (S2) has no - // device-resident tree (its commits took the CPU arms) and opens on - // the host. Filtering here keeps it that way even if one appeared. - #[cfg(feature = "cuda")] - let device_ok = !leaf_layout.is_one_row(); + 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 @@ -3945,13 +3955,12 @@ pub trait IsStarkProver< #[cfg(feature = "cuda")] let main_dev_proofs: Option>> = lde_trace .gpu_main() - .filter(|_| device_ok) .and_then(|h| h.tree.as_ref()) .map(|tree| { 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") }); @@ -3961,25 +3970,22 @@ pub trait IsStarkProver< let aux_dev_proofs: Option>> = round_1_result .aux .as_ref() - .filter(|_| device_ok) .and_then(|_aux| lde_trace.gpu_aux().and_then(|h| h.tree.as_ref())) .map(|tree| { 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() - .filter(|_| device_ok) - .map(|tree| { + let comp_dev_proofs: Option>> = + round_2_result.gpu_composition_tree.as_ref().map(|tree| { let stream = lde_trace .bound_stream() .expect("bound stream for device-resident composition-tree opening"); @@ -4138,18 +4144,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, r_odd) = - LeafLayout::RowPair.query_rows(*index, domain_size as usize); - let r_odd = r_odd.expect("a row pair has a symmetric row"); + leaf_layout.query_rows(*index, domain_size as usize); assert_eq!( even, lde_trace.gather_main_row_range( @@ -4161,7 +4169,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}" ); } @@ -4195,7 +4209,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). @@ -4211,6 +4226,7 @@ pub trait IsStarkProver< proofs[qi].clone(), composition_parts, *index, + leaf_layout, ); assert_eq!( even, expected.evaluations, @@ -4240,6 +4256,7 @@ pub trait IsStarkProver< proofs[qi].clone(), composition_parts, *index, + leaf_layout, ) } _ => Self::open_composition_poly( @@ -4435,7 +4452,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 @@ -4537,10 +4564,10 @@ pub trait IsStarkProver< // Stage-3 device-only gate: when it holds, `commit_main_trace` // keeps the R1 LDE device-resident and skips the host D2H. A - // one-row table never goes device-only: its trees are host - // trees (the device arms build row pairs only). + // 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) && !layout.is_one_row(); + let device_only = Self::device_only_for(*air, domain); Self::commit_main_trace( air.name(), @@ -4626,16 +4653,6 @@ pub trait IsStarkProver< } } - // One-row tables (S2) commit every tree on the host (the device arms - // build row-pair leaves only), so their aux build stays host-side too: - // a resident aux would leave no host aux trace for the CPU commit. - #[cfg(feature = "cuda")] - for ((_, trace, _), layout) in air_trace_pairs.iter_mut().zip(&leaf_layouts) { - if layout.is_one_row() { - trace.set_resident_aux_ok(false); - } - } - // `RecomputeLde` already forced the main commit onto the host path; // keeping the aux build there too makes the mode wholly host-side, which // is what its aux release at the end of each fused task acts on. @@ -4690,7 +4707,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 @@ -4796,8 +4819,7 @@ pub trait IsStarkProver< 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() - && !layout.is_one_row(); + && gpu_main_cells[idx].lock().unwrap().is_some(); // Resident GPU path: aux columns already on device (from // the resident LogUp aux build) — LDE straight from device @@ -4807,7 +4829,7 @@ pub trait IsStarkProver< // a clean error (falling through as-is would commit a // zero aux trace). #[cfg(feature = "cuda")] - if trace.aux_resident().is_some() && !layout.is_one_row() { + if trace.aux_resident().is_some() { #[cfg(feature = "instruments")] let t_sub = Instant::now(); let num_cols = trace.aux_resident().map_or(0, |ra| ra.num_aux_cols); @@ -4822,6 +4844,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")); @@ -4869,10 +4892,10 @@ pub trait IsStarkProver< } // Fused GPU path (cuda only): row-major ext3 NTT — single - // H2D, no column extraction, no CPU transpose. Row-pair - // leaves only, so never for a one-row table. + // H2D, no column extraction, no CPU transpose. The tree + // follows the table's leaf layout. #[cfg(feature = "cuda")] - if !layout.is_one_row() { + { let (trace_slice, num_cols) = trace.aux_data_row_major(); let n = if num_cols > 0 { trace_slice.len() / num_cols @@ -4894,6 +4917,7 @@ pub trait IsStarkProver< domain.blowup_factor, &twiddles.coset_weights, !device_only, + layout.rows_per_leaf(), ) { #[cfg(feature = "instruments")] diff --git a/prover/src/lfm/commit.rs b/prover/src/lfm/commit.rs index 364033748..751ca958f 100644 --- a/prover/src/lfm/commit.rs +++ b/prover/src/lfm/commit.rs @@ -218,9 +218,9 @@ pub fn commit_group_device_or_host( } /// [`commit_group_device_or_host`] under an explicit leaf layout. The device -/// commit builds row-pair leaves only (`gpu_lde::try_commit_row_major`), so a -/// one-row root (S2) is always the host pass (REVIEW-FRI F8.1: gated, until -/// the device lane makes it layout-aware). +/// 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 (REVIEW-FRI F8.1). pub fn commit_group_device_or_host_with( label: &str, group: &ColumnGroup, @@ -228,12 +228,13 @@ pub fn commit_group_device_or_host_with( layout: LeafLayout, ) -> Commitment { #[cfg(feature = "cuda")] - if device_artifacts() && group.padded_rows > 0 && group.width > 0 && !layout.is_one_row() { - let set = stark::device_set::commit_device_set( + if device_artifacts() && group.padded_rows > 0 && group.width > 0 { + 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 @@ -249,7 +250,7 @@ pub fn commit_group_device_or_host_with( // 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, >( @@ -259,6 +260,7 @@ pub fn commit_group_device_or_host_with( 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); @@ -344,6 +346,48 @@ mod device_parity { } } + /// S2 (REVIEW-FRI F8.1): 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. From 381a8d35e4399f58d4439ae10087febafe8f460f Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 21:03:13 -0300 Subject: [PATCH 58/73] =?UTF-8?q?test(stark,prover):=20S2=20device=20parit?= =?UTF-8?q?y=20(D2)=20=E2=80=94=20trees,=20openings,=20input=20tree,=20(e)?= =?UTF-8?q?=20vectors,=20VM=20bytes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stark::s2_device_parity (cuda, test/test-utils): one-row and row-pair device trees against the host over the same evaluations — fused main, preprocessed split, aux (host input and resident), composition (host parts and resident slabs); roots, leaf counts, paths gathered off the resident trees, and the device row gathers at the query rows; the (e) leaf-digest KAT; 13 cases per hash, one S2DEV line each. - fri::device_parity: fri_parity runs one-row layouts (layer 0 = input tree, queries over the whole LDE); one_row_cases (40) and one_row_resident_cases (5), pinned by count. - tests::zf_s2_device_tests (Keccak, Blake3) and zf_rpx_device_tests (RPX): trees, FRI and resident FRI parity, and the (e) vector proofs proved on the device equal the checked-in CPU bytes (one one-row device FRI commit per proof, >= 3 one-row device trees per proof). - zf_vm_one_row_tests::one_row_vm_proof_bytes_for_the_device_comparison (ignored, box): writes the one-row VM proof bytes (grinding 0) from a CPU build and a cuda build for a byte compare; the cuda run asserts the device one-row paths fired and prints the ZF S2 DEVMEM line. --- crypto/stark/src/fri/device_parity.rs | 87 ++- crypto/stark/src/fri/vectors.rs | 2 +- crypto/stark/src/lib.rs | 2 + crypto/stark/src/s2_device_parity.rs | 632 +++++++++++++++++++ crypto/stark/src/tests/mod.rs | 2 + crypto/stark/src/tests/zf_s2_device_tests.rs | 126 ++++ prover/src/tests/zf_rpx_device_tests.rs | 76 +++ prover/src/tests/zf_vm_one_row_tests.rs | 96 +++ 8 files changed, 1015 insertions(+), 8 deletions(-) create mode 100644 crypto/stark/src/s2_device_parity.rs create mode 100644 crypto/stark/src/tests/zf_s2_device_tests.rs diff --git a/crypto/stark/src/fri/device_parity.rs b/crypto/stark/src/fri/device_parity.rs index fed4cf79c..9308ae8b6 100644 --- a/crypto/stark/src/fri/device_parity.rs +++ b/crypto/stark/src/fri/device_parity.rs @@ -35,7 +35,7 @@ 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, ProofFormat, ProofOptions}; +use crate::proof::options::{FriMode, FriScheduleOverride, OneRowMode, ProofFormat, ProofOptions}; type F = GoldilocksField; type E = Degree3GoldilocksExtensionField; @@ -144,6 +144,11 @@ fn raw(v: &[Ext]) -> Vec<[u64; 3]> { /// `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 (lane I-S2-D): 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( @@ -154,7 +159,8 @@ pub fn fri_parity( ) -> Result { let blowup_log = options.blowup_factor.trailing_zeros(); let k = u32::from(options.fri_final_poly_log_degree); - let layout = FriFoldLayout::for_options(lde_log, blowup_log, options, false) + 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; @@ -202,7 +208,7 @@ pub fn fri_parity( })?; let what = format!( - "LDE 2^{lde_log}, schedule {:?}, legacy {}, resident {resident}", + "LDE 2^{lde_log}, schedule {:?}, legacy {}, one_row {one_row}, resident {resident}", layout.schedule, layout.is_legacy() ); @@ -237,13 +243,14 @@ pub fn fri_parity( return Err(format!("{what}: the transcripts diverged")); } - // Queries: random pair indices and both ends of the range. - let half = n / 2; + // 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) % half as u64) as usize) + .map(|_| (splitmix64(&mut rng) % bound as u64) as usize) .collect(); iotas.push(0); - iotas.push(half - 1); + 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() { @@ -343,6 +350,72 @@ pub fn legacy_cases() -> Vec { 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( diff --git a/crypto/stark/src/fri/vectors.rs b/crypto/stark/src/fri/vectors.rs index 8534fe7fc..f5a731313 100644 --- a/crypto/stark/src/fri/vectors.rs +++ b/crypto/stark/src/fri/vectors.rs @@ -83,7 +83,7 @@ pub fn splitmix64(state: &mut u64) -> u64 { } /// An ext3 element from three SplitMix64 outputs, each reduced mod p. -fn next_ext(state: &mut u64) -> Ext { +pub(crate) fn next_ext(state: &mut u64) -> Ext { Ext::new([ Felt::from(splitmix64(state)), Felt::from(splitmix64(state)), diff --git a/crypto/stark/src/lib.rs b/crypto/stark/src/lib.rs index 2efb21306..bdfe6c87d 100644 --- a/crypto/stark/src/lib.rs +++ b/crypto/stark/src/lib.rs @@ -37,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/s2_device_parity.rs b/crypto/stark/src/s2_device_parity.rs new file mode 100644 index 000000000..35ba21b8d --- /dev/null +++ b/crypto/stark/src/s2_device_parity.rs @@ -0,0 +1,632 @@ +//! Device-vs-host parity for S2's one-row trees and openings (FRI.md §7.6, +//! lane I-S2-D, D2). +//! +//! 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/mod.rs b/crypto/stark/src/tests/mod.rs index 759075a21..d8db7083a 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -30,3 +30,5 @@ pub mod trace_test_helpers; 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/zf_s2_device_tests.rs b/crypto/stark/src/tests/zf_s2_device_tests.rs new file mode 100644 index 000000000..a8c2c7373 --- /dev/null +++ b/crypto/stark/src/tests/zf_s2_device_tests.rs @@ -0,0 +1,126 @@ +//! S2 on the device (FRI.md §7.6, lane I-S2-D, D2): 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 (FRI.md §10 (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 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; + println!( + "S2DEV vector proofs: {} files, {fri_commits} one-row device FRI commits, {trees} one-row device trees", + 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)" + ); + 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/prover/src/tests/zf_rpx_device_tests.rs b/prover/src/tests/zf_rpx_device_tests.rs index 0498d4eed..3a0534b2f 100644 --- a/prover/src/tests/zf_rpx_device_tests.rs +++ b/prover/src/tests/zf_rpx_device_tests.rs @@ -12,6 +12,10 @@ //! --lib tests::zf_rpx_device_tests::proved_rpx_vectors_equal_the_cpu_bytes \ //! -- --ignored --exact --test-threads=1 //! ``` +//! +//! S2 on the device (lane I-S2-D, D2): `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, @@ -82,3 +86,75 @@ fn proved_rpx_vectors_equal_the_cpu_bytes() { "device-proved RPX vectors differ from the checked-in CPU bytes: {bad:?}" ); } + +// --------------------------------------------------------------------------- +// S2 on the device (FRI.md §7.6, lane I-S2-D, D2) 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 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; + println!( + "S2DEV rpx vector proofs: {} files, {fri_commits} one-row device FRI commits, {trees} one-row device trees", + 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)" + ); + 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_vm_one_row_tests.rs b/prover/src/tests/zf_vm_one_row_tests.rs index b8a5eaebe..ffae5890a 100644 --- a/prover/src/tests/zf_vm_one_row_tests.rs +++ b/prover/src/tests/zf_vm_one_row_tests.rs @@ -10,6 +10,11 @@ //! - 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). +//! - D2 (lane I-S2-D): the one-row VM proof's BYTES at grinding 0, written to +//! `ZF_S2_PROOF_DIR` by a CPU build and by a cuda build; the box compares the +//! two files byte for byte (the device-proved one-row VM proof equals the CPU +//! one). The cuda run also asserts the one-row device paths fired and prints +//! the one-row device-memory line for the 0-fallback gate. use stark::proof::options::{FriMode, OneRowMode, ProofFormat, ProofOptions}; @@ -155,3 +160,94 @@ fn an_lfm_proof_round_trips_at_one_row() { "an honest one-row LFM proof must verify" ); } + +/// D2 (lane I-S2-D): the one-row VM proof bytes (grinding 0, so the proof is a +/// function of the ELF and the format alone), written as +/// `$ZF_S2_PROOF_DIR/{cpu|cuda}_{format}.rkyv`. The box runs this once in a +/// CPU build and once in a cuda build and `cmp`s the files: equal bytes = the +/// device-proved one-row proof is the CPU proof. 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 produce equal bytes, so the +/// counters are what make the comparison mean "device"), and the run prints +/// `ZF S2 DEVMEM` — the largest one-row tree the device was asked for, its +/// row-pair twin, the device fallbacks and the reserved high-water mark. +#[test] +#[ignore = "box: set ZF_S2_PROOF_DIR, run in a CPU build and a cuda build, then cmp the files"] +fn one_row_vm_proof_bytes_for_the_device_comparison() { + let dir = std::env::var("ZF_S2_PROOF_DIR").expect("set ZF_S2_PROOF_DIR"); + let build = if cfg!(feature = "cuda") { + "cuda" + } else { + "cpu" + }; + let elf_bytes = crate::test_utils::asm_elf_bytes("test_mul_8"); + for (name, one_row, fri_mode) in [ + ("one_row_1", OneRowMode::On, FriMode::Pair), + ("one_row_auto_dp", OneRowMode::Auto, FriMode::Dp), + ] { + let mut o = opts(4, one_row, fri_mode); + o.grinding_factor = 0; + #[cfg(feature = "cuda")] + let (trees0, fri0) = ( + stark::gpu_lde::gpu_one_row_trees(), + stark::gpu_lde::gpu_one_row_fri_calls(), + ); + let vm_proof = crate::prove_with_options(&elf_bytes, &o, &Default::default()) + .expect("the fixture must prove"); + assert!( + crate::verify_with_options(&vm_proof, &elf_bytes, &o, None, None) + .expect("honest verify must not error"), + "{name}: an honest one-row VM proof must verify" + ); + let bytes = rkyv::to_bytes::(&vm_proof) + .expect("rkyv") + .to_vec(); + let path = std::path::Path::new(&dir).join(format!("{build}_{name}.rkyv")); + std::fs::write(&path, &bytes).expect("write the proof bytes"); + 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 VMBYTES {build} {name}: {} bytes, {one_row_tables} of {} tables one-row -> {}", + bytes.len(), + vm_proof.proof.proofs.len(), + path.display() + ); + #[cfg(feature = "cuda")] + { + let trees = stark::gpu_lde::gpu_one_row_trees() - trees0; + let fri = stark::gpu_lde::gpu_one_row_fri_calls() - fri0; + println!( + "ZF S2 DEVICE {name}: {trees} one-row device trees, {fri} one-row device FRI commits" + ); + if 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" + ); + } + let peak = stark::gpu_lde::gpu_one_row_tree_peak_bytes(); + // A one-row tree over L rows is (2L - 1) nodes; its row-pair twin + // over the same rows (L - 1). + let rows = (peak / 32).div_ceil(2); + let twin = rows.saturating_sub(1) * 32; + println!( + "ZF S2 DEVMEM {name}: largest one-row tree {peak} B ({:.1} MiB) over {rows} LDE rows, \ + row-pair twin {twin} B ({:.1} MiB); device fallbacks {}; reserved high water {} B", + peak as f64 / (1u64 << 20) as f64, + twin as f64 / (1u64 << 20) as f64, + math_cuda::device::device_fallbacks(), + math_cuda::device::reserved_high_water() + ); + } + } +} From 699f0876be96149edbd4f9abe321ec03daa013d6 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 21:07:22 -0300 Subject: [PATCH 59/73] feat(stark,prover): price every emitted FRI row and the S2 DEEP term (RULINGS 22) The fri=dp schedule DP now prices every row the in-guest verifier emits for one query's opening of a committed FRI layer, not only the slot mux, the group fold and the twiddle chain: the x_g derivation (d selects + d BALU), fold-level scaling (max(0, d-2) XALU + [d>=2] BALU), the slot assert (2 XALU), the root compare (8 BALU + 1 unpack), the group's 2^d value hints and unpacks, the leaf's ceil(3*2^d/4) Pack rows, and the path's sibling hints (depth - c: a cap now also saves c hints per query). fri_group_layer_rows and fri_pair_layer_rows are the row model; lfm::fri_group_tests pins them against the emitter kind by kind and in total, for d = 1..6 and today's pair layer, at caps 0, 1 and 2. The legacy pair-layer body moves into emit_pair_layer (the same instructions in the same order) so it can be measured. The S2 auto rule gains the in-guest DEEP term: row pairs evaluate DEEP at two points, one row at one, each num_surviving + 4E + P + 3 XALU rows (deep_point_xalu_rows, pinned against emit_deep_point); TableWidths carries it from the AIR's OOD layout. The FRI chain is priced by FriFormat::chain_cost_q, with today's pair encoding priced as pair layers when the format is legacy. Format changes (default proofs unchanged; goldens green): - U1: cap auto unchanged; cap off T=9 B=16 S2 and B=17 S3 [4,3] -> [3,2,2], T=10 B=14 S2 and B=15 S3 [4] -> [2,2]. - a_schedules.json regenerated (12 of 456 schedules move, every cost moves, weights_ns gains xalu/balu). No proof vector (d, e) moved. - auto pins: the MEMW-like and the narrow short preprocessed cases now go one row. - the device parity shape list drops [4] from the DP's own set; [4] is kept as an extra shape so the sweep still covers it (29 cases). --- crypto/stark/src/fri/device_parity.rs | 3 + crypto/stark/src/fri/schedule.rs | 247 +++++- crypto/stark/src/fri/vectors.rs | 12 +- crypto/stark/src/leaf_layout.rs | 66 +- crypto/stark/src/tests/fri_schedule_tests.rs | 174 +++- crypto/stark/src/tests/one_row_tests.rs | 122 ++- crypto/stark/src/tests/zf_fri_device_tests.rs | 7 +- .../tests/vectors/zf_fri/a_schedules.json | 754 +++++++++--------- prover/src/lfm/fri.rs | 60 +- prover/src/lfm/fri_group_tests.rs | 475 ++++++----- 10 files changed, 1234 insertions(+), 686 deletions(-) diff --git a/crypto/stark/src/fri/device_parity.rs b/crypto/stark/src/fri/device_parity.rs index fed4cf79c..0bc8cdba1 100644 --- a/crypto/stark/src/fri/device_parity.rs +++ b/crypto/stark/src/fri/device_parity.rs @@ -47,6 +47,9 @@ type Ext = FieldElement; /// 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 RULINGS 22 re-priced the + // objective, kept so the sweep's coverage does not shrink. + &[4], &[6], &[1, 6], &[6, 1], diff --git a/crypto/stark/src/fri/schedule.rs b/crypto/stark/src/fri/schedule.rs index 2a6ce3022..1e9cb74b0 100644 --- a/crypto/stark/src/fri/schedule.rs +++ b/crypto/stark/src/fri/schedule.rs @@ -19,28 +19,42 @@ //! * the active Merkle-cap policy ([`CapPolicy`]; `Off` caps nothing); //! * `dmax` — the largest fold exponent the program may choose. //! -//! # The objective (RULINGS 13): the cost law, not permutations +//! # The objective (RULINGS 13, 22): 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: +//! 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^d − 1)·fold the group fold: 2^d − 1 binary folds +//! + 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 -//! − cap_gain(Q, c(depth)) / Q what the tree's cap saves, per query (0 without a cap) +//! + 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 per-operation row counts are the in-guest emitter's -//! (`prover/src/lfm/edsl.rs::fri_fold` = 5 `XALU` rows, a `Select` = 1 -//! `SELECT` row, a base `mul` = 1 `BALU` row) — the in-guest lane pins -//! "emitted rows == these rows" against its emitter. Costs are kept in units of -//! `1/Q` ns so every term is an integer. +//! `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 lane pins "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}; @@ -67,6 +81,28 @@ pub const FRI_TWIDDLE_BALU_ROWS: u64 = 1; /// 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; @@ -81,10 +117,14 @@ pub const BALU_ROW_NS: u64 = 477; pub struct FriCostWeights { /// Compression, select, unpack, hint and compare prices (the cap policy's). pub cap: CapWeights, - /// One binary fold in-guest. + /// One binary fold in-guest ([`FRI_FOLD_XALU_ROWS`] `XALU` rows). pub fold: u64, - /// One step of the twiddle chain in-guest. + /// 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 @@ -94,8 +134,90 @@ 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, @@ -109,15 +231,24 @@ pub fn fri_chain_start(lde_log: u32, one_row: bool) -> u32 { } } +/// `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 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`. See the module docs. +/// `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, @@ -125,20 +256,49 @@ pub fn fri_layer_cost_q( num_queries: u64, cap: CapPolicy, ) -> u64 { - // i128 throughout, d clamped to 64 so 2^d fits; the result is clamped into - // u64 (it is non-negative — a cap never saves more than the walk it - // shortens — but the clamp keeps that a non-assumption). - let w = |x: u64| x as i128; - let d = d.min(64); + 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 group = (1i128 << d) - 1; - let per_query = w(fri_leaf_blocks(d)) * w(weights.cap.compress) - + i128::from(depth) * (w(weights.cap.compress) + w(weights.cap.select)) - + group * (w(FRI_SLOT_SELECT_ROWS) * w(weights.cap.select) + w(weights.fold)) - + i128::from(d) * w(weights.twiddle); + 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 total = q.saturating_mul(per_query) - cap_gain(&weights.cap, queries, c); + 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) } @@ -164,7 +324,7 @@ pub fn fri_schedule_cost_by( } /// [`fri_schedule_cost_by`] under the production objective -/// ([`FRI_COST_WEIGHTS`], [`fri_layer_cost_q`]). +/// ([`FRI_COST_WEIGHTS`], [`fri_layer_cost_q`]) — group layers. pub fn fri_schedule_cost_q( b0: u32, schedule: &[u8], @@ -349,6 +509,39 @@ impl FriFormat { 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). diff --git a/crypto/stark/src/fri/vectors.rs b/crypto/stark/src/fri/vectors.rs index 8534fe7fc..88d2c270e 100644 --- a/crypto/stark/src/fri/vectors.rs +++ b/crypto/stark/src/fri/vectors.rs @@ -122,8 +122,16 @@ pub fn schedules_json() -> VectorFile { let w = FRI_COST_WEIGHTS; let _ = writeln!( s, - " \"weights_ns\": {{\"compress\": {}, \"select\": {}, \"unpack\": {}, \"hint\": {}, \"compare\": {}, \"fold\": {}, \"twiddle\": {}}},", - w.cap.compress, w.cap.select, w.cap.unpack, w.cap.hint, w.cap.compare, w.fold, w.twiddle + " \"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"); diff --git a/crypto/stark/src/leaf_layout.rs b/crypto/stark/src/leaf_layout.rs index 389b55816..e00ed3af5 100644 --- a/crypto/stark/src/leaf_layout.rs +++ b/crypto/stark/src/leaf_layout.rs @@ -26,7 +26,7 @@ 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, fri_schedule_cost_q}; +use crate::fri::schedule::{FRI_COST_WEIGHTS, FriFormat}; use crate::proof::options::{OneRowMode, ProofOptions}; use crate::traits::AIR; @@ -124,6 +124,29 @@ pub struct TableWidths { 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 { @@ -152,11 +175,24 @@ impl TableWidths { 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, + ), } } } @@ -196,12 +232,12 @@ pub fn trace_tree_cost_q(felts: u64, depth: u32, num_queries: u64, cap: CapPolic /// 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`, and the uncommitted fold 0 costs one fold -/// and one twiddle step. One row: every leaf holds one row and is `lde_log` -/// deep, and the FRI chain (layer 0 = the committed DEEP codeword) starts at -/// `lde_log`. The in-guest DEEP arithmetic (two points vs one) is NOT priced: -/// it is not a term of the shared objective, and leaving it out only ever -/// favours today's layout. +/// 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 (RULINGS 22). A DEEP point costs +/// [`TableWidths::deep_point_rows`] `XALU` rows. pub fn table_openings_cost_q( widths: &TableWidths, options: &ProofOptions, @@ -232,15 +268,13 @@ pub fn table_openings_cost_q( cap, schedule_override: options.format.fri_schedule_override, }; - let b0 = crate::fri::schedule::fri_chain_start(lde_log, one_row); - let schedule = fmt.schedule(lde_log, terminal_log); - let chain = fri_schedule_cost_q(b0, &schedule, q, cap).unwrap_or(u64::MAX); - let fold0 = if !one_row && lde_log > terminal_log { - q.saturating_mul(FRI_COST_WEIGHTS.fold + FRI_COST_WEIGHTS.twiddle) - } else { - 0 - }; - trees.saturating_add(chain).saturating_add(fold0) + 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) } /// RULINGS 6's `auto` rule: one row iff it is STRICTLY cheaper than row pairs diff --git a/crypto/stark/src/tests/fri_schedule_tests.rs b/crypto/stark/src/tests/fri_schedule_tests.rs index 721b0010f..985f1ca51 100644 --- a/crypto/stark/src/tests/fri_schedule_tests.rs +++ b/crypto/stark/src/tests/fri_schedule_tests.rs @@ -11,9 +11,9 @@ 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_layer_cost_q, fri_leaf_blocks, fri_schedule, - fri_schedule_by, fri_schedule_cost_by, fri_schedule_cost_q, fri_schedule_with_cost, - legacy_fri_schedule, + 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::{ @@ -103,8 +103,9 @@ fn leaf_blocks() { } } -/// The objective's weights are a format constant (RULINGS 13): the cap -/// policy's weights plus the in-guest fold and twiddle rows. +/// The objective's weights are a format constant (RULINGS 13, 22): 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); @@ -126,33 +127,115 @@ fn cost_weights_are_pinned() { (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. +/// One layer's cost written out by hand (RULINGS 22: every emitted row). #[test] fn layer_cost_by_hand() { - // d = 3, depth 10, no cap: leaf 3·2251 + 10·(2251+567) + 7·(567+2610) + 3·477. - let per_query = 3 * 2251 + 10 * (2251 + 567) + 7 * (567 + 2610) + 3 * 477; + // 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. + // 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 * per_query - gain as u64 - Q * 3 * 460 ); - // The legacy layer (d = 1) prices one leaf block, one select, one fold, - // one twiddle. + // 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 + 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 lane pins +/// 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) ); } @@ -646,10 +729,13 @@ fn design_model_pins_match_fri_md_table() { type CostRow = (u32, &'static [u8], &'static [u8]); /// Generated by `print_cost_law_schedule_table` (below, `--ignored`) from the -/// Rust DP at the commit that introduced the cost-law objective. Independent -/// cross-check: design/REVIEW-FRI.md F2's cost-law column (its own model, -/// ASSUMED widths, cap = ruling 1) gives [2,2] / [3,3,3] / [3,3,3,2] / -/// [3,3,3,3,2] at B = 14 / 19 / 21 / 24, T = 9 — exactly the Auto rows here. +/// Rust DP. Re-pinned for RULINGS 22 (every emitted row priced): the cap-auto +/// tables did NOT move; four cap-off entries did — T = 9: B = 16 S2 [4,3] → +/// [3,2,2], B = 17 S3 [4,3] → [3,2,2]; T = 10: B = 14 S2 [4] → [2,2], +/// B = 15 S3 [4] → [2,2]. The whole table was cross-checked against an +/// independent Python reproduction of the objective (lane I-PRICE scratch): +/// identical. REVIEW-FRI F2's cost-law column 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, &[], &[]), @@ -661,8 +747,8 @@ const PIN_COST_T9_CAP_OFF: &[CostRow] = &[ (13, &[3], &[2, 2]), (14, &[2, 2], &[3, 2]), (15, &[3, 2], &[3, 3]), - (16, &[3, 3], &[4, 3]), - (17, &[4, 3], &[3, 3, 2]), + (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]), @@ -680,8 +766,8 @@ const PIN_COST_T10_CAP_OFF: &[CostRow] = &[ (11, &[], &[1]), (12, &[1], &[2]), (13, &[2], &[3]), - (14, &[3], &[4]), - (15, &[4], &[3, 2]), + (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]), @@ -793,8 +879,18 @@ fn print_cost_law_schedule_table() { fn cost_law_b21_by_hand() { let layer = |d: u64, depth: u64| -> u64 { let leaf = (3u64 << d).div_ceil(8).max(1); - let g = (1u64 << d) - 1; - Q * (leaf * 2251 + depth * (2251 + 567) + g * (567 + 2610) + d * 477) + 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; @@ -819,22 +915,36 @@ fn cost_law_b21_by_hand() { // --------------------------------------------------------------------------- /// Independent oracle for one layer's cost-law price (the module docs' -/// formula, written out again with the cap's gain recomputed from its terms). +/// 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 (fold, tw) = (2610i128, 477i128); + let (xalu, balu) = (522i128, 477i128); let leaf = i128::from((3u64 << d).div_ceil(8).max(1) as u32); - let g = (1i128 << d) - 1; - let per_query = - leaf * wc + i128::from(depth) * (wc + ws) + g * (ws + fold) + i128::from(d) * tw; + let n = 1i128 << d; + let d = i128::from(d); let c = cap.height(q as usize, depth as usize) as i128; - let gain = if c == 0 { + 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 { - let n = 1i128 << c; - q as i128 * (c * (wc + ws) - (n - 1) * ws - wu) - ((n - 1) * wc + n * wh + wq) + (cap_nodes - 1) * wc + cap_nodes * wh + wq }; - (q as i128 * per_query - gain) as u64 + (q as i128 * per_query + per_tree) as u64 } /// Every composition of `b0 − t` into parts in `1..=dmax`, with its cost; diff --git a/crypto/stark/src/tests/one_row_tests.rs b/crypto/stark/src/tests/one_row_tests.rs index c385fc543..523518041 100644 --- a/crypto/stark/src/tests/one_row_tests.rs +++ b/crypto/stark/src/tests/one_row_tests.rs @@ -19,11 +19,12 @@ 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, resolve_leaf_layout, table_leaf_layout, - table_openings_cost_q, + 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; @@ -738,6 +739,7 @@ fn auto_is_the_strict_cost_comparison() { 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); @@ -757,6 +759,7 @@ fn auto_is_the_strict_cost_comparison() { 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); @@ -769,14 +772,26 @@ fn auto_is_the_strict_cost_comparison() { } } +/// 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 as REVIEW-FRI §C reads them; the others are round -/// numbers), not a census: at generation the MEMW-like and CPU-like cases sat -/// within 0.5% and 2% of the threshold (row 49,988,402 vs pair 49,741,452; -/// row 51,474,062 vs pair 52,465,162, ×Q ns), so they pin the rule's edge. +/// numbers), not a census. Re-pinned for RULINGS 22 (every emitted FRI row +/// priced, DEEP at two points vs one): two choices moved to one row — the +/// MEMW-like case (row pairs by 0.5% before; one row by 5.5% now, and only +/// because of the DEEP term, 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); @@ -785,24 +800,90 @@ fn auto_choices_are_pinned() { ("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, false), - ("memw-like", 21, 0, 49, 13, 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 { - let w = TableWidths { - precomputed: pre, - main, - aux: aux * 3, - composition: parts * 3, - }; - got.push((name, resolve_leaf_layout(&w, &o, b, 2).is_one_row())); + 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"); + } + } +} + +/// RULINGS 22: 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. @@ -884,5 +965,18 @@ fn widths_of_an_extension_air() { 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/zf_fri_device_tests.rs b/crypto/stark/src/tests/zf_fri_device_tests.rs index 480176893..63f4e09c7 100644 --- a/crypto/stark/src/tests/zf_fri_device_tests.rs +++ b/crypto/stark/src/tests/zf_fri_device_tests.rs @@ -38,7 +38,8 @@ fn dp_shapes_are_pinned() { } const PINNED_SHAPES: &[&[u8]] = &[ - // The DP's own (22). + // The DP's own (21; RULINGS 22 dropped [4] and moved [4, 3] after + // [4, 3, 3, 3] in first-appearance order). &[1], &[2], &[3], @@ -57,11 +58,11 @@ const PINNED_SHAPES: &[&[u8]] = &[ &[3, 3, 3, 3, 2, 2], &[3, 3, 3, 3, 3, 2], &[3, 3, 3, 3, 3, 3], - &[4, 3], &[4, 3, 3], &[4, 3, 3, 3], + &[4, 3], + // EXTRA_SHAPES (8). &[4], - // EXTRA_SHAPES (7). &[6], &[1, 6], &[6, 1], diff --git a/crypto/stark/tests/vectors/zf_fri/a_schedules.json b/crypto/stark/tests/vectors/zf_fri/a_schedules.json index e6605b3c0..38e9fdf84 100644 --- a/crypto/stark/tests/vectors/zf_fri/a_schedules.json +++ b/crypto/stark/tests/vectors/zf_fri/a_schedules.json @@ -1,160 +1,160 @@ { "generator": "stark::fri::vectors::schedules_json", - "weights_ns": {"compress": 2251, "select": 567, "unpack": 528, "hint": 460, "compare": 3789, "fold": 2610, "twiddle": 477}, + "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": 51531}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 78777}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 78777}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 125085}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 125085}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 174462}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 174462}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 220770}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 220770}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 275532}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 275532}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 333363}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 333363}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 388125}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 388125}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 451341}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 451341}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 517626}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 517626}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 580842}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 580842}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 652512}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 652512}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 727251}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 727251}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 798921}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 798921}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 879045}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 879045}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 962238}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 962238}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1042362}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1042362}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1130940}, - {"terminal_log": 4, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1130940}, - {"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": 1222587}, - {"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": 1222587}, - {"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": 1311165}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [1], "cost_q_ns": 51531}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 78777}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 78777}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 125085}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 125085}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 174462}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 174462}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 220770}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 220770}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 275532}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 275532}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 333363}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 333363}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 388125}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 388125}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 451341}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 451341}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 517626}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 517626}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 580842}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 580842}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 652512}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 652512}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 727251}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 727251}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 798921}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 798921}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 879045}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 879045}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 962238}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 962238}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1042362}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 1042362}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1130940}, - {"terminal_log": 4, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 1130940}, - {"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": 1222587}, - {"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": 1222587}, - {"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": 1311165}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [1], "cost_q_ns": 1889470}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 2888490}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 2888490}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 4586450}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 4586450}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 6396940}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 6396940}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 8094900}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 8094900}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 10102840}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 10102840}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 12223310}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 12223310}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 14231250}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 14231250}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 16549170}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 16549170}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 18979620}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 18979620}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 21297540}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 21297540}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 23925440}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 23925440}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 26665870}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 26665870}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 29293770}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 29293770}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 32231650}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 32231650}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 35282060}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 35282060}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 38219940}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 38219940}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 41467800}, - {"terminal_log": 4, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 41467800}, - {"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": 44828190}, - {"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": 44828190}, - {"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": 48076050}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 6, "chain": "s3", "b0": 5, "schedule": [1], "cost_q_ns": 1477426}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 6, "chain": "s2", "b0": 6, "schedule": [2], "cost_q_ns": 2476446}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 7, "chain": "s3", "b0": 6, "schedule": [2], "cost_q_ns": 2476446}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 7, "chain": "s2", "b0": 7, "schedule": [3], "cost_q_ns": 4174406}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 8, "chain": "s3", "b0": 7, "schedule": [3], "cost_q_ns": 4174406}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 8, "chain": "s2", "b0": 8, "schedule": [2, 2], "cost_q_ns": 5572852}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 9, "chain": "s3", "b0": 8, "schedule": [2, 2], "cost_q_ns": 5572852}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 9, "chain": "s2", "b0": 9, "schedule": [3, 2], "cost_q_ns": 7270812}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 10, "chain": "s3", "b0": 9, "schedule": [3, 2], "cost_q_ns": 7270812}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 10, "chain": "s2", "b0": 10, "schedule": [3, 3], "cost_q_ns": 9278752}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [3, 3], "cost_q_ns": 9278752}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 10987178}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [3, 2, 2], "cost_q_ns": 10987178}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 12995118}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3, 3, 2], "cost_q_ns": 12995118}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 15313038}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3, 3, 3], "cost_q_ns": 15313038}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 17331444}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 3, 2, 2], "cost_q_ns": 17331444}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 19649364}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3, 3, 2], "cost_q_ns": 19649364}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 22277264}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3, 3, 3], "cost_q_ns": 22277264}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 24605650}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 3, 2, 2], "cost_q_ns": 24605650}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 27233550}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 27233550}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 30171430}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 30171430}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 32809796}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 3, 2, 2], "cost_q_ns": 32809796}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 35747676}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3, 3, 2], "cost_q_ns": 35747676}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 38995536}, - {"terminal_log": 4, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3, 3, 3], "cost_q_ns": 38995536}, - {"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": 41943882}, - {"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": 41943882}, - {"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": 45191742}, + {"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}, @@ -164,35 +164,35 @@ {"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": 93801}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 93801}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 121047}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 121047}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 167355}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 167355}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 259002}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 259002}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 305310}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 305310}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 360072}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 360072}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [4, 3], "cost_q_ns": 458010}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [4, 3], "cost_q_ns": 458010}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 514935}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 514935}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 578151}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 578151}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 676089}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 676089}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 749922}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 749922}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 821592}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 821592}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 919530}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 919530}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1010271}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1010271}, - {"terminal_log": 9, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 1090395}, + {"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}, @@ -202,35 +202,35 @@ {"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": 93801}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 93801}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 121047}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 121047}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 167355}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 167355}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 259002}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 259002}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 305310}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 305310}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 360072}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 360072}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [4, 3], "cost_q_ns": 458010}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [4, 3], "cost_q_ns": 458010}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 514935}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 514935}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 578151}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 578151}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 676089}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 676089}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 749922}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 749922}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 821592}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 821592}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 919530}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 919530}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1010271}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1010271}, - {"terminal_log": 9, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 1090395}, + {"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}, @@ -240,35 +240,35 @@ {"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": 3439370}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 3439370}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 4438390}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 4438390}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 6136350}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 6136350}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 9496740}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 9496740}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 11194700}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 11194700}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 13202640}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 13202640}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [4, 3], "cost_q_ns": 16793700}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [4, 3], "cost_q_ns": 16793700}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 18880950}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 18880950}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 21198870}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 21198870}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 24789930}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [4, 3, 3], "cost_q_ns": 24789930}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 27497140}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 27497140}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 30125040}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 30125040}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 33716100}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 33716100}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 37043270}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 37043270}, - {"terminal_log": 9, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 39981150}, + {"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}, @@ -278,35 +278,35 @@ {"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": 3027326}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s3", "b0": 10, "schedule": [1], "cost_q_ns": 3027326}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 11, "chain": "s2", "b0": 11, "schedule": [2], "cost_q_ns": 4026346}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [2], "cost_q_ns": 4026346}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [3], "cost_q_ns": 5724306}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [3], "cost_q_ns": 5724306}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [2, 2], "cost_q_ns": 8672652}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [2, 2], "cost_q_ns": 8672652}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [3, 2], "cost_q_ns": 10370612}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [3, 2], "cost_q_ns": 10370612}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 3], "cost_q_ns": 12378552}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 3], "cost_q_ns": 12378552}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 15636878}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 2, 2], "cost_q_ns": 15636878}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 17644818}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 3, 2], "cost_q_ns": 17644818}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 19962738}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 3], "cost_q_ns": 19962738}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 2, 2], "cost_q_ns": 23531044}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 2, 2], "cost_q_ns": 23531044}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 25848964}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [3, 3, 3, 2], "cost_q_ns": 25848964}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 28476864}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 3], "cost_q_ns": 28476864}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 32067924}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [4, 3, 3, 3], "cost_q_ns": 32067924}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 34983050}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 34983050}, - {"terminal_log": 9, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 3], "cost_q_ns": 37920930}, + {"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}, @@ -318,33 +318,33 @@ {"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": 102255}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 102255}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 129501}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 129501}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 175809}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 175809}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [4], "cost_q_ns": 273747}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [4], "cost_q_ns": 273747}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 322218}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 322218}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 376980}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 376980}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [4, 3], "cost_q_ns": 474918}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [4, 3], "cost_q_ns": 474918}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 540297}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 540297}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 603513}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 603513}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 701451}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 701451}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 783738}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 783738}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 855408}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 855408}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 953346}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 953346}, - {"terminal_log": 10, "queries": 3, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1052541}, + {"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}, @@ -356,33 +356,33 @@ {"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": 102255}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 102255}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 129501}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 129501}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 175809}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 175809}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [4], "cost_q_ns": 273747}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [4], "cost_q_ns": 273747}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 322218}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 322218}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 376980}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 376980}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [4, 3], "cost_q_ns": 474918}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [4, 3], "cost_q_ns": 474918}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 540297}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 540297}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 603513}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 603513}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 701451}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 701451}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 783738}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 783738}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 855408}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 855408}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 953346}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 953346}, - {"terminal_log": 10, "queries": 3, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 1052541}, + {"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}, @@ -394,33 +394,33 @@ {"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": 3749350}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 3749350}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 4748370}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 4748370}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 6446330}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 6446330}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [4], "cost_q_ns": 10037390}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [4], "cost_q_ns": 10037390}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 11814660}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 11814660}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 13822600}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 13822600}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [4, 3], "cost_q_ns": 17413660}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [4, 3], "cost_q_ns": 17413660}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 19810890}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 19810890}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 22128810}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 22128810}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 25719870}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 25719870}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 28737060}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 28737060}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 31364960}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 31364960}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 34956020}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 34956020}, - {"terminal_log": 10, "queries": 110, "cap": "off", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 38593170}, + {"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}, @@ -432,32 +432,32 @@ {"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": 3337306}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s3", "b0": 11, "schedule": [1], "cost_q_ns": 3337306}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 12, "chain": "s2", "b0": 12, "schedule": [2], "cost_q_ns": 4336326}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s3", "b0": 12, "schedule": [2], "cost_q_ns": 4336326}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 13, "chain": "s2", "b0": 13, "schedule": [3], "cost_q_ns": 6034286}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s3", "b0": 13, "schedule": [3], "cost_q_ns": 6034286}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 14, "chain": "s2", "b0": 14, "schedule": [2, 2], "cost_q_ns": 9292612}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s3", "b0": 14, "schedule": [2, 2], "cost_q_ns": 9292612}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 15, "chain": "s2", "b0": 15, "schedule": [3, 2], "cost_q_ns": 10990572}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s3", "b0": 15, "schedule": [3, 2], "cost_q_ns": 10990572}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 16, "chain": "s2", "b0": 16, "schedule": [3, 3], "cost_q_ns": 12998512}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s3", "b0": 16, "schedule": [3, 3], "cost_q_ns": 12998512}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 17, "chain": "s2", "b0": 17, "schedule": [3, 2, 2], "cost_q_ns": 16566818}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s3", "b0": 17, "schedule": [3, 2, 2], "cost_q_ns": 16566818}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 18, "chain": "s2", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 18574758}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s3", "b0": 18, "schedule": [3, 3, 2], "cost_q_ns": 18574758}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 19, "chain": "s2", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 20892678}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s3", "b0": 19, "schedule": [3, 3, 3], "cost_q_ns": 20892678}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 20, "chain": "s2", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 24483738}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s3", "b0": 20, "schedule": [4, 3, 3], "cost_q_ns": 24483738}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 21, "chain": "s2", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 27088884}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s3", "b0": 21, "schedule": [3, 3, 3, 2], "cost_q_ns": 27088884}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 22, "chain": "s2", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 29716784}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s3", "b0": 22, "schedule": [3, 3, 3, 3], "cost_q_ns": 29716784}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 23, "chain": "s2", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 33307844}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s3", "b0": 23, "schedule": [4, 3, 3, 3], "cost_q_ns": 33307844}, - {"terminal_log": 10, "queries": 110, "cap": "auto", "lde_log": 24, "chain": "s2", "b0": 24, "schedule": [3, 3, 3, 3, 2], "cost_q_ns": 36532950} + {"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/prover/src/lfm/fri.rs b/prover/src/lfm/fri.rs index 194c28b83..26ca7a347 100644 --- a/prover/src/lfm/fri.rs +++ b/prover/src/lfm/fri.rs @@ -872,21 +872,16 @@ pub fn emit_query_fri( if shape.is_legacy() { let mut inv_pow = inv; for (i, opening) in openings.iter().enumerate() { - 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(q.bits[i], v.as_cell(), sym.as_cell()); - let leaf = sub_proof::emit_leaf_hash(b, FRI_LEAF_GROUP, &[first, second]); - // `bits[i+1..]` is this layer tree's whole leaf index; a cap walks its - // low bits and muxes the top ones. - fri.layers[i].authenticate(b, leaf, &q.bits[i + 1..], &opening.siblings); - - // `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, sym, fri.zetas[i + 1], inv_pow); + (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 @@ -924,6 +919,41 @@ 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` (FRI.md §1.3), 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)`): diff --git a/prover/src/lfm/fri_group_tests.rs b/prover/src/lfm/fri_group_tests.rs index bdeaeb29b..64d0c156c 100644 --- a/prover/src/lfm/fri_group_tests.rs +++ b/prover/src/lfm/fri_group_tests.rs @@ -12,16 +12,18 @@ //! 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 (F9), both legs as one program; -//! - RULINGS 13: the rows the emitter emits per group layer, against the DP's -//! cost-model terms (`stark::fri::schedule`), with every unmodelled row named. +//! - RULINGS 13 + 22: 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, FRI_FOLD_XALU_ROWS, FRI_SLOT_SELECT_ROWS, FRI_TWIDDLE_BALU_ROWS, - fri_leaf_blocks, fri_schedule_by, + 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; @@ -29,10 +31,12 @@ 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}; +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}; @@ -454,239 +458,310 @@ fn the_cap_and_fri_matrix_round_trips_in_guest() { } // ============================================================================= -// RULINGS 13 — the emitted rows per group layer against the DP's cost terms +// RULINGS 13 + 22 — every emitted row per FRI layer and per DEEP point, against +// the host's cost model (`stark::fri::schedule`, `stark::leaf_layout`) // ============================================================================= -/// Rows one group layer of fold exponent `d` emits, by kind, measured on the -/// emitter itself: the layer is emitted TWICE in one builder over hinted -/// inputs and the second emission is counted, so interned program constants -/// (paid once per program) are out of the figure. The tree is two levels -/// deep and uncapped, which isolates the model's path term. -struct LayerRows { - selects: usize, - xalu: usize, - balu: usize, - hashes: usize, - unpacks: usize, - hints: usize, - total: usize, +/// 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 } -fn measure_group_layer(d: u32) -> LayerRows { - let once = group_layer_program(d, 1); - let twice = group_layer_program(d, 2); +/// 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)); - LayerRows { - selects: b.0 - a.0, - xalu: b.1 - a.1, - balu: b.2 - a.2, - hashes: b.3 - a.3, - unpacks: b.4 - a.4, - hints: b.5 - a.5, - total: twice.instrs.len() - once.instrs.len(), + 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], } } -/// A program emitting `times` group layers of exponent `d` over hinted -/// inputs that are all hinted BEFORE the first emission, so the difference -/// between `times = 2` and `times = 1` is exactly one layer's rows. One -/// committed layer over a two-level tree: `n − 1 = d + 2` index bits and a -/// terminal at `2^2` (blowup `2^1`, `k = 1`). -fn group_layer_program(d: u32, times: usize) -> LfmProgram { +/// 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: d + 3, + log2_lde_length: fold + MEASURED_DEPTH as u32 + 1, blowup_log: 1, final_poly_log_degree: 1, coset_offset: 3, num_queries: 1, - format: ProofFormat { - fri_mode: FriMode::Dp, - fri_schedule_override: FriScheduleOverride::new(&[d as u8]), - ..ProofFormat::DEFAULT + 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.schedule(), vec![d as u8]); - assert_eq!(shape.layer_depth(0), 2); + assert_eq!(shape.is_legacy(), pair); + assert_eq!(shape.schedule(), vec![fold as u8]); + assert_eq!(shape.layer_depth(0), MEASURED_DEPTH); - let n = 1usize << d; + 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()); - let arena = b.declare_arena((4 + d as usize + times * (n + 2)) as u32); + 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 commitment = LayerCommitment::from_lanes(vec![b.unpack(root)]); + 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..d).map(|i| b.hint_word(arena, 4 + i).as_ext()).collect(); - let mut at = 4 + d; - let openings: Vec = (0..times) - .map(|_| { - let values = (0..n) - .map(|_| { - at += 1; - b.hint_word(arena, at - 1).as_ext() - }) - .collect(); - let siblings = (0..2) - .map(|_| { - at += 1; - super::edsl::WrapDigest::from_cell(b.hint_word(arena, at - 1)) - }) - .collect(); - LayerOpening { values, siblings } - }) + let zetas: Vec<_> = (0..fold) + .map(|i| b.hint_word(arena, 4 + i).as_ext()) .collect(); - for opening in &openings { - emit_group_layer( - &mut b, - shape, - 0, - &commitment, - &zetas, - v, - y_inv, - opening, - &bits, - ); - } - compile(b.finish()) -} - -/// `(selects, XALU, BALU, hashes, unpacks, hints)` over an instruction list. -fn count_kinds(instrs: &[Instr]) -> (usize, usize, usize, usize, usize, usize) { - let mut k = (0, 0, 0, 0, 0, 0); - for i in instrs { - match i { - Instr::Select { .. } => k.0 += 1, - Instr::ExtAlu { .. } => k.1 += 1, - Instr::BaseAlu { .. } => k.2 += 1, - Instr::Hash { .. } => k.3 += 1, - Instr::Unpack { .. } => k.4 += 1, - Instr::Hint { .. } => k.5 += 1, - _ => {} + 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, + ); } } - k + compile(b.finish()) } -/// ★ RULINGS 13: the rows the emitter emits per group layer, against the -/// DP's cost-model terms (I-FRI-H's weights, `stark::fri::schedule`): +/// ★ RULINGS 13 + 22: 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 -/// model, per query per committed layer of exponent d over a depth-D tree: -/// leaf(d)·compress + D·(compress + select) + (2^d − 1)·select -/// + (2^d − 1)·fold(5 XALU) + d·twiddle(1 BALU) +/// 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 three terms the ruling names — the slot mux, the group fold and the -/// twiddle chain — each MATCH the emitter row for row (and so do the leaf and -/// the walk). The emitter ALSO emits rows the model does not price, and this -/// test pins them rather than hiding them, because the schedule is a format -/// constant and a change of weights is the lead's ruling (RULINGS 13): -/// -/// - `x_g⁻¹ = y⁻¹·ω^{br(slot)}`: `d` selects of constants and `d` base muls; -/// - fold-level scaling: one `emul_base` per level with more than two pairs -/// (`max(0, d − 2)` XALU) and one base mul on the level with two pairs -/// (`[d ≥ 2]` BALU); -/// - the slot check's `assert_eq_ext`: 2 XALU; -/// - the per-opening root (or cap node) compare: 8 BALU rows (four lowered -/// asserts) and one unpack — which today's pair layer pays as well; -/// - the group's `2^d` unpacks (the leaf reads three lanes of each value) and -/// `2^d` value hints, plus the walked root's one unpack and the path hints. -/// -/// At `d = 1` the model is today's pair layer exactly (1 select, 5 XALU, -/// 1 BALU); the group encoding at `d = 1` pays the extras on top. +/// 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 the_group_layer_rows_against_the_dp_cost_model() { +fn every_emitted_fri_row_is_priced() { let w = FRI_COST_WEIGHTS; - let depth = 2usize; - println!( - "\n d | model sel/XALU/BALU/hash | emitted sel/XALU/BALU/hash | unmodelled \ - sel/XALU/BALU unpack hint | model ns unmodelled ns" - ); - for d in 1..=6u32 { - let r = measure_group_layer(d); - let n = 1usize << d; - // The model's rows (the ruling's terms plus the leaf and the walk). - let m_sel = (n - 1) * FRI_SLOT_SELECT_ROWS as usize + depth; - let m_xalu = (n - 1) * FRI_FOLD_XALU_ROWS as usize; - let m_balu = d as usize * FRI_TWIDDLE_BALU_ROWS as usize; - let m_hash = fri_leaf_blocks(d) as usize + depth; - // What the emitter adds on top, by construction (see the doc). - let x_sel = d as usize; - let x_xalu = 2 + (d as usize).saturating_sub(2); - // + the per-opening root compare: four lowered base asserts (a `sub` - // and a `div` each), today's pair layer pays it too. - let x_balu = d as usize + usize::from(d >= 2) + 8; - assert_eq!( - r.hashes, m_hash, - "d={d}: leaf blocks + one compression per level" - ); - assert_eq!(r.selects, m_sel + x_sel, "d={d}: selects"); - assert_eq!(r.xalu, m_xalu + x_xalu, "d={d}: XALU rows"); - assert_eq!(r.balu, m_balu + x_balu, "d={d}: BALU rows"); - assert_eq!( - r.unpacks, - n + 1, - "d={d}: the group's unpacks and the walked root's" - ); - assert_eq!(r.hints, n + depth, "d={d}: the group's values and its path"); - let model_ns = m_sel as u64 * w.cap.select - + (n as u64 - 1) * w.fold - + d as u64 * w.twiddle - + m_hash as u64 * w.cap.compress; - let unmodelled_ns = x_sel as u64 * w.cap.select - + x_xalu as u64 * XALU_NS - + x_balu as u64 * BALU_NS - + (n as u64 + 1) * w.cap.unpack - + n as u64 * w.cap.hint; - println!( - " {d} | {m_sel:>3}/{m_xalu:>4}/{m_balu:>2}/{m_hash:>2} | \ - {:>3}/{:>4}/{:>2}/{:>2} | {x_sel:>3}/{x_xalu:>4}/{x_balu:>2} \ - {:>4} {:>4} | {model_ns:>8} {unmodelled_ns:>8} ({} instructions)", - r.selects, - r.xalu, - r.balu, - r.hashes, - n + 1, - n, - r.total, - ); + 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) + ); + } } - // What the unmodelled rows would do to the schedule, for the lead: the DP - // re-run with them priced (hint words priced at the cap policy's hint - // weight), at the production terminals and Q = 110 under cap = auto. - // Printed, not asserted: changing the objective is a format change. - let cap = CapPolicy::Auto; - let q = 110u64; - let with_extras = |d: u32, depth: u32| -> u64 { - let base = stark::fri::schedule::fri_layer_cost_q(&w, d, depth, q, cap); - let n = 1u64 << d; - let extra = u64::from(d) * w.cap.select - + (2 + u64::from(d.saturating_sub(2))) * XALU_NS - + (u64::from(d) + u64::from(d >= 2) + 8) * BALU_NS - + (n + 1) * w.cap.unpack - + n * w.cap.hint; - base + q * extra - }; - println!("\n schedules at Q = 110, cap = auto: the ruled objective vs the emitted rows"); - for t in [9u32, 10] { - for b0 in [13u32, 18, 20, 21, 23] { - let ruled = fri_schedule_by(b0, t, 6, &|d, depth| { - stark::fri::schedule::fri_layer_cost_q(&w, d, depth, q, cap) - }); - let emitted = fri_schedule_by(b0, t, 6, &with_extras); - println!( - " T={t} b0={b0}: ruled {:?} (ns·Q {}) | with the emitted rows {:?} \ - (ns·Q {})", - ruled.schedule, ruled.cost_q, emitted.schedule, emitted.cost_q + // 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:?}" ); } } } -/// Cost-law prices of an `XALU` and a `BALU` row, the schedule module's. -const XALU_NS: u64 = stark::fri::schedule::XALU_ROW_NS; -const BALU_NS: u64 = stark::fri::schedule::BALU_ROW_NS; +/// 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()) +} + +/// ★ RULINGS 22: 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" + ); + } +} From 3e867c46347c153c3a5ca1a36f64b323fca176d8 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 21:11:33 -0300 Subject: [PATCH 60/73] test(prover): the one-row VM bytes test creates ZF_S2_PROOF_DIR The box wrapper names a fresh directory per run; creating it in the test keeps the extras line self-contained. --- prover/src/tests/zf_vm_one_row_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/prover/src/tests/zf_vm_one_row_tests.rs b/prover/src/tests/zf_vm_one_row_tests.rs index ffae5890a..051cd34fd 100644 --- a/prover/src/tests/zf_vm_one_row_tests.rs +++ b/prover/src/tests/zf_vm_one_row_tests.rs @@ -175,6 +175,7 @@ fn an_lfm_proof_round_trips_at_one_row() { #[ignore = "box: set ZF_S2_PROOF_DIR, run in a CPU build and a cuda build, then cmp the files"] fn one_row_vm_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 { From ff78f0455b7f858d53e3d4a8f649c770c6c7b3b2 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 22:32:49 -0300 Subject: [PATCH 61/73] feat(prover): the measured ZF configuration is the default format (RULINGS 26) ZfFormat::DEFAULT, what every production site stamps when no knob is set, becomes the configuration the block runs measured net positive: cap=auto, whir_cap=auto, fri=dp, whir_folds=first6 (one_row stays 0 here; it flips in its own commit). ZfFormat::LEGACY is every lever off, and every knob keeps its off spelling (cap=off, whir_cap=off, fri=pair, one_row=0, whir_folds=uniform4), so all five at off reproduce the pre-campaign format for rollback and A/B. Security parameters (queries, grinding, blowup) do not move. The crypto crates' own defaults (stark ProofFormat::DEFAULT, multilinear ChainFormat::DEFAULT) stay the legacy format: ProofFormat gains LEGACY and is_legacy(), ProofOptions gains has_legacy_format(). ProofOptions' format is still skipped by serde and rkyv, so no serialized byte moves (RULINGS 10). The RV64 recursion guest stays on the legacy format explicitly: every Preset and MIN_PROOF_OPTIONS name ProofFormat::LEGACY, and both guest entries refuse anything but the legacy format, the production default included (the_recursion_guest_stays_on_the_legacy_format). Pins: - RPX goldens: the legacy set is kept (legacy_format_rpx_goldens_are_byte_identical, bytes unmoved) and a production-default set is added. - WHIR production chain at the default (first6 under the auto cap, S=25, Q=112, grind 20): 16,443 permutations, 203,426 rows, emitted == closed form; the legacy chain pins (22,828 / 185,509) are unchanged. - whir_epoch_program_tests::the_production_epoch_recount is a record of sh1, measured at the legacy format: its config is now named LEGACY, and the default's 6 rounds at 25 are asserted beside it. - The hash-metrics transcript pins were measured at the legacy WHIR format: their closed-form tests use the legacy config, the runtime pins skip (and say so) at any other WHIR format, and the DECODE opening's schedule assert follows the process format ([6,4,4,4,4,1] at the default; same counts). - transcript_counts drives ChainConfig::schedule instead of a uniform fold width, so its closed form prices first6 as proved. --- crypto/stark/src/proof/options.rs | 38 ++- prover/src/lfm/proof.rs | 7 +- prover/src/lfm/whir_chain_tests.rs | 96 ++++++ prover/src/lfm/whir_epoch_program_tests.rs | 17 +- prover/src/multilinear_prove.rs | 5 +- prover/src/recursion.rs | 38 ++- prover/src/tests/multilinear_bench_tests.rs | 73 ++++- prover/src/tests/transcript_counts.rs | 51 +++- prover/src/tests/zf_rpx_golden_tests.rs | 113 ++++++-- prover/src/zf_format.rs | 305 +++++++++++++++++--- 10 files changed, 640 insertions(+), 103 deletions(-) diff --git a/crypto/stark/src/proof/options.rs b/crypto/stark/src/proof/options.rs index 59b2e6234..d6db6b5c0 100644 --- a/crypto/stark/src/proof/options.rs +++ b/crypto/stark/src/proof/options.rs @@ -43,7 +43,7 @@ impl fmt::Display for ProofOptionsError { /// - `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 campaign's levers). -/// Its default is today's format, byte for byte. +/// Its default is the legacy (pre-campaign) format, byte for byte. /// /// # The format is not serialized /// @@ -74,7 +74,8 @@ 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`] = today. Not serialized. + /// 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))] @@ -106,16 +107,33 @@ pub struct ProofFormat { } impl ProofFormat { - /// Today's format: every lever off. - pub const DEFAULT: Self = Self { + /// 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 pre-campaign 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 today's format (`Fixed(0)` counts as `Off`). + /// 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 pre-campaign 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 @@ -294,12 +312,18 @@ pub const FRI_MODE_IMPLEMENTED: bool = true; pub const ONE_ROW_IMPLEMENTED: bool = true; impl ProofOptions { - /// True when every format field is at its default: the proof this - /// produces is today's format, byte for byte. + /// True when every format field is at this crate's default (the legacy + /// format): the proof this produces is the pre-campaign 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 { diff --git a/prover/src/lfm/proof.rs b/prover/src/lfm/proof.rs index 7f0811d6e..f8cf391b6 100644 --- a/prover/src/lfm/proof.rs +++ b/prover/src/lfm/proof.rs @@ -589,7 +589,9 @@ fn expected_public_balance( /// ★ 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 today's options. +/// 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"); @@ -603,7 +605,8 @@ pub fn aggregation_wrap_options() -> ProofOptions { /// SITE, like [`aggregation_wrap_options`]. /// /// Not [`crate::recursion::Preset::options`] itself: that value also fixes -/// the RV64 recursion guest's verifier, which stays default-format only. +/// the RV64 recursion guest's verifier, which stays on the LEGACY format +/// (its presets name it; RULINGS 26). pub fn block_base_options() -> ProofOptions { crate::zf_format::ZfFormat::global().options(crate::recursion::Preset::Blowup4.options()) } diff --git a/prover/src/lfm/whir_chain_tests.rs b/prover/src/lfm/whir_chain_tests.rs index aaf0c2bd7..96c530843 100644 --- a/prover/src/lfm/whir_chain_tests.rs +++ b/prover/src/lfm/whir_chain_tests.rs @@ -1881,6 +1881,102 @@ fn the_first_fold_production_chains_cost_what_the_design_derived() { } } +/// ★ RULINGS 26: 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 the WHIR block measured together +/// (wt54–wt57, −9.10 s): 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 (RULINGS 26), 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). diff --git a/prover/src/lfm/whir_epoch_program_tests.rs b/prover/src/lfm/whir_epoch_program_tests.rs index fc83127a7..ecedd6a9b 100644 --- a/prover/src/lfm/whir_epoch_program_tests.rs +++ b/prover/src/lfm/whir_epoch_program_tests.rs @@ -148,7 +148,22 @@ 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; RULINGS 26) 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 = chain_config(&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 diff --git a/prover/src/multilinear_prove.rs b/prover/src/multilinear_prove.rs index b71301230..25f90f2df 100644 --- a/prover/src/multilinear_prove.rs +++ b/prover/src/multilinear_prove.rs @@ -87,7 +87,10 @@ pub struct MultilinearVmProof { /// /// ★ 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 today's config. +/// `_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) } diff --git a/prover/src/recursion.rs b/prover/src/recursion.rs index 1c2a23108..96a2fb02f 100644 --- a/prover/src/recursion.rs +++ b/prover/src/recursion.rs @@ -42,7 +42,9 @@ pub const MIN_PROOF_OPTIONS: ProofOptions = ProofOptions { coset_offset: 3, grinding_factor: 1, fri_final_poly_log_degree: 7, - format: stark::proof::options::ProofFormat::DEFAULT, + // RULINGS 26: 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 @@ -74,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 (RULINGS 26): 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"), @@ -83,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/` @@ -266,17 +276,19 @@ pub fn program_id_from_elf( )) } -/// The RV64 recursion guest verifies today's proof format only: its presets -/// fix the options at build time, and the archived verifier it runs is not -/// threaded with the ZF format levers. A non-default format 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_default_format(proof_options: &ProofOptions) -> Result<(), Error> { - if proof_options.has_default_format() { +/// The RV64 recursion guest verifies the LEGACY proof format only (RULINGS 11, +/// as amended by RULINGS 26): 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 default-format proofs only (ZF format levers off)", + "the recursion guest verifies legacy-format proofs only (every ZF format lever off)", ))) } } @@ -295,7 +307,7 @@ pub fn verify_and_attest_blob( blob: &[u8], proof_options: &ProofOptions, ) -> Result>, Error> { - require_default_format(proof_options)?; + require_legacy_format(proof_options)?; let verification = crate::verify_recursion_blob(blob, proof_options)?; if !verification.ok { return Ok(None); @@ -331,7 +343,7 @@ pub fn verify_continuation_and_attest( ) -> Result>, Error> { use rkyv::rancor::Error as RkyvError; - require_default_format(proof_options)?; + require_legacy_format(proof_options)?; let archive_bytes = crate::recursion_archive_bytes(blob).ok_or_else(|| { Error::Execution(String::from( diff --git a/prover/src/tests/multilinear_bench_tests.rs b/prover/src/tests/multilinear_bench_tests.rs index 6098585d3..af5b9558a 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(); + // ★ RULINGS 26: 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 (RULINGS 26): 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 (RULINGS 26): 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" ); + // ★ RULINGS 26: 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/transcript_counts.rs b/prover/src/tests/transcript_counts.rs index 6166cce19..22206884d 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 since RULINGS 26) 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/zf_rpx_golden_tests.rs b/prover/src/tests/zf_rpx_golden_tests.rs index d3cdd1888..599736c22 100644 --- a/prover/src/tests/zf_rpx_golden_tests.rs +++ b/prover/src/tests/zf_rpx_golden_tests.rs @@ -1,13 +1,21 @@ -//! Default-format golden proofs under the production RPX pin (REVIEW-FRI F1): -//! the RPX half of `stark::tests::zf_golden_tests` (which covers Keccak and -//! Blake3 and cannot name `RpxStarkHash`, a prover-crate type). +//! Golden proofs under the production RPX pin (REVIEW-FRI F1), 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 pre-campaign 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()`, RULINGS 26): +//! the bytes every production site now stamps. Pinned at the default flip +//! (lane I-FLIP); 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. Generated at -//! the default format before any S3 prover code existed; regenerate only for a -//! deliberate format change: +//! 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; @@ -151,7 +159,15 @@ pub(crate) fn verify_logup( } fn compute_goldens() -> Vec<(String, String)> { - let d = ProofFormat::DEFAULT; + 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); @@ -174,6 +190,27 @@ fn compute_goldens() -> Vec<(String, String)> { 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", @@ -193,25 +230,57 @@ const GOLDENS: &[(&str, &str)] = &[ ), ]; +/// The LEGACY-format RPX goldens: the pre-campaign bytes, unmoved by the +/// default flip. #[test] -fn default_format_rpx_goldens_are_byte_identical() { +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 default-format RPX proof moved (a field whose digest differs is where)" + "{name}: the legacy-format RPX proof moved (a field whose digest differs is where)" ); } } +/// The PRODUCTION-default RPX goldens (RULINGS 26: cap auto, `fri=dp`, and +/// whatever `ZfFormat::DEFAULT` stamps). A move here is a production format +/// change. #[test] -#[ignore = "generator for GOLDENS"] +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}\"),"); + } } // --------------------------------------------------------------------------- @@ -226,7 +295,7 @@ fn dp(schedule: Option<&[u8]>) -> ProofFormat { fri_mode: stark::proof::options::FriMode::Dp, fri_schedule_override: schedule .map(|s| stark::proof::options::FriScheduleOverride::new(s).expect("fits")), - ..ProofFormat::DEFAULT + ..ProofFormat::LEGACY } } @@ -262,7 +331,7 @@ fn rpx_dp_round_trips() { #[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::DEFAULT)).1; + 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!( @@ -286,12 +355,12 @@ fn rpx_group_path_at_all_ones_equals_legacy() { /// 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). Meant for knob-on runs — `LAMBDA_VM_ZF_FRI=dp` (both -/// sites stamp `Dp` and the proofs use group layers) and +/// (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); -/// without a knob it proves the same at the default format. Either way it -/// proves. +/// 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}; @@ -300,14 +369,20 @@ fn production_sites_prove_at_the_process_format() { .ok() .map(|v| v.trim().to_ascii_lowercase()) }; + // An unset knob is the production default's value (RULINGS 26). + let default = crate::zf_format::ZfFormat::DEFAULT; let want = match knob(crate::zf_format::ENV_FRI).as_deref() { Some("dp") => FriMode::Dp, - _ => FriMode::Pair, + 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, - _ => OneRowMode::Off, + 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); diff --git a/prover/src/zf_format.rs b/prover/src/zf_format.rs index 8cfda686d..1d3ccc643 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -8,8 +8,17 @@ //! LAMBDA_VM_ZF_WHIR_FOLDS uniform4 | first5 | first6 WHIR first-round fold (W2) //! ``` //! -//! Every unset knob is today's format, so an unconfigured run proves exactly -//! what it proved before this module existed. +//! ★ Every unset knob is [`ZfFormat::DEFAULT`], the MEASURED configuration +//! (RULINGS 26): `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 pre-campaign format, 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 //! @@ -36,7 +45,7 @@ //! flips its `*_IMPLEMENTED` constant when its lever is real. //! //! **The banner prints on every setting, including the default**: -//! `ZF FORMAT: cap=off whir_cap=off fri=pair one_row=0 whir_folds=uniform4`. +//! `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; @@ -56,8 +65,9 @@ pub const ENV_WHIR_FOLDS: &str = "LAMBDA_VM_ZF_WHIR_FOLDS"; /// `uniform4` after it. pub const PRODUCTION_WHIR_LOG_FOLDING: usize = 4; -/// One process's proof format. Every field's default is today's format. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +/// 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, @@ -71,9 +81,37 @@ pub struct ZfFormat { 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 { - /// Today's format: every lever off. + /// ★ The production format when no knob is set: the MEASURED + /// configuration (RULINGS 26). 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. 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 pre-campaign 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 (RULINGS 26). + pub const LEGACY: Self = Self { cap: CapPolicy::Off, whir_cap: CapPolicy::Off, fri: FriMode::Pair, @@ -81,8 +119,18 @@ impl ZfFormat { whir_folds: WhirFolds::Uniform, }; + /// True when every lever is off: the format proves exactly what the + /// pre-campaign prover proved. + 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 the default; a set one + /// 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 { @@ -308,20 +356,35 @@ mod tests { ZfFormat::from_lookup(|k| map.get(k).cloned()) } + /// ★ RULINGS 26: with no knob set the process proves the MEASURED + /// configuration. #[test] - fn nothing_set_is_todays_format() { + 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=off whir_cap=off fri=pair one_row=0 whir_folds=uniform4" + "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 + /// (pre-campaign) format: the rollback and A/B arm. #[test] - fn the_default_spellings_parse_to_the_default() { + fn the_off_spellings_parse_to_the_legacy_format() { let f = parse(&[ (ENV_CAP, "off"), (ENV_WHIR_CAP, "0"), @@ -330,8 +393,60 @@ mod tests { (ENV_WHIR_FOLDS, "uniform4"), ]) .unwrap(); - assert_eq!(f, ZfFormat::DEFAULT); + 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] @@ -473,7 +588,7 @@ mod tests { parse(&[(ENV_ONE_ROW, "auto"), (ENV_FRI, "dp")]) .unwrap() .banner(), - "ZF FORMAT: cap=off whir_cap=off fri=dp one_row=auto whir_folds=uniform4" + "ZF FORMAT: cap=auto whir_cap=auto fri=dp one_row=auto whir_folds=first6" ); } @@ -535,15 +650,40 @@ mod tests { 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 default format leaves options untouched. - let d = ZfFormat::DEFAULT.options(base.clone()); + // 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(&[(8, 20)]); + 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::DEFAULT + ..ZfFormat::LEGACY } .chain(chain); assert_eq!(c.format.cap, CapPolicy::Fixed(3)); @@ -562,15 +702,20 @@ mod tests { #[test] fn the_schedule_line_states_the_rounds() { assert_eq!( - ZfFormat::DEFAULT.whir_schedule_line(), + 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::DEFAULT + ..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] \ @@ -585,9 +730,28 @@ mod tests { #[test] fn the_production_chain_config_under_each_arm() { use crate::multilinear_prove::chain_config_under; - let today = chain_config_under(&ZfFormat::DEFAULT, &[(1, 25)]); - assert_eq!(today, crate::multilinear_prove::chain_config(&[(1, 25)])); + 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)]); @@ -604,49 +768,108 @@ mod tests { #[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 produce today's values. + // production constructors must stamp the MEASURED configuration. assert_eq!(*ZfFormat::global(), ZfFormat::DEFAULT); - assert!(crate::lfm::proof::aggregation_wrap_options().has_default_format()); - assert!(crate::lfm::proof::block_base_options().has_default_format()); + 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' (RULINGS 26). + 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, ChainFormat::DEFAULT); + assert_eq!(chain.format, ZfFormat::DEFAULT.chain_format()); assert_eq!(chain.log_folding, PRODUCTION_WHIR_LOG_FOLDING); } + /// ★ RULINGS 26 (RULINGS 11 amended): 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_entries_refuse_a_non_default_format() { - // RULINGS 11: the RV64 guest verifier stays default-only. + 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 process default is NOT legacy, so the presets cannot have + // inherited their format from it. + assert!(!ZfFormat::global().is_legacy()); + assert!(!ZfFormat::global().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::DEFAULT + ..ZfFormat::LEGACY }, ZfFormat { fri: FriMode::Dp, - ..ZfFormat::DEFAULT + ..ZfFormat::LEGACY }, ZfFormat { one_row: OneRowMode::On, - ..ZfFormat::DEFAULT + ..ZfFormat::LEGACY + }, + ZfFormat { + one_row: OneRowMode::Auto, + ..ZfFormat::LEGACY }, ] { - let opts = f.options(base.clone()); + refused(&f.options(base.clone())); + } + // The production STARK base options (the process default) are refused. + 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), ] { - let err = result.expect_err("a non-default format must be refused"); - assert!(format!("{err:?}").contains("default-format"), "{err:?}"); - } - } - // The default format gets past the guard (and fails on the empty blob). - for result in [ - crate::recursion::verify_and_attest_blob(&[], &base), - crate::recursion::verify_continuation_and_attest(&[], &base), - ] { - if let Err(err) = result { - assert!(!format!("{err:?}").contains("default-format"), "{err:?}"); + if let Err(err) = result { + assert!(!format!("{err:?}").contains("legacy-format"), "{err:?}"); + } } } } From cfbff6acc866474280ce4cda4063d8cdfca2b7c1 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 22:36:02 -0300 Subject: [PATCH 62/73] test(prover): the recursion-guest legacy test holds under any process format It asserted that the PROCESS format is not legacy, so the legacy A/B arm (every ZF knob at off) turned it red. It now checks ZfFormat::DEFAULT for that, and refuses block_base_options() only when the process format is not legacy (always, with no knob set). --- prover/src/zf_format.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/prover/src/zf_format.rs b/prover/src/zf_format.rs index 1d3ccc643..fc0e3b8ab 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -823,10 +823,10 @@ mod tests { crate::recursion::MIN_PROOF_OPTIONS.format, ProofFormat::LEGACY ); - // The process default is NOT legacy, so the presets cannot have + // The production default is NOT legacy, so the presets cannot have // inherited their format from it. - assert!(!ZfFormat::global().is_legacy()); - assert!(!ZfFormat::global().proof_format().is_legacy()); + assert!(!ZfFormat::DEFAULT.is_legacy()); + assert!(!ZfFormat::DEFAULT.proof_format().is_legacy()); let base = crate::recursion::Preset::Blowup4.options(); let refused = |opts: &ProofOptions| { @@ -859,8 +859,11 @@ mod tests { ] { refused(&f.options(base.clone())); } - // The production STARK base options (the process default) are refused. - refused(&crate::lfm::proof::block_base_options()); + // 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 [ From 0dd6341a927ad7f34e48add8ecb75ff954f55b44 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 22:36:50 -0300 Subject: [PATCH 63/73] feat(prover): one_row=auto joins the default format, provisionally (RULINGS 26) ZfFormat::DEFAULT gains S2 one_row=auto: per table, one-row openings where the shared cost function prices them cheaper, row pairs elsewhere. It is PROVISIONAL (pre-registered net positive on STARK, neutral on WHIR) and is its own commit so it reverts cleanly if the ds30-35 / wt72-77 arms disagree. LAMBDA_VM_ZF_ONE_ROW=0 keeps selecting row pairs. No pinned byte moves: the production RPX goldens' AIRs all resolve to row pairs under auto (now asserted), one-row bytes stay pinned by the (e) vectors and the VM device comparison, the static one-row twins exist at blowup 4 (the production blowup), and the LFM registry policy is read off the options each caller passes (a one-row format builds the roots at run time), not off the process format. Banner: cap=auto whir_cap=auto fri=dp one_row=auto whir_folds=first6. --- prover/src/tests/zf_rpx_golden_tests.rs | 24 ++++++++++++++++++++ prover/src/zf_format.rs | 30 +++++++++++++++++-------- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/prover/src/tests/zf_rpx_golden_tests.rs b/prover/src/tests/zf_rpx_golden_tests.rs index 599736c22..75cde5959 100644 --- a/prover/src/tests/zf_rpx_golden_tests.rs +++ b/prover/src/tests/zf_rpx_golden_tests.rs @@ -191,6 +191,13 @@ fn compute_goldens_at(d: ProofFormat) -> Vec<(String, String)> { } /// PRODUCTION-default pins, generated by `print_goldens` at the default flip. +/// +/// ⚠ Under `one_row=auto` (the provisional default, RULINGS 26) every case +/// here resolves to ROW PAIRS — these AIRs are narrow and short, where the +/// auto rule keeps pairs — so these bytes did not move when `one_row` flipped +/// from 0 to auto (asserted in `production_format_rpx_goldens_are_byte_identical`). +/// One-row proof bytes are pinned by the (e) vectors (`tests::zf_rpx_vectors`, +/// `stark::tests::zf_fri_vectors`) and the VM bytes device comparison. const PRODUCTION_GOLDENS: &[(&str, &str)] = &[ ( "simple_addition/rpx/rows16/blowup2", @@ -256,6 +263,23 @@ fn production_format_rpx_goldens_are_byte_identical() { "the production default is not the legacy format" ); let got = compute_goldens_at(f); + // What the pins cover: every case at row pairs, whatever `one_row` says. + for (rows, blowup) in [(16usize, 2u8), (64, 4)] { + let o = options(blowup, 2, 5, f); + let air = SimpleAdditionAIR::::new(&o); + assert!( + !stark::leaf_layout::table_leaf_layout(&air, rows).is_one_row(), + "simple_addition rows {rows}: the production pins assume row pairs" + ); + } + for (rows, blowup) in [(32usize, 4u8), (128, 2)] { + let o = options(blowup, 1, 7, f); + let air = LogReadOnlyRAP::::new(&o); + assert!( + !stark::leaf_layout::table_leaf_layout(&air, rows).is_one_row(), + "logup rows {rows}: the production pins assume row pairs" + ); + } 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); diff --git a/prover/src/zf_format.rs b/prover/src/zf_format.rs index fc0e3b8ab..2d3c69632 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -9,9 +9,11 @@ //! ``` //! //! ★ Every unset knob is [`ZfFormat::DEFAULT`], the MEASURED configuration -//! (RULINGS 26): `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`, +//! (RULINGS 26): `cap=auto whir_cap=auto fri=dp one_row=auto whir_folds=first6`. +//! Each lever but `one_row=auto` was measured net positive on block runs +//! before it became the default; `one_row=auto` is the default PROVISIONALLY +//! (pre-registered net positive on STARK, neutral on WHIR; its own commit, so +//! it reverts cleanly if the ds30–35 / wt72–77 arms disagree). 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 pre-campaign format, byte for byte — //! for rollback and for A/B arms. The crypto crates' own defaults @@ -45,7 +47,7 @@ //! flips its `*_IMPLEMENTED` constant 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`. +//! `ZF FORMAT: cap=auto whir_cap=auto fri=dp one_row=auto whir_folds=first6`. //! Its absence in a log is then a fact about the run, not an ambiguity. use std::sync::OnceLock; @@ -98,13 +100,15 @@ impl ZfFormat { /// configuration (RULINGS 26). 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. Security parameters (queries, grinding, blowup) are the - /// legacy ones: no lever touches them. + /// block run. S2 `one_row=auto` is the default PROVISIONALLY (RULINGS 26: + /// pre-registered net positive on STARK, neutral on WHIR, pending the + /// ds30–35 / wt72–77 arms). 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, + one_row: OneRowMode::Auto, whir_folds: WhirFolds::First(DEFAULT_WHIR_FIRST_FOLD), }; @@ -369,13 +373,13 @@ mod tests { cap: CapPolicy::Auto, whir_cap: CapPolicy::Auto, fri: FriMode::Dp, - one_row: OneRowMode::Off, + one_row: OneRowMode::Auto, 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" + "ZF FORMAT: cap=auto whir_cap=auto fri=dp one_row=auto whir_folds=first6" ); assert!(!f.is_legacy()); assert!(f.unimplemented_levers().is_empty()); @@ -434,6 +438,14 @@ mod tests { ..ZfFormat::DEFAULT }, ), + ( + ENV_ONE_ROW, + "0", + ZfFormat { + one_row: OneRowMode::Off, + ..ZfFormat::DEFAULT + }, + ), ( ENV_WHIR_FOLDS, "uniform4", From 5ed2f157a59cd0cf58e082bdf475baad27aa4eed Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 22:40:17 -0300 Subject: [PATCH 64/73] test(prover): the sh1 recount states the default's rounds without reading the process format It built the production chain through chain_config, so the legacy A/B arm (every ZF knob off) read uniform4's seven rounds where the assert expects the default's six. It now builds ZfFormat::DEFAULT's config explicitly. --- prover/src/lfm/whir_epoch_program_tests.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/prover/src/lfm/whir_epoch_program_tests.rs b/prover/src/lfm/whir_epoch_program_tests.rs index ecedd6a9b..a52bcd0ff 100644 --- a/prover/src/lfm/whir_epoch_program_tests.rs +++ b/prover/src/lfm/whir_epoch_program_tests.rs @@ -156,7 +156,10 @@ fn the_production_epoch_recount() { let config = crate::multilinear_prove::chain_config_under(&crate::zf_format::ZfFormat::LEGACY, &shapes); { - let production = chain_config(&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() From 6a4a24468f6b53fe8bbf4b5a9d453d4dcddaa511 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 23:00:09 -0300 Subject: [PATCH 65/73] fix(prover): the Phase-A replay absorbs each preprocessed root at its leaf layout Under one-row openings (S2) the STARK prover absorbs a preprocessed table's root of that table's leaf layout (`precomputed_commitment_for(layout)`) before sampling the shared LogUp challenges z and alpha, and the STARK verifier does the same. `replay_transcript_phase_a_view`, which the LFM verify path and the VM/continuation commit-bus balance use to recover z and alpha, still absorbed the row-pair root (`precomputed_commitment()`) for every table. For a one-row preprocessed table the replay diverged, the expected public balance was computed at the wrong z and alpha, and `multi_verify_views` rejected an honest proof whenever that balance depended on them: every LFM proof (the published words) and every VM proof with public output. This is why the block tree's level-0 wraps failed `verify_against_artifacts` at one_row=auto (wt73, ds31) while the base epochs, which publish nothing, verified. The replay now resolves the layout exactly as the prover and verifier do (`table_leaf_layout(air, proof.trace_length())`) and absorbs that layout's root; a table with no root for its layout returns None and the caller rejects (RULINGS 14), so the replay returns Option<(z, alpha)>. Verifier-side only: no proof byte moves. At the default format every layout is row pairs and the absorbed root is byte-identical to before. --- prover/src/lfm/epoch_verify.rs | 3 ++- prover/src/lfm/logup_tests.rs | 3 ++- prover/src/lfm/per_table_aggregator.rs | 3 ++- prover/src/lfm/proof.rs | 4 +++- prover/src/lib.rs | 23 +++++++++++++++++++---- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/prover/src/lfm/epoch_verify.rs b/prover/src/lfm/epoch_verify.rs index 18565fe96..6a25dd324 100644 --- a/prover/src/lfm/epoch_verify.rs +++ b/prover/src/lfm/epoch_verify.rs @@ -204,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. 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/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/proof.rs b/prover/src/lfm/proof.rs index 7f0811d6e..cd1462682 100644 --- a/prover/src/lfm/proof.rs +++ b/prover/src/lfm/proof.rs @@ -523,7 +523,9 @@ fn verify_against_chunked_with( // 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; }; diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 2da4ca4f7..874a6a283 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -1379,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 (RULINGS 14): 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 @@ -1407,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) } From 91764db5d103d1812768e99a5d646204b688725e Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 23:00:09 -0300 Subject: [PATCH 66/73] test(prover): one-row Phase-A replay regressions (LFM wrap options at auto, VM with public output) Two round trips that fail before the replay fix and pass after: - an LFM proof (TrivialV0) at the wrap's options (blowup 4, terminal 2^8, 128-bit queries) under one_row=auto: layouts mix within the proof and 8 preprocessed chips go one-row (asserted, so the test keeps exercising the bug); verified through verify_against_artifacts, the call the tree harness makes, and through lfm_verify; a moved public word still rejects. - a VM proof with public output (test_commit_4) at one_row=1; a moved output byte still rejects. --- prover/src/tests/zf_vm_one_row_tests.rs | 125 ++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/prover/src/tests/zf_vm_one_row_tests.rs b/prover/src/tests/zf_vm_one_row_tests.rs index 051cd34fd..d851a9ac7 100644 --- a/prover/src/tests/zf_vm_one_row_tests.rs +++ b/prover/src/tests/zf_vm_one_row_tests.rs @@ -10,6 +10,11 @@ //! - 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). +//! - Lane I-FIX-S2's 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`. //! - D2 (lane I-S2-D): the one-row VM proof's BYTES at grinding 0, written to //! `ZF_S2_PROOF_DIR` by a CPU build and by a cuda build; the box compares the //! two files byte for byte (the device-proved one-row VM proof equals the CPU @@ -161,6 +166,126 @@ fn an_lfm_proof_round_trips_at_one_row() { ); } +/// ★ REGRESSION (lane I-FIX-S2): 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" + ); +} + /// D2 (lane I-S2-D): the one-row VM proof bytes (grinding 0, so the proof is a /// function of the ELF and the format alone), written as /// `$ZF_S2_PROOF_DIR/{cpu|cuda}_{format}.rkyv`. The box runs this once in a From 7c9b577614689688d61e6573402760563b24d06a Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 23:08:34 -0300 Subject: [PATCH 67/73] feat(stark): 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 (d) and S2 (e) proof vectors, had none, so the four device full-proof byte tests (`proved_vectors_equal_the_cpu_bytes`, `proved_one_row_vectors_equal_the_cpu_bytes` and their RPX twins) panicked in the trait default before comparing a byte. The program is captured once (OnceLock) from the same `LogReadOnlyRAPConstraints` body the CPU folders run, so the device composes the same polynomials and no vector byte can move; the CPU prover never reads the program. New CPU tests pin folder == interpreted program == lowered device program (host model of the kernel) on random frames. The four device tests now also assert that every proof composed on the device (`gpu_composition_calls` moves once per proof), next to their existing FRI and one-row tree counters, so a host composition fallback fails them. --- .../src/examples/read_only_memory_logup.rs | 22 ++- .../src/tests/log_read_only_program_tests.rs | 139 ++++++++++++++++++ crypto/stark/src/tests/mod.rs | 1 + crypto/stark/src/tests/zf_fri_device_tests.rs | 10 +- crypto/stark/src/tests/zf_s2_device_tests.rs | 10 +- prover/src/tests/zf_rpx_device_tests.rs | 20 ++- 6 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 crypto/stark/src/tests/log_read_only_program_tests.rs diff --git a/crypto/stark/src/examples/read_only_memory_logup.rs b/crypto/stark/src/examples/read_only_memory_logup.rs index 9068e7276..2b921cf99 100644 --- a/crypto/stark/src/examples/read_only_memory_logup.rs +++ b/crypto/stark/src/examples/read_only_memory_logup.rs @@ -3,15 +3,17 @@ //! use std::marker::PhantomData; +use std::sync::OnceLock; use crate::{ 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, }, }, + constraint_ir::ConstraintProgram, context::AirContext, proof::options::ProofOptions, trace::TraceTable, @@ -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/tests/log_read_only_program_tests.rs b/crypto/stark/src/tests/log_read_only_program_tests.rs new file mode 100644 index 000000000..7874abac7 --- /dev/null +++ b/crypto/stark/src/tests/log_read_only_program_tests.rs @@ -0,0 +1,139 @@ +//! `LogReadOnlyRAP` carries a constraint program (I-FIX-D2). +//! +//! 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/mod.rs b/crypto/stark/src/tests/mod.rs index d8db7083a..de94682af 100644 --- a/crypto/stark/src/tests/mod.rs +++ b/crypto/stark/src/tests/mod.rs @@ -12,6 +12,7 @@ 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; diff --git a/crypto/stark/src/tests/zf_fri_device_tests.rs b/crypto/stark/src/tests/zf_fri_device_tests.rs index 63f4e09c7..2a94509cd 100644 --- a/crypto/stark/src/tests/zf_fri_device_tests.rs +++ b/crypto/stark/src/tests/zf_fri_device_tests.rs @@ -138,11 +138,13 @@ fn parity_legacy_encoding_blake3() { 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", + "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 @@ -152,6 +154,12 @@ fn proved_vectors_equal_the_cpu_bytes() { 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, + // I-FIX-D2); 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(), diff --git a/crypto/stark/src/tests/zf_s2_device_tests.rs b/crypto/stark/src/tests/zf_s2_device_tests.rs index a8c2c7373..b18d22dba 100644 --- a/crypto/stark/src/tests/zf_s2_device_tests.rs +++ b/crypto/stark/src/tests/zf_s2_device_tests.rs @@ -99,12 +99,14 @@ 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", + "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. @@ -118,6 +120,12 @@ fn proved_one_row_vectors_equal_the_cpu_bytes() { "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, + // I-FIX-D2); 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(), diff --git a/prover/src/tests/zf_rpx_device_tests.rs b/prover/src/tests/zf_rpx_device_tests.rs index 3a0534b2f..f675e3e27 100644 --- a/prover/src/tests/zf_rpx_device_tests.rs +++ b/prover/src/tests/zf_rpx_device_tests.rs @@ -67,10 +67,12 @@ fn parity_legacy_encoding_rpx() { 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", + "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 @@ -80,6 +82,12 @@ fn proved_rpx_vectors_equal_the_cpu_bytes() { 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, + // I-FIX-D2); 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(), @@ -135,11 +143,13 @@ 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", + "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); @@ -152,6 +162,12 @@ fn proved_rpx_one_row_vectors_equal_the_cpu_bytes() { "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, + // I-FIX-D2); 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(), From c5b91a0a2a8f4ec12ff3ee0342ed606a4b2b5023 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 23:08:52 -0300 Subject: [PATCH 68/73] test(prover): a valid device-comparison oracle: LFM proof bytes, proved twice per process `one_row_vm_proof_bytes_for_the_device_comparison` compared a CPU-build and a cuda-build RV64 VM proof of `test_mul_8`, on the premise that at grinding 0 the proof is a function of the ELF and the format. It is not: six base-table builders dedup through a std HashMap (RandomState) and lay rows out in iteration order, so the main roots and the whole transcript change per process; the lead's control showed the same build differing from itself in ~80% of the bytes. The test is deleted (pinning the VM row order would move every proof and is not this lane's call). The replacement, `zf_lfm_bytes_tests::lfm_proof_bytes_for_the_device_comparison` (ignored, box), proves the `TrivialV0` LFM machine program (public output, so the balance depends on z, alpha) at blowup 4, 128-bit queries, grinding 0, under `legacy`, `one_row_1` and `production` (cap auto, fri dp, one_row auto), proves each TWICE in the same process and asserts the two byte strings equal, writes `$ZF_S2_PROOF_DIR/{cpu,cuda}_.rkyv`, then verifies. Under cuda the `one_row_1` arm must build one-row trees and take the one-row FRI commit on the device; the legacy arm must do neither. --- prover/src/tests/mod.rs | 2 + prover/src/tests/zf_lfm_bytes_tests.rs | 202 ++++++++++++++++++++++++ prover/src/tests/zf_vm_one_row_tests.rs | 92 ----------- 3 files changed, 204 insertions(+), 92 deletions(-) create mode 100644 prover/src/tests/zf_lfm_bytes_tests.rs diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index 5791b4904..1a9bc513a 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -135,6 +135,8 @@ pub mod zf_air_cache_tests; #[cfg(all(test, feature = "cuda"))] pub mod zf_rpx_device_tests; #[cfg(test)] +pub mod zf_lfm_bytes_tests; +#[cfg(test)] pub mod zf_rpx_golden_tests; #[cfg(test)] pub mod zf_rpx_vectors; 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..e1eec2dd6 --- /dev/null +++ b/prover/src/tests/zf_lfm_bytes_tests.rs @@ -0,0 +1,202 @@ +//! D2 device parity at the proof level, on a VALID oracle (lane I-FIX-D2). +//! +//! The one-row RV64 VM bytes test this replaces compared a CPU-build proof with +//! a cuda-build proof of `test_mul_8`, but an RV64 VM proof 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 lead's control: 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 `legacy` (every lever +//! off), `one_row_1` (legacy + one row on every chip) and `production` (the +//! measured configuration of RULINGS 26: 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 three formats compared across builds. +fn formats() -> [(&'static str, ProofFormat); 3] { + let legacy = ProofFormat { + merkle_cap: crypto::merkle_tree::cap::CapPolicy::Off, + fri_mode: FriMode::Pair, + one_row: OneRowMode::Off, + fri_schedule_override: None, + }; + [ + ("legacy", legacy), + ( + "one_row_1", + ProofFormat { + one_row: OneRowMode::On, + ..legacy + }, + ), + ( + "production", + ProofFormat { + merkle_cap: crypto::merkle_tree::cap::CapPolicy::Auto, + fri_mode: FriMode::Dp, + one_row: OneRowMode::Auto, + fri_schedule_override: None, + }, + ), + ] +} + +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) = ( + stark::gpu_lde::gpu_one_row_trees(), + stark::gpu_lde::gpu_one_row_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) = ( + stark::gpu_lde::gpu_one_row_trees() - trees0, + stark::gpu_lde::gpu_one_row_fri_calls() - 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}: {trees} one-row device trees, {fri} one-row device FRI \ + commits (two proofs)" + ); + 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_vm_one_row_tests.rs b/prover/src/tests/zf_vm_one_row_tests.rs index 051cd34fd..269294bde 100644 --- a/prover/src/tests/zf_vm_one_row_tests.rs +++ b/prover/src/tests/zf_vm_one_row_tests.rs @@ -160,95 +160,3 @@ fn an_lfm_proof_round_trips_at_one_row() { "an honest one-row LFM proof must verify" ); } - -/// D2 (lane I-S2-D): the one-row VM proof bytes (grinding 0, so the proof is a -/// function of the ELF and the format alone), written as -/// `$ZF_S2_PROOF_DIR/{cpu|cuda}_{format}.rkyv`. The box runs this once in a -/// CPU build and once in a cuda build and `cmp`s the files: equal bytes = the -/// device-proved one-row proof is the CPU proof. 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 produce equal bytes, so the -/// counters are what make the comparison mean "device"), and the run prints -/// `ZF S2 DEVMEM` — the largest one-row tree the device was asked for, its -/// row-pair twin, the device fallbacks and the reserved high-water mark. -#[test] -#[ignore = "box: set ZF_S2_PROOF_DIR, run in a CPU build and a cuda build, then cmp the files"] -fn one_row_vm_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 elf_bytes = crate::test_utils::asm_elf_bytes("test_mul_8"); - for (name, one_row, fri_mode) in [ - ("one_row_1", OneRowMode::On, FriMode::Pair), - ("one_row_auto_dp", OneRowMode::Auto, FriMode::Dp), - ] { - let mut o = opts(4, one_row, fri_mode); - o.grinding_factor = 0; - #[cfg(feature = "cuda")] - let (trees0, fri0) = ( - stark::gpu_lde::gpu_one_row_trees(), - stark::gpu_lde::gpu_one_row_fri_calls(), - ); - let vm_proof = crate::prove_with_options(&elf_bytes, &o, &Default::default()) - .expect("the fixture must prove"); - assert!( - crate::verify_with_options(&vm_proof, &elf_bytes, &o, None, None) - .expect("honest verify must not error"), - "{name}: an honest one-row VM proof must verify" - ); - let bytes = rkyv::to_bytes::(&vm_proof) - .expect("rkyv") - .to_vec(); - let path = std::path::Path::new(&dir).join(format!("{build}_{name}.rkyv")); - std::fs::write(&path, &bytes).expect("write the proof bytes"); - 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 VMBYTES {build} {name}: {} bytes, {one_row_tables} of {} tables one-row -> {}", - bytes.len(), - vm_proof.proof.proofs.len(), - path.display() - ); - #[cfg(feature = "cuda")] - { - let trees = stark::gpu_lde::gpu_one_row_trees() - trees0; - let fri = stark::gpu_lde::gpu_one_row_fri_calls() - fri0; - println!( - "ZF S2 DEVICE {name}: {trees} one-row device trees, {fri} one-row device FRI commits" - ); - if 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" - ); - } - let peak = stark::gpu_lde::gpu_one_row_tree_peak_bytes(); - // A one-row tree over L rows is (2L - 1) nodes; its row-pair twin - // over the same rows (L - 1). - let rows = (peak / 32).div_ceil(2); - let twin = rows.saturating_sub(1) * 32; - println!( - "ZF S2 DEVMEM {name}: largest one-row tree {peak} B ({:.1} MiB) over {rows} LDE rows, \ - row-pair twin {twin} B ({:.1} MiB); device fallbacks {}; reserved high water {} B", - peak as f64 / (1u64 << 20) as f64, - twin as f64 / (1u64 << 20) as f64, - math_cuda::device::device_fallbacks(), - math_cuda::device::reserved_high_water() - ); - } - } -} From 03fe7270736a5fee0b5c20f6a8a7b6c75419b6c0 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 23:19:14 -0300 Subject: [PATCH 69/73] style(stark): make fmt on the LogReadOnlyRAP program and its tests --- .../src/examples/read_only_memory_logup.rs | 2 +- .../src/tests/log_read_only_program_tests.rs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crypto/stark/src/examples/read_only_memory_logup.rs b/crypto/stark/src/examples/read_only_memory_logup.rs index 2b921cf99..ef4696201 100644 --- a/crypto/stark/src/examples/read_only_memory_logup.rs +++ b/crypto/stark/src/examples/read_only_memory_logup.rs @@ -6,6 +6,7 @@ use std::marker::PhantomData; use std::sync::OnceLock; use crate::{ + constraint_ir::ConstraintProgram, constraints::{ boundary::{BoundaryConstraint, BoundaryConstraints}, builder::{ @@ -13,7 +14,6 @@ use crate::{ num_base_from_meta, run_transition_prover, run_transition_verifier, }, }, - constraint_ir::ConstraintProgram, context::AirContext, proof::options::ProofOptions, trace::TraceTable, diff --git a/crypto/stark/src/tests/log_read_only_program_tests.rs b/crypto/stark/src/tests/log_read_only_program_tests.rs index 7874abac7..004f9bd5e 100644 --- a/crypto/stark/src/tests/log_read_only_program_tests.rs +++ b/crypto/stark/src/tests/log_read_only_program_tests.rs @@ -107,7 +107,11 @@ fn the_log_read_only_program_equals_the_prover_folder_and_the_device_model() { 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}"); + 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 @@ -130,10 +134,18 @@ fn the_log_read_only_program_equals_the_prover_folder_and_the_device_model() { &mut ext_dev, ); for c in 0..nb { - assert_eq!(Felt::from(base_dev[c]), folder_base[c], "device base {c}, trial {trial}"); + 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}"); + assert_eq!( + from_limbs(ext_dev[c]), + folder_ext[c], + "device ext {c}, trial {trial}" + ); } } } From 39dea55520f66f0d26cfa3c6cd4fdcaf3aa8aae0 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 23:19:14 -0300 Subject: [PATCH 70/73] test(prover): the LFM bytes oracle asserts a device FRI commit in every arm Under cuda each format must commit FRI on the device for its two large chips (2^16 and 2^20 rows, above the default device floor), so a host proof cannot pass as a device proof in the cross-build cmp; the counter is printed on the `ZF LFM DEVICE` line. Also make fmt. --- prover/src/tests/mod.rs | 4 ++-- prover/src/tests/zf_lfm_bytes_tests.rs | 27 ++++++++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index 1a9bc513a..58690e97d 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -132,10 +132,10 @@ pub mod whir_hash_tests; pub mod whir_identity_tests; #[cfg(test)] pub mod zf_air_cache_tests; -#[cfg(all(test, feature = "cuda"))] -pub mod zf_rpx_device_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)] diff --git a/prover/src/tests/zf_lfm_bytes_tests.rs b/prover/src/tests/zf_lfm_bytes_tests.rs index e1eec2dd6..9a09517d6 100644 --- a/prover/src/tests/zf_lfm_bytes_tests.rs +++ b/prover/src/tests/zf_lfm_bytes_tests.rs @@ -100,13 +100,17 @@ fn lfm_proof_bytes_for_the_device_comparison() { 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"), + OneRowMode::Off => assert!( + artifacts.one_row_roots.is_none(), + "{name}: no one-row roots" + ), _ => {} } #[cfg(feature = "cuda")] - let (trees0, fri0) = ( + 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; @@ -125,9 +129,10 @@ fn lfm_proof_bytes_for_the_device_comparison() { last = Some(proved); } #[cfg(feature = "cuda")] - let (trees, fri) = ( + 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!( @@ -176,8 +181,14 @@ fn lfm_proof_bytes_for_the_device_comparison() { #[cfg(feature = "cuda")] { println!( - "ZF LFM DEVICE {name}: {trees} one-row device trees, {fri} one-row device FRI \ - commits (two proofs)" + "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!( @@ -188,7 +199,11 @@ fn lfm_proof_bytes_for_the_device_comparison() { ); } if format.one_row == OneRowMode::Off { - assert_eq!(trees + fri, 0, "{name}: no one-row device work without one row"); + 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 {}; \ From cdf0238f17c2afbbf794427a2cd21a0d334f2fb0 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 24 Sep 2026 23:42:49 -0300 Subject: [PATCH 71/73] Revert "feat(prover): one_row=auto joins the default format, provisionally (RULINGS 26)" This reverts commit 0dd6341a927ad7f34e48add8ecb75ff954f55b44 (I-FLIP commit B). The DROP-B variant of candidate-f, prepared for the lead's decision on the provisional one_row=auto default (RULINGS 26): the default format keeps commit A (cap=auto whir_cap=auto fri=dp whir_folds=first6) with one_row=0. LAMBDA_VM_ZF_ONE_ROW=auto still selects S2. Commits cfbff6acc and 5ed2f157a belong with A and stay. --- prover/src/tests/zf_rpx_golden_tests.rs | 24 -------------------- prover/src/zf_format.rs | 30 ++++++++----------------- 2 files changed, 9 insertions(+), 45 deletions(-) diff --git a/prover/src/tests/zf_rpx_golden_tests.rs b/prover/src/tests/zf_rpx_golden_tests.rs index 75cde5959..599736c22 100644 --- a/prover/src/tests/zf_rpx_golden_tests.rs +++ b/prover/src/tests/zf_rpx_golden_tests.rs @@ -191,13 +191,6 @@ fn compute_goldens_at(d: ProofFormat) -> Vec<(String, String)> { } /// PRODUCTION-default pins, generated by `print_goldens` at the default flip. -/// -/// ⚠ Under `one_row=auto` (the provisional default, RULINGS 26) every case -/// here resolves to ROW PAIRS — these AIRs are narrow and short, where the -/// auto rule keeps pairs — so these bytes did not move when `one_row` flipped -/// from 0 to auto (asserted in `production_format_rpx_goldens_are_byte_identical`). -/// One-row proof bytes are pinned by the (e) vectors (`tests::zf_rpx_vectors`, -/// `stark::tests::zf_fri_vectors`) and the VM bytes device comparison. const PRODUCTION_GOLDENS: &[(&str, &str)] = &[ ( "simple_addition/rpx/rows16/blowup2", @@ -263,23 +256,6 @@ fn production_format_rpx_goldens_are_byte_identical() { "the production default is not the legacy format" ); let got = compute_goldens_at(f); - // What the pins cover: every case at row pairs, whatever `one_row` says. - for (rows, blowup) in [(16usize, 2u8), (64, 4)] { - let o = options(blowup, 2, 5, f); - let air = SimpleAdditionAIR::::new(&o); - assert!( - !stark::leaf_layout::table_leaf_layout(&air, rows).is_one_row(), - "simple_addition rows {rows}: the production pins assume row pairs" - ); - } - for (rows, blowup) in [(32usize, 4u8), (128, 2)] { - let o = options(blowup, 1, 7, f); - let air = LogReadOnlyRAP::::new(&o); - assert!( - !stark::leaf_layout::table_leaf_layout(&air, rows).is_one_row(), - "logup rows {rows}: the production pins assume row pairs" - ); - } 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); diff --git a/prover/src/zf_format.rs b/prover/src/zf_format.rs index 2d3c69632..fc0e3b8ab 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -9,11 +9,9 @@ //! ``` //! //! ★ Every unset knob is [`ZfFormat::DEFAULT`], the MEASURED configuration -//! (RULINGS 26): `cap=auto whir_cap=auto fri=dp one_row=auto whir_folds=first6`. -//! Each lever but `one_row=auto` was measured net positive on block runs -//! before it became the default; `one_row=auto` is the default PROVISIONALLY -//! (pre-registered net positive on STARK, neutral on WHIR; its own commit, so -//! it reverts cleanly if the ds30–35 / wt72–77 arms disagree). Every knob keeps its OFF spelling (`cap=off`, `whir_cap=off`, +//! (RULINGS 26): `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 pre-campaign format, byte for byte — //! for rollback and for A/B arms. The crypto crates' own defaults @@ -47,7 +45,7 @@ //! flips its `*_IMPLEMENTED` constant 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=auto whir_folds=first6`. +//! `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; @@ -100,15 +98,13 @@ impl ZfFormat { /// configuration (RULINGS 26). 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. S2 `one_row=auto` is the default PROVISIONALLY (RULINGS 26: - /// pre-registered net positive on STARK, neutral on WHIR, pending the - /// ds30–35 / wt72–77 arms). Security parameters (queries, grinding, - /// blowup) are the legacy ones: no lever touches them. + /// block run. 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::Auto, + one_row: OneRowMode::Off, whir_folds: WhirFolds::First(DEFAULT_WHIR_FIRST_FOLD), }; @@ -373,13 +369,13 @@ mod tests { cap: CapPolicy::Auto, whir_cap: CapPolicy::Auto, fri: FriMode::Dp, - one_row: OneRowMode::Auto, + 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=auto whir_folds=first6" + "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()); @@ -438,14 +434,6 @@ mod tests { ..ZfFormat::DEFAULT }, ), - ( - ENV_ONE_ROW, - "0", - ZfFormat { - one_row: OneRowMode::Off, - ..ZfFormat::DEFAULT - }, - ), ( ENV_WHIR_FOLDS, "uniform4", From 65df270f0affcc9a40145b6b0b896a082689b5f5 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 25 Sep 2026 00:32:25 -0300 Subject: [PATCH 72/73] test(prover): the LFM bytes oracle proves the default format and all levers as separate arms The `production` arm proved cap auto + fri dp + one_row auto, which is no longer the prover's default format (one_row stays off by default). The oracle now has four arms: - legacy: every lever off (bytes unchanged); - one_row_1: legacy + one row on every chip (bytes unchanged); - production: the STARK part of ZfFormat::DEFAULT (cap auto, fri dp, one_row off), asserted equal to what the default stamps so the arm cannot drift; - all_levers: cap auto + fri dp + one_row auto, the former `production` arm, with the same bytes. Every existing assertion is kept: twice-equal bytes per process, verify, and the cuda counters (one-row device work only where one_row is on). --- prover/src/tests/zf_lfm_bytes_tests.rs | 43 +++++++++++++++++--------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/prover/src/tests/zf_lfm_bytes_tests.rs b/prover/src/tests/zf_lfm_bytes_tests.rs index 9a09517d6..f9767e349 100644 --- a/prover/src/tests/zf_lfm_bytes_tests.rs +++ b/prover/src/tests/zf_lfm_bytes_tests.rs @@ -1,12 +1,11 @@ -//! D2 device parity at the proof level, on a VALID oracle (lane I-FIX-D2). +//! Device parity at the proof level, on an LFM machine proof. //! -//! The one-row RV64 VM bytes test this replaces compared a CPU-build proof with -//! a cuda-build proof of `test_mul_8`, but an RV64 VM proof is not a function -//! of the ELF and the format alone: six base-table builders dedup through a std +//! 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 lead's control: the same build differs from itself). A cross-build -//! `cmp` of such bytes means nothing. +//! (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 @@ -17,9 +16,13 @@ //! 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 `legacy` (every lever -//! off), `one_row_1` (legacy + one row on every chip) and `production` (the -//! measured configuration of RULINGS 26: cap auto, fri dp, one_row auto). +//! 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 @@ -34,14 +37,25 @@ use crate::lfm::registry::{LfmProgramKind, build_artifacts}; use crate::lfm::word::LfmWord; use crate::tables::types::FE; -/// The three formats compared across builds. -fn formats() -> [(&'static str, ProofFormat); 3] { +/// 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), ( @@ -51,13 +65,12 @@ fn formats() -> [(&'static str, ProofFormat); 3] { ..legacy }, ), + ("production", production), ( - "production", + "all_levers", ProofFormat { - merkle_cap: crypto::merkle_tree::cap::CapPolicy::Auto, - fri_mode: FriMode::Dp, one_row: OneRowMode::Auto, - fri_schedule_override: None, + ..production }, ), ] From d8ffc07027c4dc397a16eece7ab64e56c581438a Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 25 Sep 2026 00:41:35 -0300 Subject: [PATCH 73/73] docs: make the proof-format comments self-contained Comments only. Every comment line added since the proof-format work began now states its reason in place instead of pointing to material outside the repository (design notes, review findings, rulings, run tags, work-lane names). Soundness reasons are written out where they apply: a one-row layout with no preprocessed root is a hard miss, never a recompute; exact path lengths keep a leaf hash from being compared with an internal node; the RV64 recursion guest verifies the legacy format only. The ZfFormat::DEFAULT doc now says why one_row stays off: in ABBA block runs it costs +3.2 s on the WHIR pipeline 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. Unchanged because they are code, not comments: the test name the_production_shape_reproduces_the_campaigns_permutation_count (and its two doc links) and one assertion message in whir_chain_tests.rs. --- crypto/crypto/src/merkle_tree/cap.rs | 16 +++---- crypto/math-cuda/kernels/keccak.cu | 4 +- crypto/math-cuda/src/fri.rs | 2 +- crypto/math-cuda/src/merkle.rs | 2 +- crypto/math-cuda/tests/fri_group_tree.rs | 2 +- crypto/math-cuda/tests/merkle_cap.rs | 2 +- crypto/multilinear/src/whir_cap_tests.rs | 6 +-- crypto/multilinear/src/whir_chain.rs | 19 ++++---- crypto/multilinear/src/whir_commit.rs | 2 +- crypto/multilinear/src/whir_round.rs | 8 ++-- crypto/stark/src/device_set.rs | 4 +- crypto/stark/src/fri/capture.rs | 4 +- crypto/stark/src/fri/device_parity.rs | 6 +-- crypto/stark/src/fri/group.rs | 2 +- crypto/stark/src/fri/mod.rs | 4 +- crypto/stark/src/fri/schedule.rs | 8 ++-- crypto/stark/src/fri/vectors.rs | 12 ++--- crypto/stark/src/gpu_lde.rs | 8 ++-- crypto/stark/src/leaf_layout.rs | 16 +++---- crypto/stark/src/lookup.rs | 2 +- crypto/stark/src/merkle_caps.rs | 6 +-- crypto/stark/src/proof/options.rs | 36 +++++++------- crypto/stark/src/prover.rs | 13 +++-- crypto/stark/src/s2_device_parity.rs | 3 +- .../stark/src/tests/cap_fri_matrix_tests.rs | 9 ++-- crypto/stark/src/tests/fri_group_tests.rs | 8 ++-- crypto/stark/src/tests/fri_schedule_tests.rs | 45 ++++++++--------- .../src/tests/log_read_only_program_tests.rs | 2 +- crypto/stark/src/tests/merkle_cap_tests.rs | 14 +++--- crypto/stark/src/tests/one_row_tests.rs | 31 ++++++------ crypto/stark/src/tests/opening_width_tests.rs | 2 +- crypto/stark/src/tests/path_length_tests.rs | 5 +- crypto/stark/src/tests/zf_fri_device_tests.rs | 11 ++--- crypto/stark/src/tests/zf_fri_vectors.rs | 2 +- crypto/stark/src/tests/zf_golden_tests.rs | 7 ++- crypto/stark/src/tests/zf_s2_device_tests.rs | 8 ++-- crypto/stark/src/traits.rs | 2 +- crypto/stark/src/verifier.rs | 18 +++---- crypto/stark/tests/vectors/zf_fri/README.md | 8 ++-- prover/src/lfm/airs.rs | 2 +- prover/src/lfm/commit.rs | 4 +- prover/src/lfm/epoch.rs | 8 ++-- prover/src/lfm/epoch_tests.rs | 4 +- prover/src/lfm/epoch_verify.rs | 4 +- prover/src/lfm/epoch_verify_tests.rs | 12 ++--- prover/src/lfm/fri.rs | 30 ++++++------ prover/src/lfm/fri_group_tests.rs | 26 +++++----- prover/src/lfm/fri_tests.rs | 4 +- prover/src/lfm/merkle_cap.rs | 9 ++-- prover/src/lfm/one_row_guest_tests.rs | 16 +++---- prover/src/lfm/one_row_tests.rs | 10 ++-- prover/src/lfm/proof.rs | 6 +-- prover/src/lfm/registry.rs | 6 +-- prover/src/lfm/sub_proof.rs | 10 ++-- prover/src/lfm/whir_chain_tests.rs | 24 +++++----- prover/src/lfm/whir_epoch_program_tests.rs | 2 +- prover/src/lfm/whir_open_tests.rs | 2 +- prover/src/lfm/whir_statement.rs | 2 +- prover/src/lib.rs | 2 +- prover/src/recursion.rs | 8 ++-- prover/src/tables/bitwise.rs | 4 +- prover/src/tables/keccak_rc.rs | 4 +- prover/src/tables/mod.rs | 4 +- prover/src/tables/page.rs | 6 +-- prover/src/tests/multilinear_bench_tests.rs | 8 ++-- prover/src/tests/static_commitments_tests.rs | 4 +- prover/src/tests/transcript_counts.rs | 2 +- prover/src/tests/zf_air_cache_tests.rs | 25 +++++----- prover/src/tests/zf_rpx_device_tests.rs | 14 +++--- prover/src/tests/zf_rpx_golden_tests.rs | 18 +++---- prover/src/tests/zf_rpx_vectors.rs | 2 +- prover/src/tests/zf_vm_one_row_tests.rs | 8 ++-- prover/src/zf_format.rs | 48 ++++++++++--------- prover/tests/merkle_cap_vm.rs | 2 +- 74 files changed, 345 insertions(+), 354 deletions(-) diff --git a/crypto/crypto/src/merkle_tree/cap.rs b/crypto/crypto/src/merkle_tree/cap.rs index 43c21daf0..b23cb3669 100644 --- a/crypto/crypto/src/merkle_tree/cap.rs +++ b/crypto/crypto/src/merkle_tree/cap.rs @@ -403,7 +403,7 @@ impl CapPolicy { /// 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 RULINGS 1's table, stated directly — 3 for a tree opened at + /// `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. @@ -433,11 +433,11 @@ impl CapPolicy { } /// `Auto` gives a height-3 cap to a tree opened at least this many times -/// (RULINGS 1). ⚠ A FORMAT CONSTANT, like [`AUTO_WEIGHTS`]. +/// ⚠ 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`] (RULINGS 1). ⚠ A FORMAT CONSTANT. +/// fewer than [`AUTO_CAP3_MIN_OPENINGS`]. ⚠ A FORMAT CONSTANT. pub const AUTO_CAP2_MIN_OPENINGS: usize = 4; impl fmt::Display for CapPolicy { @@ -1016,7 +1016,7 @@ mod tests { // ------------------------------------------- the only-rejecting-check fixtures // - // REVIEW-CAP M1: a tamper that some OTHER check also rejects cannot show a + // 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. @@ -1033,7 +1033,7 @@ mod tests { /// 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 C1b case. + /// 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); @@ -1147,7 +1147,7 @@ mod tests { best.0 } - /// REVIEW-CAP S5: `Auto` is RULINGS 1's table; this pins that the table is + /// `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] @@ -1165,7 +1165,7 @@ mod tests { } } - /// Clamping the table to the depth (RULINGS 1) is not the same function as + /// 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 @@ -1238,7 +1238,7 @@ mod tests { compare: 3789, } ); - // The gains the pinned heights rest on (CAP.md §2). + // 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); diff --git a/crypto/math-cuda/kernels/keccak.cu b/crypto/math-cuda/kernels/keccak.cu index c372348cf..923a978b6 100644 --- a/crypto/math-cuda/kernels/keccak.cu +++ b/crypto/math-cuda/kernels/keccak.cu @@ -695,8 +695,8 @@ extern "C" __global__ void keccak256_leaves_base_row_major_row_pair_range( // 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 -// (I-FRI-D note 1), so one row per leaf needs its own read pattern. +// `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, diff --git a/crypto/math-cuda/src/fri.rs b/crypto/math-cuda/src/fri.rs index 24f78d9e4..da86b5829 100644 --- a/crypto/math-cuda/src/fri.rs +++ b/crypto/math-cuda/src/fri.rs @@ -375,7 +375,7 @@ impl FriCommitState { /// /// 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` (FRI.md §3.1). Only the last fold's output is kept; the + /// 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 diff --git a/crypto/math-cuda/src/merkle.rs b/crypto/math-cuda/src/merkle.rs index 2326e587c..036d9c525 100644 --- a/crypto/math-cuda/src/merkle.rs +++ b/crypto/math-cuda/src/merkle.rs @@ -445,7 +445,7 @@ pub fn gather_merkle_paths_dev( /// /// 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)` (design/CAP.md §1.3). 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. diff --git a/crypto/math-cuda/tests/fri_group_tree.rs b/crypto/math-cuda/tests/fri_group_tree.rs index 1a846d3a4..5bdd51d42 100644 --- a/crypto/math-cuda/tests/fri_group_tree.rs +++ b/crypto/math-cuda/tests/fri_group_tree.rs @@ -1,4 +1,4 @@ -//! S3 group-leaf FRI layers on the device (FRI.md §5, lane I-FRI-D). +//! 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 diff --git a/crypto/math-cuda/tests/merkle_cap.rs b/crypto/math-cuda/tests/merkle_cap.rs index f2fa2d320..283bcb54c 100644 --- a/crypto/math-cuda/tests/merkle_cap.rs +++ b/crypto/math-cuda/tests/merkle_cap.rs @@ -2,7 +2,7 @@ //! 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 -//! (design/CAP.md §4.2) instead of copying the whole tree. +//! instead of copying the whole tree. use crypto::merkle_tree::backends::field_element_vector::FieldElementVectorBackend; use crypto::merkle_tree::merkle::MerkleTree; diff --git a/crypto/multilinear/src/whir_cap_tests.rs b/crypto/multilinear/src/whir_cap_tests.rs index cbc483c93..221f4cc34 100644 --- a/crypto/multilinear/src/whir_cap_tests.rs +++ b/crypto/multilinear/src/whir_cap_tests.rs @@ -1,9 +1,9 @@ -//! W1 — the Merkle cap on WHIR chains (design/CAP.md §5), end to end on the +//! 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 internal-node leaf of REVIEW-CAP M1) live in `whir_round::tests`, +//! 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; @@ -239,7 +239,7 @@ fn the_default_format_is_byte_identical_to_a_zero_cap() { } } -/// REVIEW-CAP S2: the cap changes no transcript value. The same witness under +/// 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. diff --git a/crypto/multilinear/src/whir_chain.rs b/crypto/multilinear/src/whir_chain.rs index fc9dbc956..35076b775 100644 --- a/crypto/multilinear/src/whir_chain.rs +++ b/crypto/multilinear/src/whir_chain.rs @@ -178,7 +178,7 @@ impl GrindBits { /// Blowup, fold factor, query count and proof of work. /// -/// `format` is the proof FORMAT ([`ChainFormat`], the ZF campaign's W1 and W2 +/// `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 @@ -226,7 +226,7 @@ impl ChainFormat { /// 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 lane flips its own flag in the commit that +/// 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:: @@ -239,7 +239,7 @@ pub const WHIR_CAP_IMPLEMENTED: bool = true; /// 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 (design/WHIR.md §3.2), so nothing above 6 is opened. +/// instructions, so nothing above 6 is opened. pub const MAX_FOLD: usize = 6; /// The per-round fold schedule of a chain (W2). @@ -247,11 +247,11 @@ pub const MAX_FOLD: usize = 6; /// ★ 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 design/WHIR.md measured is the first fold alone — +/// 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 (RULINGS 15). +/// 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. @@ -381,7 +381,7 @@ impl ChainConfig { /// /// - `Uniform` → `log_folding`: `4u64` at the default, today's bytes. /// - `First(k0)` → `FOLD_WORD_TAG | log_folding << 52 | 1 << 48 | k0`: - /// design/WHIR.md §4.3's prefix encoding with a one-entry prefix (tail + /// 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 @@ -402,8 +402,7 @@ impl ChainConfig { } /// 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, - /// design/CAP.md §5.1). + /// `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 @@ -1417,7 +1416,7 @@ 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 (REVIEW-CAP M2). With no openings there is nothing to check. + // guard above, so indexing it never panics. With no openings there is nothing to check. let Some(first) = openings.current.first() else { return Ok(()); }; @@ -1659,7 +1658,7 @@ mod tests { assert_eq!(config(4).fold_word().to_le_bytes(), 4u64.to_le_bytes()); } - /// design/WHIR.md §3.2's schedules, by hand, and the clamp at small heights. + /// 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)); diff --git a/crypto/multilinear/src/whir_commit.rs b/crypto/multilinear/src/whir_commit.rs index 580bb54d5..e2cd5fcca 100644 --- a/crypto/multilinear/src/whir_commit.rs +++ b/crypto/multilinear/src/whir_commit.rs @@ -535,7 +535,7 @@ where /// `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 (design/CAP.md §9.4). +/// never be compared with an internal node. pub fn verify_opening( root: &Commitment, depth: usize, diff --git a/crypto/multilinear/src/whir_round.rs b/crypto/multilinear/src/whir_round.rs index 770130411..d1962ed55 100644 --- a/crypto/multilinear/src/whir_round.rs +++ b/crypto/multilinear/src/whir_round.rs @@ -37,7 +37,7 @@ pub struct RoundConfig { /// How a tree's openings are authenticated in a round. /// -/// A tree is authenticated ONCE (design/CAP.md §5.3, §9.3): tree 0 by the +/// 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 @@ -205,7 +205,7 @@ where /// /// ⚠ 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 (design/REVIEW-CAP.md M2). +/// panics. pub fn verify<'a, F, C, N, T, H>( proof: &'a RoundProof, commitments: RoundCommitments<'a>, @@ -638,7 +638,7 @@ mod tests { } } - /// REVIEW-CAP M1(b): a cap node no query reaches, flipped. Every + /// 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] @@ -705,7 +705,7 @@ mod tests { )); } - /// REVIEW-CAP M1(a), the WHIR analogue of C1b: a leaf forged from an + /// 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 diff --git a/crypto/stark/src/device_set.rs b/crypto/stark/src/device_set.rs index 020a4538b..1ae496ca4 100644 --- a/crypto/stark/src/device_set.rs +++ b/crypto/stark/src/device_set.rs @@ -288,9 +288,9 @@ mod tests { /// The dispatch layer's row floor (`gpu_lde::DEFAULT_GPU_LDE_THRESHOLD`). const FLOOR: usize = 1 << 14; - /// S2 (lane I-S2-D): a one-row tree has twice the leaves, so its node + /// 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, FRI.md §7.6) — and + /// +`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] diff --git a/crypto/stark/src/fri/capture.rs b/crypto/stark/src/fri/capture.rs index 5ba960f38..b875c954b 100644 --- a/crypto/stark/src/fri/capture.rs +++ b/crypto/stark/src/fri/capture.rs @@ -1,7 +1,7 @@ //! Test-only capture of the verifier's FRI challenges and DEEP values, for the -//! exported test vectors (`tests/vectors/zf_fri`, FRI.md §10 (d)): a vector +//! 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 and in-guest lanes can check each stage separately. +//! 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 diff --git a/crypto/stark/src/fri/device_parity.rs b/crypto/stark/src/fri/device_parity.rs index ae53566b4..b7834ed53 100644 --- a/crypto/stark/src/fri/device_parity.rs +++ b/crypto/stark/src/fri/device_parity.rs @@ -47,8 +47,8 @@ type Ext = FieldElement; /// 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 RULINGS 22 re-priced the - // objective, kept so the sweep's coverage does not shrink. + // 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], @@ -147,7 +147,7 @@ fn raw(v: &[Ext]) -> Vec<[u64; 3]> { /// `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 (lane I-S2-D): layer 0 is +/// `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). diff --git a/crypto/stark/src/fri/group.rs b/crypto/stark/src/fri/group.rs index 78d597fa4..310b202fd 100644 --- a/crypto/stark/src/fri/group.rs +++ b/crypto/stark/src/fri/group.rs @@ -1,5 +1,5 @@ //! Group-leaf FRI layers (S3): a committed layer of fold exponent `d` groups -//! `2^d` consecutive bit-reversed evaluations per leaf (FRI.md §1). +//! `2^d` consecutive bit-reversed evaluations per leaf. //! //! # Why a group is a coset, and how it folds //! diff --git a/crypto/stark/src/fri/mod.rs b/crypto/stark/src/fri/mod.rs index 67e94c0bf..fb79b9d70 100644 --- a/crypto/stark/src/fri/mod.rs +++ b/crypto/stark/src/fri/mod.rs @@ -211,7 +211,7 @@ where // 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 (FRI.md §7.3; a root absorbed after its challenge + // 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 }; @@ -387,7 +387,7 @@ where /// [`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, FRI.md §3.4) and the path of +/// (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`]). diff --git a/crypto/stark/src/fri/schedule.rs b/crypto/stark/src/fri/schedule.rs index 1e9cb74b0..d601934e3 100644 --- a/crypto/stark/src/fri/schedule.rs +++ b/crypto/stark/src/fri/schedule.rs @@ -19,7 +19,7 @@ //! * the active Merkle-cap policy ([`CapPolicy`]; `Off` caps nothing); //! * `dmax` — the largest fold exponent the program may choose. //! -//! # The objective (RULINGS 13, 22): the cost law of every emitted row +//! # 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 @@ -52,7 +52,7 @@ //! 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 lane pins "emitted rows == [`fri_group_layer_rows`]" kind by +//! 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. @@ -469,7 +469,7 @@ pub struct FriFormat { 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, RULINGS 7). + /// 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, @@ -501,7 +501,7 @@ impl FriFormat { } /// Whether the proof uses today's FRI encoding: one sibling value per - /// committed layer, pair leaves (FRI.md §3.4). True exactly for pair + /// 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. diff --git a/crypto/stark/src/fri/vectors.rs b/crypto/stark/src/fri/vectors.rs index 32c724f8a..0906f26a0 100644 --- a/crypto/stark/src/fri/vectors.rs +++ b/crypto/stark/src/fri/vectors.rs @@ -1,5 +1,5 @@ -//! The S3 test vectors the host lane exports (FRI.md §10, "Vectors the host -//! lane exports" (a)–(d)) for the device and in-guest lanes, checked in under +//! 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 @@ -72,7 +72,7 @@ pub fn check_or_write(files: &[VectorFile], write: bool) -> Vec { bad } -/// SplitMix64: the KAT input generator (stated in the README so any lane can +/// 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); @@ -276,8 +276,8 @@ pub fn leaf_digests_json(hash_name: &str) -> VectorFile { 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 (RULINGS 1), so a Q = 3 proof -/// carries no cap at all (REVIEW-FRI F9). +/// 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 { @@ -295,7 +295,7 @@ pub fn proof_options(format: ProofFormat, queries: usize) -> ProofOptions { /// 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 of REVIEW-FRI F9. +/// Q = [`CAPPED_QUERIES`] — the combined S1 × S3 vector. pub fn proof_formats() -> Vec<(&'static str, ProofFormat, usize)> { let dp = ProofFormat { fri_mode: FriMode::Dp, diff --git a/crypto/stark/src/gpu_lde.rs b/crypto/stark/src/gpu_lde.rs index dc6679897..e51bdc1a9 100644 --- a/crypto/stark/src/gpu_lde.rs +++ b/crypto/stark/src/gpu_lde.rs @@ -3623,8 +3623,8 @@ pub(crate) fn gather_proofs_dev( Some(proofs) } -/// Read the height-`cap_height` Merkle cap of a device-resident tree -/// (design/CAP.md §4.2): the nodes `MerkleTree::cap` returns on the host tree, +/// 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. /// @@ -4186,8 +4186,8 @@ fn zeta_powers_raw(zeta: &FieldElement, n: u32) -> Vec<[u64; 3]> /// /// 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, I-FRI-D's group kernels), its root appended with NO challenge -/// sampled before it (FRI.md §7.3, the CPU loop's `pending = 0`); every later +/// 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). diff --git a/crypto/stark/src/leaf_layout.rs b/crypto/stark/src/leaf_layout.rs index e00ed3af5..a2134d87a 100644 --- a/crypto/stark/src/leaf_layout.rs +++ b/crypto/stark/src/leaf_layout.rs @@ -1,4 +1,4 @@ -//! The trace-tree leaf layout of one table's proof (S2, design/FRI.md §7). +//! 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 @@ -19,8 +19,8 @@ //! 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 -//! (REVIEW-FRI F7): every opening site, prover and verifier, goes through it. +//! [`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; @@ -61,7 +61,7 @@ impl LeafLayout { /// 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 - /// (FRI.md §7.7 (i): under one row `r` must be uniform over ALL of `D₀`). + /// (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, @@ -90,7 +90,7 @@ impl LeafLayout { /// 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 (REVIEW-FRI F7). + /// 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 { @@ -100,7 +100,7 @@ impl LeafLayout { } } -/// Mutation M3 (FRI.md §10), test builds only: sample one-row query indexes +/// 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 @@ -236,7 +236,7 @@ pub fn trace_tree_cost_q(felts: u64, depth: u32, num_queries: u64, cap: CapPolic /// ([`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 (RULINGS 22). A DEEP point costs +/// is evaluated at ONE point. A DEEP point costs /// [`TableWidths::deep_point_rows`] `XALU` rows. pub fn table_openings_cost_q( widths: &TableWidths, @@ -277,7 +277,7 @@ pub fn table_openings_cost_q( trees.saturating_add(chain).saturating_add(deep) } -/// RULINGS 6's `auto` rule: one row iff it is STRICTLY cheaper than row pairs +/// 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, diff --git a/crypto/stark/src/lookup.rs b/crypto/stark/src/lookup.rs index a748aba3a..664c0510f 100644 --- a/crypto/stark/src/lookup.rs +++ b/crypto/stark/src/lookup.rs @@ -855,7 +855,7 @@ impl BusValue { /// 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, RULINGS 14). +/// (the prover then refuses and the verifier rejects; never a silent recompute). #[derive(Clone)] pub struct LazyCommitment { value: std::sync::Arc>, diff --git a/crypto/stark/src/merkle_caps.rs b/crypto/stark/src/merkle_caps.rs index 3c223ff54..9f182f4a7 100644 --- a/crypto/stark/src/merkle_caps.rs +++ b/crypto/stark/src/merkle_caps.rs @@ -1,4 +1,4 @@ -//! Merkle caps of a univariate STARK proof (design/CAP.md §4, lever S1). +//! 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 @@ -14,8 +14,8 @@ //! //! [`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 C1b -//! exact-length check, so the default format verifies the bytes it did. +//! 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; diff --git a/crypto/stark/src/proof/options.rs b/crypto/stark/src/proof/options.rs index d6db6b5c0..1262b38a5 100644 --- a/crypto/stark/src/proof/options.rs +++ b/crypto/stark/src/proof/options.rs @@ -42,8 +42,8 @@ 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 campaign's levers). -/// Its default is the legacy (pre-campaign) format, byte for byte. +/// - `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 /// @@ -116,7 +116,7 @@ impl ProofFormat { /// the legacy format. pub const DEFAULT: Self = Self::LEGACY; - /// The pre-campaign format: every lever off. The only format the RV64 + /// The legacy format: every lever off. The only format the RV64 /// recursion guest verifies. pub const LEGACY: Self = Self { merkle_cap: CapPolicy::Off, @@ -132,7 +132,7 @@ impl ProofFormat { } /// True when every lever is off (`Fixed(0)` counts as `Off`): the proof - /// this produces is the pre-campaign format, byte for byte. + /// 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 @@ -255,15 +255,15 @@ impl FromStr for OneRowMode { /// 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 while the campaign lands it — must not be selectable, or a run -/// could print a non-default format and prove the default one. Each lane -/// flips its own flag in the commit that makes the lever real. +/// 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 (design/CAP.md C3 + C4) and the LFM in-guest STARK verifier (C5: -/// `lfm::merkle_cap::CapCells`, one caps arena per sub-proof), on pair and on +/// 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 -/// default-only (RULINGS 11). +/// 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: @@ -279,8 +279,8 @@ pub const MERKLE_CAP_IMPLEMENTED: bool = true; /// 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 (default-only by RULINGS 11; it -/// refuses a non-default format). +/// 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 @@ -289,14 +289,14 @@ pub const FRI_MODE_IMPLEMENTED: bool = true; /// 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`, RULINGS 6); +/// `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 (RULINGS 14) +/// 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 (lane I-S2-D, D2): one-row trees for the fused main commit, +/// - 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 @@ -304,16 +304,16 @@ pub const FRI_MODE_IMPLEMENTED: bool = true; /// 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 (lane I-FRI-G, G3: an emitter +/// 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 (default-only, RULINGS 11). A block run under +/// 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 pre-campaign format, byte for + /// format): the proof this produces is the legacy format, byte for /// byte. pub fn has_default_format(&self) -> bool { self.format.is_default() diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 3c8fa6002..ca4329fb2 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -104,7 +104,7 @@ pub enum ProvingError { 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 (RULINGS 14): proving on would either + /// 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), @@ -238,7 +238,7 @@ type PrecomputedTreeMap = /// 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 (FRI.md §7.5.5). +/// argument is not a hash-collision argument. type PrecomputedTreeKey = (Commitment, usize); fn precomputed_tree_cache() -> &'static Mutex { @@ -3060,7 +3060,7 @@ pub trait IsStarkProver< leaf_layout, ); - // Merkle caps (design/CAP.md §4.2): a post-pass over the finished + // 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. @@ -3110,7 +3110,7 @@ pub trait IsStarkProver< } /// Embed every capped tree's cap into its owner path and cut every path of - /// that tree to `depth − c` siblings (design/CAP.md §3–§4.2). + /// 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 @@ -3274,8 +3274,7 @@ pub trait IsStarkProver< /// 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 - /// (REVIEW-CAP S6). + /// full-length paths the verifier rejects with no pointer to the cause. fn tree_cap( host: &MerkleTree, depth: usize, @@ -4549,7 +4548,7 @@ pub trait IsStarkProver< 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 (RULINGS 14). + // 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!( diff --git a/crypto/stark/src/s2_device_parity.rs b/crypto/stark/src/s2_device_parity.rs index 35ba21b8d..bcd96c08d 100644 --- a/crypto/stark/src/s2_device_parity.rs +++ b/crypto/stark/src/s2_device_parity.rs @@ -1,5 +1,4 @@ -//! Device-vs-host parity for S2's one-row trees and openings (FRI.md §7.6, -//! lane I-S2-D, D2). +//! 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 diff --git a/crypto/stark/src/tests/cap_fri_matrix_tests.rs b/crypto/stark/src/tests/cap_fri_matrix_tests.rs index 9764c58af..f4278816a 100644 --- a/crypto/stark/src/tests/cap_fri_matrix_tests.rs +++ b/crypto/stark/src/tests/cap_fri_matrix_tests.rs @@ -1,5 +1,5 @@ //! Merkle caps (S1) composed with group-leaf FRI layers (S3) on the host path: -//! REVIEW-FRI F9's round-trip matrix {cap off, fixed, auto} × {pair, dp, +//! 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 @@ -11,8 +11,7 @@ //! - 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 (REVIEW-CAP M1(b) on a group -//! tree); +//! 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; @@ -46,7 +45,7 @@ type Leaf = ::Batched; const ROWS: usize = 1024; const LDE_LOG: u32 = 11; const TERMINAL_LOG: u32 = 3; -/// `auto` caps at height 3 from 20 openings on (RULINGS 1). +/// `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 { @@ -214,7 +213,7 @@ fn every_cap_node_of_a_capped_group_layer_is_bound() { } } -/// REVIEW-CAP M1(b) on a group tree: with three queries and a height-3 cap on +/// 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 diff --git a/crypto/stark/src/tests/fri_group_tests.rs b/crypto/stark/src/tests/fri_group_tests.rs index 077377b0c..b0ac56418 100644 --- a/crypto/stark/src/tests/fri_group_tests.rs +++ b/crypto/stark/src/tests/fri_group_tests.rs @@ -1,7 +1,7 @@ -//! S3 (group-leaf FRI layers) on the CPU prover and host verifier: FRI.md §10 -//! U4–U6, the tamper tests T1–T3, the load-bearing mutations M1–M2 and the +//! 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 (REVIEW-FRI F1.2). +//! path. use crypto::fiat_shamir::default_transcript::DefaultTranscript; use crypto::fiat_shamir::is_transcript::IsTranscript; @@ -503,7 +503,7 @@ fn the_format_is_a_verifier_constant() { } // --------------------------------------------------------------------------- -// F1.2: the group path at the all-ones schedule vs the legacy path. +// 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 diff --git a/crypto/stark/src/tests/fri_schedule_tests.rs b/crypto/stark/src/tests/fri_schedule_tests.rs index 985f1ca51..a29a7aa0a 100644 --- a/crypto/stark/src/tests/fri_schedule_tests.rs +++ b/crypto/stark/src/tests/fri_schedule_tests.rs @@ -1,11 +1,11 @@ //! Tests for the FRI fold schedule (`crate::fri::schedule`) and the generalised -//! `FriFoldLayout` (FRI.md §10 U1–U3). +//! `FriFoldLayout` (U1–U3). //! -//! Two objectives appear here. The PRODUCTION one is the cost law (RULINGS 13, -//! `FRI_COST_WEIGHTS`): U1 pins its schedules as the Rust DP computes them, U2 +//! 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 -//! (FRI.md §2.1, the §2.2 table) is kept as a second instance of the generic DP -//! (`fri_schedule_by`), pinned against the design document: it shows the DP +//! (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. @@ -32,7 +32,7 @@ fn no_cap(_depth: u32) -> u32 { 0 } -/// The cap rule FRI.md §2.2's table was computed with (PLAN §4): +/// 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); @@ -45,14 +45,14 @@ fn cap_design_model(depth: u32) -> u32 { best_c } -/// The adopted policy (RULINGS 1): every FRI tree is opened once per query. +/// 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() { - // CAP.md §11 "CapPolicy pins", at a depth large enough not to clamp. + // 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), @@ -64,12 +64,12 @@ fn cap_auto_heights_match_cap_md() { 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 (FRI.md §2.2 used it). + // The design model's rule reaches 7 at Q = 110. assert_eq!(cap_design_model(20), 7); assert_eq!(cap_design_model(5), 5); } -/// FRI.md §2.1's per-layer cost, `Q ×` permutations: `Q·leaf(d) + Q·(depth − +/// 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); @@ -103,7 +103,7 @@ fn leaf_blocks() { } } -/// The objective's weights are a format constant (RULINGS 13, 22): the cap +/// 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] @@ -142,7 +142,7 @@ fn cost_weights_are_pinned() { assert_eq!(((421.0f64 + 5.63 * 10.0).round()) as u64, BALU_ROW_NS); } -/// One layer's cost written out by hand (RULINGS 22: every emitted row). +/// One layer's cost written out by hand (every emitted row). #[test] fn layer_cost_by_hand() { // d = 3, depth 10, no cap: @@ -197,7 +197,7 @@ fn layer_cost_by_hand() { ); } -/// The row model's kinds at `d = 1..=6`, written out (the in-guest lane pins +/// 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() { @@ -249,13 +249,13 @@ fn schedule_cost_rejects_malformed_schedules() { } // --------------------------------------------------------------------------- -// The design model (permutation objective): the FRI.md §2.2 table, reproduced. +// 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 FRI.md §2.1 in exact -/// integer units (lane I-FRI-H scratch), and cross-checked against -/// `lanes/D-FRI/model_output.txt` for the OFF and MODEL caps (cost / 110). +/// 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]), @@ -692,7 +692,7 @@ fn design_model_reproduces_the_fri_md_table() { check_pin("T10 cap auto", 10, &cap_auto, PIN_T10_CAP_AUTO); } -/// Spot checks tying the design-model pins to the printed FRI.md §2.2 table +/// 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() { @@ -729,12 +729,9 @@ fn design_model_pins_match_fri_md_table() { type CostRow = (u32, &'static [u8], &'static [u8]); /// Generated by `print_cost_law_schedule_table` (below, `--ignored`) from the -/// Rust DP. Re-pinned for RULINGS 22 (every emitted row priced): the cap-auto -/// tables did NOT move; four cap-off entries did — T = 9: B = 16 S2 [4,3] → -/// [3,2,2], B = 17 S3 [4,3] → [3,2,2]; T = 10: B = 14 S2 [4] → [2,2], -/// B = 15 S3 [4] → [2,2]. The whole table was cross-checked against an -/// independent Python reproduction of the objective (lane I-PRICE scratch): -/// identical. REVIEW-FRI F2's cost-law column gives [2,2] / [3,3,3] / +/// 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, &[], &[]), diff --git a/crypto/stark/src/tests/log_read_only_program_tests.rs b/crypto/stark/src/tests/log_read_only_program_tests.rs index 004f9bd5e..08e389bb7 100644 --- a/crypto/stark/src/tests/log_read_only_program_tests.rs +++ b/crypto/stark/src/tests/log_read_only_program_tests.rs @@ -1,4 +1,4 @@ -//! `LogReadOnlyRAP` carries a constraint program (I-FIX-D2). +//! `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 diff --git a/crypto/stark/src/tests/merkle_cap_tests.rs b/crypto/stark/src/tests/merkle_cap_tests.rs index 73ab1ebcb..d00d4ca70 100644 --- a/crypto/stark/src/tests/merkle_cap_tests.rs +++ b/crypto/stark/src/tests/merkle_cap_tests.rs @@ -1,4 +1,4 @@ -//! Merkle caps on univariate STARK proofs (design/CAP.md §4, lever S1, commit C3). +//! 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 @@ -7,9 +7,9 @@ //! - 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 (REVIEW-CAP S2); +//! differ only in their Merkle paths; //! - tampers of every tree class, of the owner split, and of the policy; -//! - REVIEW-CAP M1 at the verifier level: an unreached cap node that only the +//! - 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. @@ -298,7 +298,7 @@ fn a_zero_height_policy_is_byte_identical_to_off() { assert_eq!(off, bytes(CapPolicy::Auto)); } -/// REVIEW-CAP S2: the transcript does not change under a cap. One witness +/// 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 @@ -587,7 +587,7 @@ fn an_unreached_cap_node_is_rejected_by_the_cap_to_root_check_alone() { /// 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`, C1b) and +/// 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() { @@ -651,7 +651,7 @@ fn an_internal_node_passed_as_a_leaf_is_rejected_by_the_length_check_alone() { // ------------------------------------------------------------- device trees -/// REVIEW-CAP S6: a device-resident tree (a root-only host tree) whose cap has +/// 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. @@ -706,7 +706,7 @@ fn a_device_resident_tree_without_a_cap_read_is_an_error() { assert!(verify_cap::(&cap, &host.root, 2)); } -/// C4 on a real device (box only; `--features cuda -- --ignored`): a LogUp +/// 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 diff --git a/crypto/stark/src/tests/one_row_tests.rs b/crypto/stark/src/tests/one_row_tests.rs index 523518041..ab5f384cd 100644 --- a/crypto/stark/src/tests/one_row_tests.rs +++ b/crypto/stark/src/tests/one_row_tests.rs @@ -1,8 +1,8 @@ //! S2 (one-row trace openings with a committed FRI input) on the CPU prover -//! and host verifier: design/FRI.md §7 and §10 — U6 at one_row, the tamper +//! 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 (RULINGS 6, REVIEW-FRI F5), the preprocessed-root -//! miss (RULINGS 14) and the cap × FRI × one-row matrix (REVIEW-FRI F9). +//! 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; @@ -61,7 +61,7 @@ fn on(fri_mode: FriMode) -> ProofFormat { } // --------------------------------------------------------------------------- -// The layout helper (REVIEW-FRI F7): one place a query becomes rows. +// The layout helper: one place a query becomes rows. // --------------------------------------------------------------------------- #[test] @@ -97,7 +97,7 @@ fn query_rows_bounds_and_depths() { } } -/// REVIEW-FRI F7: no stray `2·iota(+1)` row arithmetic outside the helper in +/// 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). @@ -428,7 +428,7 @@ fn one_row_zero_fold_case() { } // --------------------------------------------------------------------------- -// FRI.md §7.7 (i): r is uniform over ALL of D₀. M3 shows the test that says so +// Soundness: r is uniform over ALL of D₀. M3 shows the test that says so // is load-bearing. // --------------------------------------------------------------------------- @@ -478,7 +478,7 @@ fn m3_the_query_bound_test_is_load_bearing() { } // --------------------------------------------------------------------------- -// FRI.md §7.7 (ii): the input root is absorbed before ζ₀ (transcript KAT). +// Soundness: the input root is absorbed before ζ₀ (transcript KAT). // --------------------------------------------------------------------------- #[test] @@ -640,7 +640,7 @@ fn m1_the_input_slot_check_is_load_bearing() { } // --------------------------------------------------------------------------- -// Preprocessed tables: one-row roots, and RULINGS 14 (a miss is an error). +// Preprocessed tables: one-row roots, and a miss is an error (never a recompute). // --------------------------------------------------------------------------- #[test] @@ -716,7 +716,7 @@ fn one_row_preprocessed_table_and_a_missing_root() { } // --------------------------------------------------------------------------- -// RULINGS 6 / REVIEW-FRI F5: the per-table `auto` rule. +// The per-table `auto` rule. // --------------------------------------------------------------------------- fn opts_q(q: usize, one_row: OneRowMode, fri: FriMode, cap: CapPolicy) -> ProofOptions { @@ -785,11 +785,10 @@ fn pinned_deep_rows(pre: u64, main: u64, aux: u64, parts: u64) -> u64 { /// 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 as REVIEW-FRI §C reads them; the others are round -/// numbers), not a census. Re-pinned for RULINGS 22 (every emitted FRI row -/// priced, DEEP at two points vs one): two choices moved to one row — the -/// MEMW-like case (row pairs by 0.5% before; one row by 5.5% now, and only -/// because of the DEEP term, see `auto_choices_margins`) and the narrow short +/// (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] @@ -854,7 +853,7 @@ fn auto_choices_margins() { } } -/// RULINGS 22: DEEP costs two points under row pairs and one under one row, +/// 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] @@ -907,7 +906,7 @@ fn auto_resolves_per_table_from_the_air() { } // --------------------------------------------------------------------------- -// REVIEW-FRI F9: {cap off, auto} × {pair, dp} × {0, 1, auto}, Q ≥ 20. +// The format matrix: {cap off, auto} × {pair, dp} × {0, 1, auto}, Q ≥ 20. // --------------------------------------------------------------------------- #[test] diff --git a/crypto/stark/src/tests/opening_width_tests.rs b/crypto/stark/src/tests/opening_width_tests.rs index f50717f12..8d86da29a 100644 --- a/crypto/stark/src/tests/opening_width_tests.rs +++ b/crypto/stark/src/tests/opening_width_tests.rs @@ -72,7 +72,7 @@ pub struct FibonacciSplitAIR { 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, RULINGS 14). + /// AIR has none (the one-row prover must refuse, never recompute). precomputed_commitment_row: Option, phantom: PhantomData, } diff --git a/crypto/stark/src/tests/path_length_tests.rs b/crypto/stark/src/tests/path_length_tests.rs index 88927ee3f..7634d3bff 100644 --- a/crypto/stark/src/tests/path_length_tests.rs +++ b/crypto/stark/src/tests/path_length_tests.rs @@ -3,9 +3,8 @@ //! 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 used to fold a path of any length and -//! compare the result with the root; it now requires the exact length -//! (design/CAP.md §9.4, commit C1b). These tests pin that honest proofs meet +//! `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. diff --git a/crypto/stark/src/tests/zf_fri_device_tests.rs b/crypto/stark/src/tests/zf_fri_device_tests.rs index 2a94509cd..8dda1e81e 100644 --- a/crypto/stark/src/tests/zf_fri_device_tests.rs +++ b/crypto/stark/src/tests/zf_fri_device_tests.rs @@ -1,4 +1,4 @@ -//! S3 on the device (FRI.md §5, lane I-FRI-D, D1): the device FRI commit and +//! 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`). //! @@ -38,8 +38,7 @@ fn dp_shapes_are_pinned() { } const PINNED_SHAPES: &[&[u8]] = &[ - // The DP's own (21; RULINGS 22 dropped [4] and moved [4, 3] after - // [4, 3, 3, 3] in first-appearance order). + // The DP's own (21, in first-appearance order). &[1], &[2], &[3], @@ -126,7 +125,7 @@ fn parity_legacy_encoding_blake3() { check::("blake3", &legacy_cases(), false, 0x5a49_0000); } -/// The (d) vector proofs (FRI.md §10 (d): `pair`, `dp`, `dp_3_1_3`, and the +/// 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 @@ -154,8 +153,8 @@ fn proved_vectors_equal_the_cpu_bytes() { 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, - // I-FIX-D2); a host composition would not be counted here. + // 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)" diff --git a/crypto/stark/src/tests/zf_fri_vectors.rs b/crypto/stark/src/tests/zf_fri_vectors.rs index f0b940ab8..0561e63aa 100644 --- a/crypto/stark/src/tests/zf_fri_vectors.rs +++ b/crypto/stark/src/tests/zf_fri_vectors.rs @@ -1,4 +1,4 @@ -//! The exported S3 and S2 vectors (FRI.md §10 (a)–(e)) under Keccak and Blake3 are +//! 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: diff --git a/crypto/stark/src/tests/zf_golden_tests.rs b/crypto/stark/src/tests/zf_golden_tests.rs index c5200bae6..97e9647d4 100644 --- a/crypto/stark/src/tests/zf_golden_tests.rs +++ b/crypto/stark/src/tests/zf_golden_tests.rs @@ -1,4 +1,4 @@ -//! Default-format golden proofs (REVIEW-FRI F1): the bytes today's prover emits, +//! 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). @@ -18,9 +18,8 @@ //! 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 (commit -//! "H0" of lane I-FRI-H, on `zf/cap-stark` @ 77ea1ab89 + the schedule DP, which -//! changes no prover path). Regenerate only for a deliberate format change: +//! 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; diff --git a/crypto/stark/src/tests/zf_s2_device_tests.rs b/crypto/stark/src/tests/zf_s2_device_tests.rs index b18d22dba..97bbbfb26 100644 --- a/crypto/stark/src/tests/zf_s2_device_tests.rs +++ b/crypto/stark/src/tests/zf_s2_device_tests.rs @@ -1,4 +1,4 @@ -//! S2 on the device (FRI.md §7.6, lane I-S2-D, D2): one-row trees and +//! 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`). @@ -85,7 +85,7 @@ fn fri_one_row_resident_blake3() { fri::("blake3", &one_row_resident_cases(), true, 0x5235_0000); } -/// The (e) vector proofs (FRI.md §10 (e): `one_row_pair` and +/// 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 @@ -120,8 +120,8 @@ fn proved_one_row_vectors_equal_the_cpu_bytes() { "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, - // I-FIX-D2); a host composition would not be counted here. + // 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)" diff --git a/crypto/stark/src/traits.rs b/crypto/stark/src/traits.rs index 9b77884ff..fcb65fc11 100644 --- a/crypto/stark/src/traits.rs +++ b/crypto/stark/src/traits.rs @@ -215,7 +215,7 @@ pub trait AIR: Send + Sync { /// 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 (RULINGS 14 — never a silent recompute, never + /// 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( diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 2750870bd..c946c20aa 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -151,7 +151,7 @@ pub trait IsStarkVerifier< { /// 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₀` (FRI.md §7.7 (i): + /// 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, @@ -833,19 +833,19 @@ pub trait IsStarkVerifier< } /// The per-tree Merkle checks of one table's proof, built ONCE per tree - /// before any query is verified (design/CAP.md §4.3). + /// 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 (C1b at `c = 0`: before that a path of any length was - /// folded and compared with the root, design/CAP.md §9.4). + /// `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 (REVIEW-CAP M2): the caller runs this only after the + /// 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 @@ -976,8 +976,8 @@ pub trait IsStarkVerifier< /// 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)` - /// (FRI.md §7.4). With nothing to fold the terminal codeword is the DEEP + /// 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)] @@ -1746,7 +1746,7 @@ pub trait IsStarkVerifier< // Preprocessed table: VERIFY precomputed commitment matches hardcoded. // This is the critical soundness check - ensures prover used correct precomputed values. // The root of THIS table's leaf layout (a verifier constant); - // a layout the AIR has no root for rejects (RULINGS 14). + // 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!( @@ -2132,7 +2132,7 @@ pub trait IsStarkVerifier< // 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 (REVIEW-CAP M2). A capped + // 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 { diff --git a/crypto/stark/tests/vectors/zf_fri/README.md b/crypto/stark/tests/vectors/zf_fri/README.md index 68c263858..10539a060 100644 --- a/crypto/stark/tests/vectors/zf_fri/README.md +++ b/crypto/stark/tests/vectors/zf_fri/README.md @@ -3,8 +3,8 @@ 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 lane (group-leaf commits, multi-fold kernels, query -gathers) and the in-guest lane (group folds, group-leaf walks, slot checks). +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: @@ -60,7 +60,7 @@ the fold, the leaf encoding): a stale file means the format moved. 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 (RULINGS 13; `weights_ns` in the file header). Production: +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 @@ -93,7 +93,7 @@ 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 (REVIEW-FRI F9): `cap_pair` (`LAMBDA_VM_ZF_CAP=auto`, today's +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 diff --git a/prover/src/lfm/airs.rs b/prover/src/lfm/airs.rs index 78fdcfd69..73b51eabd 100644 --- a/prover/src/lfm/airs.rs +++ b/prover/src/lfm/airs.rs @@ -903,7 +903,7 @@ 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 (RULINGS 14). `KECCAK_RND` has no preprocessed columns. + /// 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]); diff --git a/prover/src/lfm/commit.rs b/prover/src/lfm/commit.rs index 751ca958f..223f00058 100644 --- a/prover/src/lfm/commit.rs +++ b/prover/src/lfm/commit.rs @@ -220,7 +220,7 @@ pub fn commit_group_device_or_host( /// [`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 (REVIEW-FRI F8.1). +/// device like a row-pair one. pub fn commit_group_device_or_host_with( label: &str, group: &ColumnGroup, @@ -346,7 +346,7 @@ mod device_parity { } } - /// S2 (REVIEW-FRI F8.1): the one-row artifact root on the device equals the + /// 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 diff --git a/prover/src/lfm/epoch.rs b/prover/src/lfm/epoch.rs index 972658ccd..4263fe96b 100644 --- a/prover/src/lfm/epoch.rs +++ b/prover/src/lfm/epoch.rs @@ -707,7 +707,7 @@ pub(super) fn nonce_halves(b: &mut LfmBuilder, nonce: Felt) -> [Felt; 2] { super::transcript_replay::felt_be_halves(b, nonce) } -// The transcript-order mutation (FRI.md §10 T6 in-guest): a test build can +// 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)] @@ -813,11 +813,11 @@ pub fn emit_table_challenges( // Sample FIRST, absorb SECOND — a ζ drawn after its own layer root is a // challenge the prover answers rather than one that binds them. // - // ★ Except the one-row INPUT tree (S2, design/FRI.md §7.3): root 0 is + // ★ 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 λ₁ (FRI.md §7.7 - // (ii)); the host replay (`verifier.rs`, `replay_rounds_after_round_1`) + // 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)); diff --git a/prover/src/lfm/epoch_tests.rs b/prover/src/lfm/epoch_tests.rs index 07d9f6607..6b2819d98 100644 --- a/prover/src/lfm/epoch_tests.rs +++ b/prover/src/lfm/epoch_tests.rs @@ -344,7 +344,7 @@ fn the_challenge_replay_matches_production() { } } -/// ★ S2 (one-row leaves, design/FRI.md §7.2–§7.3): the in-machine replay of a +/// ★ 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 @@ -1308,7 +1308,7 @@ fn harvest_real_epoch( // 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, FRI.md §7.5.2). + // attestation id moves with the knob). expected_program_id: crate::recursion::program_id_from_digest( &crate::statement::elf_digest(&elf_bytes), elf.entry_point, diff --git a/prover/src/lfm/epoch_verify.rs b/prover/src/lfm/epoch_verify.rs index 6a25dd324..6e3dab9e1 100644 --- a/prover/src/lfm/epoch_verify.rs +++ b/prover/src/lfm/epoch_verify.rs @@ -351,7 +351,7 @@ pub fn emit_table_verification( ); // ---- the Merkle caps, once per tree, against the SAME root cells the - // transcript absorbed (design/CAP.md §6.1): the matrices in group order, + // 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!( @@ -673,7 +673,7 @@ pub fn query_permutations_for(shape: &TableVerifyShape, hash: WrapHash) -> usize /// 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 (design/CAP.md §6.1 `cap_permutations`). Zero at the +/// `2^c − 1` parents. Zero at the /// default format. pub fn cap_permutations(shape: &TableVerifyShape) -> usize { shape.sub.cap_permutations() + shape.fri.cap_permutations() diff --git a/prover/src/lfm/epoch_verify_tests.rs b/prover/src/lfm/epoch_verify_tests.rs index 009d8d0a1..153f1a49d 100644 --- a/prover/src/lfm/epoch_verify_tests.rs +++ b/prover/src/lfm/epoch_verify_tests.rs @@ -347,7 +347,7 @@ pub(super) fn build_table_legs( /// 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, RULINGS 14 — a layout with no root is a hard +/// 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( @@ -364,7 +364,7 @@ pub(super) fn layout_precomputed_commitment( /// /// 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 (FRI.md §3.4); `FriShape::layer_values` says which. +/// 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)] @@ -1546,7 +1546,7 @@ fn the_candidate_rate_model_is_derived_not_remembered() { } /// Queries the knob-on twin proves at: enough openings that `auto` caps every -/// tall tree at height 3 (RULINGS 1: from 20 openings on). +/// 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 @@ -1559,8 +1559,8 @@ const PROCESS_FORMAT_QUERIES: usize = 24; /// 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 the lead -/// compares across arms (instructions, permutations, `Select`s, cells per +/// `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"] @@ -1572,7 +1572,7 @@ fn the_assembled_epoch_verifier_runs_at_the_process_format() { /// ★ [`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`, RULINGS 14: a missing twin is a +/// 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 diff --git a/prover/src/lfm/fri.rs b/prover/src/lfm/fri.rs index b2beaf44a..a8b6df958 100644 --- a/prover/src/lfm/fri.rs +++ b/prover/src/lfm/fri.rs @@ -61,7 +61,7 @@ pub struct FriShape { pub coset_offset: u64, /// Queries the sub-proof carries. pub num_queries: usize, - /// The inner proof's FORMAT (design/CAP.md, design/FRI.md): its Merkle cap + /// 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. /// @@ -174,7 +174,7 @@ impl FriShape { /// ★ 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 RULINGS-13 cost-law DP under `dp`). A format + /// 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 @@ -198,7 +198,7 @@ impl FriShape { self.schedule().len() } - /// Folding challenges the proof draws (FRI.md §7.3, `FriFoldLayout::num_zetas`): + /// 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. @@ -224,14 +224,14 @@ impl FriShape { /// 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 - /// (FRI.md §3.4 — the query's own value included). + /// (the query's own value included). pub fn layer_values(self, layer: usize) -> usize { if self.is_legacy() { 1 @@ -341,8 +341,8 @@ impl FriShape { } /// 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, - /// FRI.md §7.2) — which is both the TRACE trees' Merkle depth and the bit + /// 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 @@ -618,7 +618,7 @@ pub struct FriCommitments { /// 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`]) (FRI.md §1.2) — squared ONCE per + /// [`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>, @@ -672,7 +672,7 @@ pub struct LayerOpening { /// /// 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 (FRI.md §3.4) — the leaf is hashed straight from them and the + /// 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. @@ -953,7 +953,7 @@ pub fn emit_query_fri( let inv = b.div(one, q.point); if shape.one_row() { - // ★ S2 (design/FRI.md §7.3-§7.4): layer 0 IS the committed DEEP + // ★ 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 @@ -1008,7 +1008,7 @@ pub fn emit_query_fri( } 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}`, FRI.md §1.1). + // layer hands the next its own point (`x_g^{2^d}`). assert_eq!( fri.zeta_powers.len(), c, @@ -1088,7 +1088,7 @@ pub fn emit_pair_layer( (edsl::fri_fold(b, v, sym, zeta, inv_pow), inv_pow) } -/// The program constants of one group fold of exponent `d` (FRI.md §1.3), in +/// 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)`): /// @@ -1166,7 +1166,7 @@ fn emit_value_mux(b: &mut LfmBuilder, values: &[Ext], slot_bits: &[Bit]) -> Ext level[0].as_ext() } -/// ★ One committed layer under the group encoding (S3; FRI.md §1.3, §3.2, §6). +/// ★ 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 @@ -1176,8 +1176,8 @@ fn emit_value_mux(b: &mut LfmBuilder, values: &[Ext], slot_bits: &[Bit]) -> Ext /// 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; REVIEW-FRI -/// F6), and authenticated at the tree's leaf index `bits[G+d..]` against the +/// `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 diff --git a/prover/src/lfm/fri_group_tests.rs b/prover/src/lfm/fri_group_tests.rs index 64d0c156c..ef68ca2b1 100644 --- a/prover/src/lfm/fri_group_tests.rs +++ b/prover/src/lfm/fri_group_tests.rs @@ -1,18 +1,18 @@ //! S3 in the in-guest FRI verifier: the shape from the shared schedule (G1) -//! and the group-layer emitter (G2), design/FRI.md §6, §11. +//! 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 I-FRI-H's checked-in RPX vectors +//! - 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 of REVIEW-FRI F9), +//! `[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 (F9), both legs as one program; -//! - RULINGS 13 + 22: every row the emitter emits per FRI layer (group and +//! 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. @@ -100,7 +100,7 @@ fn the_in_guest_fri_shape_is_the_hosts_layout() { } // ============================================================================= -// G2 — the emitted verifier on I-FRI-H's RPX vectors +// G2 — the emitted verifier on the host's RPX vectors // ============================================================================= fn ext_of(v: &Value) -> FEE { @@ -201,8 +201,8 @@ impl Vector { /// ★ 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, REVIEW-FRI F6) and both capped Q = 20 -/// formats (F9) — with the vector's schedule, depths and caps derived by the +/// 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() { @@ -351,10 +351,10 @@ fn the_slot_check_is_load_bearing() { } // ============================================================================= -// F9 — the {cap} × {fri} round-trip matrix, both legs, on a real proof +// The {cap} × {fri} round-trip matrix, both legs, on a real proof // ============================================================================= -/// ★ REVIEW-FRI F9's matrix on a real laptop-scale proof (L2G_MEMORY, 2048 +/// ★ 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 @@ -458,7 +458,7 @@ fn the_cap_and_fri_matrix_round_trips_in_guest() { } // ============================================================================= -// RULINGS 13 + 22 — every emitted row per FRI layer and per DEEP point, against +// Every emitted row per FRI layer and per DEEP point, against // the host's cost model (`stark::fri::schedule`, `stark::leaf_layout`) // ============================================================================= @@ -593,7 +593,7 @@ fn fri_layer_program(d: u32, c: usize, times: usize) -> LfmProgram { compile(b.finish()) } -/// ★ RULINGS 13 + 22: the rows one query's opening of a committed FRI layer +/// ★ 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): @@ -719,7 +719,7 @@ fn deep_point_program(shape: &DeepShape, times: usize) -> LfmProgram { compile(b.finish()) } -/// ★ RULINGS 22: the XALU rows of ONE in-guest DEEP point EQUAL the S2 `auto` +/// ★ 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 diff --git a/prover/src/lfm/fri_tests.rs b/prover/src/lfm/fri_tests.rs index e405825ba..b68c59479 100644 --- a/prover/src/lfm/fri_tests.rs +++ b/prover/src/lfm/fri_tests.rs @@ -1317,7 +1317,7 @@ fn the_fri_leg_proves_and_verifies() { } // ============================================================================= -// Merkle caps in the FRI leg (S1, design/CAP.md §6.1, C5) +// Merkle caps in the FRI leg (S1) // ============================================================================= /// The folding fixture's options under a cap policy: blowup 2, `queries` @@ -1408,7 +1408,7 @@ fn the_fri_emitter_verifies_a_capped_folding_proof() { /// ★ 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 -/// (REVIEW-CAP M1(b) in-guest). One query at a height-3 cap reaches one of +/// (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] diff --git a/prover/src/lfm/merkle_cap.rs b/prover/src/lfm/merkle_cap.rs index c5d7612e6..c71521e72 100644 --- a/prover/src/lfm/merkle_cap.rs +++ b/prover/src/lfm/merkle_cap.rs @@ -1,6 +1,5 @@ //! ★ One tree's authenticated Merkle cap, in-guest — the one gadget the WHIR -//! chain verifier (W1) and the STARK sub-proof verifier (S1) share -//! (design/CAP.md §6.1, §6.2, §9.2; REVIEW-CAP S1). +//! 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 @@ -10,18 +9,18 @@ //! 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 (REVIEW-CAP (e)). +//! 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 (REVIEW-CAP (d)). +//! 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 (RULINGS 1). +//! 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 diff --git a/prover/src/lfm/one_row_guest_tests.rs b/prover/src/lfm/one_row_guest_tests.rs index ac036401b..4826044de 100644 --- a/prover/src/lfm/one_row_guest_tests.rs +++ b/prover/src/lfm/one_row_guest_tests.rs @@ -1,11 +1,11 @@ -//! S2 in the in-guest (LFM) STARK verifier (G3, design/FRI.md §7.2–§7.4, -//! §11): one-row trace leaves, DEEP at ONE point, the committed FRI input and +//! 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 I-S2-H's checked-in RPX (e) proofs +//! - 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 @@ -129,7 +129,7 @@ fn an_unresolved_auto_layout_is_refused() { } // ============================================================================= -// (e) — the emitted FRI verifier on I-S2-H's one-row RPX proofs +// (e) — the emitted FRI verifier on the host's one-row RPX proofs // ============================================================================= fn ext_of(v: &Value) -> FEE { @@ -366,8 +366,8 @@ fn no_tampered_input_tree_value_can_pass() { } } -/// ★ The INPUT-SLOT check is LOAD-BEARING (the in-guest M1 at the input tree, -/// FRI.md §7.7). Under one-row leaves `DEEP(x_r)` meets the committed FRI +/// ★ 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, @@ -403,7 +403,7 @@ fn the_input_slot_check_is_load_bearing() { // ============================================================================= /// ★ The in-guest trace leaf at `rows_per_leaf = 1` (and at 2, today's) is -/// the host's: every leaf of I-S2-H's (e) KAT matrices (16 rows × 5 base +/// 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`. @@ -692,7 +692,7 @@ fn a_one_row_and_a_row_pair_table_verify_in_one_program() { /// The one-row query index needs a schedule override that the DP never /// picks to exercise unequal neighbouring exponents from the INPUT tree -/// (REVIEW-FRI F6 at layer 0): `[3, 1, 3, 2]` over the 9 committed folds of a +/// (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] diff --git a/prover/src/lfm/one_row_tests.rs b/prover/src/lfm/one_row_tests.rs index e145eccfa..c5d81566f 100644 --- a/prover/src/lfm/one_row_tests.rs +++ b/prover/src/lfm/one_row_tests.rs @@ -1,5 +1,5 @@ -//! S2 (one-row openings) on the LFM side, host only (design/FRI.md §7.5.2–4, -//! REVIEW-FRI F8): the commit helpers at both leaf layouts, the registry +//! 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 @@ -59,7 +59,7 @@ fn the_commit_helpers_follow_the_layout() { assert_ne!(row, pair); } -/// ★ The registry policy (FRI.md §7.5.4): `LFM_REGISTRY` stays row-pair only. +/// ★ 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 @@ -93,7 +93,7 @@ fn a_one_row_format_never_reads_the_registry() { 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 (RULINGS 14), not recomputes. + // BITWISE roots are hard misses, not recomputes. assert_eq!(one_row.roots[13], None); assert_eq!(one_row.roots[14], None); } @@ -166,7 +166,7 @@ fn digest_bytes(public: &[(u32, LfmWord)]) -> [u8; 32] { } /// ★ The in-circuit register commitment against its host twin at BOTH leaf -/// layouts (FRI.md §7.5.3). A mismatch would show only as a runtime +/// 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] diff --git a/prover/src/lfm/proof.rs b/prover/src/lfm/proof.rs index 2cf740752..c54a62928 100644 --- a/prover/src/lfm/proof.rs +++ b/prover/src/lfm/proof.rs @@ -294,7 +294,7 @@ pub(crate) fn prove_traces_with_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` (RULINGS 14). + // 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); } @@ -467,7 +467,7 @@ pub fn verify_against_chunked( /// [`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, RULINGS 14). +/// rejects). #[allow(clippy::too_many_arguments)] fn verify_against_chunked_with( one_row_roots: Option<&super::registry::LfmOneRowRoots>, @@ -608,7 +608,7 @@ pub fn aggregation_wrap_options() -> ProofOptions { /// /// 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; RULINGS 26). +/// (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 c88b1cce4..9835f8bab 100644 --- a/prover/src/lfm/registry.rs +++ b/prover/src/lfm/registry.rs @@ -64,7 +64,7 @@ impl LfmProgramKind { /// ★ The artifacts a fixture program is verified against under `options`. /// -/// The registry policy (design/FRI.md §7.5.4): `LFM_REGISTRY` is blessed at +/// 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 @@ -159,7 +159,7 @@ impl LfmRegistryEntry { hasher: self.hasher, chip_set: self.chip_set, program_id: self.program_id, - // The registry is ROW-PAIR ONLY (design/FRI.md §7.5.4): a one-row + // The registry is ROW-PAIR ONLY: a one-row // format never reads it — `resolve_artifacts` builds at run time. one_row_roots: None, } @@ -216,7 +216,7 @@ pub struct LfmArtifacts { 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, RULINGS 14). Slot 12 (`KECCAK_RND`) has no + /// 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`. diff --git a/prover/src/lfm/sub_proof.rs b/prover/src/lfm/sub_proof.rs index 791c56564..c5c7bbffc 100644 --- a/prover/src/lfm/sub_proof.rs +++ b/prover/src/lfm/sub_proof.rs @@ -51,7 +51,7 @@ //! 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, design/FRI.md §7) +//! # 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 @@ -102,7 +102,7 @@ impl GroupShape { /// 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, design/FRI.md §7.4). + /// under one-row leaves (S2). pub fn values_at(&self, rows_per_leaf: usize) -> usize { rows_per_leaf * self.num_columns } @@ -145,11 +145,11 @@ pub struct SubProofShape { /// 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 (design/CAP.md §6.1). A verifier + /// 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, design/FRI.md §7): today's row + /// 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 @@ -748,7 +748,7 @@ pub fn emit_query_from_bits( } } -/// The one-row half of [`emit_query_from_bits`] (S2, design/FRI.md §7.4), after +/// 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]`). diff --git a/prover/src/lfm/whir_chain_tests.rs b/prover/src/lfm/whir_chain_tests.rs index 96c530843..ba5854d12 100644 --- a/prover/src/lfm/whir_chain_tests.rs +++ b/prover/src/lfm/whir_chain_tests.rs @@ -767,7 +767,7 @@ const COST_SHAPES: [(usize, usize, u8); 5] = /// ★ 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]` (design/WHIR.md §4.8), each at grind 0 and 8 for the reason +/// `[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. @@ -1543,7 +1543,7 @@ fn the_schedule_is_the_host_transcripts_under_the_cap() { } /// ★ 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 — REVIEW-CAP M1(b)), +/// (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] @@ -1626,7 +1626,7 @@ fn a_tampered_capped_chain_cannot_execute() { /// [`the_production_chain_costs_what_the_census_quotes`] and /// [`the_production_shape_reproduces_the_campaigns_permutation_count`]. /// -/// Hand derivation (design/CAP.md §10): trees of depth 23, 19, 15, 11, 7, 3, 2 +/// 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` @@ -1694,7 +1694,7 @@ fn the_production_chain_emits_its_closed_form_under_the_auto_cap() { assert_eq!(perm_rows(&program), chain_perms(&shape, entry)); } -/// ⛔ RULINGS 4: `PREPARED_LEG_ROWS` is a ROUTING constant and stays fixed +/// ⛔ `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 @@ -1838,9 +1838,9 @@ fn a_tampered_first_fold_chain_cannot_execute() { /// 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) are design/WHIR.md §4.8's, -/// from D-WHIR's independent Python re-implementation of these forms -/// (`whir_model.py`), which reproduces today's 22,828 / 185,509: first6 +/// 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] @@ -1881,12 +1881,12 @@ fn the_first_fold_production_chains_cost_what_the_design_derived() { } } -/// ★ RULINGS 26: THE PRODUCTION DEFAULT CHAIN — what `chain_config` builds with +/// ★ 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 the WHIR block measured together -/// (wt54–wt57, −9.10 s): W2's six rounds and W1's cap. +/// 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( @@ -1935,7 +1935,7 @@ fn the_production_default_chain_is_first6_under_the_auto_cap() { const { assert!(DEFAULT_CHAIN_PERMS < 18_729 && DEFAULT_CHAIN_PERMS < 19_877) }; } -/// The production default chain's pins (RULINGS 26), derived by the closed +/// 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]; @@ -2004,7 +2004,7 @@ fn the_first_fold_production_chains_emit_their_closed_forms() { } } -/// ⛔ `PREPARED_LEG_ROWS` stays FIXED under the fold knob (RULINGS 15), and +/// ⛔ `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 diff --git a/prover/src/lfm/whir_epoch_program_tests.rs b/prover/src/lfm/whir_epoch_program_tests.rs index a52bcd0ff..d70a6f9c2 100644 --- a/prover/src/lfm/whir_epoch_program_tests.rs +++ b/prover/src/lfm/whir_epoch_program_tests.rs @@ -151,7 +151,7 @@ fn the_production_epoch_recount() { // ⚠ 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; RULINGS 26) proves this epoch in 8 x 6 = 48 rounds — asserted + // 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); diff --git a/prover/src/lfm/whir_open_tests.rs b/prover/src/lfm/whir_open_tests.rs index a50a7e332..fa6804649 100644 --- a/prover/src/lfm/whir_open_tests.rs +++ b/prover/src/lfm/whir_open_tests.rs @@ -680,7 +680,7 @@ fn the_cap_mux_selects_every_index() { } } -/// ★ REVIEW-CAP M1(b) in-guest: a cap word NO opening reaches, tampered. The +/// ★ 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. diff --git a/prover/src/lfm/whir_statement.rs b/prover/src/lfm/whir_statement.rs index 6560bbdb1..fd28a4b2e 100644 --- a/prover/src/lfm/whir_statement.rs +++ b/prover/src/lfm/whir_statement.rs @@ -108,7 +108,7 @@ fn push_config(bytes: &mut Vec, config: &ChainConfig) { // ⚠ 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 lane that changes a lever's effect on the statement + // 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: _, diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 874a6a283..66db8cf3c 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -1391,7 +1391,7 @@ pub(crate) fn compute_commit_bus_offset( /// 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 (RULINGS 14): the +/// `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], diff --git a/prover/src/recursion.rs b/prover/src/recursion.rs index 96a2fb02f..ab7065a36 100644 --- a/prover/src/recursion.rs +++ b/prover/src/recursion.rs @@ -42,7 +42,7 @@ pub const MIN_PROOF_OPTIONS: ProofOptions = ProofOptions { coset_offset: 3, grinding_factor: 1, fri_final_poly_log_degree: 7, - // RULINGS 26: the RV64 guest verifies the LEGACY format, named here rather + // The RV64 guest verifies the LEGACY format, named here rather // than inherited from a default. format: stark::proof::options::ProofFormat::LEGACY, }; @@ -78,7 +78,7 @@ 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 (RULINGS 26): the RV64 guest's archived verifier is + /// 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`]). @@ -276,8 +276,8 @@ pub fn program_id_from_elf( )) } -/// The RV64 recursion guest verifies the LEGACY proof format only (RULINGS 11, -/// as amended by RULINGS 26): its presets fix the options at build time and +/// 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 diff --git a/prover/src/tables/bitwise.rs b/prover/src/tables/bitwise.rs index 78e1c2b38..da9d946fc 100644 --- a/prover/src/tables/bitwise.rs +++ b/prover/src/tables/bitwise.rs @@ -221,7 +221,7 @@ fn static_commitment(blowup_factor: u8) -> Option { /// [`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 (RULINGS 14): no recompute. +/// 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([ @@ -497,7 +497,7 @@ pub fn preprocessed_commitment(options: &ProofOptions) -> Commitment { /// 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 (RULINGS 14), never 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, diff --git a/prover/src/tables/keccak_rc.rs b/prover/src/tables/keccak_rc.rs index 7fd256a17..a036aee04 100644 --- a/prover/src/tables/keccak_rc.rs +++ b/prover/src/tables/keccak_rc.rs @@ -120,7 +120,7 @@ fn static_commitment(blowup_factor: u8) -> Option { /// [`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 (RULINGS 14): no recompute. +/// 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([ @@ -222,7 +222,7 @@ pub fn preprocessed_commitment(options: &ProofOptions) -> Commitment { /// 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, RULINGS 14). +/// hard miss, never a recompute). pub fn preprocessed_commitment_for( options: &ProofOptions, layout: LeafLayout, diff --git a/prover/src/tables/mod.rs b/prover/src/tables/mod.rs index 5d57eeb87..8835e39d0 100644 --- a/prover/src/tables/mod.rs +++ b/prover/src/tables/mod.rs @@ -64,9 +64,9 @@ 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 (design/FRI.md §7.5: 4 for +/// 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 (RULINGS 14), never a recompute. +/// 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. diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index 2016b87d8..ac53351d3 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -434,7 +434,7 @@ pub(crate) fn static_zero_page_commitment(blowup_factor: u8) -> Option Option { match blowup_factor { 4 => Some([ @@ -477,7 +477,7 @@ pub(crate) fn static_private_page_commitment(blowup_factor: u8) -> Option Option { match blowup_factor { 4 => Some([ @@ -674,7 +674,7 @@ pub fn private_page_preprocessed_commitment(options: &ProofOptions) -> Commitmen /// 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, RULINGS 14). +/// 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, diff --git a/prover/src/tests/multilinear_bench_tests.rs b/prover/src/tests/multilinear_bench_tests.rs index af5b9558a..88f329bec 100644 --- a/prover/src/tests/multilinear_bench_tests.rs +++ b/prover/src/tests/multilinear_bench_tests.rs @@ -1165,7 +1165,7 @@ 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(); - // ★ RULINGS 26: the bases were MEASURED at the legacy WHIR format (uniform + // ★ 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. @@ -1449,7 +1449,7 @@ 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. - // ⚠ AT THE LEGACY WHIR FORMAT, named (RULINGS 26): the bases and lb17/lb18 + // ⚠ 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( @@ -1559,7 +1559,7 @@ 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. - // ⚠ THE LEGACY WHIR FORMAT (RULINGS 26): lb17/lb18 ran before the flip; + // ⚠ 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, @@ -1710,7 +1710,7 @@ fn the_prepared_opening_is_the_schedule_the_shape_implies() { columns, "one placement per column, which is what the opening's wrapper absorbs" ); - // ★ RULINGS 26: the DECODE group is committed under the PROCESS format + // ★ 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, diff --git a/prover/src/tests/static_commitments_tests.rs b/prover/src/tests/static_commitments_tests.rs index 4169a70c6..5e051217a 100644 --- a/prover/src/tests/static_commitments_tests.rs +++ b/prover/src/tests/static_commitments_tests.rs @@ -298,7 +298,7 @@ fn bitwise_non_three_coset_recomputes_and_differs_from_static() { } // ========================================================================= -// One-row (S2) twins: design/FRI.md §7.5.1, RULINGS 14 +// 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 @@ -382,7 +382,7 @@ fn pages_one_row_static_match_recompute() { } } -/// RULINGS 14: under one row, a blowup with no twin and a non-3 coset are +/// 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. diff --git a/prover/src/tests/transcript_counts.rs b/prover/src/tests/transcript_counts.rs index 22206884d..07677dafa 100644 --- a/prover/src/tests/transcript_counts.rs +++ b/prover/src/tests/transcript_counts.rs @@ -177,7 +177,7 @@ fn schedule(num_vars: usize, k: usize) -> Vec { /// 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 since RULINGS 26) is priced as it is proved. +/// 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() { diff --git a/prover/src/tests/zf_air_cache_tests.rs b/prover/src/tests/zf_air_cache_tests.rs index cf3d8ba88..cdba728e9 100644 --- a/prover/src/tests/zf_air_cache_tests.rs +++ b/prover/src/tests/zf_air_cache_tests.rs @@ -2,20 +2,19 @@ //! FORMAT: an AIR built for one format and asked for under another is a //! different verifier. //! -//! Before this test the key was `(name, blowup, queries, coset, grinding, -//! final degree)` — every `ProofOptions` field except `format`, which the ZF -//! campaign added later. The first AIR built in a process then fixed the -//! format of every later AIR with the same name and parameters, whatever -//! format the caller asked for. Two gate reds on candidate-b were this and -//! nothing else: +//! 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 cached capped -//! AIRs, so `verify_with_options(.., &default, ..)` verified the capped proof -//! with those capped AIRs and accepted it. -//! - `zf_vm_dp_tests`: in a fresh process the dp prove cached dp AIRs and the -//! "default" verifier accepted the dp proof; in the lib suite an earlier test -//! had cached default AIRs, so the dp prove proved at `pair` and the -//! non-vacuity assertion fired. +//! - `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. diff --git a/prover/src/tests/zf_rpx_device_tests.rs b/prover/src/tests/zf_rpx_device_tests.rs index f675e3e27..10467e071 100644 --- a/prover/src/tests/zf_rpx_device_tests.rs +++ b/prover/src/tests/zf_rpx_device_tests.rs @@ -1,4 +1,4 @@ -//! S3 on the device under the production RPX pin (lane I-FRI-D, D1): the RPX +//! 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`). //! @@ -13,7 +13,7 @@ //! -- --ignored --exact --test-threads=1 //! ``` //! -//! S2 on the device (lane I-S2-D, D2): `trees_one_row_rpx` (default threshold), +//! 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`. @@ -82,8 +82,8 @@ fn proved_rpx_vectors_equal_the_cpu_bytes() { 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, - // I-FIX-D2); a host composition would not be counted here. + // 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)" @@ -96,7 +96,7 @@ fn proved_rpx_vectors_equal_the_cpu_bytes() { } // --------------------------------------------------------------------------- -// S2 on the device (FRI.md §7.6, lane I-S2-D, D2) under the RPX pin. +// S2 on the device under the RPX pin. // --------------------------------------------------------------------------- /// One-row main / preprocessed split / aux (host and resident) / composition @@ -162,8 +162,8 @@ fn proved_rpx_one_row_vectors_equal_the_cpu_bytes() { "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, - // I-FIX-D2); a host composition would not be counted here. + // 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)" diff --git a/prover/src/tests/zf_rpx_golden_tests.rs b/prover/src/tests/zf_rpx_golden_tests.rs index 599736c22..e750ce706 100644 --- a/prover/src/tests/zf_rpx_golden_tests.rs +++ b/prover/src/tests/zf_rpx_golden_tests.rs @@ -1,14 +1,14 @@ -//! Golden proofs under the production RPX pin (REVIEW-FRI F1), in TWO formats: +//! 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 pre-campaign bytes pinned after the default flip, so +//! 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()`, RULINGS 26): -//! the bytes every production site now stamps. Pinned at the default flip -//! (lane I-FLIP); regenerate only for a deliberate format change. +//! - 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 @@ -230,7 +230,7 @@ const GOLDENS: &[(&str, &str)] = &[ ), ]; -/// The LEGACY-format RPX goldens: the pre-campaign bytes, unmoved by the +/// The LEGACY-format RPX goldens: the legacy bytes, unmoved by the /// default flip. #[test] fn legacy_format_rpx_goldens_are_byte_identical() { @@ -245,7 +245,7 @@ fn legacy_format_rpx_goldens_are_byte_identical() { } } -/// The PRODUCTION-default RPX goldens (RULINGS 26: cap auto, `fri=dp`, and +/// The PRODUCTION-default RPX goldens (cap auto, `fri=dp`, and /// whatever `ZfFormat::DEFAULT` stamps). A move here is a production format /// change. #[test] @@ -324,7 +324,7 @@ fn rpx_dp_round_trips() { } } -/// REVIEW-FRI F1.2 under RPX: the group path at an all-ones schedule commits +/// 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). @@ -369,7 +369,7 @@ fn production_sites_prove_at_the_process_format() { .ok() .map(|v| v.trim().to_ascii_lowercase()) }; - // An unset knob is the production default's value (RULINGS 26). + // 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, diff --git a/prover/src/tests/zf_rpx_vectors.rs b/prover/src/tests/zf_rpx_vectors.rs index ccc30c38d..a5e3b3c2e 100644 --- a/prover/src/tests/zf_rpx_vectors.rs +++ b/prover/src/tests/zf_rpx_vectors.rs @@ -1,4 +1,4 @@ -//! The exported S3 and S2 vectors (FRI.md §10 (c), (d), (e)) under the production RPX pin, +//! 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 diff --git a/prover/src/tests/zf_vm_one_row_tests.rs b/prover/src/tests/zf_vm_one_row_tests.rs index 6800212fd..459fd70b5 100644 --- a/prover/src/tests/zf_vm_one_row_tests.rs +++ b/prover/src/tests/zf_vm_one_row_tests.rs @@ -4,13 +4,13 @@ //! - 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). -//! - RULINGS 14 at the VM level: at blowup 2 there is no one-row twin, so +//! - 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). -//! - Lane I-FIX-S2's regression: the Phase-A replay that recovers `z`, `α` for +//! - 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 @@ -110,7 +110,7 @@ fn a_vm_proof_round_trips_at_one_row_auto_with_dp() { ); } -/// RULINGS 14: no one-row static twin at blowup 2 ⇒ a proving error naming +/// 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() { @@ -161,7 +161,7 @@ fn an_lfm_proof_round_trips_at_one_row() { ); } -/// ★ REGRESSION (lane I-FIX-S2): one-row PREPROCESSED tables and the Phase-A +/// ★ 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 diff --git a/prover/src/zf_format.rs b/prover/src/zf_format.rs index fc0e3b8ab..05842565d 100644 --- a/prover/src/zf_format.rs +++ b/prover/src/zf_format.rs @@ -1,4 +1,4 @@ -//! ★ The proof FORMAT this process proves under — the ZF campaign's levers. +//! ★ 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) @@ -8,12 +8,12 @@ //! LAMBDA_VM_ZF_WHIR_FOLDS uniform4 | first5 | first6 WHIR first-round fold (W2) //! ``` //! -//! ★ Every unset knob is [`ZfFormat::DEFAULT`], the MEASURED configuration -//! (RULINGS 26): `cap=auto whir_cap=auto fri=dp one_row=0 whir_folds=first6`. +//! ★ 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 pre-campaign format, byte for byte — +//! 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 @@ -40,9 +40,9 @@ //! //! **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 -//! while the campaign lands them), and a knob set on a build that only parses -//! it would print a non-default format and prove the default one. Each lane -//! flips its `*_IMPLEMENTED` constant when its lever is real. +//! 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`. @@ -95,10 +95,14 @@ impl Default for ZfFormat { impl ZfFormat { /// ★ The production format when no knob is set: the MEASURED - /// configuration (RULINGS 26). S1 `cap=auto` (STARK block −15.35 s), + /// 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. Security parameters (queries, grinding, blowup) are the + /// 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, @@ -108,9 +112,9 @@ impl ZfFormat { whir_folds: WhirFolds::First(DEFAULT_WHIR_FIRST_FOLD), }; - /// The pre-campaign format: every lever off. What all five knobs at their + /// 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 (RULINGS 26). + /// only format the RV64 recursion guest verifies. pub const LEGACY: Self = Self { cap: CapPolicy::Off, whir_cap: CapPolicy::Off, @@ -120,7 +124,7 @@ impl ZfFormat { }; /// True when every lever is off: the format proves exactly what the - /// pre-campaign prover proved. + /// prover proved before any lever existed. pub fn is_legacy(&self) -> bool { self.cap.is_off() && self.whir_cap.is_off() @@ -306,12 +310,12 @@ 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 RULINGS 15 builds. +/// 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`: RULINGS -/// 15, no DP. Widening this list is a format decision, not a parser one. +/// 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`. @@ -356,7 +360,7 @@ mod tests { ZfFormat::from_lookup(|k| map.get(k).cloned()) } - /// ★ RULINGS 26: with no knob set the process proves the MEASURED + /// ★ With no knob set the process proves the MEASURED /// configuration. #[test] fn nothing_set_is_the_measured_default() { @@ -382,7 +386,7 @@ mod tests { } /// Every knob keeps its OFF spelling, and all five at off are the legacy - /// (pre-campaign) format: the rollback and A/B arm. + /// format (every lever off): the rollback and A/B arm. #[test] fn the_off_spellings_parse_to_the_legacy_format() { let f = parse(&[ @@ -594,8 +598,8 @@ mod tests { #[test] fn the_merkle_cap_knob_is_selectable() { - // C3 + C4 made the STARK cap real, so `LAMBDA_VM_ZF_CAP` no longer - // aborts; every spelling reaches the options unchanged. + // 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), @@ -784,7 +788,7 @@ mod tests { assert_eq!(o.format, want, "{site}"); assert!(!o.has_legacy_format(), "{site}"); } - // Security parameters are the legacy presets' (RULINGS 26). + // Security parameters are the legacy presets'. let base = crate::lfm::proof::block_base_options(); let preset = crate::recursion::Preset::Blowup4.options(); assert_eq!( @@ -808,7 +812,7 @@ mod tests { assert_eq!(chain.log_folding, PRODUCTION_WHIR_LOG_FOLDING); } - /// ★ RULINGS 26 (RULINGS 11 amended): the RV64 guest verifier stays on + /// ★ 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. @@ -879,7 +883,7 @@ mod tests { #[test] fn the_serialized_options_bytes_ignore_the_format_fields() { - // RULINGS 10: the format fields are skipped by serde and rkyv, so a + // 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(); diff --git a/prover/tests/merkle_cap_vm.rs b/prover/tests/merkle_cap_vm.rs index 8a5c47293..b9cbee11e 100644 --- a/prover/tests/merkle_cap_vm.rs +++ b/prover/tests/merkle_cap_vm.rs @@ -1,5 +1,5 @@ //! A real VM proof under the Merkle cap policy the PROCESS FORMAT names -//! (`LAMBDA_VM_ZF_CAP`, design/CAP.md §4): every production table — the +//! (`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` /