Repository navigation
Expand file tree
/
Copy pathverifier.rs
More file actions
1679 lines (1538 loc) · 73.7 KB
/
Copy pathverifier.rs
File metadata and controls
1679 lines (1538 loc) · 73.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
use super::{
config::BatchedMerkleTreeBackend,
domain::VerifierDomain,
grinding,
proof::stark::StarkProof,
traits::{AIR, TransitionEvaluationContext},
};
pub use crate::proof::view::PiDeserializer;
use crate::{
config::Commitment,
domain::new_verifier_domain,
lookup::{BusPublicInputs, LOGUP_CHALLENGE_ALPHA, LOGUP_NUM_CHALLENGES, compute_alpha_powers},
proof::stark::{ArchivedMultiProof, MultiProof},
proof::view::{
DeepPolynomialOpeningView, FriDecommitmentView, MultiProofView, PolynomialOpeningsView,
ProofViewSource, StarkProofView, StarkTableView,
},
table::Table,
};
use crypto::fiat_shamir::is_transcript::IsStarkTranscript;
use crypto::merkle_tree::proof::{verify_merkle_path, verify_merkle_path_from_leaf_hash};
#[cfg(not(feature = "test_fiat_shamir"))]
use log::error;
#[cfg(feature = "debug-checks")]
use log::info;
use math::{
fft::bit_reversing::reverse_index,
field::{
element::FieldElement,
traits::{IsFFTField, IsField, IsSubFieldOf},
},
traits::AsBytes,
};
use std::collections::HashMap;
use std::marker::PhantomData;
#[cfg(feature = "instruments")]
use std::time::Instant;
/// A default STARK verifier implementing `IsStarkVerifier`.
pub struct Verifier<
Field: IsSubFieldOf<FieldExtension> + IsFFTField + Send + Sync,
FieldExtension: Send + Sync + IsField,
PI,
> {
phantom: PhantomData<(Field, FieldExtension, PI)>,
}
impl<
Field: IsSubFieldOf<FieldExtension> + IsFFTField + Send + Sync,
FieldExtension: IsField + Send + Sync,
PI,
> IsStarkVerifier<Field, FieldExtension, PI> for Verifier<Field, FieldExtension, PI>
where
Field::BaseType: math::field::element::NativeArchived,
FieldExtension::BaseType: math::field::element::NativeArchived,
PI: rkyv::Archive + Clone,
<PI as rkyv::Archive>::Archived: rkyv::Deserialize<PI, PiDeserializer>,
{
}
/// A container holding the complete list of challenges sent to the prover along with the seed used
/// to validate the proof-of-work nonce.
pub struct Challenges<FieldExtension>
where
FieldExtension: Send + Sync + IsField,
{
/// The out-of-domain challenge.
pub z: FieldElement<FieldExtension>,
/// The composition polynomial coefficients corresponding to the boundary constraints terms.
pub boundary_coeffs: Vec<FieldElement<FieldExtension>>,
/// The composition polynomial coefficients corresponding to the transition constraints terms.
pub transition_coeffs: Vec<FieldElement<FieldExtension>>,
/// The deep composition polynomial coefficients corresponding to the trace polynomial terms.
pub trace_term_coeffs: Vec<Vec<FieldElement<FieldExtension>>>,
/// The deep composition polynomial coefficients corresponding to the composition polynomial parts terms.
pub gammas: Vec<FieldElement<FieldExtension>>,
/// The list of FRI commit phase folding challenges.
pub zetas: Vec<FieldElement<FieldExtension>>,
/// The list of FRI query phase index challenges.
pub iotas: Vec<usize>,
/// The challenges used to build the auxiliary trace.
pub rap_challenges: Vec<FieldElement<FieldExtension>>,
/// The seed used to verify the proof-of-work nonce.
pub grinding_seed: [u8; 32],
}
pub type DeepPolynomialEvaluations<F> = (Vec<FieldElement<F>>, Vec<FieldElement<F>>);
/// Deep-composition sums that are identical across all FRI queries of a
/// single proof (see `compute_query_invariant_deep_terms`).
pub struct QueryInvariantDeepTerms<FieldExtension>
where
FieldExtension: Send + Sync + IsField,
{
/// `ood_row_sum[row] = sum_col trace_term_coeffs[col][row] * ood(row, col)`,
/// over the reconstructed full OOD grid (g·z-pruned positions are zero).
ood_row_sum: Vec<FieldElement<FieldExtension>>,
/// Width of the reconstructed full OOD grid (= full trace width).
ood_width: usize,
/// Derived from `proof.composition_poly_parts_ood_evaluation().len()`.
number_of_parts: usize,
/// `challenges.z.pow(number_of_parts)`.
z_pow: FieldElement<FieldExtension>,
/// `sum_j composition_poly_parts_ood_evaluation[j] * challenges.gammas[j]`.
h_sum_zpow: FieldElement<FieldExtension>,
}
// The verifier reads proofs in place from their rkyv archive; archived field
// elements are viewed as native ones, which is only valid on little-endian.
#[cfg(not(target_endian = "little"))]
compile_error!("the zero-copy STARK verifier requires a little-endian target");
/// The functionality of a STARK verifier providing methods to run the STARK Verify protocol
/// https://lambdaclass.github.io/lambdaworks/starks/protocol.html
///
/// Every method below takes proof data through a [`StarkProofView`] (and its
/// nested `*View` types), a borrowed view implemented once for a real owned
/// [`StarkProof`] and once for an rkyv-archived proof read in place. This is
/// the single verification implementation: [`Self::multi_verify`] (owned) and
/// [`Self::multi_verify_archived`] (archived, used by the recursion guest)
/// are thin entry points that build the matching view and share every
/// downstream check — no serialization, no duplicated logic.
pub trait IsStarkVerifier<
Field: IsSubFieldOf<FieldExtension> + IsFFTField + Send + Sync,
FieldExtension: Send + Sync + IsField,
PI,
> where
Field::BaseType: math::field::element::NativeArchived,
FieldExtension::BaseType: math::field::element::NativeArchived,
PI: rkyv::Archive + Clone,
<PI as rkyv::Archive>::Archived: rkyv::Deserialize<PI, PiDeserializer>,
{
fn sample_query_indexes(
number_of_queries: usize,
domain: &VerifierDomain<Field>,
transcript: &mut impl IsStarkTranscript<FieldExtension, Field>,
) -> Vec<usize> {
let domain_size = domain.lde_length as u64;
(0..number_of_queries)
.map(|_| (transcript.sample_u64(domain_size >> 1)) as usize)
.collect::<Vec<usize>>()
}
/// The pruned-OOD layout for this AIR — the single place in the verifier that
/// reads the shape metadata (`trace_columns`, `step_size`, the
/// transition-offset count, and the next-row column set). Everything that used
/// to recompute these values now derives them from the returned
/// [`crate::ood::OodLayout`]. Pure AIR metadata, never a proof dimension.
fn ood_layout(
air: &dyn AIR<Field = Field, FieldExtension = FieldExtension, PublicInputs = PI>,
) -> crate::ood::OodLayout {
crate::ood::OodLayout::new(
air.context().trace_columns,
air.context().transition_offsets.len() * air.step_size(),
air.step_size(),
air.trace_ood_next_row_columns(),
)
}
/// Checks whether the purported evaluations of the composition polynomial parts and the trace
/// polynomials at the out-of-domain challenge are consistent.
/// See https://lambdaclass.github.io/lambdaworks/starks/protocol.html#step-2-verify-claimed-composition-polynomial
/// Soundness (I3): both OOD blocks' shapes are a public function of the AIR,
/// never of the (prover-controlled) proof. The current-row block opens every
/// column over `step_size` rows; the next-row block opens only the
/// transition-window columns over the remaining rows, and is empty when the
/// AIR reads none.
///
/// Must run before Round 3, which absorbs the next-row block through
/// `get_row` — an unchecked `data[start..start + width]` slice. A hostile
/// archive whose advertised dims disagree with its data length would panic
/// there rather than be rejected as a false proof; `dimensions_consistent()`
/// closes that gap, which rkyv's bytecheck leaves open.
fn ood_blocks_well_formed(
air: &dyn AIR<Field = Field, FieldExtension = FieldExtension, PublicInputs = PI>,
proof: StarkProofView<'_, Field, FieldExtension, PI>,
) -> bool {
let step_size = air.step_size();
let num_eval_points = air.context().transition_offsets.len() * step_size;
let expected_next_width = air.trace_ood_next_row_columns().len();
let expected_next_height = if expected_next_width == 0 {
0
} else {
num_eval_points.saturating_sub(step_size)
};
let current = proof.trace_ood_evaluations();
let next = proof.trace_ood_next_evaluations();
// `height == step_size` also rejects a height-0 current block: every AIR
// reports `step_size >= 1`.
current.dimensions_consistent()
&& current.width() == air.trace_layout().0 + air.num_auxiliary_rap_columns()
&& current.height() == step_size
&& next.dimensions_consistent()
&& next.width() == expected_next_width
&& next.height() == expected_next_height
}
fn step_2_verify_claimed_composition_polynomial(
air: &dyn AIR<Field = Field, FieldExtension = FieldExtension, PublicInputs = PI>,
proof: StarkProofView<'_, Field, FieldExtension, PI>,
public_inputs: &PI,
domain: &VerifierDomain<Field>,
challenges: &Challenges<FieldExtension>,
// The full current+next-row OOD grid, shape-checked and reconstructed once
// by the caller (after `ood_blocks_well_formed`) and shared with
// `step_3_verify_fri`. Its pruned next-row entries are zero — those are
// never read by any constraint. `step_size` accompanies it for the frame
// split below.
ood_full: &Table<FieldExtension>,
step_size: usize,
) -> bool {
crate::profile_markers::step_marker::<
{ crate::profile_markers::STEP_VERIFY_CLAIMED_COMPOSITION_POLYNOMIAL },
>();
let trace_length = proof.trace_length();
// Owned `BusPublicInputs` (just the table contribution L — one field
// element) reconstructed for the AIR boundary call.
let bus_public_inputs = proof
.bus_table_contribution()
.map(BusPublicInputs::from_contribution);
let boundary_constraints = air.boundary_constraints(
public_inputs,
&challenges.rap_challenges,
bus_public_inputs.as_ref(),
trace_length,
);
// Precompute g^step once per distinct step to avoid the prior O(B^2)
// linear scan. A single pass populates a memo and resolves each
// constraint's step to its point in O(1) amortized.
let mut step_to_point: HashMap<usize, FieldElement<Field>> = HashMap::new();
let boundary_points: Vec<FieldElement<Field>> = boundary_constraints
.constraints
.iter()
.map(|c| {
step_to_point
.entry(c.step)
.or_insert_with(|| domain.trace_primitive_root.pow(c.step as u64))
.clone()
})
.collect();
let main_trace_width = air.trace_layout().0;
let trace_ood_evaluations = proof.trace_ood_evaluations();
let ood_row = trace_ood_evaluations.get_row(0);
let (boundary_c_i_evaluations_num, mut boundary_c_i_evaluations_den): (
Vec<FieldElement<FieldExtension>>,
Vec<FieldElement<FieldExtension>>,
) = boundary_constraints
.constraints
.iter()
.zip(&boundary_points)
.map(|(c, point)| {
let column_idx = if c.is_aux {
main_trace_width + c.col
} else {
c.col
};
let trace_evaluation = &ood_row[column_idx];
let boundary_zerofier_challenges_z_den = -point + &challenges.z;
let boundary_quotient_ood_evaluation_num = -&c.value + trace_evaluation;
(
boundary_quotient_ood_evaluation_num,
boundary_zerofier_challenges_z_den,
)
})
.unzip();
// A malformed proof can land `z` on a boundary step, making a denominator zero.
if FieldElement::inplace_batch_inverse(&mut boundary_c_i_evaluations_den).is_err() {
return false;
}
let boundary_quotient_ood_evaluation: FieldElement<FieldExtension> =
boundary_c_i_evaluations_num
.iter()
.zip(&boundary_c_i_evaluations_den)
.zip(&challenges.boundary_coeffs)
.map(|((num, den), beta)| num * den * beta)
.fold(FieldElement::<FieldExtension>::zero(), |acc, x| acc + x);
// A malformed archive can advertise fewer OOD columns than the AIR's
// aux count; reject instead of underflowing. The current-row block keeps
// the full trace width even under g·z pruning, so this still yields the
// main width.
let num_main_trace_columns = match trace_ood_evaluations
.width()
.checked_sub(air.num_auxiliary_rap_columns())
{
Some(n) => n,
None => return false,
};
let logup_alpha_powers: Vec<FieldElement<FieldExtension>> =
if challenges.rap_challenges.len() > LOGUP_CHALLENGE_ALPHA {
compute_alpha_powers(
&challenges.rap_challenges[LOGUP_CHALLENGE_ALPHA],
air.max_bus_elements(),
)
} else {
Vec::new()
};
let logup_table_offset = match proof.bus_table_contribution() {
Some(contribution) => {
let n = FieldElement::<Field>::from(trace_length as u64);
match n.inv() {
Ok(n_inv) => n_inv * &contribution,
Err(_) => return false, // trace_length == 0 is invalid
}
}
None => FieldElement::zero(),
};
// Frame from the reconstructed full grid: the next-row step reads only
// its transition-window columns; the zero-filled remainder is never read.
// `into_frame` lives on the borrowed table view, so wrap the owned grid.
let ood_frame =
StarkTableView::Owned(ood_full).into_frame(num_main_trace_columns, step_size);
let transition_evaluation_context = TransitionEvaluationContext::new_verifier(
&ood_frame,
&challenges.rap_challenges,
&logup_alpha_powers,
&logup_table_offset,
);
let transition_ood_frame_evaluations =
air.compute_transition(&transition_evaluation_context);
let mut denominators =
vec![FieldElement::<FieldExtension>::zero(); air.num_transition_constraints()];
air.constraints_meta().iter().for_each(|m| {
denominators[m.constraint_idx] = crate::constraints::zerofier::evaluate_zerofier(
m,
&challenges.z,
&domain.trace_primitive_root,
trace_length,
);
});
let transition_c_i_evaluations_sum = itertools::izip!(
transition_ood_frame_evaluations,
&challenges.transition_coeffs,
denominators
)
.fold(FieldElement::zero(), |acc, (eval, beta, denominator)| {
acc + beta * eval * &denominator
});
let composition_poly_ood_evaluation =
&boundary_quotient_ood_evaluation + transition_c_i_evaluations_sum;
let composition_poly_claimed_ood_evaluation = proof
.composition_poly_parts_ood_evaluation()
.iter()
.rev()
.fold(FieldElement::zero(), |acc, coeff| {
acc * &challenges.z + coeff
});
composition_poly_claimed_ood_evaluation == composition_poly_ood_evaluation
}
/// The FRI fold layout for this proof, derived from options + domain.
///
/// Delegates to the shared [`crate::fri::terminal::FriFoldLayout`] so the
/// verifier's Fiat-Shamir replay and structural checks use exactly the same
/// arithmetic as the CPU and GPU provers; drift between them would break all
/// proofs. `VerifierDomain.lde_length` is the codeword size and
/// `lde_length / trace_length` the blowup factor.
// `FriFoldLayout` is a crate-internal helper type returned from a default method
// of this public trait; the exposure is intentional (internal helper).
#[allow(private_interfaces)]
fn fri_termination_params(
air: &dyn AIR<Field = Field, FieldExtension = FieldExtension, PublicInputs = PI>,
domain: &VerifierDomain<Field>,
) -> crate::fri::terminal::FriFoldLayout {
let k = air.options().fri_final_poly_log_degree as u32;
let blowup_log = (domain.lde_length / domain.trace_length).trailing_zeros();
crate::fri::terminal::FriFoldLayout::new(domain.lde_length.trailing_zeros(), blowup_log, k)
}
/// Reconstructs the Deep composition polynomial evaluations at the challenge indices values using the provided
/// openings of the trace polynomials and the composition polynomial parts. It then uses these to verify that the
/// FRI decommitments are valid and correspond to the Deep composition polynomial.
fn step_3_verify_fri(
air: &dyn AIR<Field = Field, FieldExtension = FieldExtension, PublicInputs = PI>,
proof: StarkProofView<'_, Field, FieldExtension, PI>,
domain: &VerifierDomain<Field>,
challenges: &Challenges<FieldExtension>,
// g·z pruning: the full OOD grid (reconstructed once by the caller and
// shared with `step_2`) plus the transition-window column indices, so the
// DEEP reconstruction can skip pruned next-row openings.
ood_full: &Table<FieldExtension>,
next_row_cols: &[usize],
step_size: usize,
) -> bool
where
FieldElement<Field>: AsBytes + Sync + Send,
FieldElement<FieldExtension>: AsBytes + Sync + Send,
{
crate::profile_markers::step_marker::<{ crate::profile_markers::STEP_VERIFY_FRI }>();
let (deep_poly_evaluations, deep_poly_evaluations_sym) =
match Self::reconstruct_deep_composition_poly_evaluations_for_all_queries(
challenges,
domain,
proof,
ood_full,
next_row_cols,
step_size,
) {
Some(pair) => pair,
None => return false,
};
// ---- Reconstruct the FRI terminal codeword from the final-poly coeffs ----
// The prover folds the deep composition codeword down to a terminal
// codeword of length `terminal_len = 2^(blowup_log + effective_k)` and sends
// the `2^effective_k` coefficients of the low-degree polynomial it encodes.
let layout = Self::fri_termination_params(air, domain);
let num_committed = layout.num_committed;
// Structural check: number of committed FRI layers must equal
// `num_committed` (zero when no fold or a single final fold happened).
if proof.fri_layers_merkle_roots().len() != num_committed {
return false;
}
// Structural check: the final polynomial must have exactly `2^effective_k`
// coefficients; otherwise the reconstruction below is ill-defined.
if proof.fri_final_poly_coeffs().len() != (1usize << layout.effective_k) {
return false;
}
// Structural check: every per-query FRI decommitment must carry exactly
// `num_committed` layers. The fold loop in `verify_query_and_sym_openings`
// zips these untrusted, variable-length vecs against the committed layer
// roots, and they are NOT bound into the Fiat-Shamir transcript. Without
// this check a prover could send them empty (making the fold run zero
// iterations and accept the query vacuously) or padded (making the loop
// skip the terminal low-degree check), bypassing FRI entirely. This length
// check is the only thing that pins them, so it must run before the loop.
if (0..proof.query_list_len()).any(|i| {
let decommitment = proof.query(i);
decommitment.layers_auth_paths_len() != num_committed
|| decommitment.layers_evaluations_sym().len() != num_committed
}) {
return false;
}
let terminal_offset = domain.coset_offset.pow(1u64 << layout.total_folds);
let terminal_codeword =
crate::fri::terminal::terminal_codeword_from_coeffs::<Field, FieldExtension>(
proof.fri_final_poly_coeffs(),
&terminal_offset,
layout.terminal_len,
);
// verify FRI
let mut evaluation_point_inverse = challenges
.iotas
.iter()
.map(|iota| Self::query_challenge_to_evaluation_point(*iota, false, domain))
.collect::<Vec<FieldElement<Field>>>();
// Any zero evaluation point means a malformed query index, reject.
if FieldElement::inplace_batch_inverse(&mut evaluation_point_inverse).is_err() {
return false;
}
(0..challenges.iotas.len())
.zip(evaluation_point_inverse)
.all(|(i, eval)| {
Self::verify_query_and_sym_openings(
proof,
&challenges.zetas,
challenges.iotas[i],
proof.query(i),
eval,
&deep_poly_evaluations[i],
&deep_poly_evaluations_sym[i],
&terminal_codeword,
)
})
}
/// Returns the field element element of the domain `domain` corresponding to the given FRI query index challenge `iota`.
/// Returns the LDE-coset element for FRI query challenge `iota`. The
/// `sym` flag picks the symmetric counterpart (`iota*2+1`) instead of the
/// primary index (`iota*2`).
fn query_challenge_to_evaluation_point(
iota: usize,
sym: bool,
domain: &VerifierDomain<Field>,
) -> FieldElement<Field> {
let raw = iota * 2 + if sym { 1 } else { 0 };
domain.lde_coset_element(reverse_index(raw, domain.lde_length as u64))
}
/// Verify a row-paired `PolynomialOpenings` against `root`. The row pair
/// (`2·iota`, `2·iota+1`) is committed as the single leaf at position `iota`,
/// so one Merkle path authenticates both `evaluations` (the row) and
/// `evaluations_sym` (its symmetric). Same layout used for trace and composition.
fn verify_opening_pair<E>(
opening: PolynomialOpeningsView<'_, E>,
root: &Commitment,
iota: usize,
) -> bool
where
FieldElement<Field>: AsBytes + Sync + Send,
FieldElement<E>: AsBytes + Sync + Send,
E: IsField,
E::BaseType: math::field::element::NativeArchived,
Field: IsSubFieldOf<E>,
{
// Two-slice leaf hash: the committed leaf is `evaluations ‖ evaluations_sym`,
// hashed without allocating the concatenation (see `hash_data_from_slices`).
let leaf_hash = BatchedMerkleTreeBackend::<E>::hash_data_from_slices(
opening.evaluations(),
opening.evaluations_sym(),
);
verify_merkle_path_from_leaf_hash::<BatchedMerkleTreeBackend<E>>(
opening.merkle_path(),
root,
iota,
leaf_hash,
)
}
/// Verify opening Open(tⱼ(D_LDE), 𝜐) and Open(tⱼ(D_LDE), -𝜐) for all trace polynomials tⱼ,
/// where 𝜐 and -𝜐 are the elements corresponding to the index challenge `iota`.
fn verify_trace_openings(
proof: StarkProofView<'_, Field, FieldExtension, PI>,
deep_poly_openings: DeepPolynomialOpeningView<'_, Field, FieldExtension>,
iota: usize,
) -> bool
where
FieldElement<Field>: AsBytes + Sync + Send,
FieldElement<FieldExtension>: AsBytes + Sync + Send,
{
// Main trace (multiplicities for preprocessed, full trace for normal).
let mut ok = Self::verify_opening_pair::<Field>(
deep_poly_openings.main_trace_polys(),
proof.lde_trace_main_merkle_root(),
iota,
);
// Precomputed trace (preprocessed tables only). Mismatched presence is
// unreachable in practice (multi_verify rejects such proofs upstream),
// but a defensive check keeps this function self-contained.
ok &= match (
proof.lde_trace_precomputed_merkle_root(),
deep_poly_openings.precomputed_trace_polys(),
) {
(Some(root), Some(opening)) => Self::verify_opening_pair::<Field>(opening, root, iota),
(None, None) => true,
_ => false,
};
// Auxiliary trace.
ok &= match (
proof.lde_trace_aux_merkle_root(),
deep_poly_openings.aux_trace_polys(),
) {
(Some(root), Some(opening)) => {
Self::verify_opening_pair::<FieldExtension>(opening, root, iota)
}
(None, None) => true,
_ => false,
};
ok
}
/// Verify opening Open(Hᵢ(D_LDE), 𝜐) and Open(Hᵢ(D_LDE), -𝜐) for all parts Hᵢof the composition
/// polynomial, where 𝜐 and -𝜐 are the elements corresponding to the index challenge `iota`.
fn verify_composition_poly_opening(
deep_poly_openings: DeepPolynomialOpeningView<'_, Field, FieldExtension>,
composition_poly_merkle_root: &Commitment,
iota: &usize,
) -> bool
where
FieldElement<Field>: AsBytes + Sync + Send,
FieldElement<FieldExtension>: AsBytes + Sync + Send,
{
let composition_poly = deep_poly_openings.composition_poly();
// Two-slice leaf hash of `evaluations ‖ evaluations_sym`, no concat alloc.
let leaf_hash = BatchedMerkleTreeBackend::<FieldExtension>::hash_data_from_slices(
composition_poly.evaluations(),
composition_poly.evaluations_sym(),
);
verify_merkle_path_from_leaf_hash::<BatchedMerkleTreeBackend<FieldExtension>>(
composition_poly.merkle_path(),
composition_poly_merkle_root,
*iota,
leaf_hash,
)
}
/// Verifies the validity of the purported values of the trace polynomials and the composition polynomial
/// parts at the domain elements and their symmetric counterparts corresponding to all the FRI query
/// index challenges.
fn step_4_verify_trace_and_composition_openings(
proof: StarkProofView<'_, Field, FieldExtension, PI>,
challenges: &Challenges<FieldExtension>,
) -> bool
where
FieldElement<Field>: AsBytes + Sync + Send,
FieldElement<FieldExtension>: AsBytes + Sync + Send,
{
crate::profile_markers::step_marker::<
{ crate::profile_markers::STEP_VERIFY_TRACE_AND_COMPOSITION_OPENINGS },
>();
// `step_3_verify_fri` (which runs before this) already rejects proofs
// whose `deep_poly_openings` is shorter than `challenges.iotas`.
challenges.iotas.iter().enumerate().all(|(i, iota_n)| {
let deep_poly_opening = proof.deep_poly_opening(i);
Self::verify_composition_poly_opening(
deep_poly_opening,
proof.composition_poly_root(),
iota_n,
) && Self::verify_trace_openings(proof, deep_poly_opening, *iota_n)
})
}
/// Verifies the openings of a fold polynomial of an inner layer of FRI.
fn verify_fri_layer_openings(
merkle_root: &Commitment,
auth_path_sym: &[Commitment],
evaluation: &FieldElement<FieldExtension>,
evaluation_sym: &FieldElement<FieldExtension>,
iota: usize,
) -> bool
where
FieldElement<Field>: AsBytes + Sync + Send,
FieldElement<FieldExtension>: AsBytes + Sync + Send,
{
let evaluations = if iota % 2 == 1 {
vec![evaluation_sym.clone(), evaluation.clone()]
} else {
vec![evaluation.clone(), evaluation_sym.clone()]
};
verify_merkle_path::<BatchedMerkleTreeBackend<FieldExtension>>(
auth_path_sym,
merkle_root,
iota >> 1,
&evaluations,
)
}
/// Verify a single FRI query
/// `zetas`: the vector of all challenges sent by the verifier to the prover at the commit
/// phase to fold polynomials.
/// `iota`: the index challenge of this FRI query. This index uniquely determines two elements 𝜐 and -𝜐
/// of the evaluation domain of FRI layer 0.
/// `evaluation_point_inv`: precomputed value of 𝜐⁻¹.
/// `deep_composition_evaluation`: precomputed value of p₀(𝜐), where p₀ is the deep composition polynomial.
/// `deep_composition_evaluation_sym`: precomputed value of p₀(-𝜐), where p₀ is the deep composition polynomial.
#[allow(clippy::too_many_arguments)]
fn verify_query_and_sym_openings(
proof: StarkProofView<'_, Field, FieldExtension, PI>,
zetas: &[FieldElement<FieldExtension>],
iota: usize,
fri_decommitment: FriDecommitmentView<'_, FieldExtension>,
evaluation_point_inv: FieldElement<Field>,
deep_composition_evaluation: &FieldElement<FieldExtension>,
deep_composition_evaluation_sym: &FieldElement<FieldExtension>,
terminal_codeword: &[FieldElement<FieldExtension>],
) -> bool
where
FieldElement<Field>: AsBytes + Sync + Send,
FieldElement<FieldExtension>: AsBytes + Sync + Send,
{
let fri_layers_merkle_roots = proof.fri_layers_merkle_roots();
let p0_eval = deep_composition_evaluation;
let p0_eval_sym = deep_composition_evaluation_sym;
// No-fold (clamp) case: the codeword never folds (`total_folds == 0`), so
// no folding challenges were drawn and the terminal codeword *is* the deep
// composition codeword p₀ itself. The query's two points 𝜐 and -𝜐 sit at
// FRI-order positions `iota*2` and `iota*2 + 1` of the terminal codeword.
if zetas.is_empty() {
return terminal_codeword
.get(iota * 2)
.is_some_and(|t| p0_eval == t)
&& terminal_codeword
.get(iota * 2 + 1)
.is_some_and(|t| p0_eval_sym == t);
}
let evaluation_point_vec: Vec<FieldElement<Field>> =
core::iter::successors(Some(evaluation_point_inv.square()), |evaluation_point| {
Some(evaluation_point.square())
})
.take(fri_layers_merkle_roots.len())
.collect();
// Reconstruct p₁(𝜐²)
let mut v =
(p0_eval + p0_eval_sym) + evaluation_point_inv * &zetas[0] * (p0_eval - p0_eval_sym);
let mut index = iota;
// Fold through every committed layer: use the proof to verify the openings
// of pᵢ(−𝜐^(2ⁱ)) (given by the prover) and pᵢ(𝜐^(2ⁱ)) (computed on the
// previous iteration), then obtain pᵢ₊₁(𝜐^(2ⁱ⁺¹)). When there are no
// committed layers (`total_folds == 1`, a single final fold) this fold is
// empty and `v`/`index` already hold the terminal-layer value/position.
let openings_ok = fri_layers_merkle_roots
.iter()
.zip(fri_decommitment.layers_evaluations_sym())
.zip(evaluation_point_vec)
.enumerate()
.fold(
true,
|result, (i, ((merkle_root, evaluation_sym), evaluation_point_inv))| {
// Verify opening Open(pᵢ(Dₖ), −𝜐^(2ⁱ)) and Open(pᵢ(Dₖ), 𝜐^(2ⁱ)).
// `v` is pᵢ(𝜐^(2ⁱ)).
// `evaluation_sym` is pᵢ(−𝜐^(2ⁱ)).
let openings_ok = Self::verify_fri_layer_openings(
merkle_root,
fri_decommitment.layer_auth_path(i),
&v,
evaluation_sym,
index,
);
// Update `v` with next value pᵢ₊₁(𝜐^(2ⁱ⁺¹)).
v = (&v + evaluation_sym)
+ evaluation_point_inv * &zetas[i + 1] * (&v - evaluation_sym);
// Update index for next iteration. The index of the squares in the next layer
// is obtained by halving the current index. This is due to the bit-reverse
// ordering of the elements in the Merkle tree.
index >>= 1;
result & openings_ok
},
);
// After folding through all committed layers, `v` is the query's value at
// the terminal layer and `index` its FRI-order position there. Check it
// against the reconstructed terminal codeword. This single check covers
// both the single-fold (`total_folds == 1`, empty fold above) and
// multi-fold regimes; `.get()` fails closed on an out-of-range index.
let terminal_ok = terminal_codeword.get(index).is_some_and(|t| &v == t);
openings_ok & terminal_ok
}
/// Sums that depend only on `challenges` and proof-level OOD/gamma data —
/// identical for every FRI query — computed once instead of once per
/// query.
///
/// g·z pruning: the trace OOD values come from the reconstructed full grid
/// `ood_full` (current-row block plus the scattered next-row window, zeros
/// elsewhere), not from `proof.trace_ood_evaluations()` which now carries
/// only the current-row block. Pruned positions are zero in both the grid
/// and `trace_term_coeffs`, so next rows sum only the window columns.
fn compute_query_invariant_deep_terms(
challenges: &Challenges<FieldExtension>,
proof: StarkProofView<'_, Field, FieldExtension, PI>,
ood_full: &Table<FieldExtension>,
next_row_cols: &[usize],
step_size: usize,
) -> Option<QueryInvariantDeepTerms<FieldExtension>> {
let ood_evaluations_table_height = ood_full.height;
let ood_evaluations_table_width = ood_full.width;
let ood_data = ood_full.row_major_data();
let trace_term_coeffs = &challenges.trace_term_coeffs;
if trace_term_coeffs.is_empty()
|| trace_term_coeffs.len() * trace_term_coeffs[0].len()
!= ood_evaluations_table_height * ood_evaluations_table_width
{
return None;
}
let mut ood_row_sum = Vec::with_capacity(ood_evaluations_table_height);
for row_idx in 0..ood_evaluations_table_height {
let ood_row = &ood_data[row_idx * ood_evaluations_table_width
..(row_idx + 1) * ood_evaluations_table_width];
let mut sum = FieldElement::<FieldExtension>::zero();
if row_idx < step_size {
for col_idx in 0..ood_evaluations_table_width {
sum += &trace_term_coeffs[col_idx][row_idx] * &ood_row[col_idx];
}
} else {
// Next-row row: off-window columns contribute coeff·0 with a
// zero coeff too, so the window-only sum is exact.
for &col_idx in next_row_cols {
sum += &trace_term_coeffs[col_idx][row_idx] * &ood_row[col_idx];
}
}
ood_row_sum.push(sum);
}
let composition_parts_ood = proof.composition_poly_parts_ood_evaluation();
let number_of_parts = composition_parts_ood.len();
let z_pow = challenges.z.pow(number_of_parts);
// A malformed proof/challenge set can advertise more composition
// parts than sampled gammas; reject rather than silently truncate
// the sum below.
if challenges.gammas.len() < number_of_parts {
return None;
}
let mut h_sum_zpow = FieldElement::<FieldExtension>::zero();
for (h_i_zpower, gamma) in composition_parts_ood.iter().zip(challenges.gammas.iter()) {
h_sum_zpow += h_i_zpower * gamma;
}
Some(QueryInvariantDeepTerms {
ood_row_sum,
ood_width: ood_evaluations_table_width,
number_of_parts,
z_pow,
h_sum_zpow,
})
}
fn reconstruct_deep_composition_poly_evaluations_for_all_queries(
challenges: &Challenges<FieldExtension>,
domain: &VerifierDomain<Field>,
proof: StarkProofView<'_, Field, FieldExtension, PI>,
ood_full: &Table<FieldExtension>,
next_row_cols: &[usize],
step_size: usize,
) -> Option<DeepPolynomialEvaluations<FieldExtension>> {
let num_queries = challenges.iotas.len();
// `deep_poly_openings` comes straight from the untrusted proof and its
// length is not otherwise pinned (the `query_list.len()` guard checks a
// different field). The loop below indexes `deep_poly_openings[i]` for
// every `i` in `0..num_queries`, so a truncated Vec would panic the
// verifier with an out-of-bounds index on a malicious proof. Reject
// instead. (Extra entries are harmless — they are never indexed —
// matching the `<` convention of the `query_list` guard.)
if proof.deep_poly_openings_len() < num_queries {
return None;
}
let mut deep_poly_evaluations = Vec::with_capacity(num_queries);
let mut deep_poly_evaluations_sym = Vec::with_capacity(num_queries);
// Build the base-field LDE evaluations as concatenated slice (precomputed + main)
// without lifting to the extension field. The helper now subtracts directly via
// the F: IsSubFieldOf<E> Sub impl, so we avoid a per-query base->extension lift.
let primitive_root = &Field::get_primitive_root_of_unity(domain.root_order as u64)
.expect("verifier domain root_order is a valid power of two");
let query_invariant_terms = Self::compute_query_invariant_deep_terms(
challenges,
proof,
ood_full,
next_row_cols,
step_size,
)?;
for (i, iota) in challenges.iotas.iter().enumerate() {
let opening = proof.deep_poly_opening(i);
// Base-field portion as two borrowed slices in commit order —
// precomputed columns FIRST, then main trace columns. The callee
// resolves a base column via `base_at`, so there is no per-query
// concat allocation.
let lde_precomputed: &[FieldElement<Field>] = opening
.precomputed_trace_polys()
.map(|p| p.evaluations())
.unwrap_or(&[]);
let lde_main = opening.main_trace_polys().evaluations();
let lde_aux: &[FieldElement<FieldExtension>] = opening
.aux_trace_polys()
.map(|a| a.evaluations())
.unwrap_or(&[]);
let lde_precomputed_sym: &[FieldElement<Field>] = opening
.precomputed_trace_polys()
.map(|p| p.evaluations_sym())
.unwrap_or(&[]);
let lde_main_sym = opening.main_trace_polys().evaluations_sym();
let lde_aux_sym: &[FieldElement<FieldExtension>] = opening
.aux_trace_polys()
.map(|a| a.evaluations_sym())
.unwrap_or(&[]);
let evaluation_point = Self::query_challenge_to_evaluation_point(*iota, false, domain);
let evaluation_point_sym =
Self::query_challenge_to_evaluation_point(*iota, true, domain);
let (evaluation, evaluation_sym) =
Self::reconstruct_deep_composition_poly_evaluation_pair(
&evaluation_point,
&evaluation_point_sym,
primitive_root,
challenges,
&query_invariant_terms,
next_row_cols,
step_size,
lde_precomputed,
lde_main,
lde_aux,
opening.composition_poly().evaluations(),
lde_precomputed_sym,
lde_main_sym,
lde_aux_sym,
opening.composition_poly().evaluations_sym(),
)?;
deep_poly_evaluations.push(evaluation);
deep_poly_evaluations_sym.push(evaluation_sym);
}
Some((deep_poly_evaluations, deep_poly_evaluations_sym))
}
/// Reconstructs the deep composition polynomial evaluation at a query's
/// point and its symmetric counterpart together. Rewriting the per-element
/// trace term `coeff*(base-ood)*denom` as `denom*(coeff*base - coeff*ood)`
/// isolates `coeff*ood` (identical for both points, hoisted into
/// `query_invariant_terms`) from `coeff*base` (per-point), so both points
/// share the OOD walk and a single batch-inverse for their denominators.
/// g·z pruning restricts next rows (`row_idx >= step_size`) to the
/// transition-window columns `next_row_cols` — all other next-row
/// coefficients are zero, so those terms vanish from both sums.
#[allow(clippy::too_many_arguments)]
fn reconstruct_deep_composition_poly_evaluation_pair<'b>(
evaluation_point: &FieldElement<Field>,
evaluation_point_sym: &FieldElement<Field>,
primitive_root: &FieldElement<Field>,
challenges: &Challenges<FieldExtension>,
query_invariant_terms: &QueryInvariantDeepTerms<FieldExtension>,
next_row_cols: &[usize],
step_size: usize,
lde_trace_precomputed_evaluations: &'b [FieldElement<Field>],
lde_trace_main_evaluations: &'b [FieldElement<Field>],
lde_trace_aux_evaluations: &[FieldElement<FieldExtension>],
lde_composition_poly_parts_evaluation: &[FieldElement<FieldExtension>],
lde_trace_precomputed_evaluations_sym: &'b [FieldElement<Field>],
lde_trace_main_evaluations_sym: &'b [FieldElement<Field>],
lde_trace_aux_evaluations_sym: &[FieldElement<FieldExtension>],
lde_composition_poly_parts_evaluation_sym: &[FieldElement<FieldExtension>],
) -> Option<(FieldElement<FieldExtension>, FieldElement<FieldExtension>)> {
let ood_evaluations_table_height = query_invariant_terms.ood_row_sum.len();
let ood_evaluations_table_width = query_invariant_terms.ood_width;
let trace_term_coeffs = &challenges.trace_term_coeffs;
// Base columns are supplied as two slices (precomputed ‖ main) that the
// prover concatenated in this order; `num_base`/`base_at` index into
// them as if concatenated, without allocating.
let num_precomputed = lde_trace_precomputed_evaluations.len();
let num_base = num_precomputed + lde_trace_main_evaluations.len();
let base_at = move |col: usize| -> &'b FieldElement<Field> {
if col < num_precomputed {
&lde_trace_precomputed_evaluations[col]
} else {
&lde_trace_main_evaluations[col - num_precomputed]
}
};
let num_precomputed_sym = lde_trace_precomputed_evaluations_sym.len();
let num_base_sym = num_precomputed_sym + lde_trace_main_evaluations_sym.len();
let base_at_sym = move |col: usize| -> &'b FieldElement<Field> {
if col < num_precomputed_sym {
&lde_trace_precomputed_evaluations_sym[col]
} else {
&lde_trace_main_evaluations_sym[col - num_precomputed_sym]
}
};
// Runtime guards: a malformed proof may supply opening evaluations
// whose column count does not match the OOD table width, or whose
// regular/symmetric base-column split disagree. Without these checks
// the indexing below would panic in release builds.
if num_base != num_base_sym {
return None;
}
if num_base + lde_trace_aux_evaluations.len() != ood_evaluations_table_width
|| num_base + lde_trace_aux_evaluations_sym.len() != ood_evaluations_table_width
{
return None;
}
// Build both denominator sets (regular, then symmetric) and invert
// them together in a single batch.
let mut denoms = Vec::with_capacity(2 * ood_evaluations_table_height);
let mut current_z = challenges.z.clone();
for _ in 0..ood_evaluations_table_height {
denoms.push(evaluation_point - ¤t_z);
current_z = primitive_root * ¤t_z;
}
let mut current_z = challenges.z.clone();
for _ in 0..ood_evaluations_table_height {
denoms.push(evaluation_point_sym - ¤t_z);
current_z = primitive_root * ¤t_z;
}
// A malformed proof can land an OOD evaluation point on the LDE coset, reject.
FieldElement::inplace_batch_inverse(&mut denoms).ok()?;
let (denoms_trace, denoms_trace_sym) = denoms.split_at(ood_evaluations_table_height);
let mut trace_term = FieldElement::<FieldExtension>::zero();
let mut trace_term_sym = FieldElement::<FieldExtension>::zero();
for row_idx in 0..ood_evaluations_table_height {