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    SessionKeyDerivationAir,
177    SessionKeyInput,
178};
179pub use unlinkable_membership::{
180    CTX_ELEMS,
181    MEMBERSHIP_DOMAIN_STR,
182    MEMBERSHIP_NUM_PUBLIC,
183    MEMBERSHIP_ROW_WIDTH,
184    SECRET_T_ELEMS,
185    UnlinkableMembershipAir,
186    generate_membership_trace,
187    membership_domain,
188    membership_leaf,
189    membership_nullifier,
190    membership_public_values,
191};
192pub use wide_hash::{
193    WIDE_DIGEST_ELEMS,
194    WideDigest,
195    poseidon256_perm_truncated,
196    poseidon256_wide_hash,
197};
198pub use wide_merkle::{
199    NODE_NUM_PERMS,
200    NODE_ROW_WIDTH,
201    WideNodeHashAir,
202    generate_node_trace,
203    node_public_values,
204};
205pub use wide_merkle_path::{
206    PATH_NUM_PUBLIC,
207    PATH_ROW_WIDTH,
208    WideMerklePathAir,
209    generate_path_trace,
210    path_public_values,
211};
212pub use wide_sponge::{
213    constrain_wide_sponge,
214    generate_wide_sponge_cells,
215    wide_sponge_interm_cols,
216    wide_sponge_num_perms,
217};
218/// Trait for PCS commitments that are Poseidon Merkle roots. Used by the recursive verifier.
219/// The only implementation is when `recursive-proofs-experimental` is enabled (Hash in stark_verifier).
220pub trait PoseidonCommitmentRoot {
221    fn to_poseidon_root_bytes(&self) -> [u8; recursive_types::COMMITMENT_HASH_SIZE];
222}
223
224#[cfg(feature = "recursive-proofs-experimental")]
225pub use stark_verifier::{
226    MerklePathExtractable,
227    build_recursive_verification_input_from_proof,
228    build_recursive_verification_input_from_proof_with_poseidon,
229};
230pub use stark_verifier::{
231    RecursiveStarkVerificationInput,
232    StarkVerifierAir,
233    build_recursive_verification_input,
234};
235
236/// Maximum number of operations allowed in a single AIR instance
237/// to prevent memory exhaustion attacks.
238pub const MAX_OPERATIONS: usize = 1 << 20; // ~1 million operations
239
240/// Maximum trace width to prevent excessive memory allocation.
241/// Recursive StarkVerifierAir can exceed 65536; raised to 131072 for aggregation.
242pub const MAX_TRACE_WIDTH: usize = 1 << 17; // 131072 columns
243
244/// Maximum trace height (number of rows) to prevent memory exhaustion.
245pub const MAX_TRACE_HEIGHT: usize = 1 << 24; // ~16 million rows
246
247/// Error type for AIR operations
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub enum AirError {
250    /// AIR configuration has invalid dimensions
251    InvalidDimensions {
252        /// Description of the dimension error
253        reason: String,
254    },
255
256    /// AIR exceeds maximum allowed size
257    ExceedsMaxSize {
258        /// Name of the parameter that exceeded limits
259        parameter: String,
260        /// Maximum allowed value
261        max: usize,
262        /// Actual value provided
263        actual: usize,
264    },
265
266    /// Invalid input data for trace generation
267    InvalidInput {
268        /// Description of what was invalid
269        reason: String,
270    },
271
272    /// Trace dimensions don't match AIR requirements
273    TraceMismatch {
274        /// Expected width
275        expected_width: usize,
276        /// Actual width
277        actual_width: usize,
278    },
279
280    /// Witness values don't satisfy constraints
281    InvalidWitness {
282        /// Description of which constraint failed
283        constraint: String,
284    },
285
286    /// Internal error during AIR evaluation
287    InternalError {
288        /// Description of the error
289        reason: String,
290    },
291
292    /// Feature required but not enabled
293    NotSupported {
294        /// Description of what is not supported
295        reason: String,
296    },
297
298    /// FRI commit-phase openings missing for the query index
299    MissingFriCommitPhaseOpenings,
300
301    /// FRI commit-phase step count does not match number of rounds
302    FriRoundCountMismatch,
303}
304
305impl fmt::Display for AirError {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        match self {
308            AirError::InvalidDimensions { reason } => {
309                write!(f, "Invalid AIR dimensions: {}", reason)
310            }
311            AirError::ExceedsMaxSize {
312                parameter,
313                max,
314                actual,
315            } => {
316                write!(
317                    f,
318                    "AIR parameter '{}' exceeds maximum: max={}, actual={}",
319                    parameter, max, actual
320                )
321            }
322            AirError::InvalidInput { reason } => {
323                write!(f, "Invalid input for trace generation: {}", reason)
324            }
325            AirError::TraceMismatch {
326                expected_width,
327                actual_width,
328            } => {
329                write!(
330                    f,
331                    "Trace width mismatch: expected {}, got {}",
332                    expected_width, actual_width
333                )
334            }
335            AirError::InvalidWitness { constraint } => {
336                write!(
337                    f,
338                    "Invalid witness: constraint '{}' not satisfied",
339                    constraint
340                )
341            }
342            AirError::InternalError { reason } => {
343                write!(f, "Internal AIR error: {}", reason)
344            }
345            AirError::NotSupported { reason } => {
346                write!(f, "Not supported: {}", reason)
347            }
348            AirError::MissingFriCommitPhaseOpenings => {
349                write!(f, "FRI commit-phase openings missing for query index")
350            }
351            AirError::FriRoundCountMismatch => {
352                write!(
353                    f,
354                    "FRI commit-phase step count does not match number of rounds"
355                )
356            }
357        }
358    }
359}
360
361impl From<AirError> for lib_q_core::Error {
362    fn from(err: AirError) -> Self {
363        lib_q_core::Error::InternalError {
364            operation: "AIR operation".into(),
365            details: err.to_string(),
366        }
367    }
368}
369
370/// Trait for AIRs that can generate execution traces from inputs
371///
372/// This trait extends the basic AIR functionality with the ability to
373/// generate valid execution traces from given inputs. The trace can then
374/// be used with STARK proving to generate proofs.
375///
376/// # Type Parameters
377///
378/// - `F`: The field type for trace values
379/// - `I`: The input type for trace generation
380pub trait TraceGenerator<F: Field, I> {
381    /// Generate an execution trace from the given inputs
382    ///
383    /// # Arguments
384    ///
385    /// * `inputs` - The inputs to generate the trace from
386    ///
387    /// # Returns
388    ///
389    /// A `RowMajorMatrix<F>` containing the trace, or an error if trace
390    /// generation fails.
391    ///
392    /// # Errors
393    ///
394    /// Returns `AirError` if:
395    /// - Input dimensions are invalid
396    /// - Input values don't produce a valid trace
397    /// - Memory allocation fails
398    fn generate_trace(&self, inputs: &I) -> Result<RowMajorMatrix<F>, AirError>;
399
400    /// Get the public values from the given inputs
401    ///
402    /// Public values are the values that are shared between prover and verifier.
403    /// These are typically outputs or commitments that are part of the statement
404    /// being proven.
405    ///
406    /// # Arguments
407    ///
408    /// * `inputs` - The inputs to extract public values from
409    ///
410    /// # Returns
411    ///
412    /// A vector of public field elements
413    fn public_values(&self, inputs: &I) -> Vec<F> {
414        let _ = inputs;
415        Vec::new()
416    }
417}
418
419/// Helper function to validate trace dimensions
420///
421/// # Arguments
422///
423/// * `width` - Trace width (number of columns)
424/// * `height` - Trace height (number of rows)
425///
426/// # Returns
427///
428/// `Ok(())` if dimensions are valid, `Err(AirError)` otherwise
429pub fn validate_trace_dimensions(width: usize, height: usize) -> Result<(), AirError> {
430    if width == 0 {
431        return Err(AirError::InvalidDimensions {
432            reason: "Trace width must be greater than 0".into(),
433        });
434    }
435
436    if width > MAX_TRACE_WIDTH {
437        return Err(AirError::ExceedsMaxSize {
438            parameter: "width".into(),
439            max: MAX_TRACE_WIDTH,
440            actual: width,
441        });
442    }
443
444    if height == 0 {
445        return Err(AirError::InvalidDimensions {
446            reason: "Trace height must be greater than 0".into(),
447        });
448    }
449
450    if height > MAX_TRACE_HEIGHT {
451        return Err(AirError::ExceedsMaxSize {
452            parameter: "height".into(),
453            max: MAX_TRACE_HEIGHT,
454            actual: height,
455        });
456    }
457
458    if !height.is_power_of_two() {
459        return Err(AirError::InvalidDimensions {
460            reason: "Trace height must be a power of 2".into(),
461        });
462    }
463
464    Ok(())
465}
466
467/// Round up to the next power of 2
468///
469/// # Arguments
470///
471/// * `n` - The number to round up
472///
473/// # Returns
474///
475/// The smallest power of 2 that is >= n
476pub fn next_power_of_two(n: usize) -> usize {
477    if n == 0 {
478        return 1;
479    }
480    n.next_power_of_two()
481}
482
483/// Convert PoseidonField to any Field F that supports u32 conversion
484///
485/// Converts PoseidonField (`Complex<Mersenne31>`) to the target field type,
486/// preserving both real and imaginary parts via basis decomposition.
487///
488/// # Arguments
489///
490/// * `pf` - The PoseidonField (`Complex<Mersenne31>`) to convert
491///
492/// # Returns
493///
494/// The field element in type F
495pub fn poseidon_to_field<F: Field + BasedVectorSpace<Mersenne31>>(pf: &PoseidonField) -> F {
496    let coeffs: &[Mersenne31] = pf.as_basis_coefficients_slice();
497    F::from_basis_coefficients_fn(|i| {
498        if i < coeffs.len() {
499            coeffs[i]
500        } else {
501            <Mersenne31 as PrimeCharacteristicRing>::ZERO
502        }
503    })
504}
505
506/// Convert slice of PoseidonField to `Vec<F>`
507///
508/// # Arguments
509///
510/// * `slice` - Slice of PoseidonField elements
511///
512/// # Returns
513///
514/// Vector of field elements in type F
515pub fn poseidon_slice_to_field<F: Field + BasedVectorSpace<Mersenne31>>(
516    slice: &[PoseidonField],
517) -> Vec<F> {
518    slice.iter().map(poseidon_to_field).collect()
519}
520
521/// Convert PoseidonField hash output to bytes
522///
523/// Uses RawDataSerializable to convert `Complex<Mersenne31>` elements to bytes.
524/// Each Complex element produces 8 bytes (4 for real, 4 for imag).
525///
526/// # Arguments
527///
528/// * `hash` - Slice of PoseidonField elements (hash output)
529///
530/// # Returns
531///
532/// Vector of bytes representing the hash
533pub fn poseidon_field_to_bytes(hash: &[PoseidonField]) -> Vec<u8> {
534    use lib_q_stark_field::RawDataSerializable;
535    // Complex<Mersenne31> has NUM_BYTES = 8 (4 real + 4 imag)
536    hash.iter().flat_map(|f| (*f).into_bytes()).collect()
537}
538
539/// Serialize a Merkle root (single PoseidonField) to a fixed 32-byte array.
540///
541/// Uses RawDataSerializable: one Complex&lt;Mersenne31&gt; produces 8 bytes (4 real + 4 imag, LE).
542/// The result is zero-padded to 32 bytes for a fixed-size root representation.
543///
544/// # Arguments
545///
546/// * `root` - The Merkle root as a Poseidon field element
547///
548/// # Returns
549///
550/// 32-byte array suitable for `verify_membership` and related APIs
551#[must_use]
552pub fn merkle_root_to_bytes(root: &PoseidonField) -> [u8; 32] {
553    use lib_q_stark_field::RawDataSerializable;
554    let mut out = [0u8; 32];
555    let bytes: Vec<u8> = (*root).into_bytes().into_iter().collect();
556    let n = core::cmp::min(bytes.len(), 32);
557    out[..n].copy_from_slice(&bytes[..n]);
558    out
559}
560
561/// Deserialize a Merkle root from bytes back to a PoseidonField.
562///
563/// Expects at least 8 bytes: first 4 bytes (u32 LE) = real part, next 4 = imag part
564/// of Complex&lt;Mersenne31&gt;. Used by verifiers to reconstruct the expected public value.
565///
566/// # Arguments
567///
568/// * `bytes` - At least 8 bytes (extra bytes are ignored)
569///
570/// # Returns
571///
572/// The root as PoseidonField, or InvalidInput if bytes.len() &lt; 8
573pub fn merkle_root_from_bytes(bytes: &[u8]) -> Result<PoseidonField, AirError> {
574    use lib_q_stark_field::extension::Complex;
575    use lib_q_stark_field::integers::QuotientMap;
576    use lib_q_stark_mersenne31::Mersenne31;
577
578    if bytes.len() < 8 {
579        return Err(AirError::InvalidInput {
580            reason: alloc::format!(
581                "Merkle root bytes must have at least 8 bytes, got {}",
582                bytes.len()
583            ),
584        });
585    }
586    let mut real_bytes = [0u8; 4];
587    let mut imag_bytes = [0u8; 4];
588    real_bytes.copy_from_slice(&bytes[0..4]);
589    imag_bytes.copy_from_slice(&bytes[4..8]);
590    // Freeze-gate O6: canonical-checked decode (reject `int >= 2^31-1`) so the byte encoding
591    // of a public root is injective. A reducing `from_int` would alias `v` and `v+p`, letting a
592    // caller key a nullifier/double-spend set on a non-canonical byte string that decodes to the
593    // same field element. Mirrors `membership::field_from_canonical_le` / `wide_digest_from_bytes`.
594    let real =
595        Mersenne31::from_canonical_checked(u32::from_le_bytes(real_bytes)).ok_or_else(|| {
596            AirError::InvalidInput {
597                reason: String::from("non-canonical real limb in merkle root (>= 2^31-1)"),
598            }
599        })?;
600    let imag =
601        Mersenne31::from_canonical_checked(u32::from_le_bytes(imag_bytes)).ok_or_else(|| {
602            AirError::InvalidInput {
603                reason: String::from("non-canonical imag limb in merkle root (>= 2^31-1)"),
604            }
605        })?;
606    Ok(Complex::new_complex(real, imag))
607}
608
609/// Compute one Poseidon permutation row: state in, intermediates, state out.
610///
611/// Uses `params.state_width` (e.g. 5 for Poseidon-128). Caller must pass at least
612/// `params.state_width` elements in `state`. Returns (final_state, intermediates).
613pub fn compute_poseidon_row(
614    state: &[PoseidonField],
615    params: &PoseidonParams,
616) -> (Vec<PoseidonField>, Vec<PoseidonField>) {
617    use lib_q_stark_field::extension::Complex;
618    use lib_q_stark_mersenne31::Mersenne31;
619
620    let n = params.state_width;
621    assert!(state.len() >= n, "state must have at least {} elements", n);
622    let zero = Complex::<Mersenne31>::new_complex(Mersenne31::ZERO, Mersenne31::ZERO);
623    let mut intermediates = Vec::new();
624    let mut round_idx = 0usize;
625    let mut s: Vec<PoseidonField> = state[0..n].to_vec();
626    let full_half = params.full_rounds / 2;
627
628    for _ in 0..full_half {
629        let after_arc: Vec<PoseidonField> = (0..n)
630            .map(|i| s[i] + params.round_constants[round_idx + i])
631            .collect();
632        round_idx += n;
633        intermediates.extend(after_arc.iter().cloned());
634        let after_sbox: Vec<PoseidonField> = (0..n).map(|i| sbox(after_arc[i])).collect();
635        intermediates.extend(after_sbox.iter().cloned());
636        let mut next_s = alloc::vec![zero; n];
637        for (i, next_s_i) in next_s.iter_mut().enumerate().take(n) {
638            for (j, &after_sbox_j) in after_sbox.iter().enumerate().take(n) {
639                *next_s_i += params.mds_matrix[i][j] * after_sbox_j;
640            }
641        }
642        intermediates.extend(next_s.iter().cloned());
643        s = next_s;
644    }
645    for _ in 0..params.partial_rounds {
646        let after_arc: Vec<PoseidonField> = (0..n)
647            .map(|i| s[i] + params.round_constants[round_idx + i])
648            .collect();
649        round_idx += n;
650        intermediates.extend(after_arc.iter().cloned());
651        let mut after_sbox = alloc::vec![zero; n];
652        after_sbox[0] = sbox(after_arc[0]);
653        after_sbox[1..n].copy_from_slice(&after_arc[1..n]);
654        intermediates.extend(after_sbox.iter().cloned());
655        let mut next_s = alloc::vec![zero; n];
656        for (i, next_s_i) in next_s.iter_mut().enumerate().take(n) {
657            for (j, &after_sbox_j) in after_sbox.iter().enumerate().take(n) {
658                *next_s_i += params.mds_matrix[i][j] * after_sbox_j;
659            }
660        }
661        intermediates.extend(next_s.iter().cloned());
662        s = next_s;
663    }
664    for _ in 0..full_half {
665        let after_arc: Vec<PoseidonField> = (0..n)
666            .map(|i| s[i] + params.round_constants[round_idx + i])
667            .collect();
668        round_idx += n;
669        intermediates.extend(after_arc.iter().cloned());
670        let after_sbox: Vec<PoseidonField> = (0..n).map(|i| sbox(after_arc[i])).collect();
671        intermediates.extend(after_sbox.iter().cloned());
672        let mut next_s = alloc::vec![zero; n];
673        for (i, next_s_i) in next_s.iter_mut().enumerate().take(n) {
674            for (j, &after_sbox_j) in after_sbox.iter().enumerate().take(n) {
675                *next_s_i += params.mds_matrix[i][j] * after_sbox_j;
676            }
677        }
678        intermediates.extend(next_s.iter().cloned());
679        s = next_s;
680    }
681    (s, intermediates)
682}
683
684/// Convert bytes to PoseidonField elements
685///
686/// This is a helper function to consistently convert byte slices to PoseidonField
687/// (`Complex<Mersenne31>`) elements. Each byte is converted to a field element.
688///
689/// # Arguments
690///
691/// * `bytes` - Slice of bytes to convert
692///
693/// # Returns
694///
695/// Vector of PoseidonField elements
696pub fn bytes_to_poseidon_field(bytes: &[u8]) -> Vec<PoseidonField> {
697    use lib_q_stark_field::extension::Complex;
698    use lib_q_stark_mersenne31::Mersenne31;
699    bytes
700        .iter()
701        .map(|b| Complex::<Mersenne31>::from(Mersenne31::new(*b as u32)))
702        .collect()
703}
704
705/// Decode the first 8 bytes of an Identity Token (IT) to the expected public value.
706/// The IT is the first 16 bytes of the encoding of the Poseidon hash output; the first 8 bytes
707/// encode one `Complex<Mersenne31>` (4 bytes real + 4 bytes imag, little-endian).
708pub fn it_bytes_to_public_value<F: Field + BasedVectorSpace<Mersenne31>>(it: &[u8; 16]) -> F {
709    use lib_q_stark_field::extension::Complex;
710    use lib_q_stark_mersenne31::Mersenne31;
711    let mut real_bytes = [0u8; 4];
712    let mut imag_bytes = [0u8; 4];
713    real_bytes.copy_from_slice(&it[0..4]);
714    imag_bytes.copy_from_slice(&it[4..8]);
715    let real = Mersenne31::new(u32::from_le_bytes(real_bytes));
716    let imag = Mersenne31::new(u32::from_le_bytes(imag_bytes));
717    let c = Complex::new_complex(real, imag);
718    poseidon_to_field::<F>(&c)
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724
725    #[test]
726    fn test_validate_trace_dimensions_valid() {
727        assert!(validate_trace_dimensions(8, 16).is_ok());
728        assert!(validate_trace_dimensions(1, 1).is_ok());
729        assert!(validate_trace_dimensions(100, 1024).is_ok());
730    }
731
732    #[test]
733    fn test_validate_trace_dimensions_zero_width() {
734        let result = validate_trace_dimensions(0, 16);
735        assert!(matches!(result, Err(AirError::InvalidDimensions { .. })));
736    }
737
738    #[test]
739    fn test_validate_trace_dimensions_zero_height() {
740        let result = validate_trace_dimensions(8, 0);
741        assert!(matches!(result, Err(AirError::InvalidDimensions { .. })));
742    }
743
744    #[test]
745    fn test_validate_trace_dimensions_not_power_of_two() {
746        let result = validate_trace_dimensions(8, 15);
747        assert!(matches!(result, Err(AirError::InvalidDimensions { .. })));
748    }
749
750    #[test]
751    fn test_next_power_of_two() {
752        assert_eq!(next_power_of_two(0), 1);
753        assert_eq!(next_power_of_two(1), 1);
754        assert_eq!(next_power_of_two(2), 2);
755        assert_eq!(next_power_of_two(3), 4);
756        assert_eq!(next_power_of_two(5), 8);
757        assert_eq!(next_power_of_two(16), 16);
758    }
759
760    #[test]
761    fn test_air_error_display() {
762        let err = AirError::InvalidDimensions {
763            reason: "test".into(),
764        };
765        assert!(err.to_string().contains("Invalid AIR dimensions"));
766
767        let err = AirError::ExceedsMaxSize {
768            parameter: "width".into(),
769            max: 100,
770            actual: 200,
771        };
772        assert!(err.to_string().contains("exceeds maximum"));
773    }
774}