Skip to content
152 changes: 150 additions & 2 deletions crypto/stark/src/proof/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use crate::config::Commitment;
use crate::frame::Frame;
use crate::fri::fri_decommit::{ArchivedFriDecommitment, FriDecommitment};
use crate::proof::stark::{
ArchivedDeepPolynomialOpening, ArchivedPolynomialOpenings, ArchivedStarkProof,
DeepPolynomialOpening, PolynomialOpenings, StarkProof,
ArchivedDeepPolynomialOpening, ArchivedMultiProof, ArchivedPolynomialOpenings,
ArchivedStarkProof, DeepPolynomialOpening, MultiProof, PolynomialOpenings, StarkProof,
};
use crate::table::{ArchivedTable, Table, TableView};
use math::field::element::{ArchivedFieldElement, FieldElement};
Expand Down Expand Up @@ -481,6 +481,154 @@ where
}
}

/// Borrowed view over a [`MultiProof`] (owned or archived-in-place),
/// producing per-proof [`StarkProofView`]s without ever materializing an
/// owned `MultiProof` from an archive. Replaces the
/// `proofs.iter().map(StarkProofView::Owned/Archived).collect()` boilerplate
/// that used to appear at every `MultiProof` verify call site.
pub enum MultiProofView<'a, F: IsSubFieldOf<E>, E: IsField, PI>
where
F::BaseType: math::field::element::NativeArchived,
E::BaseType: math::field::element::NativeArchived,
PI: rkyv::Archive,
<PI as rkyv::Archive>::Archived: rkyv::Deserialize<PI, PiDeserializer>,
{
Owned(&'a MultiProof<F, E, PI>),
Archived(&'a ArchivedMultiProof<F, E, PI>),
}

impl<'a, F: IsSubFieldOf<E>, E: IsField, PI> Clone for MultiProofView<'a, F, E, PI>
where
F::BaseType: math::field::element::NativeArchived,
E::BaseType: math::field::element::NativeArchived,
PI: rkyv::Archive,
<PI as rkyv::Archive>::Archived: rkyv::Deserialize<PI, PiDeserializer>,
{
fn clone(&self) -> Self {
*self
}
}
impl<'a, F: IsSubFieldOf<E>, E: IsField, PI> Copy for MultiProofView<'a, F, E, PI>
where
F::BaseType: math::field::element::NativeArchived,
E::BaseType: math::field::element::NativeArchived,
PI: rkyv::Archive,
<PI as rkyv::Archive>::Archived: rkyv::Deserialize<PI, PiDeserializer>,
{
}

impl<'a, F: IsSubFieldOf<E>, E: IsField, PI> MultiProofView<'a, F, E, PI>
where
F::BaseType: math::field::element::NativeArchived,
E::BaseType: math::field::element::NativeArchived,
PI: rkyv::Archive,
<PI as rkyv::Archive>::Archived: rkyv::Deserialize<PI, PiDeserializer>,
{
#[inline(always)]
pub fn len(&self) -> usize {
match self {
Self::Owned(p) => p.proofs.len(),
Self::Archived(p) => p.proofs.len(),
}
}

#[inline(always)]
pub fn is_empty(&self) -> bool {
self.len() == 0
}

#[inline(always)]
pub fn get(&self, i: usize) -> StarkProofView<'a, F, E, PI> {
match self {
Self::Owned(p) => StarkProofView::Owned(&p.proofs[i]),
Self::Archived(p) => StarkProofView::Archived(&p.proofs.as_slice()[i]),
}
}

#[inline(always)]
pub fn last(&self) -> Option<StarkProofView<'a, F, E, PI>> {
let len = self.len();
(len > 0).then(|| self.get(len - 1))
}

#[inline(always)]
pub fn iter(&self) -> impl Iterator<Item = StarkProofView<'a, F, E, PI>> + 'a {
let this = *self;
(0..this.len()).map(move |i| this.get(i))
}
}

/// A source of [`StarkProofView`]s the verifier can iterate over more than
/// once without ever materializing a `Vec` — implemented for a plain slice
/// (or `Vec`) of views and for [`MultiProofView`] alike, so
/// [`crate::verifier::IsStarkVerifier::multi_verify_views`] runs identically
/// whether its caller already had a slice or is reading straight out of a
/// (owned or archived) `MultiProof`.
pub trait ProofViewSource<'a, F: IsSubFieldOf<E> + 'a, E: IsField + 'a, PI: 'a>: Copy
where
F::BaseType: math::field::element::NativeArchived,
E::BaseType: math::field::element::NativeArchived,
PI: rkyv::Archive,
<PI as rkyv::Archive>::Archived: rkyv::Deserialize<PI, PiDeserializer>,
{
fn view_len(&self) -> usize;
fn view_iter(&self) -> impl Iterator<Item = StarkProofView<'a, F, E, PI>>;
}

