Skip to content

Add support for having Z[X] and F_q[X] typed constraints simultaneously, with two "branching proofs" #186

@albert-garreta

Description

@albert-garreta

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).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions