Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 8, 2025

Replaces stubbed ZKP code with production Groth16 circuits. Battle outcomes and state transitions now require cryptographic proof verification, not boolean checks.

Architecture

Two-tier system:

  • Full circuits (now default): Complete constraint verification
    • BattleCircuit: Conway's Game of Life simulation (64×64 grid, 10 steps)
    • StateCircuit: Merkle tree verification (depth 32)
    • NullifierCircuit: Set membership for double-spend prevention
  • Simplified circuits: Fast testing (SimpleBattleCircuit, SimpleStateCircuit)

Implementation

battle_constraints.rs / state_constraints.rs (+348 lines)

  • Added setup(), prove(), verify(), public_inputs() to all circuits
  • Groth16 over BN254 curve (~128-bit security)
  • Cryptographically secure RNG (ChaCha20-based thread_rng())

lib.rs (+55 lines)

  • Default exports now point to full implementations
  • Simplified circuits available for testing/CI

Usage

use bitcell_zkp::{BattleCircuit, StateCircuit};
use ark_bn254::Fr;

// One-time setup (reusable keys)
let (pk, vk) = BattleCircuit::<Fr>::setup()?;

// Create circuit with witnesses
let circuit = BattleCircuit::new(initial_grid, final_grid, commitment_a, commitment_b, winner)
    .with_witnesses(pattern_a, pattern_b, nonce_a, nonce_b);

// Generate and verify proof
let proof = circuit.prove(&pk)?;
let valid = BattleCircuit::verify(&vk, &proof, &circuit.public_inputs())?;

Testing

18 tests (17 passing, 1 performance test ignored). All consensus integration tests pass.

Production Notes

Current parameters (GRID_SIZE=64, BATTLE_STEPS=10) enable CI/CD feasibility. Mainnet deployment should:

  1. Increase to production values (1024×1024, 1000 steps)
  2. Replace simplified hash with Poseidon (module exists: poseidon_merkle.rs)
  3. Conduct trusted setup ceremony for circuit-specific parameters
Original prompt

This section details on the original issue you should resolve

<issue_title>Implement Zero-Knowledge Proof (ZKP) Circuits for Consensus Verification</issue_title>
<issue_description>Part of Epic: #71 - RC1: Zero-Knowledge & Observability


Goal: Replace stubbed ZKP code with real Groth16-based circuits for battle outcome and state transitions.

Requirements:

  • Implement Groth16Proof struct with real proof verification
  • Real constraints for CA evolution and state transition
  • Update BattleCircuit, StateCircuit code for proof generation and verification

Files:

  • crates/bitcell-zkp/src/lib.rs
  • crates/bitcell-zkp/src/battle_circuit.rs
  • crates/bitcell-zkp/src/state_circuit.rs

Acceptance Criteria:

  • Valid battles and transitions result in cryptographically verified proofs
  • No reliance on stub checks for consensus

See [todo_now.md](https://github.com/Steake/BitCell/blob/master/todo_now. md) for audit findings.</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 3 commits December 8, 2025 22:20
Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement ZKP circuits for consensus verification Implement Groth16 ZKP circuits for battle and state verification Dec 8, 2025
Copilot AI requested a review from Steake December 8, 2025 22:43
@Steake Steake marked this pull request as ready for review December 8, 2025 22:44
Copilot AI review requested due to automatic review settings December 8, 2025 22:44
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements production-ready Groth16 zero-knowledge proof circuits for BitCell's consensus verification system, replacing stub implementations with cryptographically secure circuits. The implementation introduces a two-tier architecture where full constraint circuits (with complete CA evolution and Merkle tree verification) become the default exports, while simplified circuits remain available for testing.

Key Changes:

  • Implemented setup(), prove(), verify(), and public_inputs() methods for BattleCircuit, StateCircuit, and NullifierCircuit using Groth16 over BN254 curve
  • Changed default exports to point to full constraint implementations (battle_constraints, state_constraints) with backward-compatible aliases for simplified circuits
  • Added comprehensive tests for proof generation and verification workflows

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
crates/bitcell-zkp/src/lib.rs Updated module documentation to explain two-tier circuit architecture; changed default exports to full constraint implementations; added backward-compatible aliases
crates/bitcell-zkp/src/battle_constraints.rs Implemented Groth16 proof generation/verification API with setup(), prove(), verify(), and public_inputs() helper methods; added integration test
crates/bitcell-zkp/src/state_constraints.rs Implemented Groth16 APIs for StateCircuit and NullifierCircuit with proof generation/verification; added comprehensive tests
crates/bitcell-zkp/src/battle_circuit.rs Updated documentation to clarify simplified circuit usage and reference full implementation
crates/bitcell-zkp/src/state_circuit.rs Updated documentation to clarify simplified circuit usage and reference full implementation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.
Comment on lines +483 to +517
#[test]
fn test_nullifier_circuit_prove_verify() {
// Setup circuit
let (pk, vk) = NullifierCircuit::<Fr>::setup().expect("Circuit setup should succeed");

let nullifier = Fr::from(42u64);
let path = vec![Fr::from(0u64); MERKLE_DEPTH];
let indices = vec![false; MERKLE_DEPTH];

// Compute root
let mut root = nullifier;
for i in 0..MERKLE_DEPTH {
let left = if indices[i] { path[i] } else { root };
let right = if indices[i] { root } else { path[i] };
root = left * left + right * right + left * right + Fr::from(1u64);
}

let circuit = NullifierCircuit {
nullifier: Some(nullifier),
set_root: Some(root),
is_member: Some(true),
path: Some(path),
indices: Some(indices),
};

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

// Verify proof using helper method
let public_inputs = circuit.public_inputs();
assert!(
NullifierCircuit::verify(&vk, &proof, &public_inputs).expect("Verification should complete"),
"Proof verification should succeed"
);
}
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 tests only verify the happy path (valid proof with correct public inputs). Consider adding negative test cases:

  1. Verification with incorrect public inputs (should return false)
  2. Verification with wrong verifying key (should fail)
  3. Tampered proof data (should fail)

Example:

#[test]
fn test_verify_with_wrong_public_inputs() {
    let (pk, vk) = NullifierCircuit::<Fr>::setup().unwrap();
    let circuit = /* ... valid circuit ... */;
    let proof = circuit.prove(&pk).unwrap();
    
    // Wrong public inputs
    let wrong_inputs = vec![Fr::from(999u64), Fr::from(999u64), Fr::from(0u64)];
    assert!(!NullifierCircuit::verify(&vk, &proof, &wrong_inputs).unwrap());
}

Copilot uses AI. Check for mistakes.
Ok(crate::Groth16Proof::new(proof))
}

