Skip to content

zorch.round

Homogeneous IOP rounds and their recurrence drivers.

A round is one step of a repeated protocol recurrence. The two roles have genuinely different shapes, so they are separate protocols: a prover round advances its carry and emits a proof message; a verifier round consumes that message, advances its own carry, and reports both the protocol data the driver accumulates and its consistency verdict. Stages pair the two at protocol boundaries.

RunningClaim dataclass

A partially built evaluation claim: the value after the rounds bound so far.

index is the write cursor into point; both are fixed-shape so the whole carry stays one shape across the loop.

Source code in zorch/round.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@register_dataclass
@dataclass(frozen=True)
class RunningClaim:
    """A partially built evaluation claim: the value after the rounds bound so far.

    `index` is the write cursor into `point`; both are fixed-shape so the whole
    carry stays one shape across the loop.
    """

    value: Array
    point: Array
    index: Array

    def bind(self, value: Array, challenge: Array) -> RunningClaim:
        """Advance to the reduced claim, recording this round's challenge.

        The single definition of the write, so every wire form records its
        challenge identically and a new one cannot get the bookkeeping wrong.
        """
        return RunningClaim(
            value, self.point.at[self.index].set(challenge), self.index + 1
        )

bind

bind(value: Array, challenge: Array) -> RunningClaim

Advance to the reduced claim, recording this round's challenge.

The single definition of the write, so every wire form records its challenge identically and a new one cannot get the bookkeeping wrong.

Source code in zorch/round.py
41
42
43
44
45
46
47
48
49
def bind(self, value: Array, challenge: Array) -> RunningClaim:
    """Advance to the reduced claim, recording this round's challenge.

    The single definition of the write, so every wire form records its
    challenge identically and a new one cannot get the bookkeeping wrong.
    """
    return RunningClaim(
        value, self.point.at[self.index].set(challenge), self.index + 1
    )

ProverRound

Bases: Protocol[Carry, Message_co, TranscriptT]

One prover recurrence step: fold the carry and emit a proof message.

Source code in zorch/round.py
52
53
54
55
56
57
58
59
60
class ProverRound(Protocol[Carry, Message_co, TranscriptT]):
    """One prover recurrence step: fold the carry and emit a proof message."""

    def __call__(
        self,
        carry: Carry,
        transcript: TranscriptT,
        /,
    ) -> tuple[Carry, TranscriptT, Message_co]: ...

VerifierRound

Bases: Protocol[Carry, Message_contra, TranscriptT]

The dual step: consume the message, fold the carry, report a verdict.

Anything the round derives rather than receives — a sumcheck round's challenge, a fold challenge — belongs in the carry, not in a second return slot. Only the message crosses between roles, so only the message is a separate position, and every recurrence shape shares this one contract.

Source code in zorch/round.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class VerifierRound(Protocol[Carry, Message_contra, TranscriptT]):
    """The dual step: consume the message, fold the carry, report a verdict.

    Anything the round derives rather than receives — a sumcheck round's
    challenge, a fold challenge — belongs in the carry, not in a second return
    slot. Only the message crosses between roles, so only the message is a
    separate position, and every recurrence shape shares this one contract.
    """

    def __call__(
        self,
        carry: Carry,
        transcript: TranscriptT,
        message: Message_contra,
        /,
    ) -> tuple[Carry, TranscriptT, Array]: ...

prove_rounds

prove_rounds(
    rounds: Iterable[ProverRound[Any, Any, TranscriptT]],
    carry: Any,
    transcript: TranscriptT,
) -> tuple[Any, TranscriptT, list[Any]]

Run prover rounds, collecting each step's message.

Source code in zorch/round.py
81
82
83
84
85
86
87
88
89
90
91
def prove_rounds(
    rounds: Iterable[ProverRound[Any, Any, TranscriptT]],
    carry: Any,
    transcript: TranscriptT,
) -> tuple[Any, TranscriptT, list[Any]]:
    """Run prover rounds, collecting each step's message."""
    msgs = []
    for rnd in rounds:
        carry, transcript, msg = rnd(carry, transcript)
        msgs.append(msg)
    return carry, transcript, msgs

verify_rounds

verify_rounds(
    rounds: Iterable[VerifierRound[Any, Any, TranscriptT]],
    carry: Any,
    msgs: Sequence[Any],
    transcript: TranscriptT,
) -> tuple[Any, TranscriptT, Array]

Replay verifier rounds over a heterogeneous message list, ANDing verdicts.

The sibling of zorch.verify.verify, which scans one round over a dense proof array; both consume the same protocol.

Source code in zorch/round.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def verify_rounds(
    rounds: Iterable[VerifierRound[Any, Any, TranscriptT]],
    carry: Any,
    msgs: Sequence[Any],
    transcript: TranscriptT,
) -> tuple[Any, TranscriptT, Array]:
    """Replay verifier rounds over a heterogeneous message list, ANDing verdicts.

    The sibling of `zorch.verify.verify`, which scans one round over a dense
    proof array; both consume the same protocol.
    """
    materialized = list(rounds)
    if len(msgs) != len(materialized):
        raise ValueError(
            f"need one message per round: {len(materialized)} rounds, "
            f"got {len(msgs)} messages"
        )
    ok: Any = True
    for rnd, msg in zip(materialized, msgs, strict=True):
        carry, transcript, ok_round = rnd(carry, transcript, msg)
        ok = ok & ok_round
    return carry, transcript, ok