Skip to content

refactor(jolt-field): Land Solinas field from akita-field in jolt-field - #1684

Open
Acentelles wants to merge 41 commits into
a16z:mainfrom
Acentelles:feat/solinas-field-stack
Open

refactor(jolt-field): Land Solinas field from akita-field in jolt-field#1684
Acentelles wants to merge 41 commits into
a16z:mainfrom
Acentelles:feat/solinas-field-stack

Conversation

@Acentelles

@Acentelles Acentelles commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR does four things toward a single audited field implementation:

  1. Lands the Solinas field stack from akita-field in jolt-field: the 32-, 64-, and 128-bit prime fields, the FpExt2/FpExt4/FpExt8 extension towers, NEON/AVX2/AVX-512 packed backends, unreduced accumulator paths, tests, fuzz coverage, and benchmarks. One implementation, one Rust type identity: Akita consumes jolt-field directly, with no dependency back except a temporary feature-gated bootstrap adapter (see Changes).
  2. Consolidates the resulting trait surface per specs/consolidate-field-traits.md. The import brought the crate to 46 public traits across 82 files, with 15 one-trait-per-file micro-files at the root. The consolidation reduces this to 23 public traits (see the CanonicalBytes note under Changes) in four root trait modules (algebra.rs, canonical.rs, accumulator.rs, field.rs) plus the feature-gated backend modules, switches Solinas wire serialization to serde + bincode, and adopts the explicit arkworks/bn254.rs per-type layout. This is a refactor of trait boundaries, not arithmetic.
  3. Fixes two pre-existing jolt-field bugs surfaced by differential testing: Fp64::reduce_u128 truncated the fold multiply for sub-word moduli on inputs $\ge 2^{64+\text{BITS}}$, and the S160 two-limb multiply could overflow a fused u128 sum (a debug-only panic; release output was provably correct). Both fixed in 79944f2 with regression tests that fail pre-fix.
  4. Carries the jolt-field-two rebuild to completion. jolt-field-two is a from-scratch minimal-LOC rebuild of this crate, started in feat/field-crate-refactor (checkpoints 1–4 plus its working SPEC.md, cherry-picked here as 5635b98); checkpoints 5–9 (fp128, extensions, unreduced, packed, parallel) complete the spec's build order at 5,103 counted LOC against the 6,240 budget, versus the 11,410-LOC baseline, differential-tested against jolt-field as the oracle. It is intended to replace the implementation from items 1–2 in a follow-up PR; the per-checkpoint summary is in the comments below.
Akita source jolt-field destination (post-consolidation)
crates/akita-field/src/traits.rs src/algebra.rs, src/canonical.rs, src/accumulator.rs, src/field.rs, src/solinas_traits.rs
crates/akita-field/src/prime/ src/prime/
crates/akita-field/src/ext/ src/ext/
crates/akita-field/src/packed/ src/packed/
crates/akita-field/src/unreduced/ src/unreduced/
crates/akita-field/src/fft.rs Not imported in final form: removed in the consolidation (zero consumers in this workspace)
crates/akita-field/src/parallel.rs src/parallel.rs
crates/akita-pcs/benches/field_arith.rs and field_arith/ benches/solinas_field_arith.rs and benches/solinas_field_arith/
Akita field arithmetic fuzz coverage fuzz/fuzz_targets/solinas_field_arith.rs

Changes

Solinas import

  • Add solinas and parallel features to jolt-field while retaining the existing BN254 default.
  • Import the complete Akita Solinas implementation (prime fields, extension towers, packed backends, unreduced/wide accumulators, parallel helpers).
  • Keep Akita proof and field serialization concerns out of the shared crate: no AkitaSerialize, no Akita validation policy or protocol framing. Add FieldError for backend-independent input and shape failures; Akita converts it at its repository boundary.
  • Keep src/akita.rs and the akita feature as a temporary, feature-gated bootstrap so the adapter stays buildable until the Akita cutover; both are removed in the final migration PR. akita-config/akita-field are optional dependencies behind that feature only.
  • Update jolt-akita to the current Akita runtime ring dimension, opening claim, setup, commitment, and batching APIs.
  • Move the Solinas Criterion benchmark into jolt-field, add a Solinas arithmetic fuzz target, and record file-level source provenance.
  • Add a CI feature matrix for backend-free, BN254-only, Solinas-only, and combined builds, plus scripts/check-shared-field-identity.sh.

Trait consolidation (spec phases 1–5)

  • Phase 1 — delete dead surface. Remove traits with no generic consumer in this workspace or Akita: both signed-accumulator families (SignedScalarAccumulator/WithSmallScalarAccumulator, SignedProductAccumulator/WithSignedProductAccumulator), ExtensionCoeff, ScaleI32, BalancedDigitLookup (now a free function), and SmoothFftField together with the 1,100-line fft.rs.
  • Phase 2 — one accumulator trait. Merge AdditiveAccumulator + RingAccumulator into a single Accumulator.
  • Phase 3 — root traits and wire format. FieldCore absorbs Invertible and RandomSampling; FromPrimitiveInt absorbs MulPow2 and MulPrimitiveInt; a new CanonicalRepr replaces the seven byte/introspection/challenge traits (CanonicalBytes, ReducingBytes, FixedByteSize, FixedBytes<N>, CanonicalU64, CanonicalBitLength, TranscriptChallenge). Solinas wire serialization becomes serde + bincode, matching the existing serde implementations on arkworks/bn254.rs; Fiat-Shamir transcript bytes keep the explicit canonical little-endian encoding and never go through bincode. A later commit (ff5bf9c) re-splits the byte surface back out as CanonicalBytes, now a supertrait of CanonicalRepr, after feat: Akita lattice PCS integration — verifier path and shared semantics #1675's NoCommitment showed that transcript absorption should not require the full field-decode contract; the final surface is 23 public traits.
  • Phase 4 — extension cluster. ExtField<F> absorbs LiftBase, MulBase, and FrobeniusExtField; FpExt4MulBackend + FpExt8MulBackend merge into one ExtMulBackend; PackedField absorbs PackedValue; the degree-4 mul/square schedule now exists in exactly one place, shared between scalar and packed backends.
  • Phase 5 — per-type layout. Everything a concrete type implements is visible in that type's file, as an explicit impl or a one-line shared-macro invocation; the three native_algebra.rs side-files are replaced by one shared macro module.

