Skip to content

refactor(lifted-stark): replace bus-identity reduction with general eval_external + flat external_public_inputs#992

Draft
adr1anh wants to merge 1 commit intonextfrom
adr1anh/external-public-inputs
Draft

refactor(lifted-stark): replace bus-identity reduction with general eval_external + flat external_public_inputs#992
adr1anh wants to merge 1 commit intonextfrom
adr1anh/external-public-inputs

Conversation

@adr1anh
Copy link
Copy Markdown
Contributor

@adr1anh adr1anh commented May 5, 2026

Summary

Generalizes the cross-AIR bus-identity check (ReducedAuxValues { prod, sum }) into per-AIR external assertions, and simplifies the variable-length public-input plumbing into a flat &[F] slice that the AIR decodes itself.

Motivation

The previous design baked two assumptions into the trait surface:

  1. External assertions had a fixed shape — every AIR's contribution had to be a (product, sum) pair representing a multiset/logup bus identity. This is fine for those bus shapes but doesn't generalize to other assertions over aux values + challenges + public inputs.
  2. Variable-length public inputs were a list of fixed-width matrices — the framework declared widths up front via num_var_len_public_inputs() and validated them on every instance. This was extra plumbing for callers without buying flexibility: AIRs that wanted heterogeneous inputs (length-prefixed sublists, scalar headers, etc.) couldn't express them.

Both pieces are folded into a single, narrower contract: the AIR returns a vector of scalar assertions and decodes its inputs from a flat slice.

API changes

LiftedAir

Before After
fn reduced_aux_values(...) -> Result<ReducedAuxValues<EF>, ReductionError> fn eval_external(...) -> Result<Vec<EF>, ReductionError>
fn num_var_len_public_inputs() -> usize (removed; AIR owns its own schema)

eval_external returns a list of extension-field values that must each equal zero. The verifier checks them individually (no Horner/alpha fold) so a non-zero entry is reported with its caller-original instance index and assertion position:

VerifierError::ExternalAssertionFailed { caller_instance, assertion }

Instance / witness types

Before After
var_len_public_inputs: &[&[F]] external_public_inputs: &[F]

AirInstance, AirWitness, prove_single, and verify_single now take a flat slice. The AIR is responsible for any decode/length checks and reports failures via ReductionError.

Removed

  • ReducedAuxValues<EF> type and the auxiliary/values.rs module
  • InstanceValidationError::VarLenPublicInputsMismatch
  • InstanceValidationError::VarLenPublicInputWidthMismatch
  • Matrix-width validation block in instance::validate_inputs and debug::check_constraints_multi
  • VerifierError::InvalidReducedAux

Fiat-Shamir

Caller absorption simplifies to a single line:

ch.observe_slice(public_values);
ch.observe_slice(external_public_inputs);  // was a per-matrix loop

Both prover and verifier MUST absorb the same slice.

AIR migration

For an AIR that previously did:

fn reduced_aux_values(&self, aux, ch, pv, var_len) -> Result<ReducedAuxValues<EF>, _> {
    let pi_0 = QuadFelt::from(var_len[0][0]);
    let pi_1 = QuadFelt::from(var_len[1][0]);
    Ok(ReducedAuxValues {
        prod: aux[0] * (ch[0] + pi_0),
        sum: (aux[1] - ch[1]) - pi_1,
    })
}

The equivalent now is:

fn eval_external(&self, aux, ch, pv, external) -> Result<Vec<EF>, _> {
    let mut it = external.iter().copied();
    let pi_0 = QuadFelt::from(it.next().ok_or("missing pi_0")?);
    let pi_1 = QuadFelt::from(it.next().ok_or("missing pi_1")?);
    Ok(vec![
        aux[0] * (ch[0] + pi_0) - QuadFelt::ONE,         // multiset: prod == 1
        (aux[1] - ch[1]) - pi_1,                          // logup: sum == 0
    ])
}

Note the multiset assertion is rewritten to prod - 1 == 0 (each entry is its own zero-equation; there's no separate "identity" form anymore).

Test plan

  • cargo test -p miden-lifted-stark --lib — 73/73 pass, including:
    • bus_identity_check (positive case)
    • bus_wrong_external_pi_fails (mismatched value → ExternalAssertionFailed)
    • bus_short_external_inputs_fails (truncated input → Reduction(_) from the AIR's decoder)
  • cargo clippy -p miden-lifted-air -p miden-lifted-stark -p miden-bench --all-targets clean
  • cargo check clean across all three affected crates

Notes for reviewers

  • Soundness: per-assertion zero-check is equivalent to a Horner fold over concrete EF values. The fold was bookkeeping, not adding any cryptographic guarantee.
  • The framework no longer cross-checks shape against the AIR for external_public_inputs. If the caller and AIR disagree on encoding, the AIR's decoder fails (returning ReductionError), which yields VerifierError::Reduction(_) rather than a panic.

… flat external_public_inputs

Generalize the bus-identity check into per-AIR external assertions and
simplify the public-input plumbing.

API changes on `LiftedAir`:
- `reduced_aux_values()` -> `eval_external()`, returning `Vec<EF>` of
  scalar assertions that must each equal zero (in place of the bus-shaped
  `(prod, sum)` reduction).
- `num_var_len_public_inputs()` is gone; external inputs are now a flat
  `&[F]` slice with no framework-imposed shape. The AIR owns the schema
  and reports decode errors via `ReductionError`.

Type changes:
- `VarLenPublicInputs<'a, F> = &'a [&'a [F]]` -> `ExternalPublicInputs<'a, F> = &'a [F]`.
- `AirInstance` / `AirWitness` now carry `external_public_inputs: &[F]`.
- `auxiliary/values.rs` (the `ReducedAuxValues` type) is deleted.

Verifier:
- Each entry of every AIR's assertion list is checked individually against
  zero (no Horner / alpha fold). A non-zero entry returns
  `ExternalAssertionFailed { caller_instance, assertion }`, naming the
  caller's original instance index and the assertion's position in the
  AIR's returned vector.

Validation:
- The framework no longer pre-checks shape on external inputs; the AIR's
  decoder is responsible. `InstanceValidationError::VarLen*` variants are
  removed; the `validate_inputs` and `check_constraints_multi` blocks for
  matrix-shape validation are dropped.

Fiat-Shamir:
- Caller absorption simplifies to a single `observe_slice(external_public_inputs)`.
/// Default: returns identity (correct for AIRs without buses).
fn reduced_aux_values(
/// Default: no external assertions (correct for AIRs that do not need them).
fn eval_external(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should relax the bounds to allow symbolic representation of these constraints.

/// Implementors of [`reduced_aux_values`](Self::reduced_aux_values) should verify
/// that `var_len_public_inputs` contains exactly this many slices, returning
/// [`ReductionError`] otherwise.
fn num_var_len_public_inputs(&self) -> usize;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be replaced by has_external_inputs, in which case the eval_external function is a constant circuit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant