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