zorch.lnp.masking¶
The masking and rejection machinery every ABDLOP proof shares.
Fig. 4 (opening.py) and Fig. 6 (quadratic.py) are siblings, not
consumers of one another: both mask the witness with Gaussians y_i ~
D_{s_i}, absorb their own first-round messages, squeeze one challenge
c ∈ C, answer z_i = c·s_i + y_i over the integers, and rejection-
sample both responses. What differs between them is only which messages
go into the transcript and which equation the verifier checks — the
parameter point, the draw, the response, the Rej1 gates and the [Ban93]
norm bounds are one thing, held here.
They also have to be the same thing rather than merely alike. Fig. 8 runs a Π_eval-shaped layer over Π_many^(2), so a single proof carries both protocols against one commitment; two parameter objects that drifted apart would be a soundness bug no single-protocol suite could see.
Rejection is paid with a precomputed budget, the sampler discipline
lifted to the protocol loop: one attempt accepts with probability
≈ 1/(rep1·rep2) (Lemma 2.14-1: M = exp(14/γ + 1/(2γ²)) at s = γ·T,
T ≥ ‖c·s_i‖), so a prover runs at most
ceil(log(fail_prob)/log(1 − 1/(rep1·rep2))) attempts and raises rather
than looping open-endedly. Each attempt restarts from the caller's
transcript value — ByteTranscript is functional, so a rejected attempt
leaves no trace, which is exactly the Fiat-Shamir-with-aborts convention.
Host/device boundary per docs/reference/conventions.md: the responses
live on the host by necessity — z = c·s + y must be computed over
unreduced ℤ (the norm statement is about magnitudes a field dtype would
fold away), and the rejection inner products and norm checks run over
exact Python ints (lattice_frx.norms). The host touches verdicts and
integer vectors, not ring arrays.
Prover randomness is the caller's np.random.Generator — private coins,
not transcript-derived.
Masking ¶
The parameter point a Fig.-4/Fig.-6 proof masks and rejects against.
Takes the derived numbers: the masking standard deviations s?_std,
the Lemma 2.14-1 repetition rates rep? they were derived with, the
challenge point (ChallengeParams, carrying its own budget), and the
fail_prob pricing the rejection loop. Parameter derivation (the
γ-factors of §2.6/§6.1) stays with the consumer — this seam takes the
derived numbers, like every other seam in this package.
The relation count is deliberately not among them: it is a property of the statement each protocol is given, so storing it here would be a second representation of one number with nothing gating the two.
Source code in zorch/lnp/masking.py
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 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | |
draw ¶
draw(
rng: np.random.Generator,
) -> tuple[np.ndarray, np.ndarray]
One masking pair (y1, y2) ~ (D_{s1}, D_{s2}), as signed integer
(m_i, d) arrays.
Source code in zorch/lnp/masking.py
146 147 148 149 150 151 152 | |
challenge_from ¶
challenge_from(
transcript: ByteTranscript,
label: bytes,
*stacks: np.ndarray
) -> tuple[ByteTranscript, np.ndarray]
Absorb a protocol's first-round messages under its own label and
squeeze the challenge — the one derivation both sides replay.
The label is the caller's because it is what separates two protocols that would otherwise hash the same stacks to the same challenge.
Source code in zorch/lnp/masking.py
154 155 156 157 158 159 160 161 162 163 164 | |
ajtai_image ¶
ajtai_image(
a1: np.ndarray,
a2: np.ndarray,
x1: np.ndarray,
x2: np.ndarray,
) -> np.ndarray
A1·x1 + A2·x2, the Ajtai half of the commitment equation.
Both siblings send it as their first message at x = y and both
rebuild it at x = z to check it, so it is one expression at four
sites, not two protocols that happen to agree. Here rather than on
AbdlopCommitment because what the layers pass is a masking or a
response, not a witness — the scheme's own commit is the
witness-shaped caller and keeps its bound checks.
Source code in zorch/lnp/masking.py
166 167 168 169 170 171 172 173 174 175 176 177 178 | |
masked_message ¶
masked_message(
c: np.ndarray,
b: np.ndarray,
t_b: np.ndarray,
z2: np.ndarray,
) -> np.ndarray
c·t_B − B·z2 — c·m for the message the BDLOP half commits to
but never sends.
The only route either verifier has to m: Fig. 4 feeds it to the
linear check, Fig. 6 lifts it as eq. 30's message half. Named once
because the two must agree on it and a suite of either alone cannot
see them drift.
Source code in zorch/lnp/masking.py
180 181 182 183 184 185 186 187 188 189 190 191 | |
respond ¶
respond(
c: np.ndarray, s1: np.ndarray, s2: np.ndarray
) -> tuple[np.ndarray, np.ndarray]
c·s_i over the integers, one per witness half — the term both
the response z_i = c·s_i + y_i and its Rej1 gate are stated in.
Source code in zorch/lnp/masking.py
193 194 195 196 197 198 | |
accepts ¶
accepts(
rng: np.random.Generator,
z1: np.ndarray,
cs1: np.ndarray,
z2: np.ndarray,
cs2: np.ndarray,
) -> bool
Both Rej1 gates (Lemma 2.14-1) on one attempt's responses.
Both coins are drawn before either gate is evaluated, so the rng
stream a given seed produces stays a function of the attempt count
alone — and the gates are then free to short-circuit, which matters
because the exact-integer arithmetic is the expensive half of an
attempt and a 1 − 1/rep1 share of attempts are already lost at the
first gate.
Source code in zorch/lnp/masking.py
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
within_bounds ¶
within_bounds(z1: np.ndarray, z2: np.ndarray) -> bool
‖z_i‖₂ ≤ s_i·√(2·m_i·d) [Ban93], the verifier's norm checks.
Source code in zorch/lnp/masking.py
221 222 223 224 225 226 | |
exhausted ¶
exhausted(protocol: str) -> RuntimeError
The error a prover raises when its whole budget was rejected — one message, because the diagnosis is the same for every protocol that masks this way.
Source code in zorch/lnp/masking.py
228 229 230 231 232 233 234 235 236 237 238 | |
require_witness ¶
require_witness(
name: str, s1: np.ndarray, s2: np.ndarray
) -> None
The witness halves are the caller's, so a malformed one raises —
zorch/lnp/wire.py's statement side.
Source code in zorch/lnp/masking.py
240 241 242 243 244 245 | |
is_response ¶
is_response(
c: np.ndarray, z1: np.ndarray, z2: np.ndarray
) -> bool
Whether an untrusted (c, z1, z2) triple is structurally usable —
the shared half of every masked protocol's _is_well_formed.
Source code in zorch/lnp/masking.py
247 248 249 250 251 252 253 254 255 | |
BimodalMasking ¶
The parameter point Fig. 9's projection masks and rejects against.
Masking above is the masking of a witness: two Gaussians, one per
ABDLOP half, gated by Rej1. This is the masking of a projection — a
single Gaussian y ~ D_{s3}^{256/d} over the 256 integers R⃗s shrinks
the witness to, gated by the bimodal Rej0 of Fig. 2. The two are not
the same object and must not be one: they mask different vectors, at
different standard deviations, under different rejection algorithms.
Why bimodal here and not there. The projection is masked as
z = b·R⃗s + y for a secret sign b ∈ {−1, 1}, which makes z's
distribution the average of two Gaussians centred at ±R⃗s. Rej0
(Lemma 2.14-3) accepts that average against M = exp(1/(2γ²)) rather
than Rej1's exp(14/γ + 1/(2γ²)), so the same repetition rate is
reached at a much smaller s3 — and s3 is what the revealed z's bit
length, and therefore the proof size, is set by. The price is proving
b really is a sign, which is why Fig. 9 hands the layer above a
quadratic relation and d − 1 evaluations it would not otherwise need.
Rej1 cannot be swapped in as a "safer default": it is stated for a
unimodal z = v + y and says nothing about this distribution.
The witness masking is not absorbed here even though both are
rejection loops, because a rejected attempt at this layer redraws
(b, y) and re-derives R, while a rejected attempt inside the proof
below redraws that proof's own (y1, y2). One loop around both would
throw away work the inner loop had already accepted.
Parameters arrive derived, as everywhere in this package: mask_std is
the paper's s3 = γ·√337·β (the √337 is Lemma 2.8's projection
growth, the γ the usual s = γ·T), rep0 its Lemma 2.14-3 rate, and
accept_t the t ≥ 1.64 of Prop. 5.1 sizing the verifier's norm gate.
Deriving them from a witness bound is the consumer's, since β is a
property of the statement rather than of this seam.
Source code in zorch/lnp/masking.py
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 | |
draw ¶
draw(rng: np.random.Generator) -> tuple[int, np.ndarray]
One attempt's (b, y): a sign and a (256/d, d) Gaussian mask.
Both are private coins off the caller's generator, never off the
transcript — the sign especially, since a transcript-derived b
would be public and the bimodal trick buys nothing.
Drawn together because they are one attempt: a loop that redrew the mask while holding the sign would be sampling neither Rej0's distribution nor Rej1's.
Source code in zorch/lnp/masking.py
388 389 390 391 392 393 394 395 396 397 398 399 | |
accepts ¶
accepts(
rng: np.random.Generator, z: np.ndarray, v: np.ndarray
) -> bool
Rej0 (Fig. 2) on one attempt's revealed projection.
v is the signed centre b·R⃗s, not R⃗s — Lemma 2.14-3 is
stated for z = y + (−1)^β v and the gate reads ⟨z, v⟩ at that
same v. cosh is even, so the two spellings agree here by luck
rather than by contract; passing the centre the response was built
from is what stays true when a later leg is not symmetric.
Source code in zorch/lnp/masking.py
401 402 403 404 405 406 407 408 409 | |
within_bounds ¶
within_bounds(z: np.ndarray) -> bool
‖z‖₂ ≤ t·√256·s3 (Prop. 5.1) — the verifier's gate on the
revealed projection, and the only place the range statement's slack
is actually enforced.
Source code in zorch/lnp/masking.py
411 412 413 414 415 | |
exhausted ¶
exhausted(protocol: str) -> RuntimeError
The Rej0 twin of Masking.exhausted.
Source code in zorch/lnp/masking.py
417 418 419 420 421 422 423 424 | |