Skip to content

zorch.pcs.basefold.verifier

BaseFold verifier — the verifier half of the multilinear PCS.

verify rebuilds the queried codeword leaves from the committed roots and checks the fold consistency of the batch open: the staggered RLC of the committed matrices' opened rows must agree with the batched codeword's first pair-leaf, and each fold layer's opened pair must fold to the next layer's, down to the constant final poly. It holds only the public params (code for the block geometry and fold, tree for the Merkle config) — never the prover's retained codeword.

The replay is driven by a (BasefoldConfig, BasefoldChoreography, SumcheckKernel) triple — the verify dual of BasefoldProver: the config fixes the fold schedule (commit cadence), the choreography fixes the Fiat-Shamir framing (message/root/ terminal observes, query sampling, grind checks), and the kernel owns the round algebra (the per-round round_check consistency + reduce_claim recurrence). BasefoldVerifier's defaults are zorch's native wire — so the plain BasefoldVerifier(code, tree, num_queries=…) construction replays byte-for-byte today's implementation and accepts/rejects identically. Prover and verifier must share ONE choreography + kernel so their Fiat-Shamir streams stay equal by construction. A byte-fixed consumer supplies its own config + choreography and drives verify_with_basis (raw basis, bind_statement's point=None) — the dual of BasefoldProver.open_with_basis.

BasefoldVerifier dataclass

Bases: VerifierStage[OpeningClaim[BasefoldCommitment], TrivialClaim, OpeningProof[BasefoldProof], TranscriptT], Generic[TranscriptT]

BaseFold PCS verifier. code fixes the block geometry + fold; tree the Merkle config; choreography the Fiat-Shamir wire (share the instance with the prover); config the fold schedule. The defaults are zorch's native wire — the plain BasefoldVerifier(code, tree, num_queries=…) construction replays byte-for-byte today's implementation. config=None derives the native per-verify config (commits_per_round, num_queries from the verifier).

Source code in zorch/pcs/basefold/verifier.py
 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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 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