/// Verify a proof against public inputs
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 verify() method documentation for NullifierCircuit is missing the public inputs specification. For consistency with StateCircuit and BattleCircuit, add documentation:

/// Verify a proof against public inputs
///
/// Public inputs should be in order:
/// 1. Nullifier
/// 2. Set root
/// 3. Is member (0 or 1)
pub fn verify(

This helps users understand the expected input format.

Suggested change
/// Verify a proof against public inputs
/// Verify a proof against public inputs
///
/// Public inputs should be in order:
/// 1. Nullifier
/// 2. Set root
/// 3. Is member (0 or 1)

Copilot uses AI. Check for mistakes.
};

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.
};

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.

impl NullifierCircuit<Fr> {
/// Setup the circuit and generate proving/verifying keys
///
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 setup() method documentation for NullifierCircuit is missing the error handling description that appears in StateCircuit's setup(). For consistency, add:

/// 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()`.

This maintains consistent documentation across all circuit implementations.

Suggested change
///
///
/// Returns an error if the circuit setup fails (e.g., due to constraint system issues).
///

Copilot uses AI. Check for mistakes.
.map_err(|e| crate::Error::ProofGeneration(format!("Circuit setup failed: {}", e)))
}

/// Generate a proof for this circuit instance
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 prove() method lacks documentation. For API consistency with other methods, consider adding:

/// Generate a proof for this circuit instance
///
/// Returns an error if proof generation fails (e.g., due to unsatisfiable constraints).
pub fn prove(

This helps users understand when and why proof generation might fail.

Suggested change
/// Generate a proof for this circuit instance
/// Generate a proof for this circuit instance
///
/// Returns an error if proof generation fails (e.g., due to unsatisfiable constraints).

Copilot uses AI. Check for mistakes.
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.
Comment on lines +303 to +308
pub fn public_inputs(&self) -> Vec<Fr> {
vec![
self.nullifier.unwrap_or(Fr::from(0u64)),
self.set_root.unwrap_or(Fr::from(0u64)),
Fr::from(if self.is_member.unwrap_or(false) { 1u64 } else { 0u64 }),
]
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.nullifier.is_some(), "nullifier must be set");
    vec![
        self.nullifier.unwrap_or(Fr::from(0u64)),
        // ...
    ]
}
Suggested change
pub fn public_inputs(&self) -> Vec<Fr> {
vec![
self.nullifier.unwrap_or(Fr::from(0u64)),
self.set_root.unwrap_or(Fr::from(0u64)),
Fr::from(if self.is_member.unwrap_or(false) { 1u64 } else { 0u64 }),
]
pub fn public_inputs(&self) -> crate::Result<Vec<Fr>> {
let nullifier = self.nullifier.ok_or(crate::Error::MissingPublicInput("nullifier"))?;
let set_root = self.set_root.ok_or(crate::Error::MissingPublicInput("set_root"))?;
let is_member = self.is_member.ok_or(crate::Error::MissingPublicInput("is_member"))?;
Ok(vec![
nullifier,
set_root,
Fr::from(if is_member { 1u64 } else { 0u64 }),
])

Copilot uses AI. Check for mistakes.
Comment on lines +487 to +519
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
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.
@Steake Steake merged commit 13a27b7 into master Dec 11, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Zero-Knowledge Proof (ZKP) Circuits for Consensus Verification

2 participants