Skip to content

zorch.pcs.whir.prover

WHIR prover — the prover half of the multilinear PCS.

commit Reed-Solomon-encodes the polynomial's hypercube evaluations and binds the codeword rows under a query-strided Merkle root. open runs the WHIR rounds: each round folds the MLE by k_whir sumcheck sub-folds, re-encodes the folded MLE as a fresh (shrinking) RS codeword, commits it, answers one out-of-domain point, and opens the previous round's codeword at strided query cosets — accumulating the out-of-domain and in-domain constraints into the weight polynomial. The last round sends the folded coefficients in the clear.

Unlike fri/basefold this never folds a codeword in place, so it does not use pcs/fold.py; the round driver is WHIR's own. See config.py for the divergence.

WhirProverData dataclass

Retained witness from WhirProver.commit: the message-domain columns mle (S, num_polys) the open μ-combines then sumcheck-folds, the initial RS codeword matrix (block_len, num_polys) (its strided rows are the Merkle leaves the round-0 queries open), and the codeword's Merkle digest layers. A pytree so commit/open ride a @jit zone.

Source code in zorch/pcs/whir/prover.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@partial(
    frx.tree_util.register_dataclass,
    data_fields=["mle", "codeword", "digest_layers"],
    meta_fields=[],
)
@dataclass(frozen=True)
class WhirProverData:
    """Retained witness from `WhirProver.commit`: the message-domain columns
    `mle` `(S, num_polys)` the open μ-combines then sumcheck-folds, the initial RS
    codeword matrix `(block_len, num_polys)` (its strided rows are the Merkle
    leaves the round-0 queries open), and the codeword's Merkle digest layers. A
    pytree so `commit`/`open` ride a `@jit` zone."""

    mle: Array  # (S, num_polys)
    codeword: Array  # (block_len, num_polys)
    digest_layers: list[Array]

WhirProver dataclass

Bases: ProverStage[OpeningClaim[WhirCommitment], OpeningWitness[WhirProverData], TrivialClaim, OpeningProof[WhirProof], Transcript]

WHIR PCS prover. code is the initial-round RS encoder (the round driver re-encodes at shrinking sizes); tree commits the codeword's 2^k_whir-row query cosets; params carries the per-round knobs; scheme supplies the initial message + weight maps (default: a plain multilinear opening — see scheme.py).

Source code in zorch/pcs/whir/prover.py
 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
@dataclass(frozen=True)
class WhirProver(
    ProverStage[
        OpeningClaim[WhirCommitment],
        OpeningWitness[WhirProverData],
        TrivialClaim,
        OpeningProof[WhirProof],
        Transcript,
    ]
):
    """WHIR PCS prover. `code` is the initial-round RS encoder (the
    round driver re-encodes at shrinking sizes); `tree` commits the codeword's
    `2^k_whir`-row query cosets; `params` carries the per-round knobs; `scheme`
    supplies the initial message + weight maps (default: a plain multilinear
    opening — see `scheme.py`)."""

    code: ReedSolomon
    tree: StridedMerkleTree
    params: WhirParams
    scheme: WhirScheme = EqWhirScheme()

    def commit(self, polys: Sequence[Array]) -> tuple[WhirCommitment, WhirProverData]:
        """Bind a batch of multilinears sharing one point, each given by its
        `2^m` hypercube evaluations. Each column is RS-encoded; the codeword
        matrix binds under one query-strided Merkle root (a matrix commitment,
        like BaseFold). `open` reduces the columns to one polynomial by a μ-power
        random linear combination."""
        if not polys:
            raise ValueError("WHIR commits at least one polynomial, got none")
        for p in polys:
            if p.ndim != 1:
                raise ValueError(f"each polynomial must be 1-D, got ndim {p.ndim}")
            if p.shape[0] != self.code.message_len:
                raise ValueError(
                    f"polynomial length {p.shape[0]} != code message_len "
                    f"{self.code.message_len}"
                )
        return _commit_body(self.code, self.tree, list(polys))

    def prove(
        self,
        claim: OpeningClaim[WhirCommitment],
        witness: OpeningWitness[WhirProverData],
        transcript: TranscriptT,
    ) -> ProveResult[TrivialClaim, OpeningProof[WhirProof], TranscriptT]:
        """Open the committed polynomials at the claim's points.

        Terminal: an opening closes its claim rather than reducing it."""
        values, proof, transcript = self._open(
            witness.prover_data, claim.points, transcript
        )
        return ProveResult(TrivialClaim(), OpeningProof(values, proof), transcript)

    def _open(
        self,
        prover_data: WhirProverData,
        points: Sequence[Array],
        transcript: TranscriptT,
    ) -> tuple[Array, WhirProof, TranscriptT]:
        """Open a single committed matrix at `points[0]` — the degenerate
        one-commitment μ-batch. Returns
        `(values, proof, transcript)` with `values` the matrix's per-column
        evaluations `f̂ᵢ(z)` `(W,)`."""
        return self.open_batch([prover_data], points, transcript)

    def open_batch(
        self,
        prover_datas: Sequence[WhirProverData],
        points: Sequence[Array],
        transcript: TranscriptT,
    ) -> tuple[Array, WhirProof, TranscriptT]:
        """μ-batch-open the committed matrices at the shared point `points[0]`,
        threading Fiat-Shamir.

        The committed columns of every commitment join into one `(S, ΣWᵢ)`
        message — first commitment's columns first — over which the scheme's
        μ-power combine runs; each commitment keeps its own codeword tree and
        round 0 opens them all. Returns `(values, proof, transcript)` with
        `values` the per-column evaluations across all commitments `(ΣWᵢ,)`. A
        single-element `prover_datas` is the degenerate (un-batched) open."""
        datas = list(prover_datas)
        if not datas:
            raise ValueError("WHIR opens at least one commitment, got none")
        if len(points) != 1:
            raise ValueError(f"WHIR opens at one point, got {len(points)}")
        z = points[0]
        m = z.shape[0]
        k = self.params.k_whir
        num_rounds = len(self.params.num_queries)
        if not (0 < num_rounds * k <= m):
            raise ValueError(
                f"num_rounds·k_whir ({num_rounds}·{k}) must fold between 1 and "
                f"num_variables ({m}) inclusive"
            )
        return _open_body(self, datas, z, transcript)

