The good news (fields). In a lot of crypto, you need to verify a bunch of equations at once. Think of equations
$$ E_1=0,\; E_2=0,\; \dots,\; E_s=0 $$
where each $E_i$ is an expression over a large finite field $\mathbb F$ (e.g., constraints of a circuit, polynomial identities, signature checks in a batch). Instead of checking all $s$ equalities separately, you can batch them by picking random coefficients $$r_1,\dots,r_s \stackrel{\$}{\leftarrow} \mathbb F$$ and verifying a single equation:
$$ \sum_{i=1}^s r_i\,E_i \;\stackrel{?}{=}\; 0 \quad\text{over }\mathbb F. $$
If at least one $E_i\neq 0$, then (because this is one linear relation over a field) the combined check fails except with probability exactly $1/|\mathbb F|$. When $|\mathbb F|\ge 2^{128}$, that’s already negligible. This “random linear batching” is the engine behind Freivalds’ check, Schwartz–Zippel style tests, PCS/IPA batching, and many signature batch verifiers.
The trap (groups with cofactors).
We want to convince a verifier that an elliptic-curve group really has prime order $n$. A standard necessary-and-sufficient check is
$$ [n]P_i = O \quad \text{for } i=1,\dots,s, $$
where each $P_i$ is sampled independently (e.g., via hash-to-curve) and $O$ is the identity. If the true group size is actually $N=k\cdot n$ with a cofactor $k\ge2$, then for a random $P$ we have $[n]P$ essentially uniform in a size-$k$ subgroup, so
$$ \Pr([n]P=O)=1/k. $$
Independence across the $s$ equations gives the naïve soundness
$$ \Pr\big(\forall i,\ [n]P_i=O\big)=(1/k)^s, $$
which is great: exponential decay in $s$.
Why it’s tempting to batch them like in the field setting. From the world of fields, we know a powerful trick: to check many equalities $E_i=0$ over a large field $\mathbb F$, pick random $r_i\in\mathbb F$ and verify a single combined equality $\sum r_i E_i=0$. If any $E_i\neq0$, this fails except with probability $1/|\mathbb F|$ (already negligible). It’s natural to try the same batching move here: sample random coefficients $c_1,\dots,c_s$, form one point
$$ Q \;=\; \sum_{i=1}^s [c_i]\,P_i, $$
and replace the $s$ checks by a single group equation
$$ [n]Q \stackrel{?}= O. $$
Why that “field-style” batching quietly breaks soundness in groups.
If $|G| = k n$ with $n$ prime:
For random $P$, define $R = [n]P \in H$; then $R$ is (close to) uniform in $H$, so $\Pr(R=O) = 1/k$.
Pick independent $P_1,\dots,P_s$; set $R_i = [n]P_i \in H$. Check $[n]P_i = O$ for each $i$. Then
$$ \Pr(\forall i,\; R_i = O) = (1/k)^s. $$
Pick random coefficients $c_1,\dots,c_s$ (from the field $\mathbb{F}_p$), set
$$ Q = \sum_i [c_i] P_i,\qquad \text{check } [n]Q = \sum_i [c_i] R_i = O \text{ in } H. $$
Key fact: on a subgroup of size $k$, multiplying by any integer $c$ depends only on $c \bmod k$. (Because every $h \in H$ satisfies $[k]h = O$, so $[c]h = [\,c \bmod k\,]h$.) Thus only the residues $c_i \bmod k$ matter; with random field coefficients these residues are essentially uniform in $\mathbb{Z}_k$.
Conditioned on the $R_i$ being random (they are), they almost surely generate all of $H$. Then the sum $\sum_i (c_i \bmod k)\, R_i$ is uniform in $H$. A uniform element of a $k$-element group is the identity with probability exactly $1/k$. One random linear relation removes one degree of freedom—no more.
Counting picture (cyclic $H$). If $H=\langle g\rangle$ and $R_i = b_i g$ (with some $b_i \not\equiv 0 \mod k$), the acceptance condition is one linear equation $\sum_i (c_i b_i) \equiv 0 \ (\bmod\ k)$. Among the $k^s$ residue vectors $(c_1,\dots,c_s) \bmod k$, exactly $k^{s-1}$ satisfy it ⇒ probability $1/k$.
Conclusion: collapsing $s$ checks into one random linear combination buys you cost (one check instead of $s$) but destroys soundness, from $(1/k)^s$ down to $1/k$.
Take $G = \mathbb{Z}_{15}$ under addition (identity $0$). We “want” order $n=5$, but the true size is $15 = 3 \cdot 5$, so the cofactor is $k=3$.
Naïve (s=2). Pick $P_1,P_2 \in {0,\dots,14}$ uniformly; set $R_i = 5 P_i \pmod{15}$. Each $R_i$ is uniform in ${0,5,10}$ so $\Pr(R_i=0)=1/3$. Both zero with probability $(1/3)^2 = 1/9$.
Compressed (s=2). Pick random coefficients $c_1,c_2$; form $Q = c_1 P_1 + c_2 P_2 \pmod{15}$; accept if $5Q \equiv 0$. This is equivalent to a single linear equation modulo 3: $c_1 (5P_1) + c_2 (5P_2) \equiv 0 \ (\bmod 15)$ ⇔ $(c_1 \bmod 3) (R_1/5) + (c_2 \bmod 3) (R_2/5) \equiv 0 \ (\bmod 3)$. Exactly one third of coefficient pairs satisfy it ⇒ acceptance probability $1/3$, not $1/9$.
import random
import math
def simulate_small_example(
N=15, n_claim=5, s_values=(1,2,3,4,5), trials=200_000, p_field=15, seed=123
):
"""
Monte Carlo on G = Z_N (additive). True size N = k * n_claim.
- Naive: check [n]P_i == 0 for i=1..s independently -> expected accept (1/k)^s
- Compressed: pick random coeffs c_i in F_p, form Q = sum c_i P_i, check [n]Q == 0
-> conditional on NOT(all [n]P_i==0), accept ~ 1/k exactly.
Unconditional accept ≈ 1/k + (1/k)^s - (1/k)*(1/k)^s.
"""
rng = random.Random(seed)
if N % n_claim != 0:
raise ValueError("Require N to be a multiple of n_claim.")
k = N // n_claim # cofactor
print(f"=== Monte Carlo on Z_{N}, claimed n = {n_claim}, cofactor k = {k}, trials per s = {trials} ===")
print(f"(Coefficients sampled from F_{p_field}, effectively reduced mod k={k} on the image subgroup)\n")
header = (
f"{'s':>2} {'naive_accept':>13} {'naive_pred':>11} "
f"{'compr_accept':>13} {'compr_pred(~1/k)':>17} "
f"{'compr_accept | not-all-zero':>28}"
)
print(header)
print("-" * len(header))
for s in s_values:
naive_accept = 0
comp_accept = 0
comp_accept_and_notallzero = 0
comp_notallzero_count = 0
for _ in range(trials):
# s random points P_i in Z_N
P = [rng.randrange(N) for _ in range(s)]
# R_i = [n]P_i = n_claim * P_i mod N (lies in image subgroup of size k)
R = [(n_claim * Pi) % N for Pi in P]
# Naive: all R_i must be 0
if all(r == 0 for r in R):
naive_accept += 1
# Compressed: random field coeffs, one combined check
coeffs = [rng.randrange(p_field) for _ in range(s)]
Q = sum(ci * Pi for ci, Pi in zip(coeffs, P)) % N
if (n_claim * Q) % N == 0:
comp_accept += 1
# Conditional acceptance given NOT(all R_i == 0)
not_all_zero = not all(r == 0 for r in R)
if not_all_zero:
comp_notallzero_count += 1
if (n_claim * Q) % N == 0:
comp_accept_and_notallzero += 1
naive_rate = naive_accept / trials
naive_pred = (1 / k) ** s
comp_rate = comp_accept / trials
comp_pred = 1 / k # asymptotically (exact conditional on not-all-zero)
comp_cond_rate = (
comp_accept_and_notallzero / comp_notallzero_count
if comp_notallzero_count > 0 else float('nan')
)
print(
f"{s:2d} {naive_rate:13.6f} {naive_pred:11.6f} "
f"{comp_rate:13.6f} {comp_pred:17.6f} "
f"{comp_cond_rate:28.6f}"
)
if __name__ == "__main__":
# Default demo: Z_15, n=5 -> k=3
simulate_small_example()
=== Monte Carlo on Z_15, claimed n = 5, cofactor k = 3, trials per s = 200000 ===
(Coefficients sampled from F_15, effectively reduced mod k=3 on the image subgroup)
s naive_accept naive_pred compr_accept compr_pred(~1/k) compr_accept | not-all-zero
----------------------------------------------------------------------------------------------
1 0.333620 0.333333 0.554870 0.333333 0.332018
2 0.112160 0.111111 0.408285 0.333333 0.333534
3 0.036600 0.037037 0.357465 0.333333 0.333055
4 0.012750 0.012346 0.341315 0.333333 0.332808
5 0.003980 0.004115 0.334105 0.333333 0.331444