Skip to content

PCS abstraction: crate-owned traits + BLS12-381 KZG backend - #73

Draft
gabriel-barrett wants to merge 13 commits into
mainfrom
pcs-traits
Draft

PCS abstraction: crate-owned traits + BLS12-381 KZG backend#73
gabriel-barrett wants to merge 13 commits into
mainfrom
pcs-traits

Conversation

@gabriel-barrett

@gabriel-barrett gabriel-barrett commented Aug 16, 2026

Copy link
Copy Markdown
Member

Generalizes the prover/verifier core over a crate-owned trait layer so it can run
against either FRI or KZG, then proves the abstraction by landing a second backend:
monomial KZG over BLS12-381 via arkworks. Design doc: docs/pcs-abstraction.md
(includes the phased plan and what's deferred).

Phase 0 — crate-owned traits, behavioral no-op

The core (prover, verifier, system, eval, graph, lookup) now imports no
p3 proof-system traits — only p3_matrix (container), p3_util (log2), and
p3_maybe_rayon (parallelism) remain as utility libraries. The measured surface
lives in src/traits/:

  • Transcript — Fiat-Shamir operations (associated types, inference-unambiguous)
  • EvaluationDomain + LagrangeSelectors — coset domains and selector math
  • Pcs — commit / commit_quotient / open / verify, plus max_quotient_degree()
    as an explicit cost-model query (FRI: the blowup wall; KZG: an FFT budget)
  • the field layer — Field, TwoAdicField, Algebra, Packed, ExtensionOf
    (with D = 1 first-class), PackedExtension

Naming is natural (no prefixes); where a backend collides, the backend's import
is renamed (use p3_commit::Pcs as P3Pcs). No blanket impls. The p3 instantiation
moved into p3_adapter/ (now a directory), including everything
FRI-representation-specific about the quotient commit.

Safety gate: a proof_bytes_pin test pins the serialized proof hash of a fixed
system+witness; it held byte-identical through every Phase-0 commit, so the FRI
path is provably unchanged. All ix-side pins are unaffected.

Notable core change: the quotient is committed as coefficient slices
(Q(X) = Σₖ X^{k·n}·cₖ(X), recombined at ζ by the verifier) with backends owning
the representation — the FRI adapter reproduces its previous bytes exactly (pinned),
and the convention is what makes KZG's quotient commit a single coset iFFT.

Phase 1 — ark_adapter/ (feature kzg)

The second implementation of the traits, additive and feature-gated (default build
untouched):

  • Scalar: serde-able newtype over Fr; its own challenge field (D = 1
    |Fr| ≈ 2²⁵⁵ needs no extension) and its own width-1 packing (MSM dominates this
    prover, not the constraint sweep)
  • Radix2Coset: crate-owned domain with the p3 selector semantics reimplemented
    and pinned by a pointwise on-coset/at-point consistency test
  • Blake3Transcript: byte-oriented absorb/squeeze chain; samples Fr by 64-byte
    wide reduction
  • Srs: plain data — public parameters are the library user's to supply (any
    powers-of-tau ceremony, monomial form, truncatable to a power of two).
    Srs::validate() checks an untrusted load (G1 powers form one τ-progression
    against the G2 pair; two MSMs + one 2-pairing product). unsafe_dev_setup is
    tests/dev only and loudly marked.
  • KzgPcs: per-column iFFT + MSM commits (~48 B/column); opening batches all
    polynomials per distinct point under one challenge into a single witness
    commitment (proof = one G1 per distinct point, query-free); verification is one
    2-pairing product, cross-batched over points. τ·G1/τ·G2 are bound into the
    transcript seed.

