Skip to content

zorch.logup_gkr.jagged_verifier

Jagged LogUp-GKR verifier -- the dual of the jagged prover chain.

A JaggedGkrLayerRound replays one layer's coefficient-form sumcheck (the agnostic zorch.verify driver over sumcheck.verifier.CoeffsSumcheckRound), checks the LogUp oracle at the bound point via the shared logup_combine, then reduces the claim across the child selector -- the same (num_eval, den_eval, eval_point) carry the dense chain threads. The jagged layout never reaches the verifier: the prover's virtual-mass corrections make its round polynomials exactly those of the virtual dense hypercube, so the verifier is layout-blind and stays succinct (eval_eq, no 2^n vector).

The prover binds LSB-first, so the bound point is the sampled challenges reversed -- the one place the jagged dual differs from the dense verifier, whose MSB-first prover emits challenges already in point order.

It stops at the reduced point-claim. The final claim == leaf_mle(point) check needs a PCS opening of the input trace and is the consumer's, keeping this block PCS-agnostic; the roundtrip tests close it directly against the virtual dense leaf MLE.

JaggedGkrLayerRound

Bases: VerifierRound

Verify one jagged GKR layer; the chain of these is the jagged GKR verifier. Its ChallengePolicy must match the prover's because every challenge in the layer follows that one schedule.

Source code in zorch/logup_gkr/jagged_verifier.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
class JaggedGkrLayerRound(VerifierRound):
    """Verify one jagged GKR layer; the chain of these is the jagged GKR
    verifier. Its `ChallengePolicy` must match the prover's because every
    challenge in the layer follows that one schedule."""

    def __init__(self, challenges: ChallengePolicy) -> None:
        self.challenges = challenges

    def __call__(
        self, claim: LayerClaim, transcript: Transcript, layer_proof: JaggedLayerProof
    ) -> tuple[LayerClaim, Transcript, Array]:
        num_eval, den_eval, eval_point = claim
        n0, n1 = layer_proof.numerator_0, layer_proof.numerator_1
        d0, d1 = layer_proof.denominator_0, layer_proof.denominator_1
        transcript, lam = self.challenges.sample(transcript)
        claim = lam * num_eval + den_eval
        point, final_claim, transcript, ok_sc = verify(
            CoeffsSumcheckRound(_DEGREE, self.challenges),
            claim,
            layer_proof.round_polys,
            transcript,
        )
        # LSB-first binding: the last challenge bound the MSB, so the
        # MSB-first bound point is the sample order reversed.
        point = point[::-1]
        # The carry's eval_point must have one coordinate per sumcheck round, or
        # `eval_eq` reads the wrong eq weight (a degenerate length-1 carry would
        # broadcast silently against the bound point) -- reject, never broadcast.
        if eval_point.shape[0] != point.shape[0]:
            raise ValueError(
                f"eq point mismatch: claim has {eval_point.shape[0]} coords, "
                f"layer ran {point.shape[0]} rounds"
            )
        # LogUp oracle: the reduced claim equals the combine at the bound
        # point, with eq evaluated in closed form (both points MSB-first).
        eq_eval = eval_eq(eval_point, point)
        combined = logup_combine(lam, eq_eval, n0, d1, n1, d0)
        ok = ok_sc & (combined == final_claim)

        transcript, r = self.challenges.observe_and_sample(
            transcript, fnp.stack([n0, n1, d0, d1])
        )
        num_eval, den_eval, eval_point = fold_carry(n0, n1, d0, d1, point, r)
        return (num_eval, den_eval, eval_point), transcript, ok