impl<'a, F: IsSubFieldOf<E> + 'a, E: IsField + 'a, PI: 'a> ProofViewSource<'a, F, E, PI>
for &'a [StarkProofView<'a, F, E, PI>]
where
F::BaseType: math::field::element::NativeArchived,
E::BaseType: math::field::element::NativeArchived,
PI: rkyv::Archive,
<PI as rkyv::Archive>::Archived: rkyv::Deserialize<PI, PiDeserializer>,
{
#[inline(always)]
fn view_len(&self) -> usize {
self.len()
}
#[inline(always)]
fn view_iter(&self) -> impl Iterator<Item = StarkProofView<'a, F, E, PI>> {
self.iter().copied()
}
}

impl<'a, F: IsSubFieldOf<E> + 'a, E: IsField + 'a, PI: 'a> ProofViewSource<'a, F, E, PI>
for &'a Vec<StarkProofView<'a, F, E, PI>>
where
F::BaseType: math::field::element::NativeArchived,
E::BaseType: math::field::element::NativeArchived,
PI: rkyv::Archive,
<PI as rkyv::Archive>::Archived: rkyv::Deserialize<PI, PiDeserializer>,
{
#[inline(always)]
fn view_len(&self) -> usize {
self.len()
}
#[inline(always)]
fn view_iter(&self) -> impl Iterator<Item = StarkProofView<'a, F, E, PI>> {
self.iter().copied()
}
}

impl<'a, F: IsSubFieldOf<E> + 'a, E: IsField + 'a, PI: 'a> ProofViewSource<'a, F, E, PI>
for MultiProofView<'a, F, E, PI>
where
F::BaseType: math::field::element::NativeArchived,
E::BaseType: math::field::element::NativeArchived,
PI: rkyv::Archive,
<PI as rkyv::Archive>::Archived: rkyv::Deserialize<PI, PiDeserializer>,
{
#[inline(always)]
fn view_len(&self) -> usize {
MultiProofView::len(self)
}
#[inline(always)]
fn view_iter(&self) -> impl Iterator<Item = StarkProofView<'a, F, E, PI>> {
MultiProofView::iter(self)
}
}

