Skip to content
Open
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.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, CompressedSNARK proof generated
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 and consistency failures (e.g., invalid commitment key length, sumcheck mismatch, unsat checks, ptau validation) |

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