Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions circuit/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,25 @@ where
}
NonPrimitiveOpType::FriVerify => {
todo!() // TODO: Add FRIVerify when it lands
}
NonPrimitiveOpType::HashAbsorb { reset } => {
let inputs = witness_exprs
.iter()
.map(|&expr| Self::get_witness_id(expr_to_widx, expr, "HashAbsorb input"))
.collect::<Result<_, _>>()?;

lowered_ops.push(NonPrimitiveOp::HashAbsorb {
reset_flag: *reset,
inputs,
});
}
NonPrimitiveOpType::HashSqueeze => {
let outputs = witness_exprs
.iter()
.map(|&expr| Self::get_witness_id(expr_to_widx, expr, "HashSqueeze output"))
.collect::<Result<_, _>>()?;

lowered_ops.push(NonPrimitiveOp::HashSqueeze { outputs });
} // Add more variants here as needed
}
}
Expand Down Expand Up @@ -1000,6 +1019,9 @@ mod tests {
assert_eq!(circuit.non_primitive_ops.len(), 1);
match &circuit.non_primitive_ops[0] {
NonPrimitiveOp::MmcsVerify { .. } => {}
NonPrimitiveOp::HashAbsorb { .. } | NonPrimitiveOp::HashSqueeze { .. } => {
panic!("Expected MmcsVerify operation");
}
}
}

Expand Down
23 changes: 22 additions & 1 deletion circuit/src/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ pub enum NonPrimitiveOpType {
// Mmcs Verify gate with the argument is the size of the path
MmcsVerify,
FriVerify,
// Future: FriVerify, HashAbsorb, etc.
/// Hash absorb operation - absorbs field elements into sponge state
HashAbsorb {
reset: bool,
},
/// Hash squeeze operation - extracts field elements from sponge state
HashSqueeze,
}

/// Non-primitive operation types
Expand Down Expand Up @@ -98,6 +103,22 @@ pub enum NonPrimitiveOp {
index: WitnessId,
root: MmcsWitnessId,
},

/// Hash absorb operation - absorbs inputs into sponge state.
///
/// Public interface (on witness bus):
/// - `inputs`: Field elements to absorb into the sponge
/// - `reset_flag`: Whether to reset the sponge state before absorbing
HashAbsorb {
reset_flag: bool,
inputs: Vec<WitnessId>,
},

/// Hash squeeze operation - extracts outputs from sponge state.
///
/// Public interface (on witness bus):
/// - `outputs`: Field elements extracted from the sponge
HashSqueeze { outputs: Vec<WitnessId> },
}

/// Configuration parameters for Mmcs verification operations. When
Expand Down
118 changes: 118 additions & 0 deletions circuit/src/ops/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! Module defining hash operations for circuit builder.
//!
//! Provides methods for absorbing and squeezing elements using a sponge
//! construction within the circuit.

use p3_field::PrimeCharacteristicRing;

use crate::builder::{CircuitBuilder, CircuitBuilderError};
use crate::op::NonPrimitiveOpType;
use crate::types::ExprId;

/// Hash operations trait for `CircuitBuilder`.
pub trait HashOps<F: Clone + PrimeCharacteristicRing + Eq + core::hash::Hash> {
/// Absorb field elements into the sponge state.
///
/// # Arguments
///
/// * `inputs` - The `ExprId`s to absorb
/// * `reset` - Whether to reset the sponge state before absorbing
fn add_hash_absorb(
&mut self,
inputs: &[ExprId],
reset: bool,
) -> Result<(), CircuitBuilderError>;

/// Squeeze field elements from the sponge state.
///
/// # Arguments
///
/// * `outputs` - The `ExprId`s to store squeezed values in
fn add_hash_squeeze(&mut self, outputs: &[ExprId]) -> Result<(), CircuitBuilderError>;
}

impl<F> HashOps<F> for CircuitBuilder<F>
where
F: Clone + PrimeCharacteristicRing + Eq + core::hash::Hash,
{
fn add_hash_absorb(
&mut self,
inputs: &[ExprId],
reset: bool,
) -> Result<(), CircuitBuilderError> {
self.ensure_op_enabled(NonPrimitiveOpType::HashAbsorb { reset })?;

self.push_non_primitive_op(NonPrimitiveOpType::HashAbsorb { reset }, inputs.to_vec());

Ok(())
}

fn add_hash_squeeze(&mut self, outputs: &[ExprId]) -> Result<(), CircuitBuilderError> {
self.ensure_op_enabled(NonPrimitiveOpType::HashSqueeze)?;

self.push_non_primitive_op(NonPrimitiveOpType::HashSqueeze, outputs.to_vec());

Ok(())
}
}

