Skip to content

zorch.challenge

Shared Fiat-Shamir challenge-field policy.

ChallengePolicy dataclass

The field Fiat-Shamir challenges are drawn in.

Explicit because it is a soundness parameter: an extension challenge raises the soundness floor over a base-field one. Naming it also promotes — an extension policy draws extension challenges against a base-field claim.

Naming the transcript's own field costs nothing: reinterpret_challenge is then the identity, so it is the one-squeeze schedule.

Source code in zorch/challenge.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@dataclass(frozen=True)
class ChallengePolicy:
    """The field Fiat-Shamir challenges are drawn in.

    Explicit because it is a soundness parameter: an extension challenge raises
    the soundness floor over a base-field one. Naming it also *promotes* — an
    extension policy draws extension challenges against a base-field claim.

    Naming the transcript's own field costs nothing: ``reinterpret_challenge``
    is then the identity, so it is the one-squeeze schedule.
    """

    dtype: Any

    @property
    def base_limbs(self) -> int:
        """Words per challenge over the challenge field's own base field, for
        sites fixing a static jit-zone width before a transcript is in hand."""
        return challenge_limbs(self.dtype)

    def limbs_over(self, transcript_field: Any) -> int:
        """Transcript words per challenge: the degree ratio, so an
        extension-native sponge spends one word where a base-field sponge
        spends the extension's degree."""
        words = challenge_limbs(self.dtype)
        per_word = challenge_limbs(transcript_field)
        if words % per_word:
            raise ValueError(
                f"{self.dtype} does not tile {transcript_field}: "
                f"{words} coefficients over words of {per_word}"
            )
        return words // per_word

    def _regroup(self, raw: Array, count: int, limbs: int) -> Array:
        """Read ``count`` challenges out of consecutive transcript words."""
        return fnp.stack(
            [
                reinterpret_challenge(raw[i * limbs : (i + 1) * limbs], self.dtype)
                for i in range(count)
            ]
        )

    def sample(self, transcript: TranscriptT) -> tuple[TranscriptT, Array]:
        transcript, challenges = self.sample_many(transcript, 1)
        return transcript, challenges[0]

    def sample_many(
        self, transcript: TranscriptT, count: int
    ) -> tuple[TranscriptT, Array]:
        # One squeeze call for all `count * limbs` limbs: the duplex squeezes a
        # rate-block per permutation, so batching costs ~ceil(n/rate) permutes
        # where a per-challenge loop costs ~n, for the same stream.
        limbs = self.limbs_over(transcript.field)
        transcript, raw = transcript.sample(count * limbs)
        return transcript, self._regroup(raw, count, limbs)

    def observe_and_sample(
        self, transcript: TranscriptT, values: Array
    ) -> tuple[TranscriptT, Array]:
        # The absorb and the squeeze stay one hop: splitting them into `observe`
        # then `sample` bypasses the duplex-FS fusion marker and scatters ~9
        # kernels per round, which `zorch`'s fusion rule does not allow.
        limbs = self.limbs_over(transcript.field)
        transcript, raw = transcript.observe_and_sample(values, limbs)
        return transcript, self._regroup(raw, 1, limbs)[0]

base_limbs property

base_limbs: int

Words per challenge over the challenge field's own base field, for sites fixing a static jit-zone width before a transcript is in hand.

limbs_over

limbs_over(transcript_field: Any) -> int

Transcript words per challenge: the degree ratio, so an extension-native sponge spends one word where a base-field sponge spends the extension's degree.

Source code in zorch/challenge.py
44
45
46
47
48
49
50
51
52
53
54
55
def limbs_over(self, transcript_field: Any) -> int:
    """Transcript words per challenge: the degree ratio, so an
    extension-native sponge spends one word where a base-field sponge
    spends the extension's degree."""
    words = challenge_limbs(self.dtype)
    per_word = challenge_limbs(transcript_field)
    if words % per_word:
        raise ValueError(
            f"{self.dtype} does not tile {transcript_field}: "
            f"{words} coefficients over words of {per_word}"
        )
    return words // per_word

challenge_limbs

challenge_limbs(dtype: Any) -> int

Base-field squeezes required to construct one element of dtype.

Source code in zorch/challenge.py
16
17
18
19
20
21
def challenge_limbs(dtype: Any) -> int:
    """Base-field squeezes required to construct one element of ``dtype``."""
    try:
        return efinfo(dtype).degree
    except ValueError:
        return 1