diff --git a/Cargo.toml b/Cargo.toml index 216bcc08..cfc70581 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/README.md b/README.md index 70a833a9..3b489e51 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/src/frontend/gadgets/num.rs b/src/frontend/gadgets/num.rs index a857476d..3641b3a9 100644 --- a/src/frontend/gadgets/num.rs +++ b/src/frontend/gadgets/num.rs @@ -33,7 +33,7 @@ impl AllocatedNum { /// /// 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) -> Self { AllocatedNum { value, variable } } diff --git a/src/neutron/mod.rs b/src/neutron/mod.rs index c7fd0c77..450f919f 100644 --- a/src/neutron/mod.rs +++ b/src/neutron/mod.rs @@ -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; @@ -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, @@ -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 { @@ -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) } @@ -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, @@ -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 { @@ -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) } @@ -256,6 +265,7 @@ where C: StepCircuit, { /// Create new instance of recursive SNARK + #[instrument(skip_all, name = "neutron::RecursiveSNARK::new")] pub fn new(pp: &PublicParams, c: &C, z0: &[E1::Scalar]) -> Result { if z0.len() != pp.F_arity { return Err(NovaError::InvalidInitialInputLength); @@ -291,6 +301,8 @@ where .map(|v| v.get_value().ok_or(SynthesisError::AssignmentMissing)) .collect::::Scalar>, _>>()?; + debug!("base case initialized"); + Ok(Self { z0: z0.to_vec(), r_W: FoldedWitness::default(&pp.structure), @@ -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, c: &C) -> Result<(), NovaError> { // first step was already done in the constructor if self.i == 0 { @@ -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, @@ -428,6 +444,8 @@ where res_r?; res_l?; + debug!("verification passed"); + Ok(self.zi.clone()) } diff --git a/src/neutron/nifs.rs b/src/neutron/nifs.rs index 03e0de6d..6f01177e 100644 --- a/src/neutron/nifs.rs +++ b/src/neutron/nifs.rs @@ -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)] @@ -197,6 +198,7 @@ impl NIFS { /// 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, ro_consts: &RO2Constants, @@ -294,6 +296,7 @@ impl NIFS { /// 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, diff --git a/src/nova/mod.rs b/src/nova/mod.rs index bc082c4b..75cbf4a5 100644 --- a/src/nova/mod.rs +++ b/src/nova/mod.rs @@ -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; @@ -122,6 +123,7 @@ where /// Ok(()) /// # } /// ``` + #[instrument(skip_all, name = "nova::PublicParams::setup")] pub fn setup( c: &C, ck_hint1: &CommitmentKeyHint, @@ -159,6 +161,8 @@ where return Err(NovaError::InvalidStepCircuitIO); } + let num_cons = r1cs_shape_primary.num_cons; + let pp = PublicParams { F_arity, @@ -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) } @@ -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, @@ -264,6 +271,8 @@ where return Err(NovaError::InvalidStepCircuitIO); } + let num_cons = r1cs_shape_primary.num_cons; + let pp = PublicParams { F_arity, @@ -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) } @@ -351,6 +362,7 @@ where C: StepCircuit, { /// Create new instance of recursive SNARK + #[instrument(skip_all, name = "nova::RecursiveSNARK::new")] pub fn new(pp: &PublicParams, c: &C, z0: &[E1::Scalar]) -> Result { if z0.len() != pp.F_arity { return Err(NovaError::InvalidInitialInputLength); @@ -430,6 +442,8 @@ where .map(|v| v.get_value().ok_or(SynthesisError::AssignmentMissing)) .collect::::Scalar>, _>>()?; + debug!("base case initialized"); + Ok(Self { z0: z0.to_vec(), @@ -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, c: &C) -> Result<(), NovaError> { // first step was already done in the constructor if self.i == 0 { @@ -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, @@ -661,6 +679,8 @@ where res_r_secondary?; res_l_secondary?; + debug!("verification passed"); + Ok(self.zi.clone()) } @@ -759,6 +779,7 @@ where S2: RelaxedR1CSSNARKTrait, { /// Creates prover and verifier keys for `CompressedSNARK` + #[instrument(skip_all, name = "nova::CompressedSNARK::setup")] pub fn setup( pp: &PublicParams, ) -> Result<(ProverKey, VerifierKey), NovaError> { @@ -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, pk: &ProverKey, @@ -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, @@ -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, @@ -1021,6 +1048,8 @@ where res_primary?; res_secondary?; + debug!("CompressedSNARK verification passed"); + Ok(self.zn.clone()) } } diff --git a/src/nova/nifs.rs b/src/nova/nifs.rs index bafc0602..6ff237d5 100644 --- a/src/nova/nifs.rs +++ b/src/nova/nifs.rs @@ -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)] @@ -33,6 +34,7 @@ impl NIFS { /// 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, ro_consts: &ROConstants, @@ -77,6 +79,7 @@ impl NIFS { /// 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, @@ -117,6 +120,7 @@ pub struct NIFSRelaxed { impl NIFSRelaxed { /// Same as `prove`, but takes two Relaxed R1CS Instance/Witness pairs + #[instrument(skip_all, name = "nova::NIFSRelaxed::prove")] pub fn prove( ck: &CommitmentKey, ro_consts: &ROConstants, @@ -167,6 +171,7 @@ impl NIFSRelaxed { } /// 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, diff --git a/src/provider/ptau.rs b/src/provider/ptau.rs index 199d764f..f6696f7d 100644 --- a/src/provider/ptau.rs +++ b/src/provider/ptau.rs @@ -96,6 +96,7 @@ use std::{ path::Path, str::{from_utf8, Utf8Error}, }; +use tracing::{debug, info, warn}; /// Errors that can occur when reading or writing PTAU files. #[derive(thiserror::Error, Debug)] @@ -259,12 +260,14 @@ fn read_meta_data(reader: &mut (impl Read + Seek)) -> Result()?; if version != PTAU_VERSION { + warn!(version, expected = PTAU_VERSION, "unsupported ptau version"); return Err(PtauFileError::UnsupportedVersion(version)); } } @@ -272,6 +275,7 @@ fn read_meta_data(reader: &mut (impl Read + Seek)) -> Result()?; // Accept both full (11 sections) and pruned (3 sections) ptau files if num_sections != NUM_SECTIONS_FULL && num_sections != NUM_SECTIONS_PRUNED { + warn!(num_sections, "invalid number of sections in ptau file"); return Err(PtauFileError::InvalidNumSections(num_sections)); } num_sections @@ -330,6 +334,7 @@ fn read_header( let modulus_expected = BigUint::parse_bytes(&Base::MODULUS.as_bytes()[2..], 16).unwrap(); if modulus != modulus_expected { + warn!("ptau file has wrong prime modulus"); return Err(PtauFileError::InvalidPrime(modulus)); } } @@ -340,12 +345,24 @@ fn read_header( let max_num_g2 = 1 << power; let max_num_g1 = max_num_g2 * 2 - 1; if num_g1 > max_num_g1 { + warn!( + power, + required = max_num_g1, + requested = num_g1, + "insufficient G1 generators in ptau" + ); return Err(PtauFileError::InsufficientPowerForG1 { power, required: max_num_g1, }); } if num_g2 > max_num_g2 { + warn!( + power, + required = max_num_g2, + requested = num_g2, + "insufficient G2 generators in ptau" + ); return Err(PtauFileError::InsufficientPowerForG2 { power, required: max_num_g2, @@ -379,6 +396,8 @@ where G1: halo2curves::serde::SerdeObject + CurveAffine, G2: halo2curves::serde::SerdeObject + CurveAffine, { + debug!(num_g1, num_g2, "reading ptau file"); + let metadata = read_meta_data(&mut reader)?; reader.seek(SeekFrom::Start(metadata.pos_header))?; @@ -390,6 +409,8 @@ where reader.seek(SeekFrom::Start(metadata.pos_tau_g2))?; let g2_points = read_points::(&mut reader, num_g2)?; + info!(num_g1, num_g2, "ptau file loaded successfully"); + Ok((g1_points, g2_points)) } diff --git a/src/r1cs/mod.rs b/src/r1cs/mod.rs index 0e8a90f5..829c3ba7 100644 --- a/src/r1cs/mod.rs +++ b/src/r1cs/mod.rs @@ -21,6 +21,9 @@ use rand_core::OsRng; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +#[cfg(feature = "io")] +use tracing::info; +use tracing::warn; mod sparse; use sparse::PrecomputedSparseMatrix; @@ -322,13 +325,17 @@ impl R1CSShape { } let ptau_path = ptau_path.ok_or_else(|| { - crate::errors::NovaError::PtauFileError(format!( + let err = format!( "No suitable ptau file found in {:?}. Need at least {} generators (power >= {}). \ Expected files named ppot_pruned_XX.ptau or ppot_0080_XX.ptau where XX >= {:02}", ptau_dir, max_size, min_power, min_power - )) + ); + warn!("{}", err); + crate::errors::NovaError::PtauFileError(err) })?; + info!(path = %ptau_path.display(), num_generators = max_size, "loading ptau file"); + let file = File::open(&ptau_path).map_err(|e| { crate::errors::NovaError::PtauFileError(format!( "Failed to open {}: {}", @@ -475,12 +482,14 @@ impl R1CSShape { }; if !res_eq { + warn!("relaxed R1CS is unsatisfiable"); return Err(NovaError::UnSat { reason: "Relaxed R1CS is unsatisfiable".to_string(), }); } if !res_comm { + warn!("relaxed R1CS commitment mismatch"); return Err(NovaError::UnSat { reason: "Invalid commitments".to_string(), }); @@ -514,12 +523,14 @@ impl R1CSShape { let res_comm = U.comm_W == CE::::commit(ck, &W.W, &W.r_W); if !res_eq { + warn!("R1CS is unsatisfiable"); return Err(NovaError::UnSat { reason: "R1CS is unsatisfiable".to_string(), }); } if !res_comm { + warn!("R1CS commitment mismatch"); return Err(NovaError::UnSat { reason: "Invalid commitment".to_string(), }); diff --git a/src/spartan/direct.rs b/src/spartan/direct.rs index 0f55ee4d..e63b5345 100644 --- a/src/spartan/direct.rs +++ b/src/spartan/direct.rs @@ -24,6 +24,7 @@ use core::marker::PhantomData; use ff::Field; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use tracing::{debug, instrument}; /// A direct circuit that can be synthesized pub struct DirectCircuit> { @@ -118,6 +119,7 @@ where impl, C: StepCircuit> DirectSNARK { /// Produces prover and verifier keys for the direct SNARK + #[instrument(skip_all, name = "DirectSNARK::setup")] pub fn setup(sc: C) -> Result<(ProverKey, VerifierKey), NovaError> { // construct a circuit that can be synthesized let circuit: DirectCircuit = DirectCircuit { z_i: None, sc }; @@ -140,6 +142,7 @@ impl, C: StepCircuit> DirectSN } /// Produces a proof of satisfiability of the provided circuit + #[instrument(skip_all, name = "DirectSNARK::prove")] pub fn prove(pk: &ProverKey, sc: C, z_i: &[E::Scalar]) -> Result { let mut cs = SatisfyingAssignment::::new(); @@ -183,6 +186,7 @@ impl, C: StepCircuit> DirectSN } /// Verifies a proof of satisfiability + #[instrument(skip_all, name = "DirectSNARK::verify")] pub fn verify(&self, vk: &VerifierKey, io: &[E::Scalar]) -> Result<(), NovaError> { // derandomize/unblind commitments let comm_W = E::CE::derandomize(&vk.dk, &self.comm_W, &self.blind_r_W); @@ -193,6 +197,8 @@ impl, C: StepCircuit> DirectSN // verify the snark using the constructed instance self.snark.verify(&vk.vk, &u_relaxed)?; + debug!("DirectSNARK verification passed"); + Ok(()) } } diff --git a/src/spartan/ppsnark.rs b/src/spartan/ppsnark.rs index d38f9331..49ef39f4 100644 --- a/src/spartan/ppsnark.rs +++ b/src/spartan/ppsnark.rs @@ -37,6 +37,7 @@ use once_cell::sync::OnceCell; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use tracing::{debug, instrument, warn}; fn padded(v: &[E::Scalar], n: usize, e: &E::Scalar) -> Vec { let mut v_padded = vec![*e; n]; @@ -1024,12 +1025,18 @@ impl> RelaxedR1CSSNARKTrait for Relax }) } + #[instrument(skip_all, name = "spartan::ppsnark::setup")] fn setup( ck: &CommitmentKey, S: &R1CSShape, ) -> Result<(Self::ProverKey, Self::VerifierKey), NovaError> { // check the provided commitment key meets minimal requirements if ck.length() < Self::ck_floor()(S) { + warn!( + required = Self::ck_floor()(S), + actual = ck.length(), + "invalid commitment key length" + ); return Err(NovaError::InvalidCommitmentKeyLength); } let (pk_ee, vk_ee) = EE::setup(ck)?; @@ -1053,6 +1060,7 @@ impl> RelaxedR1CSSNARKTrait for Relax } /// produces a succinct proof of satisfiability of a `RelaxedR1CS` instance + #[instrument(skip_all, name = "spartan::ppsnark::prove")] fn prove( ck: &CommitmentKey, pk: &Self::ProverKey, @@ -1385,6 +1393,7 @@ impl> RelaxedR1CSSNARKTrait for Relax } /// verifies a proof of satisfiability of a `RelaxedR1CS` instance + #[instrument(skip_all, name = "spartan::ppsnark::verify")] fn verify(&self, vk: &Self::VerifierKey, U: &RelaxedR1CSInstance) -> Result<(), NovaError> { let mut transcript = E::TE::new(b"RelaxedR1CSSNARK"); @@ -1415,6 +1424,7 @@ impl> RelaxedR1CSSNARKTrait for Relax - U.u * self.eval_Cz_at_r_outer - self.eval_E_at_r_outer); if claim_sc_outer_expected != claim_sc_outer_final { + warn!("outer sumcheck claim mismatch"); return Err(NovaError::InvalidSumcheckProof); } @@ -1651,6 +1661,8 @@ impl> RelaxedR1CSSNARKTrait for Relax &self.eval_arg, )?; + debug!("ppsnark verification passed"); + Ok(()) } } diff --git a/src/spartan/snark.rs b/src/spartan/snark.rs index 4c6148d3..e174da88 100644 --- a/src/spartan/snark.rs +++ b/src/spartan/snark.rs @@ -26,6 +26,7 @@ use ff::Field; use once_cell::sync::OnceCell; use rayon::prelude::*; use serde::{Deserialize, Serialize}; +use tracing::{debug, instrument}; /// A type that represents the prover's key #[derive(Serialize, Deserialize)] @@ -91,6 +92,7 @@ impl> RelaxedR1CSSNARKTrait for Relax type ProverKey = ProverKey; type VerifierKey = VerifierKey; + #[instrument(skip_all, name = "spartan::RelaxedR1CSSNARK::setup")] fn setup( ck: &CommitmentKey, S: &R1CSShape, @@ -110,6 +112,7 @@ impl> RelaxedR1CSSNARKTrait for Relax } /// produces a succinct proof of satisfiability of a `RelaxedR1CS` instance + #[instrument(skip_all, name = "spartan::RelaxedR1CSSNARK::prove")] fn prove( ck: &CommitmentKey, pk: &Self::ProverKey, @@ -254,6 +257,7 @@ impl> RelaxedR1CSSNARKTrait for Relax } /// verifies a proof of satisfiability of a `RelaxedR1CS` instance + #[instrument(skip_all, name = "spartan::RelaxedR1CSSNARK::verify")] fn verify(&self, vk: &Self::VerifierKey, U: &RelaxedR1CSInstance) -> Result<(), NovaError> { let mut transcript = E::TE::new(b"RelaxedR1CSSNARK"); @@ -386,6 +390,8 @@ impl> RelaxedR1CSSNARKTrait for Relax &self.eval_arg, )?; + debug!("SNARK verification passed"); + Ok(()) } }