diff --git a/.github/workflows/pcs.yml b/.github/workflows/pcs.yml
index e565538f..bb91cdeb 100644
--- a/.github/workflows/pcs.yml
+++ b/.github/workflows/pcs.yml
@@ -6,18 +6,35 @@ on:
paths:
- 'misc/pcs.tex'
- '.github/workflows/pcs.yml'
+ pull_request:
+ paths:
+ - 'misc/pcs.tex'
+ - '.github/workflows/pcs.yml'
workflow_dispatch:
-permissions:
- contents: write
-
concurrency:
group: pcs-${{ github.ref }}
cancel-in-progress: true
jobs:
+ check-pdf:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+ - name: Compile LaTeX
+ uses: xu-cheng/latex-action@v3
+ with:
+ working_directory: misc
+ root_file: pcs.tex
+
build-pdf:
+ if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
- uses: actions/checkout@v4
- name: Compile LaTeX
@@ -26,7 +43,6 @@ jobs:
working_directory: misc
root_file: pcs.tex
- name: Publish PDF as release asset
- if: github.event_name != 'pull_request'
uses: softprops/action-gh-release@v2
with:
tag_name: pcs-latest
diff --git a/.github/workflows/pdf.yml b/.github/workflows/pdf.yml
index 7098ce24..b9434ccf 100644
--- a/.github/workflows/pdf.yml
+++ b/.github/workflows/pdf.yml
@@ -7,18 +7,36 @@ on:
- 'misc/doc.tex'
- 'misc/images/**'
- '.github/workflows/pdf.yml'
+ pull_request:
+ paths:
+ - 'misc/doc.tex'
+ - 'misc/images/**'
+ - '.github/workflows/pdf.yml'
workflow_dispatch:
-permissions:
- contents: write
-
concurrency:
group: pdf-${{ github.ref }}
cancel-in-progress: true
jobs:
+ check-pdf:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+ - name: Compile LaTeX
+ uses: xu-cheng/latex-action@v3
+ with:
+ working_directory: misc
+ root_file: doc.tex
+
build-pdf:
+ if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
- uses: actions/checkout@v4
- name: Compile LaTeX
@@ -27,7 +45,6 @@ jobs:
working_directory: misc
root_file: doc.tex
- name: Publish PDF as release asset
- if: github.event_name != 'pull_request'
uses: softprops/action-gh-release@v2
with:
tag_name: spec-latest
diff --git a/.github/workflows/ring-switching.yml b/.github/workflows/ring-switching.yml
index f0588d7f..71eba98c 100644
--- a/.github/workflows/ring-switching.yml
+++ b/.github/workflows/ring-switching.yml
@@ -6,18 +6,35 @@ on:
paths:
- 'misc/ring-switching.tex'
- '.github/workflows/ring-switching.yml'
+ pull_request:
+ paths:
+ - 'misc/ring-switching.tex'
+ - '.github/workflows/ring-switching.yml'
workflow_dispatch:
-permissions:
- contents: write
-
concurrency:
group: ring-switching-${{ github.ref }}
cancel-in-progress: true
jobs:
+ check-pdf:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+ - name: Compile LaTeX
+ uses: xu-cheng/latex-action@v3
+ with:
+ working_directory: misc
+ root_file: ring-switching.tex
+
build-pdf:
+ if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
- uses: actions/checkout@v4
- name: Compile LaTeX
@@ -26,7 +43,6 @@ jobs:
working_directory: misc
root_file: ring-switching.tex
- name: Publish PDF as release asset
- if: github.event_name != 'pull_request'
uses: softprops/action-gh-release@v2
with:
tag_name: ring-switching-latest
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
new file mode 100644
index 00000000..aa8d84e4
--- /dev/null
+++ b/.github/workflows/rust.yml
@@ -0,0 +1,32 @@
+name: Rust
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: rust-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ components: clippy
+ - uses: Swatinem/rust-cache@v2
+ - name: Test
+ run: cargo test --release --workspace
+ - name: Clippy
+ run: cargo clippy --release --workspace --all-targets -- -D warnings
+ - name: Rustdoc
+ env:
+ RUSTDOCFLAGS: -D warnings
+ run: cargo doc --release --workspace --no-deps
diff --git a/Cargo.lock b/Cargo.lock
index d4203dbc..8151ed67 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -152,7 +152,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 2.0.118",
+ "syn",
]
[[package]]
@@ -278,6 +278,7 @@ dependencies = [
"blake3",
"lean_vm",
"primitives",
+ "rand",
]
[[package]]
@@ -355,6 +356,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
name = "pcs"
version = "0.1.0"
dependencies = [
+ "bincode",
"blake3",
"fiat_shamir",
"primitives",
@@ -382,6 +384,7 @@ dependencies = [
name = "primitives"
version = "0.1.0"
dependencies = [
+ "bincode",
"rayon",
"serde",
"tracing-forest",
@@ -522,7 +525,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.118",
+ "syn",
]
[[package]]
@@ -563,35 +566,24 @@ dependencies = [
"unicode-ident",
]
-[[package]]
-name = "syn"
-version = "3.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
[[package]]
name = "thiserror"
-version = "2.0.19"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "2.0.19"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.2",
+ "syn",
]
[[package]]
@@ -622,7 +614,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.118",
+ "syn",
]
[[package]]
@@ -775,5 +767,5 @@ checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.118",
+ "syn",
]
diff --git a/README.md b/README.md
index 5f8b682d..de58b90e 100644
--- a/README.md
+++ b/README.md
@@ -1,72 +1,96 @@
leanVM-b
-
+
-
+
+
-- WARNING: Highly experimental / sloppy (currently).
-- Proving architecture is volontarily kept simple for now
+- Warning: highly experimental.
+- The proving architecture is intentionally kept simple.
# Benchmarks
-Machine: M4 Max
+Measured on an AMD Ryzen 7 PRO 8700GE with 11 Rayon workers. Timings vary by machine; cycles and proof sizes describe the protocol configuration more reliably.
### XMSS aggregation
```bash
-RAYON_NUM_THREADS=11 cargo run --release -- xmss --n-signatures 890
+RAYON_NUM_THREADS=11 cargo run --release -- xmss --n-signatures 890 --log-inv-rate 1
```
```
XMSS aggregation, 890 signatures
- cycles (VM steps) : 1,528,683 = 2^20.544 ( 1,717.621 / XMSS)
- XOR instructions : 125,491 = 2^16.937 ( 141.001 / XMSS)
- MUL instructions : 295,495 = 2^18.173 ( 332.017 / XMSS)
- SET instructions : 354,194 = 2^18.434 ( 397.971 / XMSS)
- DEREF instructions : 508,569 = 2^18.956 ( 571.426 / XMSS)
- JUMP instructions : 114,813 = 2^16.809 ( 129.003 / XMSS)
- BLAKE3 instructions : 130,121 = 2^16.989 ( 146.203 / XMSS)
- committed witness size : 2^25.662
- data memory : 2^22 padded (2^21.707 used)
- proof size : 588.68 KiB
- proving (incl. witness gen) : 1.358 s
- verifying : 0.00594 s
- throughput : 655.559 XMSS/s
+ cycles (VM steps) : 1,513,580 = 2^20.53 ( 1,700.652 / XMSS)
+ XOR instructions : 125,491 = 2^16.937 ( 141.001 / XMSS)
+ MUL instructions : 292,821 = 2^18.16 ( 329.012 / XMSS)
+ SET instructions : 341,765 = 2^18.383 ( 384.006 / XMSS)
+ DEREF instructions : 508,569 = 2^18.956 ( 571.426 / XMSS)
+ JUMP instructions : 114,813 = 2^16.809 ( 129.003 / XMSS)
+ BLAKE3 instructions : 130,121 = 2^16.989 ( 146.203 / XMSS)
+ PACK64X2 instructions : 0 = - ( 0 / XMSS)
+ committed witness size : 2^26.364
+ data memory : 2^22 padded (2^21.701 used)
+ proof size : 359.617 KiB
+ proving (incl. witness gen) : 1.905 s
+ verifying : 0.00426 s
+ throughput : 467.072 XMSS/s
```
### Recursion
```bash
-RAYON_NUM_THREADS=11 cargo run --release -- recursion --n 2
+RAYON_NUM_THREADS=11 cargo run --release -- recursion --n 2 --log-inv-rate 2
```
```
-recursion 2→1: 2 inner proofs of 852,207 cycles each
- guest cycles (VM steps) : 1,955,570 = 2^20.899 (1.147 / inner cycle)
- XOR instructions : 519,080 = 2^18.986
- MUL instructions : 651,514 = 2^19.313
- SET instructions : 161,965 = 2^17.305
- DEREF instructions : 561,871 = 2^19.1
- JUMP instructions : 18,431 = 2^14.17
- BLAKE3 instructions : 42,709 = 2^15.382
- committed witness size : 2^25.902
- data memory : 2^22 padded (2^21.21 used)
- recursive proof size : 590.859 KiB
- outer proving : 1.548 s
- complete recursive verify : 0.292 s
+recursion 2→1: 2 inner proofs of 1,472,224 cycles each
+ guest cycles (VM steps) : 869,886 = 2^19.73 (0.295 / inner cycle)
+ XOR instructions : 215,490 = 2^17.717
+ MUL instructions : 268,787 = 2^18.036
+ SET instructions : 70,556 = 2^16.106
+ DEREF instructions : 265,183 = 2^18.017
+ JUMP instructions : 9,299 = 2^13.183
+ BLAKE3 instructions : 18,115 = 2^14.145
+ PACK64X2 instructions : 22,456 = 2^14.455
+ committed witness size : 2^25.506
+ data memory : 2^21 padded (2^20.044 used)
+ recursive proof size : 235.305 KiB
+ outer proving : 1.26 s
+ complete recursive verify : 0.0303 s
```
-## Security, proof size etc
+### Fibonacci
-- security = 120 bits, proven, unique-decoding regime, Ligerito
-- proof size = BIG (≈ 0.7 MiB)
-Both will be improved later.
+```bash
+RAYON_NUM_THREADS=11 cargo run --release -- fibonacci --n 2000000 --log-inv-rate 1
+```
+
+```
+Fibonacci (in the exponent, i.e. modulo 2^64 - 1), N = 2,000,000
+ cycles (VM steps) : 2,034,017
+ XOR instructions : 2^10.966
+ MUL instructions : 2^20.937
+ SET instructions : 2^12.552
+ DEREF instructions : 2^13.967
+ JUMP instructions : 2^10.968
+ BLAKE3 instructions : 0
+ PACK64X2 instructions : 0
+ committed witness size : 2^25.658
+ proof size : 336.4 KiB
+ proving (incl. witness gen) : 1.031051291s
+ verifying : 3.274ms
+ throughput : 1,972,760 cycles/s
+```
+
+## Security
+
+- 128-bit proven (LDR Johnson)
## Credits
diff --git a/crates/fiat_shamir/src/sponge.rs b/crates/fiat_shamir/src/sponge.rs
index 4051e2cf..e21e4bf0 100644
--- a/crates/fiat_shamir/src/sponge.rs
+++ b/crates/fiat_shamir/src/sponge.rs
@@ -1,3 +1,4 @@
+// CREDIT: https://github.com/signalapp/libsignal/blob/main/rust/poksho/src/shosha256.rs, AGPL-3.0-only.
//! The VM-native Fiat–Shamir sponge: THE verifier-randomness source for the
//! whole stack — flock's zerocheck / lincheck, the Ligerito PCS, and
//! leanVM-b's own protocol (whose `ProverState` / `VerifierState` wrap this
@@ -11,11 +12,15 @@
//! the streaming hasher cannot be reproduced by the one 64-byte compression the
//! machine has.
//!
+//! Scalars are `E = F192` (the tower challenge field): their three
+//! little-endian `K = F64` limbs occupy the first three compression lanes,
+//! with the scalar domain tag in the fourth.
+//!
//! Construction adapted from Signal's ShoSha256 "Stateful Hash Object"
//! (`libsignal/rust/poksho/src/shosha256.rs`, © 2020 Signal Messenger, LLC,
//! AGPL-3.0-only): a chaining value advanced by domain-separated absorb /
//! squeeze steps. Here the underlying hash is the VM's BLAKE3 compression
-//! rather than SHA-256, inputs are GF(2^128) field elements, and — because
+//! rather than SHA-256, inputs are `K = GF(2^64)` field words, and — because
//! every absorb is domain-tagged per compression — no explicit double-hash
//! ratchet is needed.
//!
@@ -24,42 +29,42 @@
//! integer, and a byte string cannot alias), byte strings are length-framed,
//! and each squeeze ratchets the state (binding challenge order).
-use primitives::field::F128;
+use primitives::field::{F64, F192};
/// `f(a, b) = BLAKE3(a‖b)` on two 256-bit halves laid out little-endian into 64
/// bytes — *exactly* the VM's `Blake3` opcode: 64 input bytes → 32-byte digest,
-/// split back into two field words. THE primitive; the sponge is a chain of
+/// split back into four field words. THE primitive; the sponge is a chain of
/// these, so a zkDSL program replays it with one `blake3(...)` per step.
-pub fn compress(a: [F128; 2], b: [F128; 2]) -> [F128; 2] {
+pub fn compress(a: [F64; 4], b: [F64; 4]) -> [F64; 4] {
let mut input = [0u8; 64];
- for (slot, w) in input.chunks_exact_mut(16).zip([a[0], a[1], b[0], b[1]]) {
- slot.copy_from_slice(&w.to_le_bytes());
+ for (slot, w) in input.chunks_exact_mut(8).zip(a.into_iter().chain(b)) {
+ slot.copy_from_slice(&w.0.to_le_bytes());
}
let d = *blake3::hash(&input).as_bytes();
- let word = |b: &[u8]| F128::from_le_bytes(b.try_into().unwrap());
- [word(&d[..16]), word(&d[16..])]
+ std::array::from_fn(|k| F64(u64::from_le_bytes(d[8 * k..8 * k + 8].try_into().unwrap())))
}
-// Domain-separation tags, carried in the SECOND input word of every absorbed
-// block, so no two roles (a scalar, a byte word, a length frame, a squeeze, a
-// PoW step) can alias: the adversary controls only the FIRST word (the datum),
-// never the tag. Distinct nonzero constants suffice.
-const DS_SCALAR: F128 = F128::new(1, 0);
-const DS_BYTE: F128 = F128::new(2, 0);
-const DS_LEN: F128 = F128::new(3, 0);
-const DS_SQUEEZE: F128 = F128::new(4, 0);
-const DS_POW: F128 = F128::new(5, 0);
-
-/// `compress(base, (nonce, DS_POW))` has its low `bits` bits zero — the grinding
-/// predicate over the VM compression. A CONTIGUOUS low-bit window (rather than
-/// byte-wise leading zeros) so a recursive verifier re-checks it with a single
-/// loop over the bit decomposition of the digest word (`grind_check` in
-/// `guests/recursion.py`). `bits` is always `< 64`.
+// Domain-separation tags. Scalar absorbs fill three data lanes and put the tag
+// in lane 3; byte/length/PoW absorbs use at most two data lanes and put the tag
+// in lane 2. No two roles can alias: the adversary never controls the tag.
+// Distinct nonzero constants suffice.
+const DS_SCALAR: F64 = F64(1);
+const DS_BYTE: F64 = F64(2);
+const DS_LEN: F64 = F64(3);
+const DS_SQUEEZE: F64 = F64(4);
+const DS_POW: F64 = F64(5);
+
+/// `compress(base, (nonce.c0, nonce.c1, nonce.c2, DS_POW))` has its low `bits`
+/// bits zero — the
+/// grinding predicate over the VM compression. A CONTIGUOUS low-bit window
+/// (rather than byte-wise leading zeros) so a recursive verifier re-checks it
+/// with a single loop over the bit decomposition of the digest word
+/// (`grind_check` in `guests/recursion.py`). `bits` is always `< 64`.
#[inline]
-fn pow_bits_ok(base: [F128; 2], nonce: F128, bits: u32) -> bool {
+fn pow_bits_ok(base: [F64; 4], nonce: F192, bits: u32) -> bool {
debug_assert!(bits < 64, "grinding deficit fits the digest's low word");
- let digest = compress(base, [nonce, DS_POW])[0];
- digest.lo & ((1u64 << bits) - 1) == 0
+ let digest = compress(base, [F64(nonce.c0), F64(nonce.c1), F64(nonce.c2), DS_POW])[0];
+ digest.0 & ((1u64 << bits) - 1) == 0
}
/// The shared Fiat–Shamir state (see the module docs). Protocol functions take
@@ -68,7 +73,7 @@ fn pow_bits_ok(base: [F128; 2], nonce: F128, bits: u32) -> bool {
#[derive(Clone)]
pub struct Sponge {
/// The 256-bit chaining value: a Merkle–Damgård hash of the transcript so far.
- cv: [F128; 2],
+ cv: [F64; 4],
}
impl Sponge {
@@ -77,9 +82,9 @@ impl Sponge {
/// any challenge — there is no mid-protocol "observe public data" step to get
/// wrong (or forget). (Untraced: the seed is the replay STARTING state, not an
/// op of the recorded transcript.)
- pub fn new(label: &[u8], statement: &[F128]) -> Self {
- let mut s = Self { cv: [F128::ZERO, F128::ZERO] };
- s.absorb_bytes_untraced(b"leanvm-b/transcript/v1");
+ pub fn new(label: &[u8], statement: &[F192]) -> Self {
+ let mut s = Self { cv: [F64::ZERO; 4] };
+ s.absorb_bytes_untraced(b"leanvm-b/transcript/v2");
s.absorb_bytes_untraced(label);
for &x in statement {
s.observe_untraced(x);
@@ -90,74 +95,77 @@ impl Sponge {
/// A fresh chain at the zero state: the guest-side aggregation and export
/// transcripts start here (no label), and the harness mirrors them.
pub fn empty() -> Self {
- Self { cv: [F128::ZERO; 2] }
+ Self { cv: [F64::ZERO; 4] }
}
- /// Absorb one scalar: `cv ← compress(cv, (x, DS_SCALAR))`.
- pub fn observe(&mut self, x: F128) {
+ /// Absorb one 24-byte scalar (three little-endian `K` limbs):
+ /// `cv ← compress(cv, (c0, c1, c2, DS_SCALAR))`.
+ pub fn observe(&mut self, x: F192) {
self.observe_untraced(x);
trace(|| TraceOp::Observe(x));
}
- fn observe_untraced(&mut self, x: F128) {
- self.cv = compress(self.cv, [x, DS_SCALAR]);
+ fn observe_untraced(&mut self, x: F192) {
+ self.cv = compress(self.cv, [F64(x.c0), F64(x.c1), F64(x.c2), DS_SCALAR]);
}
/// Absorb a byte string (a protocol label, a Merkle root): a length frame
- /// then its 16-byte words as tagged blocks, so a field element, a raw
- /// integer, and a byte string cannot alias.
+ /// then its 16-byte (two-word) chunks as tagged blocks (the domain tag
+ /// occupies the third lane, leaving two data words per block), so a field
+ /// element, a raw integer, and a byte string cannot alias.
pub fn absorb_bytes(&mut self, bytes: &[u8]) {
self.absorb_bytes_untraced(bytes);
trace(|| TraceOp::AbsorbBytes(bytes.to_vec()));
}
fn absorb_bytes_untraced(&mut self, bytes: &[u8]) {
- self.cv = compress(self.cv, [F128::new(bytes.len() as u64, 0), DS_LEN]);
+ self.cv = compress(self.cv, [F64(bytes.len() as u64), F64::ZERO, DS_LEN, F64::ZERO]);
for chunk in bytes.chunks(16) {
let mut buf = [0u8; 16];
buf[..chunk.len()].copy_from_slice(chunk);
- let w = F128::from_le_bytes(buf);
- self.cv = compress(self.cv, [w, DS_BYTE]);
+ let w = |o: usize| F64(u64::from_le_bytes(buf[o..o + 8].try_into().unwrap()));
+ self.cv = compress(self.cv, [w(0), w(8), DS_BYTE, F64::ZERO]);
}
}
- /// Squeeze a challenge and ratchet: the challenge is the first word of
- /// `compress(cv, (0, DS_SQUEEZE))`, whose full output becomes the new state —
- /// domain-separated from absorbs, so a challenge cannot be confused with a
- /// continued absorb. In Fiat–Shamir everything is public; soundness comes from
- /// each challenge being a random-oracle image of the entire prior transcript.
- pub fn sample(&mut self) -> F128 {
+ /// Squeeze a challenge and ratchet: the challenge's three limbs are the
+ /// first three words of `compress(cv, (0, 0, DS_SQUEEZE, 0))`, whose full output
+ /// becomes the new state — domain-separated from absorbs, so a challenge
+ /// cannot be confused with a continued absorb. In Fiat–Shamir everything is
+ /// public; soundness comes from each challenge being a random-oracle image
+ /// of the entire prior transcript.
+ pub fn sample(&mut self) -> F192 {
let v = self.sample_untraced();
trace(|| TraceOp::Sample(v));
v
}
- fn sample_untraced(&mut self) -> F128 {
- let out = compress(self.cv, [F128::ZERO, DS_SQUEEZE]);
+ fn sample_untraced(&mut self) -> F192 {
+ let out = compress(self.cv, [F64::ZERO, F64::ZERO, DS_SQUEEZE, F64::ZERO]);
self.cv = out;
- out[0]
+ F192::new(out[0].0, out[1].0, out[2].0)
}
/// Squeeze `n` challenges, in order.
- pub fn sample_vec(&mut self, n: usize) -> Vec {
+ pub fn sample_vec(&mut self, n: usize) -> Vec {
(0..n).map(|_| self.sample()).collect()
}
- /// The PoW base `compress(cv, (0, DS_POW))`, read without mutating the live
- /// state (the nonce is bound separately by [`Self::absorb_nonce`]).
- fn pow_base(&self) -> [F128; 2] {
- compress(self.cv, [F128::ZERO, DS_POW])
+ /// The PoW base `compress(cv, (0, 0, DS_POW, 0))`, read without mutating the
+ /// live state (the nonce is bound separately by [`Self::absorb_nonce`]).
+ fn pow_base(&self) -> [F64; 4] {
+ compress(self.cv, [F64::ZERO, F64::ZERO, DS_POW, F64::ZERO])
}
/// The current 256-bit chaining value.
- pub fn state(&self) -> [F128; 2] {
+ pub fn state(&self) -> [F64; 4] {
self.cv
}
- /// The grinding digest this state yields for `nonce` (read-only preview;
+ /// The grinding digest word this state yields for `nonce` (read-only preview;
/// [`Self::verify_pow`] is the mutating check).
- pub fn pow_digest(&self, nonce: F128) -> F128 {
- compress(self.pow_base(), [nonce, DS_POW])[0]
+ pub fn pow_digest(&self, nonce: F192) -> F64 {
+ compress(self.pow_base(), [F64(nonce.c0), F64(nonce.c1), F64(nonce.c2), DS_POW])[0]
}
/// Re-run recorded verifier transcript ops through this sponge, asserting
@@ -173,7 +181,10 @@ impl Sponge {
assert_eq!(self.sample_untraced(), *v, "trace replay diverged")
}
TraceOp::Pow { nonce, bits, .. } => {
- assert!(self.verify_pow_field_untraced(*nonce, *bits), "trace replay: grind failed")
+ assert!(
+ self.verify_pow_field_untraced(*nonce, *bits),
+ "trace replay: grind failed"
+ )
}
TraceOp::StreamRaw(_) | TraceOp::Opening => {}
}
@@ -181,8 +192,8 @@ impl Sponge {
}
/// Bind a grinding nonce into the state (both sides, so they stay in lockstep).
- fn absorb_nonce(&mut self, nonce: F128) {
- self.cv = compress(self.cv, [nonce, DS_POW]);
+ fn absorb_nonce(&mut self, nonce: F192) {
+ self.cv = compress(self.cv, [F64(nonce.c0), F64(nonce.c1), F64(nonce.c2), DS_POW]);
}
/// Prover-side PoW grind: find the smallest `u64` nonce whose PoW hash clears
@@ -197,7 +208,7 @@ impl Sponge {
} else if (1u64 << bits.min(63)) < PARALLEL_GRIND_MIN_HASHES {
let mut n: u64 = 0;
loop {
- if pow_bits_ok(base, F128::new(n, 0), bits) {
+ if pow_bits_ok(base, F192::new(n, 0, 0), bits) {
break n;
}
n = n.wrapping_add(1);
@@ -211,37 +222,46 @@ impl Sponge {
loop {
if let Some(n) = (start..start.saturating_add(block))
.into_par_iter()
- .find_first(|&n| pow_bits_ok(base, F128::new(n, 0), bits))
+ .find_first(|&n| pow_bits_ok(base, F192::new(n, 0, 0), bits))
{
break n;
}
start = start.saturating_add(block);
}
};
- self.absorb_nonce(F128::new(nonce, 0));
+ self.absorb_nonce(F192::new(nonce, 0, 0));
nonce
}
/// Verifier-side mirror of [`Self::grind_pow`]: check `nonce` clears the `bits`
/// PoW against the current state, then bind it regardless (so the sponge stays
/// in lockstep with an honest prover — a failed check rejects at the call
- /// site). `bits = 0` accepts only the canonical nonce `0`.
+ /// site). `bits = 0` accepts only the canonical nonce `0`, which keeps proofs
+ /// non-malleable at zero-bit grinding sites.
pub fn verify_pow(&mut self, nonce: u64, bits: u32) -> bool {
- self.verify_pow_field(F128::new(nonce, 0), bits)
+ self.verify_pow_field(F192::new(nonce, 0, 0), bits)
}
/// Verify a nonce transported as a field word. Allowing the complete field
/// domain does not weaken grinding: each candidate still requires one hash
/// and succeeds with probability 2^-bits. Honest provers remain canonical
/// and search the deterministic u64 subset in [`Self::grind_pow`].
- pub fn verify_pow_field(&mut self, nonce: F128, bits: u32) -> bool {
- trace(|| TraceOp::Pow { nonce, bits, digest: self.pow_digest(nonce) });
+ pub fn verify_pow_field(&mut self, nonce: F192, bits: u32) -> bool {
+ trace(|| TraceOp::Pow {
+ nonce,
+ bits,
+ digest: self.pow_digest(nonce),
+ });
self.verify_pow_field_untraced(nonce, bits)
}
- fn verify_pow_field_untraced(&mut self, nonce: F128, bits: u32) -> bool {
+ fn verify_pow_field_untraced(&mut self, nonce: F192, bits: u32) -> bool {
let base = self.pow_base();
- let ok = if bits == 0 { nonce == F128::ZERO } else { pow_bits_ok(base, nonce, bits) };
+ let ok = if bits == 0 {
+ nonce == F192::ZERO
+ } else {
+ pow_bits_ok(base, nonce, bits)
+ };
self.absorb_nonce(nonce);
ok
}
@@ -257,16 +277,20 @@ impl Sponge {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TraceOp {
/// A stream word consumed without binding (grinding nonces).
- StreamRaw(F128),
+ StreamRaw(F192),
/// An absorbed scalar (transmitted or derived — the sponge cannot tell).
- Observe(F128),
+ Observe(F192),
/// `absorb_bytes` (labels, roots).
AbsorbBytes(Vec),
- Sample(F128),
- /// A grinding check: the nonce, the required bits, and the digest the
+ Sample(F192),
+ /// A grinding check: the nonce, the required bits, and the digest word the
/// pre-absorb state yields for that nonce (so trace consumers never need
/// to track sponge state in lockstep).
- Pow { nonce: F128, bits: u32, digest: F128 },
+ Pow {
+ nonce: F192,
+ bits: u32,
+ digest: F64,
+ },
/// An opening hint consumed (the Ligerito hint channel).
Opening,
}
@@ -300,8 +324,8 @@ pub fn trace(op: impl FnOnce() -> TraceOp) {
mod tests {
use super::*;
- fn f(k: u64) -> F128 {
- F128::new(k, k ^ 0x1234)
+ fn f(k: u64) -> F192 {
+ F192::new(k, k ^ 0x1234, k.rotate_left(17))
}
/// A challenge binds every prior absorbed scalar: flipping one observed value
@@ -326,14 +350,17 @@ mod tests {
}
/// A scalar and a byte string cannot alias (distinct domain tags), so
- /// observing a scalar vs absorbing its 16-byte encoding diverge.
+ /// observing a scalar vs absorbing its 24-byte encoding diverge.
#[test]
fn sponge_domain_separation() {
let x = f(9);
let mut a = Sponge::new(b"t", &[]);
a.observe(x);
let mut b = Sponge::new(b"t", &[]);
- let bytes = x.to_le_bytes();
+ let mut bytes = [0u8; 24];
+ bytes[..8].copy_from_slice(&x.c0.to_le_bytes());
+ bytes[8..16].copy_from_slice(&x.c1.to_le_bytes());
+ bytes[16..].copy_from_slice(&x.c2.to_le_bytes());
b.absorb_bytes(&bytes);
assert_ne!(a.sample(), b.sample());
}
@@ -347,12 +374,9 @@ mod tests {
let mut clone = sp.clone();
clone.grind_pow(8)
};
- assert!(pow_bits_ok(base, F128::new(good, 0), 8));
+ assert!(pow_bits_ok(base, F192::new(good, 0, 0), 8));
// A random wrong nonce almost surely fails an 8-bit grind.
- assert!(
- !pow_bits_ok(base, F128::new(good.wrapping_add(1).wrapping_mul(3) | 1, 0), 8)
- || good != 0
- );
+ assert!(!pow_bits_ok(base, F192::new(good.wrapping_add(1).wrapping_mul(3) | 1, 0, 0), 8,) || good != 0);
}
/// Recursive proofs transport the nonce as one field word. Its high limb is
@@ -362,7 +386,7 @@ mod tests {
let mut verifier = Sponge::new(b"t", &[f(1)]);
let base = verifier.pow_base();
let nonce = (0..u64::MAX)
- .map(|lo| F128::new(lo, 1))
+ .map(|lo| F192::new(lo, 1, 2))
.find(|&nonce| pow_bits_ok(base, nonce, 8))
.expect("an 8-bit grind has a solution");
@@ -372,6 +396,6 @@ mod tests {
assert_eq!(verifier.state(), expected.state());
let mut zero_bits = Sponge::new(b"t", &[f(1)]);
- assert!(!zero_bits.verify_pow_field(F128::new(0, 1), 0));
+ assert!(!zero_bits.verify_pow_field(F192::new(0, 1, 0), 0));
}
}
diff --git a/crates/fiat_shamir/src/transcript.rs b/crates/fiat_shamir/src/transcript.rs
index 59d7c7e6..73d0928a 100644
--- a/crates/fiat_shamir/src/transcript.rs
+++ b/crates/fiat_shamir/src/transcript.rs
@@ -24,9 +24,9 @@
//! functions take these SAME states (`ps`/`vs`), drawing their challenges from
//! the one shared sponge while their proof data rides its own structs.
-use primitives::field::F128;
use crate::sponge::trace;
pub use crate::sponge::{Sponge, TraceOp, trace_start, trace_take};
+use primitives::field::{F64, F192};
/// A complete proof: the scalar transcript stream plus the Ligerito opening hint
/// channel — **two** channels, no bolted-on side field. The commitment root and
@@ -44,7 +44,7 @@ pub use crate::sponge::{Sponge, TraceOp, trace_start, trace_take};
pub struct Proof {
/// Every transmitted field scalar, in protocol order (plus flock's scalar
/// sub-proof as trailing raw transport words).
- pub stream: Vec,
+ pub stream: Vec,
/// Ligerito openings (sumcheck messages + Merkle roots/paths), in order.
pub openings: Vec,
}
@@ -59,18 +59,21 @@ pub enum Error {
NotFullyConsumed,
/// A grinding nonce failed its proof-of-work check.
PowFailed,
+ /// A transmitted field element used as a narrower encoding had nonzero
+ /// limbs outside that encoding.
+ NonCanonicalEncoding,
}
/// Prover side: writes scalars into the stream and opening hints to the side.
pub struct ProverState {
sponge: Sponge,
- stream: Vec,
+ stream: Vec,
openings: Vec,
}
impl ProverState {
/// `statement` is the public input, seeded into the sponge (see [`Sponge::new`]).
- pub fn new(label: &[u8], statement: &[F128]) -> Self {
+ pub fn new(label: &[u8], statement: &[F192]) -> Self {
Self {
sponge: Sponge::new(label, statement),
stream: Vec::new(),
@@ -81,27 +84,27 @@ impl ProverState {
/// Transmit a scalar into the proof AND bind it into the sponge (the two are
/// inseparable — you cannot send without binding).
#[inline]
- pub fn add_scalar(&mut self, x: F128) {
+ pub fn add_scalar(&mut self, x: F192) {
self.sponge.observe(x);
self.stream.push(x);
}
- pub fn add_scalars(&mut self, xs: &[F128]) {
+ pub fn add_scalars(&mut self, xs: &[F192]) {
for &x in xs {
self.add_scalar(x);
}
}
- pub fn sample(&mut self) -> F128 {
+ pub fn sample(&mut self) -> F192 {
self.sponge.sample()
}
/// Prover mirror of [`VerifierState::observe_scalar`].
- pub fn observe_scalar(&mut self, x: F128) {
+ pub fn observe_scalar(&mut self, x: F192) {
self.sponge.observe(x);
}
- pub fn sample_vec(&mut self, n: usize) -> Vec {
+ pub fn sample_vec(&mut self, n: usize) -> Vec {
(0..n).map(|_| self.sponge.sample()).collect()
}
@@ -117,7 +120,7 @@ impl ProverState {
/// no-work nonce `0`.
pub fn grind(&mut self, bits: u32) {
let nonce = self.sponge.grind_pow(bits);
- self.stream.push(F128::new(nonce, 0));
+ self.stream.push(F192::new(nonce, 0, 0));
}
/// Absorb a byte string (a sub-protocol label, a Merkle root) — data both
@@ -152,7 +155,7 @@ impl ProverState {
/// hints in order.
pub struct VerifierState<'a, O> {
sponge: Sponge,
- stream: &'a [F128],
+ stream: &'a [F192],
offset: usize,
openings: &'a [O],
oi: usize,
@@ -161,7 +164,7 @@ pub struct VerifierState<'a, O> {
impl<'a, O> VerifierState<'a, O> {
/// `statement` is the public input, seeded into the sponge (see [`Sponge::new`])
/// — must match the prover's, or the sponges diverge and verification fails.
- pub fn new(label: &[u8], proof: &'a Proof, statement: &[F128]) -> Self {
+ pub fn new(label: &[u8], proof: &'a Proof, statement: &[F192]) -> Self {
Self {
sponge: Sponge::new(label, statement),
stream: &proof.stream,
@@ -174,46 +177,48 @@ impl<'a, O> VerifierState<'a, O> {
/// A verifier state with EMPTY transport channels — a challenge source for
/// unit tests that drive sub-protocols without a transmitted stream (leaks
/// one small allocation; do not use outside tests).
- pub fn detached(label: &[u8], statement: &[F128]) -> VerifierState<'static, O> {
- let empty = Box::leak(Box::new(Proof { stream: Vec::new(), openings: Vec::new() }));
+ pub fn detached(label: &[u8], statement: &[F192]) -> VerifierState<'static, O> {
+ let empty = Box::leak(Box::new(Proof {
+ stream: Vec::new(),
+ openings: Vec::new(),
+ }));
VerifierState::new(label, empty, statement)
}
/// Read the next scalar, binding it into the sponge (mirrors `add_scalar`).
#[inline]
- pub fn next_scalar(&mut self) -> Result {
+ pub fn next_scalar(&mut self) -> Result {
let x = *self.stream.get(self.offset).ok_or(Error::ExceededStream)?;
self.offset += 1;
self.sponge.observe(x);
Ok(x)
}
- pub fn next_scalars(&mut self, n: usize) -> Result, Error> {
+ pub fn next_scalars(&mut self, n: usize) -> Result, Error> {
(0..n).map(|_| self.next_scalar()).collect()
}
/// Advance the stream cursor by one **without** binding into the sponge — the
/// read counterpart of the raw nonce push in [`ProverState::grind`].
- fn take_raw(&mut self) -> Result {
+ fn take_raw(&mut self) -> Result {
let x = *self.stream.get(self.offset).ok_or(Error::ExceededStream)?;
self.offset += 1;
trace(|| TraceOp::StreamRaw(x));
Ok(x)
}
-
- pub fn sample(&mut self) -> F128 {
+ pub fn sample(&mut self) -> F192 {
self.sponge.sample()
}
- pub fn sample_vec(&mut self, n: usize) -> Vec {
+ pub fn sample_vec(&mut self, n: usize) -> Vec {
(0..n).map(|_| self.sample()).collect()
}
/// Absorb a value both parties compute themselves (never transmitted):
/// protocol steps that bind derived values before sampling, e.g. the
/// stacked-bytecode claim reduction (`leaf::verify_balance`).
- pub fn observe_scalar(&mut self, x: F128) {
+ pub fn observe_scalar(&mut self, x: F192) {
self.sponge.observe(x);
}
@@ -238,7 +243,7 @@ impl<'a, O> VerifierState<'a, O> {
/// The sponge's current chaining value (recursion harnesses snapshot the
/// phase-boundary states as guest debug checkpoints).
- pub fn sponge_state(&self) -> [F128; 2] {
+ pub fn sponge_state(&self) -> [F64; 4] {
self.sponge.state()
}
@@ -275,8 +280,8 @@ impl<'a, O> VerifierState<'a, O> {
mod tests {
use super::*;
- fn f(k: u64) -> F128 {
- F128::new(k, k ^ 0x1234)
+ fn f(k: u64) -> F192 {
+ F192::new(k, k ^ 0x1234, k.rotate_left(17))
}
/// Prover and verifier stay in lockstep across a mixed transcript
@@ -298,5 +303,4 @@ mod tests {
assert_eq!(vs.sample(), c2);
assert!(vs.finish().is_ok());
}
-
}
diff --git a/crates/flock/src/blake3.rs b/crates/flock/src/blake3.rs
index febaaba4..b79c7066 100644
--- a/crates/flock/src/blake3.rs
+++ b/crates/flock/src/blake3.rs
@@ -1,4 +1,4 @@
-// Credit: https://github.com/succinctlabs/flock (flock-prover), MIT OR Apache-2.0.
+// CREDIT: https://github.com/succinctlabs/flock (flock-prover), MIT OR Apache-2.0.
//! Monolithic BLAKE3 compression-function R1CS — one R1CS instance per
//! `compress(cv, m, counter, block_len, flags) → state[16]` call. Encodes
//! the 16-word state init, all 7 rounds (8 G's per round + the message
@@ -97,11 +97,9 @@
//! openings at fixed indices pin them to claimed memory and bytecode values.
use crate::blake3_witness::{BitRecord, add_carry_parts, or_bit_at, or_u32_at_bit, xor_dedup};
-use pcs::{ProverState, VerifierState};
-use primitives::field::F128;
-use pcs::Commitment;
use crate::r1cs::{BlockR1cs, SparseBinaryMatrix};
use crate::verifier;
+use primitives::field::F192;
// ---------------------------------------------------------------------------
// Public constants
@@ -156,16 +154,8 @@ pub const G_LANES: [[usize; 4]; N_G_PER_ROUND] = [
/// Message-index pairs `(mx, my)` consumed by G index `g` within a round,
/// indexing into the (already-permuted) per-round message buffer.
-pub const G_MSG_IDX: [[usize; 2]; N_G_PER_ROUND] = [
- [0, 1],
- [2, 3],
- [4, 5],
- [6, 7],
- [8, 9],
- [10, 11],
- [12, 13],
- [14, 15],
-];
+pub const G_MSG_IDX: [[usize; 2]; N_G_PER_ROUND] =
+ [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11], [12, 13], [14, 15]];
// ---------------------------------------------------------------------------
// Layout positions (bit indices into the per-block z slice of length K)
@@ -272,13 +262,7 @@ fn permute(m: &mut [u32; 16]) {
/// BLAKE3 compression function. Returns the full 16-word output state
/// (post-finalization XOR). For chaining, the new CV is `out[0..8]`.
-pub fn blake3_compress(
- cv: &[u32; 8],
- block_words: &[u32; 16],
- counter: u64,
- block_len: u32,
- flags: u32,
-) -> [u32; 16] {
+pub fn blake3_compress(cv: &[u32; 8], block_words: &[u32; 16], counter: u64, block_len: u32, flags: u32) -> [u32; 16] {
let counter_low = counter as u32;
let counter_high = (counter >> 32) as u32;
let mut state = [
@@ -524,59 +508,23 @@ pub fn build_matrices() -> (SparseBinaryMatrix, SparseBinaryMatrix) {
let my = Word::from_slot_base(m_bit(my_idx, 0));
// tmp_0 = a + b
- let tmp_0 = write_add_carry_rows(
- &mut a_rows,
- &mut b_rows,
- &a,
- &b,
- g_add_carry_bit(g, ADD_TMP0, 0),
- );
+ let tmp_0 = write_add_carry_rows(&mut a_rows, &mut b_rows, &a, &b, g_add_carry_bit(g, ADD_TMP0, 0));
// a_1 = tmp_0 + mx
- let a_1 = write_add_carry_rows(
- &mut a_rows,
- &mut b_rows,
- &tmp_0,
- &mx,
- g_add_carry_bit(g, ADD_A1, 0),
- );
+ let a_1 = write_add_carry_rows(&mut a_rows, &mut b_rows, &tmp_0, &mx, g_add_carry_bit(g, ADD_A1, 0));
// d_1 = rotr16(d ^ a_1)
let d_1 = d.xor(&a_1).dedup().rotr(16);
// c_1 = c + d_1
- let c_1 = write_add_carry_rows(
- &mut a_rows,
- &mut b_rows,
- &c,
- &d_1,
- g_add_carry_bit(g, ADD_C1, 0),
- );
+ let c_1 = write_add_carry_rows(&mut a_rows, &mut b_rows, &c, &d_1, g_add_carry_bit(g, ADD_C1, 0));
// b_1 = rotr12(b ^ c_1)
let b_1 = b.xor(&c_1).dedup().rotr(12);
// tmp_1 = a_1 + b_1
- let tmp_1 = write_add_carry_rows(
- &mut a_rows,
- &mut b_rows,
- &a_1,
- &b_1,
- g_add_carry_bit(g, ADD_TMP1, 0),
- );
+ let tmp_1 = write_add_carry_rows(&mut a_rows, &mut b_rows, &a_1, &b_1, g_add_carry_bit(g, ADD_TMP1, 0));
// a_2 = tmp_1 + my (= a_new — cascades)
- let a_2 = write_add_carry_rows(
- &mut a_rows,
- &mut b_rows,
- &tmp_1,
- &my,
- g_add_carry_bit(g, ADD_A2, 0),
- );
+ let a_2 = write_add_carry_rows(&mut a_rows, &mut b_rows, &tmp_1, &my, g_add_carry_bit(g, ADD_A2, 0));
// d_2 = rotr8(d_1 ^ a_2)
let d_2 = d_1.xor(&a_2).dedup().rotr(8);
// c_2 = c_1 + d_2 (= c_new — cascades)
- let c_2 = write_add_carry_rows(
- &mut a_rows,
- &mut b_rows,
- &c_1,
- &d_2,
- g_add_carry_bit(g, ADD_C2, 0),
- );
+ let c_2 = write_add_carry_rows(&mut a_rows, &mut b_rows, &c_1, &d_2, g_add_carry_bit(g, ADD_C2, 0));
// b_new = rotr7(b_1 ^ c_2) (materialized lin-id)
let b_new_word = b_1.xor(&c_2).dedup().rotr(7);
for i in 0..WORD_BITS {
@@ -630,15 +578,245 @@ pub fn build_matrices() -> (SparseBinaryMatrix, SparseBinaryMatrix) {
(to_mat(a_rows), to_mat(b_rows))
}
+// ---------------------------------------------------------------------------
+// Circuit-walk evaluation (flock §Circuit walking)
+//
+// Evaluates the two bilinear forms
+//
+// uᵀ A_0 w and uᵀ B_0 w
+//
+// for arbitrary row weights `u` and column weights `w` (length K each) by
+// walking the UNSUBSTITUTED compression circuit forward: the same cascade
+// `build_matrices` threads symbolically, evaluated over F192 values. A lane
+// is a 32-vector of wire values; a committed slot contributes `w[slot]`, an
+// intermediate wire the running linear combination. Row i's contribution
+// `u[i]·⟨A_i, w⟩` / `u[i]·⟨B_i, w⟩` is accumulated exactly where
+// `build_matrices` would emit that row, with `⟨row, w⟩` read off the threaded
+// wire values. Cost: O(circuit) field ops (~50K muls), never the ~21M
+// substituted nonzeros — and the matrices need not be materialized at all.
+// This is what lets a verifier evaluate the matrix MLEs directly instead of
+// paying the sparse-matrix cost (or deferring the claim).
+// ---------------------------------------------------------------------------
+
+/// One lane's wire values: bit `i` of the word, as the F192 combination
+/// `⟨lin_func_i, w⟩`.
+type WireWord = [F192; WORD_BITS];
+
+#[inline]
+fn wire_from_slot_base(w: &[F192], base: usize) -> WireWord {
+ std::array::from_fn(|i| w[base + i])
+}
+
+/// Constant word: a set bit is the `[Z_CONST]` lin_func, a clear bit empty.
+#[inline]
+fn wire_from_const(w: &[F192], val: u32) -> WireWord {
+ std::array::from_fn(|i| {
+ if (val >> i) & 1 == 1 {
+ w[Z_CONST_POS]
+ } else {
+ F192::ZERO
+ }
+ })
+}
+
+#[inline]
+fn wire_xor(x: &WireWord, y: &WireWord) -> WireWord {
+ std::array::from_fn(|i| x[i] + y[i])
+}
+
+#[inline]
+fn wire_rotr(x: &WireWord, n: usize) -> WireWord {
+ std::array::from_fn(|i| x[(i + n) % WORD_BITS])
+}
+
+/// Pair of accumulators for the A-side and B-side bilinear forms, plus the
+/// running sum of `u` over rows whose B-side is the single `[Z_CONST]` entry
+/// (lin-id / free-input rows) — factored so those rows cost one B-side
+/// F-addition instead of a multiplication each.
+struct WalkAcc {
+ a: F192,
+ b: F192,
+ /// Σ u[row] over rows with `B_row = [Z_CONST]`; folded in once at the end
+ /// as `b += w[Z_CONST_POS] · u_bconst`.
+ u_bconst: F192,
+}
+
+/// Walk one 32-bit ADD (mirror of `write_add_carry_rows` + `Word::add_sum`):
+/// accumulate the 31 carry rows into `acc` and return the sum-bit wires.
+///
+/// carry row cb+i: A = X[i] ⊕ cin[i], B = Y[i] ⊕ cin[i]
+/// sum[i] = X[i] ⊕ Y[i] ⊕ cin[i]
+///
+/// with `cin[i] = ⊕_{j WireWord {
+ let mut out = [F192::ZERO; WORD_BITS];
+ let mut cin = F192::ZERO;
+ for i in 0..WORD_BITS {
+ let a_side = x[i] + cin;
+ let b_side = y[i] + cin;
+ out[i] = a_side + y[i];
+ if i < CARRY_BITS_PER_ADD {
+ let ui = u[carry_base + i];
+ acc.a += ui * a_side;
+ acc.b += ui * b_side;
+ cin += w[carry_base + i];
+ }
+ }
+ out
+}
+
+/// Walk 32 consecutive `lin_func · 1` rows (lin-id / out_lo / out_hi):
+/// row base+i has `A = `, `B = [Z_CONST]`.
+fn walk_lin_rows(acc: &mut WalkAcc, u: &[F192], vals: &WireWord, base: usize) {
+ for i in 0..WORD_BITS {
+ acc.a += u[base + i] * vals[i];
+ acc.u_bconst += u[base + i];
+ }
+}
+
+/// `(uᵀ A_0 w, uᵀ B_0 w)` by the forward circuit walk — the exact matrices
+/// [`build_matrices`] emits, never materialized.
+pub fn bilinear_walk_pair(u: &[F192], w: &[F192]) -> (F192, F192) {
+ assert_eq!(u.len(), K);
+ assert_eq!(w.len(), K);
+ let wc = w[Z_CONST_POS];
+ let mut acc = WalkAcc {
+ a: F192::ZERO,
+ b: F192::ZERO,
+ u_bconst: F192::ZERO,
+ };
+ // Σ u[row] over rows with A = B = [Z_CONST] (just the constant row now
+ // that every compression input is a free row): folded in at the end on
+ // both sides.
+ let u_abconst = u[Z_CONST_POS];
+
+ // Free-input rows for the 512 message bits: A = [slot], B = [Z_CONST].
+ for j in 0..16 * WORD_BITS {
+ let s = M_BASE + j;
+ acc.a += u[s] * w[s];
+ acc.u_bconst += u[s];
+ }
+
+ // Free-input rows for the 256 chaining-value bits and the 128 metadata
+ // bits (counter lo/hi, block_len, flags): A = [slot], B = [Z_CONST] — the
+ // same shape as the message bits (the generalized circuit no longer pins
+ // them to constants; the embedding protocol binds them instead).
+ for j in 0..8 * WORD_BITS {
+ let s = CV_BASE + j;
+ acc.a += u[s] * w[s];
+ acc.u_bconst += u[s];
+ }
+ for base in [T_LO_BASE, T_HI_BASE, BLEN_BASE, FLAGS_BASE] {
+ for j in 0..WORD_BITS {
+ let s = base + j;
+ acc.a += u[s] * w[s];
+ acc.u_bconst += u[s];
+ }
+ }
+
+ // The G cascade, over wire values (mirrors `initial_lane_words`).
+ let msg_idx = per_round_msg_idx();
+ let mut state: [WireWord; 16] = std::array::from_fn(|_| [F192::ZERO; WORD_BITS]);
+ for wd in 0..8 {
+ state[wd] = wire_from_slot_base(w, cv_bit(wd, 0));
+ }
+ for i in 0..4 {
+ state[8 + i] = wire_from_const(w, BLAKE3_IV[i]);
+ }
+ state[12] = wire_from_slot_base(w, T_LO_BASE);
+ state[13] = wire_from_slot_base(w, T_HI_BASE);
+ state[14] = wire_from_slot_base(w, BLEN_BASE);
+ state[15] = wire_from_slot_base(w, FLAGS_BASE);
+
+ for r in 0..N_ROUNDS {
+ for g_in_round in 0..N_G_PER_ROUND {
+ let g = r * N_G_PER_ROUND + g_in_round;
+ let [la, lb, lc, ld] = G_LANES[g_in_round];
+ let [mx_idx, my_idx] = msg_idx[r][g_in_round];
+ let (a, b, c, d) = (state[la], state[lb], state[lc], state[ld]);
+ let mx = wire_from_slot_base(w, m_bit(mx_idx, 0));
+ let my = wire_from_slot_base(w, m_bit(my_idx, 0));
+
+ let tmp_0 = walk_add(&mut acc, u, w, &a, &b, g_add_carry_bit(g, ADD_TMP0, 0));
+ let a_1 = walk_add(&mut acc, u, w, &tmp_0, &mx, g_add_carry_bit(g, ADD_A1, 0));
+ let d_1 = wire_rotr(&wire_xor(&d, &a_1), 16);
+ let c_1 = walk_add(&mut acc, u, w, &c, &d_1, g_add_carry_bit(g, ADD_C1, 0));
+ let b_1 = wire_rotr(&wire_xor(&b, &c_1), 12);
+ let tmp_1 = walk_add(&mut acc, u, w, &a_1, &b_1, g_add_carry_bit(g, ADD_TMP1, 0));
+ let a_2 = walk_add(&mut acc, u, w, &tmp_1, &my, g_add_carry_bit(g, ADD_A2, 0));
+ let d_2 = wire_rotr(&wire_xor(&d_1, &a_2), 8);
+ let c_2 = walk_add(&mut acc, u, w, &c_1, &d_2, g_add_carry_bit(g, ADD_C2, 0));
+ let b_new = wire_rotr(&wire_xor(&b_1, &c_2), 7);
+ walk_lin_rows(&mut acc, u, &b_new, g_lin_bit(g, LIN_B_NEW, 0));
+ walk_lin_rows(&mut acc, u, &d_2, g_lin_bit(g, LIN_D_NEW, 0));
+
+ state[la] = a_2;
+ state[lb] = wire_from_slot_base(w, g_lin_bit(g, LIN_B_NEW, 0));
+ state[lc] = c_2;
+ state[ld] = wire_from_slot_base(w, g_lin_bit(g, LIN_D_NEW, 0));
+ }
+ }
+
+ // Finalization rows: out_lo[w] = state[w] ⊕ state[w+8],
+ // out_hi[w] = state[w+8] ⊕ cv[w]. Padding rows are empty: no contribution.
+ for wd in 0..8 {
+ let lo = wire_xor(&state[wd], &state[wd + 8]);
+ walk_lin_rows(&mut acc, u, &lo, out_lo_bit(wd, 0));
+ let cv_w = wire_from_slot_base(w, cv_bit(wd, 0));
+ let hi = wire_xor(&state[wd + 8], &cv_w);
+ walk_lin_rows(&mut acc, u, &hi, out_hi_bit(wd, 0));
+ }
+
+ // Fold in the factored constant-B and constant-A/B row sums.
+ (acc.a + wc * u_abconst, acc.b + wc * (acc.u_bconst + u_abconst))
+}
+
+/// `α·(uᵀ A_0 w) + (uᵀ B_0 w)` — the α-batched form lincheck's verifier
+/// consumes, by one circuit walk.
+pub fn bilinear_walk(alpha: F192, u: &[F192], w: &[F192]) -> F192 {
+ let (va, vb) = bilinear_walk_pair(u, w);
+ alpha * va + vb
+}
+
+/// Walk-capable [`crate::lincheck::LincheckCircuit`] over the BLAKE3 R1CS:
+/// `bilinear_form` answers lincheck's verifier in O(circuit) field ops via
+/// [`bilinear_walk`], so `lincheck::verify` never materializes the
+/// ~21M-nonzero substituted matrices' column marginal. The prover-side
+/// `fold_alpha_batched` delegates to the (lazily built) CSC fold — the
+/// verifier's fast path never calls it.
+pub struct WalkLincheckCircuit<'a> {
+ r1cs: &'a BlockR1cs,
+}
+
+impl<'a> WalkLincheckCircuit<'a> {
+ pub fn new(r1cs: &'a BlockR1cs) -> Self {
+ Self { r1cs }
+ }
+}
+
+impl crate::lincheck::LincheckCircuit for WalkLincheckCircuit<'_> {
+ fn n_cols(&self) -> usize {
+ K
+ }
+ fn const_pin_col(&self) -> Option {
+ self.r1cs.const_pin
+ }
+ fn fold_alpha_batched(&self, alpha: F192, eq_inner: &[F192]) -> Vec {
+ self.r1cs.csc_lincheck_circuit().fold_alpha_batched(alpha, eq_inner)
+ }
+ fn bilinear_form(&self, alpha: F192, u: &[F192], w: &[F192]) -> Option {
+ Some(bilinear_walk(alpha, u, w))
+ }
+}
+
/// [`BlockR1cs::family_digest`] of this module's circuit, baked as a constant:
/// recomputing it means building and hashing ~21M matrix entries (~300 ms),
/// which embedding protocols would otherwise pay inside their first prove.
/// The `family_digest_matches_baked` test recomputes and compares — a circuit
/// change fails it until this constant is updated alongside.
pub const FAMILY_DIGEST: [u8; 32] = [
- 0xaf, 0xed, 0x74, 0x72, 0xc6, 0xf7, 0x71, 0xa8, 0x57, 0x59, 0x92, 0x72, 0xff, 0x33, 0xa4,
- 0xda, 0x86, 0xb2, 0x1f, 0x26, 0x00, 0xf0, 0x57, 0xfa, 0x0d, 0xa7, 0x97, 0xd1, 0x58, 0x63,
- 0xeb, 0x58,
+ 0xaf, 0xed, 0x74, 0x72, 0xc6, 0xf7, 0x71, 0xa8, 0x57, 0x59, 0x92, 0x72, 0xff, 0x33, 0xa4, 0xda, 0x86, 0xb2, 0x1f,
+ 0x26, 0x00, 0xf0, 0x57, 0xfa, 0x0d, 0xa7, 0x97, 0xd1, 0x58, 0x63, 0xeb, 0x58,
];
/// Build a [`BlockR1cs`] batching `2^n_blocks_log` independent BLAKE3
@@ -692,13 +870,7 @@ fn write_word(z: &mut [bool], base: usize, val: u32) {
}
/// Build the witness block for ONE compression. Length = `K`.
-pub fn build_block_witness(
- cv: &[u32; 8],
- m: &[u32; 16],
- counter: u64,
- block_len: u32,
- flags: u32,
-) -> Vec {
+pub fn build_block_witness(cv: &[u32; 8], m: &[u32; 16], counter: u64, block_len: u32, flags: u32) -> Vec {
let mut z = vec![false; K];
z[Z_CONST_POS] = true;
// Inputs.
@@ -755,8 +927,7 @@ pub fn build_block_witness(
let d_1 = (d ^ a_1).rotate_right(16);
let c_1 = add_with_witness_carry_only(c, d_1, &mut z, g_add_carry_bit(g, ADD_C1, 0));
let b_1 = (b ^ c_1).rotate_right(12);
- let tmp_1 =
- add_with_witness_carry_only(a_1, b_1, &mut z, g_add_carry_bit(g, ADD_TMP1, 0));
+ let tmp_1 = add_with_witness_carry_only(a_1, b_1, &mut z, g_add_carry_bit(g, ADD_TMP1, 0));
let a_2 = add_with_witness_carry_only(tmp_1, my, &mut z, g_add_carry_bit(g, ADD_A2, 0));
let d_2 = (d_1 ^ a_2).rotate_right(8);
let c_2 = add_with_witness_carry_only(c_1, d_2, &mut z, g_add_carry_bit(g, ADD_C2, 0));
@@ -837,7 +1008,7 @@ pub fn generate_witness(blocks: &[Compression], n_blocks_log: usize) -> Vec (
- Vec,
- Vec,
- Vec,
+ Vec,
+ Vec,
+ Vec,
) {
- use primitives::field::F128;
+ use primitives::field::F192;
use rayon::prelude::*;
let n_total = 1usize << n_blocks_log;
let n_blocks = blocks.len();
@@ -1035,39 +1207,36 @@ pub fn generate_witness_with_ab_packed(
"{n_blocks} compressions > 2^{n_blocks_log} = {n_total} slots"
);
- const F128_PER_BLOCK: usize = K / 128;
- let total_f128 = n_total * F128_PER_BLOCK;
- let mut z = vec![F128::ZERO; total_f128];
- let mut a = vec![F128::ZERO; total_f128];
- let mut b = vec![F128::ZERO; total_f128];
+ const PACKED_PER_BLOCK: usize = K / 128;
+ let total_packed = n_total * PACKED_PER_BLOCK;
+ let mut z = vec![F192::ZERO; total_packed];
+ let mut a = vec![F192::ZERO; total_packed];
+ let mut b = vec![F192::ZERO; total_packed];
// Constant-wire pin (see lincheck's `LincheckCircuit::const_pin_col`): padding slots get the pinned
// compression of the all-zero message (constant wire = 1), matching
// [`generate_witness_with_ab_packed_and_lincheck`].
let padding = padding_block();
- z.par_chunks_mut(F128_PER_BLOCK)
- .zip(a.par_chunks_mut(F128_PER_BLOCK))
- .zip(b.par_chunks_mut(F128_PER_BLOCK))
+ z.par_chunks_mut(PACKED_PER_BLOCK)
+ .zip(a.par_chunks_mut(PACKED_PER_BLOCK))
+ .zip(b.par_chunks_mut(PACKED_PER_BLOCK))
.enumerate()
.for_each(|(idx, ((z_c, a_c), b_c))| {
- let (cv, m, t, bl, fl) = if idx < n_blocks {
- &blocks[idx]
- } else {
- &padding
- };
- // SAFETY: F128 is repr(C, align(16)) with LE u64 halves — same
- // byte layout as a u64 pair.
- let z_u64: &mut [u64] = unsafe {
- std::slice::from_raw_parts_mut(z_c.as_mut_ptr() as *mut u64, z_c.len() * 2)
- };
- let a_u64: &mut [u64] = unsafe {
- std::slice::from_raw_parts_mut(a_c.as_mut_ptr() as *mut u64, a_c.len() * 2)
- };
- let b_u64: &mut [u64] = unsafe {
- std::slice::from_raw_parts_mut(b_c.as_mut_ptr() as *mut u64, b_c.len() * 2)
- };
- build_block_witness_ab_packed_into(cv, m, *t, *bl, *fl, z_u64, a_u64, b_u64);
+ let (cv, m, t, bl, fl) = if idx < n_blocks { &blocks[idx] } else { &padding };
+ let mut z_u64 = vec![0u64; z_c.len() * 2];
+ let mut a_u64 = vec![0u64; a_c.len() * 2];
+ let mut b_u64 = vec![0u64; b_c.len() * 2];
+ build_block_witness_ab_packed_into(cv, m, *t, *bl, *fl, &mut z_u64, &mut a_u64, &mut b_u64);
+ for (dst, words) in z_c.iter_mut().zip(z_u64.chunks_exact(2)) {
+ *dst = F192::new(words[0], words[1], 0);
+ }
+ for (dst, words) in a_c.iter_mut().zip(a_u64.chunks_exact(2)) {
+ *dst = F192::new(words[0], words[1], 0);
+ }
+ for (dst, words) in b_c.iter_mut().zip(b_u64.chunks_exact(2)) {
+ *dst = F192::new(words[0], words[1], 0);
+ }
});
(z, a, b)
@@ -1090,9 +1259,9 @@ pub fn generate_witness_with_ab_packed_and_lincheck(
blocks: &[Compression],
n_blocks_log: usize,
) -> (
- Vec,
- Vec,
- Vec,
+ Vec,
+ Vec,
+ Vec,
Vec,
) {
// Constant-wire pin (see lincheck's `LincheckCircuit::const_pin_col`): fill padding blocks with the
@@ -1112,15 +1281,26 @@ pub fn generate_witness_with_ab_packed_and_lincheck(
)
}
+/// Serialize the 128-bit packed-witness subspace of F192. The third limb is
+/// constrained to zero by construction and is not part of Flock's bit cube.
+fn packed_128_bytes(words: &[F192]) -> Vec {
+ let mut out = Vec::with_capacity(words.len() * 16);
+ for word in words {
+ debug_assert_eq!(word.c2, 0, "packed Flock witness escaped 128-bit subspace");
+ out.extend_from_slice(&word.c0.to_le_bytes());
+ out.extend_from_slice(&word.c1.to_le_bytes());
+ }
+ out
+}
+
// ---------------------------------------------------------------------------
// Convenience API: Blake3Setup
// ---------------------------------------------------------------------------
-/// Bundles the monolithic BLAKE3 compression R1CS sized for `n_blocks`
-/// compressions.
+/// Bundles the monolithic BLAKE3 compression R1CS for the smallest supported
+/// power-of-two shape that can hold `n_blocks` compressions.
#[derive(Clone, Debug)]
pub struct Blake3Setup {
- pub n_blocks: usize,
pub r1cs: BlockR1cs,
}
@@ -1135,7 +1315,7 @@ impl Blake3Setup {
// the prove-cycle scratch buffers (see scratch::prewarm_prover).
r1cs.csc_lincheck_circuit();
primitives::scratch::prewarm_prover(r1cs.m);
- Self { n_blocks, r1cs }
+ Self { r1cs }
}
pub fn m(&self) -> usize {
@@ -1157,6 +1337,7 @@ impl Blake3Setup {
mod tests {
use super::*;
use crate::test_rng::Rng;
+ use primitives::pretty_integer;
#[test]
fn family_digest_matches_baked() {
@@ -1167,6 +1348,97 @@ mod tests {
);
}
+ /// Timing: the three ways the native verifier can evaluate the A_0/B_0
+ /// bilinear forms. Run with
+ /// `cargo test --release -p flock bench_bilinear -- --ignored --nocapture`.
+ #[test]
+ #[ignore]
+ fn bench_bilinear_walk_vs_matrices() {
+ let mut rng = Rng::new(0xBE9C);
+ let u: Vec = rng.ext_vec(K);
+ let w: Vec = rng.ext_vec(K);
+ let alpha = rng.ext();
+
+ // One-time setup costs the sparse paths pay (process-cached in prod,
+ // but real for a one-shot native verifier).
+ let t = std::time::Instant::now();
+ let (ma, mb) = (build_matrices().0, build_matrices().1);
+ println!("build_matrices (×2 redundant here): {:?}", t.elapsed());
+ let nnz: usize =
+ ma.rows.iter().map(|r| r.len()).sum::() + mb.rows.iter().map(|r| r.len()).sum::();
+ println!("total nonzeros (A_0 + B_0): {}", pretty_integer(nnz));
+ let r1cs = build_block_r1cs(3);
+ let t = std::time::Instant::now();
+ let csc = r1cs.csc_lincheck_circuit();
+ println!("CSC transpose build: {:?}", t.elapsed());
+
+ // (a) check_reduced-style naive contraction, both matrices.
+ let contract = |m: &SparseBinaryMatrix| -> F192 {
+ let mut acc = F192::ZERO;
+ for (i, row) in m.rows.iter().enumerate() {
+ let s = row.iter().map(|&j| w[j]).fold(F192::ZERO, |a, x| a + x);
+ acc += u[i] * s;
+ }
+ acc
+ };
+ let t = std::time::Instant::now();
+ let (da, db) = (contract(&ma), contract(&mb));
+ let t_naive = t.elapsed();
+ println!("naive sparse contraction (A + B): {t_naive:?}");
+
+ // (b) lincheck-verifier-style CSC marginal + inner product.
+ use crate::lincheck::LincheckCircuit;
+ let t = std::time::Instant::now();
+ let marginal = csc.fold_alpha_batched(alpha, &u);
+ let form_csc = pcs::ring_switch::inner_product_ext(&marginal, &w);
+ let t_csc = t.elapsed();
+ println!("CSC marginal fold + inner product: {t_csc:?}");
+
+ // (c) the circuit walk.
+ let t = std::time::Instant::now();
+ let (wa, wb) = bilinear_walk_pair(&u, &w);
+ let t_walk = t.elapsed();
+ println!("bilinear_walk_pair: {t_walk:?}");
+
+ assert_eq!((wa, wb), (da, db));
+ assert_eq!(alpha * wa + wb, form_csc);
+ println!(
+ "speedup: {:.1}× vs naive, {:.1}× vs CSC",
+ t_naive.as_secs_f64() / t_walk.as_secs_f64(),
+ t_csc.as_secs_f64() / t_walk.as_secs_f64()
+ );
+ }
+
+ /// The circuit walk computes the same bilinear forms as the materialized
+ /// matrices, for fully random (unstructured) row/column weights: any
+ /// missing, extra, or misplaced row contribution would break equality.
+ #[test]
+ fn bilinear_walk_matches_matrices() {
+ let (ma, mb) = matrices();
+ let mut rng = Rng::new(0xC12C);
+ for trial in 0..3 {
+ let alpha = rng.ext();
+ let u: Vec = rng.ext_vec(K);
+ let w: Vec = rng.ext_vec(K);
+ let contract = |m: &SparseBinaryMatrix| -> F192 {
+ m.rows
+ .iter()
+ .enumerate()
+ .map(|(i, row)| u[i] * row.iter().map(|&j| w[j]).fold(F192::ZERO, |acc, x| acc + x))
+ .fold(F192::ZERO, |acc, x| acc + x)
+ };
+ let (direct_a, direct_b) = (contract(ma), contract(mb));
+ let (walk_a, walk_b) = bilinear_walk_pair(&u, &w);
+ assert_eq!(walk_a, direct_a, "A-side, trial {trial}");
+ assert_eq!(walk_b, direct_b, "B-side, trial {trial}");
+ assert_eq!(
+ bilinear_walk(alpha, &u, &w),
+ alpha * direct_a + direct_b,
+ "alpha-batched, trial {trial}"
+ );
+ }
+ }
+
/// BLAKE3 chunk flags (subset).
const CHUNK_START: u32 = 1 << 0;
const CHUNK_END: u32 = 1 << 1;
@@ -1193,13 +1465,7 @@ mod tests {
/// (a single root-block, single-chunk, ROOT-flagged compression).
#[test]
fn compress_matches_blake3_crate_empty() {
- let state = blake3_compress(
- &BLAKE3_IV,
- &[0u32; 16],
- 0,
- 0,
- CHUNK_START | CHUNK_END | ROOT,
- );
+ let state = blake3_compress(&BLAKE3_IV, &[0u32; 16], 0, 0, CHUNK_START | CHUNK_END | ROOT);
let mut got = [0u8; 32];
for w in 0..8 {
got[w * 4..w * 4 + 4].copy_from_slice(&state[w].to_le_bytes());
@@ -1266,15 +1532,18 @@ mod tests {
.map(|_| {
let cv: [u32; 8] = std::array::from_fn(|_| rng.next_u32());
let m: [u32; 16] = std::array::from_fn(|_| rng.next_u32());
- (cv, m, rng.next_u32() as u64 | ((rng.next_u32() as u64) << 32), rng.next_u32() % 65, rng.next_u32())
+ (
+ cv,
+ m,
+ rng.next_u32() as u64 | ((rng.next_u32() as u64) << 32),
+ rng.next_u32() % 65,
+ rng.next_u32(),
+ )
})
.collect();
let z = generate_witness(&blocks, n_log);
assert_eq!(z.len(), r1cs.n());
- assert!(
- r1cs.satisfies(&z),
- "witness for {n_blocks} compressions fails R1CS"
- );
+ assert!(r1cs.satisfies(&z), "witness for {n_blocks} compressions fails R1CS");
}
}
@@ -1288,10 +1557,7 @@ mod tests {
assert!(r1cs.satisfies(&z));
// Flip a carry_aux bit inside G #10 (middle of round 1).
z[g_add_carry_bit(10, ADD_A2, 5)] ^= true;
- assert!(
- !r1cs.satisfies(&z),
- "tampered carry bit should violate R1CS"
- );
+ assert!(!r1cs.satisfies(&z), "tampered carry bit should violate R1CS");
}
/// The fused generator produces (z, a, b) byte-identical to
@@ -1313,8 +1579,7 @@ mod tests {
let (z1, a1, b1) = generate_witness_with_ab_packed(&blocks, n_log);
let lincheck_ref = pack_z_lincheck_from_packed(&z1, r1cs.m, r1cs.k_log);
- let (z2, a2, b2, lincheck_new) =
- generate_witness_with_ab_packed_and_lincheck(&blocks, n_log);
+ let (z2, a2, b2, lincheck_new) = generate_witness_with_ab_packed_and_lincheck(&blocks, n_log);
assert_eq!(z1, z2, "z mismatch at n_blocks={n_blocks}");
assert_eq!(a1, a2, "a mismatch at n_blocks={n_blocks}");
assert_eq!(b1, b2, "b mismatch at n_blocks={n_blocks}");
@@ -1327,9 +1592,7 @@ mod tests {
#[test]
fn setup_sizes_correctly() {
- for &(n_blocks, expected_n_log) in
- &[(1usize, 3), (8, 3), (9, 4), (16, 4), (17, 5), (1000, 10)]
- {
+ for &(n_blocks, expected_n_log) in &[(1usize, 3), (8, 3), (9, 4), (16, 4), (17, 5), (1000, 10)] {
let setup = Blake3Setup::new(n_blocks);
assert_eq!(setup.n_blocks_log(), expected_n_log, "n_blocks={n_blocks}");
assert_eq!(setup.m(), K_LOG + expected_n_log);
@@ -1349,11 +1612,10 @@ mod tests {
let inner_rest_len = r1cs.k_log - r1cs.k_skip;
// Correctly-shaped buffers (padding-only generation), then zeroed.
- let (mut z, mut a, mut b, mut zlc) =
- generate_witness_with_ab_packed_and_lincheck(&[], setup.n_blocks_log());
- z.fill(F128::ZERO);
- a.fill(F128::ZERO);
- b.fill(F128::ZERO);
+ let (mut z, mut a, mut b, mut zlc) = generate_witness_with_ab_packed_and_lincheck(&[], setup.n_blocks_log());
+ z.fill(F192::ZERO);
+ a.fill(F192::ZERO);
+ b.fill(F192::ZERO);
zlc.fill(0);
// Prover side: the reduction happily runs on the zero witness.
@@ -1361,20 +1623,13 @@ mod tests {
k_log: r1cs.k_log,
useful_bits_per_block: r1cs.useful_bits,
};
- let as_bytes = |v: &[F128]| unsafe {
- std::slice::from_raw_parts(
- v.as_ptr() as *const u8,
- std::mem::size_of_val(v),
- )
- };
+ let a_bytes = packed_128_bytes(&a);
+ let b_bytes = packed_128_bytes(&b);
+ let z_bytes = packed_128_bytes(&z);
let mut ps = pcs::ProverState::new(b"const-pin-poc", &[]);
- let (zc_claim, _s_hat_v_c) = crate::zerocheck::prove_packed_padded(
- as_bytes(&a),
- as_bytes(&b),
- as_bytes(&z), // C = I, so c == z
- r1cs.m,
- &padding,
- &mut ps,
+ let (zc_claim, _s_hat_v_c) = crate::zerocheck::prove_packed_padded_capture_s_hat_v_c(
+ &a_bytes, &b_bytes, &z_bytes, // C = I, so c == z
+ r1cs.m, &padding, &mut ps,
);
let x_ab = crate::lincheck::QuirkyPoint {
z_skip: zc_claim.z,
@@ -1395,8 +1650,7 @@ mod tests {
// Verifier side: zerocheck accepts, the lincheck const-wire pin rejects.
let mut vs = pcs::VerifierState::new(b"const-pin-poc", &proof_t, &[]);
- let zc = crate::zerocheck::verify(r1cs.m, &mut vs)
- .expect("zerocheck accepts the all-zero witness");
+ let zc = crate::zerocheck::verify(r1cs.m, &mut vs).expect("zerocheck accepts the all-zero witness");
let x_ab_v = crate::lincheck::QuirkyPoint {
z_skip: zc.z,
x_inner_rest: zc.mlv_challenges[..inner_rest_len].to_vec(),
@@ -1413,19 +1667,14 @@ mod tests {
&mut vs,
);
assert!(
- matches!(
- res,
- Err(crate::lincheck::VerifyError::ConsistencyFailed { .. })
- ),
+ matches!(res, Err(crate::lincheck::VerifyError::ConsistencyFailed { .. })),
"all-zero witness must be rejected by the constant-wire pin; got {res:?}"
);
}
}
-// ===== leanVM-b stacked BLAKE3 reduction (grafted) =====
-// (No Blake3StackProof struct: the zerocheck / lincheck / ring-switch scalars
-// ride the shared transcript stream, and the one hash-bearing Ligerito rides
-// the caller's opening channel.)
+// The zerocheck, lincheck, and ring-switch scalars use the shared transcript;
+// the caller carries the Ligerito opening.
/// One claim on the committed packed BLAKE3 witness `q_pkd`, as left by the
/// Flock reduction and handed to the PCS. `claim` is the `ẑ(point) = value`
@@ -1435,7 +1684,7 @@ mod tests {
#[derive(Clone, Debug)]
pub struct WitnessClaim {
pub claim: crate::proof::ZClaim,
- pub s_hat_v: Option>,
+ pub s_hat_v: Option>,
}
/// The two claims on the committed witness `q_pkd` left by the Flock BLAKE3
@@ -1446,7 +1695,7 @@ pub struct WitnessClaim {
/// This is the clean seam between Flock's reduction and the PCS: the reduction
/// produces these; the PCS opens them (see [`Blake3Setup::prove_reduction`]).
#[derive(Clone, Debug)]
-pub struct ReducedClaims {
+pub struct PackedWitnessClaims {
pub ab: WitnessClaim,
pub c: WitnessClaim,
}
@@ -1461,17 +1710,6 @@ pub struct ReductionReplay {
pub lc_claim: crate::lincheck::LincheckClaim,
}
-/// Construct a multilinear `x_outer_full` of length `m − k_skip` from a
-/// QuirkyPoint: concatenate `x_inner_rest` and `x_outer`. This is the format
-/// the PCS expects (k_skip = 6 absorbed via `z_skip`; everything else is
-/// multilinear).
-fn quirky_x_outer_full(point: &crate::lincheck::QuirkyPoint) -> Vec {
- let mut v = Vec::with_capacity(point.x_inner_rest.len() + point.x_outer.len());
- v.extend_from_slice(&point.x_inner_rest);
- v.extend_from_slice(&point.x_outer);
- v
-}
-
impl Blake3Setup {
/// **Flock reduction (prover).** Run the BLAKE3 zerocheck and lincheck on
/// the shared transcript, reducing R1CS validity of `blocks` to two
@@ -1479,21 +1717,24 @@ impl Blake3Setup {
/// statement is already transcript-bound: the embedding protocol seeds
/// with the circuit family digest and announces the count.) Returns:
/// - `z_packed`: the regenerated packed witness the PCS later opens against;
- /// - the [`ReducedClaims`] `(ab, c)` on `q_pkd`, with ring-switch weights.
+ /// - the [`PackedWitnessClaims`] `(ab, c)` on `q_pkd`, with ring-switch weights.
///
/// Does NOT open the PCS; the caller discharges the returned claims in the
- /// one stacked opening (`lean_vm`'s `pcs::open`, or
- /// [`Self::prove_validity_stacked`] for a standalone roundtrip).
- pub fn prove_reduction(
+ /// one stacked opening (`lean_vm`'s `pcs::open`).
+ pub fn prove_reduction(
&self,
blocks: &[Compression],
- stack_commitment: &Commitment,
- ps: &mut ProverState,
- ) -> (Vec, ReducedClaims) {
- assert_eq!(blocks.len(), self.n_blocks);
+ ps: &mut fiat_shamir::transcript::ProverState,
+ ) -> (Vec, PackedWitnessClaims) {
+ assert!(
+ blocks.len() <= self.n_block_slots(),
+ "{} compressions exceed this setup's {} slots",
+ blocks.len(),
+ self.n_block_slots()
+ );
let n_log = self.n_blocks_log();
let t_witness = std::time::Instant::now();
- let (z_packed, a_packed_f128, b_packed_f128, z_packed_lincheck) =
+ let (z_packed, a_packed_words, b_packed_words, z_packed_lincheck) =
generate_witness_with_ab_packed_and_lincheck(blocks, n_log);
if std::env::var_os("FLOCK_PROVE_TRACE").is_some() {
eprintln!(
@@ -1501,43 +1742,33 @@ impl Blake3Setup {
t_witness.elapsed().as_secs_f64() * 1e3,
);
}
- let reduced = self.prove_reduction_precomputed(
- &z_packed,
- &a_packed_f128,
- &b_packed_f128,
- &z_packed_lincheck,
- ps,
- );
- // The embedding protocol has already transcript-bound the commitment.
- let _ = stack_commitment;
+ let reduced =
+ self.prove_reduction_precomputed(&z_packed, &a_packed_words, &b_packed_words, &z_packed_lincheck, ps);
(z_packed, reduced)
}
/// **Flock reduction from a prepared witness (prover).** This is the
/// witness-generation-free counterpart of [`Self::prove_reduction`] for
- /// embedders that already generated `q_pkd` together with its `A·z`, `B·z`,
- /// and lincheck-stripe buffers before committing it. Reusing those buffers
- /// avoids repeating the fused witness pass after commitment.
- pub fn prove_reduction_precomputed(
+ /// embedders that already generated the packed `z`, `A·z`, `B·z`, and
+ /// lincheck-stripe buffers before committing the flattened witness.
+ pub fn prove_reduction_precomputed(
&self,
- z_packed: &[F128],
- a_packed_f128: &[F128],
- b_packed_f128: &[F128],
+ z_packed: &[F192],
+ a_packed_words: &[F192],
+ b_packed_words: &[F192],
z_packed_lincheck: &[u8],
- ps: &mut ProverState,
- ) -> ReducedClaims {
+ ps: &mut fiat_shamir::transcript::ProverState,
+ ) -> PackedWitnessClaims {
let trace = std::env::var_os("FLOCK_PROVE_TRACE").is_some();
let t_reduction = std::time::Instant::now();
- let packed_len = 1usize << (self.r1cs.m - pcs::LOG_PACKING);
+ // The fused generator packs 128 Boolean coordinates in each F192
+ // container; the third tower limb is constrained to zero.
+ let packed_len = 1usize << (self.r1cs.m - 7);
assert_eq!(z_packed.len(), packed_len, "wrong packed witness length");
- assert_eq!(a_packed_f128.len(), packed_len, "wrong packed A·z length");
- assert_eq!(b_packed_f128.len(), packed_len, "wrong packed B·z length");
- assert_eq!(
- z_packed_lincheck.len(),
- packed_len * core::mem::size_of::(),
- "wrong lincheck stripe length"
- );
+ assert_eq!(a_packed_words.len(), packed_len, "wrong packed A·z length");
+ assert_eq!(b_packed_words.len(), packed_len, "wrong packed B·z length");
+ assert_eq!(z_packed_lincheck.len(), packed_len * 16, "wrong lincheck stripe length");
// No bind_statement here: the embedding protocol (leanVM-b) seeds its
// transcript with the circuit-FAMILY digest and binds the instance
@@ -1550,26 +1781,16 @@ impl Blake3Setup {
};
let t_zerocheck = std::time::Instant::now();
let (zc_claim, s_hat_v_c) = {
- let a_packed: &[u8] = unsafe {
- std::slice::from_raw_parts(
- a_packed_f128.as_ptr() as *const u8,
- a_packed_f128.len() * core::mem::size_of::(),
- )
- };
- let b_packed: &[u8] = unsafe {
- std::slice::from_raw_parts(
- b_packed_f128.as_ptr() as *const u8,
- b_packed_f128.len() * core::mem::size_of::(),
- )
- };
- let c_packed: &[u8] = unsafe {
- std::slice::from_raw_parts(
- z_packed.as_ptr() as *const u8,
- z_packed.len() * core::mem::size_of::(),
- )
- };
- crate::zerocheck::prove_packed_padded(
- a_packed, b_packed, c_packed, self.r1cs.m, &padding, ps,
+ let a_packed = packed_128_bytes(a_packed_words);
+ let b_packed = packed_128_bytes(b_packed_words);
+ let c_packed = packed_128_bytes(z_packed);
+ crate::zerocheck::prove_packed_padded_capture_s_hat_v_c(
+ &a_packed,
+ &b_packed,
+ &c_packed,
+ self.r1cs.m,
+ &padding,
+ ps,
)
};
let zerocheck_time = t_zerocheck.elapsed();
@@ -1609,18 +1830,21 @@ impl Blake3Setup {
},
value: zc_claim.c_eval,
};
- let s_hat_v_ab = if self.r1cs.k_log >= pcs::LOG_PACKING {
- Some(pcs::ring_switch::s_hat_v_from_z_vec(
- &z_vec_pre,
- &lc_claim.r_inner_rest[1..],
- ))
+ let s_hat_v_ab = if self.r1cs.k_log >= pcs::pack::LOG_PACKING {
+ Some(pcs::ring_switch::s_hat_v_from_z_vec(&z_vec_pre, &lc_claim.r_inner_rest))
} else {
None
};
- let reduced = ReducedClaims {
- ab: WitnessClaim { claim: ab, s_hat_v: s_hat_v_ab },
- c: WitnessClaim { claim: c, s_hat_v: Some(s_hat_v_c) },
+ let reduced = PackedWitnessClaims {
+ ab: WitnessClaim {
+ claim: ab,
+ s_hat_v: s_hat_v_ab,
+ },
+ c: WitnessClaim {
+ claim: c,
+ s_hat_v: Some(s_hat_v_c),
+ },
};
if trace {
let reduction_time = t_reduction.elapsed();
@@ -1636,132 +1860,32 @@ impl Blake3Setup {
reduced
}
- /// Prove `blocks` are valid compressions in two clean phases:
- /// 1. [`Self::prove_reduction`] — Flock zerocheck + lincheck → the `(ab, c)`
- /// claims on the committed witness `q_pkd`;
- /// 2. the PCS: discharge those claims *together with* the caller's own
- /// `stack_pd` point claims in ONE stacked Ligerito open over `stack` (the
- /// caller's committed witness, with `q_pkd` the aligned sub-block at
- /// `stack_offset`).
- ///
- /// `stack_data`/`stack_commitment` are the caller's commit; the transcript
- /// `sponge` is shared.
- #[allow(clippy::too_many_arguments)]
- pub fn prove_validity_stacked(
- &self,
- blocks: &[Compression],
- stack: &[F128],
- stack_offset: usize,
- stack_data: &pcs::ProverData,
- stack_commitment: &Commitment,
- stack_pd: &[(Vec, F128)],
- ps: &mut ProverState,
- ) -> pcs::ligerito::LigeritoProof {
- let trace = std::env::var_os("FLOCK_PROVE_TRACE").is_some();
- let t_total = std::time::Instant::now();
-
- // Phase 1 — Flock reduction: zerocheck + lincheck → claims on q_pkd.
- let t_reduction = std::time::Instant::now();
- let (z_packed, reduced) = self.prove_reduction(blocks, stack_commitment, ps);
- let reduction_time = t_reduction.elapsed();
- debug_assert_eq!(
- &stack[stack_offset..stack_offset + z_packed.len()],
- z_packed.as_slice(),
- "committed q_pkd slice must equal the regenerated packed witness"
- );
-
- // Phase 2 — PCS: discharge the reduction's claims (plus the caller's
- // full-stack point claims) in one stacked open.
- let t_open = std::time::Instant::now();
- let proof = self.discharge_reduction_stacked(
- &z_packed,
- &reduced,
- stack,
- stack_offset,
- stack_data,
- stack_commitment,
- stack_pd,
- ps,
- );
- if trace {
- eprintln!(
- "[flock prove] stacked: {:.2} ms (reduction: {:.2} ms, open: {:.2} ms)",
- t_total.elapsed().as_secs_f64() * 1e3,
- reduction_time.as_secs_f64() * 1e3,
- t_open.elapsed().as_secs_f64() * 1e3,
- );
- }
- proof
- }
-
- /// Phase 2 of [`Self::prove_validity_stacked`]: the PCS open of the
- /// reduction's `(ab, c)` claims on `q_pkd` (`z_packed`), lifted into the
- /// caller's `stack` and batched with the caller's `stack_pd` point claims.
- #[allow(clippy::too_many_arguments)]
- pub fn discharge_reduction_stacked(
- &self,
- z_packed: &[F128],
- reduced: &ReducedClaims,
- stack: &[F128],
- stack_offset: usize,
- stack_data: &pcs::ProverData,
- stack_commitment: &Commitment,
- stack_pd: &[(Vec, F128)],
- ps: &mut ProverState,
- ) -> pcs::ligerito::LigeritoProof {
- let padding = crate::zerocheck::PaddingSpec {
- k_log: self.r1cs.k_log,
- useful_bits_per_block: self.r1cs.useful_bits,
- };
- let ab_x = quirky_x_outer_full(&reduced.ab.claim.point);
- let c_x = quirky_x_outer_full(&reduced.c.claim.point);
- // This standalone-flock path takes general full-stack point claims.
- let pd: Vec = stack_pd
- .iter()
- .map(|(point, value)| pcs::StackClaim::Point { point, value: *value })
- .collect();
- let lig_config = stacked_lig_config(stack_commitment);
- pcs::open_batch_mixed_ligerito_stacked(
- z_packed,
- &[ab_x.as_slice(), c_x.as_slice()],
- &[reduced.ab.s_hat_v.as_deref(), reduced.c.s_hat_v.as_deref()],
- &padding,
- stack,
- stack_offset,
- stack_data,
- stack_commitment,
- &pd,
- &lig_config,
- ps,
- )
- }
-
/// **Flock reduction (verifier).** Replay the BLAKE3 zerocheck and
/// lincheck straight off the shared transcript stream, recovering the two
/// `(ab, c)` evaluation claims on the committed witness `q_pkd`. Mirror of
/// [`Self::prove_reduction`]; the PCS then discharges the returned claims.
- pub fn verify_reduction(
+ pub fn verify_reduction(
&self,
- stack_commitment: &Commitment,
- vs: &mut VerifierState<'_>,
+ vs: &mut fiat_shamir::transcript::VerifierState<'_, O>,
) -> Result {
// Mirror of prove_reduction: the statement is bound by the embedding
// protocol's seed (family digest) + announced count + commitment root.
- let _ = stack_commitment;
- let zc_claim = crate::zerocheck::verify(self.r1cs.m, vs)
- .map_err(verifier::VerifyError::Zerocheck)?;
+ let zc_claim = crate::zerocheck::verify(self.r1cs.m, vs).map_err(verifier::VerifyError::Zerocheck)?;
let inner_rest_len = self.r1cs.k_log - self.r1cs.k_skip;
let x_ab = crate::lincheck::QuirkyPoint {
z_skip: zc_claim.z,
x_inner_rest: zc_claim.mlv_challenges[..inner_rest_len].to_vec(),
x_outer: zc_claim.mlv_challenges[inner_rest_len..].to_vec(),
};
+ // Walk-capable circuit: the verifier's lincheck consistency check is
+ // one circuit walk (O(circuit) field ops) instead of the ∝ NNZ CSC
+ // marginal fold. Same transcript, same accept/reject.
let lc_claim = crate::lincheck::verify(
self.r1cs.m,
self.r1cs.k_log,
self.r1cs.k_skip,
- self.r1cs.csc_lincheck_circuit(),
+ &WalkLincheckCircuit::new(&self.r1cs),
&x_ab,
zc_claim.a_eval,
zc_claim.b_eval,
@@ -1785,57 +1909,11 @@ impl Blake3Setup {
},
value: zc_claim.c_eval,
};
- Ok(ReductionReplay { ab, c, zc_claim, lc_claim })
- }
-
- /// Verifier mirror of [`Self::prove_validity_stacked`], in the same two
- /// phases: (1) [`Self::verify_reduction`] replays zerocheck + lincheck to
- /// recover the `(ab, c)` claims on `q_pkd`, then (2) the stacked Ligerito
- /// opening of those claims (and the caller's `stack_pd`) is verified against
- /// `stack_commitment`. `stack_offset` and the derived `qpkd_vars` locate
- /// `q_pkd` inside the stack.
- pub fn verify_validity_stacked(
- &self,
- stack_commitment: &Commitment,
- stack_offset: usize,
- stack_pd: &[(Vec, F128)],
- open: &pcs::ligerito::LigeritoProof,
- vs: &mut VerifierState<'_>,
- ) -> Result<(), verifier::VerifyError> {
- // Phase 1 — Flock reduction: replay zerocheck + lincheck → (ab, c).
- let ReductionReplay { ab, c, .. } = self.verify_reduction(stack_commitment, vs)?;
-
- // Phase 2 — PCS: verify the stacked opening of (ab, c) + stack_pd.
- let ab_x = quirky_x_outer_full(&ab.point);
- let c_x = quirky_x_outer_full(&c.point);
- let qpkd_vars = self.r1cs.m - pcs::LOG_PACKING;
- let pd: Vec = stack_pd
- .iter()
- .map(|(point, value)| pcs::StackClaim::Point { point, value: *value })
- .collect();
- let lig_config = stacked_lig_config(stack_commitment);
- pcs::verify_opening_batch_mixed_ligerito_stacked(
- stack_commitment,
- stack_offset,
- qpkd_vars,
- &[ab.value, c.value],
- &[ab.point.z_skip, c.point.z_skip],
- &[ab_x.as_slice(), c_x.as_slice()],
- &pd,
- open,
- &lig_config,
- vs,
- )
- .map(|_| ())
- .map_err(verifier::VerifyError::Pcs)
+ Ok(ReductionReplay {
+ ab,
+ c,
+ zc_claim,
+ lc_claim,
+ })
}
}
-
-/// The Ligerito config for a stacked open against
-/// `stack_commitment` — derived from the commitment's own `(m, profile)` params,
-/// so both sides agree by construction.
-fn stacked_lig_config(stack_commitment: &Commitment) -> pcs::ligerito::LigeritoConfig {
- pcs::ligerito::LigeritoSecurityConfig::derive_config(stack_commitment.params.m)
- .and_then(|sec| sec.to_config())
- .expect("ligerito config for stacked open")
-}
diff --git a/crates/flock/src/blake3_witness.rs b/crates/flock/src/blake3_witness.rs
index 56eeca32..8c0a4eb8 100644
--- a/crates/flock/src/blake3_witness.rs
+++ b/crates/flock/src/blake3_witness.rs
@@ -1,12 +1,12 @@
-// Credit: https://github.com/succinctlabs/flock (flock-prover), MIT OR Apache-2.0.
+// CREDIT: https://github.com/succinctlabs/flock (flock-prover), MIT OR Apache-2.0.
//! Bit-packing and R1CS-row helpers for the monolithic hash R1CS modules
//! (only `blake3` in this vendored subset).
use std::sync::OnceLock;
-use primitives::bits::transpose_8_u64s_to_64_bytes;
-use primitives::field::F128;
use crate::r1cs::{BlockR1cs, SparseBinaryMatrix, WitnessLayout};
+use primitives::bits::transpose_8_u64s_to_64_bytes;
+use primitives::field::F192;
/// OR the low 32 bits of `val` into `buf` starting at bit-offset `bit_off`.
/// Handles u64 straddling when `bit_off % 64 > 32`.
@@ -122,15 +122,9 @@ pub(crate) fn build_block_r1cs_with_matrices(
b_0: SparseBinaryMatrix,
const_pin: Option,
) -> BlockR1cs {
- assert!(
- n_blocks_log >= 3,
- "lincheck needs n_outer ≥ 8 — pick n_blocks_log ≥ 3"
- );
+ assert!(n_blocks_log >= 3, "lincheck needs n_outer ≥ 8 — pick n_blocks_log ≥ 3");
let k = 1usize << k_log;
- assert!(
- useful_bits <= k,
- "useful_bits ({useful_bits}) must be ≤ 2^k_log ({k})"
- );
+ assert!(useful_bits <= k, "useful_bits ({useful_bits}) must be ≤ 2^k_log ({k})");
BlockR1cs {
m: k_log + n_blocks_log,
k_log,
@@ -157,7 +151,7 @@ pub(crate) fn build_block_r1cs_with_matrices(
/// Drive the parallel chunked witness build for `n_blocks` instances padded
/// to `2^n_blocks_log` slots. Returns `(z, a, b, z_lincheck)` packed in
-/// F128 form (z/a/b) and byte-stripe form (z_lincheck).
+/// F192 form (z/a/b) and byte-stripe form (z_lincheck).
///
/// `per_block(initial, z_u64, a_u64, b_u64)` populates one block's worth of
/// `(z, a, b)` data — 3 zero-initialized `u64`-buffers of length `K / 64`.
@@ -176,14 +170,14 @@ pub(crate) fn drive_witness_packed_and_lincheck(
n_blocks_log: usize,
k_log: usize,
per_block: F,
-) -> (Vec, Vec, Vec, Vec)
+) -> (Vec, Vec, Vec, Vec)
where
F: Fn(&S, &mut [u64], &mut [u64], &mut [u64]) + Sync,
{
use rayon::prelude::*;
let k = 1usize << k_log;
- let f128_per_block = k / 128;
+ let packed_per_block = k / 128;
let u64_per_block = k / 64;
let n_total = 1usize << n_blocks_log;
let n_blocks = initial_states.len();
@@ -196,35 +190,31 @@ where
"lincheck stripe layout requires n_total ≥ 8 and divisible by 8"
);
- let total_f128 = n_total * f128_per_block;
+ let total_packed = n_total * packed_per_block;
// z/a/b are allocated uninitialized and zeroed *inside* the parallel loop
// (one memset per 8-block group), so the ~192 MB zero-fill scales with the
// thread count instead of running serially on the main thread before the
// parallel build. The per-block builders OR 1-bits into pre-zeroed words,
// so each group must be zeroed before its `per_block` calls. `z_lincheck`
// stays `vec![0u8; _]` (lazy `alloc_zeroed`/mmap — no eager memset).
- let mut z = primitives::scratch::take_f128(total_f128);
- let mut a = primitives::scratch::take_f128(total_f128);
- let mut b = primitives::scratch::take_f128(total_f128);
+ let mut z = primitives::scratch::take_f192(total_packed);
+ let mut a = primitives::scratch::take_f192(total_packed);
+ let mut b = primitives::scratch::take_f192(total_packed);
let mut z_lincheck = vec![0u8; (n_total / 8) * k];
- z.par_chunks_mut(8 * f128_per_block)
- .zip(a.par_chunks_mut(8 * f128_per_block))
- .zip(b.par_chunks_mut(8 * f128_per_block))
+ z.par_chunks_mut(8 * packed_per_block)
+ .zip(a.par_chunks_mut(8 * packed_per_block))
+ .zip(b.par_chunks_mut(8 * packed_per_block))
.zip(z_lincheck.par_chunks_mut(k))
.enumerate()
.for_each(|(g, (((z_grp, a_grp), b_grp), stripe))| {
- // Zero this group's z/a/b up front (parallel memset — the buffers
- // were uninit-allocated). The per-block builder ORs 1-bits into
- // pre-zeroed words; any slot left unbuilt (no padding block) stays
- // zero, which the lincheck transpose below reads correctly.
- // SAFETY: F128 is `Copy` (no Drop) and the all-zero bit pattern is
- // the valid `F128::ZERO`, so a byte memset is a correct init.
- unsafe {
- std::ptr::write_bytes(z_grp.as_mut_ptr(), 0, z_grp.len());
- std::ptr::write_bytes(a_grp.as_mut_ptr(), 0, a_grp.len());
- std::ptr::write_bytes(b_grp.as_mut_ptr(), 0, b_grp.len());
- }
+ // The circuit witness remains 128-bit packed even though protocol
+ // scalars are F192. Build contiguous u64 pairs, then embed each
+ // pair as (lo, hi, 0); F192's 24-byte stride cannot be viewed as a
+ // contiguous u64-pair array.
+ let mut z_words = vec![0u64; 8 * u64_per_block];
+ let mut a_words = vec![0u64; 8 * u64_per_block];
+ let mut b_words = vec![0u64; 8 * u64_per_block];
for k_in in 0..8 {
let global_idx = 8 * g + k_in;
let init: &S = if global_idx < n_blocks {
@@ -237,46 +227,34 @@ where
// No padding block — leave this slot zero.
continue;
};
- let z_chunk = &mut z_grp[k_in * f128_per_block..(k_in + 1) * f128_per_block];
- let a_chunk = &mut a_grp[k_in * f128_per_block..(k_in + 1) * f128_per_block];
- let b_chunk = &mut b_grp[k_in * f128_per_block..(k_in + 1) * f128_per_block];
- // SAFETY: F128 is `repr(C, align(16))` with two `u64` fields in
- // LE order — same byte layout as a u64 pair.
- let z_u64: &mut [u64] = unsafe {
- std::slice::from_raw_parts_mut(
- z_chunk.as_mut_ptr() as *mut u64,
- z_chunk.len() * 2,
- )
- };
- let a_u64: &mut [u64] = unsafe {
- std::slice::from_raw_parts_mut(
- a_chunk.as_mut_ptr() as *mut u64,
- a_chunk.len() * 2,
- )
- };
- let b_u64: &mut [u64] = unsafe {
- std::slice::from_raw_parts_mut(
- b_chunk.as_mut_ptr() as *mut u64,
- b_chunk.len() * 2,
- )
- };
+ let range = k_in * u64_per_block..(k_in + 1) * u64_per_block;
+ let z_u64 = &mut z_words[range.clone()];
+ let a_u64 = &mut a_words[range.clone()];
+ let b_u64 = &mut b_words[range];
per_block(init, z_u64, a_u64, b_u64);
}
+ for (dst, words) in z_grp.iter_mut().zip(z_words.chunks_exact(2)) {
+ *dst = F192::new(words[0], words[1], 0);
+ }
+ for (dst, words) in a_grp.iter_mut().zip(a_words.chunks_exact(2)) {
+ *dst = F192::new(words[0], words[1], 0);
+ }
+ for (dst, words) in b_grp.iter_mut().zip(b_words.chunks_exact(2)) {
+ *dst = F192::new(words[0], words[1], 0);
+ }
+
// Bit-transpose 8 z chunks into the lincheck stripe.
- let z_u64_all: &[u64] = unsafe {
- std::slice::from_raw_parts(z_grp.as_ptr() as *const u64, z_grp.len() * 2)
- };
for i in 0..u64_per_block {
let lanes: [u64; 8] = [
- z_u64_all[i],
- z_u64_all[u64_per_block + i],
- z_u64_all[2 * u64_per_block + i],
- z_u64_all[3 * u64_per_block + i],
- z_u64_all[4 * u64_per_block + i],
- z_u64_all[5 * u64_per_block + i],
- z_u64_all[6 * u64_per_block + i],
- z_u64_all[7 * u64_per_block + i],
+ z_words[i],
+ z_words[u64_per_block + i],
+ z_words[2 * u64_per_block + i],
+ z_words[3 * u64_per_block + i],
+ z_words[4 * u64_per_block + i],
+ z_words[5 * u64_per_block + i],
+ z_words[6 * u64_per_block + i],
+ z_words[7 * u64_per_block + i],
];
transpose_8_u64s_to_64_bytes(&lanes, &mut stripe[i * 64..i * 64 + 64]);
}
diff --git a/crates/flock/src/lib.rs b/crates/flock/src/lib.rs
index 990c324d..d95d4a4b 100644
--- a/crates/flock/src/lib.rs
+++ b/crates/flock/src/lib.rs
@@ -1,4 +1,4 @@
-// Credit: https://github.com/succinctlabs/flock, MIT OR Apache-2.0.
+// CREDIT: https://github.com/succinctlabs/flock, MIT OR Apache-2.0.
//! flock: a batched R1CS proving system for hash circuits over GF(2), reduced
//! to evaluation claims on the committed packed witness.
//!
diff --git a/crates/flock/src/lincheck.rs b/crates/flock/src/lincheck.rs
index 7ec8cb87..b7e023e6 100644
--- a/crates/flock/src/lincheck.rs
+++ b/crates/flock/src/lincheck.rs
@@ -1,4 +1,4 @@
-// Credit: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
+// CREDIT: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
//! Lincheck PIOP for **block-diagonal** R1CS over GF(2).
//!
//! Reduces three MLE evaluation claims (`â(x)=v`, `b̂(x')=v'`, `ĉ(x'')=v''`)
@@ -34,7 +34,7 @@
//! `(z, ρ-values)`, so lincheck only needs to fold `z` **once** at that
//! shared point.
//!
-//! 1. **Prover sends** one length-`k = 2^k_log` F128 vector
+//! 1. **Prover sends** one length-`k = 2^k_log` F192 vector
//! `z_vec[i_inner] = ẑ(i_inner, x_ab.x_outer)`.
//! 2. **Verifier checks** *two* consistency equations against the same
//! `z_vec`:
@@ -67,7 +67,7 @@
//! To compose with the **zerocheck's univariate skip** for the first `k_skip`
//! variables, claim points use the [`QuirkyPoint`] representation:
//!
-//! `x = (z_skip ∈ F_{2^128}, x_inner_rest ∈ F_{2^128}^{k_log − k_skip}, x_outer ∈ F_{2^128}^{n_log})`
+//! `x = (z_skip ∈ F_{2^192}, x_inner_rest ∈ F_{2^192}^{k_log − k_skip}, x_outer ∈ F_{2^192}^{n_log})`
//!
//! - `z_skip` is the univariate-skip challenge; it represents all `k_skip`
//! skip variables collapsed via the polynomial extension with Lagrange
@@ -117,11 +117,11 @@
//! `byte_idx` and apply it across all `i_inner` with one lookup + one XOR
//! per byte.
-use pcs::{ProverState, VerifierState};
-use primitives::field::F128;
-use primitives::multilinear::{build_eq, inner_product};
use crate::r1cs::SparseBinaryMatrix;
-use crate::zerocheck::multilinear::lagrange_weights_naive;
+use fiat_shamir::transcript::{ProverState, VerifierState};
+use pcs::ring_switch::inner_product_ext;
+use primitives::field::F192;
+use primitives::multilinear::{eq_table as build_eq, lagrange_weights_naive};
// ---------------------------------------------------------------------------
// LincheckCircuit: the per-block linear structure lincheck consumes
@@ -135,9 +135,11 @@ use crate::zerocheck::multilinear::lagrange_weights_naive;
// marginal of base matrix `M ∈ {A_0, B_0}` — cost ∝ NNZ.
//
// `LincheckCircuit` is the seam: the prover and verifier take
-// `&dyn LincheckCircuit` instead of a pair of matrices. The one live impl is
+// `&dyn LincheckCircuit` instead of a pair of matrices. Two live impls:
// [`CscCircuit`] (the cached column-major transpose of BLAKE3's `(A_0, B_0)`,
-// see `BlockR1cs::csc_lincheck_circuit`).
+// see `BlockR1cs::csc_lincheck_circuit`) — the prover's marginal fold — and
+// `blake3::WalkLincheckCircuit`, whose `bilinear_form` lets the verifier skip
+// the marginal entirely via the circuit walk (flock.tex §Circuit walking).
/// Per-block linear structure consumed by lincheck. Implementations produce
/// the α-batched column marginal `comb_vec[c] = α · ξ_A(c) + ξ_B(c)` either
@@ -148,7 +150,7 @@ pub trait LincheckCircuit: Sync {
/// Compute `comb_vec[c] = α · (eq^T · A_0)[c] + (eq^T · B_0)[c]` over
/// `c ∈ [0, n_cols())`. `eq_inner.len() == n_cols()`.
- fn fold_alpha_batched(&self, alpha: F128, eq_inner: &[F128]) -> Vec;
+ fn fold_alpha_batched(&self, alpha: F192, eq_inner: &[F192]) -> Vec;
/// Column index of a constant-one wire to pin, or `None` if the circuit has
/// no such wire. When `Some(col)`, lincheck folds one extra `β`-term into the
@@ -160,6 +162,22 @@ pub trait LincheckCircuit: Sync {
fn const_pin_col(&self) -> Option {
None
}
+
+ /// Optional verifier-side fast path (flock.tex §Circuit walking): the
+ /// α-batched bilinear form
+ ///
+ /// `α·(uᵀ A_0 w) + (uᵀ B_0 w)`
+ ///
+ /// for arbitrary row weights `u` and column weights `w` (length
+ /// `n_cols()` each), WITHOUT materializing the length-k column marginal.
+ /// [`verify`] only ever consumes the marginal through one inner product
+ /// against a column-weight vector, so an implementation that can walk its
+ /// circuit (O(circuit) field ops — see `blake3::bilinear_walk`) answers
+ /// here and never pays the ∝ NNZ marginal. Default `None`: the verifier
+ /// falls back to `fold_alpha_batched`.
+ fn bilinear_form(&self, _alpha: F192, _u: &[F192], _w: &[F192]) -> Option {
+ None
+ }
}
/// Column-major (CSC) `LincheckCircuit`: `(A_0, B_0)` transposed once into
@@ -253,15 +271,15 @@ impl LincheckCircuit for CscCircuit {
fn const_pin_col(&self) -> Option {
self.const_pin
}
- fn fold_alpha_batched(&self, alpha: F128, eq_inner: &[F128]) -> Vec {
+ fn fold_alpha_batched(&self, alpha: F192, eq_inner: &[F192]) -> Vec {
use rayon::prelude::*;
assert_eq!(eq_inner.len(), self.n_cols);
let one_col = |c: usize| {
- let mut sa = F128::ZERO;
+ let mut sa = F192::ZERO;
for &r in &self.a_rows[self.a_col_ptr[c] as usize..self.a_col_ptr[c + 1] as usize] {
sa += eq_inner[r as usize];
}
- let mut sb = F128::ZERO;
+ let mut sb = F192::ZERO;
for &r in &self.b_rows[self.b_col_ptr[c] as usize..self.b_col_ptr[c + 1] as usize] {
sb += eq_inner[r as usize];
}
@@ -270,10 +288,8 @@ impl LincheckCircuit for CscCircuit {
if self.n_cols < SUMCHECK_PAR_THRESHOLD {
return (0..self.n_cols).map(one_col).collect();
}
- let mut out = vec![F128::ZERO; self.n_cols];
- out.par_iter_mut()
- .enumerate()
- .for_each(|(c, slot)| *slot = one_col(c));
+ let mut out = vec![F192::ZERO; self.n_cols];
+ out.par_iter_mut().enumerate().for_each(|(c, slot)| *slot = one_col(c));
out
}
}
@@ -290,13 +306,13 @@ impl LincheckCircuit for CscCircuit {
/// zerocheck's extract_c output uses.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QuirkyPoint {
- /// Univariate-skip challenge ∈ F₁₂₈. Binds all `k_skip` skip variables.
- pub z_skip: F128,
+ /// Univariate-skip challenge ∈ F₁₉₂ (tower). Binds all `k_skip` skip variables.
+ pub z_skip: F192,
/// Multilinear coords for the inner dims *after* the skip block. Length
/// `k_log − k_skip`.
- pub x_inner_rest: Vec,
+ pub x_inner_rest: Vec,
/// Multilinear coords for the outer dims. Length `n_log = m − k_log`.
- pub x_outer: Vec,
+ pub x_outer: Vec,
}
// Lincheck prover message: a partial product-sumcheck that proves the two
@@ -312,19 +328,19 @@ pub struct QuirkyPoint {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LincheckClaim {
/// The A/B batching challenge (sampled first).
- pub alpha: F128,
+ pub alpha: F192,
/// The constant-pin challenge (sampled after `alpha`; zero when the
/// circuit has no pin column).
- pub beta: F128,
+ pub beta: F192,
/// The sumcheck round challenges, in round order (MSB-first binding).
- pub r_rounds: Vec,
+ pub r_rounds: Vec,
/// Univariate-skip post-vector random sample.
- pub r_inner_skip: F128,
+ pub r_inner_skip: F192,
/// Multilinear post-vector random sample, length `k_log − k_skip`.
- pub r_inner_rest: Vec,
+ pub r_inner_rest: Vec,
/// `ẑ((r_inner_skip, r_inner_rest), x_ab.x_outer)` — the single
/// `z`-claim derived from the A and B consistency checks.
- pub w: F128,
+ pub w: F192,
}
/// Reasons the verifier may reject.
@@ -376,16 +392,11 @@ pub enum VerifyError {
///
/// `output[i_inner] = Σ_{i_outer ∈ {0,1}^n_log} z[i_inner, i_outer] · eq_outer[i_outer]`
///
-/// Equivalently, `output[i_inner] = ẑ(i_inner_as_F128, x_outer)` for boolean
+/// Equivalently, `output[i_inner] = ẑ(i_inner_as_F192, x_outer)` for boolean
/// `i_inner`. Used as the cross-check oracle for the production
/// `partial_fold_packed_z_triple`.
#[cfg(test)]
-pub fn partial_fold_packed_z(
- z_packed: &[u8],
- m: usize,
- k_log: usize,
- eq_outer: &[F128],
-) -> Vec {
+pub fn partial_fold_packed_z(z_packed: &[u8], m: usize, k_log: usize, eq_outer: &[F192]) -> Vec {
let n_log = m - k_log;
let k = 1usize << k_log;
let n_outer = 1usize << n_log;
@@ -394,7 +405,7 @@ pub fn partial_fold_packed_z(
assert!(n_log >= 3, "need n_outer ≥ 8 for byte stripes");
let n_stripes = n_outer / 8;
- let mut out = vec![F128::ZERO; k];
+ let mut out = vec![F192::ZERO; k];
for byte_idx in 0..n_stripes {
let stripe = &z_packed[byte_idx * k..(byte_idx + 1) * k];
for (i_inner, &byte) in stripe.iter().enumerate() {
@@ -413,7 +424,7 @@ pub fn partial_fold_packed_z(
out
}
-/// Padding-aware variant of [`partial_fold_packed_z_fast`]. Skips rows
+/// Padding-aware variant of `partial_fold_packed_z_fast`. Skips rows
/// `i_inner ∈ [useful_bits, k)` — those rows hold zero in every block of an
/// honestly padded witness, so the fold over the outer dim is zero. Output
/// is byte-identical to the dense path on such witnesses.
@@ -422,8 +433,8 @@ pub fn partial_fold_packed_z_fast_padded(
m: usize,
k_log: usize,
useful_bits: usize,
- eq_outer: &[F128],
-) -> Vec {
+ eq_outer: &[F192],
+) -> Vec {
use rayon::prelude::*;
let n_log = m - k_log;
@@ -446,10 +457,10 @@ pub fn partial_fold_packed_z_fast_padded(
.par_chunks(bytes_per_chunk)
.enumerate()
.fold(
- || vec![F128::ZERO; k],
+ || vec![F192::ZERO; k],
|mut acc, (chunk_idx, chunk_bytes)| {
let stripe_start = chunk_idx * stripes_per_chunk;
- let mut table = vec![F128::ZERO; 256];
+ let mut table = vec![F192::ZERO; 256];
for (rel_stripe, stripe) in chunk_bytes.chunks(k).enumerate() {
let byte_idx = stripe_start + rel_stripe;
build_sum_table(&eq_outer[8 * byte_idx..8 * byte_idx + 8], &mut table);
@@ -461,7 +472,7 @@ pub fn partial_fold_packed_z_fast_padded(
},
)
.reduce(
- || vec![F128::ZERO; k],
+ || vec![F192::ZERO; k],
|mut a, b| {
for (x, y) in a.iter_mut().zip(b.iter()) {
*x += *y;
@@ -481,21 +492,86 @@ const NEON_TILE_T: usize = 8;
/// for one BLOCK_K=8 block of i_inner positions, keeping all 8 accumulators
/// in NEON Q-registers.
///
-/// The 8 z index bytes of a stripe are consecutive, so they are fetched with
-/// **one** unaligned 8-byte scalar load and shifted out of the register rather
-/// than with eight `LDRB`s: the gather already issues one 128-bit table load
-/// per index, and a second load per index would nearly double this kernel's
-/// load-port pressure for data that is already in a register.
+/// # Safety
+/// - `tile_bytes_ptr` must point to at least `TILE_T * k` bytes.
+/// - `tables_ptr` must point to at least `TILE_T * 256` F192 entries.
+/// - `out_ptr` must point to at least 8 F192 entries of mutable storage.
+/// x86-64 twin of the tiled gather kernel below.
///
-/// Stripes are swept two at a time so each accumulator update folds both table
-/// lookups with one `EOR3`, halving the number of accumulator updates and the
-/// length of the serial dependency chain through each accumulator.
+/// `VPTERNLOGQ` is the exact counterpart of AArch64's `EOR3`: an arbitrary
+/// three-input bitwise function in one instruction, so immediate `0x96`
+/// (`a ^ b ^ c`) folds a paired-stripe accumulate the same way. Only the
+/// `c0`/`c1` limbs ride in the vector — `c2` is scalar and takes two XORs.
+///
+/// Before this, x86 had no tiled kernel at all: the dispatcher sent every
+/// non-AArch64 target to the generic `partial_fold_packed_z_fast_padded`.
///
/// # Safety
/// - `tile_bytes_ptr` must point to at least `TILE_T * k` bytes, with `bs + 8`
-/// readable in every stripe row (guaranteed by `bs + BLOCK_K ≤ k`).
-/// - `tables_ptr` must point to at least `TILE_T * 256 * 16` bytes.
-/// - `out_ptr` must point to at least 8 F128 (128 bytes) of mutable storage.
+/// readable in every stripe row (guaranteed by `bs + BLOCK_K <= k`).
+/// - `tables_ptr` must point to at least `TILE_T * 256` `F192`.
+/// - `out_ptr` must point to at least 8 writable `F192`.
+#[cfg(all(target_arch = "x86_64", target_feature = "avx512f", target_feature = "avx512vl"))]
+#[inline(never)]
+#[allow(unsafe_op_in_unsafe_fn)]
+unsafe fn process_block_neon_single(
+ tile_bytes_ptr: *const u8,
+ k: usize,
+ bs: usize,
+ tables_ptr: *const F192,
+ out_ptr: *mut F192,
+) {
+ use std::arch::x86_64::*;
+ const TILE_T: usize = NEON_TILE_T;
+ // `F192` is `#[repr(C)]` with `c0, c1, c2`, so a 128-bit load at `&c0`
+ // covers exactly the `(c0, c1)` pair.
+ const XOR3: i32 = 0x96;
+
+ let mut acc01 = [_mm_setzero_si128(); 8];
+ let mut acc2 = [0u64; 8];
+ for i in 0..8 {
+ let out = &*out_ptr.add(i);
+ acc01[i] = _mm_loadu_si128((&out.c0 as *const u64).cast());
+ acc2[i] = out.c2;
+ }
+
+ // One unaligned 8-byte load per stripe replaces eight LDRB-equivalents,
+ // and stripes are swept in pairs so each vector accumulator folds both
+ // table entries with a single VPTERNLOGQ.
+ let mut t = 0;
+ while t + 1 < TILE_T {
+ let ta0 = tables_ptr.add(t * 256);
+ let ta1 = tables_ptr.add((t + 1) * 256);
+ let w0 = (tile_bytes_ptr.add(t * k + bs) as *const u64).read_unaligned();
+ let w1 = (tile_bytes_ptr.add((t + 1) * k + bs) as *const u64).read_unaligned();
+ for i in 0..8 {
+ let e0 = &*ta0.add(((w0 >> (8 * i)) & 0xff) as usize);
+ let e1 = &*ta1.add(((w1 >> (8 * i)) & 0xff) as usize);
+ let v0 = _mm_loadu_si128((&e0.c0 as *const u64).cast());
+ let v1 = _mm_loadu_si128((&e1.c0 as *const u64).cast());
+ acc01[i] = _mm_ternarylogic_epi64::(acc01[i], v0, v1);
+ acc2[i] ^= e0.c2 ^ e1.c2;
+ }
+ t += 2;
+ }
+ if t < TILE_T {
+ let ta = tables_ptr.add(t * 256);
+ let w = (tile_bytes_ptr.add(t * k + bs) as *const u64).read_unaligned();
+ for i in 0..8 {
+ let entry = &*ta.add(((w >> (8 * i)) & 0xff) as usize);
+ let v = _mm_loadu_si128((&entry.c0 as *const u64).cast());
+ acc01[i] = _mm_xor_si128(acc01[i], v);
+ acc2[i] ^= entry.c2;
+ }
+ }
+
+ for i in 0..8 {
+ let out = &mut *out_ptr.add(i);
+ _mm_storeu_si128((&mut out.c0 as *mut u64).cast(), acc01[i]);
+ out.c2 = acc2[i];
+ }
+}
+
#[cfg(target_arch = "aarch64")]
#[inline(never)]
#[allow(unsafe_op_in_unsafe_fn)]
@@ -503,94 +579,60 @@ unsafe fn process_block_neon_single(
tile_bytes_ptr: *const u8,
k: usize,
bs: usize,
- tables_ptr: *const u8,
- out_ptr: *mut F128,
+ tables_ptr: *const F192,
+ out_ptr: *mut F192,
) {
- use primitives::field::neon::xor3_u8;
+ use primitives::field::neon::xor3_u64;
use std::arch::aarch64::*;
const TILE_T: usize = NEON_TILE_T;
- let o = out_ptr as *mut u8;
-
- let mut a0 = vld1q_u8(o);
- let mut a1 = vld1q_u8(o.add(16));
- let mut a2 = vld1q_u8(o.add(32));
- let mut a3 = vld1q_u8(o.add(48));
- let mut a4 = vld1q_u8(o.add(64));
- let mut a5 = vld1q_u8(o.add(80));
- let mut a6 = vld1q_u8(o.add(96));
- let mut a7 = vld1q_u8(o.add(112));
+ let mut acc01 = [vdupq_n_u64(0); 8];
+ let mut acc2 = [0u64; 8];
+ for i in 0..8 {
+ let out = &*out_ptr.add(i);
+ acc01[i] = vld1q_u64(&out.c0);
+ acc2[i] = out.c2;
+ }
+ // The 8 z index bytes of a stripe are consecutive, so fetch them with one
+ // unaligned 8-byte scalar load and shift them out of the register rather
+ // than with eight LDRBs: the gather already issues a table load per index,
+ // and a second load per index would nearly double this kernel's load-port
+ // pressure for data that is already in a register.
+ //
+ // Stripes are swept in pairs so each vector accumulator folds both table
+ // lookups with one EOR3, halving the accumulator updates and the serial
+ // dependency chain through each of the 8 live accumulators. The `c2`
+ // limbs are scalar, so they just take two XORs.
let mut t = 0;
while t + 1 < TILE_T {
- let ta0 = tables_ptr.add(t * 256 * 16);
- let ta1 = tables_ptr.add((t + 1) * 256 * 16);
+ let ta0 = tables_ptr.add(t * 256);
+ let ta1 = tables_ptr.add((t + 1) * 256);
let w0 = (tile_bytes_ptr.add(t * k + bs) as *const u64).read_unaligned();
let w1 = (tile_bytes_ptr.add((t + 1) * k + bs) as *const u64).read_unaligned();
-
- a0 = xor3_u8(
- a0,
- vld1q_u8(ta0.add((w0 & 0xff) as usize * 16)),
- vld1q_u8(ta1.add((w1 & 0xff) as usize * 16)),
- );
- a1 = xor3_u8(
- a1,
- vld1q_u8(ta0.add(((w0 >> 8) & 0xff) as usize * 16)),
- vld1q_u8(ta1.add(((w1 >> 8) & 0xff) as usize * 16)),
- );
- a2 = xor3_u8(
- a2,
- vld1q_u8(ta0.add(((w0 >> 16) & 0xff) as usize * 16)),
- vld1q_u8(ta1.add(((w1 >> 16) & 0xff) as usize * 16)),
- );
- a3 = xor3_u8(
- a3,
- vld1q_u8(ta0.add(((w0 >> 24) & 0xff) as usize * 16)),
- vld1q_u8(ta1.add(((w1 >> 24) & 0xff) as usize * 16)),
- );
- a4 = xor3_u8(
- a4,
- vld1q_u8(ta0.add(((w0 >> 32) & 0xff) as usize * 16)),
- vld1q_u8(ta1.add(((w1 >> 32) & 0xff) as usize * 16)),
- );
- a5 = xor3_u8(
- a5,
- vld1q_u8(ta0.add(((w0 >> 40) & 0xff) as usize * 16)),
- vld1q_u8(ta1.add(((w1 >> 40) & 0xff) as usize * 16)),
- );
- a6 = xor3_u8(
- a6,
- vld1q_u8(ta0.add(((w0 >> 48) & 0xff) as usize * 16)),
- vld1q_u8(ta1.add(((w1 >> 48) & 0xff) as usize * 16)),
- );
- a7 = xor3_u8(
- a7,
- vld1q_u8(ta0.add((w0 >> 56) as usize * 16)),
- vld1q_u8(ta1.add((w1 >> 56) as usize * 16)),
- );
+ for i in 0..8 {
+ let e0 = &*ta0.add(((w0 >> (8 * i)) & 0xff) as usize);
+ let e1 = &*ta1.add(((w1 >> (8 * i)) & 0xff) as usize);
+ acc01[i] = xor3_u64(acc01[i], vld1q_u64(&e0.c0), vld1q_u64(&e1.c0));
+ acc2[i] ^= e0.c2 ^ e1.c2;
+ }
t += 2;
}
if t < TILE_T {
- let ta = tables_ptr.add(t * 256 * 16);
+ let ta = tables_ptr.add(t * 256);
let w = (tile_bytes_ptr.add(t * k + bs) as *const u64).read_unaligned();
- a0 = veorq_u8(a0, vld1q_u8(ta.add((w & 0xff) as usize * 16)));
- a1 = veorq_u8(a1, vld1q_u8(ta.add(((w >> 8) & 0xff) as usize * 16)));
- a2 = veorq_u8(a2, vld1q_u8(ta.add(((w >> 16) & 0xff) as usize * 16)));
- a3 = veorq_u8(a3, vld1q_u8(ta.add(((w >> 24) & 0xff) as usize * 16)));
- a4 = veorq_u8(a4, vld1q_u8(ta.add(((w >> 32) & 0xff) as usize * 16)));
- a5 = veorq_u8(a5, vld1q_u8(ta.add(((w >> 40) & 0xff) as usize * 16)));
- a6 = veorq_u8(a6, vld1q_u8(ta.add(((w >> 48) & 0xff) as usize * 16)));
- a7 = veorq_u8(a7, vld1q_u8(ta.add((w >> 56) as usize * 16)));
- }
-
- vst1q_u8(o, a0);
- vst1q_u8(o.add(16), a1);
- vst1q_u8(o.add(32), a2);
- vst1q_u8(o.add(48), a3);
- vst1q_u8(o.add(64), a4);
- vst1q_u8(o.add(80), a5);
- vst1q_u8(o.add(96), a6);
- vst1q_u8(o.add(112), a7);
+ for i in 0..8 {
+ let entry = &*ta.add(((w >> (8 * i)) & 0xff) as usize);
+ acc01[i] = veorq_u64(acc01[i], vld1q_u64(&entry.c0));
+ acc2[i] ^= entry.c2;
+ }
+ }
+
+ for i in 0..8 {
+ let out = &mut *out_ptr.add(i);
+ vst1q_u64(&mut out.c0, acc01[i]);
+ out.c2 = acc2[i];
+ }
}
/// **i_inner-partitioned** NEON partial fold: parallelizes over the
@@ -600,24 +642,27 @@ unsafe fn process_block_neon_single(
/// accumulator (2 MB at k = 2¹⁷). With P workers that's `P · 2 MB` of live
/// accumulators — past ~3 workers it exceeds L2, so each worker's accumulator
/// spills and gets re-streamed from **main memory** once per stripe-tile
-/// (≈ `n_tiles · 2·k` F128 of memory traffic). Measured: scaling saturates at
+/// (≈ `n_tiles · 2·k` F192 of memory traffic). Measured: scaling saturates at
/// ~5× on 10 cores (memory-bound), not ~10×.
///
/// Here the workers own **disjoint** slices of a single shared `out`, so the
-/// total live accumulator is just `k` F128 = 2 MB — it stays L2-resident, never
+/// total live accumulator is just `k` F192 = 2 MB — it stays L2-resident, never
/// re-streamed from memory, and there is **no final reduction**. Main-memory
/// traffic drops to one pass over z plus one write of `out`. Each worker still
/// uses the register-tiled inner kernel (8 accumulators across `TILE_T`
/// stripes); it just rebuilds the per-tile sum tables for its own slice (a few
/// % of redundant table-build XORs, far cheaper than the memory re-streaming).
-#[cfg(target_arch = "aarch64")]
+#[cfg(any(
+ target_arch = "aarch64",
+ all(target_arch = "x86_64", target_feature = "avx512f", target_feature = "avx512vl")
+))]
pub fn partial_fold_packed_z_neon_iblock_padded(
z_packed: &[u8],
m: usize,
k_log: usize,
useful_bits: usize,
- eq_outer: &[F128],
-) -> Vec {
+ eq_outer: &[F192],
+) -> Vec {
use rayon::prelude::*;
const TILE_T: usize = NEON_TILE_T;
@@ -644,7 +689,7 @@ pub fn partial_fold_packed_z_neon_iblock_padded(
// contribute nothing. Rows [useful, k) stay zero from the vec init.
let useful = (useful_bits.div_ceil(BLOCK_K) * BLOCK_K).min(k);
- let mut out = vec![F128::ZERO; k];
+ let mut out = vec![F192::ZERO; k];
if useful == 0 {
return out;
}
@@ -674,31 +719,22 @@ pub fn partial_fold_packed_z_neon_iblock_padded(
.for_each(|(ci, out_slice)| {
let i_base = ci * i_chunk;
let n_block = out_slice.len() / BLOCK_K;
- // TILE_T × 256 F128 = 32 KB tables, L1-resident, rebuilt per tile.
- let mut tables = vec![F128::ZERO; TILE_T * 256];
+ // TILE_T × 256 F192 = 32 KB tables, L1-resident, rebuilt per tile.
+ let mut tables = vec![F192::ZERO; TILE_T * 256];
for tile in 0..n_tiles {
let stripe_base = tile * TILE_T;
for t in 0..TILE_T {
let eq_off = 8 * (stripe_base + t);
- build_sum_table(
- &eq_outer[eq_off..eq_off + 8],
- &mut tables[t * 256..(t + 1) * 256],
- );
+ build_sum_table(&eq_outer[eq_off..eq_off + 8], &mut tables[t * 256..(t + 1) * 256]);
}
- let tables_ptr = tables.as_ptr() as *const u8;
+ let tables_ptr = tables.as_ptr();
// Base of this (tile, i_base): process_block reads
// z_base[t·k + bs] = z[(stripe_base+t)·k + i_base + bs].
let z_base = unsafe { z_packed.as_ptr().add(stripe_base * k + i_base) };
for b in 0..n_block {
let i = b * BLOCK_K;
unsafe {
- process_block_neon_single(
- z_base,
- k,
- i,
- tables_ptr,
- out_slice.as_mut_ptr().add(i),
- );
+ process_block_neon_single(z_base, k, i, tables_ptr, out_slice.as_mut_ptr().add(i));
}
}
}
@@ -718,20 +754,23 @@ pub fn partial_fold_packed_z_neon_iblock_padded(
/// tile tables exactly **once**, folds them into a private length-k partial, and the
/// `p` partials are XOR-reduced at the end. The partial is the full length-k
/// (256 KB at k_log=14 ⇒ spills L1 to L2), but the register-tiled inner kernel keeps
-/// 8 F128 accumulators in NEON registers, so the L2 traffic is mild — measured ≈2 %
+/// 8 F192 accumulators in NEON registers, so the L2 traffic is mild — measured ≈2 %
/// ST cost at m=32, none at m=30 — and far cheaper than iblock's redundant tables:
/// the fold scales ~8.5× vs iblock's ~6.5× on 10 P-cores at m=32, and the margin
/// grows with the outer dim (the redundant-table cost it removes is ∝ `n_stripes`).
///
/// # Safety / preconditions: identical to the iblock kernel.
-#[cfg(target_arch = "aarch64")]
+#[cfg(any(
+ target_arch = "aarch64",
+ all(target_arch = "x86_64", target_feature = "avx512f", target_feature = "avx512vl")
+))]
pub fn partial_fold_packed_z_neon_oblock_padded(
z_packed: &[u8],
m: usize,
k_log: usize,
useful_bits: usize,
- eq_outer: &[F128],
-) -> Vec {
+ eq_outer: &[F192],
+) -> Vec {
use rayon::prelude::*;
const TILE_T: usize = NEON_TILE_T;
@@ -757,7 +796,7 @@ pub fn partial_fold_packed_z_neon_oblock_padded(
// up to BLOCK_K; columns [useful, k) stay zero from the partial init.
let useful = (useful_bits.div_ceil(BLOCK_K) * BLOCK_K).min(k);
if useful == 0 {
- return vec![F128::ZERO; k];
+ return vec![F192::ZERO; k];
}
// One private length-k partial per worker; workers own contiguous tile bands,
@@ -766,50 +805,36 @@ pub fn partial_fold_packed_z_neon_oblock_padded(
let tiles_per_worker = n_tiles.div_ceil(p);
let n_workers = n_tiles.div_ceil(tiles_per_worker); // ≤ p, every band non-empty
- let mut partials = vec![F128::ZERO; n_workers * k];
- partials
- .par_chunks_mut(k)
- .enumerate()
- .for_each(|(w, partial)| {
- let tile_lo = w * tiles_per_worker;
- let tile_hi = ((w + 1) * tiles_per_worker).min(n_tiles);
- // TILE_T × 256 F128 = 32 KB tables, L1-resident, built once per tile.
- let mut tables = vec![F128::ZERO; TILE_T * 256];
- for tile in tile_lo..tile_hi {
- let stripe_base = tile * TILE_T;
- for t in 0..TILE_T {
- let eq_off = 8 * (stripe_base + t);
- build_sum_table(
- &eq_outer[eq_off..eq_off + 8],
- &mut tables[t * 256..(t + 1) * 256],
- );
- }
- let tables_ptr = tables.as_ptr() as *const u8;
- let z_base = unsafe { z_packed.as_ptr().add(stripe_base * k) };
- let mut bs = 0usize;
- while bs < useful {
- unsafe {
- process_block_neon_single(
- z_base,
- k,
- bs,
- tables_ptr,
- partial.as_mut_ptr().add(bs),
- );
- }
- bs += BLOCK_K;
+ let mut partials = vec![F192::ZERO; n_workers * k];
+ partials.par_chunks_mut(k).enumerate().for_each(|(w, partial)| {
+ let tile_lo = w * tiles_per_worker;
+ let tile_hi = ((w + 1) * tiles_per_worker).min(n_tiles);
+ // TILE_T × 256 F192 = 32 KB tables, L1-resident, built once per tile.
+ let mut tables = vec![F192::ZERO; TILE_T * 256];
+ for tile in tile_lo..tile_hi {
+ let stripe_base = tile * TILE_T;
+ for t in 0..TILE_T {
+ let eq_off = 8 * (stripe_base + t);
+ build_sum_table(&eq_outer[eq_off..eq_off + 8], &mut tables[t * 256..(t + 1) * 256]);
+ }
+ let tables_ptr = tables.as_ptr();
+ let z_base = unsafe { z_packed.as_ptr().add(stripe_base * k) };
+ let mut bs = 0usize;
+ while bs < useful {
+ unsafe {
+ process_block_neon_single(z_base, k, bs, tables_ptr, partial.as_mut_ptr().add(bs));
}
+ bs += BLOCK_K;
}
- });
+ }
+ });
// XOR-reduce the per-worker partials: parallel over columns, sequential over
// workers so each 256 KB partial is streamed once (cache-friendly).
let (first, rest) = partials.split_at(k);
let mut out = first.to_vec();
for chunk in rest.chunks(k) {
- out.par_iter_mut()
- .zip(chunk.par_iter())
- .for_each(|(o, s)| *o += *s);
+ out.par_iter_mut().zip(chunk.par_iter()).for_each(|(o, s)| *o += *s);
}
out
}
@@ -823,10 +848,13 @@ fn partial_fold_packed_z_best(
m: usize,
k_log: usize,
useful_bits: usize,
- eq_outer: &[F128],
-) -> Vec {
+ eq_outer: &[F192],
+) -> Vec {
if n_log_ok_for_tile(m, k_log, NEON_TILE_T) {
- #[cfg(target_arch = "aarch64")]
+ #[cfg(any(
+ target_arch = "aarch64",
+ all(target_arch = "x86_64", target_feature = "avx512f", target_feature = "avx512vl")
+ ))]
{
// Pick the partition that wins for this size. The outer(tile)-partitioned
// `oblock` builds each tile's sum-tables once instead of once per worker,
@@ -836,19 +864,15 @@ fn partial_fold_packed_z_best(
// crossover sits at n_log ≈ 15–16 across k_log ∈ {11,14}, so gate oblock at
// n_log ≥ 16; below that the L1-resident `iblock` wins.
let n_log = m - k_log;
- if n_log >= OBLOCK_MIN_N_LOG
- {
- return partial_fold_packed_z_neon_oblock_padded(
- z_packed,
- m,
- k_log,
- useful_bits,
- eq_outer,
- );
+ if n_log >= OBLOCK_MIN_N_LOG {
+ return partial_fold_packed_z_neon_oblock_padded(z_packed, m, k_log, useful_bits, eq_outer);
}
partial_fold_packed_z_neon_iblock_padded(z_packed, m, k_log, useful_bits, eq_outer)
}
- #[cfg(not(target_arch = "aarch64"))]
+ #[cfg(not(any(
+ target_arch = "aarch64",
+ all(target_arch = "x86_64", target_feature = "avx512f", target_feature = "avx512vl")
+ )))]
{
partial_fold_packed_z_fast_padded(z_packed, m, k_log, useful_bits, eq_outer)
}
@@ -860,7 +884,10 @@ fn partial_fold_packed_z_best(
/// Outer-dimension threshold (`n_log = m − k_log`) at/above which the
/// outer(tile)-partitioned fold beats the i_inner-partitioned one. See
/// [`partial_fold_packed_z_best`] for the crossover calibration.
-#[cfg(target_arch = "aarch64")]
+#[cfg(any(
+ target_arch = "aarch64",
+ all(target_arch = "x86_64", target_feature = "avx512f", target_feature = "avx512vl")
+))]
const OBLOCK_MIN_N_LOG: usize = 16;
/// Quick test for "can we use the tiled fast path?". Tile uses `TILE_T`
@@ -874,17 +901,17 @@ fn n_log_ok_for_tile(m: usize, k_log: usize, tile_t: usize) -> bool {
n_stripes.is_multiple_of(tile_t)
}
-/// Build a 256-entry sum table over 8 F128 values:
+/// Build a 256-entry sum table over 8 F192 values:
/// `table[b] = Σ_{r: bit r of b is set} eq8[r]`
///
/// Doubling construction (255 XORs): for each new bit position `i ∈ 0..8`,
/// extend the table by XORing `eq8[i]` into each existing entry. This
/// avoids the naive 8·256 = 2048 operations.
#[inline]
-fn build_sum_table(eq8: &[F128], table: &mut [F128]) {
+fn build_sum_table(eq8: &[F192], table: &mut [F192]) {
debug_assert_eq!(eq8.len(), 8);
debug_assert_eq!(table.len(), 256);
- table[0] = F128::ZERO;
+ table[0] = F192::ZERO;
for i in 0..8 {
let e = eq8[i];
let len = 1usize << i;
@@ -910,8 +937,7 @@ pub fn pack_z_lincheck(z_logical: &[bool], m: usize, k_log: usize) -> Vec {
assert_eq!(n_outer % 8, 0, "need n_outer ≥ 8 for byte stripes");
let n_stripes = n_outer / 8;
- // Uninit alloc — every byte is written exactly once in the loop below.
- let mut z_packed: Vec = primitives::alloc_uninit_vec(n_total / 8);
+ let mut z_packed = primitives::alloc_uninit(n_total / 8);
for byte_idx in 0..n_stripes {
for i_inner in 0..k {
let mut byte = 0u8;
@@ -922,57 +948,49 @@ pub fn pack_z_lincheck(z_logical: &[bool], m: usize, k_log: usize) -> Vec {
byte |= 1u8 << r;
}
}
- z_packed[byte_idx * k + i_inner] = byte;
+ z_packed[byte_idx * k + i_inner].write(byte);
}
}
- z_packed
+ // SAFETY: the nested loops write every output byte exactly once.
+ unsafe { primitives::assume_init(z_packed) }
}
-/// Same output as [`pack_z_lincheck`] but reads bits from an F_{2^128}-packed
-/// witness (polynomial basis: bit `i` of logical = bit `i % 128` of
-/// `z_packed_f128[i / 128]`).
-pub fn pack_z_lincheck_from_packed(
- z_packed_f128: &[primitives::field::F128],
- m: usize,
- k_log: usize,
-) -> Vec {
+/// Same output as `pack_z_lincheck`, but reads bits from a 128-bit packed
+/// witness embedded in F192. In the polynomial basis, logical bit `i` is bit
+/// `i % 128` of `z_packed_words[i / 128]`.
+pub fn pack_z_lincheck_from_packed(z_packed_words: &[primitives::field::F192], m: usize, k_log: usize) -> Vec {
use rayon::prelude::*;
let k = 1usize << k_log;
let n_total = 1usize << m;
- assert_eq!(z_packed_f128.len(), n_total / 128);
+ assert_eq!(z_packed_words.len(), n_total / 128);
let n_outer = n_total / k;
assert_eq!(n_outer % 8, 0, "need n_outer ≥ 8 for byte stripes");
- // Uninit alloc — the par_chunks_mut loop below writes every byte of
- // every k-byte stripe exactly once. Saves ~10 ms of sequential
- // zero-fill at m=29 (64 MB byte buffer) on the main thread.
- let mut z_packed: Vec = primitives::alloc_uninit_vec(n_total / 8);
+ let mut z_packed = primitives::alloc_uninit(n_total / 8);
// Each stripe (byte_idx) writes a disjoint k-byte chunk — process them in
// parallel. Inside one stripe, k independent output bytes.
- z_packed
- .par_chunks_mut(k)
- .enumerate()
- .for_each(|(byte_idx, chunk)| {
- for i_inner in 0..k {
- let mut byte = 0u8;
- for r in 0..8 {
- let i_outer = 8 * byte_idx + r;
- let logical_idx = i_inner + i_outer * k;
- let f128_idx = logical_idx / 128;
- let local_bit = logical_idx % 128;
- let bit = if local_bit < 64 {
- (z_packed_f128[f128_idx].lo >> local_bit) & 1 == 1
- } else {
- (z_packed_f128[f128_idx].hi >> (local_bit - 64)) & 1 == 1
- };
- if bit {
- byte |= 1u8 << r;
- }
+ z_packed.par_chunks_mut(k).enumerate().for_each(|(byte_idx, chunk)| {
+ for i_inner in 0..k {
+ let mut byte = 0u8;
+ for r in 0..8 {
+ let i_outer = 8 * byte_idx + r;
+ let logical_idx = i_inner + i_outer * k;
+ let packed_word = logical_idx / 128;
+ let local_bit = logical_idx % 128;
+ let bit = if local_bit < 64 {
+ (z_packed_words[packed_word].c0 >> local_bit) & 1 == 1
+ } else {
+ (z_packed_words[packed_word].c1 >> (local_bit - 64)) & 1 == 1
+ };
+ if bit {
+ byte |= 1u8 << r;
}
- chunk[i_inner] = byte;
}
- });
- z_packed
+ chunk[i_inner].write(byte);
+ }
+ });
+ // SAFETY: every parallel chunk writes each of its output bytes exactly once.
+ unsafe { primitives::assume_init(z_packed) }
}
/// Build the **quirky eq table** for a claim point on the inner half:
@@ -989,7 +1007,7 @@ pub fn pack_z_lincheck_from_packed(
/// inner-rest dims occupy the next bits.
///
/// Cost: 64 (Lagrange) + 32 (eq) + 2048 outer products ≈ tiny.
-pub fn build_quirky_eq_table(z_skip: F128, x_inner_rest: &[F128], k_skip: usize) -> Vec {
+pub fn build_quirky_eq_table(z_skip: F192, x_inner_rest: &[F192], k_skip: usize) -> Vec {
let ell_skip = 1usize << k_skip;
let ell_rest = 1usize << x_inner_rest.len();
let lambda_skip = lagrange_weights_naive(k_skip, z_skip);
@@ -1013,15 +1031,15 @@ const SUMCHECK_PAR_THRESHOLD: usize = 1usize << 12;
/// One round of product-sumcheck on `(c, z)`: compute `(q(1), q(∞))` =
/// `(Σ c_hi·z_hi, Σ (c_hi+c_lo)·(z_hi+z_lo))` over the top-bit split. The
/// `len()` of `c` and `z` is even; `half = len/2`.
-fn sumcheck_round_eval_par(c: &[F128], z: &[F128]) -> (F128, F128) {
+fn sumcheck_round_eval_par(c: &[F192], z: &[F192]) -> (F192, F192) {
use rayon::prelude::*;
let half = c.len() / 2;
debug_assert_eq!(z.len(), c.len());
let (clo, chi) = c.split_at(half);
let (zlo, zhi) = z.split_at(half);
if half < SUMCHECK_PAR_THRESHOLD {
- let mut e1 = F128::ZERO;
- let mut einf = F128::ZERO;
+ let mut e1 = F192::ZERO;
+ let mut einf = F192::ZERO;
for i in 0..half {
e1 += chi[i] * zhi[i];
einf += (chi[i] + clo[i]) * (zhi[i] + zlo[i]);
@@ -1035,12 +1053,12 @@ fn sumcheck_round_eval_par(c: &[F128], z: &[F128]) -> (F128, F128) {
let einf_i = (chi[i] + clo[i]) * (zhi[i] + zlo[i]);
(e1_i, einf_i)
})
- .reduce(|| (F128::ZERO, F128::ZERO), |a, b| (a.0 + b.0, a.1 + b.1))
+ .reduce(|| (F192::ZERO, F192::ZERO), |a, b| (a.0 + b.0, a.1 + b.1))
}
/// Bind the top remaining variable of `v` at challenge `r`: `v[i] ← v[i] +
/// r·(v[i+half] + v[i])` for `i ∈ [0, half)`, then truncate to `half`. In-place.
-pub fn sumcheck_bind_top_in_place_par(v: &mut Vec, r: F128) {
+pub fn sumcheck_bind_top_in_place_par(v: &mut Vec, r: F192) {
use rayon::prelude::*;
let half = v.len() / 2;
if half < SUMCHECK_PAR_THRESHOLD {
@@ -1050,11 +1068,27 @@ pub fn sumcheck_bind_top_in_place_par(v: &mut Vec, r: F128) {
} else {
let (lo, hi) = v.split_at_mut(half);
let hi = &hi[..half];
- lo.par_iter_mut()
- .zip(hi.par_iter())
- .for_each(|(lo_i, &hi_i)| {
- *lo_i = *lo_i + r * (hi_i + *lo_i);
- });
+ lo.par_iter_mut().zip(hi.par_iter()).for_each(|(lo_i, &hi_i)| {
+ *lo_i = *lo_i + r * (hi_i + *lo_i);
+ });
+ }
+ v.truncate(half);
+}
+
+/// Tower (`F192`) twin of [`sumcheck_bind_top_in_place_par`], for the verifier.
+pub fn sumcheck_bind_top_in_place_par_t(v: &mut Vec, r: F192) {
+ use rayon::prelude::*;
+ let half = v.len() / 2;
+ if half < SUMCHECK_PAR_THRESHOLD {
+ for i in 0..half {
+ v[i] = v[i] + r * (v[i + half] + v[i]);
+ }
+ } else {
+ let (lo, hi) = v.split_at_mut(half);
+ let hi = &hi[..half];
+ lo.par_iter_mut().zip(hi.par_iter()).for_each(|(lo_i, &hi_i)| {
+ *lo_i = *lo_i + r * (hi_i + *lo_i);
+ });
}
v.truncate(half);
}
@@ -1084,11 +1118,7 @@ pub fn sumcheck_bind_top_in_place_par(v: &mut Vec, r: F128) {
/// well-defined next round — the caller guarantees this by only fusing when a
/// later round exists). The returned message is bit-identical to
/// `sumcheck_round_eval_par` run on the bound tables.
-fn sumcheck_bind_both_and_eval_next(
- comb: &mut Vec,
- z: &mut Vec,
- r: F128,
-) -> (F128, F128) {
+fn sumcheck_bind_both_and_eval_next(comb: &mut Vec, z: &mut Vec, r: F192) -> (F192, F192) {
use rayon::prelude::*;
let len = comb.len();
debug_assert_eq!(z.len(), len);
@@ -1105,8 +1135,8 @@ fn sumcheck_bind_both_and_eval_next(
let (zq2, zq3) = z_hi.split_at(half2);
let (e1, einf) = if half2 < SUMCHECK_PAR_THRESHOLD {
- let mut e1 = F128::ZERO;
- let mut einf = F128::ZERO;
+ let mut e1 = F192::ZERO;
+ let mut einf = F192::ZERO;
for i in 0..half2 {
let lo = cq0[i] + r * (cq2[i] + cq0[i]);
let hi = cq1[i] + r * (cq3[i] + cq1[i]);
@@ -1140,7 +1170,7 @@ fn sumcheck_bind_both_and_eval_next(
*z1 = zhi;
(hi * zhi, (hi + lo) * (zhi + zlo))
})
- .reduce(|| (F128::ZERO, F128::ZERO), |a, b| (a.0 + b.0, a.1 + b.1))
+ .reduce(|| (F192::ZERO, F192::ZERO), |a, b| (a.0 + b.0, a.1 + b.1))
};
comb.truncate(half);
@@ -1158,8 +1188,8 @@ fn sumcheck_bind_both_and_eval_next(
/// `s_hat_v` via [`pcs::ring_switch::s_hat_v_from_z_vec`], skipping a
/// `fold_1b_rows` pass at open time.
///
-/// Pays one extra `2^k_log` F128 clone (~2 MB at k_log=17) before the
-pub fn prove_padded_capture_z_vec(
+/// Pays one extra `2^k_log` F192 clone (~2 MB at k_log=17) before the
+pub fn prove_padded_capture_z_vec(
z_packed: &[u8],
m: usize,
k_log: usize,
@@ -1167,24 +1197,14 @@ pub fn prove_padded_capture_z_vec(
useful_bits: usize,
circuit: &dyn LincheckCircuit,
x_ab: &QuirkyPoint,
- ps: &mut ProverState,
-) -> (LincheckClaim, Vec) {
- let (claim, captured) = prove_padded_inner(
- z_packed,
- m,
- k_log,
- k_skip,
- useful_bits,
- circuit,
- x_ab,
- true,
- ps,
- );
+ ps: &mut ProverState,
+) -> (LincheckClaim, Vec) {
+ let (claim, captured) = prove_padded_inner(z_packed, m, k_log, k_skip, useful_bits, circuit, x_ab, true, ps);
(claim, captured.expect("capture=true must produce z_vec"))
}
#[allow(clippy::too_many_arguments)]
-fn prove_padded_inner(
+fn prove_padded_inner(
z_packed: &[u8],
m: usize,
k_log: usize,
@@ -1193,8 +1213,8 @@ fn prove_padded_inner(
circuit: &dyn LincheckCircuit,
x_ab: &QuirkyPoint,
capture_z_vec: bool,
- ps: &mut ProverState,
-) -> (LincheckClaim, Option>) {
+ ps: &mut ProverState,
+) -> (LincheckClaim, Option>) {
let k = 1usize << k_log;
let n_log = m - k_log;
assert!(m >= k_log);
@@ -1207,6 +1227,11 @@ fn prove_padded_inner(
let trace = std::env::var("LINCHECK_TRACE").is_ok();
+ // Keep local copies of the tower-valued claim point used throughout the fold.
+ let z_skip_g = x_ab.z_skip;
+ let x_inner_rest_g = x_ab.x_inner_rest.to_vec();
+ let x_outer_g = x_ab.x_outer.to_vec();
+
// 1. Sample α (matches verifier's order). Used to batch the two scalar
// consistency checks v_a, v_b into a single sumcheck.
let alpha = ps.sample();
@@ -1215,12 +1240,8 @@ fn prove_padded_inner(
// the sparse-matrix default this is the fused single-pass row-fold;
// per-hash circuit walkers compute the same `comb_vec` directly from
// the constraint graph.
- let t = if trace {
- Some(std::time::Instant::now())
- } else {
- None
- };
- let eq_inner = build_quirky_eq_table(x_ab.z_skip, &x_ab.x_inner_rest, k_skip);
+ let t = if trace { Some(std::time::Instant::now()) } else { None };
+ let eq_inner = build_quirky_eq_table(z_skip_g, &x_inner_rest_g, k_skip);
if let Some(t) = t {
eprintln!(
"[lc] {:<26} {:>7.2} ms",
@@ -1228,11 +1249,7 @@ fn prove_padded_inner(
t.elapsed().as_secs_f64() * 1e3
);
}
- let t = if trace {
- Some(std::time::Instant::now())
- } else {
- None
- };
+ let t = if trace { Some(std::time::Instant::now()) } else { None };
let mut comb_vec = circuit.fold_alpha_batched(alpha, &eq_inner);
if let Some(t) = t {
eprintln!(
@@ -1247,19 +1264,15 @@ fn prove_padded_inner(
// boolean index, eq(j*, ·) is the one-hot vector and this is a single
// entry update. β is sampled after α; the verifier mirrors both. See
// lincheck's `LincheckCircuit::const_pin_col`.
- let mut beta = F128::ZERO;
+ let mut beta = F192::ZERO;
if let Some(col) = circuit.const_pin_col() {
beta = ps.sample();
comb_vec[col] += beta;
}
- // 3. Partial fold of z at the shared outer half (length-k F128 vector).
- let t = if trace {
- Some(std::time::Instant::now())
- } else {
- None
- };
- let eq_x_outer = build_eq(&x_ab.x_outer);
+ // 3. Partial fold of z at the shared outer half (length-k F192 vector).
+ let t = if trace { Some(std::time::Instant::now()) } else { None };
+ let eq_x_outer = build_eq(&x_outer_g);
let mut z_vec = partial_fold_packed_z_best(z_packed, m, k_log, useful_bits, &eq_x_outer);
if let Some(t) = t {
eprintln!(
@@ -1271,16 +1284,8 @@ fn prove_padded_inner(
// 3b. Optional capture: clone the pre-sumcheck z_vec for downstream reuse
// (PCS open's AB-claim s_hat_v skipping fold_1b_rows). Only pay the
// clone when explicitly requested.
- let captured_z_vec: Option> = if capture_z_vec {
- Some(z_vec.clone())
- } else {
- None
- };
- let t_sumcheck_start = if trace {
- Some(std::time::Instant::now())
- } else {
- None
- };
+ let captured_z_vec: Option> = if capture_z_vec { Some(z_vec.clone()) } else { None };
+ let t_sumcheck_start = if trace { Some(std::time::Instant::now()) } else { None };
// 5. Standard multilinear product-sumcheck over the high `inner_rest_len`
// bits of `i`. Each round binds the TOP remaining bit. After `inner_rest_len` rounds, both
@@ -1319,7 +1324,9 @@ fn prove_padded_inner(
// 6. Send `z_partial` (the post-sumcheck collapsed z_vec). Length 2^k_skip.
let z_partial = z_vec.clone();
- ps.add_scalars(&z_partial);
+ for &x in z_partial.iter() {
+ ps.add_scalar(x);
+ }
// 7. Sample fresh z_skip AFTER observing z_partial — gives Schwartz-Zippel
// soundness on the φ8 (univariate-skip) dim.
@@ -1329,7 +1336,7 @@ fn prove_padded_inner(
// Equals ẑ_φ8(z_skip, r_rest, x_outer) when z_partial is honest; the
// PCS catches mismatches downstream.
let lambda = lagrange_weights_naive(k_skip, r_inner_skip);
- let w = inner_product(&lambda, &z_partial);
+ let w = inner_product_ext(&lambda, &z_partial);
// 9. Convert sumcheck challenges to LSB-first `x_inner_rest` order. The
// loop binds the TOP bit each round, so r_rounds[0] bound bit
@@ -1353,15 +1360,15 @@ fn prove_padded_inner(
/// Verify a lincheck proof. Walks the sponge in lockstep with the prover,
/// replays the α-batched product sumcheck against `v_a` and `v_b`, and
/// derives the single output z-claim `w`.
-pub fn verify(
+pub fn verify(
m: usize,
k_log: usize,
k_skip: usize,
circuit: &dyn LincheckCircuit,
x_ab: &QuirkyPoint,
- v_a: F128,
- v_b: F128,
- vs: &mut VerifierState<'_>,
+ v_a: F192,
+ v_b: F192,
+ vs: &mut VerifierState<'_, O>,
) -> Result {
let k = 1usize << k_log;
let n_log = m - k_log;
@@ -1408,9 +1415,12 @@ pub fn verify(
// 1. Sample α (matches prover's order).
let alpha = vs.sample();
- // 2. Build α-batched comb_vec via the circuit's per-block fold (same call
- // the prover made — sparse default delegates to the fused row-fold;
- // per-hash impls walk the constraint graph directly).
+ // 2. Row weights: the quirky eq table over the inner claim point — `u` in
+ // the final bilinear form. The α-batched column marginal the prover
+ // materializes (`fold_alpha_batched`, cost ∝ NNZ) is NOT built here:
+ // the verifier only ever consumes it through one inner product, so
+ // that work is deferred to step 5 (and walk-capable circuits answer it
+ // in O(circuit) ops without the marginal at all).
let t = std::time::Instant::now();
let eq_inner = build_quirky_eq_table(x_ab.z_skip, &x_ab.x_inner_rest, k_skip);
if trace {
@@ -1419,27 +1429,19 @@ pub fn verify(
fmt(t.elapsed().as_secs_f64())
);
}
- let t = std::time::Instant::now();
- let mut comb_vec = circuit.fold_alpha_batched(alpha, &eq_inner);
- if trace {
- eprintln!(
- " [lcv] circuit.fold_alpha_batched: {}",
- fmt(t.elapsed().as_secs_f64())
- );
- }
- // 3. Replay the multilinear product-sumcheck (inner_rest_len rounds),
- // folding comb_vec in lockstep so we end up with the "comb_partial"
- // vector of length 2^k_skip. Parallel fold for the early (large) rounds.
- let t = std::time::Instant::now();
- // Constant-wire pin (mirror of prove): β sampled after α, comb gains +β at
- // the constant column, and the initial target gains +β·1 — the honest
- // all-ones constant column folds to 1. See lincheck's `LincheckCircuit::const_pin_col`.
+ // 3. Replay the multilinear product-sumcheck (inner_rest_len rounds).
+ // Only the transcript messages drive the running claim; the prover's
+ // lockstep comb_vec fold is linear, so its end state is reconstructed
+ // in step 5 as column weights instead of being folded here.
+ // Constant-wire pin (mirror of prove): β sampled after α, the comb's +β
+ // at the constant column surfaces in step 5 as `+β·w_col[col]`, and the
+ // initial target gains +β·1 — the honest all-ones constant column folds
+ // to 1. See lincheck's `LincheckCircuit::const_pin_col`.
let mut target = alpha * v_a + v_b;
- let mut beta = F128::ZERO;
- if let Some(col) = circuit.const_pin_col() {
+ let mut beta = F192::ZERO;
+ if circuit.const_pin_col().is_some() {
beta = vs.sample();
- comb_vec[col] += beta;
target += beta;
}
let mut running = target;
@@ -1451,27 +1453,52 @@ pub fn verify(
// q(0) = claim + q(1) in char 2; q(X) = einf·X² + c1·X + e0.
let e0 = running + e1;
let c1 = e0 + e1 + einf;
- running = einf * r * r + c1 * r + e0;
- // Fold comb_vec at the same r (mirrors prover's fold).
- sumcheck_bind_top_in_place_par(&mut comb_vec, r);
+ running = (einf * r + c1) * r + e0;
r_rounds.push(r);
}
- debug_assert_eq!(comb_vec.len(), n_skip);
+
+ // 4. Read + bind z_partial AFTER the sumcheck rounds (matches prover order).
+ let z_partial: Vec = vs.next_scalars(n_skip).map_err(VerifyError::Transcript)?;
+
+ // Convert sumcheck challenges to LSB-first x_inner_rest order (same
+ // convention as prover; also the eq-ordering of the step-5 column weights).
+ let mut r_inner_rest = r_rounds.clone();
+ r_inner_rest.reverse();
+
+ // 5. Final sumcheck consistency. The prover's comb_partial — comb_vec
+ // bound MSB-first at r_rounds — satisfies
+ //
+ // ⟨comb_partial, z_partial⟩ = Σ_c comb_vec[c] · w_col[c],
+ // w_col[i_skip + i_rest·2^k_skip] = z_partial[i_skip] · eq(r_inner_rest, i_rest),
+ //
+ // so the whole check collapses to ONE bilinear form
+ // `eq_innerᵀ·(α·A_0 + B_0)·w_col + β·w_col[pin]` against the running
+ // claim. Ties z_partial to the upstream v_a, v_b. Walk-capable circuits
+ // (`bilinear_form`) evaluate it in O(circuit) field ops; the fallback
+ // materializes the marginal and takes the inner product (identical
+ // value — exact field arithmetic).
+ let t = std::time::Instant::now();
+ let eq_rest = build_eq(&r_inner_rest);
+ let mut w_col = Vec::with_capacity(k);
+ for &er in &eq_rest {
+ for &zp in &z_partial {
+ w_col.push(zp * er);
+ }
+ }
+ debug_assert_eq!(w_col.len(), k);
+ let mut final_sum = match circuit.bilinear_form(alpha, &eq_inner, &w_col) {
+ Some(v) => v,
+ None => inner_product_ext(&circuit.fold_alpha_batched(alpha, &eq_inner), &w_col),
+ };
+ if let Some(col) = circuit.const_pin_col() {
+ final_sum += beta * w_col[col];
+ }
if trace {
eprintln!(
- " [lcv] sumcheck replay + comb_vec fold ({} rounds): {}",
- inner_rest_len,
+ " [lcv] final bilinear form (walk or marginal): {}",
fmt(t.elapsed().as_secs_f64())
);
}
-
- // 4. Read + bind z_partial AFTER the sumcheck rounds (matches prover order).
- let z_partial = vs.next_scalars(n_skip).map_err(VerifyError::Transcript)?;
-
- // 5. Final sumcheck consistency: Σ comb_partial[i_skip] · z_partial[i_skip]
- // must equal the running claim. Ties z_partial to the upstream v_a, v_b.
- // Small (length 2^k_skip = 64); sequential.
- let final_sum = inner_product(&comb_vec, &z_partial);
if running != final_sum {
return Err(VerifyError::ConsistencyFailed {
which: "sumcheck-final",
@@ -1486,7 +1513,7 @@ pub fn verify(
// PCS catches mismatches downstream.
let t = std::time::Instant::now();
let lambda = lagrange_weights_naive(k_skip, r_inner_skip);
- let w = inner_product(&lambda, &z_partial);
+ let w = inner_product_ext(&lambda, &z_partial);
if trace {
eprintln!(
" [lcv] final consistency + lagrange_weights_naive: {}",
@@ -1494,11 +1521,6 @@ pub fn verify(
);
}
- // 8. Convert sumcheck challenges to LSB-first x_inner_rest order
- // (same convention as prover).
- let mut r_inner_rest = r_rounds.clone();
- r_inner_rest.reverse();
-
Ok(LincheckClaim {
alpha,
beta,
@@ -1527,10 +1549,9 @@ mod tests {
k_skip: usize,
circuit: &dyn LincheckCircuit,
x_ab: &QuirkyPoint,
- ps: &mut ProverState,
+ ps: &mut pcs::ProverState,
) -> LincheckClaim {
- let (claim, _) =
- prove_padded_capture_z_vec(z_packed, m, k_log, k_skip, 1 << k_log, circuit, x_ab, ps);
+ let (claim, _) = prove_padded_capture_z_vec(z_packed, m, k_log, k_skip, 1 << k_log, circuit, x_ab, ps);
claim
}
@@ -1539,16 +1560,16 @@ mod tests {
z_packed: &[u8],
m: usize,
k_log: usize,
- eq_outer: &[F128],
- ) -> Vec {
+ eq_outer: &[F192],
+ ) -> Vec {
partial_fold_packed_z_fast_padded(z_packed, m, k_log, 1 << k_log, eq_outer)
}
/// Reference fold `M_0^T · eq` (the row-MLE at all boolean column indices),
/// used to locate meaningful mutation targets and as a dense oracle.
- fn sparse_row_fold(matrix: &SparseBinaryMatrix, eq_table: &[F128]) -> Vec {
+ fn sparse_row_fold(matrix: &SparseBinaryMatrix, eq_table: &[F192]) -> Vec {
assert_eq!(eq_table.len(), matrix.num_rows);
- let mut out = vec![F128::ZERO; matrix.num_cols];
+ let mut out = vec![F192::ZERO; matrix.num_cols];
for (row_idx, row) in matrix.rows.iter().enumerate() {
let e = eq_table[row_idx];
for &col in row {
@@ -1560,11 +1581,11 @@ mod tests {
/// Naive MLE evaluation: `f̂(point) = Σ_i eq(point, i) · f[i]` where i ∈
/// {0,1}^d and f[i] is given as a bool slice.
- fn mle_eval_bool(f: &[bool], point: &[F128]) -> F128 {
+ fn mle_eval_bool(f: &[bool], point: &[F192]) -> F192 {
let d = point.len();
assert_eq!(f.len(), 1 << d);
let eq = build_eq(point);
- let mut acc = F128::ZERO;
+ let mut acc = F192::ZERO;
for (i, &b) in f.iter().enumerate() {
if b {
acc += eq[i];
@@ -1573,13 +1594,13 @@ mod tests {
acc
}
- /// Sample a random `QuirkyPoint` for testing: z_skip ∈ F₁₂₈,
+ /// Sample a random `QuirkyPoint` for testing: z_skip ∈ F₁₉₂,
/// x_inner_rest of length `k_log − k_skip`, x_outer of length `n_log`.
fn random_quirky_point(m: usize, k_log: usize, k_skip: usize, rng: &mut Rng) -> QuirkyPoint {
QuirkyPoint {
- z_skip: rng.f128(),
- x_inner_rest: rng.f128_vec(k_log - k_skip),
- x_outer: rng.f128_vec(m - k_log),
+ z_skip: rng.ext(),
+ x_inner_rest: rng.ext_vec(k_log - k_skip),
+ x_outer: rng.ext_vec(m - k_log),
}
}
@@ -1591,13 +1612,7 @@ mod tests {
///
/// where `i = i_skip + 2^k_skip · i_inner_rest + 2^k_log · i_outer` (matches
/// the linear-LSB indexing of `f`).
- fn mle_eval_bool_quirky(
- f: &[bool],
- m: usize,
- k_log: usize,
- k_skip: usize,
- point: &QuirkyPoint,
- ) -> F128 {
+ fn mle_eval_bool_quirky(f: &[bool], m: usize, k_log: usize, k_skip: usize, point: &QuirkyPoint) -> F192 {
let k_skip_dim = 1usize << k_skip;
let inner_rest_len = k_log - k_skip;
let inner_rest_dim = 1usize << inner_rest_len;
@@ -1605,14 +1620,16 @@ mod tests {
let n_outer = 1usize << (m - k_log);
assert_eq!(f.len(), 1 << m);
- let lambda = crate::zerocheck::multilinear::lagrange_weights_naive(k_skip, point.z_skip);
+ // Tower helpers: the point is F192 (the verifier's field), and the
+ // expected value must equal the F192 claim the verifier derives.
+ let lambda = lagrange_weights_naive(k_skip, point.z_skip);
let eq_rest = build_eq(&point.x_inner_rest);
let eq_outer = build_eq(&point.x_outer);
debug_assert_eq!(lambda.len(), k_skip_dim);
debug_assert_eq!(eq_rest.len(), inner_rest_dim);
debug_assert_eq!(eq_outer.len(), n_outer);
- let mut acc = F128::ZERO;
+ let mut acc = F192::ZERO;
for i in 0..(1 << m) {
if !f[i] {
continue;
@@ -1689,19 +1706,15 @@ mod tests {
fn eq_table_matches_direct_formula() {
for &d in &[1usize, 2, 3, 5, 8] {
let mut rng = Rng::new(11 + d as u64);
- let point = rng.f128_vec(d);
+ let point = rng.ext_vec(d);
let table = build_eq(&point);
assert_eq!(table.len(), 1 << d);
for i in 0..(1 << d) {
- let mut expected = F128::ONE;
+ let mut expected = F192::ONE;
for j in 0..d {
let bit = ((i >> j) & 1) as u64;
// eq(r, bit) = (1 + r) if bit = 0 else r
- let factor = if bit == 0 {
- F128::ONE + point[j]
- } else {
- point[j]
- };
+ let factor = if bit == 0 { F192::ONE + point[j] } else { point[j] };
expected *= factor;
}
assert_eq!(table[i], expected, "mismatch at d={d}, i={i}");
@@ -1716,12 +1729,12 @@ mod tests {
let k = 16;
let nnz = 40;
let matrix = random_sparse_matrix(k, nnz, &mut rng);
- let eq_table: Vec = rng.f128_vec(k);
+ let eq_table: Vec = rng.ext_vec(k);
let got = sparse_row_fold(&matrix, &eq_table);
// Brute force: for each col j, sum eq[i] over rows i where M[i,j] = 1.
- let mut expected = vec![F128::ZERO; k];
+ let mut expected = vec![F192::ZERO; k];
for (i, row) in matrix.rows.iter().enumerate() {
for &j in row {
expected[j] += eq_table[i];
@@ -1738,7 +1751,7 @@ mod tests {
let z = rng.bits(1 << m);
let z_packed = pack_z_lincheck(&z, m, k_log);
let n_log = m - k_log;
- let outer_point = rng.f128_vec(n_log);
+ let outer_point = rng.ext_vec(n_log);
let eq_outer = build_eq(&outer_point);
let got = partial_fold_packed_z(&z_packed, m, k_log, &eq_outer);
@@ -1746,7 +1759,7 @@ mod tests {
let k = 1usize << k_log;
assert_eq!(got.len(), k);
for i_inner in 0..k {
- let mut acc = F128::ZERO;
+ let mut acc = F192::ZERO;
for i_outer in 0..(1usize << n_log) {
let i = i_inner + i_outer * k;
if z[i] {
@@ -1767,7 +1780,7 @@ mod tests {
let z = rng.bits(1 << m);
let z_packed = pack_z_lincheck(&z, m, k_log);
let n_log = m - k_log;
- let p = rng.f128_vec(n_log);
+ let p = rng.ext_vec(n_log);
let eq = build_eq(&p);
let serial = partial_fold_packed_z(&z_packed, m, k_log, &eq);
@@ -1788,12 +1801,11 @@ mod tests {
let z = rng.bits(1 << m);
let z_packed = pack_z_lincheck(&z, m, k_log);
let n_log = m - k_log;
- let p = rng.f128_vec(n_log);
+ let p = rng.ext_vec(n_log);
let eq = build_eq(&p);
let serial = partial_fold_packed_z(&z_packed, m, k_log, &eq);
- let iblock =
- partial_fold_packed_z_neon_iblock_padded(&z_packed, m, k_log, 1usize << k_log, &eq);
+ let iblock = partial_fold_packed_z_neon_iblock_padded(&z_packed, m, k_log, 1usize << k_log, &eq);
assert_eq!(serial, iblock, "iblock at m={m}, k_log={k_log}");
}
}
@@ -1815,10 +1827,7 @@ mod tests {
(22, 14, 15_409), // padded, non-byte-aligned (k=16384)
];
for &(m, k_log, useful_bits) in cases {
- assert!(
- n_log_ok_for_tile(m, k_log, NEON_TILE_T),
- "case must be tile-eligible"
- );
+ assert!(n_log_ok_for_tile(m, k_log, NEON_TILE_T), "case must be tile-eligible");
let k = 1usize << k_log;
let n_log = m - k_log;
let n_blocks = 1usize << n_log;
@@ -1831,11 +1840,9 @@ mod tests {
}
}
let z_packed = pack_z_lincheck(&z, m, k_log);
- let eq = build_eq(&rng.f128_vec(n_log));
- let want =
- partial_fold_packed_z_neon_iblock_padded(&z_packed, m, k_log, useful_bits, &eq);
- let got =
- partial_fold_packed_z_neon_oblock_padded(&z_packed, m, k_log, useful_bits, &eq);
+ let eq = build_eq(&rng.ext_vec(n_log));
+ let want = partial_fold_packed_z_neon_iblock_padded(&z_packed, m, k_log, useful_bits, &eq);
+ let got = partial_fold_packed_z_neon_oblock_padded(&z_packed, m, k_log, useful_bits, &eq);
assert_eq!(want, got, "m={m} k_log={k_log} useful={useful_bits}");
}
}
@@ -1876,12 +1883,11 @@ mod tests {
}
}
let z_packed = pack_z_lincheck(&z, m, k_log);
- let outer_point = rng.f128_vec(n_log);
+ let outer_point = rng.ext_vec(n_log);
let eq_outer = build_eq(&outer_point);
let dense_fast = partial_fold_packed_z_fast_padded_dense(&z_packed, m, k_log, &eq_outer);
- let padded_fast =
- partial_fold_packed_z_fast_padded(&z_packed, m, k_log, useful_bits, &eq_outer);
+ let padded_fast = partial_fold_packed_z_fast_padded(&z_packed, m, k_log, useful_bits, &eq_outer);
assert_eq!(
dense_fast, padded_fast,
"fast: m={m}, k_log={k_log}, useful={useful_bits}"
@@ -1889,39 +1895,18 @@ mod tests {
#[cfg(target_arch = "aarch64")]
if n_log_ok_for_tile(m, k_log, NEON_TILE_T) {
- let dense_neon = partial_fold_packed_z_neon_iblock_padded(
- &z_packed,
- m,
- k_log,
- 1usize << k_log,
- &eq_outer,
- );
- let padded_neon = partial_fold_packed_z_neon_iblock_padded(
- &z_packed,
- m,
- k_log,
- useful_bits,
- &eq_outer,
- );
+ let dense_neon =
+ partial_fold_packed_z_neon_iblock_padded(&z_packed, m, k_log, 1usize << k_log, &eq_outer);
+ let padded_neon = partial_fold_packed_z_neon_iblock_padded(&z_packed, m, k_log, useful_bits, &eq_outer);
assert_eq!(
dense_neon, padded_neon,
"neon: m={m}, k_log={k_log}, useful={useful_bits}"
);
// i_inner-partitioned kernel: dense and padded must both match.
- let dense_iblock = partial_fold_packed_z_neon_iblock_padded(
- &z_packed,
- m,
- k_log,
- 1usize << k_log,
- &eq_outer,
- );
- let padded_iblock = partial_fold_packed_z_neon_iblock_padded(
- &z_packed,
- m,
- k_log,
- useful_bits,
- &eq_outer,
- );
+ let dense_iblock =
+ partial_fold_packed_z_neon_iblock_padded(&z_packed, m, k_log, 1usize << k_log, &eq_outer);
+ let padded_iblock =
+ partial_fold_packed_z_neon_iblock_padded(&z_packed, m, k_log, useful_bits, &eq_outer);
assert_eq!(
dense_neon, dense_iblock,
"iblock dense: m={m}, k_log={k_log}, useful={useful_bits}"
@@ -1944,7 +1929,7 @@ mod tests {
let mut rng = Rng::new(44);
let z = rng.bits(1 << m);
let z_packed = pack_z_lincheck(&z, m, k_log);
- let x_outer = rng.f128_vec(m - k_log);
+ let x_outer = rng.ext_vec(m - k_log);
let eq_outer = build_eq(&x_outer);
let z_partial = partial_fold_packed_z(&z_packed, m, k_log, &eq_outer);
@@ -1956,11 +1941,7 @@ mod tests {
// then m-k_log coords from x_outer.
let mut point = Vec::with_capacity(m);
for j in 0..k_log {
- point.push(if (i_inner >> j) & 1 == 1 {
- F128::ONE
- } else {
- F128::ZERO
- });
+ point.push(if (i_inner >> j) & 1 == 1 { F192::ONE } else { F192::ZERO });
}
point.extend_from_slice(&x_outer);
let z_eval = mle_eval_bool(&z, &point);
@@ -2017,10 +1998,7 @@ mod tests {
let proof_t = ch_p.into_proof();
let mut ch_v = pcs::VerifierState::new(b"flock-test-v0", &proof_t, &[]);
- let claim_v = verify(
- m, k_log, k_skip, &circuit, &x_ab, v_a, v_b, &mut ch_v,
- )
- .unwrap_or_else(|e| {
+ let claim_v = verify(m, k_log, k_skip, &circuit, &x_ab, v_a, v_b, &mut ch_v).unwrap_or_else(|e| {
panic!("verify rejected honest proof at m={m},k_log={k_log},k_skip={k_skip}: {e:?}")
});
@@ -2074,11 +2052,13 @@ mod tests {
// Pick a mutation position where BOTH row vectors are nonzero so the
// mutation guarantees both checks would diverge.
- let eq_inner = build_quirky_eq_table(x_ab.z_skip, &x_ab.x_inner_rest, k_skip);
+ let z_skip_g = x_ab.z_skip;
+ let x_inner_rest_g = x_ab.x_inner_rest.to_vec();
+ let eq_inner = build_quirky_eq_table(z_skip_g, &x_inner_rest_g, k_skip);
let row_a = sparse_row_fold(&a_0, &eq_inner);
let row_b = sparse_row_fold(&b_0, &eq_inner);
let idx = (0..k)
- .find(|&i| row_a[i] != F128::ZERO || row_b[i] != F128::ZERO)
+ .find(|&i| row_a[i] != F192::ZERO || row_b[i] != F192::ZERO)
.expect("no row-vector slot is nonzero in either A or B — test degenerate");
// Mutations target `z_partial` (the post-sumcheck length-2^k_skip
@@ -2091,9 +2071,9 @@ mod tests {
for (label, hi) in [("lo", false), ("hi", true)] {
let mut bad = proof_t.clone();
if hi {
- bad.stream[zp_word].hi ^= 1;
+ bad.stream[zp_word].c1 ^= 1;
} else {
- bad.stream[zp_word].lo ^= 1;
+ bad.stream[zp_word].c0 ^= 1;
}
let mut ch = pcs::VerifierState::new(b"flock-test-v0", &bad, &[]);
let res = verify(m, k_log, k_skip, &circuit, &x_ab, v_a, v_b, &mut ch);
@@ -2160,14 +2140,14 @@ mod tests {
/// general-purpose `fold_1b_rows` over the materialized suffix tensor.
#[test]
fn s_hat_v_from_z_vec_matches_fold_1b_rows_ab() {
- const K_SKIP: usize = 6;
+ const K_SKIP: usize = 6;
// (m, k_log) — K_SKIP fixed at 6 (so x_inner_rest has k_log − 6 coords;
// x_inner_rest[0] becomes ring-switch's prefix0 because
// K_SKIP + 1 = LOG_PACKING = 7). n_log = m − k_log must be ≥ 3 for
// partial_fold_packed_z's stripe layout.
let cases: &[(usize, usize)] = &[(13, 10), (15, 11), (17, 13)];
for &(m, k_log) in cases {
- assert!(k_log >= pcs::LOG_PACKING);
+ assert!(k_log >= pcs::pack::LOG_PACKING);
assert!(k_log >= K_SKIP);
let n_log = m - k_log;
assert!(n_log >= 3);
@@ -2180,23 +2160,22 @@ mod tests {
// AB-shaped quirky point: x_inner_rest has k_log − K_SKIP coords;
// x_outer has n_log coords.
- let x_inner_rest: Vec = (0..(k_log - K_SKIP)).map(|_| rng.f128()).collect();
- let x_outer: Vec = (0..n_log).map(|_| rng.f128()).collect();
+ let x_inner_rest: Vec = (0..(k_log - K_SKIP)).map(|_| rng.ext()).collect();
+ let x_outer: Vec = (0..n_log).map(|_| rng.ext()).collect();
// Reference: ring-switch's fold_1b_rows over the materialized
// suffix tensor, exactly the path open_batch hits today.
let mut x_outer_full = Vec::with_capacity(x_inner_rest.len() + x_outer.len());
x_outer_full.extend_from_slice(&x_inner_rest);
x_outer_full.extend_from_slice(&x_outer);
- let suffix = &x_outer_full[1..];
- let suffix_tensor = primitives::multilinear::build_eq(suffix);
- let want = pcs::ring_switch::fold_1b_rows_naive(&packed, &suffix_tensor);
+ let suffix_tensor = primitives::multilinear::eq_table(&x_outer_full);
+ let want = pcs::ring_switch::fold_1b_rows(&packed, &suffix_tensor);
// New path: lincheck-shaped partial fold of z at x_outer, then a
// strided fold against the inner-rest tail.
- let eq_x_outer = primitives::multilinear::build_eq(&x_outer);
+ let eq_x_outer = primitives::multilinear::eq_table(&x_outer);
let z_vec = partial_fold_packed_z(&z_packed_lincheck, m, k_log, &eq_x_outer);
- let got = pcs::ring_switch::s_hat_v_from_z_vec(&z_vec, &x_inner_rest[1..]);
+ let got = pcs::ring_switch::s_hat_v_from_z_vec(&z_vec, &x_inner_rest);
assert_eq!(got, want, "s_hat_v mismatch at m={m}, k_log={k_log}");
}
diff --git a/crates/flock/src/proof.rs b/crates/flock/src/proof.rs
index af7e33dd..f34e7fdd 100644
--- a/crates/flock/src/proof.rs
+++ b/crates/flock/src/proof.rs
@@ -1,13 +1,14 @@
-// Credit: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
+// CREDIT: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
//! The evaluation-claim type shared by the zerocheck/lincheck reduction and
//! the PCS.
-use primitives::field::F128;
use crate::lincheck::QuirkyPoint;
+use primitives::field::F192;
-/// A claim of the form `ẑ(point) = value` for the witness `z`.
+/// A claim of the form `ẑ(point) = value` for the witness `z`. Tower-valued:
+/// the flock verifier and the downstream PCS run over `F192`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ZClaim {
pub point: QuirkyPoint,
- pub value: F128,
+ pub value: F192,
}
diff --git a/crates/flock/src/r1cs.rs b/crates/flock/src/r1cs.rs
index 4744919c..508c2db7 100644
--- a/crates/flock/src/r1cs.rs
+++ b/crates/flock/src/r1cs.rs
@@ -1,4 +1,4 @@
-// Credit: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
+// CREDIT: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
//! Block-diagonal R1CS over GF(2).
//!
//! The standard R1CS is `(A·z) ⊙ (B·z) ⊕ (C·z) = 0`. We fix `C = I` (the
@@ -35,9 +35,9 @@ pub enum WitnessLayout {
///
/// `k_skip` is the zerocheck's univariate-skip dimension (`k_skip ≤ k_log`).
/// It defines how the m-dim claim point is laid out in the protocol: one
-/// univariate F128 coord binds the LSB `k_skip` bits, `k_log − k_skip`
-/// multilinear F128 coords bind the next inner bits, and `n_log` multilinear
-/// F128 coords bind the outer bits.
+/// univariate F192 coord binds the LSB `k_skip` bits, `k_log − k_skip`
+/// multilinear F192 coords bind the next inner bits, and `n_log` multilinear
+/// F192 coords bind the outer bits.
#[derive(Debug)]
pub struct BlockR1cs {
pub m: usize,
@@ -103,8 +103,7 @@ impl BlockR1cs {
/// over the nonzeros) out of the prove path.
pub fn csc_lincheck_circuit(&self) -> &crate::lincheck::CscCircuit {
self.csc_cache.get_or_init(|| {
- crate::lincheck::CscCircuit::from_matrices(&self.a_0, &self.b_0)
- .with_const_pin(self.const_pin)
+ crate::lincheck::CscCircuit::from_matrices(&self.a_0, &self.b_0).with_const_pin(self.const_pin)
})
}
@@ -157,7 +156,6 @@ impl BlockR1cs {
absorb_matrix(&mut h, &self.c_0);
*h.finalize().as_bytes()
}
-
}
/// Length-prefixed absorption of a sparse matrix into a BLAKE3 hasher.
diff --git a/crates/flock/src/test_rng.rs b/crates/flock/src/test_rng.rs
index 14f2e021..4bb824c0 100644
--- a/crates/flock/src/test_rng.rs
+++ b/crates/flock/src/test_rng.rs
@@ -1,4 +1,4 @@
-use primitives::field::F128;
+use primitives::field::F192;
/// Deterministic SplitMix64 generator shared by the crate's unit tests.
pub(crate) struct Rng(u64);
@@ -28,11 +28,11 @@ impl Rng {
(0..n).map(|_| self.bit()).collect()
}
- pub(crate) fn f128(&mut self) -> F128 {
- F128::new(self.next_u64(), self.next_u64())
+ pub(crate) fn ext(&mut self) -> F192 {
+ F192::new(self.next_u64(), self.next_u64(), self.next_u64())
}
- pub(crate) fn f128_vec(&mut self, n: usize) -> Vec {
- (0..n).map(|_| self.f128()).collect()
+ pub(crate) fn ext_vec(&mut self, n: usize) -> Vec {
+ (0..n).map(|_| self.ext()).collect()
}
}
diff --git a/crates/flock/src/verifier.rs b/crates/flock/src/verifier.rs
index 4cf434e3..f9e756bd 100644
--- a/crates/flock/src/verifier.rs
+++ b/crates/flock/src/verifier.rs
@@ -1,4 +1,4 @@
-// Credit: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
+// CREDIT: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
//! Errors of the R1CS reduction (zerocheck + lincheck + PCS opening).
use crate::lincheck;
@@ -9,5 +9,4 @@ use crate::zerocheck;
pub enum VerifyError {
Zerocheck(zerocheck::VerifyError),
Lincheck(lincheck::VerifyError),
- Pcs(::pcs::VerifyError),
}
diff --git a/crates/flock/src/zerocheck.rs b/crates/flock/src/zerocheck.rs
index 48a37a7b..60301388 100644
--- a/crates/flock/src/zerocheck.rs
+++ b/crates/flock/src/zerocheck.rs
@@ -1,13 +1,13 @@
-// Credit: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
+// CREDIT: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
//! Zerocheck PIOP: prove a(y) · b(y) ⊕ c(y) = 0 for all y ∈ {0,1}^m.
//!
//! Inputs are three bit vectors of length 2^m. Output is an evaluation claim
//! on the multilinear extensions â, b̂, ĉ at the protocol-derived point.
//!
//! Protocol shape (m = log_n, k_skip = [`K_SKIP`] = 6):
-//! 1. Verifier samples `r ∈ F_{2^128}^m` (the zerocheck challenge).
+//! 1. Verifier samples `r ∈ F_{2^192}^m` (the zerocheck challenge).
//! 2. Prover sends `P^{AB}(λ)` and `P^C(λ)` for λ ∈ Λ, |Λ| = 2^k_skip.
-//! 3. Verifier samples `z ∈ F_{2^128}` (univariate-skip fold point).
+//! 3. Verifier samples `z ∈ F_{2^192}` (univariate-skip fold point).
//! 4. For each of the `m - k_skip` multilinear rounds, prover sends
//! `(P_r(1), P_r(∞))` and verifier samples `ρ_r`.
//! 5. Prover sends final MLE evaluations `(â, b̂, ĉ)` at the resulting point.
@@ -16,8 +16,9 @@
//! is tested on honest witnesses; verify also rejects byte-mutated proofs and
//! shape-corrupted ones.
-use pcs::{ProverState, VerifierState};
-use primitives::field::{F8, F128};
+use fiat_shamir::transcript::{ProverState, VerifierState};
+use primitives::field::{F8, F192};
+
use pcs::ntt::{AdditiveNttGf8, InvNttTableByteSingleGf8};
pub mod multilinear;
@@ -25,28 +26,27 @@ pub mod univariate_skip;
pub mod univariate_skip_optimized;
use multilinear::{
- UniSkipFoldTable, fold_and_compute_round_pair_into, fold_in_place_pair,
- interpolate_at_z_combined, interpolate_at_z_on_lambda, round_pair_naive,
- uni_skip_fold_and_round_pair_optimized_packed_padded,
+ UniSkipFoldTable, fold_and_compute_round_pair_into, fold_in_place_pair, interpolate_at_z_combined,
+ interpolate_at_z_on_lambda, round_pair_naive, uni_skip_fold_and_round_pair_optimized_packed_padded,
};
use univariate_skip_optimized::{
- c_s_f128, medium_challenges_ghash, small_challenges_ghash,
+ c_s, medium_challenges, round1_shift_reduce_extract_c_packed_padded, small_challenges,
};
/// Number of variables folded in round 1 via the additive-NTT univariate skip.
/// |Λ| = 2^K_SKIP = 64 elements; the round-1 prover message is two length-64
-/// vectors of F128.
+/// vectors of F192.
pub const K_SKIP: usize = 6;
const N_INNER: usize = 7; // 3 small + 4 medium fixed-constant eq dimensions
/// Build the zerocheck challenge vector in the shared prover/verifier order:
/// sampled skip coordinates, fixed inner coordinates, then sampled outer ones.
-fn challenge_vector(m: usize, mut sample_vec: impl FnMut(usize) -> Vec) -> Vec {
+fn challenge_vector(m: usize, mut sample_vec: impl FnMut(usize) -> Vec) -> Vec {
let skip = sample_vec(K_SKIP);
let outer = sample_vec(m - K_SKIP - N_INNER);
skip.into_iter()
- .chain(small_challenges_ghash())
- .chain(medium_challenges_ghash())
+ .chain(small_challenges())
+ .chain(medium_challenges())
.chain(outer)
.collect()
}
@@ -82,20 +82,20 @@ pub use pcs::pack::PaddingSpec;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ZerocheckClaim {
/// Univariate-skip challenge sampled after round 1 (binds the K_SKIP
- /// skip variables).
- pub z: F128,
+ /// skip variables), represented directly in `F192`.
+ pub z: F192,
/// AB sumcheck bind challenges, one per multilinear round; length = `m - K_SKIP`.
- pub mlv_challenges: Vec,
+ pub mlv_challenges: Vec,
/// Eq weights for the rest variables = the zerocheck challenge restricted
/// to `r[K_SKIP..m]`. This is the *rest part of the c-claim's point*.
/// Length = `m - K_SKIP`.
- pub r_rest: Vec,
+ pub r_rest: Vec,
/// `â(z, mlv_challenges)`.
- pub a_eval: F128,
+ pub a_eval: F192,
/// `b̂(z, mlv_challenges)`.
- pub b_eval: F128,
+ pub b_eval: F192,
/// `ĉ(z, r_rest)` — at a *different point* than a_eval, b_eval.
- pub c_eval: F128,
+ pub c_eval: F192,
}
// (No ZerocheckProof struct: every round message rides the shared transcript
@@ -126,15 +126,28 @@ pub enum VerifyError {
/// ([`univariate_skip_optimized::round1_shift_reduce_extract_c_packed_padded_with_s_hat_v`]),
/// which the downstream PCS open consumes to skip `fold_1b_rows` for the
/// c-claim.
+pub fn prove_packed_padded_capture_s_hat_v_c(
+ a_packed: &[u8],
+ b_packed: &[u8],
+ c_packed: &[u8],
+ m: usize,
+ padding: &PaddingSpec,
+ ps: &mut ProverState,
+) -> (ZerocheckClaim, Vec) {
+ let (claim, captured) = prove_packed_padded_inner(a_packed, b_packed, c_packed, m, padding, true, ps);
+ (claim, captured.expect("capture=true must produce s_hat_v_c"))
+}
+
#[allow(clippy::too_many_arguments)]
-pub fn prove_packed_padded(
+fn prove_packed_padded_inner(
a_packed: &[u8],
b_packed: &[u8],
c_packed: &[u8],
m: usize,
padding: &PaddingSpec,
- ps: &mut ProverState,
-) -> (ZerocheckClaim, Vec) {
+ capture_s_hat_v_c: bool,
+ ps: &mut ProverState,
+) -> (ZerocheckClaim, Option>) {
let k_skip = K_SKIP;
assert!(
m >= k_skip + N_INNER,
@@ -156,7 +169,8 @@ pub fn prove_packed_padded(
// r[k_skip+3..k_skip+7] — protocol medium-eq constants β_i
// r[k_skip+7..m] — sampled (the "outer" eq weights for
// the URM and multilinear rounds)
- let r = challenge_vector(m, |n| ps.sample_vec(n));
+ // Prover and verifier use the same tower-valued challenges directly.
+ let r = challenge_vector(m, |n| ps.sample_vec(n).into_iter().collect());
// ---- 3. Round 1: URM (extract_c, parallel) ----
//
@@ -170,20 +184,21 @@ pub fn prove_packed_padded(
let ntt_s = AdditiveNttGf8::new(k_skip, F8::ZERO);
let ntt_l = AdditiveNttGf8::new(k_skip, F8(1u8 << k_skip));
let inv_table = InvNttTableByteSingleGf8::new(&ntt_s, &ntt_l);
- let (round1_ab_opt, round1_c_opt, s_hat_v_c) =
- crate::zerocheck::univariate_skip_optimized::round1_shift_reduce_extract_c_packed_padded_with_s_hat_v(
- a_packed,
- b_packed,
- c_packed,
- m,
- k_skip,
- &r,
- &inv_table,
- padding,
+ let (round1_ab_opt, round1_c_opt, s_hat_v_c) = if capture_s_hat_v_c {
+ let (ab, c, s) =
+ crate::zerocheck::univariate_skip_optimized::round1_shift_reduce_extract_c_packed_padded_with_s_hat_v(
+ a_packed, b_packed, c_packed, m, k_skip, &r, &inv_table, padding,
+ );
+ (ab, c, Some(s))
+ } else {
+ let (ab, c) = round1_shift_reduce_extract_c_packed_padded(
+ a_packed, b_packed, c_packed, m, k_skip, &r, &inv_table, padding,
);
- let c_s = c_s_f128();
- let round1_ab: Vec = round1_ab_opt.iter().map(|x| c_s * *x).collect();
- let round1_c: Vec = round1_c_opt.iter().map(|x| c_s * *x).collect();
+ (ab, c, None)
+ };
+ let c_s = c_s();
+ let round1_ab: Vec = round1_ab_opt.iter().map(|x| c_s * *x).collect();
+ let round1_c: Vec = round1_c_opt.iter().map(|x| c_s * *x).collect();
if zc_timing {
eprintln!(
"[zc-timing] round1 URM: {:.2} ms",
@@ -192,8 +207,12 @@ pub fn prove_packed_padded(
}
// ---- 4. Transmit + bind round-1 message on the stream, sample z ----
- ps.add_scalars(&round1_ab);
- ps.add_scalars(&round1_c);
+ for &x in round1_ab.iter() {
+ ps.add_scalar(x);
+ }
+ for &x in round1_c.iter() {
+ ps.add_scalar(x);
+ }
let z = ps.sample();
// ---- 5. c_eval = ĉ(z, r_rest) via interpolation of round1_c at z ----
@@ -202,7 +221,7 @@ pub fn prove_packed_padded(
// as its 2^k_skip evaluations on Λ. Interpolating to λ=z gives
// `ĉ(z, r_rest)` directly (the eq-weighted sum collapses to the MLE
// evaluation because ĉ is linear). This is **the c-claim** — at point
- // `(z, r_rest)`, *not* `(z, ρ-values)`. ~64 F128 muls + Lagrange weights.
+ // `(z, r_rest)`, *not* `(z, ρ-values)`. ~64 F192 muls + Lagrange weights.
let final_c_eval = interpolate_at_z_on_lambda(&round1_c, k_skip, z);
// ---- 6. Round 2: fused fold + first multilinear message ----
@@ -212,18 +231,17 @@ pub fn prove_packed_padded(
// verifier samples ρ_1 after observing this message.
let t_round2 = std::time::Instant::now();
let fold_table = UniSkipFoldTable::new(k_skip, z);
- let mut mlv_arg = vec![F128::ONE; n_mlv];
+ let mut mlv_arg = vec![F192::ONE; n_mlv];
mlv_arg[1..].copy_from_slice(&r[k_skip + 1..]);
- let (mut a_mlv, mut b_mlv, msg_1, msg_inf) =
- uni_skip_fold_and_round_pair_optimized_packed_padded(
- a_packed,
- b_packed,
- m,
- k_skip,
- &fold_table,
- &mlv_arg,
- padding,
- );
+ let (mut a_mlv, mut b_mlv, msg_1, msg_inf) = uni_skip_fold_and_round_pair_optimized_packed_padded(
+ a_packed,
+ b_packed,
+ m,
+ k_skip,
+ &fold_table,
+ &mlv_arg,
+ padding,
+ );
if zc_timing {
eprintln!(
@@ -236,14 +254,14 @@ pub fn prove_packed_padded(
multilinear_msgs.push((msg_1, msg_inf));
ps.add_scalar(msg_1);
ps.add_scalar(msg_inf);
- let mut mlv_rhos: Vec = Vec::with_capacity(n_mlv);
+ let mut mlv_rhos: Vec = Vec::with_capacity(n_mlv);
mlv_rhos.push(ps.sample());
// ---- 7. Rounds 3..(n_mlv + 1) — AB only (c is done) ----
//
// Iter i: fold (a, b) at ρ_{i+1}, compute round (i+3) message, sample
// ρ_{i+2}. Use the fused parallel path while log_n ≥ 10; below that the
- // SplitEqGhash inner can't form lo_size ≥ 2, so we fall back to
+ // SplitEq inner can't form lo_size ≥ 2, so we fall back to
// fold_in_place_pair + round_pair_naive.
//
// Ping-pong scratch buffers for the fused path: each fused round folds
@@ -255,8 +273,8 @@ pub fn prove_packed_padded(
let n_in = a_mlv.len();
let (mut a_nxt, mut b_nxt) = if n_in >= 1024 {
(
- primitives::scratch::take_f128(n_in / 2),
- primitives::scratch::take_f128(n_in / 2),
+ primitives::scratch::take_f192(n_in / 2),
+ primitives::scratch::take_f192(n_in / 2),
)
} else {
(Vec::new(), Vec::new())
@@ -269,7 +287,7 @@ pub fn prove_packed_padded(
// r_next for the next round's message: length log_n_before - 1.
// r_next[0] = ONE (Convention A factor); r_next[1..] are the eq
// weights for the remaining variables = r[k_skip + i + 2..m].
- let mut r_next = vec![F128::ONE; log_n_before - 1];
+ let mut r_next = vec![F192::ONE; log_n_before - 1];
r_next[1..].copy_from_slice(&r[k_skip + i + 2..]);
let (m1, mi) = if log_n_before >= 10 {
@@ -327,10 +345,10 @@ pub fn prove_packed_padded(
// Recycle the four tail buffers (the two len-1 survivors still own their
// full round-2 capacity) for the next phase/prove.
- primitives::scratch::give_f128(a_mlv);
- primitives::scratch::give_f128(b_mlv);
- primitives::scratch::give_f128(a_nxt);
- primitives::scratch::give_f128(b_nxt);
+ primitives::scratch::give_f192(a_mlv);
+ primitives::scratch::give_f192(b_mlv);
+ primitives::scratch::give_f192(a_nxt);
+ primitives::scratch::give_f192(b_nxt);
if zc_timing {
eprintln!(
@@ -339,12 +357,10 @@ pub fn prove_packed_padded(
);
}
- let r_rest: Vec = r[k_skip..].to_vec();
-
let claim = ZerocheckClaim {
z,
- mlv_challenges: mlv_rhos,
- r_rest,
+ mlv_challenges: mlv_rhos.to_vec(),
+ r_rest: r[k_skip..].to_vec(),
a_eval: final_a_eval,
b_eval: final_b_eval,
c_eval: final_c_eval,
@@ -360,10 +376,7 @@ pub fn prove_packed_padded(
/// On accept: returns the [`ZerocheckClaim`] the caller must check against
/// its PCS opening of `â`, `b̂`, `ĉ`.
/// On reject: returns a [`VerifyError`] indicating which check failed.
-pub fn verify(
- log_n: usize,
- vs: &mut VerifierState<'_>,
-) -> Result {
+pub fn verify(log_n: usize, vs: &mut VerifierState<'_, O>) -> Result {
let m = log_n;
let k_skip = K_SKIP;
@@ -374,11 +387,12 @@ pub fn verify(
let ell = 1usize << k_skip;
// ---- Re-derive r (in lockstep with prove_packed) ----
+ // The verifier samples tower challenges directly, matching the prover.
let r = challenge_vector(m, |n| vs.sample_vec(n));
// ---- Read + bind round-1 messages off the stream, sample z ----
- let round1_ab = vs.next_scalars(ell).map_err(VerifyError::Transcript)?;
- let round1_c = vs.next_scalars(ell).map_err(VerifyError::Transcript)?;
+ let round1_ab: Vec = vs.next_scalars(ell).map_err(VerifyError::Transcript)?;
+ let round1_c: Vec = vs.next_scalars(ell).map_err(VerifyError::Transcript)?;
let z = vs.sample();
// ---- Reconstruct ĉ(z, r_rest) from round1_c ----
@@ -402,11 +416,7 @@ pub fn verify(
// If the prover's witness is dishonest the S-zero assumption fails, the
// reconstructed c_0 is wrong, and the running-claim chain ends at a value
// inconsistent with `â · b̂`. We catch that at the final sumcheck check.
- let combined_at_lambda: Vec = round1_ab
- .iter()
- .zip(&round1_c)
- .map(|(x, y)| *x + *y)
- .collect();
+ let combined_at_lambda: Vec = round1_ab.iter().zip(&round1_c).map(|(x, y)| *x + *y).collect();
let combined_at_z = interpolate_at_z_combined(&combined_at_lambda, k_skip, z);
let p_c_at_z = interpolate_at_z_on_lambda(&round1_c, k_skip, z);
let mut c_running = combined_at_z + p_c_at_z;
@@ -427,14 +437,14 @@ pub fn verify(
// 3. update `c_running ← G(ρ_i)`,
// where `G(X) = G(0)·(1+X) + G(1)·X + G(∞)·X·(X+1)` (char-2 quadratic
// interpolation through G(0), G(1), G(∞)).
- let mut mlv_rhos: Vec = Vec::with_capacity(n_mlv);
- let mut multilinear_rounds: Vec<(F128, F128)> = Vec::with_capacity(n_mlv);
+ let mut mlv_rhos: Vec = Vec::with_capacity(n_mlv);
+ let mut multilinear_rounds: Vec<(F192, F192)> = Vec::with_capacity(n_mlv);
for i in 0..n_mlv {
let msg_1 = vs.next_scalar().map_err(VerifyError::Transcript)?;
let msg_inf = vs.next_scalar().map_err(VerifyError::Transcript)?;
multilinear_rounds.push((msg_1, msg_inf));
let r_eq = r[k_skip + i];
- let one_plus_r_eq = F128::ONE + r_eq;
+ let one_plus_r_eq = F192::ONE + r_eq;
let g1 = msg_1;
let g_inf = msg_inf;
@@ -443,9 +453,8 @@ pub fn verify(
let rho = vs.sample();
mlv_rhos.push(rho);
- let one_plus_rho = F128::ONE + rho;
// G(ρ) = G(0)·(1+ρ) + G(1)·ρ + G(∞)·ρ·(1+ρ).
- c_running = g0 * one_plus_rho + g1 * rho + g_inf * rho * one_plus_rho;
+ c_running = g0 + rho * (g0 + g1 + (F192::ONE + rho) * g_inf);
}
// ---- AB sumcheck final consistency ----
@@ -456,11 +465,11 @@ pub fn verify(
// (The eq factors were absorbed round-by-round into the consistency checks,
// never accumulating into the running claim.)
// Read + bind the final â, b̂ claims off the stream (mirrors
- // `prove_packed_padded`): binding must land before the next challenge
+ // `prove_packed_padded_inner`): binding must land before the next challenge
// (lincheck's α) is drawn, so the α-batched reduction of these two claims is
// sound. `final_c_eval` is the verifier's OWN interpolation of the
// already-bound `round1_c` at `z` — never transported.
- let r_rest: Vec = r[k_skip..].to_vec();
+ let r_rest: Vec = r[k_skip..].to_vec();
let final_a_eval = vs.next_scalar().map_err(VerifyError::Transcript)?;
let final_b_eval = vs.next_scalar().map_err(VerifyError::Transcript)?;
if c_running != final_a_eval * final_b_eval {
@@ -491,14 +500,8 @@ mod tests {
m: usize,
ps: &mut pcs::ProverState,
) -> ZerocheckClaim {
- let (claim, _) = prove_packed_padded(
- a_packed,
- b_packed,
- c_packed,
- m,
- &PaddingSpec::dense(m),
- ps,
- );
+ let (claim, _) =
+ prove_packed_padded_capture_s_hat_v_c(a_packed, b_packed, c_packed, m, &PaddingSpec::dense(m), ps);
claim
}
@@ -535,7 +538,8 @@ mod tests {
assert_eq!(stream.len(), 2 * (1 << K_SKIP) + 2 * (m - K_SKIP) + 2, "m={m}");
assert_eq!(claim.mlv_challenges.len(), m - K_SKIP, "m={m}");
- // Claim's eval fields agree with the streamed final evals.
+ // Claim's eval fields agree with the streamed final evals (both are
+ // now tower values — the prover streams eval).
assert_eq!(claim.a_eval, stream[stream.len() - 2], "m={m}");
assert_eq!(claim.b_eval, stream[stream.len() - 1], "m={m}");
}
@@ -566,7 +570,7 @@ mod tests {
}
/// **Verify rejects byte-mutated proofs.** Walk each component of the
- /// proof and flip one F128 entry; the verifier must return an `Err`
+ /// proof and flip one F192 entry; the verifier must return an `Err`
/// (rather than panicking or silently accepting).
#[test]
fn verify_rejects_mutations() {
@@ -595,7 +599,7 @@ mod tests {
];
for (label, word) in mutations {
let mut bad = proof_t.clone();
- bad.stream[word].lo ^= 1;
+ bad.stream[word].c0 ^= 1;
let mut ch = pcs::VerifierState::new(b"flock-test-v0", &bad, &[]);
let result = verify(m, &mut ch);
assert!(
@@ -622,10 +626,7 @@ mod tests {
let mut bad = proof_t.clone();
bad.stream.truncate(bad.stream.len() - 3);
let mut ch = pcs::VerifierState::new(b"flock-test-v0", &bad, &[]);
- assert!(matches!(
- verify(m, &mut ch),
- Err(VerifyError::Transcript(_))
- ));
+ assert!(matches!(verify(m, &mut ch), Err(VerifyError::Transcript(_))));
// log_n too small.
let mut ch = pcs::VerifierState::new(b"flock-test-v0", &proof_t, &[]);
@@ -655,10 +656,7 @@ mod tests {
let mut ch_verify = pcs::VerifierState::new(b"flock-test-v0", &proof_t, &[]);
let res = verify(m, &mut ch_verify);
- assert!(
- res.is_err(),
- "verify ACCEPTED a false statement at m={m}: {res:?}"
- );
+ assert!(res.is_err(), "verify ACCEPTED a false statement at m={m}: {res:?}");
}
}
@@ -683,7 +681,7 @@ mod tests {
// verifier should reject (overwhelming probability).
for idx in 0..(m - K_SKIP) {
let mut bad = proof_t.clone();
- bad.stream[2 * (1 << K_SKIP) + 2 * idx + 1] += F128::ONE;
+ bad.stream[2 * (1 << K_SKIP) + 2 * idx + 1] += F192::ONE;
let mut ch = pcs::VerifierState::new(b"flock-test-v0", &bad, &[]);
let res = verify(m, &mut ch);
assert!(res.is_err(), "msg_inf tamper at round {idx} ACCEPTED");
@@ -708,12 +706,9 @@ mod tests {
let last = m - K_SKIP - 1;
let mut bad = proof_t.clone();
- bad.stream[2 * (1 << K_SKIP) + 2 * last + 1] += F128::ONE;
+ bad.stream[2 * (1 << K_SKIP) + 2 * last + 1] += F192::ONE;
let mut ch = pcs::VerifierState::new(b"flock-test-v0", &bad, &[]);
- assert!(
- verify(m, &mut ch).is_err(),
- "last-round msg_inf unconstrained"
- );
+ assert!(verify(m, &mut ch).is_err(), "last-round msg_inf unconstrained");
}
/// AUDIT (Fiat–Shamir binding of the final â, b̂ claims). Regression test
@@ -749,19 +744,14 @@ mod tests {
// Honest verify, then capture the next challenge the transcript feeds
// downstream — this is exactly the slot lincheck samples α from.
let mut ch_honest = pcs::VerifierState::new(b"flock-test-v0", &proof_t, &[]);
- assert!(
- verify(m, &mut ch_honest).is_ok(),
- "honest verify rejected"
- );
+ assert!(verify(m, &mut ch_honest).is_ok(), "honest verify rejected");
let alpha_honest = ch_honest.sample();
// Product-preserving tamper: â' = â·t, b̂' = b̂·t⁻¹ ⇒ â'·b̂' = â·b̂, so the
// zerocheck's `c_running == â·b̂` check still holds for the tampered pair.
- let t = F128 {
- lo: 0x0123_4567_89ab_cdef,
- hi: 0xfedc_ba98_7654_3210,
- };
- assert!(t != F128::ZERO && t != F128::ONE, "t must be nontrivial");
+ // The stream now carries tower (F192) values, so tamper in F192.
+ let t = F192::new(0x0123_4567_89ab_cdef, 0xfedc_ba98_7654_3210, 0x55aa_aa55_0123_4567);
+ assert!(t != F192::ZERO && t != F192::ONE, "t must be nontrivial");
// The finals are the LAST two stream words of this standalone proof.
let n = proof_t.stream.len();
let mut bad = proof_t.clone();
@@ -819,10 +809,7 @@ mod tests {
let proof_t = ch_prove.into_proof();
let mut ch_verify = pcs::VerifierState::new(b"flock-test-v0", &proof_t, &[]);
let res = verify(m, &mut ch_verify);
- assert!(
- res.is_err(),
- "false statement (seed={seed}) ACCEPTED: {res:?}"
- );
+ assert!(res.is_err(), "false statement (seed={seed}) ACCEPTED: {res:?}");
}
}
@@ -840,12 +827,9 @@ mod tests {
let proof_t = ch_prove.into_proof();
for idx in 0..(m - K_SKIP) {
let mut bad = proof_t.clone();
- bad.stream[2 * (1 << K_SKIP) + 2 * idx] += F128::ONE;
+ bad.stream[2 * (1 << K_SKIP) + 2 * idx] += F192::ONE;
let mut ch = pcs::VerifierState::new(b"flock-test-v0", &bad, &[]);
- assert!(
- verify(m, &mut ch).is_err(),
- "msg_1 tamper round {idx} ACCEPTED"
- );
+ assert!(verify(m, &mut ch).is_err(), "msg_1 tamper round {idx} ACCEPTED");
}
}
diff --git a/crates/flock/src/zerocheck/multilinear.rs b/crates/flock/src/zerocheck/multilinear.rs
index b38576a0..478e71f6 100644
--- a/crates/flock/src/zerocheck/multilinear.rs
+++ b/crates/flock/src/zerocheck/multilinear.rs
@@ -1,4 +1,4 @@
-// Credit: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
+// CREDIT: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
//! Multilinear sumcheck — rounds 2..(m − k_skip + 1) of the zerocheck protocol.
//!
//! After the round-1 URM and the verifier's univariate-skip fold-point `z`, the
@@ -33,10 +33,10 @@
//! Verifier reconstructs `G(0)` from the running claim via
//! `current_claim = (1+r_now)·G(0) + r_now·G(1)`.
-use primitives::field::{F128, F256Unreduced, PHI_8_TABLE};
-pub use primitives::multilinear::eq_eval;
use crate::zerocheck::PaddingSpec;
-use crate::zerocheck::univariate_skip::{SplitEqGhash, build_eq, pack_bits};
+use crate::zerocheck::univariate_skip::{SplitEq, build_eq, pack_bits};
+use primitives::field::{F192, F192Unreduced, PHI_8_TABLE_192 as PHI_8_TABLE};
+pub use primitives::multilinear::eq_eval;
/// Returns `(pair_in_block_mask, useful_pairs_inclusive)` for the round-2
/// fused-fold kernel. A pair (post-URM chunks `2k`, `2k+1`) is fully inside
@@ -70,7 +70,7 @@ fn round2_pair_skip(padding: &PaddingSpec, k_skip: usize) -> (usize, usize) {
///
/// `L_i(z) = ∏_{j ≠ i} (z + φ_8(j)) / (φ_8(i) + φ_8(j))` — the standard Lagrange
/// formula, with the nodes being the F_8 elements `0..2^k_skip` embedded into
-/// F_{2^128} via `φ_8`. Subtraction is XOR in characteristic 2.
+/// F_{2^192} via `φ_8`. Subtraction is XOR in characteristic 2.
///
pub use primitives::multilinear::lagrange_weights_naive;
@@ -80,14 +80,14 @@ pub use primitives::multilinear::lagrange_weights_naive;
///
/// Used to interpolate the extract_c round-1 output `round1_c` (which carries
/// the polynomial `P^C` as its 2^k_skip evaluations on Λ) at the URM challenge `z`.
-pub fn lagrange_weights_lambda_naive(k_skip: usize, z: F128) -> Vec {
+pub fn lagrange_weights_lambda_naive(k_skip: usize, z: F192) -> Vec {
let ell = 1usize << k_skip;
assert!(2 * ell <= 256, "Λ ∪ S must fit in F_8 (need k_skip ≤ 7)");
- let mut weights = vec![F128::ZERO; ell];
+ let mut weights = vec![F192::ZERO; ell];
for i in 0..ell {
let si = PHI_8_TABLE[ell + i];
- let mut num = F128::ONE;
- let mut den = F128::ONE;
+ let mut num = F192::ONE;
+ let mut den = F192::ONE;
for j in 0..ell {
if j == i {
continue;
@@ -107,11 +107,11 @@ pub fn lagrange_weights_lambda_naive(k_skip: usize, z: F128) -> Vec {
/// In the extract_c protocol the prover ships `round1_c` (the `P^C` polynomial
/// in Λ-form) and the verifier (or higher-level prover) needs `P^C(z) = ĉ(z, r_rest)`.
/// That value is *the c-claim* at the bound point `(z, r_rest)`.
-pub fn interpolate_at_z_on_lambda(values: &[F128], k_skip: usize, z: F128) -> F128 {
+pub fn interpolate_at_z_on_lambda(values: &[F192], k_skip: usize, z: F192) -> F192 {
let ell = 1usize << k_skip;
assert_eq!(values.len(), ell);
let weights = lagrange_weights_lambda_naive(k_skip, z);
- let mut acc = F128::ZERO;
+ let mut acc = F192::ZERO;
for i in 0..ell {
acc += weights[i] * values[i];
}
@@ -127,21 +127,21 @@ pub fn interpolate_at_z_on_lambda(values: &[F128], k_skip: usize, z: F128) -> F1
/// evaluations on Λ that the prover sends, that's `2·2^k_skip` evaluations —
/// enough to interpolate the degree-`< 2·2^k_skip` polynomial uniquely.
///
-/// Cost: `2·ell × (2·ell − 1)` F128 muls + `ell` inversions for the Lagrange
+/// Cost: `2·ell × (2·ell − 1)` F192 muls + `ell` inversions for the Lagrange
/// weights. At ell=64 that's ~16K muls + 64 inversions. Sub-millisecond
/// one-time cost in the verifier.
-pub fn interpolate_at_z_combined(values_on_lambda: &[F128], k_skip: usize, z: F128) -> F128 {
+pub fn interpolate_at_z_combined(values_on_lambda: &[F192], k_skip: usize, z: F192) -> F192 {
let ell = 1usize << k_skip;
assert_eq!(values_on_lambda.len(), ell);
assert!(2 * ell <= 256, "Λ ∪ S must fit in F_8 (need k_skip ≤ 7)");
let n_total = 2 * ell;
- let mut acc = F128::ZERO;
+ let mut acc = F192::ZERO;
for i in 0..ell {
// i-th Λ node = node index `ell + i` in PHI_8_TABLE.
let node_idx = ell + i;
let si = PHI_8_TABLE[node_idx];
- let mut num = F128::ONE;
- let mut den = F128::ONE;
+ let mut num = F192::ONE;
+ let mut den = F192::ONE;
for j in 0..n_total {
if j == node_idx {
continue;
@@ -162,23 +162,23 @@ pub fn interpolate_at_z_combined(values_on_lambda: &[F128], k_skip: usize, z: F1
/// Evaluate the univariate-skip polynomial at the fold point `z`, given the
/// precomputed Lagrange `weights`. Returns the multilinear extension table
-/// `a_mlv` of length `2^(m − k_skip)` over F_{2^128}.
+/// `a_mlv` of length `2^(m − k_skip)` over F_{2^192}.
///
/// `a_mlv[x_rest] = Σ_s a(s, x_rest) · L_s(z)`
///
/// `a(s, x_rest)` is the witness bit at index `x_rest * 2^k_skip + s` (low
/// bits = skip variable, high bits = rest variables).
-pub fn fold_at_z_naive(witness: &[bool], m: usize, k_skip: usize, weights: &[F128]) -> Vec {
+pub fn fold_at_z_naive(witness: &[bool], m: usize, k_skip: usize, weights: &[F192]) -> Vec {
assert!(k_skip <= m);
let ell = 1usize << k_skip;
let n_rest = 1usize << (m - k_skip);
assert_eq!(witness.len(), 1usize << m);
assert_eq!(weights.len(), ell);
- let mut folded = vec![F128::ZERO; n_rest];
+ let mut folded = vec![F192::ZERO; n_rest];
for x_rest in 0..n_rest {
let base = x_rest * ell;
- let mut acc = F128::ZERO;
+ let mut acc = F192::ZERO;
for s in 0..ell {
if witness[base + s] {
acc += weights[s];
@@ -197,7 +197,7 @@ pub fn fold_at_z_naive(witness: &[bool], m: usize, k_skip: usize, weights: &[F12
/// multilinear sumcheck.
///
/// Inputs:
-/// - `a_mlv`, `b_mlv`: F128 vectors of length `2^n` for some `n ≥ 1`.
+/// - `a_mlv`, `b_mlv`: F192 vectors of length `2^n` for some `n ≥ 1`.
/// - `r`: full eq challenges, length `n`. `r[0]` is the challenge for the
/// variable being bound *this* round; `r[1..]` is for the remaining `n − 1`
/// variables.
@@ -209,7 +209,7 @@ pub fn fold_at_z_naive(witness: &[bool], m: usize, k_skip: usize, weights: &[F12
/// The `r[0]` prefactor matches the C++ `sumcheck_round_pair` convention: the
/// quantity sent on the wire is `Π(1) = eq(r[0], 1) · G(1) = r[0] · G(1)`,
/// where `Π(X) = eq(r[0], X) · G(X)` is the actual round polynomial.
-pub fn round_pair_naive(a_mlv: &[F128], b_mlv: &[F128], r: &[F128]) -> (F128, F128) {
+pub fn round_pair_naive(a_mlv: &[F192], b_mlv: &[F192], r: &[F192]) -> (F192, F192) {
let n = a_mlv.len();
assert_eq!(b_mlv.len(), n);
assert!(n.is_power_of_two() && n >= 2);
@@ -220,8 +220,8 @@ pub fn round_pair_naive(a_mlv: &[F128], b_mlv: &[F128], r: &[F128]) -> (F128, F1
let eq_remaining = build_eq(&r[1..]);
assert_eq!(eq_remaining.len(), half);
- let mut g_one = F128::ZERO;
- let mut g_inf = F128::ZERO;
+ let mut g_one = F192::ZERO;
+ let mut g_inf = F192::ZERO;
for x_prime in 0..half {
let a0 = a_mlv[2 * x_prime];
let a1 = a_mlv[2 * x_prime + 1];
@@ -257,15 +257,12 @@ pub fn uni_skip_fold_and_round_pair_naive(
b: &[bool],
m: usize,
k_skip: usize,
- z: F128,
- mlv_challenges: &[F128],
-) -> (Vec, Vec, F128, F128) {
+ z: F192,
+ mlv_challenges: &[F192],
+) -> (Vec, Vec, F192, F192) {
assert_eq!(a.len(), 1usize << m);
assert_eq!(b.len(), 1usize << m);
- assert!(
- m > k_skip,
- "need at least one multilinear variable past the skip"
- );
+ assert!(m > k_skip, "need at least one multilinear variable past the skip");
assert_eq!(mlv_challenges.len(), m - k_skip);
let weights = lagrange_weights_naive(k_skip, z);
@@ -281,7 +278,7 @@ pub fn uni_skip_fold_and_round_pair_naive(
/// Precomputed fold table for the univariate-skip fold at a fixed `z`.
///
-/// Storage: `n_chunks × 256` F128 entries (32 KB at `k_skip=6`). For each
+/// Storage: `n_chunks × 256` F192 entries (32 KB at `k_skip=6`). For each
/// byte-chunk `j ∈ 0..n_chunks` and byte value `v ∈ 0..256`:
///
/// `data[j * 256 + v] = Σ_{b : bit b of v set} weights[8j + b]`
@@ -294,17 +291,17 @@ pub fn uni_skip_fold_and_round_pair_naive(
#[derive(Clone, Debug)]
pub struct UniSkipFoldTable {
pub n_chunks: usize,
- pub data: Vec,
+ pub data: Vec,
}
impl UniSkipFoldTable {
- pub fn new(k_skip: usize, z: F128) -> Self {
+ pub fn new(k_skip: usize, z: F192) -> Self {
let ell = 1usize << k_skip;
assert_eq!(ell % 8, 0, "k_skip must be ≥ 3 (need ell divisible by 8)");
let n_chunks = ell / 8;
let weights = lagrange_weights_naive(k_skip, z);
- let mut data = vec![F128::ZERO; n_chunks * 256];
+ let mut data = vec![F192::ZERO; n_chunks * 256];
for j in 0..n_chunks {
let basis = &weights[8 * j..8 * j + 8];
// v = 0: zero (already initialized).
@@ -325,11 +322,11 @@ impl UniSkipFoldTable {
}
/// Scalar one-row fold: `Σ_j table[j][bytes[j]]`. Ports the NEON
- /// `uni_skip_fold_one_output_ghash` in scalar form.
+ /// the scalar univariate-skip fold.
#[inline]
- pub fn fold_one_row(&self, bytes: &[u8]) -> F128 {
+ pub fn fold_one_row(&self, bytes: &[u8]) -> F192 {
assert_eq!(bytes.len(), self.n_chunks);
- let mut acc = F128::ZERO;
+ let mut acc = F192::ZERO;
for j in 0..self.n_chunks {
acc += self.data[j * 256 + bytes[j] as usize];
}
@@ -337,54 +334,30 @@ impl UniSkipFoldTable {
}
}
-/// NEON one-row fold: 8 aligned 16-byte loads + 8 XORs, hand-unrolled for
-/// `n_chunks = 8` (the k_skip=6 protocol size). Returns the folded F128.
-///
-/// The table is `Vec` with each entry 16-byte aligned (F128 is
-/// `repr(C, align(16))`), so every `vld1q_u8` lands on an aligned address.
+/// NEON one-row fold, hand-unrolled for `n_chunks = 8` (the k_skip=6 protocol
+/// size). Each table entry is 24 bytes: NEON folds c0/c1 together while c2 is
+/// folded in a scalar register.
///
/// # Safety
-/// Caller must guarantee `table_data` points to ≥ 8 × 256 × 16 valid bytes
+/// Caller must guarantee `table_data` points to ≥ 8 × 256 valid F192 entries
/// (an `n_chunks ≥ 8` table) and `bytes_ptr` to ≥ 8 valid bytes.
#[cfg(target_arch = "aarch64")]
#[inline(always)]
-unsafe fn fold_one_row_neon_unchecked_8(table_data: *const u8, bytes_ptr: *const u8) -> F128 {
+unsafe fn fold_one_row_neon_unchecked_8(table_data: *const F192, bytes_ptr: *const u8) -> F192 {
use core::arch::aarch64::*;
unsafe {
- const STRIDE: usize = 256 * 16;
- let mut acc = vld1q_u8(table_data.add((*bytes_ptr) as usize * 16));
- acc = veorq_u8(
- acc,
- vld1q_u8(table_data.add(1 * STRIDE + (*bytes_ptr.add(1)) as usize * 16)),
- );
- acc = veorq_u8(
- acc,
- vld1q_u8(table_data.add(2 * STRIDE + (*bytes_ptr.add(2)) as usize * 16)),
- );
- acc = veorq_u8(
- acc,
- vld1q_u8(table_data.add(3 * STRIDE + (*bytes_ptr.add(3)) as usize * 16)),
- );
- acc = veorq_u8(
- acc,
- vld1q_u8(table_data.add(4 * STRIDE + (*bytes_ptr.add(4)) as usize * 16)),
- );
- acc = veorq_u8(
- acc,
- vld1q_u8(table_data.add(5 * STRIDE + (*bytes_ptr.add(5)) as usize * 16)),
- );
- acc = veorq_u8(
- acc,
- vld1q_u8(table_data.add(6 * STRIDE + (*bytes_ptr.add(6)) as usize * 16)),
- );
- acc = veorq_u8(
- acc,
- vld1q_u8(table_data.add(7 * STRIDE + (*bytes_ptr.add(7)) as usize * 16)),
- );
- let acc_u64 = vreinterpretq_u64_u8(acc);
- F128 {
- lo: vgetq_lane_u64::<0>(acc_u64),
- hi: vgetq_lane_u64::<1>(acc_u64),
+ let first = &*table_data.add((*bytes_ptr) as usize);
+ let mut acc = vld1q_u64(&first.c0);
+ let mut c2 = first.c2;
+ for chunk in 1..8 {
+ let entry = &*table_data.add(chunk * 256 + (*bytes_ptr.add(chunk)) as usize);
+ acc = veorq_u64(acc, vld1q_u64(&entry.c0));
+ c2 ^= entry.c2;
+ }
+ F192 {
+ c0: vgetq_lane_u64::<0>(acc),
+ c1: vgetq_lane_u64::<1>(acc),
+ c2,
}
}
}
@@ -394,7 +367,7 @@ unsafe fn fold_one_row_neon_unchecked_8(table_data: *const u8, bytes_ptr: *const
/// by default** via rayon — the outer x_hi loop is distributed across workers,
/// each writing to a disjoint chunk of `a_folded`/`b_folded` via `par_chunks_mut`
/// and accumulating its own `(sum1_contrib, sum_inf_contrib)`. The final
-/// reduce sums the per-worker contributions (commutative + associative F128
+/// reduce sums the per-worker contributions (commutative + associative F192
/// XOR/multiply).
///
/// Algorithm (per worker, one x_hi):
@@ -416,8 +389,8 @@ pub fn uni_skip_fold_and_round_pair_optimized_packed(
m: usize,
k_skip: usize,
table: &UniSkipFoldTable,
- mlv_challenges: &[F128],
-) -> (Vec, Vec, F128, F128) {
+ mlv_challenges: &[F192],
+) -> (Vec, Vec, F192, F192) {
uni_skip_fold_and_round_pair_optimized_packed_padded(
a_packed,
b_packed,
@@ -439,15 +412,12 @@ pub fn uni_skip_fold_and_round_pair_optimized_packed_padded(
m: usize,
k_skip: usize,
table: &UniSkipFoldTable,
- mlv_challenges: &[F128],
+ mlv_challenges: &[F192],
padding: &PaddingSpec,
-) -> (Vec, Vec, F128, F128) {
+) -> (Vec, Vec, F192, F192) {
use rayon::prelude::*;
- assert_eq!(
- k_skip, 6,
- "optimized fold-and-round_pair variant is k_skip=6 only"
- );
+ assert_eq!(k_skip, 6, "optimized fold-and-round_pair variant is k_skip=6 only");
assert_eq!(table.n_chunks, 8);
let n_chunks = table.n_chunks;
let n_out = 1usize << (m - k_skip);
@@ -455,14 +425,12 @@ pub fn uni_skip_fold_and_round_pair_optimized_packed_padded(
assert_eq!(b_packed.len(), n_out * n_chunks);
assert_eq!(mlv_challenges.len(), m - k_skip);
- // Uninit alloc — the parallel loop below writes every slot (dense path)
- // or explicitly writes F128::ZERO at padding holes (padded path).
- // Saves ~22 ms of sequential zero-fill at m=29 (256 MB total) that would
- // otherwise cap the parallel speedup of this phase at ~2.5× on 8 cores.
- let mut a_folded: Vec = primitives::scratch::take_f128(n_out);
- let mut b_folded: Vec = primitives::scratch::take_f128(n_out);
+ // The parallel loop overwrites every pooled slot (including padding holes),
+ // avoiding a separate sequential clear of 256 MB at m=29.
+ let mut a_folded: Vec = primitives::scratch::take_f192(n_out);
+ let mut b_folded: Vec = primitives::scratch::take_f192(n_out);
- let eq = SplitEqGhash::new(&mlv_challenges[1..]);
+ let eq = SplitEq::new(&mlv_challenges[1..]);
let lo_size = 1usize << eq.n_lo;
let hi_size = 1usize << eq.n_hi;
assert_eq!(lo_size * hi_size * 2, n_out);
@@ -473,19 +441,19 @@ pub fn uni_skip_fold_and_round_pair_optimized_packed_padded(
let (pair_in_block_mask, useful_pairs_inclusive) = round2_pair_skip(padding, k_skip);
// Parallel: each worker writes one disjoint chunk of a_folded/b_folded
- // and returns its (sum1, sum_inf) contribution. Reduce by F128 XOR.
+ // and returns its (sum1, sum_inf) contribution. Reduce by F192 XOR.
let (sum1, sum_inf) = a_folded
.par_chunks_mut(chunk_size)
.zip(b_folded.par_chunks_mut(chunk_size))
.enumerate()
.map(|(x_hi, (a_chunk, b_chunk))| {
- let mut p1_acc = F256Unreduced::ZERO;
- let mut pinf_acc = F256Unreduced::ZERO;
+ let mut p1_acc = F192Unreduced::ZERO;
+ let mut pinf_acc = F192Unreduced::ZERO;
let pair_idx_base = x_hi * lo_size;
#[cfg(target_arch = "aarch64")]
unsafe {
- let table_ptr = table.data.as_ptr() as *const u8;
+ let table_ptr = table.data.as_ptr();
let a_pkt_ptr = a_packed.as_ptr();
let b_pkt_ptr = b_packed.as_ptr();
let base = x_hi * chunk_size;
@@ -496,10 +464,10 @@ pub fn uni_skip_fold_and_round_pair_optimized_packed_padded(
if ((pair_idx_base + x_lo) & pair_in_block_mask) >= useful_pairs_inclusive {
// Padding hole: write zero (a_folded/b_folded were alloc'd
// uninit, so we have to write every slot we don't fold into).
- a_chunk[x0l] = F128::ZERO;
- a_chunk[x1l] = F128::ZERO;
- b_chunk[x0l] = F128::ZERO;
- b_chunk[x1l] = F128::ZERO;
+ a_chunk[x0l] = F192::ZERO;
+ a_chunk[x1l] = F192::ZERO;
+ b_chunk[x0l] = F192::ZERO;
+ b_chunk[x1l] = F192::ZERO;
continue;
}
let x0g = base + 2 * x_lo;
@@ -530,10 +498,10 @@ pub fn uni_skip_fold_and_round_pair_optimized_packed_padded(
let x1l = x0l + 1;
if ((pair_idx_base + x_lo) & pair_in_block_mask) >= useful_pairs_inclusive {
// See aarch64 branch above for why this zero write is needed.
- a_chunk[x0l] = F128::ZERO;
- a_chunk[x1l] = F128::ZERO;
- b_chunk[x0l] = F128::ZERO;
- b_chunk[x1l] = F128::ZERO;
+ a_chunk[x0l] = F192::ZERO;
+ a_chunk[x1l] = F192::ZERO;
+ b_chunk[x0l] = F192::ZERO;
+ b_chunk[x1l] = F192::ZERO;
continue;
}
let x0g = base + 2 * x_lo;
@@ -560,7 +528,7 @@ pub fn uni_skip_fold_and_round_pair_optimized_packed_padded(
(eq_h * p1, eq_h * pinf)
})
.reduce(
- || (F128::ZERO, F128::ZERO),
+ || (F192::ZERO, F192::ZERO),
|(s1, sinf), (c1, cinf)| (s1 + c1, sinf + cinf),
);
@@ -574,7 +542,7 @@ pub fn uni_skip_fold_and_round_pair_optimized_packed_padded(
/// In-place fold of a single multilinear polynomial table at `challenge`.
/// Pairs `(a[2x], a[2x+1])` collapse to `a[x] = a[2x] + challenge · (a[2x+1] + a[2x])`.
/// After the call, `a.len()` is halved.
-pub fn fold_in_place_single(a: &mut Vec, challenge: F128) {
+pub fn fold_in_place_single(a: &mut Vec, challenge: F192) {
let n = a.len();
assert!(n.is_power_of_two() && n >= 2);
let half = n / 2;
@@ -593,7 +561,7 @@ pub fn fold_in_place_single(a: &mut Vec, challenge: F128) {
///
/// Used at the tail of the multilinear-round sequence where the polynomial is
/// small enough that parallel/fusion overhead outweighs benefit.
-pub fn fold_in_place_pair(a: &mut Vec, b: &mut Vec, challenge: F128) {
+pub fn fold_in_place_pair(a: &mut Vec, b: &mut Vec, challenge: F192) {
let n = a.len();
assert_eq!(b.len(), n);
assert!(n.is_power_of_two() && n >= 2);
@@ -621,19 +589,40 @@ pub fn fold_in_place_pair(a: &mut Vec, b: &mut Vec, challenge: F128)
/// one bit of x_lo (lo_size ≥ 2). Smaller polynomials should use the
/// unfused `fold_in_place_pair + round_pair_naive` pair.
pub fn fold_and_compute_round_pair_optimized(
- a: &[F128],
- b: &[F128],
- r_fold: F128,
- r_next: &[F128],
-) -> (Vec, Vec, F128, F128) {
+ a: &[F192],
+ b: &[F192],
+ r_fold: F192,
+ r_next: &[F192],
+) -> (Vec, Vec, F192, F192) {
let half = a.len() / 2;
- // Uninit alloc — `_into` writes every slot of a_new/b_new.
- let mut a_new = primitives::alloc_uninit_vec::(half);
- let mut b_new = primitives::alloc_uninit_vec::(half);
- let (m1, mi) = fold_and_compute_round_pair_into(a, b, &mut a_new, &mut b_new, r_fold, r_next);
+ let mut a_new = primitives::alloc_uninit(half);
+ let mut b_new = primitives::alloc_uninit(half);
+ let (m1, mi) = fold_and_compute_round_pair_into_slots(a, b, &mut a_new, &mut b_new, r_fold, r_next);
+ // SAFETY: the fold writes every slot of both output vectors exactly once.
+ let a_new = unsafe { primitives::assume_init(a_new) };
+ // SAFETY: the fold writes every slot of both output vectors exactly once.
+ let b_new = unsafe { primitives::assume_init(b_new) };
(a_new, b_new, m1, mi)
}
+trait OutputSlot {
+ fn put(&mut self, value: F192);
+}
+
+impl OutputSlot for F192 {
+ #[inline(always)]
+ fn put(&mut self, value: F192) {
+ *self = value;
+ }
+}
+
+impl OutputSlot for std::mem::MaybeUninit {
+ #[inline(always)]
+ fn put(&mut self, value: F192) {
+ self.write(value);
+ }
+}
+
/// Buffer-reusing variant of [`fold_and_compute_round_pair_optimized`]: writes
/// the folded `a`/`b` into the caller-provided `a_out`/`b_out` (each length
/// `a.len() / 2`) instead of allocating. Returns `(r_next[0] · G(1), G(∞))`.
@@ -643,13 +632,24 @@ pub fn fold_and_compute_round_pair_optimized(
/// than per round. The per-round `munmap` of the old buffer (64 MB at m=29)
/// runs single-threaded and otherwise caps the tail's parallel speedup.
pub fn fold_and_compute_round_pair_into(
- a: &[F128],
- b: &[F128],
- a_out: &mut [F128],
- b_out: &mut [F128],
- r_fold: F128,
- r_next: &[F128],
-) -> (F128, F128) {
+ a: &[F192],
+ b: &[F192],
+ a_out: &mut [F192],
+ b_out: &mut [F192],
+ r_fold: F192,
+ r_next: &[F192],
+) -> (F192, F192) {
+ fold_and_compute_round_pair_into_slots(a, b, a_out, b_out, r_fold, r_next)
+}
+
+fn fold_and_compute_round_pair_into_slots(
+ a: &[F192],
+ b: &[F192],
+ a_out: &mut [O],
+ b_out: &mut [O],
+ r_fold: F192,
+ r_next: &[F192],
+) -> (F192, F192) {
use rayon::prelude::*;
let n = a.len();
@@ -661,7 +661,7 @@ pub fn fold_and_compute_round_pair_into(
let log_n = n.trailing_zeros() as usize;
assert_eq!(r_next.len(), log_n - 1);
- let eq = SplitEqGhash::new(&r_next[1..]);
+ let eq = SplitEq::new(&r_next[1..]);
let lo_size = 1usize << eq.n_lo;
let hi_size = 1usize << eq.n_hi;
assert!(lo_size >= 2, "fold_and_compute requires lo_size ≥ 2");
@@ -681,8 +681,8 @@ pub fn fold_and_compute_round_pair_into(
let a_in = &a[x_hi * chunk_in..(x_hi + 1) * chunk_in];
let b_in = &b[x_hi * chunk_in..(x_hi + 1) * chunk_in];
- let mut p1_acc = F256Unreduced::ZERO;
- let mut pinf_acc = F256Unreduced::ZERO;
+ let mut p1_acc = F192Unreduced::ZERO;
+ let mut pinf_acc = F192Unreduced::ZERO;
// Unroll 4 x_lo's per iteration when lo_size % 4 == 0 (the common
// case for the fused path; falls back to 2-wide for lo_size==2 at
@@ -757,22 +757,22 @@ pub fn fold_and_compute_round_pair_into(
let oi_b = 2 * x_lo_b;
let oi_c = 2 * x_lo_c;
let oi_d = 2 * x_lo_d;
- a_out[oi_a] = a0_a;
- a_out[oi_a + 1] = a1_a;
- b_out[oi_a] = b0_a;
- b_out[oi_a + 1] = b1_a;
- a_out[oi_b] = a0_b;
- a_out[oi_b + 1] = a1_b;
- b_out[oi_b] = b0_b;
- b_out[oi_b + 1] = b1_b;
- a_out[oi_c] = a0_c;
- a_out[oi_c + 1] = a1_c;
- b_out[oi_c] = b0_c;
- b_out[oi_c + 1] = b1_c;
- a_out[oi_d] = a0_d;
- a_out[oi_d + 1] = a1_d;
- b_out[oi_d] = b0_d;
- b_out[oi_d + 1] = b1_d;
+ a_out[oi_a].put(a0_a);
+ a_out[oi_a + 1].put(a1_a);
+ b_out[oi_a].put(b0_a);
+ b_out[oi_a + 1].put(b1_a);
+ a_out[oi_b].put(a0_b);
+ a_out[oi_b + 1].put(a1_b);
+ b_out[oi_b].put(b0_b);
+ b_out[oi_b + 1].put(b1_b);
+ a_out[oi_c].put(a0_c);
+ a_out[oi_c + 1].put(a1_c);
+ b_out[oi_c].put(b0_c);
+ b_out[oi_c + 1].put(b1_c);
+ a_out[oi_d].put(a0_d);
+ a_out[oi_d + 1].put(a1_d);
+ b_out[oi_d].put(b0_d);
+ b_out[oi_d + 1].put(b1_d);
// 8 independent msg muls.
let eq_l_a = eq_lo[x_lo_a];
@@ -835,14 +835,14 @@ pub fn fold_and_compute_round_pair_into(
let oi_a = 2 * x_lo_a;
let oi_b = 2 * x_lo_b;
- a_out[oi_a] = a0_a;
- a_out[oi_a + 1] = a1_a;
- b_out[oi_a] = b0_a;
- b_out[oi_a + 1] = b1_a;
- a_out[oi_b] = a0_b;
- a_out[oi_b + 1] = a1_b;
- b_out[oi_b] = b0_b;
- b_out[oi_b + 1] = b1_b;
+ a_out[oi_a].put(a0_a);
+ a_out[oi_a + 1].put(a1_a);
+ b_out[oi_a].put(b0_a);
+ b_out[oi_a + 1].put(b1_a);
+ a_out[oi_b].put(a0_b);
+ a_out[oi_b + 1].put(a1_b);
+ b_out[oi_b].put(b0_b);
+ b_out[oi_b + 1].put(b1_b);
let eq_l_a = eq_lo[x_lo_a];
let eq_l_b = eq_lo[x_lo_b];
@@ -864,7 +864,7 @@ pub fn fold_and_compute_round_pair_into(
(eq_h * p1, eq_h * pinf)
})
.reduce(
- || (F128::ZERO, F128::ZERO),
+ || (F192::ZERO, F192::ZERO),
|(s1, sinf), (c1, cinf)| (s1 + c1, sinf + cinf),
);
@@ -881,22 +881,22 @@ fn uni_skip_fold_and_round_pair_optimized_packed_serial(
m: usize,
k_skip: usize,
table: &UniSkipFoldTable,
- mlv_challenges: &[F128],
-) -> (Vec, Vec, F128, F128) {
+ mlv_challenges: &[F192],
+) -> (Vec, Vec, F192, F192) {
assert_eq!(k_skip, 6);
assert_eq!(table.n_chunks, 8);
let n_chunks = table.n_chunks;
let n_out = 1usize << (m - k_skip);
- let mut a_folded = vec![F128::ZERO; n_out];
- let mut b_folded = vec![F128::ZERO; n_out];
- let eq = SplitEqGhash::new(&mlv_challenges[1..]);
+ let mut a_folded = vec![F192::ZERO; n_out];
+ let mut b_folded = vec![F192::ZERO; n_out];
+ let eq = SplitEq::new(&mlv_challenges[1..]);
let lo_size = 1usize << eq.n_lo;
let hi_size = 1usize << eq.n_hi;
- let mut sum1 = F128::ZERO;
- let mut sum_inf = F128::ZERO;
+ let mut sum1 = F192::ZERO;
+ let mut sum_inf = F192::ZERO;
for x_hi in 0..hi_size {
- let mut p1_acc = F256Unreduced::ZERO;
- let mut pinf_acc = F256Unreduced::ZERO;
+ let mut p1_acc = F192Unreduced::ZERO;
+ let mut pinf_acc = F192Unreduced::ZERO;
let k_base = x_hi << eq.n_lo;
for x_lo in 0..lo_size {
let k = k_base | x_lo;
@@ -932,22 +932,15 @@ pub fn uni_skip_fold_and_round_pair_optimized(
b: &[bool],
m: usize,
k_skip: usize,
- z: F128,
- mlv_challenges: &[F128],
-) -> (Vec, Vec, F128, F128) {
+ z: F192,
+ mlv_challenges: &[F192],
+) -> (Vec, Vec, F192, F192) {
assert_eq!(a.len(), 1usize << m);
assert_eq!(b.len(), 1usize << m);
let a_packed = pack_bits(a);
let b_packed = pack_bits(b);
let table = UniSkipFoldTable::new(k_skip, z);
- uni_skip_fold_and_round_pair_optimized_packed(
- &a_packed,
- &b_packed,
- m,
- k_skip,
- &table,
- mlv_challenges,
- )
+ uni_skip_fold_and_round_pair_optimized_packed(&a_packed, &b_packed, m, k_skip, &table, mlv_challenges)
}
// ---------------------------------------------------------------------------
@@ -970,10 +963,10 @@ mod tests {
let mut rng = Rng::new(1);
for &k_skip in &[1usize, 2, 3, 4, 5, 6] {
for _ in 0..4 {
- let z = rng.f128();
+ let z = rng.ext();
let weights = lagrange_weights_naive(k_skip, z);
- let sum: F128 = weights.iter().copied().fold(F128::ZERO, |a, b| a + b);
- assert_eq!(sum, F128::ONE, "Σ L_i ≠ 1 at k_skip={k_skip}");
+ let sum: F192 = weights.iter().copied().fold(F192::ZERO, |a, b| a + b);
+ assert_eq!(sum, F192::ONE, "Σ L_i ≠ 1 at k_skip={k_skip}");
}
}
}
@@ -987,7 +980,7 @@ mod tests {
let z = PHI_8_TABLE[i];
let weights = lagrange_weights_naive(k_skip, z);
for j in 0..ell {
- let expected = if j == i { F128::ONE } else { F128::ZERO };
+ let expected = if j == i { F192::ONE } else { F192::ZERO };
assert_eq!(weights[j], expected, "k_skip={k_skip}, z=node{i}, j={j}");
}
}
@@ -999,7 +992,7 @@ mod tests {
// ----------------------------------------------------------------------
/// At a node `z = φ_8(i)`, fold reduces to the witness restricted to s=i:
- /// `a_mlv[x_rest] = a[x_rest · 2^k_skip + i]` (lifted to F_128).
+ /// `a_mlv[x_rest] = a[x_rest · 2^k_skip + i]` (lifted to F192).
#[test]
fn fold_at_node_recovers_witness_slice() {
let m = 8;
@@ -1013,15 +1006,8 @@ mod tests {
let weights = lagrange_weights_naive(k_skip, z);
let a_mlv = fold_at_z_naive(&a, m, k_skip, &weights);
for x_rest in 0..n_rest {
- let expected = if a[x_rest * ell + i] {
- F128::ONE
- } else {
- F128::ZERO
- };
- assert_eq!(
- a_mlv[x_rest], expected,
- "fold at node {i} mismatch at x_rest={x_rest}"
- );
+ let expected = if a[x_rest * ell + i] { F192::ONE } else { F192::ZERO };
+ assert_eq!(a_mlv[x_rest], expected, "fold at node {i} mismatch at x_rest={x_rest}");
}
}
}
@@ -1036,7 +1022,7 @@ mod tests {
let a = rng.bits(1 << m);
let aprime = rng.bits(1 << m);
let a_xor: Vec = a.iter().zip(&aprime).map(|(x, y)| x ^ y).collect();
- let z = rng.f128();
+ let z = rng.ext();
let weights = lagrange_weights_naive(k_skip, z);
let fa = fold_at_z_naive(&a, m, k_skip, &weights);
@@ -1058,15 +1044,15 @@ mod tests {
let m = 6;
let k_skip = 3;
let mut rng = Rng::new(20);
- let z = rng.f128();
- let mlv_challenges = rng.f128_vec(m - k_skip);
+ let z = rng.ext();
+ let mlv_challenges = rng.ext_vec(m - k_skip);
let zeros = vec![false; 1 << m];
let (a_mlv, b_mlv, msg_1, msg_inf) =
uni_skip_fold_and_round_pair_naive(&zeros, &zeros, m, k_skip, z, &mlv_challenges);
assert!(a_mlv.iter().all(|v| v.is_zero()));
assert!(b_mlv.iter().all(|v| v.is_zero()));
- assert_eq!(msg_1, F128::ZERO);
- assert_eq!(msg_inf, F128::ZERO);
+ assert_eq!(msg_1, F192::ZERO);
+ assert_eq!(msg_inf, F192::ZERO);
}
#[test]
@@ -1076,8 +1062,8 @@ mod tests {
let mut rng = Rng::new(33);
let a = rng.bits(1 << m);
let b = rng.bits(1 << m);
- let z = rng.f128();
- let mlv_challenges = rng.f128_vec(m - k_skip);
+ let z = rng.ext();
+ let mlv_challenges = rng.ext_vec(m - k_skip);
let o1 = uni_skip_fold_and_round_pair_naive(&a, &b, m, k_skip, z, &mlv_challenges);
let o2 = uni_skip_fold_and_round_pair_naive(&a, &b, m, k_skip, z, &mlv_challenges);
assert_eq!(o1, o2);
@@ -1093,12 +1079,10 @@ mod tests {
let mut rng = Rng::new(40);
let a = rng.bits(1 << m);
let b = rng.bits(1 << m);
- let z = rng.f128();
- let mlv_challenges = rng.f128_vec(m - k_skip);
- let (_, _, m1_ab, minf_ab) =
- uni_skip_fold_and_round_pair_naive(&a, &b, m, k_skip, z, &mlv_challenges);
- let (_, _, m1_ba, minf_ba) =
- uni_skip_fold_and_round_pair_naive(&b, &a, m, k_skip, z, &mlv_challenges);
+ let z = rng.ext();
+ let mlv_challenges = rng.ext_vec(m - k_skip);
+ let (_, _, m1_ab, minf_ab) = uni_skip_fold_and_round_pair_naive(&a, &b, m, k_skip, z, &mlv_challenges);
+ let (_, _, m1_ba, minf_ba) = uni_skip_fold_and_round_pair_naive(&b, &a, m, k_skip, z, &mlv_challenges);
assert_eq!(m1_ab, m1_ba);
assert_eq!(minf_ab, minf_ba);
}
@@ -1113,7 +1097,7 @@ mod tests {
fn fold_one_row_neon_matches_scalar() {
let k_skip = 6;
let mut rng = Rng::new(70);
- let z = rng.f128();
+ let z = rng.ext();
let table = UniSkipFoldTable::new(k_skip, z);
for _ in 0..256 {
@@ -1123,9 +1107,7 @@ mod tests {
}
let scalar = table.fold_one_row(&bytes);
// SAFETY: on aarch64; bytes has 8 entries; table has 8 chunks.
- let neon = unsafe {
- fold_one_row_neon_unchecked_8(table.data.as_ptr() as *const u8, bytes.as_ptr())
- };
+ let neon = unsafe { fold_one_row_neon_unchecked_8(table.data.as_ptr(), bytes.as_ptr()) };
assert_eq!(scalar, neon, "fold mismatch bytes={bytes:02x?}");
}
}
@@ -1136,9 +1118,9 @@ mod tests {
let mut rng = Rng::new(300);
for &log_n in &[1usize, 2, 3, 4, 6] {
let n = 1usize << log_n;
- let a_orig: Vec = (0..n).map(|_| rng.f128()).collect();
- let b_orig: Vec = (0..n).map(|_| rng.f128()).collect();
- let challenge = rng.f128();
+ let a_orig: Vec = (0..n).map(|_| rng.ext()).collect();
+ let b_orig: Vec = (0..n).map(|_| rng.ext()).collect();
+ let challenge = rng.ext();
let mut a = a_orig.clone();
let mut b = b_orig.clone();
@@ -1163,12 +1145,11 @@ mod tests {
/// prover skip per-round c tracking entirely.
#[test]
fn c_eval_from_round1_c_matches_direct_fold() {
- use primitives::field::F8;
- use pcs::ntt::{AdditiveNttGf8, InvNttTableByteSingleGf8};
use crate::zerocheck::univariate_skip_optimized::{
- c_s_f128, medium_challenges_ghash, round1_shift_reduce_extract_c_packed,
- small_challenges_ghash,
+ c_s, medium_challenges, round1_shift_reduce_extract_c_packed, small_challenges,
};
+ use pcs::ntt::{AdditiveNttGf8, InvNttTableByteSingleGf8};
+ use primitives::field::F8;
const K_SKIP: usize = 6;
const N_INNER: usize = 7;
@@ -1181,20 +1162,20 @@ mod tests {
// Build r with protocol-fixed constants in the middle 7 dims,
// matching how `prove` constructs it.
- let mut r = vec![F128::ZERO; m];
+ let mut r = vec![F192::ZERO; m];
for slot in r[..K_SKIP].iter_mut() {
- *slot = rng.f128();
+ *slot = rng.ext();
}
- for (i, v) in small_challenges_ghash().iter().enumerate() {
+ for (i, v) in small_challenges().iter().enumerate() {
r[K_SKIP + i] = *v;
}
- for (i, v) in medium_challenges_ghash().iter().enumerate() {
+ for (i, v) in medium_challenges().iter().enumerate() {
r[K_SKIP + 3 + i] = *v;
}
for slot in r[K_SKIP + N_INNER..].iter_mut() {
- *slot = rng.f128();
+ *slot = rng.ext();
}
- let z = rng.f128();
+ let z = rng.ext();
let a_packed = pack_bits(&a);
let b_packed = pack_bits(&b);
@@ -1203,13 +1184,11 @@ mod tests {
let ntt_s = AdditiveNttGf8::new(K_SKIP, F8::ZERO);
let ntt_l = AdditiveNttGf8::new(K_SKIP, F8(1u8 << K_SKIP));
let inv_table = InvNttTableByteSingleGf8::new(&ntt_s, &ntt_l);
- let (_round1_ab, round1_c) = round1_shift_reduce_extract_c_packed(
- &a_packed, &b_packed, &c_packed, m, K_SKIP, &r, &inv_table,
- );
+ let (_round1_ab, round1_c) =
+ round1_shift_reduce_extract_c_packed(&a_packed, &b_packed, &c_packed, m, K_SKIP, &r, &inv_table);
// Path A: interpolate round1_c at z, scale by C_s.
- let c_eval_via_interpolation =
- c_s_f128() * interpolate_at_z_on_lambda(&round1_c, K_SKIP, z);
+ let c_eval_via_interpolation = c_s() * interpolate_at_z_on_lambda(&round1_c, K_SKIP, z);
// Path B: direct fold of c at z (Lagrange) then bind each
// r_rest = r[K_SKIP..m] element with fold_in_place_single.
@@ -1234,15 +1213,15 @@ mod tests {
#[test]
fn fused_round_matches_unfused() {
let mut rng = Rng::new(310);
- // fold_and_compute requires lo_size ≥ 2 in SplitEqGhash. eq is over
+ // fold_and_compute requires lo_size ≥ 2 in SplitEq. eq is over
// r_next[1..] (size log_n − 2); with MAX_N_HI = 7, n_lo ≥ 1 needs
// eq size ≥ 8 ⇒ log_n ≥ 10. Smaller cases use the unfused path.
for &log_n in &[10usize, 11, 12] {
let n = 1usize << log_n;
- let a: Vec = (0..n).map(|_| rng.f128()).collect();
- let b: Vec = (0..n).map(|_| rng.f128()).collect();
- let r_fold = rng.f128();
- let r_next = rng.f128_vec(log_n - 1);
+ let a: Vec = (0..n).map(|_| rng.ext()).collect();
+ let b: Vec = (0..n).map(|_| rng.ext()).collect();
+ let r_fold = rng.ext();
+ let r_next = rng.ext_vec(log_n - 1);
// Fused path.
let (a_fused, b_fused, m1_fused, minf_fused) =
@@ -1262,7 +1241,7 @@ mod tests {
}
/// Parallel `uni_skip_fold_and_round_pair_optimized_packed` produces
- /// byte-identical output to the serial version. F128 XOR + multiply sum
+ /// byte-identical output to the serial version. F192 XOR + multiply sum
/// is commutative + associative, so worker scheduling order doesn't
/// affect the result.
#[test]
@@ -1275,20 +1254,14 @@ mod tests {
let mut rng = Rng::new(200 + m as u64);
let a = rng.bits(1 << m);
let b = rng.bits(1 << m);
- let z = rng.f128();
- let mlv_challenges = rng.f128_vec(m - k_skip);
+ let z = rng.ext();
+ let mlv_challenges = rng.ext_vec(m - k_skip);
let a_packed = pack_bits(&a);
let b_packed = pack_bits(&b);
let table = UniSkipFoldTable::new(k_skip, z);
- let par = uni_skip_fold_and_round_pair_optimized_packed(
- &a_packed,
- &b_packed,
- m,
- k_skip,
- &table,
- &mlv_challenges,
- );
+ let par =
+ uni_skip_fold_and_round_pair_optimized_packed(&a_packed, &b_packed, m, k_skip, &table, &mlv_challenges);
let ser = uni_skip_fold_and_round_pair_optimized_packed_serial(
&a_packed,
&b_packed,
@@ -1315,8 +1288,7 @@ mod tests {
#[test]
fn uni_skip_fold_round_pair_padded_matches_dense() {
const K_SKIP: usize = 6;
- let cases: &[(usize, usize, usize)] =
- &[(17, 14, 15_409), (18, 15, 31_401), (19, 16, 42_560)];
+ let cases: &[(usize, usize, usize)] = &[(17, 14, 15_409), (18, 15, 31_401), (19, 16, 42_560)];
for &(m, k_log, useful_bits) in cases {
let mut rng = Rng::new(0xFADE_F00D_u64.wrapping_add((k_log * 31 + m) as u64));
let total_bits = 1usize << m;
@@ -1336,22 +1308,16 @@ mod tests {
let a_packed = pack_bits(&a);
let b_packed = pack_bits(&b);
- let z = rng.f128();
- let mlv_challenges = rng.f128_vec(m - K_SKIP);
+ let z = rng.ext();
+ let mlv_challenges = rng.ext_vec(m - K_SKIP);
let table = UniSkipFoldTable::new(K_SKIP, z);
let padding = PaddingSpec {
k_log,
useful_bits_per_block: useful_bits,
};
- let dense = uni_skip_fold_and_round_pair_optimized_packed(
- &a_packed,
- &b_packed,
- m,
- K_SKIP,
- &table,
- &mlv_challenges,
- );
+ let dense =
+ uni_skip_fold_and_round_pair_optimized_packed(&a_packed, &b_packed, m, K_SKIP, &table, &mlv_challenges);
let padded = uni_skip_fold_and_round_pair_optimized_packed_padded(
&a_packed,
&b_packed,
@@ -1361,22 +1327,10 @@ mod tests {
&mlv_challenges,
&padding,
);
- assert_eq!(
- dense.0, padded.0,
- "a_mlv: m={m}, k_log={k_log}, useful={useful_bits}"
- );
- assert_eq!(
- dense.1, padded.1,
- "b_mlv: m={m}, k_log={k_log}, useful={useful_bits}"
- );
- assert_eq!(
- dense.2, padded.2,
- "msg_1: m={m}, k_log={k_log}, useful={useful_bits}"
- );
- assert_eq!(
- dense.3, padded.3,
- "msg_inf: m={m}, k_log={k_log}, useful={useful_bits}"
- );
+ assert_eq!(dense.0, padded.0, "a_mlv: m={m}, k_log={k_log}, useful={useful_bits}");
+ assert_eq!(dense.1, padded.1, "b_mlv: m={m}, k_log={k_log}, useful={useful_bits}");
+ assert_eq!(dense.2, padded.2, "msg_1: m={m}, k_log={k_log}, useful={useful_bits}");
+ assert_eq!(dense.3, padded.3, "msg_inf: m={m}, k_log={k_log}, useful={useful_bits}");
}
}
@@ -1386,7 +1340,7 @@ mod tests {
let m = 8;
let k_skip = 3;
let mut rng = Rng::new(60);
- let z = rng.f128();
+ let z = rng.ext();
let a = rng.bits(1 << m);
let weights = lagrange_weights_naive(k_skip, z);
let table = UniSkipFoldTable::new(k_skip, z);
@@ -1398,7 +1352,7 @@ mod tests {
for x_rest in 0..(1usize << (m - k_skip)) {
let direct = {
- let mut acc = F128::ZERO;
+ let mut acc = F192::ZERO;
for s in 0..(1usize << k_skip) {
if a[x_rest * (1usize << k_skip) + s] {
acc += weights[s];
@@ -1406,8 +1360,7 @@ mod tests {
}
acc
};
- let via_table =
- table.fold_one_row(&a_packed[x_rest * n_chunks..(x_rest + 1) * n_chunks]);
+ let via_table = table.fold_one_row(&a_packed[x_rest * n_chunks..(x_rest + 1) * n_chunks]);
assert_eq!(via_table, direct, "x_rest={x_rest}");
}
}
@@ -1426,11 +1379,10 @@ mod tests {
let mut rng = Rng::new(100 + m as u64);
let a = rng.bits(1 << m);
let b = rng.bits(1 << m);
- let z = rng.f128();
- let mlv_challenges = rng.f128_vec(m - k_skip);
+ let z = rng.ext();
+ let mlv_challenges = rng.ext_vec(m - k_skip);
- let (a_n, b_n, m1_n, minf_n) =
- uni_skip_fold_and_round_pair_naive(&a, &b, m, k_skip, z, &mlv_challenges);
+ let (a_n, b_n, m1_n, minf_n) = uni_skip_fold_and_round_pair_naive(&a, &b, m, k_skip, z, &mlv_challenges);
let (a_o, b_o, m1_o, minf_o) =
uni_skip_fold_and_round_pair_optimized(&a, &b, m, k_skip, z, &mlv_challenges);
@@ -1455,8 +1407,8 @@ mod tests {
let mut rng = Rng::new(55);
let a = rng.bits(1 << m);
let b = rng.bits(1 << m);
- let z = rng.f128();
- let r = rng.f128_vec(m - k_skip);
+ let z = rng.ext();
+ let r = rng.ext_vec(m - k_skip);
let weights = lagrange_weights_naive(k_skip, z);
let a_mlv = fold_at_z_naive(&a, m, k_skip, &weights);
@@ -1467,9 +1419,9 @@ mod tests {
let eq_remaining = build_eq(&r[1..]);
// G(0), G(1), G(∞) by direct definition.
- let mut g0 = F128::ZERO;
- let mut g1 = F128::ZERO;
- let mut g_inf = F128::ZERO;
+ let mut g0 = F192::ZERO;
+ let mut g1 = F192::ZERO;
+ let mut g_inf = F192::ZERO;
for x_prime in 0..half {
let a0 = a_mlv[2 * x_prime];
let a1 = a_mlv[2 * x_prime + 1];
@@ -1489,9 +1441,9 @@ mod tests {
// Degree-2 check: G(X) reconstructed through (G(0), G(1), G(∞)) must
// agree with the direct multilinear evaluation at a fresh point X.
// Char-2 interpolation: G(X) = G(0) + X·(G(0)+G(1)) + X·(X+1)·G(∞).
- let x = rng.f128();
- let g_via_poly = g0 + x * (g0 + g1) + x * (x + F128::ONE) * g_inf;
- let mut g_via_sum = F128::ZERO;
+ let x = rng.ext();
+ let g_via_poly = g0 + x * (g0 + g1) + x * (x + F192::ONE) * g_inf;
+ let mut g_via_sum = F192::ZERO;
for x_prime in 0..half {
let a0 = a_mlv[2 * x_prime];
let a1 = a_mlv[2 * x_prime + 1];
diff --git a/crates/flock/src/zerocheck/univariate_skip.rs b/crates/flock/src/zerocheck/univariate_skip.rs
index 25b60183..8d4784eb 100644
--- a/crates/flock/src/zerocheck/univariate_skip.rs
+++ b/crates/flock/src/zerocheck/univariate_skip.rs
@@ -1,8 +1,8 @@
-// Credit: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
+// CREDIT: https://github.com/succinctlabs/flock (flock-core), MIT OR Apache-2.0.
//! Round-1 prover message (univariate skip).
//!
//! The round-1 message is `(P^{AB}, P^C)`, each a length-`2^k_skip` vector
-//! of F128 values. They are evaluations on the NTT domain `Λ` of the
+//! of F192 values. They are evaluations on the NTT domain `Λ` of the
//! polynomial (over λ) defined by
//!
//! P^{AB}(λ) = Σ_{x ∈ {0,1}^{m-k_skip}} eq(r_rest, x) · φ₈(â(λ, x) · b̂(λ, x))
@@ -19,14 +19,14 @@
//! [`super::univariate_skip_optimized`] drops a constant F₈ factor
//! `C_s = φ₈(0x1C)` from the eq-on-S weights; this one keeps it.
-use primitives::field::{F8, F128, mul_by_x, phi8};
use pcs::ntt::{AdditiveNttGf8, InvNttTableByteSingleGf8};
+use primitives::field::{F8, F192, phi8_192 as phi8};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
-pub use primitives::multilinear::build_eq;
+pub use primitives::multilinear::eq_table as build_eq;
// ---------------------------------------------------------------------------
// Naive round-1 prover message (extract_c form)
@@ -36,7 +36,7 @@ pub use primitives::multilinear::build_eq;
/// inner, no deferred reduction — direct algorithmic translation of the
/// protocol formula).
///
-/// Returns `(p_ab, p_c)`, each a length-`2^k_skip` F128 vector of evaluations
+/// Returns `(p_ab, p_c)`, each a length-`2^k_skip` F192 vector of evaluations
/// on Λ.
///
/// Preconditions:
@@ -47,14 +47,7 @@ pub use primitives::multilinear::build_eq;
/// Index convention: for index `i ∈ 0..2^m`, the low `k_skip` bits address
/// the *skip* variables (`y_skip ∈ S`), the high `m - k_skip` bits address
/// the *rest* variables (`y_rest`).
-pub fn round1_naive(
- a: &[bool],
- b: &[bool],
- c: &[bool],
- m: usize,
- k_skip: usize,
- r: &[F128],
-) -> (Vec, Vec) {
+pub fn round1_naive(a: &[bool], b: &[bool], c: &[bool], m: usize, k_skip: usize, r: &[F192]) -> (Vec, Vec) {
assert!(k_skip <= m, "k_skip must be ≤ m");
assert_eq!(a.len(), 1usize << m);
assert_eq!(b.len(), 1usize << m);
@@ -72,8 +65,8 @@ pub fn round1_naive(
// (the skip portion r[0..k_skip] is consumed by the verifier later).
let eq_full = build_eq(&r[k_skip..]);
- let mut p_ab = vec![F128::ZERO; ell];
- let mut p_c = vec![F128::ZERO; ell];
+ let mut p_ab = vec![F192::ZERO; ell];
+ let mut p_c = vec![F192::ZERO; ell];
let mut a_col = vec![F8::ZERO; ell];
let mut b_col = vec![F8::ZERO; ell];
@@ -127,43 +120,41 @@ pub fn pack_bits(bits: &[bool]) -> Vec {
let mut out = vec![0u8; n_bytes];
// Each output byte depends on 8 contiguous input bits — disjoint, so
// process bytes in parallel.
- out.par_chunks_mut(1)
- .enumerate()
- .for_each(|(byte_idx, slot)| {
- let mut byte = 0u8;
- let base = byte_idx * 8;
- for j in 0..8 {
- let bit_idx = base + j;
- if bit_idx < bits.len() && bits[bit_idx] {
- byte |= 1u8 << j;
- }
+ out.par_chunks_mut(1).enumerate().for_each(|(byte_idx, slot)| {
+ let mut byte = 0u8;
+ let base = byte_idx * 8;
+ for j in 0..8 {
+ let bit_idx = base + j;
+ if bit_idx < bits.len() && bits[bit_idx] {
+ byte |= 1u8 << j;
}
- slot[0] = byte;
- });
+ }
+ slot[0] = byte;
+ });
out
}
/// Eq table split into a lo half (large, L2-resident) and a hi half (small,
/// kept in registers across the inner loop).
#[derive(Clone, Debug)]
-pub struct SplitEqGhash {
+pub struct SplitEq {
pub n_lo: usize,
pub n_hi: usize,
- pub lo: Vec