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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## Run `just` to see recipes.

# List available recipes
default:
@just --list

# Format all crates
fmt:
cargo fmt --all

# Check formatting without modifying files (CI-friendly)
fmt-check:
cargo fmt --all --check

# Lint with clippy across the workspace
lint:
cargo clippy --workspace --all-targets

# Lint including the native proving backends
lint-all:
cargo clippy --workspace --all-targets --features "prove-arkworks,prove-lambdaworks"

# Run the test suite (e2e needs `circom` + `snarkjs` on PATH)
test:
cargo test --workspace

# Run tests including the native proving backends
test-all:
cargo test --workspace --features "prove-arkworks,prove-lambdaworks"

# Format, lint, and test — the pre-commit gate
check: fmt lint test

# Same as `check` but with native backends enabled
check-all: fmt lint-all test-all
12 changes: 10 additions & 2 deletions crates/circomkit-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::{Context, Result};
use clap::{Parser, Subcommand};

use circomkit::Circomkit;
use circomkit::ProvingBackendKind;

#[derive(Parser)]
#[command(
Expand Down Expand Up @@ -94,6 +95,9 @@ enum Commands {
circuit: String,
/// Input name
input: String,
/// Override the proving backend (snarkjs, arkworks, lambdaworks)
#[arg(long)]
backend: Option<ProvingBackendKind>,
},

/// Verify a proof
Expand Down Expand Up @@ -243,8 +247,12 @@ fn main() -> Result<()> {
println!("witness: {}", path.display());
}

Commands::Prove { circuit, input } => {
let path = ck.prove(&circuit, &input, None)?;
Commands::Prove {
circuit,
input,
backend,
} => {
let path = ck.prove(&circuit, &input, None, backend)?;
println!("proof: {}", path.display());
}

Expand Down
3 changes: 3 additions & 0 deletions crates/circomkit-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ download = ["dep:ureq"]
[dependencies.ureq]
workspace = true
optional = true

[dev-dependencies]
tempfile.workspace = true
56 changes: 56 additions & 0 deletions crates/circomkit-core/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,31 @@ pub enum ProvingBackendKind {
Lambdaworks,
}

impl fmt::Display for ProvingBackendKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Snarkjs => write!(f, "snarkjs"),
Self::Arkworks => write!(f, "arkworks"),
Self::Lambdaworks => write!(f, "lambdaworks"),
}
}
}

impl std::str::FromStr for ProvingBackendKind {
type Err = String;

fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"snarkjs" => Ok(Self::Snarkjs),
"arkworks" => Ok(Self::Arkworks),
"lambdaworks" => Ok(Self::Lambdaworks),
other => Err(format!(
"unknown proving backend '{other}' (expected: snarkjs, arkworks, lambdaworks)"
)),
}
}
}

/// Log level for circomkit operations.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
Expand All @@ -95,3 +120,34 @@ impl LogLevel {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn proving_backend_kind_from_str_roundtrips() {
for kind in [
ProvingBackendKind::Snarkjs,
ProvingBackendKind::Arkworks,
ProvingBackendKind::Lambdaworks,
] {
let parsed: ProvingBackendKind = kind.to_string().parse().unwrap();
assert_eq!(parsed, kind);
}
// Case-insensitive parsing.
assert_eq!(
"ARKWORKS".parse::<ProvingBackendKind>().unwrap(),
ProvingBackendKind::Arkworks
);
}

#[test]
fn proving_backend_kind_from_str_rejects_unknown() {
let err = "plonky2".parse::<ProvingBackendKind>().unwrap_err();
assert!(
err.contains("plonky2"),
"error should name the bad input: {err}"
);
}
}
2 changes: 1 addition & 1 deletion crates/circomkit-core/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod ptau;
mod r1cs;
mod witness;

pub use primes::{prime_from_value, prime_value};
pub use primes::{prime_field_n8, prime_from_value, prime_value};
pub use ptau::{download_ptau, ptau_name_for_constraints, ptau_path_if_exists};
pub use r1cs::{parse_r1cs_bytes, parse_r1cs_info, read_r1cs_file, read_r1cs_info};
pub use witness::{
Expand Down
26 changes: 26 additions & 0 deletions crates/circomkit-core/src/utils/primes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ pub fn prime_value(prime: Prime) -> BigUint {
}
}

