Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ byteorder = "1.4.3"
thiserror = "2.0.11"
once_cell = "1.18.0"
itertools = "0.14.0"
tracing = "0.1"

# Use halo2curves ASM on x86_64 by default; disable ASM on non-x86_64
[target.'cfg(target_arch = "x86_64")'.dependencies]
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,37 @@ To run an example:
cargo run --release --example minroot
```

## Logging

This library uses the [`tracing`](https://docs.rs/tracing) crate for structured, leveled logging. It emits tracing events and spans but **does not install a subscriber** — the application decides how (and whether) to consume output. When no subscriber is registered, all tracing macros are no-ops with near-zero overhead.

To see log output, install a `tracing-subscriber` in your application:

```rust
use tracing_subscriber::EnvFilter;

tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
```

Then control verbosity with the `RUST_LOG` environment variable:

```bash
RUST_LOG=info cargo test --release ... # setup complete, compression complete
Comment thread
srinathsetty marked this conversation as resolved.
Outdated
RUST_LOG=debug cargo test --release ... # per-step prove_step, base case, verification
```

**Tracing levels used:**

| Level | What it covers |
|---------|----------------|
| `info` | Setup complete, CompressedSNARK generated, constraint counts |
| `debug` | Per-step prove_step, base case init, verification passed |
| `warn` | Validation failures (unused currently — reserved for downstream) |
Comment thread
srinathsetty marked this conversation as resolved.
Outdated

Applications that depend on this library automatically see these spans nested under their own tracing subscriber with zero integration work.

## References
The following paper, which appeared at CRYPTO 2022, provides details of the Nova proof system and a proof of security:

Expand Down
2 changes: 1 addition & 1 deletion src/frontend/gadgets/num.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl<Scalar: PrimeField> AllocatedNum<Scalar> {
///
/// This is useful when a variable is known to hold a valid field element
/// due to constraints added separately, enabling zero-cost reinterpretation
/// (e.g., wrapping an [`AllocatedBit`](super::boolean::AllocatedBit)'s variable as a number).
/// (e.g., wrapping an [`AllocatedBit`]'s variable as a number).
pub fn from_parts(variable: Variable, value: Option<Scalar>) -> Self {
AllocatedNum { value, variable }
}
Expand Down
18 changes: 18 additions & 0 deletions src/neutron/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use ff::Field;
use once_cell::sync::OnceCell;
use rand_core::OsRng;
use serde::{Deserialize, Serialize};
use tracing::{debug, info, instrument};

mod circuit;
pub mod nifs;
Expand Down Expand Up @@ -107,6 +108,7 @@ where
/// let pp = PublicParams::setup(&circuit, ck_hint1, ck_hint2)?;
/// Ok(())
/// ```
#[instrument(skip_all, name = "neutron::PublicParams::setup")]
pub fn setup(
c: &C,
ck_hint1: &CommitmentKeyHint<E1>,
Expand All @@ -131,6 +133,7 @@ where
// Generate the commitment key
let ck = R1CSShape::commitment_key(&[&r1cs_shape], &[ck_hint1])?;

let num_cons = r1cs_shape.num_cons;
let structure = Structure::new(&r1cs_shape);

let pp = PublicParams {
Expand All @@ -148,6 +151,8 @@ where
// call pp.digest() so the digest is computed here rather than in RecursiveSNARK methods
let _ = pp.digest();

info!(num_cons = %num_cons, "setup complete");

Ok(pp)
}

Expand All @@ -166,6 +171,7 @@ where
/// * `ck_hint2`: A `CommitmentKeyHint` for the secondary circuit (unused but kept for API consistency).
/// * `ptau_dir`: Path to the directory containing pruned ptau files.
#[cfg(feature = "io")]
#[instrument(skip_all, name = "neutron::PublicParams::setup_with_ptau_dir")]
pub fn setup_with_ptau_dir(
c: &C,
ck_hint1: &CommitmentKeyHint<E1>,
Expand Down Expand Up @@ -194,6 +200,7 @@ where
// Load the commitment key from ptau directory
let ck = R1CSShape::commitment_key_from_ptau_dir(&[&r1cs_shape], &[ck_hint1], ptau_dir)?;

let num_cons = r1cs_shape.num_cons;
let structure = Structure::new(&r1cs_shape);

let pp = PublicParams {
Expand All @@ -211,6 +218,8 @@ where
// call pp.digest() so the digest is computed here rather than in RecursiveSNARK methods
let _ = pp.digest();

info!(num_cons = %num_cons, "setup complete");

Ok(pp)
}

Expand Down Expand Up @@ -256,6 +265,7 @@ where
C: StepCircuit<E1::Scalar>,
{
/// Create new instance of recursive SNARK
#[instrument(skip_all, name = "neutron::RecursiveSNARK::new")]
pub fn new(pp: &PublicParams<E1, E2, C>, c: &C, z0: &[E1::Scalar]) -> Result<Self, NovaError> {
if z0.len() != pp.F_arity {
return Err(NovaError::InvalidInitialInputLength);
Expand Down Expand Up @@ -291,6 +301,8 @@ where
.map(|v| v.get_value().ok_or(SynthesisError::AssignmentMissing))
.collect::<Result<Vec<<E1 as Engine>::Scalar>, _>>()?;

debug!("base case initialized");

Ok(Self {
z0: z0.to_vec(),
r_W: FoldedWitness::default(&pp.structure),
Expand All @@ -305,6 +317,7 @@ where
}

/// Updates the provided `RecursiveSNARK` by executing a step of the incremental computation
#[instrument(skip_all, name = "neutron::RecursiveSNARK::prove_step", fields(step = self.i))]
pub fn prove_step(&mut self, pp: &PublicParams<E1, E2, C>, c: &C) -> Result<(), NovaError> {
Comment on lines 319 to 321
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

#[instrument(..., fields(step = self.i))] captures self.i at entry, but self.i is incremented during the step; the later "step complete" debug event logs the incremented value. This makes the span field misleading/off-by-one. Consider recording a stable field name/value (e.g., steps_executed), using a local let step = ... for both, or recording the span field after increment.

Copilot uses AI. Check for mistakes.
// first step was already done in the constructor
if self.i == 0 {
Expand Down Expand Up @@ -363,10 +376,13 @@ where
self.l_u = l_u;
self.l_w = l_w;

debug!(step = self.i, "step complete");

Ok(())
}

/// Verify the correctness of the `RecursiveSNARK`
#[instrument(skip_all, name = "neutron::RecursiveSNARK::verify", fields(num_steps))]
pub fn verify(
&self,
pp: &PublicParams<E1, E2, C>,
Expand Down Expand Up @@ -428,6 +444,8 @@ where
res_r?;
res_l?;

debug!("verification passed");

Ok(self.zi.clone())
}

Expand Down
3 changes: 3 additions & 0 deletions src/neutron/nifs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use ff::Field;
use rand_core::OsRng;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use tracing::instrument;

/// An NIFS message from NeutronNova's folding scheme
#[allow(clippy::upper_case_acronyms)]
Expand Down Expand Up @@ -197,6 +198,7 @@ impl<E: Engine> NIFS<E> {
/// In particular, it requires that `U1` and `U2` are such that the hash of `U1` is stored in the public IO of `U2`.
/// In this particular setting, this means that if `U2` is absorbed in the RO, it implicitly absorbs `U1` as well.
/// So the code below avoids absorbing `U1` in the RO.
#[instrument(skip_all, name = "neutron::NIFS::prove")]
pub fn prove(
ck: &CommitmentKey<E>,
ro_consts: &RO2Constants<E>,
Expand Down Expand Up @@ -294,6 +296,7 @@ impl<E: Engine> NIFS<E> {
/// with the guarantee that the folded instance `U`
/// if and only if `U1` and `U2` are satisfiable.
#[cfg(test)]
#[instrument(skip_all, name = "neutron::NIFS::verify")]
pub fn verify(
&self,
ro_consts: &RO2Constants<E>,
Expand Down
29 changes: 29 additions & 0 deletions src/nova/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use ff::Field;
use once_cell::sync::OnceCell;
use rand_core::OsRng;
use serde::{Deserialize, Serialize};
use tracing::{debug, info, instrument};

mod circuit;
pub mod nifs;
Expand Down Expand Up @@ -122,6 +123,7 @@ where
/// Ok(())
/// # }
/// ```
#[instrument(skip_all, name = "nova::PublicParams::setup")]
pub fn setup(
c: &C,
ck_hint1: &CommitmentKeyHint<E1>,
Expand Down Expand Up @@ -159,6 +161,8 @@ where
return Err(NovaError::InvalidStepCircuitIO);
}

let num_cons = r1cs_shape_primary.num_cons;

let pp = PublicParams {
F_arity,

Expand All @@ -181,6 +185,8 @@ where
// call pp.digest() so the digest is computed here rather than in RecursiveSNARK methods
let _ = pp.digest();

info!(num_cons = %num_cons, "setup complete");

Ok(pp)
}

Expand Down Expand Up @@ -219,6 +225,7 @@ where
/// )?;
/// ```
#[cfg(feature = "io")]
#[instrument(skip_all, name = "nova::PublicParams::setup_with_ptau_dir")]
pub fn setup_with_ptau_dir(
c: &C,
ck_hint1: &CommitmentKeyHint<E1>,
Expand Down Expand Up @@ -264,6 +271,8 @@ where
return Err(NovaError::InvalidStepCircuitIO);
}

let num_cons = r1cs_shape_primary.num_cons;

let pp = PublicParams {
F_arity,

Expand All @@ -286,6 +295,8 @@ where
// call pp.digest() so the digest is computed here rather than in RecursiveSNARK methods
let _ = pp.digest();

info!(num_cons = %num_cons, "setup complete");

Ok(pp)
}

Expand Down Expand Up @@ -351,6 +362,7 @@ where
C: StepCircuit<E1::Scalar>,
{
/// Create new instance of recursive SNARK
#[instrument(skip_all, name = "nova::RecursiveSNARK::new")]
pub fn new(pp: &PublicParams<E1, E2, C>, c: &C, z0: &[E1::Scalar]) -> Result<Self, NovaError> {
if z0.len() != pp.F_arity {
return Err(NovaError::InvalidInitialInputLength);
Expand Down Expand Up @@ -430,6 +442,8 @@ where
.map(|v| v.get_value().ok_or(SynthesisError::AssignmentMissing))
.collect::<Result<Vec<<E1 as Engine>::Scalar>, _>>()?;

debug!("base case initialized");

Ok(Self {
z0: z0.to_vec(),

Expand All @@ -453,6 +467,7 @@ where
}

/// Updates the provided `RecursiveSNARK` by executing a step of the incremental computation
#[instrument(skip_all, name = "nova::RecursiveSNARK::prove_step", fields(step = self.i))]
pub fn prove_step(&mut self, pp: &PublicParams<E1, E2, C>, c: &C) -> Result<(), NovaError> {
Comment on lines 469 to 471
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

#[instrument(..., fields(step = self.i))] records the value of self.i at function entry, but self.i is incremented before the "step complete" debug event. This makes the span field and debug event disagree (and is off-by-one for the first call where self.i is bumped from 0→1). Consider recording a stable value (e.g., steps_executed = self.i), or compute the step number in a local and use that for both span fields and logs (or record the field after increment).

Copilot uses AI. Check for mistakes.
// first step was already done in the constructor
if self.i == 0 {
Expand Down Expand Up @@ -560,10 +575,13 @@ where
self.ri_primary = r_next_primary;
self.ri_secondary = r_next_secondary;

debug!(step = self.i, "step complete");

Ok(())
}

/// Verify the correctness of the `RecursiveSNARK`
#[instrument(skip_all, name = "nova::RecursiveSNARK::verify", fields(num_steps))]
pub fn verify(
&self,
pp: &PublicParams<E1, E2, C>,
Expand Down Expand Up @@ -661,6 +679,8 @@ where
res_r_secondary?;
res_l_secondary?;

debug!("verification passed");

Ok(self.zi.clone())
}

Expand Down Expand Up @@ -759,6 +779,7 @@ where
S2: RelaxedR1CSSNARKTrait<E2>,
{
/// Creates prover and verifier keys for `CompressedSNARK`
#[instrument(skip_all, name = "nova::CompressedSNARK::setup")]
pub fn setup(
pp: &PublicParams<E1, E2, C>,
) -> Result<(ProverKey<E1, E2, C, S1, S2>, VerifierKey<E1, E2, C, S1, S2>), NovaError> {
Expand All @@ -783,10 +804,13 @@ where
_p: Default::default(),
};

info!("CompressedSNARK setup complete");

Ok((pk, vk))
}

/// Create a new `CompressedSNARK` (provides zero-knowledge)
#[instrument(skip_all, name = "nova::CompressedSNARK::prove")]
pub fn prove(
pp: &PublicParams<E1, E2, C>,
pk: &ProverKey<E1, E2, C, S1, S2>,
Expand Down Expand Up @@ -877,6 +901,8 @@ where
},
);

info!("CompressedSNARK proof generated");

Ok(Self {
r_U_secondary: recursive_snark.r_U_secondary.clone(),
ri_secondary: recursive_snark.ri_secondary,
Expand Down Expand Up @@ -906,6 +932,7 @@ where
}

/// Verify the correctness of the `CompressedSNARK` (provides zero-knowledge)
#[instrument(skip_all, name = "nova::CompressedSNARK::verify", fields(num_steps))]
pub fn verify(
&self,
vk: &VerifierKey<E1, E2, C, S1, S2>,
Expand Down Expand Up @@ -1021,6 +1048,8 @@ where
res_primary?;
res_secondary?;

debug!("CompressedSNARK verification passed");

Ok(self.zn.clone())
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/nova/nifs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::{
use ff::Field;
use rand_core::OsRng;
use serde::{Deserialize, Serialize};
use tracing::instrument;

/// An NIFS message from Nova's folding scheme
#[allow(clippy::upper_case_acronyms)]
Expand All @@ -33,6 +34,7 @@ impl<E: Engine> NIFS<E> {
/// In particular, it requires that `U1` and `U2` are such that the hash of `U1` is stored in the public IO of `U2`.
/// In this particular setting, this means that if `U2` is absorbed in the RO, it implicitly absorbs `U1` as well.
/// So the code below avoids absorbing `U1` in the RO.
#[instrument(skip_all, name = "nova::NIFS::prove")]
pub fn prove(
ck: &CommitmentKey<E>,
ro_consts: &ROConstants<E>,
Expand Down Expand Up @@ -77,6 +79,7 @@ impl<E: Engine> NIFS<E> {
/// and outputs a folded instance `U` with the same shape,
/// with the guarantee that the folded instance `U`
/// if and only if `U1` and `U2` are satisfiable.
#[instrument(skip_all, name = "nova::NIFS::verify")]
pub fn verify(
&self,
ro_consts: &ROConstants<E>,
Expand Down Expand Up @@ -117,6 +120,7 @@ pub struct NIFSRelaxed<E: Engine> {

impl<E: Engine> NIFSRelaxed<E> {
/// Same as `prove`, but takes two Relaxed R1CS Instance/Witness pairs
#[instrument(skip_all, name = "nova::NIFSRelaxed::prove")]
pub fn prove(
ck: &CommitmentKey<E>,
ro_consts: &ROConstants<E>,
Expand Down Expand Up @@ -167,6 +171,7 @@ impl<E: Engine> NIFSRelaxed<E> {
}

/// Same as `verify`, but takes two Relaxed R1CS Instance/Witness pairs
#[instrument(skip_all, name = "nova::NIFSRelaxed::verify")]
pub fn verify(
&self,
ro_consts: &ROConstants<E>,
Expand Down
Loading
Loading