Skip to main content

lib_q_zkp/
lib.rs

1//! lib-Q ZKP - Post-quantum Zero-Knowledge Proofs
2//!
3//! This crate provides implementations of post-quantum zero-knowledge proofs.
4//!
5//! # zk-STARK Implementation
6//!
7//! This crate provides a high-level API for creating and verifying zk-STARK proofs.
8//! The underlying implementation is based on Plonky3, adapted for lib-Q's post-quantum
9//! security requirements using SHAKE256.
10//!
11//! ## Field Configuration
12//!
13//! The implementation uses **`Complex<Mersenne31>`** as the base field, which provides:
14//! - **TWO_ADICITY = 32**: Sufficient for FRI protocol and efficient FFT operations
15//! - **Post-quantum security**: soundness rests on hash and algebraic assumptions rather than
16//!   number-theoretic ones. Note that no ZK proof system is NIST-approved, and some AIR gadgets
17//!   in this crate use Poseidon/Poseidon2, which are not NIST-standardized primitives.
18//! - **Efficient arithmetic**: Optimized field operations for STARK proofs
19//!
20//! ## Example Usage
21//!
22//! ```rust,ignore
23//! use lib_q_zkp::stark::{StarkProver, StarkVerifier, default_config};
24//! use lib_q_stark_field::extension::Complex;
25//! use lib_q_stark_mersenne31::Mersenne31;
26//!
27//! type Val = Complex<Mersenne31>;
28//!
29//! // Create prover and verifier with default configuration
30//! let config = default_config();
31//! let prover = StarkProver::new(config.clone());
32//! let verifier = StarkVerifier::new(config);
33//!
34//! // Generate proof (requires AIR implementation)
35//! // let proof = prover.prove(&air, trace, &public_values);
36//!
37//! // Verify proof
38//! // verifier.verify(&air, &proof, &public_values)?;
39//! ```
40//!
41//! ## Testing
42//!
43//! - **Recursive aggregation**: The test `test_recursive_verifier_trace_satisfies_constraints_then_prove_verify` (in `aggregation_tests`) runs the full prove → aggregate → verify pipeline. It is slow in dev (unoptimized); run with `--release` for completion in a few minutes. CI runs this test in release with a 15-minute timeout.
44//! - **Merkle tree builder**: `tests/merkle_tree_builder_tests.rs` uses [`stark::fast_proof_config`] for prove/verify round-trips (fast FRI); wrong-root and cross-tree rejection use [`stark::default_config`] because minimal FRI is not sound for those negatives.
45//! - **Merkle tree certificates**: Create/use and security checks (wrong root, wrong depth, cross-tree) are covered by `tests/merkle_certificate_tests.rs`. Additional Merkle and group-membership tests live in `air_integration` and `ip_soundness_tests`.
46
47#![cfg_attr(not(feature = "std"), no_std)]
48// Bounds on generic type parameters in aliases are not enforced by the type checker (Rust RFC
49// follow-up); we still document constraints via `C: StarkGenericConfig` / `F: Field` on aliases.
50#![allow(type_alias_bounds)]
51#![deny(unsafe_code)]
52#![deny(unused_qualifications)]
53#![allow(clippy::bool_assert_comparison)]
54#![allow(clippy::clone_on_copy)]
55#![allow(clippy::collapsible_if)]
56#![allow(clippy::get_first)]
57#![allow(clippy::iter_cloned_collect)]
58#![allow(clippy::manual_is_multiple_of)]
59#![allow(clippy::too_many_arguments)]
60#![allow(clippy::unnecessary_lazy_evaluations)]
61// AIR trace/gadget construction reads clearest with explicit indexing and `a = a + b`
62// accumulation; suppress these style lints crate-wide (same approach as lib-q-hqc's SIMD code).
63#![allow(clippy::assign_op_pattern)]
64#![allow(clippy::needless_range_loop)]
65#![allow(clippy::manual_memcpy)]
66#![allow(clippy::double_must_use)]
67
68#[cfg(feature = "alloc")]
69extern crate alloc;
70
71// Re-export core types for public use
72#[cfg(feature = "alloc")]
73use alloc::boxed::Box;
74#[cfg(feature = "alloc")]
75use alloc::string::ToString;
76#[cfg(feature = "alloc")]
77use alloc::vec;
78#[cfg(feature = "alloc")]
79use alloc::vec::Vec;
80
81pub use lib_q_core::Result;
82/// Plonky3-derived components (Keccak AIR, lookup, batch STARK, etc.).
83///
84/// Enabled when any of `plonky` or the granular `plonky-*` features is on (each pulls in
85/// `lib-q-plonky` with the corresponding sub-features; `plonky` enables the full set).
86#[cfg(any(
87    feature = "plonky",
88    feature = "plonky-keccak-air",
89    feature = "plonky-lookup",
90    feature = "plonky-uni-stark",
91    feature = "plonky-batch-stark",
92))]
93pub use lib_q_plonky as plonky;
94
95/// zk-STARK implementation
96#[cfg(feature = "zkp")]
97pub mod stark;
98
99/// BabyBear / Poseidon2 zk-STARK config + membership prover/verifier (Arm B).
100#[cfg(feature = "zkp")]
101pub mod stark_baby_bear;
102
103/// Circuit builder for arithmetic constraints
104#[cfg(feature = "zkp")]
105pub mod circuit;
106
107/// AIR implementations for common proof types
108#[cfg(feature = "zkp")]
109pub mod air;
110
111/// Proof aggregation for combining multiple proofs
112#[cfg(feature = "zkp")]
113pub mod aggregation;
114
115/// IP (Identity Protocol) integration
116#[cfg(feature = "zkp")]
117pub mod ip;
118
119/// Poseidon Merkle tree builder (compatible with MerkleInclusionAir)
120#[cfg(feature = "zkp")]
121pub mod merkle;
122
123/// BabyBear / Poseidon2 wide-digest Merkle tree builder (Arm B analogue of `merkle`).
124#[cfg(feature = "zkp")]
125pub mod merkle_baby_bear;
126
127/// High-level lib q API
128#[cfg(feature = "zkp")]
129pub mod api;
130
131#[cfg(feature = "zkp")]
132pub mod wire;
133
134/// Unlinkable set-membership proofs (`libq.zkfri.membership.v0`): Semaphore/Tornado
135/// nullifier shape over the Poseidon-256 wide-digest Merkle tree. RED (ADR 113 freeze-gate).
136#[cfg(feature = "zkp")]
137pub mod membership;
138
139#[cfg(feature = "zkp")]
140pub use api::{
141    MerklePath,
142    build_merkle_tree,
143    prove_membership,
144    prove_membership_with_config,
145    prove_preimage,
146    prove_preimage_nist,
147    verify_membership,
148    verify_membership_with_config,
149    verify_membership_with_depth,
150    verify_membership_with_depth_and_config,
151    verify_preimage,
152    verify_preimage_nist,
153};
154
155#[cfg(feature = "wasm")]
156mod wasm;
157
158#[cfg(feature = "zkp")]
159pub use lib_q_stark::{
160    Proof as StarkProof,
161    StarkConfig,
162    StarkGenericConfig,
163    check_constraints,
164    prove,
165    verify,
166};
167#[cfg(feature = "zkp")]
168pub use lib_q_stark_air::Air;
169#[cfg(feature = "zkp")]
170use lib_q_stark_field::extension::Complex;
171#[cfg(feature = "zkp")]
172use lib_q_stark_matrix::dense::RowMajorMatrix;
173#[cfg(feature = "zkp")]
174use lib_q_stark_mersenne31::Mersenne31;
175#[cfg(feature = "zkp")]
176pub use merkle::PoseidonMerkleTree;
177#[cfg(feature = "zkp")]
178use serde::{
179    Deserialize,
180    Serialize,
181};
182
183#[cfg(feature = "zkp")]
184#[allow(unused_imports)]
185use crate::air::TraceGenerator;
186
187/// The field type used for ZKP operations
188///
189/// Uses `Complex<Mersenne31>` which provides TWO_ADICITY = 32, sufficient for
190/// FRI protocol and efficient FFT operations.
191#[cfg(feature = "zkp")]
192pub type ZkpField = Complex<Mersenne31>;
193
194/// Metadata specific to different proof types
195///
196/// This enum stores proof-specific parameters that are required for verification.
197/// The metadata is serialized alongside the proof to make proofs self-describing.
198///
199/// All proofs must include appropriate metadata for their proof type.
200#[derive(Debug, Clone, PartialEq, Eq, Default)]
201#[cfg_attr(feature = "zkp", derive(Serialize, Deserialize))]
202pub enum ProofMetadata {
203    /// No metadata (default variant, but proofs should use specific metadata types)
204    #[default]
205    None,
206    /// Merkle tree inclusion proof metadata
207    MerkleInclusion {
208        /// Depth of the Merkle tree (required for AIR reconstruction)
209        tree_depth: u8,
210    },
211    /// Hash preimage proof metadata (Poseidon-128)
212    HashPreimage {
213        /// Output size in bytes
214        output_size: u16,
215    },
216    /// NIST hash preimage proof metadata (cSHAKE256)
217    HashPreimageNist {
218        /// Output size in bytes (e.g. 32 for cSHAKE256)
219        output_size: u16,
220    },
221    /// Circuit computation proof metadata
222    Circuit {
223        /// Number of witness values
224        num_witnesses: u32,
225        /// Number of public values
226        num_public: u32,
227    },
228    /// Credential proof metadata for selective disclosure
229    Credential {
230        /// Serialized credential schema (attribute sizes in bytes)
231        attribute_sizes: Vec<u16>,
232        /// Reveal mask (which attributes are revealed: true = revealed, false = hidden)
233        reveal_mask: Vec<bool>,
234    },
235    /// Identity Token ownership proof metadata
236    Identity {
237        /// ML-DSA security level: 44, 65, or 87
238        dsa_level: u8,
239    },
240    /// Recovery policy threshold proof metadata
241    RecoveryPolicy {
242        /// Number of keys in policy
243        key_count: u32,
244        /// Circuit air id
245        air_id: u8,
246    },
247    /// Unlinkable set-membership proof metadata (`libq.zkfri.membership.v0`)
248    UnlinkableMembership {
249        /// Real Merkle path depth. The verifier authenticates this against the proof's actual
250        /// STARK trace height so the declared depth cannot be relabelled (the depth-confusion
251        /// guard; see `membership::verify_unlinkable_membership_with_config`).
252        tree_depth: u8,
253        /// Wide-digest width in field elements (5 for Poseidon-256)
254        digest_width: u8,
255        /// Whether the proof was produced with the hiding (zero-knowledge) PCS. Selects the
256        /// STARK config type at verification — a ZK proof (`StarkProof<ZkConfig>`) and a
257        /// transparent proof (`StarkProof<DefaultConfig>`) are distinct serialized types.
258        zk: bool,
259    },
260}
261
262/// A zero-knowledge proof
263#[derive(Debug, Clone)]
264#[cfg_attr(feature = "zkp", derive(serde::Serialize, serde::Deserialize))]
265pub struct ZkpProof {
266    /// The proof data (serialized STARK proof)
267    pub data: Vec<u8>,
268    /// The proof type
269    pub proof_type: ProofType,
270    /// Security level
271    pub security_level: u32,
272    /// Proof-specific metadata (required for verification)
273    pub metadata: ProofMetadata,
274}
275
276#[cfg(feature = "zkp")]
277impl ZkpProof {
278    /// Serialize a STARK proof into a ZkpProof with metadata
279    ///
280    /// All proofs must include metadata for proper verification.
281    /// This method is used internally by the high-level API functions.
282    pub fn from_stark_proof<C: StarkGenericConfig>(
283        proof: &StarkProof<C>,
284        metadata: ProofMetadata,
285    ) -> Result<Self>
286    where
287        StarkProof<C>: Serialize,
288    {
289        let data = postcard::to_allocvec(proof).map_err(|_| lib_q_core::Error::InternalError {
290            operation: "ZKP proof serialization".to_string(),
291            details: "Failed to serialize STARK proof".to_string(),
292        })?;
293        Ok(Self {
294            data,
295            proof_type: ProofType::Stark,
296            security_level: 1,
297            metadata,
298        })
299    }
300
301    /// Deserialize a ZkpProof into a STARK proof
302    pub fn to_stark_proof<C: StarkGenericConfig>(&self) -> Result<StarkProof<C>>
303    where
304        StarkProof<C>: for<'de> Deserialize<'de>,
305    {
306        postcard::from_bytes(&self.data).map_err(|_| lib_q_core::Error::InternalError {
307            operation: "ZKP proof deserialization".to_string(),
308            details: "Failed to deserialize STARK proof".to_string(),
309        })
310    }
311
312    /// Get the tree depth from Merkle inclusion proof metadata
313    ///
314    /// Returns `Some(depth)` if this is a Merkle inclusion proof with metadata,
315    /// `None` otherwise.
316    pub fn merkle_tree_depth(&self) -> Option<u8> {
317        match &self.metadata {
318            ProofMetadata::MerkleInclusion { tree_depth } => Some(*tree_depth),
319            ProofMetadata::UnlinkableMembership { tree_depth, .. } => Some(*tree_depth),
320            _ => None,
321        }
322    }
323}
324
325/// Types of zero-knowledge proofs supported by lib-Q
326///
327/// No zero-knowledge proof system is NIST-approved or NIST-standardized -- NIST has not
328/// standardized any ZK proof system, PQC or otherwise. Only hash-based, transparent-setup
329/// proof systems (believed post-quantum secure because they rely on collision-resistant
330/// hashing rather than number-theoretic assumptions) are included here. Classical
331/// pairing/discrete-log-based schemes (SNARKs, Bulletproofs) are intentionally excluded
332/// because those assumptions are broken by a quantum computer.
333#[derive(Debug, Clone, PartialEq, Eq)]
334#[cfg_attr(feature = "zkp", derive(serde::Serialize, serde::Deserialize))]
335pub enum ProofType {
336    /// zk-STARK proof (transparent, post-quantum secure)
337    Stark,
338}
339
340/// Prover for creating zero-knowledge proofs
341#[cfg(feature = "zkp")]
342pub struct ZkpProver {
343    // Using default config for now - can be extended to support custom configs
344}
345
346#[cfg(not(feature = "zkp"))]
347pub struct ZkpProver;
348
349/// Verifier for verifying zero-knowledge proofs
350#[cfg(feature = "zkp")]
351pub struct ZkpVerifier {
352    // Using default config for now - can be extended to support custom configs
353}
354
355#[cfg(not(feature = "zkp"))]
356pub struct ZkpVerifier;
357
358#[cfg(feature = "zkp")]
359impl ZkpProver {
360    /// Create a new ZKP prover
361    pub fn new() -> Self {
362        Self {}
363    }
364
365    /// Prove knowledge of a secret value without revealing it
366    ///
367    /// This generates a STARK proof that the prover knows a preimage `secret_value`
368    /// whose **Poseidon-128** hash equals the public commitment. The proof uses
369    /// Poseidon for constraint encoding (industry-standard for STARKs; e.g. StarkWare,
370    /// RISC Zero, Succinct). For a NIST-only hash, use [`prove_secret_value_nist`](ZkpProver::prove_secret_value_nist).
371    ///
372    /// # Arguments
373    ///
374    /// * `secret_value` - The secret preimage to prove knowledge of
375    /// * `public_statement` - Additional public data (currently unused; reserved for future use)
376    ///
377    /// # Returns
378    ///
379    /// A zero-knowledge proof that can be verified without revealing the secret
380    ///
381    /// # Example
382    ///
383    /// ```rust,ignore
384    /// use lib_q_zkp::{ZkpProver, ZkpVerifier};
385    ///
386    /// let mut prover = ZkpProver::new();
387    /// let secret = b"my secret password";
388    /// let public = b"challenge";
389    ///
390    /// let proof = prover.prove_secret_value(secret, public)?;
391    /// ```
392    pub fn prove_secret_value(
393        &mut self,
394        secret_value: &[u8],
395        _public_statement: &[u8],
396    ) -> Result<ZkpProof> {
397        use crate::air::{
398            HashPreimageAir,
399            TraceGenerator,
400        };
401        use crate::stark::{
402            StarkProver,
403            default_config,
404        };
405
406        // Validate input size
407        if secret_value.is_empty() {
408            return Err(lib_q_core::Error::InvalidState {
409                operation: "prove_secret_value".to_string(),
410                reason: "Secret value cannot be empty".to_string(),
411            });
412        }
413
414        if secret_value.len() > air::hash_preimage::MAX_PREIMAGE_SIZE {
415            return Err(lib_q_core::Error::InvalidState {
416                operation: "prove_secret_value".to_string(),
417                reason: "Secret value exceeds maximum size".to_string(),
418            });
419        }
420
421        // Create the hash preimage AIR
422        let air = HashPreimageAir::new();
423
424        // Generate trace from the secret preimage
425        let input = secret_value.to_vec();
426        let trace: RowMajorMatrix<ZkpField> =
427            air.generate_trace(&input)
428                .map_err(|e| lib_q_core::Error::InternalError {
429                    operation: "prove_secret_value".to_string(),
430                    details: e.to_string(),
431                })?;
432
433        // Get public values (the hash output)
434        let public_values: Vec<ZkpField> = air.public_values(&input);
435
436        // Create prover with default config
437        let config = default_config();
438        let prover = StarkProver::new(config);
439
440        // Generate STARK proof
441        let proof = prover.prove(&air, trace, &public_values).map_err(|e| {
442            lib_q_core::Error::InternalError {
443                operation: "STARK proof generation".to_string(),
444                details: e.to_string(),
445            }
446        })?;
447
448        // Store output size in proof metadata
449        let metadata = ProofMetadata::HashPreimage { output_size: 1u16 };
450
451        // Serialize into ZkpProof
452        ZkpProof::from_stark_proof(&proof, metadata)
453    }
454
455    /// Prove knowledge of a secret value using NIST cSHAKE256 (100% NIST compliance)
456    ///
457    /// Same semantics as [`prove_secret_value`](ZkpProver::prove_secret_value) but uses
458    /// cSHAKE256 with domain `b"HashPreimageNistAir"` for the commitment. Use this when
459    /// NIST-only hashes are required; prover cost is higher than Poseidon-based proofs.
460    ///
461    /// # Arguments
462    ///
463    /// * `secret_value` - The secret preimage to prove knowledge of
464    /// * `_public_statement` - Reserved for future use
465    ///
466    /// # Status
467    ///
468    /// NOT IMPLEMENTED. The underlying [`HashPreimageNistAir`](crate::air::HashPreimageNistAir)
469    /// does not yet encode Keccak-f / cSHAKE256 constraints, so a generated proof would not
470    /// soundly bind the secret to the public hash. This function therefore returns
471    /// [`Error::NotImplemented`](lib_q_core::Error::NotImplemented) until the constraints exist.
472    pub fn prove_secret_value_nist(
473        &mut self,
474        secret_value: &[u8],
475        public_statement: &[u8],
476    ) -> Result<ZkpProof> {
477        // SOUNDNESS GATE: cSHAKE256/Keccak-f AIR constraints are not implemented yet (see
478        // `crate::air::hash_preimage_nist`). A proof from the current AIR would not bind the
479        // secret to the public hash, so we refuse rather than emit a proof that proves nothing.
480        // Once real Keccak-f constraints exist, replace this with the trace-generation and
481        // STARK-proving flow used by `prove_secret_value`.
482        let _ = (secret_value, public_statement);
483        Err(lib_q_core::Error::NotImplemented {
484            feature: "NIST (cSHAKE256) preimage proofs: Keccak-f AIR constraints not implemented"
485                .to_string(),
486        })
487    }
488
489    /// Prove a computation using a circuit
490    ///
491    /// This generates a STARK proof that the prover knows witness values that
492    /// satisfy all constraints in the arithmetic circuit.
493    ///
494    /// # Arguments
495    ///
496    /// * `circuit` - The arithmetic circuit defining the computation
497    /// * `witness` - The witness values (private inputs)
498    /// * `public` - The public input values
499    ///
500    /// # Returns
501    ///
502    /// A zero-knowledge proof of computation correctness
503    ///
504    /// # Example
505    ///
506    /// ```rust,ignore
507    /// use lib_q_zkp::{ZkpProver, circuit::CircuitBuilder};
508    /// use lib_q_stark_field::extension::Complex;
509    /// use lib_q_stark_mersenne31::Mersenne31;
510    ///
511    /// type Val = Complex<Mersenne31>;
512    ///
513    /// // Build a circuit: prove knowledge of a, b such that a * b = public_output
514    /// let mut builder = CircuitBuilder::<Val>::new(2, 1);
515    /// let a = builder.wire(0);
516    /// let b = builder.wire(1);
517    /// let output = builder.wire(2);
518    /// let product = builder.mul(a, b);
519    /// builder.assert_eq(product, output);
520    /// let circuit = builder.build();
521    ///
522    /// // Generate proof
523    /// let witness = vec![Val::from(3u32), Val::from(4u32)];
524    /// let public = vec![Val::from(12u32)];
525    ///
526    /// let mut prover = ZkpProver::new();
527    /// let proof = prover.prove_computation(&circuit, &witness, &public)?;
528    /// ```
529    pub fn prove_computation(
530        &mut self,
531        circuit: &circuit::ArithmeticCircuit<ZkpField>,
532        witness: &[ZkpField],
533        public: &[ZkpField],
534    ) -> Result<ZkpProof> {
535        use crate::circuit::CircuitAir;
536        use crate::stark::{
537            StarkProver,
538            default_config,
539        };
540
541        // Create the circuit AIR
542        let air = CircuitAir::new(circuit.clone());
543
544        // Generate trace from witness and public values
545        let trace = air.generate_trace(witness, public)?;
546
547        // Create prover with default config
548        let config = default_config();
549        let prover = StarkProver::new(config);
550
551        // Generate STARK proof
552        let proof =
553            prover
554                .prove(&air, trace, public)
555                .map_err(|e| lib_q_core::Error::InternalError {
556                    operation: "STARK proof generation".to_string(),
557                    details: e.to_string(),
558                })?;
559
560        // Store circuit parameters in proof metadata
561        let metadata = ProofMetadata::Circuit {
562            num_witnesses: witness.len().min(u32::MAX as usize) as u32,
563            num_public: public.len().min(u32::MAX as usize) as u32,
564        };
565
566        // Serialize into ZkpProof
567        ZkpProof::from_stark_proof(&proof, metadata)
568    }
569}
570
571#[cfg(not(feature = "zkp"))]
572impl ZkpProver {
573    /// Create a new ZKP prover
574    pub fn new() -> Self {
575        Self {}
576    }
577
578    /// Prove knowledge of a secret value without revealing it
579    pub fn prove_secret_value(
580        &mut self,
581        _secret_value: &[u8],
582        _public_statement: &[u8],
583    ) -> Result<ZkpProof> {
584        Err(lib_q_core::Error::NotImplemented {
585            feature: "ZKP feature not enabled".to_string(),
586        })
587    }
588
589    /// Prove knowledge of a secret value (NIST variant)
590    pub fn prove_secret_value_nist(
591        &mut self,
592        _secret_value: &[u8],
593        _public_statement: &[u8],
594    ) -> Result<ZkpProof> {
595        Err(lib_q_core::Error::NotImplemented {
596            feature: "ZKP feature not enabled".to_string(),
597        })
598    }
599}
600
601/// Crate-private helper for NIST secret value verification. Used by both
602/// `ZkpVerifier::verify` and `ZkpVerifier::verify_secret_value_nist`.
603#[cfg(feature = "zkp")]
604fn verify_secret_value_nist_impl(proof: &ZkpProof, expected_hash: &[u8]) -> Result<bool> {
605    if proof.proof_type != ProofType::Stark {
606        return Ok(false);
607    }
608    if proof.data.is_empty() {
609        return Ok(false);
610    }
611
612    let ProofMetadata::HashPreimageNist { .. } = &proof.metadata else {
613        return Ok(false);
614    };
615
616    // SOUNDNESS GATE: the NIST AIR has no Keccak-f constraints yet (see
617    // `crate::air::hash_preimage_nist`), so a "valid" proof would prove nothing. Refuse to
618    // verify NIST proofs rather than accept them. Once the constraints exist, restore the
619    // StarkVerifier flow against `expected_hash_to_public_values(expected_hash)`.
620    let _ = expected_hash;
621    Err(lib_q_core::Error::NotImplemented {
622        feature: "NIST (cSHAKE256) preimage proofs: Keccak-f AIR constraints not implemented"
623            .to_string(),
624    })
625}
626
627#[cfg(feature = "zkp")]
628impl ZkpVerifier {
629    /// Create a new ZKP verifier
630    pub fn new() -> Self {
631        Self {}
632    }
633
634    /// Verify a zero-knowledge proof of secret value (preimage) knowledge
635    ///
636    /// This verifies a proof generated by [`ZkpProver::prove_secret_value`]. The verifier
637    /// recomputes the public Poseidon commitment from the **preimage**, so the caller passes
638    /// the same secret preimage that was given to the prover (NOT the hash output). The proof
639    /// then attests that the prover knew a preimage hashing to that commitment.
640    /// For NIST proofs use [`verify_secret_value_nist`](ZkpVerifier::verify_secret_value_nist).
641    ///
642    /// # Arguments
643    ///
644    /// * `proof` - The proof to verify
645    /// * `preimage` - The secret preimage (same bytes passed to `prove_secret_value`); the
646    ///   verifier hashes it with Poseidon-128 to reconstruct the public commitment
647    ///
648    /// # Returns
649    ///
650    /// `Ok(true)` if the proof is valid, `Ok(false)` or `Err` otherwise
651    pub fn verify_secret_value(&self, proof: &ZkpProof, preimage: &[u8]) -> Result<bool> {
652        use crate::air::{
653            HashPreimageAir,
654            TraceGenerator,
655        };
656        use crate::stark::{
657            StarkVerifier,
658            default_config,
659        };
660
661        if proof.proof_type != ProofType::Stark {
662            return Ok(false);
663        }
664
665        if proof.data.is_empty() {
666            return Ok(false);
667        }
668
669        // Proof must contain output size metadata
670        let ProofMetadata::HashPreimage { output_size } = &proof.metadata else {
671            // Missing metadata - proof is invalid
672            return Ok(false);
673        };
674
675        // Create the same AIR used for proving (output_size in metadata retained for compatibility)
676        let _ = output_size;
677        let air = HashPreimageAir::new();
678
679        // Reconstruct the public commitment exactly as the prover did, from the preimage.
680        // Using the AIR's own `public_values` guarantees the same padding/encoding rules.
681        let public_values: Vec<ZkpField> = air.public_values(&preimage.to_vec());
682
683        // Deserialize the STARK proof
684        let stark_proof = proof.to_stark_proof()?;
685
686        // Create verifier with default config
687        let config = default_config();
688        let verifier = StarkVerifier::new(config);
689
690        // Verify the proof
691        match verifier.verify(&air, &stark_proof, &public_values) {
692            Ok(()) => Ok(true),
693            Err(_) => Ok(false),
694        }
695    }
696
697    /// Verify a NIST (cSHAKE256) secret value proof
698    ///
699    /// Verifies a proof from [`prove_secret_value_nist`](ZkpProver::prove_secret_value_nist).
700    /// `expected_hash` is the raw 32-byte cSHAKE256 output (same as used in proving).
701    pub fn verify_secret_value_nist(&self, proof: &ZkpProof, expected_hash: &[u8]) -> Result<bool> {
702        verify_secret_value_nist_impl(proof, expected_hash)
703    }
704
705    /// Verify a zero-knowledge proof of computation
706    ///
707    /// This verifies a proof generated by `ZkpProver::prove_computation`.
708    ///
709    /// # Arguments
710    ///
711    /// * `proof` - The proof to verify
712    /// * `circuit` - The arithmetic circuit that was proven
713    /// * `public` - The public input values
714    ///
715    /// # Returns
716    ///
717    /// `Ok(true)` if the proof is valid, `Ok(false)` or `Err` otherwise
718    pub fn verify_computation(
719        &self,
720        proof: &ZkpProof,
721        circuit: &circuit::ArithmeticCircuit<ZkpField>,
722        public: &[ZkpField],
723    ) -> Result<bool> {
724        use crate::circuit::CircuitAir;
725        use crate::stark::{
726            StarkVerifier,
727            default_config,
728        };
729
730        if proof.proof_type != ProofType::Stark {
731            return Ok(false);
732        }
733
734        if proof.data.is_empty() {
735            return Ok(false);
736        }
737
738        // Create the same AIR used for proving
739        let air = CircuitAir::new(circuit.clone());
740
741        // Deserialize the STARK proof
742        let stark_proof = proof.to_stark_proof()?;
743
744        // Create verifier with default config
745        let config = default_config();
746        let verifier = StarkVerifier::new(config);
747
748        // Verify the proof
749        match verifier.verify(&air, &stark_proof, public) {
750            Ok(()) => Ok(true),
751            Err(_) => Ok(false),
752        }
753    }
754
755    /// Verify a zero-knowledge proof.
756    ///
757    /// Performs full cryptographic (STARK) verification for proof types whose public
758    /// inputs are fully described by a byte slice:
759    ///
760    /// - `ProofMetadata::HashPreimage`: `public_statement` is the expected hash output
761    ///   (same semantics as `verify_secret_value`).
762    /// - `ProofMetadata::HashPreimageNist`: `public_statement` is the expected cSHAKE256
763    ///   hash output (same semantics as `verify_secret_value_nist`).
764    /// - `ProofMetadata::MerkleInclusion`: `public_statement` is the expected Merkle
765    ///   root hash (same semantics as `api::verify_membership`).
766    ///
767    /// Returns `Ok(false)` for `Circuit`, `Credential`, `Identity`, and `None`
768    /// metadata variants. Those proof types require a type-specific verifier that accepts
769    /// the additional inputs needed to reconstruct verification state.
770    ///
771    /// `batch_verify` delegates to this method, so the same rules apply in bulk.
772    pub fn verify(&self, proof: ZkpProof, public_statement: &[u8]) -> Result<bool> {
773        if proof.proof_type != ProofType::Stark {
774            return Ok(false);
775        }
776        if proof.data.is_empty() {
777            return Ok(false);
778        }
779        match &proof.metadata {
780            ProofMetadata::HashPreimage { .. } => {
781                self.verify_secret_value(&proof, public_statement)
782            }
783            ProofMetadata::HashPreimageNist { .. } => {
784                verify_secret_value_nist_impl(&proof, public_statement)
785            }
786            ProofMetadata::MerkleInclusion { .. } => verify_membership(&proof, public_statement),
787            ProofMetadata::UnlinkableMembership { .. } => {
788                membership::verify_unlinkable_membership_bytes(&proof, public_statement)
789            }
790            _ => Ok(false),
791        }
792    }
793
794    /// Batch verify multiple proofs
795    ///
796    /// # Arguments
797    ///
798    /// * `proofs` - The proofs to verify
799    /// * `publics` - The public statements for each proof
800    ///
801    /// # Returns
802    ///
803    /// `true` if all proofs are valid, `false` otherwise
804    pub fn batch_verify(&self, proofs: &[ZkpProof], publics: &[&[u8]]) -> Result<bool> {
805        if proofs.len() != publics.len() {
806            return Err(lib_q_core::Error::InvalidState {
807                operation: "batch_verify".to_string(),
808                reason: "Number of proofs must match number of public statements".to_string(),
809            });
810        }
811
812        for (proof, public) in proofs.iter().zip(publics.iter()) {
813            match self.verify(proof.clone(), public) {
814                Ok(true) => continue,
815                Ok(false) => return Ok(false),
816                Err(e) => return Err(e),
817            }
818        }
819
820        Ok(true)
821    }
822}
823
824#[cfg(not(feature = "zkp"))]
825impl ZkpVerifier {
826    /// Create a new ZKP verifier
827    pub fn new() -> Self {
828        Self {}
829    }
830
831    /// Verify a zero-knowledge proof
832    pub fn verify(&self, _proof: ZkpProof, _public_statement: &[u8]) -> Result<bool> {
833        Err(lib_q_core::Error::NotImplemented {
834            feature: "ZKP feature not enabled".to_string(),
835        })
836    }
837
838    /// Verify a NIST secret value proof
839    pub fn verify_secret_value_nist(
840        &self,
841        _proof: &ZkpProof,
842        _expected_hash: &[u8],
843    ) -> Result<bool> {
844        Err(lib_q_core::Error::NotImplemented {
845            feature: "ZKP feature not enabled".to_string(),
846        })
847    }
848
849    /// Batch verify multiple proofs
850    pub fn batch_verify(&self, _proofs: &[ZkpProof], _publics: &[&[u8]]) -> Result<bool> {
851        Err(lib_q_core::Error::NotImplemented {
852            feature: "ZKP feature not enabled".to_string(),
853        })
854    }
855}
856
857impl Default for ZkpProver {
858    fn default() -> Self {
859        Self::new()
860    }
861}
862
863impl Default for ZkpVerifier {
864    fn default() -> Self {
865        Self::new()
866    }
867}
868
869/// Get available ZKP algorithms (STARK when zkp feature is enabled).
870pub fn available_algorithms() -> Vec<&'static str> {
871    let algorithms = vec![
872        #[cfg(feature = "zkp")]
873        "stark",
874    ];
875
876    algorithms
877}
878
879/// Create a ZKP instance by algorithm name
880pub fn create_zkp(algorithm: &str) -> Result<Box<dyn core::any::Any>> {
881    match algorithm {
882        #[cfg(feature = "zkp")]
883        "stark" => Ok(Box::new(ZkpProver::new())),
884
885        _ => Err(lib_q_core::Error::InvalidAlgorithm {
886            algorithm: "Unknown ZKP algorithm",
887        }),
888    }
889}
890
891#[cfg(test)]
892mod tests {
893    use super::*;
894
895    #[test]
896    fn test_zkp_prover_creation() {
897        let _prover = ZkpProver::new();
898        // Just check that creation doesn't panic
899    }
900
901    #[test]
902    fn test_zkp_verifier_creation() {
903        let _verifier = ZkpVerifier::new();
904        // Just check that creation doesn't panic
905    }
906
907    #[test]
908    fn test_zkp_proof_creation() {
909        let mut prover = ZkpProver::new();
910        let secret_value = b"secret_value";
911        let public_statement = b"public_statement";
912
913        // Now that prove_secret_value is implemented, it should succeed
914        let result = prover.prove_secret_value(secret_value, public_statement);
915        // The proof generation should succeed (though it may take some time)
916        assert!(
917            result.is_ok(),
918            "Proof generation should succeed: {:?}",
919            result.err()
920        );
921    }
922
923    #[cfg(feature = "zkp")]
924    #[test]
925    fn test_nist_secret_value_not_implemented() {
926        // The NIST AIR has no Keccak-f constraints, so prove/verify must refuse rather
927        // than emit/accept an unsound proof.
928        let secret = b"nist_secret_value";
929        let mut prover = ZkpProver::new();
930        assert!(
931            matches!(
932                prover.prove_secret_value_nist(secret, b""),
933                Err(lib_q_core::Error::NotImplemented { .. })
934            ),
935            "NIST prove must return NotImplemented"
936        );
937
938        let verifier = ZkpVerifier::new();
939        let mut dummy = ZkpProof {
940            data: alloc::vec![1u8; 8],
941            proof_type: ProofType::Stark,
942            security_level: 1,
943            metadata: ProofMetadata::HashPreimageNist { output_size: 32 },
944        };
945        assert!(
946            matches!(
947                verifier.verify_secret_value_nist(&dummy, &[0u8; 32]),
948                Err(lib_q_core::Error::NotImplemented { .. })
949            ),
950            "NIST verify must return NotImplemented"
951        );
952        // Generic verify() dispatches to the NIST impl, which must also refuse.
953        dummy.data = alloc::vec![1u8; 8];
954        assert!(
955            matches!(
956                verifier.verify(dummy, &[0u8; 32]),
957                Err(lib_q_core::Error::NotImplemented { .. })
958            ),
959            "verify() must return NotImplemented for NIST proofs"
960        );
961    }
962
963    #[cfg(feature = "zkp")]
964    #[test]
965    fn test_poseidon_proof_rejected_by_nist_verifier() {
966        let secret = b"poseidon_only";
967        let mut prover = ZkpProver::new();
968        let proof = prover
969            .prove_secret_value(secret, b"")
970            .expect("Poseidon prove");
971        let verifier = ZkpVerifier::new();
972        assert!(
973            !verifier
974                .verify_secret_value_nist(&proof, &[0u8; 32])
975                .unwrap(),
976            "Poseidon proof must not be accepted by NIST verifier"
977        );
978    }
979
980    #[cfg(feature = "zkp")]
981    #[test]
982    fn test_verify_rejects_unknown_metadata() {
983        use lib_q_stark_field::PrimeCharacteristicRing;
984        use lib_q_stark_mersenne31::Mersenne31;
985
986        use crate::air::{
987            ArithmeticAir,
988            TraceGenerator,
989        };
990        use crate::stark::{
991            StarkProver,
992            default_config,
993        };
994
995        let air = ArithmeticAir::new(1).expect("ArithmeticAir");
996        let one = <ZkpField as PrimeCharacteristicRing>::ONE;
997        let seven = ZkpField::from(Mersenne31::new(7));
998        let input = alloc::vec![(one, seven)];
999        let trace = air.generate_trace(&input).expect("trace generation");
1000        let public_values = air.public_values(&input);
1001        let proof_inner = StarkProver::new(default_config())
1002            .prove(&air, trace, &public_values)
1003            .expect("prove");
1004        let proof_bytes = postcard::to_allocvec(&proof_inner).expect("serialize STARK proof");
1005
1006        let proof = ZkpProof {
1007            data: proof_bytes,
1008            proof_type: ProofType::Stark,
1009            security_level: 1,
1010            metadata: ProofMetadata::None,
1011        };
1012
1013        let verifier = ZkpVerifier::new();
1014        assert_eq!(
1015            verifier.verify(proof, b"public_statement").unwrap(),
1016            false,
1017            "ProofMetadata::None must return false -- use a type-specific verifier"
1018        );
1019    }
1020
1021    #[test]
1022    fn test_batch_verify_mismatched_lengths() {
1023        let verifier = ZkpVerifier::new();
1024        let proofs = vec![ZkpProof {
1025            data: vec![0u8; 64],
1026            proof_type: ProofType::Stark,
1027            security_level: 1,
1028            metadata: ProofMetadata::None,
1029        }];
1030        let publics: &[&[u8]] = &[b"public1" as &[u8], b"public2" as &[u8]];
1031
1032        let result = verifier.batch_verify(&proofs, publics);
1033        assert!(result.is_err());
1034        if let Err(lib_q_core::Error::InvalidState { .. }) = result {
1035            // Expected
1036        } else {
1037            panic!("Expected InvalidState error");
1038        }
1039    }
1040
1041    #[cfg(feature = "zkp")]
1042    #[test]
1043    fn test_proof_metadata_merkle() {
1044        let metadata = ProofMetadata::MerkleInclusion { tree_depth: 8 };
1045        let proof = ZkpProof {
1046            data: vec![0u8; 64],
1047            proof_type: ProofType::Stark,
1048            security_level: 1,
1049            metadata,
1050        };
1051        assert_eq!(proof.merkle_tree_depth(), Some(8));
1052    }
1053
1054    #[cfg(feature = "zkp")]
1055    #[test]
1056    fn test_proof_metadata_none() {
1057        let proof = ZkpProof {
1058            data: vec![0u8; 64],
1059            proof_type: ProofType::Stark,
1060            security_level: 1,
1061            metadata: ProofMetadata::None,
1062        };
1063        assert_eq!(proof.merkle_tree_depth(), None);
1064    }
1065
1066    #[test]
1067    fn test_available_algorithms() {
1068        let algorithms = available_algorithms();
1069        #[cfg(feature = "zkp")]
1070        assert!(!algorithms.is_empty(), "zkp feature enables STARK");
1071        #[cfg(not(feature = "zkp"))]
1072        let _ = algorithms;
1073    }
1074
1075    #[cfg(feature = "zkp")]
1076    #[test]
1077    fn test_create_zkp() {
1078        let algorithms = available_algorithms();
1079        assert!(!algorithms.is_empty());
1080        let algorithm = algorithms[0];
1081        assert!(create_zkp(algorithm).is_ok());
1082    }
1083
1084    #[cfg(feature = "zkp")]
1085    #[test]
1086    fn test_verify_rejects_forged_proof_with_hash_preimage_metadata() {
1087        let proof = ZkpProof {
1088            data: alloc::vec![
1089                0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0xAA, 0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0xAA, 0xDE, 0xAD,
1090                0xBE, 0xEF, 0xFF, 0xAA, 0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0xAA, 0xDE, 0xAD, 0xBE, 0xEF,
1091                0xFF, 0xAA, 0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0xAA, 0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0xAA,
1092                0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0xAA, 0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0xAA, 0xDE, 0xAD,
1093                0xBE, 0xEF, 0xFF, 0xAA, 0xDE, 0xAD, 0xBE, 0xEF, 0xFF, 0xAA, 0xDE, 0xAD, 0xBE, 0xEF,
1094            ],
1095            proof_type: ProofType::Stark,
1096            security_level: 1,
1097            metadata: ProofMetadata::HashPreimage { output_size: 1 },
1098        };
1099        let verifier = ZkpVerifier::new();
1100        let result = verifier.verify(proof, b"expected_hash");
1101        assert!(
1102            matches!(result, Ok(false) | Err(_)),
1103            "forged HashPreimage proof must not return Ok(true)"
1104        );
1105    }
1106
1107    #[cfg(feature = "zkp")]
1108    #[test]
1109    fn test_verify_rejects_forged_proof_with_merkle_metadata() {
1110        let proof = ZkpProof {
1111            data: alloc::vec![
1112                0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE,
1113                0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE,
1114                0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE,
1115                0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE,
1116                0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE, 0xCA, 0xFE, 0xBA, 0xBE,
1117            ],
1118            proof_type: ProofType::Stark,
1119            security_level: 1,
1120            metadata: ProofMetadata::MerkleInclusion { tree_depth: 4 },
1121        };
1122        let verifier = ZkpVerifier::new();
1123        let result = verifier.verify(proof, b"wrong_root");
1124        assert!(
1125            matches!(result, Ok(false) | Err(_)),
1126            "forged MerkleInclusion proof must not return Ok(true)"
1127        );
1128    }
1129
1130    #[cfg(feature = "zkp")]
1131    #[test]
1132    fn test_verify_rejects_circuit_metadata_proof() {
1133        let proof = ZkpProof {
1134            data: alloc::vec![0u8; 64],
1135            proof_type: ProofType::Stark,
1136            security_level: 1,
1137            metadata: ProofMetadata::Circuit {
1138                num_witnesses: 2,
1139                num_public: 1,
1140            },
1141        };
1142        let verifier = ZkpVerifier::new();
1143        assert_eq!(
1144            verifier.verify(proof, b"anything").unwrap(),
1145            false,
1146            "Circuit proofs must be rejected by generic verify; use verify_computation"
1147        );
1148    }
1149
1150    #[cfg(feature = "zkp")]
1151    #[test]
1152    fn test_verify_rejects_credential_metadata_proof() {
1153        let proof = ZkpProof {
1154            data: alloc::vec![0u8; 64],
1155            proof_type: ProofType::Stark,
1156            security_level: 1,
1157            metadata: ProofMetadata::Credential {
1158                attribute_sizes: alloc::vec![8, 4],
1159                reveal_mask: alloc::vec![true, false],
1160            },
1161        };
1162        let verifier = ZkpVerifier::new();
1163        assert_eq!(
1164            verifier.verify(proof, b"anything").unwrap(),
1165            false,
1166            "Credential proofs must be rejected by generic verify; use ip::verify_credential_proof"
1167        );
1168    }
1169
1170    #[cfg(feature = "zkp")]
1171    #[test]
1172    fn test_verify_rejects_identity_metadata_proof() {
1173        let proof = ZkpProof {
1174            data: alloc::vec![0u8; 64],
1175            proof_type: ProofType::Stark,
1176            security_level: 1,
1177            metadata: ProofMetadata::Identity { dsa_level: 65 },
1178        };
1179        let verifier = ZkpVerifier::new();
1180        assert_eq!(
1181            verifier.verify(proof, b"anything").unwrap(),
1182            false,
1183            "Identity proofs must be rejected by generic verify; use ip::verify_it_ownership"
1184        );
1185    }
1186
1187    #[cfg(feature = "zkp")]
1188    #[test]
1189    fn test_verify_empty_data_is_rejected() {
1190        let proof = ZkpProof {
1191            data: alloc::vec![],
1192            proof_type: ProofType::Stark,
1193            security_level: 1,
1194            metadata: ProofMetadata::HashPreimage { output_size: 1 },
1195        };
1196        let verifier = ZkpVerifier::new();
1197        assert_eq!(
1198            verifier.verify(proof, b"anything").unwrap(),
1199            false,
1200            "empty proof data must be rejected regardless of metadata"
1201        );
1202    }
1203
1204    #[cfg(feature = "zkp")]
1205    #[test]
1206    fn test_proof_type_only_stark_exists() {
1207        let _stark = ProofType::Stark;
1208        // ProofType::Snark and ProofType::Bulletproof have been removed.
1209        // Only transparent, hash-based proof systems believed post-quantum secure are
1210        // supported (no ZK proof system is NIST-approved -- see `ProofType`).
1211    }
1212
1213    #[cfg(feature = "zkp")]
1214    #[test]
1215    fn test_batch_verify_rejects_forged_hash_preimage_proof() {
1216        let forged = ZkpProof {
1217            data: alloc::vec![0xFF; 64],
1218            proof_type: ProofType::Stark,
1219            security_level: 1,
1220            metadata: ProofMetadata::HashPreimage { output_size: 1 },
1221        };
1222        let verifier = ZkpVerifier::new();
1223        let proofs = alloc::vec![forged];
1224        let publics: &[&[u8]] = &[b"anything"];
1225        let result = verifier.batch_verify(&proofs, publics);
1226        assert!(
1227            matches!(result, Ok(false) | Err(_)),
1228            "batch_verify must not accept a forged proof"
1229        );
1230    }
1231
1232    #[cfg(feature = "zkp")]
1233    #[test]
1234    fn test_prove_secret_value_rejects_empty_and_oversized_input() {
1235        let mut prover = ZkpProver::new();
1236        let empty = prover.prove_secret_value(b"", b"");
1237        assert!(empty.is_err());
1238
1239        let oversized = vec![0u8; air::hash_preimage::MAX_PREIMAGE_SIZE + 1];
1240        let too_large = prover.prove_secret_value(&oversized, b"");
1241        assert!(too_large.is_err());
1242    }
1243
1244    #[cfg(feature = "zkp")]
1245    #[test]
1246    fn test_prove_secret_value_nist_rejects_empty_and_oversized_input() {
1247        let mut prover = ZkpProver::new();
1248        let empty = prover.prove_secret_value_nist(b"", b"");
1249        assert!(empty.is_err());
1250
1251        let oversized = vec![0u8; air::hash_preimage_nist::MAX_PREIMAGE_SIZE + 1];
1252        let too_large = prover.prove_secret_value_nist(&oversized, b"");
1253        assert!(too_large.is_err());
1254    }
1255
1256    #[cfg(feature = "zkp")]
1257    #[test]
1258    fn test_verify_secret_value_rejects_invalid_proof_shape_inputs() {
1259        let verifier = ZkpVerifier::new();
1260        let non_stark = ZkpProof {
1261            data: vec![1u8; 16],
1262            proof_type: ProofType::Stark,
1263            security_level: 1,
1264            metadata: ProofMetadata::HashPreimageNist { output_size: 32 },
1265        };
1266        assert!(!verifier.verify_secret_value(&non_stark, b"hash").unwrap());
1267
1268        let empty = ZkpProof {
1269            data: vec![],
1270            proof_type: ProofType::Stark,
1271            security_level: 1,
1272            metadata: ProofMetadata::HashPreimage { output_size: 1 },
1273        };
1274        assert!(!verifier.verify_secret_value(&empty, b"hash").unwrap());
1275    }
1276
1277    #[cfg(feature = "zkp")]
1278    #[test]
1279    fn test_verify_secret_value_nist_rejects_wrong_metadata_and_bad_bytes() {
1280        let verifier = ZkpVerifier::new();
1281        let wrong_meta = ZkpProof {
1282            data: vec![1u8; 16],
1283            proof_type: ProofType::Stark,
1284            security_level: 1,
1285            metadata: ProofMetadata::HashPreimage { output_size: 1 },
1286        };
1287        assert!(
1288            !verifier
1289                .verify_secret_value_nist(&wrong_meta, &[0u8; 32])
1290                .unwrap()
1291        );
1292
1293        // A proof carrying NIST metadata reaches the soundness gate, which refuses to
1294        // verify (NIST constraints not implemented) rather than returning a boolean.
1295        let nist_meta = ZkpProof {
1296            data: vec![0xAA; 16],
1297            proof_type: ProofType::Stark,
1298            security_level: 1,
1299            metadata: ProofMetadata::HashPreimageNist { output_size: 32 },
1300        };
1301        assert!(matches!(
1302            verifier.verify_secret_value_nist(&nist_meta, &[0u8; 32]),
1303            Err(lib_q_core::Error::NotImplemented { .. })
1304        ));
1305    }
1306
1307    #[cfg(feature = "zkp")]
1308    #[test]
1309    fn test_verify_computation_rejects_empty_or_non_stark_data() {
1310        use crate::circuit::CircuitBuilder;
1311
1312        let verifier = ZkpVerifier::new();
1313        let circuit = CircuitBuilder::<ZkpField>::new(1, 0).build();
1314
1315        let empty = ZkpProof {
1316            data: vec![],
1317            proof_type: ProofType::Stark,
1318            security_level: 1,
1319            metadata: ProofMetadata::Circuit {
1320                num_witnesses: 1,
1321                num_public: 0,
1322            },
1323        };
1324        assert!(!verifier.verify_computation(&empty, &circuit, &[]).unwrap());
1325    }
1326}