Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
0962b18
refactor: expose verifier stages and ordered replay control flow
kevaundray Oct 9, 2026
2aa30a5
Bind PCS openings to immutable commitment anchors
kevaundray Oct 9, 2026
e7b7288
Merge commit '0962b187' into kw/whir-anchor-20261010
kevaundray Oct 9, 2026
3ff8eeb
Document immutable PCS records and retain behavioral length checks
kevaundray Oct 9, 2026
49092fe
Refine verifier loops for supported extraction
kevaundray Oct 10, 2026
0ebf57b
Bind immutable commitment records across opening sessions and version…
kevaundray Oct 10, 2026
51bfe46
Merge commit '49092fe8' into kw/whir-anchor-20261010
kevaundray Oct 10, 2026
ffae9e9
Merge commit 'a40a7fa6de0f5ba75214aa53aceff49a5c61d7e2' into kw/whir-…
kevaundray Oct 10, 2026
726aa9d
Format anchored protocol source with Rust 1.99
kevaundray Oct 10, 2026
08fbe89
Remove obsolete root-only receiver import from migrated benchmark
kevaundray Oct 10, 2026
a2e83bd
Reserve anchored proof identities above all open protocol proposals
kevaundray Oct 10, 2026
8d139bc
Retain consumer nonce regression and remove incidental scope wording …
kevaundray Oct 10, 2026
732333d
Make anchored verification control flow extractable
kevaundray Oct 10, 2026
83709e9
Merge commit '732333d7' into kw/whir-anchor-20261010
kevaundray Oct 10, 2026
8ab1d9c
Merge commit '8d139bcf' into kw/whir-aeneas-20261009
kevaundray Oct 10, 2026
cd99bb5
Keep anchor coefficient in existing basis batch and satisfy source CI
kevaundray Oct 10, 2026
ce08c70
Merge commit 'cd99bb5175eb7f18c25ad62ebb44523659724c62' into kw/whir-…
kevaundray Oct 10, 2026
087c39a
Disclose deployed Fiat Shamir refinement gap for anchored PCS
kevaundray Oct 10, 2026
29789a4
Make WHIR verifier weights statically explicit for extraction
kevaundray Oct 10, 2026
61f0bc5
Merge supported anchored verifier callback extraction
kevaundray Oct 10, 2026
71c741d
Update public nonce rejection for complete commitment records
kevaundray Oct 10, 2026
32c6297
Merge deployed duplex transcript before anchored protocol publication
kevaundray Oct 10, 2026
983e003
Finalize duplex context snapshots and verifier commitment type selection
kevaundray Oct 10, 2026
8f83305
Make the PCS arithmetic dictionary sequential and structure verifier …
kevaundray Oct 10, 2026
ac08c3b
Merge commit '983e003d51bd8f03af2f1ab226f19298d012d722' into kw/whir-…
kevaundray Oct 10, 2026
b6967e6
Merge upstream PCS rewrite and retain anchored records
kevaundray Oct 10, 2026
aeb0a33
Merge latest upstream scalar and batched Merkle hashing
kevaundray Oct 10, 2026
ac0ca96
Merge commit 'aeb0a3307f8c2ddef44cb69c636a81b18a98f324' into kw/whir-…
kevaundray Oct 10, 2026
538aa13
Record checked Aeneas tooling and unified Lean backend
kevaundray Oct 10, 2026
8f9d328
Merge commit '538aa130' into kw/whir-aeneas-current-20261010
kevaundray Oct 10, 2026
8a7afbd
Finish typed anchored PCS integration and current protocol docs
kevaundray Oct 10, 2026
7de1b28
refactor(pcs): preserve verifier semantics with extractable control flow
kevaundray Oct 10, 2026
48a5d7c
Merge commit '8a7afbd01e592f2058f348852daa0f428d411e64' into kw/whir-…
kevaundray Oct 10, 2026
1c3b913
Merge extractable verifier control flow into anchored PCS protocol
kevaundray Oct 10, 2026
5798b4d
Merge branch 'riscv-exploration' of https://github.com/leanEthereum/l…
kevaundray Oct 10, 2026
12eea88
Keep extractable verifier control flow compatible with Clippy
kevaundray Oct 10, 2026
826dd79
Keep Merkle authentication in sequential private phases
kevaundray Oct 10, 2026
4ad777c
Merge commit '12eea88c41dd964d82d4eb15ce91bba17ef91339' into kw/whir-…
kevaundray Oct 10, 2026
1abc198
pcs: use guarded shifts in stack claim validation
kevaundray Oct 10, 2026
a578135
Prove the extracted stack statement check against the WHIR model
kevaundray Oct 11, 2026
9ce194d
pcs: keep the closure-free extraction test root clean under Clippy
kevaundray Oct 11, 2026
e07bcd5
pcs: run verifier loops under no nested shared borrow
kevaundray Oct 11, 2026
113d8e6
pcs: index the ring map's Horner loops instead of holding split_last
kevaundray Oct 11, 2026
827ba19
pcs: name prefix-group points by index instead of borrowing them
kevaundray Oct 11, 2026
7c36bf9
fiat_shamir: fold Merkle opening levels without moving out of a loop …
kevaundray Oct 11, 2026
d7f99e5
pcs: keep verifier loops generic over Arith and name WeightAt's eleme…
kevaundray Oct 11, 2026
62b8c9d
Prove the extracted fold, closure, OOD and basis steps against the WH…
kevaundray Oct 11, 2026
2a5369e
verification: list the arithmetic operations and laws the fold premis…
kevaundray Oct 11, 2026
660f590
pcs: replay loops without iterator adaptors or projected element types
kevaundray Oct 11, 2026
726f529
pcs: spell out the grinding-bits panic message so the replay formats …
kevaundray Oct 11, 2026
5c765bc
verification: extract the generic WHIR replay and prove its query sum…
kevaundray Oct 11, 2026
7fce487
verification: document the replay surface, its premises, remaining ga…
kevaundray Oct 11, 2026
7542153
verification: replay step lemmas for fold rounds, OOD replay, query b…
kevaundray Oct 11, 2026
acd11fe
verification: replay residual rounds and terminal check
kevaundray Oct 11, 2026
c1ca2b5
verification: replay terminal formula equals the model's source terminal
kevaundray Oct 11, 2026
020e7e7
verification: replay last level against the model's final level and c…
kevaundray Oct 11, 2026
f5848cf
verification: the replay's configuration check accepts the model's va…
kevaundray Oct 11, 2026
a90366c
verification: replay script data, level facts and audit of the replay…
kevaundray Oct 11, 2026
598013f
verification: compose middle and last replay levels with the model's …
kevaundray Oct 11, 2026
67b485c
verification: replay level loop against the model's checked level replay
kevaundray Oct 11, 2026
647b185
verification: base level and model verifier lemmas for the composed r…
kevaundray Oct 11, 2026
943590f
verification: level-0 prefix of the replay's run against the model
kevaundray Oct 11, 2026
ba28f7e
verification: composed replay theorem verify_eq against nativeVerify
kevaundray Oct 11, 2026
4315f37
verification: audit the composed replay theorems
kevaundray Oct 11, 2026
882a882
verification: one chunk of sample_queries_ordered is the model's bit …
kevaundray Oct 11, 2026
7eb9c61
verification: sample_queries_ordered's squeeze loop yields the model'…
kevaundray Oct 11, 2026
dd50bbf
verification: generated strata are the model's strata
kevaundray Oct 11, 2026
3dfb9f0
verification: stratum replacement loop of sample_queries_ordered
kevaundray Oct 11, 2026
ead4e3c
verification: sample_queries_ordered against deriveQueries; README fo…
kevaundray Oct 11, 2026
75d4562
Merge upstream spongefish transcript into anchored PCS
kevaundray Oct 11, 2026
d14628d
Merge anchored PCS on the spongefish transcript (75d4562b) into kw/wh…
kevaundray Oct 11, 2026
7d1224b
fiat_shamir: sum a round polynomial's tail in an index loop
kevaundray Oct 11, 2026
2b4f3bf
verification: re-extract the replay surface on the spongefish transcript
kevaundray Oct 11, 2026
0ccecec
verification: Merkle opening extraction surface on pcs::merkle
kevaundray Oct 11, 2026
db6ad0c
Merge remote-tracking branch 'origin/kw/whir-aeneas-607' into kw/whir…
kevaundray Oct 11, 2026
a837846
verification: re-prove the replay, composed and sampler theorems on t…
kevaundray Oct 11, 2026
457a1a0
pcs: Merkle opening and hint decoding in index loops for extraction
kevaundray Oct 11, 2026
706a90e
verification: extract the Merkle opening surface; hint decoding and o…
kevaundray Oct 11, 2026
4cc741a
verification: Merkle rebuild loop and binary searches proved
kevaundray Oct 11, 2026
9f0f366
verification: Merkle audit and README for the extracted opening surface
kevaundray Oct 11, 2026
54d6b71
pcs: native open_rows as a free function, hint split and rows by inde…
kevaundray Oct 11, 2026
3d85d35
verification: rows read carries the replay's arguments
kevaundray Oct 11, 2026
2107e2b
verification: Merkle open composition, expansion and rebuild invariants
kevaundray Oct 11, 2026
577b884
verification: Merkle opening acceptance and binding under injective h…
kevaundray Oct 11, 2026
3e5ecc8
verification: extract the native open_rows and prove it reads, decode…
kevaundray Oct 11, 2026
a774bec
verification: AuthenticatedRows for the script's opening with committ…
kevaundray Oct 11, 2026
6e967f1
verification: backend names discriminant instances after their inductive
kevaundray Oct 11, 2026
2d77f42
verification: native rows read derived from the extracted open_rows; …
kevaundray Oct 11, 2026
b688fd9
verification: README for the native composed theorem, Merkle contract…
kevaundray Oct 11, 2026
c9a5763
verification: README records the paired ab for the native rows repair
kevaundray Oct 11, 2026
8662197
fiat_shamir: read and draw message batches in index loops for extraction
kevaundray Oct 12, 2026
3dd16df
verification: native transcript reads derived from the unified extrac…
kevaundray Oct 12, 2026
74a2121
verification: verify_refines_native concludes Protocol.verify; Ensure…
kevaundray Oct 12, 2026
7782218
verification: the native element clone is the identity by definition,…
kevaundray Oct 12, 2026
a640ae6
verification: README summary of the final theorems, remaining premise…
kevaundray Oct 12, 2026
bbeb1ba
Merge upstream claim-set trees and height tiers into anchored PCS
kevaundray Oct 12, 2026
39ec6aa
Merge upstream lone-child claim-set nodes and the stacked-opening lin…
kevaundray Oct 12, 2026
07ff5b2
Refresh the pinned tree-12 session tags for the anchored program dige…
kevaundray Oct 12, 2026
cdb3423
Merge upstream dfb7570a (flock before the bus) into the anchored PCS …
kevaundray Oct 12, 2026
15a733d
Merge upstream 37c1525f (zero-knowledge leaves, module layout, std-gr…
kevaundray Oct 13, 2026
c5acc61
pcs: index loop in the weighted claim's terminal weight, and no redun…
kevaundray Oct 14, 2026
627f2c4
Merge the anchored PCS opening on upstream's zero-knowledge leaves an…
kevaundray Oct 14, 2026
c14d27d
Merge commit 'c5acc61f5ace49714599037e406434445006962b' into kw/whir-…
kevaundray Oct 14, 2026
35ed839
pcs, fiat_shamir: index loops in the hint reader, the novel basis and…
kevaundray Oct 14, 2026
fb2975f
verification: re-extract the replay surface on the merged layout; Fol…
kevaundray Oct 14, 2026
2613dce
verification: Query re-proved on batch_oods, RoundQuad::read and the …
kevaundray Oct 14, 2026
faf4c90
verification: Merkle native reads re-proved on Hints::next and Openin…
kevaundray Oct 14, 2026
208b522
verification: re-extract the statement surface on the merged layout
kevaundray Oct 14, 2026
6363a8d
verification: Replay re-proved on the default read_round, fold_challe…
kevaundray Oct 14, 2026
866329a
verification: Sampling re-proved on query::sample_queries(depth) and …
kevaundray Oct 14, 2026
0e8d3c4
verification: re-extract the portable surface on the merged layout (g…
kevaundray Oct 14, 2026
5fb78cf
verification: Compose re-proved on the inlined verify body; ConfigRel…
kevaundray Oct 14, 2026
a457e5e
verification: NativeReads re-proved on the concrete duplex, F192::fro…
kevaundray Oct 14, 2026
e523f57
verification: README for the merge of PR 600 at c5acc61f5: pins, sour…
kevaundray Oct 14, 2026
11d1ae8
verification: README records the kernel workloads of the paired run a…
kevaundray Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

leanVM is designed for security:

* 128-bit ROM (64-bit QROM) soundness
* a 128-bit ROM (64-bit QROM) soundness target, not a proved deployed security level
* no proximity gap conjecture
* end-to-end formal verification
* a traditional hash function
Expand Down Expand Up @@ -180,3 +180,17 @@ Each instruction class's circuit proven alone by flock, on `2^FLOCK_N_LOG` rando
- [WHIR](https://eprint.iacr.org/2024/1586) PCS, aka [Ligerito](https://eprint.iacr.org/2025/1187)
- [Flock](https://github.com/succinctlabs/flock/tree/main) hash proving
- [Binius](https://github.com/IrreducibleOSS/binius)/[Binius64](https://github.com/binius-zk/binius64) ring switching, M3 arithmetisation, and more (see [DP23](https://eprint.iacr.org/2023/1784) and [DP24](https://eprint.iacr.org/2024/504))

### Immutable PCS commitment anchors

PCS commitment identity is now the complete immutable Merkle root, public shape, exact pre-commit transcript chaining value, normalized pending bytes and byte count, first-block flag and previous and current squeeze cursors, commitment-time extension-field point and advertised MLE value. Capturing that context does not flush, hash, sample, allocate or emit recursive rows. A valid duplex state's cached output is derivable from its chaining value and cursor; snapshots are read-only and cannot inject arbitrary states. `CommittedStack::new` binds the `whir-anchor-v1` marker, dimension/interleaving/rate tuple and occupied lane count before the root and the one anchor challenge. `StackCommitment::receive` checks the same context, derives that point once and reads its value. CPU proofs use `LVMP18` and the `leanvm-rv64im-14` program domain; tree proofs use `LVMT20` and the `leanvm-tree-17` domain. These identities are above current upstream. Legacy proofs are not accepted.

Every public stacked opening requires this complete record. Its `whir-opening-v1` frame binds and compares the shape, raw 32-byte root, raw 32-byte original chaining value, pending-byte metadata and normalized buffer, point and advertised value before any opening batching challenge. Cursor metadata precedes pending bytes, packed as three little endian words per scalar with canonical final padding. This permits opening the same immutable record in a fresh session without receiving it again or resampling its point. Root-only WHIR kernels and dense-weight interfaces are crate-private protocol building blocks, not alternate public PCS verification paths. Callers must bind their complete opening statement before entry: values must already be observed or public, coordinates must be public constants or earlier verifier challenges, and addressing and shape must follow the bound layout. The identity frame does not bind arbitrary external claims. Omitted lanes remain mandated zero, and the anchor's linear weight is restricted to the occupied prefix in both native and row arithmetic.

The crate-private terminal weight provider dispatches statically and is consumed once at the terminal point after rotating fold challenges into witness-coordinate order. Named weight values borrow their existing inputs without callback allocation.

The separate Lean investigation proves ideal interactive uniform anchor candidate uniqueness with a commitment-fixed list, including arbitrary malicious advertised values and cross-session reuse. Its anchor ambiguity term is separate from opening, Fiat-Shamir, Merkle and concrete cryptographic losses. This branch normally merges upstream #607 at `d3c7f9d9a6ad4f7b88f917284076b34430502ae2`: session-IV derivation, public-instance absorption, raw-byte roots, the new codecs, proof-of-work and squeezed final digests. Proof PR #551's existing duplex refinement chain models the historical pre-#607 #552 transcript. Its checked adaptive ideal-compression coupling has loss `min(1,(2Q^2-Q)/2^256)`, but the conditional `2^-42` historical unanchored endpoint does not currently apply to the deployed #607 transcript or these new anchor inputs without the missing source adapters and composition. Its checked reset knowledge extractor is interactive only, not a Fiat-Shamir knowledge result. This cutover claims no proved whole-system security level, unconditional deterministic BLAKE2s security or universal Rust-to-Lean equivalence.

A hiding commitment, which zero-knowledge proofs take for their stack and their key commitment, carries no anchor: its advertised value would be one more evaluation of the hidden witness, which the hiding argument does not cover. `CommittedStack::new_hiding` and `StackCommitment::hiding` stay root-only; `StackCommitment::receive` refuses a padding configuration and `StackCommitment::hiding` a plain one with `WhirError::Anchoring`, and weighted openings, which only the key commitment takes, are refused on an anchored commitment.

The typed stacked receiver retains upstream's aligned plain-selector, checked strided-width and slot guards, including #608's machine-width checks. Native and Rows use the same typed verifier and record frame. The security theorem and a universal source correspondence must still discharge their stated model and caller-binding hypotheses.
176 changes: 131 additions & 45 deletions crates/fiat_shamir/src/arith.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,6 @@ pub trait Arith {
/// `a^(2^128)`: two Frobenius maps of `E` over `K`, which take `a` to `a^(2^-64)`.
fn frobenius2(&mut self, a: Self::E) -> Self::E;

/// The multilinear extension of public `K` words at `point`, lowest coordinate first.
fn public_mle(&mut self, values: &[F64], point: &[Self::E]) -> Self::E {
assert_eq!(values.len(), 1 << point.len(), "a column has a word per vertex");
let eq = self.eq_table(point);
let zero = self.zero();
(eq.iter().zip(values)).fold(zero, |acc, (&e, &v)| self.mul_const_add(e, F192::from(v), acc))
}

/// The constant zero.
fn zero(&mut self) -> Self::E {
self.constant(F192::ZERO)
Expand Down Expand Up @@ -76,13 +68,23 @@ pub trait Arith {
/// `prod_i factors_i`.
fn product(&mut self, factors: &[Self::E]) -> Self::E {
let one = self.one();
factors.iter().fold(one, |acc, &f| self.mul(acc, f))
let mut acc = one;
for &f in factors {
acc = self.mul(acc, f);
}
acc
}

/// `sum_i c_i x^i`, by Horner, constant first.
fn poly_eval(&mut self, coeffs: &[Self::E], x: Self::E) -> Self::E {
let zero = self.zero();
coeffs.iter().rev().fold(zero, |acc, &c| self.mul_add(acc, x, c))
let mut acc = zero;
let mut i = coeffs.len();
while i > 0 {
i -= 1;
acc = self.mul_add(acc, x, coeffs[i]);
}
acc
}

/// The line through `(0, lo)` and `(1, hi)` at `t`: `lo + t·(lo + hi)`.
Expand All @@ -100,10 +102,12 @@ pub trait Arith {
fn eq_eval(&mut self, x: &[Self::E], y: &[Self::E]) -> Self::E {
assert_eq!(x.len(), y.len(), "two points of one cube");
let one = self.one();
x.iter().zip(y).fold(one, |acc, (&u, &v)| {
let s = self.add(u, v);
self.times_one_plus(acc, s)
})
let mut acc = one;
for i in 0..x.len() {
let s = self.add(x[i], y[i]);
acc = self.times_one_plus(acc, s);
}
acc
}

/// `eq(bits, point)` for public bits, lowest first: `prod_j (bit_j ? z_j : 1 + z_j)`.
Expand All @@ -112,13 +116,17 @@ pub trait Arith {
/// Why: selectors of nearby offsets share their high bits, so in rows their common partial products are made once.
fn eq_bits(&mut self, bits: usize, point: &[Self::E]) -> Self::E {
let one = self.one();
(point.iter().enumerate()).rev().fold(one, |acc, (j, &z)| {
if bits >> j & 1 == 1 {
self.mul(acc, z)
let mut acc = one;
let mut j = point.len();
while j > 0 {
j -= 1;
acc = if bits >> j & 1 == 1 {
self.mul(acc, point[j])
} else {
self.times_one_plus(acc, z)
}
})
self.times_one_plus(acc, point[j])
};
}
acc
}

/// `eq(point, x)` for every vertex `x`, lowest coordinate first.
Expand All @@ -129,17 +137,26 @@ pub trait Arith {
/// `eq(point, x)` for the first `len` vertices `x`, lowest coordinate first, with no work for the others.
fn eq_table_prefix(&mut self, point: &[Self::E], len: usize) -> Vec<Self::E> {
assert!(len <= 1 << point.len(), "a prefix of the cube");
let mut table = vec![self.one()];
for (i, &r) in point.iter().enumerate() {
let mut table = Vec::with_capacity(1);
table.push(self.one());
let mut i = 0;
while i < point.len() {
let r = point[i];
let need = len.min(2 << i);
let high: Vec<Self::E> = table[..need.saturating_sub(table.len())]
.iter()
.map(|&v| self.mul(v, r))
.collect();
for v in &mut table {
*v = self.times_one_plus(*v, r);
let count = need.saturating_sub(table.len());
let mut high = Vec::with_capacity(count);
let mut j = 0;
while j < count {
high.push(self.mul(table[j], r));
j += 1;
}
let mut j = 0;
while j < table.len() {
table[j] = self.times_one_plus(table[j], r);
j += 1;
}
table.extend(high);
i += 1;
}
table.truncate(len);
table
Expand All @@ -150,7 +167,14 @@ pub trait Arith {
assert_eq!(values.len(), 1 << point.len(), "a value per vertex");
let mut folded = values.to_vec();
for &x in point {
folded = folded.chunks(2).map(|pair| self.interp(pair[0], pair[1], x)).collect();
let len = folded.len();
let mut next = Vec::with_capacity(len / 2 + len % 2);
let mut i = 0;
while i < len {
next.push(self.interp(folded[i], folded[i + 1], x));
i += 2;
}
folded = next;
}
folded[0]
}
Expand All @@ -169,15 +193,46 @@ pub trait Arith {
/// The integer index column `base ^ (z << shift)` at `point`: `base + sum_i point_i 2^(i + shift)`.
fn int_index(&mut self, base: F64, shift: u32, point: &[Self::E]) -> Self::E {
let base = self.constant(F192::from(base));
(point.iter().enumerate()).fold(base, |acc, (i, &z)| {
self.mul_const_add(z, F192::from(F64(1 << (i as u32 + shift))), acc)
})
let mut acc = base;
for (i, &z) in point.iter().enumerate() {
acc = self.mul_const_add(z, F192::from(F64(1 << (i as u32 + shift))), acc);
}
acc
}
}

/// A verifier: arithmetic, a transcript to read and draw from, and equalities to check.
///
/// Natively the transcript is the proof's; in rows it is the circuit's replay of it.
/// Arithmetic that also evaluates multilinear extensions of public columns.
pub trait PublicMle: Arith {
/// The multilinear extension of public `K` words at `point`, lowest coordinate first.
fn public_mle(&mut self, values: &[F64], point: &[Self::E]) -> Self::E {
assert_eq!(values.len(), 1 << point.len(), "a column has a word per vertex");
let eq = self.eq_table(point);
let zero = self.zero();
let mut acc = zero;
for i in 0..eq.len().min(values.len()) {
acc = self.mul_const_add(eq[i], F192::from(values[i]), acc);
}
acc
}
}

/// Named stages of the generic verifier, independent of its native or row representation.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Stage {
Announcement,
BusAndTables,
Flock,
Lincheck,
Opening,
Rows,
Target,
Terminal,
Transcript,
Whir,
Zerocheck,
}

/// A verifier: arithmetic, a transcript to read and sample, and equalities to check.
pub trait Verifier: Arith {
/// The next prover message, an element of `E`, read and absorbed.
///
Expand Down Expand Up @@ -227,26 +282,41 @@ pub trait Verifier: Arith {
false
}

/// Run `f` as a named stage of the verifier.
/// Start a named stage of the verifier.
///
/// A verifier that records its failed checks rather than refusing reports each under its stages' names.
fn scope<T>(&mut self, name: &'static str, f: impl FnOnce(&mut Self) -> T) -> T {
let _ = name;
f(self)
}
/// A verifier that records failed checks reports them under the active stages.
/// The caller must evaluate the stage's result, end the stage, then propagate errors.
/// A panicking body leaves the stage active.
fn begin_scope(&mut self, stage: Stage);

/// End the most recently started stage.
fn end_scope(&mut self);

/// The next `n` prover messages.
///
/// # Errors
///
/// Returns an error past the end of the proof.
fn prover_messages(&mut self, n: usize) -> Result<Vec<Self::E>, TranscriptError> {
(0..n).map(|_| self.prover_message()).collect()
// An index loop rather than a collecting closure, so no closure captures the verifier.
let mut out = Vec::with_capacity(n);
let mut i = 0;
while i < n {
out.push(self.prover_message()?);
i += 1;
}
Ok(out)
}

/// `n` verifier messages.
fn verifier_messages(&mut self, n: usize) -> Vec<Self::E> {
(0..n).map(|_| self.verifier_message()).collect()
let mut out = Vec::with_capacity(n);
let mut i = 0;
while i < n {
out.push(self.verifier_message());
i += 1;
}
out
}

/// Read one sumcheck round's polynomial of `n_coeffs` coefficients, constant first.
Expand Down Expand Up @@ -278,9 +348,15 @@ pub trait Verifier: Arith {
let mut coeffs = self.prover_messages(n_coeffs - 1)?;
coeffs.insert(fixed, claim);

// The coefficients past the fixed one, summed.
// The coefficients past the fixed one, summed in order.
// An index loop rather than a folding closure, so no closure captures the verifier.
let zero = self.zero();
let tail = coeffs[fixed + 1..].iter().fold(zero, |acc, &c| self.add(acc, c));
let mut tail = zero;
let mut i = fixed + 1;
while i < coeffs.len() {
tail = self.add(tail, coeffs[i]);
i += 1;
}

// c_1 = claim + sum_{i>=2} c_i, or c_0 = claim + r sum_{i>=1} c_i.
coeffs[fixed] = match eq {
Expand Down Expand Up @@ -342,7 +418,9 @@ impl Arith for Native {
fn frobenius2(&mut self, a: F192) -> F192 {
a.frobenius().frobenius()
}
}

impl PublicMle for Native {
fn public_mle(&mut self, values: &[F64], point: &[F192]) -> F192 {
mle_eval_par(values, point)
}
Expand Down Expand Up @@ -375,7 +453,9 @@ impl Arith for VerifierState<'_> {
fn frobenius2(&mut self, a: F192) -> F192 {
Native.frobenius2(a)
}
}

impl PublicMle for VerifierState<'_> {
fn public_mle(&mut self, values: &[F64], point: &[F192]) -> F192 {
Native.public_mle(values, point)
}
Expand All @@ -402,6 +482,12 @@ impl Verifier for VerifierState<'_> {
Self::check_eof(self)
}

#[inline]
fn begin_scope(&mut self, _: Stage) {}

#[inline]
fn end_scope(&mut self) {}

fn set_hidden(&mut self, hidden: bool) {
Self::set_hidden(self, hidden);
}
Expand Down
70 changes: 70 additions & 0 deletions crates/fiat_shamir/src/duplex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,31 @@ impl Blake2sDuplex {
T::from_uniform(repr)
}

/// Capture the immutable logical state without flushing, hashing, squeezing, or allocating.
///
/// A live cached output suffix is determined by the chaining value and squeeze cursor; inactive cached output has no effect on future operations.
#[must_use]
pub fn context(&self) -> crate::TranscriptContext<primitives::field::F64, [u8; 32]> {
let mut pending = [None; 8];
let (words, remainder) = self.pending[..self.n_pending].as_chunks::<8>();
for (slot, bytes) in pending.iter_mut().zip(words) {
*slot = Some(primitives::field::F64(u64::from_le_bytes(*bytes)));
}
if !remainder.is_empty() {
let mut padded = [0; 8];
padded[..remainder.len()].copy_from_slice(remainder);
pending[words.len()] = Some(primitives::field::F64(u64::from_le_bytes(padded)));
}
crate::TranscriptContext {
state: state_bytes(self.cv),
pending,
pending_bytes: self.n_pending,
first: self.first,
previous: self.previous,
squeezed: self.squeezed,
}
}

/// Compress the pending block into the chaining value.
///
/// `last` says whether it closes its absorb run.
Expand Down Expand Up @@ -336,6 +361,51 @@ mod tests {
}
}

#[test]
fn context_preserves_partial_absorb_and_squeeze_continuations() {
let input: [u8; 129] = std::array::from_fn(|i| (i + 1) as u8);
for pending_bytes in 0..=64 {
for squeezed in 0..=65 {
let mut sponge = Blake2sDuplex::new([17; 32]);
if squeezed > 0 {
sponge.absorb(&input[..65]);
sponge.squeeze(&mut [0; 65][..squeezed]);
}
sponge.absorb(&input[..pending_bytes]);
let mut before = sponge.clone();
let context = sponge.context();
assert_eq!(context, sponge.context());
assert_eq!(context.state, state_bytes(before.cv));
assert_eq!(context.pending_bytes, before.n_pending);
assert_eq!(context.first, before.first);
assert_eq!(context.previous, before.previous);
assert_eq!(context.squeezed, before.squeezed);
for (i, word) in context.pending.iter().enumerate() {
let start = 8 * i;
if start < pending_bytes {
let end = (start + 8).min(pending_bytes);
let mut padded = [0; 8];
padded[..end - start].copy_from_slice(&input[start..end]);
assert_eq!(*word, Some(primitives::field::F64(u64::from_le_bytes(padded))));
} else {
assert_eq!(*word, None);
}
}
let mut actual = [0; 97];
let mut expected = [0; 97];
sponge.squeeze(&mut actual);
before.squeeze(&mut expected);
assert_eq!(actual, expected);
sponge.absorb(&input[..19]);
before.absorb(&input[..19]);
sponge.squeeze(&mut actual);
before.squeeze(&mut expected);
assert_eq!(actual, expected);
assert_eq!(sponge.context(), before.context());
}
}
}

#[test]
fn padding_and_squeeze_counts_are_bound() {
// The framing tells apart transcripts a plain concatenation would merge.
Expand Down
Loading
Loading