commit

commit(
    polys: Sequence[Array],
) -> tuple[WhirCommitment, WhirProverData]

Bind a batch of multilinears sharing one point, each given by its 2^m hypercube evaluations. Each column is RS-encoded; the codeword matrix binds under one query-strided Merkle root (a matrix commitment, like BaseFold). open reduces the columns to one polynomial by a μ-power random linear combination.

Source code in zorch/pcs/whir/prover.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def commit(self, polys: Sequence[Array]) -> tuple[WhirCommitment, WhirProverData]:
    """Bind a batch of multilinears sharing one point, each given by its
    `2^m` hypercube evaluations. Each column is RS-encoded; the codeword
    matrix binds under one query-strided Merkle root (a matrix commitment,
    like BaseFold). `open` reduces the columns to one polynomial by a μ-power
    random linear combination."""
    if not polys:
        raise ValueError("WHIR commits at least one polynomial, got none")
    for p in polys:
        if p.ndim != 1:
            raise ValueError(f"each polynomial must be 1-D, got ndim {p.ndim}")
        if p.shape[0] != self.code.message_len:
            raise ValueError(
                f"polynomial length {p.shape[0]} != code message_len "
                f"{self.code.message_len}"
            )
    return _commit_body(self.code, self.tree, list(polys))

prove

prove(
    claim: OpeningClaim[WhirCommitment],
    witness: OpeningWitness[WhirProverData],
    transcript: TranscriptT,
) -> ProveResult[
    TrivialClaim, OpeningProof[WhirProof], TranscriptT
]

Open the committed polynomials at the claim's points.

Terminal: an opening closes its claim rather than reducing it.

Source code in zorch/pcs/whir/prover.py
110
111
112
113
114
115
116
117
118
119
120
121
122
def prove(
    self,
    claim: OpeningClaim[WhirCommitment],
    witness: OpeningWitness[WhirProverData],
    transcript: TranscriptT,
) -> ProveResult[TrivialClaim, OpeningProof[WhirProof], TranscriptT]:
    """Open the committed polynomials at the claim's points.

    Terminal: an opening closes its claim rather than reducing it."""
    values, proof, transcript = self._open(
        witness.prover_data, claim.points, transcript
    )
    return ProveResult(TrivialClaim(), OpeningProof(values, proof), transcript)

open_batch

open_batch(
    prover_datas: Sequence[WhirProverData],
    points: Sequence[Array],
    transcript: TranscriptT,
) -> tuple[Array, WhirProof, TranscriptT]

μ-batch-open the committed matrices at the shared point points[0], threading Fiat-Shamir.

The committed columns of every commitment join into one (S, ΣWᵢ) message — first commitment's columns first — over which the scheme's μ-power combine runs; each commitment keeps its own codeword tree and round 0 opens them all. Returns (values, proof, transcript) with values the per-column evaluations across all commitments (ΣWᵢ,). A single-element prover_datas is the degenerate (un-batched) open.

Source code in zorch/pcs/whir/prover.py
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
def open_batch(
    self,
    prover_datas: Sequence[WhirProverData],
    points: Sequence[Array],
    transcript: TranscriptT,
) -> tuple[Array, WhirProof, TranscriptT]:
    """μ-batch-open the committed matrices at the shared point `points[0]`,
    threading Fiat-Shamir.

    The committed columns of every commitment join into one `(S, ΣWᵢ)`
    message — first commitment's columns first — over which the scheme's
    μ-power combine runs; each commitment keeps its own codeword tree and
    round 0 opens them all. Returns `(values, proof, transcript)` with
    `values` the per-column evaluations across all commitments `(ΣWᵢ,)`. A
    single-element `prover_datas` is the degenerate (un-batched) open."""
    datas = list(prover_datas)
    if not datas:
        raise ValueError("WHIR opens at least one commitment, got none")
    if len(points) != 1:
        raise ValueError(f"WHIR opens at one point, got {len(points)}")
    z = points[0]
    m = z.shape[0]
    k = self.params.k_whir
    num_rounds = len(self.params.num_queries)
    if not (0 < num_rounds * k <= m):
        raise ValueError(
            f"num_rounds·k_whir ({num_rounds}·{k}) must fold between 1 and "
            f"num_variables ({m}) inclusive"
        )
    return _open_body(self, datas, z, transcript)