BN254 is untouched by the consolidation: Fr's serde and byte encodings, the Fiat-Shamir stream, and static dispatch are unchanged (spec invariants 1 and 6). MontgomeryConstants is retained pending confirmation that no out-of-tree GPU/Metal consumer exists (spec OPEN item; 23 traits become 22 when resolved).

Testing

  • muldiv e2e passes in --features host and --features host,zk after the consolidation.
  • jolt-field feature matrix passes: backend-free (--no-default-features), BN254-only, Solinas-only, combined, and Solinas-plus-parallel.
  • mersenne61_compat (in jolt-sumcheck) compiles and passes against jolt-field --no-default-features with bounds updated to the merged traits: the slim hierarchy remains implementable without arkworks.
  • New bincode round-trip tests for Fp32/Fp64/Fp128/FpExt2/FpExt4/FpExt8; serialized-size tests assert each element encodes to exactly NUM_BYTES bytes and a Vec of $n$ elements to $n \cdot \texttt{NUM BYTES}$ plus one length prefix.
  • A compile test verifies the merged CanonicalRepr challenge derivation matches the previous TranscriptChallenge behavior on Fr (identical bytes in, identical element out).
  • The solinas_field_arith fuzz target and both Criterion benches build; bench results before/after the consolidation are within noise.
  • scripts/check-shared-field-identity.sh reports one jolt-field identity and no akita-field in the integrated dependency graph.
  • Byte-for-byte comparison of a standard-mode muldiv proof against a main-built proof: identical in all 63,371 bytes, proving is deterministic run-to-run, and the main-built verifier accepts the branch-built proof. (Reproduction note: cfg(test) builds randomize the Dory URS per process via DoryGlobals::configure_test_cache_root, so cross-process comparison requires pinning both builds to a shared URS cache.)
  • Regression tests for the Fp64::reduce_u128 and S160 fixes fail on the pre-fix code.
  • jolt-field-two: 121 differential tests green across the full feature matrix, plus +avx2 and +avx512f,+avx512dq compile cross-checks against x86_64-apple-darwin.

Security Considerations

This change touches security-sensitive field arithmetic used by the PCS and therefore affects the trusted audit surface for prover and verifier correctness. The intended arithmetic change is zero across both stages: moduli, canonical representations, extension bases, reduction algorithms, and packed kernel behavior are preserved, and architecture-specific arithmetic kernels (NEON, AVX2, AVX-512) are moved without semantic modification.

For the import stage, 22 of the imported files are byte-for-byte copies of their Akita sources and 38 are adapted imports; the main semantic adaptations are removal of Akita serialization bounds and replacement of protocol-level AkitaError with the narrower FieldError. The consolidation stage then rewrites trait boundaries only: merges are compile-time renames, #[inline] discipline is carried over onto both trait methods and impls, and all dispatch remains static. The net effect is a smaller audit surface: 46 public traits become 23, and the crate root drops from 15 trait micro-files to 4 trait modules.

Breaking Changes

  • Removes akita-field from Jolt's dependency graph (jolt-field keeps optional akita-* dependencies behind the temporary akita bootstrap feature only).
  • Deletes or merges 24 public traits; downstream code generic over the old names (Invertible, RandomSampling, TranscriptChallenge, LiftBase, MulBase, FrobeniusExtField, AdditiveAccumulator, PackedValue, ...) must rebind to the survivors (FieldCore, CanonicalBytes, CanonicalRepr, ExtField, Accumulator, PackedField, ...); CanonicalBytes survives with a slimmer, bytes-only contract. Akita adapts at its next pin bump.
  • Removes fft.rs and SmoothFftField from jolt-field.
  • Solinas types' wire format is now serde + bincode (BN254/Fr encodings unchanged).

Move the Solinas field stack from Akita revision
3b674abb66186037745af256e6a20d60bdcd7e89 into jolt-field behind a new
`solinas` feature: 32/64/128-bit pseudo-Mersenne prime fields, degree
2/4/8 extension fields, packed NEON/AVX2/AVX-512 backends, unreduced
accumulators, smooth-domain FFT helpers, and a `parallel` feature
gating rayon.

Jolt's existing capability traits remain canonical; Solinas-only
operations live in narrow capability traits in solinas_traits.rs.
FieldError carries field-layer input and size failures only.

The temporary `akita` feature, its optional akita-config/akita-field
git-pinned dependencies, and src/akita.rs are retained as a bootstrap
edge so the current jolt-akita revision keeps building; they are
removed in the final migration PR.
Move the Criterion field-arithmetic benchmark from Akita as
benches/solinas_field_arith (with plonky3 baselines as dev-only
comparisons) and add a Solinas arithmetic fuzz target to the field fuzz
workspace.
New field-stack job checks jolt-field with no backend, BN254, Solinas,
and combined backends, runs the Solinas test suite, and enforces the
shared field package identity: exactly one jolt-field in the workspace
graph, with the pre-cutover akita-field allowed only from the immutable
bootstrap Git pin. The final migration PR tightens the check to reject
every akita-field identity.
@github-actions

Copy link
Copy Markdown
Contributor

Warning

This PR has more than 500 changed lines and does not include a spec.

Large features and architectural changes benefit from a spec-driven workflow.
See CONTRIBUTING.md for details on how to create a spec.

If this PR is a bug fix, refactor, or doesn't warrant a spec, feel free to ignore this message.

@github-actions github-actions Bot added the no-spec PR has no spec file label Jul 17, 2026
@Acentelles Acentelles changed the title Land the shared Solinas field stack in jolt-field refactor(jolt-field): Land the shared Solinas field stack in jolt-field Jul 17, 2026
@Acentelles Acentelles closed this Jul 17, 2026
@Acentelles Acentelles reopened this Jul 17, 2026
@Acentelles Acentelles changed the title refactor(jolt-field): Land the shared Solinas field stack in jolt-field refactor(jolt-field): Land Solinas field from akita-field in jolt-field Jul 17, 2026
@Acentelles
Acentelles marked this pull request as ready for review July 17, 2026 01:49
@Acentelles