129
130
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
@dataclass(frozen=True)
class BasefoldVerifier(
    VerifierStage[
        OpeningClaim[BasefoldCommitment],
        TrivialClaim,
        OpeningProof[BasefoldProof],
        TranscriptT,
    ],
    Generic[TranscriptT],
):
    """BaseFold PCS verifier. `code` fixes the block geometry +
    fold; `tree` the Merkle config; `choreography` the Fiat-Shamir wire (share
    the instance with the prover); `config` the fold schedule. The defaults are
    zorch's native wire — the plain `BasefoldVerifier(code, tree, num_queries=…)`
    construction replays byte-for-byte today's implementation. `config=None`
    derives the native per-verify config (`commits_per_round`, `num_queries`
    from the verifier)."""

    code: FoldableCode
    tree: MerkleTree
    # Must match the prover's; placeholder count, not soundness-calibrated.
    num_queries: int = 4
    choreography: BasefoldChoreography[TranscriptT] = BasefoldChoreography()
    kernel: SumcheckKernel = SumcheckKernel()
    config: BasefoldConfig | None = None

    def _resolved_config(self, num_vars: int) -> BasefoldConfig:
        """The config driving a verify over `num_vars` variables: the explicit
        one (checked against the point dimension) or the native default
        (`commits_per_round`, `num_queries` from the verifier). Mirrors
        `BasefoldProver._resolved_config` so both sides resolve identically."""
        if self.config is None:
            return BasefoldConfig(num_vars=num_vars, num_queries=self.num_queries)
        if self.config.num_vars != num_vars:
            raise ValueError(
                f"config.num_vars={self.config.num_vars} doesn't match the "
                f"verify's variable count {num_vars}"
            )
        return self.config

    def verify(
        self,
        claim: OpeningClaim[BasefoldCommitment],
        reduction_proof: OpeningProof[BasefoldProof],
        transcript: TranscriptT,
    ) -> VerifyResult[TrivialClaim, TranscriptT]:
        """Verify a single-matrix open — the degenerate one-round batch."""
        ok, transcript = self.verify_batch(
            [claim.commitment],
            claim.points,
            [reduction_proof.values],
            reduction_proof.proof,
            transcript,
        )
        return VerifyResult(TrivialClaim(), transcript, ok)

    def verify_batch(
        self,
        commitments: Sequence[BasefoldCommitment],
        points: Sequence[Array],
        values: Sequence[Array],
        proof: BasefoldProof,
        transcript: TranscriptT,
    ) -> tuple[Array, TranscriptT]:
        if len(points) != 1:
            raise ValueError(
                f"BaseFold opens the matrices at one shared point, got {len(points)}"
            )
        if not (len(commitments) == len(values) == len(proof.component_openings)):
            raise ValueError(
                f"batch mismatch: {len(commitments)} commitments, {len(values)} "
                f"value vectors, {len(proof.component_openings)} component openings"
            )
        z = points[0]
        num_vars = z.shape[0]
        # Eager shape guards, ahead of the jit zone (mirrors the prover): with
        # zero variables the fold replay below would index an empty layer list.
        if num_vars < 1:
            raise ValueError("BaseFold opens over at least one variable, got none")
        if self.code.message_len != (1 << num_vars):
            raise ValueError(
                f"point dimension {num_vars} doesn't match message_len "
                f"{self.code.message_len} (expected 2^{num_vars})"
            )
        self._check_proof_shape(proof, num_vars)
        # Fail loud on a non-native cadence / scheduled grind BEFORE any verdict,
        # symmetric to the prover's deferrals (P2 deferred, fail-loud).
        config = self._resolved_config(num_vars)
        config.require_native("verify")
        _require_no_grind(self.choreography, config)
        return _verify_batch_body(
            self, list(commitments), z, list(values), proof, transcript
        )

    def verify_with_basis(
        self,
        commitment: BasefoldCommitment,
        basis: Array,
        value: Array,
        proof: BasefoldProof | CadenceProof,
        transcript: TranscriptT,
    ) -> tuple[Array, TranscriptT]:
        """Verify a RAW-basis open — the dual of `BasefoldProver.open_with_basis`
        (`bind_statement` receives `point=None`), dispatching on the fold schedule
        exactly as the prover entry does.

        Under a non-native schedule (`row_batch_prefix` / `fold_arities`) this
        replays the generic cadence (row-batch prefix + multi-arity FRI epochs)
        against a `CadenceProof`, the symmetric dual of `_open_with_basis_cadence`:
        `commitment` is the prover's initial codeword root (bound, not observed —
        the outer protocol committed it), `value` the claimed target the kernel's
        `reduce_claim`/`verify_final` fold against.

        Under the native uniform schedule the per-round check evaluates the
        sumcheck message at the opening point's coordinates, which a raw basis
        lacks, so that path has no basis replay yet — a fail-loud consumer delta
        (the native binding also refuses `point=None`)."""
        if basis.shape[0] < 2:
            raise ValueError("BaseFold opens over at least one variable, got none")
        num_vars = log2_strict_usize(basis.shape[0])
        config = self._resolved_config(num_vars)
        if not config.commits_per_round:
            if config.row_batch_prefix == 0:
                raise NotImplementedError(
                    "non-native cadence verify is wired for a row-batch prefix "
                    "(row_batch_prefix > 0, the row-batch-prefix shape); the "
                    "prefix-free multi-arity sub-case commits no post-prefix "
                    "layer and needs its own bridge — not replayed here"
                )
            # The schedule fixes the proof type (dispatch mirrors the prover):
            # a non-native config carries a `CadenceProof`.
            return _verify_with_basis_cadence(
                self,
                commitment,
                basis,
                value,
                config,
                cast(CadenceProof, proof),
                transcript,
            )
        # Native single-MLE basis path (deferred, as before): a raw basis lacks
        # the opening point the native per-round check needs.
        if self.code.message_len != (1 << num_vars):
            raise ValueError(
                f"basis length {basis.shape[0]} doesn't match message_len "
                f"{self.code.message_len} (expected 2^num_vars)"
            )
        self._check_proof_shape(cast(BasefoldProof, proof), num_vars)
        _require_no_grind(self.choreography, config)
        # Bind the statement via the choreography with point=None (native refuses;
        # a basis consumer binds via the basis). Even a basis consumer then hits
        # the deferred basis-path replay below.
        self.choreography.bind_statement(transcript, commitment, None, value)
        raise NotImplementedError(
            "verify_with_basis's raw-basis replay is not wired here: the native "
            "per-round check evaluates the sumcheck message at the opening point, "
            "which a raw basis lacks; a basis consumer supplies its own per-round "
            "check from (message, basis), symmetric to open_with_basis"
        )

    def _check_proof_shape(self, proof: BasefoldProof, num_vars: int) -> None:
        """Fail loud on a structurally malformed proof — a short message/layer
        list would otherwise let the round loop silently skip checks."""
        if (
            len(proof.univariate_messages) != num_vars
            or len(proof.fri_roots) != num_vars
            or len(proof.query_openings) != num_vars
        ):
            raise ValueError(
                f"malformed proof: expected {num_vars} sumcheck messages / fold "
                f"layers, got {len(proof.univariate_messages)} / "
                f"{len(proof.fri_roots)} / {len(proof.query_openings)}"
            )

verify

