From d7e46e7bf7ebc41c0ce9ce2f0bfed2984ad7936a Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:26:08 +0100 Subject: [PATCH 01/21] bump to gr1cs interface --- Cargo.toml | 9 ++- src/crypto/blake3_crh/fields.rs | 35 +++++++++++ src/crypto/blake3_crh/mod.rs | 70 +++++++++++++++++++++ src/crypto/merkle/blake3.rs | 5 +- src/crypto/mod.rs | 1 + src/lib.rs | 1 - src/relations/description.rs | 36 ++++++----- src/relations/r1cs/hashchain/mod.rs | 2 +- src/relations/r1cs/hashchain/relation.rs | 18 ++++-- src/relations/r1cs/hashchain/synthesizer.rs | 2 +- src/relations/r1cs/mod.rs | 26 +++++--- src/serialize.rs | 11 ++-- 12 files changed, 171 insertions(+), 45 deletions(-) create mode 100644 src/crypto/blake3_crh/fields.rs create mode 100644 src/crypto/blake3_crh/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 7e3e30b..3a3440a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" ark-crypto-primitives = { version = "0.5.0", features = [ "merkle_tree", "crh", - "r1cs", + "constraints", "sponge", ] } ark-ff = "0.5.0" @@ -29,10 +29,13 @@ ark-codes = { git = "https://github.com/dmpierre/ark-codes.git" } [patch.crates-io] -ark-crypto-primitives = { git = "https://github.com/benbencik/crypto-primitives.git", branch = "smallfp-absorb-trait" } +ark-crypto-primitives = { git = "https://github.com/benbencik/crypto-primitives.git", branch = "smallfp-absorb-clean" } ark-ff = { git = "https://github.com/arkworks-rs/algebra.git" } ark-poly = { git = "https://github.com/arkworks-rs/algebra.git" } ark-serialize = { git = "https://github.com/arkworks-rs/algebra.git" } +ark-r1cs-std = { git = "https://github.com/arkworks-rs/r1cs-std" } +ark-relations = { git = "https://github.com/arkworks-rs/snark.git" } +ark-snark = { git = "https://github.com/arkworks-rs/snark.git" } # resolve transitive pull of spongefish from efficient-sumcheck [patch."https://github.com/arkworks-rs/spongefish"] @@ -41,7 +44,7 @@ spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "small [dev-dependencies] ark-bls12-381 = "0.5.0" ark-bn254 = "0.5.0" -criterion = "0.7" +criterion = "0.8" [features] default = ["asm"] diff --git a/src/crypto/blake3_crh/fields.rs b/src/crypto/blake3_crh/fields.rs new file mode 100644 index 0000000..0cc9417 --- /dev/null +++ b/src/crypto/blake3_crh/fields.rs @@ -0,0 +1,35 @@ +use ark_ff::Field; +use ark_serialize::CanonicalSerialize; +use ark_std::rand::RngCore; +use core::borrow::Borrow; +use core::marker::PhantomData; + +use ark_crypto_primitives::{crh::CRHScheme, Error}; + +use super::GenericDigest; + +/// Blake3 leaf hash that takes field elements as input. +#[derive(Clone)] +pub struct Blake3F { + _f: PhantomData, +} + +impl CRHScheme for Blake3F { + type Input = [F]; + type Output = GenericDigest<32>; + type Parameters = (); + + fn setup(_: &mut R) -> Result { + Ok(()) + } + + fn evaluate>( + (): &Self::Parameters, + input: T, + ) -> Result { + let mut buf = Vec::new(); + input.borrow().serialize_compressed(&mut buf)?; + let output: [_; 32] = blake3::hash(&buf).into(); + Ok(output.into()) + } +} diff --git a/src/crypto/blake3_crh/mod.rs b/src/crypto/blake3_crh/mod.rs new file mode 100644 index 0000000..404033b --- /dev/null +++ b/src/crypto/blake3_crh/mod.rs @@ -0,0 +1,70 @@ +pub mod fields; + +use ark_crypto_primitives::{crh::TwoToOneCRHScheme, sponge::Absorb, Error}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_std::rand::RngCore; +use core::borrow::Borrow; + +/// A generic fixed-size digest (copied from whir). +#[derive(Clone, Debug, Eq, PartialEq, Hash, CanonicalSerialize, CanonicalDeserialize)] +pub struct GenericDigest(pub [u8; N]); + +impl Default for GenericDigest { + fn default() -> Self { + Self([0; N]) + } +} + +impl AsRef<[u8]> for GenericDigest { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From<[u8; N]> for GenericDigest { + fn from(value: [u8; N]) -> Self { + Self(value) + } +} + +impl Absorb for GenericDigest { + fn to_sponge_bytes(&self, dest: &mut Vec) { + dest.extend_from_slice(&self.0); + } + + fn to_sponge_field_elements(&self, dest: &mut Vec) { + dest.push(F::from_be_bytes_mod_order(&self.0)); + } +} + +/// Blake3 two-to-one hash for internal Merkle tree nodes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Blake3; + +impl TwoToOneCRHScheme for Blake3 { + type Input = GenericDigest<32>; + type Output = GenericDigest<32>; + type Parameters = (); + + fn setup(_: &mut R) -> Result { + Ok(()) + } + + fn evaluate>( + (): &Self::Parameters, + left_input: T, + right_input: T, + ) -> Result { + let output: [_; 32] = + blake3::hash(&[left_input.borrow().0, right_input.borrow().0].concat()).into(); + Ok(output.into()) + } + + fn compress>( + parameters: &Self::Parameters, + left_input: T, + right_input: T, + ) -> Result { + Self::evaluate(parameters, left_input, right_input) + } +} diff --git a/src/crypto/merkle/blake3.rs b/src/crypto/merkle/blake3.rs index 963a7d4..e6ca88c 100644 --- a/src/crypto/merkle/blake3.rs +++ b/src/crypto/merkle/blake3.rs @@ -1,7 +1,6 @@ use super::parameters::MerkleTreeParams; -use ark_crypto_primitives::crh::blake3::fields::Blake3F; -use ark_crypto_primitives::crh::blake3::Blake3; -use ark_crypto_primitives::crh::blake3::GenericDigest; +use crate::crypto::blake3_crh::fields::Blake3F; +use crate::crypto::blake3_crh::{Blake3, GenericDigest}; use ark_crypto_primitives::{ crh::{CRHScheme, TwoToOneCRHScheme}, merkle_tree::{Config as MerkleConfig, IdentityDigestConverter}, diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index bdf9eb8..6a3489a 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1 +1,2 @@ +pub mod blake3_crh; pub mod merkle; diff --git a/src/lib.rs b/src/lib.rs index 698dbdc..c05cc00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -733,7 +733,6 @@ impl< )?; (rt[0] == computed_mt.root()).ok_or_err(DeciderError::MerkleRoot)?; (mt[0].root() == computed_mt.root()).ok_or_err(DeciderError::MerkleTrapDoor)?; - (mt[0].leaf_nodes == computed_mt.leaf_nodes).ok_or_err(DeciderError::MerkleRoot)?; let f_hat = DenseMultilinearExtension::from_evaluations_slice( log2(self.code.code_len()) as usize, diff --git a/src/relations/description.rs b/src/relations/description.rs index 032dfeb..16a749d 100644 --- a/src/relations/description.rs +++ b/src/relations/description.rs @@ -1,5 +1,5 @@ use ark_ff::Field; -use ark_relations::r1cs::{ConstraintMatrices, ConstraintSynthesizer, ConstraintSystem}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem, R1CS_PREDICATE_LABEL}; use serde::Serialize; #[derive(Serialize)] @@ -37,22 +37,26 @@ impl SerializableConstraintMatrices { .generate_constraints(constraint_system.clone()) .unwrap(); constraint_system.finalize(); - let matrices: ConstraintMatrices = constraint_system.to_matrices().unwrap(); - let serializable = SerializableConstraintMatrices::from(matrices); + + 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 num_constraints = cs + .get_predicate_num_constraints(R1CS_PREDICATE_LABEL) + .unwrap_or(0); + + let serializable = SerializableConstraintMatrices { + num_instance_variables: cs.num_instance_variables(), + num_witness_variables: cs.num_witness_variables(), + num_constraints, + 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() } } - -impl From> for SerializableConstraintMatrices { - fn from(m: ConstraintMatrices) -> Self { - Self { - num_instance_variables: m.num_instance_variables, - num_witness_variables: m.num_witness_variables, - num_constraints: m.num_constraints, - a: SerializableConstraintMatrices::serialize_nested_field(m.a), - b: SerializableConstraintMatrices::serialize_nested_field(m.b), - c: SerializableConstraintMatrices::serialize_nested_field(m.c), - } - } -} diff --git a/src/relations/r1cs/hashchain/mod.rs b/src/relations/r1cs/hashchain/mod.rs index 45c664c..4f34fe3 100644 --- a/src/relations/r1cs/hashchain/mod.rs +++ b/src/relations/r1cs/hashchain/mod.rs @@ -12,7 +12,7 @@ use ark_crypto_primitives::{ }; use ark_ff::PrimeField; use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem}; pub use config::HashChainConfig; pub use instance::HashChainInstance; pub use relation::compute_hash_chain; diff --git a/src/relations/r1cs/hashchain/relation.rs b/src/relations/r1cs/hashchain/relation.rs index 6742e0a..e681158 100644 --- a/src/relations/r1cs/hashchain/relation.rs +++ b/src/relations/r1cs/hashchain/relation.rs @@ -4,7 +4,7 @@ use ark_crypto_primitives::{ }; use ark_ff::{Field, PrimeField}; use ark_r1cs_std::fields::fp::FpVar; -use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef}; use ark_serialize::CanonicalSerialize; use ark_std::marker::PhantomData; @@ -94,14 +94,22 @@ where .unwrap(); constraint_system.finalize(); - let cs = constraint_system.into_inner().unwrap(); + // 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.clone()), + constraint_system, config: hash_config, instance, witness, - x: cs.instance_assignment, - w: cs.witness_assignment, + x, + w, _crhs_scheme: PhantomData, _crhs_scheme_gadget: PhantomData, } diff --git a/src/relations/r1cs/hashchain/synthesizer.rs b/src/relations/r1cs/hashchain/synthesizer.rs index beb4a90..511b1ff 100644 --- a/src/relations/r1cs/hashchain/synthesizer.rs +++ b/src/relations/r1cs/hashchain/synthesizer.rs @@ -1,7 +1,7 @@ use ark_crypto_primitives::crh::{CRHScheme, CRHSchemeGadget}; use ark_ff::{Field, PrimeField}; use ark_r1cs_std::{alloc::AllocVar, eq::EqGadget, fields::fp::FpVar}; -use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; +use ark_relations::gr1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}; use ark_std::marker::PhantomData; use crate::relations::r1cs::hashchain::{HashChainInstance, HashChainWitness}; diff --git a/src/relations/r1cs/mod.rs b/src/relations/r1cs/mod.rs index 0232a7e..6377cfe 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::r1cs::ConstraintSystemRef; +use ark_relations::gr1cs::ConstraintSystemRef; use efficient_sumcheck::{hypercube::Hypercube, order_strategy::AscendingOrder}; use crate::error::WARPError; @@ -27,21 +27,31 @@ impl TryFrom> for R1CS { type Error = WARPError; fn try_from(cs: ConstraintSystemRef) -> Result { - let matrices = cs.to_matrices().unwrap(); + use ark_relations::gr1cs::R1CS_PREDICATE_LABEL; + + 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 = matrices.num_constraints.next_power_of_two(); - let n = matrices.num_instance_variables + matrices.num_witness_variables; - let k = matrices.num_witness_variables; + let m = num_constraints.next_power_of_two(); + 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 = matrices.a.into_iter(); - let mut b = matrices.b.into_iter(); - let mut c = matrices.c.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 e03ceff..5d5bb0d 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -29,9 +29,9 @@ pub struct AccWitnessSerializer< F: Field + PrimeField, MT: Config + From<[u8; 32]>>, > { - pub td: Vec, pub f: Vec, pub w: Vec, + _mt: std::marker::PhantomData, } impl + From<[u8; 32]>>> @@ -41,13 +41,10 @@ impl + Fr 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(); - assert_eq!(f.len(), acc_witness.0[0].leaf_nodes.len()); - let w = acc_witness.2[0].clone(); Self { - td: acc_witness.0[0].clone().leaf_nodes, - f, - w, + f: acc_witness.1[0].clone(), + w: acc_witness.2[0].clone(), + _mt: std::marker::PhantomData, } } } From 6b753e5591f75d14b64934df265d2b0e2d5951de Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:18:22 +0100 Subject: [PATCH 02/21] chkpt --- src/ior.rs | 61 ++++ src/lib.rs | 581 +++++++++++++++++++--------------- src/protocol/domainsep/mod.rs | 32 +- src/protocol/mod.rs | 1 + src/protocol/query.rs | 182 +++++++++++ src/serialize.rs | 79 ++--- src/traits.rs | 32 +- src/types.rs | 106 +++++++ src/utils/mod.rs | 24 -- 9 files changed, 733 insertions(+), 365 deletions(-) create mode 100644 src/ior.rs create mode 100644 src/protocol/query.rs create mode 100644 src/types.rs diff --git a/src/ior.rs b/src/ior.rs new file mode 100644 index 0000000..4654fe2 --- /dev/null +++ b/src/ior.rs @@ -0,0 +1,61 @@ +use ark_ff::Field; +use spongefish::{ProverState, VerifierState}; + +use crate::error::{DeciderError, ProverError, VerifierError}; + +/// An interactive oracle reduction transforms a claim on an input relation +/// into a claim on an (ideally simpler) output relation, +/// through prover/verifier interaction. +/// +/// This trait captures the general pattern described in the WARP paper: +/// InputRelation --IOR--> OutputRelation +/// +/// The prover produces a proof alongside the output claim. +/// The verifier, given the input instance and proof, derives the output instance. +pub trait InteractiveOracleReduction { + /// The relation being reduced FROM (input claim). + type InputInstance; + type InputWitness; + + /// The relation being reduced TO (output claim). + type OutputInstance; + type OutputWitness; + + /// Proof messages produced during this reduction step. + type Proof; + + /// Parameters needed for this reduction (e.g., index, code, Merkle params). + type Parameters; + + /// Prover side of the reduction: given an input claim (instance + witness), + /// interact with the transcript and produce an output claim + proof. + fn reduce_prove( + params: &Self::Parameters, + prover_state: &mut ProverState, + input_instance: &Self::InputInstance, + input_witness: &Self::InputWitness, + ) -> Result<(Self::OutputInstance, Self::OutputWitness, Self::Proof), ProverError>; + + /// Verifier side of the reduction: given an input instance + proof, + /// interact with the transcript and derive the output instance. + fn reduce_verify<'a>( + params: &Self::Parameters, + verifier_state: &mut VerifierState<'a>, + input_instance: &Self::InputInstance, + proof: &Self::Proof, + ) -> Result; +} + +/// A relation that can be checked directly without further reduction. +/// This is the "base case" — the decider checks the final accumulator. +pub trait Decidable { + type Instance; + type Witness; + type Parameters; + + fn decide( + params: &Self::Parameters, + instance: &Self::Instance, + witness: &Self::Witness, + ) -> Result<(), DeciderError>; +} diff --git a/src/lib.rs b/src/lib.rs index c05cc00..8797c2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #![allow(clippy::assign_op_pattern)] // generated by SmallFpConfig derive macro use crate::error::WARPError; use crate::traits::AccumulationScheme; +use crate::types::{AccumulatorInstance, AccumulatorWitness, PesatOutput, WARPParams, WARPProof}; use ark_codes::traits::LinearCode; use ark_crypto_primitives::{ crh::{CRHScheme, TwoToOneCRHScheme}, @@ -23,11 +24,10 @@ use efficient_sumcheck::{ order_strategy::AscendingOrder, }; use protocol::domainsep::parse_statement; +use protocol::query::QueryIndices; use relations::{r1cs::R1CSConstraints, BundledPESAT}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; use std::marker::PhantomData; -use utils::binary_field_elements_to_usize; -use utils::byte_to_binary_field_array; use utils::scale_and_sum; use utils::{ concat_slices, @@ -45,6 +45,7 @@ pub mod protocol; pub mod relations; pub mod serialize; pub mod traits; +pub mod types; pub mod utils; use ark_crypto_primitives::Error; @@ -139,12 +140,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< @@ -162,12 +158,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, + }, } } } @@ -177,106 +175,29 @@ impl< P: Clone + BundledPESAT, Config = (usize, usize, usize)>, // m, n, k C: LinearCode + Clone, MT: Config + From<[u8; 32]>>, - > AccumulationScheme for WARP + > WARP { - type Index = P; - type ProverKey = (P, usize, usize, usize); - type VerifierKey = (usize, usize, usize); - 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, - ) -> spongefish::VerificationResult<(Self::ProverKey, Self::VerifierKey)> { - let (m, n, k) = index.config(); - // initialize prover state for fs - // TODO for R1CS - prover_state.public_message(&index.description()); - 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))) - } - - fn prove( + /// Phase 2: PESAT Reduction + /// + /// Encodes fresh witnesses into codewords, commits via Merkle tree, + /// absorbs the commitment and code evaluations, and derives τ challenges. + fn pesat_reduce( &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, - > { - debug_assert!(instances.len() > 1); - debug_assert_eq!(witnesses.len(), instances.len()); - debug_assert_eq!(acc_witnesses.0.len(), acc_instances.0.len()); - - let (l1, l) = (self.config.l1, self.config.l); - let l2 = l - l1; - debug_assert_eq!(l1 + l2, l); - - debug_assert!(l.is_power_of_two()); - - //////////////////////// - // 1. Parsing phase - //////////////////////// - // a. index - #[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); - - 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; - + witnesses: &[Vec], + l1: usize, + #[allow(non_snake_case)] log_M: usize, + ) -> Result, ProverError> { // a. encode witnesses - let (codewords, leaves) = build_codeword_leaves(&self.code, &witnesses, l1); + let (codewords, leaves) = build_codeword_leaves(&self.params.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, + &self.params.mt_leaf_hash_params, + &self.params.mt_two_to_one_hash_params, leaves.chunks_exact(l1).collect::>(), )?; @@ -294,12 +215,51 @@ impl< .map(|_| prover_state.verifier_messages_vec::(log_M)) .collect::>(); - //////////////////////// - // 3. Constrained Code Accumulation - //////////////////////// + Ok(PesatOutput { + codewords, + td_0, + mus, + taus, + }) + } + + /// Phase 3: Constrained Code Accumulation + /// + /// Combines PESAT output with accumulated state, runs: + /// - Twin constraint sumcheck + /// - OOD sampling + /// - Batching sumcheck + /// - Authentication path computation + /// + /// Returns the new accumulator and proof. + #[allow(clippy::too_many_arguments)] + fn constrained_code_accumulate( + &self, + prover_state: &mut ProverState, + pesat: PesatOutput, + instances: &[Vec], + witnesses: &[Vec], + acc_instance: AccumulatorInstance, + acc_witness: AccumulatorWitness, + #[allow(non_snake_case)] M: usize, + #[allow(non_snake_case)] N: usize, + k: usize, + log_l: usize, + ) -> Result< + ( + (AccumulatorInstance, AccumulatorWitness), + WARPProof, + ), + ProverError, + > { + let n = self.params.code.code_len(); + let log_n = log2(n) as usize; + #[allow(non_snake_case)] + let log_M = log2(M) as usize; + let l1 = pesat.codewords.len(); + // a. zero check randomness let omega: F = prover_state.verifier_message(); - let tau = prover_state.verifier_messages_vec::(log_l); // b. define [...] @@ -308,30 +268,30 @@ impl< .map(|(index, _point)| eq_poly(&tau, index)) .collect::>(); - let alpha_vecs = concat_slices(&acc_instances.1, &vec![vec![F::zero(); log_n]; l1]); + let alpha_vecs = concat_slices(&acc_instance.alpha, &vec![vec![F::zero(); log_n]; l1]); // build the z (x, w) vectors - let z_vecs: Vec> = acc_instances - .3 - .1 + let z_vecs: Vec> = acc_instance + .beta + .1 .iter() - .zip(&acc_witnesses.2) - .chain(instances.iter().zip(&witnesses)) + .zip(&acc_witness.w) + .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(); + let beta_vecs: Vec> = acc_instance.beta.0.into_iter().chain(pesat.taus).collect(); // Twin Constraint sumcheck let mut tablewise = [ - concat_slices(&acc_witnesses.1, &codewords), // u - z_vecs, // z - alpha_vecs, // a - beta_vecs, // b + concat_slices(&acc_witness.f, &pesat.codewords), // u + z_vecs, // z + alpha_vecs, // a + beta_vecs, // b ]; let mut pw = [tau_eq_evals]; // tau - let r1cs = self.p.constraints(); + let r1cs = self.params.p.constraints(); let expected_num_coeffs = 2 + (log_n + 1).max(log_M + 2); let sc = coefficient_sumcheck( |tw, pw| twin_constraint_round_poly(tw, pw, r1cs, omega, expected_num_coeffs), @@ -355,6 +315,7 @@ impl< let beta_eq_evals = (0..M).map(|i| eq_poly(&beta_tau, i)).collect::>(); let eta = self + .params .p .evaluate_bundled(&beta_eq_evals, &z) .map_err(|_| ProverError::SpongeFish)?; @@ -366,8 +327,8 @@ impl< // f. new commitment let td = MerkleTree::::new( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, + &self.params.mt_leaf_hash_params, + &self.params.mt_two_to_one_hash_params, f.chunks(1).collect::>(), )?; @@ -382,7 +343,7 @@ impl< prover_state.prover_message(&nu_0); // h. ood samples - let n_ood_samples = self.config.s * log_n; + let n_ood_samples = self.params.config.s * log_n; let ood_samples = prover_state.verifier_messages_vec::(n_ood_samples); let ood_samples = ood_samples.chunks(log_n).collect::>(); @@ -401,35 +362,19 @@ impl< nus.extend(ood_answers); // k. shift queries and zerocheck randomness - let r = 1 + self.config.s + self.config.t; + let r = 1 + self.params.config.s + self.params.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 queries = QueryIndices::sample(prover_state, log_n, self.params.config.t); 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); + zetas.extend(queries.evaluation_points.iter().map(|v| v.as_slice())); // l. sumcheck polynomials // compute evaluations for xi let xi_eq_evals = (0..r).map(|i| eq_poly(&xis, i)).collect::>(); - let ood_evals_vec = (0..1 + self.config.s) + let ood_evals_vec = (0..1 + self.params.config.s) .map(|i| { (0..n) .map(|a| eq_poly(zetas[i], a) * xi_eq_evals[i]) @@ -439,7 +384,7 @@ impl< // [CBBZ23] optimization from hyperplonk let id_non_0_eval_sums = - accumulate_sparse_evaluations(zetas, xi_eq_evals, self.config.s, r); + accumulate_sparse_evaluations(zetas, xi_eq_evals, self.params.config.s, r); // call efficient sumcheck for batched_constraint checks let alpha = inner_product_sumcheck( @@ -453,53 +398,148 @@ impl< let mu = f_hat.fix_variables(&alpha)[0]; // n. compute authentication paths - let auth_0 = compute_auth_paths(&td_0, &shift_queries_indexes)?; + let auth_0 = compute_auth_paths(&pesat.td_0, &queries.leaf_positions)?; - let auth = acc_witnesses - .0 // for each accumulated witness and for each + let auth = acc_witness + .td // for each accumulated witness and for each .iter() - .map(|td| compute_auth_paths(td, &shift_queries_indexes)) + .map(|td| compute_auth_paths(td, &queries.leaf_positions)) .collect::>>, Error>>()?; - let all_codewords = acc_witnesses - .1 + let all_codewords = acc_witness + .f .into_iter() - .chain(codewords) + .chain(pesat.codewords) .collect::>(); 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() { + vec![vec![F::default(); all_codewords.len()]; queries.leaf_positions.len()]; + for (i, idx) in queries.leaf_positions.iter().enumerate() { let answers = all_codewords.iter().map(|f| f[*idx]).collect::>(); shift_queries_answers[i] = answers; } - let acc_instance = (vec![td.root()], vec![alpha], vec![mu], beta, vec![eta]); - let acc_witness = (vec![td], vec![f], vec![w.to_vec()]); - - // 4. return - Ok(( - (acc_instance, acc_witness), - ( - td_0.root(), - mus, - nu_0, - nus, - auth_0, - auth, - shift_queries_answers, - ), - )) + let new_acc_instance = AccumulatorInstance { + rt: vec![td.root()], + alpha: vec![alpha], + mu: vec![mu], + beta, + eta: vec![eta], + }; + let new_acc_witness = AccumulatorWitness { + td: vec![td], + f: vec![f], + w: vec![w.to_vec()], + }; + + let proof = WARPProof { + rt_0: pesat.td_0.root(), + mu_i: pesat.mus, + nu_0, + nu_i: nus, + auth_0, + auth_j: auth, + shift_query_answers: shift_queries_answers, + }; + + Ok(((new_acc_instance, new_acc_witness), proof)) + } +} + +impl< + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + P: Clone + BundledPESAT, Config = (usize, usize, usize)>, // m, n, k + C: LinearCode + Clone, + MT: Config + From<[u8; 32]>>, + > AccumulationScheme for WARP +{ + type Index = P; + type ProverKey = (P, usize, usize, usize); + type VerifierKey = (usize, usize, usize); + type Instances = Vec>; + type Witnesses = Vec>; + + fn index( + prover_state: &mut ProverState, + index: Self::Index, + ) -> spongefish::VerificationResult<(Self::ProverKey, Self::VerifierKey)> { + let (m, n, k) = index.config(); + // initialize prover state for fs + // TODO for R1CS + prover_state.public_message(&index.description()); + 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, m, n, k), (m, n, k))) + } + + fn prove( + &self, + pk: Self::ProverKey, + prover_state: &mut ProverState, + witnesses: Self::Witnesses, + instances: Self::Instances, + acc_instance: AccumulatorInstance, + acc_witness: AccumulatorWitness, + ) -> Result< + ( + (AccumulatorInstance, AccumulatorWitness), + WARPProof, + ), + ProverError, + > { + debug_assert!(instances.len() > 1); + debug_assert_eq!(witnesses.len(), instances.len()); + debug_assert_eq!(acc_witness.td.len(), acc_instance.rt.len()); + + 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 + //////////////////////// + #[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); + + debug_assert_eq!(instances[0].len(), N - k); + + absorb_instances(prover_state, &instances); + absorb_accumulated_instances::(prover_state, &acc_instance); + + //////////////////////// + // 2. PESAT Reduction + //////////////////////// + let pesat = self.pesat_reduce(prover_state, &witnesses, l1, log_M)?; + + //////////////////////// + // 3. Constrained Code Accumulation + //////////////////////// + self.constrained_code_accumulate( + prover_state, + pesat, + &instances, + &witnesses, + acc_instance, + acc_witness, + M, + N, + k, + log_l, + ) } 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; //////////////////////// @@ -511,12 +551,20 @@ impl< #[allow(non_snake_case)] let (log_M, log_l) = (log2(M) as usize, log2(l) as usize); - let n = self.code.code_len(); + let n = self.params.code.code_len(); let log_n = log2(n) as usize; // f. absorb parameters - 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)?; + let ( + l1_xs, + AccumulatorInstance { + rt: l2_roots, + alpha: l2_alphas, + mu: l2_mus, + beta: (l2_taus, l2_xs), + eta: l2_etas, + }, + ) = parse_statement::(verifier_state, l1, l2, N - k, log_n, log_M)?; //////////////////////// // 2. Derive randomness @@ -542,12 +590,12 @@ impl< l1, log_n, log_l, - self.config.s, - self.config.t, + self.params.config.s, + self.params.config.t, log_M, )?; - let r = 1 + self.config.s + self.config.t; + let r = 1 + self.params.config.s + self.params.config.t; let log_r = log2(r) as usize; //////////////////////// @@ -561,8 +609,8 @@ impl< let zeta_0 = scale_and_sum(&alpha_vecs, &gamma_eq_evals); // compute \eta_{s + k} - let mut nu_s_t = vec![F::default(); self.config.t]; - for (i, v_jk) in proof.6.iter().enumerate() { + let mut nu_s_t = vec![F::default(); self.params.config.t]; + for (i, v_jk) in proof.shift_query_answers.iter().enumerate() { let res = v_jk .iter() .zip(&gamma_eq_evals) @@ -596,7 +644,7 @@ impl< // 4. Decision phase //////////////////////// // a. new code evaluation point - (acc_instance.1[0] == alpha_sumcheck).ok_or_err(VerifierError::CodeEvaluationPoint)?; + (acc_instance.alpha[0] == alpha_sumcheck).ok_or_err(VerifierError::CodeEvaluationPoint)?; // b. new circuit evaluation point let betas = l2_taus @@ -606,55 +654,46 @@ impl< .map(|(tau, x)| concat_slices(&tau, &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_instance.beta.0[0], &acc_instance.beta.1[0]); (expected_beta == beta).ok_or_err(VerifierError::CircuitEvaluationPoint)?; // c. check auth paths - 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::>(); - - let shift_queries_indexes: Vec = binary_shift_queries - .iter() - .map(|vals| binary_field_elements_to_usize(vals)) - .collect(); + let queries: QueryIndices = + QueryIndices::from_squeezed_bytes(&bytes_shift_queries, log_n, self.params.config.t); // check: // that the leaf index corresponds to the shift query // that the path is correct - (proof.6.len() == self.config.t).ok_or_err(VerifierError::NumShiftQueries)?; + (proof.shift_query_answers.len() == self.params.config.t) + .ok_or_err(VerifierError::NumShiftQueries)?; - // proof.4 is auth_0 - for (i, path) in proof.4.iter().enumerate() { - (path.leaf_index == shift_queries_indexes[i]) + // proof.auth_0 + for (i, path) in proof.auth_0.iter().enumerate() { + (path.leaf_index == queries.leaf_positions[i]) .ok_or_err(VerifierError::ShiftQueryIndex)?; let is_valid = path.verify( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, + &self.params.mt_leaf_hash_params, + &self.params.mt_two_to_one_hash_params, &rt_0, - &proof.6[i][l2..], // leaves are evaluations of the l1 codewords + &proof.shift_query_answers[i][l2..], // leaves are evaluations of the l1 codewords )?; is_valid.ok_or_err(VerifierError::ShiftQuery)? } - // proof.5 holds merkle proofs for l2 accumulated instances - (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)?; + // proof.auth_j holds merkle proofs for l2 accumulated instances + (proof.auth_j.len() == l2).ok_or_err(VerifierError::NumL2Instances)?; + for (i, paths) in proof.auth_j.iter().enumerate() { + (paths.len() == self.params.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]) + (path.leaf_index == queries.leaf_positions[j]) .ok_or_err(VerifierError::ShiftQueryIndex)?; let is_valid = path.verify( - &self.mt_leaf_hash_params, - &self.mt_two_to_one_hash_params, + &self.params.mt_leaf_hash_params, + &self.params.mt_two_to_one_hash_params, root, - [proof.6[j][i]], // proof.6[j][i] holds f_i(x_j) + [proof.shift_query_answers[j][i]], // shift_query_answers[j][i] holds f_i(x_j) )?; is_valid.ok_or_err(VerifierError::ShiftQuery)? @@ -699,7 +738,8 @@ impl< .collect::>(), ); zeta_eqs.extend( - binary_shift_queries + queries + .evaluation_points .iter() .map(|zeta| eq_poly_non_binary(zeta, &alpha_sumcheck)) .collect::>(), @@ -707,7 +747,7 @@ impl< (zeta_eqs.len() == r).ok_or_err(VerifierError::NumShiftQueries)?; // mul by \mu and compare to target_2 - (acc_instance.2[0] + (acc_instance.mu[0] * zeta_eqs .into_iter() .zip(xi_eq_evals) @@ -720,39 +760,41 @@ 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], + log2(self.params.code.code_len()) as usize, + &acc_witness.f[0], ); - (f_hat.evaluate(&alpha[0]) == mu[0]).ok_or_err(DeciderError::MLExtensionEvaluation)?; + (f_hat.evaluate(&acc_instance.alpha[0]) == acc_instance.mu[0]) + .ok_or_err(DeciderError::MLExtensionEvaluation)?; - let tau = &beta.0[0]; + let tau = &acc_instance.beta.0[0]; let tau_zero_evader = Hypercube::::new(tau.len()) .map(|(index, _point)| eq_poly(tau, index)) .collect::>(); - 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 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 computed_f = self.code.encode(&w[0]); - (f[0] == computed_f).ok_or_err(DeciderError::EncodedWitness)?; + let computed_f = self.params.code.encode(&acc_witness.w[0]); + (acc_witness.f[0] == computed_f).ok_or_err(DeciderError::EncodedWitness)?; Ok(()) } @@ -763,6 +805,7 @@ pub mod test { use super::crypto::merkle::blake3::Blake3MerkleTreeParams; use super::AccumulationScheme; use crate::serialize::{AccInstanceSerializer, AccWitnessSerializer, ProofSerializer}; + use crate::types::{AccumulatorInstance, AccumulatorWitness}; use crate::{ relations::{ r1cs::{ @@ -859,20 +902,20 @@ pub mod test { &mut prover_state, instances_witnesses.1.clone(), instances_witnesses.0.clone(), - (vec![], vec![], vec![], (vec![], vec![]), vec![]), - (vec![], vec![], vec![]), + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), ) .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()); + 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"); @@ -895,8 +938,18 @@ pub mod test { &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), + 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(); @@ -1007,20 +1060,20 @@ pub mod test { &mut prover_state, instances_witnesses.1.clone(), instances_witnesses.0.clone(), - (vec![], vec![], vec![], (vec![], vec![]), vec![]), - (vec![], vec![], vec![]), + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), ) .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()); + 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"); @@ -1044,8 +1097,18 @@ pub mod test { &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), + 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(); diff --git a/src/protocol/domainsep/mod.rs b/src/protocol/domainsep/mod.rs index 4bd0d1f..a3dd928 100644 --- a/src/protocol/domainsep/mod.rs +++ b/src/protocol/domainsep/mod.rs @@ -6,13 +6,7 @@ use spongefish::{ Decoding, Encoding, NargDeserialize, ProverState, VerificationResult, VerifierState, }; -pub type AccInstances = ( - Vec<::InnerDigest>, // rt - Vec>, // alpha - Vec, // mu - (Vec>, Vec>), // (tau, x) - Vec, // eta -); +use crate::types::AccumulatorInstance; pub fn absorb_instances>( prover_state: &mut ProverState, @@ -30,47 +24,47 @@ pub fn absorb_accumulated_instances< MT: Config + From<[u8; 32]>>, >( prover_state: &mut ProverState, - acc_instances: &AccInstances, + acc_instance: &AccumulatorInstance, ) { // digests (rt) - for digest in &acc_instances.0 { + for digest in &acc_instance.rt { 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 alpha in &acc_instance.alpha { for f in alpha { prover_state.prover_message(f); } } // mu - for f in &acc_instances.2 { + for f in &acc_instance.mu { prover_state.prover_message(f); } // taus - for tau in &acc_instances.3 .0 { + for tau in &acc_instance.beta.0 { for f in tau { prover_state.prover_message(f); } } // xs - for x in &acc_instances.3 .1 { + for x in &acc_instance.beta.1 { for f in x { prover_state.prover_message(f); } } // etas - for f in &acc_instances.4 { + for f in &acc_instance.eta { prover_state.prover_message(f); } } -pub type ParsedStatement = (Vec>, AccInstances); +pub type ParsedStatement = (Vec>, AccumulatorInstance); pub fn parse_statement< F: Field + NargDeserialize + Encoding<[u8]> + Decoding<[u8]>, @@ -121,7 +115,13 @@ pub fn parse_statement< Ok(( l1_xs, - (l2_roots, l2_alphas, l2_mus, (l2_taus, l2_xs), l2_etas), + AccumulatorInstance { + rt: l2_roots, + alpha: l2_alphas, + mu: l2_mus, + beta: (l2_taus, l2_xs), + eta: l2_etas, + }, )) } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 6aa899d..d7fa0fa 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1 +1,2 @@ pub mod domainsep; +pub mod query; diff --git a/src/protocol/query.rs b/src/protocol/query.rs new file mode 100644 index 0000000..c727d93 --- /dev/null +++ b/src/protocol/query.rs @@ -0,0 +1,182 @@ +use ark_ff::Field; +use spongefish::ProverState; + +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 ark_bls12_381::Fr as BLS12_381; + use ark_std::{One, Zero}; + use crate::utils::fields::Goldilocks; + + // 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/serialize.rs b/src/serialize.rs index 5d5bb0d..84d159a 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -1,28 +1,8 @@ -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< @@ -37,13 +17,13 @@ pub struct AccWitnessSerializer< 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); + 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 { - f: acc_witness.1[0].clone(), - w: acc_witness.2[0].clone(), + f: acc_witness.f.into_iter().next().unwrap(), + w: acc_witness.w.into_iter().next().unwrap(), _mt: std::marker::PhantomData, } } @@ -64,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], } } } @@ -99,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..e5b3bd9 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -3,24 +3,14 @@ use ark_ff::Field; use spongefish::{ProverState, VerificationResult, VerifierState}; use crate::error::{ProverError, VerifierError, WARPError}; - -pub type WARPAccumResult = ( - ( - >::AccumulatorInstances, - >::AccumulatorWitnesses, - ), - >::Proof, -); +use crate::types::{AccumulatorInstance, AccumulatorWitness, 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,27 @@ 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, + ) -> Result< + ( + (AccumulatorInstance, AccumulatorWitness), + WARPProof, + ), + ProverError, + >; 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..6434bb6 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,106 @@ +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::relations::BundledPESAT; + +/// 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(crate) 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 eb85d34..644cb1b 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -12,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); From 6f836e1f0fce8c2912e1b4ae859a2ee9852b4e7a Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:30:43 +0100 Subject: [PATCH 03/21] clippy --- src/ior.rs | 61 ------- src/lib.rs | 49 +++--- src/protocol/domainsep/mod.rs | 247 ---------------------------- src/protocol/mod.rs | 2 +- src/protocol/query.rs | 5 +- src/protocol/transcript/mod.rs | 5 + src/protocol/transcript/prover.rs | 57 +++++++ src/protocol/transcript/verifier.rs | 199 ++++++++++++++++++++++ src/traits.rs | 12 +- src/types.rs | 10 ++ 10 files changed, 299 insertions(+), 348 deletions(-) delete mode 100644 src/ior.rs delete mode 100644 src/protocol/domainsep/mod.rs create mode 100644 src/protocol/transcript/mod.rs create mode 100644 src/protocol/transcript/prover.rs create mode 100644 src/protocol/transcript/verifier.rs diff --git a/src/ior.rs b/src/ior.rs deleted file mode 100644 index 4654fe2..0000000 --- a/src/ior.rs +++ /dev/null @@ -1,61 +0,0 @@ -use ark_ff::Field; -use spongefish::{ProverState, VerifierState}; - -use crate::error::{DeciderError, ProverError, VerifierError}; - -/// An interactive oracle reduction transforms a claim on an input relation -/// into a claim on an (ideally simpler) output relation, -/// through prover/verifier interaction. -/// -/// This trait captures the general pattern described in the WARP paper: -/// InputRelation --IOR--> OutputRelation -/// -/// The prover produces a proof alongside the output claim. -/// The verifier, given the input instance and proof, derives the output instance. -pub trait InteractiveOracleReduction { - /// The relation being reduced FROM (input claim). - type InputInstance; - type InputWitness; - - /// The relation being reduced TO (output claim). - type OutputInstance; - type OutputWitness; - - /// Proof messages produced during this reduction step. - type Proof; - - /// Parameters needed for this reduction (e.g., index, code, Merkle params). - type Parameters; - - /// Prover side of the reduction: given an input claim (instance + witness), - /// interact with the transcript and produce an output claim + proof. - fn reduce_prove( - params: &Self::Parameters, - prover_state: &mut ProverState, - input_instance: &Self::InputInstance, - input_witness: &Self::InputWitness, - ) -> Result<(Self::OutputInstance, Self::OutputWitness, Self::Proof), ProverError>; - - /// Verifier side of the reduction: given an input instance + proof, - /// interact with the transcript and derive the output instance. - fn reduce_verify<'a>( - params: &Self::Parameters, - verifier_state: &mut VerifierState<'a>, - input_instance: &Self::InputInstance, - proof: &Self::Proof, - ) -> Result; -} - -/// A relation that can be checked directly without further reduction. -/// This is the "base case" — the decider checks the final accumulator. -pub trait Decidable { - type Instance; - type Witness; - type Parameters; - - fn decide( - params: &Self::Parameters, - instance: &Self::Instance, - witness: &Self::Witness, - ) -> Result<(), DeciderError>; -} diff --git a/src/lib.rs b/src/lib.rs index 8797c2b..ade683d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,9 @@ #![allow(clippy::assign_op_pattern)] // generated by SmallFpConfig derive macro use crate::error::WARPError; use crate::traits::AccumulationScheme; -use crate::types::{AccumulatorInstance, AccumulatorWitness, PesatOutput, WARPParams, WARPProof}; +use crate::types::{ + AccumulatorInstance, AccumulatorWitness, PesatOutput, ProveResult, WARPParams, WARPProof, +}; use ark_codes::traits::LinearCode; use ark_crypto_primitives::{ crh::{CRHScheme, TwoToOneCRHScheme}, @@ -13,6 +15,7 @@ use ark_poly::{ MultilinearExtension, Polynomial, }; use ark_std::log2; +use config::WARPConfig; use crypto::merkle::build_codeword_leaves; use crypto::merkle::compute_auth_paths; use efficient_sumcheck::{ @@ -23,8 +26,10 @@ use efficient_sumcheck::{ inner_product_sumcheck, order_strategy::AscendingOrder, }; -use protocol::domainsep::parse_statement; use protocol::query::QueryIndices; +use protocol::transcript::{ + absorb_instances, derive_randomness, parse_statement, DerivedRandomness, +}; use relations::{r1cs::R1CSConstraints, BundledPESAT}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; use std::marker::PhantomData; @@ -34,9 +39,6 @@ use utils::{ poly::{eq_poly, eq_poly_non_binary}, }; -use config::WARPConfig; -use protocol::domainsep::{absorb_accumulated_instances, absorb_instances, derive_randomness}; - pub mod config; pub mod constraints; pub mod crypto; @@ -186,7 +188,7 @@ impl< prover_state: &mut ProverState, witnesses: &[Vec], l1: usize, - #[allow(non_snake_case)] log_M: usize, + log_m: usize, ) -> Result, ProverError> { // a. encode witnesses let (codewords, leaves) = build_codeword_leaves(&self.params.code, witnesses, l1); @@ -212,7 +214,7 @@ impl< // e. zero check randomness and f. bundled evaluations let taus = (0..l1) - .map(|_| prover_state.verifier_messages_vec::(log_M)) + .map(|_| prover_state.verifier_messages_vec::(log_m)) .collect::>(); Ok(PesatOutput { @@ -245,17 +247,10 @@ impl< #[allow(non_snake_case)] N: usize, k: usize, log_l: usize, - ) -> Result< - ( - (AccumulatorInstance, AccumulatorWitness), - WARPProof, - ), - ProverError, - > { + ) -> ProveResult { let n = self.params.code.code_len(); let log_n = log2(n) as usize; - #[allow(non_snake_case)] - let log_M = log2(M) as usize; + let log_m = log2(M) as usize; let l1 = pesat.codewords.len(); // a. zero check randomness @@ -292,7 +287,7 @@ impl< let mut pw = [tau_eq_evals]; // tau let r1cs = self.params.p.constraints(); - let expected_num_coeffs = 2 + (log_n + 1).max(log_M + 2); + let expected_num_coeffs = 2 + (log_n + 1).max(log_m + 2); let sc = coefficient_sumcheck( |tw, pw| twin_constraint_round_poly(tw, pw, r1cs, omega, expected_num_coeffs), &mut tablewise, @@ -502,18 +497,17 @@ impl< //////////////////////// #[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); debug_assert_eq!(instances[0].len(), N - k); absorb_instances(prover_state, &instances); - absorb_accumulated_instances::(prover_state, &acc_instance); + acc_instance.absorb_into(prover_state); //////////////////////// // 2. PESAT Reduction //////////////////////// - let pesat = self.pesat_reduce(prover_state, &witnesses, l1, log_M)?; + let pesat = self.pesat_reduce(prover_state, &witnesses, l1, log_m)?; //////////////////////// // 3. Constrained Code Accumulation @@ -548,8 +542,7 @@ impl< // a. verification key #[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 (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; @@ -564,12 +557,12 @@ impl< beta: (l2_taus, l2_xs), eta: l2_etas, }, - ) = parse_statement::(verifier_state, l1, l2, N - k, log_n, log_M)?; + ) = parse_statement::(verifier_state, l1, l2, N - k, log_n, log_m)?; //////////////////////// // 2. Derive randomness //////////////////////// - let ( + let DerivedRandomness { rt_0, l1_mus, l1_taus, @@ -577,7 +570,7 @@ impl< tau, gamma_sumcheck, coeffs_twinc_sumcheck, - _rt, + td: _, eta, mut nus, ood_samples, @@ -585,14 +578,14 @@ impl< xi, alpha_sumcheck, sums_batching_sumcheck, - ) = derive_randomness::( + } = derive_randomness::( verifier_state, l1, log_n, log_l, self.params.config.s, self.params.config.t, - log_M, + log_m, )?; let r = 1 + self.params.config.s + self.params.config.t; diff --git a/src/protocol/domainsep/mod.rs b/src/protocol/domainsep/mod.rs deleted file mode 100644 index a3dd928..0000000 --- a/src/protocol/domainsep/mod.rs +++ /dev/null @@ -1,247 +0,0 @@ -use ark_crypto_primitives::merkle_tree::Config; -use ark_ff::Field; -use ark_std::log2; - -use spongefish::{ - Decoding, Encoding, NargDeserialize, ProverState, VerificationResult, VerifierState, -}; - -use crate::types::AccumulatorInstance; - -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_instance: &AccumulatorInstance, -) { - // digests (rt) - for digest in &acc_instance.rt { - 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_instance.alpha { - for f in alpha { - prover_state.prover_message(f); - } - } - - // mu - for f in &acc_instance.mu { - prover_state.prover_message(f); - } - - // taus - for tau in &acc_instance.beta.0 { - for f in tau { - prover_state.prover_message(f); - } - } - - // xs - for x in &acc_instance.beta.1 { - for f in x { - prover_state.prover_message(f); - } - } - - // etas - for f in &acc_instance.eta { - prover_state.prover_message(f); - } -} - -pub type ParsedStatement = (Vec>, AccumulatorInstance); - -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, - AccumulatorInstance { - rt: l2_roots, - alpha: l2_alphas, - mu: l2_mus, - beta: (l2_taus, l2_xs), - eta: l2_etas, - }, - )) -} - -pub type DerivedRandomness = ( - ::InnerDigest, - Vec, - Vec>, - F, - Vec, - Vec, - Vec>, - ::InnerDigest, - F, - Vec, - Vec, - Vec, - Vec, - Vec, - Vec<[F; 3]>, -); - -pub fn derive_randomness< - F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, - MT: Config + From<[u8; 32]>>, ->( - verifier_state: &mut VerifierState<'_>, - l1: usize, - log_n: usize, - log_l: usize, - s: usize, - t: usize, - #[allow(non_snake_case)] log_M: usize, -) -> VerificationResult> { - // read commitment digest - let rt_0_bytes: [u8; 32] = verifier_state.prover_message()?; - let rt_0: MT::InnerDigest = rt_0_bytes.into(); - - // read mus - let l1_mus: Vec = verifier_state.prover_messages_vec(l1)?; - - // challenge taus - let mut l1_taus = Vec::with_capacity(l1); - for _ in 0..l1 { - let tau: Vec = (0..log_M) - .map(|_| verifier_state.verifier_message::()) - .collect(); - l1_taus.push(tau); - } - - let omega: F = verifier_state.verifier_message(); - let tau: Vec = (0..log_l) - .map(|_| verifier_state.verifier_message::()) - .collect(); - - // e. twin constraints sumcheck - let mut gamma_sumcheck = Vec::new(); - let mut coeffs_twinc_sumcheck = Vec::new(); - for _ in 0..log_l { - let h_coeffs: Vec = - verifier_state.prover_messages_vec(2 + (log_n + 1).max(log_M + 2))?; - let c: F = verifier_state.verifier_message(); - gamma_sumcheck.push(c); - coeffs_twinc_sumcheck.push(h_coeffs); - } - - // read td digest - let td_bytes: [u8; 32] = verifier_state.prover_message()?; - let _td: MT::InnerDigest = td_bytes.into(); - - // read 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]; - - // g. ood samples - let n_ood_samples = s * log_n; - let ood_samples: Vec = (0..n_ood_samples) - .map(|_| verifier_state.verifier_message::()) - .collect(); - - // h. ood answers - let ood_answers: Vec = verifier_state.prover_messages_vec(s)?; - nus.extend(ood_answers); - - // i. shift queries and zero check - 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(); - - // j. batching sumcheck - let mut alpha_sumcheck = Vec::new(); - let mut sums_batching_sumcheck = Vec::new(); - for _ in 0..log_n { - let sums: [F; 3] = verifier_state.prover_messages()?; - let c: F = verifier_state.verifier_message(); - alpha_sumcheck.push(c); - sums_batching_sumcheck.push(sums); - } - - Ok(( - rt_0, - l1_mus, - l1_taus, - omega, - tau, - gamma_sumcheck, - coeffs_twinc_sumcheck, - _td, - eta, - nus, - ood_samples, - bytes_shift_queries, - xi, - alpha_sumcheck, - sums_batching_sumcheck, - )) -} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index d7fa0fa..fc4cd34 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,2 +1,2 @@ -pub mod domainsep; pub mod query; +pub mod transcript; diff --git a/src/protocol/query.rs b/src/protocol/query.rs index c727d93..1921af9 100644 --- a/src/protocol/query.rs +++ b/src/protocol/query.rs @@ -24,7 +24,8 @@ impl QueryIndices { // 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); + 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 { @@ -63,9 +64,9 @@ impl QueryIndices { #[cfg(test)] mod tests { use super::*; + use crate::utils::fields::Goldilocks; use ark_bls12_381::Fr as BLS12_381; use ark_std::{One, Zero}; - use crate::utils::fields::Goldilocks; // check dimensions, binary values, and leaf position range fn check_query_indices(bytes: &[u8], log_n: usize, num_queries: usize) { diff --git a/src/protocol/transcript/mod.rs b/src/protocol/transcript/mod.rs new file mode 100644 index 0000000..2283192 --- /dev/null +++ b/src/protocol/transcript/mod.rs @@ -0,0 +1,5 @@ +pub mod prover; +pub mod verifier; + +pub use prover::*; +pub use verifier::*; 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..21865a9 --- /dev/null +++ b/src/protocol/transcript/verifier.rs @@ -0,0 +1,199 @@ +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, + }) + } +} + +pub struct DerivedRandomness { + pub rt_0: MT::InnerDigest, + pub l1_mus: Vec, + pub l1_taus: Vec>, + pub omega: F, + pub tau: Vec, + pub gamma_sumcheck: Vec, + pub coeffs_twinc_sumcheck: Vec>, + pub td: MT::InnerDigest, + pub eta: F, + pub nus: Vec, + pub ood_samples: Vec, + pub bytes_shift_queries: Vec, + pub xi: Vec, + pub alpha_sumcheck: Vec, + pub sums_batching_sumcheck: Vec<[F; 3]>, +} + +pub fn derive_randomness< + F: Field + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize, + MT: Config + From<[u8; 32]>>, +>( + verifier_state: &mut VerifierState<'_>, + l1: usize, + log_n: usize, + log_l: usize, + s: usize, + t: 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 + 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(); + + // twin constraints sumcheck + let mut gamma_sumcheck = Vec::new(); + let mut coeffs_twinc_sumcheck = Vec::new(); + for _ in 0..log_l { + let h_coeffs: Vec = + verifier_state.prover_messages_vec(2 + (log_n + 1).max(log_m + 2))?; + let c: F = verifier_state.verifier_message(); + gamma_sumcheck.push(c); + coeffs_twinc_sumcheck.push(h_coeffs); + } + + // 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 zero check + 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(); + + // batching sumcheck + let mut alpha_sumcheck = Vec::new(); + let mut sums_batching_sumcheck = Vec::new(); + for _ in 0..log_n { + let sums: [F; 3] = verifier_state.prover_messages()?; + let c: F = verifier_state.verifier_message(); + alpha_sumcheck.push(c); + sums_batching_sumcheck.push(sums); + } + + Ok(DerivedRandomness { + rt_0, + l1_mus, + l1_taus, + omega, + tau, + gamma_sumcheck, + coeffs_twinc_sumcheck, + td, + eta, + nus, + ood_samples, + bytes_shift_queries, + xi, + alpha_sumcheck, + sums_batching_sumcheck, + }) +} diff --git a/src/traits.rs b/src/traits.rs index e5b3bd9..7f0b8f2 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,8 +2,8 @@ use ark_crypto_primitives::merkle_tree::Config; use ark_ff::Field; use spongefish::{ProverState, VerificationResult, VerifierState}; -use crate::error::{ProverError, VerifierError, WARPError}; -use crate::types::{AccumulatorInstance, AccumulatorWitness, WARPProof}; +use crate::error::{VerifierError, WARPError}; +use crate::types::{AccumulatorInstance, AccumulatorWitness, ProveResult, WARPProof}; pub trait AccumulationScheme { type Index; @@ -27,13 +27,7 @@ pub trait AccumulationScheme { instances: Self::Instances, acc_instance: AccumulatorInstance, acc_witness: AccumulatorWitness, - ) -> Result< - ( - (AccumulatorInstance, AccumulatorWitness), - WARPProof, - ), - ProverError, - >; + ) -> ProveResult; fn verify<'a>( &self, diff --git a/src/types.rs b/src/types.rs index 6434bb6..dbb8de7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -7,8 +7,18 @@ 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, From e4920ef3366d8675375da336bc437fcae0c8bd60 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:08:04 +0100 Subject: [PATCH 04/21] fix upstream crypto primitives --- Cargo.toml | 2 +- src/crypto/blake3_crh/fields.rs | 35 --------------- src/crypto/blake3_crh/mod.rs | 70 ----------------------------- src/crypto/merkle/blake3.rs | 26 ----------- src/crypto/merkle/mod.rs | 4 -- src/crypto/merkle/parameters.rs | 76 -------------------------------- src/crypto/merkle/poseidon.rs | 52 ---------------------- src/crypto/mod.rs | 1 - src/lib.rs | 78 ++++++++++++++++----------------- 9 files changed, 38 insertions(+), 306 deletions(-) delete mode 100644 src/crypto/blake3_crh/fields.rs delete mode 100644 src/crypto/blake3_crh/mod.rs delete mode 100644 src/crypto/merkle/blake3.rs delete mode 100644 src/crypto/merkle/parameters.rs delete mode 100644 src/crypto/merkle/poseidon.rs diff --git a/Cargo.toml b/Cargo.toml index 3a3440a..d8a6ecd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ ark-codes = { git = "https://github.com/dmpierre/ark-codes.git" } [patch.crates-io] -ark-crypto-primitives = { git = "https://github.com/benbencik/crypto-primitives.git", branch = "smallfp-absorb-clean" } +ark-crypto-primitives = { git = "https://github.com/arkworks-rs/crypto-primitives.git", branch = "z-tech/smallfp-absorb" } ark-ff = { git = "https://github.com/arkworks-rs/algebra.git" } ark-poly = { git = "https://github.com/arkworks-rs/algebra.git" } ark-serialize = { git = "https://github.com/arkworks-rs/algebra.git" } diff --git a/src/crypto/blake3_crh/fields.rs b/src/crypto/blake3_crh/fields.rs deleted file mode 100644 index 0cc9417..0000000 --- a/src/crypto/blake3_crh/fields.rs +++ /dev/null @@ -1,35 +0,0 @@ -use ark_ff::Field; -use ark_serialize::CanonicalSerialize; -use ark_std::rand::RngCore; -use core::borrow::Borrow; -use core::marker::PhantomData; - -use ark_crypto_primitives::{crh::CRHScheme, Error}; - -use super::GenericDigest; - -/// Blake3 leaf hash that takes field elements as input. -#[derive(Clone)] -pub struct Blake3F { - _f: PhantomData, -} - -impl CRHScheme for Blake3F { - type Input = [F]; - type Output = GenericDigest<32>; - type Parameters = (); - - fn setup(_: &mut R) -> Result { - Ok(()) - } - - fn evaluate>( - (): &Self::Parameters, - input: T, - ) -> Result { - let mut buf = Vec::new(); - input.borrow().serialize_compressed(&mut buf)?; - let output: [_; 32] = blake3::hash(&buf).into(); - Ok(output.into()) - } -} diff --git a/src/crypto/blake3_crh/mod.rs b/src/crypto/blake3_crh/mod.rs deleted file mode 100644 index 404033b..0000000 --- a/src/crypto/blake3_crh/mod.rs +++ /dev/null @@ -1,70 +0,0 @@ -pub mod fields; - -use ark_crypto_primitives::{crh::TwoToOneCRHScheme, sponge::Absorb, Error}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::rand::RngCore; -use core::borrow::Borrow; - -/// A generic fixed-size digest (copied from whir). -#[derive(Clone, Debug, Eq, PartialEq, Hash, CanonicalSerialize, CanonicalDeserialize)] -pub struct GenericDigest(pub [u8; N]); - -impl Default for GenericDigest { - fn default() -> Self { - Self([0; N]) - } -} - -impl AsRef<[u8]> for GenericDigest { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl From<[u8; N]> for GenericDigest { - fn from(value: [u8; N]) -> Self { - Self(value) - } -} - -impl Absorb for GenericDigest { - fn to_sponge_bytes(&self, dest: &mut Vec) { - dest.extend_from_slice(&self.0); - } - - fn to_sponge_field_elements(&self, dest: &mut Vec) { - dest.push(F::from_be_bytes_mod_order(&self.0)); - } -} - -/// Blake3 two-to-one hash for internal Merkle tree nodes. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Blake3; - -impl TwoToOneCRHScheme for Blake3 { - type Input = GenericDigest<32>; - type Output = GenericDigest<32>; - type Parameters = (); - - fn setup(_: &mut R) -> Result { - Ok(()) - } - - fn evaluate>( - (): &Self::Parameters, - left_input: T, - right_input: T, - ) -> Result { - let output: [_; 32] = - blake3::hash(&[left_input.borrow().0, right_input.borrow().0].concat()).into(); - Ok(output.into()) - } - - fn compress>( - parameters: &Self::Parameters, - left_input: T, - right_input: T, - ) -> Result { - Self::evaluate(parameters, left_input, right_input) - } -} diff --git a/src/crypto/merkle/blake3.rs b/src/crypto/merkle/blake3.rs deleted file mode 100644 index e6ca88c..0000000 --- a/src/crypto/merkle/blake3.rs +++ /dev/null @@ -1,26 +0,0 @@ -use super::parameters::MerkleTreeParams; -use crate::crypto::blake3_crh::fields::Blake3F; -use crate::crypto::blake3_crh::{Blake3, GenericDigest}; -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, Blake3, GenericDigest<32>>; - -impl MerkleConfig for Blake3MerkleConfig { - type Leaf = [F]; - type LeafDigest = ::Output; - type LeafInnerDigestConverter = IdentityDigestConverter; - type InnerDigest = ::Output; - type LeafHash = Blake3F; // blake3, over field elements - type TwoToOneHash = Blake3; -} 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/crypto/mod.rs b/src/crypto/mod.rs index 6a3489a..bdf9eb8 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,2 +1 @@ -pub mod blake3_crh; pub mod merkle; diff --git a/src/lib.rs b/src/lib.rs index ade683d..911e540 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -795,7 +795,6 @@ impl< #[cfg(test)] pub mod test { - use super::crypto::merkle::blake3::Blake3MerkleTreeParams; use super::AccumulationScheme; use crate::serialize::{AccInstanceSerializer, AccWitnessSerializer, ProofSerializer}; use crate::types::{AccumulatorInstance, AccumulatorWitness}; @@ -811,6 +810,7 @@ pub mod test { }, utils::poseidon, }; + use ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig; use ark_bls12_381::Fr as BLS12_381; use ark_codes::{ @@ -873,14 +873,14 @@ pub mod test { .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(), (), () - ); + 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![]); @@ -915,14 +915,14 @@ pub mod test { 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 hash_chain_warp = + WARP::, _, Blake3MerkleConfig>::new( + warp_config.clone(), + code.clone(), + r1cs.clone(), + (), + (), + ); let mut prover_state = domainsep.instance(&0u32).std_prover(); let ((acc_x, acc_w), pf) = hash_chain_warp @@ -961,10 +961,8 @@ pub mod test { .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 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!( @@ -1031,14 +1029,14 @@ pub mod test { .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 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![]); @@ -1074,14 +1072,14 @@ pub mod test { 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 hash_chain_warp = + WARP::, _, Blake3MerkleConfig>::new( + warp_config.clone(), + code.clone(), + r1cs.clone(), + (), + (), + ); let mut prover_state = domainsep.instance(&0u32).std_prover(); let ((acc_x, acc_w), pf) = hash_chain_warp @@ -1120,10 +1118,8 @@ pub mod test { .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 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!( From a66f1225a925dd6b7d1905f70b8e97653ad139c5 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:10:09 +0200 Subject: [PATCH 05/21] comm complexity reduction inner product sumcheck api --- Cargo.toml | 8 ++-- benches/warp_rs.rs | 62 ++--------------------------- src/lib.rs | 11 ++--- src/protocol/transcript/verifier.rs | 4 +- 4 files changed, 14 insertions(+), 71 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d8a6ecd..1e20d5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,16 +23,16 @@ serde_json = "1.0" spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "smallfp-support", features = [ "ark-ff", ] } -efficient-sumcheck = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git" } +efficient-sumcheck = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git", branch = "z-tech/simd_goldilocks_experimental" } thiserror = "2.0.16" ark-codes = { git = "https://github.com/dmpierre/ark-codes.git" } [patch.crates-io] ark-crypto-primitives = { git = "https://github.com/arkworks-rs/crypto-primitives.git", branch = "z-tech/smallfp-absorb" } -ark-ff = { git = "https://github.com/arkworks-rs/algebra.git" } -ark-poly = { git = "https://github.com/arkworks-rs/algebra.git" } -ark-serialize = { git = "https://github.com/arkworks-rs/algebra.git" } +ark-ff = { git = "https://github.com/arkworks-rs/algebra.git", rev = "285dac2" } +ark-poly = { git = "https://github.com/arkworks-rs/algebra.git", rev = "285dac2" } +ark-serialize = { git = "https://github.com/arkworks-rs/algebra.git", rev = "285dac2" } ark-r1cs-std = { git = "https://github.com/arkworks-rs/r1cs-std" } ark-relations = { git = "https://github.com/arkworks-rs/snark.git" } ark-snark = { git = "https://github.com/arkworks-rs/snark.git" } diff --git a/benches/warp_rs.rs b/benches/warp_rs.rs index 1943dfd..f798892 100644 --- a/benches/warp_rs.rs +++ b/benches/warp_rs.rs @@ -1,4 +1,3 @@ -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; @@ -8,7 +7,7 @@ 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 ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig; 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/src/lib.rs b/src/lib.rs index 911e540..8daf117 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -708,13 +708,10 @@ impl< // multilinear batching sumcheck (sums_batching_sumcheck.len() == log_n).ok_or_err(VerifierError::NumSumcheckRounds)?; let mut target_2 = sigma_2; - for ([sum_00, sum_11, sum_0110], alpha) in - sums_batching_sumcheck.into_iter().zip(&alpha_sumcheck) - { - (sum_00 + sum_11 == target_2).ok_or_err(VerifierError::SumcheckRound)?; - target_2 = (target_2 - sum_0110) * alpha.square() - + sum_00 * (F::one() - alpha.double()) - + sum_0110 * alpha; + for ([a, b], alpha) in sums_batching_sumcheck.into_iter().zip(&alpha_sumcheck) { + target_2 = (target_2 - b) * alpha.square() + + a * (F::one() - alpha.double()) + + b * alpha; } // e. new target decision diff --git a/src/protocol/transcript/verifier.rs b/src/protocol/transcript/verifier.rs index 21865a9..19ae9c6 100644 --- a/src/protocol/transcript/verifier.rs +++ b/src/protocol/transcript/verifier.rs @@ -92,7 +92,7 @@ pub struct DerivedRandomness { pub bytes_shift_queries: Vec, pub xi: Vec, pub alpha_sumcheck: Vec, - pub sums_batching_sumcheck: Vec<[F; 3]>, + pub sums_batching_sumcheck: Vec<[F; 2]>, } pub fn derive_randomness< @@ -173,7 +173,7 @@ pub fn derive_randomness< let mut alpha_sumcheck = Vec::new(); let mut sums_batching_sumcheck = Vec::new(); for _ in 0..log_n { - let sums: [F; 3] = verifier_state.prover_messages()?; + let sums: [F; 2] = verifier_state.prover_messages()?; let c: F = verifier_state.verifier_message(); alpha_sumcheck.push(c); sums_batching_sumcheck.push(sums); From 77067669aa25c218dcd2a50ff2bb45b5f10b39fb Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:27:10 +0200 Subject: [PATCH 06/21] chkpt --- Cargo.toml | 2 +- benches/warp_rs.rs | 2 +- src/lib.rs | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1e20d5b..dcef7af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ serde_json = "1.0" spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "smallfp-support", features = [ "ark-ff", ] } -efficient-sumcheck = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git", branch = "z-tech/simd_goldilocks_experimental" } +efficient-sumcheck = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git", rev = "913b26d" } thiserror = "2.0.16" ark-codes = { git = "https://github.com/dmpierre/ark-codes.git" } diff --git a/benches/warp_rs.rs b/benches/warp_rs.rs index f798892..2f238b8 100644 --- a/benches/warp_rs.rs +++ b/benches/warp_rs.rs @@ -2,12 +2,12 @@ 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 ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig; use warp::traits::AccumulationScheme; use warp::WARP; diff --git a/src/lib.rs b/src/lib.rs index 8daf117..3c3d8fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -709,9 +709,8 @@ impl< (sums_batching_sumcheck.len() == log_n).ok_or_err(VerifierError::NumSumcheckRounds)?; let mut target_2 = sigma_2; for ([a, b], alpha) in sums_batching_sumcheck.into_iter().zip(&alpha_sumcheck) { - target_2 = (target_2 - b) * alpha.square() - + a * (F::one() - alpha.double()) - + b * alpha; + target_2 = + (target_2 - b) * alpha.square() + a * (F::one() - alpha.double()) + b * alpha; } // e. new target decision From dabc15011b51d81267760c89b4dd467109595379 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:41:53 +0200 Subject: [PATCH 07/21] rm rev pinning sumcheck --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index dcef7af..1e20d5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ serde_json = "1.0" spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "smallfp-support", features = [ "ark-ff", ] } -efficient-sumcheck = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git", rev = "913b26d" } +efficient-sumcheck = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git", branch = "z-tech/simd_goldilocks_experimental" } thiserror = "2.0.16" ark-codes = { git = "https://github.com/dmpierre/ark-codes.git" } From 312e22049e753135d144776aece3cb68d32ebaad Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:37:44 +0200 Subject: [PATCH 08/21] more opts --- src/lib.rs | 166 +++++++++++++++++++--------- src/protocol/transcript/verifier.rs | 2 +- 2 files changed, 112 insertions(+), 56 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3c3d8fa..fa4ae5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ use ark_crypto_primitives::{ crh::{CRHScheme, TwoToOneCRHScheme}, merkle_tree::{Config, MerkleTree, Path}, }; -use ark_ff::{Field, PrimeField, Zero}; +use ark_ff::{Field, PrimeField}; use ark_poly::{ univariate::DensePolynomial, DenseMultilinearExtension, DenseUVPolynomial, MultilinearExtension, Polynomial, @@ -20,7 +20,7 @@ use crypto::merkle::build_codeword_leaves; use crypto::merkle::compute_auth_paths; use efficient_sumcheck::{ accumulate_sparse_evaluations, batched_constraint_poly, - coefficient_sumcheck::coefficient_sumcheck, + coefficient_sumcheck::{coefficient_sumcheck, RoundPolyEvaluator}, folding::protogalaxy, hypercube::{compute_hypercube_eq_evals, Hypercube}, inner_product_sumcheck, @@ -74,56 +74,64 @@ fn eval_r1cs_constraint_poly( DensePolynomial::from_coefficients_vec(vec![a0 * b0 - c0, a0 * b1 + a1 * b0 - c1, a1 * b1]) } -/// Compute the twin-constraint round polynomial `h(X)`. +/// Evaluator for the twin-constraint sumcheck round polynomial. /// /// Proves `∑_i τ(i) · (f(i) + ω · p(i)) = 0` 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 /// -/// Returns `h(X) = Σ (f(X) + ω·p(X)) · t(X)`. -/// -/// `expected_num_coeffs` is the number of coefficients the verifier expects to read. -/// The result is padded with zeros to ensure the prover always writes exactly that many. -fn twin_constraint_round_poly( - tablewise: &[Vec>], - pairwise: &[Vec], - r1cs: &R1CSConstraints, +/// Each pair contributes `h(X) = (f(X) + ω·p(X)) · t(X)`. +struct TwinConstraintEvaluator<'a, F: Field> { + r1cs: &'a R1CSConstraints, omega: F, - expected_num_coeffs: usize, -) -> DensePolynomial { - let (u, z, a, b) = (&tablewise[0], &tablewise[1], &tablewise[2], &tablewise[3]); - let tau = &pairwise[0]; - - let f_iter = u.chunks(2).zip(a.chunks(2)).map(|(u, a)| { - protogalaxy::fold( - a[0].iter().zip(&a[1]).map(|(&l, &r)| (l, r - l)), - u[0].iter() - .zip(&u[1]) + 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]; + + // 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(), - ) - }); - let p_iter = b.chunks(2).zip(z.chunks(2)).map(|(b, z)| { - protogalaxy::fold( - b[0].iter().zip(&b[1]).map(|(&l, &r)| (l, r - l)), - r1cs.iter() - .map(|c| eval_r1cs_constraint_poly(c, &z[0], &z[1])) + ); + + // 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(), - ) - }); - let t_iter = tau.chunks(2).map(|t| linear_poly(t[0], t[1])); - - let mut h = f_iter - .zip(p_iter) - .zip(t_iter) - .map(|((f, p), t)| (f + p * omega).naive_mul(&t)) - .fold(DensePolynomial::zero(), |acc, r| acc + r); - - // Pad coefficients to the expected count so the prover writes exactly - // as many field elements as the verifier reads in derive_randomness. - h.coeffs.resize(expected_num_coeffs, F::zero()); - h + ); + + // 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; + } + } } pub trait BoolResult { @@ -191,19 +199,24 @@ impl< log_m: usize, ) -> Result, ProverError> { // a. encode witnesses + let t1 = std::time::Instant::now(); let (codewords, leaves) = build_codeword_leaves(&self.params.code, witnesses, l1); + eprintln!("[PROFILE] rs_encode (FFT): {:?}", t1.elapsed()); // b. evaluation claims let mus = codewords.iter().map(|f| f[0]).collect::>(); // c. commit to witnesses + let t1 = std::time::Instant::now(); let td_0 = MerkleTree::::new( &self.params.mt_leaf_hash_params, &self.params.mt_two_to_one_hash_params, leaves.chunks_exact(l1).collect::>(), )?; + eprintln!("[PROFILE] pesat_merkle_tree: {:?}", t1.elapsed()); // d. absorb commitment and code evaluations + let t1 = std::time::Instant::now(); let root_bytes: [u8; 32] = td_0 .root() .as_ref() @@ -216,6 +229,10 @@ impl< let taus = (0..l1) .map(|_| prover_state.verifier_messages_vec::(log_m)) .collect::>(); + eprintln!( + "[PROFILE] pesat_absorb + transcript: {:?}", + t1.elapsed() + ); Ok(PesatOutput { codewords, @@ -253,6 +270,11 @@ impl< let log_m = log2(M) as usize; let l1 = pesat.codewords.len(); + let _t_total = std::time::Instant::now(); + eprintln!( + "[PROFILE] M={M}, N={N}, k={k}, n(code_len)={n}, log_n={log_n}, log_l={log_l}, l1={l1}" + ); + // a. zero check randomness let omega: F = prover_state.verifier_message(); let tau = prover_state.verifier_messages_vec::(log_l); @@ -287,15 +309,16 @@ impl< let mut pw = [tau_eq_evals]; // tau let r1cs = self.params.p.constraints(); - let expected_num_coeffs = 2 + (log_n + 1).max(log_m + 2); - let sc = coefficient_sumcheck( - |tw, pw| twin_constraint_round_poly(tw, pw, r1cs, omega, expected_num_coeffs), - &mut tablewise, - &mut pw, - log_l, - prover_state, - ); + let degree = 1 + (log_n + 1).max(log_m + 2); + let evaluator = TwinConstraintEvaluator { + r1cs, + omega, + degree, + }; + let t1 = std::time::Instant::now(); + let sc = coefficient_sumcheck(&evaluator, &mut tablewise, &mut pw, log_l, prover_state); let gamma = sc.verifier_messages; + eprintln!("[PROFILE] twin_constraint_sumcheck: {:?}", t1.elapsed()); debug_assert_eq!(gamma.len(), log_l); @@ -307,6 +330,7 @@ impl< let beta_tau = b_red.pop().unwrap(); // eval the bundled r1cs + let t1 = std::time::Instant::now(); let beta_eq_evals = (0..M).map(|i| eq_poly(&beta_tau, i)).collect::>(); let eta = self @@ -320,7 +344,10 @@ impl< let f_hat = DenseMultilinearExtension::from_evaluations_slice(log_n, &f); let nu_0 = f_hat.fix_variables(&zeta_0)[0]; + eprintln!("[PROFILE] eval_bundled_r1cs: {:?}", t1.elapsed()); + // f. new commitment + let t1 = std::time::Instant::now(); let td = MerkleTree::::new( &self.params.mt_leaf_hash_params, &self.params.mt_two_to_one_hash_params, @@ -337,7 +364,10 @@ impl< prover_state.prover_message(&eta); prover_state.prover_message(&nu_0); + eprintln!("[PROFILE] merkle_commit: {:?}", t1.elapsed()); + // h. ood samples + let t1 = std::time::Instant::now(); let n_ood_samples = self.params.config.s * log_n; let ood_samples = prover_state.verifier_messages_vec::(n_ood_samples); let ood_samples = ood_samples.chunks(log_n).collect::>(); @@ -347,8 +377,10 @@ impl< .iter() .map(|ood_p| f_hat.fix_variables(ood_p)[0]) .collect::>(); + eprintln!("[PROFILE] ood fix_variables: {:?}", t1.elapsed()); // j. absorb ood answers + let t1 = std::time::Instant::now(); prover_state.prover_messages(&ood_answers); let mut zetas = vec![zeta_0.as_slice()]; @@ -360,6 +392,9 @@ impl< let r = 1 + self.params.config.s + self.params.config.t; let log_r = log2(r) as usize; let queries = QueryIndices::sample(prover_state, log_n, self.params.config.t); + eprintln!("[PROFILE] query_sampling: {:?}", t1.elapsed()); + + let t1 = std::time::Instant::now(); let xis = prover_state.verifier_messages_vec(log_r); zetas.extend(queries.evaluation_points.iter().map(|v| v.as_slice())); @@ -377,7 +412,13 @@ impl< }) .collect::>(); + eprintln!( + "[PROFILE] eq_poly_evals + ood_evals_vec: {:?}", + t1.elapsed() + ); + // [CBBZ23] optimization from hyperplonk + let t1 = std::time::Instant::now(); let id_non_0_eval_sums = accumulate_sparse_evaluations(zetas, xi_eq_evals, self.params.config.s, r); @@ -389,10 +430,16 @@ impl< ) .verifier_messages; + eprintln!( + "[PROFILE] batching_sumcheck (inner product): {:?}", + t1.elapsed() + ); + // m. new target let mu = f_hat.fix_variables(&alpha)[0]; // n. compute authentication paths + let t1 = std::time::Instant::now(); let auth_0 = compute_auth_paths(&pesat.td_0, &queries.leaf_positions)?; let auth = acc_witness @@ -414,6 +461,8 @@ impl< shift_queries_answers[i] = answers; } + eprintln!("[PROFILE] auth_paths + shift_queries: {:?}", t1.elapsed()); + let new_acc_instance = AccumulatorInstance { rt: vec![td.root()], alpha: vec![alpha], @@ -507,12 +556,15 @@ impl< //////////////////////// // 2. PESAT Reduction //////////////////////// + let t0 = std::time::Instant::now(); let pesat = self.pesat_reduce(prover_state, &witnesses, l1, log_m)?; + eprintln!("[PROFILE] pesat_reduce: {:?}", t0.elapsed()); //////////////////////// // 3. Constrained Code Accumulation //////////////////////// - self.constrained_code_accumulate( + let t0 = std::time::Instant::now(); + let result = self.constrained_code_accumulate( prover_state, pesat, &instances, @@ -523,7 +575,9 @@ impl< N, k, log_l, - ) + ); + eprintln!("[PROFILE] constrained_code_accumulate: {:?}", t0.elapsed()); + result } fn verify<'a>( @@ -698,10 +752,12 @@ impl< (coeffs_twinc_sumcheck.len() == log_l).ok_or_err(VerifierError::NumSumcheckRounds)?; let mut target_1 = sigma_1; - for (coeffs, gamma) in coeffs_twinc_sumcheck.into_iter().zip(&gamma_sumcheck) { + for (mut coeffs, gamma) in coeffs_twinc_sumcheck.into_iter().zip(&gamma_sumcheck) { + // Derive leading coefficient: c_d = claim - 2*c_0 - c_1 - ... - c_{d-1} + let partial_sum: F = coeffs.iter().skip(1).copied().sum(); + let leading = target_1 - coeffs[0].double() - partial_sum; + coeffs.push(leading); let h = DensePolynomial::from_coefficients_vec(coeffs); - (h.evaluate(&F::one()) + h.evaluate(&F::zero()) == target_1) - .ok_or_err(VerifierError::SumcheckRound)?; target_1 = h.evaluate(gamma); } diff --git a/src/protocol/transcript/verifier.rs b/src/protocol/transcript/verifier.rs index 19ae9c6..36e58d4 100644 --- a/src/protocol/transcript/verifier.rs +++ b/src/protocol/transcript/verifier.rs @@ -133,7 +133,7 @@ pub fn derive_randomness< let mut coeffs_twinc_sumcheck = Vec::new(); for _ in 0..log_l { let h_coeffs: Vec = - verifier_state.prover_messages_vec(2 + (log_n + 1).max(log_m + 2))?; + verifier_state.prover_messages_vec(1 + (log_n + 1).max(log_m + 2))?; let c: F = verifier_state.verifier_message(); gamma_sumcheck.push(c); coeffs_twinc_sumcheck.push(h_coeffs); From 5bf67533c4aa0ab63806a43d98fbfcc3b4138c7d Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sat, 18 Apr 2026 13:40:05 +0200 Subject: [PATCH 09/21] Plan 0: phase modules + Oracle abstraction + tracing scaffold Extracts WARP::prove and ::verify into per-phase modules under src/protocol/phases/ (pesat, twin_constraint, ood, batching, proximity), with a shared Oracle type in src/protocol/oracle.rs that owns the codeword and a lazily-materialised multilinear extension. lib.rs is now an orchestrator that threads oracles and accumulator state through the phases. Adds tracing spans at each phase boundary, gated behind a new `profile` cargo feature that optionally pulls in tracing-subscriber via src/profile.rs. Release builds do not pull tracing-subscriber as a direct dep of warp. Sets up docs/paper-mods/ as a living spec of notation modifications to the Warp paper, paired 1:1 with Rust modules. mod1_oracle.tex authored in full; mod2/3/4 stubbed for downstream plans. Framing stays inside IOP/BCS rather than moving to AHP. No behavior change: 14/14 tests pass (BLS12-381 + Goldilocks warp_test, query + relation tests), clippy clean under --all-features. --- .gitignore | 3 +- Cargo.toml | 7 + docs/paper-mods/README.md | 40 ++ docs/paper-mods/mod1_oracle.tex | 129 ++++ docs/paper-mods/mod2_structured_sumcheck.tex | 34 + docs/paper-mods/mod3_accumulator_state.tex | 30 + docs/paper-mods/mod4_parameter_selection.tex | 29 + docs/paper-mods/notation.tex | 58 ++ src/lib.rs | 633 +++++-------------- src/profile.rs | 41 ++ src/protocol/mod.rs | 2 + src/protocol/oracle.rs | 78 +++ src/protocol/phases/batching.rs | 112 ++++ src/protocol/phases/mod.rs | 23 + src/protocol/phases/ood.rs | 44 ++ src/protocol/phases/pesat.rs | 89 +++ src/protocol/phases/proximity.rs | 148 +++++ src/protocol/phases/twin_constraint.rs | 242 +++++++ 18 files changed, 1250 insertions(+), 492 deletions(-) create mode 100644 docs/paper-mods/README.md create mode 100644 docs/paper-mods/mod1_oracle.tex create mode 100644 docs/paper-mods/mod2_structured_sumcheck.tex create mode 100644 docs/paper-mods/mod3_accumulator_state.tex create mode 100644 docs/paper-mods/mod4_parameter_selection.tex create mode 100644 docs/paper-mods/notation.tex create mode 100644 src/profile.rs create mode 100644 src/protocol/oracle.rs create mode 100644 src/protocol/phases/batching.rs create mode 100644 src/protocol/phases/mod.rs create mode 100644 src/protocol/phases/ood.rs create mode 100644 src/protocol/phases/pesat.rs create mode 100644 src/protocol/phases/proximity.rs create mode 100644 src/protocol/phases/twin_constraint.rs diff --git a/.gitignore b/.gitignore index 7b4eda9..1857d77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target /Cargo.lock .vscode -.DS_Store \ No newline at end of file +.DS_Store +.claude/settings.local.json \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 1e20d5b..4f9e0f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "small ] } efficient-sumcheck = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git", branch = "z-tech/simd_goldilocks_experimental" } 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"], optional = true } ark-codes = { git = "https://github.com/dmpierre/ark-codes.git" } @@ -51,6 +53,11 @@ default = ["asm"] asm = ["ark-ff/asm"] +# Turns on a `tracing-subscriber` that emits phase timing and dimension fields +# to stderr in the same spirit as the old `[PROFILE]` eprintln! lines. +# Plan O replaces this with structured JSON + op counters. +profile = ["dep:tracing-subscriber"] + [[bench]] name = "warp_rs" harness = false 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..f755d99 --- /dev/null +++ b/docs/paper-mods/mod4_parameter_selection.tex @@ -0,0 +1,29 @@ +\documentclass{article} +\input{notation.tex} + +\title{Modification 4: \\ Soundness-driven parameter selection (stub)} +\author{Warp paper-mods} +\date{} + +\begin{document} +\maketitle + +\paragraph{Status.} Stub. Full spec authored in Plan P. Paired code: +\codemod{src/params/} (to be created). + +\section*{Abstract} + +The paper states security bounds symbolically; the implementation currently +hard-codes parameter values (e.g.\ \(s = 8\), \(t = 7\)) with no derivation +record. Modification 4 introduces, both in the paper and in code, an +\emph{inverse} to the soundness analysis: given a security target +\(\lambda\), a field \(\F\), a code rate \(\rho\), and an accumulation +depth, produce the minimum-cost parameter tuple \((s, t, l, l_1)\) hitting +that target. Both list-decoding regimes (provable and conjectured) are +supported via a user toggle. + +The code module \codemod{src/params/} will expose pure \texttt{select} and +\texttt{validate} functions plus a preset table to replace the ad-hoc +constants currently appearing in tests. See Plan P for the full treatment. + +\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/src/lib.rs b/src/lib.rs index fa4ae5a..82e08a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,29 +1,18 @@ #![allow(clippy::assign_op_pattern)] // generated by SmallFpConfig derive macro use crate::error::WARPError; use crate::traits::AccumulationScheme; -use crate::types::{ - AccumulatorInstance, AccumulatorWitness, PesatOutput, ProveResult, WARPParams, WARPProof, -}; +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 config::WARPConfig; -use crypto::merkle::build_codeword_leaves; -use crypto::merkle::compute_auth_paths; use efficient_sumcheck::{ - accumulate_sparse_evaluations, batched_constraint_poly, - coefficient_sumcheck::{coefficient_sumcheck, RoundPolyEvaluator}, - folding::protogalaxy, hypercube::{compute_hypercube_eq_evals, Hypercube}, - inner_product_sumcheck, order_strategy::AscendingOrder, }; use protocol::query::QueryIndices; @@ -43,6 +32,7 @@ pub mod config; pub mod constraints; pub mod crypto; pub mod error; +pub mod profile; pub mod protocol; pub mod relations; pub mod serialize; @@ -50,89 +40,8 @@ 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]) -} - -/// Evaluator for the twin-constraint sumcheck round polynomial. -/// -/// Proves `∑_i τ(i) · (f(i) + ω · p(i)) = 0` 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 pair contributes `h(X) = (f(X) + ω·p(X)) · t(X)`. -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]; - - // 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; - } - } -} +use protocol::phases::{batching, ood, pesat, proximity, twin_constraint}; pub trait BoolResult { fn ok_or_err(self, err: E) -> Result<(), E>; @@ -185,176 +94,115 @@ impl< P: Clone + BundledPESAT, Config = (usize, usize, usize)>, // m, n, k C: LinearCode + Clone, MT: Config + From<[u8; 32]>>, - > WARP + > AccumulationScheme for WARP { - /// Phase 2: PESAT Reduction - /// - /// Encodes fresh witnesses into codewords, commits via Merkle tree, - /// absorbs the commitment and code evaluations, and derives τ challenges. - fn pesat_reduce( - &self, - prover_state: &mut ProverState, - witnesses: &[Vec], - l1: usize, - log_m: usize, - ) -> Result, ProverError> { - // a. encode witnesses - let t1 = std::time::Instant::now(); - let (codewords, leaves) = build_codeword_leaves(&self.params.code, witnesses, l1); - eprintln!("[PROFILE] rs_encode (FFT): {:?}", t1.elapsed()); - - // b. evaluation claims - let mus = codewords.iter().map(|f| f[0]).collect::>(); - - // c. commit to witnesses - let t1 = std::time::Instant::now(); - let td_0 = MerkleTree::::new( - &self.params.mt_leaf_hash_params, - &self.params.mt_two_to_one_hash_params, - leaves.chunks_exact(l1).collect::>(), - )?; - eprintln!("[PROFILE] pesat_merkle_tree: {:?}", t1.elapsed()); - - // d. absorb commitment and code evaluations - let t1 = std::time::Instant::now(); - 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::>(); - eprintln!( - "[PROFILE] pesat_absorb + transcript: {:?}", - t1.elapsed() - ); + type Index = P; + type ProverKey = (P, usize, usize, usize); + type VerifierKey = (usize, usize, usize); + type Instances = Vec>; + type Witnesses = Vec>; - Ok(PesatOutput { - codewords, - td_0, - mus, - taus, - }) + fn index( + prover_state: &mut ProverState, + index: Self::Index, + ) -> spongefish::VerificationResult<(Self::ProverKey, Self::VerifierKey)> { + let (m, n, k) = index.config(); + // initialize prover state for fs + // TODO for R1CS + prover_state.public_message(&index.description()); + 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, m, n, k), (m, n, k))) } - /// Phase 3: Constrained Code Accumulation - /// - /// Combines PESAT output with accumulated state, runs: - /// - Twin constraint sumcheck - /// - OOD sampling - /// - Batching sumcheck - /// - Authentication path computation - /// - /// Returns the new accumulator and proof. - #[allow(clippy::too_many_arguments)] - fn constrained_code_accumulate( + #[tracing::instrument(name = "warp.prove", skip_all)] + fn prove( &self, + pk: Self::ProverKey, prover_state: &mut ProverState, - pesat: PesatOutput, - instances: &[Vec], - witnesses: &[Vec], + witnesses: Self::Witnesses, + instances: Self::Instances, acc_instance: AccumulatorInstance, acc_witness: AccumulatorWitness, - #[allow(non_snake_case)] M: usize, - #[allow(non_snake_case)] N: usize, - k: usize, - log_l: usize, ) -> ProveResult { - let n = self.params.code.code_len(); - let log_n = log2(n) as usize; - let log_m = log2(M) as usize; - let l1 = pesat.codewords.len(); - - let _t_total = std::time::Instant::now(); - eprintln!( - "[PROFILE] M={M}, N={N}, k={k}, n(code_len)={n}, log_n={log_n}, log_l={log_l}, l1={l1}" - ); - - // a. zero check randomness - let omega: F = prover_state.verifier_message(); - let tau = prover_state.verifier_messages_vec::(log_l); + debug_assert!(instances.len() > 1); + debug_assert_eq!(witnesses.len(), instances.len()); + debug_assert_eq!(acc_witness.td.len(), acc_instance.rt.len()); - // b. define [...] - // c. sumcheck protocol - let tau_eq_evals = Hypercube::::new(log_l) - .map(|(index, _point)| eq_poly(&tau, index)) - .collect::>(); + 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()); - let alpha_vecs = concat_slices(&acc_instance.alpha, &vec![vec![F::zero(); log_n]; l1]); + // Parse phase: dimensions and transcript priming. + #[allow(non_snake_case)] + let (M, N, k) = (pk.1, pk.2, pk.3); + let (log_m, log_l) = (log2(M) as usize, log2(l) as usize); + let log_n = log2(self.params.code.code_len()) as usize; - // build the z (x, w) vectors - let z_vecs: Vec> = acc_instance - .beta - .1 - .iter() - .zip(&acc_witness.w) - .chain(instances.iter().zip(witnesses)) - .map(|(x, w)| concat_slices(x, w)) - .collect(); - - let beta_vecs: Vec> = acc_instance.beta.0.into_iter().chain(pesat.taus).collect(); - - // Twin Constraint sumcheck - let mut tablewise = [ - concat_slices(&acc_witness.f, &pesat.codewords), // u - z_vecs, // z - alpha_vecs, // a - beta_vecs, // b - ]; - let mut pw = [tau_eq_evals]; // tau - - let r1cs = self.params.p.constraints(); - let degree = 1 + (log_n + 1).max(log_m + 2); - let evaluator = TwinConstraintEvaluator { - r1cs, - omega, - degree, - }; - let t1 = std::time::Instant::now(); - let sc = coefficient_sumcheck(&evaluator, &mut tablewise, &mut pw, log_l, prover_state); - let gamma = sc.verifier_messages; - eprintln!("[PROFILE] twin_constraint_sumcheck: {:?}", t1.elapsed()); + debug_assert_eq!(instances[0].len(), N - k); + absorb_instances(prover_state, &instances); + acc_instance.absorb_into(prover_state); - debug_assert_eq!(gamma.len(), log_l); + // 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; - // e. new oracle and target — after log_l rounds each group has one table left - let [mut u_red, mut z_red, mut a_red, mut b_red] = tablewise; - let f = u_red.pop().unwrap(); - let z = z_red.pop().unwrap(); - let zeta_0 = a_red.pop().unwrap(); - let beta_tau = b_red.pop().unwrap(); + // Phase 2: PESAT — emit oracles (codewords), commit, squeeze τs. + let pesat = pesat::prove::( + prover_state, + &self.params.code, + &self.params.mt_leaf_hash_params, + &self.params.mt_two_to_one_hash_params, + &witnesses, + l1, + log_m, + )?; - // eval the bundled r1cs - let t1 = std::time::Instant::now(); - let beta_eq_evals = (0..M).map(|i| eq_poly(&beta_tau, i)).collect::>(); + // Phase 3a: twin-constraint sumcheck. + let tc = twin_constraint::prove::( + prover_state, + &pesat.codewords, + pesat.taus, + acc_instance, + &acc_fs, + &acc_ws, + &instances, + &witnesses, + self.params.p.constraints(), + log_l, + log_m, + log_n, + ); + // 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.beta_tau, i)).collect::>(); let eta = self .params .p - .evaluate_bundled(&beta_eq_evals, &z) + .evaluate_bundled(&beta_eq_evals, &tc.z) .map_err(|_| ProverError::SpongeFish)?; + let nu_0 = tc.f.query_at_point(&tc.zeta_0); - 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]; + let (new_x, new_w) = tc.z.split_at(N - k); + let new_x = new_x.to_vec(); + let new_w = new_w.to_vec(); + let new_beta = (vec![tc.beta_tau.clone()], vec![new_x]); - eprintln!("[PROFILE] eval_bundled_r1cs: {:?}", t1.elapsed()); - - // f. new commitment - let t1 = std::time::Instant::now(); - let td = MerkleTree::::new( - &self.params.mt_leaf_hash_params, - &self.params.mt_two_to_one_hash_params, - f.chunks(1).collect::>(), - )?; - - // g. absorb new commitment and target + let td = { + let _s = tracing::info_span!("warp.commit_new_oracle").entered(); + MerkleTree::::new( + &self.params.mt_leaf_hash_params, + &self.params.mt_two_to_one_hash_params, + tc.f.evals().chunks(1).collect::>(), + )? + }; let td_root_bytes: [u8; 32] = td .root() .as_ref() @@ -364,221 +212,64 @@ impl< prover_state.prover_message(&eta); prover_state.prover_message(&nu_0); - eprintln!("[PROFILE] merkle_commit: {:?}", t1.elapsed()); - - // h. ood samples - let t1 = std::time::Instant::now(); - let n_ood_samples = self.params.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::>(); - eprintln!("[PROFILE] ood fix_variables: {:?}", t1.elapsed()); - - // j. absorb ood answers - let t1 = std::time::Instant::now(); - prover_state.prover_messages(&ood_answers); + // Phase 3c: OOD — point queries on the oracle. + let ood_out = ood::prove::(prover_state, &tc.f, self.params.config.s, log_n); - 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.params.config.s + self.params.config.t; - let log_r = log2(r) as usize; - let queries = QueryIndices::sample(prover_state, log_n, self.params.config.t); - eprintln!("[PROFILE] query_sampling: {:?}", t1.elapsed()); - - let t1 = std::time::Instant::now(); - let xis = prover_state.verifier_messages_vec(log_r); + // Sample shift queries — ordering-coupled to the batching ξ below. + let queries = QueryIndices::::sample(prover_state, log_n, self.params.config.t); + // Phase 3d: batching sumcheck. Assemble zetas = [ζ₀, ood_j…, query_k…]. + let ood_chunks: Vec<&[F]> = ood_out.samples_flat.chunks(log_n).collect(); + let mut zetas: Vec<&[F]> = + Vec::with_capacity(1 + self.params.config.s + self.params.config.t); + zetas.push(tc.zeta_0.as_slice()); + zetas.extend(ood_chunks); zetas.extend(queries.evaluation_points.iter().map(|v| v.as_slice())); - // l. sumcheck polynomials - // compute evaluations for xi - - let xi_eq_evals = (0..r).map(|i| eq_poly(&xis, i)).collect::>(); - - let ood_evals_vec = (0..1 + self.params.config.s) - .map(|i| { - (0..n) - .map(|a| eq_poly(zetas[i], a) * xi_eq_evals[i]) - .collect::>() - }) - .collect::>(); - - eprintln!( - "[PROFILE] eq_poly_evals + ood_evals_vec: {:?}", - t1.elapsed() - ); - - // [CBBZ23] optimization from hyperplonk - let t1 = std::time::Instant::now(); - let id_non_0_eval_sums = - accumulate_sparse_evaluations(zetas, xi_eq_evals, self.params.config.s, r); - - // call efficient sumcheck for batched_constraint checks - let alpha = inner_product_sumcheck( - &mut f.clone(), - &mut batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), + let batching_out = batching::prove::( prover_state, - ) - .verifier_messages; - - eprintln!( - "[PROFILE] batching_sumcheck (inner product): {:?}", - t1.elapsed() + &tc.f, + &zetas, + self.params.config.s, + self.params.config.t, + log_n, ); - // m. new target - let mu = f_hat.fix_variables(&alpha)[0]; - - // n. compute authentication paths - let t1 = std::time::Instant::now(); - let auth_0 = compute_auth_paths(&pesat.td_0, &queries.leaf_positions)?; - - let auth = acc_witness - .td // for each accumulated witness and for each - .iter() - .map(|td| compute_auth_paths(td, &queries.leaf_positions)) - .collect::>>, Error>>()?; - - let all_codewords = acc_witness - .f - .into_iter() - .chain(pesat.codewords) - .collect::>(); - - let mut shift_queries_answers = - vec![vec![F::default(); all_codewords.len()]; queries.leaf_positions.len()]; - for (i, idx) in queries.leaf_positions.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.codewords).collect(); + let prox = proximity::prove::(&queries, &pesat.td_0, &acc_tds, &all_codewords)?; - eprintln!("[PROFILE] auth_paths + shift_queries: {:?}", t1.elapsed()); + // Assemble new accumulator state and proof. + let mut nus = Vec::with_capacity(1 + self.params.config.s); + nus.push(nu_0); + nus.extend(ood_out.answers); let new_acc_instance = AccumulatorInstance { rt: vec![td.root()], - alpha: vec![alpha], - mu: vec![mu], - beta, + alpha: vec![batching_out.alpha], + mu: vec![batching_out.mu], + beta: new_beta, eta: vec![eta], }; let new_acc_witness = AccumulatorWitness { td: vec![td], - f: vec![f], - w: vec![w.to_vec()], + f: vec![tc.f.into_evals()], + w: vec![new_w], }; - let proof = WARPProof { rt_0: pesat.td_0.root(), mu_i: pesat.mus, nu_0, nu_i: nus, - auth_0, - auth_j: auth, - shift_query_answers: shift_queries_answers, + auth_0: prox.auth_0, + auth_j: prox.auth_j, + shift_query_answers: prox.shift_query_answers, }; Ok(((new_acc_instance, new_acc_witness), proof)) } -} - -impl< - F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, - P: Clone + BundledPESAT, Config = (usize, usize, usize)>, // m, n, k - C: LinearCode + Clone, - MT: Config + From<[u8; 32]>>, - > AccumulationScheme for WARP -{ - type Index = P; - type ProverKey = (P, usize, usize, usize); - type VerifierKey = (usize, usize, usize); - type Instances = Vec>; - type Witnesses = Vec>; - - fn index( - prover_state: &mut ProverState, - index: Self::Index, - ) -> spongefish::VerificationResult<(Self::ProverKey, Self::VerifierKey)> { - let (m, n, k) = index.config(); - // initialize prover state for fs - // TODO for R1CS - prover_state.public_message(&index.description()); - 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, m, n, k), (m, n, k))) - } - - fn prove( - &self, - pk: Self::ProverKey, - prover_state: &mut ProverState, - witnesses: Self::Witnesses, - instances: Self::Instances, - acc_instance: AccumulatorInstance, - acc_witness: AccumulatorWitness, - ) -> Result< - ( - (AccumulatorInstance, AccumulatorWitness), - WARPProof, - ), - ProverError, - > { - debug_assert!(instances.len() > 1); - debug_assert_eq!(witnesses.len(), instances.len()); - debug_assert_eq!(acc_witness.td.len(), acc_instance.rt.len()); - - 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 - //////////////////////// - #[allow(non_snake_case)] - let (M, N, k) = (pk.1, pk.2, pk.3); - let (log_m, log_l) = (log2(M) as usize, log2(l) as usize); - - debug_assert_eq!(instances[0].len(), N - k); - - absorb_instances(prover_state, &instances); - acc_instance.absorb_into(prover_state); - - //////////////////////// - // 2. PESAT Reduction - //////////////////////// - let t0 = std::time::Instant::now(); - let pesat = self.pesat_reduce(prover_state, &witnesses, l1, log_m)?; - eprintln!("[PROFILE] pesat_reduce: {:?}", t0.elapsed()); - - //////////////////////// - // 3. Constrained Code Accumulation - //////////////////////// - let t0 = std::time::Instant::now(); - let result = self.constrained_code_accumulate( - prover_state, - pesat, - &instances, - &witnesses, - acc_instance, - acc_witness, - M, - N, - k, - log_l, - ); - eprintln!("[PROFILE] constrained_code_accumulate: {:?}", t0.elapsed()); - result - } fn verify<'a>( &self, @@ -704,70 +395,30 @@ impl< let expected_beta = concat_slices(&acc_instance.beta.0[0], &acc_instance.beta.1[0]); (expected_beta == beta).ok_or_err(VerifierError::CircuitEvaluationPoint)?; - // c. check auth paths + // c. proximity: check auth paths + leaf-position consistency. let queries: QueryIndices = QueryIndices::from_squeezed_bytes(&bytes_shift_queries, log_n, self.params.config.t); - // check: - // that the leaf index corresponds to the shift query - // that the path is correct - (proof.shift_query_answers.len() == self.params.config.t) - .ok_or_err(VerifierError::NumShiftQueries)?; - - // proof.auth_0 - for (i, path) in proof.auth_0.iter().enumerate() { - (path.leaf_index == queries.leaf_positions[i]) - .ok_or_err(VerifierError::ShiftQueryIndex)?; - - let is_valid = path.verify( - &self.params.mt_leaf_hash_params, - &self.params.mt_two_to_one_hash_params, - &rt_0, - &proof.shift_query_answers[i][l2..], // leaves are evaluations of the l1 codewords - )?; - is_valid.ok_or_err(VerifierError::ShiftQuery)? - } - - // proof.auth_j holds merkle proofs for l2 accumulated instances - (proof.auth_j.len() == l2).ok_or_err(VerifierError::NumL2Instances)?; - for (i, paths) in proof.auth_j.iter().enumerate() { - (paths.len() == self.params.config.t).ok_or_err(VerifierError::NumShiftQueries)?; - let root = &l2_roots[i]; - for (j, path) in paths.iter().enumerate() { - (path.leaf_index == queries.leaf_positions[j]) - .ok_or_err(VerifierError::ShiftQueryIndex)?; - let is_valid = path.verify( - &self.params.mt_leaf_hash_params, - &self.params.mt_two_to_one_hash_params, - root, - [proof.shift_query_answers[j][i]], // shift_query_answers[j][i] holds f_i(x_j) - )?; - - is_valid.ok_or_err(VerifierError::ShiftQuery)? - } - } + proximity::verify::( + &queries, + &rt_0, + &l2_roots, + &proof.auth_0, + &proof.auth_j, + &proof.shift_query_answers, + &self.params.mt_leaf_hash_params, + &self.params.mt_two_to_one_hash_params, + l2, + self.params.config.t, + )?; - // d. sumcheck decisions - // twin constraints sumcheck + // d. sumcheck decisions: reduce each transcript's round messages to + // a single target, then compare to the expected claim in (e). (coeffs_twinc_sumcheck.len() == log_l).ok_or_err(VerifierError::NumSumcheckRounds)?; + let target_1 = twin_constraint::verify_claim(sigma_1, coeffs_twinc_sumcheck, &gamma_sumcheck); - let mut target_1 = sigma_1; - for (mut coeffs, gamma) in coeffs_twinc_sumcheck.into_iter().zip(&gamma_sumcheck) { - // Derive leading coefficient: c_d = claim - 2*c_0 - c_1 - ... - c_{d-1} - let partial_sum: F = coeffs.iter().skip(1).copied().sum(); - let leading = target_1 - coeffs[0].double() - partial_sum; - coeffs.push(leading); - let h = DensePolynomial::from_coefficients_vec(coeffs); - target_1 = h.evaluate(gamma); - } - - // multilinear batching sumcheck (sums_batching_sumcheck.len() == log_n).ok_or_err(VerifierError::NumSumcheckRounds)?; - let mut target_2 = sigma_2; - for ([a, b], alpha) in sums_batching_sumcheck.into_iter().zip(&alpha_sumcheck) { - target_2 = - (target_2 - b) * alpha.square() + a * (F::one() - alpha.double()) + b * alpha; - } + let target_2 = batching::verify_claim(sigma_2, sums_batching_sumcheck, &alpha_sumcheck); // e. new target decision // build eq^{\star}(\alpha) diff --git a/src/profile.rs b/src/profile.rs new file mode 100644 index 0000000..51a3b17 --- /dev/null +++ b/src/profile.rs @@ -0,0 +1,41 @@ +//! Profile instrumentation helpers. +//! +//! Spans are always emitted by the phase modules (via the `tracing` crate, +//! which is zero-cost without a subscriber). Installing a subscriber that +//! renders those spans is gated on the `profile` feature so release builds +//! do not pull in `tracing-subscriber`. +//! +//! Plan O replaces the human-format subscriber below with JSON output plus +//! op-counter macros (`field_muls`, `merkle_hashes`, …) for ingestion by +//! Plan B's regression detector. + +/// Install a human-readable `tracing-subscriber` on stderr that mimics the +/// old `[PROFILE]` lines. Feature-gated: a no-op when `profile` is off. +/// +/// Safe to call multiple times; only the first call installs the global +/// subscriber. Returns `true` if this call performed the install. +#[cfg(feature = "profile")] +pub fn init() -> bool { + use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + + // The default filter shows every span at INFO+; override via RUST_LOG. + 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() +} + +/// No-op when the `profile` feature is off. +#[cfg(not(feature = "profile"))] +pub fn init() -> bool { + false +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index fc4cd34..55ed4d7 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,2 +1,4 @@ +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..9248e47 --- /dev/null +++ b/src/protocol/oracle.rs @@ -0,0 +1,78 @@ +//! 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; + +/// 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 { + 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 { + let mle = self.mle.get_or_init(|| { + 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..b977424 --- /dev/null +++ b/src/protocol/phases/batching.rs @@ -0,0 +1,112 @@ +//! 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. +//! +//! Takes the already-sampled shift-query evaluation points as input so the +//! transcript order (queries sampled, then ξ, then sumcheck messages) is +//! preserved. See the orchestrator in `src/lib.rs`. + +use ark_ff::{Field, PrimeField}; +use ark_std::log2; +use efficient_sumcheck::{ + accumulate_sparse_evaluations, batched_constraint_poly, inner_product_sumcheck, +}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; + +use crate::protocol::oracle::Oracle; +use crate::utils::poly::eq_poly; + +/// Output of the batching sumcheck: the reduced point `α` and the target +/// `μ = \hat f(α)`. +pub struct BatchingOutput { + pub alpha: Vec, + pub mu: F, +} + +/// Run the batching sumcheck. +/// +/// `zetas_prefix` must contain `1 + s + t` evaluation points in the order +/// `[ζ_0, ood_0, ..., ood_{s-1}, query_0, ..., query_{t-1}]`. +#[tracing::instrument( + name = "batching", + skip_all, + fields(s = s, t = t, log_n = log_n) +)] +pub fn prove( + prover_state: &mut ProverState, + oracle: &Oracle, + zetas_prefix: &[&[F]], + s: usize, + t: usize, + log_n: usize, +) -> BatchingOutput +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, +{ + let n = oracle.len(); + let r = 1 + s + t; + let log_r = log2(r) as usize; + debug_assert_eq!(zetas_prefix.len(), r); + + let xis = prover_state.verifier_messages_vec::(log_r); + + // compute evaluations for xi and the dense ood_evals_vec for the first 1+s zetas + 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 + s) + .map(|i| { + (0..n) + .map(|a| eq_poly(zetas_prefix[i], a) * xi_eq_evals[i]) + .collect::>() + }) + .collect::>(); + (xi_eq_evals, ood_evals_vec) + }; + + // [CBBZ23] / HyperPlonk optimization for the t sparse shift-query zetas. + let id_non_0_eval_sums = { + let _s = tracing::info_span!("batching.accumulate_sparse").entered(); + accumulate_sparse_evaluations(zetas_prefix.to_vec(), xi_eq_evals, s, r) + }; + + // call efficient sumcheck for batched_constraint checks + let alpha = { + let _s = tracing::info_span!("batching.sumcheck").entered(); + inner_product_sumcheck( + &mut oracle.evals().to_vec(), + &mut batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), + prover_state, + ) + .verifier_messages + }; + + let mu = oracle.query_at_point(&alpha); + + BatchingOutput { alpha, mu } +} + +/// Reduce the batching (inner-product / multilinear) sumcheck's per-round +/// messages against the verifier challenges `α`. Each round message is +/// `[a, b]` where `h(X) = a·(1-2X) + b·X + (prev_target - b)·X²`; the +/// caller's `a, b` unpacking mirrors `src/protocol/transcript/verifier.rs`. +#[tracing::instrument(name = "batching.verify", skip_all)] +pub(crate) fn verify_claim(sigma_2: F, sums_per_round: Vec<[F; 2]>, alpha: &[F]) -> F +where + F: Field, +{ + let mut target = sigma_2; + for ([a, b], x) in sums_per_round.into_iter().zip(alpha) { + target = (target - b) * x.square() + a * (F::one() - x.double()) + b * x; + } + target +} diff --git a/src/protocol/phases/mod.rs b/src/protocol/phases/mod.rs new file mode 100644 index 0000000..9eb1c4c --- /dev/null +++ b/src/protocol/phases/mod.rs @@ -0,0 +1,23 @@ +//! Warp IOR phases as first-class modules. +//! +//! Paired spec: `docs/paper-mods/mod1_oracle.tex` (shared Oracle composition) +//! and the forthcoming `docs/paper-mods/mod2_structured_sumcheck.tex`, +//! `mod3_accumulator_state.tex`. +//! +//! Each submodule implements one IOR from the Warp construction. The phase +//! functions are concrete (no `IOR` trait) but share a consistent shape: +//! +//! - **prove**: takes the current prover state, the accumulator, and any +//! fresh inputs; runs the IOR's prover; returns the reduced claim and any +//! emitted [`Oracle`](super::oracle::Oracle)s. +//! - **verify** (where applicable): consumes a subset of +//! `DerivedRandomness` plus the prior claim; returns the reduced claim. +//! +//! The top-level orchestrators live in `src/lib.rs::WARP::prove` and +//! `::verify`, which thread state between phases. + +pub mod batching; +pub mod ood; +pub mod pesat; +pub mod proximity; +pub mod twin_constraint; diff --git a/src/protocol/phases/ood.rs b/src/protocol/phases/ood.rs new file mode 100644 index 0000000..4e29945 --- /dev/null +++ b/src/protocol/phases/ood.rs @@ -0,0 +1,44 @@ +//! 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. + +use ark_ff::{Field, PrimeField}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; + +use crate::protocol::oracle::Oracle; + +/// Output of the OOD phase: the flat challenge vector and the prover's +/// answers at each chunked evaluation point. +pub struct OodOutput { + /// 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, +} + +/// Run the OOD phase: sample `s` evaluation points, query the oracle at +/// each, absorb the answers. +#[tracing::instrument(name = "ood", skip_all, fields(s = s, log_n = log_n))] +pub fn prove( + prover_state: &mut ProverState, + oracle: &Oracle, + s: usize, + log_n: usize, +) -> OodOutput +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, +{ + let samples_flat = prover_state.verifier_messages_vec::(s * log_n); + let answers = samples_flat + .chunks(log_n) + .map(|zeta| oracle.query_at_point(zeta)) + .collect::>(); + prover_state.prover_messages(&answers); + OodOutput { + samples_flat, + answers, + } +} diff --git a/src/protocol/phases/pesat.rs b/src/protocol/phases/pesat.rs new file mode 100644 index 0000000..766c93f --- /dev/null +++ b/src/protocol/phases/pesat.rs @@ -0,0 +1,89 @@ +//! 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. +//! +//! Under Modification 1's oracle framing, this IOR has signature +//! `(statement, witnesses) -> (PesatOutput{codewords, commit, claims, τs})`; +//! downstream phases consume these as oracles. +//! +//! No verifier-side work lives here — PESAT emits oracles and τs that the +//! verifier derives afresh from the transcript via +//! `src/protocol/transcript/verifier.rs::derive_randomness`. + +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}; + +use crate::crypto::merkle::build_codeword_leaves; +use crate::error::ProverError; +use crate::types::PesatOutput; + +/// Run the PESAT Reduction prover. +#[tracing::instrument( + name = "pesat", + skip_all, + fields(l1 = l1, log_m = log_m, n_witnesses = witnesses.len()) +)] +pub(crate) fn prove( + prover_state: &mut ProverState, + code: &C, + mt_leaf_hash_params: &::Parameters, + mt_two_to_one_hash_params: &::Parameters, + witnesses: &[Vec], + l1: usize, + log_m: usize, +) -> Result, ProverError> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + C: LinearCode, + MT: Config + From<[u8; 32]>>, +{ + // a. encode witnesses + let (codewords, leaves) = { + let _s = tracing::info_span!("pesat.encode").entered(); + build_codeword_leaves(code, witnesses, 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(); + MerkleTree::::new( + mt_leaf_hash_params, + mt_two_to_one_hash_params, + leaves.chunks_exact(l1).collect::>(), + )? + }; + + // d. absorb commitment and code evaluations; 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..l1) + .map(|_| prover_state.verifier_messages_vec::(log_m)) + .collect::>() + }; + + Ok(PesatOutput { + codewords, + td_0, + mus, + taus, + }) +} diff --git a/src/protocol/phases/proximity.rs b/src/protocol/phases/proximity.rs new file mode 100644 index 0000000..e1e2876 --- /dev/null +++ b/src/protocol/phases/proximity.rs @@ -0,0 +1,148 @@ +//! 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. + +use ark_crypto_primitives::{ + crh::{CRHScheme, TwoToOneCRHScheme}, + merkle_tree::{Config, MerkleTree, Path}, + Error, +}; +use ark_ff::Field; + +use crate::crypto::merkle::compute_auth_paths; +use crate::error::VerifierError; +use crate::protocol::query::QueryIndices; +use crate::BoolResult; + +pub struct ProximityOutput { + pub auth_0: Vec>, + pub auth_j: Vec>>, + pub shift_query_answers: Vec>, +} + +/// Open the proximity queries: generate auth paths for the fresh commitment +/// and each accumulated commitment, and collect the codeword values at every +/// queried leaf. +/// +/// `all_codewords` must list the **accumulated** codewords first, then the +/// **fresh** PESAT codewords, matching the verifier's expectation at +/// `lib.rs::verify` (the `[l2..]` slice is the fresh chunk). +#[tracing::instrument( + name = "proximity", + skip_all, + fields( + n_queries = queries.leaf_positions.len(), + n_accumulators = acc_td.len(), + n_codewords = all_codewords.len(), + ) +)] +pub fn prove( + queries: &QueryIndices, + td_0: &MerkleTree, + acc_td: &[MerkleTree], + all_codewords: &[Vec], +) -> Result, Error> +where + F: Field, + MT: Config, +{ + let auth_0 = { + let _s = tracing::info_span!("proximity.auth_0").entered(); + compute_auth_paths(td_0, &queries.leaf_positions)? + }; + + let auth_j = { + let _s = tracing::info_span!("proximity.auth_j").entered(); + acc_td + .iter() + .map(|td| compute_auth_paths(td, &queries.leaf_positions)) + .collect::>>, Error>>()? + }; + + let shift_query_answers = { + let _s = tracing::info_span!("proximity.shift_queries").entered(); + let mut answers = + vec![vec![F::default(); all_codewords.len()]; queries.leaf_positions.len()]; + for (i, idx) in queries.leaf_positions.iter().enumerate() { + let row = all_codewords.iter().map(|f| f[*idx]).collect::>(); + answers[i] = row; + } + answers + }; + + Ok(ProximityOutput { + auth_0, + auth_j, + shift_query_answers, + }) +} + +/// Verify the proximity (shift-query) openings. +/// +/// - `auth_0` opens the fresh PESAT tree at each query; expected leaves are +/// the `l2..` slice of each `shift_query_answers` row (the fresh chunk). +/// - `auth_j` opens each of `l2` accumulated trees at each query; expected +/// leaf for accumulator `i` at query `j` is `shift_query_answers[j][i]`. +#[allow(clippy::too_many_arguments)] +#[tracing::instrument( + name = "proximity.verify", + skip_all, + fields(t = t, l2 = l2) +)] +pub(crate) fn verify( + queries: &QueryIndices, + rt_0: &MT::InnerDigest, + l2_roots: &[MT::InnerDigest], + auth_0: &[Path], + auth_j: &[Vec>], + shift_query_answers: &[Vec], + mt_leaf_hash_params: &::Parameters, + mt_two_to_one_hash_params: &::Parameters, + l2: usize, + t: usize, +) -> Result<(), VerifierError> +where + F: Field, + MT: Config, +{ + (shift_query_answers.len() == t).ok_or_err(VerifierError::NumShiftQueries)?; + + for (i, path) in auth_0.iter().enumerate() { + (path.leaf_index == queries.leaf_positions[i]) + .ok_or_err(VerifierError::ShiftQueryIndex)?; + + let is_valid = path.verify( + mt_leaf_hash_params, + mt_two_to_one_hash_params, + rt_0, + &shift_query_answers[i][l2..], // leaves are evaluations of the l1 codewords + )?; + is_valid.ok_or_err(VerifierError::ShiftQuery)?; + } + + (auth_j.len() == l2).ok_or_err(VerifierError::NumL2Instances)?; + for (i, paths) in auth_j.iter().enumerate() { + (paths.len() == t).ok_or_err(VerifierError::NumShiftQueries)?; + let root = &l2_roots[i]; + for (j, path) in paths.iter().enumerate() { + (path.leaf_index == queries.leaf_positions[j]) + .ok_or_err(VerifierError::ShiftQueryIndex)?; + let is_valid = path.verify( + mt_leaf_hash_params, + mt_two_to_one_hash_params, + root, + [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..779ee8f --- /dev/null +++ b/src/protocol/phases/twin_constraint.rs @@ -0,0 +1,242 @@ +//! 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)`. + +use ark_ff::{Field, PrimeField}; +use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial, Polynomial}; +use efficient_sumcheck::{ + coefficient_sumcheck::{coefficient_sumcheck, RoundPolyEvaluator}, + folding::protogalaxy, + hypercube::Hypercube, + order_strategy::AscendingOrder, +}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; + +use crate::protocol::oracle::Oracle; +use crate::relations::r1cs::R1CSConstraints; +use crate::types::AccumulatorInstance; +use crate::utils::{concat_slices, poly::eq_poly}; +use ark_crypto_primitives::merkle_tree::Config; + +/// 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]) +} + +/// Round-polynomial evaluator fusing α-fold, β-fold, and τ-linear into a +/// single sumcheck pass. See `mod2_structured_sumcheck.tex` (stub) — the +/// fusion will be promoted to a paper-level primitive in Plan B'. +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]; + + // 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; + } + } +} + +/// Output of the twin-constraint sumcheck. All oracles / vectors are the +/// reduced claim state consumed by downstream phases (OOD, batching, final +/// target). +pub struct TwinConstraintOutput { + /// Reduced codeword oracle `f` — consumed by OOD, batching, proximity. + pub f: Oracle, + /// Reduced witness vector `z = (x, w)` — consumed by η evaluation. + pub z: Vec, + /// New code evaluation point `ζ₀` — becomes the new accumulator α. + pub zeta_0: Vec, + /// Reduced τ — becomes the τ component of the new accumulator β. + pub beta_tau: Vec, +} + +/// Run the twin-constraint sumcheck prover. +/// +/// Takes codeword slices by reference so the caller (orchestrator) can hand +/// them to [`proximity::prove`](super::proximity::prove) after this phase +/// returns. `fresh_taus` and `acc_instance` are consumed into the sumcheck +/// tables — neither is needed downstream. +#[allow(clippy::too_many_arguments)] +#[tracing::instrument( + name = "twin_constraint", + skip_all, + fields(log_l = log_l, log_m = log_m, log_n = log_n) +)] +pub fn prove( + prover_state: &mut ProverState, + fresh_codewords: &[Vec], + fresh_taus: Vec>, + acc_instance: AccumulatorInstance, + acc_witness_f: &[Vec], + acc_witness_w: &[Vec], + instances: &[Vec], + witnesses: &[Vec], + r1cs: &R1CSConstraints, + log_l: usize, + log_m: usize, + log_n: usize, +) -> TwinConstraintOutput +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + MT: Config + From<[u8; 32]>>, +{ + let l1 = fresh_codewords.len(); + + // 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 = Hypercube::::new(log_l) + .map(|(index, _point)| eq_poly(&tau, index)) + .collect::>(); + + let alpha_vecs = concat_slices(&acc_instance.alpha, &vec![vec![F::zero(); log_n]; l1]); + + let z_vecs: Vec> = acc_instance + .beta + .1 + .iter() + .zip(acc_witness_w) + .chain(instances.iter().zip(witnesses)) + .map(|(x, w)| concat_slices(x, w)) + .collect(); + + let beta_vecs: Vec> = acc_instance.beta.0.into_iter().chain(fresh_taus).collect(); + + let mut tablewise = [ + concat_slices(acc_witness_f, fresh_codewords), // u + z_vecs, // z + alpha_vecs, // a + beta_vecs, // b + ]; + let mut pw = [tau_eq_evals]; // tau + + let degree = 1 + (log_n + 1).max(log_m + 2); + let evaluator = TwinConstraintEvaluator { + r1cs, + omega, + degree, + }; + + // c. run the sumcheck + let sc = { + let _s = tracing::info_span!("twin_constraint.sumcheck").entered(); + coefficient_sumcheck(&evaluator, &mut tablewise, &mut pw, log_l, prover_state) + }; + debug_assert_eq!(sc.verifier_messages.len(), log_l); + + // d. pop reduced tables — each group has one table left after log_l rounds + let [mut u_red, mut z_red, mut a_red, mut b_red] = tablewise; + let f = u_red.pop().unwrap(); + let z = z_red.pop().unwrap(); + let zeta_0 = a_red.pop().unwrap(); + let beta_tau = b_red.pop().unwrap(); + + TwinConstraintOutput { + f: Oracle::from_evals(f), + z, + zeta_0, + beta_tau, + } +} + +/// Reduce the twin-constraint sumcheck's per-round coefficient messages +/// against the verifier challenges `γ`. Returns the final reduced target. +/// +/// The prover sends only `d` coefficients per round; the leading coefficient +/// `c_d` is derived from the round claim `T = 2·c_0 + c_1 + … + c_d` so +/// `c_d = T − 2·c_0 − c_1 − … − c_{d−1}`. Matches the encoding in +/// `src/protocol/transcript/verifier.rs::derive_randomness`. +#[tracing::instrument(name = "twin_constraint.verify", skip_all)] +pub(crate) fn verify_claim( + sigma_1: F, + coeffs_per_round: Vec>, + gamma: &[F], +) -> F +where + F: Field, +{ + let mut target = sigma_1; + for (mut coeffs, g) in coeffs_per_round.into_iter().zip(gamma) { + let partial_sum: F = coeffs.iter().skip(1).copied().sum(); + let leading = target - coeffs[0].double() - partial_sum; + coeffs.push(leading); + let h = DensePolynomial::from_coefficients_vec(coeffs); + target = h.evaluate(g); + } + target +} From 2a5379ba5d504df5fbc61e9fe88175f9b02e56fe Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sat, 18 Apr 2026 13:50:05 +0200 Subject: [PATCH 10/21] =?UTF-8?q?Plan=20O:=20structured=20observability=20?= =?UTF-8?q?=E2=80=94=20op=20counters,=20cpu=5Fns,=20JSON=20layer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructures src/profile.rs into a module directory with four parts, all gated behind the existing `profile` cargo feature: - counters: thread-local Cell counters with a `count_ops!` macro. Snapshots + deltas let a subscriber diff across a span's lifetime. Tracks coarse call-site events (MerkleTreeBuilds, MerklePathsGenerated, MleMaterializations, OracleLeafQueries, OraclePointQueries, TwinConstraintRounds, BatchingRounds, OodPointQueries, EncodeCalls, MerklePathsVerified). Field-level op counts are out of scope — they need an F newtype or an arkworks fork; deferred. - timing: `thread_cpu_ns()` via clock_gettime(CLOCK_THREAD_CPUTIME_ID) on Linux and macOS. Distinguishes blocked-on-IO from blocked-on-compute in a way wall-time cannot. - rss: `peak_rss_bytes()` via getrusage(RUSAGE_SELF), normalising the Linux-kB vs macOS-bytes discrepancy. - layer: a tracing `Layer` that captures a span's counters + timing + rss on enter, differences them on close, and emits one newline- delimited JSON record per span. Schema tag `warp.profile.v1`; per-record fields are {phase, wall_ns, cpu_ns, rss_delta_bytes, counters, dimensions}. Dimensions come from span numeric fields (log_l, log_m, log_n, etc.), captured via a tracing Visit. Phase modules and the Oracle are instrumented at call sites so the JSON records carry meaningful counter deltas. Without the feature every count_ops! call and every timing/rss wrapper compiles to a no-op. tests/profile_json.rs is a feature-gated integration test that installs the JSON layer against an in-memory sink, runs a full hashchain prove, and asserts every phase emits a well-formed record. It's the reference shape Plan B's regression detector will consume. Verification matrix (all green): - cargo test (no features) : 14/14 - cargo test --features profile : 14/14 + profile_json 1/1 - cargo clippy --all-targets : clean - cargo clippy --all-targets --all-features : clean --- Cargo.toml | 12 +- src/lib.rs | 1 + src/profile.rs | 41 ----- src/profile/counters.rs | 237 +++++++++++++++++++++++++ src/profile/layer.rs | 189 ++++++++++++++++++++ src/profile/mod.rs | 84 +++++++++ src/profile/rss.rs | 42 +++++ src/profile/timing.rs | 38 ++++ src/protocol/oracle.rs | 5 + src/protocol/phases/batching.rs | 3 + src/protocol/phases/ood.rs | 2 + src/protocol/phases/pesat.rs | 3 + src/protocol/phases/proximity.rs | 8 + src/protocol/phases/twin_constraint.rs | 2 + tests/profile_json.rs | 169 ++++++++++++++++++ 15 files changed, 790 insertions(+), 46 deletions(-) delete mode 100644 src/profile.rs create mode 100644 src/profile/counters.rs create mode 100644 src/profile/layer.rs create mode 100644 src/profile/mod.rs create mode 100644 src/profile/rss.rs create mode 100644 src/profile/timing.rs create mode 100644 tests/profile_json.rs diff --git a/Cargo.toml b/Cargo.toml index 4f9e0f3..d388d68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,8 @@ spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "small efficient-sumcheck = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git", branch = "z-tech/simd_goldilocks_experimental" } 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"], optional = true } +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/dmpierre/ark-codes.git" } @@ -53,10 +54,11 @@ default = ["asm"] asm = ["ark-ff/asm"] -# Turns on a `tracing-subscriber` that emits phase timing and dimension fields -# to stderr in the same spirit as the old `[PROFILE]` eprintln! lines. -# Plan O replaces this with structured JSON + op counters. -profile = ["dep:tracing-subscriber"] +# 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" diff --git a/src/lib.rs b/src/lib.rs index 82e08a1..827273a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -197,6 +197,7 @@ impl< 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, diff --git a/src/profile.rs b/src/profile.rs deleted file mode 100644 index 51a3b17..0000000 --- a/src/profile.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Profile instrumentation helpers. -//! -//! Spans are always emitted by the phase modules (via the `tracing` crate, -//! which is zero-cost without a subscriber). Installing a subscriber that -//! renders those spans is gated on the `profile` feature so release builds -//! do not pull in `tracing-subscriber`. -//! -//! Plan O replaces the human-format subscriber below with JSON output plus -//! op-counter macros (`field_muls`, `merkle_hashes`, …) for ingestion by -//! Plan B's regression detector. - -/// Install a human-readable `tracing-subscriber` on stderr that mimics the -/// old `[PROFILE]` lines. Feature-gated: a no-op when `profile` is off. -/// -/// Safe to call multiple times; only the first call installs the global -/// subscriber. Returns `true` if this call performed the install. -#[cfg(feature = "profile")] -pub fn init() -> bool { - use tracing_subscriber::{fmt, prelude::*, EnvFilter}; - - // The default filter shows every span at INFO+; override via RUST_LOG. - 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() -} - -/// No-op when the `profile` feature is off. -#[cfg(not(feature = "profile"))] -pub fn init() -> bool { - false -} 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..3dc7baa --- /dev/null +++ b/src/profile/layer.rs @@ -0,0 +1,189 @@ +//! 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, Counter, 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); + } + // ignore counter on unused-import warning for Counter when delta is empty + let _ = Counter::ALL.len(); + } +} + +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/oracle.rs b/src/protocol/oracle.rs index 9248e47..966582f 100644 --- a/src/protocol/oracle.rs +++ b/src/protocol/oracle.rs @@ -20,6 +20,8 @@ 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 { @@ -57,13 +59,16 @@ impl Oracle { /// 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) }); diff --git a/src/protocol/phases/batching.rs b/src/protocol/phases/batching.rs index b977424..c1ee7ba 100644 --- a/src/protocol/phases/batching.rs +++ b/src/protocol/phases/batching.rs @@ -22,6 +22,7 @@ use efficient_sumcheck::{ }; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; +use crate::count_ops; use crate::protocol::oracle::Oracle; use crate::utils::poly::eq_poly; @@ -82,6 +83,8 @@ where // call efficient sumcheck for batched_constraint checks let alpha = { let _s = tracing::info_span!("batching.sumcheck").entered(); + let log_n = ark_std::log2(n) as u64; + count_ops!(BatchingRounds, log_n); inner_product_sumcheck( &mut oracle.evals().to_vec(), &mut batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), diff --git a/src/protocol/phases/ood.rs b/src/protocol/phases/ood.rs index 4e29945..591cc94 100644 --- a/src/protocol/phases/ood.rs +++ b/src/protocol/phases/ood.rs @@ -8,6 +8,7 @@ use ark_ff::{Field, PrimeField}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; +use crate::count_ops; use crate::protocol::oracle::Oracle; /// Output of the OOD phase: the flat challenge vector and the prover's @@ -32,6 +33,7 @@ where F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, { let samples_flat = prover_state.verifier_messages_vec::(s * log_n); + count_ops!(OodPointQueries, s as u64); let answers = samples_flat .chunks(log_n) .map(|zeta| oracle.query_at_point(zeta)) diff --git a/src/protocol/phases/pesat.rs b/src/protocol/phases/pesat.rs index 766c93f..f65f631 100644 --- a/src/protocol/phases/pesat.rs +++ b/src/protocol/phases/pesat.rs @@ -21,6 +21,7 @@ use ark_crypto_primitives::{ use ark_ff::{Field, PrimeField}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; +use crate::count_ops; use crate::crypto::merkle::build_codeword_leaves; use crate::error::ProverError; use crate::types::PesatOutput; @@ -48,6 +49,7 @@ where // a. encode witnesses let (codewords, leaves) = { let _s = tracing::info_span!("pesat.encode").entered(); + count_ops!(EncodeCalls, witnesses.len() as u64); build_codeword_leaves(code, witnesses, l1) }; @@ -57,6 +59,7 @@ where // c. commit to witnesses let td_0 = { let _s = tracing::info_span!("pesat.merkle_commit").entered(); + count_ops!(MerkleTreeBuilds); MerkleTree::::new( mt_leaf_hash_params, mt_two_to_one_hash_params, diff --git a/src/protocol/phases/proximity.rs b/src/protocol/phases/proximity.rs index e1e2876..814658e 100644 --- a/src/protocol/phases/proximity.rs +++ b/src/protocol/phases/proximity.rs @@ -16,6 +16,7 @@ use ark_crypto_primitives::{ }; use ark_ff::Field; +use crate::count_ops; use crate::crypto::merkle::compute_auth_paths; use crate::error::VerifierError; use crate::protocol::query::QueryIndices; @@ -55,11 +56,16 @@ where { let auth_0 = { let _s = tracing::info_span!("proximity.auth_0").entered(); + count_ops!(MerklePathsGenerated, queries.leaf_positions.len() as u64); compute_auth_paths(td_0, &queries.leaf_positions)? }; let auth_j = { let _s = tracing::info_span!("proximity.auth_j").entered(); + count_ops!( + MerklePathsGenerated, + (acc_td.len() * queries.leaf_positions.len()) as u64 + ); acc_td .iter() .map(|td| compute_auth_paths(td, &queries.leaf_positions)) @@ -118,6 +124,7 @@ where (path.leaf_index == queries.leaf_positions[i]) .ok_or_err(VerifierError::ShiftQueryIndex)?; + count_ops!(MerklePathsVerified); let is_valid = path.verify( mt_leaf_hash_params, mt_two_to_one_hash_params, @@ -134,6 +141,7 @@ where for (j, path) in paths.iter().enumerate() { (path.leaf_index == queries.leaf_positions[j]) .ok_or_err(VerifierError::ShiftQueryIndex)?; + count_ops!(MerklePathsVerified); let is_valid = path.verify( mt_leaf_hash_params, mt_two_to_one_hash_params, diff --git a/src/protocol/phases/twin_constraint.rs b/src/protocol/phases/twin_constraint.rs index 779ee8f..80ef2b1 100644 --- a/src/protocol/phases/twin_constraint.rs +++ b/src/protocol/phases/twin_constraint.rs @@ -27,6 +27,7 @@ use efficient_sumcheck::{ }; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; +use crate::count_ops; use crate::protocol::oracle::Oracle; use crate::relations::r1cs::R1CSConstraints; use crate::types::AccumulatorInstance; @@ -195,6 +196,7 @@ where // c. run the sumcheck let sc = { let _s = tracing::info_span!("twin_constraint.sumcheck").entered(); + count_ops!(TwinConstraintRounds, log_l as u64); coefficient_sumcheck(&evaluator, &mut tablewise, &mut pw, log_l, prover_state) }; debug_assert_eq!(sc.verifier_messages.len(), log_l); diff --git a/tests/profile_json.rs b/tests/profile_json.rs new file mode 100644 index 0000000..0e6bbf2 --- /dev/null +++ b/tests/profile_json.rs @@ -0,0 +1,169 @@ +//! 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.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}" + ); +} From dfbab4a2c9b1ce676e62c9d2838e6e5146f7933e Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sat, 18 Apr 2026 14:03:46 +0200 Subject: [PATCH 11/21] Plan B v1: iai-callgrind bench harness + Mac-Docker wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a deterministic instruction-count bench for WARP::prove, intended as the regression signal for a future CI gate. Complements the existing criterion wall-time bench (which stays informational). - benches/iai_phases.rs: one library_benchmark for `prove` at the unit-test parameter shape (l1=4, s=2, t=7, hashchain=10). v1 scope is intentionally narrow — the docstring explains why the parameterised `#[bench::case(setup = ...)]` form didn't compile under iai-callgrind 0.14 from this crate's bench root. Plumbing more sizes is deferred. - Cargo.toml: iai-callgrind 0.14 as a dev-dep, second `[[bench]]` entry wired with `harness = false`. - benches/docker/Dockerfile.iai: slim Debian image with valgrind + the iai-callgrind-runner binary pre-installed, for macOS hosts where valgrind has been unsupported since Big Sur. - Makefile: `bench-wall` (criterion, any host), `bench-ci` (iai natively, Linux/valgrind required), `bench-ci-local` (builds and runs the Docker image with cargo registry/git/target caches mounted from target/iai-docker-cache/ so arkworks isn't redownloaded each run). Plus `test` and `clippy` convenience targets. - benches/README.md: explains the split (noisy wall time vs deterministic instruction count), installation, Docker pathway, and v1 scope limits. Deferred (tracked in the plan): multiple parameter points, baseline capture + commit, and the GitHub Actions workflow that gates PRs. Those come as small separate commits so the CI change can be reviewed on its own. Verification: - cargo check / --features profile : clean - cargo build --bench iai_phases : clean (run needs valgrind) - cargo build --bench warp_rs : clean - cargo clippy --all-features : clean - cargo test : 14/14 (unchanged) --- Cargo.toml | 11 ++++ Makefile | 48 +++++++++++++++ benches/README.md | 62 +++++++++++++++++++ benches/docker/Dockerfile.iai | 26 ++++++++ benches/iai_phases.rs | 108 ++++++++++++++++++++++++++++++++++ 5 files changed, 255 insertions(+) create mode 100644 Makefile create mode 100644 benches/README.md create mode 100644 benches/docker/Dockerfile.iai create mode 100644 benches/iai_phases.rs diff --git a/Cargo.toml b/Cargo.toml index d388d68..5f0a9bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,11 @@ spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "small ark-bls12-381 = "0.5.0" ark-bn254 = "0.5.0" criterion = "0.8" +# Deterministic instruction-count benches. Requires valgrind + the +# `iai-callgrind-runner` binary at runtime (see benches/README.md). +# Unused on macOS except via Docker — `cargo build --bench iai_phases` +# still works everywhere, only `cargo bench` fails without valgrind. +iai-callgrind = "0.14" [features] default = ["asm"] @@ -63,3 +68,9 @@ profile = ["dep:tracing-subscriber", "dep:libc"] [[bench]] name = "warp_rs" harness = false + +# iai-callgrind: deterministic instruction counts per prove phase. Runs +# under valgrind; use `make bench-ci-local` on macOS to run inside Docker. +[[bench]] +name = "iai_phases" +harness = false diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..91be7a9 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +# Convenience targets for Plan B (benchmarking + regression detection). +# Plan 0 / O deliverables are consumed via cargo directly — no make target +# needed. + +.PHONY: help test clippy bench-wall bench-ci bench-ci-local bench-docker-build + +help: + @echo "Targets:" + @echo " test cargo test (both feature configs)" + @echo " clippy cargo clippy --all-targets --all-features" + @echo " bench-wall criterion wall-time benches (runs anywhere)" + @echo " bench-ci iai-callgrind instruction-count benches (Linux native)" + @echo " bench-ci-local iai-callgrind benches inside Docker (macOS-friendly)" + +test: + cargo test + cargo test --features profile + +clippy: + cargo clippy --all-targets -- -D warnings + cargo clippy --all-targets --all-features -- -D warnings + +bench-wall: + cargo bench --bench warp_rs + +# Native iai-callgrind bench. Requires valgrind + iai-callgrind-runner +# installed on the host. Will fail on macOS (valgrind is unsupported +# since Big Sur) — use `bench-ci-local` instead. +bench-ci: + cargo bench --bench iai_phases + +IAI_IMAGE := warp-iai-bench +IAI_CACHE := $(CURDIR)/target/iai-docker-cache + +bench-docker-build: + docker build -f benches/docker/Dockerfile.iai -t $(IAI_IMAGE) . + +# Run the iai bench inside a Linux container. Mounts the repo read-write +# so the target/ dir (including bench output) is reused across runs. +# Caches cargo registry + git + target in host-side directories to avoid +# re-downloading arkworks on every invocation. +bench-ci-local: bench-docker-build + mkdir -p $(IAI_CACHE)/registry $(IAI_CACHE)/git + docker run --rm \ + -v $(CURDIR):/workspace \ + -v $(IAI_CACHE)/registry:/usr/local/cargo/registry \ + -v $(IAI_CACHE)/git:/usr/local/cargo/git \ + $(IAI_IMAGE) diff --git a/benches/README.md b/benches/README.md new file mode 100644 index 0000000..7b0f118 --- /dev/null +++ b/benches/README.md @@ -0,0 +1,62 @@ +# Warp benches + +Two bench suites with different roles: + +| Bench | Signal | Scope | Cost | +|--------------------------|--------------------------|----------------------------|------------------| +| `warp_rs` (criterion) | Wall time, noisy | Whole `prove` at 5 sizes | Fast, informational | +| `iai_phases` (iai-callgrind) | Instruction count, deterministic | Whole `prove` at 1 size (v1) | Slow, CI gate | + +The criterion suite is for local feedback — it reports wall time in +milliseconds, which is human-intuitive but varies across machines and +loads. It runs natively on any host. + +The iai-callgrind suite is for **regression detection**. Callgrind counts +executed instructions, so the number is reproducible across CI runs. A +1% change is real signal. Plan B uses it as the PR gate. + +## Running + +### Criterion (works on macOS, Linux, Windows) + +```bash +make bench-wall +# or +cargo bench --bench warp_rs +``` + +### iai-callgrind (native — requires valgrind) + +```bash +cargo install iai-callgrind-runner --version 0.14.0 +make bench-ci +# or +cargo bench --bench iai_phases +``` + +### iai-callgrind (macOS via Docker) + +Valgrind hasn't worked on macOS since Big Sur, so on a Mac host run the +bench inside a Linux container: + +```bash +make bench-ci-local +``` + +This builds `benches/docker/Dockerfile.iai` (cached after the first +run), mounts the repo read-write, and caches cargo registry + git in +`target/iai-docker-cache/` so subsequent runs don't re-download +arkworks. First run: ~5 min build + ~5 min bench. Later runs: ~30 s +build check + bench time. + +## v1 scope + +`iai_phases` currently benches **one** prove configuration +(`l1=4, s=2, t=7, hashchain=10`). Larger parameter points and +per-phase attribution are deferred — see the docstring at the top of +`iai_phases.rs` for why. + +Baseline instruction counts are not yet committed; the plan is to +capture them in a follow-up once the CI workflow is wired up. See +`~/.claude/plans/nested-conjuring-scott.md` → Plan B for the full +roadmap, and the TODO about a GitHub Actions workflow. diff --git a/benches/docker/Dockerfile.iai b/benches/docker/Dockerfile.iai new file mode 100644 index 0000000..ff92433 --- /dev/null +++ b/benches/docker/Dockerfile.iai @@ -0,0 +1,26 @@ +# Dockerfile for running iai-callgrind benches on hosts without valgrind +# (primarily macOS). Build + run via the repo-root Makefile: +# +# make bench-ci-local +# +# The image caches the iai-callgrind-runner binary + valgrind so repeat +# runs only re-build the warp crate. +FROM rust:1.81-slim-bookworm + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + valgrind \ + pkg-config \ + libssl-dev \ + ca-certificates \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Match the version in Cargo.toml dev-dependencies. +ARG IAI_RUNNER_VERSION=0.14.0 +RUN cargo install iai-callgrind-runner --version ${IAI_RUNNER_VERSION} --locked + +WORKDIR /workspace + +# Default: run the bench. The repo is expected to be mounted at /workspace. +CMD ["cargo", "bench", "--bench", "iai_phases"] diff --git a/benches/iai_phases.rs b/benches/iai_phases.rs new file mode 100644 index 0000000..2e9a976 --- /dev/null +++ b/benches/iai_phases.rs @@ -0,0 +1,108 @@ +//! iai-callgrind benchmarks measuring deterministic instruction counts +//! per prove call at fixed parameter points. +//! +//! Unlike [`benches/warp_rs.rs`] (criterion, wall time), these numbers +//! are reproducible across machines: callgrind counts executed +//! instructions, not time. A 1% change is real signal. +//! +//! Runs under valgrind. On macOS valgrind is not available natively — +//! use `make bench-ci-local` which invokes Docker (see benches/README.md). +//! +//! Installation: +//! cargo install iai-callgrind-runner --version 0.14.0 +//! then +//! cargo bench --bench iai_phases +//! +//! **v1 scope**: one size (l1=4, hashchain=10). Setup (encoding, +//! poseidon config build, instance gen) is measured inside the bench +//! because plumbing the `setup = expr` attribute through +//! parameterised `#[bench::...]` cases didn't resolve cleanly on +//! iai-callgrind 0.14 — the macro failed to see the target function +//! from the bench-crate root. Revisit when adding more parameter +//! points; a working pattern is either the non-parameterised form +//! below, or `iai_callgrind::BinaryBenchmarkConfig`. + +use ark_bls12_381::Fr as BLS12_381; +use ark_codes::{ + reed_solomon::{config::ReedSolomonConfig, ReedSolomon}, + traits::LinearCode, +}; +use ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig; +use ark_std::rand::thread_rng; + +use iai_callgrind::{black_box, library_benchmark, library_benchmark_group, main}; +use warp::config::WARPConfig; +use warp::relations::BundledPESAT; +use warp::traits::AccumulationScheme as _; +use warp::types::{AccumulatorInstance, AccumulatorWitness}; +use warp::WARP; + +mod utils; +use utils::domainsep::init_prover_state; +use utils::hash_chain::{get_hashchain_instance_witness_pairs, get_hashchain_r1cs}; +use utils::poseidon; + +type F = BLS12_381; + +/// Output of [`setup_prove`]: everything needed to call `warp.prove` once, +/// assembled in setup time (NOT counted in the measurement). +struct ProveInputs { + warp: WARP, ReedSolomon, Blake3MerkleConfig>, + pk: (warp::relations::r1cs::R1CS, usize, usize, usize), + prover_state: spongefish::ProverState, + instances: Vec>, + witnesses: Vec>, +} + +fn setup_prove(l: usize, s: usize, t: usize, hashchain_size: usize) -> ProveInputs { + 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); + + let warp_config = WARPConfig::new(l, l, s, t, r1cs.config(), code.code_len()); + let warp = WARP::>::new(warp_config, code, r1cs.clone(), (), ()); + + let (instances, witnesses) = + get_hashchain_instance_witness_pairs(l, &poseidon_config, hashchain_size, &mut rng); + + ProveInputs { + warp, + pk: (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), + prover_state: init_prover_state(), + instances, + witnesses, + } +} + +fn run_prove(mut inputs: ProveInputs) { + black_box( + inputs + .warp + .prove( + inputs.pk, + &mut inputs.prover_state, + inputs.witnesses, + inputs.instances, + AccumulatorInstance::empty(), + AccumulatorWitness::empty(), + ) + .unwrap(), + ); +} + +// Smallest configuration — matches the unit-test shape (l1=4, hashchain=10). +#[library_benchmark] +fn bench_prove_small() { + let inputs = setup_prove(4, 2, 7, 10); + run_prove(black_box(inputs)); +} + +library_benchmark_group!( + name = prove_benches; + benchmarks = bench_prove_small +); + +main!(library_benchmark_groups = prove_benches); From 9fb6fe179b3e92b02c08bc1ea863ee0a2808626a Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sat, 18 Apr 2026 14:23:08 +0200 Subject: [PATCH 12/21] Plan P: soundness-driven parameter selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds src/params/ — given (λ, |F|, code rate, list-decoding regime), pick the minimum (s, t) that achieves λ bits of soundness on a Reed–Solomon code. - types: Regime::{Provable, Conjectured}, SecurityLevel, Params, SoundnessBound, ParamError. SoundnessBound::meets(λ) answers the "is this enough?" question directly. - select(λ, field_bits, code_rate, regime) → Params. Uses the Johnson-bound proximity-query formula (provable: t ≥ 2λ/log₂(1/ρ), conjectured: t ≥ λ/log₂(1/ρ)), with a field-admissibility check (log₂|F| ≥ λ + 40) to ensure polylog noise terms are negligible. - validate(params, field_bits, code_rate, regime, target): the inverse — computes the achieved soundness and reports per-term admissibility so callers can see partial failures. - presets::PRESETS: canonical (λ, rate, regime) → (s, t) rows for 80/128-bit targets at common rates (1/2, 1/8). A test enforces that every row matches `select` output, so drift between the table and the formulas is caught at build time. - src/bin/warp-params.rs: dependency-free CLI with `select`, `validate`, and `table` subcommands. Dumps PRESETS as TSV and exits non-zero if a validate call doesn't meet the target. - docs/paper-mods/mod4_parameter_selection.tex: replaces the stub with the full derivation, citing STIR / WHIR for the proximity- gap bounds and marking the deferred items (batching-sumcheck calibration of s, non-RS codes, a reference table with matching proofs). The hard-coded `s=8, t=7` in warp_test is intentionally unchanged for this pass — those are functional-test shapes, not security values. A later pass can thread PRESETS through callers that care about real targets. Verification: - 23 tests pass (14 original + 9 new in params::tests) - cargo clippy, both feature configs: clean - ./target/debug/warp-params table prints PRESETS; select / validate round-trip; validate correctly rejects insufficient (s, t) --- docs/paper-mods/mod4_parameter_selection.tex | 91 ++++++-- src/bin/warp-params.rs | 232 +++++++++++++++++++ src/lib.rs | 1 + src/params/mod.rs | 148 ++++++++++++ src/params/presets.rs | 101 ++++++++ src/params/select.rs | 54 +++++ src/params/types.rs | 86 +++++++ src/params/validate.rs | 43 ++++ 8 files changed, 741 insertions(+), 15 deletions(-) create mode 100644 src/bin/warp-params.rs create mode 100644 src/params/mod.rs create mode 100644 src/params/presets.rs create mode 100644 src/params/select.rs create mode 100644 src/params/types.rs create mode 100644 src/params/validate.rs diff --git a/docs/paper-mods/mod4_parameter_selection.tex b/docs/paper-mods/mod4_parameter_selection.tex index f755d99..1ec203b 100644 --- a/docs/paper-mods/mod4_parameter_selection.tex +++ b/docs/paper-mods/mod4_parameter_selection.tex @@ -1,29 +1,90 @@ \documentclass{article} \input{notation.tex} -\title{Modification 4: \\ Soundness-driven parameter selection (stub)} +\title{Modification 4: \\ Soundness-driven parameter selection} \author{Warp paper-mods} \date{} \begin{document} \maketitle -\paragraph{Status.} Stub. Full spec authored in Plan P. Paired code: -\codemod{src/params/} (to be created). +\paragraph{Paired Rust module.} \codemod{src/params/}, with a CLI front-end +at \codemod{src/bin/warp-params.rs}. -\section*{Abstract} +\section{Motivation} -The paper states security bounds symbolically; the implementation currently -hard-codes parameter values (e.g.\ \(s = 8\), \(t = 7\)) with no derivation -record. Modification 4 introduces, both in the paper and in code, an -\emph{inverse} to the soundness analysis: given a security target -\(\lambda\), a field \(\F\), a code rate \(\rho\), and an accumulation -depth, produce the minimum-cost parameter tuple \((s, t, l, l_1)\) hitting -that target. Both list-decoding regimes (provable and conjectured) are -supported via a user toggle. +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\). -The code module \codemod{src/params/} will expose pure \texttt{select} and -\texttt{validate} functions plus a preset table to replace the ad-hoc -constants currently appearing in tests. See Plan P for the full treatment. +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/src/bin/warp-params.rs b/src/bin/warp-params.rs new file mode 100644 index 0000000..77f3508 --- /dev/null +++ b/src/bin/warp-params.rs @@ -0,0 +1,232 @@ +//! `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, regime) { + Ok(p) => { + // Prefer a preset if we happen to have one on file — lets + // users see the canonical attested row, not just a recomputed + // tuple. (The two are identical by `presets_match_select_output`.) + if let Some(preset) = lookup(lambda, rate, regime) { + println!( + "λ={} rate={:.4} regime={:?} → s={} t={} (preset)", + preset.lambda.bits(), + preset.code_rate(), + preset.regime, + preset.params.s, + preset.params.t + ); + } else { + println!( + "λ={} rate={:.4} regime={:?} → s={} t={}", + lambda.bits(), + rate, + 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, 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, +} + +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: f64 = num.parse().map_err(|e| format!("rate num `{num}`: {e}"))?; + let d: f64 = den.parse().map_err(|e| format!("rate den `{den}`: {e}"))?; + if d == 0.0 { + return Err("rate denominator must be non-zero".into()); + } + Ok(n / d) + } else { + s.parse().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/lib.rs b/src/lib.rs index 827273a..b26c788 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ pub mod config; pub mod constraints; pub mod crypto; pub mod error; +pub mod params; pub mod profile; pub mod protocol; pub mod relations; diff --git a/src/params/mod.rs b/src/params/mod.rs new file mode 100644 index 0000000..937dbc9 --- /dev/null +++ b/src/params/mod.rs @@ -0,0 +1,148 @@ +//! 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, + 0.5, + Regime::Conjectured, + ) + .unwrap(); + assert_eq!(p.params.t, 128); + } +} diff --git a/src/params/presets.rs b/src/params/presets.rs new file mode 100644 index 0000000..b62ba75 --- /dev/null +++ b/src/params/presets.rs @@ -0,0 +1,101 @@ +//! 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 `(λ, code_rate, regime)`. Returns `None` if no +/// exact row matches; use [`super::select`] for arbitrary inputs. +pub fn lookup(lambda: SecurityLevel, code_rate: f64, regime: Regime) -> Option<&'static Preset> { + PRESETS.iter().find(|p| { + p.lambda == lambda && (p.code_rate() - code_rate).abs() < 1e-9 && 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..c85e833 --- /dev/null +++ b/src/params/types.rs @@ -0,0 +1,86 @@ +//! 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, + }) +} From 695eac93cc7b960bdc18d726add2f452da5301ce Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sat, 18 Apr 2026 14:39:11 +0200 Subject: [PATCH 13/21] Plan T v1: negative-path verifier tests + Fiat-Shamir audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the two highest-value pieces of test hardening from the plan; the rest (proptest, golden serialization, xtask ref lint, runtime F-S harness) is deferred and tracked in the todo list. tests/verifier_negative.rs - Shared `make_fixture()` runs the full two-phase accumulation so l2 > 0 and every cleanly-reachable `VerifierError` variant is triggerable. - One test per tamperable field, each confirming the verifier rejects the proof with the *specific* expected error (not just "some error"): * CodeEvaluationPoint (α tamper) * CircuitEvaluationPoint (β.0 and β.1 tampers — two tests) * NumShiftQueries (truncate shift_query_answers) * ShiftQueryIndex (swap auth_0 paths) * ShiftQuery (tamper shift_query_answer value) * NumL2Instances (truncate auth_j) * Target (tamper μ) - A happy-path test keeps the fixture honest. - Docstring enumerates the variants we did NOT reach through single-field tampering (SpongeFish/ArkError wrap lower-level errors; NumSumcheckRounds is transcript-derived; SumcheckRound is unraised in current code). docs/audits/fiat_shamir.md - Ordered, line-for-line mapping of every prover-side transcript write to its verifier-side read. 25 steps, each with file:line links on both sides. - A "what reviewers should spot-check" section calls out the specific squeeze-before-absorb patterns F-S-soundness bugs tend to take. - Scope: this is a **manual** audit (compensating control). The runtime ordering harness that would make drift undetectable at CI time is deferred because it requires instrumenting spongefish; noted explicitly at the bottom of the doc. Verification: - 23/23 unit tests + 9/9 negative-path tests pass - cargo clippy, both feature configs: clean --- docs/audits/fiat_shamir.md | 89 ++++++++++++ tests/verifier_negative.rs | 281 +++++++++++++++++++++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 docs/audits/fiat_shamir.md create mode 100644 tests/verifier_negative.rs 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/tests/verifier_negative.rs b/tests/verifier_negative.rs new file mode 100644 index 0000000..fe44b3e --- /dev/null +++ b/tests/verifier_negative.rs @@ -0,0 +1,281 @@ +//! 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.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.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.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"); +} From e6452ec978ba02e09323a764fd7773a6e2274a49 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sat, 18 Apr 2026 15:00:48 +0200 Subject: [PATCH 14/21] Cleanup after Plans 0 / O / B / P / T MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five small fixups called out during the Plan T post-mortem. No behaviour changes — all tests keep passing on both feature configs. 1. Move the big `warp_test` / `warp_test_goldilocks` suites from `src/lib.rs` into `tests/integration_warp.rs`. They're end-to-end prove / verify / decide runs; keeping them as inline unit tests kept the file ~340 lines longer than necessary. `src/lib.rs` is now 500 lines (down from 844); the test content is unchanged. 2. Normalise phase-fn visibility to `pub`. Before, `pesat::prove` was `pub(crate)` while the other four phases' `prove` / `verify` / `verify_claim` entry points were `pub`. All output structs (`Oracle`, `OodOutput`, `BatchingOutput`, etc.) are already `pub`, so there was no reason for `pesat` to be the odd one out. `PesatOutput` promoted from `pub(crate)` → `pub` for the same reason. 3. Cross-reference lint audit — verified every module in `src/params`, `src/protocol/phases`, plus `src/protocol/oracle.rs` and `src/bin/warp-params.rs`, carries a doc-comment reference to its paired `docs/paper-mods/modN_*.tex`. No drift. 4. `presets::lookup` now takes an exact `(num, den)` pair instead of `f64` with an epsilon-compare. Preserves the caller's intent (`1/2` is distinct from `0.5`); the CLI plumbs a small `Rate` enum through parsing so ratios hit the preset table while bare decimals fall through to the computed-value branch. 5. Remove the `let _ = Counter::ALL.len()` silencer in `profile/layer.rs`. The import it was suppressing was an artifact of an earlier iteration and isn't needed now; dropping it and the `Counter` import lets clippy stay clean. Verification: - cargo test : 33/33 pass (22 unit + 2 integ + 9 negative) - cargo test --features profile : 34/34 pass (adds profile_json) - cargo clippy --all-features -Dwarnings : clean - warp-params select verified with both `1/2` and `0.5` inputs --- src/bin/warp-params.rs | 59 +++-- src/lib.rs | 344 ------------------------ src/params/mod.rs | 13 +- src/params/presets.rs | 15 +- src/profile/layer.rs | 4 +- src/protocol/phases/batching.rs | 2 +- src/protocol/phases/pesat.rs | 2 +- src/protocol/phases/proximity.rs | 2 +- src/protocol/phases/twin_constraint.rs | 2 +- src/types.rs | 2 +- tests/integration_warp.rs | 350 +++++++++++++++++++++++++ 11 files changed, 417 insertions(+), 378 deletions(-) create mode 100644 tests/integration_warp.rs diff --git a/src/bin/warp-params.rs b/src/bin/warp-params.rs index 77f3508..71d3662 100644 --- a/src/bin/warp-params.rs +++ b/src/bin/warp-params.rs @@ -65,16 +65,18 @@ fn cmd_select(args: &[String]) -> ExitCode { return ExitCode::from(2); } }; - match select(lambda, field_bits, rate, regime) { + match select(lambda, field_bits, rate.as_f64(), regime) { Ok(p) => { - // Prefer a preset if we happen to have one on file — lets - // users see the canonical attested row, not just a recomputed - // tuple. (The two are identical by `presets_match_select_output`.) - if let Some(preset) = lookup(lambda, rate, regime) { + // 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={:.4} regime={:?} → s={} t={} (preset)", + "λ={} rate={}/{} regime={:?} → s={} t={} (preset)", preset.lambda.bits(), - preset.code_rate(), + preset.code_rate_num, + preset.code_rate_den, preset.regime, preset.params.s, preset.params.t @@ -83,7 +85,7 @@ fn cmd_select(args: &[String]) -> ExitCode { println!( "λ={} rate={:.4} regime={:?} → s={} t={}", lambda.bits(), - rate, + rate.as_f64(), regime, p.s, p.t @@ -123,7 +125,7 @@ fn cmd_validate(args: &[String]) -> ExitCode { } }; let params = Params { s, t }; - match validate(¶ms, field_bits, rate, regime, lambda) { + match validate(¶ms, field_bits, rate.as_f64(), regime, lambda) { Ok(bound) => { println!( "proximity_bits={:.2} field_admissible={} ood_admissible={} meets_target={}", @@ -164,13 +166,36 @@ fn cmd_table() -> ExitCode { #[derive(Default)] struct Flags { lambda: Option, - rate: 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(); @@ -195,16 +220,18 @@ fn parse_u32(s: &str) -> Result { s.parse().map_err(|e| format!("expected u32, got `{s}`: {e}")) } -fn parse_rate(s: &str) -> Result { +fn parse_rate(s: &str) -> Result { if let Some((num, den)) = s.split_once('/') { - let n: f64 = num.parse().map_err(|e| format!("rate num `{num}`: {e}"))?; - let d: f64 = den.parse().map_err(|e| format!("rate den `{den}`: {e}"))?; - if d == 0.0 { + 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(n / d) + Ok(Rate::Ratio { num: n, den: d }) } else { - s.parse().map_err(|e| format!("rate `{s}`: {e}")) + s.parse::() + .map(Rate::Float) + .map_err(|e| format!("rate `{s}`: {e}")) } } diff --git a/src/lib.rs b/src/lib.rs index b26c788..3564783 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -498,347 +498,3 @@ impl< } } -#[cfg(test)] -pub mod test { - use super::AccumulationScheme; - use crate::serialize::{AccInstanceSerializer, AccWitnessSerializer, ProofSerializer}; - use crate::types::{AccumulatorInstance, AccumulatorWitness}; - use crate::{ - relations::{ - r1cs::{ - hashchain::{ - compute_hash_chain, HashChainInstance, HashChainRelation, HashChainWitness, - }, - R1CS, - }, - BundledPESAT, Relation, ToPolySystem, - }, - utils::poseidon, - }; - use ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig; - - 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::, _, 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.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.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.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] - 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::, _, 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.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.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.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/src/params/mod.rs b/src/params/mod.rs index 937dbc9..fe6fb0d 100644 --- a/src/params/mod.rs +++ b/src/params/mod.rs @@ -137,12 +137,13 @@ mod tests { #[test] fn lookup_round_trips() { - let p = lookup( - SecurityLevel::STANDARD_128, - 0.5, - Regime::Conjectured, - ) - .unwrap(); + 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 index b62ba75..a9f7366 100644 --- a/src/params/presets.rs +++ b/src/params/presets.rs @@ -92,10 +92,17 @@ pub const PRESETS: &[Preset] = &[ }, ]; -/// Look up a preset by `(λ, code_rate, regime)`. Returns `None` if no -/// exact row matches; use [`super::select`] for arbitrary inputs. -pub fn lookup(lambda: SecurityLevel, code_rate: f64, regime: Regime) -> Option<&'static Preset> { +/// 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() - code_rate).abs() < 1e-9 && p.regime == regime + p.lambda == lambda && p.code_rate_num == num && p.code_rate_den == den && p.regime == regime }) } diff --git a/src/profile/layer.rs b/src/profile/layer.rs index 3dc7baa..98000ea 100644 --- a/src/profile/layer.rs +++ b/src/profile/layer.rs @@ -34,7 +34,7 @@ use tracing_subscriber::layer::Context; use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::Layer; -use crate::profile::counters::{self, Counter, Snapshot}; +use crate::profile::counters::{self, Snapshot}; use crate::profile::{rss, timing}; /// What we stash on each span at enter-time. @@ -165,8 +165,6 @@ where if let Ok(mut w) = self.writer.lock() { let _ = w.write_all(&out); } - // ignore counter on unused-import warning for Counter when delta is empty - let _ = Counter::ALL.len(); } } diff --git a/src/protocol/phases/batching.rs b/src/protocol/phases/batching.rs index c1ee7ba..ab3c44d 100644 --- a/src/protocol/phases/batching.rs +++ b/src/protocol/phases/batching.rs @@ -103,7 +103,7 @@ where /// `[a, b]` where `h(X) = a·(1-2X) + b·X + (prev_target - b)·X²`; the /// caller's `a, b` unpacking mirrors `src/protocol/transcript/verifier.rs`. #[tracing::instrument(name = "batching.verify", skip_all)] -pub(crate) fn verify_claim(sigma_2: F, sums_per_round: Vec<[F; 2]>, alpha: &[F]) -> F +pub fn verify_claim(sigma_2: F, sums_per_round: Vec<[F; 2]>, alpha: &[F]) -> F where F: Field, { diff --git a/src/protocol/phases/pesat.rs b/src/protocol/phases/pesat.rs index f65f631..dad8f6f 100644 --- a/src/protocol/phases/pesat.rs +++ b/src/protocol/phases/pesat.rs @@ -32,7 +32,7 @@ use crate::types::PesatOutput; skip_all, fields(l1 = l1, log_m = log_m, n_witnesses = witnesses.len()) )] -pub(crate) fn prove( +pub fn prove( prover_state: &mut ProverState, code: &C, mt_leaf_hash_params: &::Parameters, diff --git a/src/protocol/phases/proximity.rs b/src/protocol/phases/proximity.rs index 814658e..280df70 100644 --- a/src/protocol/phases/proximity.rs +++ b/src/protocol/phases/proximity.rs @@ -102,7 +102,7 @@ where skip_all, fields(t = t, l2 = l2) )] -pub(crate) fn verify( +pub fn verify( queries: &QueryIndices, rt_0: &MT::InnerDigest, l2_roots: &[MT::InnerDigest], diff --git a/src/protocol/phases/twin_constraint.rs b/src/protocol/phases/twin_constraint.rs index 80ef2b1..d2f652a 100644 --- a/src/protocol/phases/twin_constraint.rs +++ b/src/protocol/phases/twin_constraint.rs @@ -224,7 +224,7 @@ where /// `c_d = T − 2·c_0 − c_1 − … − c_{d−1}`. Matches the encoding in /// `src/protocol/transcript/verifier.rs::derive_randomness`. #[tracing::instrument(name = "twin_constraint.verify", skip_all)] -pub(crate) fn verify_claim( +pub fn verify_claim( sigma_1: F, coeffs_per_round: Vec>, gamma: &[F], diff --git a/src/types.rs b/src/types.rs index dbb8de7..4db018d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -104,7 +104,7 @@ pub struct WARPProof { /// Intermediate output of the PESAT reduction phase. /// /// This data flows from Phase 2 (PESAT Reduction) into Phase 3 (Constrained Code Accumulation). -pub(crate) struct PesatOutput { +pub struct PesatOutput { /// Encoded codewords from fresh witnesses. pub codewords: Vec>, /// Merkle tree over the interleaved codeword leaves. diff --git a/tests/integration_warp.rs b/tests/integration_warp.rs new file mode 100644 index 0000000..98c2cb5 --- /dev/null +++ b/tests/integration_warp.rs @@ -0,0 +1,350 @@ +//! 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.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.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.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.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.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.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()); +} From 8b6e66a1ebb1e5ed9124a3c198cddd0c67d36f2e Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sat, 18 Apr 2026 15:02:05 +0200 Subject: [PATCH 15/21] CHANGELOG: document sumcheck communication-reduction work Captures the changelog entries that were uncommitted in the working tree at the start of the Plan 0 / O / B / P / T refactor session. Describes the already-landed sumcheck work in the preceding five commits (a66f122 .. 312e220): inner-product sumcheck 2-coefficient round messages, RoundPolyEvaluator adoption, twin-constraint coefficient count reduction, and the ark-ff rev pin. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) 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. From 28ab1805a3a045d8bd1154b59d4dd49fc9eaa8fe Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sat, 18 Apr 2026 15:03:08 +0200 Subject: [PATCH 16/21] gitignore: widen from .claude/settings.local.json to .claude/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers every personal Claude Code artefact (custom agents, plans, skills, settings.json, settings.local.json) for this repo — not just the local-settings file that was already ignored. Nothing under .claude/ has ever been tracked here, so this is a purely belt-and-suspenders tightening: the next `git add .` can't pick up anything from the directory. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1857d77..d6cf5e8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ /Cargo.lock .vscode .DS_Store -.claude/settings.local.json \ No newline at end of file +.claude/ \ No newline at end of file From f36d25866bd90c2ad71ae4fc391ad10c1487d031 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:44:46 +0200 Subject: [PATCH 17/21] integrate new effsc --- Cargo.toml | 2 +- benches/utils/domainsep.rs | 2 +- examples/profile_phases.rs | 212 ++++++++++++++++++++ src/bin/warp-params.rs | 26 +-- src/error.rs | 20 ++ src/lib.rs | 256 ++++++++++++++----------- src/params/mod.rs | 6 +- src/params/types.rs | 9 +- src/protocol/phases/batching.rs | 90 ++++++--- src/protocol/phases/proximity.rs | 3 +- src/protocol/phases/twin_constraint.rs | 72 +++---- src/protocol/transcript/mod.rs | 26 +++ src/protocol/transcript/verifier.rs | 72 +++---- src/relations/r1cs/mod.rs | 7 +- src/utils/poly.rs | 28 +-- tests/integration_warp.rs | 51 ++--- tests/profile_json.rs | 37 ++-- tests/verifier_negative.rs | 20 +- 18 files changed, 616 insertions(+), 323 deletions(-) create mode 100644 examples/profile_phases.rs diff --git a/Cargo.toml b/Cargo.toml index 5f0a9bb..5b4084b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ serde_json = "1.0" spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "smallfp-support", features = [ "ark-ff", ] } -efficient-sumcheck = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git", branch = "z-tech/simd_goldilocks_experimental" } +effsc = { git = "https://github.com/compsec-epfl/efficient-sumcheck.git", branch = "z-tech/rewrite-v2" } 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 } diff --git a/benches/utils/domainsep.rs b/benches/utils/domainsep.rs index 9c537ab..5ef1355 100644 --- a/benches/utils/domainsep.rs +++ b/benches/utils/domainsep.rs @@ -2,5 +2,5 @@ use spongefish::ProverState; pub fn init_prover_state() -> ProverState { let domainsep = spongefish::domain_separator!("warp::rs"); - domainsep.instance(&0u32).std_prover() + domainsep.without_session().instance(&0u32).std_prover() } 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 index 71d3662..2015992 100644 --- a/src/bin/warp-params.rs +++ b/src/bin/warp-params.rs @@ -15,9 +15,7 @@ use std::process::ExitCode; -use warp::params::{ - lookup, select, validate, ParamError, Params, Regime, SecurityLevel, PRESETS, -}; +use warp::params::{lookup, select, validate, ParamError, Params, Regime, SecurityLevel, PRESETS}; fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); @@ -58,13 +56,14 @@ fn cmd_select(args: &[String]) -> ExitCode { 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); - } - }; + 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 — @@ -120,7 +119,9 @@ fn cmd_validate(args: &[String]) -> ExitCode { (s, t, SecurityLevel(l), r, fb, rg) } _ => { - eprintln!("warp-params validate: need --s, --t, --lambda, --rate, --field-bits, --regime"); + eprintln!( + "warp-params validate: need --s, --t, --lambda, --rate, --field-bits, --regime" + ); return ExitCode::from(2); } }; @@ -217,7 +218,8 @@ fn parse_flags(args: &[String]) -> Result { } fn parse_u32(s: &str) -> Result { - s.parse().map_err(|e| format!("expected u32, got `{s}`: {e}")) + s.parse() + .map_err(|e| format!("expected u32, got `{s}`: {e}")) } fn parse_rate(s: &str) -> Result { diff --git a/src/error.rs b/src/error.rs index 3f016ab..681124c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -73,6 +73,26 @@ impl From for VerifierError { } } +impl From for VerifierError { + fn from(err: effsc::proof::SumcheckError) -> Self { + use effsc::proof::SumcheckError; + match err { + // Round consistency or degree checks failed inside the library. + SumcheckError::ConsistencyCheck { .. } | SumcheckError::DegreeMismatch { .. } => { + Self::SumcheckRound + } + // 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, + // Not reachable: warp passes `noop_hook_verify`. + SumcheckError::HookError { .. } => Self::SumcheckRound, + } + } +} + #[derive(Error, Debug)] pub enum DeciderError { #[error("Invalid merkle root")] diff --git a/src/lib.rs b/src/lib.rs index 3564783..3f655c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,13 +11,14 @@ use ark_ff::{Field, PrimeField}; use ark_poly::{DenseMultilinearExtension, Polynomial}; use ark_std::log2; use config::WARPConfig; -use efficient_sumcheck::{ - hypercube::{compute_hypercube_eq_evals, Hypercube}, - order_strategy::AscendingOrder, +use effsc::{ + hypercube::{compute_hypercube_eq_evals, Ascending}, + verifier::sumcheck_verify, }; use protocol::query::QueryIndices; use protocol::transcript::{ - absorb_instances, derive_randomness, parse_statement, DerivedRandomness, + absorb_instances, derive_between_sumchecks, derive_pre_twin_constraint, parse_statement, + BetweenSumchecks, EffscVerifierTranscript, PreTwinConstraint, }; use relations::{r1cs::R1CSConstraints, BundledPESAT}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; @@ -283,18 +284,15 @@ impl< let (l1, l) = (self.params.config.l1, self.params.config.l); let l2 = l - l1; - //////////////////////// - // 1. Parsing phase - //////////////////////// - // a. verification key #[allow(non_snake_case)] let (M, N, k) = (vk.0, vk.1, vk.2); 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.params.config.s + self.params.config.t; + let log_r = log2(r) as usize; - // f. absorb parameters + // 1. Parse statement (fresh instances + accumulator). let ( l1_xs, AccumulatorInstance { @@ -306,153 +304,180 @@ impl< }, ) = parse_statement::(verifier_state, l1, l2, N - k, log_n, log_m)?; - //////////////////////// - // 2. Derive randomness - //////////////////////// - let DerivedRandomness { + // 2. Read PESAT phase: rt_0 + l1_mus, squeeze l1_taus, ω, τ. + let PreTwinConstraint { rt_0, l1_mus, l1_taus, omega, tau, - gamma_sumcheck, - coeffs_twinc_sumcheck, + } = derive_pre_twin_constraint::(verifier_state, l1, log_l, log_m)?; + + // 3. Twin-constraint sumcheck claim σ₁ = Σ_i τ_eq(i)·(μ_i + ω·η_i). + let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); + let etas_l2_first = 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_l2_first), + ) + .fold(F::zero(), |acc, (eq_tau, (mu, eta))| { + acc + eq_tau * (mu + omega * eta) + }); + + // 4. Run the twin-constraint sumcheck via effsc. The real oracle + // check is `final_claim == eq(τ, γ) · (ν₀ + ω·η)`, but ν₀ and η + // arrive on the transcript *after* the sumcheck rounds — so we + // capture `final_claim` in the closure and finish the check + // below after reading them. `sumcheck_verify` still enforces all + // per-round consistency (`q(0)+q(1)==claim`) automatically. + let tc_degree = 1 + (log_n + 1).max(log_m + 2); + let mut tc_final_claim = F::zero(); + let gamma_sumcheck = { + let mut wrap = EffscVerifierTranscript(verifier_state); + sumcheck_verify( + sigma_1, + tc_degree, + log_l, + &mut wrap, + |_, _| Ok(()), + |final_claim, _challenges| { + tc_final_claim = final_claim; + Ok(()) + }, + )? + }; + + // 5. Read between-sumchecks state: td, η, ν₀, OOD, shift-query + // byte challenges, ξ. + let BetweenSumchecks { td: _, eta, - mut nus, + nus: mut nus_from_transcript, ood_samples, bytes_shift_queries, xi, - alpha_sumcheck, - sums_batching_sumcheck, - } = derive_randomness::( + } = derive_between_sumchecks::( verifier_state, - l1, log_n, - log_l, self.params.config.s, self.params.config.t, - log_m, )?; - let r = 1 + self.params.config.s + self.params.config.t; - let log_r = log2(r) as usize; + // 6. Deferred twin-constraint oracle check. + (eq_poly_non_binary(&tau, &gamma_sumcheck) * (nus_from_transcript[0] + omega * eta) + == tc_final_claim) + .ok_or_err(VerifierError::Target)?; - //////////////////////// - // 3. Derive values - //////////////////////// - // b. - let alpha_vecs = concat_slices(&l2_alphas, &vec![vec![F::zero(); log_n]; l1]); + // 7. Proximity first: both the batching σ₂ and the batching oracle + // check consume `proof.shift_query_answers`, so we must reject + // malformed / tampered openings *before* feeding them into the + // sumcheck. Running proximity here preserves the existing + // negative-test error distinctions (`ShiftQuery`, `ShiftQueryIndex`, + // `NumShiftQueries`, `NumL2Instances`). + let queries: QueryIndices = + QueryIndices::from_squeezed_bytes(&bytes_shift_queries, log_n, self.params.config.t); - let gamma_eq_evals = compute_hypercube_eq_evals(log_l, &gamma_sumcheck); + proximity::verify::( + &queries, + &rt_0, + &l2_roots, + &proof.auth_0, + &proof.auth_j, + &proof.shift_query_answers, + &self.params.mt_leaf_hash_params, + &self.params.mt_two_to_one_hash_params, + l2, + self.params.config.t, + )?; + // 8. Derive everything needed to state the batching claim. + 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); - // compute \eta_{s + k} + // ν_{s+k}: inner product of the (now proximity-verified) shift-query + // answers with the γ-equality table. let mut nu_s_t = vec![F::default(); self.params.config.t]; for (i, v_jk) in proof.shift_query_answers.iter().enumerate() { - let res = v_jk + nu_s_t[i] = v_jk .iter() .zip(&gamma_eq_evals) .fold(F::zero(), |acc, (v, eq)| acc + *eq * *v); - nu_s_t[i] = res; } - - nus.extend(nu_s_t); - - // d. set \sigma^{(1)} and \sigma^{(2)} - // compute eq(\tau, i) and eq(\xi, i) - let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); - - let etas = concat_slices(&l2_etas, &vec![F::zero(); l1]); - - let sigma_1 = tau_eq_evals - .into_iter() - .zip(l2_mus.into_iter().chain(l1_mus.to_vec()).zip(etas)) - .fold(F::zero(), |acc, (eq_tau, (mu, eta))| { - acc + eq_tau * (mu + omega * eta) - }); + nus_from_transcript.extend(nu_s_t); + let nus = nus_from_transcript; 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); - //////////////////////// - // 4. Decision phase - //////////////////////// - // a. new code evaluation point - (acc_instance.alpha[0] == alpha_sumcheck).ok_or_err(VerifierError::CodeEvaluationPoint)?; + // 8. Run the batching (inner-product) sumcheck via effsc. The + // oracle check `final_claim == μ · Σ_i eq(ζ_i, α)·ξ_eq(i)` is + // self-contained and runs inside the closure — a mismatch maps + // to `VerifierError::Target`. We still need the reduced α to + // compare against `acc_instance.alpha[0]` below, so the closure + // returns Ok via a side-channel capture of the zeta-eq sum. + // + // α arrives in MSB (sumcheck round) order; the prover stored + // the LSB-reversed form in `acc_instance.alpha[0]`, so we + // reverse once for MLE/eq consistency with arkworks. + let acc_mu = acc_instance.mu[0]; + let alpha_sumcheck_msb = { + let mut wrap = EffscVerifierTranscript(verifier_state); + sumcheck_verify( + sigma_2, + 2, + log_n, + &mut wrap, + |_, _| Ok(()), + |final_claim, alpha_msb| { + let alpha_lsb: Vec = alpha_msb.iter().rev().copied().collect(); + let mut zeta_eqs = Vec::with_capacity(r); + zeta_eqs.push(eq_poly_non_binary(&zeta_0, &alpha_lsb)); + for chunk in ood_samples.chunks(log_n) { + zeta_eqs.push(eq_poly_non_binary(chunk, &alpha_lsb)); + } + for pt in &queries.evaluation_points { + zeta_eqs.push(eq_poly_non_binary(pt, &alpha_lsb)); + } + debug_assert_eq!(zeta_eqs.len(), r); + let expected = acc_mu + * zeta_eqs + .into_iter() + .zip(&xi_eq_evals) + .fold(F::zero(), |acc, (a, b)| acc + a * *b); + if expected == final_claim { + Ok(()) + } else { + Err(effsc::proof::SumcheckError::FinalEvaluation) + } + }, + )? + }; + + // 10. Accumulator consistency checks for the new code / circuit + // evaluation points. + let alpha_sumcheck_lsb: Vec = alpha_sumcheck_msb.iter().rev().copied().collect(); + (acc_instance.alpha[0] == alpha_sumcheck_lsb) + .ok_or_err(VerifierError::CodeEvaluationPoint)?; - // b. new circuit evaluation point let betas = l2_taus .into_iter() .chain(l1_taus) .zip(l2_xs.clone().into_iter().chain(l1_xs)) - .map(|(tau, x)| concat_slices(&tau, &x)) + .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.beta.0[0], &acc_instance.beta.1[0]); (expected_beta == beta).ok_or_err(VerifierError::CircuitEvaluationPoint)?; - // c. proximity: check auth paths + leaf-position consistency. - let queries: QueryIndices = - QueryIndices::from_squeezed_bytes(&bytes_shift_queries, log_n, self.params.config.t); - - proximity::verify::( - &queries, - &rt_0, - &l2_roots, - &proof.auth_0, - &proof.auth_j, - &proof.shift_query_answers, - &self.params.mt_leaf_hash_params, - &self.params.mt_two_to_one_hash_params, - l2, - self.params.config.t, - )?; - - // d. sumcheck decisions: reduce each transcript's round messages to - // a single target, then compare to the expected claim in (e). - (coeffs_twinc_sumcheck.len() == log_l).ok_or_err(VerifierError::NumSumcheckRounds)?; - let target_1 = twin_constraint::verify_claim(sigma_1, coeffs_twinc_sumcheck, &gamma_sumcheck); - - (sums_batching_sumcheck.len() == log_n).ok_or_err(VerifierError::NumSumcheckRounds)?; - let target_2 = batching::verify_claim(sigma_2, sums_batching_sumcheck, &alpha_sumcheck); - - // e. new target decision - // build eq^{\star}(\alpha) - (eq_poly_non_binary(&tau, &gamma_sumcheck) * (nus[0] + omega * eta) == target_1) - .ok_or_err(VerifierError::Target)?; - - let mut zeta_eqs = vec![eq_poly_non_binary(&zeta_0, &alpha_sumcheck)]; - - zeta_eqs.extend( - ood_samples - .chunks(log_n) - .map(|zeta| eq_poly_non_binary(zeta, &alpha_sumcheck)) - .collect::>(), - ); - zeta_eqs.extend( - queries - .evaluation_points - .iter() - .map(|zeta| eq_poly_non_binary(zeta, &alpha_sumcheck)) - .collect::>(), - ); - (zeta_eqs.len() == r).ok_or_err(VerifierError::NumShiftQueries)?; - - // mul by \mu and compare to target_2 - (acc_instance.mu[0] - * zeta_eqs - .into_iter() - .zip(xi_eq_evals) - .fold(F::zero(), |acc, (a, b)| acc + a * b) - == target_2) - .ok_or_err(VerifierError::Target)?; - Ok(()) } @@ -478,8 +503,8 @@ impl< let tau = &acc_instance.beta.0[0]; - let tau_zero_evader = Hypercube::::new(tau.len()) - .map(|(index, _point)| eq_poly(tau, index)) + let tau_zero_evader = Ascending::new(tau.len()) + .map(|p| eq_poly(tau, p.index)) .collect::>(); let mut z = acc_instance.beta.1[0].clone(); @@ -497,4 +522,3 @@ impl< Ok(()) } } - diff --git a/src/params/mod.rs b/src/params/mod.rs index fe6fb0d..401d433 100644 --- a/src/params/mod.rs +++ b/src/params/mod.rs @@ -123,10 +123,10 @@ mod tests { #[test] fn presets_match_select_output() { for preset in PRESETS { - let recomputed = - select(preset.lambda, 254, preset.code_rate(), preset.regime).unwrap(); + let recomputed = select(preset.lambda, 254, preset.code_rate(), preset.regime).unwrap(); assert_eq!( - recomputed, preset.params, + recomputed, + preset.params, "preset drift: {:?} rate={:?} regime={:?}", preset.lambda, preset.code_rate(), diff --git a/src/params/types.rs b/src/params/types.rs index c85e833..c2e73c8 100644 --- a/src/params/types.rs +++ b/src/params/types.rs @@ -65,9 +65,7 @@ pub struct SoundnessBound { 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 + self.field_admissible && self.ood_admissible && self.proximity_bits >= target.bits() as f64 } } @@ -79,8 +77,5 @@ pub enum ParamError { InvalidRate, /// Field is too small to support the target soundness even with /// infinite queries. - FieldTooSmall { - field_bits: u32, - lambda: u32, - }, + FieldTooSmall { field_bits: u32, lambda: u32 }, } diff --git a/src/protocol/phases/batching.rs b/src/protocol/phases/batching.rs index ab3c44d..3e44be1 100644 --- a/src/protocol/phases/batching.rs +++ b/src/protocol/phases/batching.rs @@ -17,15 +17,58 @@ use ark_ff::{Field, PrimeField}; use ark_std::log2; -use efficient_sumcheck::{ - accumulate_sparse_evaluations, batched_constraint_poly, inner_product_sumcheck, -}; +use effsc::{noop_hook, provers::inner_product::InnerProductProver, runner::sumcheck}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; +use std::collections::HashMap; use crate::count_ops; use crate::protocol::oracle::Oracle; use crate::utils::poly::eq_poly; +/// [CBBZ23] / HyperPlonk sparse-evaluation optimization: for shift-query +/// zetas (indices `1+s..r`), each ζ is a 0/1 vector representing a single +/// hypercube point. We accumulate the corresponding `eq_evals[i]` into a +/// sparse map keyed by that point's index. +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 +} + +/// Sum `dense_polys` column-wise and add the sparse contributions into the +/// resulting vector. Used to build the `g` side of the inner-product +/// sumcheck `∑_x f(x)·g(x)`. +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 +} + /// Output of the batching sumcheck: the reduced point `α` and the target /// `μ = \hat f(α)`. pub struct BatchingOutput { @@ -80,36 +123,27 @@ where accumulate_sparse_evaluations(zetas_prefix.to_vec(), xi_eq_evals, s, r) }; - // call efficient sumcheck for batched_constraint checks + // Run the inner-product sumcheck. `InnerProductProver` + `runner::sumcheck` + // is the new-style `SumcheckProver` entry point; wire format is three + // evaluations `[q(0), q(1), q(2)]` per round (`effsc::sumcheck_verify` + // reads them on the verifier side). The prover is MSB half-split, so the + // challenge vector arrives in MSB order — reverse once here so downstream + // MLE / eq_poly queries (arkworks' LSB-first convention) line up. let alpha = { let _s = tracing::info_span!("batching.sumcheck").entered(); - let log_n = ark_std::log2(n) as u64; - count_ops!(BatchingRounds, log_n); - inner_product_sumcheck( - &mut oracle.evals().to_vec(), - &mut batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), - prover_state, - ) - .verifier_messages + let log_n_bits = ark_std::log2(n) as u64; + count_ops!(BatchingRounds, log_n_bits); + let mut ip = InnerProductProver::new( + 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 = oracle.query_at_point(&alpha); BatchingOutput { alpha, mu } } - -/// Reduce the batching (inner-product / multilinear) sumcheck's per-round -/// messages against the verifier challenges `α`. Each round message is -/// `[a, b]` where `h(X) = a·(1-2X) + b·X + (prev_target - b)·X²`; the -/// caller's `a, b` unpacking mirrors `src/protocol/transcript/verifier.rs`. -#[tracing::instrument(name = "batching.verify", skip_all)] -pub fn verify_claim(sigma_2: F, sums_per_round: Vec<[F; 2]>, alpha: &[F]) -> F -where - F: Field, -{ - let mut target = sigma_2; - for ([a, b], x) in sums_per_round.into_iter().zip(alpha) { - target = (target - b) * x.square() + a * (F::one() - x.double()) + b * x; - } - target -} diff --git a/src/protocol/phases/proximity.rs b/src/protocol/phases/proximity.rs index 280df70..d35ad75 100644 --- a/src/protocol/phases/proximity.rs +++ b/src/protocol/phases/proximity.rs @@ -121,8 +121,7 @@ where (shift_query_answers.len() == t).ok_or_err(VerifierError::NumShiftQueries)?; for (i, path) in auth_0.iter().enumerate() { - (path.leaf_index == queries.leaf_positions[i]) - .ok_or_err(VerifierError::ShiftQueryIndex)?; + (path.leaf_index == queries.leaf_positions[i]).ok_or_err(VerifierError::ShiftQueryIndex)?; count_ops!(MerklePathsVerified); let is_valid = path.verify( diff --git a/src/protocol/phases/twin_constraint.rs b/src/protocol/phases/twin_constraint.rs index d2f652a..211fdf2 100644 --- a/src/protocol/phases/twin_constraint.rs +++ b/src/protocol/phases/twin_constraint.rs @@ -18,12 +18,10 @@ //! Each round's round polynomial has the form `h(X) = (f(X) + ω·p(X))·t(X)`. use ark_ff::{Field, PrimeField}; -use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial, Polynomial}; -use efficient_sumcheck::{ - coefficient_sumcheck::{coefficient_sumcheck, RoundPolyEvaluator}, - folding::protogalaxy, - hypercube::Hypercube, - order_strategy::AscendingOrder, +use ark_poly::{univariate::DensePolynomial, DenseUVPolynomial}; +use effsc::{ + coefficient_sumcheck::RoundPolyEvaluator, folding::protogalaxy, hypercube::Ascending, + noop_hook, provers::coefficient_lsb::CoefficientProverLSB, runner::sumcheck, }; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; @@ -161,8 +159,8 @@ where let tau = prover_state.verifier_messages_vec::(log_l); // b. assemble sumcheck tables - let tau_eq_evals = Hypercube::::new(log_l) - .map(|(index, _point)| eq_poly(&tau, index)) + let tau_eq_evals = Ascending::new(log_l) + .map(|p| eq_poly(&tau, p.index)) .collect::>(); let alpha_vecs = concat_slices(&acc_instance.alpha, &vec![vec![F::zero(); log_n]; l1]); @@ -178,13 +176,13 @@ where let beta_vecs: Vec> = acc_instance.beta.0.into_iter().chain(fresh_taus).collect(); - let mut tablewise = [ + let tablewise = vec![ concat_slices(acc_witness_f, fresh_codewords), // u z_vecs, // z alpha_vecs, // a beta_vecs, // b ]; - let mut pw = [tau_eq_evals]; // tau + let pw = vec![tau_eq_evals]; // tau let degree = 1 + (log_n + 1).max(log_m + 2); let evaluator = TwinConstraintEvaluator { @@ -193,20 +191,25 @@ where degree, }; - // c. run the sumcheck - let sc = { + // c. run the sumcheck. `CoefficientProverLSB` + `runner::sumcheck` is + // the new-style `SumcheckProver` entry point; wire format is `d+1` + // evaluations per round (the verifier uses `effsc::sumcheck_verify` on + // the other side). + let mut cc = CoefficientProverLSB::new(&evaluator, tablewise, pw); + { let _s = tracing::info_span!("twin_constraint.sumcheck").entered(); count_ops!(TwinConstraintRounds, log_l as u64); - coefficient_sumcheck(&evaluator, &mut tablewise, &mut pw, log_l, prover_state) - }; - debug_assert_eq!(sc.verifier_messages.len(), log_l); + let proof = sumcheck(&mut cc, log_l, prover_state, noop_hook); + debug_assert_eq!(proof.challenges.len(), log_l); + } - // d. pop reduced tables — each group has one table left after log_l rounds - let [mut u_red, mut z_red, mut a_red, mut b_red] = tablewise; - let f = u_red.pop().unwrap(); - let z = z_red.pop().unwrap(); - let zeta_0 = a_red.pop().unwrap(); - let beta_tau = b_red.pop().unwrap(); + // 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(); TwinConstraintOutput { f: Oracle::from_evals(f), @@ -215,30 +218,3 @@ where beta_tau, } } - -/// Reduce the twin-constraint sumcheck's per-round coefficient messages -/// against the verifier challenges `γ`. Returns the final reduced target. -/// -/// The prover sends only `d` coefficients per round; the leading coefficient -/// `c_d` is derived from the round claim `T = 2·c_0 + c_1 + … + c_d` so -/// `c_d = T − 2·c_0 − c_1 − … − c_{d−1}`. Matches the encoding in -/// `src/protocol/transcript/verifier.rs::derive_randomness`. -#[tracing::instrument(name = "twin_constraint.verify", skip_all)] -pub fn verify_claim( - sigma_1: F, - coeffs_per_round: Vec>, - gamma: &[F], -) -> F -where - F: Field, -{ - let mut target = sigma_1; - for (mut coeffs, g) in coeffs_per_round.into_iter().zip(gamma) { - let partial_sum: F = coeffs.iter().skip(1).copied().sum(); - let leading = target - coeffs[0].double() - partial_sum; - coeffs.push(leading); - let h = DensePolynomial::from_coefficients_vec(coeffs); - target = h.evaluate(g); - } - target -} diff --git a/src/protocol/transcript/mod.rs b/src/protocol/transcript/mod.rs index 2283192..fffeacb 100644 --- a/src/protocol/transcript/mod.rs +++ b/src/protocol/transcript/mod.rs @@ -3,3 +3,29 @@ 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/verifier.rs b/src/protocol/transcript/verifier.rs index 36e58d4..6e06c1e 100644 --- a/src/protocol/transcript/verifier.rs +++ b/src/protocol/transcript/verifier.rs @@ -77,36 +77,36 @@ impl< } } -pub struct DerivedRandomness { +/// 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, - pub gamma_sumcheck: Vec, - pub coeffs_twinc_sumcheck: 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 alpha_sumcheck: Vec, - pub sums_batching_sumcheck: Vec<[F; 2]>, } -pub fn derive_randomness< +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_n: usize, log_l: usize, - s: usize, - t: usize, log_m: usize, -) -> VerificationResult> { +) -> VerificationResult> { // commitment digest let rt_0_bytes: [u8; 32] = verifier_state.prover_message()?; let rt_0: MT::InnerDigest = rt_0_bytes.into(); @@ -114,7 +114,7 @@ pub fn derive_randomness< // mus let l1_mus: Vec = verifier_state.prover_messages_vec(l1)?; - // challenge taus + // challenge taus (squeezed) let l1_taus: Vec> = (0..l1) .map(|_| { (0..log_m) @@ -128,17 +128,24 @@ pub fn derive_randomness< .map(|_| verifier_state.verifier_message::()) .collect(); - // twin constraints sumcheck - let mut gamma_sumcheck = Vec::new(); - let mut coeffs_twinc_sumcheck = Vec::new(); - for _ in 0..log_l { - let h_coeffs: Vec = - verifier_state.prover_messages_vec(1 + (log_n + 1).max(log_m + 2))?; - let c: F = verifier_state.verifier_message(); - gamma_sumcheck.push(c); - coeffs_twinc_sumcheck.push(h_coeffs); - } + 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(); @@ -158,7 +165,7 @@ pub fn derive_randomness< let ood_answers: Vec = verifier_state.prover_messages_vec(s)?; nus.extend(ood_answers); - // shift queries and zero check + // 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); @@ -169,31 +176,12 @@ pub fn derive_randomness< .map(|_| verifier_state.verifier_message::()) .collect(); - // batching sumcheck - let mut alpha_sumcheck = Vec::new(); - let mut sums_batching_sumcheck = Vec::new(); - for _ in 0..log_n { - let sums: [F; 2] = verifier_state.prover_messages()?; - let c: F = verifier_state.verifier_message(); - alpha_sumcheck.push(c); - sums_batching_sumcheck.push(sums); - } - - Ok(DerivedRandomness { - rt_0, - l1_mus, - l1_taus, - omega, - tau, - gamma_sumcheck, - coeffs_twinc_sumcheck, + Ok(BetweenSumchecks { td, eta, nus, ood_samples, bytes_shift_queries, xi, - alpha_sumcheck, - sums_batching_sumcheck, }) } diff --git a/src/relations/r1cs/mod.rs b/src/relations/r1cs/mod.rs index 6377cfe..a23ee4f 100644 --- a/src/relations/r1cs/mod.rs +++ b/src/relations/r1cs/mod.rs @@ -2,7 +2,7 @@ pub mod hashchain; use ark_ff::Field; use ark_relations::gr1cs::ConstraintSystemRef; -use efficient_sumcheck::{hypercube::Hypercube, order_strategy::AscendingOrder}; +use effsc::hypercube::Ascending; use crate::error::WARPError; @@ -99,10 +99,9 @@ impl BundledPESAT for R1CS { type Constraints = R1CSConstraints; fn evaluate_bundled(&self, zero_evader_evals: &[F], z: &[F]) -> Result { - let mut cube = Hypercube::::new(self.log_m); - // TODO: multithread this - cube.try_fold(F::ZERO, |acc, (index, _point)| { + Ascending::new(self.log_m).try_fold(F::ZERO, |acc, p| { + let index = p.index; let eq_tau_i = *zero_evader_evals .get(index) .ok_or(WARPError::ZeroEvaderSize(zero_evader_evals.len(), index))?; diff --git a/src/utils/poly.rs b/src/utils/poly.rs index b477c1c..efefdde 100644 --- a/src/utils/poly.rs +++ b/src/utils/poly.rs @@ -1,19 +1,19 @@ use ark_ff::Field; -use efficient_sumcheck::{ - hypercube::HypercubeMember, interpolation::LagrangePolynomial, order_strategy::AscendingOrder, -}; -pub fn eq_poly(original_tau: &[F], point: usize) -> F { - // TODO (z-tech): will fix and get rid of this function - let num_variables = original_tau.len(); - let mut tau = original_tau.to_vec(); - tau.reverse(); - let tau_hat: Vec = tau.iter().map(|t| F::ONE - *t).collect(); - LagrangePolynomial::::lag_poly( - tau, - tau_hat, - HypercubeMember::new(num_variables, point), - ) +/// `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 { diff --git a/tests/integration_warp.rs b/tests/integration_warp.rs index 98c2cb5..17086cf 100644 --- a/tests/integration_warp.rs +++ b/tests/integration_warp.rs @@ -85,14 +85,13 @@ fn warp_test() { .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 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![]); @@ -100,7 +99,7 @@ fn warp_test() { for _ in 0..l1 { let domainsep = spongefish::domain_separator!("test::warp"); - let mut prover_state = domainsep.instance(&0u32).std_prover(); + 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), @@ -127,16 +126,15 @@ fn warp_test() { 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 hash_chain_warp = WARP::, _, Blake3MerkleConfig>::new( + warp_config.clone(), + code.clone(), + r1cs.clone(), + (), + (), + ); - let mut prover_state = domainsep.instance(&0u32).std_prover(); + 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), @@ -160,7 +158,10 @@ fn warp_test() { let narg_str = prover_state.narg_string().to_vec(); let domainsep_v = spongefish::domain_separator!("test::warp"); - let mut verifier_state = domainsep_v.instance(&0u32).std_verifier(&narg_str); + let mut verifier_state = domainsep_v + .without_session() + .instance(&0u32) + .std_verifier(&narg_str); hash_chain_warp .verify( (r1cs.m, r1cs.n, r1cs.k), @@ -207,8 +208,7 @@ fn warp_test_goldilocks() { hash_chain_size, )) .unwrap(); - let code_config = - ReedSolomonConfig::::default(r1cs.k, r1cs.k.next_power_of_two()); + 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) @@ -256,7 +256,7 @@ fn warp_test_goldilocks() { for _ in 0..l1 { let domainsep = spongefish::domain_separator!("test::warp"); - let mut prover_state = domainsep.instance(&0u32).std_prover(); + 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), @@ -293,7 +293,7 @@ fn warp_test_goldilocks() { (), ); - let mut prover_state = domainsep.instance(&0u32).std_prover(); + 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), @@ -317,7 +317,10 @@ fn warp_test_goldilocks() { let narg_str = prover_state.narg_string().to_vec(); let domainsep_v = spongefish::domain_separator!("test::warp"); - let mut verifier_state = domainsep_v.instance(&0u32).std_verifier(&narg_str); + let mut verifier_state = domainsep_v + .without_session() + .instance(&0u32) + .std_verifier(&narg_str); hash_chain_warp .verify( (r1cs.m, r1cs.n, r1cs.k), diff --git a/tests/profile_json.rs b/tests/profile_json.rs index 0e6bbf2..5a7d6ff 100644 --- a/tests/profile_json.rs +++ b/tests/profile_json.rs @@ -52,7 +52,10 @@ impl Write for SharedBuf { 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"); + 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; @@ -93,17 +96,16 @@ fn json_layer_emits_phase_records() { .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 hash_chain_warp = WARP::, _, Blake3MerkleConfig>::new( + warp_config, + code, + r1cs.clone(), + (), + (), + ); let domainsep = spongefish::domain_separator!("test::profile_json"); - let mut prover_state = domainsep.instance(&0u32).std_prover(); + let mut prover_state = domainsep.without_session().instance(&0u32).std_prover(); hash_chain_warp .prove( @@ -119,7 +121,10 @@ fn json_layer_emits_phase_records() { // 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"); + assert!( + !text.is_empty(), + "JSON sink should contain at least one record" + ); let lines: Vec<&str> = text.lines().collect(); assert!( @@ -133,8 +138,14 @@ fn json_layer_emits_phase_records() { 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#""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}" diff --git a/tests/verifier_negative.rs b/tests/verifier_negative.rs index fe44b3e..06ddef8 100644 --- a/tests/verifier_negative.rs +++ b/tests/verifier_negative.rs @@ -61,9 +61,16 @@ struct Fixture { } impl Fixture { - fn verify(&self, acc_x: AccumulatorInstance, proof: WARPProof) -> Result<(), VerifierError> { + 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.instance(&0u32).std_verifier(&self.narg_str); + 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) } } @@ -119,7 +126,7 @@ fn make_fixture() -> Fixture { for _ in 0..l1 { let ds = spongefish::domain_separator!("test::warp::negative"); - let mut ps = ds.instance(&0u32).std_prover(); + 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), @@ -146,7 +153,7 @@ fn make_fixture() -> Fixture { let warp = WARP::, _, MT>::new(warp_cfg2, code, r1cs.clone(), (), ()); let ds = spongefish::domain_separator!("test::warp::negative"); - let mut ps = ds.instance(&0u32).std_prover(); + 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), @@ -182,10 +189,7 @@ fn assert_err(result: Result<(), VerifierError>, expected: &str) { Ok(()) => panic!("expected `{expected}`, got Ok(())"), Err(err) => { let dbg = format!("{err:?}"); - assert!( - dbg.contains(expected), - "expected `{expected}`, got `{dbg}`" - ); + assert!(dbg.contains(expected), "expected `{expected}`, got `{dbg}`"); } } } From 02b7e13cba1d73a650c619188a041902ebe58d61 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:49:15 +0200 Subject: [PATCH 18/21] fix conflicts --- Cargo.toml | 11 ---- Makefile | 48 --------------- benches/README.md | 62 ------------------- benches/docker/Dockerfile.iai | 26 -------- benches/iai_phases.rs | 108 ---------------------------------- 5 files changed, 255 deletions(-) delete mode 100644 Makefile delete mode 100644 benches/README.md delete mode 100644 benches/docker/Dockerfile.iai delete mode 100644 benches/iai_phases.rs diff --git a/Cargo.toml b/Cargo.toml index 5b4084b..e67a5f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,11 +48,6 @@ spongefish = { git = "https://github.com/z-tech/spongefish.git", branch = "small ark-bls12-381 = "0.5.0" ark-bn254 = "0.5.0" criterion = "0.8" -# Deterministic instruction-count benches. Requires valgrind + the -# `iai-callgrind-runner` binary at runtime (see benches/README.md). -# Unused on macOS except via Docker — `cargo build --bench iai_phases` -# still works everywhere, only `cargo bench` fails without valgrind. -iai-callgrind = "0.14" [features] default = ["asm"] @@ -68,9 +63,3 @@ profile = ["dep:tracing-subscriber", "dep:libc"] [[bench]] name = "warp_rs" harness = false - -# iai-callgrind: deterministic instruction counts per prove phase. Runs -# under valgrind; use `make bench-ci-local` on macOS to run inside Docker. -[[bench]] -name = "iai_phases" -harness = false diff --git a/Makefile b/Makefile deleted file mode 100644 index 91be7a9..0000000 --- a/Makefile +++ /dev/null @@ -1,48 +0,0 @@ -# Convenience targets for Plan B (benchmarking + regression detection). -# Plan 0 / O deliverables are consumed via cargo directly — no make target -# needed. - -.PHONY: help test clippy bench-wall bench-ci bench-ci-local bench-docker-build - -help: - @echo "Targets:" - @echo " test cargo test (both feature configs)" - @echo " clippy cargo clippy --all-targets --all-features" - @echo " bench-wall criterion wall-time benches (runs anywhere)" - @echo " bench-ci iai-callgrind instruction-count benches (Linux native)" - @echo " bench-ci-local iai-callgrind benches inside Docker (macOS-friendly)" - -test: - cargo test - cargo test --features profile - -clippy: - cargo clippy --all-targets -- -D warnings - cargo clippy --all-targets --all-features -- -D warnings - -bench-wall: - cargo bench --bench warp_rs - -# Native iai-callgrind bench. Requires valgrind + iai-callgrind-runner -# installed on the host. Will fail on macOS (valgrind is unsupported -# since Big Sur) — use `bench-ci-local` instead. -bench-ci: - cargo bench --bench iai_phases - -IAI_IMAGE := warp-iai-bench -IAI_CACHE := $(CURDIR)/target/iai-docker-cache - -bench-docker-build: - docker build -f benches/docker/Dockerfile.iai -t $(IAI_IMAGE) . - -# Run the iai bench inside a Linux container. Mounts the repo read-write -# so the target/ dir (including bench output) is reused across runs. -# Caches cargo registry + git + target in host-side directories to avoid -# re-downloading arkworks on every invocation. -bench-ci-local: bench-docker-build - mkdir -p $(IAI_CACHE)/registry $(IAI_CACHE)/git - docker run --rm \ - -v $(CURDIR):/workspace \ - -v $(IAI_CACHE)/registry:/usr/local/cargo/registry \ - -v $(IAI_CACHE)/git:/usr/local/cargo/git \ - $(IAI_IMAGE) diff --git a/benches/README.md b/benches/README.md deleted file mode 100644 index 7b0f118..0000000 --- a/benches/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Warp benches - -Two bench suites with different roles: - -| Bench | Signal | Scope | Cost | -|--------------------------|--------------------------|----------------------------|------------------| -| `warp_rs` (criterion) | Wall time, noisy | Whole `prove` at 5 sizes | Fast, informational | -| `iai_phases` (iai-callgrind) | Instruction count, deterministic | Whole `prove` at 1 size (v1) | Slow, CI gate | - -The criterion suite is for local feedback — it reports wall time in -milliseconds, which is human-intuitive but varies across machines and -loads. It runs natively on any host. - -The iai-callgrind suite is for **regression detection**. Callgrind counts -executed instructions, so the number is reproducible across CI runs. A -1% change is real signal. Plan B uses it as the PR gate. - -## Running - -### Criterion (works on macOS, Linux, Windows) - -```bash -make bench-wall -# or -cargo bench --bench warp_rs -``` - -### iai-callgrind (native — requires valgrind) - -```bash -cargo install iai-callgrind-runner --version 0.14.0 -make bench-ci -# or -cargo bench --bench iai_phases -``` - -### iai-callgrind (macOS via Docker) - -Valgrind hasn't worked on macOS since Big Sur, so on a Mac host run the -bench inside a Linux container: - -```bash -make bench-ci-local -``` - -This builds `benches/docker/Dockerfile.iai` (cached after the first -run), mounts the repo read-write, and caches cargo registry + git in -`target/iai-docker-cache/` so subsequent runs don't re-download -arkworks. First run: ~5 min build + ~5 min bench. Later runs: ~30 s -build check + bench time. - -## v1 scope - -`iai_phases` currently benches **one** prove configuration -(`l1=4, s=2, t=7, hashchain=10`). Larger parameter points and -per-phase attribution are deferred — see the docstring at the top of -`iai_phases.rs` for why. - -Baseline instruction counts are not yet committed; the plan is to -capture them in a follow-up once the CI workflow is wired up. See -`~/.claude/plans/nested-conjuring-scott.md` → Plan B for the full -roadmap, and the TODO about a GitHub Actions workflow. diff --git a/benches/docker/Dockerfile.iai b/benches/docker/Dockerfile.iai deleted file mode 100644 index ff92433..0000000 --- a/benches/docker/Dockerfile.iai +++ /dev/null @@ -1,26 +0,0 @@ -# Dockerfile for running iai-callgrind benches on hosts without valgrind -# (primarily macOS). Build + run via the repo-root Makefile: -# -# make bench-ci-local -# -# The image caches the iai-callgrind-runner binary + valgrind so repeat -# runs only re-build the warp crate. -FROM rust:1.81-slim-bookworm - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - valgrind \ - pkg-config \ - libssl-dev \ - ca-certificates \ - git \ - && rm -rf /var/lib/apt/lists/* - -# Match the version in Cargo.toml dev-dependencies. -ARG IAI_RUNNER_VERSION=0.14.0 -RUN cargo install iai-callgrind-runner --version ${IAI_RUNNER_VERSION} --locked - -WORKDIR /workspace - -# Default: run the bench. The repo is expected to be mounted at /workspace. -CMD ["cargo", "bench", "--bench", "iai_phases"] diff --git a/benches/iai_phases.rs b/benches/iai_phases.rs deleted file mode 100644 index 2e9a976..0000000 --- a/benches/iai_phases.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! iai-callgrind benchmarks measuring deterministic instruction counts -//! per prove call at fixed parameter points. -//! -//! Unlike [`benches/warp_rs.rs`] (criterion, wall time), these numbers -//! are reproducible across machines: callgrind counts executed -//! instructions, not time. A 1% change is real signal. -//! -//! Runs under valgrind. On macOS valgrind is not available natively — -//! use `make bench-ci-local` which invokes Docker (see benches/README.md). -//! -//! Installation: -//! cargo install iai-callgrind-runner --version 0.14.0 -//! then -//! cargo bench --bench iai_phases -//! -//! **v1 scope**: one size (l1=4, hashchain=10). Setup (encoding, -//! poseidon config build, instance gen) is measured inside the bench -//! because plumbing the `setup = expr` attribute through -//! parameterised `#[bench::...]` cases didn't resolve cleanly on -//! iai-callgrind 0.14 — the macro failed to see the target function -//! from the bench-crate root. Revisit when adding more parameter -//! points; a working pattern is either the non-parameterised form -//! below, or `iai_callgrind::BinaryBenchmarkConfig`. - -use ark_bls12_381::Fr as BLS12_381; -use ark_codes::{ - reed_solomon::{config::ReedSolomonConfig, ReedSolomon}, - traits::LinearCode, -}; -use ark_crypto_primitives::merkle_tree::configs::Blake3MerkleConfig; -use ark_std::rand::thread_rng; - -use iai_callgrind::{black_box, library_benchmark, library_benchmark_group, main}; -use warp::config::WARPConfig; -use warp::relations::BundledPESAT; -use warp::traits::AccumulationScheme as _; -use warp::types::{AccumulatorInstance, AccumulatorWitness}; -use warp::WARP; - -mod utils; -use utils::domainsep::init_prover_state; -use utils::hash_chain::{get_hashchain_instance_witness_pairs, get_hashchain_r1cs}; -use utils::poseidon; - -type F = BLS12_381; - -/// Output of [`setup_prove`]: everything needed to call `warp.prove` once, -/// assembled in setup time (NOT counted in the measurement). -struct ProveInputs { - warp: WARP, ReedSolomon, Blake3MerkleConfig>, - pk: (warp::relations::r1cs::R1CS, usize, usize, usize), - prover_state: spongefish::ProverState, - instances: Vec>, - witnesses: Vec>, -} - -fn setup_prove(l: usize, s: usize, t: usize, hashchain_size: usize) -> ProveInputs { - 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); - - let warp_config = WARPConfig::new(l, l, s, t, r1cs.config(), code.code_len()); - let warp = WARP::>::new(warp_config, code, r1cs.clone(), (), ()); - - let (instances, witnesses) = - get_hashchain_instance_witness_pairs(l, &poseidon_config, hashchain_size, &mut rng); - - ProveInputs { - warp, - pk: (r1cs.clone(), r1cs.m, r1cs.n, r1cs.k), - prover_state: init_prover_state(), - instances, - witnesses, - } -} - -fn run_prove(mut inputs: ProveInputs) { - black_box( - inputs - .warp - .prove( - inputs.pk, - &mut inputs.prover_state, - inputs.witnesses, - inputs.instances, - AccumulatorInstance::empty(), - AccumulatorWitness::empty(), - ) - .unwrap(), - ); -} - -// Smallest configuration — matches the unit-test shape (l1=4, hashchain=10). -#[library_benchmark] -fn bench_prove_small() { - let inputs = setup_prove(4, 2, 7, 10); - run_prove(black_box(inputs)); -} - -library_benchmark_group!( - name = prove_benches; - benchmarks = bench_prove_small -); - -main!(library_benchmark_groups = prove_benches); From 2e752a0d0f024825224216b58fcc751919e479a7 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sun, 3 May 2026 16:34:42 +0200 Subject: [PATCH 19/21] merge --- src/protocol/phases/twin_constraint.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/protocol/phases/twin_constraint.rs b/src/protocol/phases/twin_constraint.rs index 5f0245f..b190ea4 100644 --- a/src/protocol/phases/twin_constraint.rs +++ b/src/protocol/phases/twin_constraint.rs @@ -103,9 +103,8 @@ impl<'a, F: Field> RoundPolyEvaluator for TwinConstraintEvaluator<'a, F> { .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::() - }; + let eval = + |lc: &[(F, usize)]| lc.iter().map(|(t, idx)| z_even[*idx] * t).sum::(); eq * (eval(a) * eval(b) - eval(c)) }) .sum::(); From 0de18585d9b7e60e34ead8c711365e011e4265e1 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sun, 3 May 2026 17:47:49 +0200 Subject: [PATCH 20/21] phases --- src/lib.rs | 92 +++++----- src/protocol/phases/batching.rs | 123 +++++++------- src/protocol/phases/mod.rs | 58 +++++-- src/protocol/phases/ood.rs | 45 ++--- src/protocol/phases/pesat.rs | 119 +++++++------ src/protocol/phases/proximity.rs | 225 ++++++++++++++----------- src/protocol/phases/twin_constraint.rs | 176 ++++++++++--------- 7 files changed, 482 insertions(+), 356 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ef5fcca..ee1faa8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,10 @@ pub mod types; pub mod utils; use error::{DeciderError, ProverError, VerifierError}; -use protocol::phases::{batching, ood, pesat, proximity, twin_constraint}; +use protocol::phases::{ + batching::Batching, ood::Ood, pesat::Pesat, proximity::Proximity, + proximity::ProximityVerify, twin_constraint::TwinConstraint, ProverPhase, VerifierPhase, +}; pub trait BoolResult { fn ok_or_err(self, err: E) -> Result<(), E>; @@ -156,31 +159,32 @@ impl< } = acc_witness; // Phase 2: PESAT — emit oracles (codewords), commit, squeeze τs. - let pesat = pesat::prove::( - prover_state, - &self.params.code, - &self.params.mt_leaf_hash_params, - &self.params.mt_two_to_one_hash_params, - &witnesses, + let pesat = 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, + witnesses: &witnesses, l1, log_m, - )?; + _phantom: PhantomData, + } + .prove(prover_state)?; // Phase 3a: twin-constraint sumcheck. - let tc = twin_constraint::prove::( - prover_state, - &pesat.codewords, - pesat.taus, + let tc = TwinConstraint:: { + fresh_codewords: &pesat.codewords, + fresh_taus: pesat.taus, acc_instance, - &acc_fs, - &acc_ws, - &instances, - &witnesses, - self.params.p.constraints(), + acc_witness_f: &acc_fs, + acc_witness_w: &acc_ws, + instances: &instances, + witnesses: &witnesses, + r1cs: self.params.p.constraints(), log_l, log_m, log_n, - ); + } + .prove(prover_state)?; // Phase 3b: bundled η, ν₀, new commitment, absorb — the "emit new // oracle + claims" step between twin-constraint and OOD. @@ -216,7 +220,12 @@ impl< prover_state.prover_message(&nu_0); // Phase 3c: OOD — point queries on the oracle. - let ood_out = ood::prove::(prover_state, &tc.f, self.params.config.s, log_n); + let ood_out = Ood { + oracle: &tc.f, + s: self.params.config.s, + log_n, + } + .prove(prover_state)?; // Sample shift queries — ordering-coupled to the batching ξ below. let queries = QueryIndices::::sample(prover_state, log_n, self.params.config.t); @@ -229,20 +238,26 @@ impl< zetas.extend(ood_chunks); zetas.extend(queries.evaluation_points.iter().map(|v| v.as_slice())); - let batching_out = batching::prove::( - prover_state, - &tc.f, - &zetas, - self.params.config.s, - self.params.config.t, + let batching_out = Batching { + oracle: &tc.f, + zetas_prefix: &zetas, + s: self.params.config.s, + t: self.params.config.t, log_n, - ); + } + .prove(prover_state)?; // 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.codewords).collect(); - let prox = proximity::prove::(&queries, &pesat.td_0, &acc_tds, &all_codewords)?; + let prox = Proximity:: { + queries: &queries, + td_0: &pesat.td_0, + acc_td: &acc_tds, + all_codewords: &all_codewords, + } + .prove(prover_state)?; // Assemble new accumulator state and proof. let mut nus = Vec::with_capacity(1 + self.params.config.s); @@ -372,18 +387,19 @@ impl< let queries: QueryIndices = QueryIndices::from_squeezed_bytes(&bytes_shift_queries, log_n, self.params.config.t); - proximity::verify::( - &queries, - &rt_0, - &l2_roots, - &proof.auth_0, - &proof.auth_j, - &proof.shift_query_answers, - &self.params.mt_leaf_hash_params, - &self.params.mt_two_to_one_hash_params, + ProximityVerify:: { + queries: &queries, + 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, + mt_leaf_hash_params: &self.params.mt_leaf_hash_params, + mt_two_to_one_hash_params: &self.params.mt_two_to_one_hash_params, l2, - self.params.config.t, - )?; + t: self.params.config.t, + } + .verify(verifier_state)?; // 8. Derive everything needed to state the batching claim. let gamma_eq_evals = compute_hypercube_eq_evals(log_l, &gamma_sumcheck); diff --git a/src/protocol/phases/batching.rs b/src/protocol/phases/batching.rs index 3e44be1..f48e886 100644 --- a/src/protocol/phases/batching.rs +++ b/src/protocol/phases/batching.rs @@ -22,7 +22,9 @@ use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState use std::collections::HashMap; use crate::count_ops; +use crate::error::ProverError; use crate::protocol::oracle::Oracle; +use crate::protocol::phases::ProverPhase; use crate::utils::poly::eq_poly; /// [CBBZ23] / HyperPlonk sparse-evaluation optimization: for shift-query @@ -76,74 +78,79 @@ pub struct BatchingOutput { pub mu: F, } -/// Run the batching sumcheck. +/// Batching sumcheck phase. /// /// `zetas_prefix` must contain `1 + s + t` evaluation points in the order /// `[ζ_0, ood_0, ..., ood_{s-1}, query_0, ..., query_{t-1}]`. -#[tracing::instrument( - name = "batching", - skip_all, - fields(s = s, t = t, log_n = log_n) -)] -pub fn prove( - prover_state: &mut ProverState, - oracle: &Oracle, - zetas_prefix: &[&[F]], - s: usize, - t: usize, - log_n: usize, -) -> BatchingOutput +pub struct Batching<'a, F: Field> { + pub oracle: &'a Oracle, + pub zetas_prefix: &'a [&'a [F]], + pub s: usize, + pub t: usize, + pub log_n: usize, +} + +impl<'a, F> ProverPhase for Batching<'a, F> where F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, { - let n = oracle.len(); - let r = 1 + s + t; - let log_r = log2(r) as usize; - debug_assert_eq!(zetas_prefix.len(), r); + type Output = BatchingOutput; - let xis = prover_state.verifier_messages_vec::(log_r); + #[tracing::instrument( + name = "batching", + skip_all, + fields(s = self.s, t = self.t, log_n = self.log_n) + )] + fn prove(self, prover_state: &mut ProverState) -> Result { + let n = self.oracle.len(); + let r = 1 + self.s + self.t; + let log_r = log2(r) as usize; + debug_assert_eq!(self.zetas_prefix.len(), r); - // compute evaluations for xi and the dense ood_evals_vec for the first 1+s zetas - 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 + s) - .map(|i| { - (0..n) - .map(|a| eq_poly(zetas_prefix[i], a) * xi_eq_evals[i]) - .collect::>() - }) - .collect::>(); - (xi_eq_evals, ood_evals_vec) - }; + let xis = prover_state.verifier_messages_vec::(log_r); - // [CBBZ23] / HyperPlonk optimization for the t sparse shift-query zetas. - let id_non_0_eval_sums = { - let _s = tracing::info_span!("batching.accumulate_sparse").entered(); - accumulate_sparse_evaluations(zetas_prefix.to_vec(), xi_eq_evals, s, r) - }; + // compute evaluations for xi and the dense ood_evals_vec for the first 1+s zetas + 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 + self.s) + .map(|i| { + (0..n) + .map(|a| eq_poly(self.zetas_prefix[i], a) * xi_eq_evals[i]) + .collect::>() + }) + .collect::>(); + (xi_eq_evals, ood_evals_vec) + }; - // Run the inner-product sumcheck. `InnerProductProver` + `runner::sumcheck` - // is the new-style `SumcheckProver` entry point; wire format is three - // evaluations `[q(0), q(1), q(2)]` per round (`effsc::sumcheck_verify` - // reads them on the verifier side). The prover is MSB half-split, so the - // challenge vector arrives in MSB order — reverse once here so downstream - // MLE / eq_poly queries (arkworks' LSB-first convention) line up. - 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( - 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 - }; + // [CBBZ23] / HyperPlonk optimization for the t sparse shift-query zetas. + let id_non_0_eval_sums = { + let _s = tracing::info_span!("batching.accumulate_sparse").entered(); + accumulate_sparse_evaluations(self.zetas_prefix.to_vec(), xi_eq_evals, self.s, r) + }; - let mu = oracle.query_at_point(&alpha); + // Run the inner-product sumcheck. `InnerProductProver` + `runner::sumcheck` + // is the new-style `SumcheckProver` entry point; wire format is three + // evaluations `[q(0), q(1), q(2)]` per round (`effsc::sumcheck_verify` + // reads them on the verifier side). The prover is MSB half-split, so the + // challenge vector arrives in MSB order — reverse once here so downstream + // MLE / eq_poly queries (arkworks' LSB-first convention) line up. + 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( + self.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 + }; - BatchingOutput { alpha, mu } + let mu = self.oracle.query_at_point(&alpha); + + Ok(BatchingOutput { alpha, mu }) + } } diff --git a/src/protocol/phases/mod.rs b/src/protocol/phases/mod.rs index 9eb1c4c..8d328f6 100644 --- a/src/protocol/phases/mod.rs +++ b/src/protocol/phases/mod.rs @@ -1,23 +1,57 @@ //! Warp IOR phases as first-class modules. //! -//! Paired spec: `docs/paper-mods/mod1_oracle.tex` (shared Oracle composition) -//! and the forthcoming `docs/paper-mods/mod2_structured_sumcheck.tex`, -//! `mod3_accumulator_state.tex`. +//! Paired spec: `docs/paper-mods/mod3_accumulator_state.tex` (forthcoming) — +//! the accumulator-as-state framing where each phase is a typed transition +//! in the protocol. //! -//! Each submodule implements one IOR from the Warp construction. The phase -//! functions are concrete (no `IOR` trait) but share a consistent shape: +//! Each submodule implements one IOR from the Warp construction. They share +//! a consistent shape, lifted into the [`ProverPhase`] / [`VerifierPhase`] +//! traits below: a phase is a struct that captures the static / borrowed +//! context it needs (codes, R1CS, merkle params, residues from upstream +//! phases), consumed by `prove` / `verify` along with a shared transcript +//! handle. //! -//! - **prove**: takes the current prover state, the accumulator, and any -//! fresh inputs; runs the IOR's prover; returns the reduced claim and any -//! emitted [`Oracle`](super::oracle::Oracle)s. -//! - **verify** (where applicable): consumes a subset of -//! `DerivedRandomness` plus the prior claim; returns the reduced claim. +//! The top-level orchestrators in `src/lib.rs::WARP::prove` and `::verify` +//! thread state between phases by chaining `Phase::prove` / `Phase::verify` +//! calls — each phase's typed [`Output`](ProverPhase::Output) feeds the next +//! phase's struct. //! -//! The top-level orchestrators live in `src/lib.rs::WARP::prove` and -//! `::verify`, which thread state between phases. +//! The traits cover what is genuinely uniform across phases (consume static +//! context + transcript → produce typed residue). Verifier-side phases like +//! "derive transcript randomness" that are pure plumbing are not wrapped in +//! `VerifierPhase` — they live in `src/protocol/transcript` and are called +//! directly from the orchestrator. 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}; + +/// Prover-side IOR phase. +/// +/// Implementors are structs whose fields hold the borrowed context the phase +/// needs. `prove` consumes the phase struct together with the shared +/// `ProverState`, returns a typed [`Output`](Self::Output) residue threaded +/// forward to downstream phases. +pub trait ProverPhase { + type Output; + fn prove(self, prover_state: &mut ProverState) -> Result; +} + +/// Verifier-side IOR phase. +/// +/// Mirrors [`ProverPhase`] for the verifier. Phases whose verifier counterpart +/// is purely transcript-derived (PESAT, OOD) skip this trait — the orchestrator +/// reads the relevant randomness via helpers in `src/protocol/transcript`. +pub trait VerifierPhase { + type Output; + fn verify<'a>( + self, + verifier_state: &mut VerifierState<'a>, + ) -> Result; +} diff --git a/src/protocol/phases/ood.rs b/src/protocol/phases/ood.rs index 591cc94..7b56594 100644 --- a/src/protocol/phases/ood.rs +++ b/src/protocol/phases/ood.rs @@ -9,7 +9,9 @@ use ark_ff::{Field, PrimeField}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; use crate::count_ops; +use crate::error::ProverError; use crate::protocol::oracle::Oracle; +use crate::protocol::phases::ProverPhase; /// Output of the OOD phase: the flat challenge vector and the prover's /// answers at each chunked evaluation point. @@ -20,27 +22,32 @@ pub struct OodOutput { pub answers: Vec, } -/// Run the OOD phase: sample `s` evaluation points, query the oracle at -/// each, absorb the answers. -#[tracing::instrument(name = "ood", skip_all, fields(s = s, log_n = log_n))] -pub fn prove( - prover_state: &mut ProverState, - oracle: &Oracle, - s: usize, - log_n: usize, -) -> OodOutput +/// OOD phase: sample `s` evaluation points, query the oracle at each, absorb +/// the answers. +pub struct Ood<'a, F: Field> { + pub oracle: &'a Oracle, + pub s: usize, + pub log_n: usize, +} + +impl<'a, F> ProverPhase for Ood<'a, F> where F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, { - let samples_flat = prover_state.verifier_messages_vec::(s * log_n); - count_ops!(OodPointQueries, s as u64); - let answers = samples_flat - .chunks(log_n) - .map(|zeta| oracle.query_at_point(zeta)) - .collect::>(); - prover_state.prover_messages(&answers); - OodOutput { - samples_flat, - answers, + type Output = OodOutput; + + #[tracing::instrument(name = "ood", skip_all, fields(s = self.s, log_n = self.log_n))] + fn prove(self, prover_state: &mut ProverState) -> Result { + let samples_flat = prover_state.verifier_messages_vec::(self.s * self.log_n); + count_ops!(OodPointQueries, self.s as u64); + let answers = samples_flat + .chunks(self.log_n) + .map(|zeta| self.oracle.query_at_point(zeta)) + .collect::>(); + prover_state.prover_messages(&answers); + Ok(OodOutput { + samples_flat, + answers, + }) } } diff --git a/src/protocol/phases/pesat.rs b/src/protocol/phases/pesat.rs index dad8f6f..0f43090 100644 --- a/src/protocol/phases/pesat.rs +++ b/src/protocol/phases/pesat.rs @@ -20,73 +20,86 @@ use ark_crypto_primitives::{ }; use ark_ff::{Field, PrimeField}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; +use std::marker::PhantomData; use crate::count_ops; use crate::crypto::merkle::build_codeword_leaves; use crate::error::ProverError; +use crate::protocol::phases::ProverPhase; use crate::types::PesatOutput; -/// Run the PESAT Reduction prover. -#[tracing::instrument( - name = "pesat", - skip_all, - fields(l1 = l1, log_m = log_m, n_witnesses = witnesses.len()) -)] -pub fn prove( - prover_state: &mut ProverState, - code: &C, - mt_leaf_hash_params: &::Parameters, - mt_two_to_one_hash_params: &::Parameters, - witnesses: &[Vec], - l1: usize, - log_m: usize, -) -> Result, ProverError> +/// PESAT phase: encode + commit + absorb + squeeze τ. +pub struct Pesat<'a, F, C, MT> where F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, C: LinearCode, MT: Config + From<[u8; 32]>>, { - // a. encode witnesses - let (codewords, leaves) = { - let _s = tracing::info_span!("pesat.encode").entered(); - count_ops!(EncodeCalls, witnesses.len() as u64); - build_codeword_leaves(code, witnesses, l1) - }; + pub code: &'a C, + pub mt_leaf_hash_params: &'a ::Parameters, + pub mt_two_to_one_hash_params: &'a ::Parameters, + pub witnesses: &'a [Vec], + pub l1: usize, + pub log_m: usize, + pub _phantom: PhantomData<(F, MT)>, +} + +impl<'a, F, C, MT> ProverPhase for Pesat<'a, F, C, MT> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + C: LinearCode, + MT: Config + From<[u8; 32]>>, +{ + type Output = PesatOutput; + + #[tracing::instrument( + name = "pesat", + skip_all, + fields(l1 = self.l1, log_m = self.log_m, n_witnesses = self.witnesses.len()) + )] + fn prove(self, prover_state: &mut ProverState) -> Result { + // a. encode witnesses + let (codewords, leaves) = { + let _s = tracing::info_span!("pesat.encode").entered(); + count_ops!(EncodeCalls, self.witnesses.len() as u64); + build_codeword_leaves(self.code, self.witnesses, self.l1) + }; - // b. evaluation claims - let mus = codewords.iter().map(|f| f[0]).collect::>(); + // 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( - mt_leaf_hash_params, - mt_two_to_one_hash_params, - leaves.chunks_exact(l1).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(self.l1).collect::>(), + )? + }; - // d. absorb commitment and code evaluations; 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); + // d. absorb commitment and code evaluations; 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..l1) - .map(|_| prover_state.verifier_messages_vec::(log_m)) - .collect::>() - }; + (0..self.l1) + .map(|_| prover_state.verifier_messages_vec::(self.log_m)) + .collect::>() + }; - Ok(PesatOutput { - codewords, - td_0, - mus, - taus, - }) + Ok(PesatOutput { + codewords, + td_0, + mus, + taus, + }) + } } diff --git a/src/protocol/phases/proximity.rs b/src/protocol/phases/proximity.rs index d35ad75..88a9542 100644 --- a/src/protocol/phases/proximity.rs +++ b/src/protocol/phases/proximity.rs @@ -7,18 +7,22 @@ //! //! 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. +//! 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. use ark_crypto_primitives::{ crh::{CRHScheme, TwoToOneCRHScheme}, merkle_tree::{Config, MerkleTree, Path}, - Error, }; use ark_ff::Field; +use spongefish::{ProverState, VerifierState}; use crate::count_ops; use crate::crypto::merkle::compute_auth_paths; -use crate::error::VerifierError; +use crate::error::{ProverError, VerifierError}; +use crate::protocol::phases::{ProverPhase, VerifierPhase}; use crate::protocol::query::QueryIndices; use crate::BoolResult; @@ -28,128 +32,149 @@ pub struct ProximityOutput { pub shift_query_answers: Vec>, } -/// Open the proximity queries: generate auth paths for the fresh commitment -/// and each accumulated commitment, and collect the codeword values at every -/// queried leaf. +/// Proximity prover: open the queries against the fresh and accumulated +/// commitments, collect codeword values at each queried leaf. /// /// `all_codewords` must list the **accumulated** codewords first, then the /// **fresh** PESAT codewords, matching the verifier's expectation at /// `lib.rs::verify` (the `[l2..]` slice is the fresh chunk). -#[tracing::instrument( - name = "proximity", - skip_all, - fields( - n_queries = queries.leaf_positions.len(), - n_accumulators = acc_td.len(), - n_codewords = all_codewords.len(), - ) -)] -pub fn prove( - queries: &QueryIndices, - td_0: &MerkleTree, - acc_td: &[MerkleTree], - all_codewords: &[Vec], -) -> Result, Error> +pub struct Proximity<'a, F: Field, MT: Config> { + pub queries: &'a QueryIndices, + pub td_0: &'a MerkleTree, + pub acc_td: &'a [MerkleTree], + pub all_codewords: &'a [Vec], +} + +impl<'a, F, MT> ProverPhase for Proximity<'a, F, MT> where F: Field, MT: Config, { - let auth_0 = { - let _s = tracing::info_span!("proximity.auth_0").entered(); - count_ops!(MerklePathsGenerated, queries.leaf_positions.len() as u64); - compute_auth_paths(td_0, &queries.leaf_positions)? - }; - - let auth_j = { - let _s = tracing::info_span!("proximity.auth_j").entered(); - count_ops!( - MerklePathsGenerated, - (acc_td.len() * queries.leaf_positions.len()) as u64 - ); - acc_td - .iter() - .map(|td| compute_auth_paths(td, &queries.leaf_positions)) - .collect::>>, Error>>()? - }; - - let shift_query_answers = { - let _s = tracing::info_span!("proximity.shift_queries").entered(); - let mut answers = - vec![vec![F::default(); all_codewords.len()]; queries.leaf_positions.len()]; - for (i, idx) in queries.leaf_positions.iter().enumerate() { - let row = all_codewords.iter().map(|f| f[*idx]).collect::>(); - answers[i] = row; - } - answers - }; - - Ok(ProximityOutput { - auth_0, - auth_j, - shift_query_answers, - }) + type Output = ProximityOutput; + + #[tracing::instrument( + name = "proximity", + skip_all, + fields( + n_queries = self.queries.leaf_positions.len(), + n_accumulators = self.acc_td.len(), + n_codewords = self.all_codewords.len(), + ) + )] + fn prove(self, _prover_state: &mut ProverState) -> Result { + let auth_0 = { + let _s = tracing::info_span!("proximity.auth_0").entered(); + count_ops!(MerklePathsGenerated, self.queries.leaf_positions.len() as u64); + compute_auth_paths(self.td_0, &self.queries.leaf_positions)? + }; + + let auth_j = { + let _s = tracing::info_span!("proximity.auth_j").entered(); + count_ops!( + MerklePathsGenerated, + (self.acc_td.len() * self.queries.leaf_positions.len()) as u64 + ); + self.acc_td + .iter() + .map(|td| compute_auth_paths(td, &self.queries.leaf_positions)) + .collect::>>, _>>()? + }; + + let shift_query_answers = { + let _s = tracing::info_span!("proximity.shift_queries").entered(); + let mut answers = vec![ + vec![F::default(); self.all_codewords.len()]; + self.queries.leaf_positions.len() + ]; + for (i, idx) in self.queries.leaf_positions.iter().enumerate() { + let row = self + .all_codewords + .iter() + .map(|f| f[*idx]) + .collect::>(); + answers[i] = row; + } + answers + }; + + Ok(ProximityOutput { + auth_0, + auth_j, + shift_query_answers, + }) + } } -/// Verify the proximity (shift-query) openings. +/// Proximity verifier: validate the auth paths against the rt_0 / l2_roots +/// commitments and check that the leaf claims line up. /// /// - `auth_0` opens the fresh PESAT tree at each query; expected leaves are /// the `l2..` slice of each `shift_query_answers` row (the fresh chunk). /// - `auth_j` opens each of `l2` accumulated trees at each query; expected /// leaf for accumulator `i` at query `j` is `shift_query_answers[j][i]`. -#[allow(clippy::too_many_arguments)] -#[tracing::instrument( - name = "proximity.verify", - skip_all, - fields(t = t, l2 = l2) -)] -pub fn verify( - queries: &QueryIndices, - rt_0: &MT::InnerDigest, - l2_roots: &[MT::InnerDigest], - auth_0: &[Path], - auth_j: &[Vec>], - shift_query_answers: &[Vec], - mt_leaf_hash_params: &::Parameters, - mt_two_to_one_hash_params: &::Parameters, - l2: usize, - t: usize, -) -> Result<(), VerifierError> +pub struct ProximityVerify<'a, F: Field, MT: Config> { + pub queries: &'a QueryIndices, + 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 mt_leaf_hash_params: &'a ::Parameters, + pub mt_two_to_one_hash_params: &'a ::Parameters, + pub l2: usize, + pub t: usize, +} + +impl<'a, F, MT> VerifierPhase for ProximityVerify<'a, F, MT> where F: Field, MT: Config, { - (shift_query_answers.len() == t).ok_or_err(VerifierError::NumShiftQueries)?; - - for (i, path) in auth_0.iter().enumerate() { - (path.leaf_index == queries.leaf_positions[i]).ok_or_err(VerifierError::ShiftQueryIndex)?; - - count_ops!(MerklePathsVerified); - let is_valid = path.verify( - mt_leaf_hash_params, - mt_two_to_one_hash_params, - rt_0, - &shift_query_answers[i][l2..], // leaves are evaluations of the l1 codewords - )?; - is_valid.ok_or_err(VerifierError::ShiftQuery)?; - } + type Output = (); - (auth_j.len() == l2).ok_or_err(VerifierError::NumL2Instances)?; - for (i, paths) in auth_j.iter().enumerate() { - (paths.len() == t).ok_or_err(VerifierError::NumShiftQueries)?; - let root = &l2_roots[i]; - for (j, path) in paths.iter().enumerate() { - (path.leaf_index == queries.leaf_positions[j]) + #[tracing::instrument( + name = "proximity.verify", + skip_all, + fields(t = self.t, l2 = self.l2) + )] + fn verify<'b>( + self, + _verifier_state: &mut VerifierState<'b>, + ) -> Result { + (self.shift_query_answers.len() == self.t).ok_or_err(VerifierError::NumShiftQueries)?; + + for (i, path) in self.auth_0.iter().enumerate() { + (path.leaf_index == self.queries.leaf_positions[i]) .ok_or_err(VerifierError::ShiftQueryIndex)?; + count_ops!(MerklePathsVerified); let is_valid = path.verify( - mt_leaf_hash_params, - mt_two_to_one_hash_params, - root, - [shift_query_answers[j][i]], + self.mt_leaf_hash_params, + self.mt_two_to_one_hash_params, + self.rt_0, + &self.shift_query_answers[i][self.l2..], // leaves are evaluations of the l1 codewords )?; is_valid.ok_or_err(VerifierError::ShiftQuery)?; } - } - Ok(()) + (self.auth_j.len() == self.l2).ok_or_err(VerifierError::NumL2Instances)?; + for (i, paths) in self.auth_j.iter().enumerate() { + (paths.len() == self.t).ok_or_err(VerifierError::NumShiftQueries)?; + let root = &self.l2_roots[i]; + for (j, path) in paths.iter().enumerate() { + (path.leaf_index == self.queries.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, + [self.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 index b190ea4..7f05f0f 100644 --- a/src/protocol/phases/twin_constraint.rs +++ b/src/protocol/phases/twin_constraint.rs @@ -26,7 +26,9 @@ use effsc::{ use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; use crate::count_ops; +use crate::error::ProverError; use crate::protocol::oracle::Oracle; +use crate::protocol::phases::ProverPhase; use crate::relations::r1cs::R1CSConstraints; use crate::types::AccumulatorInstance; use crate::utils::{concat_slices, poly::eq_poly}; @@ -161,99 +163,121 @@ pub struct TwinConstraintOutput { pub beta_tau: Vec, } -/// Run the twin-constraint sumcheck prover. +/// Twin-constraint sumcheck phase: fold τ-zero-check + α-codeword check + +/// β-R1CS check into a single coefficient-form sumcheck. /// /// Takes codeword slices by reference so the caller (orchestrator) can hand -/// them to [`proximity::prove`](super::proximity::prove) after this phase +/// them to [`Proximity`](super::proximity::Proximity) after this phase /// returns. `fresh_taus` and `acc_instance` are consumed into the sumcheck /// tables — neither is needed downstream. -#[allow(clippy::too_many_arguments)] -#[tracing::instrument( - name = "twin_constraint", - skip_all, - fields(log_l = log_l, log_m = log_m, log_n = log_n) -)] -pub fn prove( - prover_state: &mut ProverState, - fresh_codewords: &[Vec], - fresh_taus: Vec>, - acc_instance: AccumulatorInstance, - acc_witness_f: &[Vec], - acc_witness_w: &[Vec], - instances: &[Vec], - witnesses: &[Vec], - r1cs: &R1CSConstraints, - log_l: usize, - log_m: usize, - log_n: usize, -) -> TwinConstraintOutput +pub struct TwinConstraint<'a, F, MT> where F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, MT: Config + From<[u8; 32]>>, { - let l1 = fresh_codewords.len(); + pub fresh_codewords: &'a [Vec], + pub fresh_taus: Vec>, + pub acc_instance: AccumulatorInstance, + pub acc_witness_f: &'a [Vec], + pub acc_witness_w: &'a [Vec], + pub instances: &'a [Vec], + pub witnesses: &'a [Vec], + pub r1cs: &'a R1CSConstraints, + pub log_l: usize, + pub log_m: usize, + pub log_n: usize, +} + +impl<'a, F, MT> ProverPhase for TwinConstraint<'a, F, MT> +where + F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, + MT: Config + From<[u8; 32]>>, +{ + type Output = TwinConstraintOutput; - // a. zero-check randomness - let omega: F = prover_state.verifier_message(); - let tau = prover_state.verifier_messages_vec::(log_l); + #[tracing::instrument( + name = "twin_constraint", + skip_all, + fields(log_l = self.log_l, log_m = self.log_m, log_n = self.log_n) + )] + fn prove(self, prover_state: &mut ProverState) -> Result { + let TwinConstraint { + fresh_codewords, + fresh_taus, + acc_instance, + acc_witness_f, + acc_witness_w, + instances, + witnesses, + r1cs, + log_l, + log_m, + log_n, + } = self; + let l1 = fresh_codewords.len(); - // b. assemble sumcheck tables - let tau_eq_evals = Ascending::new(log_l) - .map(|p| eq_poly(&tau, p.index)) - .collect::>(); + // a. zero-check randomness + let omega: F = prover_state.verifier_message(); + let tau = prover_state.verifier_messages_vec::(log_l); - let alpha_vecs = concat_slices(&acc_instance.alpha, &vec![vec![F::zero(); log_n]; l1]); + // b. assemble sumcheck tables + let tau_eq_evals = Ascending::new(log_l) + .map(|p| eq_poly(&tau, p.index)) + .collect::>(); - let z_vecs: Vec> = acc_instance - .beta - .1 - .iter() - .zip(acc_witness_w) - .chain(instances.iter().zip(witnesses)) - .map(|(x, w)| concat_slices(x, w)) - .collect(); + let alpha_vecs = concat_slices(&acc_instance.alpha, &vec![vec![F::zero(); log_n]; l1]); - let beta_vecs: Vec> = acc_instance.beta.0.into_iter().chain(fresh_taus).collect(); + let z_vecs: Vec> = acc_instance + .beta + .1 + .iter() + .zip(acc_witness_w) + .chain(instances.iter().zip(witnesses)) + .map(|(x, w)| concat_slices(x, w)) + .collect(); - let tablewise = vec![ - concat_slices(acc_witness_f, fresh_codewords), // u - z_vecs, // z - alpha_vecs, // a - beta_vecs, // b - ]; - let pw = vec![tau_eq_evals]; // tau + let beta_vecs: Vec> = acc_instance.beta.0.into_iter().chain(fresh_taus).collect(); - let degree = 1 + (log_n + 1).max(log_m + 2); - let evaluator = TwinConstraintEvaluator { - r1cs, - omega, - degree, - }; + let tablewise = vec![ + concat_slices(acc_witness_f, fresh_codewords), // u + z_vecs, // z + alpha_vecs, // a + beta_vecs, // b + ]; + let pw = vec![tau_eq_evals]; // tau - // c. run the sumcheck. `CoefficientProverLSB` + `runner::sumcheck` is - // the new-style `SumcheckProver` entry point; wire format is `d+1` - // evaluations per round (the verifier uses `effsc::sumcheck_verify` on - // the other side). - let mut cc = CoefficientProverLSB::new(&evaluator, tablewise, pw); - { - let _s = tracing::info_span!("twin_constraint.sumcheck").entered(); - count_ops!(TwinConstraintRounds, log_l as u64); - let proof = sumcheck(&mut cc, log_l, prover_state, noop_hook); - debug_assert_eq!(proof.challenges.len(), log_l); - } + let degree = 1 + (log_n + 1).max(log_m + 2); + let evaluator = TwinConstraintEvaluator { + r1cs, + omega, + degree, + }; + + // c. run the sumcheck. `CoefficientProverLSB` + `runner::sumcheck` is + // the new-style `SumcheckProver` entry point; wire format is `d+1` + // evaluations per round (the verifier uses `effsc::sumcheck_verify` on + // the other side). + let mut cc = CoefficientProverLSB::new(&evaluator, tablewise, pw); + { + let _s = tracing::info_span!("twin_constraint.sumcheck").entered(); + count_ops!(TwinConstraintRounds, log_l as u64); + let proof = 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(); + // 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(); - TwinConstraintOutput { - f: Oracle::from_evals(f), - z, - zeta_0, - beta_tau, + Ok(TwinConstraintOutput { + f: Oracle::from_evals(f), + z, + zeta_0, + beta_tau, + }) } } From 8ed68eac18816421f126878ab3503de652ed74a3 Mon Sep 17 00:00:00 2001 From: Andrew Z <1497456+z-tech@users.noreply.github.com> Date: Sun, 3 May 2026 20:12:25 +0200 Subject: [PATCH 21/21] more ior trait --- docs/paper-mods/slides/iors.aux | 53 + docs/paper-mods/slides/iors.log | 1855 ++++++++++++++++++++++++ docs/paper-mods/slides/iors.nav | 37 + docs/paper-mods/slides/iors.out | 0 docs/paper-mods/slides/iors.pdf | Bin 0 -> 128936 bytes docs/paper-mods/slides/iors.snm | 0 docs/paper-mods/slides/iors.tex | 343 +++++ docs/paper-mods/slides/iors.toc | 0 docs/paper-mods/slides/iors.vrb | 22 + src/lib.rs | 449 +++--- src/protocol/phases/batching.rs | 196 ++- src/protocol/phases/mod.rs | 96 +- src/protocol/phases/ood.rs | 116 +- src/protocol/phases/pesat.rs | 130 +- src/protocol/phases/proximity.rs | 179 ++- src/protocol/phases/twin_constraint.rs | 337 ++++- src/protocol/query.rs | 1 + texput.log | 20 + 18 files changed, 3326 insertions(+), 508 deletions(-) create mode 100644 docs/paper-mods/slides/iors.aux create mode 100644 docs/paper-mods/slides/iors.log create mode 100644 docs/paper-mods/slides/iors.nav create mode 100644 docs/paper-mods/slides/iors.out create mode 100644 docs/paper-mods/slides/iors.pdf create mode 100644 docs/paper-mods/slides/iors.snm create mode 100644 docs/paper-mods/slides/iors.tex create mode 100644 docs/paper-mods/slides/iors.toc create mode 100644 docs/paper-mods/slides/iors.vrb create mode 100644 texput.log 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 0000000000000000000000000000000000000000..5712224230e99da6f3d5a6e57ee4fa0a45e4f74c GIT binary patch literal 128936 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 literal 0 HcmV?d00001 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/src/lib.rs b/src/lib.rs index ee1faa8..8b57bf8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,23 +11,14 @@ use ark_ff::{Field, PrimeField}; use ark_poly::{DenseMultilinearExtension, Polynomial}; use ark_std::log2; use config::WARPConfig; -use effsc::{ - hypercube::{compute_hypercube_eq_evals, Ascending}, - verifier::sumcheck_verify, -}; +use effsc::hypercube::{compute_hypercube_eq_evals, Ascending}; use protocol::query::QueryIndices; -use protocol::transcript::{ - absorb_instances, derive_between_sumchecks, derive_pre_twin_constraint, parse_statement, - BetweenSumchecks, EffscVerifierTranscript, PreTwinConstraint, -}; +use protocol::transcript::{absorb_instances, parse_statement}; use relations::{r1cs::R1CSConstraints, BundledPESAT}; use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; use std::marker::PhantomData; use utils::scale_and_sum; -use utils::{ - concat_slices, - poly::{eq_poly, eq_poly_non_binary}, -}; +use utils::{concat_slices, poly::eq_poly}; pub mod config; pub mod constraints; @@ -44,8 +35,14 @@ pub mod utils; use error::{DeciderError, ProverError, VerifierError}; use protocol::phases::{ - batching::Batching, ood::Ood, pesat::Pesat, proximity::Proximity, - proximity::ProximityVerify, twin_constraint::TwinConstraint, ProverPhase, VerifierPhase, + 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 { @@ -159,47 +156,63 @@ impl< } = acc_witness; // Phase 2: PESAT — emit oracles (codewords), commit, squeeze τs. - let pesat = Pesat:: { + 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, - witnesses: &witnesses, - l1, - log_m, _phantom: PhantomData, - } - .prove(prover_state)?; + }; + let (pesat_red, pesat_out) = pesat_phase.prove( + prover_state, + &PesatStatement { l1, log_m }, + PesatWitness { + witnesses: &witnesses, + }, + (), + )?; // Phase 3a: twin-constraint sumcheck. - let tc = TwinConstraint:: { - fresh_codewords: &pesat.codewords, - fresh_taus: pesat.taus, - acc_instance, - acc_witness_f: &acc_fs, - acc_witness_w: &acc_ws, - instances: &instances, - witnesses: &witnesses, + let tc_phase = TwinConstraint:: { r1cs: self.params.p.constraints(), - log_l, - log_m, - log_n, - } - .prove(prover_state)?; + _phantom: PhantomData, + }; + 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, + }, + )?; // 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.beta_tau, i)).collect::>(); + 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, &tc.z) + .evaluate_bundled(&beta_eq_evals, &tc_out.z) .map_err(|_| ProverError::SpongeFish)?; - let nu_0 = tc.f.query_at_point(&tc.zeta_0); + let nu_0 = tc_out.f.query_at_point(&tc_red.zeta_0); - let (new_x, new_w) = tc.z.split_at(N - k); + 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.beta_tau.clone()], vec![new_x]); + let new_beta = (vec![tc_red.beta_tau.clone()], vec![new_x]); let td = { let _s = tracing::info_span!("warp.commit_new_oracle").entered(); @@ -207,7 +220,7 @@ impl< MerkleTree::::new( &self.params.mt_leaf_hash_params, &self.params.mt_two_to_one_hash_params, - tc.f.evals().chunks(1).collect::>(), + tc_out.f.evals().chunks(1).collect::>(), )? }; let td_root_bytes: [u8; 32] = td @@ -220,65 +233,89 @@ impl< prover_state.prover_message(&nu_0); // Phase 3c: OOD — point queries on the oracle. - let ood_out = Ood { - oracle: &tc.f, - s: self.params.config.s, - log_n, - } - .prove(prover_state)?; + 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 }, + )?; // Sample shift queries — ordering-coupled to the batching ξ below. let queries = QueryIndices::::sample(prover_state, log_n, self.params.config.t); // Phase 3d: batching sumcheck. Assemble zetas = [ζ₀, ood_j…, query_k…]. - let ood_chunks: Vec<&[F]> = ood_out.samples_flat.chunks(log_n).collect(); - let mut zetas: Vec<&[F]> = + let mut zetas: Vec> = Vec::with_capacity(1 + self.params.config.s + self.params.config.t); - zetas.push(tc.zeta_0.as_slice()); - zetas.extend(ood_chunks); - zetas.extend(queries.evaluation_points.iter().map(|v| v.as_slice())); - - let batching_out = Batching { - oracle: &tc.f, - zetas_prefix: &zetas, - s: self.params.config.s, - t: self.params.config.t, - log_n, + 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()); } - .prove(prover_state)?; + + 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 }, + )?; // 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.codewords).collect(); - let prox = Proximity:: { - queries: &queries, - td_0: &pesat.td_0, - acc_td: &acc_tds, - all_codewords: &all_codewords, - } - .prove(prover_state)?; + 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, + }, + )?; // Assemble new accumulator state and proof. let mut nus = Vec::with_capacity(1 + self.params.config.s); nus.push(nu_0); - nus.extend(ood_out.answers); + nus.extend(ood_red.answers); let new_acc_instance = AccumulatorInstance { rt: vec![td.root()], - alpha: vec![batching_out.alpha], + 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.f.into_evals()], + f: vec![tc_out.f.into_evals()], w: vec![new_w], }; let proof = WARPProof { - rt_0: pesat.td_0.root(), - mu_i: pesat.mus, + rt_0: pesat_out.td_0.root(), + mu_i: pesat_red.mus, nu_0, nu_i: nus, auth_0: prox.auth_0, @@ -304,173 +341,157 @@ impl< 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.params.config.s + self.params.config.t; - let log_r = log2(r) as usize; - - // 1. Parse statement (fresh instances + accumulator). - let ( - l1_xs, - AccumulatorInstance { - rt: l2_roots, - alpha: l2_alphas, - mu: l2_mus, - beta: (l2_taus, l2_xs), - eta: l2_etas, - }, - ) = parse_statement::(verifier_state, l1, l2, N - k, log_n, log_m)?; - - // 2. Read PESAT phase: rt_0 + l1_mus, squeeze l1_taus, ω, τ. - let PreTwinConstraint { - rt_0, - l1_mus, - l1_taus, - omega, - tau, - } = derive_pre_twin_constraint::(verifier_state, l1, log_l, log_m)?; - - // 3. Twin-constraint sumcheck claim σ₁ = Σ_i τ_eq(i)·(μ_i + ω·η_i). - let tau_eq_evals = compute_hypercube_eq_evals(log_l, &tau); - let etas_l2_first = 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_l2_first), - ) - .fold(F::zero(), |acc, (eq_tau, (mu, eta))| { - acc + eq_tau * (mu + omega * eta) - }); - - // 4. Run the twin-constraint sumcheck via effsc. The real oracle - // check is `final_claim == eq(τ, γ) · (ν₀ + ω·η)`, but ν₀ and η - // arrive on the transcript *after* the sumcheck rounds — so we - // capture `final_claim` in the closure and finish the check - // below after reading them. `sumcheck_verify` still enforces all - // per-round consistency (`q(0)+q(1)==claim`) automatically. - 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, }; + 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, + }, + (), + )?; + + // 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)?; - // 5. Read between-sumchecks state: td, η, ν₀, OOD, shift-query - // byte challenges, ξ. - let BetweenSumchecks { - td: _, - eta, - nus: mut nus_from_transcript, - ood_samples, - bytes_shift_queries, - xi, - } = derive_between_sumchecks::( + // 6. OOD::verify — squeeze samples; read answers. + let ood_phase = Ood::::new(); + let (ood_red, _) = ood_phase.verify( verifier_state, - log_n, - self.params.config.s, - self.params.config.t, + &OodStatement { + s: self.params.config.s, + log_n, + }, + (), )?; - // 6. Deferred twin-constraint oracle check. - (eq_poly_non_binary(&tau, &gamma_sumcheck) * (nus_from_transcript[0] + omega * eta) - == tc_final_claim) - .ok_or_err(VerifierError::Target)?; - - // 7. Proximity first: both the batching σ₂ and the batching oracle - // check consume `proof.shift_query_answers`, so we must reject - // malformed / tampered openings *before* feeding them into the - // sumcheck. Running proximity here preserves the existing - // negative-test error distinctions (`ShiftQuery`, `ShiftQueryIndex`, - // `NumShiftQueries`, `NumL2Instances`). + // 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); - ProximityVerify:: { - queries: &queries, - 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, + 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, - l2, - t: self.params.config.t, - } - .verify(verifier_state)?; - - // 8. Derive everything needed to state the batching claim. - 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); - - // ν_{s+k}: inner product of the (now proximity-verified) shift-query - // answers with the γ-equality table. - let mut nu_s_t = vec![F::default(); self.params.config.t]; - for (i, v_jk) in proof.shift_query_answers.iter().enumerate() { - nu_s_t[i] = v_jk + _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, + }, + )?; + + // 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_from_transcript.extend(nu_s_t); - let nus = nus_from_transcript; - - 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); - - // 8. Run the batching (inner-product) sumcheck via effsc. The - // oracle check `final_claim == μ · Σ_i eq(ζ_i, α)·ξ_eq(i)` is - // self-contained and runs inside the closure — a mismatch maps - // to `VerifierError::Target`. We still need the reduced α to - // compare against `acc_instance.alpha[0]` below, so the closure - // returns Ok via a side-channel capture of the zeta-eq sum. - // - // α arrives in MSB (sumcheck round) order; the prover stored - // the LSB-reversed form in `acc_instance.alpha[0]`, so we - // reverse once for MLE/eq consistency with arkworks. - let acc_mu = acc_instance.mu[0]; - let alpha_sumcheck_msb = { - let mut wrap = EffscVerifierTranscript(verifier_state); - let res = sumcheck_verify(sigma_2, 2, log_n, &mut wrap, |_, _| Ok(()))?; - let alpha_lsb: Vec = res.challenges.iter().rev().copied().collect(); - let mut zeta_eqs = Vec::with_capacity(r); - zeta_eqs.push(eq_poly_non_binary(&zeta_0, &alpha_lsb)); - for chunk in ood_samples.chunks(log_n) { - zeta_eqs.push(eq_poly_non_binary(chunk, &alpha_lsb)); - } - for pt in &queries.evaluation_points { - zeta_eqs.push(eq_poly_non_binary(pt, &alpha_lsb)); - } - debug_assert_eq!(zeta_eqs.len(), r); - let expected = 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)?; - res.challenges - }; + + // 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 pt in &queries.evaluation_points { + zetas.push(pt.clone()); + } + + 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. - let alpha_sumcheck_lsb: Vec = alpha_sumcheck_msb.iter().rev().copied().collect(); - (acc_instance.alpha[0] == alpha_sumcheck_lsb) - .ok_or_err(VerifierError::CodeEvaluationPoint)?; + (acc_alpha_first == batching_red.alpha).ok_or_err(VerifierError::CodeEvaluationPoint)?; let betas = l2_taus .into_iter() .chain(l1_taus) - .zip(l2_xs.clone().into_iter().chain(l1_xs)) + .zip(l2_xs.into_iter().chain(l1_xs)) .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.beta.0[0], &acc_instance.beta.1[0]); + let expected_beta = concat_slices(&acc_beta_0_first, &acc_beta_1_first); (expected_beta == beta).ok_or_err(VerifierError::CircuitEvaluationPoint)?; Ok(()) diff --git a/src/protocol/phases/batching.rs b/src/protocol/phases/batching.rs index f48e886..bbdcda9 100644 --- a/src/protocol/phases/batching.rs +++ b/src/protocol/phases/batching.rs @@ -11,29 +11,41 @@ //! sumcheck, with the CBBZ23 / HyperPlonk sparse-evaluation optimization //! (`accumulate_sparse_evaluations`) folded in. //! -//! Takes the already-sampled shift-query evaluation points as input so the -//! transcript order (queries sampled, then ξ, then sumcheck messages) is -//! preserved. See the orchestrator in `src/lib.rs`. +//! 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}; -use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; +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; +use crate::error::{ProverError, VerifierError}; use crate::protocol::oracle::Oracle; -use crate::protocol::phases::ProverPhase; -use crate::utils::poly::eq_poly; +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. We accumulate the corresponding `eq_evals[i]` into a -/// sparse map keyed by that point's index. +/// hypercube point. fn accumulate_sparse_evaluations( - zetas: Vec<&[F]>, - eq_evals: Vec, + zetas: &[Vec], + eq_evals: &[F], s: usize, r: usize, ) -> HashMap { @@ -50,8 +62,7 @@ fn accumulate_sparse_evaluations( } /// Sum `dense_polys` column-wise and add the sparse contributions into the -/// resulting vector. Used to build the `g` side of the inner-product -/// sumcheck `∑_x f(x)·g(x)`. +/// resulting vector. fn batched_constraint_poly( dense_polys: &[Vec], sparse_polys: &HashMap, @@ -71,76 +82,113 @@ fn batched_constraint_poly( result } -/// Output of the batching sumcheck: the reduced point `α` and the target -/// `μ = \hat f(α)`. -pub struct BatchingOutput { +// ─── 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 sumcheck phase. -/// -/// `zetas_prefix` must contain `1 + s + t` evaluation points in the order -/// `[ζ_0, ood_0, ..., ood_{s-1}, query_0, ..., query_{t-1}]`. +/// Batching phase configuration. pub struct Batching<'a, F: Field> { - pub oracle: &'a Oracle, - pub zetas_prefix: &'a [&'a [F]], - pub s: usize, - pub t: usize, - pub log_n: usize, + 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> ProverPhase for Batching<'a, F> +impl<'a, F> IOR for Batching<'a, F> where F: Field + PrimeField + Encoding<[u8]> + Decoding<[u8]> + NargDeserialize + NargSerialize, { - type Output = BatchingOutput; + 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 = self.s, t = self.t, log_n = self.log_n) + fields(s = statement.s, t = statement.t, log_n = statement.log_n) )] - fn prove(self, prover_state: &mut ProverState) -> Result { - let n = self.oracle.len(); - let r = 1 + self.s + self.t; + 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!(self.zetas_prefix.len(), r); + debug_assert_eq!(statement.zetas_prefix.len(), r); let xis = prover_state.verifier_messages_vec::(log_r); - // compute evaluations for xi and the dense ood_evals_vec for the first 1+s zetas 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 + self.s) + let ood_evals_vec = (0..1 + statement.s) .map(|i| { (0..n) - .map(|a| eq_poly(self.zetas_prefix[i], a) * xi_eq_evals[i]) + .map(|a| eq_poly(&statement.zetas_prefix[i], a) * xi_eq_evals[i]) .collect::>() }) .collect::>(); (xi_eq_evals, ood_evals_vec) }; - // [CBBZ23] / HyperPlonk optimization for the t sparse shift-query zetas. let id_non_0_eval_sums = { let _s = tracing::info_span!("batching.accumulate_sparse").entered(); - accumulate_sparse_evaluations(self.zetas_prefix.to_vec(), xi_eq_evals, self.s, r) + accumulate_sparse_evaluations(&statement.zetas_prefix, &xi_eq_evals, statement.s, r) }; - // Run the inner-product sumcheck. `InnerProductProver` + `runner::sumcheck` - // is the new-style `SumcheckProver` entry point; wire format is three - // evaluations `[q(0), q(1), q(2)]` per round (`effsc::sumcheck_verify` - // reads them on the verifier side). The prover is MSB half-split, so the - // challenge vector arrives in MSB order — reverse once here so downstream - // MLE / eq_poly queries (arkworks' LSB-first convention) line up. + // 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( - self.oracle.evals().to_vec(), + inputs.oracle.evals().to_vec(), batched_constraint_poly(&ood_evals_vec, &id_non_0_eval_sums), ); let mut challenges = @@ -149,8 +197,62 @@ where challenges }; - let mu = self.oracle.query_at_point(&alpha); + 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(BatchingOutput { alpha, mu }) + Ok((BatchingReducedStatement { alpha: alpha_lsb }, ())) } } diff --git a/src/protocol/phases/mod.rs b/src/protocol/phases/mod.rs index 8d328f6..d1ed7fa 100644 --- a/src/protocol/phases/mod.rs +++ b/src/protocol/phases/mod.rs @@ -1,26 +1,24 @@ //! Warp IOR phases as first-class modules. //! -//! Paired spec: `docs/paper-mods/mod3_accumulator_state.tex` (forthcoming) — -//! the accumulator-as-state framing where each phase is a typed transition -//! in the protocol. +//! Paired spec: `docs/paper-mods/mod1_oracle.tex` (composition rule) and the +//! forthcoming `docs/paper-mods/mod3_accumulator_state.tex`. //! -//! Each submodule implements one IOR from the Warp construction. They share -//! a consistent shape, lifted into the [`ProverPhase`] / [`VerifierPhase`] -//! traits below: a phase is a struct that captures the static / borrowed -//! context it needs (codes, R1CS, merkle params, residues from upstream -//! phases), consumed by `prove` / `verify` along with a shared transcript -//! handle. +//! 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. //! -//! The top-level orchestrators in `src/lib.rs::WARP::prove` and `::verify` -//! thread state between phases by chaining `Phase::prove` / `Phase::verify` -//! calls — each phase's typed [`Output`](ProverPhase::Output) feeds the next -//! phase's 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 traits cover what is genuinely uniform across phases (consume static -//! context + transcript → produce typed residue). Verifier-side phases like -//! "derive transcript randomness" that are pure plumbing are not wrapped in -//! `VerifierPhase` — they live in `src/protocol/transcript` and are called -//! directly from the orchestrator. +//! 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; @@ -32,26 +30,52 @@ use spongefish::{ProverState, VerifierState}; use crate::error::{ProverError, VerifierError}; -/// Prover-side IOR phase. +/// Interactive Oracle Reduction. /// -/// Implementors are structs whose fields hold the borrowed context the phase -/// needs. `prove` consumes the phase struct together with the shared -/// `ProverState`, returns a typed [`Output`](Self::Output) residue threaded -/// forward to downstream phases. -pub trait ProverPhase { - type Output; - fn prove(self, prover_state: &mut ProverState) -> Result; -} - -/// Verifier-side IOR phase. +/// Mirrors the IOR signature from `docs/paper-mods/mod1_oracle.tex` §4: /// -/// Mirrors [`ProverPhase`] for the verifier. Phases whose verifier counterpart -/// is purely transcript-derived (PESAT, OOD) skip this trait — the orchestrator -/// reads the relevant randomness via helpers in `src/protocol/transcript`. -pub trait VerifierPhase { - type Output; +/// ```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, + &self, verifier_state: &mut VerifierState<'a>, - ) -> Result; + 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 index 7b56594..8aee302 100644 --- a/src/protocol/phases/ood.rs +++ b/src/protocol/phases/ood.rs @@ -4,50 +4,120 @@ //! 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}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::marker::PhantomData; use crate::count_ops; -use crate::error::ProverError; +use crate::error::{ProverError, VerifierError}; use crate::protocol::oracle::Oracle; -use crate::protocol::phases::ProverPhase; +use crate::protocol::phases::IOR; + +pub struct OodStatement { + pub s: usize, + pub log_n: usize, +} -/// Output of the OOD phase: the flat challenge vector and the prover's -/// answers at each chunked evaluation point. -pub struct OodOutput { +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: sample `s` evaluation points, query the oracle at each, absorb -/// the answers. +/// OOD phase configuration. Stateless; the lifetime parameter exists only +/// to anchor `ProverInputs<'a>` for the trait impl. pub struct Ood<'a, F: Field> { - pub oracle: &'a Oracle, - pub s: usize, - pub log_n: usize, + pub _phantom: PhantomData<&'a F>, } -impl<'a, F> ProverPhase for Ood<'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 Output = OodOutput; + 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 = self.s, log_n = self.log_n))] - fn prove(self, prover_state: &mut ProverState) -> Result { - let samples_flat = prover_state.verifier_messages_vec::(self.s * self.log_n); - count_ops!(OodPointQueries, self.s as u64); + #[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(self.log_n) - .map(|zeta| self.oracle.query_at_point(zeta)) + .chunks(statement.log_n) + .map(|zeta| inputs.oracle.query_at_point(zeta)) .collect::>(); prover_state.prover_messages(&answers); - Ok(OodOutput { - samples_flat, - 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 index 0f43090..7f7b325 100644 --- a/src/protocol/phases/pesat.rs +++ b/src/protocol/phases/pesat.rs @@ -5,13 +5,15 @@ //! codewords, commit via an interleaved Merkle tree, absorb commitment and //! code evaluations, and derive the τ zero-check challenges. //! -//! Under Modification 1's oracle framing, this IOR has signature -//! `(statement, witnesses) -> (PesatOutput{codewords, commit, claims, τs})`; -//! downstream phases consume these as oracles. -//! -//! No verifier-side work lives here — PESAT emits oracles and τs that the -//! verifier derives afresh from the transcript via -//! `src/protocol/transcript/verifier.rs::derive_randomness`. +//! 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::{ @@ -19,16 +21,39 @@ use ark_crypto_primitives::{ merkle_tree::{Config, MerkleTree}, }; use ark_ff::{Field, PrimeField}; -use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState}; +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; -use crate::protocol::phases::ProverPhase; -use crate::types::PesatOutput; +use crate::error::{ProverError, VerifierError}; +use crate::protocol::phases::IOR; -/// PESAT phase: encode + commit + absorb + squeeze τ. +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, @@ -38,31 +63,40 @@ where pub code: &'a C, pub mt_leaf_hash_params: &'a ::Parameters, pub mt_two_to_one_hash_params: &'a ::Parameters, - pub witnesses: &'a [Vec], - pub l1: usize, - pub log_m: usize, pub _phantom: PhantomData<(F, MT)>, } -impl<'a, F, C, MT> ProverPhase for Pesat<'a, F, C, 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 Output = PesatOutput; + 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 = self.l1, log_m = self.log_m, n_witnesses = self.witnesses.len()) + fields(l1 = statement.l1, log_m = statement.log_m, n_witnesses = witness.witnesses.len()) )] - fn prove(self, prover_state: &mut ProverState) -> Result { + 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, self.witnesses.len() as u64); - build_codeword_leaves(self.code, self.witnesses, self.l1) + count_ops!(EncodeCalls, witness.witnesses.len() as u64); + build_codeword_leaves(self.code, witness.witnesses, statement.l1) }; // b. evaluation claims @@ -75,11 +109,11 @@ where MerkleTree::::new( self.mt_leaf_hash_params, self.mt_two_to_one_hash_params, - leaves.chunks_exact(self.l1).collect::>(), + leaves.chunks_exact(statement.l1).collect::>(), )? }; - // d. absorb commitment and code evaluations; e/f. derive τ challenges. + // 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 @@ -90,16 +124,50 @@ where prover_state.prover_message(&root_bytes); prover_state.prover_messages(&mus); - (0..self.l1) - .map(|_| prover_state.verifier_messages_vec::(self.log_m)) + (0..statement.l1) + .map(|_| prover_state.verifier_messages_vec::(statement.log_m)) .collect::>() }; - Ok(PesatOutput { - codewords, - td_0, - mus, - taus, - }) + 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 index 88a9542..f5887ff 100644 --- a/src/protocol/phases/proximity.rs +++ b/src/protocol/phases/proximity.rs @@ -11,6 +11,16 @@ //! 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}, @@ -18,76 +28,108 @@ use ark_crypto_primitives::{ }; 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::{ProverPhase, VerifierPhase}; +use crate::protocol::phases::IOR; use crate::protocol::query::QueryIndices; use crate::BoolResult; -pub struct ProximityOutput { +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 prover: open the queries against the fresh and accumulated -/// commitments, collect codeword values at each queried leaf. -/// -/// `all_codewords` must list the **accumulated** codewords first, then the -/// **fresh** PESAT codewords, matching the verifier's expectation at -/// `lib.rs::verify` (the `[l2..]` slice is the fresh chunk). +/// 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 queries: &'a QueryIndices, - pub td_0: &'a MerkleTree, - pub acc_td: &'a [MerkleTree], - pub all_codewords: &'a [Vec], + pub mt_leaf_hash_params: &'a ::Parameters, + pub mt_two_to_one_hash_params: &'a ::Parameters, + pub _phantom: PhantomData, } -impl<'a, F, MT> ProverPhase for Proximity<'a, F, MT> +impl<'a, F, MT> IOR for Proximity<'a, F, MT> where F: Field, - MT: Config, + MT: Config + 'a, + MT::InnerDigest: 'a, { - type Output = ProximityOutput; + 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 = self.queries.leaf_positions.len(), - n_accumulators = self.acc_td.len(), - n_codewords = self.all_codewords.len(), + 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) -> Result { + 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, self.queries.leaf_positions.len() as u64); - compute_auth_paths(self.td_0, &self.queries.leaf_positions)? + 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, - (self.acc_td.len() * self.queries.leaf_positions.len()) as u64 + (inputs.acc_td.len() * leaf_positions.len()) as u64 ); - self.acc_td + inputs + .acc_td .iter() - .map(|td| compute_auth_paths(td, &self.queries.leaf_positions)) + .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(); self.all_codewords.len()]; - self.queries.leaf_positions.len() - ]; - for (i, idx) in self.queries.leaf_positions.iter().enumerate() { - let row = self + 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]) @@ -97,84 +139,61 @@ where answers }; - Ok(ProximityOutput { - auth_0, - auth_j, - shift_query_answers, - }) + Ok(( + (), + ProximityProverOutputs { + auth_0, + auth_j, + shift_query_answers, + }, + )) } -} - -/// Proximity verifier: validate the auth paths against the rt_0 / l2_roots -/// commitments and check that the leaf claims line up. -/// -/// - `auth_0` opens the fresh PESAT tree at each query; expected leaves are -/// the `l2..` slice of each `shift_query_answers` row (the fresh chunk). -/// - `auth_j` opens each of `l2` accumulated trees at each query; expected -/// leaf for accumulator `i` at query `j` is `shift_query_answers[j][i]`. -pub struct ProximityVerify<'a, F: Field, MT: Config> { - pub queries: &'a QueryIndices, - 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 mt_leaf_hash_params: &'a ::Parameters, - pub mt_two_to_one_hash_params: &'a ::Parameters, - pub l2: usize, - pub t: usize, -} - -impl<'a, F, MT> VerifierPhase for ProximityVerify<'a, F, MT> -where - F: Field, - MT: Config, -{ - type Output = (); #[tracing::instrument( name = "proximity.verify", skip_all, - fields(t = self.t, l2 = self.l2) + fields(t = statement.t, l2 = statement.l2) )] fn verify<'b>( - self, + &self, _verifier_state: &mut VerifierState<'b>, - ) -> Result { - (self.shift_query_answers.len() == self.t).ok_or_err(VerifierError::NumShiftQueries)?; + statement: &Self::Statement, + inputs: Self::VerifierInputs, + ) -> Result<(Self::ReducedStatement, Self::VerifierOutputs), VerifierError> { + let leaf_positions = &statement.queries.leaf_positions; - for (i, path) in self.auth_0.iter().enumerate() { - (path.leaf_index == self.queries.leaf_positions[i]) - .ok_or_err(VerifierError::ShiftQueryIndex)?; + (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, - self.rt_0, - &self.shift_query_answers[i][self.l2..], // leaves are evaluations of the l1 codewords + inputs.rt_0, + &inputs.shift_query_answers[i][statement.l2..], )?; is_valid.ok_or_err(VerifierError::ShiftQuery)?; } - (self.auth_j.len() == self.l2).ok_or_err(VerifierError::NumL2Instances)?; - for (i, paths) in self.auth_j.iter().enumerate() { - (paths.len() == self.t).ok_or_err(VerifierError::NumShiftQueries)?; - let root = &self.l2_roots[i]; + (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 == self.queries.leaf_positions[j]) - .ok_or_err(VerifierError::ShiftQueryIndex)?; + (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, - [self.shift_query_answers[j][i]], + [inputs.shift_query_answers[j][i]], )?; is_valid.ok_or_err(VerifierError::ShiftQuery)?; } } - Ok(()) + Ok(((), ())) } } diff --git a/src/protocol/phases/twin_constraint.rs b/src/protocol/phases/twin_constraint.rs index 7f05f0f..c787b24 100644 --- a/src/protocol/phases/twin_constraint.rs +++ b/src/protocol/phases/twin_constraint.rs @@ -16,23 +16,46 @@ //! - `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::Ascending, - noop_hook, provers::coefficient_lsb::CoefficientProverLSB, runner::sumcheck, + 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}; +use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize, ProverState, VerifierState}; +use std::marker::PhantomData; use crate::count_ops; -use crate::error::ProverError; +use crate::error::{ProverError, VerifierError}; use crate::protocol::oracle::Oracle; -use crate::protocol::phases::ProverPhase; +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}; -use ark_crypto_primitives::merkle_tree::Config; +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 { @@ -65,8 +88,7 @@ fn eval_r1cs_constraint_poly( } /// Round-polynomial evaluator fusing α-fold, β-fold, and τ-linear into a -/// single sumcheck pass. See `mod2_structured_sumcheck.tex` (stub) — the -/// fusion will be promoted to a paper-level primitive in Plan B'. +/// single sumcheck pass. struct TwinConstraintEvaluator<'a, F: Field> { r1cs: &'a R1CSConstraints, omega: F, @@ -149,72 +171,122 @@ impl<'a, F: Field> RoundPolyEvaluator for TwinConstraintEvaluator<'a, F> { } } -/// Output of the twin-constraint sumcheck. All oracles / vectors are the -/// reduced claim state consumed by downstream phases (OOD, batching, final -/// target). -pub struct TwinConstraintOutput { - /// Reduced codeword oracle `f` — consumed by OOD, batching, proximity. +// ─── 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, - /// New code evaluation point `ζ₀` — becomes the new accumulator α. - pub zeta_0: Vec, - /// Reduced τ — becomes the τ component of the new accumulator β. - pub beta_tau: Vec, } -/// Twin-constraint sumcheck phase: fold τ-zero-check + α-codeword check + -/// β-R1CS check into a single coefficient-form sumcheck. -/// -/// Takes codeword slices by reference so the caller (orchestrator) can hand -/// them to [`Proximity`](super::proximity::Proximity) after this phase -/// returns. `fresh_taus` and `acc_instance` are consumed into the sumcheck -/// tables — neither is needed downstream. +/// 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 fresh_codewords: &'a [Vec], - pub fresh_taus: Vec>, - pub acc_instance: AccumulatorInstance, - pub acc_witness_f: &'a [Vec], - pub acc_witness_w: &'a [Vec], - pub instances: &'a [Vec], - pub witnesses: &'a [Vec], pub r1cs: &'a R1CSConstraints, - pub log_l: usize, - pub log_m: usize, - pub log_n: usize, + pub _phantom: PhantomData, } -impl<'a, F, MT> ProverPhase for TwinConstraint<'a, F, MT> +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 Output = TwinConstraintOutput; + 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 = self.log_l, log_m = self.log_m, log_n = self.log_n) + fields(log_l = statement.log_l, log_m = statement.log_m, log_n = statement.log_n) )] - fn prove(self, prover_state: &mut ProverState) -> Result { - let TwinConstraint { - fresh_codewords, - fresh_taus, - acc_instance, - acc_witness_f, - acc_witness_w, - instances, - witnesses, - r1cs, - log_l, - log_m, - log_n, - } = self; - let l1 = fresh_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 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(); @@ -225,45 +297,54 @@ where .map(|p| eq_poly(&tau, p.index)) .collect::>(); - let alpha_vecs = concat_slices(&acc_instance.alpha, &vec![vec![F::zero(); log_n]; l1]); + let alpha_vecs = concat_slices( + &statement.acc_instance.alpha, + &vec![vec![F::zero(); log_n]; l1], + ); - let z_vecs: Vec> = acc_instance + let z_vecs: Vec> = statement + .acc_instance .beta .1 .iter() - .zip(acc_witness_w) - .chain(instances.iter().zip(witnesses)) + .zip(witness.acc_witness_w) + .chain(witness.instances.iter().zip(witness.witnesses)) .map(|(x, w)| concat_slices(x, w)) .collect(); - let beta_vecs: Vec> = acc_instance.beta.0.into_iter().chain(fresh_taus).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(acc_witness_f, fresh_codewords), // u - z_vecs, // z - alpha_vecs, // a - beta_vecs, // b + 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, + r1cs: self.r1cs, omega, degree, }; - // c. run the sumcheck. `CoefficientProverLSB` + `runner::sumcheck` is - // the new-style `SumcheckProver` entry point; wire format is `d+1` - // evaluations per round (the verifier uses `effsc::sumcheck_verify` on - // the other side). + // 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); - let proof = sumcheck(&mut cc, log_l, prover_state, noop_hook); - debug_assert_eq!(proof.challenges.len(), log_l); - } + 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(); @@ -273,11 +354,113 @@ where let zeta_0 = reduced[2][0].clone(); let beta_tau = reduced[3][0].clone(); - Ok(TwinConstraintOutput { - f: Oracle::from_evals(f), - z, - zeta_0, - beta_tau, - }) + 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 index 1921af9..bbe207f 100644 --- a/src/protocol/query.rs +++ b/src/protocol/query.rs @@ -1,6 +1,7 @@ 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 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!