Copy link
Copy Markdown
Collaborator Author

Once LayerZero-Labs/akita#307 is merged, we can remove the temporary akita-field compatibility dependency from jolt-field and change jolt-akita from the temporary jolt-field/akita feature to jolt-field/solinas.

@markosg04
markosg04 self-requested a review July 17, 2026 01:59
Acentelles and others added 8 commits July 21, 2026 19:01
Plan to shrink jolt-field from 46 public traits to at most 22: delete
dead surface, merge single-purpose capability traits, adopt the
arkworks/bn254.rs per-type impl style, and move wire serialization of
the Solinas types to serde + bincode. Supersedes the granularity of
unify-field-hierarchy.md while preserving its layering invariants.
Remove traits with no generic consumer in this workspace or akita:

- SignedScalarAccumulator/WithSmallScalarAccumulator and
  SignedProductAccumulator/WithSignedProductAccumulator families,
  including the Fr and Naive implementations and the two bn254_ops
  kernels only they used; the Field umbrella no longer requires them
- ExtensionCoeff (single blanket impl; bounds inlined at use sites)
- BalancedDigitLookup (replaced by the free fn balanced_digit_lut)
- SmoothFftField and fft.rs (no consumer outside their own tests)

MontgomeryConstants is retained pending confirmation that no
out-of-tree GPU backend consumes it.
…phase 2)

AdditiveAccumulator and RingAccumulator were only ever implemented and
consumed together (WideAccumulator, NaiveAccumulator); merge them into
a single Accumulator trait carrying add/merge/reduce plus the fmadd
family. WithAccumulator moves into accumulator.rs and now supertraits
RingCore + FromPrimitiveInt, which its associated type already implied.

Because WithAccumulator now guarantees Accumulator<Element = Self> by
declaration, the 18 'Accumulator: RingAccumulator<Element = F>'
where-clauses across jolt-crypto, jolt-blindfold, and jolt-verifier are
redundant and removed.
…rmat (spec phase 3)

Merge the fine-grained capability traits into cohesive ones:

- FieldCore absorbs Invertible (inverse, inv_or_zero) and RandomSampling
- FromPrimitiveInt gains a RingCore supertrait and absorbs the MulPow2
  and MulPrimitiveInt default-method helpers
- new CanonicalRepr (the Fiat-Shamir transcript surface) replaces
  CanonicalBytes, ReducingBytes, FixedByteSize, FixedBytes<N>,
  CanonicalU64, CanonicalBitLength, and TranscriptChallenge; per-type
  challenge derivations (Fr's masked 125-bit path) move over verbatim
- the crate root shrinks from 15 one-trait micro-files to algebra.rs,
  canonical.rs, accumulator.rs, and field.rs

Wire serialization of the Solinas types is now serde + bincode,
mirroring Fr's existing canonical [u8; N] pattern: prime fields encode
as exactly NUM_BYTES bytes with checked canonical deserialization,
extension fields as [F; K]. Fiat-Shamir bytes stay on CanonicalRepr's
explicit encoding, never bincode. New serde_roundtrip tests pin
per-element sizes, single length prefixes on vectors, and rejection of
non-canonical encodings, so proof size cannot grow.

The mersenne61_compat and Gf2 compatibility tests keep passing with
merged bounds and no arkworks dependency.
…phase 4)

- ExtField<F> absorbs LiftBase (lift_base), MulBase (mul_base), and
  FrobeniusExtField (frobenius_pow / frobenius_inv_pow); one trait now
  carries the full extension contract. The FpExt2/4/8 impls require a
  pseudo-Mersenne base, which every current base is.
- FpExt4MulBackend and FpExt8MulBackend merge into a single
  ExtMulBackend with the same three implementors; Fp32 keeps its fused
  delayed-reduction quartic overrides.
- The degree-4 mul/square schedule now exists once: the packed
  PackedField defaults call the shared fp_ext4_mul_coeffs /
  fp_ext4_square_coeffs (previously copy-pasted verbatim), matching how
  degree-8 already shares its schedule.
Every concrete type's trait surface now lives in its own file, with
three shared macros carrying the mechanical expansions:

- impl_native_ring_algebra! / impl_native_additive! (new
  native_algebra.rs) replace the three per-module native_algebra.rs
  side-files, including the 226 hand-written lines in ext/; invocations
  sit in each type's file
- impl_prime_ops! collapses the 30 hand-duplicated operator impl blocks
  across Fp32/Fp64/Fp128 into 3 invocations; the *_raw reduction
  kernels stay hand-written per type
- select_packing! collapses the three near-identical cfg cascades that
  choose the packed backend per prime width
- impl_prime_native_capability! invocations move into the per-type
  files

Also finishes the disposition table: ScaleI32 merges into ReduceTo,
PackedValue merges into PackedField, and jolt-field's OptimizedMul is
deleted outright (jolt-prover-legacy has its own identical trait; the
jolt-field copy had no consumer).

jolt-field is now at 22 public traits. Criterion solinas_field_arith
before/after shows no regression on any of the 20 benchmarks.
The fuzz crate's target/ directory (630 generated files) was accidentally
committed in d8b2830. Remove it from the index and ignore it going forward.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added spec Tracking issue for a feature spec implementation PR contains implementation of a spec and removed no-spec PR has no spec file labels Jul 22, 2026
- repair the from_bytes fuzz target's duplicate import (the fuzz crate
  is its own workspace, so workspace clippy never compiles it)
- dedupe mechanical-rename residue bounds in the solinas benches and
  jolt-sumcheck's SumcheckScalar
- move the Solinas prime traits (CanonicalField, HalvingField,
  PseudoMersenneField, balanced_digit_lut) from the crate root into
  prime/traits.rs per the spec's layout