verify(
    claim: OpeningClaim[BasefoldCommitment],
    reduction_proof: OpeningProof[BasefoldProof],
    transcript: TranscriptT,
) -> VerifyResult[TrivialClaim, TranscriptT]

Verify a single-matrix open — the degenerate one-round batch.

Source code in zorch/pcs/basefold/verifier.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def verify(
    self,
    claim: OpeningClaim[BasefoldCommitment],
    reduction_proof: OpeningProof[BasefoldProof],
    transcript: TranscriptT,
) -> VerifyResult[TrivialClaim, TranscriptT]:
    """Verify a single-matrix open — the degenerate one-round batch."""
    ok, transcript = self.verify_batch(
        [claim.commitment],
        claim.points,
        [reduction_proof.values],
        reduction_proof.proof,
        transcript,
    )
    return VerifyResult(TrivialClaim(), transcript, ok)

verify_with_basis

verify_with_basis(
    commitment: BasefoldCommitment,
    basis: Array,
    value: Array,
    proof: BasefoldProof | CadenceProof,
    transcript: TranscriptT,
) -> tuple[Array, TranscriptT]

Verify a RAW-basis open — the dual of BasefoldProver.open_with_basis (bind_statement receives point=None), dispatching on the fold schedule exactly as the prover entry does.

Under a non-native schedule (row_batch_prefix / fold_arities) this replays the generic cadence (row-batch prefix + multi-arity FRI epochs) against a CadenceProof, the symmetric dual of _open_with_basis_cadence: commitment is the prover's initial codeword root (bound, not observed — the outer protocol committed it), value the claimed target the kernel's reduce_claim/verify_final fold against.

Under the native uniform schedule the per-round check evaluates the sumcheck message at the opening point's coordinates, which a raw basis lacks, so that path has no basis replay yet — a fail-loud consumer delta (the native binding also refuses point=None).

Source code in zorch/pcs/basefold/verifier.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def verify_with_basis(
    self,
    commitment: BasefoldCommitment,
    basis: Array,
    value: Array,
    proof: BasefoldProof | CadenceProof,
    transcript: TranscriptT,
) -> tuple[Array, TranscriptT]:
    """Verify a RAW-basis open — the dual of `BasefoldProver.open_with_basis`
    (`bind_statement` receives `point=None`), dispatching on the fold schedule
    exactly as the prover entry does.

    Under a non-native schedule (`row_batch_prefix` / `fold_arities`) this
    replays the generic cadence (row-batch prefix + multi-arity FRI epochs)
    against a `CadenceProof`, the symmetric dual of `_open_with_basis_cadence`:
    `commitment` is the prover's initial codeword root (bound, not observed —
    the outer protocol committed it), `value` the claimed target the kernel's
    `reduce_claim`/`verify_final` fold against.

    Under the native uniform schedule the per-round check evaluates the
    sumcheck message at the opening point's coordinates, which a raw basis
    lacks, so that path has no basis replay yet — a fail-loud consumer delta
    (the native binding also refuses `point=None`)."""
    if basis.shape[0] < 2:
        raise ValueError("BaseFold opens over at least one variable, got none")
    num_vars = log2_strict_usize(basis.shape[0])
    config = self._resolved_config(num_vars)
    if not config.commits_per_round:
        if config.row_batch_prefix == 0:
            raise NotImplementedError(
                "non-native cadence verify is wired for a row-batch prefix "
                "(row_batch_prefix > 0, the row-batch-prefix shape); the "
                "prefix-free multi-arity sub-case commits no post-prefix "
                "layer and needs its own bridge — not replayed here"
            )
        # The schedule fixes the proof type (dispatch mirrors the prover):
        # a non-native config carries a `CadenceProof`.
        return _verify_with_basis_cadence(
            self,
            commitment,
            basis,
            value,
            config,
            cast(CadenceProof, proof),
            transcript,
        )
    # Native single-MLE basis path (deferred, as before): a raw basis lacks
    # the opening point the native per-round check needs.
    if self.code.message_len != (1 << num_vars):
        raise ValueError(
            f"basis length {basis.shape[0]} doesn't match message_len "
            f"{self.code.message_len} (expected 2^num_vars)"
        )
    self._check_proof_shape(cast(BasefoldProof, proof), num_vars)
    _require_no_grind(self.choreography, config)
    # Bind the statement via the choreography with point=None (native refuses;
    # a basis consumer binds via the basis). Even a basis consumer then hits
    # the deferred basis-path replay below.
    self.choreography.bind_statement(transcript, commitment, None, value)
    raise NotImplementedError(
        "verify_with_basis's raw-basis replay is not wired here: the native "
        "per-round check evaluates the sumcheck message at the opening point, "
        "which a raw basis lacks; a basis consumer supplies its own per-round "
        "check from (message, basis), symmetric to open_with_basis"
    )