Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions circuit-prover/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ path = "examples/fibonacci.rs"
[[example]]
name = "fake_merkle_verify"
path = "examples/fake_merkle_verify.rs"

[[example]]
name = "hash"
path = "examples/hash.rs"
60 changes: 60 additions & 0 deletions circuit-prover/examples/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use core::array;
use std::env;

/// Hash verification circuit: prove the correct computation of a hash.
/// Public inputs: inputs, output
use p3_baby_bear::{BabyBear, Poseidon2BabyBear};
use p3_circuit::builder::{CIRCUIT_HASH_RATE, CIRCUIT_HASH_STATE_SIZE, CircuitBuilder};
use p3_circuit::{Challenger, FakeMerklePrivateData, NonPrimitiveOpPrivateData};
use p3_circuit_prover::MultiTableProver;
use p3_field::PrimeCharacteristicRing;
use p3_symmetric::{CryptographicHasher, PaddingFreeSponge};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};

type F = BabyBear;

pub type CircuitPerm = Poseidon2BabyBear<CIRCUIT_HASH_STATE_SIZE>;
pub type CircuitHash =
PaddingFreeSponge<CircuitPerm, CIRCUIT_HASH_STATE_SIZE, CIRCUIT_HASH_RATE, CIRCUIT_HASH_RATE>;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rng = SmallRng::seed_from_u64(1);
let perm = CircuitPerm::new_from_rng_128(&mut rng);

let mut builder = CircuitBuilder::<F>::new();

let mut challenger = Challenger::new();

// Public inputs: hash inputs and hash output
let inputs: [_; 16] = array::from_fn(|_| builder.add_public_input());
let outputs: [_; 8] = array::from_fn(|_| builder.add_public_input());

// Add hash operations
challenger.add_inputs(&inputs);
challenger.squeeze(&mut builder, &outputs);

let circuit = builder.build();
let mut runner = circuit.runner();

// Generate random inputs
let input_values: [_; 16] = array::from_fn(|_| rng.random::<F>());

// Set public inputs
let hasher = CircuitHash::new(perm.clone());
let output_values = hasher.hash_iter(input_values);
let public_inputs = input_values
.into_iter()
.chain(output_values.into_iter())
.collect::<Vec<_>>();
runner.set_public_inputs(&public_inputs)?;

let traces = runner.run_with_hash(perm)?;
let multi_prover = MultiTableProver::new();
let proof = multi_prover.prove_all_tables(&traces)?;
multi_prover.verify_all_tables(&proof)?;

println!("✅ Verified hash({:?}) = {:?}", input_values, output_values);

Ok(())
}
1 change: 1 addition & 0 deletions circuit-prover/src/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub struct MultiTableProof {
pub mul: TableProof,
pub sub: TableProof,
pub fake_merkle: TableProof,
// TODO: add HashSponge table
// Extension field degree used for proving (1 or 4 for now)
pub ext_degree: usize,
}
Expand Down
2 changes: 2 additions & 0 deletions circuit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ categories.workspace = true
hashbrown = "0.16.0"
p3-baby-bear.workspace = true
p3-field.workspace = true
p3-symmetric.workspace = true
rand.workspace = true
serde.workspace = true
49 changes: 48 additions & 1 deletion circuit/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
use std::collections::HashMap;

use p3_baby_bear::Poseidon2BabyBear;
use p3_field::PrimeCharacteristicRing;
use p3_symmetric::PaddingFreeSponge;

use crate::circuit::Circuit;
use crate::expr::{Expr, ExpressionGraph};
use crate::op::{NonPrimitiveOp, NonPrimitiveOpType, Prim};
use crate::types::{ExprId, NonPrimitiveOpId, WitnessAllocator, WitnessId};

pub const CIRCUIT_HASH_RATE: usize = 8;
pub const CIRCUIT_HASH_CAPACITY: usize = 8;
pub const CIRCUIT_HASH_STATE_SIZE: usize = CIRCUIT_HASH_RATE + CIRCUIT_HASH_CAPACITY;

/// Builder for constructing circuits using a fluent API
///
/// This struct provides methods to build up a computation graph by adding:
Expand Down Expand Up @@ -104,6 +110,26 @@ impl<F: Clone> CircuitBuilder<F> {

op_id
}

/// Add a hash absorb operation (non-primitive operation)
pub fn add_hash_absorb(&mut self, input_exprs: &[ExprId], reset: bool) -> NonPrimitiveOpId {
let op_id = NonPrimitiveOpId(self.non_primitive_ops.len() as u32);
let witness_exprs = input_exprs.to_vec();
self.non_primitive_ops
.push((op_id, NonPrimitiveOpType::HashAbsorb(reset), witness_exprs));

op_id
}

/// Add a hash squeeze operation (non-primitive operation)
pub fn add_hash_squeeze(&mut self, output_exprs: &[ExprId]) -> NonPrimitiveOpId {
let op_id = NonPrimitiveOpId(self.non_primitive_ops.len() as u32);
let witness_exprs = output_exprs.to_vec();
self.non_primitive_ops
.push((op_id, NonPrimitiveOpType::HashSqueeze, witness_exprs));

op_id
}
}

