Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 2 deletions crates/bitcell-zkp/src/battle_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
//! 1. The winner ID is valid (0, 1, or 2)
//! 2. The commitments match the public inputs
//!
//! Full battle verification requires extensive constraint programming to
//! verify the CA simulation steps, which is a complex undertaking.
//! **Note**: This is a simplified circuit for testing and development.
//! For production use with full CA evolution simulation, see `battle_constraints::BattleCircuit`.

use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError};
use ark_bn254::Fr;
Expand Down
138 changes: 138 additions & 0 deletions crates/bitcell-zkp/src/battle_constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,104 @@ fn compare_bits<F: PrimeField>(a: &[Boolean<F>], b: &[Boolean<F>]) -> Result<(Bo
Ok((greater, equal))
}

// Groth16 proof generation and verification for Bn254
use ark_bn254::{Bn254, Fr};
use ark_groth16::{Groth16, ProvingKey, VerifyingKey};
use ark_snark::SNARK;
use ark_std::rand::thread_rng;

impl BattleCircuit<Fr> {
/// Setup the circuit and generate proving/verifying keys
///
/// Returns an error if the circuit setup fails (e.g., due to constraint system issues).
///
/// **Note on RNG**: Uses `thread_rng()` which is cryptographically secure (ChaCha20-based).
/// For deterministic testing, consider using a seeded RNG from `ark_std::test_rng()`.
pub fn setup() -> crate::Result<(ProvingKey<Bn254>, VerifyingKey<Bn254>)> {
let rng = &mut thread_rng();

// Create empty circuit for setup
let circuit = Self {
initial_grid: Some(vec![vec![0u8; GRID_SIZE]; GRID_SIZE]),
final_grid: Some(vec![vec![0u8; GRID_SIZE]; GRID_SIZE]),
commitment_a: Some(Fr::from(0u64)),
commitment_b: Some(Fr::from(0u64)),
winner: Some(0),
pattern_a: Some(vec![vec![0u8; 3]; 3]),
pattern_b: Some(vec![vec![0u8; 3]; 3]),
nonce_a: Some(Fr::from(0u64)),
nonce_b: Some(Fr::from(0u64)),
};

Groth16::<Bn254>::circuit_specific_setup(circuit, rng)
.map_err(|e| crate::Error::ProofGeneration(format!("Circuit setup failed: {}", e)))
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

The setup() method returns Error::ProofGeneration for circuit setup failures. Since this is specifically a setup error (not proof generation), consider using Error::Setup instead for semantic correctness:

.map_err(|e| crate::Error::Setup(format!("Circuit setup failed: {}", e)))

This better matches the error type defined in lib.rs and makes debugging clearer.

Suggested change
.map_err(|e| crate::Error::ProofGeneration(format!("Circuit setup failed: {}", e)))
.map_err(|e| crate::Error::Setup(format!("Circuit setup failed: {}", e)))

Copilot uses AI. Check for mistakes.
}

/// Generate a proof for this circuit instance
pub fn prove(
&self,
pk: &ProvingKey<Bn254>,
) -> crate::Result<crate::Groth16Proof> {
let rng = &mut thread_rng();
let proof = Groth16::<Bn254>::prove(pk, self.clone(), rng)
.map_err(|e| crate::Error::ProofGeneration(e.to_string()))?;
Ok(crate::Groth16Proof::new(proof))
}

/// Verify a proof against public inputs
///
/// Public inputs should be in order:
/// 1. Initial grid cells (flattened)
/// 2. Final grid cells (flattened)
/// 3. Commitment A
/// 4. Commitment B
/// 5. Winner
pub fn verify(
vk: &VerifyingKey<Bn254>,
proof: &crate::Groth16Proof,
public_inputs: &[Fr],
) -> crate::Result<bool> {
Groth16::<Bn254>::verify(vk, public_inputs, &proof.proof)
.map_err(|e| crate::Error::ProofVerification)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

The error from Groth16::verify() is being discarded. This loses valuable debugging information about why verification failed. Consider preserving the error message like in the prove() method:

.map_err(|e| crate::Error::ProofVerification(e.to_string()))

Note: This would require updating the Error::ProofVerification variant to accept a String parameter.

Suggested change
.map_err(|e| crate::Error::ProofVerification)
.map_err(|e| crate::Error::ProofVerification(e.to_string()))

Copilot uses AI. Check for mistakes.
}

/// Helper to construct public inputs vector from circuit components
pub fn public_inputs(&self) -> Vec<Fr> {
let mut inputs = Vec::new();

// Add initial grid (flattened)
if let Some(ref grid) = self.initial_grid {
for row in grid {
for &cell in row {
inputs.push(Fr::from(cell as u64));
}
}
}

// Add final grid (flattened)
if let Some(ref grid) = self.final_grid {
for row in grid {
for &cell in row {
inputs.push(Fr::from(cell as u64));
}
}
}

// Add commitments and winner
if let Some(commitment_a) = self.commitment_a {
inputs.push(commitment_a);
}
if let Some(commitment_b) = self.commitment_b {
inputs.push(commitment_b);
}
if let Some(winner) = self.winner {
inputs.push(Fr::from(winner as u64));
}

inputs
Comment on lines +487 to +519
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

The public_inputs() method uses unwrap_or() with default values, which could silently hide missing data. If a field is None when it shouldn't be, this will use Fr::from(0u64) as the public input, potentially leading to incorrect verification. Consider either:

  1. Panicking when fields are None (they should be populated for proving/verification)
  2. Returning Result<Vec> to propagate errors
  3. Adding debug assertions to catch missing fields during development

Example:

pub fn public_inputs(&self) -> Vec<Fr> {
    debug_assert!(self.initial_grid.is_some(), "initial_grid must be set");
    let mut inputs = Vec::new();
    if let Some(ref grid) = self.initial_grid {
        // ...
    }
    // ...
}
Suggested change
pub fn public_inputs(&self) -> Vec<Fr> {
let mut inputs = Vec::new();
// Add initial grid (flattened)
if let Some(ref grid) = self.initial_grid {
for row in grid {
for &cell in row {
inputs.push(Fr::from(cell as u64));
}
}
}
// Add final grid (flattened)
if let Some(ref grid) = self.final_grid {
for row in grid {
for &cell in row {
inputs.push(Fr::from(cell as u64));
}
}
}
// Add commitments and winner
if let Some(commitment_a) = self.commitment_a {
inputs.push(commitment_a);
}
if let Some(commitment_b) = self.commitment_b {
inputs.push(commitment_b);
}
if let Some(winner) = self.winner {
inputs.push(Fr::from(winner as u64));
}
inputs
pub fn public_inputs(&self) -> crate::Result<Vec<Fr>> {
let mut inputs = Vec::new();
// Check required fields
let initial_grid = self.initial_grid.as_ref().ok_or(crate::Error::MissingField("initial_grid"))?;
let final_grid = self.final_grid.as_ref().ok_or(crate::Error::MissingField("final_grid"))?;
let commitment_a = self.commitment_a.ok_or(crate::Error::MissingField("commitment_a"))?;
let commitment_b = self.commitment_b.ok_or(crate::Error::MissingField("commitment_b"))?;
let winner = self.winner.ok_or(crate::Error::MissingField("winner"))?;
// Add initial grid (flattened)
for row in initial_grid {
for &cell in row {
inputs.push(Fr::from(cell as u64));
}
}
// Add final grid (flattened)
for row in final_grid {
for &cell in row {
inputs.push(Fr::from(cell as u64));
}
}
// Add commitments and winner
inputs.push(commitment_a);
inputs.push(commitment_b);
inputs.push(Fr::from(winner as u64));
Ok(inputs)

Copilot uses AI. Check for mistakes.
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -463,4 +561,44 @@ mod tests {
circuit.generate_constraints(cs.clone()).unwrap();
assert!(cs.is_satisfied().unwrap());
}

#[test]
#[ignore] // Expensive test - enable for full validation
fn test_battle_circuit_prove_verify_full() {
// Setup circuit
let (pk, vk) = BattleCircuit::<Fr>::setup().expect("Circuit setup should succeed");

// Use an empty grid - stable state
let initial_grid = vec![vec![0u8; GRID_SIZE]; GRID_SIZE];
let final_grid = initial_grid.clone();

let pattern_a = vec![vec![0u8; 3]; 3];
let pattern_b = vec![vec![0u8; 3]; 3];
let nonce_a = Fr::from(0u64);
let nonce_b = Fr::from(0u64);
let commitment_a = Fr::from(0u64);
let commitment_b = Fr::from(0u64);

let circuit = BattleCircuit {
initial_grid: Some(initial_grid.clone()),
final_grid: Some(final_grid),
commitment_a: Some(commitment_a),
commitment_b: Some(commitment_b),
winner: Some(2), // Tie
pattern_a: Some(pattern_a),
pattern_b: Some(pattern_b),
nonce_a: Some(nonce_a),
nonce_b: Some(nonce_b),
};

// Generate proof
let proof = circuit.prove(&pk).expect("Proof generation should succeed");

// Verify proof
let public_inputs = circuit.public_inputs();
assert!(
BattleCircuit::verify(&vk, &proof, &public_inputs).expect("Verification should complete"),
"Proof verification should succeed"
);
}
Comment on lines +565 to +603
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

The test is marked with #[ignore] and only verifies the happy path. Consider adding negative test cases (can also be marked as ignored if expensive):

  1. Verification with incorrect public inputs (should return false)
  2. Verification with wrong verifying key (should fail)
  3. Invalid CA evolution (final grid doesn't match simulation)

Example:

#[test]
#[ignore]
fn test_verify_with_wrong_public_inputs() {
    let (pk, vk) = BattleCircuit::<Fr>::setup().unwrap();
    let circuit = /* ... valid circuit ... */;
    let proof = circuit.prove(&pk).unwrap();
    
    // Change one public input
    let mut wrong_inputs = circuit.public_inputs();
    wrong_inputs[0] = Fr::from(999u64);
    assert!(!BattleCircuit::verify(&vk, &proof, &wrong_inputs).unwrap());
}

Copilot uses AI. Check for mistakes.
}
55 changes: 50 additions & 5 deletions crates/bitcell-zkp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,52 @@
//! - State transition verification (Merkle updates)
//! - Merkle tree inclusion proofs
//!
//! Note: v0.1 provides circuit structure and basic constraints.
//! Full CA evolution verification requires extensive constraint programming.
//! ## Circuit Implementations
//!
//! This crate provides two tiers of circuit implementations:
//!
//! ### Simplified Circuits (battle_circuit, state_circuit)
//! - **Purpose**: Fast testing, development, and basic validation
//! - **Constraints**: Minimal (winner validation, root non-equality)
//! - **Performance**: Very fast proof generation (~1-2 seconds)
//! - **Security**: Cryptographically sound but doesn't verify full computation
//!
//! ### Full Constraint Circuits (battle_constraints, state_constraints)
//! - **Purpose**: Production deployment with complete verification
//! - **Constraints**: Complete CA evolution simulation and Merkle tree verification
//! - **Performance**: Slower proof generation (30-60 seconds for battles)
//! - **Security**: Fully verifies all computation steps
//!
//! ## Usage
//!
//! ```rust,ignore
//! use bitcell_zkp::{battle_constraints::BattleCircuit, Groth16Proof};
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

[nitpick] The example shows importing battle_constraints::BattleCircuit, but since line 67 exports BattleCircuit as the default, users can now directly use use bitcell_zkp::BattleCircuit; instead. Consider updating the example to reflect the simpler import path:

use bitcell_zkp::{BattleCircuit, Groth16Proof};

This would better demonstrate the intended default usage pattern.

Suggested change
//! use bitcell_zkp::{battle_constraints::BattleCircuit, Groth16Proof};
//! use bitcell_zkp::{BattleCircuit, Groth16Proof};

Copilot uses AI. Check for mistakes.
//! use ark_bn254::Fr;
//!
//! // Setup (one-time, reusable)
//! let (pk, vk) = BattleCircuit::<Fr>::setup().unwrap();
//!
//! // Create circuit instance
//! let circuit = BattleCircuit::new(
//! initial_grid,
//! final_grid,
//! commitment_a,
//! commitment_b,
//! winner_id,
//! ).with_witnesses(pattern_a, pattern_b, nonce_a, nonce_b);
//!
//! // Generate proof
//! let proof = circuit.prove(&pk).unwrap();
//!
//! // Verify proof
//! let public_inputs = circuit.public_inputs();
//! assert!(BattleCircuit::verify(&vk, &proof, &public_inputs).unwrap());
//! ```

pub mod battle_circuit;
pub mod state_circuit;

// New: Full constraint implementations
// Full constraint implementations for production
pub mod battle_constraints;
pub mod state_constraints;

Expand All @@ -20,8 +59,14 @@ pub mod merkle_gadget;
// Production-ready Poseidon-based Merkle verification
pub mod poseidon_merkle;

pub use battle_circuit::BattleCircuit;
pub use state_circuit::StateCircuit;
// Export simplified circuits for backward compatibility
pub use battle_circuit::BattleCircuit as SimpleBattleCircuit;
pub use state_circuit::StateCircuit as SimpleStateCircuit;

// Export full circuits as recommended defaults
pub use battle_constraints::BattleCircuit;
pub use state_constraints::{StateCircuit, NullifierCircuit};

pub use merkle_gadget::{MerklePathGadget, MERKLE_DEPTH};
pub use poseidon_merkle::{PoseidonMerkleGadget, POSEIDON_MERKLE_DEPTH};

Expand Down
9 changes: 6 additions & 3 deletions crates/bitcell-zkp/src/state_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ use ark_std::Zero;
/// This circuit proves that a state transition occurred correctly by verifying:
/// 1. The old and new state roots are different (state changed)
/// 2. The nullifier is properly computed to prevent double-spending
/// 3. The Merkle tree update is valid (TODO: full implementation)
///
/// **Note**: This is a simplified circuit for testing and development.
/// For production use with full Merkle tree verification, see `state_constraints::StateCircuit`.
#[derive(Clone)]
pub struct StateCircuit {
// Public inputs
Expand Down Expand Up @@ -135,8 +137,9 @@ impl ConstraintSynthesizer<Fr> for StateCircuit {
ark_relations::lc!() + ark_relations::r1cs::Variable::One,
)?;

// TODO: Add full Merkle tree verification constraints
// This would include:
// Note: This simplified circuit only verifies state change (old_root != new_root).
// Full Merkle tree verification is implemented in state_constraints::StateCircuit,
// which includes:
// - Verifying the old leaf at leaf_index against old_state_root
// - Verifying the new leaf at leaf_index against new_state_root
// - Ensuring the nullifier is derived from the old leaf
Expand Down
Loading