- amend the spec: OptimizedMul was deleted as a duplicate of
  jolt-prover-legacy's own trait rather than kept; record the actual
  Solinas-trait location and the montgomery_constants.rs remainder
@Acentelles
Acentelles force-pushed the feat/solinas-field-stack branch from 8d58e10 to 9aff1cd Compare July 22, 2026 20:28
Standard-mode muldiv proofs from this branch and main are identical in
all 63,371 bytes when both builds share a Dory URS, and the main-built
verifier accepts the branch-built proof. cfg(test) builds randomize the
URS per process (DoryGlobals::configure_test_cache_root), so the
comparison requires pinning a shared URS cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@markosg04 markosg04 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think there is still hopefully some room to clean up this crate and especially the surface area of the solina-related traits and impls.

You can find a spec and the first few components implemented for such additional cleanup here: https://github.com/a16z/jolt/tree/feat/field-crate-refactor. The tl;dr is that I think this crate can probably be done in half the src loc than what it is now, and we can make the distinction between the backend and impl for the solina stack clearer. I also am generally a fan of using macros for these types of primitive crates because AI are very efficient with them and they seemingly help to catch/prevent some context drift.

Two arithmetic bugs found by the jolt-field-two differential harness
(see PR a16z#1684 review), both predating this PR's refactor:

- Fp64::reduce_u128 cast the fold's high part to u64. For sub-word
  primes (40/48/56-bit) inputs >= 2^(64+BITS) reduced incorrectly,
  which includes essentially all 16-byte Fiat-Shamir challenge
  derivations. The fold multiply now stays in u128 (bounded by 2^127
  since the registry enforces C(C+1) < P). Fp32's fold was already
  correct.

- S160::mul_magnitudes fused two (three) full 64x64 products into one
  u128 cross-term sum, overflowing for large second limbs: debug panic,
  release wraparound. Word 1 now uses overflow-tracked accumulation
  (the true carry into word 2 reaches 2^65 and must include the
  overflow bit at weight 2^64); word 2 uses wrapping adds since only
  its low 32 bits survive in the 160-bit result. The N=1 arm was safe
  (32-bit high limb); the general N>=3 path already accumulated safely.
  The identical S160 kernel exists in the arkworks fork and needs the
  same fix there (reported separately).

Regression tests: u128-mod oracle over all registered sub-word primes
incl. the challenge-bytes path, and a 32-bit-digit schoolbook oracle
for S160 with boundary limbs. Both tests fail against the unfixed
kernels.
# Conflicts:
#	Cargo.lock
#	crates/jolt-field/src/akita.rs
#	crates/jolt-field/src/transcript_challenge.rs
#	crates/jolt-verifier-derive/src/lib.rs
#	crates/jolt-verifier/src/verifier.rs
#	crates/jolt-witness/src/field_inline/mod.rs
- differential asserts for from_scalar_challenge_bytes on the Solinas
  words (all lengths) and on Fq (the Fr side already had one)
- drop a dead binding left by the un-gating commit
- SPEC amendments: impl_group_ops! is a third exported stamping macro;
  the registry has 9 offsets at this baseline, not 12; note that
  PseudoMersenne is defined unconditionally in algebra.rs per the file
  table
… split

The baseline's byte surface (NUM_BYTES, to_bytes_le) moved from
CanonicalRepr to its new CanonicalBytes supertrait; import both where
the oracles compare encodings.
One hand-written module (528 counted LOC vs the 700 budget) porting the
baseline prime/fp128/{core,add_sub,mul,reduce,wide,primes,traits}:
[u64; 2] lo/hi representation, branchless carry-chain add/sub,
schoolbook 2x2 mul/sqr with the AArch64 inline-asm kernels kept
verbatim (1.29x on M4 per the baseline's own measurement; an
AArch64-only unit test cross-checks asm vs portable on all four
offsets), C = 2^a +/- 1 shift specialization, <=10-limb Solinas fold,
Fermat inversion, rejection-sampling random, and the four registered
128-bit primes re-exported under their baseline names.

Every reduction step carries a re-derived bound comment. One sharpening
over the baseline's argument: fold2_canonicalize is exact and canonical
for any u64 third limb given only C < 2^32 (overflow leaves the partial
sum below 2^96, so the single +C correction cannot re-overflow); the
C(C+1) < P assert is kept and documented as implied for the fused
paths.

Dropped with evidence recorded in SPEC.md: the add/sub asm kernels
(~470 lines, no in-tree benchmark, same algorithm as the branchless
portable path), mul_add and its asm, mul_wide_limbs, from_i64_const
(akita-only consumers, drop already approved).

Differential suite: per-offset parity vs the baseline Fp128 (ops
owned+by-ref, algebra identities, bincode wire bytes, canonical and
wrong-length rejection, byte/num_bits/u64 views, challenges at
8/16/32), an independent 4x64-limb schoolbook + long-division oracle,
boundary sets, and all-max limbs at every fold length 0-10. Also fixes
tests/solinas_words_differential.rs, which did not compile under
--all-features since the CanonicalBytes split (the reconcile pass ran
without the solinas feature - the recurring feature-gate lesson).

39/39 tests, clippy all-features clean, no-default-features and
solinas-only builds clean.
Contracts in src/extension.rs (54/60 counted): ExtField (degree, lift,
mul_base, coefficient access, Frobenius) and Ext2Config with the
NegOneNr/TwoNr ZSTs. MulBaseUnreduced is deferred to checkpoint 7 with
its reason recorded: the contract is stated in terms of
Unreduced::Product, which does not exist yet.

Lane-generic deg-4/8 coefficient schedules live in a new unconditional
src/schedules.rs (133/140): the PseudoMersenne kernel-hook defaults in
algebra.rs and the packed lanes (checkpoint 8) share the same formulas,
so they sit in the root layer as backend-neutral algebra; the old
ext.rs budget is re-carved 890 -> 750 + 140 with the component total
unchanged. PseudoMersenne gains the four hooks (ext4/8 mul + square);
an ext8_square hook is new relative to the baseline (which squared
degree 8 via full mul) - value-identical, fewer ops, flagged in SPEC.

Headline: the baseline's fused-accumulation Fp32 deg-4 override LOST
the SPEC's checkpoint-6 bench gate - generic default 12.3 ns/op vs
fused 31.1 ns/op for mul (2.5x), 15.3 vs 28.3 for square, on
aarch64/M4 with the ported override reproducing the baseline's own
timing (31.0) as the sanity anchor. Immediate word-sized reductions
pipeline better than u128 accumulation chains here. The override is
dropped from src with evidence in SPEC.md; the port (with re-derived
column-sum bounds) survives in benches/ext4_kernels.rs for x86-64
re-evaluation, where the verdict could flip.

Impls in src/solinas/ext.rs (617/750): Karatsuba FpExt2 with norm
inverse, FpExt4 subfield-tower inverse, FpExt8 Gaussian-elimination
inverse, Frobenius via base-modulus powers, Moore thetas/solve/
validate, serde as [F; K] coefficient arrays byte-identical to the
baseline.

Differential suite (25 new tests, 64/64 all-features total): five base
fields x {FpExt2 both configs, FpExt4, FpExt8, Moore} against the
baseline plus an independent schoolbook oracle (binary-long-division
modular arithmetic, phi-rule Chebyshev multiply), boundary coefficient
patterns, ring-identity spot checks, fixed seeds. Baseline oddities
recorded: the fused-square bits!=32 guard has no recorded rationale;
NegOneNr is a field on no registered prime (all are 1 mod 4); baseline
ext types implement no canonical-bytes trait, so ext types here are
deliberately not JoltField (parity).
…t 7)

Contracts (src/unreduced.rs, 18/70): one fused Unreduced companion
surface (Product/SmallProduct/Wide, SUM_IS_EXACT, widening muls,
reduce_product/reduce_small_product/reduce_wide, scale_wide) replacing
the baseline's HasUnreducedOps + HasWide + ReduceTo, and Fold
(precompute/fold_one) replacing HasOptimizedFold, documented as the
multilinear bind. The checkpoint-6-deferred MulBaseUnreduced lands in
extension.rs (60/60 exactly).

Impls (src/solinas/unreduced.rs, 501/530): the i32-lane wide types,
u128-slot product accumulators, AccumPair, fold matrices, and the fused
kernels with explicit carry tracking everywhere two full-width products
meet (overflowing adds; the P^2-bias branch tracks add-carry and
sub-borrow jointly, with the impossible underflow guarded by debug
panic). One wrapping-ops policy across product accumulators (the
baseline mixed conventions in Fp128MulU64Accum); FpExt8 gets single
generic impls where the baseline stamped three copies.

Baseline corrections found by re-derivation, recorded in SPEC.md:
- lane headroom is exactly 32768 additions, not the baseline's
  "~32,769" (32769 * 0xFFFF > 2^31 - 1); tested at the boundary and
  one-past asserts in debug
- FpExt4Fp32ProductAccum docs claimed 7*P^2 ~ 2^65 and 2^63 terms;
  correct is < 2^67 and >= 2^61, re-derived per slot
- the fused FpExt2<Fp64> path silently assumed NR in {-1, 2} while
  generic over any config; now debug-asserted
- Fp128 accumulator headroom sharpened to 2^64 - 1 terms (the reduce
  carry chain binds)

NEON intrinsics dropped with evidence: LLVM auto-vectorizes the
portable [i32; N] ops to the identical add.4s/sub.4s/neg.4s on
aarch64 (checked via --emit asm), plus mul.4s for lane scaling which
the baseline never vectorized; ~120 unsafe lines gone and debug builds
now catch lane overflow.

Tests: 38 new (102/102 all-features): per-accumulator exactness vs
per-term multiplication and an independent limb-schoolbook oracle,
adversarial all-max batches over every carry corner, wrap-through
sequences, 18 fold-parity instantiations vs baseline, MulBaseUnreduced
override parity, SUM_IS_EXACT flag parity.

Budget flag for review: solinas/mod.rs sits at 108 vs 90 (was 102
before this checkpoint; re-exports and registry accumulate across
checkpoints) - needs the budget discussion, not golf.
One packed-algebra source of truth: the entire Solinas fold/
canonicalize algebra (fp32 2-or-3-fold with TWO_FOLD_OK, C = 2^a +/- 1
shift-add offset multiply, N-ary deferred-reduction dot products with
the BITS==32 prefold, fused deg-4 kernels; fp64 dual-path add/sub and
both reduce128 variants; fp128 SoA carry-chain add/sub) written once in
engine.rs/fp128.rs, generic over a SimdWord vocabulary; the genuinely
algorithmic per-ISA differences confined to simd.rs (AVX2's missing
64-bit multiply assembled from 32x32 products, AVX-512 mask-register
selects and native mullo_epi64, NEON's vqdmulhq_s32 kernel for the
31-bit prime). Note for review: the SPEC pillar says engine *macro*;
generic types over SimdWord achieve the same single-source outcome with
less expansion (recorded in SPEC.md). Packed ext consumes the shared
schedules.rs; select_packing! cfg cascade ported token-identically.

Counted LOC 1,067 vs the 1,600 budget (engine 284/550, fp128 99/350,
ext 191/230, simd 352/350). Two overages flagged for review: packed.rs
105/90 (the ext4/8 kernel hooks on Packed - overridable defaults are
the only stable-Rust route for the fp32 engines' fused kernels) and
packed/mod.rs 36/30 (cfg formatting of the cascade).

Improvements over baseline: packed Fp128 mul calls the scalar kernel
per lane on all ISAs (on aarch64 that is the inline-asm multiply,
strictly better than the baseline NEON backend's duplicated portable
fold); the baseline's implicit C < 2^(64-BITS) assumption in
reduce128_small_k is now debug-asserted. Dropped with SPEC evidence:
Mersenne31 C==1 kernels (no registered prime), BITS==31 fold clones,
NEON per-C shift-add chains, NEON dot-product carry tracking (unified
on prefold), vectorized ext2/4 inverses (inversion is lane-serial in
every formulation). Baseline oddity: NEON PackedFp64::fp_ext2_mul
override is byte-identical to the trait default (dead specialization).

cfg coverage: every introduced region compiled - native NEON (tests
RUN), +avx2 and +avx512f,+avx512dq x86_64-apple-darwin checks, plain
x86 fallback, aarch64 -neon fallback, no-default-features and
solinas-only. 14 new differential tests (116/116 all-features):
packed-vs-scalar for all 12 registered primes across widths, packed
ext over fp32/fp64/fp128 bases and both NR configs, lane/slice laws,
NEON lane-exact differentials vs the baseline packed types.

x86 backends are compile-verified only on this machine; performance
re-evaluation of dropped specializations is bench-gated per SPEC.
…kpoint 9)

