1use std::collections::BTreeSet;
23
24use exo_core::{
25 hash::{merkle_proof, merkle_root, verify_merkle_proof},
26 types::Hash256,
27};
28use serde::{Deserialize, Serialize};
29
30use crate::error::{ProofError, Result};
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct StarkConfig {
39 pub field_size: u64,
41 pub expansion_factor: usize,
43 pub num_queries: usize,
45}
46
47impl StarkConfig {
48 #[must_use]
50 pub fn default_config() -> Self {
51 Self {
52 field_size: (1u64 << 31) - 1, expansion_factor: 4,
54 num_queries: 8,
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct StarkConstraint {
69 pub name: String,
71 pub column_indices: Vec<usize>,
73 pub coefficients: Vec<(u64, u64)>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct FriProof {
86 pub layer_commitments: Vec<Hash256>,
88 pub query_values: Vec<Vec<u64>>,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct TraceQueryProof {
95 pub index: usize,
97 pub row: Vec<u64>,
99 pub authentication_path: Vec<Hash256>,
101 pub next_row: Vec<u64>,
103 pub next_authentication_path: Vec<Hash256>,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct StarkProof {
114 pub trace_commitment: Hash256,
116 pub constraint_commitment: Hash256,
118 pub query_indices: Vec<usize>,
120 pub fri_proof: FriProof,
122 pub config: StarkConfig,
124 pub trace_length: usize,
126 pub public_input_hash: Hash256,
128 pub constraints: Vec<StarkConstraint>,
130 pub public_input_authentication_path: Vec<Hash256>,
132 pub trace_query_proofs: Vec<TraceQueryProof>,
134 pub trace_rows: Vec<Vec<u64>>,
141}
142
143impl PartialEq for StarkConfig {
144 fn eq(&self, other: &Self) -> bool {
145 self.field_size == other.field_size
146 && self.expansion_factor == other.expansion_factor
147 && self.num_queries == other.num_queries
148 }
149}
150
151impl Eq for StarkConfig {}
152
153pub fn prove_stark(
164 trace: &[Vec<u64>],
165 constraints: &[StarkConstraint],
166 config: &StarkConfig,
167) -> Result<StarkProof> {
168 crate::guard_unaudited("stark::prove_stark")?;
169 if trace.is_empty() {
170 return Err(ProofError::ProofGenerationFailed("empty trace".to_string()));
171 }
172 if trace.len() < 2 {
173 return Err(ProofError::ProofGenerationFailed(
174 "trace must have at least 2 rows".to_string(),
175 ));
176 }
177 if constraints.is_empty() {
178 return Err(ProofError::ProofGenerationFailed(
179 "STARK proof requires at least one constraint".to_string(),
180 ));
181 }
182
183 let num_cols = trace[0].len();
184 for row in trace {
185 if row.len() != num_cols {
186 return Err(ProofError::ProofGenerationFailed(
187 "inconsistent column count".to_string(),
188 ));
189 }
190 }
191 for constraint in constraints {
192 validate_constraint_shape(constraint, num_cols)?;
193 }
194
195 for (row_idx, window) in trace.windows(2).enumerate() {
197 let current = &window[0];
198 let next = &window[1];
199
200 for constraint in constraints {
201 let val = evaluate_constraint(constraint, current, next, config.field_size)?;
202 if val != 0 {
203 return Err(ProofError::ProofGenerationFailed(format!(
204 "constraint '{}' not satisfied at row {row_idx}",
205 constraint.name
206 )));
207 }
208 }
209 }
210
211 let trace_leaves = trace_leaf_hashes(trace);
212
213 let trace_commitment = commit_trace_from_leaves(&trace_leaves);
215
216 let constraint_commitment = commit_constraints(trace, constraints, config.field_size)?;
218
219 let query_indices = derive_queries(
221 &trace_commitment,
222 &constraint_commitment,
223 config.num_queries,
224 trace.len() - 1,
225 )?;
226
227 let fri_proof = build_fri_proof(
229 trace,
230 &query_indices,
231 config,
232 &trace_commitment,
233 &constraint_commitment,
234 );
235 let public_input_authentication_path = merkle_proof(&trace_leaves, 0).map_err(|_| {
236 ProofError::ProofGenerationFailed(
237 "failed to build public input authentication path".to_string(),
238 )
239 })?;
240 let trace_query_proofs = build_trace_query_proofs(trace, &trace_leaves, &query_indices)?;
241
242 Ok(StarkProof {
243 trace_commitment,
244 constraint_commitment,
245 query_indices,
246 fri_proof,
247 config: config.clone(),
248 trace_length: trace.len(),
249 public_input_hash: hash_row(&trace[0]),
250 constraints: constraints.to_vec(),
251 public_input_authentication_path,
252 trace_query_proofs,
253 trace_rows: trace.to_vec(),
254 })
255}
256
257pub fn verify_stark(_proof: &StarkProof, _public_inputs: &[u64]) -> Result<bool> {
271 crate::guard_unaudited("stark::verify_stark")?;
272 Err(ProofError::InvalidProofFormat(
273 "trusted STARK constraints are required; use verify_stark_with_constraints".to_string(),
274 ))
275}
276
277pub fn verify_stark_with_constraints(
283 proof: &StarkProof,
284 public_inputs: &[u64],
285 constraints: &[StarkConstraint],
286) -> Result<bool> {
287 crate::guard_unaudited("stark::verify_stark")?;
288 if proof.trace_length < 2
289 || constraints.is_empty()
290 || proof.constraints.is_empty()
291 || proof.constraints != constraints
292 || proof.trace_rows.len() != proof.trace_length
293 {
294 return Ok(false);
295 }
296
297 if validate_trace_shape(&proof.trace_rows).is_err() {
298 return Ok(false);
299 }
300
301 let trace_leaves = trace_leaf_hashes(&proof.trace_rows);
302 if commit_trace_from_leaves(&trace_leaves) != proof.trace_commitment {
303 return Ok(false);
304 }
305
306 let expected_constraint_commitment =
307 match commit_constraints(&proof.trace_rows, constraints, proof.config.field_size) {
308 Ok(commitment) => commitment,
309 Err(_) => return Ok(false),
310 };
311 if expected_constraint_commitment != proof.constraint_commitment {
312 return Ok(false);
313 }
314
315 for window in proof.trace_rows.windows(2) {
316 for constraint in constraints {
317 let constraint_value = match evaluate_constraint(
318 constraint,
319 &window[0],
320 &window[1],
321 proof.config.field_size,
322 ) {
323 Ok(value) => value,
324 Err(_) => return Ok(false),
325 };
326 if constraint_value != 0 {
327 return Ok(false);
328 }
329 }
330 }
331
332 let expected_queries = match derive_queries(
334 &proof.trace_commitment,
335 &proof.constraint_commitment,
336 proof.config.num_queries,
337 proof.trace_length - 1,
338 ) {
339 Ok(q) => q,
340 Err(_) => return Ok(false),
341 };
342
343 if expected_queries != proof.query_indices {
344 return Ok(false);
345 }
346
347 if proof.fri_proof.layer_commitments.is_empty() {
349 return Ok(false);
350 }
351
352 let Some(first_row) = proof.trace_rows.first() else {
354 return Ok(false);
355 };
356 if first_row.as_slice() != public_inputs {
357 return Ok(false);
358 }
359 let public_hash = hash_row(public_inputs);
360 if public_hash != proof.public_input_hash {
361 return Ok(false);
362 }
363 if !verify_merkle_proof(
364 &proof.trace_commitment,
365 &proof.public_input_hash,
366 &proof.public_input_authentication_path,
367 0,
368 ) {
369 return Ok(false);
370 }
371
372 if proof.trace_query_proofs.len() != proof.query_indices.len()
375 || proof.fri_proof.query_values.len() != proof.query_indices.len()
376 {
377 return Ok(false);
378 }
379
380 for ((expected_index, query_proof), query_values) in proof
381 .query_indices
382 .iter()
383 .zip(&proof.trace_query_proofs)
384 .zip(&proof.fri_proof.query_values)
385 {
386 if query_proof.index != *expected_index || query_values != &query_proof.row {
387 return Ok(false);
388 }
389
390 let Some(committed_row) = proof.trace_rows.get(query_proof.index) else {
391 return Ok(false);
392 };
393 if committed_row != &query_proof.row {
394 return Ok(false);
395 }
396
397 let Some(next_index) = query_proof.index.checked_add(1) else {
398 return Ok(false);
399 };
400 if next_index >= proof.trace_length {
401 return Ok(false);
402 }
403 let Some(committed_next_row) = proof.trace_rows.get(next_index) else {
404 return Ok(false);
405 };
406 if committed_next_row != &query_proof.next_row {
407 return Ok(false);
408 }
409
410 let leaf = hash_row(&query_proof.row);
411 if !verify_merkle_proof(
412 &proof.trace_commitment,
413 &leaf,
414 &query_proof.authentication_path,
415 query_proof.index,
416 ) {
417 return Ok(false);
418 }
419
420 let next_leaf = hash_row(&query_proof.next_row);
421 if !verify_merkle_proof(
422 &proof.trace_commitment,
423 &next_leaf,
424 &query_proof.next_authentication_path,
425 next_index,
426 ) {
427 return Ok(false);
428 }
429
430 for constraint in constraints {
431 let constraint_value = match evaluate_constraint(
432 constraint,
433 &query_proof.row,
434 &query_proof.next_row,
435 proof.config.field_size,
436 ) {
437 Ok(value) => value,
438 Err(_) => return Ok(false),
439 };
440 if constraint_value != 0 {
441 return Ok(false);
442 }
443 }
444 }
445
446 let expected_fri_base =
449 compute_fri_base_commitment(&proof.trace_commitment, &proof.constraint_commitment);
450
451 if proof.fri_proof.layer_commitments[0] != expected_fri_base {
452 return Ok(false);
453 }
454
455 for window in proof.fri_proof.layer_commitments.windows(2) {
457 let mut h = blake3::Hasher::new();
458 h.update(b"stark:fri:fold:");
459 h.update(window[0].as_bytes());
460 let expected_next = Hash256::from_bytes(*h.finalize().as_bytes());
461 if window[1] != expected_next {
462 return Ok(false);
463 }
464 }
465
466 Ok(true)
467}
468
469fn evaluate_constraint(
474 constraint: &StarkConstraint,
475 current: &[u64],
476 next: &[u64],
477 field_size: u64,
478) -> Result<u64> {
479 if field_size == 0 {
480 return Err(ProofError::ProofGenerationFailed(
481 "field_size must be non-zero".to_string(),
482 ));
483 }
484 if current.len() != next.len() {
485 return Err(ProofError::ConstraintError(format!(
486 "constraint '{}' cannot evaluate rows with mismatched widths: current {} != next {}",
487 constraint.name,
488 current.len(),
489 next.len()
490 )));
491 }
492 validate_constraint_shape(constraint, current.len())?;
493
494 let modulus = u128::from(field_size);
495 let mut sum: u128 = 0;
496 for (i, &col_idx) in constraint.column_indices.iter().enumerate() {
497 let (curr_coeff, next_coeff) = constraint.coefficients[i];
498 let curr_val = current[col_idx];
499 let next_val = next[col_idx];
500 let curr_term = (u128::from(curr_coeff) * u128::from(curr_val)) % modulus;
501 let next_term = (u128::from(next_coeff) * u128::from(next_val)) % modulus;
502 sum = (sum + curr_term + next_term) % modulus;
503 }
504 u64::try_from(sum).map_err(|_| {
505 ProofError::ProofGenerationFailed("constraint evaluation overflowed u64".to_string())
506 })
507}
508
509fn validate_constraint_shape(constraint: &StarkConstraint, trace_width: usize) -> Result<()> {
510 if constraint.column_indices.is_empty() {
511 return Err(ProofError::ConstraintError(format!(
512 "constraint '{}' must reference at least one trace column",
513 constraint.name
514 )));
515 }
516 if constraint.column_indices.len() != constraint.coefficients.len() {
517 return Err(ProofError::ConstraintError(format!(
518 "constraint '{}' column_indices length {} must match coefficients length {}",
519 constraint.name,
520 constraint.column_indices.len(),
521 constraint.coefficients.len()
522 )));
523 }
524 for &col_idx in &constraint.column_indices {
525 if col_idx >= trace_width {
526 return Err(ProofError::ConstraintError(format!(
527 "constraint '{}' column index {col_idx} is outside trace width {trace_width}",
528 constraint.name
529 )));
530 }
531 }
532 Ok(())
533}
534
535fn hash_row(row: &[u64]) -> Hash256 {
536 let mut hasher = blake3::Hasher::new();
537 hasher.update(b"stark:row:");
538 for &val in row {
539 hasher.update(&val.to_le_bytes());
540 }
541 Hash256::from_bytes(*hasher.finalize().as_bytes())
542}
543
544fn trace_leaf_hashes(trace: &[Vec<u64>]) -> Vec<Hash256> {
545 trace.iter().map(|row| hash_row(row)).collect()
546}
547
548fn commit_trace_from_leaves(leaves: &[Hash256]) -> Hash256 {
549 merkle_root(leaves)
550}
551
552fn validate_trace_shape(trace: &[Vec<u64>]) -> Result<()> {
553 let Some(first_row) = trace.first() else {
554 return Err(ProofError::ProofGenerationFailed("empty trace".to_string()));
555 };
556 let trace_width = first_row.len();
557 if trace_width == 0 {
558 return Err(ProofError::ProofGenerationFailed(
559 "trace rows must contain at least one column".to_string(),
560 ));
561 }
562 for (row_idx, row) in trace.iter().enumerate() {
563 if row.len() != trace_width {
564 return Err(ProofError::ProofGenerationFailed(format!(
565 "trace row {row_idx} has width {}, expected {trace_width}",
566 row.len()
567 )));
568 }
569 }
570 Ok(())
571}
572
573fn commit_constraints(
574 trace: &[Vec<u64>],
575 constraints: &[StarkConstraint],
576 field_size: u64,
577) -> Result<Hash256> {
578 let mut hasher = blake3::Hasher::new();
579 hasher.update(b"stark:constraints:");
580 for window in trace.windows(2) {
581 for constraint in constraints {
582 let val = evaluate_constraint(constraint, &window[0], &window[1], field_size)?;
583 hasher.update(&val.to_le_bytes());
584 }
585 }
586 Ok(Hash256::from_bytes(*hasher.finalize().as_bytes()))
587}
588
589fn derive_queries(
590 trace_commitment: &Hash256,
591 constraint_commitment: &Hash256,
592 num_queries: usize,
593 trace_len: usize,
594) -> Result<Vec<usize>> {
595 if num_queries == 0 {
596 return Ok(Vec::new());
597 }
598 if num_queries > trace_len {
599 return Err(ProofError::ProofGenerationFailed(format!(
600 "query budget {num_queries} exceeds available transition rows {trace_len}"
601 )));
602 }
603 let trace_len_u64 = u64::try_from(trace_len).map_err(|_| {
604 ProofError::ProofGenerationFailed(format!("trace length {trace_len} overflows u64"))
605 })?;
606 let mut seen = BTreeSet::new();
607 let mut indices = Vec::with_capacity(num_queries);
608
609 for i in 0..num_queries {
610 let query_round = u64::try_from(i).map_err(|_| {
611 ProofError::ProofGenerationFailed(format!("query round {i} overflows u64"))
612 })?;
613 let mut attempt = 0u64;
614
615 loop {
616 let mut hasher = blake3::Hasher::new();
617 hasher.update(b"stark:query_index:");
618 hasher.update(trace_commitment.as_bytes());
619 hasher.update(constraint_commitment.as_bytes());
620 hasher.update(&query_round.to_le_bytes());
621 hasher.update(&attempt.to_le_bytes());
622 let seed = hasher.finalize();
623 let mut idx_bytes = [0u8; 8];
624 idx_bytes.copy_from_slice(&seed.as_bytes()[..8]);
625
626 let idx_u64 = u64::from_le_bytes(idx_bytes) % trace_len_u64;
627 let idx = usize::try_from(idx_u64).map_err(|_| {
628 ProofError::ProofGenerationFailed(format!("query index {idx_u64} overflows usize"))
629 })?;
630
631 if seen.insert(idx) {
632 indices.push(idx);
633 break;
634 }
635
636 attempt = attempt.checked_add(1).ok_or_else(|| {
637 ProofError::ProofGenerationFailed(format!(
638 "query index derivation exhausted attempts for round {i}"
639 ))
640 })?;
641 }
642 }
643
644 Ok(indices)
645}
646
647fn compute_fri_base_commitment(
648 trace_commitment: &Hash256,
649 constraint_commitment: &Hash256,
650) -> Hash256 {
651 let mut hasher = blake3::Hasher::new();
652 hasher.update(b"stark:fri:base:");
653 hasher.update(trace_commitment.as_bytes());
654 hasher.update(constraint_commitment.as_bytes());
655 Hash256::from_bytes(*hasher.finalize().as_bytes())
656}
657
658fn build_fri_proof(
659 trace: &[Vec<u64>],
660 query_indices: &[usize],
661 config: &StarkConfig,
662 trace_commitment: &Hash256,
663 constraint_commitment: &Hash256,
664) -> FriProof {
665 let base = compute_fri_base_commitment(trace_commitment, constraint_commitment);
666
667 let num_layers = config.expansion_factor;
668 let mut layer_commitments = Vec::with_capacity(num_layers);
669 let mut current = base;
670 layer_commitments.push(current);
671
672 for _ in 1..num_layers {
673 let mut h = blake3::Hasher::new();
674 h.update(b"stark:fri:fold:");
675 h.update(current.as_bytes());
676 current = Hash256::from_bytes(*h.finalize().as_bytes());
677 layer_commitments.push(current);
678 }
679
680 let query_values: Vec<Vec<u64>> = query_indices
682 .iter()
683 .map(|&idx| {
684 if idx < trace.len() {
685 trace[idx].clone()
686 } else {
687 Vec::new()
688 }
689 })
690 .collect();
691
692 FriProof {
693 layer_commitments,
694 query_values,
695 }
696}
697
698fn build_trace_query_proofs(
699 trace: &[Vec<u64>],
700 trace_leaves: &[Hash256],
701 query_indices: &[usize],
702) -> Result<Vec<TraceQueryProof>> {
703 query_indices
704 .iter()
705 .map(|&idx| {
706 let Some(row) = trace.get(idx) else {
707 return Err(ProofError::ProofGenerationFailed(format!(
708 "query index {idx} is outside trace length {}",
709 trace.len()
710 )));
711 };
712 let Some(next_idx) = idx.checked_add(1) else {
713 return Err(ProofError::ProofGenerationFailed(format!(
714 "query index {idx} cannot address a successor row"
715 )));
716 };
717 let Some(next_row) = trace.get(next_idx) else {
718 return Err(ProofError::ProofGenerationFailed(format!(
719 "query index {idx} has no successor row in trace length {}",
720 trace.len()
721 )));
722 };
723 let authentication_path = merkle_proof(trace_leaves, idx).map_err(|_| {
724 ProofError::ProofGenerationFailed(format!(
725 "failed to build trace authentication path for query index {idx}"
726 ))
727 })?;
728 let next_authentication_path = merkle_proof(trace_leaves, next_idx).map_err(|_| {
729 ProofError::ProofGenerationFailed(format!(
730 "failed to build trace authentication path for successor query index {next_idx}"
731 ))
732 })?;
733 Ok(TraceQueryProof {
734 index: idx,
735 row: row.clone(),
736 authentication_path,
737 next_row: next_row.clone(),
738 next_authentication_path,
739 })
740 })
741 .collect()
742}
743
744#[cfg(all(test, feature = "unaudited-pedagogical-proofs"))]
749mod tests {
750 use super::*;
751
752 fn make_fibonacci_trace(n: usize, field_size: u64) -> Vec<Vec<u64>> {
753 let mut trace = Vec::with_capacity(n);
754 trace.push(vec![0, 1]);
755 for i in 1..n {
756 let prev = &trace[i - 1];
757 let next_val = (prev[0] + prev[1]) % field_size;
758 trace.push(vec![prev[1], next_val]);
759 }
760 trace
761 }
762
763 fn fib_constraint() -> StarkConstraint {
764 let field_size = (1u64 << 31) - 1;
770 StarkConstraint {
771 name: "fibonacci".to_string(),
772 column_indices: vec![0, 1],
773 coefficients: vec![(1, 0), (1, field_size - 1)],
774 }
775 }
776
777 fn equality_constraint() -> StarkConstraint {
778 let field_size = (1u64 << 31) - 1;
779 StarkConstraint {
780 name: "same-value".to_string(),
781 column_indices: vec![0],
782 coefficients: vec![(1, field_size - 1)],
783 }
784 }
785
786 fn vacuous_constraint() -> StarkConstraint {
787 StarkConstraint {
788 name: "vacuous".to_string(),
789 column_indices: vec![0],
790 coefficients: vec![(0, 0)],
791 }
792 }
793
794 #[test]
795 fn prove_and_verify_fibonacci() {
796 let config = StarkConfig::default_config();
797 let trace = make_fibonacci_trace(16, config.field_size);
798 let constraints = vec![fib_constraint()];
799
800 let proof = prove_stark(&trace, &constraints, &config).unwrap();
801 let public_inputs = &trace[0];
802 assert!(verify_stark_with_constraints(&proof, public_inputs, &constraints).unwrap());
803 }
804
805 #[test]
806 fn verify_stark_rejects_embedded_constraints_even_for_valid_proof() {
807 let config = StarkConfig::default_config();
808 let trace = make_fibonacci_trace(16, config.field_size);
809 let constraints = vec![fib_constraint()];
810
811 let proof = prove_stark(&trace, &constraints, &config).unwrap();
812 let err = verify_stark(&proof, &trace[0]).unwrap_err();
813
814 assert!(matches!(
815 err,
816 ProofError::InvalidProofFormat(message)
817 if message.contains("trusted STARK constraints are required")
818 ));
819 }
820
821 #[test]
822 fn verify_stark_source_does_not_trust_proof_embedded_constraints() {
823 let source = include_str!("stark.rs");
824 let verify_body = source
825 .split("pub fn verify_stark(")
826 .nth(1)
827 .expect("verify_stark source exists")
828 .split("/// Verify a STARK proof for caller-supplied public constraints.")
829 .next()
830 .expect("verify_stark source ends before trusted verifier");
831
832 assert!(
833 !verify_body.contains("&proof.constraints"),
834 "direct STARK verification must not promote prover-embedded constraints to trusted verifier constraints"
835 );
836 assert!(
837 verify_body.contains("trusted STARK constraints are required"),
838 "direct STARK verification must fail closed toward the trusted-constraints API"
839 );
840 }
841
842 #[test]
843 fn verify_stark_rejects_vacuous_prover_embedded_constraints() {
844 let config = StarkConfig::default_config();
845 let trace: Vec<Vec<u64>> = (0..16usize)
846 .map(|row| vec![u64::try_from(row).expect("test row index fits u64"), 999])
847 .collect();
848 let attacker_constraints = vec![vacuous_constraint()];
849 let proof = prove_stark(&trace, &attacker_constraints, &config).unwrap();
850
851 let err = verify_stark(&proof, &trace[0]).unwrap_err();
852
853 assert!(matches!(
854 err,
855 ProofError::InvalidProofFormat(message)
856 if message.contains("trusted STARK constraints are required")
857 ));
858 }
859
860 #[test]
861 fn empty_trace_rejected() {
862 let config = StarkConfig::default_config();
863 let err = prove_stark(&[], &[], &config).unwrap_err();
864 assert!(matches!(err, ProofError::ProofGenerationFailed(_)));
865 }
866
867 #[test]
868 fn single_row_trace_rejected() {
869 let config = StarkConfig::default_config();
870 let constraints = vec![equality_constraint()];
871 let err = prove_stark(&[vec![1, 2]], &constraints, &config).unwrap_err();
872 assert!(matches!(err, ProofError::ProofGenerationFailed(_)));
873 }
874
875 #[test]
876 fn inconsistent_columns_rejected() {
877 let config = StarkConfig::default_config();
878 let trace = vec![vec![1, 2], vec![3]];
879 let constraints = vec![equality_constraint()];
880 let err = prove_stark(&trace, &constraints, &config).unwrap_err();
881 assert!(matches!(err, ProofError::ProofGenerationFailed(_)));
882 }
883
884 #[test]
885 fn unsatisfied_constraint_rejected() {
886 let config = StarkConfig::default_config();
887 let trace = vec![vec![0, 1], vec![1, 999]]; let constraints = vec![fib_constraint()];
889 let err = prove_stark(&trace, &constraints, &config).unwrap_err();
890 assert!(matches!(err, ProofError::ProofGenerationFailed(_)));
891 }
892
893 #[test]
894 fn zero_field_size_rejected_without_panic() {
895 let config = StarkConfig {
896 field_size: 0,
897 expansion_factor: 4,
898 num_queries: 2,
899 };
900 let trace = vec![vec![1], vec![1]];
901 let constraints = vec![equality_constraint()];
902
903 let err = prove_stark(&trace, &constraints, &config).unwrap_err();
904
905 assert!(matches!(err, ProofError::ProofGenerationFailed(_)));
906 }
907
908 #[test]
909 fn large_constraint_terms_do_not_overflow_before_modular_reduction() {
910 let config = StarkConfig {
911 field_size: u64::MAX - 58,
912 expansion_factor: 4,
913 num_queries: 2,
914 };
915 let trace = vec![vec![1, 1], vec![1, 1]];
916 let constraint = StarkConstraint {
917 name: "large-terms".to_string(),
918 column_indices: vec![0, 1],
919 coefficients: vec![(u64::MAX, 0), (u64::MAX, 0)],
920 };
921
922 let result = prove_stark(&trace, &[constraint], &config);
923
924 assert!(matches!(result, Err(ProofError::ProofGenerationFailed(_))));
925 }
926
927 #[test]
928 fn proof_deterministic() {
929 let config = StarkConfig::default_config();
930 let trace = make_fibonacci_trace(16, config.field_size);
931 let constraints = vec![fib_constraint()];
932
933 let p1 = prove_stark(&trace, &constraints, &config).unwrap();
934 let p2 = prove_stark(&trace, &constraints, &config).unwrap();
935 assert_eq!(p1, p2);
936 }
937
938 #[test]
939 fn different_traces_different_proofs() {
940 let config = StarkConfig::default_config();
941 let t1 = make_fibonacci_trace(16, config.field_size);
942 let t2 = {
943 let mut t = vec![vec![1, 1]];
944 for i in 1..16 {
945 let prev = &t[i - 1];
946 let next_val = (prev[0] + prev[1]) % config.field_size;
947 t.push(vec![prev[1], next_val]);
948 }
949 t
950 };
951 let constraints = vec![fib_constraint()];
952
953 let p1 = prove_stark(&t1, &constraints, &config).unwrap();
954 let p2 = prove_stark(&t2, &constraints, &config).unwrap();
955 assert_ne!(p1.trace_commitment, p2.trace_commitment);
956 }
957
958 #[test]
959 fn verify_rejects_wrong_public_inputs() {
960 let config = StarkConfig::default_config();
961 let trace = make_fibonacci_trace(16, config.field_size);
962 let constraints = vec![fib_constraint()];
963
964 let proof = prove_stark(&trace, &constraints, &config).unwrap();
965 assert!(!verify_stark_with_constraints(&proof, &[99, 99], &constraints).unwrap());
967 }
968
969 #[test]
970 fn verify_rejects_public_inputs_not_bound_to_trace_commitment() {
971 let config = StarkConfig::default_config();
972 let trace = make_fibonacci_trace(16, config.field_size);
973 let constraints = vec![fib_constraint()];
974
975 let mut proof = prove_stark(&trace, &constraints, &config).unwrap();
976 let forged_public_inputs = vec![99, 99];
977 proof.public_input_hash = hash_row(&forged_public_inputs);
978
979 assert!(
980 !verify_stark_with_constraints(&proof, &forged_public_inputs, &constraints).unwrap()
981 );
982 }
983
984 #[test]
985 fn verify_rejects_tampered_query_values() {
986 let config = StarkConfig::default_config();
987 let trace = make_fibonacci_trace(16, config.field_size);
988 let constraints = vec![fib_constraint()];
989
990 let mut proof = prove_stark(&trace, &constraints, &config).unwrap();
991 proof.fri_proof.query_values = vec![vec![u64::MAX; 4]; config.num_queries];
992
993 assert!(!verify_stark_with_constraints(&proof, &trace[0], &constraints).unwrap());
994 }
995
996 #[test]
997 fn verify_with_constraints_rejects_authenticated_trace_that_violates_constraints() {
998 let config = StarkConfig {
999 num_queries: 5,
1000 ..StarkConfig::default_config()
1001 };
1002 let constraints = vec![equality_constraint()];
1003 let trace = vec![vec![0], vec![1], vec![2], vec![3], vec![4], vec![5]];
1004 let trace_leaves = trace_leaf_hashes(&trace);
1005 let trace_commitment = commit_trace_from_leaves(&trace_leaves);
1006 let constraint_commitment =
1007 commit_constraints(&trace, &constraints, config.field_size).unwrap();
1008 let query_indices = derive_queries(
1009 &trace_commitment,
1010 &constraint_commitment,
1011 config.num_queries,
1012 trace.len() - 1,
1013 )
1014 .unwrap();
1015 let fri_proof = build_fri_proof(
1016 &trace,
1017 &query_indices,
1018 &config,
1019 &trace_commitment,
1020 &constraint_commitment,
1021 );
1022 let public_input_authentication_path = merkle_proof(&trace_leaves, 0).unwrap();
1023 let trace_query_proofs =
1024 build_trace_query_proofs(&trace, &trace_leaves, &query_indices).unwrap();
1025 let proof = StarkProof {
1026 trace_commitment,
1027 constraint_commitment,
1028 query_indices,
1029 fri_proof,
1030 config,
1031 trace_length: trace.len(),
1032 public_input_hash: hash_row(&trace[0]),
1033 constraints: constraints.clone(),
1034 public_input_authentication_path,
1035 trace_query_proofs,
1036 trace_rows: trace.clone(),
1037 };
1038
1039 assert!(!verify_stark_with_constraints(&proof, &trace[0], &constraints).unwrap());
1040 }
1041
1042 #[test]
1043 fn verify_rejects_forged_trace_with_unqueried_constraint_violation() {
1044 let config = StarkConfig {
1045 num_queries: 4,
1046 ..StarkConfig::default_config()
1047 };
1048 let constraints = vec![equality_constraint()];
1049 let trace_len = 32usize;
1050
1051 let mut forged = None;
1052 for bad_row in 1..(trace_len - 1) {
1053 let mut trace = vec![vec![7u64]; trace_len];
1054 trace[bad_row] = vec![9u64];
1055 let trace_leaves = trace_leaf_hashes(&trace);
1056 let trace_commitment = commit_trace_from_leaves(&trace_leaves);
1057 let constraint_commitment =
1058 commit_constraints(&trace, &constraints, config.field_size).unwrap();
1059 let query_indices = derive_queries(
1060 &trace_commitment,
1061 &constraint_commitment,
1062 config.num_queries,
1063 trace.len() - 1,
1064 )
1065 .unwrap();
1066 let queried: BTreeSet<usize> = query_indices.iter().copied().collect();
1067 if !queried.contains(&(bad_row - 1)) && !queried.contains(&bad_row) {
1068 forged = Some((
1069 trace,
1070 trace_leaves,
1071 trace_commitment,
1072 constraint_commitment,
1073 query_indices,
1074 ));
1075 break;
1076 }
1077 }
1078
1079 let Some((trace, trace_leaves, trace_commitment, constraint_commitment, query_indices)) =
1080 forged
1081 else {
1082 panic!("test fixture must find an unqueried bad transition");
1083 };
1084 let fri_proof = build_fri_proof(
1085 &trace,
1086 &query_indices,
1087 &config,
1088 &trace_commitment,
1089 &constraint_commitment,
1090 );
1091 let public_input_authentication_path = merkle_proof(&trace_leaves, 0).unwrap();
1092 let trace_query_proofs =
1093 build_trace_query_proofs(&trace, &trace_leaves, &query_indices).unwrap();
1094 let proof = StarkProof {
1095 trace_commitment,
1096 constraint_commitment,
1097 query_indices,
1098 fri_proof,
1099 config,
1100 trace_length: trace.len(),
1101 public_input_hash: hash_row(&trace[0]),
1102 constraints: constraints.clone(),
1103 public_input_authentication_path,
1104 trace_query_proofs,
1105 trace_rows: trace.clone(),
1106 };
1107
1108 assert!(!verify_stark_with_constraints(&proof, &trace[0], &constraints).unwrap());
1109 }
1110
1111 #[test]
1112 fn derive_queries_produces_unique_indices_when_domain_permits() {
1113 let trace_commitment = Hash256::from_bytes([1u8; 32]);
1114 let constraint_commitment = Hash256::from_bytes([2u8; 32]);
1115 let queries = derive_queries(&trace_commitment, &constraint_commitment, 16, 64).unwrap();
1116 let unique: std::collections::BTreeSet<usize> = queries.iter().copied().collect();
1117
1118 assert_eq!(queries.len(), 16);
1119 assert_eq!(unique.len(), queries.len());
1120 }
1121
1122 #[test]
1123 fn derive_queries_rejects_query_budget_exceeding_transition_domain() {
1124 let trace_commitment = Hash256::from_bytes([1u8; 32]);
1125 let constraint_commitment = Hash256::from_bytes([2u8; 32]);
1126 let err = derive_queries(&trace_commitment, &constraint_commitment, 8, 7).unwrap_err();
1127
1128 assert!(
1129 err.to_string()
1130 .contains("exceeds available transition rows"),
1131 "query derivation must not silently reuse duplicate indices: {err}"
1132 );
1133 }
1134
1135 #[test]
1136 fn prove_rejects_query_budget_exceeding_transition_domain() {
1137 let config = StarkConfig::default_config();
1138 let trace = make_fibonacci_trace(8, config.field_size);
1139 let constraints = vec![fib_constraint()];
1140
1141 let err = prove_stark(&trace, &constraints, &config).unwrap_err();
1142
1143 assert!(
1144 err.to_string()
1145 .contains("exceeds available transition rows"),
1146 "proof generation must fail closed instead of proving duplicate query indices: {err}"
1147 );
1148 }
1149
1150 #[test]
1151 fn verify_rejects_tampered_proof() {
1152 let config = StarkConfig::default_config();
1153 let trace = make_fibonacci_trace(16, config.field_size);
1154 let constraints = vec![fib_constraint()];
1155
1156 let mut proof = prove_stark(&trace, &constraints, &config).unwrap();
1157 proof.trace_commitment = Hash256::ZERO;
1158 assert!(!verify_stark_with_constraints(&proof, &trace[0], &constraints).unwrap());
1159 }
1160
1161 #[test]
1162 fn prove_stark_rejects_empty_constraints() {
1163 let config = StarkConfig {
1164 num_queries: 2,
1165 ..StarkConfig::default_config()
1166 };
1167 let trace = vec![vec![1, 2], vec![3, 4], vec![5, 6]];
1168
1169 let err = prove_stark(&trace, &[], &config).unwrap_err();
1170
1171 assert!(
1172 err.to_string().contains("at least one constraint"),
1173 "empty STARK constraints must fail closed: {err}"
1174 );
1175 }
1176
1177 #[test]
1178 fn verify_rejects_manually_constructed_empty_constraint_proof() {
1179 let config = StarkConfig {
1180 num_queries: 2,
1181 ..StarkConfig::default_config()
1182 };
1183 let trace = vec![vec![1, 2], vec![3, 4], vec![5, 6]];
1184 let constraints = Vec::new();
1185 let trace_leaves = trace_leaf_hashes(&trace);
1186 let trace_commitment = commit_trace_from_leaves(&trace_leaves);
1187 let constraint_commitment =
1188 commit_constraints(&trace, &constraints, config.field_size).unwrap();
1189 let query_indices = derive_queries(
1190 &trace_commitment,
1191 &constraint_commitment,
1192 config.num_queries,
1193 trace.len() - 1,
1194 )
1195 .unwrap();
1196 let fri_proof = build_fri_proof(
1197 &trace,
1198 &query_indices,
1199 &config,
1200 &trace_commitment,
1201 &constraint_commitment,
1202 );
1203 let public_input_authentication_path = merkle_proof(&trace_leaves, 0).unwrap();
1204 let trace_query_proofs =
1205 build_trace_query_proofs(&trace, &trace_leaves, &query_indices).unwrap();
1206 let proof = StarkProof {
1207 trace_commitment,
1208 constraint_commitment,
1209 query_indices,
1210 fri_proof,
1211 config,
1212 trace_length: trace.len(),
1213 public_input_hash: hash_row(&trace[0]),
1214 constraints: constraints.clone(),
1215 public_input_authentication_path,
1216 trace_query_proofs,
1217 trace_rows: trace.clone(),
1218 };
1219
1220 assert!(!verify_stark_with_constraints(&proof, &trace[0], &constraints).unwrap());
1221 }
1222
1223 #[test]
1224 fn stark_config_eq() {
1225 let c1 = StarkConfig::default_config();
1226 let c2 = StarkConfig::default_config();
1227 assert_eq!(c1, c2);
1228 }
1229
1230 #[test]
1231 fn fri_proof_structure() {
1232 let config = StarkConfig::default_config();
1233 let trace = make_fibonacci_trace(16, config.field_size);
1234 let constraints = vec![fib_constraint()];
1235 let proof = prove_stark(&trace, &constraints, &config).unwrap();
1236
1237 assert_eq!(
1238 proof.fri_proof.layer_commitments.len(),
1239 config.expansion_factor
1240 );
1241 assert_eq!(proof.fri_proof.query_values.len(), config.num_queries);
1242 }
1243
1244 #[test]
1245 fn malformed_constraint_oob_column_is_rejected() {
1246 let config = StarkConfig::default_config();
1247 let trace = vec![vec![7u64], vec![11u64]];
1248 let constraint = StarkConstraint {
1249 name: "oob".to_string(),
1250 column_indices: vec![5],
1251 coefficients: vec![(1, 1)],
1252 };
1253 let constraints = vec![constraint];
1254 let err = prove_stark(&trace, &constraints, &config).unwrap_err();
1255 assert!(
1256 err.to_string().contains("outside trace width"),
1257 "out-of-bounds constraint columns must fail closed: {err}"
1258 );
1259 }
1260
1261 #[test]
1262 fn malformed_constraint_missing_coefficients_is_rejected() {
1263 let config = StarkConfig::default_config();
1264 let trace = vec![vec![0u64, 5u64], vec![0u64, 7u64]];
1265 let constraint = StarkConstraint {
1266 name: "partial".to_string(),
1267 column_indices: vec![0, 1],
1268 coefficients: vec![(1, 1)],
1269 };
1270 let constraints = vec![constraint];
1271 let err = prove_stark(&trace, &constraints, &config).unwrap_err();
1272 assert!(
1273 err.to_string().contains("must match coefficients"),
1274 "constraint columns without matching coefficients must fail closed: {err}"
1275 );
1276 }
1277}