PCS abstraction: crate-owned traits + BLS12-381 KZG backend - #73
Draft
gabriel-barrett wants to merge 13 commits into
Draft
PCS abstraction: crate-owned traits + BLS12-381 KZG backend#73gabriel-barrett wants to merge 13 commits into
gabriel-barrett wants to merge 13 commits into
Conversation
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.
gabriel-barrett
force-pushed
the
pcs-traits
branch
from
August 16, 2026 09:44
8180972 to
2c1d74f
Compare
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.
gabriel-barrett
force-pushed
the
pcs-traits
branch
from
August 16, 2026 12:35
2c1d74f to
99883b4
Compare
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.)
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 nop3 proof-system traits — only
p3_matrix(container),p3_util(log2), andp3_maybe_rayon(parallelism) remain as utility libraries. The measured surfacelives in
src/traits/:Transcript— Fiat-Shamir operations (associated types, inference-unambiguous)EvaluationDomain+LagrangeSelectors— coset domains and selector mathPcs— commit / commit_quotient / open / verify, plusmax_quotient_degree()as an explicit cost-model query (FRI: the blowup wall; KZG: an FFT budget)
Field,TwoAdicField,Algebra,Packed,ExtensionOf(with
D = 1first-class),PackedExtensionNaming 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 instantiationmoved into
p3_adapter/(now a directory), including everythingFRI-representation-specific about the quotient commit.
Safety gate: a
proof_bytes_pintest pins the serialized proof hash of a fixedsystem+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 owningthe 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/(featurekzg)The second implementation of the traits, additive and feature-gated (default build
untouched):
Scalar: serde-able newtype overFr; 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 reimplementedand pinned by a pointwise on-coset/at-point consistency test
Blake3Transcript: byte-oriented absorb/squeeze chain; samples Fr by 64-bytewide reduction
Srs: plain data — public parameters are the library user's to supply (anypowers-of-tau ceremony, monomial form, truncatable to a power of two).
Srs::validate()checks an untrusted load (G1 powers form one τ-progressionagainst the G2 pair; two MSMs + one 2-pairing product).
unsafe_dev_setupistests/dev only and loudly marked.
KzgPcs: per-column iFFT + MSM commits (~48 B/column); opening batches allpolynomials 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
KzgConfigonhand-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
ark-poly-commit: the shipped crate doesn't matchour
Pcsshape (multi-matrix rounds, coefficient-slice quotient commits); thebackend 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 underr, both post-commitment).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)
and fit
Srsdirectly;validate()is ready for them)CircuitInputsReview order
src/traits/→src/p3_adapter/(mechanical delegation + the quotient-commitrelocation 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).