Skip to content

zorch.sumcheck.verifier

Sumcheck verifier rounds -- the per-variable duals of the prover family.

SumcheckRound checks the round-poly identity and reduces the claim. It is summand-agnostic: it sees only the round polynomials, so one verifier serves every prover summand (product, LogUp, ...) at a given degree. The observe -> sample order matches prover.SumcheckRound.__call__ exactly, so the prover's and verifier's Fiat-Shamir transcripts cannot diverge.

CoeffsSumcheckRound is the same check for a prover that sends ascending coefficients instead of natural-domain values -- the wire form of a round interpolated off a non-natural node set (e.g. through an eq factor's root), where value form would force the verifier to know the sender's nodes. Like all other sumcheck rounds, it receives one shared ChallengePolicy; coefficient claims may therefore live in an extension of the transcript field.

SumcheckRound dataclass

Bases: VerifierRound

Verifier for any sumcheck round; the dual of prover.SumcheckRound.

Source code in zorch/sumcheck/verifier.py
39
40
41
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
@partial(
    frx.tree_util.register_dataclass,
    data_fields=[],
    meta_fields=["degree", "challenges"],
)
@dataclass(frozen=True)
class SumcheckRound(VerifierRound):
    """Verifier for any sumcheck round; the dual of `prover.SumcheckRound`."""

    degree: int
    challenges: ChallengePolicy

    def __post_init__(self) -> None:
        if self.degree < 1:
            raise ValueError("degree must be >= 1")

    def check_reduce(self, claim: Array, msg: Array, r: Array) -> tuple[Array, Array]:
        """The round identity + claim reduction alone, for an externally
        sampled challenge — the dual of `prover.SumcheckRound.round_poly`,
        so a driver whose choreography owns the Fiat-Shamir hop still routes
        the round math through one definition. Returns `(reduced, ok)`."""
        return reduce_evals(claim, msg, r, self.degree)

    def __call__(
        self, claim: RunningClaim, transcript: Transcript, msg: Array
    ) -> tuple[RunningClaim, Transcript, Array]:
        transcript, r = self.challenges.observe_and_sample(transcript, msg)
        reduced, ok = self.check_reduce(claim.value, msg, r)
        return claim.bind(reduced, r), transcript, ok

check_reduce

check_reduce(
    claim: Array, msg: Array, r: Array
) -> tuple[Array, Array]

The round identity + claim reduction alone, for an externally sampled challenge — the dual of prover.SumcheckRound.round_poly, so a driver whose choreography owns the Fiat-Shamir hop still routes the round math through one definition. Returns (reduced, ok).

Source code in zorch/sumcheck/verifier.py
55
56
57
58
59
60
def check_reduce(self, claim: Array, msg: Array, r: Array) -> tuple[Array, Array]:
    """The round identity + claim reduction alone, for an externally
    sampled challenge — the dual of `prover.SumcheckRound.round_poly`,
    so a driver whose choreography owns the Fiat-Shamir hop still routes
    the round math through one definition. Returns `(reduced, ok)`."""
    return reduce_evals(claim, msg, r, self.degree)

CoeffsSumcheckRound dataclass

Bases: VerifierRound

Verifier for a coefficient-form sumcheck round: s(0) = c_0 and s(1) = sum(c), so the identity check and the claim reduction read the coefficients directly.

Source code in zorch/sumcheck/verifier.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@partial(
    frx.tree_util.register_dataclass,
    data_fields=[],
    meta_fields=["degree", "challenges"],
)
@dataclass(frozen=True)
class CoeffsSumcheckRound(VerifierRound):
    """Verifier for a coefficient-form sumcheck round: `s(0) = c_0` and
    `s(1) = sum(c)`, so the identity check and the claim reduction read the
    coefficients directly."""

    degree: int
    challenges: ChallengePolicy

    def __post_init__(self) -> None:
        if self.degree < 1:
            raise ValueError("degree must be >= 1")

    def __call__(
        self, claim: RunningClaim, transcript: Transcript, msg: Array
    ) -> tuple[RunningClaim, Transcript, Array]:
        # Structural rejection precedes any read of the claim.
        require_width(msg, self.degree + 1, "coefficients")
        transcript, r = self.challenges.observe_and_sample(transcript, msg)
        reduced, ok = reduce_coeffs(claim.value, msg, r, self.degree)
        return claim.bind(reduced, r), transcript, ok

CompressedCoeffsSumcheckRound dataclass

Bases: VerifierRound

Verifier for the compressed coefficient wire (prover.CompressedProductRound): the message carries [c_0, c_2] of the degree-2 round polynomial; the linear coefficient never rides the wire and is reconstructed from the running claim — s(1) = claim - s(0) with s(0) = c_0, so c_1 = s(1) - c_0 - c_2. The reconstruction consumes the s(0) + s(1) == claim identity, so there is no per-round redundancy left to check (ok is constant true); binding rests on the terminal claim check, the trade the compressed form makes for wire size.

Source code in zorch/sumcheck/verifier.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
@partial(
    frx.tree_util.register_dataclass,
    data_fields=[],
    meta_fields=["challenges"],
)
@dataclass(frozen=True)
class CompressedCoeffsSumcheckRound(VerifierRound):
    """Verifier for the compressed coefficient wire
    (`prover.CompressedProductRound`): the message carries `[c_0, c_2]` of the
    degree-2 round polynomial; the linear coefficient never rides the wire and
    is reconstructed from the running claim — `s(1) = claim - s(0)` with
    `s(0) = c_0`, so `c_1 = s(1) - c_0 - c_2`. The reconstruction consumes the
    `s(0) + s(1) == claim` identity, so there is no per-round redundancy left to
    check (`ok` is constant true); binding rests on the terminal claim check,
    the trade the compressed form makes for wire size."""

    challenges: ChallengePolicy

    def check_reduce(self, claim: Array, msg: Array, r: Array) -> tuple[Array, Array]:
        """Reconstruct `c_1` from the claim and reduce, for an externally
        sampled challenge — mirrors `SumcheckRound.check_reduce`. `ok` is the
        constant true of the compressed form (the redundancy was spent on the
        reconstruction)."""
        return reduce_compressed(claim, msg, r)

    def __call__(
        self, claim: RunningClaim, transcript: Transcript, msg: Array
    ) -> tuple[RunningClaim, Transcript, Array]:
        transcript, r = self.challenges.observe_and_sample(transcript, msg)
        reduced, ok = self.check_reduce(claim.value, msg, r)
        return claim.bind(reduced, r), transcript, ok

check_reduce

check_reduce(
    claim: Array, msg: Array, r: Array
) -> tuple[Array, Array]

Reconstruct c_1 from the claim and reduce, for an externally sampled challenge — mirrors SumcheckRound.check_reduce. ok is the constant true of the compressed form (the redundancy was spent on the reconstruction).

Source code in zorch/sumcheck/verifier.py
116
117
118
119
120
121
def check_reduce(self, claim: Array, msg: Array, r: Array) -> tuple[Array, Array]:
    """Reconstruct `c_1` from the claim and reduce, for an externally
    sampled challenge — mirrors `SumcheckRound.check_reduce`. `ok` is the
    constant true of the compressed form (the redundancy was spent on the
    reconstruction)."""
    return reduce_compressed(claim, msg, r)

UnivariateSkipRound dataclass

Bases: VerifierRound

Verifier for the univariate skip's round 0 (sumcheck.univariate_skip): the message is the round polynomial s₀ in ascending-coefficient form (degree degree·(|D|−1), |D| = 2^skip_rounds), so the round identity is the SUBGROUP-sum check c == Σ_{z∈D} s₀(z)subgroup_sum reads it off the coefficients at multiples of |D| — and the claim reduces to s₀(r₀). The subgroup sibling of CoeffsSumcheckRound, whose hypercube identity (s(0)=c₀, s(1)=Σc) it swaps for the subgroup sum. Its ChallengePolicy selects the target field and squeeze width; evaluating the base coefficients at that challenge promotes the reduced claim as needed. skip_rounds is the number of collapsed leading rounds (|D| = 2^skip_rounds).

Source code in zorch/sumcheck/verifier.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
@partial(
    frx.tree_util.register_dataclass,
    data_fields=[],
    meta_fields=["skip_rounds", "degree", "challenges"],
)
@dataclass(frozen=True)
class UnivariateSkipRound(VerifierRound):
    """Verifier for the univariate skip's round 0 (`sumcheck.univariate_skip`): the
    message is the round polynomial s₀ in ascending-coefficient form (degree
    `degree·(|D|−1)`, |D| = 2^skip_rounds), so the round identity is the SUBGROUP-sum
    check
    `c == Σ_{z∈D} s₀(z)` — `subgroup_sum` reads it off the coefficients at multiples
    of |D| — and the claim reduces to `s₀(r₀)`. The subgroup sibling of
    `CoeffsSumcheckRound`, whose hypercube identity (`s(0)=c₀`, `s(1)=Σc`) it swaps for
    the subgroup sum. Its `ChallengePolicy` selects the target field and squeeze
    width; evaluating the base coefficients at that challenge promotes the reduced
    claim as needed. `skip_rounds` is
    the number of collapsed leading rounds (|D| = 2^skip_rounds)."""

    skip_rounds: int
    degree: int
    challenges: ChallengePolicy

    def __post_init__(self) -> None:
        if self.skip_rounds < 1:
            raise ValueError(
                "skip_rounds must be >= 1 (skip_rounds=0 is the plain sumcheck run)"
            )
        if self.degree < 1:
            raise ValueError("degree must be >= 1")

    def __call__(
        self, claim: RunningClaim, transcript: Transcript, msg: Array
    ) -> tuple[RunningClaim, Transcript, Array]:
        require_width(
            msg,
            self.degree * ((1 << self.skip_rounds) - 1) + 1,
            "coefficients",
        )
        transcript, r = self.challenges.observe_and_sample(transcript, msg)
        reduced, ok = reduce_subgroup(
            claim.value, msg, r, self.skip_rounds, self.degree
        )
        return claim.bind(reduced, r), transcript, ok