// ---------------------------------------------------------------------------
// Field-coverage guards.
//
Expand Down
4 changes: 2 additions & 2 deletions crypto/stark/src/tests/bus_tests/soundness_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,7 @@ fn test_malformed_ood_next_block_shape_rejected_archived() {
assert!(
!Verifier::multi_verify_archived(
&airs,
&archived.proofs,
archived,
&mut DefaultTranscript::<E>::new(&[]),
&FieldElement::zero(),
),
Expand Down Expand Up @@ -1279,7 +1279,7 @@ fn test_gz_pruning_reduces_next_row_openings() {
.unwrap();
assert!(Verifier::multi_verify_archived(
&airs,
&archived.proofs,
archived,
&mut DefaultTranscript::<E>::new(&[]),
&FieldElement::zero(),
));
Expand Down
53 changes: 28 additions & 25 deletions crypto/stark/src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ use crate::{
config::Commitment,
domain::new_verifier_domain,
lookup::{BusPublicInputs, LOGUP_CHALLENGE_ALPHA, LOGUP_NUM_CHALLENGES, compute_alpha_powers},
proof::stark::{ArchivedStarkProof, MultiProof},
proof::stark::{ArchivedMultiProof, MultiProof},
proof::view::{
DeepPolynomialOpeningView, FriDecommitmentView, PolynomialOpeningsView, StarkProofView,
StarkTableView,
DeepPolynomialOpeningView, FriDecommitmentView, MultiProofView, PolynomialOpeningsView,
ProofViewSource, StarkProofView, StarkTableView,
},
table::Table,
};
Expand Down Expand Up @@ -1095,49 +1095,55 @@ pub trait IsStarkVerifier<
FieldElement<Field>: AsBytes + Sync + Send,
FieldElement<FieldExtension>: AsBytes + Sync + Send,
{
let views: Vec<StarkProofView<Field, FieldExtension, PI>> = multi_proof
.proofs
.iter()
.map(StarkProofView::Owned)
.collect();
Self::multi_verify_views(airs, &views, transcript, expected_bus_balance)
Self::multi_verify_views(
airs,
MultiProofView::Owned(multi_proof),
transcript,
expected_bus_balance,
)
}

/// Verifies one or more rkyv-archived STARK proofs read **in place** from
/// their archive buffer — no proof deserialization, no per-field allocation.
fn multi_verify_archived(
airs: &[&dyn AIR<Field = Field, FieldExtension = FieldExtension, PublicInputs = PI>],
proofs: &[ArchivedStarkProof<Field, FieldExtension, PI>],
multi_proof: &ArchivedMultiProof<Field, FieldExtension, PI>,
transcript: &mut (impl IsStarkTranscript<FieldExtension, Field> + Clone),
expected_bus_balance: &FieldElement<FieldExtension>,
) -> bool
where
FieldElement<Field>: AsBytes + Sync + Send,
FieldElement<FieldExtension>: AsBytes + Sync + Send,
{
let views: Vec<StarkProofView<Field, FieldExtension, PI>> =
proofs.iter().map(StarkProofView::Archived).collect();
Self::multi_verify_views(airs, &views, transcript, expected_bus_balance)
Self::multi_verify_views(
airs,
MultiProofView::Archived(multi_proof),
transcript,
expected_bus_balance,
)
}

/// The single verification implementation, shared by [`Self::multi_verify`]
/// (owned) and [`Self::multi_verify_archived`] (archived), operating on
/// proof views rather than either's concrete type.
fn multi_verify_views(
fn multi_verify_views<'p>(
airs: &[&dyn AIR<Field = Field, FieldExtension = FieldExtension, PublicInputs = PI>],
proofs: &[StarkProofView<Field, FieldExtension, PI>],
proofs: impl ProofViewSource<'p, Field, FieldExtension, PI>,
transcript: &mut (impl IsStarkTranscript<FieldExtension, Field> + Clone),
expected_bus_balance: &FieldElement<FieldExtension>,
) -> bool
where
Field: 'p,
FieldExtension: 'p,
PI: 'p,
FieldElement<Field>: AsBytes + Sync + Send,
FieldElement<FieldExtension>: AsBytes + Sync + Send,
{
if airs.len() != proofs.len() {
if airs.len() != proofs.view_len() {
error!(
"AIR count ({}) does not match proof count ({})",
airs.len(),
proofs.len()
proofs.view_len()
);
return false;
}
Expand All @@ -1151,8 +1157,7 @@ pub trait IsStarkVerifier<
// For preprocessed tables, use the hardcoded commitment (verifier cannot
// trust the prover). For normal tables, use the commitment from the proof.

for (idx, (air, proof)) in airs.iter().zip(proofs).enumerate() {
let proof = *proof;
for (idx, (air, proof)) in airs.iter().zip(proofs.view_iter()).enumerate() {
// Soundness: the number of composition-poly parts is fixed by the AIR's
// degree bound, NOT chosen by the prover. Deriving it from the proof would
// let a malicious prover inflate the part count, widening the composition
Expand Down Expand Up @@ -1229,8 +1234,7 @@ pub trait IsStarkVerifier<
// boundary constraints on LogUp columns, so the bus balance check is
// the only cross-table validation.

for (idx, (air, proof)) in airs.iter().zip(proofs).enumerate() {
let proof = *proof;
for (idx, (air, proof)) in airs.iter().zip(proofs.view_iter()).enumerate() {
if air.has_trace_interaction() && !proof.has_bus_public_inputs() {
error!(
"Table {idx}: AIR has LogUp interactions but proof is missing bus_public_inputs"
Expand All @@ -1252,8 +1256,7 @@ pub trait IsStarkVerifier<
// state after Phase B, domain-separated by table index). This matches
// the prover's forking and makes per-table verification independent.

for (idx, (air, proof)) in airs.iter().zip(proofs).enumerate() {
let proof = *proof;
for (idx, (air, proof)) in airs.iter().zip(proofs.view_iter()).enumerate() {
// Must match prover: fork with domain separator for multi-table,
// use original transcript directly for single-table.
let num_tables = airs.len();
Expand Down Expand Up @@ -1309,7 +1312,7 @@ pub trait IsStarkVerifier<

if needs_lookup_challenges {
let mut total = FieldElement::<FieldExtension>::zero();
for (air, proof) in airs.iter().zip(proofs) {
for (air, proof) in airs.iter().zip(proofs.view_iter()) {
if air.has_trace_interaction()
&& let Some(contribution) = proof.bus_table_contribution()
{
Expand Down Expand Up @@ -1345,7 +1348,7 @@ pub trait IsStarkVerifier<
{
Self::multi_verify_views(
&[air],
&[StarkProofView::Owned(proof)],
&[StarkProofView::Owned(proof)][..],
transcript,
&FieldElement::zero(),
)
Expand Down
Loading
Loading