diff --git a/CHANGELOG.md b/CHANGELOG.md index 23dbfa2..e9569dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Consolidated error types.** Reduced from 6 error enums to 4. Folded `WARPSumcheckProverError` into `ProverError`, inlined `WARPSumcheckVerifierError` into `VerifierError`, dropped `WARP` prefix. Moved errors from `utils/errs.rs` to `error.rs`. - **Made `chunk_size` compile-time.** Replaced runtime serialization with a `const fn` computed from `PrimeField::MODULUS_BIT_SIZE`. +### Optimized + +- **Reduced inner product sumcheck communication by 1/3.** Updated to `efficient-sumcheck` 2-coefficient round messages: prover sends `(a, b)` instead of `(s(0), s(1), s(1/2))`. Verifier derives the third coefficient as `c = claim - 2a - b`. Updated verifier transcript to read `[F; 2]` instead of `[F; 3]` per round. +- **Adopted `RoundPolyEvaluator` trait for twin constraint sumcheck.** Replaced closure-based `twin_constraint_round_poly` with `TwinConstraintEvaluator` struct implementing the new `RoundPolyEvaluator` trait from `efficient-sumcheck`. The library now handles pair iteration, parallel summation, and SIMD-accelerated reduce. +- **Reduced twin constraint sumcheck communication.** Prover now sends `d` coefficients per round instead of `d+1`; verifier derives the leading coefficient from the sumcheck constraint. Updated `derive_randomness` to read `1 + max(log_n+1, log_m+2)` instead of `2 + max(log_n+1, log_m+2)`. +- **Twin constraint sumcheck 64% faster.** Combined effect of `RoundPolyEvaluator` refactor, library-side optimizations (zero-allocation `poly_ops`, optimized `protogalaxy::fold`), and SIMD-accelerated pairwise reduce. End-to-end prover time improved ~23%. +- **Pinned `ark-ff`/`ark-poly`/`ark-serialize` to rev `285dac2`.** Resolves spongefish BigInt mismatch with upstream `ark-ff` HEAD. + ### Fixed - **BLS test desync.** Fixed `WARPConfig` in the BLS test from `l=4` to `l=8`, matching `l2=4` accumulated instances. The prover absorbed all accumulators unconditionally while the verifier only read `l2` of them, causing a sponge state mismatch. diff --git a/Cargo.toml b/Cargo.toml index c79b099..5ea082b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,9 @@ serde_json = "1.0" spongefish = { version = "0.7.0", features = ["ark-ff"] } effsc = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git" } thiserror = "2.0.16" +tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } +tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi", "env-filter", "json", "registry"], optional = true } +libc = { version = "0.2", optional = true } ark-codes = { git = "https://github.com/z-tech/ark-codes.git", branch = "z-tech/arkworks-0.6.0" } [dev-dependencies] @@ -36,6 +39,12 @@ default = ["asm"] asm = ["ark-ff/asm"] +# Opt-in profiling: brings in tracing-subscriber + libc so we can emit +# structured per-phase records (wall_ns, cpu_ns, peak_rss_delta, op counters). +# Release builds with `profile` off pull in neither tracing-subscriber nor +# libc as direct deps of `warp`. +profile = ["dep:tracing-subscriber", "dep:libc"] + [[bench]] name = "warp_rs" harness = false diff --git a/benches/warp_rs.rs b/benches/warp_rs.rs index 1943dfd..2f238b8 100644 --- a/benches/warp_rs.rs +++ b/benches/warp_rs.rs @@ -1,14 +1,13 @@ -use ark_bls12_381::Fr as BLS12_381; use ark_codes::reed_solomon::config::ReedSolomonConfig; use ark_codes::reed_solomon::ReedSolomon; use ark_codes::traits::LinearCode; +use ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig; use ark_std::rand::thread_rng; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use utils::domainsep::init_prover_state; use utils::hash_chain::{get_hashchain_instance_witness_pairs, get_hashchain_r1cs}; use warp::config::WARPConfig; -use warp::crypto::merkle::blake3::Blake3MerkleTreeParams; use warp::traits::AccumulationScheme; use warp::WARP; @@ -19,59 +18,6 @@ use warp::utils::fields::Goldilocks; const HASHCHAIN_SIZE: usize = 800; -pub fn bench_rs_warp(c: &mut Criterion) { - let mut rng = thread_rng(); - let poseidon_config = poseidon::initialize_poseidon_config::(); - let r1cs = get_hashchain_r1cs(&poseidon_config, HASHCHAIN_SIZE); - - let code_config = ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); - let code = ReedSolomon::new(code_config.clone()); - let s = 8; - let t = 7; - - for l in [32, 64, 128, 256, 512] { - let warp_config = WARPConfig::new(l, l, s, t, r1cs.config(), code.code_len()); - - let hash_chain_warp = WARP::<_, _, _, Blake3MerkleTreeParams<_>>::new( - warp_config.clone(), - code.clone(), - r1cs.clone(), - (), - (), - ); - - let instances_witnesses = - get_hashchain_instance_witness_pairs(l, &poseidon_config, HASHCHAIN_SIZE, &mut rng); - - let mut group = c.benchmark_group("warp_rs_bls12_381_hash_chain"); - group.sample_size(10); - group.bench_with_input( - BenchmarkId::from_parameter(l), - &instances_witnesses, - |b, instance_witnesses| { - b.iter_with_setup( - || { - let prover_state = init_prover_state(); - (prover_state, instance_witnesses.clone()) - }, - |(mut prover_state, _x_w)| { - let _ = hash_chain_warp - .prove( - (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), - &mut prover_state, - instances_witnesses.1.clone(), - instances_witnesses.0.clone(), - (vec![], vec![], vec![], (vec![], vec![]), vec![]), - (vec![], vec![], vec![]), - ) - .unwrap(); - }, - ); - }, - ); - } -} - pub fn bench_rs_warp_fields(c: &mut Criterion) { pub type F = Goldilocks; let mut rng = thread_rng(); @@ -86,7 +32,7 @@ pub fn bench_rs_warp_fields(c: &mut Criterion) { for l in [32, 64, 128, 256, 512] { let warp_config = WARPConfig::new(l, l, s, t, r1cs.config(), code.code_len()); - let hash_chain_warp = WARP::<_, _, _, Blake3MerkleTreeParams<_>>::new( + let hash_chain_warp = WARP::<_, _, _, Blake3MerkleConfig<_>>::new( warp_config.clone(), code.clone(), r1cs.clone(), @@ -115,8 +61,8 @@ pub fn bench_rs_warp_fields(c: &mut Criterion) { &mut prover_state, instances_witnesses.1.clone(), instances_witnesses.0.clone(), - (vec![], vec![], vec![], (vec![], vec![]), vec![]), - (vec![], vec![], vec![]), + warp::types::AccumulatorInstance::empty(), + warp::types::AccumulatorWitness::empty(), ) .unwrap(); }, diff --git a/docs/audits/fiat_shamir.md b/docs/audits/fiat_shamir.md new file mode 100644 index 0000000..ecd1944 --- /dev/null +++ b/docs/audits/fiat_shamir.md @@ -0,0 +1,89 @@ +# Fiat–Shamir audit + +Status: **v1 manual audit**, current as of commit `9bf4f43`. Automated +runtime-ordering enforcement is deferred (see bottom). + +## What this document is + +An ordered, line-for-line mapping of every `prover_message(…)` / +`prover_messages(…)` call on the prover side to its matching +`prover_message()` / `prover_messages_vec(…)` call on the verifier +side, and similarly for every `verifier_message(…)` / +`verifier_messages_vec(…)` challenge squeeze. + +A Fiat–Shamir soundness flaw typically has a simple shape: a challenge +is squeezed **before** some prover message that should have influenced +it. This document lets a reviewer walk both sides in order and satisfy +themselves that every squeeze happens after every absorb that should +determine it. It is the compensating control for us not yet having a +runtime harness that asserts this automatically. + +## Transcript ordering + +| Step | Prover call | File:line | Verifier call | File:line | +|------|-------------|-----------|---------------|-----------| +| 1. Index — public params | `public_message(description)` | [src/lib.rs:113](../../src/lib.rs#L113) | (via `public_message` domain-sep, no explicit read) | — | +| 2. Index — `m` | `prover_message(m)` | [src/lib.rs:114](../../src/lib.rs#L114) | `prover_messages_vec` (absorbed as part of `vk`) | via `index()` | +| 3. Index — `n` | `prover_message(n)` | [src/lib.rs:115](../../src/lib.rs#L115) | " | " | +| 4. Index — `k` | `prover_message(k)` | [src/lib.rs:116](../../src/lib.rs#L116) | " | " | +| 5. Fresh instances `x_i` | `absorb_instances` → `prover_message` loop | [src/protocol/transcript/prover.rs:14](../../src/protocol/transcript/prover.rs#L14) | `prover_messages_vec(instance_len)` loop | [src/protocol/transcript/verifier.rs:25](../../src/protocol/transcript/verifier.rs#L25) | +| 6. Accumulator `rt[i]` | `prover_message(bytes)` | [src/protocol/transcript/prover.rs:28](../../src/protocol/transcript/prover.rs#L28) | `prover_message() -> [u8;32]` loop | [src/protocol/transcript/verifier.rs:49](../../src/protocol/transcript/verifier.rs#L49) | +| 7. Accumulator `α[i]` | `prover_message` loop | [src/protocol/transcript/prover.rs:33](../../src/protocol/transcript/prover.rs#L33) | `prover_messages_vec(log_n)` loop | [src/protocol/transcript/verifier.rs:55](../../src/protocol/transcript/verifier.rs#L55) | +| 8. Accumulator `μ[i]` | `prover_message` | [src/protocol/transcript/prover.rs:38](../../src/protocol/transcript/prover.rs#L38) | `prover_messages_vec(l2)` | [src/protocol/transcript/verifier.rs:58](../../src/protocol/transcript/verifier.rs#L58) | +| 9. Accumulator `τ[i]` | `prover_message` loop | [src/protocol/transcript/prover.rs:43](../../src/protocol/transcript/prover.rs#L43) | `prover_messages_vec(log_m)` loop | [src/protocol/transcript/verifier.rs:61](../../src/protocol/transcript/verifier.rs#L61) | +| 10. Accumulator `x[i]` | `prover_message` loop | [src/protocol/transcript/prover.rs:49](../../src/protocol/transcript/prover.rs#L49) | `prover_messages_vec(instance_len)` loop | [src/protocol/transcript/verifier.rs:65](../../src/protocol/transcript/verifier.rs#L65) | +| 11. Accumulator `η[i]` | `prover_message` | [src/protocol/transcript/prover.rs:54](../../src/protocol/transcript/prover.rs#L54) | `prover_messages_vec(l2)` | [src/protocol/transcript/verifier.rs:68](../../src/protocol/transcript/verifier.rs#L68) | +| 12. PESAT — `rt₀` | `prover_message(root_bytes)` | [src/protocol/phases/pesat.rs:78](../../src/protocol/phases/pesat.rs#L78) | `prover_message() -> [u8;32]` | [src/protocol/transcript/verifier.rs:111](../../src/protocol/transcript/verifier.rs#L111) | +| 13. PESAT — `μ_i` | `prover_messages(&mus)` | [src/protocol/phases/pesat.rs:79](../../src/protocol/phases/pesat.rs#L79) | `prover_messages_vec(l1)` | [src/protocol/transcript/verifier.rs:115](../../src/protocol/transcript/verifier.rs#L115) | +| 14. PESAT — τ squeeze | `verifier_messages_vec::(log_m)` × l1 | [src/protocol/phases/pesat.rs:82](../../src/protocol/phases/pesat.rs#L82) | `verifier_message::()` × (l1 · log_m) | [src/protocol/transcript/verifier.rs:121](../../src/protocol/transcript/verifier.rs#L121) | +| 15. Twin-constraint — ω | `verifier_message()` | [src/protocol/phases/twin_constraint.rs:160](../../src/protocol/phases/twin_constraint.rs#L160) | `verifier_message::()` | [src/protocol/transcript/verifier.rs:126](../../src/protocol/transcript/verifier.rs#L126) | +| 16. Twin-constraint — τ | `verifier_messages_vec::(log_l)` | [src/protocol/phases/twin_constraint.rs:161](../../src/protocol/phases/twin_constraint.rs#L161) | `verifier_message::()` × log_l | [src/protocol/transcript/verifier.rs:128](../../src/protocol/transcript/verifier.rs#L128) | +| 17. Twin-constraint — sumcheck | per round: coeffs absorbed, γ squeezed (inside `coefficient_sumcheck`) | [src/protocol/phases/twin_constraint.rs:202](../../src/protocol/phases/twin_constraint.rs#L202) | per round: `prover_messages_vec` coeffs, `verifier_message` γ | [src/protocol/transcript/verifier.rs:136](../../src/protocol/transcript/verifier.rs#L136) | +| 18. Post-TC — new root | `prover_message(td_root_bytes)` | [src/lib.rs:213](../../src/lib.rs#L213) | `prover_message() -> [u8;32]` | [src/protocol/transcript/verifier.rs:143](../../src/protocol/transcript/verifier.rs#L143) | +| 19. Post-TC — η | `prover_message(&eta)` | [src/lib.rs:214](../../src/lib.rs#L214) | `prover_message() -> F` | [src/protocol/transcript/verifier.rs:147](../../src/protocol/transcript/verifier.rs#L147) | +| 20. Post-TC — ν₀ | `prover_message(&nu_0)` | [src/lib.rs:215](../../src/lib.rs#L215) | `prover_message() -> F` | [src/protocol/transcript/verifier.rs:148](../../src/protocol/transcript/verifier.rs#L148) | +| 21. OOD — sample points | `verifier_messages_vec::(s·log_n)` | [src/protocol/phases/ood.rs:35](../../src/protocol/phases/ood.rs#L35) | `verifier_message::()` × (s · log_n) | [src/protocol/transcript/verifier.rs:154](../../src/protocol/transcript/verifier.rs#L154) | +| 22. OOD — answers | `prover_messages(&answers)` | [src/protocol/phases/ood.rs:41](../../src/protocol/phases/ood.rs#L41) | `prover_messages_vec(s)` | [src/protocol/transcript/verifier.rs:158](../../src/protocol/transcript/verifier.rs#L158) | +| 23. Proximity — query bytes | `verifier_messages_vec::<[u8;1]>(num_bytes)` via `QueryIndices::sample` | [src/protocol/query.rs:18](../../src/protocol/query.rs#L18) | `verifier_message::<[u8;1]>()` × num_bytes | [src/protocol/transcript/verifier.rs:166](../../src/protocol/transcript/verifier.rs#L166) | +| 24. Batching — ξ | `verifier_messages_vec::(log_r)` | [src/protocol/phases/batching.rs:61](../../src/protocol/phases/batching.rs#L61) | `verifier_message::()` × log_r | [src/protocol/transcript/verifier.rs:169](../../src/protocol/transcript/verifier.rs#L169) | +| 25. Batching — sumcheck | per round: `[a, b]` absorbed, α squeezed (inside `inner_product_sumcheck`) | [src/protocol/phases/batching.rs:93](../../src/protocol/phases/batching.rs#L93) | per round: `prover_messages() -> [F;2]`, `verifier_message()` α | [src/protocol/transcript/verifier.rs:176](../../src/protocol/transcript/verifier.rs#L176) | + +## What every reviewer should spot-check + +For each challenge squeeze, confirm that every prover message that +**defines** that challenge's semantic purpose has already been absorbed +above it. Examples: + +- **τ challenges (step 14).** Squeezed after the PESAT root and fresh + μ_i (steps 12–13). ✓ Both values determine which witness the prover + committed to; binding τ to them is required so the prover can't pick + τ after learning which witness is rejected. +- **ω challenge (step 15).** Squeezed after all prior PESAT state and + after τ. ✓ ω linearly combines two claims; if the prover could pick + ω *before* τ, they could cancel the two terms against a dishonest + witness. +- **Shift-query bytes (step 23).** Squeezed after the new commitment + `td_root_bytes`, η, ν₀, and the OOD answers (steps 18–22). ✓ The + query index must be unpredictable relative to the committed oracle. +- **Batching-sumcheck α challenges (step 25).** Squeezed per round + after each `[a, b]` is absorbed (inside the sumcheck). ✓ Standard. + +## What is **not** in this table + +- `domain_separator!("…")` invocations: those are audited via the + domain-sep macro's own per-call string matching; if the same + domainsep string is used on prover and verifier, the transcripts + align. Rotate the string if the layout changes. +- `AccumulatorInstance::empty()` / `AccumulatorWitness::empty()` + paths: these do nothing to the transcript, so they are + unaudited here. + +## Deferred — a runtime Fiat–Shamir harness + +Plan T originally proposed a test that captures the ordered list of +`prover_message` / `verifier_message` calls during a prove, and +asserts it matches a golden sequence. That would make drift between +prover and verifier impossible to land undetected. Implementing it +requires instrumenting `spongefish::ProverState` (external crate), so +it's deferred. The current table is the compensating control; it is +*manually* regenerated when any file above changes. diff --git a/docs/paper-mods/README.md b/docs/paper-mods/README.md new file mode 100644 index 0000000..7346f59 --- /dev/null +++ b/docs/paper-mods/README.md @@ -0,0 +1,40 @@ +# Warp paper-mods + +Living spec of notation and structural modifications we make to the Warp paper +so that the paper's IOR decomposition and the Rust implementation agree +exactly. This is **not** a paper in its own right — it is a working document +tracking deltas from the published construction, each paired with the code +module that realises it. + +## Scope + +Warp is framed as an **IOP** (Ben-Sasson–Chiesa–Spooner 2016), **not** as an +AHP. Oracles are functions `f: [n] → F` queried by index under the BCS +compiler, with multilinear-extension semantics layered on top for point +queries. See `notation.tex` for the shared preamble and rationale. + +## Convention + +- One `.tex` file per modification, paired with one Rust module. +- Shared preamble lives in `notation.tex`; every `.tex` file `\input`s it. +- **Authoring order**: the `.tex` file is written *before* the code module. + Code doc comments cite the `.tex` filename; `.tex` files cite the Rust + module path. Drift is caught in code review. +- No CI compilation. Build locally with `latexmk -pdf mod1_oracle.tex`. + +## Modification numbering (reserved) + +| File | Modification | Owning plan | +|-----------------------------------|------------------------------------------|-------------| +| `mod1_oracle.tex` | Oracle as first-class IOR output | Plan 0 | +| `mod2_structured_sumcheck.tex` | Structured-sumcheck primitive | Plan B' | +| `mod3_accumulator_state.tex` | Accumulator as explicit IOR state | Plan C | +| `mod4_parameter_selection.tex` | Soundness-driven parameter selection | Plan P | + +Future modifications take the next free number in sequence. + +## Cross-reference lint + +Each phase module's doc comment must name a `.tex` file in this folder. Each +`.tex` must name at least one Rust module path it pairs with. A manual check +is in the Plan 0 verification list; automation is Plan T's job. diff --git a/docs/paper-mods/mod1_oracle.tex b/docs/paper-mods/mod1_oracle.tex new file mode 100644 index 0000000..8101507 --- /dev/null +++ b/docs/paper-mods/mod1_oracle.tex @@ -0,0 +1,129 @@ +\documentclass{article} +\input{notation.tex} + +\title{Modification 1: \\ Oracle as first-class IOR output} +\author{Warp paper-mods} +\date{} + +\begin{document} +\maketitle + +\paragraph{Paired Rust module.} \codemod{src/protocol/oracle.rs}, consumed +throughout \codemod{src/protocol/phases/}. + +\section{Motivation} + +The Warp paper decomposes its construction as a sequence of interactive +oracle reductions (IORs). In the pure IOR formalism, each IOR's prover is an +abstract Turing machine that recomputes anything it needs; there is no notion +of ``shared state'' across IORs beyond the transcript. This abstraction is +sound but imposes an unpleasant cost on any implementation: several distinct +IORs in Warp --- the twin-constraint sumcheck, OOD sampling, the batching +sumcheck, and the proximity-query phase --- all operate on \emph{the same +codeword} \(f\), its multilinear extension \(\MLE{f}\), and its Merkle +commitment. A literal reading of the paper forces the code either to +rematerialise these objects phase by phase (wasteful) or to smuggle them +across phase boundaries as ambient state (unprincipled). + +The BCS formalism already provides the right object to legitimise the +sharing: an \emph{oracle message}. BCS treats prover oracles as typed, +persistent objects that can be queried in any subsequent round. What the +paper presently leaves implicit is that \emph{composed} IORs can export and +import these oracles as typed inputs and outputs. + +This modification promotes Oracle to a first-class input/output type of every +Warp IOR. No new soundness argument is required; the BCS compiler already +handles the Merkle-commitment bookkeeping. What changes is the presentation, +which in turn legitimises a direct Rust implementation in +\codemod{src/protocol/oracle.rs}. + +\section{Definition} + +An \emph{oracle} \(\Oracle{f}\) carries the following views of a single +object: +\begin{itemize} + \item \(f : \idx{n} \to \F\) is an evaluation table (the \emph{BCS-native} + view); + \item \(\MLE{f} : \F^{\log n} \to \F\) is the unique multilinear polynomial + over \(\bool{\log n}\) agreeing with \(f\) after identifying + \(\idx{n} \leftrightarrow \bool{\log n}\) by binary expansion; + \item under BCS compilation, \(f\) is committed via a Merkle root + \(\Commit{f}\); this commitment is produced by a separate + \emph{commit} operation and is not a field of \(\Oracle{f}\). +\end{itemize} + +\(\MLE{f}\) is \emph{implied} by \(f\); the prover materialises it lazily and +caches. See \codemod{src/protocol/oracle.rs}, method \texttt{query\_at\_point}. + +\paragraph{Implementation note: commitments and oracles are not 1:1.} Warp's +\Pesat{} phase interleaves \(l_1\) codewords into a single Merkle tree +(\codemod{src/crypto/merkle/mod.rs}, \texttt{build\_codeword\_leaves}), so one +commitment covers many oracles. The Rust \codemod{Oracle} struct therefore +stores only \(f\) and the lazy \(\MLE{f}\); the Merkle tree implementing +\(\Commit{f}\) is tracked by the enclosing data structure +(\codemod{src/types.rs} --- \texttt{PesatOutput}, \texttt{AccumulatorWitness}). +This is an orthogonal implementation choice; the IOR composition rule in +\S4 is unchanged. + +\section{Query interfaces} + +Two legal query operations on \(\Oracle{f}\): + +\begin{description} + \item[Index query.] \(f[i]\) for \(i \in \idx{n}\). BCS-native. + The verifier receives \(f[i]\) alongside a Merkle opening against + \(\Commit{f}\); the BCS compiler makes this non-interactive via + Fiat--Shamir. Corresponds to + \codemod{src/protocol/oracle.rs}, method \texttt{query\_at\_leaf}. + \item[Point query.] \(\MLE{f}(\zeta)\) for \(\zeta \in \F^{\log n}\). + The prover answers directly; the verifier's check that the answer is + consistent with \(\Commit{f}\) is deferred to a downstream IOR (in + Warp, the batching sumcheck). Corresponds to + \codemod{src/protocol/oracle.rs}, method \texttt{query\_at\_point}. +\end{description} + +Both operations act on \emph{the same} oracle; a downstream IOR is free to +mix query kinds. + +\section{Composition rule} + +If IOR \(A\) has signature +\(\; (\text{stmt}_A, \text{wit}_A) \;\mapsto\; (\text{stmt}_A', \Oracle{f})\) +and IOR \(B\) has signature +\(\; (\text{stmt}_B, \Oracle{f}) \;\mapsto\; \text{stmt}_B'\), +then the sequential composition \(A; B\) has signature +\(\; (\text{stmt}_A, \text{stmt}_B, \text{wit}_A) \;\mapsto\; (\text{stmt}_A', \text{stmt}_B') \). +Soundness follows from the BCS composition theorem for oracle messages: the +Merkle commitment \(\Commit{f}\) is produced once by \(A\) and reused by any +number of downstream IORs. + +\section{Application to Warp} + +Every Warp phase can be restated in terms of oracles: + +\begin{center} +\begin{tabular}{l l} +\toprule +Phase & Oracle role \\ +\midrule +Encode-and-commit & emits \(\Oracle{f}\) per fresh witness \\ +\Pesat & emits \(\Oracle{u}\) for accumulated + fresh codewords \\ +\TwinConstraint & consumes \(\Oracle{u}\), emits reduced claim \\ +\OOD & point queries on \(\Oracle{f}\) \\ +\Batching & consumes \(\Oracle{f}\); emits new \(\Oracle{f}'\) via fold \\ +\Proximity & index queries on \(\Oracle{f}\) with auth paths \\ +\bottomrule +\end{tabular} +\end{center} + +The same object carries the codeword, the multilinear extension, and the +Merkle tree; downstream phases access whichever view the operation requires. +In Rust this is literally one struct (\codemod{src/protocol/oracle.rs}). + +\section{What this does not change} + +Soundness proofs, parameter choices, and the transcript layout are unchanged. +Modification 1 is presentational: it makes the existing data flow type-check +in a paper that stays inside BCS rather than retreating to AHP. + +\end{document} diff --git a/docs/paper-mods/mod2_structured_sumcheck.tex b/docs/paper-mods/mod2_structured_sumcheck.tex new file mode 100644 index 0000000..c2128c7 --- /dev/null +++ b/docs/paper-mods/mod2_structured_sumcheck.tex @@ -0,0 +1,34 @@ +\documentclass{article} +\input{notation.tex} + +\title{Modification 2: \\ Structured-sumcheck primitive (stub)} +\author{Warp paper-mods} +\date{} + +\begin{document} +\maketitle + +\paragraph{Status.} Stub. Full spec deferred to Plan B' in the multi-plan +roadmap. Paired code: refactor of +\codemod{src/protocol/phases/twin_constraint.rs} once authored. + +\section*{Abstract} + +Warp's twin-constraint sumcheck reduces the claim +\(\sum_i \tau(i) \cdot (f(i) + \omega \cdot p(i)) = 0\) +where \(f\) and \(p\) are themselves defined as protogalaxy folds over +\(\alpha\) and \(\beta\), respectively. At the paper level this is presented +as a single \Sumcheck{} IOR; at the code level, the prover fuses the folds +across rounds to share passes over tables, which is what lets the phase +dominate only \(\sim 55\%\) of prover time rather than \(\sim 4\times\) +that under the naive sequencing. + +Modification 2 promotes the structure \emph{sum of products of linear +folds} to a first-class IOR primitive in the paper, in the style of +HyperPlonk's \emph{ZeroCheck}. Both Warp's twin-constraint sumcheck and +the batching sumcheck become instances. The primitive justifies the +fusion at the paper level; the code then exposes a +\texttt{StructuredSumcheck} trait whose instances correspond to the +paper's variants. See Plan B' for the full treatment. + +\end{document} diff --git a/docs/paper-mods/mod3_accumulator_state.tex b/docs/paper-mods/mod3_accumulator_state.tex new file mode 100644 index 0000000..dddc6d4 --- /dev/null +++ b/docs/paper-mods/mod3_accumulator_state.tex @@ -0,0 +1,30 @@ +\documentclass{article} +\input{notation.tex} + +\title{Modification 3: \\ Accumulator as explicit IOR state (stub)} +\author{Warp paper-mods} +\date{} + +\begin{document} +\maketitle + +\paragraph{Status.} Stub. Full spec deferred to Plan C in the multi-plan +roadmap. Paired code: signature-level refactor across +\codemod{src/protocol/phases/} once authored. + +\section*{Abstract} + +The paper presently treats the accumulator \((\AccX, \AccW)\) partly as an +object the protocol operates on and partly as ambient state read and written +by each phase. Modification 3 makes the accumulator an explicit pre- and +post-condition of every IOR: +\[ + \IOR{Phase}\ :\ (\AccState, \text{input}) \;\longmapsto\; (\AccState', \text{output}). +\] +This matches Nova-style folding-scheme presentation and lets the composition +theorem be stated mechanically. At the code level it gives every phase +function the same \texttt{AccState} parameter and return and makes prover +and verifier phases exact structural duals. See Plan C for the full +treatment. + +\end{document} diff --git a/docs/paper-mods/mod4_parameter_selection.tex b/docs/paper-mods/mod4_parameter_selection.tex new file mode 100644 index 0000000..1ec203b --- /dev/null +++ b/docs/paper-mods/mod4_parameter_selection.tex @@ -0,0 +1,90 @@ +\documentclass{article} +\input{notation.tex} + +\title{Modification 4: \\ Soundness-driven parameter selection} +\author{Warp paper-mods} +\date{} + +\begin{document} +\maketitle + +\paragraph{Paired Rust module.} \codemod{src/params/}, with a CLI front-end +at \codemod{src/bin/warp-params.rs}. + +\section{Motivation} + +The paper states the soundness bound of Warp symbolically; the implementation +has so far hard-coded parameter values (for instance, \(s = 8\), \(t = 7\) +in the regression tests). There is no record of which security target those +choices correspond to, nor is there a way to ask the code for the minimum +parameter tuple that hits a chosen target \(\lambda\). + +Modification 4 adds, on both sides, an \emph{inverse} to the soundness +analysis. Given +\(\lambda \in \mathbb{N}\), field size \(\size{\F}\), Reed--Solomon code rate +\(\rho = k/n\), and a choice of \emph{list-decoding regime}, produce the +smallest parameter tuple \((s,\ t)\) that provably (or conjecturally) gives +\(\lambda\) bits of soundness. The workload parameters \((l,\ l_1)\) remain +caller-chosen; Modification 4 does not select them. + +\section{Regimes} + +\begin{description} + \item[\IOR{Provable}.] Each proximity query rejects a word at distance + \(e\) from the nearest codeword with probability at least \(e\). + Taking the Johnson bound \(e = 1 - \sqrt{\rho}\) as the largest + value supported by a proof for Reed--Solomon codes, the soundness + error of \(t\) independent proximity queries is at most + \(\bigl(1 - e\bigr)^{t} = \rho^{t/2}\). Therefore + \[ + t \;\geq\; \frac{2\lambda}{\log_2(1/\rho)}. + \] + \item[\IOR{Conjectured}.] The list-decodability conjecture for + Reed--Solomon codes extends the argument up to radius + \(e = 1 - \rho\). The same calculation gives + \[ + t \;\geq\; \frac{\lambda}{\log_2(1/\rho)}. + \] +\end{description} + +For \(s\) (OOD samples) we use the conservative constant \(s = 8\), +which dominates the round-complexity of batching without materially +affecting soundness at the scales we run. A tighter bound is deferred to +a follow-up; the current code documents this choice explicitly. + +\section{Field-size admissibility} + +Several other soundness terms are each at most +\(\mathrm{polylog}(\cdot) / \size{\F}\) (sumcheck rounds, zero-check +randomness absorbs, transcript-squeezed challenges). They are negligible +provided \(\log_2 \size{\F} \geq \lambda + \epsilon\) for a small constant +\(\epsilon\); the code rejects any choice that violates this admissibility. + +\section{References used, and scope limits} + +The formulas above are the standard proximity-gap / list-decoding bounds +from the Reed--Solomon STARK lineage, in the precise formulation used in +STIR~\cite{STIR} and WHIR~\cite{WHIR}. They are \emph{not} a full +re-derivation of the Warp paper's soundness proof; they capture the +dominant term (proximity queries) and ignore polylogarithmic noise +controlled by the field-size check. The Rust module +\codemod{src/params/} uses the same formulas and marks every constant +it uses, so refining against the paper's exact proof reduces to editing +one table. + +\paragraph{Deferred.} Calibration of \(s\) against the batching-sumcheck +soundness, derivation for non-Reed--Solomon codes, and a reference +table of attested parameter tuples with matching proofs. + +\begin{thebibliography}{9} +\bibitem{STIR} + Arnon, Chiesa, Fenzi, Yogev. + \emph{STIR: Reed--Solomon Proximity Testing with Fewer Queries.} + CRYPTO 2024. +\bibitem{WHIR} + Arnon, Chiesa, Fenzi, Yogev. + \emph{WHIR: Reed--Solomon Proximity Testing with Super-Fast Verification.} + 2024. +\end{thebibliography} + +\end{document} diff --git a/docs/paper-mods/notation.tex b/docs/paper-mods/notation.tex new file mode 100644 index 0000000..de81c81 --- /dev/null +++ b/docs/paper-mods/notation.tex @@ -0,0 +1,58 @@ +% notation.tex --- shared preamble for Warp paper-mods. +% +% Every mod*.tex file begins with: +% +% \input{notation.tex} +% +% Conventions +% ----------- +% * Framing: IOP in the sense of Ben-Sasson--Chiesa--Spooner (BCS, TCC 2016), +% NOT Algebraic Holographic Proofs. Oracles are functions $f:[n] \to \F$, +% queried by index; multilinear-extension semantics layer on top for point +% queries. +% * Every modification file cites the Rust module path it pairs with. + +\usepackage{amsmath,amssymb,amsthm} +\usepackage{mathtools} +\usepackage{hyperref} + +% ---- Fields, indexing, domains ------------------------------------------- +\newcommand{\F}{\mathbb{F}} +\newcommand{\idx}[1]{\ensuremath{[#1]}} % index set [n] +\newcommand{\bool}[1]{\ensuremath{\{0,1\}^{#1}}} % boolean hypercube +\newcommand{\size}[1]{\ensuremath{\left\lvert#1\right\rvert}} + +% ---- Oracles (Modification 1) -------------------------------------------- +% An Oracle carries three views of the same object: +% (i) an evaluation table f : [n] -> F (BCS-native) +% (ii) a multilinear extension \hat f : F^{log n} -> F +% (iii) a commitment handle (Merkle root under BCS compilation) +% See mod1_oracle.tex. +\newcommand{\Oracle}[1]{\ensuremath{\mathsf{O}[#1]}} +\newcommand{\MLE}[1]{\ensuremath{\widehat{#1}}} +\newcommand{\Commit}[1]{\ensuremath{\mathsf{com}(#1)}} +\newcommand{\query}{\ensuremath{\mathsf{query}}} % generic query operator + +% ---- Accumulator state (Modification 3, reserved) ------------------------- +\newcommand{\AccX}{\ensuremath{\mathsf{acc}_x}} % accumulator instance +\newcommand{\AccW}{\ensuremath{\mathsf{acc}_w}} % accumulator witness +\newcommand{\AccState}{\ensuremath{(\AccX, \AccW)}} + +% ---- Common structural IORs ---------------------------------------------- +\newcommand{\IOR}[1]{\textsc{#1}} +\newcommand{\Zerocheck}{\IOR{Zerocheck}} +\newcommand{\Sumcheck}{\IOR{Sumcheck}} +\newcommand{\Pesat}{\IOR{Pesat}} +\newcommand{\TwinConstraint}{\IOR{TwinConstraint}} +\newcommand{\Proximity}{\IOR{Proximity}} +\newcommand{\Batching}{\IOR{Batching}} +\newcommand{\OOD}{\IOR{OOD}} + +% ---- Transcript notation -------------------------------------------------- +\newcommand{\absorb}{\ensuremath{\lhd}} % absorb into transcript +\newcommand{\squeeze}{\ensuremath{\rhd}} % derive from transcript + +% ---- Code / module cross-reference --------------------------------------- +% Use \codemod{src/protocol/oracle.rs} in .tex files to point at a Rust +% module. Renders as inline typewriter with a URL when compiled with hyperref. +\newcommand{\codemod}[1]{\href{../../#1}{\texttt{#1}}} diff --git a/docs/paper-mods/slides/iors.aux b/docs/paper-mods/slides/iors.aux new file mode 100644 index 0000000..f70d6f4 --- /dev/null +++ b/docs/paper-mods/slides/iors.aux @@ -0,0 +1,53 @@ +\relax +\providecommand\hyper@newdestlabel[2]{} +\providecommand\HyperFirstAtBeginDocument{\AtBeginDocument} +\HyperFirstAtBeginDocument{\ifx\hyper@anchor\@undefined +\global\let\oldnewlabel\newlabel +\gdef\newlabel#1#2{\newlabelxx{#1}#2} +\gdef\newlabelxx#1#2#3#4#5#6{\oldnewlabel{#1}{{#2}{#3}}} +\AtEndDocument{\ifx\hyper@anchor\@undefined +\let\newlabel\oldnewlabel +\fi} +\fi} +\global\let\hyper@last\relax +\gdef\HyperFirstAtBeginDocument#1{#1} +\providecommand\HyField@AuxAddToFields[1]{} +\providecommand\HyField@AuxAddToCoFields[2]{} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{1}{1/1}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {1}{1}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{2}{2/2}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {2}{2}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{3}{3/3}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {3}{3}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{4}{4/4}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {4}{4}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{5}{5/5}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {5}{5}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{6}{6/6}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {6}{6}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{7}{7/7}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {7}{7}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{8}{8/8}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {8}{8}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{9}{9/9}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {9}{9}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{10}{10/10}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {10}{10}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{11}{11/11}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {11}{11}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{12}{12/12}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {12}{12}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{13}{13/13}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {13}{13}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{14}{14/14}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {14}{14}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{15}{15/15}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {15}{15}}} +\@writefile{nav}{\headcommand {\slideentry {0}{0}{16}{16/16}{}{0}}} +\@writefile{nav}{\headcommand {\beamer@framepages {16}{16}}} +\@writefile{nav}{\headcommand {\beamer@partpages {1}{16}}} +\@writefile{nav}{\headcommand {\beamer@subsectionpages {1}{16}}} +\@writefile{nav}{\headcommand {\beamer@sectionpages {1}{16}}} +\@writefile{nav}{\headcommand {\beamer@documentpages {16}}} +\@writefile{nav}{\headcommand {\gdef \inserttotalframenumber {15}}} +\gdef \@abspage@last{16} diff --git a/docs/paper-mods/slides/iors.log b/docs/paper-mods/slides/iors.log new file mode 100644 index 0000000..e2215ff --- /dev/null +++ b/docs/paper-mods/slides/iors.log @@ -0,0 +1,1855 @@ +This is LuaHBTeX, Version 1.16.0 (TeX Live 2023) (format=lualatex 2024.1.16) 3 MAY 2026 20:04 + restricted system commands enabled. +**iors.tex +(./iors.tex +LaTeX2e <2022-11-01> patch level 1 +Lua module: luaotfload 2022-10-03 3.23 Lua based OpenType font support +Lua module: lualibs 2022-10-04 2.75 ConTeXt Lua standard libraries. +Lua module: lualibs-extended 2022-10-04 2.75 ConTeXt Lua libraries -- extended c +ollection. +luaotfload | conf : Root cache directory is "/Users/zitek/Library/texlive/2023/t +exmf-var/luatex-cache/generic/names". +luaotfload | init : Loading fontloader "fontloader-2022-10-03.lua" from kpse-res +olved path "/usr/local/texlive/2023/texmf-dist/tex/luatex/luaotfload/fontloader- +2022-10-03.lua". +Lua-only attribute luaotfload@noligature = 1 +luaotfload | init : Context OpenType loader version 3.120 +Inserting `luaotfload.node_processor' in `pre_linebreak_filter'. +Inserting `luaotfload.node_processor' in `hpack_filter'. +Inserting `luaotfload.glyph_stream' in `glyph_stream_provider'. +Inserting `luaotfload.define_font' in `define_font'. +Lua-only attribute luaotfload_color_attribute = 2 +luaotfload | conf : Root cache directory is "/Users/zitek/Library/texlive/2023/t +exmf-var/luatex-cache/generic/names". +Inserting `luaotfload.harf.strip_prefix' in `find_opentype_file'. +Inserting `luaotfload.harf.strip_prefix' in `find_truetype_file'. +Removing `luaotfload.glyph_stream' from `glyph_stream_provider'. +Inserting `luaotfload.harf.glyphstream' in `glyph_stream_provider'. +Inserting `luaotfload.harf.finalize_vlist' in `post_linebreak_filter'. +Inserting `luaotfload.harf.finalize_hlist' in `hpack_filter'. +Inserting `luaotfload.cleanup_files' in `wrapup_run'. +Inserting `luaotfload.harf.finalize_unicode' in `finish_pdffile'. +Inserting `luaotfload.glyphinfo' in `glyph_info'. +Lua-only attribute luaotfload.letterspace_done = 3 +Inserting `luaotfload.aux.set_sscale_dimens' in `luaotfload.patch_font'. +Inserting `luaotfload.aux.set_font_index' in `luaotfload.patch_font'. +Inserting `luaotfload.aux.patch_cambria_domh' in `luaotfload.patch_font'. +Inserting `luaotfload.aux.fixup_fontdata' in `luaotfload.patch_font_unsafe'. +Inserting `luaotfload.aux.set_capheight' in `luaotfload.patch_font'. +Inserting `luaotfload.aux.set_xheight' in `luaotfload.patch_font'. +Inserting `luaotfload.rewrite_fontname' in `luaotfload.patch_font'. L3 programm +ing layer <2023-02-22> +Inserting `tracingstacklevels' in `input_level_string'. +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamer.cls +Document Class: beamer 2023/02/20 v3.69 A class for typesetting presentations +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasemodes.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/etoolbox/etoolbox.sty +Package: etoolbox 2020/10/05 v2.5k e-TeX tools for LaTeX (JAW) +\etb@tempcnta=\count183 +) +\beamer@tempbox=\box51 +\beamer@tempcount=\count184 +\c@beamerpauses=\count185 + +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasedecode.sty +\beamer@slideinframe=\count186 +\beamer@minimum=\count187 +\beamer@decode@box=\box52 +) +\beamer@commentbox=\box53 +\beamer@modecount=\count188 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/iftex/iftex.sty +Package: iftex 2022/02/03 v1.0f TeX engine tests +) +\headdp=\dimen139 +\footheight=\dimen140 +\sidebarheight=\dimen141 +\beamer@tempdim=\dimen142 +\beamer@finalheight=\dimen143 +\beamer@animht=\dimen144 +\beamer@animdp=\dimen145 +\beamer@animwd=\dimen146 +\beamer@leftmargin=\dimen147 +\beamer@rightmargin=\dimen148 +\beamer@leftsidebar=\dimen149 +\beamer@rightsidebar=\dimen150 +\beamer@boxsize=\dimen151 +\beamer@vboxoffset=\dimen152 +\beamer@descdefault=\dimen153 +\beamer@descriptionwidth=\dimen154 +\beamer@lastskip=\skip48 +\beamer@areabox=\box54 +\beamer@animcurrent=\box55 +\beamer@animshowbox=\box56 +\beamer@sectionbox=\box57 +\beamer@logobox=\box58 +\beamer@linebox=\box59 +\beamer@sectioncount=\count189 +\beamer@subsubsectionmax=\count190 +\beamer@subsectionmax=\count191 +\beamer@sectionmax=\count192 +\beamer@totalheads=\count193 +\beamer@headcounter=\count194 +\beamer@partstartpage=\count195 +\beamer@sectionstartpage=\count196 +\beamer@subsectionstartpage=\count197 +\beamer@animationtempa=\count198 +\beamer@animationtempb=\count199 +\beamer@xpos=\count266 +\beamer@ypos=\count267 +\beamer@ypos@offset=\count268 +\beamer@showpartnumber=\count269 +\beamer@currentsubsection=\count270 +\beamer@coveringdepth=\count271 +\beamer@sectionadjust=\count272 +\beamer@toclastsection=\count273 +\beamer@tocsectionnumber=\count274 + +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbaseoptions.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/graphics/keyval.sty +Package: keyval 2022/05/29 v1.15 key=value parser (DPC) +\KV@toks@=\toks16 +)) +\beamer@paperwidth=\skip49 +\beamer@paperheight=\skip50 + +(/usr/local/texlive/2023/texmf-dist/tex/latex/geometry/geometry.sty +Package: geometry 2020/01/02 v5.9 Page Geometry + +(/usr/local/texlive/2023/texmf-dist/tex/generic/iftex/ifvtex.sty +Package: ifvtex 2019/10/25 v1.7 ifvtex legacy package. Use iftex instead. +) +\Gm@cnth=\count275 +\Gm@cntv=\count276 +\c@Gm@tempcnt=\count277 +\Gm@bindingoffset=\dimen155 +\Gm@wd@mp=\dimen156 +\Gm@odd@mp=\dimen157 +\Gm@even@mp=\dimen158 +\Gm@layoutwidth=\dimen159 +\Gm@layoutheight=\dimen160 +\Gm@layouthoffset=\dimen161 +\Gm@layoutvoffset=\dimen162 +\Gm@dimlist=\toks17 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/pgf/math/pgfmath.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/utilities/pgfutil-common.te +x +\pgfutil@everybye=\toks18 +\pgfutil@tempdima=\dimen163 +\pgfutil@tempdimb=\dimen164 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/utilities/pgfutil-latex.def +\pgfutil@abb=\box60 +) (/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/pgf.revision.tex) +Package: pgfrcs 2023-01-15 v3.1.10 (3.1.10) +)) +(/usr/local/texlive/2023/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex +\pgfkeys@pathtoks=\toks19 +\pgfkeys@temptoks=\toks20 + +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/utilities/pgfkeyslibraryfil +tered.code.tex +\pgfkeys@tmptoks=\toks21 +))) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathutil.code.tex +\pgf@x=\dimen165 +\pgf@xa=\dimen166 +\pgf@xb=\dimen167 +\pgf@xc=\dimen168 +\pgf@y=\dimen169 +\pgf@ya=\dimen170 +\pgf@yb=\dimen171 +\pgf@yc=\dimen172 +\c@pgf@counta=\count278 +\c@pgf@countb=\count279 +\c@pgf@countc=\count280 +\c@pgf@countd=\count281 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathparser.code.tex +\pgfmath@dimen=\dimen173 +\pgfmath@count=\count282 +\pgfmath@box=\box61 +\pgfmath@toks=\toks22 +\pgfmath@stack@operand=\toks23 +\pgfmath@stack@operation=\toks24 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.code. +tex) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.basic +.code.tex) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.trigo +nometric.code.tex) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.rando +m.code.tex) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.compa +rison.code.tex) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.base. +code.tex) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.round +.code.tex) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.misc. +code.tex) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.integ +erarithmetics.code.tex) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathcalc.code.tex) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfmathfloat.code.tex +\c@pgfmathroundto@lastzeros=\count283 +))) (/usr/local/texlive/2023/texmf-dist/tex/latex/base/size10.clo +File: size10.clo 2022/07/02 v1.4n Standard LaTeX file (size option) +luaotfload | db : Font names database loaded from /Users/zitek/Library/texlive/2 +023/texmf-var/luatex-cache/generic/names/luaotfload-names.luc.gz) +(/usr/local/texlive/2023/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/graphics/graphicx.sty +Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR) + +(/usr/local/texlive/2023/texmf-dist/tex/latex/graphics/graphics.sty +Package: graphics 2022/03/10 v1.4e Standard LaTeX Graphics (DPC,SPQR) + +(/usr/local/texlive/2023/texmf-dist/tex/latex/graphics/trig.sty +Package: trig 2021/08/11 v1.11 sin cos tan (DPC) +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/graphics-cfg/graphics.cfg +File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration +) +Package graphics Info: Driver file: luatex.def on input line 107. + +(/usr/local/texlive/2023/texmf-dist/tex/latex/graphics-def/luatex.def +File: luatex.def 2022/09/22 v1.2d Graphics/color driver for luatex +)) +\Gin@req@height=\dimen174 +\Gin@req@width=\dimen175 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex +Package: pgfsys 2023-01-15 v3.1.10 (3.1.10) +\pgf@x=\dimen176 +\pgf@y=\dimen177 +\pgf@xa=\dimen178 +\pgf@ya=\dimen179 +\pgf@xb=\dimen180 +\pgf@yb=\dimen181 +\pgf@xc=\dimen182 +\pgf@yc=\dimen183 +\pgf@xd=\dimen184 +\pgf@yd=\dimen185 +\w@pgf@writea=\write3 +\r@pgf@reada=\read2 +\c@pgf@counta=\count284 +\c@pgf@countb=\count285 +\c@pgf@countc=\count286 +\c@pgf@countd=\count287 +\t@pgf@toka=\toks25 +\t@pgf@tokb=\toks26 +\t@pgf@tokc=\toks27 +\pgf@sys@id@count=\count288 +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/systemlayer/pgf.cfg +File: pgf.cfg 2023-01-15 v3.1.10 (3.1.10) +) +Driver file for pgf: pgfsys-luatex.def + +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-luatex.d +ef +File: pgfsys-luatex.def 2023-01-15 v3.1.10 (3.1.10) + +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-common-p +df.def +File: pgfsys-common-pdf.def 2023-01-15 v3.1.10 (3.1.10) +))) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath. +code.tex +File: pgfsyssoftpath.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgfsyssoftpath@smallbuffer@items=\count289 +\pgfsyssoftpath@bigbuffer@items=\count290 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol. +code.tex +File: pgfsysprotocol.code.tex 2023-01-15 v3.1.10 (3.1.10) +)) (/usr/local/texlive/2023/texmf-dist/tex/latex/xcolor/xcolor.sty +Package: xcolor 2022/06/12 v2.14 LaTeX color extensions (UK) + +(/usr/local/texlive/2023/texmf-dist/tex/latex/graphics-cfg/color.cfg +File: color.cfg 2016/01/02 v1.6 sample color configuration +) +Package xcolor Info: Driver file: luatex.def on input line 227. + +(/usr/local/texlive/2023/texmf-dist/tex/latex/graphics/mathcolor.ltx) +Package xcolor Info: Model `cmy' substituted by `cmy0' on input line 1353. +Package xcolor Info: Model `hsb' substituted by `rgb' on input line 1357. +Package xcolor Info: Model `RGB' extended on input line 1369. +Package xcolor Info: Model `HTML' substituted by `rgb' on input line 1371. +Package xcolor Info: Model `Hsb' substituted by `hsb' on input line 1372. +Package xcolor Info: Model `tHsb' substituted by `hsb' on input line 1373. +Package xcolor Info: Model `HSB' substituted by `hsb' on input line 1374. +Package xcolor Info: Model `Gray' substituted by `gray' on input line 1375. +Package xcolor Info: Model `wave' substituted by `hsb' on input line 1376. +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex +Package: pgfcore 2023-01-15 v3.1.10 (3.1.10) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/math/pgfint.code.tex) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepoints.co +de.tex +File: pgfcorepoints.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgf@picminx=\dimen186 +\pgf@picmaxx=\dimen187 +\pgf@picminy=\dimen188 +\pgf@picmaxy=\dimen189 +\pgf@pathminx=\dimen190 +\pgf@pathmaxx=\dimen191 +\pgf@pathminy=\dimen192 +\pgf@pathmaxy=\dimen193 +\pgf@xx=\dimen194 +\pgf@xy=\dimen195 +\pgf@yx=\dimen196 +\pgf@yy=\dimen197 +\pgf@zx=\dimen198 +\pgf@zy=\dimen199 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathconst +ruct.code.tex +File: pgfcorepathconstruct.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgf@path@lastx=\dimen256 +\pgf@path@lasty=\dimen257 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathusage +.code.tex +File: pgfcorepathusage.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgf@shorten@end@additional=\dimen258 +\pgf@shorten@start@additional=\dimen259 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcorescopes.co +de.tex +File: pgfcorescopes.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgfpic=\box62 +\pgf@hbox=\box63 +\pgf@layerbox@main=\box64 +\pgf@picture@serial@count=\count291 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcoregraphicst +ate.code.tex +File: pgfcoregraphicstate.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgflinewidth=\dimen260 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransform +ations.code.tex +File: pgfcoretransformations.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgf@pt@x=\dimen261 +\pgf@pt@y=\dimen262 +\pgf@pt@temp=\dimen263 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcorequick.cod +e.tex +File: pgfcorequick.code.tex 2023-01-15 v3.1.10 (3.1.10) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreobjects.c +ode.tex +File: pgfcoreobjects.code.tex 2023-01-15 v3.1.10 (3.1.10) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathproce +ssing.code.tex +File: pgfcorepathprocessing.code.tex 2023-01-15 v3.1.10 (3.1.10) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcorearrows.co +de.tex +File: pgfcorearrows.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgfarrowsep=\dimen264 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreshade.cod +e.tex +File: pgfcoreshade.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgf@max=\dimen265 +\pgf@sys@shading@range@num=\count292 +\pgf@shadingcount=\count293 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreimage.cod +e.tex +File: pgfcoreimage.code.tex 2023-01-15 v3.1.10 (3.1.10) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreexternal. +code.tex +File: pgfcoreexternal.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgfexternal@startupbox=\box65 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcorelayers.co +de.tex +File: pgfcorelayers.code.tex 2023-01-15 v3.1.10 (3.1.10) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretranspare +ncy.code.tex +File: pgfcoretransparency.code.tex 2023-01-15 v3.1.10 (3.1.10) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepatterns. +code.tex +File: pgfcorepatterns.code.tex 2023-01-15 v3.1.10 (3.1.10) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/basiclayer/pgfcorerdf.code. +tex +File: pgfcorerdf.code.tex 2023-01-15 v3.1.10 (3.1.10) +))) (/usr/local/texlive/2023/texmf-dist/tex/latex/pgf/utilities/xxcolor.sty +Package: xxcolor 2003/10/24 ver 0.1 +\XC@nummixins=\count294 +\XC@countmixins=\count295 +) (/usr/local/texlive/2023/texmf-dist/tex/latex/base/atbegshi-ltx.sty +Package: atbegshi-ltx 2021/01/10 v1.0c Emulation of the original atbegshi +package with kernel methods +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/hyperref/hyperref.sty +Package: hyperref 2023-02-07 v7.00v Hypertext links for LaTeX + +(/usr/local/texlive/2023/texmf-dist/tex/generic/ltxcmds/ltxcmds.sty +Package: ltxcmds 2020-05-10 v1.25 LaTeX kernel commands for general use (HO) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pdftexcmds/pdftexcmds.sty +Package: pdftexcmds 2020-06-27 v0.33 Utility functions of pdfTeX for LuaTeX (HO +) + +(/usr/local/texlive/2023/texmf-dist/tex/generic/infwarerr/infwarerr.sty +Package: infwarerr 2019/12/03 v1.5 Providing info/warning/error messages (HO) +) +Package pdftexcmds Info: \pdf@primitive is available. +Package pdftexcmds Info: \pdf@ifprimitive is available. +Package pdftexcmds Info: \pdfdraftmode found. +\pdftexcmds@toks=\toks28 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/kvsetkeys/kvsetkeys.sty +Package: kvsetkeys 2022-10-05 v1.19 Key value parser (HO) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/kvdefinekeys/kvdefinekeys.sty +Package: kvdefinekeys 2019-12-19 v1.6 Define keys (HO) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pdfescape/pdfescape.sty +Package: pdfescape 2019/12/09 v1.15 Implements pdfTeX's escape features (HO) +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/hycolor/hycolor.sty +Package: hycolor 2020-01-27 v1.10 Color options for hyperref/bookmark (HO) +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/letltxmacro/letltxmacro.sty +Package: letltxmacro 2019/12/03 v1.6 Let assignment for LaTeX macros (HO) +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/auxhook/auxhook.sty +Package: auxhook 2019-12-17 v1.6 Hooks for auxiliary files (HO) +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/hyperref/nameref.sty +Package: nameref 2022-05-17 v2.50 Cross-referencing by name of section + +(/usr/local/texlive/2023/texmf-dist/tex/latex/refcount/refcount.sty +Package: refcount 2019/12/15 v3.6 Data extraction from label references (HO) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/gettitlestring/gettitlestring.s +ty +Package: gettitlestring 2019/12/15 v1.6 Cleanup title references (HO) + (/usr/local/texlive/2023/texmf-dist/tex/latex/kvoptions/kvoptions.sty +Package: kvoptions 2022-06-15 v3.15 Key value format for package options (HO) +)) +\c@section@level=\count296 +) +\@linkdim=\dimen266 +\Hy@linkcounter=\count297 +\Hy@pagecounter=\count298 + +(/usr/local/texlive/2023/texmf-dist/tex/latex/hyperref/pd1enc.def +File: pd1enc.def 2023-02-07 v7.00v Hyperref: PDFDocEncoding definition (HO) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/intcalc/intcalc.sty +Package: intcalc 2019/12/15 v1.3 Expandable calculations with integers (HO) +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/etexcmds/etexcmds.sty +Package: etexcmds 2019/12/15 v1.7 Avoid name clashes with e-TeX commands (HO) +) +\Hy@SavedSpaceFactor=\count299 + +(/usr/local/texlive/2023/texmf-dist/tex/latex/hyperref/puenc.def +File: puenc.def 2023-02-07 v7.00v Hyperref: PDF Unicode definition (HO) +) +Package hyperref Info: Option `bookmarks' set `true' on input line 4060. +Package hyperref Info: Option `bookmarksopen' set `true' on input line 4060. +Package hyperref Info: Option `implicit' set `false' on input line 4060. +Package hyperref Info: Hyper figures OFF on input line 4177. +Package hyperref Info: Link nesting OFF on input line 4182. +Package hyperref Info: Hyper index ON on input line 4185. +Package hyperref Info: Plain pages OFF on input line 4192. +Package hyperref Info: Backreferencing OFF on input line 4197. +Package hyperref Info: Implicit mode OFF; no redefinition of LaTeX internals. +Package hyperref Info: Bookmarks ON on input line 4425. +\c@Hy@tempcnt=\count300 + +(/usr/local/texlive/2023/texmf-dist/tex/latex/url/url.sty +\Urlmuskip=\muskip16 +Package: url 2013/09/16 ver 3.4 Verb mode for urls, etc. +) +LaTeX Info: Redefining \url on input line 4763. +\XeTeXLinkMargin=\dimen267 + +(/usr/local/texlive/2023/texmf-dist/tex/generic/bitset/bitset.sty +Package: bitset 2019/12/09 v1.3 Handle bit-vector datatype (HO) + +(/usr/local/texlive/2023/texmf-dist/tex/generic/bigintcalc/bigintcalc.sty +Package: bigintcalc 2019/12/15 v1.5 Expandable calculations on big integers (HO +) +)) +\Fld@menulength=\count301 +\Field@Width=\dimen268 +\Fld@charsize=\dimen269 +Package hyperref Info: Hyper figures OFF on input line 6042. +Package hyperref Info: Link nesting OFF on input line 6047. +Package hyperref Info: Hyper index ON on input line 6050. +Package hyperref Info: backreferencing OFF on input line 6057. +Package hyperref Info: Link coloring OFF on input line 6062. +Package hyperref Info: Link coloring with OCG OFF on input line 6067. +Package hyperref Info: PDF/A mode OFF on input line 6072. +\Hy@abspage=\count302 + + +Package hyperref Message: Stopped early. + +) +Package hyperref Info: Driver (autodetected): hluatex. + (/usr/local/texlive/2023/texmf-dist/tex/latex/hyperref/hluatex.def +File: hluatex.def 2023-02-07 v7.00v Hyperref driver for luaTeX + +(/usr/local/texlive/2023/texmf-dist/tex/generic/stringenc/stringenc.sty +Package: stringenc 2019/11/29 v1.12 Convert strings between diff. encodings (HO +) +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/base/atveryend-ltx.sty +Package: atveryend-ltx 2020/08/19 v1.0a Emulation of the original atveryend pac +kage +with kernel methods +) +\Fld@listcount=\count303 +\c@bookmark@seq@number=\count304 + +(/usr/local/texlive/2023/texmf-dist/tex/latex/rerunfilecheck/rerunfilecheck.sty +Package: rerunfilecheck 2022-07-10 v1.10 Rerun checks for auxiliary files (HO) + +(/usr/local/texlive/2023/texmf-dist/tex/generic/uniquecounter/uniquecounter.sty +Package: uniquecounter 2019/12/15 v1.4 Provide unlimited unique counter (HO) +) +Package uniquecounter Info: New unique counter `rerunfilecheck' on input line 2 +85. +)) (/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbaserequires.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasecompatibility.st +y) (/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasefont.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/amsfonts/amssymb.sty +Package: amssymb 2013/01/14 v3.01 AMS font symbols + +(/usr/local/texlive/2023/texmf-dist/tex/latex/amsfonts/amsfonts.sty +Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support +\@emptytoks=\toks29 +\symAMSa=\mathgroup4 +\symAMSb=\mathgroup5 +LaTeX Font Info: Redeclaring math symbol \hbar on input line 98. +LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold' +(Font) U/euf/m/n --> U/euf/b/n on input line 106. +)) +(/usr/local/texlive/2023/texmf-dist/tex/latex/sansmathaccent/sansmathaccent.sty +Package: sansmathaccent 2020/01/31 +(/usr/local/texlive/2023/texmf-dist/tex/latex/koma-script/scrlfile.sty +Package: scrlfile 2022/10/12 v3.38 KOMA-Script package (file load hooks) + +(/usr/local/texlive/2023/texmf-dist/tex/latex/koma-script/scrlfile-hook.sty +Package: scrlfile-hook 2022/10/12 v3.38 KOMA-Script package (using LaTeX hooks) + + +(/usr/local/texlive/2023/texmf-dist/tex/latex/koma-script/scrlogo.sty +Package: scrlogo 2022/10/12 v3.38 KOMA-Script package (logo) +))))) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasetranslator.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/translator/translator.sty +Package: translator 2021-05-31 v1.12d Easy translation of strings in LaTeX +)) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasemisc.sty) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasetwoscreens.sty) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbaseoverlay.sty +\beamer@argscount=\count305 +\beamer@lastskipcover=\skip51 +\beamer@trivlistdepth=\count306 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasetitle.sty) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasesection.sty +\c@lecture=\count307 +\c@part=\count308 +\c@section=\count309 +\c@subsection=\count310 +\c@subsubsection=\count311 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbaseframe.sty +\beamer@framebox=\box66 +\beamer@frametitlebox=\box67 +\beamer@zoombox=\box68 +\beamer@zoomcount=\count312 +\beamer@zoomframecount=\count313 +\beamer@frametextheight=\dimen270 +\c@subsectionslide=\count314 +\beamer@frametopskip=\skip52 +\beamer@framebottomskip=\skip53 +\beamer@frametopskipautobreak=\skip54 +\beamer@framebottomskipautobreak=\skip55 +\beamer@envbody=\toks30 +\framewidth=\dimen271 +\c@framenumber=\count315 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbaseverbatim.sty +\beamer@verbatimfileout=\write4 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbaseframesize.sty +\beamer@splitbox=\box69 +\beamer@autobreakcount=\count316 +\beamer@autobreaklastheight=\dimen272 +\beamer@frametitletoks=\toks31 +\beamer@framesubtitletoks=\toks32 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbaseframecomponents. +sty +\beamer@footins=\box70 +) (/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasecolor.sty) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasenotes.sty +\beamer@frameboxcopy=\box71 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasetoc.sty) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasetemplates.sty +\beamer@sbttoks=\toks33 + +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbaseauxtemplates.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbaseboxes.sty +\bmb@box=\box72 +\bmb@colorbox=\box73 +\bmb@boxwidth=\dimen273 +\bmb@boxheight=\dimen274 +\bmb@prevheight=\dimen275 +\bmb@temp=\dimen276 +\bmb@dima=\dimen277 +\bmb@dimb=\dimen278 +\bmb@prevheight=\dimen279 +) +\beamer@blockheadheight=\dimen280 +)) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbaselocalstructure.s +ty (/usr/local/texlive/2023/texmf-dist/tex/latex/tools/enumerate.sty +Package: enumerate 2015/07/23 v3.00 enumerate extensions (DPC) +\@enLab=\toks34 +) +\beamer@bibiconwidth=\skip56 +\c@figure=\count317 +\c@table=\count318 +\abovecaptionskip=\skip57 +\belowcaptionskip=\skip58 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasenavigation.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasenavigationsymbol +s.tex) +\beamer@section@min@dim=\dimen281 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasetheorems.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/amsmath/amsmath.sty +Package: amsmath 2022/04/08 v2.17n AMS math features +\@mathmargin=\skip59 + +For additional information on amsmath, use the `?' option. +(/usr/local/texlive/2023/texmf-dist/tex/latex/amsmath/amstext.sty +Package: amstext 2021/08/26 v2.01 AMS text + +(/usr/local/texlive/2023/texmf-dist/tex/latex/amsmath/amsgen.sty +File: amsgen.sty 1999/11/30 v2.0 generic functions +\@emptytoks=\toks35 +\ex@=\dimen282 +)) +(/usr/local/texlive/2023/texmf-dist/tex/latex/amsmath/amsbsy.sty +Package: amsbsy 1999/11/29 v1.2d Bold Symbols +\pmbraise@=\dimen283 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/amsmath/amsopn.sty +Package: amsopn 2022/04/08 v2.04 operator names +) +\inf@bad=\count319 +LaTeX Info: Redefining \frac on input line 236. +\uproot@=\count320 +\leftroot@=\count321 +LaTeX Info: Redefining \overline on input line 399. +LaTeX Info: Redefining \colon on input line 410. +\classnum@=\count322 +\DOTSCASE@=\count323 +LaTeX Info: Redefining \ldots on input line 496. +LaTeX Info: Redefining \dots on input line 499. +LaTeX Info: Redefining \cdots on input line 620. +\Mathstrutbox@=\box74 +\strutbox@=\box75 +LaTeX Info: Redefining \big on input line 722. +LaTeX Info: Redefining \Big on input line 723. +LaTeX Info: Redefining \bigg on input line 724. +LaTeX Info: Redefining \Bigg on input line 725. +\big@size=\dimen284 +LaTeX Font Info: Redeclaring font encoding OML on input line 743. +LaTeX Font Info: Redeclaring font encoding OMS on input line 744. +\macc@depth=\count324 +LaTeX Info: Redefining \bmod on input line 905. +LaTeX Info: Redefining \pmod on input line 910. +LaTeX Info: Redefining \smash on input line 940. +LaTeX Info: Redefining \relbar on input line 970. +LaTeX Info: Redefining \Relbar on input line 971. +\c@MaxMatrixCols=\count325 +\dotsspace@=\muskip17 +\c@parentequation=\count326 +\dspbrk@lvl=\count327 +\tag@help=\toks36 +\row@=\count328 +\column@=\count329 +\maxfields@=\count330 +\andhelp@=\toks37 +\eqnshift@=\dimen285 +\alignsep@=\dimen286 +\tagshift@=\dimen287 +\tagwidth@=\dimen288 +\totwidth@=\dimen289 +\lineht@=\dimen290 +\@envbody=\toks38 +\multlinegap=\skip60 +\multlinetaggap=\skip61 +\mathdisplay@stack=\toks39 +LaTeX Info: Redefining \[ on input line 2953. +LaTeX Info: Redefining \] on input line 2954. +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/amscls/amsthm.sty +Package: amsthm 2020/05/29 v2.20.6 +\thm@style=\toks40 +\thm@bodyfont=\toks41 +\thm@headfont=\toks42 +\thm@notefont=\toks43 +\thm@headpunct=\toks44 +\thm@preskip=\skip62 +\thm@postskip=\skip63 +\thm@headsep=\skip64 +\dth@everypar=\toks45 +) +\c@theorem=\count331 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerbasethemes.sty)) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerthemedefault.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerfontthemedefault.sty +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamercolorthemedefault.st +y) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerinnerthemedefault.st +y +\beamer@dima=\dimen291 +\beamer@dimb=\dimen292 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamer/beamerouterthemedefault.st +y))) (/usr/local/texlive/2023/texmf-dist/tex/latex/listings/listings.sty +\lst@mode=\count332 +\lst@gtempboxa=\box76 +\lst@token=\toks46 +\lst@length=\count333 +\lst@currlwidth=\dimen293 +\lst@column=\count334 +\lst@pos=\count335 +\lst@lostspace=\dimen294 +\lst@width=\dimen295 +\lst@newlines=\count336 +\lst@lineno=\count337 +\lst@maxwidth=\dimen296 + +(/usr/local/texlive/2023/texmf-dist/tex/latex/listings/lstmisc.sty +File: lstmisc.sty 2023/02/27 1.9 (Carsten Heinz) +\c@lstnumber=\count338 +\lst@skipnumbers=\count339 +\lst@framebox=\box77 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/listings/listings.cfg +File: listings.cfg 2023/02/27 1.9 listings configuration +)) +Package: listings 2023/02/27 1.9 (Carsten Heinz) + +(/usr/local/texlive/2023/texmf-dist/tex/latex/booktabs/booktabs.sty +Package: booktabs 2020/01/12 v1.61803398 Publication quality tables +\heavyrulewidth=\dimen297 +\lightrulewidth=\dimen298 +\cmidrulewidth=\dimen299 +\belowrulesep=\dimen300 +\belowbottomsep=\dimen301 +\aboverulesep=\dimen302 +\abovetopsep=\dimen303 +\cmidrulesep=\dimen304 +\cmidrulekern=\dimen305 +\defaultaddspace=\dimen306 +\@cmidla=\count340 +\@cmidlb=\count341 +\@aboverulesep=\dimen307 +\@belowrulesep=\dimen308 +\@thisruleclass=\count342 +\@lastruleclass=\count343 +\@thisrulewidth=\dimen309 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/tools/array.sty +Package: array 2022/09/04 v2.5g Tabular extension package (FMi) +\col@sep=\dimen310 +\ar@mcellbox=\box78 +\extrarowheight=\dimen311 +\NC@list=\toks47 +\extratabsurround=\skip65 +\backup@length=\skip66 +\ar@cellbox=\box79 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamertheme-metropolis/beamerthem +emetropolis.sty +Package: beamerthememetropolis 2017/01/23 v1.2 Metropolis Beamer theme + +(/usr/local/texlive/2023/texmf-dist/tex/latex/pgfopts/pgfopts.sty +Package: pgfopts 2014/07/10 v2.1a LaTeX package options with pgfkeys +\pgfopts@list@add@a@toks=\toks48 +\pgfopts@list@add@b@toks=\toks49 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamertheme-metropolis/beamerinne +rthememetropolis.sty +Package: beamerinnerthememetropolis 2017/01/23 Metropolis inner theme + +(/usr/local/texlive/2023/texmf-dist/tex/latex/tools/calc.sty +Package: calc 2017/05/25 v4.3 Infix arithmetic (KKT,FJ) +\calc@Acount=\count344 +\calc@Bcount=\count345 +\calc@Adimen=\dimen312 +\calc@Bdimen=\dimen313 +\calc@Askip=\skip67 +\calc@Bskip=\skip68 +LaTeX Info: Redefining \setlength on input line 80. +LaTeX Info: Redefining \addtolength on input line 81. +\calc@Ccount=\count346 +\calc@Cskip=\skip69 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty +Package: pgf 2023-01-15 v3.1.10 (3.1.10) + +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/modules/pgfmoduleshapes.cod +e.tex +File: pgfmoduleshapes.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgfnodeparttextbox=\box80 +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/modules/pgfmoduleplot.code. +tex +File: pgfmoduleplot.code.tex 2023-01-15 v3.1.10 (3.1.10) +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version +-0-65.sty +Package: pgfcomp-version-0-65 2023-01-15 v3.1.10 (3.1.10) +\pgf@nodesepstart=\dimen314 +\pgf@nodesepend=\dimen315 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version +-1-18.sty +Package: pgfcomp-version-1-18 2023-01-15 v3.1.10 (3.1.10) +)) +(/usr/local/texlive/2023/texmf-dist/tex/latex/pgf/utilities/pgffor.sty +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex +Package: pgffor 2023-01-15 v3.1.10 (3.1.10) +\pgffor@iter=\dimen316 +\pgffor@skip=\dimen317 +\pgffor@stack=\toks50 +\pgffor@toks=\toks51 +)) +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.cod +e.tex +Package: tikz 2023-01-15 v3.1.10 (3.1.10) + +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/libraries/pgflibraryplothan +dlers.code.tex +File: pgflibraryplothandlers.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgf@plot@mark@count=\count347 +\pgfplotmarksize=\dimen318 +) +\tikz@lastx=\dimen319 +\tikz@lasty=\dimen320 +\tikz@lastxsaved=\dimen321 +\tikz@lastysaved=\dimen322 +\tikz@lastmovetox=\dimen323 +\tikz@lastmovetoy=\dimen324 +\tikzleveldistance=\dimen325 +\tikzsiblingdistance=\dimen326 +\tikz@figbox=\box81 +\tikz@figbox@bg=\box82 +\tikz@tempbox=\box83 +\tikz@tempbox@bg=\box84 +\tikztreelevel=\count348 +\tikznumberofchildren=\count349 +\tikznumberofcurrentchild=\count350 +\tikz@fig@count=\count351 + +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/modules/pgfmodulematrix.cod +e.tex +File: pgfmodulematrix.code.tex 2023-01-15 v3.1.10 (3.1.10) +\pgfmatrixcurrentrow=\count352 +\pgfmatrixcurrentcolumn=\count353 +\pgf@matrix@numberofcolumns=\count354 +) +\tikz@expandcount=\count355 + +(/usr/local/texlive/2023/texmf-dist/tex/generic/pgf/frontendlayer/tikz/librarie +s/tikzlibrarytopaths.code.tex +File: tikzlibrarytopaths.code.tex 2023-01-15 v3.1.10 (3.1.10) +))) +\metropolis@titleseparator@linewidth=\skip70 +\metropolis@progressonsectionpage=\skip71 +\metropolis@progressonsectionpage@linewidth=\skip72 +\metropolis@blocksep=\skip73 +\metropolis@blockadjust=\skip74 +\metropolis@parskip=\skip75 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamertheme-metropolis/beameroute +rthememetropolis.sty +Package: beamerouterthememetropolis 2017/01/23 Metropolis outer theme +\metropolis@frametitle@padding=\skip76 +\metropolis@progressinheadfoot=\skip77 +\metropolis@progressinheadfoot@linewidth=\skip78 +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamertheme-metropolis/beamercolo +rthememetropolis.sty +Package: beamercolorthememetropolis 2017/01/23 Metropolis color theme +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/beamertheme-metropolis/beamerfont +thememetropolis.sty +Package: beamerfontthememetropolis 2017/01/23 Metropolis font theme + +(/usr/local/texlive/2023/texmf-dist/tex/generic/iftex/ifxetex.sty +Package: ifxetex 2019/10/25 v0.7 ifxetex legacy package. Use iftex instead. +) +(/usr/local/texlive/2023/texmf-dist/tex/generic/iftex/ifluatex.sty +Package: ifluatex 2019/10/25 v1.5 ifluatex legacy package. Use iftex instead. +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/fontspec/fontspec.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/l3packages/xparse/xparse.sty +(/usr/local/texlive/2023/texmf-dist/tex/latex/l3kernel/expl3.sty +Package: expl3 2023-02-22 L3 programming layer (loader) + +(/usr/local/texlive/2023/texmf-dist/tex/latex/l3backend/l3backend-luatex.def +File: l3backend-luatex.def 2023-01-16 L3 backend support: PDF output (LuaTeX) +\l__color_backend_stack_int=\count356 +\l__pdf_internal_box=\box85 +)) +Package: xparse 2023-02-02 L3 Experimental document command parser +) +Package: fontspec 2022/01/15 v2.8a Font selection for XeLaTeX and LuaLaTeX +Lua module: fontspec 2022/01/15 2.8a Font selection for XeLaTeX and LuaLaTeX (/ +usr/local/texlive/2023/texmf-dist/tex/latex/fontspec/fontspec-luatex.sty +Package: fontspec-luatex 2022/01/15 v2.8a Font selection for XeLaTeX and LuaLaT +eX +\l__fontspec_script_int=\count357 +\l__fontspec_language_int=\count358 +\l__fontspec_strnum_int=\count359 +\l__fontspec_tmp_int=\count360 +\l__fontspec_tmpa_int=\count361 +\l__fontspec_tmpb_int=\count362 +\l__fontspec_tmpc_int=\count363 +\l__fontspec_em_int=\count364 +\l__fontspec_emdef_int=\count365 +\l__fontspec_strong_int=\count366 +\l__fontspec_strongdef_int=\count367 +\l__fontspec_tmpa_dim=\dimen327 +\l__fontspec_tmpb_dim=\dimen328 +\l__fontspec_tmpc_dim=\dimen329 + +(/usr/local/texlive/2023/texmf-dist/tex/latex/base/fontenc.sty +Package: fontenc 2021/04/29 v2.0v Standard LaTeX package +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/fontspec/fontspec.cfg))) +\c@fontsnotfound=\count368 +luaotfload | cache : Lookup cache loaded from /Users/zitek/Library/texlive/2023/ +texmf-var/luatex-cache/generic/names/luaotfload-lookup-cache.luc. + +Package fontspec Info: Font family 'FiraSansLight(0)' created for font 'Fira +(fontspec) Sans Light' with options +(fontspec) [Ligatures=TeX,ItalicFont={Fira Sans Light +(fontspec) Italic},BoldFont={Fira Sans},BoldItalicFont={Fira Sans +(fontspec) Italic}]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->"FiraSansLight:mode=node;script=latn;language=dflt;+t +lig;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->"FiraSansLight:mode=node;script=latn;language=dflt;+t +lig;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->"FiraSans:mode=node;script=latn;language=dflt;+tlig;" + +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->"FiraSans:mode=node;script=latn;language=dflt;+tlig;+ +smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->"FiraSansLightItalic:mode=node;script=latn;language=d +flt;+tlig;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->"FiraSansLightItalic:mode=node;script=latn;language=d +flt;+tlig;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->"FiraSansItalic:mode=node;script=latn;language=dflt;+ +tlig;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->"FiraSansItalic:mode=node;script=latn;language=dflt;+ +tlig;+smcp;" + + +Package fontspec Info: Could not resolve font "FiraMonoMedium/I" (it probably +(fontspec) doesn't exist). + +luaotfload | aux : font no 37 (nil) does not define feature smcp for script latn + with language dflt +luaotfload | aux : font no 38 (nil) does not define feature smcp for script latn + with language dflt +luaotfload | aux : font no 39 (nil) does not define feature smcp for script latn + with language dflt +luaotfload | aux : font no 40 (nil) does not define feature smcp for script latn + with language dflt + +Package fontspec Info: Font family 'FiraMono(0)' created for font 'Fira Mono' +(fontspec) with options +(fontspec) [WordSpace={1,0,0},HyphenChar=None,PunctuationSpace=Word +Space,BoldFont={Fira +(fontspec) Mono Medium}]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->"FiraMono:mode=node;script=latn;language=dflt;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) and font adjustment code: +(fontspec) \fontdimen 2\font =1\fontdimen 2\font \fontdimen 3\font +(fontspec) =0\fontdimen 3\font \fontdimen 4\font =0\fontdimen +(fontspec) 4\font \fontdimen 7\font =0\fontdimen 2\font +(fontspec) \tex_hyphenchar:D \font =-1\scan_stop: +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->"FiraMonoMedium:mode=node;script=latn;language=dflt;" + +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) and font adjustment code: +(fontspec) \fontdimen 2\font =1\fontdimen 2\font \fontdimen 3\font +(fontspec) =0\fontdimen 3\font \fontdimen 4\font =0\fontdimen +(fontspec) 4\font \fontdimen 7\font =0\fontdimen 2\font +(fontspec) \tex_hyphenchar:D \font =-1\scan_stop: +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->"FiraMono/I:mode=node;script=latn;language=dflt;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) and font adjustment code: +(fontspec) \fontdimen 2\font =1\fontdimen 2\font \fontdimen 3\font +(fontspec) =0\fontdimen 3\font \fontdimen 4\font =0\fontdimen +(fontspec) 4\font \fontdimen 7\font =0\fontdimen 2\font +(fontspec) \tex_hyphenchar:D \font =-1\scan_stop: +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->"FiraMono/BI:mode=node;script=latn;language=dflt;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) and font adjustment code: +(fontspec) \fontdimen 2\font =1\fontdimen 2\font \fontdimen 3\font +(fontspec) =0\fontdimen 3\font \fontdimen 4\font =0\fontdimen +(fontspec) 4\font \fontdimen 7\font =0\fontdimen 2\font +(fontspec) \tex_hyphenchar:D \font =-1\scan_stop: + +)) +(/usr/local/texlive/2023/texmf-dist/tex/latex/fira/FiraSans.sty +Package: FiraSans 2022/09/17 (Bob Tennent and autoinst) Style file for Fira San +s fonts. + +(/usr/local/texlive/2023/texmf-dist/tex/latex/xkeyval/xkeyval.sty +Package: xkeyval 2022/06/16 v2.9 package option processing (HA) + +(/usr/local/texlive/2023/texmf-dist/tex/generic/xkeyval/xkeyval.tex +(/usr/local/texlive/2023/texmf-dist/tex/generic/xkeyval/xkvutils.tex +\XKV@toks=\toks52 +\XKV@tempa@toks=\toks53 +) +\XKV@depth=\count369 +File: xkeyval.tex 2014/12/03 v2.7a key=value parser (HA) +)) +(/usr/local/texlive/2023/texmf-dist/tex/latex/base/textcomp.sty +Package: textcomp 2020/02/02 v2.0n Standard LaTeX package +) + +Package fontspec Info: Font family 'FiraSans(0)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Ligatures=TeX,Numbers = +(fontspec) {Proportional,OldStyle},UprightFont = +(fontspec) *-Regular,ItalicFont = *-Italic,BoldFont = +(fontspec) *-Bold,BoldItalicFont = *-BoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Regular.otf]:mode=node;script=latn +;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Regular.otf]:mode=node;script=latn +;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Italic.otf]:mode=node;script=latn; +language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Italic.otf]:mode=node;script=latn; +language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" + +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/n' will be +(Font) scaled to size 8.50006pt on input line 138. + +Package fontspec Info: Font family 'FiraSans(1)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Proportional,OldStyle},UprightFont = +(fontspec) *-Regular,ItalicFont = *-Italic,BoldFont = +(fontspec) *-Bold,BoldItalicFont = *-BoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Regular.otf]:mode=node;script=latn +;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Regular.otf]:mode=node;script=latn +;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Italic.otf]:mode=node;script=latn; +language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Italic.otf]:mode=node;script=latn; +language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" + + +Package fontspec Info: Font family 'FiraSans(2)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Proportional,Lining},UprightFont = +(fontspec) *-Regular,ItalicFont = *-Italic,BoldFont = +(fontspec) *-Bold,BoldItalicFont = *-BoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Regular.otf]:mode=node;script=latn +;language=dflt;+tlig;+pnum;+lnum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Regular.otf]:mode=node;script=latn +;language=dflt;+tlig;+pnum;+lnum;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+lnum;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+lnum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Italic.otf]:mode=node;script=latn; +language=dflt;+tlig;+pnum;+lnum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Italic.otf]:mode=node;script=latn; +language=dflt;+tlig;+pnum;+lnum;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+lnum;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+lnum;+smcp;" + + +Package fontspec Info: Font family 'FiraSans(3)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Monospaced,OldStyle},UprightFont = +(fontspec) *-Regular,ItalicFont = *-Italic,BoldFont = +(fontspec) *-Bold,BoldItalicFont = *-BoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Regular.otf]:mode=node;script=latn +;language=dflt;+tlig;+tnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Regular.otf]:mode=node;script=latn +;language=dflt;+tlig;+tnum;+onum;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+tnum;+onum;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+tnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Italic.otf]:mode=node;script=latn; +language=dflt;+tlig;+tnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Italic.otf]:mode=node;script=latn; +language=dflt;+tlig;+tnum;+onum;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+tnum;+onum;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+tnum;+onum;+smcp;" + + +Package fontspec Info: Font family 'FiraSans(4)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Proportional,OldStyle},UprightFont = +(fontspec) *-Thin,ItalicFont = *-ThinItalic,BoldFont = +(fontspec) *-Bold,BoldItalicFont = *-BoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Thin.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Thin.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-ThinItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-ThinItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" + + +Package fontspec Info: Font family 'FiraSans(5)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Proportional,OldStyle},UprightFont = +(fontspec) *-Light,ItalicFont = *-LightItalic,BoldFont = +(fontspec) *-Bold,BoldItalicFont = *-BoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Light.otf]:mode=node;script=latn;l +anguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Light.otf]:mode=node;script=latn;l +anguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-LightItalic.otf]:mode=node;script= +latn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-LightItalic.otf]:mode=node;script= +latn;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" + + +Package fontspec Info: Font family 'FiraSans(6)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Proportional,OldStyle},UprightFont = +(fontspec) *-ExtraLight,ItalicFont = *-ExtraLightItalic,BoldFont = +(fontspec) *-Bold,BoldItalicFont = *-BoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-ExtraLight.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-ExtraLight.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-ExtraLightItalic.otf]:mode=node;sc +ript=latn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-ExtraLightItalic.otf]:mode=node;sc +ript=latn;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" + + +Package fontspec Info: Font family 'FiraSans(7)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Proportional,OldStyle},UprightFont = +(fontspec) *-UltraLight,ItalicFont = *-UltraLightItalic,BoldFont = +(fontspec) *-Bold,BoldItalicFont = *-BoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-UltraLight.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-UltraLight.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-UltraLightItalic.otf]:mode=node;sc +ript=latn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-UltraLightItalic.otf]:mode=node;sc +ript=latn;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" + + +Package fontspec Info: Font family 'FiraSans(8)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Proportional,OldStyle},UprightFont = +(fontspec) *-Medium,ItalicFont = *-MediumItalic,BoldFont = +(fontspec) *-Bold,BoldItalicFont = *-BoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Medium.otf]:mode=node;script=latn; +language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Medium.otf]:mode=node;script=latn; +language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-MediumItalic.otf]:mode=node;script +=latn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-MediumItalic.otf]:mode=node;script +=latn;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" + + +Package fontspec Info: Font family 'FiraSans(9)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Proportional,OldStyle},UprightFont = +(fontspec) *-Book,ItalicFont = *-BookItalic,BoldFont = +(fontspec) *-Bold,BoldItalicFont = *-BoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Book.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Book.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BookItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BookItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+pnum;+onum;+smcp;" + + +Package fontspec Info: Font family 'FiraSans(10)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Proportional,OldStyle},UprightFont = +(fontspec) *-SemiBold,ItalicFont = *-SemiBoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-SemiBold.otf]:mode=node;script=lat +n;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-SemiBold.otf]:mode=node;script=lat +n;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-SemiBoldItalic.otf]:mode=node;scri +pt=latn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-SemiBoldItalic.otf]:mode=node;scri +pt=latn;language=dflt;+tlig;+pnum;+onum;+smcp;" + + +Package fontspec Info: Font family 'FiraSans(11)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Proportional,OldStyle},UprightFont = +(fontspec) *-ExtraBold,ItalicFont = *-ExtraBoldItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-ExtraBold.otf]:mode=node;script=la +tn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-ExtraBold.otf]:mode=node;script=la +tn;language=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-ExtraBoldItalic.otf]:mode=node;scr +ipt=latn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-ExtraBoldItalic.otf]:mode=node;scr +ipt=latn;language=dflt;+tlig;+pnum;+onum;+smcp;" + + +Package fontspec Info: Font family 'FiraSans(12)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Numbers = {Proportional,OldStyle},UprightFont = +(fontspec) *-Heavy,ItalicFont = *-HeavyItalic]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Heavy.otf]:mode=node;script=latn;l +anguage=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Heavy.otf]:mode=node;script=latn;l +anguage=dflt;+tlig;+pnum;+onum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-HeavyItalic.otf]:mode=node;script= +latn;language=dflt;+tlig;+pnum;+onum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-HeavyItalic.otf]:mode=node;script= +latn;language=dflt;+tlig;+pnum;+onum;+smcp;" + +) (./iors.aux) +\openout1 = iors.aux + +LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 37. +LaTeX Font Info: ... okay on input line 37. +LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 37. +LaTeX Font Info: ... okay on input line 37. +LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 37. +LaTeX Font Info: ... okay on input line 37. +LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 37. +LaTeX Font Info: ... okay on input line 37. +LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 37. +LaTeX Font Info: Trying to load font information for TS1+cmr on input line 3 +7. + +(/usr/local/texlive/2023/texmf-dist/tex/latex/base/ts1cmr.fd +File: ts1cmr.fd 2022/07/10 v2.5l Standard LaTeX font definitions +) +LaTeX Font Info: ... okay on input line 37. +LaTeX Font Info: Checking defaults for TU/lmr/m/n on input line 37. +LaTeX Font Info: ... okay on input line 37. +LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 37. +LaTeX Font Info: ... okay on input line 37. +LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 37. +LaTeX Font Info: ... okay on input line 37. +LaTeX Font Info: Checking defaults for PD1/pdf/m/n on input line 37. +LaTeX Font Info: ... okay on input line 37. +LaTeX Font Info: Checking defaults for PU/pdf/m/n on input line 37. +LaTeX Font Info: ... okay on input line 37. + +*geometry* driver: auto-detecting +*geometry* detected driver: luatex +*geometry* verbose mode - [ preamble ] result: +* driver: luatex +* paper: custom +* layout: +* layoutoffset:(h,v)=(0.0pt,0.0pt) +* modes: includehead includefoot +* h-part:(L,W,R)=(28.45274pt, 398.3386pt, 28.45274pt) +* v-part:(T,H,B)=(0.0pt, 256.0748pt, 0.0pt) +* \paperwidth=455.24408pt +* \paperheight=256.0748pt +* \textwidth=398.3386pt +* \textheight=227.62207pt +* \oddsidemargin=-43.81725pt +* \evensidemargin=-43.81725pt +* \topmargin=-72.26999pt +* \headheight=14.22636pt +* \headsep=0.0pt +* \topskip=10.0pt +* \footskip=14.22636pt +* \marginparwidth=3.0pt +* \marginparsep=11.0pt +* \columnsep=10.0pt +* \skip\footins=9.0pt plus 4.0pt minus 2.0pt +* \hoffset=0.0pt +* \voffset=0.0pt +* \mag=1000 +* \@twocolumnfalse +* \@twosidefalse +* \@mparswitchfalse +* \@reversemarginfalse +* (1in=72.27pt=25.4mm, 1cm=28.453pt) + +(/usr/local/texlive/2023/texmf-dist/tex/context/base/mkii/supp-pdf.mkii +[Loading MPS to PDF converter (version 2006.09.02).] +\scratchcounter=\count370 +\scratchdimen=\dimen330 +\scratchbox=\box86 +\nofMPsegments=\count371 +\nofMParguments=\count372 +\everyMPshowfont=\toks54 +\MPscratchCnt=\count373 +\MPscratchDim=\dimen331 +\MPnumerator=\count374 +\makeMPintoPDFobject=\count375 +\everyMPtoPDFconversion=\toks55 +) (/usr/local/texlive/2023/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty +Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf +Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4 +85. + +(/usr/local/texlive/2023/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg +File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv +e +)) +Package hyperref Info: Link coloring OFF on input line 37. + +(./iors.out) (./iors.out) +\@outlinefile=\write5 + +\openout5 = iors.out +LaTeX Font Info: Overwriting symbol font `operators' in version `normal' +(Font) OT1/cmr/m/n --> OT1/cmss/m/n on input line 37. +LaTeX Font Info: Overwriting symbol font `operators' in version `bold' +(Font) OT1/cmr/bx/n --> OT1/cmss/b/n on input line 37. +\symnumbers=\mathgroup6 +\sympureletters=\mathgroup7 +LaTeX Font Info: Overwriting math alphabet `\mathrm' in version `normal' +(Font) OT1/cmss/m/n --> TU/lmr/m/n on input line 37. +LaTeX Font Info: Redeclaring math alphabet \mathbf on input line 37. +LaTeX Font Info: Overwriting math alphabet `\mathbf' in version `normal' +(Font) OT1/cmr/bx/n --> TU/FiraSans(0)/b/n on input line 37. +LaTeX Font Info: Overwriting math alphabet `\mathbf' in version `bold' +(Font) OT1/cmr/bx/n --> TU/FiraSans(0)/b/n on input line 37. +LaTeX Font Info: Redeclaring math alphabet \mathsf on input line 37. +LaTeX Font Info: Overwriting math alphabet `\mathsf' in version `normal' +(Font) OT1/cmss/m/n --> TU/FiraSans(0)/m/n on input line 37. +LaTeX Font Info: Overwriting math alphabet `\mathsf' in version `bold' +(Font) OT1/cmss/bx/n --> TU/FiraSans(0)/m/n on input line 37. +LaTeX Font Info: Redeclaring math alphabet \mathit on input line 37. +LaTeX Font Info: Overwriting math alphabet `\mathit' in version `normal' +(Font) OT1/cmr/m/it --> TU/FiraSans(0)/m/it on input line 37. +LaTeX Font Info: Overwriting math alphabet `\mathit' in version `bold' +(Font) OT1/cmr/bx/it --> TU/FiraSans(0)/m/it on input line 37. + +LaTeX Font Info: Redeclaring math alphabet \mathtt on input line 37. +LaTeX Font Info: Overwriting math alphabet `\mathtt' in version `normal' +(Font) OT1/cmtt/m/n --> TU/FiraMono(0)/m/n on input line 37. +LaTeX Font Info: Overwriting math alphabet `\mathtt' in version `bold' +(Font) OT1/cmtt/m/n --> TU/FiraMono(0)/m/n on input line 37. +LaTeX Font Info: Overwriting symbol font `numbers' in version `bold' +(Font) TU/FiraSans(0)/m/n --> TU/FiraSans(0)/b/n on input line + 37. +LaTeX Font Info: Overwriting symbol font `pureletters' in version `bold' +(Font) TU/FiraSans(0)/m/it --> TU/FiraSans(0)/b/it on input li +ne 37. +LaTeX Font Info: Overwriting math alphabet `\mathrm' in version `bold' +(Font) OT1/cmss/b/n --> TU/lmr/b/n on input line 37. +LaTeX Font Info: Overwriting math alphabet `\mathbf' in version `bold' +(Font) TU/FiraSans(0)/b/n --> TU/FiraSans(0)/b/n on input line + 37. +LaTeX Font Info: Overwriting math alphabet `\mathsf' in version `bold' +(Font) TU/FiraSans(0)/m/n --> TU/FiraSans(0)/b/n on input line + 37. +LaTeX Font Info: Overwriting math alphabet `\mathit' in version `bold' +(Font) TU/FiraSans(0)/m/it --> TU/FiraSans(0)/b/it on input li +ne 37. +LaTeX Font Info: Overwriting math alphabet `\mathtt' in version `bold' +(Font) TU/FiraMono(0)/m/n --> TU/FiraMono(0)/b/n on input line + 37. + +(/usr/local/texlive/2023/texmf-dist/tex/latex/translator/translator-basic-dicti +onary-English.dict +Dictionary: translator-basic-dictionary, Language: English +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/translator/translator-bibliograph +y-dictionary-English.dict +Dictionary: translator-bibliography-dictionary, Language: English +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/translator/translator-environment +-dictionary-English.dict +Dictionary: translator-environment-dictionary, Language: English +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/translator/translator-months-dict +ionary-English.dict +Dictionary: translator-months-dictionary, Language: English +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/translator/translator-numbers-dic +tionary-English.dict +Dictionary: translator-numbers-dictionary, Language: English +) +(/usr/local/texlive/2023/texmf-dist/tex/latex/translator/translator-theorem-dic +tionary-English.dict +Dictionary: translator-theorem-dictionary, Language: English +) +\c@lstlisting=\count376 + (./iors.nav) +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/n' will be +(Font) scaled to size 4.25003pt on input line 37. +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/n' will be +(Font) scaled to size 5.95004pt on input line 37. +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/n' will be +(Font) scaled to size 12.24008pt on input line 39. +LaTeX Font Info: Font shape `TU/FiraSans(0)/b/n' will be +(Font) scaled to size 12.24008pt on input line 39. +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/n' will be +(Font) scaled to size 10.20007pt on input line 39. +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/n' will be +(Font) scaled to size 7.65005pt on input line 39. + +Overfull \vbox (13.79993pt too high) detected at line 39 + [] + +[1 + +{/usr/local/texlive/2023/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] +LaTeX Font Info: Trying to load font information for U+msa on input line 53. + + +(/usr/local/texlive/2023/texmf-dist/tex/latex/amsfonts/umsa.fd +File: umsa.fd 2013/01/14 v3.01 AMS symbols A +) +LaTeX Font Info: Trying to load font information for U+msb on input line 53. + + +(/usr/local/texlive/2023/texmf-dist/tex/latex/amsfonts/umsb.fd +File: umsb.fd 2013/01/14 v3.01 AMS symbols B +) +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/it' will be +(Font) scaled to size 8.50006pt on input line 53. +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/it' will be +(Font) scaled to size 5.95004pt on input line 53. +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/it' will be +(Font) scaled to size 4.25003pt on input line 53. +LaTeX Font Info: Font shape `TU/FiraSans(0)/b/n' will be +(Font) scaled to size 10.20007pt on input line 53. +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/n' will be +(Font) scaled to size 3.40002pt on input line 53. + [2 + +] +LaTeX Font Info: Font shape `TU/FiraSans(0)/b/n' will be +(Font) scaled to size 8.50006pt on input line 66. + [3 + +] + +Package fontspec Info: Font family 'FiraSans(13)' created for font 'FiraSans' +(fontspec) with options [Ligatures = TeX,Scale = 0.85,Extension = +(fontspec) .otf,Ligatures=TeX,Numbers = +(fontspec) {Proportional,OldStyle},UprightFont = +(fontspec) *-Regular,ItalicFont = *-Italic,BoldFont = +(fontspec) *-Bold,BoldItalicFont = +(fontspec) *-BoldItalic,Numbers={Monospaced}]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Regular.otf]:mode=node;script=latn +;language=dflt;+tlig;+onum;+tnum;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Regular.otf]:mode=node;script=latn +;language=dflt;+tlig;+onum;+tnum;+smcp;" +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+onum;+tnum;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Bold.otf]:mode=node;script=latn;la +nguage=dflt;+tlig;+onum;+tnum;+smcp;" +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Italic.otf]:mode=node;script=latn; +language=dflt;+tlig;+onum;+tnum;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-Italic.otf]:mode=node;script=latn; +language=dflt;+tlig;+onum;+tnum;+smcp;" +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+onum;+tnum;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: +(fontspec) <->s*[0.85]"[FiraSans-BoldItalic.otf]:mode=node;script=l +atn;language=dflt;+tlig;+onum;+tnum;+smcp;" + +LaTeX Font Info: Font shape `TU/FiraSans(13)/m/n' will be +(Font) scaled to size 7.65005pt on input line 91. +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/n' will be +(Font) scaled to size 5.10004pt on input line 91. +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/it' will be +(Font) scaled to size 7.65005pt on input line 91. +LaTeX Font Info: Font shape `TU/FiraSans(0)/m/it' will be +(Font) scaled to size 5.10004pt on input line 91. +LaTeX Font Info: Font shape `TU/FiraSans(13)/b/n' will be +(Font) scaled to size 7.65005pt on input line 91. + +Overfull \vbox (1.61246pt too high) detected at line 91 + [] + +[4 + +] +\openout4 = iors.vrb + (./iors.vrb) [5 + +] +LaTeX Font Info: Font shape `TU/FiraSans(13)/m/n' will be +(Font) scaled to size 5.95004pt on input line 139. +LaTeX Font Info: Font shape `TU/FiraSans(13)/b/n' will be +(Font) scaled to size 5.95004pt on input line 139. +LaTeX Font Info: Font shape `TU/FiraSans(0)/b/n' will be +(Font) scaled to size 5.95004pt on input line 139. + [6 + +] [7 + +] +\openout4 = iors.vrb + (./iors.vrb) [8 + +] +\openout4 = iors.vrb + (./iors.vrb) [9 + +] +\openout4 = iors.vrb + (./iors.vrb) +[10 + +] [11 + +] +LaTeX Font Info: Font shape `TU/FiraSans(0)/b/n' will be +(Font) scaled to size 7.65005pt on input line 263. + [12 + +] [13 + +] +\openout4 = iors.vrb + (./iors.vrb) [14 + +] [15 + +] [16 + +] +\tf@nav=\write6 + +\openout6 = iors.nav +\tf@toc=\write7 + +\openout7 = iors.toc +\tf@snm=\write8 + +\openout8 = iors.snm + (./iors.aux) +Package rerunfilecheck Info: File `iors.out' has not changed. +(rerunfilecheck) Checksum: D41D8CD98F00B204E9800998ECF8427E;0. +) + +Here is how much of LuaTeX's memory you used: + 31159 strings out of 478285 + 244236,1977958 words of node,token memory allocated + 10339 words of node memory still in use: + 35 hlist, 8 vlist, 5 rule, 75 disc, 6 local_par, 2 math, 185 glue, 185 kern, +19 penalty, 850 glyph, 77 attribute, 71 glue_spec, 77 attribute_list, 4 write, 1 +6 pdf_colorstack nodes + avail lists: 1:3,2:3847,3:247,4:266,5:6816,6:106,7:6156,8:42,9:13512,10:41,11 +:753 + 49861 multiletter control sequences out of 65536+600000 + 147 fonts using 43462351 bytes + 128i,13n,131p,1870b,652s stack positions out of 10000i,1000n,20000p,200000b,200000s + + +Output written on iors.pdf (16 pages, 128936 bytes). + +PDF statistics: 185 PDF objects out of 1000 (max. 8388607) + 131 compressed objects within 2 object streams + 33 named destinations out of 1000 (max. 131072) + 16 words of extra memory for PDF output out of 10000 (max. 100000000) + diff --git a/docs/paper-mods/slides/iors.nav b/docs/paper-mods/slides/iors.nav new file mode 100644 index 0000000..d5ca61a --- /dev/null +++ b/docs/paper-mods/slides/iors.nav @@ -0,0 +1,37 @@ +\headcommand {\slideentry {0}{0}{1}{1/1}{}{0}} +\headcommand {\beamer@framepages {1}{1}} +\headcommand {\slideentry {0}{0}{2}{2/2}{}{0}} +\headcommand {\beamer@framepages {2}{2}} +\headcommand {\slideentry {0}{0}{3}{3/3}{}{0}} +\headcommand {\beamer@framepages {3}{3}} +\headcommand {\slideentry {0}{0}{4}{4/4}{}{0}} +\headcommand {\beamer@framepages {4}{4}} +\headcommand {\slideentry {0}{0}{5}{5/5}{}{0}} +\headcommand {\beamer@framepages {5}{5}} +\headcommand {\slideentry {0}{0}{6}{6/6}{}{0}} +\headcommand {\beamer@framepages {6}{6}} +\headcommand {\slideentry {0}{0}{7}{7/7}{}{0}} +\headcommand {\beamer@framepages {7}{7}} +\headcommand {\slideentry {0}{0}{8}{8/8}{}{0}} +\headcommand {\beamer@framepages {8}{8}} +\headcommand {\slideentry {0}{0}{9}{9/9}{}{0}} +\headcommand {\beamer@framepages {9}{9}} +\headcommand {\slideentry {0}{0}{10}{10/10}{}{0}} +\headcommand {\beamer@framepages {10}{10}} +\headcommand {\slideentry {0}{0}{11}{11/11}{}{0}} +\headcommand {\beamer@framepages {11}{11}} +\headcommand {\slideentry {0}{0}{12}{12/12}{}{0}} +\headcommand {\beamer@framepages {12}{12}} +\headcommand {\slideentry {0}{0}{13}{13/13}{}{0}} +\headcommand {\beamer@framepages {13}{13}} +\headcommand {\slideentry {0}{0}{14}{14/14}{}{0}} +\headcommand {\beamer@framepages {14}{14}} +\headcommand {\slideentry {0}{0}{15}{15/15}{}{0}} +\headcommand {\beamer@framepages {15}{15}} +\headcommand {\slideentry {0}{0}{16}{16/16}{}{0}} +\headcommand {\beamer@framepages {16}{16}} +\headcommand {\beamer@partpages {1}{16}} +\headcommand {\beamer@subsectionpages {1}{16}} +\headcommand {\beamer@sectionpages {1}{16}} +\headcommand {\beamer@documentpages {16}} +\headcommand {\gdef \inserttotalframenumber {15}} diff --git a/docs/paper-mods/slides/iors.out b/docs/paper-mods/slides/iors.out new file mode 100644 index 0000000..e69de29 diff --git a/docs/paper-mods/slides/iors.pdf b/docs/paper-mods/slides/iors.pdf new file mode 100644 index 0000000..5712224 Binary files /dev/null and b/docs/paper-mods/slides/iors.pdf differ diff --git a/docs/paper-mods/slides/iors.snm b/docs/paper-mods/slides/iors.snm new file mode 100644 index 0000000..e69de29 diff --git a/docs/paper-mods/slides/iors.tex b/docs/paper-mods/slides/iors.tex new file mode 100644 index 0000000..47b3ba5 --- /dev/null +++ b/docs/paper-mods/slides/iors.tex @@ -0,0 +1,343 @@ +\documentclass[10pt,aspectratio=169]{beamer} + +\usepackage{listings} +\usepackage{xcolor} +\usepackage{booktabs} +\usepackage{array} + +\usetheme{metropolis} +\usepackage[scale=0.85]{FiraSans} + +\definecolor{warpaccent}{HTML}{2C3E50} +\definecolor{warpcode}{HTML}{1B5E20} +\definecolor{warpgray}{HTML}{555555} + +\setbeamercolor{frametitle}{bg=warpaccent} +\setbeamercolor{title separator}{fg=warpaccent} +\setbeamertemplate{frame numbering}[fraction] + +\lstset{ + basicstyle=\ttfamily\footnotesize, + keywordstyle=\color{warpaccent}\bfseries, + commentstyle=\color{warpgray}\itshape, + stringstyle=\color{warpcode}, + showstringspaces=false, + morekeywords={Self, dyn, impl, async, await, fn, type, trait, where, Result, Vec, Box, Option, mut, pub, struct, let, return, use}, + keepspaces=true, + columns=fullflexible, + frame=none, + breaklines=true, +} + +\title{Warp Modularity} +\subtitle{From paper IORs to a code-level interface} +\author{Internal note} +\date{} + +\begin{document} + +\maketitle + +% -------------------------------------------------------------------- +\begin{frame}{What we want} +A single interface every Warp phase satisfies, that is visibly the same +shape as the paper's IOR signature. + +\vspace{0.5em} +\begin{itemize} + \item \texttt{lib.rs::prove} reads as a chain of typed transitions. + \item Each transition: $(\text{stmt}, \text{wit}, \text{or}_\text{in}) \mapsto (\text{stmt}', \text{or}_\text{out})$. + \item Adding a phase: a struct + a trait impl. No bespoke plumbing. +\end{itemize} + +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{Why Warp wasn't easy modularly} +The paper IOR formalism doesn't share state across IORs. Real Warp does. + +\vspace{0.4em} +\textbf{Three concrete frictions:} +\begin{enumerate} + \item \textbf{Shared oracle $f$.} TwinConstraint, OOD, Batching, Proximity all consume the same codeword. A literal IOR-per-phase would rematerialise. + \item \textbf{Transcript ordering coupling.} Shift-query indices are squeezed once and consumed by both Batching and Proximity. No phase ``owns'' the squeeze. + \item \textbf{Statement / witness / oracle conflation.} The pre-refactor function had $\sim$10 mixed-purpose args; the paper's tripartite split was invisible. +\end{enumerate} +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{What an IOR signature is} +The Warp paper structures the protocol as a sequence of Interactive Oracle Reductions, each one having the composition rule: +\[ A: (\mathrm{stmt}_A, \mathrm{wit}_A) \mapsto (\mathrm{stmt}_A', \mathsf{O}[f]) \] +\[ B: (\mathrm{stmt}_B, \mathsf{O}[f]) \mapsto \mathrm{stmt}_B' \] +\[ A;B: (\mathrm{stmt}_A, \mathrm{stmt}_B, \mathrm{wit}_A) \mapsto (\mathrm{stmt}_A', \mathrm{stmt}_B') \] + +\vspace{0.4em} +Five typed components per IOR: + +\begin{center}\small +\begin{tabular}{lcc} +\toprule +& \textbf{Prover} & \textbf{Verifier} \\ +\midrule +\texttt{Statement} & \checkmark & \checkmark \\ +\texttt{Witness} & \checkmark & --- \\ +\texttt{InputOracles} & by data & by handle \\ +\texttt{ReducedStatement} & \checkmark & \checkmark \\ +\texttt{OutputOracles} & by data & by handle \\ +\bottomrule +\end{tabular} +\end{center} +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}[fragile]{The IOR trait we built} +v1 was a thin \texttt{ProverPhase} with opaque \texttt{Output}; v2 mirrors the paper: +\begin{lstlisting} +pub trait IOR { + type Statement; type Witness; + type ProverInputs; type VerifierInputs; + type ReducedStatement; + type ProverOutputs; type VerifierOutputs; + + fn prove(&self, ts: &mut ProverState, stmt: &Self::Statement, + wit: Self::Witness, inputs: Self::ProverInputs) + -> Result<(Self::ReducedStatement, Self::ProverOutputs), ProverError>; + + fn verify<'a>(&self, ts: &mut VerifierState<'a>, + stmt: &Self::Statement, inputs: Self::VerifierInputs) + -> Result<(Self::ReducedStatement, Self::VerifierOutputs), VerifierError>; +} +\end{lstlisting} +33/33 tests green. \texttt{Statement}/\texttt{ReducedStatement} shared; oracle types role-split. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{Per-phase mapping} +\scriptsize +\begin{center} +\begin{tabular}{lp{1.4cm}p{1.2cm}p{1.4cm}p{1.6cm}p{1.5cm}} +\toprule +& \textbf{Stmt} & \textbf{Wit} & \textbf{Pr.\,In} & \textbf{Reduced} & \textbf{Pr.\,Out} \\ +\midrule +\texttt{Pesat} & $(l_1, \log m)$ & wits & --- & $(\mu, \tau)$ & cw + tree \\ +\texttt{TwinC.} & acc + $\mu$/$\tau$ & acc-w + ins & cw + acc-cw & $\gamma, \zeta_0, \beta_\tau,$ defer & $\mathsf{O}[f] + z$ \\ +\texttt{Ood} & $(s, \log n)$ & --- & $\mathsf{O}[f]$ & samples + ans & --- \\ +\texttt{Batching} & $\zeta$\,+\,$s,t,\log n$ & --- & $\mathsf{O}[f]$ & $\alpha$ & $\mu$ \\ +\texttt{Proximity} & queries\,+\,$l_2,t$ & --- & trees + cw & $()$ & paths + ans \\ +\bottomrule +\end{tabular} +\end{center} + +\vspace{0.4em} +\textbf{Asymmetries are now structural:} +\begin{itemize}\scriptsize + \item \texttt{Pesat} \,---\, empty \texttt{ProverInputs} (it's the source). + \item \texttt{Proximity} \,---\, empty \texttt{Reduced} (it's a check). + \item \texttt{Ood} / \texttt{Batching} \,---\, empty \texttt{Witness}. +\end{itemize} +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{Single-protocol view: a candid review} +\textbf{What's good.} +\begin{itemize}\small + \item Paper-aligned types \,---\, the \texttt{IOR} trait and the paper's IOR composition rule line up directly. + \item Real verifier symmetry \,---\, \texttt{TwinConstraint::verify} / \texttt{Batching::verify} are first-class trait impls. + \item Honest oracle role split \,---\, prover sees data, verifier sees commitments. + \item Empty-type asymmetries are visible (\texttt{Reduced = ()} for checks). + \item 33/33 tests green throughout. +\end{itemize} + +\vspace{0.4em} +\textbf{What's not.} +\begin{itemize}\small + \item Seven associated types per phase \,---\, real cognitive load. + \item Lifetimes \& \texttt{PhantomData} proliferate; structs carry \texttt{<'a, F, \dots>} for anchoring. + \item No useful composition combinators \,---\, see next slide. + \item No type-level ordering enforcement; transcript order is implicit. + \item Orchestrator got longer, not shorter \,---\, $\sim$10 lines per phase invocation. +\end{itemize} +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}[fragile]{Composition \,---\, what the trait can't do} +\begin{lstlisting} +pub struct Sequence(A, B); +impl IOR for Sequence +where + B::Statement: From, + B::ProverInputs: From, + // ... and the witness has to come from somewhere ... +{ /* run A, derive B's inputs, run B */ } +\end{lstlisting} + +\textbf{Why no useful generics fall out:} +\begin{itemize}\small + \item \texttt{From} impls are protocol-specific \,---\, written per \texttt{(A, B)} pair, no leverage. + \item ``Between-phases'' glue is itself protocol logic (WARP between TC and OOD: compute $\eta, \nu_0$, build new Merkle tree, write transcript). Doesn't fit either phase's signature. + \item Witnesses don't thread cleanly across the boundary. +\end{itemize} + +\textbf{What unlocks it:} a canonical \texttt{Claim} algebra of shared shapes \,---\, next two slides. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}[fragile]{Claim \,---\, the shared vocabulary} +Lift ``what's being claimed'' from per-protocol bags into a small shared enum. + +\begin{lstlisting} +pub enum Claim { + Sumcheck(SumcheckClaim), // p sums to v on {0,1}^n + Eval(EvalClaim), // f_hat(zeta) = y + Proximity(ProximityClaim), // codeword delta-close to code + ZeroCheck(ZeroCheckClaim), // p == 0 on {0,1}^n + Batched(Box>), +} +\end{lstlisting} + +Operations on these claims become \emph{protocol-agnostic}: +\begin{lstlisting} +pub fn reduce_sumcheck( + claim: SumcheckClaim, + prover: &mut impl SumcheckProver, + transcript: &mut T, +) -> EvalClaim { ... } +\end{lstlisting} + +STIR uses it. WHIR uses it. WARP uses it. The \emph{transformation} is the abstraction \,---\, not the protocol. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}[fragile]{Protocol becomes a script over claim transformations} +\begin{lstlisting} +fn warp_round(...) -> Claim { + let c = Claim::ZeroCheck(...); // R1CS satisfaction + let c = c.bundle_with(...); // -> SumcheckClaim + let c = reduce_sumcheck(c, ...); // -> EvalClaim at gamma + let oods = ood_sample(...); // s EvalClaims + let qs = shift_query_claims(...); // t more + let c = batch_eval_claims(vec![...]); // -> 1 EvalClaim + let c = reduce_sumcheck(c, ...); // -> EvalClaim at alpha + Claim::Eval(c) +} +\end{lstlisting} + +Each line is a typed claim transformation. Protocol identity is the \emph{script}; the transformations are reusable. + +\textbf{Limits + cost.} Only the public claim flow gets typified \,---\, witnesses, transcript threading, setup, and proof shapes stay per-protocol. Canonical shapes only emerge from 3+ implementations, and soundness arguments must thread through the algebra. Multi-protocol research thrust, not a tactical project. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{Reframing: a family of protocols} +The single-protocol view above was honest \,---\, but the actual goal isn't ``a clean WARP.'' + +\vspace{0.4em} +We design protocols like \textbf{STIR}, \textbf{WHIR}, \textbf{WARP} all the time. They share: +\begin{itemize}\small + \item Encode-and-commit (codeword \,---\, Reed-Solomon, or otherwise). + \item Sumcheck reductions (different flavours: degree-1, multilinear, fused twin-constraint, $\dots$). + \item Out-of-domain (OOD) sampling for proximity boost. + \item Shift-query opening with auth paths. + \item Batching sumchecks that fold many claims into one. + \item Accumulator updates / final consistency. +\end{itemize} + +\vspace{0.4em} +\textbf{The bar for the IOR trait then shifts:} not ``does this clean up WARP?'' but ``is this the right backbone for a family?'' +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{The toolkit, audited} +What practitioners writing STIR / WHIR / WARP \emph{already} have: +\begin{itemize}\small + \item \textbf{Transcripts.} \texttt{spongefish} 0.7 \,---\, de facto in this corner of the ecosystem. Practitioners use it directly. No abstraction needed. + \item \textbf{Sumchecks.} \texttt{effsc} \,---\, \texttt{SumcheckProver}, \texttt{RoundPolyEvaluator}, \texttt{InnerProductProver}, \texttt{CoefficientProverLSB}, \texttt{MultilinearProver}, hypercube + protogalaxy utilities. WARP uses 9 of 10 effsc primitives already. + \item \textbf{Linear codes.} \texttt{ark-codes::LinearCode} \,---\, encode, code\_len. + \item \textbf{Merkle commitments.} \texttt{ark-crypto-primitives::merkle\_tree} \,---\, generic over leaf shape, hashes. + \item \textbf{Oracle struct.} \texttt{Vec} codeword + lazy MLE cache. Same storage shape across STIR / WHIR / WARP. +\end{itemize} + +\vspace{0.4em} +The primitive layer is mostly done. The discussion is really about \emph{integration} \,---\, how primitives compose, and what shape protocol authors copy. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{What practitioner experience actually needs} +Designing a STIR / WHIR / WARP-style protocol is hard. The friction is rarely ``I need a new sumcheck primitive.'' It's: +\begin{itemize}\small + \item \textbf{Tying primitives together correctly.} LinearCode + Merkle is two crates; integration is per-protocol boilerplate. + \item \textbf{Knowing the right phase decomposition.} What's a phase? What's its statement? Where's the witness? When does the verifier act? + \item \textbf{Threading transcript ordering without bugs.} Squeeze before write, derive challenges in the right segments. + \item \textbf{Maintaining paper-code alignment.} The paper's IOR composition is the contract; reviewers expect to see it in code. + \item \textbf{Debugging when soundness fails.} ``Where in this 600-line \texttt{verify} did the rejection happen?'' +\end{itemize} + +\vspace{0.4em} +The fellow: \emph{practitioners benefit from patterns + examples + shared primitives}, not from cross-protocol trait contracts. That's the lever. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}[fragile]{The one new abstraction: \texttt{CodeCommitment}} +The single integration gap not already covered by the toolkit: + +\begin{lstlisting} +pub trait CodeCommitment { + type Commitment; + type Opening; + + fn commit(&self, codeword: &[F]) -> Self::Commitment; + fn open_index(&self, codeword: &[F], i: usize) -> Self::Opening; + fn verify_opening( + &self, c: &Self::Commitment, i: usize, val: F, o: &Self::Opening, + ) -> bool; +} +\end{lstlisting} + +\vspace{0.4em} +Default impl: \texttt{LinearCode} + Merkle config from \texttt{ark-crypto-primitives}. +\begin{itemize}\small + \item Wraps the only integration practitioners currently re-roll per protocol. + \item Small (one trait, one default impl), focused, validated by WARP migrating onto it. +\end{itemize} +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{What we're for, positively} +\textbf{Keep \texttt{IOR} as a structural template, not a cross-protocol contract.} +\begin{itemize}\small + \item It's the shape WARP-style protocols copy. The paper section on accumulator-as-state makes it legible. + \item Other protocols follow as a \emph{pattern}, get shared mental model + idioms, without being forced into one trait. +\end{itemize} + +\textbf{Lean on the existing primitive layer.} +\begin{itemize}\small + \item \texttt{effsc}, \texttt{spongefish}, \texttt{ark-codes}, \texttt{ark-crypto-primitives::merkle\_tree}. + \item Already strong. The discipline is to use them, not reinvent. +\end{itemize} + +\textbf{Add \texttt{CodeCommitment}.} The one missing integration trait. WARP migrates onto it; STIR-lite plugs into it. + +\textbf{Make WARP the reference implementation.} A new protocol designer reads \texttt{src/protocol/phases/}, copies the IOR-shaped pattern, plugs in their primitives. + +\textbf{Document the pattern.} Short ``how to design a Warp-style protocol'' guide \,---\, narrative, not a trait. The actual lever for practitioner ergonomics. +\end{frame} + +% -------------------------------------------------------------------- +\begin{frame}{Where this leaves us \,---\, the next move} +\textbf{Paper section (accumulator as typed state).} Describe the IOR trait as a paper-aligned realisation \emph{of WARP's protocol structure}, framed as a \emph{template other protocols can follow}, not a contract they must. + +\vspace{0.4em} +\textbf{Paper section (structured sumcheck primitive).} The fused $\alpha/\beta/\tau$-fold inside TwinConstraint is the natural extraction \,---\, lives in \texttt{effsc} as a new \texttt{RoundPolyEvaluator}-shaped impl, citable from outside Warp. + +\vspace{0.4em} +\textbf{Add \texttt{CodeCommitment}} \,---\, small focused trait, ships in a session. Migrate WARP onto it. + +\vspace{0.4em} +\textbf{Implement a second protocol.} STIR-lite or WHIR-lite, against the same primitives + IOR-shaped pattern. The friction it surfaces is the most reliable guide for the next iteration. + +\vspace{0.4em} +\textbf{Write the ``how to design a Warp-style protocol'' guide.} Walk through phase decomposition, primitive choices, IOR shape. Narrative document in \texttt{docs/}. The actual practitioner-facing artifact. +\end{frame} + +\end{document} diff --git a/docs/paper-mods/slides/iors.toc b/docs/paper-mods/slides/iors.toc new file mode 100644 index 0000000..e69de29 diff --git a/docs/paper-mods/slides/iors.vrb b/docs/paper-mods/slides/iors.vrb new file mode 100644 index 0000000..f3b76af --- /dev/null +++ b/docs/paper-mods/slides/iors.vrb @@ -0,0 +1,22 @@ +\frametitle{The one new abstraction: \texttt {CodeCommitment}} +The single integration gap not already covered by the toolkit: + +\begin{lstlisting} +pub trait CodeCommitment { + type Commitment; + type Opening; + + fn commit(&self, codeword: &[F]) -> Self::Commitment; + fn open_index(&self, codeword: &[F], i: usize) -> Self::Opening; + fn verify_opening( + &self, c: &Self::Commitment, i: usize, val: F, o: &Self::Opening, + ) -> bool; +} +\end{lstlisting} + +\vspace{0.4em} +Default impl: \texttt{LinearCode} + Merkle config from \texttt{ark-crypto-primitives}. +\begin{itemize}\small + \item Wraps the only integration practitioners currently re-roll per protocol. + \item Small (one trait, one default impl), focused, validated by WARP migrating onto it. +\end{itemize} diff --git a/examples/profile_phases.rs b/examples/profile_phases.rs new file mode 100644 index 0000000..3f4291b --- /dev/null +++ b/examples/profile_phases.rs @@ -0,0 +1,212 @@ +//! Per-phase wall-time profile of a Goldilocks WARP prove run — used to +//! report the micro-profile table after the rewrite-v2 effsc integration. +//! See the original shape in the PR #22 body. +//! +//! Run: +//! ```text +//! cargo run --release --features profile --example profile_phases +//! ``` +//! +//! The `profile` feature installs the JSON layer from `warp::profile::init_json`. +//! This binary captures the emitted records, aggregates wall_ns per phase +//! over a handful of prove invocations, and prints a markdown table that +//! mirrors the PR #22 breakdown (rs_encode, pesat_merkle_tree, +//! twin_constraint_sumcheck, etc.). +//! +//! Best run with `RAYON_NUM_THREADS` pinned and all other workloads off. + +#[cfg(not(feature = "profile"))] +fn main() { + eprintln!( + "profile_phases: build with --features profile (and preferably --release).\n\ + example: cargo run --release --features profile --example profile_phases" + ); +} + +#[cfg(feature = "profile")] +fn main() { + inner::run(); +} + +#[cfg(feature = "profile")] +mod inner { + use std::io::{self, Write}; + use std::marker::PhantomData; + use std::sync::{Arc, Mutex}; + + use ark_codes::reed_solomon::config::ReedSolomonConfig; + use ark_codes::reed_solomon::ReedSolomon; + use ark_codes::traits::LinearCode; + use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; + use ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig; + use ark_std::rand::thread_rng; + use ark_std::UniformRand; + + use warp::config::WARPConfig; + use warp::relations::{ + r1cs::{ + hashchain::{ + compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness, + }, + R1CS, + }, + BundledPESAT, Relation, ToPolySystem, + }; + use warp::traits::AccumulationScheme; + use warp::types::{AccumulatorInstance, AccumulatorWitness}; + use warp::utils::fields::Goldilocks; + use warp::utils::poseidon; + use warp::WARP; + + #[derive(Clone)] + struct SharedBuf(Arc>>); + + impl Write for SharedBuf { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.lock().unwrap().extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + pub fn run() { + let iters = std::env::var("WARP_PROFILE_ITERS") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(5); + let hash_chain_size = std::env::var("WARP_PROFILE_HCSIZE") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(10); + let warmup = 2usize; + + let sink = SharedBuf(Arc::new(Mutex::new(Vec::new()))); + let installed = warp::profile::init_json(sink.clone()); + assert!(installed, "failed to install JSON profile subscriber"); + + let l1 = 4; + let s = 8; + let t = 7; + let mut rng = thread_rng(); + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + let code_config = + ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); + let code = ReedSolomon::new(code_config); + let n = code.code_len(); + + let (instances, witnesses): (Vec<_>, Vec<_>) = (0..l1) + .map(|_| { + let preimage = vec![Goldilocks::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness { + preimage, + _crhs_scheme: PhantomData::>, + }; + let relation = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (relation.x, relation.w) + }) + .unzip(); + + let warp_config = WARPConfig::new(l1, l1, s, t, r1cs.config(), code.code_len()); + let prover = WARP::, _, Blake3MerkleConfig>::new( + warp_config, + code.clone(), + r1cs.clone(), + (), + (), + ); + + println!( + "=== warp profile (Goldilocks hashchain) — n={n}, l1={l1}, s={s}, t={t}, hashchain_size={hash_chain_size}, iters={iters} (+ {warmup} warmup)" + ); + + for i in 0..(warmup + iters) { + let domainsep = spongefish::domain_separator!("profile_phases"); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + if i == warmup { + sink.0.lock().unwrap().clear(); + } + prover + .prove( + (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), + &mut prover_state, + witnesses.clone(), + instances.clone(), + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ) + .unwrap(); + } + + let bytes = sink.0.lock().unwrap().clone(); + let text = String::from_utf8(bytes).expect("JSON output is UTF-8"); + + let rows: &[(&str, &str)] = &[ + ("pesat.encode", "rs_encode (FFT)"), + ("pesat.merkle_commit", "pesat_merkle_tree"), + ("twin_constraint.sumcheck", "twin_constraint_sumcheck"), + ("warp.commit_new_oracle", "merkle_commit"), + ("batching.eq_evals", "eq_poly_evals + ood_evals_vec"), + ("batching.sumcheck", "batching_sumcheck (inner product)"), + ("warp.prove", "end-to-end prover"), + ]; + + let mut series: std::collections::BTreeMap<&str, Vec> = + std::collections::BTreeMap::new(); + for line in text.lines() { + for (phase, _) in rows.iter() { + let needle = format!("\"phase\":\"{phase}\""); + if line.contains(&needle) { + if let Some(w) = extract_u64(line, "\"wall_ns\":") { + series.entry(*phase).or_default().push(w); + } + break; + } + } + } + + println!(); + println!("| Phase | n | mean (ms) | min (ms) | samples |"); + println!("|-------|---|-----------|----------|---------|"); + for (phase, label) in rows { + let vs = series.remove(*phase).unwrap_or_default(); + if vs.is_empty() { + println!("| {label} | {n} | n/a | n/a | 0 |"); + continue; + } + let sum_ns: u128 = vs.iter().map(|&v| v as u128).sum(); + let mean_ms = (sum_ns as f64 / vs.len() as f64) / 1e6; + let min_ms = *vs.iter().min().unwrap() as f64 / 1e6; + println!( + "| {label} | {n} | {mean_ms:.3} | {min_ms:.3} | {} |", + vs.len() + ); + } + } + + fn extract_u64(line: &str, key: &str) -> Option { + let i = line.find(key)? + key.len(); + let rest = &line[i..]; + let end = rest + .find(|c: char| !c.is_ascii_digit()) + .unwrap_or(rest.len()); + rest[..end].parse().ok() + } +} diff --git a/src/bin/warp-params.rs b/src/bin/warp-params.rs new file mode 100644 index 0000000..2015992 --- /dev/null +++ b/src/bin/warp-params.rs @@ -0,0 +1,261 @@ +//! `warp-params` — CLI for exploring parameter selection. +//! +//! Paired spec: `docs/paper-mods/mod4_parameter_selection.tex`. +//! +//! Usage: +//! +//! ```text +//! warp-params select --lambda 128 --rate 0.5 --field-bits 254 --regime conjectured +//! warp-params validate --s 8 --t 128 --lambda 128 --rate 0.5 --field-bits 254 --regime conjectured +//! warp-params table # dump PRESETS as a TSV +//! ``` +//! +//! This is intentionally dependency-free (no clap). Keeps the crate slim +//! and the exit semantics simple. + +use std::process::ExitCode; + +use warp::params::{lookup, select, validate, ParamError, Params, Regime, SecurityLevel, PRESETS}; + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + match args.first().map(String::as_str) { + Some("select") => cmd_select(&args[1..]), + Some("validate") => cmd_validate(&args[1..]), + Some("table") => cmd_table(), + Some("-h") | Some("--help") | Some("help") | None => { + print_usage(); + ExitCode::SUCCESS + } + Some(other) => { + eprintln!("warp-params: unknown subcommand `{other}`"); + print_usage(); + ExitCode::from(2) + } + } +} + +fn print_usage() { + eprintln!( + "warp-params — pick soundness-driven WARP parameters.\n\ + \n\ + Usage:\n\ + warp-params select --lambda N --rate NUM/DEN|FLOAT --field-bits N --regime provable|conjectured\n\ + warp-params validate --s N --t N --lambda N --rate ... --field-bits N --regime ...\n\ + warp-params table\n\ + \n\ + See docs/paper-mods/mod4_parameter_selection.tex for the derivation." + ); +} + +fn cmd_select(args: &[String]) -> ExitCode { + let flags = match parse_flags(args) { + Ok(f) => f, + Err(e) => { + eprintln!("warp-params select: {e}"); + return ExitCode::from(2); + } + }; + let (lambda, rate, field_bits, regime) = + match (flags.lambda, flags.rate, flags.field_bits, flags.regime) { + (Some(l), Some(r), Some(fb), Some(rg)) => (SecurityLevel(l), r, fb, rg), + _ => { + eprintln!("warp-params select: need --lambda, --rate, --field-bits, --regime"); + return ExitCode::from(2); + } + }; + match select(lambda, field_bits, rate.as_f64(), regime) { + Ok(p) => { + // Prefer a preset if we have an exact-rational match on file — + // lets users see the canonical attested row, not just a + // recomputed tuple. (Equal by `presets_match_select_output`.) + let preset = rate.ratio().and_then(|(n, d)| lookup(lambda, n, d, regime)); + if let Some(preset) = preset { + println!( + "λ={} rate={}/{} regime={:?} → s={} t={} (preset)", + preset.lambda.bits(), + preset.code_rate_num, + preset.code_rate_den, + preset.regime, + preset.params.s, + preset.params.t + ); + } else { + println!( + "λ={} rate={:.4} regime={:?} → s={} t={}", + lambda.bits(), + rate.as_f64(), + regime, + p.s, + p.t + ); + } + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("warp-params select: {}", format_err(e)); + ExitCode::from(1) + } + } +} + +fn cmd_validate(args: &[String]) -> ExitCode { + let flags = match parse_flags(args) { + Ok(f) => f, + Err(e) => { + eprintln!("warp-params validate: {e}"); + return ExitCode::from(2); + } + }; + let (s, t, lambda, rate, field_bits, regime) = match ( + flags.s, + flags.t, + flags.lambda, + flags.rate, + flags.field_bits, + flags.regime, + ) { + (Some(s), Some(t), Some(l), Some(r), Some(fb), Some(rg)) => { + (s, t, SecurityLevel(l), r, fb, rg) + } + _ => { + eprintln!( + "warp-params validate: need --s, --t, --lambda, --rate, --field-bits, --regime" + ); + return ExitCode::from(2); + } + }; + let params = Params { s, t }; + match validate(¶ms, field_bits, rate.as_f64(), regime, lambda) { + Ok(bound) => { + println!( + "proximity_bits={:.2} field_admissible={} ood_admissible={} meets_target={}", + bound.proximity_bits, + bound.field_admissible, + bound.ood_admissible, + bound.meets(lambda), + ); + if bound.meets(lambda) { + ExitCode::SUCCESS + } else { + ExitCode::from(1) + } + } + Err(e) => { + eprintln!("warp-params validate: {}", format_err(e)); + ExitCode::from(1) + } + } +} + +fn cmd_table() -> ExitCode { + println!("lambda\trate\tregime\ts\tt"); + for preset in PRESETS { + println!( + "{}\t{}/{}\t{:?}\t{}\t{}", + preset.lambda.bits(), + preset.code_rate_num, + preset.code_rate_den, + preset.regime, + preset.params.s, + preset.params.t, + ); + } + ExitCode::SUCCESS +} + +#[derive(Default)] +struct Flags { + lambda: Option, + rate: Option, + field_bits: Option, + regime: Option, + s: Option, + t: Option, +} + +/// Parsed rate that remembers whether it was written as `num/den` or as +/// a decimal. Exact-rational form enables preset lookup. +#[derive(Clone, Copy)] +enum Rate { + Ratio { num: u32, den: u32 }, + Float(f64), +} + +impl Rate { + fn as_f64(self) -> f64 { + match self { + Rate::Ratio { num, den } => num as f64 / den as f64, + Rate::Float(x) => x, + } + } + fn ratio(self) -> Option<(u32, u32)> { + match self { + Rate::Ratio { num, den } => Some((num, den)), + Rate::Float(_) => None, + } + } +} + +fn parse_flags(args: &[String]) -> Result { + let mut flags = Flags::default(); + let mut it = args.iter(); + while let Some(flag) = it.next() { + let value = it + .next() + .ok_or_else(|| format!("missing value for {flag}"))?; + match flag.as_str() { + "--lambda" => flags.lambda = Some(parse_u32(value)?), + "--rate" => flags.rate = Some(parse_rate(value)?), + "--field-bits" => flags.field_bits = Some(parse_u32(value)?), + "--regime" => flags.regime = Some(parse_regime(value)?), + "--s" => flags.s = Some(parse_u32(value)? as usize), + "--t" => flags.t = Some(parse_u32(value)? as usize), + other => return Err(format!("unknown flag: {other}")), + } + } + Ok(flags) +} + +fn parse_u32(s: &str) -> Result { + s.parse() + .map_err(|e| format!("expected u32, got `{s}`: {e}")) +} + +fn parse_rate(s: &str) -> Result { + if let Some((num, den)) = s.split_once('/') { + let n: u32 = num.parse().map_err(|e| format!("rate num `{num}`: {e}"))?; + let d: u32 = den.parse().map_err(|e| format!("rate den `{den}`: {e}"))?; + if d == 0 { + return Err("rate denominator must be non-zero".into()); + } + Ok(Rate::Ratio { num: n, den: d }) + } else { + s.parse::() + .map(Rate::Float) + .map_err(|e| format!("rate `{s}`: {e}")) + } +} + +fn parse_regime(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "provable" | "prov" => Ok(Regime::Provable), + "conjectured" | "conj" => Ok(Regime::Conjectured), + other => Err(format!( + "unknown regime `{other}`, want `provable` or `conjectured`" + )), + } +} + +fn format_err(e: ParamError) -> String { + match e { + ParamError::InvalidRate => "rate must be in (0, 1)".to_string(), + ParamError::FieldTooSmall { field_bits, lambda } => { + format!( + "field is only {field_bits} bits; \ + a target of {lambda} bits requires at least {} bits", + lambda + warp::params::select::FIELD_EPSILON + ) + } + } +} diff --git a/src/crypto/merkle/blake3.rs b/src/crypto/merkle/blake3.rs deleted file mode 100644 index d2ef41b..0000000 --- a/src/crypto/merkle/blake3.rs +++ /dev/null @@ -1,27 +0,0 @@ -use super::parameters::MerkleTreeParams; -use ark_crypto_primitives::crh::blake3::{Blake3CRH, Blake3TwoToOneCRH}; -use ark_crypto_primitives::crh::ByteDigest; -use ark_crypto_primitives::{ - crh::{CRHScheme, TwoToOneCRHScheme}, - merkle_tree::{Config as MerkleConfig, IdentityDigestConverter}, - sponge::Absorb, -}; -use ark_ff::PrimeField; -use ark_std::marker::PhantomData; - -#[derive(Clone)] -pub struct Blake3MerkleConfig { - _field: PhantomData, -} - -pub type Blake3MerkleTreeParams = - MerkleTreeParams, Blake3TwoToOneCRH, ByteDigest<32>>; - -impl MerkleConfig for Blake3MerkleConfig { - type Leaf = [F]; - type LeafDigest = ::Output; - type LeafInnerDigestConverter = IdentityDigestConverter; - type InnerDigest = ::Output; - type LeafHash = Blake3CRH; - type TwoToOneHash = Blake3TwoToOneCRH; -} diff --git a/src/crypto/merkle/mod.rs b/src/crypto/merkle/mod.rs index 43b2474..9330bf7 100644 --- a/src/crypto/merkle/mod.rs +++ b/src/crypto/merkle/mod.rs @@ -5,10 +5,6 @@ use ark_crypto_primitives::{ }; use ark_ff::Field; -pub mod blake3; -pub mod parameters; -pub mod poseidon; - pub fn build_codeword_leaves>( code: &C, witnesses: &[Vec], diff --git a/src/crypto/merkle/parameters.rs b/src/crypto/merkle/parameters.rs deleted file mode 100644 index 19667d7..0000000 --- a/src/crypto/merkle/parameters.rs +++ /dev/null @@ -1,76 +0,0 @@ -use ark_crypto_primitives::{ - crh::{CRHScheme, TwoToOneCRHScheme}, - merkle_tree::{Config, IdentityDigestConverter}, - sponge::Absorb, -}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::rand::RngCore; -use serde::Deserialize; -use serde::Serialize; -use std::{hash::Hash, marker::PhantomData}; - -/// A generic Merkle tree config usable across hash types (e.g., Blake3, Keccak). -/// -/// # Type Parameters: -/// - `F`: Field element used in the leaves -/// - `LeafH`: Leaf hash function -/// - `CompressH`: Internal node hasher -/// - `Digest`: Digest type -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(bound = "")] -pub struct MerkleTreeParams { - #[serde(skip)] - _marker: PhantomData<(F, LeafH, CompressH, Digest)>, -} - -impl Config for MerkleTreeParams -where - F: CanonicalSerialize + Send, - LeafH: CRHScheme, - CompressH: TwoToOneCRHScheme, - Digest: Clone - + std::fmt::Debug - + Default - + CanonicalSerialize - + CanonicalDeserialize - + Eq - + PartialEq - + Hash - + Send - + Absorb, -{ - type Leaf = [F]; - - type LeafDigest = Digest; - type LeafInnerDigestConverter = IdentityDigestConverter; - type InnerDigest = Digest; - - type LeafHash = LeafH; - type TwoToOneHash = CompressH; -} - -/// Returns the `(leaf_hash_params, two_to_one_hash_params)` for any compatible Merkle tree. -/// -/// # Type Parameters -/// - `F`: The leaf field element type -/// - `LeafH`: The leaf hash function -/// - `CompressH`: The two-to-one internal hash function -/// -/// # Panics -/// Panics if `setup()` fails (which should not happen for deterministic hashers). -pub fn default_config( - rng: &mut impl RngCore, -) -> ( - ::Parameters, - ::Parameters, -) -where - F: CanonicalSerialize + Send, - LeafH: CRHScheme + Send, - CompressH: TwoToOneCRHScheme + Send, -{ - ( - LeafH::setup(rng).expect("Failed to setup Leaf hash"), - CompressH::setup(rng).expect("Failed to setup Compress hash"), - ) -} diff --git a/src/crypto/merkle/poseidon.rs b/src/crypto/merkle/poseidon.rs deleted file mode 100644 index f5a989d..0000000 --- a/src/crypto/merkle/poseidon.rs +++ /dev/null @@ -1,52 +0,0 @@ -use ark_crypto_primitives::{ - crh::{ - poseidon::{ - constraints::{ - CRHGadget as PoseidonCRHGadget, TwoToOneCRHGadget as PoseidonTwoToOneCRHGadget, - }, - TwoToOneCRH as PoseidonTwoToOneCRH, CRH as PoseidonCRH, - }, - CRHScheme, CRHSchemeGadget, TwoToOneCRHScheme, TwoToOneCRHSchemeGadget, - }, - merkle_tree::{ - constraints::ConfigGadget as MerkleConfigGadget, Config as MerkleConfig, - IdentityDigestConverter, - }, - sponge::Absorb, -}; -use ark_ff::PrimeField; -use ark_r1cs_std::fields::fp::FpVar; -use ark_std::marker::PhantomData; - -#[derive(Clone)] -pub struct PoseidonMerkleConfig { - _field: PhantomData, -} - -impl MerkleConfig for PoseidonMerkleConfig { - type Leaf = [F]; - type LeafDigest = ::Output; - type LeafInnerDigestConverter = IdentityDigestConverter; - type InnerDigest = ::Output; - type LeafHash = PoseidonCRH; - type TwoToOneHash = PoseidonTwoToOneCRH; -} - -#[derive(Clone)] -pub struct PoseidonMerkleConfigGadget { - _field: PhantomData, -} - -impl MerkleConfigGadget, F> - for PoseidonMerkleConfigGadget -{ - type Leaf = [FpVar]; - type LeafDigest = as CRHSchemeGadget, F>>::OutputVar; - type LeafHash = PoseidonCRHGadget; - type LeafInnerConverter = IdentityDigestConverter; - type InnerDigest = as TwoToOneCRHSchemeGadget< - PoseidonTwoToOneCRH, - F, - >>::OutputVar; - type TwoToOneHash = PoseidonTwoToOneCRHGadget; -} diff --git a/src/error.rs b/src/error.rs index fb6aa08..681124c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -77,17 +77,17 @@ impl From for VerifierError { fn from(err: effsc::proof::SumcheckError) -> Self { use effsc::proof::SumcheckError; match err { - // Round consistency or degree mismatch detected by the library. + // Round consistency or degree checks failed inside the library. SumcheckError::ConsistencyCheck { .. } | SumcheckError::DegreeMismatch { .. } => { Self::SumcheckRound } - // Not raised by `sumcheck_verify` any more (the library dropped - // the internal oracle check); warp does the final-claim check - // itself. Retained for enum completeness. + // The caller-supplied oracle_check (the final-claim equality) + // rejected — surfaces as warp's `Target` variant for backwards + // compatibility with the existing negative tests. SumcheckError::FinalEvaluation => Self::Target, // Ran out of transcript or malformed bytes. SumcheckError::TranscriptError { .. } => Self::SpongeFish, - // Unreachable: warp always passes a noop hook. + // Not reachable: warp passes `noop_hook_verify`. SumcheckError::HookError { .. } => Self::SumcheckRound, } } diff --git a/src/lib.rs b/src/lib.rs index 30bf5e0..8b57bf8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,178 +1,49 @@ #![allow(clippy::assign_op_pattern)] // generated by SmallFpConfig derive macro use crate::error::WARPError; use crate::traits::AccumulationScheme; +use crate::types::{AccumulatorInstance, AccumulatorWitness, ProveResult, WARPParams, WARPProof}; use ark_codes::traits::LinearCode; use ark_crypto_primitives::{ crh::{CRHScheme, TwoToOneCRHScheme}, - merkle_tree::{Config, MerkleTree, Path}, + merkle_tree::{Config, MerkleTree}, }; use ark_ff::{Field, PrimeField}; -use ark_poly::{ - univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial, - MultilinearExtension, Polynomial, -}; +use ark_poly::{DenseMultilinearExtension, Polynomial}; use ark_std::log2; -use crypto::merkle::build_codeword_leaves; -use crypto::merkle::compute_auth_paths; -use effsc::{ - coefficient_sumcheck::RoundPolyEvaluator, - folding::protogalaxy, - hypercube::{compute_hypercube_eq_evals, eq_poly_non_binary}, - noop_hook, - provers::{coefficient_lsb::CoefficientProverLSB, inner_product::InnerProductProver}, - runner::sumcheck, - verifier::sumcheck_verify, -}; -use protocol::domainsep::{derive_between_sumchecks, derive_pre_twin_constraint, parse_statement}; -use protocol::EffscVerifierTranscript; +use config::WARPConfig; +use effsc::hypercube::{compute_hypercube_eq_evals, Ascending}; +use protocol::query::QueryIndices; +use protocol::transcript::{absorb_instances, parse_statement}; use relations::{r1cs::R1CSConstraints, BundledPESAT}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; -use std::collections::HashMap; use std::marker::PhantomData; -use utils::binary_field_elements_to_usize; -use utils::byte_to_binary_field_array; -use utils::concat_slices; use utils::scale_and_sum; - -use config::WARPConfig; -use protocol::domainsep::{absorb_accumulated_instances, absorb_instances}; +use utils::{concat_slices, poly::eq_poly}; pub mod config; pub mod constraints; pub mod crypto; pub mod error; +pub mod params; +pub mod profile; pub mod protocol; pub mod relations; pub mod serialize; pub mod traits; +pub mod types; pub mod utils; -use ark_crypto_primitives::Error; use error::{DeciderError, ProverError, VerifierError}; - -/// Degree-1 polynomial interpolating two field elements: `lo + (hi - lo)·X`. -fn linear_poly(lo: F, hi: F) -> DensePolynomial { - DensePolynomial::from_coefficients_vec(vec![lo, hi - lo]) -} - -/// A single R1CS constraint row: sparse representations of A, B, and C. -type R1CSConstraint = (Vec<(F, usize)>, Vec<(F, usize)>, Vec<(F, usize)>); - -/// Evaluate one R1CS constraint `Az·Bz - Cz` as a degree-2 polynomial -/// from two witness vectors `z0`, `z1`. -fn eval_r1cs_constraint_poly( - (a, b, c): &R1CSConstraint, - z0: &[F], - z1: &[F], -) -> DensePolynomial { - let eval = |lc: &[(F, usize)], z: &[F]| lc.iter().map(|(t, i)| z[*i] * t).sum::(); - let (a0, b0, c0) = (eval(a, z0), eval(b, z0), eval(c, z0)); - let (a1, b1, c1) = (eval(a, z1) - a0, eval(b, z1) - b0, eval(c, z1) - c0); - DensePolynomial::from_coefficients_vec(vec![a0 * b0 - c0, a0 * b1 + a1 * b0 - c1, a1 * b1]) -} - -/// `RoundPolyEvaluator` for the twin-constraint sumcheck. Fuses the α-fold, -/// β-fold, and τ-linear multiplication in a single pass over even/odd pairs -/// of the input tables. Replaces the closure `twin_constraint_round_poly` -/// previously handed to `coefficient_sumcheck`. -struct TwinConstraintEvaluator<'a, F: Field> { - r1cs: &'a R1CSConstraints, - omega: F, - degree: usize, -} - -impl<'a, F: Field> RoundPolyEvaluator for TwinConstraintEvaluator<'a, F> { - fn degree(&self) -> usize { - self.degree - } - - fn accumulate_pair(&self, coeffs: &mut [F], tw: &[(&[F], &[F])], pw: &[(F, F)]) { - // tw[0]=(u_even,u_odd), tw[1]=(z_even,z_odd), - // tw[2]=(a_even,a_odd), tw[3]=(b_even,b_odd); pw[0]=(tau_even,tau_odd). - let (u_even, u_odd) = tw[0]; - let (z_even, z_odd) = tw[1]; - let (a_even, a_odd) = tw[2]; - let (b_even, b_odd) = tw[3]; - let (tau_even, tau_odd) = pw[0]; - - // CoefficientProverLSB::final_value() invokes the evaluator on fully - // reduced singleton tables with empty odd slices. Warp ignores - // `SumcheckProof::final_value` (only `.challenges` is consumed), so - // bail out — leaves `coeffs` zero, `final_value` returns zero, no - // one reads it. - if u_odd.is_empty() { - return; - } - - let f = protogalaxy::fold( - a_even.iter().zip(a_odd).map(|(&l, &r)| (l, r - l)), - u_even - .iter() - .zip(u_odd) - .map(|(&l, &r)| linear_poly(l, r)) - .collect(), - ); - - let p = protogalaxy::fold( - b_even.iter().zip(b_odd).map(|(&l, &r)| (l, r - l)), - self.r1cs - .iter() - .map(|c| eval_r1cs_constraint_poly(c, z_even, z_odd)) - .collect(), - ); - - let t = linear_poly(tau_even, tau_odd); - let h = (f + p * self.omega).naive_mul(&t); - - for (c, &hc) in coeffs.iter_mut().zip(h.coeffs.iter()) { - *c += hc; - } - } -} - -/// [CBBZ23] / HyperPlonk sparse-evaluation optimization: for shift-query -/// zetas (indices `1+s..r`), each ζ is a 0/1 vector representing a single -/// hypercube point. Accumulates the corresponding `eq_evals[i]` into a sparse -/// map keyed by that point's index. Reimplemented locally; the equivalent -/// helper in the old effsc API was removed in the rewrite. -fn accumulate_sparse_evaluations( - zetas: Vec<&[F]>, - eq_evals: Vec, - s: usize, - r: usize, -) -> HashMap { - let mut result: HashMap = HashMap::new(); - for i in 1 + s..r { - let index = zetas[i] - .iter() - .enumerate() - .filter_map(|(j, bit)| bit.is_one().then_some(1 << j)) - .sum::(); - *result.entry(index).or_insert_with(F::zero) += eq_evals[i]; - } - result -} - -/// Build the `g` side of the batching inner-product sumcheck: column-sum the -/// dense OOD evaluation matrix and add the sparse shift-query contributions. -fn batched_constraint_poly( - dense_polys: &[Vec], - sparse_polys: &HashMap, -) -> Vec { - if dense_polys.is_empty() { - return Vec::new(); - } - let mut result = vec![F::ZERO; dense_polys[0].len()]; - for row in dense_polys { - for (i, val) in row.iter().enumerate() { - result[i] += *val; - } - } - for (k, v) in sparse_polys.iter() { - result[*k] += *v; - } - result -} +use protocol::phases::{ + batching::{Batching, BatchingProverInputs, BatchingStatement, BatchingVerifierInputs}, + ood::{Ood, OodProverInputs, OodStatement}, + pesat::{Pesat, PesatStatement, PesatWitness}, + proximity::{Proximity, ProximityProverInputs, ProximityStatement, ProximityVerifierInputs}, + twin_constraint::{ + TwinConstraint, TwinConstraintProverInputs, TwinConstraintStatement, TwinConstraintWitness, + }, + IOR, +}; pub trait BoolResult { fn ok_or_err(self, err: E) -> Result<(), E>; @@ -190,12 +61,7 @@ impl BoolResult for bool { } pub struct WARP, C: LinearCode + Clone, MT: Config> { - _f: PhantomData, - config: WARPConfig, - code: C, - p: P, - mt_leaf_hash_params: ::Parameters, - mt_two_to_one_hash_params: ::Parameters, + pub params: WARPParams, } impl< @@ -213,12 +79,14 @@ impl< mt_two_to_one_hash_params: ::Parameters, ) -> WARP { Self { - _f: PhantomData, - config, - code, - p, - mt_leaf_hash_params, - mt_two_to_one_hash_params, + params: WARPParams { + _f: PhantomData, + config, + code, + p, + mt_leaf_hash_params, + mt_two_to_one_hash_params, + }, } } } @@ -236,27 +104,6 @@ impl< type Instances = Vec>; type Witnesses = Vec>; - type AccumulatorInstances = ( - Vec, - Vec>, - Vec, - (Vec>, Vec>), - Vec, - ); // (rt, \alpha, \mu, \beta (\tau, x), \eta) - - type AccumulatorWitnesses = (Vec>, Vec>, Vec>); // (td, f, w) - - // (rt_0, \mu_i, \nu_0, \nu_i, auth_0, auth_j, ((f_i(x_j)))) - type Proof = ( - MT::InnerDigest, - Vec, - F, - Vec, - Vec>, - Vec>>, - Vec>, - ); - fn index( prover_state: &mut ProverState, index: Self::Index, @@ -268,165 +115,114 @@ impl< prover_state.prover_message(&F::from(m as u32)); prover_state.prover_message(&F::from(n as u32)); prover_state.prover_message(&F::from(k as u32)); - Ok(((index.clone(), m, n, k), (m, n, k))) + Ok(((index, m, n, k), (m, n, k))) } + #[tracing::instrument(name = "warp.prove", skip_all)] fn prove( &self, pk: Self::ProverKey, prover_state: &mut ProverState, witnesses: Self::Witnesses, instances: Self::Instances, - acc_instances: Self::AccumulatorInstances, - acc_witnesses: Self::AccumulatorWitnesses, - ) -> Result< - ( - (Self::AccumulatorInstances, Self::AccumulatorWitnesses), - Self::Proof, - ), - ProverError, - > { + acc_instance: AccumulatorInstance, + acc_witness: AccumulatorWitness, + ) -> ProveResult { debug_assert!(instances.len() > 1); debug_assert_eq!(witnesses.len(), instances.len()); - debug_assert_eq!(acc_witnesses.0.len(), acc_instances.0.len()); + debug_assert_eq!(acc_witness.td.len(), acc_instance.rt.len()); - let (l1, l) = (self.config.l1, self.config.l); + let (l1, l) = (self.params.config.l1, self.params.config.l); let l2 = l - l1; debug_assert_eq!(l1 + l2, l); - debug_assert!(l.is_power_of_two()); - //////////////////////// - // 1. Parsing phase - //////////////////////// - // a. index + // Parse phase: dimensions and transcript priming. #[allow(non_snake_case)] let (M, N, k) = (pk.1, pk.2, pk.3); - #[allow(non_snake_case)] - let (log_M, log_l) = (log2(M) as usize, log2(l) as usize); + let (log_m, log_l) = (log2(M) as usize, log2(l) as usize); + let log_n = log2(self.params.code.code_len()) as usize; debug_assert_eq!(instances[0].len(), N - k); - - // b. and c. statements and accumulators - // d. absorb parameters absorb_instances(prover_state, &instances); - absorb_accumulated_instances::(prover_state, &acc_instances); - - //////////////////////// - // 2. PESAT Reduction - //////////////////////// - let n = self.code.code_len(); - let log_n = log2(n) as usize; - - // a. encode witnesses - let (codewords, leaves) = build_codeword_leaves(&self.code, &witnesses, l1); - - // b. evaluation claims - let mus = codewords.iter().map(|f| f[0]).collect::>(); - - // c. commit to witnesses - let td_0 = MerkleTree::::new( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - leaves.chunks_exact(l1).collect::>(), + acc_instance.absorb_into(prover_state); + + // Destructure acc_witness so we can hand .f/.w to twin_constraint by + // reference and move .td/.f into proximity afterwards. + let AccumulatorWitness { + td: acc_tds, + f: acc_fs, + w: acc_ws, + } = acc_witness; + + // Phase 2: PESAT — emit oracles (codewords), commit, squeeze τs. + let pesat_phase = Pesat:: { + code: &self.params.code, + mt_leaf_hash_params: &self.params.mt_leaf_hash_params, + mt_two_to_one_hash_params: &self.params.mt_two_to_one_hash_params, + _phantom: PhantomData, + }; + let (pesat_red, pesat_out) = pesat_phase.prove( + prover_state, + &PesatStatement { l1, log_m }, + PesatWitness { + witnesses: &witnesses, + }, + (), )?; - // d. absorb commitment and code evaluations - let root_bytes: [u8; 32] = td_0 - .root() - .as_ref() - .try_into() - .expect("root must be 32 bytes"); - prover_state.prover_message(&root_bytes); - prover_state.prover_messages(&mus); - - // e. zero check randomness and f. bundled evaluations - let taus = (0..l1) - .map(|_| prover_state.verifier_messages_vec::(log_M)) - .collect::>(); - - //////////////////////// - // 3. Constrained Code Accumulation - //////////////////////// - // a. zero check randomness - let omega: F = prover_state.verifier_message(); - - let tau = prover_state.verifier_messages_vec::(log_l); - - // b. define [...] - // c. sumcheck protocol. `compute_hypercube_eq_evals` builds the full - // `2^log_l` table in O(2^log_l) via the incremental build-up — - // faster than the previous O(log_l · 2^log_l) per-point loop. - let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); - - let alpha_vecs = concat_slices(&acc_instances.1, &vec![vec![F::zero(); log_n]; l1]); - - // build the z (x, w) vectors - let z_vecs: Vec> = acc_instances - .3 - .1 - .iter() - .zip(&acc_witnesses.2) - .chain(instances.iter().zip(&witnesses)) - .map(|(x, w)| concat_slices(x, w)) - .collect(); - - let beta_vecs: Vec> = acc_instances.3 .0.into_iter().chain(taus).collect(); - - // Twin Constraint sumcheck via the new `SumcheckProver` trait. - // Wire format: `d+1` evaluations per round (vs. the old `d+1` - // coefficient-form emission). `CoefficientProverLSB` keeps the - // LSB/pair-split semantics the `TwinConstraintEvaluator` was built - // for. - let tablewise = vec![ - concat_slices(&acc_witnesses.1, &codewords), // u - z_vecs, // z - alpha_vecs, // a - beta_vecs, // b - ]; - let pw = vec![tau_eq_evals]; // tau - - let r1cs = self.p.constraints(); - let degree = 1 + (log_n + 1).max(log_M + 2); - let evaluator = TwinConstraintEvaluator { - r1cs, - omega, - degree, + // Phase 3a: twin-constraint sumcheck. + let tc_phase = TwinConstraint:: { + r1cs: self.params.p.constraints(), + _phantom: PhantomData, }; - let mut cc = CoefficientProverLSB::new(&evaluator, tablewise, pw); - let gamma = sumcheck(&mut cc, log_l, prover_state, noop_hook).challenges; - debug_assert_eq!(gamma.len(), log_l); - - // e. new oracle and target — after log_l rounds each group has one - // row left; extract by clone. `CoefficientProverLSB` exposes the - // reduced tables read-only. - let reduced_tw = cc.tablewise(); - debug_assert!(reduced_tw.iter().all(|t| t.len() == 1)); - let f = reduced_tw[0][0].clone(); - let z = reduced_tw[1][0].clone(); - let zeta_0 = reduced_tw[2][0].clone(); - let beta_tau = reduced_tw[3][0].clone(); + let (tc_red, tc_out) = tc_phase.prove( + prover_state, + &TwinConstraintStatement { + acc_instance, + l1_mus: pesat_red.mus.clone(), + l1_taus: pesat_red.taus, + log_l, + log_m, + log_n, + }, + TwinConstraintWitness { + acc_witness_w: &acc_ws, + instances: &instances, + witnesses: &witnesses, + }, + TwinConstraintProverInputs { + fresh_codewords: &pesat_out.codewords, + acc_codewords: &acc_fs, + }, + )?; - // eval the bundled r1cs - let beta_eq_evals = compute_hypercube_eq_evals(log_M, &beta_tau); + // Phase 3b: bundled η, ν₀, new commitment, absorb — the "emit new + // oracle + claims" step between twin-constraint and OOD. + let beta_eq_evals = (0..M) + .map(|i| eq_poly(&tc_red.beta_tau, i)) + .collect::>(); let eta = self + .params .p - .evaluate_bundled(&beta_eq_evals, &z) + .evaluate_bundled(&beta_eq_evals, &tc_out.z) .map_err(|_| ProverError::SpongeFish)?; - - let (x, w) = z.split_at(N - k); - let beta = (vec![beta_tau], vec![x.to_vec()]); - let f_hat = DenseMultilinearExtension::from_evaluations_slice(log_n, &f); - let nu_0 = f_hat.fix_variables(&zeta_0)[0]; - - // f. new commitment - let td = MerkleTree::::new( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - f.chunks(1).collect::>(), - )?; - - // g. absorb new commitment and target + let nu_0 = tc_out.f.query_at_point(&tc_red.zeta_0); + + let (new_x, new_w) = tc_out.z.split_at(N - k); + let new_x = new_x.to_vec(); + let new_w = new_w.to_vec(); + let new_beta = (vec![tc_red.beta_tau.clone()], vec![new_x]); + + let td = { + let _s = tracing::info_span!("warp.commit_new_oracle").entered(); + count_ops!(MerkleTreeBuilds); + MerkleTree::::new( + &self.params.mt_leaf_hash_params, + &self.params.mt_two_to_one_hash_params, + tc_out.f.evals().chunks(1).collect::>(), + )? + }; let td_root_bytes: [u8; 32] = td .root() .as_ref() @@ -436,289 +232,257 @@ impl< prover_state.prover_message(&eta); prover_state.prover_message(&nu_0); - // h. ood samples - let n_ood_samples = self.config.s * log_n; - let ood_samples = prover_state.verifier_messages_vec::(n_ood_samples); - let ood_samples = ood_samples.chunks(log_n).collect::>(); - - // i. ood answers - let ood_answers = ood_samples - .iter() - .map(|ood_p| f_hat.fix_variables(ood_p)[0]) - .collect::>(); - - // j. absorb ood answers - prover_state.prover_messages(&ood_answers); - - let mut zetas = vec![zeta_0.as_slice()]; - let mut nus = vec![nu_0]; - zetas.extend(ood_samples); - nus.extend(ood_answers); - - // k. shift queries and zerocheck randomness - let r = 1 + self.config.s + self.config.t; - let log_r = log2(r) as usize; - let n_shift_queries = (self.config.t * log_n).div_ceil(8); - let bytes_shift_queries = prover_state.verifier_messages_vec(n_shift_queries); - let xis = prover_state.verifier_messages_vec(log_r); - - // get shift queries as binary field elements - let binary_shift_queries = bytes_shift_queries - .iter() - .flat_map(byte_to_binary_field_array) - .take(self.config.t * log_n) - .collect::>(); - - let binary_shift_queries = binary_shift_queries.chunks(log_n).collect::>(); - - // build indexes out of the shift queries stored - let shift_queries_indexes: Vec = binary_shift_queries - .iter() - .map(|vals| binary_field_elements_to_usize(vals)) - .collect(); - - zetas.extend(binary_shift_queries); - - // l. sumcheck polynomials - // compute evaluations for xi - - // `xis` has `log_r` elements but only the first `r` entries of the - // eq-evals table are consumed (`r ≤ 2^log_r`). - let xi_eq_evals: Vec = compute_hypercube_eq_evals(log_r, &xis) - .into_iter() - .take(r) - .collect(); - - let ood_evals_vec = (0..1 + self.config.s) - .map(|i| { - let table = compute_hypercube_eq_evals(log_n, zetas[i]); - let scale = xi_eq_evals[i]; - table.into_iter().map(|v| v * scale).collect::>() - }) - .collect::>(); - - // [CBBZ23] optimization from hyperplonk - let id_non_0_eval_sums = - accumulate_sparse_evaluations(zetas, xi_eq_evals, self.config.s, r); - - // Batching inner-product sumcheck. `InnerProductProver` is MSB - // half-split, so the challenge vector is returned in MSB order; - // reverse once to arkworks' LSB-first convention. - let mut ip = InnerProductProver::new( - f.clone(), - batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), - ); - let mut alpha = sumcheck(&mut ip, log_n, prover_state, noop_hook).challenges; - alpha.reverse(); - - // m. new target - let mu = f_hat.fix_variables(&alpha)[0]; + // Phase 3c: OOD — point queries on the oracle. + let ood_phase = Ood::::new(); + let (ood_red, _) = ood_phase.prove( + prover_state, + &OodStatement { + s: self.params.config.s, + log_n, + }, + (), + OodProverInputs { oracle: &tc_out.f }, + )?; - // n. compute authentication paths - let auth_0 = compute_auth_paths(&td_0, &shift_queries_indexes)?; + // Sample shift queries — ordering-coupled to the batching ξ below. + let queries = QueryIndices::::sample(prover_state, log_n, self.params.config.t); - let auth = acc_witnesses - .0 // for each accumulated witness and for each - .iter() - .map(|td| compute_auth_paths(td, &shift_queries_indexes)) - .collect::>>, Error>>()?; + // Phase 3d: batching sumcheck. Assemble zetas = [ζ₀, ood_j…, query_k…]. + let mut zetas: Vec> = + Vec::with_capacity(1 + self.params.config.s + self.params.config.t); + zetas.push(tc_red.zeta_0.clone()); + for chunk in ood_red.samples_flat.chunks(log_n) { + zetas.push(chunk.to_vec()); + } + for q in &queries.evaluation_points { + zetas.push(q.clone()); + } - let all_codewords = acc_witnesses - .1 - .into_iter() - .chain(codewords) - .collect::>(); + let batching_phase = Batching::::new(); + let (batching_red, batching_out) = batching_phase.prove( + prover_state, + &BatchingStatement { + zetas_prefix: zetas, + s: self.params.config.s, + t: self.params.config.t, + log_n, + _phantom: PhantomData, + }, + (), + BatchingProverInputs { oracle: &tc_out.f }, + )?; - let mut shift_queries_answers = - vec![vec![F::default(); all_codewords.len()]; shift_queries_indexes.len()]; - for (i, idx) in shift_queries_indexes.iter().enumerate() { - let answers = all_codewords.iter().map(|f| f[*idx]).collect::>(); - shift_queries_answers[i] = answers; - } + // Phase 3e: proximity — index queries + auth paths on accumulated + + // fresh oracles (NOT on the reduced `f`, which is this round's new + // oracle). + let all_codewords: Vec> = acc_fs.into_iter().chain(pesat_out.codewords).collect(); + let proximity_phase = Proximity:: { + mt_leaf_hash_params: &self.params.mt_leaf_hash_params, + mt_two_to_one_hash_params: &self.params.mt_two_to_one_hash_params, + _phantom: PhantomData, + }; + let (_, prox) = proximity_phase.prove( + prover_state, + &ProximityStatement { + queries: queries.clone(), + l2, + t: self.params.config.t, + }, + (), + ProximityProverInputs { + td_0: &pesat_out.td_0, + acc_td: &acc_tds, + all_codewords: &all_codewords, + }, + )?; - let acc_instance = (vec![td.root()], vec![alpha], vec![mu], beta, vec![eta]); - let acc_witness = (vec![td], vec![f], vec![w.to_vec()]); + // Assemble new accumulator state and proof. + let mut nus = Vec::with_capacity(1 + self.params.config.s); + nus.push(nu_0); + nus.extend(ood_red.answers); + + let new_acc_instance = AccumulatorInstance { + rt: vec![td.root()], + alpha: vec![batching_red.alpha], + mu: vec![batching_out.mu], + beta: new_beta, + eta: vec![eta], + }; + let new_acc_witness = AccumulatorWitness { + td: vec![td], + f: vec![tc_out.f.into_evals()], + w: vec![new_w], + }; + let proof = WARPProof { + rt_0: pesat_out.td_0.root(), + mu_i: pesat_red.mus, + nu_0, + nu_i: nus, + auth_0: prox.auth_0, + auth_j: prox.auth_j, + shift_query_answers: prox.shift_query_answers, + }; - // 4. return - Ok(( - (acc_instance, acc_witness), - ( - td_0.root(), - mus, - nu_0, - nus, - auth_0, - auth, - shift_queries_answers, - ), - )) + Ok(((new_acc_instance, new_acc_witness), proof)) } fn verify<'a>( &self, vk: Self::VerifierKey, verifier_state: &mut VerifierState<'a>, - acc_instance: Self::AccumulatorInstances, - proof: Self::Proof, + acc_instance: AccumulatorInstance, + proof: WARPProof, ) -> Result<(), VerifierError> { - let (l1, l) = (self.config.l1, self.config.l); + let (l1, l) = (self.params.config.l1, self.params.config.l); let l2 = l - l1; #[allow(non_snake_case)] let (M, N, k) = (vk.0, vk.1, vk.2); - #[allow(non_snake_case)] - let (log_M, log_l) = (log2(M) as usize, log2(l) as usize); - let n = self.code.code_len(); + let (log_m, log_l) = (log2(M) as usize, log2(l) as usize); + let n = self.params.code.code_len(); let log_n = log2(n) as usize; - let r = 1 + self.config.s + self.config.t; - let log_r = log2(r) as usize; - // 1. Parse statement. - let (l1_xs, (l2_roots, l2_alphas, l2_mus, (l2_taus, l2_xs), l2_etas)) = - parse_statement::(verifier_state, l1, l2, N - k, log_n, log_M)?; - - // 2. Pre-twin-constraint transcript reads. - let (rt_0, l1_mus, l1_taus, omega, tau) = - derive_pre_twin_constraint::(verifier_state, l1, log_l, log_M)?; - - // 3. σ₁ = Σ_i τ_eq(i) · (μ_i + ω·η_i). - let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); - let etas_chain = concat_slices(&l2_etas, &vec![F::zero(); l1]); - let sigma_1 = tau_eq_evals - .into_iter() - .zip( - l2_mus - .iter() - .copied() - .chain(l1_mus.iter().copied()) - .zip(etas_chain), - ) - .fold(F::zero(), |acc, (eq_tau, (mu, eta_i))| { - acc + eq_tau * (mu + omega * eta_i) - }); - - // 4. Twin-constraint sumcheck via `effsc::sumcheck_verify`. The - // library enforces round consistency (`q(0)+q(1)==claim`) and - // returns `(challenges, final_claim)`. The oracle check — verifying - // `final_claim == eq(τ,γ)·(ν₀ + ω·η)` — is warp's responsibility - // and runs below once η and ν₀ have been read from the transcript. - let tc_degree = 1 + (log_n + 1).max(log_M + 2); - let (gamma_sumcheck, tc_final_claim) = { - let mut wrap = EffscVerifierTranscript(verifier_state); - let res = sumcheck_verify(sigma_1, tc_degree, log_l, &mut wrap, |_, _| Ok(()))?; - (res.challenges, res.final_claim) + // 1. Parse statement (fresh instances + accumulator from transcript). + // The acc_instance argument carries the new accumulator's α/β for the + // final consistency checks; the transcript-parsed `parsed_acc` carries + // the OLD l2 accumulator entries used during proximity / β-consistency. + let (l1_xs, parsed_acc) = + parse_statement::(verifier_state, l1, l2, N - k, log_n, log_m)?; + let acc_alpha_first = acc_instance.alpha[0].clone(); + let acc_beta_0_first = acc_instance.beta.0[0].clone(); + let acc_beta_1_first = acc_instance.beta.1[0].clone(); + let acc_mu_first = acc_instance.mu[0]; + let l2_roots = parsed_acc.rt.clone(); + let l2_taus = parsed_acc.beta.0.clone(); + let l2_xs = parsed_acc.beta.1.clone(); + + // 2. PESAT::verify — read rt_0, l1_mus; squeeze l1_taus. + let pesat_phase = Pesat:: { + code: &self.params.code, + mt_leaf_hash_params: &self.params.mt_leaf_hash_params, + mt_two_to_one_hash_params: &self.params.mt_two_to_one_hash_params, + _phantom: PhantomData, }; - - // 5. Between-sumchecks reads: td, η, ν₀, OOD, shift bytes, ξ. - let (_td, eta, mut nus, ood_samples, bytes_shift_queries, xi) = - derive_between_sumchecks::( - verifier_state, + let (pesat_red, pesat_v_out) = + pesat_phase.verify(verifier_state, &PesatStatement { l1, log_m }, ())?; + let l1_mus = pesat_red.mus; + let l1_taus = pesat_red.taus; + let rt_0 = pesat_v_out.rt_0; + + // 3. TwinConstraint::verify — squeeze ω, τ; run sumcheck. + let tc_phase = TwinConstraint:: { + r1cs: self.params.p.constraints(), + _phantom: PhantomData, + }; + let (tc_red, _) = tc_phase.verify( + verifier_state, + &TwinConstraintStatement { + acc_instance: parsed_acc, + l1_mus, + l1_taus: l1_taus.clone(), + log_l, + log_m, log_n, - self.config.s, - self.config.t, - log_r, - )?; + }, + (), + )?; - // 6. Deferred twin-constraint oracle check. - (eq_poly_non_binary(&tau, &gamma_sumcheck) * (nus[0] + omega * eta) == tc_final_claim) - .ok_or_err(VerifierError::Target)?; + // 4. Orchestrator-side reads BETWEEN twin-constraint and OOD: td, η, ν₀. + let _td_bytes: [u8; 32] = verifier_state.prover_message()?; + let eta: F = verifier_state.prover_message()?; + let nu_0: F = verifier_state.prover_message()?; + + // 5. Discharge the typed deferred oracle check from TwinConstraint. + // Equivalent to: final_claim ≟ eq(τ, γ) · (ν₀ + ω·η). + tc_red.deferred.discharge(&tc_red.gamma, nu_0, eta)?; + + // 6. OOD::verify — squeeze samples; read answers. + let ood_phase = Ood::::new(); + let (ood_red, _) = ood_phase.verify( + verifier_state, + &OodStatement { + s: self.params.config.s, + log_n, + }, + (), + )?; - // 7. Proximity check. Must run *before* the batching sumcheck because - // σ₂ consumes `proof.6` (shift-query answers); a tampered row - // would otherwise surface as a batching-round consistency failure - // rather than the expected ShiftQuery/NumShiftQueries variant. - let binary_shift_queries_flat = bytes_shift_queries - .iter() - .flat_map(byte_to_binary_field_array) - .take(self.config.t * log_n) - .collect::>(); - let binary_shift_queries = binary_shift_queries_flat - .chunks(log_n) - .collect::>(); - let shift_queries_indexes: Vec = binary_shift_queries - .iter() - .map(|vals| binary_field_elements_to_usize(vals)) + // 7. Orchestrator: squeeze shift-query byte challenges, build + // QueryIndices, run Proximity::verify. + let n_shift_query_bytes = (self.params.config.t * log_n).div_ceil(8); + let bytes_shift_queries: Vec = (0..n_shift_query_bytes) + .map(|_| verifier_state.verifier_message::<[u8; 1]>()[0]) .collect(); + let queries: QueryIndices = + QueryIndices::from_squeezed_bytes(&bytes_shift_queries, log_n, self.params.config.t); - (proof.6.len() == self.config.t).ok_or_err(VerifierError::NumShiftQueries)?; - for (i, path) in proof.4.iter().enumerate() { - (path.leaf_index == shift_queries_indexes[i]) - .ok_or_err(VerifierError::ShiftQueryIndex)?; - let is_valid = path.verify( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - &rt_0, - &proof.6[i][l2..], - )?; - is_valid.ok_or_err(VerifierError::ShiftQuery)?; - } - (proof.5.len() == l2).ok_or_err(VerifierError::NumL2Instances)?; - for (i, paths) in proof.5.iter().enumerate() { - (paths.len() == self.config.t).ok_or_err(VerifierError::NumShiftQueries)?; - let root = &l2_roots[i]; - for (j, path) in paths.iter().enumerate() { - (path.leaf_index == shift_queries_indexes[j]) - .ok_or_err(VerifierError::ShiftQueryIndex)?; - let is_valid = path.verify( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - root, - [proof.6[j][i]], - )?; - is_valid.ok_or_err(VerifierError::ShiftQuery)?; - } - } - - // 8. Derive σ₂ and the reduced α/μ expectation. - let gamma_eq_evals = compute_hypercube_eq_evals(log_l, &gamma_sumcheck); - let alpha_vecs = concat_slices(&l2_alphas, &vec![vec![F::zero(); log_n]; l1]); - let zeta_0 = scale_and_sum(&alpha_vecs, &gamma_eq_evals); + let proximity_phase = Proximity:: { + mt_leaf_hash_params: &self.params.mt_leaf_hash_params, + mt_two_to_one_hash_params: &self.params.mt_two_to_one_hash_params, + _phantom: PhantomData, + }; + proximity_phase.verify( + verifier_state, + &ProximityStatement { + queries: queries.clone(), + l2, + t: self.params.config.t, + }, + ProximityVerifierInputs { + rt_0: &rt_0, + l2_roots: &l2_roots, + auth_0: &proof.auth_0, + auth_j: &proof.auth_j, + shift_query_answers: &proof.shift_query_answers, + _phantom: PhantomData, + }, + )?; - let mut nu_s_t = vec![F::default(); self.config.t]; - for (i, v_jk) in proof.6.iter().enumerate() { - nu_s_t[i] = v_jk + // 8. Compute ν_{s+k} from the (now proximity-verified) shift answers + // and the γ-equality table; assemble the full nus vector. + let gamma_eq_evals = compute_hypercube_eq_evals(log_l, &tc_red.gamma); + let mut nus = Vec::with_capacity(1 + self.params.config.s + self.params.config.t); + nus.push(nu_0); + nus.extend(ood_red.answers); + for v_jk in proof.shift_query_answers.iter() { + let nu_st = v_jk .iter() .zip(&gamma_eq_evals) .fold(F::zero(), |acc, (v, eq)| acc + *eq * *v); + nus.push(nu_st); } - nus.extend(nu_s_t); - - let xi_eq_evals = compute_hypercube_eq_evals(log_r, &xi); - let sigma_2 = xi_eq_evals - .iter() - .zip(&nus) - .fold(F::zero(), |acc, (xi_eq, nu)| acc + *xi_eq * nu); - - // 9. Batching (inner-product) sumcheck. Library handles round - // consistency; warp performs the oracle check: - // `final_claim == μ · Σ eq(ζ_i, α_lsb)·ξ_eq(i)`. MSB half-split - // → reverse the challenge vector once before feeding to - // eq_poly_non_binary (arkworks MLE convention). - let (alpha_sumcheck, batching_final_claim) = { - let mut wrap = EffscVerifierTranscript(verifier_state); - let mut res = sumcheck_verify(sigma_2, 2, log_n, &mut wrap, |_, _| Ok(()))?; - res.challenges.reverse(); - (res.challenges, res.final_claim) - }; - let mut zeta_eqs = Vec::with_capacity(r); - zeta_eqs.push(eq_poly_non_binary(&zeta_0, &alpha_sumcheck)); - for chunk in ood_samples.chunks(log_n) { - zeta_eqs.push(eq_poly_non_binary(chunk, &alpha_sumcheck)); + // 9. Build zetas_prefix; Batching::verify squeezes ξ, computes σ₂, + // runs sumcheck, and does the final-claim oracle check. + let mut zetas: Vec> = + Vec::with_capacity(1 + self.params.config.s + self.params.config.t); + zetas.push(tc_red.zeta_0.clone()); + for chunk in ood_red.samples_flat.chunks(log_n) { + zetas.push(chunk.to_vec()); } - for zeta in &binary_shift_queries { - zeta_eqs.push(eq_poly_non_binary(zeta, &alpha_sumcheck)); + for pt in &queries.evaluation_points { + zetas.push(pt.clone()); } - debug_assert_eq!(zeta_eqs.len(), r); - let expected_batching = acc_instance.2[0] - * zeta_eqs - .into_iter() - .zip(&xi_eq_evals) - .fold(F::zero(), |acc, (a, b)| acc + a * *b); - (expected_batching == batching_final_claim).ok_or_err(VerifierError::Target)?; - // 10. Accumulator consistency: new α and β. - (acc_instance.1[0] == alpha_sumcheck).ok_or_err(VerifierError::CodeEvaluationPoint)?; + let batching_phase = Batching::::new(); + let (batching_red, _) = batching_phase.verify( + verifier_state, + &BatchingStatement { + zetas_prefix: zetas, + s: self.params.config.s, + t: self.params.config.t, + log_n, + _phantom: PhantomData, + }, + BatchingVerifierInputs { + nus, + acc_mu: acc_mu_first, + }, + )?; + + // 10. Accumulator consistency checks for the new code / circuit + // evaluation points. + (acc_alpha_first == batching_red.alpha).ok_or_err(VerifierError::CodeEvaluationPoint)?; let betas = l2_taus .into_iter() @@ -727,7 +491,7 @@ impl< .map(|(tau_i, x)| concat_slices(&tau_i, &x)) .collect::>>(); let beta = scale_and_sum(&betas, &gamma_eq_evals); - let expected_beta = concat_slices(&acc_instance.3 .0[0], &acc_instance.3 .1[0]); + let expected_beta = concat_slices(&acc_beta_0_first, &acc_beta_1_first); (expected_beta == beta).ok_or_err(VerifierError::CircuitEvaluationPoint)?; Ok(()) @@ -735,372 +499,42 @@ impl< fn decide( &self, - acc_witness: Self::AccumulatorWitnesses, - acc_instance: Self::AccumulatorInstances, + acc_witness: AccumulatorWitness, + acc_instance: AccumulatorInstance, ) -> Result<(), WARPError> { - let (mt, f, w) = acc_witness; - let (rt, alpha, mu, beta, eta) = acc_instance; - let computed_mt = MerkleTree::::new( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, - f[0].chunks(1).collect::>(), + &self.params.mt_leaf_hash_params, + &self.params.mt_two_to_one_hash_params, + acc_witness.f[0].chunks(1).collect::>(), )?; - (rt[0] == computed_mt.root()).ok_or_err(DeciderError::MerkleRoot)?; - (mt[0].root() == computed_mt.root()).ok_or_err(DeciderError::MerkleTrapDoor)?; + (acc_instance.rt[0] == computed_mt.root()).ok_or_err(DeciderError::MerkleRoot)?; + (acc_witness.td[0].root() == computed_mt.root()).ok_or_err(DeciderError::MerkleTrapDoor)?; let f_hat = DenseMultilinearExtension::from_evaluations_slice( - log2(self.code.code_len()) as usize, - &f[0], - ); - (f_hat.evaluate(&alpha[0]) == mu[0]).ok_or_err(DeciderError::MLExtensionEvaluation)?; - - let tau = &beta.0[0]; - - let tau_zero_evader = compute_hypercube_eq_evals(tau.len(), tau); - - let mut z = beta.1[0].clone(); - z.extend(w[0].clone()); - let computed_eta = self.p.evaluate_bundled(&tau_zero_evader, &z).unwrap(); - (computed_eta == eta[0]).ok_or_err(DeciderError::BundledEvaluation)?; - - let computed_f = self.code.encode(&w[0]); - (f[0] == computed_f).ok_or_err(DeciderError::EncodedWitness)?; - - Ok(()) - } -} - -#[cfg(test)] -pub mod test { - use super::crypto::merkle::blake3::Blake3MerkleTreeParams; - use super::AccumulationScheme; - use crate::serialize::{AccInstanceSerializer, AccWitnessSerializer, ProofSerializer}; - use crate::{ - relations::{ - r1cs::{ - hashchain::{ - compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness, - }, - R1CS, - }, - BundledPESAT, Relation, ToPolySystem, - }, - utils::poseidon, - }; - - use ark_bls12_381::Fr as BLS12_381; - use ark_codes::{ - reed_solomon::{config::ReedSolomonConfig, ReedSolomon}, - traits::LinearCode, - }; - use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; - use ark_ff::UniformRand; - use ark_serialize::{CanonicalSerialize, Compress}; - use ark_std::rand::thread_rng; - - use std::marker::PhantomData; - - use super::{WARPConfig, WARP}; - - #[test] - pub fn warp_test() { - let l1 = 4; - let s = 8; - let t = 7; - let hash_chain_size = 10; - let mut rng = thread_rng(); - let poseidon_config = poseidon::initialize_poseidon_config::(); - let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( - poseidon_config.clone(), - hash_chain_size, - )) - .unwrap(); - let code_config = - ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); - let code = ReedSolomon::new(code_config.clone()); - - let instances_witnesses: (Vec>, Vec>) = (0..l1) - .map(|_| { - let preimage = vec![BLS12_381::rand(&mut rng)]; - let instance = HashChainInstance { - digest: compute_hash_chain::>( - &poseidon_config, - &preimage, - hash_chain_size, - ), - }; - let witness = HashChainWitness { - preimage, - _crhs_scheme: PhantomData::>, - }; - let relation = HashChainRelation::, CRHGadget<_>>::new( - instance, - witness, - (poseidon_config.clone(), hash_chain_size), - ); - (relation.x, relation.w) - }) - .unzip(); - - let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( - poseidon_config.clone(), - hash_chain_size, - )) - .unwrap(); - - let warp_config = WARPConfig::new(l1, l1, s, t, r1cs.config(), code.code_len()); - let hash_chain_warp = WARP::< - BLS12_381, - R1CS, - _, - Blake3MerkleTreeParams, - >::new( - warp_config.clone(), code.clone(), r1cs.clone(), (), () + log2(self.params.code.code_len()) as usize, + &acc_witness.f[0], ); + (f_hat.evaluate(&acc_instance.alpha[0]) == acc_instance.mu[0]) + .ok_or_err(DeciderError::MLExtensionEvaluation)?; - let (mut acc_roots, mut acc_alphas, mut acc_mus, mut acc_taus, mut acc_xs, mut acc_eta) = - (vec![], vec![], vec![], vec![], vec![], vec![]); - let (mut acc_tds, mut acc_f, mut acc_ws) = (vec![], vec![], vec![]); + let tau = &acc_instance.beta.0[0]; - for _ in 0..l1 { - let domainsep = spongefish::domain_separator!("test::warp"); - let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); - let ((acc_x, acc_w), _pf) = hash_chain_warp - .prove( - (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), - &mut prover_state, - instances_witnesses.1.clone(), - instances_witnesses.0.clone(), - (vec![], vec![], vec![], (vec![], vec![]), vec![]), - (vec![], vec![], vec![]), - ) - .unwrap(); - acc_roots.push(acc_x.0[0].clone()); - acc_alphas.push(acc_x.1[0].clone()); - acc_mus.push(acc_x.2[0]); - acc_taus.push(acc_x.3 .0[0].clone()); - acc_xs.push(acc_x.3 .1[0].clone()); - acc_eta.push(acc_x.4[0]); - - acc_tds.push(acc_w.0[0].clone()); - acc_f.push(acc_w.1[0].clone()); - acc_ws.push(acc_w.2[0].clone()); - } - - let domainsep = spongefish::domain_separator!("test::warp"); - let warp_config = - WARPConfig::<_, R1CS>::new(8, l1, s, t, r1cs.config(), code.code_len()); - - let hash_chain_warp = WARP::< - BLS12_381, - R1CS, - _, - Blake3MerkleTreeParams, - >::new( - warp_config.clone(), code.clone(), r1cs.clone(), (), () - ); - - let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); - let ((acc_x, acc_w), pf) = hash_chain_warp - .prove( - (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), - &mut prover_state, - instances_witnesses.1, - instances_witnesses.0, - (acc_roots, acc_alphas, acc_mus, (acc_taus, acc_xs), acc_eta), - (acc_tds, acc_f, acc_ws), - ) - .unwrap(); - - let narg_str = prover_state.narg_string().to_vec(); - let domainsep_v = spongefish::domain_separator!("test::warp"); - let mut verifier_state = domainsep_v - .without_session() - .instance(&0u32) - .std_verifier(&narg_str); - hash_chain_warp - .verify( - (r1cs.m, r1cs.n, r1cs.k), - &mut verifier_state, - acc_x.clone(), - pf.clone(), - ) - .unwrap(); - hash_chain_warp - .decide(acc_w.clone(), acc_x.clone()) - .unwrap(); - - let acc_x_to_serde = - AccInstanceSerializer::<_, Blake3MerkleTreeParams>::new(acc_x); - let acc_w_to_serde = - AccWitnessSerializer::<_, Blake3MerkleTreeParams>::new(acc_w); - let proof_to_serde = ProofSerializer::new(pf); - - println!( - "acc_x size: {}", - acc_x_to_serde.serialized_size(Compress::Yes) - ); - println!( - "acc_w size: {}", - acc_w_to_serde.serialized_size(Compress::Yes) - ); - println!( - "proof size: {}", - proof_to_serde.serialized_size(Compress::Yes) - ); - println!("narg_str size: {}", narg_str.len()); - } - - #[test] - pub fn warp_test_goldilocks() { - use crate::utils::fields::Goldilocks; - - let l1 = 4; - let s = 8; - let t = 7; - let hash_chain_size = 10; - let mut rng = thread_rng(); - let poseidon_config = poseidon::initialize_poseidon_config::(); - let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( - poseidon_config.clone(), - hash_chain_size, - )) - .unwrap(); - let code_config = - ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); - let code = ReedSolomon::new(code_config); - - let instances_witnesses: (Vec>, Vec>) = (0..l1) - .map(|_| { - let preimage = vec![Goldilocks::rand(&mut rng)]; - let instance = HashChainInstance { - digest: compute_hash_chain::>( - &poseidon_config, - &preimage, - hash_chain_size, - ), - }; - let witness = HashChainWitness { - preimage, - _crhs_scheme: PhantomData::>, - }; - let relation = HashChainRelation::, CRHGadget<_>>::new( - instance, - witness, - (poseidon_config.clone(), hash_chain_size), - ); - (relation.x, relation.w) - }) - .unzip(); - - let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( - poseidon_config.clone(), - hash_chain_size, - )) - .unwrap(); - - let warp_config = WARPConfig::new(l1, l1, s, t, r1cs.config(), code.code_len()); - let hash_chain_warp = WARP::< - Goldilocks, - R1CS, - _, - Blake3MerkleTreeParams, - >::new( - warp_config.clone(), code.clone(), r1cs.clone(), (), () - ); - - let (mut acc_roots, mut acc_alphas, mut acc_mus, mut acc_taus, mut acc_xs, mut acc_eta) = - (vec![], vec![], vec![], vec![], vec![], vec![]); - let (mut acc_tds, mut acc_f, mut acc_ws) = (vec![], vec![], vec![]); - - for _ in 0..l1 { - let domainsep = spongefish::domain_separator!("test::warp"); - let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); - let ((acc_x, acc_w), _pf) = hash_chain_warp - .prove( - (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), - &mut prover_state, - instances_witnesses.1.clone(), - instances_witnesses.0.clone(), - (vec![], vec![], vec![], (vec![], vec![]), vec![]), - (vec![], vec![], vec![]), - ) - .unwrap(); - acc_roots.push(acc_x.0[0].clone()); - acc_alphas.push(acc_x.1[0].clone()); - acc_mus.push(acc_x.2[0]); - acc_taus.push(acc_x.3 .0[0].clone()); - acc_xs.push(acc_x.3 .1[0].clone()); - acc_eta.push(acc_x.4[0]); - - acc_tds.push(acc_w.0[0].clone()); - acc_f.push(acc_w.1[0].clone()); - acc_ws.push(acc_w.2[0].clone()); - } - - let domainsep = spongefish::domain_separator!("test::warp"); - // Use 8 (2*l1) for the total accumulation size to test multi-instance accumulation - let warp_config = - WARPConfig::<_, R1CS>::new(8, l1, s, t, r1cs.config(), code.code_len()); - - let hash_chain_warp = WARP::< - Goldilocks, - R1CS, - _, - Blake3MerkleTreeParams, - >::new( - warp_config.clone(), code.clone(), r1cs.clone(), (), () - ); - - let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); - let ((acc_x, acc_w), pf) = hash_chain_warp - .prove( - (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), - &mut prover_state, - instances_witnesses.1, - instances_witnesses.0, - (acc_roots, acc_alphas, acc_mus, (acc_taus, acc_xs), acc_eta), - (acc_tds, acc_f, acc_ws), - ) - .unwrap(); + let tau_zero_evader = Ascending::new(tau.len()) + .map(|p| eq_poly(tau, p.index)) + .collect::>(); - let narg_str = prover_state.narg_string().to_vec(); - let domainsep_v = spongefish::domain_separator!("test::warp"); - let mut verifier_state = domainsep_v - .without_session() - .instance(&0u32) - .std_verifier(&narg_str); - hash_chain_warp - .verify( - (r1cs.m, r1cs.n, r1cs.k), - &mut verifier_state, - acc_x.clone(), - pf.clone(), - ) - .unwrap(); - hash_chain_warp - .decide(acc_w.clone(), acc_x.clone()) + let mut z = acc_instance.beta.1[0].clone(); + z.extend(acc_witness.w[0].clone()); + let computed_eta = self + .params + .p + .evaluate_bundled(&tau_zero_evader, &z) .unwrap(); + (computed_eta == acc_instance.eta[0]).ok_or_err(DeciderError::BundledEvaluation)?; - let acc_x_to_serde = - AccInstanceSerializer::<_, Blake3MerkleTreeParams>::new(acc_x); - let acc_w_to_serde = - AccWitnessSerializer::<_, Blake3MerkleTreeParams>::new(acc_w); - let proof_to_serde = ProofSerializer::new(pf); + let computed_f = self.params.code.encode(&acc_witness.w[0]); + (acc_witness.f[0] == computed_f).ok_or_err(DeciderError::EncodedWitness)?; - println!( - "Goldilocks acc_x size: {}", - acc_x_to_serde.serialized_size(Compress::Yes) - ); - println!( - "Goldilocks acc_w size: {}", - acc_w_to_serde.serialized_size(Compress::Yes) - ); - println!( - "Goldilocks proof size: {}", - proof_to_serde.serialized_size(Compress::Yes) - ); - println!("Goldilocks narg_str size: {}", narg_str.len()); + Ok(()) } } diff --git a/src/params/mod.rs b/src/params/mod.rs new file mode 100644 index 0000000..401d433 --- /dev/null +++ b/src/params/mod.rs @@ -0,0 +1,149 @@ +//! Soundness-driven parameter selection for WARP. +//! +//! Paired spec: `docs/paper-mods/mod4_parameter_selection.tex`. +//! +//! Given a security target, a field, a Reed–Solomon code rate, and a +//! choice of list-decoding regime, [`select`] returns the smallest +//! `(s, t)` tuple that achieves the target. [`validate`] is the inverse +//! and reports the soundness of an already-chosen tuple. +//! +//! The workload parameters `l` and `l1` are **not** chosen here — they +//! are caller-driven by the batch size the application actually needs. +//! +//! Derivation limits are called out in the companion `.tex`; the +//! formulas capture the dominant proximity-query term only. + +pub mod presets; +pub mod select; +pub mod types; +pub mod validate; + +pub use presets::{lookup, Preset, PRESETS}; +pub use select::select; +pub use types::{ParamError, Params, Regime, SecurityLevel, SoundnessBound}; +pub use validate::validate; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn select_validate_roundtrip_provable() { + let lambda = SecurityLevel::STANDARD_128; + let p = select(lambda, 254, 0.5, Regime::Provable).unwrap(); + assert!( + p.t >= 256, + "provable t at λ=128, ρ=1/2 should need ≥ 256 queries, got {}", + p.t + ); + let bound = validate(&p, 254, 0.5, Regime::Provable, lambda).unwrap(); + assert!( + bound.meets(lambda), + "selected params should self-validate: {bound:?}" + ); + } + + #[test] + fn select_validate_roundtrip_conjectured() { + let lambda = SecurityLevel::STANDARD_128; + let p = select(lambda, 254, 0.5, Regime::Conjectured).unwrap(); + // Conjectured halves the query count at ρ=0.5 (2 vs 1 bits per query). + assert!(p.t >= 128 && p.t < 256); + let bound = validate(&p, 254, 0.5, Regime::Conjectured, lambda).unwrap(); + assert!(bound.meets(lambda)); + } + + #[test] + fn smaller_rate_needs_fewer_queries() { + let lambda = SecurityLevel::STANDARD_128; + let p_half = select(lambda, 254, 0.5, Regime::Provable).unwrap(); + let p_eighth = select(lambda, 254, 0.125, Regime::Provable).unwrap(); + assert!( + p_eighth.t < p_half.t, + "ρ=1/8 needs fewer queries than ρ=1/2: {} vs {}", + p_eighth.t, + p_half.t + ); + } + + #[test] + fn conjectured_needs_fewer_queries_than_provable() { + let lambda = SecurityLevel::STANDARD_128; + let p_prov = select(lambda, 254, 0.5, Regime::Provable).unwrap(); + let p_conj = select(lambda, 254, 0.5, Regime::Conjectured).unwrap(); + assert!(p_conj.t < p_prov.t); + } + + #[test] + fn field_too_small_is_rejected() { + // 64-bit field, 128-bit target: should fail admissibility. + assert!(matches!( + select(SecurityLevel::STANDARD_128, 64, 0.5, Regime::Provable), + Err(ParamError::FieldTooSmall { .. }) + )); + } + + #[test] + fn invalid_rate_is_rejected() { + let lambda = SecurityLevel::STANDARD_80; + assert_eq!( + select(lambda, 254, 0.0, Regime::Provable), + Err(ParamError::InvalidRate) + ); + assert_eq!( + select(lambda, 254, 1.0, Regime::Conjectured), + Err(ParamError::InvalidRate) + ); + } + + #[test] + fn presets_self_validate() { + // Every preset should achieve its claimed lambda under its regime. + for preset in PRESETS { + // Use 254-bit field so field_admissible passes at 128-bit. + let bound = validate( + &preset.params, + 254, + preset.code_rate(), + preset.regime, + preset.lambda, + ) + .unwrap(); + assert!( + bound.meets(preset.lambda), + "preset {:?} rate={:?} regime={:?} fails its own target: {:?}", + preset.lambda, + preset.code_rate(), + preset.regime, + bound + ); + } + } + + #[test] + fn presets_match_select_output() { + for preset in PRESETS { + let recomputed = select(preset.lambda, 254, preset.code_rate(), preset.regime).unwrap(); + assert_eq!( + recomputed, + preset.params, + "preset drift: {:?} rate={:?} regime={:?}", + preset.lambda, + preset.code_rate(), + preset.regime + ); + } + } + + #[test] + fn lookup_round_trips() { + let p = lookup(SecurityLevel::STANDARD_128, 1, 2, Regime::Conjectured).unwrap(); + assert_eq!(p.params.t, 128); + } + + #[test] + fn lookup_misses_on_unknown_rate() { + // We have 1/2 and 1/8 on file; 1/4 is not a preset. + assert!(lookup(SecurityLevel::STANDARD_128, 1, 4, Regime::Provable).is_none()); + } +} diff --git a/src/params/presets.rs b/src/params/presets.rs new file mode 100644 index 0000000..a9f7366 --- /dev/null +++ b/src/params/presets.rs @@ -0,0 +1,108 @@ +//! Pre-computed parameter tuples for common `(λ, code_rate)` points. +//! +//! Each entry is derived by [`super::select`] at the named regime and +//! stored here as a `const` lookup so tests and CLIs don't need to re-run +//! the derivation on every invocation. If you change the bounds in +//! `mod4_parameter_selection.tex`, regenerate this table via +//! `cargo run --bin warp-params -- table`. + +use super::types::{Params, Regime, SecurityLevel}; + +/// One row of [`PRESETS`]. +pub struct Preset { + pub lambda: SecurityLevel, + pub code_rate_num: u32, + pub code_rate_den: u32, + pub regime: Regime, + pub params: Params, +} + +impl Preset { + pub fn code_rate(&self) -> f64 { + self.code_rate_num as f64 / self.code_rate_den as f64 + } +} + +/// Common `(λ, rate, regime) → (s, t)` selections. See module docstring +/// on regeneration. +/// +/// All entries use the minimum `s = 8` (see +/// `mod4_parameter_selection.tex` §2); `t` is the smallest integer that +/// meets the target under the named regime. +pub const PRESETS: &[Preset] = &[ + // λ=80 @ rate 1/2 + Preset { + lambda: SecurityLevel::STANDARD_80, + code_rate_num: 1, + code_rate_den: 2, + regime: Regime::Provable, + params: Params { s: 8, t: 160 }, + }, + Preset { + lambda: SecurityLevel::STANDARD_80, + code_rate_num: 1, + code_rate_den: 2, + regime: Regime::Conjectured, + params: Params { s: 8, t: 80 }, + }, + // λ=80 @ rate 1/8 (three bits per query under provable) + Preset { + lambda: SecurityLevel::STANDARD_80, + code_rate_num: 1, + code_rate_den: 8, + regime: Regime::Provable, + params: Params { s: 8, t: 54 }, + }, + Preset { + lambda: SecurityLevel::STANDARD_80, + code_rate_num: 1, + code_rate_den: 8, + regime: Regime::Conjectured, + params: Params { s: 8, t: 27 }, + }, + // λ=128 @ rate 1/2 + Preset { + lambda: SecurityLevel::STANDARD_128, + code_rate_num: 1, + code_rate_den: 2, + regime: Regime::Provable, + params: Params { s: 8, t: 256 }, + }, + Preset { + lambda: SecurityLevel::STANDARD_128, + code_rate_num: 1, + code_rate_den: 2, + regime: Regime::Conjectured, + params: Params { s: 8, t: 128 }, + }, + // λ=128 @ rate 1/8 + Preset { + lambda: SecurityLevel::STANDARD_128, + code_rate_num: 1, + code_rate_den: 8, + regime: Regime::Provable, + params: Params { s: 8, t: 86 }, + }, + Preset { + lambda: SecurityLevel::STANDARD_128, + code_rate_num: 1, + code_rate_den: 8, + regime: Regime::Conjectured, + params: Params { s: 8, t: 43 }, + }, +]; + +/// Look up a preset by `(λ, num/den, regime)`. Exact-rational match — +/// callers that parsed the rate as a fraction preserve the exact form +/// and get a hit. Returns `None` if no exact row matches; use +/// [`super::select`] for arbitrary inputs. +pub fn lookup( + lambda: SecurityLevel, + num: u32, + den: u32, + regime: Regime, +) -> Option<&'static Preset> { + PRESETS.iter().find(|p| { + p.lambda == lambda && p.code_rate_num == num && p.code_rate_den == den && p.regime == regime + }) +} diff --git a/src/params/select.rs b/src/params/select.rs new file mode 100644 index 0000000..56f11a1 --- /dev/null +++ b/src/params/select.rs @@ -0,0 +1,54 @@ +//! Soundness-driven parameter selection. +//! +//! Paired spec: `docs/paper-mods/mod4_parameter_selection.tex`. +//! +//! Computes the smallest `(s, t)` tuple that achieves the requested +//! security level under the chosen list-decoding regime. See the `.tex` +//! for the derivation and which bounds were used from the STIR / WHIR +//! literature. + +use super::types::{ParamError, Params, Regime, SecurityLevel}; + +/// Minimum OOD samples. Covers the constant term used in the current +/// derivation; tightening this against the batching-sumcheck soundness +/// is deferred — see `mod4_parameter_selection.tex` §2. +const S_MIN: usize = 8; + +/// Additive slack on the field-size admissibility check: we require +/// `log₂|F| ≥ λ + FIELD_EPSILON` so the polylog noise terms are +/// negligible at the target level. +pub const FIELD_EPSILON: u32 = 40; + +/// Select the minimum `(s, t)` achieving `lambda` bits of soundness for +/// a Reed–Solomon code of rate `code_rate` over a field of `field_bits`, +/// under the chosen regime. +/// +/// Returns `Err(ParamError::InvalidRate)` if `code_rate ∉ (0, 1)`, and +/// `Err(ParamError::FieldTooSmall)` if the field cannot cover the polylog +/// noise at the requested target (see §3 of the paper-mods spec). +pub fn select( + lambda: SecurityLevel, + field_bits: u32, + code_rate: f64, + regime: Regime, +) -> Result { + if !(0.0 < code_rate && code_rate < 1.0) { + return Err(ParamError::InvalidRate); + } + if field_bits < lambda.bits() + FIELD_EPSILON { + return Err(ParamError::FieldTooSmall { + field_bits, + lambda: lambda.bits(), + }); + } + + let bits_per_query = match regime { + Regime::Provable => 0.5 * (-code_rate.log2()), + Regime::Conjectured => -code_rate.log2(), + }; + debug_assert!(bits_per_query > 0.0); + + let t = (lambda.bits() as f64 / bits_per_query).ceil() as usize; + + Ok(Params { s: S_MIN, t }) +} diff --git a/src/params/types.rs b/src/params/types.rs new file mode 100644 index 0000000..c2e73c8 --- /dev/null +++ b/src/params/types.rs @@ -0,0 +1,81 @@ +//! Parameter-selection types. +//! +//! Paired spec: `docs/paper-mods/mod4_parameter_selection.tex`. + +/// Target soundness, in bits. `SecurityLevel(128)` means the soundness +/// error should be at most 2⁻¹²⁸. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct SecurityLevel(pub u32); + +impl SecurityLevel { + pub const STANDARD_80: Self = Self(80); + pub const STANDARD_100: Self = Self(100); + pub const STANDARD_128: Self = Self(128); + pub const STANDARD_192: Self = Self(192); + pub const STANDARD_256: Self = Self(256); + + pub fn bits(self) -> u32 { + self.0 + } +} + +/// Which list-decoding regime to assume when bounding proximity-query +/// soundness. See `docs/paper-mods/mod4_parameter_selection.tex` §2. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Regime { + /// Johnson bound — radius `1 − √ρ`. Soundness error per query is + /// `√ρ`; proven for Reed–Solomon in the STIR / WHIR lineage. + Provable, + /// Conjectured list-decodability up to radius `1 − ρ`. Soundness + /// error per query is `ρ`; halves the required query count vs. + /// provable at the same target. + Conjectured, +} + +/// Selected security-driven WARP parameters. +/// +/// Workload parameters (`l`, `l1`) are *not* chosen here — they're caller- +/// supplied based on the batch size the application needs. This struct +/// carries only the choices whose minimum is dictated by soundness. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Params { + /// OOD samples. + pub s: usize, + /// Proximity / shift queries. + pub t: usize, +} + +/// Result of [`crate::params::validate`]. Reports the log₂ soundness error +/// achievable under the given `(params, rate, regime)`, together with the +/// field-size admissibility check. +#[derive(Clone, Copy, Debug)] +pub struct SoundnessBound { + /// `-log₂` of the proximity-query soundness error contribution. + pub proximity_bits: f64, + /// Whether the field is large enough that the polylog-noise + /// contributions are negligible at the target level. + pub field_admissible: bool, + /// Whether the selected `s` meets the minimum OOD sample count + /// used in the current formulas. Currently always true with the + /// hard-coded `S_MIN`; kept as a field so a future refinement can + /// surface a failure. + pub ood_admissible: bool, +} + +impl SoundnessBound { + /// `true` iff every component check passes at the caller's target. + pub fn meets(&self, target: SecurityLevel) -> bool { + self.field_admissible && self.ood_admissible && self.proximity_bits >= target.bits() as f64 + } +} + +/// Reasons [`crate::params::select`] or [`crate::params::validate`] can +/// reject a configuration. Never panics. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ParamError { + /// Code rate was out of range `(0, 1)`. + InvalidRate, + /// Field is too small to support the target soundness even with + /// infinite queries. + FieldTooSmall { field_bits: u32, lambda: u32 }, +} diff --git a/src/params/validate.rs b/src/params/validate.rs new file mode 100644 index 0000000..c0ce171 --- /dev/null +++ b/src/params/validate.rs @@ -0,0 +1,43 @@ +//! Soundness validation — the inverse of [`super::select`]. +//! +//! Given a `Params`, report how many bits of security it gives under the +//! chosen regime, and whether each component check passes. + +use super::select::FIELD_EPSILON; +use super::types::{ParamError, Params, Regime, SecurityLevel, SoundnessBound}; + +const S_MIN: usize = 8; + +/// Compute the soundness bound that `params` achieves on a rate-`code_rate` +/// Reed–Solomon code over a field of `field_bits`, under `regime`. +/// +/// Returns `Err(ParamError::InvalidRate)` if the rate is outside `(0, 1)`. +/// `FieldTooSmall` is not returned here — field admissibility is surfaced +/// on the returned [`SoundnessBound`] so callers can reason about partial +/// failures. +pub fn validate( + params: &Params, + field_bits: u32, + code_rate: f64, + regime: Regime, + target: SecurityLevel, +) -> Result { + if !(0.0 < code_rate && code_rate < 1.0) { + return Err(ParamError::InvalidRate); + } + + let bits_per_query = match regime { + Regime::Provable => 0.5 * (-code_rate.log2()), + Regime::Conjectured => -code_rate.log2(), + }; + + let proximity_bits = (params.t as f64) * bits_per_query; + let field_admissible = field_bits >= target.bits() + FIELD_EPSILON; + let ood_admissible = params.s >= S_MIN; + + Ok(SoundnessBound { + proximity_bits, + field_admissible, + ood_admissible, + }) +} diff --git a/src/profile/counters.rs b/src/profile/counters.rs new file mode 100644 index 0000000..3aa6077 --- /dev/null +++ b/src/profile/counters.rs @@ -0,0 +1,237 @@ +//! Thread-local op counters. +//! +//! Call-site counters for operations we can cheaply observe at the +//! boundaries of our crate (Merkle tree builds, path generations, MLE +//! materialisations, sumcheck rounds). Field-level ops (muls/adds) are +//! **not** counted here — they would require newtyping `F` or forking +//! arkworks. That's deferred; Plan O's goal is asymptotic validation, not +//! per-op accounting. +//! +//! All counters compile to no-ops without the `profile` feature: the +//! [`count_ops!`] macro expands to `()`. + +#[cfg(feature = "profile")] +use std::cell::Cell; + +/// Counter slots. Adding a new one: extend the enum, extend [`Counters`], +/// extend the [`count_ops!`] match, extend [`snapshot`] and [`delta`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Counter { + EncodeCalls, + MerkleTreeBuilds, + MerklePathsGenerated, + MerklePathsVerified, + MleMaterializations, + OracleLeafQueries, + OraclePointQueries, + TwinConstraintRounds, + BatchingRounds, + OodPointQueries, +} + +impl Counter { + pub const ALL: &'static [Counter] = &[ + Counter::EncodeCalls, + Counter::MerkleTreeBuilds, + Counter::MerklePathsGenerated, + Counter::MerklePathsVerified, + Counter::MleMaterializations, + Counter::OracleLeafQueries, + Counter::OraclePointQueries, + Counter::TwinConstraintRounds, + Counter::BatchingRounds, + Counter::OodPointQueries, + ]; + + pub fn name(self) -> &'static str { + match self { + Counter::EncodeCalls => "encode_calls", + Counter::MerkleTreeBuilds => "merkle_tree_builds", + Counter::MerklePathsGenerated => "merkle_paths_generated", + Counter::MerklePathsVerified => "merkle_paths_verified", + Counter::MleMaterializations => "mle_materializations", + Counter::OracleLeafQueries => "oracle_leaf_queries", + Counter::OraclePointQueries => "oracle_point_queries", + Counter::TwinConstraintRounds => "twin_constraint_rounds", + Counter::BatchingRounds => "batching_rounds", + Counter::OodPointQueries => "ood_point_queries", + } + } +} + +#[cfg(feature = "profile")] +thread_local! { + static COUNTERS: Counters = const { Counters::new() }; +} + +#[cfg(feature = "profile")] +struct Counters { + encode_calls: Cell, + merkle_tree_builds: Cell, + merkle_paths_generated: Cell, + merkle_paths_verified: Cell, + mle_materializations: Cell, + oracle_leaf_queries: Cell, + oracle_point_queries: Cell, + twin_constraint_rounds: Cell, + batching_rounds: Cell, + ood_point_queries: Cell, +} + +#[cfg(feature = "profile")] +impl Counters { + const fn new() -> Self { + Self { + encode_calls: Cell::new(0), + merkle_tree_builds: Cell::new(0), + merkle_paths_generated: Cell::new(0), + merkle_paths_verified: Cell::new(0), + mle_materializations: Cell::new(0), + oracle_leaf_queries: Cell::new(0), + oracle_point_queries: Cell::new(0), + twin_constraint_rounds: Cell::new(0), + batching_rounds: Cell::new(0), + ood_point_queries: Cell::new(0), + } + } + + fn cell(&self, c: Counter) -> &Cell { + match c { + Counter::EncodeCalls => &self.encode_calls, + Counter::MerkleTreeBuilds => &self.merkle_tree_builds, + Counter::MerklePathsGenerated => &self.merkle_paths_generated, + Counter::MerklePathsVerified => &self.merkle_paths_verified, + Counter::MleMaterializations => &self.mle_materializations, + Counter::OracleLeafQueries => &self.oracle_leaf_queries, + Counter::OraclePointQueries => &self.oracle_point_queries, + Counter::TwinConstraintRounds => &self.twin_constraint_rounds, + Counter::BatchingRounds => &self.batching_rounds, + Counter::OodPointQueries => &self.ood_point_queries, + } + } +} + +/// Bump a counter on the current thread. +#[cfg(feature = "profile")] +#[inline] +pub fn bump(c: Counter, n: u64) { + COUNTERS.with(|cs| { + let cell = cs.cell(c); + cell.set(cell.get().saturating_add(n)); + }); +} + +#[cfg(not(feature = "profile"))] +#[inline] +pub fn bump(_c: Counter, _n: u64) {} + +/// Snapshot every counter on the current thread. +#[cfg(feature = "profile")] +pub fn snapshot() -> Snapshot { + let mut values = [0u64; Counter::ALL.len()]; + COUNTERS.with(|cs| { + for (i, &c) in Counter::ALL.iter().enumerate() { + values[i] = cs.cell(c).get(); + } + }); + Snapshot { values } +} + +#[cfg(not(feature = "profile"))] +pub fn snapshot() -> Snapshot { + Snapshot {} +} + +/// A point-in-time reading of all counters. Subtract two snapshots with +/// [`Snapshot::delta`] to get the change over an interval. +#[cfg(feature = "profile")] +#[derive(Clone, Copy, Debug)] +pub struct Snapshot { + values: [u64; Counter::ALL.len()], +} + +#[cfg(feature = "profile")] +impl Snapshot { + pub fn get(&self, c: Counter) -> u64 { + let idx = Counter::ALL.iter().position(|x| *x == c).unwrap(); + self.values[idx] + } + + /// `later.delta(&earlier)` returns `later - earlier`, per counter. + pub fn delta(&self, earlier: &Snapshot) -> Delta { + let mut values = [0u64; Counter::ALL.len()]; + for (i, slot) in values.iter_mut().enumerate() { + *slot = self.values[i].saturating_sub(earlier.values[i]); + } + Delta { values } + } +} + +#[cfg(not(feature = "profile"))] +#[derive(Clone, Copy, Debug)] +pub struct Snapshot {} + +#[cfg(not(feature = "profile"))] +impl Snapshot { + pub fn get(&self, _c: Counter) -> u64 { + 0 + } + pub fn delta(&self, _earlier: &Snapshot) -> Delta { + Delta {} + } +} + +#[cfg(feature = "profile")] +#[derive(Clone, Copy, Debug)] +pub struct Delta { + values: [u64; Counter::ALL.len()], +} + +#[cfg(feature = "profile")] +impl Delta { + pub fn get(&self, c: Counter) -> u64 { + let idx = Counter::ALL.iter().position(|x| *x == c).unwrap(); + self.values[idx] + } + + /// Iterate over `(Counter, delta)` pairs whose delta is non-zero. + pub fn iter_nonzero(&self) -> impl Iterator + '_ { + Counter::ALL + .iter() + .zip(self.values.iter()) + .filter_map(|(c, v)| if *v > 0 { Some((*c, *v)) } else { None }) + } +} + +#[cfg(not(feature = "profile"))] +#[derive(Clone, Copy, Debug)] +pub struct Delta {} + +#[cfg(not(feature = "profile"))] +impl Delta { + pub fn get(&self, _c: Counter) -> u64 { + 0 + } + pub fn iter_nonzero(&self) -> std::iter::Empty<(Counter, u64)> { + std::iter::empty() + } +} + +/// Increment a named counter by 1 (or by a caller-supplied amount). +/// +/// Under `profile` feature: compiles to a thread-local Cell bump. +/// Without `profile`: compiles to `()`. +/// +/// ```ignore +/// count_ops!(MerkleTreeBuilds); // +1 +/// count_ops!(OraclePointQueries, 3); // +3 +/// ``` +#[macro_export] +macro_rules! count_ops { + ($counter:ident) => { + $crate::profile::counters::bump($crate::profile::counters::Counter::$counter, 1); + }; + ($counter:ident, $n:expr) => { + $crate::profile::counters::bump($crate::profile::counters::Counter::$counter, $n); + }; +} diff --git a/src/profile/layer.rs b/src/profile/layer.rs new file mode 100644 index 0000000..98000ea --- /dev/null +++ b/src/profile/layer.rs @@ -0,0 +1,187 @@ +//! Tracing Layer that emits one JSON record per closed span. +//! +//! Span fields are captured via a [`FieldVisitor`] on `on_new_span`; +//! counter / timing snapshots are stashed in span extensions on +//! `on_enter` and differenced on `on_close`. +//! +//! Schema (version `warp.profile.v1`): +//! +//! ```json +//! { +//! "schema": "warp.profile.v1", +//! "phase": "twin_constraint", +//! "wall_ns": 123456, +//! "cpu_ns": 98765, +//! "rss_delta_bytes": 1024, +//! "counters": { "twin_constraint_rounds": 10, ... }, +//! "dimensions": { "log_l": 3, "log_m": 2, "log_n": 5 } +//! } +//! ``` +//! +//! One record per line (newline-delimited JSON), written to the configured +//! `io::Write` sink. The enclosing module gates this file behind the +//! `profile` feature; no per-file `cfg` is needed here. + +use std::collections::BTreeMap; +use std::io::Write; +use std::sync::Mutex; +use std::time::Instant; + +use tracing::field::{Field, Visit}; +use tracing::span::{Attributes, Id}; +use tracing::Subscriber; +use tracing_subscriber::layer::Context; +use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::Layer; + +use crate::profile::counters::{self, Snapshot}; +use crate::profile::{rss, timing}; + +/// What we stash on each span at enter-time. +struct SpanStart { + wall: Instant, + cpu_ns: Option, + rss_bytes: Option, + counters: Snapshot, +} + +/// Dimensions (numeric span fields) captured at span-creation time. +struct Dimensions(BTreeMap); + +impl Visit for Dimensions { + fn record_i64(&mut self, field: &Field, value: i64) { + self.0.insert(field.name().to_owned(), value as i128); + } + fn record_u64(&mut self, field: &Field, value: u64) { + self.0.insert(field.name().to_owned(), value as i128); + } + fn record_i128(&mut self, field: &Field, value: i128) { + self.0.insert(field.name().to_owned(), value); + } + fn record_u128(&mut self, field: &Field, value: u128) { + self.0.insert(field.name().to_owned(), value as i128); + } + fn record_bool(&mut self, field: &Field, value: bool) { + self.0.insert(field.name().to_owned(), value as i128); + } + fn record_debug(&mut self, _field: &Field, _value: &dyn std::fmt::Debug) {} +} + +/// A tracing `Layer` that emits `warp.profile.v1` JSON records on +/// span close. Thread-safe: wraps the writer in a `Mutex`. +pub struct JsonLayer { + writer: Mutex, +} + +impl JsonLayer { + pub fn new(writer: W) -> Self { + Self { + writer: Mutex::new(writer), + } + } +} + +impl Layer for JsonLayer +where + S: Subscriber + for<'a> LookupSpan<'a>, + W: Write + Send + 'static, +{ + fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { + let mut dims = Dimensions(BTreeMap::new()); + attrs.record(&mut dims); + if let Some(span) = ctx.span(id) { + span.extensions_mut().insert(dims); + } + } + + fn on_enter(&self, id: &Id, ctx: Context<'_, S>) { + let Some(span) = ctx.span(id) else { return }; + let start = SpanStart { + wall: Instant::now(), + cpu_ns: timing::thread_cpu_ns(), + rss_bytes: rss::peak_rss_bytes(), + counters: counters::snapshot(), + }; + span.extensions_mut().insert(start); + } + + fn on_close(&self, id: Id, ctx: Context<'_, S>) { + let Some(span) = ctx.span(&id) else { return }; + let ext = span.extensions(); + let Some(start) = ext.get::() else { + return; + }; + + let wall_ns = start.wall.elapsed().as_nanos() as u64; + let cpu_ns = match (timing::thread_cpu_ns(), start.cpu_ns) { + (Some(end), Some(begin)) => Some(end.saturating_sub(begin)), + _ => None, + }; + let rss_delta = match (rss::peak_rss_bytes(), start.rss_bytes) { + (Some(end), Some(begin)) => Some(end.saturating_sub(begin) as i64), + _ => None, + }; + let delta = counters::snapshot().delta(&start.counters); + + let dims_default = Dimensions(BTreeMap::new()); + let dims = ext.get::().unwrap_or(&dims_default); + + let mut out = Vec::with_capacity(256); + let _ = write!(out, "{{\"schema\":\"warp.profile.v1\",\"phase\":"); + write_json_string(&mut out, span.name()); + let _ = write!(out, ",\"wall_ns\":{wall_ns}"); + if let Some(c) = cpu_ns { + let _ = write!(out, ",\"cpu_ns\":{c}"); + } + if let Some(r) = rss_delta { + let _ = write!(out, ",\"rss_delta_bytes\":{r}"); + } + + // counters + let _ = write!(out, ",\"counters\":{{"); + let mut first = true; + for (c, v) in delta.iter_nonzero() { + if !first { + let _ = write!(out, ","); + } + first = false; + let _ = write!(out, "\"{}\":{}", c.name(), v); + } + let _ = write!(out, "}}"); + + // dimensions + let _ = write!(out, ",\"dimensions\":{{"); + let mut first = true; + for (k, v) in &dims.0 { + if !first { + let _ = write!(out, ","); + } + first = false; + write_json_string(&mut out, k); + let _ = write!(out, ":{v}"); + } + let _ = writeln!(out, "}}}}"); + + if let Ok(mut w) = self.writer.lock() { + let _ = w.write_all(&out); + } + } +} + +fn write_json_string(out: &mut Vec, s: &str) { + out.push(b'"'); + for b in s.bytes() { + match b { + b'"' => out.extend_from_slice(b"\\\""), + b'\\' => out.extend_from_slice(b"\\\\"), + b'\n' => out.extend_from_slice(b"\\n"), + b'\r' => out.extend_from_slice(b"\\r"), + b'\t' => out.extend_from_slice(b"\\t"), + 0x00..=0x1F => { + let _ = write!(out, "\\u{b:04x}"); + } + _ => out.push(b), + } + } + out.push(b'"'); +} diff --git a/src/profile/mod.rs b/src/profile/mod.rs new file mode 100644 index 0000000..575ceaf --- /dev/null +++ b/src/profile/mod.rs @@ -0,0 +1,84 @@ +//! Profile instrumentation. +//! +//! The library always emits `tracing` spans at phase boundaries; this +//! module installs a subscriber that renders those spans. Rendering is +//! off by default — the `profile` cargo feature is required to bring in +//! `tracing-subscriber` and `libc` (for `clock_gettime` / `getrusage`). +//! +//! Two sinks are provided: +//! * [`init()`] / [`init_fmt()`] — human-readable text on stderr. +//! * [`init_json(writer)`] — newline-delimited JSON records to any +//! `io::Write` sink (stderr, a file, a pipe). Schema: +//! `warp.profile.v1`. See [`layer`] for the field list. +//! +//! Each installs a global subscriber the first time it is called; later +//! calls are no-ops. Without the feature, every init function returns +//! `false` and records nothing. + +pub mod counters; +pub mod rss; +pub mod timing; + +#[cfg(feature = "profile")] +pub mod layer; + +/// Install a human-readable stderr subscriber (mimics the old +/// `[PROFILE]` lines). No-op without the `profile` feature. +#[cfg(feature = "profile")] +pub fn init() -> bool { + init_fmt() +} + +#[cfg(not(feature = "profile"))] +pub fn init() -> bool { + false +} + +/// Human-readable fmt subscriber on stderr. Reads `RUST_LOG` to pick a +/// filter; defaults to `warp=info`. +#[cfg(feature = "profile")] +pub fn init_fmt() -> bool { + use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warp=info")); + tracing_subscriber::registry() + .with(env_filter) + .with( + fmt::layer() + .with_target(false) + .with_span_events(fmt::format::FmtSpan::CLOSE) + .with_writer(std::io::stderr), + ) + .try_init() + .is_ok() +} + +#[cfg(not(feature = "profile"))] +pub fn init_fmt() -> bool { + false +} + +/// Install the JSON subscriber. One `warp.profile.v1` record per closed +/// span is written to `writer`. Reads `RUST_LOG` like [`init_fmt`]. +#[cfg(feature = "profile")] +pub fn init_json(writer: W) -> bool +where + W: std::io::Write + Send + 'static, +{ + use tracing_subscriber::{prelude::*, EnvFilter}; + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warp=info")); + tracing_subscriber::registry() + .with(env_filter) + .with(layer::JsonLayer::new(writer)) + .try_init() + .is_ok() +} + +#[cfg(not(feature = "profile"))] +pub fn init_json(_writer: W) -> bool +where + W: std::io::Write + Send + 'static, +{ + false +} diff --git a/src/profile/rss.rs b/src/profile/rss.rs new file mode 100644 index 0000000..0d253fa --- /dev/null +++ b/src/profile/rss.rs @@ -0,0 +1,42 @@ +//! Peak resident-set-size accounting via `getrusage(RUSAGE_SELF)`. +//! +//! Linux reports `ru_maxrss` in kilobytes; macOS reports it in bytes. We +//! normalise to bytes. The kernel tracks the peak RSS for the process (not +//! the current thread), so this value is monotonic — useful only via +//! deltas across intervals. +//! +//! Returns `None` if the syscall fails. +//! +//! Without the `profile` feature this module compiles to stubs that always +//! return `None`. + +#[cfg(feature = "profile")] +pub fn peak_rss_bytes() -> Option { + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + // SAFETY: rusage is a POD; the kernel writes the full struct on success. + let mut ru: libc::rusage = unsafe { std::mem::zeroed() }; + let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) }; + if rc != 0 { + return None; + } + let maxrss = ru.ru_maxrss as u64; + #[cfg(target_os = "macos")] + { + Some(maxrss) // bytes + } + #[cfg(target_os = "linux")] + { + Some(maxrss.saturating_mul(1024)) // kilobytes -> bytes + } + } + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + { + None + } +} + +#[cfg(not(feature = "profile"))] +pub fn peak_rss_bytes() -> Option { + None +} diff --git a/src/profile/timing.rs b/src/profile/timing.rs new file mode 100644 index 0000000..e0b5e64 --- /dev/null +++ b/src/profile/timing.rs @@ -0,0 +1,38 @@ +//! CPU-time accounting for the current thread. +//! +//! Wall time is available from `std::time::Instant`. CPU time is not — we +//! need `clock_gettime(CLOCK_THREAD_CPUTIME_ID)` (Linux / macOS Monterey+) +//! or `CLOCK_PROCESS_CPUTIME_ID` (older macOS) or `GetThreadTimes` +//! (Windows; not supported here). +//! +//! Returns `None` if the platform clock isn't available. +//! +//! Without the `profile` feature this module compiles to stubs that always +//! return `None`. + +#[cfg(feature = "profile")] +pub fn thread_cpu_ns() -> Option { + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + let mut ts = libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }; + // SAFETY: timespec is a POD; the kernel writes to it on success. + let rc = unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) }; + if rc == 0 { + Some((ts.tv_sec as u64).saturating_mul(1_000_000_000) + (ts.tv_nsec as u64)) + } else { + None + } + } + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + { + None + } +} + +#[cfg(not(feature = "profile"))] +pub fn thread_cpu_ns() -> Option { + None +} diff --git a/src/protocol/domainsep/mod.rs b/src/protocol/domainsep/mod.rs deleted file mode 100644 index 32d1386..0000000 --- a/src/protocol/domainsep/mod.rs +++ /dev/null @@ -1,172 +0,0 @@ -use ark_crypto_primitives::merkle_tree::Config; -use ark_ff::Field; - -use spongefish::{ - Decoding, Encoding, NargDeserialize, ProverState, VerificationResult, VerifierState, -}; - -pub type AccInstances = ( - Vec<::InnerDigest>, // rt - Vec>, // alpha - Vec, // mu - (Vec>, Vec>), // (tau, x) - Vec, // eta -); - -pub fn absorb_instances>( - prover_state: &mut ProverState, - instances: &[Vec], -) { - for instance in instances { - for f in instance { - prover_state.prover_message(f); - } - } -} - -pub fn absorb_accumulated_instances< - F: Field + Encoding<[u8]>, - MT: Config + From<[u8; 32]>>, ->( - prover_state: &mut ProverState, - acc_instances: &AccInstances, -) { - // digests (rt) - for digest in &acc_instances.0 { - let bytes: [u8; 32] = digest.as_ref().try_into().expect("digest must be 32 bytes"); - prover_state.prover_message(&bytes); - } - - // alpha - for alpha in &acc_instances.1 { - for f in alpha { - prover_state.prover_message(f); - } - } - - // mu - for f in &acc_instances.2 { - prover_state.prover_message(f); - } - - // taus - for tau in &acc_instances.3 .0 { - for f in tau { - prover_state.prover_message(f); - } - } - - // xs - for x in &acc_instances.3 .1 { - for f in x { - prover_state.prover_message(f); - } - } - - // etas - for f in &acc_instances.4 { - prover_state.prover_message(f); - } -} - -pub type ParsedStatement = (Vec>, AccInstances); - -pub fn parse_statement< - F: Field + NargDeserialize + Encoding<[u8]> + Decoding<[u8]>, - MT: Config + From<[u8; 32]>>, ->( - verifier_state: &mut VerifierState<'_>, - l1: usize, - l2: usize, - instance_len: usize, - log_n: usize, - #[allow(non_snake_case)] log_M: usize, -) -> VerificationResult> { - // f. absorb l1 instances - let mut l1_xs = Vec::with_capacity(l1); - for _ in 0..l1 { - let inst: Vec = verifier_state.prover_messages_vec(instance_len)?; - l1_xs.push(inst); - } - - // l2 instances - let mut l2_roots = Vec::with_capacity(l2); - for _ in 0..l2 { - let bytes: [u8; 32] = verifier_state.prover_message()?; - l2_roots.push(bytes.into()); - } - - let mut l2_alphas = Vec::with_capacity(l2); - for _ in 0..l2 { - let alpha: Vec = verifier_state.prover_messages_vec(log_n)?; - l2_alphas.push(alpha); - } - - let l2_mus: Vec = verifier_state.prover_messages_vec(l2)?; - - let mut l2_taus = Vec::with_capacity(l2); - for _ in 0..l2 { - let tau: Vec = verifier_state.prover_messages_vec(log_M)?; - l2_taus.push(tau); - } - - let mut l2_xs = Vec::with_capacity(l2); - for _ in 0..l2 { - let x: Vec = verifier_state.prover_messages_vec(instance_len)?; - l2_xs.push(x); - } - - let l2_etas: Vec = verifier_state.prover_messages_vec(l2)?; - - Ok(( - l1_xs, - (l2_roots, l2_alphas, l2_mus, (l2_taus, l2_xs), l2_etas), - )) -} - -/// Read `rt_0 + l1_mus`, squeeze `l1_taus + ω + τ`. Runs before the -/// twin-constraint sumcheck on the verifier side. -#[allow(clippy::type_complexity)] -pub fn derive_pre_twin_constraint< - F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, - MT: Config + From<[u8; 32]>>, ->( - vs: &mut VerifierState<'_>, - l1: usize, - log_l: usize, - #[allow(non_snake_case)] log_M: usize, -) -> VerificationResult<(MT::InnerDigest, Vec, Vec>, F, Vec)> { - let rt_0: MT::InnerDigest = <[u8; 32]>::into(vs.prover_message()?); - let l1_mus = vs.prover_messages_vec(l1)?; - let l1_taus = (0..l1) - .map(|_| (0..log_M).map(|_| vs.verifier_message::()).collect()) - .collect(); - let omega = vs.verifier_message(); - let tau = (0..log_l).map(|_| vs.verifier_message::()).collect(); - Ok((rt_0, l1_mus, l1_taus, omega, tau)) -} - -/// Read `td + η + ν₀`, squeeze OOD points, read OOD answers, squeeze shift -/// query bytes and `ξ`. Runs between the two sumchecks on the verifier side. -#[allow(clippy::type_complexity)] -pub fn derive_between_sumchecks< - F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, - MT: Config + From<[u8; 32]>>, ->( - vs: &mut VerifierState<'_>, - log_n: usize, - s: usize, - t: usize, - log_r: usize, -) -> VerificationResult<(MT::InnerDigest, F, Vec, Vec, Vec, Vec)> { - let td: MT::InnerDigest = <[u8; 32]>::into(vs.prover_message()?); - let eta = vs.prover_message()?; - let mut nus = vec![vs.prover_message::()?]; - let ood_samples = (0..s * log_n).map(|_| vs.verifier_message::()).collect(); - nus.extend(vs.prover_messages_vec::(s)?); - let bytes_shift_queries = (0..(t * log_n).div_ceil(8)) - .map(|_| vs.verifier_message::<[u8; 1]>()[0]) - .collect(); - let xi = (0..log_r).map(|_| vs.verifier_message::()).collect(); - Ok((td, eta, nus, ood_samples, bytes_shift_queries, xi)) -} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 6a13b4e..55ed4d7 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,23 +1,4 @@ -pub mod domainsep; - -use effsc::transcript::VerifierTranscript; -use spongefish::{Decoding, Encoding, NargDeserialize, VerifierState}; - -/// Adapter plugging spongefish's [`VerifierState`] into effsc's -/// [`VerifierTranscript`] so the verifier can call -/// [`effsc::verifier::sumcheck_verify`]. Prover side is covered by a blanket -/// impl inside effsc (on `spongefish::ProverState`). -pub struct EffscVerifierTranscript<'s, 'a>(pub &'s mut VerifierState<'a>); - -impl VerifierTranscript for EffscVerifierTranscript<'_, '_> -where - F: ark_ff::Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, -{ - type Error = spongefish::VerificationError; - fn receive(&mut self) -> Result { - self.0.prover_message::() - } - fn challenge(&mut self) -> F { - self.0.verifier_message::() - } -} +pub mod oracle; +pub mod phases; +pub mod query; +pub mod transcript; diff --git a/src/protocol/oracle.rs b/src/protocol/oracle.rs new file mode 100644 index 0000000..966582f --- /dev/null +++ b/src/protocol/oracle.rs @@ -0,0 +1,83 @@ +//! Oracle abstraction for Warp's IOR phases. +//! +//! Paired spec: `docs/paper-mods/mod1_oracle.tex`. +//! +//! An [`Oracle`] is a single object that carries both views Warp's phases +//! need of a committed codeword: the raw evaluation table `f: [n] → F` +//! (BCS-native, index-queryable) and the implied multilinear extension +//! `\hat f: F^{log n} → F` (point-queryable). The multilinear extension is +//! materialised lazily on first point query and cached. +//! +//! The Merkle commitment of the codeword is **not** held here. In PESAT a +//! single Merkle tree covers many interleaved codewords +//! (`src/crypto/merkle/mod.rs::build_codeword_leaves`), so the tree is +//! tracked by the enclosing data structure (`PesatOutput`, +//! `AccumulatorWitness`) rather than 1:1 with the oracle. See the +//! Implementation note in `mod1_oracle.tex` §2. + +use ark_ff::Field; +use ark_poly::{DenseMultilinearExtension, MultilinearExtension}; +use ark_std::log2; +use std::cell::OnceCell; + +use crate::count_ops; + +/// A Warp oracle: a committed codeword together with its lazily-materialised +/// multilinear extension. +pub struct Oracle { + evals: Vec, + mle: OnceCell>, +} + +impl Oracle { + /// Wrap an existing evaluation table. + pub fn from_evals(evals: Vec) -> Self { + Self { + evals, + mle: OnceCell::new(), + } + } + + /// Borrow the evaluation table `f`. + pub fn evals(&self) -> &[F] { + &self.evals + } + + /// Consume the oracle and return the underlying evaluation table. + pub fn into_evals(self) -> Vec { + self.evals + } + + /// Length `n` of the evaluation table. + pub fn len(&self) -> usize { + self.evals.len() + } + + pub fn is_empty(&self) -> bool { + self.evals.is_empty() + } + + /// Index query: `f[i]`. + pub fn query_at_leaf(&self, idx: usize) -> F { + count_ops!(OracleLeafQueries); + self.evals[idx] + } + + /// Point query on the multilinear extension: `\hat f(ζ)` for + /// `ζ ∈ F^{log n}`. Materialises the MLE on first call and caches it. + pub fn query_at_point(&self, point: &[F]) -> F { + count_ops!(OraclePointQueries); + let mle = self.mle.get_or_init(|| { + count_ops!(MleMaterializations); + let log_n = log2(self.evals.len()) as usize; + DenseMultilinearExtension::from_evaluations_slice(log_n, &self.evals) + }); + mle.fix_variables(point)[0] + } +} + +impl From> for Oracle { + fn from(evals: Vec) -> Self { + Self::from_evals(evals) + } +} diff --git a/src/protocol/phases/batching.rs b/src/protocol/phases/batching.rs new file mode 100644 index 0000000..bbdcda9 --- /dev/null +++ b/src/protocol/phases/batching.rs @@ -0,0 +1,258 @@ +//! Batching sumcheck phase. +//! +//! Paired spec: `docs/paper-mods/mod1_oracle.tex` (oracle composition). +//! Reduces the batched claim +//! +//! ```text +//! Σ_i ξ(i) · \hat f(ζ_i) = σ₂ +//! ``` +//! +//! to a single evaluation claim `μ = \hat f(α)` via the inner-product +//! sumcheck, with the CBBZ23 / HyperPlonk sparse-evaluation optimization +//! (`accumulate_sparse_evaluations`) folded in. +//! +//! IOR signature +//! ------------- +//! - `Statement` — `(zetas_prefix, s, t, log_n)` — shared. +//! - `Witness` — `()` +//! - `ProverInputs` — `&Oracle` (the committed oracle, full data) +//! - `VerifierInputs` — `(nus, acc_mu)` — used to compute `σ₂` and the +//! final-claim oracle check. +//! - `ReducedStatement` — `alpha` — the new code-eval point (LSB-indexed) +//! - `ProverOutputs` — `mu` — the prover's reported `\hat f(α)` +//! - `VerifierOutputs` — `()` + +use ark_ff::{Field, PrimeField}; +use ark_std::log2; +use effsc::{ + noop_hook, provers::inner_product::InnerProductProver, runner::sumcheck, + verifier::sumcheck_verify, +}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::collections::HashMap; +use std::marker::PhantomData; + +use crate::count_ops; +use crate::error::{ProverError, VerifierError}; +use crate::protocol::oracle::Oracle; +use crate::protocol::phases::IOR; +use crate::protocol::transcript::EffscVerifierTranscript; +use crate::utils::poly::{eq_poly, eq_poly_non_binary}; +use crate::BoolResult; + +/// [CBBZ23] / HyperPlonk sparse-evaluation optimization: for shift-query +/// zetas (indices `1+s..r`), each ζ is a 0/1 vector representing a single +/// hypercube point. +fn accumulate_sparse_evaluations( + zetas: &[Vec], + eq_evals: &[F], + s: usize, + r: usize, +) -> HashMap { + let mut result: HashMap = HashMap::new(); + for i in 1 + s..r { + let index = zetas[i] + .iter() + .enumerate() + .filter_map(|(j, bit)| bit.is_one().then_some(1 << j)) + .sum::(); + *result.entry(index).or_insert_with(F::zero) += eq_evals[i]; + } + result +} + +/// Sum `dense_polys` column-wise and add the sparse contributions into the +/// resulting vector. +fn batched_constraint_poly( + dense_polys: &[Vec], + sparse_polys: &HashMap, +) -> Vec { + if dense_polys.is_empty() { + return Vec::new(); + } + let mut result = vec![F::ZERO; dense_polys[0].len()]; + for row in dense_polys { + for (i, val) in row.iter().enumerate() { + result[i] += *val; + } + } + for (k, v) in sparse_polys.iter() { + result[*k] += *v; + } + result +} + +// ─── IOR signature types ────────────────────────────────────────────────── + +pub struct BatchingStatement { + /// `1 + s + t` evaluation points: `[ζ_0, ood_j…, query_k…]`. + pub zetas_prefix: Vec>, + pub s: usize, + pub t: usize, + pub log_n: usize, + pub _phantom: std::marker::PhantomData, +} + +pub struct BatchingProverInputs<'a, F: Field> { + pub oracle: &'a Oracle, +} + +pub struct BatchingVerifierInputs { + /// `1 + s + t` ν values; used to compute `σ₂ = Σ ξ_eq · ν`. + pub nus: Vec, + /// Multiplier on the final-claim oracle check. + pub acc_mu: F, +} + +pub struct BatchingReducedStatement { + /// New code-eval point (LSB-indexed). + pub alpha: Vec, +} + +pub struct BatchingProverOutputs { + /// `\hat f(α)` — prover's report. + pub mu: F, +} + +/// Batching phase configuration. +pub struct Batching<'a, F: Field> { + pub _phantom: PhantomData<&'a F>, +} + +impl<'a, F: Field> Batching<'a, F> { + pub fn new() -> Self { + Self { + _phantom: PhantomData, + } + } +} + +impl<'a, F: Field> Default for Batching<'a, F> { + fn default() -> Self { + Self::new() + } +} + +impl<'a, F> IOR for Batching<'a, F> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, +{ + type Statement = BatchingStatement; + type Witness = (); + type ProverInputs = BatchingProverInputs<'a, F>; + type VerifierInputs = BatchingVerifierInputs; + type ReducedStatement = BatchingReducedStatement; + type ProverOutputs = BatchingProverOutputs; + type VerifierOutputs = (); + + #[tracing::instrument( + name = "batching", + skip_all, + fields(s = statement.s, t = statement.t, log_n = statement.log_n) + )] + fn prove( + &self, + prover_state: &mut ProverState, + statement: &Self::Statement, + _witness: Self::Witness, + inputs: Self::ProverInputs, + ) -> Result<(Self::ReducedStatement, Self::ProverOutputs), ProverError> { + let n = inputs.oracle.len(); + let r = 1 + statement.s + statement.t; + let log_r = log2(r) as usize; + debug_assert_eq!(statement.zetas_prefix.len(), r); + + let xis = prover_state.verifier_messages_vec::(log_r); + + let (xi_eq_evals, ood_evals_vec) = { + let _s = tracing::info_span!("batching.eq_evals").entered(); + let xi_eq_evals = (0..r).map(|i| eq_poly(&xis, i)).collect::>(); + let ood_evals_vec = (0..1 + statement.s) + .map(|i| { + (0..n) + .map(|a| eq_poly(&statement.zetas_prefix[i], a) * xi_eq_evals[i]) + .collect::>() + }) + .collect::>(); + (xi_eq_evals, ood_evals_vec) + }; + + let id_non_0_eval_sums = { + let _s = tracing::info_span!("batching.accumulate_sparse").entered(); + accumulate_sparse_evaluations(&statement.zetas_prefix, &xi_eq_evals, statement.s, r) + }; + + // Run the inner-product sumcheck. MSB half-split → reverse once. + let alpha = { + let _s = tracing::info_span!("batching.sumcheck").entered(); + let log_n_bits = ark_std::log2(n) as u64; + count_ops!(BatchingRounds, log_n_bits); + let mut ip = InnerProductProver::new( + inputs.oracle.evals().to_vec(), + batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), + ); + let mut challenges = + sumcheck(&mut ip, log_n_bits as usize, prover_state, noop_hook).challenges; + challenges.reverse(); + challenges + }; + + let mu = inputs.oracle.query_at_point(&alpha); + + Ok(( + BatchingReducedStatement { + alpha: alpha.clone(), + }, + BatchingProverOutputs { mu }, + )) + } + + #[tracing::instrument( + name = "batching.verify", + skip_all, + fields(s = statement.s, t = statement.t, log_n = statement.log_n) + )] + fn verify<'b>( + &self, + verifier_state: &mut VerifierState<'b>, + statement: &Self::Statement, + inputs: Self::VerifierInputs, + ) -> Result<(Self::ReducedStatement, Self::VerifierOutputs), VerifierError> { + let r = 1 + statement.s + statement.t; + let log_r = log2(r) as usize; + debug_assert_eq!(statement.zetas_prefix.len(), r); + debug_assert_eq!(inputs.nus.len(), r); + + // Squeeze ξ matching the prover. + let xis: Vec = (0..log_r) + .map(|_| verifier_state.verifier_message::()) + .collect(); + let xi_eq_evals = (0..r).map(|i| eq_poly(&xis, i)).collect::>(); + + // σ₂ = Σ ξ_eq · ν. + let sigma_2 = xi_eq_evals + .iter() + .zip(&inputs.nus) + .fold(F::zero(), |acc, (xi_eq, nu)| acc + *xi_eq * nu); + + // Run sumcheck_verify and check the final-claim oracle check. + let res = { + let mut wrap = EffscVerifierTranscript(verifier_state); + sumcheck_verify(sigma_2, 2, statement.log_n, &mut wrap, |_, _| Ok(()))? + }; + let alpha_lsb: Vec = res.challenges.iter().rev().copied().collect(); + + let mut zeta_eqs = Vec::with_capacity(r); + for zeta in &statement.zetas_prefix { + zeta_eqs.push(eq_poly_non_binary(zeta, &alpha_lsb)); + } + let expected = inputs.acc_mu + * zeta_eqs + .into_iter() + .zip(&xi_eq_evals) + .fold(F::zero(), |acc, (a, b)| acc + a * *b); + (expected == res.final_claim).ok_or_err(VerifierError::Target)?; + + Ok((BatchingReducedStatement { alpha: alpha_lsb }, ())) + } +} diff --git a/src/protocol/phases/mod.rs b/src/protocol/phases/mod.rs new file mode 100644 index 0000000..d1ed7fa --- /dev/null +++ b/src/protocol/phases/mod.rs @@ -0,0 +1,81 @@ +//! Warp IOR phases as first-class modules. +//! +//! Paired spec: `docs/paper-mods/mod1_oracle.tex` (composition rule) and the +//! forthcoming `docs/paper-mods/mod3_accumulator_state.tex`. +//! +//! Each submodule implements one Interactive Oracle Reduction from the Warp +//! construction. The [`IOR`] trait below names the five paper-level +//! components — Statement, Witness, InputOracles, ReducedStatement, +//! OutputOracles — and splits oracle types into prover-side (full data) / +//! verifier-side (commitments) halves so the trait can serve both roles +//! against the same struct. +//! +//! Implementor pattern: a phase is a struct holding setup parameters +//! (codes, merkle hash params); `prove` and `verify` are `&self` methods +//! that take statement / witness / inputs per call and return reduced +//! statement + output oracles. +//! +//! The top-level orchestrators in `src/lib.rs::WARP::prove` and `::verify` +//! thread state between phases by chaining `IOR::prove` / `IOR::verify` +//! calls — each phase's `ReducedStatement` and `Outputs` feed the next +//! phase's `Statement` / `Inputs`. + +pub mod batching; +pub mod ood; +pub mod pesat; +pub mod proximity; +pub mod twin_constraint; + +use spongefish::{ProverState, VerifierState}; + +use crate::error::{ProverError, VerifierError}; + +/// Interactive Oracle Reduction. +/// +/// Mirrors the IOR signature from `docs/paper-mods/mod1_oracle.tex` §4: +/// +/// ```text +/// (stmt, wit, oracles_in) --> (stmt', oracles_out) +/// ``` +/// +/// Seven associated types decompose the paper's tripartite split: +/// +/// - `Statement` / `ReducedStatement` are shared between prover and verifier +/// (what's claimed before and after the reduction). +/// - `Witness` is prover-only. +/// - Oracle types are split per role: the prover sees full evaluation data +/// (`ProverInputs` / `ProverOutputs`); the verifier sees commitments +/// (`VerifierInputs` / `VerifierOutputs`). For phases that don't pass any +/// oracle through one role, use `()`. +pub trait IOR { + /// Pre-reduction claim. Visible to prover and verifier. + type Statement; + /// Prover-only inputs (full witness data, etc.). + type Witness; + /// Oracles flowing in from upstream IORs (prover view: full data). + type ProverInputs; + /// Oracles flowing in from upstream IORs (verifier view: commitments). + type VerifierInputs; + /// Post-reduction claim. Visible to prover and verifier. + type ReducedStatement; + /// Oracles emitted by this IOR (prover view: full data, plus any private + /// reduced witness state). + type ProverOutputs; + /// Oracles emitted by this IOR (verifier view: commitments). + type VerifierOutputs; + + fn prove( + &self, + prover_state: &mut ProverState, + statement: &Self::Statement, + witness: Self::Witness, + inputs: Self::ProverInputs, + ) -> Result<(Self::ReducedStatement, Self::ProverOutputs), ProverError>; + + fn verify<'a>( + &self, + verifier_state: &mut VerifierState<'a>, + statement: &Self::Statement, + inputs: Self::VerifierInputs, + ) -> Result<(Self::ReducedStatement, Self::VerifierOutputs), VerifierError>; +} diff --git a/src/protocol/phases/ood.rs b/src/protocol/phases/ood.rs new file mode 100644 index 0000000..8aee302 --- /dev/null +++ b/src/protocol/phases/ood.rs @@ -0,0 +1,123 @@ +//! Out-of-domain sampling phase. +//! +//! Paired spec: `docs/paper-mods/mod1_oracle.tex`. This phase is a thin +//! composition of point queries on the committed oracle — see +//! [`Oracle::query_at_point`](crate::protocol::oracle::Oracle::query_at_point). +//! The verifier derives the same random points from the transcript. +//! +//! IOR signature +//! ------------- +//! - `Statement` — `(s, log_n)` +//! - `Witness` — `()` +//! - `ProverInputs` — `&Oracle` (the committed oracle, full data) +//! - `VerifierInputs` — `()` (the oracle check is deferred to the batching +//! sumcheck's final claim) +//! - `ReducedStatement` — `(samples_flat, answers)` — query points + their answers +//! - `ProverOutputs` — `()` +//! - `VerifierOutputs` — `()` + +use ark_ff::{Field, PrimeField}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::marker::PhantomData; + +use crate::count_ops; +use crate::error::{ProverError, VerifierError}; +use crate::protocol::oracle::Oracle; +use crate::protocol::phases::IOR; + +pub struct OodStatement { + pub s: usize, + pub log_n: usize, +} + +pub struct OodProverInputs<'a, F: Field> { + pub oracle: &'a Oracle, +} + +pub struct OodReducedStatement { + /// Flat challenge vector of length `s · log_n`. + pub samples_flat: Vec, + /// Answers `\hat f(ζ_j)` for each of the `s` chunked challenges. + pub answers: Vec, +} + +/// OOD phase configuration. Stateless; the lifetime parameter exists only +/// to anchor `ProverInputs<'a>` for the trait impl. +pub struct Ood<'a, F: Field> { + pub _phantom: PhantomData<&'a F>, +} + +impl<'a, F: Field> Ood<'a, F> { + pub fn new() -> Self { + Self { + _phantom: PhantomData, + } + } +} + +impl<'a, F: Field> Default for Ood<'a, F> { + fn default() -> Self { + Self::new() + } +} + +impl<'a, F> IOR for Ood<'a, F> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, +{ + type Statement = OodStatement; + type Witness = (); + type ProverInputs = OodProverInputs<'a, F>; + type VerifierInputs = (); + type ReducedStatement = OodReducedStatement; + type ProverOutputs = (); + type VerifierOutputs = (); + + #[tracing::instrument(name = "ood", skip_all, fields(s = statement.s, log_n = statement.log_n))] + fn prove( + &self, + prover_state: &mut ProverState, + statement: &Self::Statement, + _witness: Self::Witness, + inputs: Self::ProverInputs, + ) -> Result<(Self::ReducedStatement, Self::ProverOutputs), ProverError> { + let samples_flat = prover_state.verifier_messages_vec::(statement.s * statement.log_n); + count_ops!(OodPointQueries, statement.s as u64); + let answers = samples_flat + .chunks(statement.log_n) + .map(|zeta| inputs.oracle.query_at_point(zeta)) + .collect::>(); + prover_state.prover_messages(&answers); + Ok(( + OodReducedStatement { + samples_flat, + answers, + }, + (), + )) + } + + #[tracing::instrument( + name = "ood.verify", + skip_all, + fields(s = statement.s, log_n = statement.log_n) + )] + fn verify<'b>( + &self, + verifier_state: &mut VerifierState<'b>, + statement: &Self::Statement, + _inputs: Self::VerifierInputs, + ) -> Result<(Self::ReducedStatement, Self::VerifierOutputs), VerifierError> { + let samples_flat: Vec = (0..statement.s * statement.log_n) + .map(|_| verifier_state.verifier_message::()) + .collect(); + let answers: Vec = verifier_state.prover_messages_vec(statement.s)?; + Ok(( + OodReducedStatement { + samples_flat, + answers, + }, + (), + )) + } +} diff --git a/src/protocol/phases/pesat.rs b/src/protocol/phases/pesat.rs new file mode 100644 index 0000000..7f7b325 --- /dev/null +++ b/src/protocol/phases/pesat.rs @@ -0,0 +1,173 @@ +//! PESAT Reduction phase. +//! +//! Paired spec: `docs/paper-mods/mod1_oracle.tex` (oracle composition). +//! Implements Phase 2 of the Warp prover: encode fresh witnesses into +//! codewords, commit via an interleaved Merkle tree, absorb commitment and +//! code evaluations, and derive the τ zero-check challenges. +//! +//! IOR signature +//! ------------- +//! - `Statement` — `(l1, log_m)` +//! - `Witness` — `&[Vec]` (fresh witnesses to encode) +//! - `ProverInputs` — `()` (PESAT is the source — no upstream oracles) +//! - `VerifierInputs` — `()` +//! - `ReducedStatement` — `(mus, taus)` — code-eval claims + zero-check randomness +//! - `ProverOutputs` — full codewords + Merkle tree (\(\Oracle{u}\) bundle) +//! - `VerifierOutputs` — Merkle root only + +use ark_codes::traits::LinearCode; +use ark_crypto_primitives::{ + crh::{CRHScheme, TwoToOneCRHScheme}, + merkle_tree::{Config, MerkleTree}, +}; +use ark_ff::{Field, PrimeField}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::marker::PhantomData; + +use crate::count_ops; +use crate::crypto::merkle::build_codeword_leaves; +use crate::error::{ProverError, VerifierError}; +use crate::protocol::phases::IOR; + +pub struct PesatStatement { + pub l1: usize, + pub log_m: usize, +} + +pub struct PesatWitness<'a, F: Field> { + pub witnesses: &'a [Vec], +} + +pub struct PesatReducedStatement { + pub mus: Vec, + pub taus: Vec>, +} + +pub struct PesatProverOutputs { + pub codewords: Vec>, + pub td_0: MerkleTree, +} + +pub struct PesatVerifierOutputs { + pub rt_0: MT::InnerDigest, +} + +/// PESAT phase configuration. Holds the linear code and merkle hash +/// parameters borrowed from the enclosing `WARP` struct. +pub struct Pesat<'a, F, C, MT> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + C: LinearCode, + MT: Config + From<[u8; 32]>>, +{ + pub code: &'a C, + pub mt_leaf_hash_params: &'a ::Parameters, + pub mt_two_to_one_hash_params: &'a ::Parameters, + pub _phantom: PhantomData<(F, MT)>, +} + +impl<'a, F, C, MT> IOR for Pesat<'a, F, C, MT> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + C: LinearCode, + MT: Config + From<[u8; 32]>>, +{ + type Statement = PesatStatement; + type Witness = PesatWitness<'a, F>; + type ProverInputs = (); + type VerifierInputs = (); + type ReducedStatement = PesatReducedStatement; + type ProverOutputs = PesatProverOutputs; + type VerifierOutputs = PesatVerifierOutputs; + + #[tracing::instrument( + name = "pesat", + skip_all, + fields(l1 = statement.l1, log_m = statement.log_m, n_witnesses = witness.witnesses.len()) + )] + fn prove( + &self, + prover_state: &mut ProverState, + statement: &Self::Statement, + witness: Self::Witness, + _inputs: Self::ProverInputs, + ) -> Result<(Self::ReducedStatement, Self::ProverOutputs), ProverError> { + // a. encode witnesses + let (codewords, leaves) = { + let _s = tracing::info_span!("pesat.encode").entered(); + count_ops!(EncodeCalls, witness.witnesses.len() as u64); + build_codeword_leaves(self.code, witness.witnesses, statement.l1) + }; + + // b. evaluation claims + let mus = codewords.iter().map(|f| f[0]).collect::>(); + + // c. commit to witnesses + let td_0 = { + let _s = tracing::info_span!("pesat.merkle_commit").entered(); + count_ops!(MerkleTreeBuilds); + MerkleTree::::new( + self.mt_leaf_hash_params, + self.mt_two_to_one_hash_params, + leaves.chunks_exact(statement.l1).collect::>(), + )? + }; + + // d. absorb commitment + claims; e/f. derive τ challenges. + let taus = { + let _s = tracing::info_span!("pesat.absorb_and_derive").entered(); + let root_bytes: [u8; 32] = td_0 + .root() + .as_ref() + .try_into() + .expect("root must be 32 bytes"); + prover_state.prover_message(&root_bytes); + prover_state.prover_messages(&mus); + + (0..statement.l1) + .map(|_| prover_state.verifier_messages_vec::(statement.log_m)) + .collect::>() + }; + + Ok(( + PesatReducedStatement { + mus: mus.clone(), + taus, + }, + PesatProverOutputs { codewords, td_0 }, + )) + } + + #[tracing::instrument( + name = "pesat.verify", + skip_all, + fields(l1 = statement.l1, log_m = statement.log_m) + )] + fn verify<'b>( + &self, + verifier_state: &mut VerifierState<'b>, + statement: &Self::Statement, + _inputs: Self::VerifierInputs, + ) -> Result<(Self::ReducedStatement, Self::VerifierOutputs), VerifierError> { + // commitment digest + let rt_0_bytes: [u8; 32] = verifier_state.prover_message()?; + let rt_0: MT::InnerDigest = rt_0_bytes.into(); + + // mus (l1 evaluation claims) + let mus: Vec = verifier_state.prover_messages_vec(statement.l1)?; + + // taus (l1 zero-check challenge vectors) + let taus: Vec> = (0..statement.l1) + .map(|_| { + (0..statement.log_m) + .map(|_| verifier_state.verifier_message::()) + .collect() + }) + .collect(); + + Ok(( + PesatReducedStatement { mus, taus }, + PesatVerifierOutputs { rt_0 }, + )) + } +} diff --git a/src/protocol/phases/proximity.rs b/src/protocol/phases/proximity.rs new file mode 100644 index 0000000..f5887ff --- /dev/null +++ b/src/protocol/phases/proximity.rs @@ -0,0 +1,199 @@ +//! Proximity / shift-query phase. +//! +//! Paired spec: `docs/paper-mods/mod1_oracle.tex` — index queries on the +//! committed oracles. Generates the authentication paths for each shift +//! query leaf against both the fresh PESAT commitment and each accumulated +//! commitment, and collects the codeword values at those leaves. +//! +//! The query indices themselves are sampled from the transcript **before** +//! this phase — see the orchestrator in `src/lib.rs` — so the batching +//! sumcheck can consume the same indices. Proximity itself does not interact +//! with the transcript: it produces proof artifacts (auth paths + answers), +//! which are then attached to the proof on the prover side and verified +//! against transcript-derived commitments on the verifier side. +//! +//! IOR signature +//! ------------- +//! - `Statement` — `(queries, l2, t)` +//! - `Witness` — `()` +//! - `ProverInputs` — fresh + accumulated merkle trees + all codewords (full data) +//! - `VerifierInputs` — fresh + accumulated commitments + auth paths + answers +//! - `ReducedStatement` — `()` (Proximity is a check, not a reduction) +//! - `ProverOutputs` — auth paths + shift_query_answers (proof artifacts) +//! - `VerifierOutputs` — `()` + +use ark_crypto_primitives::{ + crh::{CRHScheme, TwoToOneCRHScheme}, + merkle_tree::{Config, MerkleTree, Path}, +}; +use ark_ff::Field; +use spongefish::{ProverState, VerifierState}; +use std::marker::PhantomData; + +use crate::count_ops; +use crate::crypto::merkle::compute_auth_paths; +use crate::error::{ProverError, VerifierError}; +use crate::protocol::phases::IOR; +use crate::protocol::query::QueryIndices; +use crate::BoolResult; + +pub struct ProximityStatement { + pub queries: QueryIndices, + pub l2: usize, + pub t: usize, +} + +pub struct ProximityProverInputs<'a, F: Field, MT: Config> { + pub td_0: &'a MerkleTree, + pub acc_td: &'a [MerkleTree], + pub all_codewords: &'a [Vec], +} + +pub struct ProximityVerifierInputs<'a, F: Field, MT: Config> { + pub rt_0: &'a MT::InnerDigest, + pub l2_roots: &'a [MT::InnerDigest], + pub auth_0: &'a [Path], + pub auth_j: &'a [Vec>], + pub shift_query_answers: &'a [Vec], + pub _phantom: PhantomData, +} + +pub struct ProximityProverOutputs { + pub auth_0: Vec>, + pub auth_j: Vec>>, + pub shift_query_answers: Vec>, +} + +/// Proximity phase configuration. Holds borrowed merkle hash parameters used +/// by the verifier-side `verify` (the prover side doesn't need them — auth +/// paths are generated from the merkle trees passed in via `ProverInputs`). +pub struct Proximity<'a, F: Field, MT: Config> { + pub mt_leaf_hash_params: &'a ::Parameters, + pub mt_two_to_one_hash_params: &'a ::Parameters, + pub _phantom: PhantomData, +} + +impl<'a, F, MT> IOR for Proximity<'a, F, MT> +where + F: Field, + MT: Config + 'a, + MT::InnerDigest: 'a, +{ + type Statement = ProximityStatement; + type Witness = (); + type ProverInputs = ProximityProverInputs<'a, F, MT>; + type VerifierInputs = ProximityVerifierInputs<'a, F, MT>; + type ReducedStatement = (); + type ProverOutputs = ProximityProverOutputs; + type VerifierOutputs = (); + + #[tracing::instrument( + name = "proximity", + skip_all, + fields( + n_queries = statement.queries.leaf_positions.len(), + n_accumulators = inputs.acc_td.len(), + n_codewords = inputs.all_codewords.len(), + ) + )] + fn prove( + &self, + _prover_state: &mut ProverState, + statement: &Self::Statement, + _witness: Self::Witness, + inputs: Self::ProverInputs, + ) -> Result<(Self::ReducedStatement, Self::ProverOutputs), ProverError> { + let leaf_positions = &statement.queries.leaf_positions; + + let auth_0 = { + let _s = tracing::info_span!("proximity.auth_0").entered(); + count_ops!(MerklePathsGenerated, leaf_positions.len() as u64); + compute_auth_paths(inputs.td_0, leaf_positions)? + }; + + let auth_j = { + let _s = tracing::info_span!("proximity.auth_j").entered(); + count_ops!( + MerklePathsGenerated, + (inputs.acc_td.len() * leaf_positions.len()) as u64 + ); + inputs + .acc_td + .iter() + .map(|td| compute_auth_paths(td, leaf_positions)) + .collect::>>, _>>()? + }; + + let shift_query_answers = { + let _s = tracing::info_span!("proximity.shift_queries").entered(); + let mut answers = + vec![vec![F::default(); inputs.all_codewords.len()]; leaf_positions.len()]; + for (i, idx) in leaf_positions.iter().enumerate() { + let row = inputs + .all_codewords + .iter() + .map(|f| f[*idx]) + .collect::>(); + answers[i] = row; + } + answers + }; + + Ok(( + (), + ProximityProverOutputs { + auth_0, + auth_j, + shift_query_answers, + }, + )) + } + + #[tracing::instrument( + name = "proximity.verify", + skip_all, + fields(t = statement.t, l2 = statement.l2) + )] + fn verify<'b>( + &self, + _verifier_state: &mut VerifierState<'b>, + statement: &Self::Statement, + inputs: Self::VerifierInputs, + ) -> Result<(Self::ReducedStatement, Self::VerifierOutputs), VerifierError> { + let leaf_positions = &statement.queries.leaf_positions; + + (inputs.shift_query_answers.len() == statement.t) + .ok_or_err(VerifierError::NumShiftQueries)?; + + for (i, path) in inputs.auth_0.iter().enumerate() { + (path.leaf_index == leaf_positions[i]).ok_or_err(VerifierError::ShiftQueryIndex)?; + count_ops!(MerklePathsVerified); + let is_valid = path.verify( + self.mt_leaf_hash_params, + self.mt_two_to_one_hash_params, + inputs.rt_0, + &inputs.shift_query_answers[i][statement.l2..], + )?; + is_valid.ok_or_err(VerifierError::ShiftQuery)?; + } + + (inputs.auth_j.len() == statement.l2).ok_or_err(VerifierError::NumL2Instances)?; + for (i, paths) in inputs.auth_j.iter().enumerate() { + (paths.len() == statement.t).ok_or_err(VerifierError::NumShiftQueries)?; + let root = &inputs.l2_roots[i]; + for (j, path) in paths.iter().enumerate() { + (path.leaf_index == leaf_positions[j]).ok_or_err(VerifierError::ShiftQueryIndex)?; + count_ops!(MerklePathsVerified); + let is_valid = path.verify( + self.mt_leaf_hash_params, + self.mt_two_to_one_hash_params, + root, + [inputs.shift_query_answers[j][i]], + )?; + is_valid.ok_or_err(VerifierError::ShiftQuery)?; + } + } + + Ok(((), ())) + } +} diff --git a/src/protocol/phases/twin_constraint.rs b/src/protocol/phases/twin_constraint.rs new file mode 100644 index 0000000..c787b24 --- /dev/null +++ b/src/protocol/phases/twin_constraint.rs @@ -0,0 +1,466 @@ +//! Twin-constraint sumcheck phase. +//! +//! Paired spec: `docs/paper-mods/mod1_oracle.tex` (oracle composition). +//! The forthcoming `docs/paper-mods/mod2_structured_sumcheck.tex` will +//! promote this phase's fused-fold prover to a first-class paper primitive. +//! +//! Reduces the claim +//! +//! ```text +//! Σ_i τ(i) · (f(i) + ω · p(i)) = σ₁ +//! ``` +//! +//! to evaluations at a random point γ via protogalaxy folding, where +//! - `f(X) = fold(α, oracle_evals)` — folded codeword check +//! - `p(X) = fold(β, Az·Bz − Cz)` — folded R1CS constraint check +//! - `t(X)` = linear interpolation of τ — equality polynomial +//! +//! Each round's round polynomial has the form `h(X) = (f(X) + ω·p(X))·t(X)`. +//! +//! IOR signature +//! ------------- +//! - `Statement` — accumulator instance + `l1_mus` + dimensions +//! - `Witness` — fresh witnesses + accumulator witness halves + R1CS +//! - `ProverInputs` — full codewords (from PESAT) + accumulated codewords +//! - `VerifierInputs` — `()` — the deferred oracle check (final\_claim ≟ eq(τ,γ)·(ν₀+ω·η)) +//! runs in the orchestrator after ν₀ and η arrive on the transcript +//! - `ReducedStatement` — sumcheck challenges γ + unchecked final_claim + ω, τ + new α (ζ₀) + new β_τ +//! - `ProverOutputs` — the new reduced oracle `f` + the reduced witness vector `z` +//! - `VerifierOutputs` — `()` (the new commitment is read from the transcript by the orchestrator) + +use ark_crypto_primitives::merkle_tree::Config; +use ark_ff::{Field, PrimeField}; +use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial}; +use effsc::{ + coefficient_sumcheck::RoundPolyEvaluator, + folding::protogalaxy, + hypercube::{compute_hypercube_eq_evals, Ascending}, + noop_hook, + provers::coefficient_lsb::CoefficientProverLSB, + runner::sumcheck, + verifier::sumcheck_verify, +}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::marker::PhantomData; + +use crate::count_ops; +use crate::error::{ProverError, VerifierError}; +use crate::protocol::oracle::Oracle; +use crate::protocol::phases::IOR; +use crate::protocol::transcript::EffscVerifierTranscript; +use crate::relations::r1cs::R1CSConstraints; +use crate::types::AccumulatorInstance; +use crate::utils::{ + concat_slices, + poly::{eq_poly, eq_poly_non_binary}, + scale_and_sum, +}; +use crate::BoolResult; + +/// Degree-1 polynomial interpolating two field elements: `lo + (hi - lo)·X`. +fn linear_poly(lo: F, hi: F) -> DensePolynomial { + DensePolynomial::from_coefficients_vec(vec![lo, hi - lo]) +} + +/// A single R1CS constraint row: sparse representations of A, B, and C. +type R1CSConstraint = (Vec<(F, usize)>, Vec<(F, usize)>, Vec<(F, usize)>); + +/// Evaluate one R1CS constraint `Az·Bz - Cz` as a degree-2 polynomial +/// from two witness vectors `z0`, `z1`. +fn eval_r1cs_constraint_poly( + (a, b, c): &R1CSConstraint, + z0: &[F], + z1: &[F], +) -> DensePolynomial { + // effsc's `final_value` calls `accumulate_pair` once with the odd half + // empty (singleton case after all rounds folded). Treat an empty `z` as + // the all-zero vector so the eval returns `F::ZERO` rather than panicking. + let eval = |lc: &[(F, usize)], z: &[F]| { + if z.is_empty() { + F::ZERO + } else { + lc.iter().map(|(t, i)| z[*i] * t).sum::() + } + }; + let (a0, b0, c0) = (eval(a, z0), eval(b, z0), eval(c, z0)); + let (a1, b1, c1) = (eval(a, z1) - a0, eval(b, z1) - b0, eval(c, z1) - c0); + DensePolynomial::from_coefficients_vec(vec![a0 * b0 - c0, a0 * b1 + a1 * b0 - c1, a1 * b1]) +} + +/// Round-polynomial evaluator fusing α-fold, β-fold, and τ-linear into a +/// single sumcheck pass. +struct TwinConstraintEvaluator<'a, F: Field> { + r1cs: &'a R1CSConstraints, + omega: F, + degree: usize, +} + +impl<'a, F: Field> RoundPolyEvaluator for TwinConstraintEvaluator<'a, F> { + fn degree(&self) -> usize { + self.degree + } + + fn accumulate_pair(&self, coeffs: &mut [F], tw: &[(&[F], &[F])], pw: &[(F, F)]) { + // tw[0] = (u_even, u_odd), tw[1] = (z_even, z_odd), + // tw[2] = (a_even, a_odd), tw[3] = (b_even, b_odd) + // pw[0] = (tau_even, tau_odd) + let (u_even, u_odd) = tw[0]; + let (z_even, z_odd) = tw[1]; + let (a_even, a_odd) = tw[2]; + let (b_even, b_odd) = tw[3]; + let (tau_even, tau_odd) = pw[0]; + + // Singleton case: effsc's `coefficient_lsb::final_value` calls + // `accumulate_pair` once after all rounds with `tw[i] = (singleton, &[])` + // and `pw[0] = (singleton, F::ZERO)`. Evaluate the polynomial directly + // at the singleton point; emit `[h, -h]` so `g(0) + g(1) == h`, matching + // the convention used by the simple pairwise-only evaluators. + if u_odd.is_empty() { + let f_val = u_even + .iter() + .enumerate() + .map(|(i, &u_i)| u_i * eq_poly(a_even, i)) + .sum::(); + let p_val = self + .r1cs + .iter() + .enumerate() + .map(|(i, (a, b, c))| { + let eq = eq_poly(b_even, i); + let eval = + |lc: &[(F, usize)]| lc.iter().map(|(t, idx)| z_even[*idx] * t).sum::(); + eq * (eval(a) * eval(b) - eval(c)) + }) + .sum::(); + let h_val = (f_val + self.omega * p_val) * tau_even; + coeffs[0] += h_val; + if coeffs.len() > 1 { + coeffs[1] -= h_val; + } + return; + } + + // f(X) = fold(α, oracle_evals): protogalaxy fold over α pairs and linear polys from u + let f = protogalaxy::fold( + a_even.iter().zip(a_odd).map(|(&l, &r)| (l, r - l)), + u_even + .iter() + .zip(u_odd) + .map(|(&l, &r)| linear_poly(l, r)) + .collect(), + ); + + // p(X) = fold(β, Az·Bz - Cz): protogalaxy fold over β pairs and R1CS constraint polys + let p = protogalaxy::fold( + b_even.iter().zip(b_odd).map(|(&l, &r)| (l, r - l)), + self.r1cs + .iter() + .map(|c| eval_r1cs_constraint_poly(c, z_even, z_odd)) + .collect(), + ); + + // t(X) = tau_even + (tau_odd - tau_even) · X + let t = linear_poly(tau_even, tau_odd); + + // h(X) = (f(X) + ω·p(X)) · t(X) + let h = (f + p * self.omega).naive_mul(&t); + + for (c, &hc) in coeffs.iter_mut().zip(h.coeffs.iter()) { + *c += hc; + } + } +} + +// ─── IOR signature types ────────────────────────────────────────────────── + +pub struct TwinConstraintStatement { + pub acc_instance: AccumulatorInstance, + pub l1_mus: Vec, + pub l1_taus: Vec>, + pub log_l: usize, + pub log_m: usize, + pub log_n: usize, +} + +pub struct TwinConstraintWitness<'a, F: Field> { + pub acc_witness_w: &'a [Vec], + pub instances: &'a [Vec], + pub witnesses: &'a [Vec], +} + +pub struct TwinConstraintProverInputs<'a, F: Field> { + /// Fresh codewords emitted by PESAT. + pub fresh_codewords: &'a [Vec], + /// Accumulated codewords (from the accumulator witness). + pub acc_codewords: &'a [Vec], +} + +/// A typed "you owe me a check" handle. +/// +/// The TwinConstraint sumcheck reduces σ₁ to a sumcheck final value, but the +/// actual oracle check `final_claim ≟ eq(τ, γ) · (ν₀ + ω·η)` cannot be +/// completed inside `TwinConstraint::verify` because ν₀ and η arrive on the +/// transcript *after* the sumcheck rounds. Rather than splitting +/// TwinConstraint into two IORs (which cascades into other phases having +/// similar shapes), we expose the obligation as a typed value. +/// +/// Discharge by calling [`Self::discharge`] with the missing inputs once the +/// orchestrator has read them from the transcript. +pub struct DeferredOracleCheck { + /// Zero-check randomness ω squeezed at TwinConstraint entry. + pub omega: F, + /// Zero-check challenge τ squeezed at TwinConstraint entry. + pub tau: Vec, + /// Sumcheck final value — what the orchestrator must verify against + /// `eq(τ, γ) · (ν₀ + ω·η)`. + pub claim: F, +} + +impl DeferredOracleCheck { + /// Discharge the deferred check. + /// + /// Returns `Ok(())` iff `eq(τ, γ) · (ν₀ + ω·η) == claim`. `γ` arrives via + /// the parent `TwinConstraintReducedStatement`; `ν₀, η` come from the + /// transcript segment immediately following the TwinConstraint sumcheck. + pub fn discharge(&self, gamma: &[F], nu_0: F, eta: F) -> Result<(), VerifierError> { + let expected = eq_poly_non_binary(&self.tau, gamma) * (nu_0 + self.omega * eta); + (expected == self.claim).ok_or_err(VerifierError::Target) + } +} + +pub struct TwinConstraintReducedStatement { + /// Sumcheck challenge vector (LSB-indexed). + pub gamma: Vec, + /// New code-evaluation point (becomes the new accumulator's α). + pub zeta_0: Vec, + /// Reduced τ point (becomes the τ component of the new accumulator's β). + pub beta_tau: Vec, + /// Typed obligation: the sumcheck's final value awaits verification + /// against `(ν₀, η)` arriving on the transcript next. Call + /// [`DeferredOracleCheck::discharge`] from the orchestrator. + pub deferred: DeferredOracleCheck, +} + +pub struct TwinConstraintProverOutputs { + /// New reduced codeword oracle. + pub f: Oracle, + /// Reduced witness vector `z = (x, w)` — consumed by η evaluation. + pub z: Vec, +} + +/// TwinConstraint phase configuration. +pub struct TwinConstraint<'a, F, MT> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + MT: Config + From<[u8; 32]>>, +{ + pub r1cs: &'a R1CSConstraints, + pub _phantom: PhantomData, +} + +impl<'a, F, MT> IOR for TwinConstraint<'a, F, MT> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + MT: Config + From<[u8; 32]>>, +{ + type Statement = TwinConstraintStatement; + type Witness = TwinConstraintWitness<'a, F>; + type ProverInputs = TwinConstraintProverInputs<'a, F>; + type VerifierInputs = (); + type ReducedStatement = TwinConstraintReducedStatement; + type ProverOutputs = TwinConstraintProverOutputs; + type VerifierOutputs = (); + + #[tracing::instrument( + name = "twin_constraint", + skip_all, + fields(log_l = statement.log_l, log_m = statement.log_m, log_n = statement.log_n) + )] + fn prove( + &self, + prover_state: &mut ProverState, + statement: &Self::Statement, + witness: Self::Witness, + inputs: Self::ProverInputs, + ) -> Result<(Self::ReducedStatement, Self::ProverOutputs), ProverError> { + let l1 = inputs.fresh_codewords.len(); + let log_l = statement.log_l; + let log_m = statement.log_m; + let log_n = statement.log_n; + + // a. zero-check randomness + let omega: F = prover_state.verifier_message(); + let tau = prover_state.verifier_messages_vec::(log_l); + + // b. assemble sumcheck tables + let tau_eq_evals = Ascending::new(log_l) + .map(|p| eq_poly(&tau, p.index)) + .collect::>(); + + let alpha_vecs = concat_slices( + &statement.acc_instance.alpha, + &vec![vec![F::zero(); log_n]; l1], + ); + + let z_vecs: Vec> = statement + .acc_instance + .beta + .1 + .iter() + .zip(witness.acc_witness_w) + .chain(witness.instances.iter().zip(witness.witnesses)) + .map(|(x, w)| concat_slices(x, w)) + .collect(); + + // β tables: accumulated β-τs first, then PESAT τs. + let beta_vecs: Vec> = statement + .acc_instance + .beta + .0 + .iter() + .cloned() + .chain(statement.l1_taus.iter().cloned()) + .collect(); + + let tablewise = vec![ + concat_slices(inputs.acc_codewords, inputs.fresh_codewords), // u + z_vecs, // z + alpha_vecs, // a + beta_vecs, // b + ]; + let pw = vec![tau_eq_evals]; // tau + + let degree = 1 + (log_n + 1).max(log_m + 2); + let evaluator = TwinConstraintEvaluator { + r1cs: self.r1cs, + omega, + degree, + }; + + // c. run the sumcheck. + let mut cc = CoefficientProverLSB::new(&evaluator, tablewise, pw); + let proof = { + let _s = tracing::info_span!("twin_constraint.sumcheck").entered(); + count_ops!(TwinConstraintRounds, log_l as u64); + sumcheck(&mut cc, log_l, prover_state, noop_hook) + }; + debug_assert_eq!(proof.challenges.len(), log_l); + + // d. pull the single remaining row out of each tablewise table. + let reduced = cc.tablewise(); + debug_assert!(reduced.iter().all(|t| t.len() == 1)); + let f = reduced[0][0].clone(); + let z = reduced[1][0].clone(); + let zeta_0 = reduced[2][0].clone(); + let beta_tau = reduced[3][0].clone(); + + let final_claim = proof.final_value; + + Ok(( + TwinConstraintReducedStatement { + gamma: proof.challenges, + zeta_0, + beta_tau, + deferred: DeferredOracleCheck { + omega, + tau, + claim: final_claim, + }, + }, + TwinConstraintProverOutputs { + f: Oracle::from_evals(f), + z, + }, + )) + } + + #[tracing::instrument( + name = "twin_constraint.verify", + skip_all, + fields(log_l = statement.log_l, log_m = statement.log_m, log_n = statement.log_n) + )] + fn verify<'b>( + &self, + verifier_state: &mut VerifierState<'b>, + statement: &Self::Statement, + _inputs: Self::VerifierInputs, + ) -> Result<(Self::ReducedStatement, Self::VerifierOutputs), VerifierError> { + let log_l = statement.log_l; + let log_n = statement.log_n; + let l1 = statement.l1_mus.len(); + let l2 = statement.acc_instance.mu.len(); + + // Squeeze ω, τ matching the prover. + let omega: F = verifier_state.verifier_message(); + let tau: Vec = (0..log_l) + .map(|_| verifier_state.verifier_message::()) + .collect(); + + // Compute σ₁ = Σ_i τ_eq(i) · (μ_i + ω · η_i). + let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); + let etas_l2_first = concat_slices(&statement.acc_instance.eta, &vec![F::zero(); l1]); + let sigma_1 = tau_eq_evals + .into_iter() + .zip( + statement + .acc_instance + .mu + .iter() + .copied() + .chain(statement.l1_mus.iter().copied()) + .zip(etas_l2_first), + ) + .fold(F::zero(), |acc, (eq_tau, (mu, eta))| { + acc + eq_tau * (mu + omega * eta) + }); + + // Run the sumcheck. The deferred oracle check + // final_claim == eq(τ, γ) · (ν₀ + ω · η) + // is left to the orchestrator because ν₀ and η arrive on the + // transcript AFTER the sumcheck rounds (between TwinConstraint and + // OOD). We surface the unchecked final_claim and ω, τ, γ via + // ReducedStatement so the orchestrator can finish the check. + let tc_degree = 1 + (log_n + 1).max(statement.log_m + 2); + let (gamma, final_claim) = { + let mut wrap = EffscVerifierTranscript(verifier_state); + let res = sumcheck_verify(sigma_1, tc_degree, log_l, &mut wrap, |_, _| Ok(()))?; + (res.challenges, res.final_claim) + }; + + // Compute ζ₀ and β_τ from the sumcheck challenges (mirror the prover's + // sumcheck-fold of α and β). + let gamma_eq_evals = compute_hypercube_eq_evals(log_l, &gamma); + let alpha_vecs = concat_slices( + &statement.acc_instance.alpha, + &vec![vec![F::zero(); log_n]; l1], + ); + let zeta_0 = scale_and_sum(&alpha_vecs, &gamma_eq_evals); + + // β τ-vectors: accumulated taus first (length l2), then PESAT taus + // (length l1). We need the τ component of the new β = sum_{i} γ_eq(i) · β_i. + let beta_taus: Vec> = statement + .acc_instance + .beta + .0 + .iter() + .cloned() + .chain(statement.l1_taus.iter().cloned()) + .collect(); + debug_assert_eq!(beta_taus.len(), l2 + l1); + let beta_tau = scale_and_sum(&beta_taus, &gamma_eq_evals); + + Ok(( + TwinConstraintReducedStatement { + gamma, + zeta_0, + beta_tau, + deferred: DeferredOracleCheck { + omega, + tau, + claim: final_claim, + }, + }, + (), + )) + } +} diff --git a/src/protocol/query.rs b/src/protocol/query.rs new file mode 100644 index 0000000..bbe207f --- /dev/null +++ b/src/protocol/query.rs @@ -0,0 +1,184 @@ +use ark_ff::Field; +use spongefish::ProverState; + +#[derive(Clone)] +pub struct QueryIndices { + pub leaf_positions: Vec, // for merkle tree lookups + pub evaluation_points: Vec>, // for eq polynomial evals +} + +impl QueryIndices { + // take the prover state and sample for queries + pub fn sample( + prover_state: &mut ProverState, + log_codeword_len: usize, + num_queries: usize, + ) -> Self { + let num_bytes = (num_queries * log_codeword_len).div_ceil(8); + let squeezed_bytes: Vec = prover_state + .verifier_messages_vec::<[u8; 1]>(num_bytes) + .into_iter() + .map(|[b]| b) + .collect(); + Self::from_squeezed_bytes(&squeezed_bytes, log_codeword_len, num_queries) + } + + // format the queries from squeezed bytes + pub fn from_squeezed_bytes(squeezed_bytes: &[u8], log_n: usize, count: usize) -> Self { + let evaluation_points = + Self::evaluation_points_from_squeezed_bytes(squeezed_bytes, log_n, count); + // Compute all leaf positions in one batch + let leaf_positions = Self::leaf_positions_from_evaluation_points(&evaluation_points); + Self { + leaf_positions, + evaluation_points, + } + } + + // Get Vec of len=num_queries, where each elements is vec of F in {0,1} len=log_codeword_len + fn evaluation_points_from_squeezed_bytes( + squeezed_bytes: &[u8], + log_codeword_len: usize, + num_queries: usize, + ) -> Vec> { + squeezed_bytes + .iter() + .flat_map(|squeezed_byte| (0..8).map(move |i| F::from((squeezed_byte >> i) & 1 == 1))) + .take(num_queries * log_codeword_len) + .collect::>() + .chunks(log_codeword_len) + .map(|chunk| chunk.to_vec()) + .collect() + } + + // Convert each evaluation point (vector of F in {0,1}) to its little-endian leaf index. + fn leaf_positions_from_evaluation_points(evaluation_points: &[Vec]) -> Vec { + let binary_to_leaf_index = |bits: &Vec| -> usize { + bits.iter() + .rev() + .fold(0, |acc, &b| (acc << 1) | b.is_one() as usize) + }; + evaluation_points.iter().map(binary_to_leaf_index).collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::fields::Goldilocks; + use ark_bls12_381::Fr as BLS12_381; + use ark_std::{One, Zero}; + + // check dimensions, binary values, and leaf position range + fn check_query_indices(bytes: &[u8], log_n: usize, num_queries: usize) { + let q = QueryIndices::::from_squeezed_bytes(bytes, log_n, num_queries); + + assert_eq!(q.leaf_positions.len(), num_queries); + assert_eq!(q.evaluation_points.len(), num_queries); + + for (i, eval_pt) in q.evaluation_points.iter().enumerate() { + assert_eq!(eval_pt.len(), log_n); + for &bit in eval_pt { + assert!(bit.is_zero() || bit.is_one()); + } + assert!(q.leaf_positions[i] < (1 << log_n)); + } + } + + // check leaf_positions match manual binary-to-index conversion + fn check_roundtrip(bytes: &[u8], log_n: usize, num_queries: usize) { + let q = QueryIndices::::from_squeezed_bytes(bytes, log_n, num_queries); + for (i, eval_pt) in q.evaluation_points.iter().enumerate() { + let expected = eval_pt + .iter() + .rev() + .fold(0usize, |acc, &b| (acc << 1) | b.is_one() as usize); + assert_eq!(q.leaf_positions[i], expected); + } + } + + // BLS12-381 (multi-limb, 256-bit) + + #[test] + fn bls12_381_basic() { + let bytes = vec![0b10110010, 0b01101001, 0b11110000, 0b00001111]; + check_query_indices::(&bytes, 4, 3); + } + + #[test] + fn bls12_381_roundtrip() { + let bytes: Vec = (0..16).collect(); + check_roundtrip::(&bytes, 8, 10); + } + + #[test] + fn bls12_381_single_bit_queries() { + // log_n = 1 → each query is a single bit + let bytes = vec![0b10101010]; + let q = QueryIndices::::from_squeezed_bytes(&bytes, 1, 8); + assert_eq!(q.leaf_positions.len(), 8); + for &pos in &q.leaf_positions { + assert!(pos <= 1); + } + } + + // Goldilocks (SmallFp, single-limb u128) + + #[test] + fn goldilocks_basic() { + let bytes = vec![0xFF, 0x00, 0xAB, 0xCD]; + check_query_indices::(&bytes, 4, 3); + } + + #[test] + fn goldilocks_roundtrip() { + let bytes: Vec = (0..16).collect(); + check_roundtrip::(&bytes, 8, 10); + } + + #[test] + fn goldilocks_large_log_n() { + // 16-bit queries → range [0, 65536) + let bytes: Vec = (0..=255).cycle().take(64).collect(); + check_query_indices::(&bytes, 16, 4); + } + + // edge cases + + #[test] + fn zero_bytes_produce_zero_indices() { + let bytes = vec![0u8; 8]; + let q = QueryIndices::::from_squeezed_bytes(&bytes, 4, 4); + for &pos in &q.leaf_positions { + assert_eq!(pos, 0); + } + for eval_pt in &q.evaluation_points { + for &bit in eval_pt { + assert!(bit.is_zero()); + } + } + } + + #[test] + fn all_ones_bytes() { + let bytes = vec![0xFF; 8]; + let q = QueryIndices::::from_squeezed_bytes(&bytes, 4, 4); + for &pos in &q.leaf_positions { + assert_eq!(pos, (1 << 4) - 1); + } + for eval_pt in &q.evaluation_points { + for &bit in eval_pt { + assert!(bit.is_one()); + } + } + } + + #[test] + fn deterministic_output() { + let bytes = vec![0x42, 0x13, 0x7F, 0xE0]; + let q1 = QueryIndices::::from_squeezed_bytes(&bytes, 4, 4); + let q2 = QueryIndices::::from_squeezed_bytes(&bytes, 4, 4); + assert_eq!(q1.leaf_positions, q2.leaf_positions); + assert_eq!(q1.evaluation_points, q2.evaluation_points); + } +} diff --git a/src/protocol/transcript/mod.rs b/src/protocol/transcript/mod.rs new file mode 100644 index 0000000..fffeacb --- /dev/null +++ b/src/protocol/transcript/mod.rs @@ -0,0 +1,31 @@ +pub mod prover; +pub mod verifier; + +pub use prover::*; +pub use verifier::*; + +use effsc::transcript::VerifierTranscript; +use spongefish::{Decoding, Encoding, NargDeserialize, VerifierState}; + +/// Adapter wrapping spongefish's [`VerifierState`] so it implements effsc's +/// [`VerifierTranscript`] trait. Used to plug warp's transcript into +/// [`effsc::verifier::sumcheck_verify`]. +/// +/// The prover-side adapter is a blanket impl inside effsc +/// (`ProverTranscript` on `spongefish::ProverState`); no wrapper needed there. +pub struct EffscVerifierTranscript<'s, 'a>(pub &'s mut VerifierState<'a>); + +impl<'s, 'a, F> VerifierTranscript for EffscVerifierTranscript<'s, 'a> +where + F: ark_ff::Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, +{ + type Error = spongefish::VerificationError; + + fn receive(&mut self) -> Result { + self.0.prover_message::() + } + + fn challenge(&mut self) -> F { + self.0.verifier_message::() + } +} diff --git a/src/protocol/transcript/prover.rs b/src/protocol/transcript/prover.rs new file mode 100644 index 0000000..c755b0b --- /dev/null +++ b/src/protocol/transcript/prover.rs @@ -0,0 +1,57 @@ +use ark_ff::Field; +use spongefish::{Encoding, ProverState}; + +use crate::types::AccumulatorInstance; +use ark_crypto_primitives::merkle_tree::Config; + +// absorb a list of plain instances into the transcript +pub fn absorb_instances>( + prover_state: &mut ProverState, + instances: &[Vec], +) { + for instance in instances { + for f in instance { + prover_state.prover_message(f); + } + } +} + +// absorb an AccumulatorInstance into the transcript +impl< + F: Field + Encoding<[u8]>, + MT: Config + From<[u8; 32]>>, + > AccumulatorInstance +{ + pub fn absorb_into(&self, prover_state: &mut ProverState) { + for digest in &self.rt { + let bytes: [u8; 32] = digest.as_ref().try_into().expect("digest must be 32 bytes"); + prover_state.prover_message(&bytes); + } + + for alpha in &self.alpha { + for f in alpha { + prover_state.prover_message(f); + } + } + + for f in &self.mu { + prover_state.prover_message(f); + } + + for tau in &self.beta.0 { + for f in tau { + prover_state.prover_message(f); + } + } + + for x in &self.beta.1 { + for f in x { + prover_state.prover_message(f); + } + } + + for f in &self.eta { + prover_state.prover_message(f); + } + } +} diff --git a/src/protocol/transcript/verifier.rs b/src/protocol/transcript/verifier.rs new file mode 100644 index 0000000..6e06c1e --- /dev/null +++ b/src/protocol/transcript/verifier.rs @@ -0,0 +1,187 @@ +use ark_crypto_primitives::merkle_tree::Config; +use ark_ff::Field; +use ark_std::log2; + +use spongefish::{Decoding, Encoding, NargDeserialize, VerificationResult, VerifierState}; + +use crate::types::AccumulatorInstance; + +// (l1 instances, accumulated instance) +pub type ParsedStatement = (Vec>, AccumulatorInstance); + +// parse l1 plain instances + an AccumulatorInstance from the transcript +pub fn parse_statement< + F: Field + NargDeserialize + Encoding<[u8]> + Decoding<[u8]>, + MT: Config + From<[u8; 32]>>, +>( + verifier_state: &mut VerifierState<'_>, + l1: usize, + l2: usize, + instance_len: usize, + log_n: usize, + log_m: usize, +) -> VerificationResult> { + let l1_xs: Vec> = (0..l1) + .map(|_| verifier_state.prover_messages_vec(instance_len)) + .collect::>()?; + + let acc = + AccumulatorInstance::::parse_from(verifier_state, l2, log_n, log_m, instance_len)?; + + Ok((l1_xs, acc)) +} + +// parse an AccumulatorInstance from the verifier transcript +impl< + F: Field + NargDeserialize + Encoding<[u8]> + Decoding<[u8]>, + MT: Config + From<[u8; 32]>>, + > AccumulatorInstance +{ + pub fn parse_from( + verifier_state: &mut VerifierState<'_>, + l2: usize, + log_n: usize, + log_m: usize, + instance_len: usize, + ) -> VerificationResult { + let rt: Vec = (0..l2) + .map(|_| -> VerificationResult<_> { + let bytes: [u8; 32] = verifier_state.prover_message()?; + Ok(bytes.into()) + }) + .collect::>()?; + + let alpha: Vec> = (0..l2) + .map(|_| verifier_state.prover_messages_vec(log_n)) + .collect::>()?; + + let mu: Vec = verifier_state.prover_messages_vec(l2)?; + + let taus: Vec> = (0..l2) + .map(|_| verifier_state.prover_messages_vec(log_m)) + .collect::>()?; + + let xs: Vec> = (0..l2) + .map(|_| verifier_state.prover_messages_vec(instance_len)) + .collect::>()?; + + let eta: Vec = verifier_state.prover_messages_vec(l2)?; + + Ok(Self { + rt, + alpha, + mu, + beta: (taus, xs), + eta, + }) + } +} + +/// Transcript values read BEFORE the twin-constraint sumcheck: PESAT +/// commitment + l1 mus + l1 τs + ω + τ. +pub struct PreTwinConstraint { + pub rt_0: MT::InnerDigest, + pub l1_mus: Vec, + pub l1_taus: Vec>, + pub omega: F, + pub tau: Vec, +} + +/// Transcript values read BETWEEN the two sumchecks: new commitment, +/// η, ν₀, OOD samples+answers, shift-query byte challenges, ξ. +pub struct BetweenSumchecks { + pub td: MT::InnerDigest, + pub eta: F, + pub nus: Vec, + pub ood_samples: Vec, + pub bytes_shift_queries: Vec, + pub xi: Vec, +} + +pub fn derive_pre_twin_constraint< + F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, + MT: Config + From<[u8; 32]>>, +>( + verifier_state: &mut VerifierState<'_>, + l1: usize, + log_l: usize, + log_m: usize, +) -> VerificationResult> { + // commitment digest + let rt_0_bytes: [u8; 32] = verifier_state.prover_message()?; + let rt_0: MT::InnerDigest = rt_0_bytes.into(); + + // mus + let l1_mus: Vec = verifier_state.prover_messages_vec(l1)?; + + // challenge taus (squeezed) + let l1_taus: Vec> = (0..l1) + .map(|_| { + (0..log_m) + .map(|_| verifier_state.verifier_message::()) + .collect() + }) + .collect(); + + let omega: F = verifier_state.verifier_message(); + let tau: Vec = (0..log_l) + .map(|_| verifier_state.verifier_message::()) + .collect(); + + Ok(PreTwinConstraint { + rt_0, + l1_mus, + l1_taus, + omega, + tau, + }) +} + +pub fn derive_between_sumchecks< + F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, + MT: Config + From<[u8; 32]>>, +>( + verifier_state: &mut VerifierState<'_>, + log_n: usize, + s: usize, + t: usize, +) -> VerificationResult> { + // td digest + let td_bytes: [u8; 32] = verifier_state.prover_message()?; + let td: MT::InnerDigest = td_bytes.into(); + + // eta and nu_0 + let eta: F = verifier_state.prover_message()?; + let nu_0: F = verifier_state.prover_message()?; + let mut nus = vec![nu_0]; + + // ood samples + let n_ood_samples = s * log_n; + let ood_samples: Vec = (0..n_ood_samples) + .map(|_| verifier_state.verifier_message::()) + .collect(); + + // ood answers + let ood_answers: Vec = verifier_state.prover_messages_vec(s)?; + nus.extend(ood_answers); + + // shift queries and ξ + let r = 1 + s + t; + let log_r = log2(r) as usize; + let n_shift_queries = (t * log_n).div_ceil(8); + let bytes_shift_queries: Vec = (0..n_shift_queries) + .map(|_| verifier_state.verifier_message::<[u8; 1]>()[0]) + .collect(); + let xi: Vec = (0..log_r) + .map(|_| verifier_state.verifier_message::()) + .collect(); + + Ok(BetweenSumchecks { + td, + eta, + nus, + ood_samples, + bytes_shift_queries, + xi, + }) +} diff --git a/src/relations/description.rs b/src/relations/description.rs index ded163e..16a749d 100644 --- a/src/relations/description.rs +++ b/src/relations/description.rs @@ -38,24 +38,23 @@ impl SerializableConstraintMatrices { .unwrap(); constraint_system.finalize(); - let num_instance_variables = constraint_system.num_instance_variables(); - let num_witness_variables = constraint_system.num_witness_variables(); - let num_constraints = constraint_system.num_constraints(); + let cs = constraint_system.into_inner().unwrap(); + let all_matrices = cs.to_matrices().unwrap(); + let r1cs_matrices = all_matrices + .get(R1CS_PREDICATE_LABEL) + .expect("R1CS predicate must exist"); - let mut matrices = constraint_system.to_matrices().unwrap(); - let mut r1cs = matrices.remove(R1CS_PREDICATE_LABEL).unwrap(); - let mut r1cs_iter = r1cs.drain(..); - let a = r1cs_iter.next().unwrap(); - let b = r1cs_iter.next().unwrap(); - let c = r1cs_iter.next().unwrap(); + let num_constraints = cs + .get_predicate_num_constraints(R1CS_PREDICATE_LABEL) + .unwrap_or(0); let serializable = SerializableConstraintMatrices { - num_instance_variables, - num_witness_variables, + num_instance_variables: cs.num_instance_variables(), + num_witness_variables: cs.num_witness_variables(), num_constraints, - a: SerializableConstraintMatrices::serialize_nested_field(a), - b: SerializableConstraintMatrices::serialize_nested_field(b), - c: SerializableConstraintMatrices::serialize_nested_field(c), + a: Self::serialize_nested_field(r1cs_matrices[0].clone()), + b: Self::serialize_nested_field(r1cs_matrices[1].clone()), + c: Self::serialize_nested_field(r1cs_matrices[2].clone()), }; let serialized = serde_json::to_string(&serializable).unwrap(); serialized.into_bytes() diff --git a/src/relations/r1cs/hashchain/relation.rs b/src/relations/r1cs/hashchain/relation.rs index 2bd4999..e681158 100644 --- a/src/relations/r1cs/hashchain/relation.rs +++ b/src/relations/r1cs/hashchain/relation.rs @@ -94,11 +94,17 @@ where .unwrap(); constraint_system.finalize(); - let cs = constraint_system.into_inner().unwrap(); - let x = cs.instance_assignment().unwrap().to_vec(); - let w = cs.witness_assignment().unwrap().to_vec(); + // Extract assignments via the ref (borrow the inner CS) + let x = constraint_system + .borrow() + .map(|cs| cs.instance_assignment().unwrap().to_vec()) + .unwrap(); + let w = constraint_system + .borrow() + .map(|cs| cs.witness_assignment().unwrap().to_vec()) + .unwrap(); Self { - constraint_system: ConstraintSystemRef::new(cs), + constraint_system, config: hash_config, instance, witness, diff --git a/src/relations/r1cs/mod.rs b/src/relations/r1cs/mod.rs index b6bd5d9..a23ee4f 100644 --- a/src/relations/r1cs/mod.rs +++ b/src/relations/r1cs/mod.rs @@ -1,7 +1,7 @@ pub mod hashchain; use ark_ff::Field; -use ark_relations::gr1cs::{ConstraintSystemRef, R1CS_PREDICATE_LABEL}; +use ark_relations::gr1cs::ConstraintSystemRef; use effsc::hypercube::Ascending; use crate::error::WARPError; @@ -27,30 +27,31 @@ impl TryFrom> for R1CS { type Error = WARPError; fn try_from(cs: ConstraintSystemRef) -> Result { - let mut matrices = cs.to_matrices().unwrap(); - let mut r1cs = matrices.remove(R1CS_PREDICATE_LABEL).unwrap(); - let mut r1cs_iter = r1cs.drain(..); - let a_mat = r1cs_iter.next().unwrap(); - let b_mat = r1cs_iter.next().unwrap(); - let c_mat = r1cs_iter.next().unwrap(); + use ark_relations::gr1cs::R1CS_PREDICATE_LABEL; - let num_constraints = cs.num_constraints(); - let num_instance_variables = cs.num_instance_variables(); - let num_witness_variables = cs.num_witness_variables(); + let inner = cs.into_inner().unwrap(); + let all_matrices = inner.to_matrices().unwrap(); + let r1cs_matrices = all_matrices + .get(R1CS_PREDICATE_LABEL) + .expect("R1CS predicate must exist"); + + let num_constraints = inner + .get_predicate_num_constraints(R1CS_PREDICATE_LABEL) + .unwrap_or(0); // number of constraints should be to be power of 2 let m = num_constraints.next_power_of_two(); - let n = num_instance_variables + num_witness_variables; - let k = num_witness_variables; + let n = inner.num_instance_variables() + inner.num_witness_variables(); + let k = inner.num_witness_variables(); // both `unwrap()` calls below are safe since warp/lib.rs forbids compiling on platforms // with 16-bits pointers width let log_m = m.ilog2().try_into().unwrap(); let log_n = n.ilog2().try_into().unwrap(); - let mut a = a_mat.into_iter(); - let mut b = b_mat.into_iter(); - let mut c = c_mat.into_iter(); + let mut a = r1cs_matrices[0].clone().into_iter(); + let mut b = r1cs_matrices[1].clone().into_iter(); + let mut c = r1cs_matrices[2].clone().into_iter(); let mut p = vec![]; for _ in 0..m { // when there are no constraints left, we store an empty one diff --git a/src/serialize.rs b/src/serialize.rs index d9846ff..84d159a 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -1,52 +1,30 @@ -use ark_crypto_primitives::merkle_tree::{Config, MerkleTree, Path}; +use ark_crypto_primitives::merkle_tree::{Config, Path}; use ark_ff::{Field, PrimeField}; use ark_serialize::CanonicalSerialize; -pub type AccWitnessTuple = (Vec>, Vec>, Vec>); - -#[allow(clippy::type_complexity)] -pub type AccInstanceTuple = ( - Vec<::InnerDigest>, - Vec>, - Vec, - (Vec>, Vec>), - Vec, -); - -#[allow(clippy::type_complexity)] -pub type ProofTuple = ( - ::InnerDigest, - Vec, - F, - Vec, - Vec>, - Vec>>, - Vec>, -); +use crate::types::{AccumulatorInstance, AccumulatorWitness, WARPProof}; #[derive(CanonicalSerialize)] pub struct AccWitnessSerializer< F: Field + PrimeField, MT: Config + From<[u8; 32]>>, > { - pub rt: MT::InnerDigest, pub f: Vec, pub w: Vec, + _mt: std::marker::PhantomData, } impl + From<[u8; 32]>>> AccWitnessSerializer { - pub fn new(acc_witness: AccWitnessTuple) -> Self { - assert_eq!(acc_witness.0.len(), 1); - assert_eq!(acc_witness.1.len(), 1); - assert_eq!(acc_witness.2.len(), 1); - let f = acc_witness.1[0].clone(); - let w = acc_witness.2[0].clone(); + pub fn new(acc_witness: AccumulatorWitness) -> Self { + assert_eq!(acc_witness.td.len(), 1); + assert_eq!(acc_witness.f.len(), 1); + assert_eq!(acc_witness.w.len(), 1); Self { - rt: acc_witness.0[0].root(), - f, - w, + f: acc_witness.f.into_iter().next().unwrap(), + w: acc_witness.w.into_iter().next().unwrap(), + _mt: std::marker::PhantomData, } } } @@ -66,20 +44,23 @@ pub struct AccInstanceSerializer< impl + From<[u8; 32]>>> AccInstanceSerializer { - pub fn new(acc_instance: AccInstanceTuple) -> Self { - assert_eq!(acc_instance.0.len(), 1); - assert_eq!(acc_instance.1.len(), 1); - assert_eq!(acc_instance.2.len(), 1); - assert_eq!(acc_instance.3 .0.len(), 1); - assert_eq!(acc_instance.3 .1.len(), 1); - assert_eq!(acc_instance.4.len(), 1); - let beta = (acc_instance.3 .0[0].clone(), acc_instance.3 .1[0].clone()); + pub fn new(acc_instance: AccumulatorInstance) -> Self { + assert_eq!(acc_instance.rt.len(), 1); + assert_eq!(acc_instance.alpha.len(), 1); + assert_eq!(acc_instance.mu.len(), 1); + assert_eq!(acc_instance.beta.0.len(), 1); + assert_eq!(acc_instance.beta.1.len(), 1); + assert_eq!(acc_instance.eta.len(), 1); + let beta = ( + acc_instance.beta.0.into_iter().next().unwrap(), + acc_instance.beta.1.into_iter().next().unwrap(), + ); Self { - rt: acc_instance.0[0].clone(), - alpha: acc_instance.1[0].clone(), - mu: acc_instance.2[0], + rt: acc_instance.rt.into_iter().next().unwrap(), + alpha: acc_instance.alpha.into_iter().next().unwrap(), + mu: acc_instance.mu[0], beta, - eta: acc_instance.4[0], + eta: acc_instance.eta[0], } } } @@ -101,15 +82,15 @@ pub struct ProofSerializer< impl + From<[u8; 32]>>> ProofSerializer { - pub fn new(proof: ProofTuple) -> Self { + pub fn new(proof: WARPProof) -> Self { Self { - rt_0: proof.0, - mu_i: proof.1, - nu_0: proof.2, - nu_i: proof.3, - auth_0: proof.4, - auth_j: proof.5, - f_i_x_j: proof.6, + rt_0: proof.rt_0, + mu_i: proof.mu_i, + nu_0: proof.nu_0, + nu_i: proof.nu_i, + auth_0: proof.auth_0, + auth_j: proof.auth_j, + f_i_x_j: proof.shift_query_answers, } } } diff --git a/src/traits.rs b/src/traits.rs index 0bc6925..7f0b8f2 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,25 +2,15 @@ use ark_crypto_primitives::merkle_tree::Config; use ark_ff::Field; use spongefish::{ProverState, VerificationResult, VerifierState}; -use crate::error::{ProverError, VerifierError, WARPError}; - -pub type WARPAccumResult = ( - ( - >::AccumulatorInstances, - >::AccumulatorWitnesses, - ), - >::Proof, -); +use crate::error::{VerifierError, WARPError}; +use crate::types::{AccumulatorInstance, AccumulatorWitness, ProveResult, WARPProof}; pub trait AccumulationScheme { type Index; type ProverKey; type VerifierKey; - type AccumulatorInstances; - type AccumulatorWitnesses; type Instances; type Witnesses; - type Proof; // on given index, returns prover and verifier keys fn index( @@ -35,21 +25,21 @@ pub trait AccumulationScheme { prover_state: &mut ProverState, witnesses: Self::Witnesses, instances: Self::Instances, - acc_instances: Self::AccumulatorInstances, - acc_witnesses: Self::AccumulatorWitnesses, - ) -> Result, ProverError>; + acc_instance: AccumulatorInstance, + acc_witness: AccumulatorWitness, + ) -> ProveResult; fn verify<'a>( &self, vk: Self::VerifierKey, verifier_state: &mut VerifierState<'a>, - acc_instance: Self::AccumulatorInstances, - proof: Self::Proof, + acc_instance: AccumulatorInstance, + proof: WARPProof, ) -> Result<(), VerifierError>; fn decide( &self, - acc_witness: Self::AccumulatorWitnesses, - acc_instance: Self::AccumulatorInstances, + acc_witness: AccumulatorWitness, + acc_instance: AccumulatorInstance, ) -> Result<(), WARPError>; } diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..4db018d --- /dev/null +++ b/src/types.rs @@ -0,0 +1,116 @@ +use ark_codes::traits::LinearCode; +use ark_crypto_primitives::{ + crh::{CRHScheme, TwoToOneCRHScheme}, + merkle_tree::{Config, MerkleTree, Path}, +}; +use ark_ff::Field; +use std::marker::PhantomData; + +use crate::config::WARPConfig; +use crate::error::ProverError; +use crate::relations::BundledPESAT; + +// result of a prove call: (new accumulator instance + witness, proof) +pub type ProveResult = Result< + ( + (AccumulatorInstance, AccumulatorWitness), + WARPProof, + ), + ProverError, +>; + +/// Protocol parameters for WARP — the shared configuration used by all IOR phases. +pub struct WARPParams, C: LinearCode + Clone, MT: Config> { + pub _f: PhantomData, + pub config: WARPConfig, + pub code: C, + pub p: P, + pub mt_leaf_hash_params: ::Parameters, + pub mt_two_to_one_hash_params: ::Parameters, +} +/// Accumulator instance — the public part of an accumulated claim. +/// +/// Corresponds to `(rt, α, μ, (τ, x), η)` in the paper. +#[derive(Clone)] +pub struct AccumulatorInstance { + /// Merkle tree root commitments. + pub rt: Vec, + /// Code evaluation points (one per accumulated oracle). + pub alpha: Vec>, + /// Code evaluation targets (one per accumulated oracle). + pub mu: Vec, + /// Circuit evaluation points: `(τ_i, x_i)` pairs. + pub beta: (Vec>, Vec>), + /// Bundled PESAT evaluation targets. + pub eta: Vec, +} + +impl AccumulatorInstance { + pub fn empty() -> Self { + Self { + rt: vec![], + alpha: vec![], + mu: vec![], + beta: (vec![], vec![]), + eta: vec![], + } + } +} + +/// Accumulator witness — the private part of an accumulated claim. +/// +/// Corresponds to `(td, f, w)` in the paper. +#[derive(Clone)] +pub struct AccumulatorWitness { + /// Merkle tree trapdoors (full trees). + pub td: Vec>, + /// Oracle evaluations (codewords). + pub f: Vec>, + /// R1CS witnesses. + pub w: Vec>, +} + +impl AccumulatorWitness { + pub fn empty() -> Self { + Self { + td: vec![], + f: vec![], + w: vec![], + } + } +} + +/// Proof produced by the WARP accumulation prover. +/// +/// Corresponds to `(rt₀, μᵢ, ν₀, νᵢ, auth₀, authⱼ, f_i(x_j))` in the paper. +#[derive(Clone)] +pub struct WARPProof { + /// Fresh Merkle tree root. + pub rt_0: MT::InnerDigest, + /// Fresh code evaluations at 0. + pub mu_i: Vec, + /// Evaluation of accumulated oracle at zeta_0. + pub nu_0: F, + /// Evaluation claims (OOD + shift query answers). + pub nu_i: Vec, + /// Authentication paths for the fresh commitment. + pub auth_0: Vec>, + /// Authentication paths for each accumulated commitment. + pub auth_j: Vec>>, + /// Shift query answers: `f_i(x_j)` for each query position `j` and oracle `i`. + pub shift_query_answers: Vec>, +} + +/// Intermediate output of the PESAT reduction phase. +/// +/// This data flows from Phase 2 (PESAT Reduction) into Phase 3 (Constrained Code Accumulation). +pub struct PesatOutput { + /// Encoded codewords from fresh witnesses. + pub codewords: Vec>, + /// Merkle tree over the interleaved codeword leaves. + pub td_0: MerkleTree, + /// Code evaluation claims: `f_i(0)` for each codeword. + pub mus: Vec, + /// PESAT evaluation challenges (one per fresh instance). + pub taus: Vec>, +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d134388..644cb1b 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,7 @@ use ark_ff::{Field, PrimeField}; pub mod fields; +pub mod poly; pub mod poseidon; pub const fn chunk_size_bytes(modulus_bit_size: u32) -> usize { @@ -11,30 +12,6 @@ pub fn chunk_size() -> usize { chunk_size_bytes(F::MODULUS_BIT_SIZE) } -pub fn bytes_to_vec_f(bytes: &[u8]) -> Vec { - bytes - .chunks(chunk_size::()) - .map(|chunk| F::from_le_bytes_mod_order(chunk)) - .collect() -} - -pub fn byte_to_binary_field_array(byte: &u8) -> Vec { - (0..8) - .map(|i| { - let val = (byte >> i) & 1 == 1; - // return in field element and in binary - F::from(val) - }) - .collect::>() -} - -pub fn binary_field_elements_to_usize(elements: &[F]) -> usize { - elements - .iter() - .rev() - .fold(0, |acc, &b| (acc << 1) | b.is_one() as usize) -} - pub fn concat_slices(a: &[F], b: &[F]) -> Vec { let mut v = Vec::::with_capacity(a.len() + b.len()); v.extend_from_slice(a); diff --git a/src/utils/poly.rs b/src/utils/poly.rs new file mode 100644 index 0000000..efefdde --- /dev/null +++ b/src/utils/poly.rs @@ -0,0 +1,25 @@ +use ark_ff::Field; + +/// `eq(τ, y)` where `y ∈ {0,1}^{τ.len()}` is the Boolean point whose bit `j` +/// is `(point >> j) & 1`. Matches the LSB-indexed formula used by +/// [`effsc::hypercube::compute_hypercube_eq_evals`], i.e. +/// `eq_poly(τ, i) == compute_hypercube_eq_evals(τ.len(), τ)[i]`. +pub fn eq_poly(tau: &[F], point: usize) -> F { + let num_variables = tau.len(); + (0..num_variables).fold(F::one(), |acc, j| { + let bit = (point >> j) & 1; + if bit == 1 { + acc * tau[j] + } else { + acc * (F::one() - tau[j]) + } + }) +} + +pub fn eq_poly_non_binary(x: &[F], y: &[F]) -> F { + assert_eq!(x.len(), y.len()); + let res = x.iter().zip(y).fold(F::one(), |acc, (x_i, y_i)| { + acc * (*x_i * *y_i + (F::one() - x_i) * (F::one() - y_i)) + }); + res +} diff --git a/tests/integration_warp.rs b/tests/integration_warp.rs new file mode 100644 index 0000000..17086cf --- /dev/null +++ b/tests/integration_warp.rs @@ -0,0 +1,353 @@ +//! End-to-end integration tests for the full WARP prove / verify / decide +//! cycle. Previously lived in `src/lib.rs` as an inline `#[cfg(test)]` +//! module; moved here so `src/lib.rs` stays focused on the orchestrator. +//! +//! Two suites: +//! +//! - `warp_test` on BLS12-381 (≈254-bit multi-limb) +//! - `warp_test_goldilocks` on Goldilocks (64-bit SmallFp) +//! +//! The suites are deliberately duplicated rather than generic-over-`F`: +//! their associated-type bounds (Merkle config, poseidon config, etc.) +//! differ enough that a single generic function would need a long +//! `where` clause for marginal DRY gain. + +use std::marker::PhantomData; + +use ark_bls12_381::Fr as BLS12_381; +use ark_codes::{ + reed_solomon::{config::ReedSolomonConfig, ReedSolomon}, + traits::LinearCode, +}; +use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; +use ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig; +use ark_ff::UniformRand; +use ark_serialize::{CanonicalSerialize, Compress}; +use ark_std::rand::thread_rng; + +use warp::config::WARPConfig; +use warp::relations::{ + r1cs::{ + hashchain::{compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness}, + R1CS, + }, + BundledPESAT, Relation, ToPolySystem, +}; +use warp::serialize::{AccInstanceSerializer, AccWitnessSerializer, ProofSerializer}; +use warp::traits::AccumulationScheme; +use warp::types::{AccumulatorInstance, AccumulatorWitness}; +use warp::utils::poseidon; +use warp::WARP; + +#[test] +fn warp_test() { + let l1 = 4; + let s = 8; + let t = 7; + let hash_chain_size = 10; + let mut rng = thread_rng(); + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + let code_config = ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); + let code = ReedSolomon::new(code_config.clone()); + + let instances_witnesses: (Vec>, Vec>) = (0..l1) + .map(|_| { + let preimage = vec![BLS12_381::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness { + preimage, + _crhs_scheme: PhantomData::>, + }; + let relation = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (relation.x, relation.w) + }) + .unzip(); + + let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + + let warp_config = WARPConfig::new(l1, l1, s, t, r1cs.config(), code.code_len()); + let hash_chain_warp = WARP::, _, Blake3MerkleConfig>::new( + warp_config.clone(), + code.clone(), + r1cs.clone(), + (), + (), + ); + + let (mut acc_roots, mut acc_alphas, mut acc_mus, mut acc_taus, mut acc_xs, mut acc_eta) = + (vec![], vec![], vec![], vec![], vec![], vec![]); + let (mut acc_tds, mut acc_f, mut acc_ws) = (vec![], vec![], vec![]); + + for _ in 0..l1 { + let domainsep = spongefish::domain_separator!("test::warp"); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + let ((acc_x, acc_w), _pf) = hash_chain_warp + .prove( + (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), + &mut prover_state, + instances_witnesses.1.clone(), + instances_witnesses.0.clone(), + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ) + .unwrap(); + acc_roots.push(acc_x.rt[0].clone()); + acc_alphas.push(acc_x.alpha[0].clone()); + acc_mus.push(acc_x.mu[0]); + acc_taus.push(acc_x.beta.0[0].clone()); + acc_xs.push(acc_x.beta.1[0].clone()); + acc_eta.push(acc_x.eta[0]); + + acc_tds.push(acc_w.td[0].clone()); + acc_f.push(acc_w.f[0].clone()); + acc_ws.push(acc_w.w[0].clone()); + } + + let domainsep = spongefish::domain_separator!("test::warp"); + let warp_config = + WARPConfig::<_, R1CS>::new(8, l1, s, t, r1cs.config(), code.code_len()); + + let hash_chain_warp = WARP::, _, Blake3MerkleConfig>::new( + warp_config.clone(), + code.clone(), + r1cs.clone(), + (), + (), + ); + + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + let ((acc_x, acc_w), pf) = hash_chain_warp + .prove( + (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), + &mut prover_state, + instances_witnesses.1, + instances_witnesses.0, + AccumulatorInstance { + rt: acc_roots, + alpha: acc_alphas, + mu: acc_mus, + beta: (acc_taus, acc_xs), + eta: acc_eta, + }, + AccumulatorWitness { + td: acc_tds, + f: acc_f, + w: acc_ws, + }, + ) + .unwrap(); + + let narg_str = prover_state.narg_string().to_vec(); + let domainsep_v = spongefish::domain_separator!("test::warp"); + let mut verifier_state = domainsep_v + .without_session() + .instance(&0u32) + .std_verifier(&narg_str); + hash_chain_warp + .verify( + (r1cs.m, r1cs.n, r1cs.k), + &mut verifier_state, + acc_x.clone(), + pf.clone(), + ) + .unwrap(); + hash_chain_warp + .decide(acc_w.clone(), acc_x.clone()) + .unwrap(); + + let acc_x_to_serde = AccInstanceSerializer::<_, Blake3MerkleConfig>::new(acc_x); + let acc_w_to_serde = AccWitnessSerializer::<_, Blake3MerkleConfig>::new(acc_w); + let proof_to_serde = ProofSerializer::new(pf); + + println!( + "acc_x size: {}", + acc_x_to_serde.serialized_size(Compress::Yes) + ); + println!( + "acc_w size: {}", + acc_w_to_serde.serialized_size(Compress::Yes) + ); + println!( + "proof size: {}", + proof_to_serde.serialized_size(Compress::Yes) + ); + println!("narg_str size: {}", narg_str.len()); +} + +#[test] +fn warp_test_goldilocks() { + use warp::utils::fields::Goldilocks; + + let l1 = 4; + let s = 8; + let t = 7; + let hash_chain_size = 10; + let mut rng = thread_rng(); + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + let code_config = ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); + let code = ReedSolomon::new(code_config); + + let instances_witnesses: (Vec>, Vec>) = (0..l1) + .map(|_| { + let preimage = vec![Goldilocks::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness { + preimage, + _crhs_scheme: PhantomData::>, + }; + let relation = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (relation.x, relation.w) + }) + .unzip(); + + let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + + let warp_config = WARPConfig::new(l1, l1, s, t, r1cs.config(), code.code_len()); + let hash_chain_warp = + WARP::, _, Blake3MerkleConfig>::new( + warp_config.clone(), + code.clone(), + r1cs.clone(), + (), + (), + ); + + let (mut acc_roots, mut acc_alphas, mut acc_mus, mut acc_taus, mut acc_xs, mut acc_eta) = + (vec![], vec![], vec![], vec![], vec![], vec![]); + let (mut acc_tds, mut acc_f, mut acc_ws) = (vec![], vec![], vec![]); + + for _ in 0..l1 { + let domainsep = spongefish::domain_separator!("test::warp"); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + let ((acc_x, acc_w), _pf) = hash_chain_warp + .prove( + (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), + &mut prover_state, + instances_witnesses.1.clone(), + instances_witnesses.0.clone(), + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ) + .unwrap(); + acc_roots.push(acc_x.rt[0].clone()); + acc_alphas.push(acc_x.alpha[0].clone()); + acc_mus.push(acc_x.mu[0]); + acc_taus.push(acc_x.beta.0[0].clone()); + acc_xs.push(acc_x.beta.1[0].clone()); + acc_eta.push(acc_x.eta[0]); + + acc_tds.push(acc_w.td[0].clone()); + acc_f.push(acc_w.f[0].clone()); + acc_ws.push(acc_w.w[0].clone()); + } + + let domainsep = spongefish::domain_separator!("test::warp"); + // Use 8 (2*l1) for the total accumulation size to test multi-instance accumulation + let warp_config = + WARPConfig::<_, R1CS>::new(8, l1, s, t, r1cs.config(), code.code_len()); + + let hash_chain_warp = + WARP::, _, Blake3MerkleConfig>::new( + warp_config.clone(), + code.clone(), + r1cs.clone(), + (), + (), + ); + + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + let ((acc_x, acc_w), pf) = hash_chain_warp + .prove( + (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), + &mut prover_state, + instances_witnesses.1, + instances_witnesses.0, + AccumulatorInstance { + rt: acc_roots, + alpha: acc_alphas, + mu: acc_mus, + beta: (acc_taus, acc_xs), + eta: acc_eta, + }, + AccumulatorWitness { + td: acc_tds, + f: acc_f, + w: acc_ws, + }, + ) + .unwrap(); + + let narg_str = prover_state.narg_string().to_vec(); + let domainsep_v = spongefish::domain_separator!("test::warp"); + let mut verifier_state = domainsep_v + .without_session() + .instance(&0u32) + .std_verifier(&narg_str); + hash_chain_warp + .verify( + (r1cs.m, r1cs.n, r1cs.k), + &mut verifier_state, + acc_x.clone(), + pf.clone(), + ) + .unwrap(); + hash_chain_warp + .decide(acc_w.clone(), acc_x.clone()) + .unwrap(); + + let acc_x_to_serde = AccInstanceSerializer::<_, Blake3MerkleConfig>::new(acc_x); + let acc_w_to_serde = AccWitnessSerializer::<_, Blake3MerkleConfig>::new(acc_w); + let proof_to_serde = ProofSerializer::new(pf); + + println!( + "Goldilocks acc_x size: {}", + acc_x_to_serde.serialized_size(Compress::Yes) + ); + println!( + "Goldilocks acc_w size: {}", + acc_w_to_serde.serialized_size(Compress::Yes) + ); + println!( + "Goldilocks proof size: {}", + proof_to_serde.serialized_size(Compress::Yes) + ); + println!("Goldilocks narg_str size: {}", narg_str.len()); +} diff --git a/tests/profile_json.rs b/tests/profile_json.rs new file mode 100644 index 0000000..5a7d6ff --- /dev/null +++ b/tests/profile_json.rs @@ -0,0 +1,180 @@ +//! End-to-end check that Plan O's JSON layer emits well-formed +//! `warp.profile.v1` records with phase names, dimensions, and non-zero +//! op counters. +//! +//! Runs only under `--features profile` (see the `cfg` below). Without +//! the feature there's no JSON layer to test. + +#![cfg(feature = "profile")] + +use std::io::{self, Write}; +use std::sync::{Arc, Mutex}; + +use ark_bls12_381::Fr as BLS12_381; +use ark_codes::{ + reed_solomon::{config::ReedSolomonConfig, ReedSolomon}, + traits::LinearCode, +}; +use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; +use ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig; +use ark_std::rand::thread_rng; +use ark_std::UniformRand; +use std::marker::PhantomData; + +use warp::config::WARPConfig; +use warp::relations::{ + r1cs::{ + hashchain::{compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness}, + R1CS, + }, + BundledPESAT, Relation, ToPolySystem, +}; +use warp::traits::AccumulationScheme; +use warp::types::{AccumulatorInstance, AccumulatorWitness}; +use warp::utils::poseidon; +use warp::WARP; + +/// `Arc>>` wrapped so it implements `io::Write`. +#[derive(Clone)] +struct SharedBuf(Arc>>); + +impl Write for SharedBuf { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.lock().unwrap().extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +#[test] +fn json_layer_emits_phase_records() { + let sink = SharedBuf(Arc::new(Mutex::new(Vec::new()))); + let installed = warp::profile::init_json(sink.clone()); + assert!( + installed, + "json subscriber install should succeed on first call" + ); + + // Minimum viable prove run — same shape as the top-level warp_test. + let l1 = 4; + let s = 8; + let t = 7; + let hash_chain_size = 10; + let mut rng = thread_rng(); + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + let code_config = ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); + let code = ReedSolomon::new(code_config.clone()); + + let (instances, witnesses): (Vec<_>, Vec<_>) = (0..l1) + .map(|_| { + let preimage = vec![BLS12_381::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness { + preimage, + _crhs_scheme: PhantomData::>, + }; + let relation = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (relation.x, relation.w) + }) + .unzip(); + + let warp_config = WARPConfig::new(l1, l1, s, t, r1cs.config(), code.code_len()); + let hash_chain_warp = WARP::, _, Blake3MerkleConfig>::new( + warp_config, + code, + r1cs.clone(), + (), + (), + ); + + let domainsep = spongefish::domain_separator!("test::profile_json"); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); + + hash_chain_warp + .prove( + (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), + &mut prover_state, + witnesses, + instances, + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ) + .unwrap(); + + // Inspect collected records. + let bytes = sink.0.lock().unwrap().clone(); + let text = String::from_utf8(bytes).expect("JSON output is UTF-8"); + assert!( + !text.is_empty(), + "JSON sink should contain at least one record" + ); + + let lines: Vec<&str> = text.lines().collect(); + assert!( + !lines.is_empty(), + "expected newline-delimited JSON, got: {text:?}" + ); + + // Every line is a record carrying the schema tag. + for (i, line) in lines.iter().enumerate() { + assert!( + line.contains(r#""schema":"warp.profile.v1""#), + "line {i} missing schema: {line}" + ); + assert!( + line.contains(r#""wall_ns""#), + "line {i} missing wall_ns: {line}" + ); + assert!( + line.contains(r#""counters""#), + "line {i} missing counters: {line}" + ); + assert!( + line.contains(r#""dimensions""#), + "line {i} missing dimensions: {line}" + ); + } + + // Every top-level phase must appear at least once. + for phase in [ + "warp.prove", + "pesat", + "twin_constraint", + "ood", + "batching", + "proximity", + ] { + let needle = format!(r#""phase":"{phase}""#); + assert!( + text.contains(&needle), + "expected a record for phase `{phase}`, got lines: {lines:#?}" + ); + } + + // At least one record must have non-empty counters (pesat bumps several). + assert!( + text.contains(r#""merkle_tree_builds":"#), + "expected merkle_tree_builds counter in output: {text}" + ); + assert!( + text.contains(r#""encode_calls":"#), + "expected encode_calls counter in output: {text}" + ); +} diff --git a/tests/verifier_negative.rs b/tests/verifier_negative.rs new file mode 100644 index 0000000..06ddef8 --- /dev/null +++ b/tests/verifier_negative.rs @@ -0,0 +1,285 @@ +//! Negative-path verifier tests. +//! +//! The existing `warp_test` exercises only the happy path. This file +//! covers the other direction: for each cleanly-triggerable +//! [`warp::error::VerifierError`] variant, produce a valid proof, tamper +//! with one field, and assert the verifier rejects the proof with the +//! *specific* expected error. +//! +//! Catches a class of bug that's otherwise invisible: "verifier looks +//! correct on valid proofs but accepts broken ones." Several real-world +//! SNARKs have shipped that way. +//! +//! Not every error variant is reachable through a one-field tamper. +//! Variants we don't cover here: +//! +//! - `SpongeFish` / `ArkError` wrap underlying errors; hitting them +//! requires transcript-byte-level corruption rather than proof-object +//! tampering, which would exercise spongefish/arkworks, not our code. +//! - `NumSumcheckRounds` is derived from the transcript; a sibling of +//! `SpongeFish`. +//! - `SumcheckRound` is not raised from any code path right now. + +use std::marker::PhantomData; + +use ark_bls12_381::Fr as BLS12_381; +use ark_codes::{ + reed_solomon::{config::ReedSolomonConfig, ReedSolomon}, + traits::LinearCode, +}; +use ark_crypto_primitives::crh::poseidon::{constraints::CRHGadget, CRH}; +use ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig; +use ark_std::rand::thread_rng; +use ark_std::UniformRand; + +use warp::config::WARPConfig; +use warp::error::VerifierError; +use warp::relations::{ + r1cs::{ + hashchain::{compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness}, + R1CS, + }, + BundledPESAT, Relation, ToPolySystem, +}; +use warp::traits::AccumulationScheme; +use warp::types::{AccumulatorInstance, AccumulatorWitness, WARPProof}; +use warp::utils::poseidon; +use warp::WARP; + +type F = BLS12_381; +type MT = Blake3MerkleConfig; +type WarpT = WARP, ReedSolomon, MT>; + +/// Everything the verifier needs to re-check, plus enough dimensions +/// to re-derive the verifier state. +struct Fixture { + warp: WarpT, + vk: (usize, usize, usize), + acc_x: AccumulatorInstance, + proof: WARPProof, + narg_str: Vec, +} + +impl Fixture { + fn verify( + &self, + acc_x: AccumulatorInstance, + proof: WARPProof, + ) -> Result<(), VerifierError> { + let domainsep_v = spongefish::domain_separator!("test::warp::negative"); + let mut verifier_state = domainsep_v + .without_session() + .instance(&0u32) + .std_verifier(&self.narg_str); + self.warp.verify(self.vk, &mut verifier_state, acc_x, proof) + } +} + +/// Build a real proof against the hash-chain relation, with both fresh +/// and accumulated components so negative tests can target any field. +fn make_fixture() -> Fixture { + let l1 = 4; + let s = 8; + let t = 7; + let hash_chain_size = 10; + let mut rng = thread_rng(); + let poseidon_config = poseidon::initialize_poseidon_config::(); + let r1cs = HashChainRelation::, CRHGadget<_>>::into_r1cs(&( + poseidon_config.clone(), + hash_chain_size, + )) + .unwrap(); + let code_config = ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); + let code = ReedSolomon::new(code_config); + + let (instances, witnesses): (Vec<_>, Vec<_>) = (0..l1) + .map(|_| { + let preimage = vec![F::rand(&mut rng)]; + let instance = HashChainInstance { + digest: compute_hash_chain::>( + &poseidon_config, + &preimage, + hash_chain_size, + ), + }; + let witness = HashChainWitness { + preimage, + _crhs_scheme: PhantomData::>, + }; + let relation = HashChainRelation::, CRHGadget<_>>::new( + instance, + witness, + (poseidon_config.clone(), hash_chain_size), + ); + (relation.x, relation.w) + }) + .unzip(); + + // Phase 1: produce `l1` single-round acc states so we have a non-trivial + // accumulator to feed phase 2 (l2 > 0 so NumL2Instances is reachable). + let warp_cfg1 = WARPConfig::new(l1, l1, s, t, r1cs.config(), code.code_len()); + let w1 = WARP::, _, MT>::new(warp_cfg1, code.clone(), r1cs.clone(), (), ()); + + let (mut roots, mut alphas, mut mus, mut taus, mut xs, mut etas) = + (vec![], vec![], vec![], vec![], vec![], vec![]); + let (mut tds, mut fs, mut ws) = (vec![], vec![], vec![]); + + for _ in 0..l1 { + let ds = spongefish::domain_separator!("test::warp::negative"); + let mut ps = ds.without_session().instance(&0u32).std_prover(); + let ((acc_x, acc_w), _) = w1 + .prove( + (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), + &mut ps, + witnesses.clone(), + instances.clone(), + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ) + .unwrap(); + roots.push(acc_x.rt[0].clone()); + alphas.push(acc_x.alpha[0].clone()); + mus.push(acc_x.mu[0]); + taus.push(acc_x.beta.0[0].clone()); + xs.push(acc_x.beta.1[0].clone()); + etas.push(acc_x.eta[0]); + tds.push(acc_w.td[0].clone()); + fs.push(acc_w.f[0].clone()); + ws.push(acc_w.w[0].clone()); + } + + // Phase 2: the "real" prove with l2 > 0 accumulated instances. + let warp_cfg2 = WARPConfig::<_, R1CS>::new(8, l1, s, t, r1cs.config(), code.code_len()); + let warp = WARP::, _, MT>::new(warp_cfg2, code, r1cs.clone(), (), ()); + + let ds = spongefish::domain_separator!("test::warp::negative"); + let mut ps = ds.without_session().instance(&0u32).std_prover(); + let ((acc_x, _acc_w), proof) = warp + .prove( + (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), + &mut ps, + witnesses, + instances, + AccumulatorInstance { + rt: roots, + alpha: alphas, + mu: mus, + beta: (taus, xs), + eta: etas, + }, + AccumulatorWitness { + td: tds, + f: fs, + w: ws, + }, + ) + .unwrap(); + + Fixture { + warp, + vk: (r1cs.m, r1cs.n, r1cs.k), + acc_x, + proof, + narg_str: ps.narg_string().to_vec(), + } +} + +fn assert_err(result: Result<(), VerifierError>, expected: &str) { + match result { + Ok(()) => panic!("expected `{expected}`, got Ok(())"), + Err(err) => { + let dbg = format!("{err:?}"); + assert!(dbg.contains(expected), "expected `{expected}`, got `{dbg}`"); + } + } +} + +// Sanity check: a fresh fixture always verifies. +#[test] +fn happy_path_verifies() { + let fix = make_fixture(); + fix.verify(fix.acc_x.clone(), fix.proof.clone()) + .expect("untampered proof must verify"); +} + +#[test] +fn tampered_alpha_raises_code_evaluation_point() { + let fix = make_fixture(); + let mut acc_x = fix.acc_x.clone(); + acc_x.alpha[0][0] += F::from(1u64); + assert_err(fix.verify(acc_x, fix.proof.clone()), "CodeEvaluationPoint"); +} + +#[test] +fn tampered_beta_tau_raises_circuit_evaluation_point() { + let fix = make_fixture(); + let mut acc_x = fix.acc_x.clone(); + acc_x.beta.0[0][0] += F::from(1u64); + assert_err( + fix.verify(acc_x, fix.proof.clone()), + "CircuitEvaluationPoint", + ); +} + +#[test] +fn tampered_beta_x_raises_circuit_evaluation_point() { + let fix = make_fixture(); + let mut acc_x = fix.acc_x.clone(); + acc_x.beta.1[0][0] += F::from(1u64); + assert_err( + fix.verify(acc_x, fix.proof.clone()), + "CircuitEvaluationPoint", + ); +} + +#[test] +fn truncated_shift_query_answers_raises_num_shift_queries() { + let fix = make_fixture(); + let mut proof = fix.proof.clone(); + proof.shift_query_answers.pop(); + assert_err(fix.verify(fix.acc_x.clone(), proof), "NumShiftQueries"); +} + +#[test] +fn swapped_auth0_leaf_index_raises_shift_query_index() { + let fix = make_fixture(); + let mut proof = fix.proof.clone(); + // Overwrite auth_0[0]'s path with auth_0[1]'s path so leaf_index + // stops matching queries.leaf_positions[0]. + let p0_is_p1 = proof.auth_0[0].leaf_index == proof.auth_0[1].leaf_index; + if p0_is_p1 { + // Extremely unlikely but keeps the test deterministic: skip with a + // clear message rather than silently pass. + eprintln!("fixture happened to sample identical leaf indices; skipping"); + return; + } + proof.auth_0.swap(0, 1); + assert_err(fix.verify(fix.acc_x.clone(), proof), "ShiftQueryIndex"); +} + +#[test] +fn tampered_shift_query_answer_raises_shift_query() { + let fix = make_fixture(); + let mut proof = fix.proof.clone(); + // Each row of shift_query_answers has l2 + l1 entries; tampering any + // of them makes path.verify fail because the leaf hash no longer + // matches the committed root. + proof.shift_query_answers[0][0] += F::from(1u64); + assert_err(fix.verify(fix.acc_x.clone(), proof), "ShiftQuery"); +} + +#[test] +fn truncated_auth_j_raises_num_l2_instances() { + let fix = make_fixture(); + let mut proof = fix.proof.clone(); + proof.auth_j.pop(); + assert_err(fix.verify(fix.acc_x.clone(), proof), "NumL2Instances"); +} + +#[test] +fn tampered_mu_raises_target() { + let fix = make_fixture(); + let mut acc_x = fix.acc_x.clone(); + acc_x.mu[0] += F::from(1u64); + assert_err(fix.verify(acc_x, fix.proof.clone()), "Target"); +} diff --git a/texput.log b/texput.log new file mode 100644 index 0000000..438b14a --- /dev/null +++ b/texput.log @@ -0,0 +1,20 @@ +This is LuaHBTeX, Version 1.16.0 (TeX Live 2023) (format=lualatex 2024.1.16) 3 MAY 2026 20:03 + restricted system commands enabled. +**iors.tex + +! Emergency stop. +<*> iors.tex + +*** (job aborted, file error in nonstop mode) + + + +Here is how much of LuaTeX's memory you used: + 5 strings out of 478285 + 100000,1977958 words of node,token memory allocated 270 words of node memory still in use: + 1 hlist, 39 glue_spec nodes + avail lists: 2:12,3:3,4:1,5:1 + 20331 multiletter control sequences out of 65536+600000 + 14 fonts using 591679 bytes + 0i,0n,0p,0b,6s stack positions out of 10000i,1000n,20000p,200000b,200000s +! ==> Fatal error occurred, no output PDF file produced!