End-to-end: the full multi-stark pipeline proves and verifies under KzgConfig on
hand-authored CircuitInputs — prove/verify, tampering + wrong-claim rejection,
serde round-trip, and a two-height system exercising multi-point batching. The
two-circuit test proof is 1,781 bytes (queries don't exist under KZG).

Decisions a reviewer will question

  • Hand-rolled KZG instead of ark-poly-commit: the shipped crate doesn't match
    our Pcs shape (multi-matrix rounds, coefficient-slice quotient commits); the
    backend is ~450 lines shaped exactly to the trait, using only
    ark-ec/ark-ff/ark-poly. The batching is the textbook multi-point scheme
    (per-point fold under v, cross-point fold under r, both post-commitment).
  • BLS12-381 despite no efficient outer curve: KZG is the terminal stage —
    verified natively, never in-circuit. All protocol logic (transcript, OOD sweep)
    is scalar-field; base-field arithmetic exists only inside the PCS (MSMs,
    pairing, point decompression), which is constant-size and deferrable by design.

Known limitations (deliberate, Phase 2)

  • No ceremony loader yet (Ethereum/Sapling/Filecoin transcripts are all monomial
    and fit Srs directly; validate() is ready for them)
  • KZG prover is serial (no parallel MSM/FFT) — measure first at scale
  • The p3 AIR frontend stays p3-only; KZG circuits are hand-authored
    CircuitInputs
  • Not hiding, matching the existing protocol's documented not-ZK stance
  • No IxVM hookup yet

Review order

src/traits/src/p3_adapter/ (mechanical delegation + the quotient-commit
relocation with its pinning tests) → core ports (config, prover, verifier,
system) → src/ark_adapter/.

CI runs the test suite with parallel,kzg; check/clippy cover --all-features;
new dependencies are all MIT OR Apache-2.0 (blake3 adds CC0, already allowed).

docs/pcs-abstraction.md: the plan for generalizing the prover/verifier
over a crate-owned trait layer (field/challenge/domain/challenger/pcs/
config), with Plonky3 (FRI) and arkworks (KZG/BLS12-381) behind
adapters. Grounded in the measured coupling surface (6 pcs + 5
challenger + 3 domain call shapes) and a quirk inventory mined from the
defunct origin/kzg rewrite (monomial vs coset-Lagrange commitment,
quotient-domain acquisition, the blowup cap as a PCS-reported economy,
chunking retention, per-column commitment granularity, transcript).

proof_bytes_pin: serial-build proofs are deterministic (byte drift is a
"parallel"-feature artifact), so pin blake3 of a fixed system+claim
proof. Every Phase-0 refactor commit must keep this hash; a deliberate
protocol change bumps it in the same commit with reasoning.
First slice of the PCS abstraction (docs/pcs-abstraction.md, Phase 0):
the Fiat-Shamir surface the core uses — observe field / field slice /
challenge / commitment, sample challenge — moves onto the crate-owned
traits::Transcript. Associated types rather than generics: a transcript
serves exactly one (field, challenge, commitment) triple per config, so
associated types keep every call site inference-unambiguous and avoid
the unconstrained-parameter problems a blanket impl over an erased base
field hits (E0207).

p3_adapter becomes a directory: air.rs is the existing AIR frontend
(unchanged, still re-exported at the old paths), challenger.rs
implements Transcript for the production DeterministicPow challenger by
delegation; the BabyBear test config carries the second instantiation,
which is that config's whole purpose. prover.rs, verifier.rs and
system.rs no longer import p3_challenger at all.

Also pin rustfmt.toml (edition only, default style): the nested-in-ix
checkout otherwise resolves ix's rustfmt config from the parent
directory and reformats the whole crate.

Behavioral no-op: proof_bytes_pin unchanged, 32 tests green, clippy
clean.
…rom p3

Second slice: the evaluation-domain surface — size, first_point,
next_point (total, not Option: two-adic domains always have one),
create_disjoint_domain, selectors_at_point, selectors_on_coset — moves
onto traits::EvaluationDomain with a crate-owned LagrangeSelectors, and
the config's Pcs bound requires Domain: EvaluationDomain. prover.rs and
verifier.rs no longer import PolynomialSpace; the (zeta, zeta*g)
opening pairs and the quotient-sweep selectors go through the crate
trait.

p3_adapter/domain.rs delegates for the production Goldilocks coset
domain (renaming the colliding p3 import: LagrangeSelectors as
P3LagrangeSelectors); the BabyBear test config carries the second
instantiation, on the concrete coset type — coherence cannot see
through a Pcs-projection alias in an impl header. system.rs keeps a
transitional PolynomialSpace<Val = F> bound where the config's Val
alias still ties through p3; that knot dissolves with the Pcs slice.

Behavioral no-op: proof_bytes_pin unchanged, 32 tests green, clippy
clean.
Third and largest slice: the commitment surface moves onto
traits::Pcs — natural_domain_for_degree, commit, commit_quotient,
get_evaluations_on_domain, open, verify, and the max_quotient_degree
degree-budget query — with crate-owned OpenedValues*/OpeningRounds/
VerifyRounds data shapes. Our trait takes the natural name; the p3
trait is renamed at the adapter imports (p3_commit::Pcs as P3Pcs).

The semantic shift is commit_quotient: the prover hands over quotient
EVALUATIONS on the disjoint domain plus the quotient degree, and the
backend owns everything representation-specific from there. The FRI
side of that — shifted_quotient_slices, lde_from_shifted_coefficients,
the shift-equals-generator cancellation, the commit_ldes call, and the
non-hiding assert — moves verbatim into p3_adapter/pcs.rs along with
its pinning tests. FriPcs wraps TwoAdicFriPcs with the blowup (which
p3 keeps private; it answers max_quotient_degree = 1 << log_blowup,
the LDE-subsetting economy) and a DFT engine for the quotient path.

config.rs now projects every alias (Val, Domain, Com, PcsProof,
PcsError, PcsData, EvaluationsOnDomain) through the crate trait, and
StarkGenericConfig's challenger bound is Transcript alone — no p3
challenger or commit trait appears anywhere in config/prover/verifier/
system; system.rs's transitional PolynomialSpace bound is gone. The
one deliberate fancy construct is the Evaluations<'a> GAT (borrowed
LDE views beat copies in the sweep); the F bounds (Clone + Send +
Sync + 'static) are container needs, satisfied by any field element
type including ark's.

Behavioral no-op: proof_bytes_pin unchanged, 32 tests green (including
the relocated quotient pins), examples and benches build, clippy
clean.
commit_quotient is evaluations-based (the evals->coeffs transform needs
a DFT engine, a backend property, so the conversion lives behind the
trait); record the naming convention (natural names, backend imports
renamed at adapters), the associated-types/no-blanket-impls decisions,
and slice status.
Final Phase-0 slice: the field surface moves onto crate traits, sized
to the measured call sites and nothing more.

- Field: scalar ops, ZERO/ONE, inversion, exp, small-integer
  embeddings, powers, zero_vec, and an associated SIMD Packing (scalar
  width 1 is a valid backend choice). TwoAdicField on top.
- Algebra<F>: the sweep's working types (base field, its packing, the
  challenge field) — ring ops, embedding from F, scaling by F. The
  constraint engine (eval.rs) was already generic over exactly this.