src/solinas/parallel.rs (78/80 counted): the baseline rayon helpers
ported whole, byte-faithful (expansion-site cfg so consumers dispatch
on their own parallel feature). The consumer audit found that none of
the seven cfg_*! macros has a single consumer anywhere in the
workspace or the rebuild - no crate even enables jolt-field's parallel
feature (crates that parallelize carry their own rayon deps) - so the
component is recorded in SPEC.md as a deletion candidate at the
replacement PR; parity scope names it explicitly, hence the whole
port rather than an unevidenced subset. New tests/parallel_macros.rs
proves serial/rayon equivalence for every macro (green under both
configurations).

Crate docs on lib.rs (two-layer architecture, backends, features,
byte-compatibility invariants); code lines drop to 43/70 since docs
are free under the counting rules.

Final audit written into SPEC.md with actuals beside budgets:
5,103 counted LOC vs the 6,240 budget (18% under; 55% below the
11,410-LOC baseline). Six files over their file budgets, all in one
consolidated budget-trades note: packed.rs 105/90 (ext kernel hooks),
solinas/mod.rs 113/90 (re-exports accumulate), limbs.rs 237/220,
bn254/mont.rs 310/300, packed/mod.rs 36/30, packed/simd.rs 352/350.

Feature matrix all clean (check + clippy -D warnings): no-default,
solinas, bn254, solinas+parallel, solinas+allocative, all-features,
plus both x86_64 target-feature cross-checks in two feature configs.
121/121 tests. SPEC status: all nine checkpoints built; remaining
before replacement recorded (x86 runtime validation, bench
re-evaluations, the replacement PR itself).
@Acentelles

Acentelles commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Per checkpoint, relative to the spec:

  • Checkpoint 5 — fp128 (e1cbc17, 528/700): one hand-written two-limb module, as specified. The AArch64 mul/sqr assembly is kept verbatim (with an asm-vs-portable cross-check test); the add/sub assembly is dropped under the spec's evidence rule — no in-tree benchmark supported it and the portable path compiles branchless. One bound sharpening over the baseline docs: fold2_canonicalize is exact and canonical for any u64 third limb given only $C &lt; 2^{32}$; the $C(C+1) &lt; P$ assert is kept but documented as implied for that kernel.
  • Checkpoint 6 — extensions (ae313af, 54 + 133 + 617): the spec's bench-before-keep gate produced its first reversal — the baseline's fused Fp32 deg-4 kernel measures 2.5x slower than the generic schedule on M4 (12.3 vs 31.1 ns/op; the ported override reproduces the baseline's own 31.0 as the anchor). Dropped from src, preserved in benches/ext4_kernels.rs for an x86 re-run that could flip the verdict. The deg-4/8 multiplication schedules moved to an unconditional schedules.rs so scalar hook defaults and packed lanes share one source of truth; the ext budget was re-carved 890 → 750 + 140 with the component total unchanged.
  • Checkpoint 7 — unreduced (d4959dd, 18 + 501): the fused Unreduced surface replacing HasUnreducedOps/HasWide/ReduceTo, plus Fold, per the trait table. Re-derivation corrected three baseline headroom docs (lane headroom is exactly 32768; the FpExt4⟨Fp32⟩ accumulator bound is $7P^2 &lt; 2^{67}$ with $\ge 2^{61}$ headroom terms) and the fused FpExt2⟨Fp64⟩ path's silent NR ∈ {−1, 2} assumption is now debug-asserted. The NEON accumulator intrinsics are dropped with disassembly evidence: LLVM emits identical add.4s/sub.4s/neg.4s from portable code, and additionally vectorizes the lane scaling the intrinsics never covered.
  • Checkpoint 8 — packed (19e88a7, 1,067/1,600): one packed-algebra source of truth, generic over a per-ISA SimdWord vocabulary. This is a recorded deviation from the spec's "engine macro" phrasing — same single-source outcome, but trait generics instead of a macro; rationale in SPEC.md. Genuine per-ISA differences are confined to simd.rs. Every cfg region is compile-covered (native NEON runs the differentials; +avx2 and +avx512f,+avx512dq cross-checks target x86_64-apple-darwin). One upgrade over baseline: packed Fp128 multiplication calls the scalar kernel per lane, which on aarch64 is the assembly multiply the baseline's NEON backend never used.
  • Checkpoint 9 — parallel + final audit (58785fa, 78/80 + docs): the rayon helpers ported whole. The audit found that none of the seven cfg_*! macros has a single consumer in the workspace — no crate enables the parallel feature — so the component is flagged in SPEC.md as a deletion candidate for the replacement PR.

Items needing reviewer judgment (consolidated in SPEC.md): six files exceed their per-file budgets (largest overage +23 lines; the recurring cost centers are re-export blocks and the four ext kernel hooks on Packed, which need overridable defaults to be reachable in stable Rust); both bench verdicts are single-machine (M4) and the dropped kernels should be re-benched on x86 before replacement; the x86 backends are compile-verified only.

@markosg04 markosg04 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice work! Can we now delete and replace jolt-field with the new crate (and hence update rest of Jolt / akita)?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's move this to specs/ and rename

…ap adapter

Pre-replacement surface work: split the byte surface out of
CanonicalEncoding as a bare CanonicalBytes supertrait (same decision as
the jolt-field split: transcript absorption must not require the field
decode contract, e.g. NoCommitment), and port the temporary akita
bootstrap adapter to the new spine so the akita lanes survive the
replacement. 121/121 tests, clippy clean on all feature lanes.
…lden byte fixtures)

- tests/bn254_differential.rs: jolt-field oracle replaced with num-bigint
  arithmetic mod r/q (modpow inverses, exact wide-accumulator sums, and
  the legacy Fr Montgomery-form / Fq plain challenge models); wire bytes
  now checked structurally, absolute bytes pinned in golden_bytes.rs.
- tests/limbs_signed_differential.rs: Limbs/SignedBigInt/SignedBigIntHi32
  parity vs jolt-field replaced with exact num-bigint integer oracles
  (wrapping sign-magnitude model); existing u128/i128 oracles kept.
- tests/solinas_words_differential.rs: baseline arms dropped; u128/bigint
  oracles extended to half, num_bits, challenge decodes, balanced-digit
  LUT, and the random-stream spec; registry and PRIME_OFFSET_* constants
  pinned as fixture data.
- tests/solinas_fp128_differential.rs: baseline arms dropped; limb oracle
  extended to wide multiplies, conversions, and decodes; the
  rejection-sampling stream checked against a test-local reimplementation
  of its spec.
- tests/solinas_ext_differential.rs: baseline parity dropped; schoolbook
  oracle extended to add/sub/neg and integer embeddings; frobenius_pow
  checked against x^(q^k) computed with the oracle-verified multiply;
  Moore checks made intrinsic (basis thetas, solution satisfies system,
  rejections, must-succeed in genuine fields).
- tests/solinas_unreduced_differential.rs: baseline HasUnreducedOps arms
  dropped; delayed-sum exactness kept vs the schoolbook oracle and
  per-term ring ops; SUM_IS_EXACT values pinned; fold_one checked against
  the field identity e + r(o - e).
- tests/solinas_packed_differential.rs: aarch64 baseline_diff module
  removed (packed==scalar plus scalar==oracle in the other suites covers
  it transitively); expected NEON lane widths pinned.
- tests/golden_bytes.rs: NEW. Golden fixtures generated from jolt-field at
  5b3e39e pinning bincode and
  to_bytes_le encodings for every registered prime field, the ext towers
  over three base widths, BN254 Fr/Fq, and the legacy Fr/Fq challenge
  derivations (generator deleted after use; regeneration instructions in
  the file header).
- tests/parallel_macros.rs, tests/spine.rs: untouched (no oracle use).
- benches/ext4_kernels.rs: jolt-field baseline timing columns removed;
  the generic-vs-fused-port comparison is unchanged.
- Cargo.toml: jolt-field dev-dependency removed; num-bigint (workspace)
  added as a dev-dependency.
Delete the baseline crates/jolt-field (11,410 counted LOC) and move the
rebuild into its place under the package name jolt-field (5,103 counted
LOC). MontgomeryConstants dies with the old crate: zero consumers in the
workspace, closing the spec OPEN item. The old crate's differential
evidence survives via the oracle-free suites and golden byte fixtures
generated from it before deletion. Consumers rebind in the follow-up
commits; the workspace does not build between this commit and the rebind.

125/125 tests and clippy clean on -p jolt-field, all features/targets.
…pine

Scripted, import-guided rename over ~400 files: umbrella Field ->
JoltField, FieldCore -> Field, RingCore/FromPrimitiveInt -> Ring,
CanonicalRepr -> CanonicalEncoding, from_le_bytes_mod_order ->
from_bytes_le_reduced, to_canonical_u64_checked -> to_u64_checked,
arkworks::bn254 paths -> root exports. Position-aware for Field:
associated types named Field (CommitmentScheme<Field = F>, syn::Field)
and all ::-qualified paths keep their names; ark_ff::Field collision
files and akita-field's shared trait names handled by hand.

Two surface restorations in jolt-field, recorded as deviations in
specs/jolt-field-rebuild.md: bn254 From<primitive> impls (94+ call
sites relied on the plain arkworks conversions), and JoltField drops
its serde bounds while the akita bootstrap edge exists (the foreign
pre-cutover type cannot implement serde here; restore at cutover).
mersenne61_compat reworked against the new spine, proving the slim
hierarchy remains implementable without arkworks.

Gates: clippy --all-targets -D warnings green on host, host+zk,
akita x3, field-inline, and +avx2/+avx512 cross-checks to
x86_64-apple-darwin.
…uild

Proof bytes verified unchanged: standard-mode muldiv proofs from the
pre-replacement build and the replacement branch are byte-identical in
all 63,372 bytes under a pinned Dory URS; ZK sizes equal at 65,947.
Test battery: 3,901 tests green across workspace default, prover-legacy
host/zk/akita, verifier akita fixtures, and solinas-only field lanes;
muldiv e2e passes both modes. All clippy lanes and x86 target-feature
cross-checks clean. Remaining-work list updated to post-replacement
items (x86 runtime validation, bench re-evals, CI SIMD lane, parallel
helpers, akita cutover follow-ups).
…o the rebuilt jolt-field

Main's a16z#1690 landed BlindFold ZK support in the modular jolt-prover
written against the pre-replacement trait names. Conflicts (7 files,
rename-only on our side) resolved by taking main's content; the rebind
was then re-applied over all main-side .rs changes. Main-only names
fixed by hand: RingAccumulator -> Accumulator (pre-consolidation name
that never existed on this branch), and two helpers bounded F: Field
that need the JoltField umbrella under the rebuilt hierarchy.

Gates on the merge: clippy host and host,zk (--all-targets,
-D warnings), jolt-prover suites in both modes, muldiv e2e in both
modes, all green.
@Acentelles

Copy link
Copy Markdown
Collaborator Author

The replacement contemplated in specs/jolt-field-rebuild.md ("Remaining before replacement", item 3) is now on this PR: the baseline jolt-field implementation is deleted and jolt-field-two has taken over crates/jolt-field under the package name jolt-field, with every consumer rebound to the new trait names. The PR now delivers the end state directly rather than two crates side by side.