impl<F: Clone + PrimeCharacteristicRing + PartialEq + Eq + std::hash::Hash> CircuitBuilder<F> {
Expand Down Expand Up @@ -313,7 +339,28 @@ impl<F: Clone + PrimeCharacteristicRing + PartialEq + Eq + std::hash::Hash> Circ
leaf: leaf_widx,
root: root_widx,
});
} // Future operations can be added here with different witness expression counts
}
NonPrimitiveOpType::HashAbsorb(reset) => {
let inputs_widx = witness_exprs
.iter()
.map(|expr| Self::get_witness_id(expr_to_widx, *expr, "HashAbsorb input"))
.collect();

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

lowered_ops.push(NonPrimitiveOp::HashSqueeze {
outputs: outputs_widx,
});
}
}
}

Expand Down
50 changes: 50 additions & 0 deletions circuit/src/challenger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use crate::{CircuitBuilder, ExprId};

pub struct Challenger<const R: usize> {
pub inputs: Vec<ExprId>,
pub reset_flag: bool,
}

impl<const R: usize> Challenger<R> {
pub fn new() -> Self {
Self {
inputs: Vec::new(),
reset_flag: false,
}
}

pub fn add_input(&mut self, expr_id: ExprId) {
self.inputs.push(expr_id);
}

pub fn add_inputs(&mut self, expr_ids: &[ExprId]) {
self.inputs.extend_from_slice(expr_ids);
}

pub fn clear(&mut self) {
self.inputs.clear();
self.reset_flag = true;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Add some comments explaining what these functions are doing?

pub fn squeeze<F: Clone>(&mut self, builder: &mut CircuitBuilder<F>, outputs: &[ExprId; R]) {
assert!(
self.inputs.len() % R == 0,
"Number of inputs must be a multiple of R"
);

let input_chunks = self.inputs.chunks_exact(R);

for chunk in input_chunks {
if self.reset_flag {
builder.add_hash_absorb(chunk, true);
self.reset_flag = false;
} else {
builder.add_hash_absorb(chunk, false);
}
}
self.inputs.clear();
self.reset_flag = false;

builder.add_hash_squeeze(outputs);
}
}
2 changes: 2 additions & 0 deletions circuit/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod builder;
pub mod challenger;
pub mod circuit;
pub mod expr;
pub mod op;
Expand All @@ -7,6 +8,7 @@ pub mod types;

// Re-export public API
pub use builder::CircuitBuilder;
pub use challenger::Challenger;
pub use circuit::Circuit;
pub use expr::{Expr, ExpressionGraph};
pub use op::{FakeMerklePrivateData, NonPrimitiveOp, NonPrimitiveOpPrivateData, Prim};
Expand Down
38 changes: 38 additions & 0 deletions circuit/src/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub enum Prim<F> {
#[derive(Debug, Clone, PartialEq)]
pub enum NonPrimitiveOpType {
FakeMerkleVerify,
HashAbsorb(bool), // bool: If true, reset the capacity before absorbing
HashSqueeze,
// Future: FriVerify, HashAbsorb, etc.
}

Expand Down Expand Up @@ -86,6 +88,22 @@ pub enum NonPrimitiveOp {
/// - Merkle path siblings and direction bits
/// - See `FakeMerklePrivateData` for complete specification
FakeMerkleVerify { leaf: WitnessId, root: WitnessId },
/// Sponge hash absorb operation. Absorbs a single chunk of input.
///
/// Public interface (on witness bus):
/// - `reset_flag`: Whether to reset the capacity before absorbing
/// - `inputs`: The input chunk to be absorbed.
///
/// Private data (set via NonPrimitiveOpId):
/// - `reset`: Whether to reset the capacity before absorbing.
/// - `capacity`: Elements of the state not directly affected by input.`
HashAbsorb {
reset_flag: bool,
inputs: Vec<WitnessId>,
},
/// Sponge hash squeeze operation. Produces a single chunk of output.
/// - `capacity`: The rest of the state, not part of the output.
HashSqueeze { outputs: Vec<WitnessId> },
}

/// Private auxiliary data for non-primitive operations
Expand All @@ -103,6 +121,10 @@ pub enum NonPrimitiveOpPrivateData<F> {
/// to generate a valid proof. This data is not part of the public
/// circuit specification.
FakeMerkleVerify(FakeMerklePrivateData<F>),
/// Private data for the hash absorb operation
HashAbsorb(HashAbsorbPrivateData<F>),
/// Private data for the hash squeeze operation
HashSqueeze(HashSqueezePrivateData<F>),
}

/// Private Merkle path data for fake Merkle verification (simplified)
Expand Down Expand Up @@ -131,3 +153,19 @@ pub struct FakeMerklePrivateData<F> {
/// hash input ordering: `hash(current, sibling)` vs `hash(sibling, current)`.
pub path_directions: Vec<bool>,
}

/// Private data for the hash absorb operation
#[derive(Debug, Clone, PartialEq)]
pub struct HashAbsorbPrivateData<F> {
/// Flag indicating whether to reset the capacity before absorbing
pub reset: bool,
/// Current capacity of the sponge state; zeros if `reset` is true
pub capacity: Vec<F>,
}

/// Private data for the hash squeeze operation
#[derive(Debug, Clone, PartialEq)]
pub struct HashSqueezePrivateData<F> {
/// Current capacity of the sponge state
pub capacity: Vec<F>,
}
Loading