/// Returns the number of bytes used to encode a field element of the given prime
/// in Circom's binary `.wtns`/`.r1cs` formats (the `n8` field).
///
/// Matches snarkjs/ffjavascript: the modulus bit-length rounded up to a whole
/// number of 64-bit words, times 8. e.g. bn128 (254 bits) → 32, goldilocks
/// (64 bits) → 8.
///
/// TODO: add link here
pub fn prime_field_n8(prime: Prime) -> u32 {
let bits = prime_value(prime).bits();
(((bits - 1) / 64 + 1) * 8) as u32
}

/// Attempts to identify a `Prime` variant from its field value.
pub fn prime_from_value(value: &BigUint) -> Option<Prime> {
let primes = [
Expand Down Expand Up @@ -81,4 +94,17 @@ mod tests {
let unknown = BigUint::from(42u32);
assert_eq!(prime_from_value(&unknown), None);
}

#[test]
fn field_n8_matches_snarkjs_word_alignment() {
// 254/255-bit curves pack into 32 bytes (4 64-bit words).
assert_eq!(prime_field_n8(Prime::Bn128), 32);
assert_eq!(prime_field_n8(Prime::Bls12381), 32);
assert_eq!(prime_field_n8(Prime::Grumpkin), 32);
assert_eq!(prime_field_n8(Prime::Pallas), 32);
assert_eq!(prime_field_n8(Prime::Vesta), 32);
assert_eq!(prime_field_n8(Prime::Secq256r1), 32);
// Goldilocks is a 64-bit field — a single word.
assert_eq!(prime_field_n8(Prime::Goldilocks), 8);
}
}
53 changes: 43 additions & 10 deletions crates/circomkit-core/src/utils/witness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::path::Path;
use num_bigint::BigInt;
use num_traits::Signed;

use super::primes::prime_value;
use super::primes::{prime_field_n8, prime_value};
use crate::error::{CoreError, Result};
use crate::types::Witness;

Expand Down Expand Up @@ -98,15 +98,14 @@ pub fn parse_witness_to_elems<T>(
))
}