Commit sequence:

  • db8e650 — pre-swap surface work in the rebuild: the byte surface split out of CanonicalEncoding as a bare CanonicalBytes supertrait (same reasoning as the earlier CanonicalRepr split: transcript absorption, e.g. NoCommitment, must not require the field decode contract), plus the temporary akita bootstrap adapter ported to the new spine.
  • 2b800e1 — the rebuild's differential suites made oracle-free so the evidence survives the baseline's deletion: independent num-bigint/schoolbook oracles for arithmetic, and golden byte fixtures generated from the baseline before deletion pinning bincode wire bytes, to_bytes_le, checked decodes, and the BN254 challenge conventions (125 tests, up from 121). Three spots are honestly weaker and are listed in the commit; the biggest is that BN254 absolute-byte pinning now rests on fixture vectors rather than random cross-crate sampling.
  • cf8a66a — the swap. MontgomeryConstants dies with the baseline: zero consumers in the workspace, closing the spec's OPEN item.
  • 079356e — the consumer rebind, ~400 files: umbrella FieldJoltField, FieldCoreField, RingCore/FromPrimitiveIntRing, CanonicalReprCanonicalEncoding, method renames, arkworks::bn254 paths → root exports. mersenne61_compat reworked against the new spine, keeping the "implementable without arkworks" property demonstrated.
  • b2e3086 — validation evidence recorded in the spec.
  • 46e2ce8 — merge of main: feat(jolt-prover): BlindFold ZK support in the modular prover #1690's BlindFold modular-prover code rebound to the new names (RingAccumulatorAccumulator, plus two F: Field bounds that need the JoltField umbrella now that Field is the narrower algebra trait).

Validation (all recorded in specs/jolt-field-rebuild.md under "Replacement validation evidence"):

  • Proof bytes unchanged. A standard-mode muldiv proof from the pre-replacement build and one from the replacement, generated against a shared pinned Dory URS, are byte-identical in all 63,372 bytes. ZK proofs are randomized (BlindFold), so the applicable check is size equality: 65,947 bytes on both builds.
  • Tests: 3,901 green across workspace default, prover-legacy host/zk/akita, verifier akita fixtures, and solinas-only lanes; muldiv e2e passes in both modes, including on the merge of main.
  • Lints: clippy --all-targets -- -D warnings on host, host+zk, the three akita lanes, field-inline; +avx2 and +avx512f,+avx512dq cross-compiles to x86_64-apple-darwin.

Two surface deviations from the rebuild spec were forced by consumers and are recorded in the spec's "Replacement-time deviations": JoltField drops its serde bounds while the akita bootstrap edge exists (the foreign pre-cutover type cannot implement serde here; restore at cutover), and the bn254 types regain the primitive From conversions the plain arkworks re-exports carried (94+ call sites).

The spec's "Remaining after replacement" section lists what stays open: x86-64 runtime validation of the packed/fp128 paths (still compile-verified only), the fused-kernel bench re-run on x86, a CI target-feature lane so SIMD is not CI-dark, the zero-consumer parallel helpers (delete or wire a consumer), and the akita cutover follow-ups.

… rebuilt jolt-field

Third rebind-the-delta sync. 20 conflicted files, all rename-only on
our side, taken from main and re-rebound; pre-consolidation accumulator
names swept proactively this round. Spec correction: the parallel
helpers' deletion candidacy is withdrawn — Akita's cutover branch
consumes jolt_field::parallel at 32 sites, so the checkpoint 9
zero-consumer verdict was workspace-blind.

Gates: clippy host and host,zk (--all-targets, -D warnings), muldiv e2e
both modes, green.
- check-shared-field-identity.sh: pass --color never to cargo tree; CI's
  colorized (*) dedup marker defeated the sed strip, making one identity
  count as two. The dependency graph was always correct.
- typos: exclude the golden byte fixtures (hex substrings false-positive).
- ext4_kernels bench: compile to a stub without the solinas feature so
  the bench workflow's --bench '*' succeeds under default features; the
  print_stdout expectation moves onto the gated module.
- fuzz: restore the five fuzz targets deleted with the old crate,
  rebound to the rebuilt trait names (the fuzz workspace is invisible to
  workspace clippy and the fuzz CI job cd's into it).
- jolt-verifier: drop an unused import only compiled under the
  prover-fixtures,zk lane.
@socket-security

socket-security Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedlibfuzzer-sys@​0.4.134710093100100

View full report

@socket-security

socket-security Bot commented Aug 4, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: cargo zerocopy is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?cargo/inferno@0.12.6cargo/rand_chacha@0.3.1cargo/rand_chacha@0.9.0cargo/inferno@0.11.21cargo/ark-ec@0.5.0cargo/zerocopy@0.8.55

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/zerocopy@0.8.55. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

…x lanes

The BN254 fixture constants and shared helpers compiled as dead code
under solinas-only and backend-free lanes, which build with -Dwarnings
in CI.
…-level gate

The file-level cfg covers backend-free builds; the BN254 fixture
constants additionally need their own gate for the solinas-only lane.
All four matrix lanes verified locally with -Dwarnings.
Upgrade from the --color never mitigation to rendering-independent
package-ID parsing (requested in Akita a16z#307 review: prefer cargo
metadata over cargo tree text). CARGO_TERM_COLOR=always is forced
inside the script as a permanent regression guard for the colorized
(*) marker bug.
Port the upstream Akita sampling change (LayerZero-Labs/akita@03f29087)
to the shared Solinas backend: Field::random on Fp32, Fp64, and Fp128
now samples through sample_uniform_below, which reads exactly
ceil(modulus_bits / 8) little-endian bytes per attempt, clears unused
high bits, and rejects non-canonical candidates. The byte-consumption
contract is deterministic for a fixed RngCore stream and documented on
Field::random; extension fields sample base coefficients independently
through the same contract.

Replaces the previous biased single-reduction paths on the word fields
and aligns the differential-test oracle with the new contract.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

implementation PR contains implementation of a spec spec Tracking issue for a feature spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants