The following is a plan that Claude wrote. It explains (for the most part) what this task means.
Dual-prime extension to Zinc+ protocol (Folded 4× path)
Context
Today the Folded 4× protocol commits over Z[X], then projects to F_p[X]
where p is the secp256k1 base prime (compile-time fixed in
protocol/src/fixed_prime.rs). All constraints
flow through that single projection.
The user wants per-constraint typing: each constraint is tagged either
Z or F_p. After commit, the verifier sends p (still secp256k1, fixed)
and a fresh prime q (drawn from Fiat–Shamir). The protocol then runs
two parallel branches:
F_p-branch: every F_p-typed constraint is projected and checked
via the small random q (this is the "usual" path of the original
pre-fixed-prime protocol).
Z-branch: every Z-typed constraint is projected and checked via
the fixed p (current behavior, repurposed for the Z-branch only).
Both branches share randomness (r*, r_0) sampled in [0, min{p,q}-1].
After step 5 each branch produces evaluation claims at the same r_0
for the columns it touched; some columns may appear in both. A single
joint batched Zip+ open at r_0 discharges all column claims.
For the initial deliverable the user wants every ShaEcdsaUair constraint
tagged F_p, so the Z-branch is wired but empty in the bench. The
existing prove_folded_4x is preserved; a new
prove_folded_4x_dual_prime lives alongside it with a new bench group.
Decision on the tagging API: assert_in_ideal_typed on the existing
single Ideal type. Two-Ideal-types (IdealZ / IdealFp) is rejected as
too invasive — Sha256Ideal<R> already supports both rings, so attaching
a ConstraintRing enum tag per constraint slot is enough and avoids
splitting the constrain method.
Step-by-step plan
1. Constraint typing — UAIR layer
In uair/src/lib.rs:
- Add
pub enum ConstraintRing { Z, Fp } (Copy, Eq).
- Extend
ConstraintBuilder with assert_in_ideal_typed(&mut self, expr, &Self::Ideal, ConstraintRing). Default assert_in_ideal calls the
typed version with ConstraintRing::Z to preserve current semantics.
In uair/src/ideal_collector.rs:
- Replace
pub ideals: Vec<IdealOrZero<I>> with a parallel
pub tags: Vec<ConstraintRing> (default Z for assert_zero —
irrelevant since these are zero-ideal). assert_in_ideal_typed
pushes the tag.
collect_ideals returns both vectors via a new
IdealCollection { ideals, tags }.
This is additive: existing UAIRs that only call assert_in_ideal
continue compiling unchanged (all constraints become Z-tagged).
2. Ideal-check filtering — piop layer
In piop/src/ideal_check.rs, generalize the
prove/verify methods to accept a tag_filter: ConstraintRing and:
- Slot
i whose tag ≠ tag_filter is treated as the zero ideal
(substitute ZERO in the combined-poly slot, exactly the same path
assert_zero already takes via CombinedPolyRowBuilder::assert_zero).
- Final per-constraint
combined_mle_values are still produced for
every slot but the verifier only IdealCheck::contains against
matching slots.
This avoids forking the whole subprotocol. The two branches each call
prove_combined/prove_linear/prove_hybrid once with their tag.
3. Two prime sources — protocol layer
In protocol/src/prover.rs and
protocol/src/verifier.rs:
p_cfg = secp256k1_field_cfg::<F, Zt::Fmod>() — fixed, as today.
q_cfg = F::make_cfg(&fs_transcript.get_prime::<Zt::Fmod, Zt::PrimeTest>())
— drawn after the commitment is absorbed (this is exactly the
pre-fixed-prime path; get_prime already exists in
transcript/src/lib.rs:58).
- Both
q_cfg and p_cfg are absorbed into the transcript so the
verifier reconstructs them deterministically.
4. Dual-prime prover — prove_folded_4x_dual_prime
New entry point next to prove_folded_4x in
protocol/src/prover.rs. Pipeline:
- Step 0 — commit witness columns to Zip+ exactly as today (one
commit, shared by both branches; columns are still Z[X]).
- Step 1 — project the trace twice:
trace_q = project_trace_coeffs_*(trace, &q_cfg) for the F_p-branch.
trace_p = project_trace_coeffs_*(trace, &p_cfg) for the Z-branch.
Reuses project_trace_coeffs_row_major / _column_major from
piop/src/projections.rs — no new code.
Optimisation note: per-branch column subsets can be skipped only if
no constraint of the branch's tag touches them; first cut projects
every column for both branches (matches today's behavior). Defer
per-column dependency analysis to a follow-up.
- Step 2 — call
U::prove_combined(.., tag_filter = Fp, &q_cfg) and
U::prove_combined(.., tag_filter = Z, &p_cfg) against the same
shared FS transcript. Each emits its own IdealCheckProof<F> and
ic_eval_point (these point sets are independent FS draws — that's
fine for soundness; only r_0 needs cross-branch sharing).
- Step 3 / 4 / 5 — for each branch run
combined_poly_resolver, MultiDegreeSumcheck,
MultipointEval::prove_as_subprotocol on its projected trace and
field_cfg. The two MultipointEval calls produce two r_0
vectors drawn from the same shared transcript: implement this by
funneling the FS challenges for r_0 through a small helper
get_field_challenges_min_pq(num_vars, &p_cfg, &q_cfg) that draws
raw bytes once and reduces with the smaller modulus, then lifts to
both fields. This guarantees the same canonical residue is used in
both F_p and F_q. Run the F_p-branch first, then call the
Z-branch with the same r_0 reused (the Z-branch's
MultipointEval::prove_as_subprotocol is forked into a thin
prove_with_fixed_r0 variant that takes r_0 instead of sampling).
Add this variant in piop/src/multipoint_eval.rs.
- Step 6 — compute lifted evals at
r_0 for all witness
columns (binary, arbitrary, int) using the existing
compute_int_fold_4x_lifted_evals helper at
protocol/src/lib.rs (referenced by
protocol/src/prover.rs:2211). Lifted
evals are Z[X]-valued and shared across both branches; each branch
will project them via its own field_cfg for its sumcheck consistency
check on the verifier side.
- Step 7 — single joint batched Zip+ open at
r_0. Today this is
three separate ZipPlus::prove_f calls (one per witness type) that
already form a MultiZip3 batch — this is reused unchanged. The
point is that both branches reduce to claims at the same r_0 over
the same Z[X] commitments, so no second open is needed.
The new Proof struct (DualPrimeProof<F>) carries:
commitments, zip, q_modulus_bytes, ideal_check_p, ideal_check_q,
resolver_p, resolver_q, combined_sumcheck_p, combined_sumcheck_q,
multipoint_eval_p, multipoint_eval_q, witness_lifted_evals (single
shared list), lookup_proof (treated as Fp-branch only for now).
5. Dual-prime verifier — verify_folded_4x_dual_prime
In protocol/src/verifier.rs:
- Reconstruct
p_cfg from secp256k1_field_cfg.
- Read
q_cfg from proof.q_modulus_bytes and re-absorb in the
transcript ordering matching the prover.
- Run
step1_prime_projection twice to populate two type-state
branches (cleanest is to make VerifierPrimeProjected carry Vec<F::Config>
— minor refactor — or to introduce VerifierDualPrimeProjected mirroring
the existing single-prime states). Plan: add a parallel state-machine
family VerifierDualPrime* rather than retrofit the existing one;
this keeps verify_folded_4x exactly as-is.
- Steps 2/4/5 each run twice (one per branch); step 6 (lift) is shared
— reuse step6_lifted_evals on q_cfg for the F_p-branch's
consistency check and reproject lifted evals via p_cfg for the
Z-branch.
- Step 7 verifies the single batched Zip+ proof at
r_0 (unchanged).
The following is a plan that Claude wrote. It explains (for the most part) what this task means.
Dual-prime extension to Zinc+ protocol (
Folded 4×path)Context
Today the
Folded 4×protocol commits overZ[X], then projects toF_p[X]where
pis the secp256k1 base prime (compile-time fixed inprotocol/src/fixed_prime.rs). All constraints
flow through that single projection.
The user wants per-constraint typing: each constraint is tagged either
ZorF_p. After commit, the verifier sendsp(still secp256k1, fixed)and a fresh prime
q(drawn from Fiat–Shamir). The protocol then runstwo parallel branches:
F_p-branch: everyF_p-typed constraint is projected and checkedvia the small random
q(this is the "usual" path of the originalpre-fixed-prime protocol).
Z-branch: everyZ-typed constraint is projected and checked viathe fixed
p(current behavior, repurposed for theZ-branch only).Both branches share randomness (
r*,r_0) sampled in[0, min{p,q}-1].After step 5 each branch produces evaluation claims at the same
r_0for the columns it touched; some columns may appear in both. A single
joint batched Zip+ open at
r_0discharges all column claims.For the initial deliverable the user wants every
ShaEcdsaUairconstrainttagged
F_p, so the Z-branch is wired but empty in the bench. Theexisting
prove_folded_4xis preserved; a newprove_folded_4x_dual_primelives alongside it with a new bench group.Decision on the tagging API:
assert_in_ideal_typedon the existingsingle Ideal type. Two-Ideal-types (
IdealZ/IdealFp) is rejected astoo invasive —
Sha256Ideal<R>already supports both rings, so attachinga
ConstraintRingenum tag per constraint slot is enough and avoidssplitting the constrain method.
Step-by-step plan
1. Constraint typing — UAIR layer
In uair/src/lib.rs:
pub enum ConstraintRing { Z, Fp }(Copy, Eq).ConstraintBuilderwithassert_in_ideal_typed(&mut self, expr, &Self::Ideal, ConstraintRing). Defaultassert_in_idealcalls thetyped version with
ConstraintRing::Zto preserve current semantics.In uair/src/ideal_collector.rs:
pub ideals: Vec<IdealOrZero<I>>with a parallelpub tags: Vec<ConstraintRing>(defaultZforassert_zero—irrelevant since these are zero-ideal).
assert_in_ideal_typedpushes the tag.
collect_idealsreturns both vectors via a newIdealCollection { ideals, tags }.This is additive: existing UAIRs that only call
assert_in_idealcontinue compiling unchanged (all constraints become
Z-tagged).2. Ideal-check filtering — piop layer
In piop/src/ideal_check.rs, generalize the
prove/verify methods to accept a
tag_filter: ConstraintRingand:iwhose tag ≠tag_filteris treated as the zero ideal(substitute
ZEROin the combined-poly slot, exactly the same pathassert_zeroalready takes viaCombinedPolyRowBuilder::assert_zero).combined_mle_valuesare still produced forevery slot but the verifier only
IdealCheck::containsagainstmatching slots.
This avoids forking the whole subprotocol. The two branches each call
prove_combined/prove_linear/prove_hybridonce with their tag.3. Two prime sources — protocol layer
In protocol/src/prover.rs and
protocol/src/verifier.rs:
p_cfg = secp256k1_field_cfg::<F, Zt::Fmod>()— fixed, as today.q_cfg = F::make_cfg(&fs_transcript.get_prime::<Zt::Fmod, Zt::PrimeTest>())— drawn after the commitment is absorbed (this is exactly the
pre-fixed-prime path;
get_primealready exists intranscript/src/lib.rs:58).
q_cfgandp_cfgare absorbed into the transcript so theverifier reconstructs them deterministically.
4. Dual-prime prover —
prove_folded_4x_dual_primeNew entry point next to
prove_folded_4xinprotocol/src/prover.rs. Pipeline:
commit, shared by both branches; columns are still
Z[X]).trace_q = project_trace_coeffs_*(trace, &q_cfg)for the F_p-branch.trace_p = project_trace_coeffs_*(trace, &p_cfg)for the Z-branch.Reuses
project_trace_coeffs_row_major/_column_majorfrompiop/src/projections.rs — no new code.
Optimisation note: per-branch column subsets can be skipped only if
no constraint of the branch's tag touches them; first cut projects
every column for both branches (matches today's behavior). Defer
per-column dependency analysis to a follow-up.
U::prove_combined(.., tag_filter = Fp, &q_cfg)andU::prove_combined(.., tag_filter = Z, &p_cfg)against the sameshared FS transcript. Each emits its own
IdealCheckProof<F>andic_eval_point(these point sets are independent FS draws — that'sfine for soundness; only
r_0needs cross-branch sharing).combined_poly_resolver,MultiDegreeSumcheck,MultipointEval::prove_as_subprotocolon its projected trace andfield_cfg. The twoMultipointEvalcalls produce twor_0vectors drawn from the same shared transcript: implement this by
funneling the FS challenges for
r_0through a small helperget_field_challenges_min_pq(num_vars, &p_cfg, &q_cfg)that drawsraw bytes once and reduces with the smaller modulus, then lifts to
both fields. This guarantees the same canonical residue is used in
both
F_pandF_q. Run the F_p-branch first, then call theZ-branch with the same
r_0reused (the Z-branch'sMultipointEval::prove_as_subprotocolis forked into a thinprove_with_fixed_r0variant that takesr_0instead of sampling).Add this variant in piop/src/multipoint_eval.rs.
r_0for all witnesscolumns (binary, arbitrary, int) using the existing
compute_int_fold_4x_lifted_evalshelper atprotocol/src/lib.rs (referenced by
protocol/src/prover.rs:2211). Lifted
evals are
Z[X]-valued and shared across both branches; each branchwill project them via its own
field_cfgfor its sumcheck consistencycheck on the verifier side.
r_0. Today this isthree separate
ZipPlus::prove_fcalls (one per witness type) thatalready form a
MultiZip3batch — this is reused unchanged. Thepoint is that both branches reduce to claims at the same
r_0overthe same Z[X] commitments, so no second open is needed.
The new
Proofstruct (DualPrimeProof<F>) carries:commitments,zip,q_modulus_bytes,ideal_check_p,ideal_check_q,resolver_p,resolver_q,combined_sumcheck_p,combined_sumcheck_q,multipoint_eval_p,multipoint_eval_q,witness_lifted_evals(singleshared list),
lookup_proof(treated asFp-branch only for now).5. Dual-prime verifier —
verify_folded_4x_dual_primeIn protocol/src/verifier.rs:
p_cfgfromsecp256k1_field_cfg.q_cfgfromproof.q_modulus_bytesand re-absorb in thetranscript ordering matching the prover.
step1_prime_projectiontwice to populate two type-statebranches (cleanest is to make
VerifierPrimeProjectedcarryVec<F::Config>— minor refactor — or to introduce
VerifierDualPrimeProjectedmirroringthe existing single-prime states). Plan: add a parallel state-machine
family
VerifierDualPrime*rather than retrofit the existing one;this keeps
verify_folded_4xexactly as-is.— reuse
step6_lifted_evalsonq_cfgfor the F_p-branch'sconsistency check and reproject lifted evals via
p_cfgfor theZ-branch.
r_0(unchanged).