- Packed<F>: WIDTH, slice reinterpretation, the alpha-fold batched
  linear combination, and the packed two-row window gather
  (packed_row_pair replaces the p3_matrix vertically_packed_row_pair
  call at the three sweep sites).
- ExtensionOf<F>: the challenge field as a binomial extension X^D = W
  with D >= 1 first-class. The D and W CONSTANTS replace
  extension_params' runtime recovery trick (which evaluated X^D and
  required D >= 2 — exactly what the KZG/BLS12-381 D = 1 case breaks).
  PackedExtension carries the packed-challenge fold surface.
- batch_inverse (Montgomery) and flatten_to_base as free functions;
  from_ext_basis rebuilt over basis constructors.

p3_adapter/field.rs: scalar impls by macro delegation (absolute paths —
macro_rules resolves at expansion site); packings wrapped in the
generic repr(transparent) newtypes P3Packing<F>/P3ExtPacking<F, EF> so
every impl is constructor-headed and the arkworks adapter's scalar
packings can never collide. The air frontend keeps a public umbrella
trait (p3 Field + crate Field) since it converts between the layers by
definition.

config/prover/verifier/system/eval/expr/graph/lookup no longer import
p3_field at all; the one wart is that a type implementing Algebra over
several bases (the challenge field) needs qualified ZERO/ONE at ~six
sites. p3_matrix stays as the container library, p3_util's log2 and
p3_maybe_rayon as utilities.

Behavioral no-op: proof_bytes_pin unchanged, 32 tests green, examples
and benches build, clippy clean.
Phase 1 of docs/pcs-abstraction.md: the second implementation of the
crate traits, proving the abstraction generalizes beyond plonky3.

- field: Scalar, a repr(transparent) serde-able newtype over Fr. Its
  own challenge field (ExtensionOf with D = 1, first-class per the
  trait contract) and its own packing (WIDTH = 1): MSM dominates this
  prover, not the constraint sweep.
- domain: Radix2Coset, the crate-owned evaluation domain with the p3
  coset selector semantics reimplemented over Scalar and pinned by a
  pointwise on-coset/at-point consistency test.
- transcript: Blake3Transcript, a byte-oriented absorb/squeeze hash
  chain sampling Fr by 64-byte wide reduction.
- srs: powers of tau in G1 plus [H, tau*H]; unsafe_dev_setup for
  tests, ceremony loading left for Phase 2.
- pcs: KzgPcs. Commit = per-column iFFT + MSM (one G1 per column).
  The quotient commit consumes the core's coefficient-slice
  convention directly: one coset iFFT, slices are ranges of the
  coefficient vector. Trace evaluations on the quotient domain are
  coset FFTs from stored coefficients, budgeted by
  max_quotient_degree (no blowup wall: exceeding it is slow, not
  unsound). Opening batches all polynomials per distinct point under
  one transcript challenge into a single witness commitment; a proof
  is one G1 point per distinct point, query-free. Verification folds
  the same combination over commitments and checks every point with
  one 2-pairing equation, cross-batched by a second challenge.
- config: KzgConfig, binding tau*G1/tau*G2 into the transcript seed.

The whole multi-stark pipeline proves and verifies under KzgConfig on
hand-authored CircuitInputs (the p3 AIR frontend stays p3-only):
prove/verify, tampering and wrong-claim rejection, serde round-trip,
and a two-height system exercising multi-point batching. The default
build is untouched (proof_bytes_pin unchanged).
Public parameters are the library user's to supply — Srs stays plain
data loadable from any powers-of-tau ceremony, and the library's side
of the contract is transparency: the transcript already binds
tau*G1/tau*G2; now Srs::validate lets an untrusted load be checked for
internal consistency (the G1 powers form one tau-progression against
the G2 pair), batched into two MSMs and a single 2-pairing product
under a combiner derived from the SRS bytes themselves. Subgroup
membership stays with validated deserialization.
Check and clippy already cover it via --all-features; without this the
adapter's tests never execute in CI. (proof_bytes_pin is unaffected:
it is gated on the parallel feature, not on kzg.)
@gabriel-barrett gabriel-barrett changed the title PCS traits PCS abstraction: crate-owned traits + BLS12-381 KZG backend Aug 16, 2026
Blowup, Merkle cap, query count, and PoW bits are properties of the
commitment scheme, but p3's TwoAdicFriPcs keeps them private — the
adapter already retained log_blowup for exactly that reason. Retain the
full CommitmentParameters/FriParameters instead and expose them back,
so downstream consumers (the Aiur verifying-key codec) read them off
the PCS rather than duplicating them at build time. Proof bytes pinned
unchanged.
The trait is the complete definition of one proof-system instance —
base field (via the PCS), challenge field, transcript, and degree
budgets — and the name should say that. "StarkGenericConfig" was a
p3-ism: awfully specific to one lineage while conveying nothing about
what the config configures.
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