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