#[cfg(test)]
mod tests {
use p3_baby_bear::BabyBear;
use p3_field::PrimeCharacteristicRing;

use super::*;
use crate::op::NonPrimitiveOpConfig;

#[test]
fn test_hash_absorb() {
let mut circuit = CircuitBuilder::<BabyBear>::new();
circuit.enable_op(
NonPrimitiveOpType::HashAbsorb { reset: true },
NonPrimitiveOpConfig::None,
);

let input1 = circuit.add_const(BabyBear::ONE);
let input2 = circuit.add_const(BabyBear::TWO);

circuit.add_hash_absorb(&[input1, input2], true).unwrap();
}

#[test]
fn test_hash_squeeze() {
let mut circuit = CircuitBuilder::<BabyBear>::new();
circuit.enable_op(NonPrimitiveOpType::HashSqueeze, NonPrimitiveOpConfig::None);

let output = circuit.add_public_input();

circuit.add_hash_squeeze(&[output]).unwrap();
}

#[test]
fn test_hash_absorb_squeeze_sequence() {
let mut circuit = CircuitBuilder::<BabyBear>::new();
circuit.enable_op(
NonPrimitiveOpType::HashAbsorb { reset: true },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need you would need to potentially separately allow with reset = true and reset = false?

NonPrimitiveOpConfig::None,
);
circuit.enable_op(NonPrimitiveOpType::HashSqueeze, NonPrimitiveOpConfig::None);

// Absorb
let input = circuit.add_const(BabyBear::ONE);
circuit.add_hash_absorb(&[input], true).unwrap();

// Squeeze
let output = circuit.add_public_input();
circuit.add_hash_squeeze(&[output]).unwrap();
}

#[test]
fn test_hash_absorb_not_enabled() {
let mut circuit = CircuitBuilder::<BabyBear>::new();

let input = circuit.add_const(BabyBear::ONE);
let result = circuit.add_hash_absorb(&[input], true);

assert!(result.is_err());
}
}
2 changes: 2 additions & 0 deletions circuit/src/ops/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod fri;
pub mod hash;
pub mod mmcs;

pub use fri::FriOps;
pub use hash::HashOps;
pub use mmcs::MmcsOps;
9 changes: 8 additions & 1 deletion circuit/src/tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@ impl<F: CircuitField> CircuitRunner<F> {
(NonPrimitiveOp::MmcsVerify { .. }, NonPrimitiveOpPrivateData::MmcsVerify(_)) => {
// Type match - good!
}
(NonPrimitiveOp::HashAbsorb { .. }, _) | (NonPrimitiveOp::HashSqueeze { .. }, _) => {
// HashAbsorb/HashSqueeze don't use private data
}
}

self.non_primitive_op_private_data[op_id.0 as usize] = Some(private_data);
Expand Down Expand Up @@ -547,7 +550,11 @@ impl<F: CircuitField> CircuitRunner<F> {
for op_idx in 0..self.circuit.non_primitive_ops.len() {
// Copy out leaf/root to end immutable borrow immediately
let NonPrimitiveOp::MmcsVerify { leaf, index, root } =
&self.circuit.non_primitive_ops[op_idx];
&self.circuit.non_primitive_ops[op_idx]
else {
// Skip non-MmcsVerify operations (e.g., HashAbsorb, HashSqueeze)
continue;
};

if let Some(Some(NonPrimitiveOpPrivateData::MmcsVerify(private_data))) =
self.non_primitive_op_private_data.get(op_idx).cloned()
Expand Down
1 change: 1 addition & 0 deletions recursion/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ p3-uni-stark.workspace = true
itertools.workspace = true
serde.workspace = true
thiserror.workspace = true
tracing.workspace = true

# Local dependencies
p3-circuit.workspace = true
Expand Down
153 changes: 153 additions & 0 deletions recursion/src/challenges.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Challenge target structures for STARK verification circuits.
//!
//! This module provides structured allocation of challenge targets,
//! encapsulating the Fiat-Shamir ordering and making challenge generation
//! more maintainable.

use alloc::vec;
use alloc::vec::Vec;

use p3_circuit::CircuitBuilder;
use p3_field::PrimeCharacteristicRing;
use p3_uni_stark::StarkGenericConfig;

use crate::Target;
use crate::circuit_challenger::CircuitChallenger;
use crate::circuit_verifier::ObservableCommitment;
use crate::recursive_challenger::RecursiveChallenger;
use crate::recursive_traits::{ProofTargets, Recursive};

/// Base STARK challenges (independent of PCS choice).
///
/// These are the fundamental challenges needed for any STARK verification:
/// - Alpha: for folding constraint polynomials
/// - Zeta, Zeta_next: for out-of-domain evaluation
#[derive(Debug, Clone)]
pub struct StarkChallenges {
/// Alpha: challenge for folding all constraint polynomials
pub alpha: Target,
/// Zeta: out-of-domain evaluation point
pub zeta: Target,
/// Zeta next: evaluation point for next row (zeta * g in the trace domain)
pub zeta_next: Target,
}

impl StarkChallenges {
/// Allocate base STARK challenge targets using Fiat-Shamir transform.
///
/// It will mutate the challenger state.
///
/// # Fiat-Shamir Ordering
/// 1. Observe domain parameters (degree_bits, log_quotient_degree)
/// 2. Observe trace commitment
/// 3. Observe public values
/// 4. **Sample alpha** (for constraint folding)
/// 5. Observe quotient chunks commitment
/// 6. Observe random commitment (if ZK mode)
/// 7. **Sample zeta** (OOD evaluation point)
/// 8. **Sample zeta_next** (next row evaluation point)
/// 9. Return challenger for PCS to continue sampling (betas, query indices)
pub fn allocate<SC, Comm, OpeningProof>(
circuit: &mut CircuitBuilder<SC::Challenge>,
challenger: &mut CircuitChallenger,
proof_targets: &ProofTargets<SC, Comm, OpeningProof>,
public_values: &[Target],
log_quotient_degree: usize,
) -> Self
where
SC: StarkGenericConfig,
SC::Challenge: PrimeCharacteristicRing,
Comm: Recursive<SC::Challenge> + ObservableCommitment,
OpeningProof: Recursive<SC::Challenge>,
{
// Extract commitment targets from proof
let trace_comm_targets = proof_targets
.commitments_targets
.trace_targets
.to_observation_targets();
let quotient_comm_targets = proof_targets
.commitments_targets
.quotient_chunks_targets
.to_observation_targets();
let random_comm_targets = proof_targets
.commitments_targets
.random_commit
.as_ref()
.map(|c| c.to_observation_targets());

// Observe domain parameters
let degree_bits_target =
circuit.add_const(SC::Challenge::from_usize(proof_targets.degree_bits));
let log_quotient_degree_target =
circuit.add_const(SC::Challenge::from_usize(log_quotient_degree));
challenger.observe(circuit, degree_bits_target);
challenger.observe(circuit, log_quotient_degree_target);

// Observe trace commitment
challenger.observe_slice(circuit, &trace_comm_targets);

// Observe public values
challenger.observe_slice(circuit, public_values);

// Sample alpha challenge
let alpha = challenger.sample(circuit);

// Observe quotient chunks commitment
challenger.observe_slice(circuit, &quotient_comm_targets);

// Observe random commitment if in ZK mode
if let Some(random_comm) = random_comm_targets {
challenger.observe_slice(circuit, &random_comm);
}

// Sample zeta and zeta_next challenges
let zeta = challenger.sample(circuit);
let zeta_next = challenger.sample(circuit);

Self {
alpha,
zeta,
zeta_next,
}
}

/// Convert to flat vector: [alpha, zeta, zeta_next]
pub fn to_vec(&self) -> Vec<Target> {
vec![self.alpha, self.zeta, self.zeta_next]
}

/// Get individual challenge targets.
pub fn alpha(&self) -> Target {
self.alpha
}

pub fn zeta(&self) -> Target {
self.zeta
}

pub fn zeta_next(&self) -> Target {
self.zeta_next
}
}

#[cfg(test)]
mod tests {
use p3_circuit::ExprId;

use super::*;

#[test]
fn test_stark_challenges_to_vec() {
let challenges = StarkChallenges {
alpha: ExprId(1),
zeta: ExprId(2),
zeta_next: ExprId(3),
};

let vec = challenges.to_vec();
assert_eq!(vec.len(), 3);
assert_eq!(vec[0], challenges.alpha);
assert_eq!(vec[1], challenges.zeta);
assert_eq!(vec[2], challenges.zeta_next);
}
}
Loading
Loading