/// Write a witness to a binary `.wtns` file (version 2, BN128 prime).
///
/// Uses 32 bytes per field element (n8 = 32), which is standard for BN128/BLS12-381.
///
/// TODO: take prime as argument
pub fn write_witness_file(path: &Path, witness: &Witness) -> Result<()> {
let n8: u32 = 32;
let prime = prime_value(crate::enums::Prime::Bn128);
let prime_bytes = prime.to_bytes_le();
/// Write a witness to a binary `.wtns` file (version 2) for the given prime.
pub fn write_witness_file(
path: &Path,
witness: &Witness,
prime: crate::enums::Prime,
) -> Result<()> {
let n8 = prime_field_n8(prime);
let prime_bytes = prime_value(prime).to_bytes_le();

let witness_count = witness.len() as u32;

Expand Down Expand Up @@ -155,3 +154,37 @@ pub fn write_witness_file(path: &Path, witness: &Witness) -> Result<()> {
std::fs::write(path, buf)?;
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::enums::Prime;

/// Extract the `n8` field from section 1 of a written witness file.
/// Layout: header (12 bytes) + section id (4) + section length (8) → n8 at offset 24.
fn n8_from_bytes(buf: &[u8]) -> u32 {
u32::from_le_bytes(buf[24..28].try_into().unwrap())
}

#[test]
fn writes_n8_per_prime_and_roundtrips() {
let dir = tempfile::tempdir().unwrap();

// create a fake witness
let witness = vec![BigInt::from(1), BigInt::from(42), BigInt::from(255)];

// bn128 packs elements into 32 bytes
let bn = dir.path().join("bn128.wtns");
write_witness_file(&bn, &witness, Prime::Bn128).unwrap();
let bytes = std::fs::read(&bn).unwrap();
assert_eq!(n8_from_bytes(&bytes), 32);
assert_eq!(parse_witness_bytes(&bytes).unwrap(), witness);

// goldilocks packs elements into 8 bytes
let gl = dir.path().join("goldilocks.wtns");
write_witness_file(&gl, &witness, Prime::Goldilocks).unwrap();
let bytes = std::fs::read(&gl).unwrap();
assert_eq!(n8_from_bytes(&bytes), 8);
assert_eq!(parse_witness_bytes(&bytes).unwrap(), witness);
}
}
8 changes: 4 additions & 4 deletions crates/circomkit-prove/src/lambdaworks/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ fn field_to_dec<F: IsPrimeField>(elem: &FieldElement<F>) -> String {
pub fn proof_to_snarkjs_json(proof: &Proof) -> serde_json::Value {
serde_json::json!({
"pi_a": [
field_to_dec(&proof.pi1.x()),
field_to_dec(&proof.pi1.y()),
field_to_dec(proof.pi1.x()),
field_to_dec(proof.pi1.y()),
"1"
],
"pi_b": [
Expand All @@ -40,8 +40,8 @@ pub fn proof_to_snarkjs_json(proof: &Proof) -> serde_json::Value {
["1", "0"]
],
"pi_c": [
field_to_dec(&proof.pi3.x()),
field_to_dec(&proof.pi3.y()),
field_to_dec(proof.pi3.x()),
field_to_dec(proof.pi3.y()),
"1"
],
"protocol": "groth16",
Expand Down
16 changes: 8 additions & 8 deletions crates/circomkit/src/circomkit/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@ impl Circomkit {
};

// Check source .circom file mtime
if let Ok(source_mtime) = source_path.metadata().and_then(|m| m.modified()) {
if source_mtime > r1cs_mtime {
return false;
}
if let Ok(source_mtime) = source_path.metadata().and_then(|m| m.modified())
&& source_mtime > r1cs_mtime
{
return false;
}

// Check generated main file mtime
if let Ok(main_mtime) = main_path.metadata().and_then(|m| m.modified()) {
if main_mtime > r1cs_mtime {
return false;
}
if let Ok(main_mtime) = main_path.metadata().and_then(|m| m.modified())
&& main_mtime > r1cs_mtime
{
return false;
}

true
Expand Down
16 changes: 10 additions & 6 deletions crates/circomkit/src/circomkit/prove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ impl Circomkit {
input: &str,
data: Option<&CircuitSignals>,
) -> Result<PathBuf> {
let prime = self.resolve(circuit)?.compiler.prime;

let input_data = match data {
Some(d) => d.clone(),
None => self.load_input(circuit, input)?,
Expand All @@ -114,27 +116,29 @@ impl Circomkit {
if let Some(parent) = wtns_path.parent() {
std::fs::create_dir_all(parent)?;
}
circomkit_core::utils::write_witness_file(&wtns_path, &witness)?;
circomkit_core::utils::write_witness_file(&wtns_path, &witness, prime)?;

log::info!("witness computed for {circuit}/{input}");
Ok(wtns_path)
}

/// Generate a proof for a circuit with the given input.
///
/// The proving backend is selected from the resolved circuit config
/// (`prover.backend`), capability-checked against the protocol and curve.
/// snarkjs uses its one-shot `full_prove`; native backends (arkworks,
/// lambdaworks) compute a witness first, then prove from it.
/// The proving backend is taken from `backend` when provided (e.g. the CLI
/// `--backend` flag), otherwise from the resolved circuit config
/// (`prover.backend`). Either way it is capability-checked against the
/// protocol and curve. snarkjs uses its one-shot `full_prove`; native
/// backends (arkworks, lambdaworks) compute a witness first, then prove from it.
pub fn prove(
&self,
circuit: &str,
input: &str,
data: Option<&CircuitSignals>,
backend: Option<ProvingBackendKind>,
) -> Result<PathBuf> {
let resolved = self.resolve(circuit)?;
let protocol = resolved.prover.protocol;
let kind = resolved.prover.backend;
let kind = backend.unwrap_or(resolved.prover.backend);
let prime = resolved.compiler.prime;

let pkey_path = self.paths.pkey(circuit, protocol);
Expand Down
2 changes: 1 addition & 1 deletion crates/circomkit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub use circomkit::Circomkit;

// Re-export key types for convenience
pub use circomkit_core::config::{CircomkitConfig, CircuitConfig};
pub use circomkit_core::enums::{Prime, Protocol};
pub use circomkit_core::enums::{Prime, Protocol, ProvingBackendKind};
pub use circomkit_core::error::CoreError;
pub use circomkit_core::pathing::CircomkitPaths;
pub use circomkit_core::types::R1CSInfo;
Expand Down
Loading