Skip to main content

lib_q_zkp/air/
mod.rs

1//! AIR (Algebraic Intermediate Representation) module
2//!
3//! This module provides standalone AIR implementations for common proof types
4//! used in zero-knowledge proofs. Each AIR defines constraints that can be
5//! verified using STARK proving systems.
6//!
7//! # Available AIRs
8//!
9//! - [`crate::air::arithmetic::ArithmeticAir`] - Basic arithmetic operations (multiplication constraints)
10//! - [`crate::air::range_proof::RangeProofAir`] - Proves a value is within a specified range
11//! - [`crate::air::hash_preimage::HashPreimageAir`] - Proves knowledge of a Poseidon-128 preimage (industry-standard for STARK constraint encoding)
12//! - [`crate::air::merkle_inclusion::MerkleInclusionAir`] - Proves membership in a Merkle tree
13//!
14//! # Security
15//!
16//! All AIR implementations follow these security principles:
17//! - Input validation to prevent DoS attacks
18//! - Constant-time operations where applicable
19//!
20//! # Example
21//!
22//! ```rust,ignore
23//! use lib_q_zkp::air::{ArithmeticAir, TraceGenerator};
24//! use lib_q_stark_field::extension::Complex;
25//! use lib_q_stark_mersenne31::Mersenne31;
26//!
27//! type Val = Complex<Mersenne31>;
28//!
29//! // Create an AIR for 3 multiplication operations
30//! let air = ArithmeticAir::new(3).unwrap();
31//!
32//! // Generate a trace
33//! let inputs = vec![(Val::from(2u32), Val::from(3u32))];
34//! let trace = air.generate_trace(&inputs)?;
35//! ```
36
37extern crate alloc;
38
39use alloc::string::{
40    String,
41    ToString,
42};
43use alloc::vec::Vec;
44use core::fmt;
45
46use lib_q_poseidon::{
47    PoseidonField,
48    PoseidonParams,
49    sbox,
50};
51use lib_q_stark_field::{
52    BasedVectorSpace,
53    Field,
54    PrimeCharacteristicRing,
55};
56use lib_q_stark_matrix::dense::RowMajorMatrix;
57use lib_q_stark_mersenne31::Mersenne31;
58
59pub mod anonymous_auth;
60pub mod arithmetic;
61pub mod batch_stark_verifier;
62pub mod commitment_verifier;
63pub mod constraint_verifier;
64pub mod credential;
65pub mod fri_verifier;
66pub mod hash_preimage;
67pub mod hash_preimage_nist;
68pub mod identity_proof;
69pub mod merkle_inclusion;
70pub mod opening_verifier;
71pub mod poseidon2_gadget;
72pub mod poseidon_gadget;
73pub mod poseidon_hash;
74pub mod range_proof;
75pub mod recovery_policy;
76pub mod recovery_policy_hybrid;
77pub mod recursive_types;
78pub mod session_key;
79pub mod stark_verifier;
80pub mod state_transition;
81pub mod transaction;
82pub mod unlinkable_membership;
83pub mod unlinkable_membership_baby_bear;
84pub mod verifier_utils;
85pub mod wide_hash;
86pub mod wide_merkle;
87pub mod wide_merkle_path;
88pub mod wide_merkle_path_baby_bear;
89pub mod wide_sponge;
90pub mod wide_sponge_baby_bear;
91
92pub use anonymous_auth::{
93    AnonymousAuthAir,
94    AnonymousAuthInput,
95};
96pub use arithmetic::ArithmeticAir;
97pub use batch_stark_verifier::{
98    BatchRecursiveStarkVerificationInput,
99    BatchStarkVerifierAir,
100    batch_recursive_verifier_public_values,
101};
102#[cfg(all(feature = "recursive-proofs-experimental", feature = "std"))]
103pub use commitment_verifier::debug_commitment_trace_sanity_check;
104pub use commitment_verifier::{
105    CommitmentVerificationInput,
106    CommitmentVerifierAir,
107};
108pub use constraint_verifier::{
109    ConstraintVerificationInput,
110    ConstraintVerifierAir,
111};
112pub use credential::{
113    CredentialAir,
114    CredentialInput,
115    CredentialSchema,
116};
117pub use fri_verifier::{
118    FriVerificationInput,
119    FriVerifierAir,
120};
121pub use hash_preimage::HashPreimageAir;
122pub use hash_preimage_nist::{
123    HASH_OUTPUT_BYTES,
124    HashPreimageNistAir,
125    HashPreimageNistInput,
126    expected_hash_to_public_values,
127};
128pub use identity_proof::{
129    IdentityProofAir,
130    IdentityProofInput,
131    MlDsaLevel,
132};
133pub use merkle_inclusion::{
134    MerkleHash,
135    MerkleInclusionAir,
136    MerkleProofInput,
137};
138pub use opening_verifier::{
139    OpeningVerificationInput,
140    OpeningVerifierAir,
141};
142pub use poseidon_gadget::PoseidonGadget;
143pub use poseidon_hash::PoseidonHashAir;
144pub use range_proof::RangeProofAir;
145pub use recovery_policy::{
146    RECOVERY_POLICY_AIR_ID,
147    RECOVERY_POLICY_COMMIT_DOMAIN,
148    RECOVERY_PUBLIC_INPUTS_LEN,
149    RECOVERY_VK_COMMIT_DOMAIN,
150    RecoveryPolicyAir,
151    RecoveryPolicyInput,
152    RecoveryPolicyKey,
153    RecoveryPolicyPublicInputs,
154    policy_commitment,
155    shake256_commit,
156    vk_commitment,
157};
158pub use recovery_policy_hybrid::{
159    RECOVERY_HYBRID_POLICY_COMMIT_DOMAIN,
160    RECOVERY_HYBRID_PUBLIC_INPUTS_LEN,
161    RECOVERY_HYBRID_VK_COMMIT_DOMAIN,
162    RECOVERY_POLICY_HYBRID_AIR_ID,
163    RecoveryPolicyHybridAir,
164    RecoveryPolicyHybridInput,
165    RecoveryPolicyHybridPublicInputs,
166    hybrid_policy_commitment,
167};
168pub use recursive_types::{
169    RecursiveStarkInput,
170    SerializedFriRound,
171    SerializedStarkProof,
172};
173pub use session_key::{
174    KdfAlgorithm,
175    KdfParams,
176    OUTPUT_LENGTH_GRANULARITY,
177    SessionKeyDerivationAir,
178    SessionKeyInput,
179    derive_session_keys,
180};
181pub use unlinkable_membership::{
182    CTX_ELEMS,
183    MEMBERSHIP_DOMAIN_STR,
184    MEMBERSHIP_NUM_PUBLIC,
185    MEMBERSHIP_ROW_WIDTH,
186    SECRET_T_ELEMS,
187    UnlinkableMembershipAir,
188    generate_membership_trace,
189    membership_domain,
190    membership_leaf,
191    membership_nullifier,
192    membership_public_values,
193};
194pub use wide_hash::{
195    WIDE_DIGEST_ELEMS,
196    WideDigest,
197    poseidon256_perm_truncated,
198    poseidon256_wide_hash,
199};
200pub use wide_merkle::{
201    NODE_NUM_PERMS,
202    NODE_ROW_WIDTH,
203    WideNodeHashAir,
204    generate_node_trace,
205    node_public_values,
206};
207pub use wide_merkle_path::{
208    PATH_NUM_PUBLIC,
209    PATH_ROW_WIDTH,
210    WideMerklePathAir,
211    generate_path_trace,
212    path_public_values,
213};
214pub use wide_sponge::{
215    constrain_wide_sponge,
216    generate_wide_sponge_cells,
217    wide_sponge_interm_cols,
218    wide_sponge_num_perms,
219};
220/// Trait for PCS commitments that are Poseidon Merkle roots. Used by the recursive verifier.
221/// The only implementation is when `recursive-proofs-experimental` is enabled (Hash in stark_verifier).
222pub trait PoseidonCommitmentRoot {
223    fn to_poseidon_root_bytes(&self) -> [u8; recursive_types::COMMITMENT_HASH_SIZE];
224}
225
226#[cfg(feature = "recursive-proofs-experimental")]
227pub use stark_verifier::{
228    MerklePathExtractable,
229    build_recursive_verification_input_from_proof,
230    build_recursive_verification_input_from_proof_with_poseidon,
231};
232pub use stark_verifier::{
233    RecursiveStarkVerificationInput,
234    StarkVerifierAir,
235    build_recursive_verification_input,
236};
237
238/// Maximum number of operations allowed in a single AIR instance
239/// to prevent memory exhaustion attacks.
240pub const MAX_OPERATIONS: usize = 1 << 20; // ~1 million operations
241
242/// Maximum trace width to prevent excessive memory allocation.
243/// Recursive StarkVerifierAir can exceed 65536; raised to 131072 for aggregation.
244pub const MAX_TRACE_WIDTH: usize = 1 << 17; // 131072 columns
245
246/// Maximum trace height (number of rows) to prevent memory exhaustion.
247pub const MAX_TRACE_HEIGHT: usize = 1 << 24; // ~16 million rows
248
249/// Error type for AIR operations
250#[derive(Debug, Clone, PartialEq, Eq)]
251pub enum AirError {
252    /// AIR configuration has invalid dimensions
253    InvalidDimensions {
254        /// Description of the dimension error
255        reason: String,
256    },
257
258    /// AIR exceeds maximum allowed size
259    ExceedsMaxSize {
260        /// Name of the parameter that exceeded limits
261        parameter: String,
262        /// Maximum allowed value
263        max: usize,
264        /// Actual value provided
265        actual: usize,
266    },
267
268    /// Invalid input data for trace generation
269    InvalidInput {
270        /// Description of what was invalid
271        reason: String,
272    },
273
274    /// Trace dimensions don't match AIR requirements
275    TraceMismatch {
276        /// Expected width
277        expected_width: usize,
278        /// Actual width
279        actual_width: usize,
280    },
281
282    /// Witness values don't satisfy constraints
283    InvalidWitness {
284        /// Description of which constraint failed
285        constraint: String,
286    },
287
288    /// Internal error during AIR evaluation
289    InternalError {
290        /// Description of the error
291        reason: String,
292    },
293
294    /// Feature required but not enabled
295    NotSupported {
296        /// Description of what is not supported
297        reason: String,
298    },
299
300    /// FRI commit-phase openings missing for the query index
301    MissingFriCommitPhaseOpenings,
302
303    /// FRI commit-phase step count does not match number of rounds
304    FriRoundCountMismatch,
305}
306
307impl fmt::Display for AirError {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        match self {
310            AirError::InvalidDimensions { reason } => {
311                write!(f, "Invalid AIR dimensions: {}", reason)
312            }
313            AirError::ExceedsMaxSize {
314                parameter,
315                max,
316                actual,
317            } => {
318                write!(
319                    f,
320                    "AIR parameter '{}' exceeds maximum: max={}, actual={}",
321                    parameter, max, actual
322                )
323            }
324            AirError::InvalidInput { reason } => {
325                write!(f, "Invalid input for trace generation: {}", reason)
326            }
327            AirError::TraceMismatch {
328                expected_width,
329                actual_width,
330            } => {
331                write!(
332                    f,
333                    "Trace width mismatch: expected {}, got {}",
334                    expected_width, actual_width
335                )
336            }
337            AirError::InvalidWitness { constraint } => {
338                write!(
339                    f,
340                    "Invalid witness: constraint '{}' not satisfied",
341                    constraint
342                )
343            }
344            AirError::InternalError { reason } => {
345                write!(f, "Internal AIR error: {}", reason)
346            }
347            AirError::NotSupported { reason } => {
348                write!(f, "Not supported: {}", reason)
349            }
350            AirError::MissingFriCommitPhaseOpenings => {
351                write!(f, "FRI commit-phase openings missing for query index")
352            }
353            AirError::FriRoundCountMismatch => {
354                write!(
355                    f,
356                    "FRI commit-phase step count does not match number of rounds"
357                )
358            }
359        }
360    }
361}
362
363impl From<AirError> for lib_q_core::Error {
364    fn from(err: AirError) -> Self {
365        lib_q_core::Error::InternalError {
366            operation: "AIR operation".into(),
367            details: err.to_string(),
368        }
369    }
370}
371
372/// Trait for AIRs that can generate execution traces from inputs
373///
374/// This trait extends the basic AIR functionality with the ability to
375/// generate valid execution traces from given inputs. The trace can then
376/// be used with STARK proving to generate proofs.
377///
378/// # Type Parameters
379///
380/// - `F`: The field type for trace values
381/// - `I`: The input type for trace generation
382pub trait TraceGenerator<F: Field, I> {
383    /// Generate an execution trace from the given inputs
384    ///
385    /// # Arguments
386    ///
387    /// * `inputs` - The inputs to generate the trace from
388    ///
389    /// # Returns
390    ///
391    /// A `RowMajorMatrix<F>` containing the trace, or an error if trace
392    /// generation fails.
393    ///
394    /// # Errors
395    ///
396    /// Returns `AirError` if:
397    /// - Input dimensions are invalid
398    /// - Input values don't produce a valid trace
399    /// - Memory allocation fails
400    fn generate_trace(&self, inputs: &I) -> Result<RowMajorMatrix<F>, AirError>;
401
402    /// Get the public values from the given inputs
403    ///
404    /// Public values are the values that are shared between prover and verifier.
405    /// These are typically outputs or commitments that are part of the statement
406    /// being proven.
407    ///
408    /// # Arguments
409    ///
410    /// * `inputs` - The inputs to extract public values from
411    ///
412    /// # Returns
413    ///
414    /// A vector of public field elements
415    fn public_values(&self, inputs: &I) -> Vec<F> {
416        let _ = inputs;
417        Vec::new()
418    }
419}
420
421/// Helper function to validate trace dimensions
422///
423/// # Arguments
424///
425/// * `width` - Trace width (number of columns)
426/// * `height` - Trace height (number of rows)
427///
428/// # Returns
429///
430/// `Ok(())` if dimensions are valid, `Err(AirError)` otherwise
431pub fn validate_trace_dimensions(width: usize, height: usize) -> Result<(), AirError> {
432    if width == 0 {
433        return Err(AirError::InvalidDimensions {
434            reason: "Trace width must be greater than 0".into(),
435        });
436    }
437
438    if width > MAX_TRACE_WIDTH {
439        return Err(AirError::ExceedsMaxSize {
440            parameter: "width".into(),
441            max: MAX_TRACE_WIDTH,
442            actual: width,
443        });
444    }
445
446    if height == 0 {
447        return Err(AirError::InvalidDimensions {
448            reason: "Trace height must be greater than 0".into(),
449        });
450    }
451
452    if height > MAX_TRACE_HEIGHT {
453        return Err(AirError::ExceedsMaxSize {
454            parameter: "height".into(),
455            max: MAX_TRACE_HEIGHT,
456            actual: height,
457        });
458    }
459
460    if !height.is_power_of_two() {
461        return Err(AirError::InvalidDimensions {
462            reason: "Trace height must be a power of 2".into(),
463        });
464    }
465
466    Ok(())
467}
468
469/// Round up to the next power of 2
470///
471/// # Arguments
472///
473/// * `n` - The number to round up
474///
475/// # Returns
476///
477/// The smallest power of 2 that is >= n
478pub fn next_power_of_two(n: usize) -> usize {
479    if n == 0 {
480        return 1;
481    }
482    n.next_power_of_two()
483}
484
485/// Convert PoseidonField to any Field F that supports u32 conversion
486///
487/// Converts PoseidonField (`Complex<Mersenne31>`) to the target field type,
488/// preserving both real and imaginary parts via basis decomposition.
489///
490/// # Arguments
491///
492/// * `pf` - The PoseidonField (`Complex<Mersenne31>`) to convert
493///
494/// # Returns
495///
496/// The field element in type F
497pub fn poseidon_to_field<F: Field + BasedVectorSpace<Mersenne31>>(pf: &PoseidonField) -> F {
498    let coeffs: &[Mersenne31] = pf.as_basis_coefficients_slice();
499    F::from_basis_coefficients_fn(|i| {
500        if i < coeffs.len() {
501            coeffs[i]
502        } else {
503            <Mersenne31 as PrimeCharacteristicRing>::ZERO
504        }
505    })
506}
507
508/// Convert slice of PoseidonField to `Vec<F>`
509///
510/// # Arguments
511///
512/// * `slice` - Slice of PoseidonField elements
513///
514/// # Returns
515///
516/// Vector of field elements in type F
517pub fn poseidon_slice_to_field<F: Field + BasedVectorSpace<Mersenne31>>(
518    slice: &[PoseidonField],
519) -> Vec<F> {
520    slice.iter().map(poseidon_to_field).collect()
521}
522
523/// Convert PoseidonField hash output to bytes
524///
525/// Uses RawDataSerializable to convert `Complex<Mersenne31>` elements to bytes.
526/// Each Complex element produces 8 bytes (4 for real, 4 for imag).
527///
528/// # Arguments
529///
530/// * `hash` - Slice of PoseidonField elements (hash output)
531///
532/// # Returns
533///
534/// Vector of bytes representing the hash
535pub fn poseidon_field_to_bytes(hash: &[PoseidonField]) -> Vec<u8> {
536    use lib_q_stark_field::RawDataSerializable;
537    // Complex<Mersenne31> has NUM_BYTES = 8 (4 real + 4 imag)
538    hash.iter().flat_map(|f| (*f).into_bytes()).collect()
539}
540
541/// Serialize a Merkle root (single PoseidonField) to a fixed 32-byte array.
542///
543/// Uses RawDataSerializable: one Complex&lt;Mersenne31&gt; produces 8 bytes (4 real + 4 imag, LE).
544/// The result is zero-padded to 32 bytes for a fixed-size root representation.
545///
546/// # Arguments
547///
548/// * `root` - The Merkle root as a Poseidon field element
549///
550/// # Returns
551///
552/// 32-byte array suitable for `verify_membership` and related APIs
553#[must_use]
554pub fn merkle_root_to_bytes(root: &PoseidonField) -> [u8; 32] {
555    use lib_q_stark_field::RawDataSerializable;
556    let mut out = [0u8; 32];
557    let bytes: Vec<u8> = (*root).into_bytes().into_iter().collect();
558    let n = core::cmp::min(bytes.len(), 32);
559    out[..n].copy_from_slice(&bytes[..n]);
560    out
561}
562
563/// Deserialize a Merkle root from bytes back to a PoseidonField.
564///
565/// Expects at least 8 bytes: first 4 bytes (u32 LE) = real part, next 4 = imag part
566/// of Complex&lt;Mersenne31&gt;. Used by verifiers to reconstruct the expected public value.
567///
568/// # Arguments
569///
570/// * `bytes` - At least 8 bytes (extra bytes are ignored)
571///
572/// # Returns
573///
574/// The root as PoseidonField, or InvalidInput if bytes.len() &lt; 8
575pub fn merkle_root_from_bytes(bytes: &[u8]) -> Result<PoseidonField, AirError> {
576    use lib_q_stark_field::extension::Complex;
577    use lib_q_stark_field::integers::QuotientMap;
578    use lib_q_stark_mersenne31::Mersenne31;
579
580    if bytes.len() < 8 {
581        return Err(AirError::InvalidInput {
582            reason: alloc::format!(
583                "Merkle root bytes must have at least 8 bytes, got {}",
584                bytes.len()
585            ),
586        });
587    }
588    let mut real_bytes = [0u8; 4];
589    let mut imag_bytes = [0u8; 4];
590    real_bytes.copy_from_slice(&bytes[0..4]);
591    imag_bytes.copy_from_slice(&bytes[4..8]);
592    // Freeze-gate O6: canonical-checked decode (reject `int >= 2^31-1`) so the byte encoding
593    // of a public root is injective. A reducing `from_int` would alias `v` and `v+p`, letting a
594    // caller key a nullifier/double-spend set on a non-canonical byte string that decodes to the
595    // same field element. Mirrors `membership::field_from_canonical_le` / `wide_digest_from_bytes`.
596    let real =
597        Mersenne31::from_canonical_checked(u32::from_le_bytes(real_bytes)).ok_or_else(|| {
598            AirError::InvalidInput {
599                reason: String::from("non-canonical real limb in merkle root (>= 2^31-1)"),
600            }
601        })?;
602    let imag =
603        Mersenne31::from_canonical_checked(u32::from_le_bytes(imag_bytes)).ok_or_else(|| {
604            AirError::InvalidInput {
605                reason: String::from("non-canonical imag limb in merkle root (>= 2^31-1)"),
606            }
607        })?;
608    Ok(Complex::new_complex(real, imag))
609}
610
611/// Compute one Poseidon permutation row: state in, intermediates, state out.
612///
613/// Uses `params.state_width` (e.g. 5 for Poseidon-128). Caller must pass at least
614/// `params.state_width` elements in `state`. Returns (final_state, intermediates).
615pub fn compute_poseidon_row(
616    state: &[PoseidonField],
617    params: &PoseidonParams,
618) -> (Vec<PoseidonField>, Vec<PoseidonField>) {
619    use lib_q_stark_field::extension::Complex;
620    use lib_q_stark_mersenne31::Mersenne31;
621
622    let n = params.state_width;
623    assert!(state.len() >= n, "state must have at least {} elements", n);
624    let zero = Complex::<Mersenne31>::new_complex(Mersenne31::ZERO, Mersenne31::ZERO);
625    let mut intermediates = Vec::new();
626    let mut round_idx = 0usize;
627    let mut s: Vec<PoseidonField> = state[0..n].to_vec();
628    let full_half = params.full_rounds / 2;
629
630    for _ in 0..full_half {
631        let after_arc: Vec<PoseidonField> = (0..n)
632            .map(|i| s[i] + params.round_constants[round_idx + i])
633            .collect();
634        round_idx += n;
635        intermediates.extend(after_arc.iter().cloned());
636        let after_sbox: Vec<PoseidonField> = (0..n).map(|i| sbox(after_arc[i])).collect();
637        intermediates.extend(after_sbox.iter().cloned());
638        let mut next_s = alloc::vec![zero; n];
639        for (i, next_s_i) in next_s.iter_mut().enumerate().take(n) {
640            for (j, &after_sbox_j) in after_sbox.iter().enumerate().take(n) {
641                *next_s_i += params.mds_matrix[i][j] * after_sbox_j;
642            }
643        }
644        intermediates.extend(next_s.iter().cloned());
645        s = next_s;
646    }
647    for _ in 0..params.partial_rounds {
648        let after_arc: Vec<PoseidonField> = (0..n)
649            .map(|i| s[i] + params.round_constants[round_idx + i])
650            .collect();
651        round_idx += n;
652        intermediates.extend(after_arc.iter().cloned());
653        let mut after_sbox = alloc::vec![zero; n];
654        after_sbox[0] = sbox(after_arc[0]);
655        after_sbox[1..n].copy_from_slice(&after_arc[1..n]);
656        intermediates.extend(after_sbox.iter().cloned());
657        let mut next_s = alloc::vec![zero; n];
658        for (i, next_s_i) in next_s.iter_mut().enumerate().take(n) {
659            for (j, &after_sbox_j) in after_sbox.iter().enumerate().take(n) {
660                *next_s_i += params.mds_matrix[i][j] * after_sbox_j;
661            }
662        }
663        intermediates.extend(next_s.iter().cloned());
664        s = next_s;
665    }
666    for _ in 0..full_half {
667        let after_arc: Vec<PoseidonField> = (0..n)
668            .map(|i| s[i] + params.round_constants[round_idx + i])
669            .collect();
670        round_idx += n;
671        intermediates.extend(after_arc.iter().cloned());
672        let after_sbox: Vec<PoseidonField> = (0..n).map(|i| sbox(after_arc[i])).collect();
673        intermediates.extend(after_sbox.iter().cloned());
674        let mut next_s = alloc::vec![zero; n];
675        for (i, next_s_i) in next_s.iter_mut().enumerate().take(n) {
676            for (j, &after_sbox_j) in after_sbox.iter().enumerate().take(n) {
677                *next_s_i += params.mds_matrix[i][j] * after_sbox_j;
678            }
679        }
680        intermediates.extend(next_s.iter().cloned());
681        s = next_s;
682    }
683    (s, intermediates)
684}
685
686/// Convert bytes to PoseidonField elements
687///
688/// This is a helper function to consistently convert byte slices to PoseidonField
689/// (`Complex<Mersenne31>`) elements. Each byte is converted to a field element.
690///
691/// # Arguments
692///
693/// * `bytes` - Slice of bytes to convert
694///
695/// # Returns
696///
697/// Vector of PoseidonField elements
698pub fn bytes_to_poseidon_field(bytes: &[u8]) -> Vec<PoseidonField> {
699    use lib_q_stark_field::extension::Complex;
700    use lib_q_stark_mersenne31::Mersenne31;
701    bytes
702        .iter()
703        .map(|b| Complex::<Mersenne31>::from(Mersenne31::new(*b as u32)))
704        .collect()
705}
706
707/// Decode the first 8 bytes of an Identity Token (IT) to the expected public value.
708/// The IT is the first 16 bytes of the encoding of the Poseidon hash output; the first 8 bytes
709/// encode one `Complex<Mersenne31>` (4 bytes real + 4 bytes imag, little-endian).
710pub fn it_bytes_to_public_value<F: Field + BasedVectorSpace<Mersenne31>>(it: &[u8; 16]) -> F {
711    use lib_q_stark_field::extension::Complex;
712    use lib_q_stark_mersenne31::Mersenne31;
713    let mut real_bytes = [0u8; 4];
714    let mut imag_bytes = [0u8; 4];
715    real_bytes.copy_from_slice(&it[0..4]);
716    imag_bytes.copy_from_slice(&it[4..8]);
717    let real = Mersenne31::new(u32::from_le_bytes(real_bytes));
718    let imag = Mersenne31::new(u32::from_le_bytes(imag_bytes));
719    let c = Complex::new_complex(real, imag);
720    poseidon_to_field::<F>(&c)
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726
727    #[test]
728    fn test_validate_trace_dimensions_valid() {
729        assert!(validate_trace_dimensions(8, 16).is_ok());
730        assert!(validate_trace_dimensions(1, 1).is_ok());
731        assert!(validate_trace_dimensions(100, 1024).is_ok());
732    }
733
734    #[test]
735    fn test_validate_trace_dimensions_zero_width() {
736        let result = validate_trace_dimensions(0, 16);
737        assert!(matches!(result, Err(AirError::InvalidDimensions { .. })));
738    }
739
740    #[test]
741    fn test_validate_trace_dimensions_zero_height() {
742        let result = validate_trace_dimensions(8, 0);
743        assert!(matches!(result, Err(AirError::InvalidDimensions { .. })));
744    }
745
746    #[test]
747    fn test_validate_trace_dimensions_not_power_of_two() {
748        let result = validate_trace_dimensions(8, 15);
749        assert!(matches!(result, Err(AirError::InvalidDimensions { .. })));
750    }
751
752    #[test]
753    fn test_next_power_of_two() {
754        assert_eq!(next_power_of_two(0), 1);
755        assert_eq!(next_power_of_two(1), 1);
756        assert_eq!(next_power_of_two(2), 2);
757        assert_eq!(next_power_of_two(3), 4);
758        assert_eq!(next_power_of_two(5), 8);
759        assert_eq!(next_power_of_two(16), 16);
760    }
761
762    #[test]
763    fn test_air_error_display() {
764        let err = AirError::InvalidDimensions {
765            reason: "test".into(),
766        };
767        assert!(err.to_string().contains("Invalid AIR dimensions"));
768
769        let err = AirError::ExceedsMaxSize {
770            parameter: "width".into(),
771            max: 100,
772            actual: 200,
773        };
774        assert!(err.to_string().contains("exceeds maximum"));
775    }
776}