Skip to content

Commit c42d8ad

Browse files
docs: algorithm-level implications note + prime scout
docs/algorithm-level.md records where the two identities pay outside hardware — deliberately kept out of the paper's claim set: embedded cores, masked implementations (linear-vs-quadratic masking cost), GPU FHE twiddle-table bandwidth, and the Goldilocks connection — and distills the fold-economics boundary into a parameter-selection criterion (signed-digit weight of k <= 2; check for a shift-friendly psi). generator/prime_scout.py makes the criterion executable: enumerates Proth primes with k = 2^a +/- 1 across 14-16 / 28-32 / 60-64 bit windows (deterministic Miller-Rabin), reports the planner's fold count and the smallest shift-friendly primitive 2048-th root. Honest finding from the sweep: cheap-fold primes are plentiful (incl. BabyBear, 3*2^30+1, 127*2^24+1, Goldilocks), but a shift-friendly psi is the scarce property — only Falcon's 12289 has one among small candidates, so multiplier-free psi-folding must be selected for explicitly. The note states plainly that its software claims are cost-model arguments, not CI-validated measurements. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2b53a83 commit c42d8ad

2 files changed

Lines changed: 194 additions & 0 deletions

File tree

docs/algorithm-level.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Algorithm-level implications
2+
3+
The paper deliberately claims hardware results only. This note records
4+
where the same ideas apply *outside* hardware — in software
5+
implementations and, most usefully, in protocol parameter selection.
6+
Nothing here is in the paper's claim set.
7+
8+
## The observation
9+
10+
Both inventions are algebraic identities, not circuits:
11+
12+
- K-RED fold (Lemma 1): `k·z₀ − z₁ ≡ k·z (mod q)` for `q = k·2^m + 1`.
13+
- ψ-fold (Lemma 3): `w[N/2+j] = ψ·w[j]` in the bit-reversed negacyclic
14+
twiddle table.
15+
16+
Identities are substrate-independent; whether they pay depends on the
17+
cost model. Hardware pays because multipliers are the scarcest resource.
18+
The software contexts below share that shape.
19+
20+
## Where K-RED pays in software
21+
22+
K-RED started as a software technique (Longa–Naehrig 2016), so this is
23+
less a port than a map of where it still wins:
24+
25+
- **Embedded cores** (Cortex-M0/M4 class): narrow multipliers make the
26+
Barrett/Montgomery multiply chains expensive; shift-add folds are the
27+
same trade the FPGA makes.
28+
- **Masked (side-channel-protected) implementations**: masking a linear
29+
operation costs linearly in the share count, masking a multiplication
30+
quadratically. Replacing two multiplies per butterfly with shift-adds
31+
is the hardware win re-priced in masking cost — plausibly larger, and
32+
unmeasured as far as we know.
33+
- **ZK fields**: the Goldilocks reduction used by ZK provers is exactly
34+
this fold shape (k = 2³² − 1, signed-digit weight 1 subtraction), and
35+
`k^F ≡ 1 (mod q)` there, so no scaling correction is needed at all
36+
(§4.3 of the paper, Table 2).
37+
38+
On a desktop CPU with a full 64-bit multiplier, the win mostly vanishes.
39+
40+
## Where ψ-fold pays in software
41+
42+
Software NTTs store the same bit-reversed tables, so halving (recursively
43+
quartering) transfers directly, at one constant multiply (or, for
44+
shift-friendly ψ, a shift-subtract) per derived access:
45+
46+
- **Flash/ROM-constrained targets**: hundreds of bytes per table; modest
47+
but free.
48+
- **GPU FHE libraries**: tables exist per RNS prime, dozens of primes,
49+
N up to 2¹⁶ — megabytes of twiddles whose traffic competes with data
50+
for memory bandwidth, which is the usual NTT bottleneck on GPUs. The
51+
ψ-fold is a middle point between storing everything and full
52+
on-the-fly generation: half (or quarter) the table for one constant
53+
multiply per derived access.
54+
55+
## The most useful transfer: parameter selection
56+
57+
The fold-economics boundary (paper §4.3) is a *selection criterion* for
58+
anyone choosing new NTT primes (FHE RNS chains, new protocol
59+
parameters):
60+
61+
> Prefer `q = k·2^m + 1` with the signed-digit weight of k at most 2
62+
> (k = 2^a ± 1), and check whether a shift-friendly ψ exists for the
63+
> target N.
64+
65+
Both properties cost nothing at the protocol level and make every future
66+
implementation — hardware and software — cheaper. `generator/prime_scout.py`
67+
enumerates the candidates:
68+
69+
```
70+
uv run generator/prime_scout.py
71+
```
72+
73+
Findings from the current sweep (14–16, 28–32 and 60–64 bit windows,
74+
m ≥ 12):
75+
76+
- Cheap-fold primes are plentiful: every window has many `2^a ± 1`
77+
candidates with F = 2–3 folds, including the classics — Falcon 12289,
78+
BabyBear `15·2²⁷+1`, the FFT primes `3·2³⁰+1` and `127·2²⁴+1`-class
79+
entries, and Goldilocks (F = 3, no scaling needed).
80+
- The scarce property is the **shift-friendly ψ**, not the cheap fold:
81+
in the sweep, only Falcon's q = 12289 has a signed-digit-weight-≤2
82+
primitive 2048-th root among small candidates (ψ = 7). Everywhere
83+
else the ψ-fold still works but needs a constant multiply instead of
84+
a shift-subtract — cheap in software, a real multiplier in hardware.
85+
A protocol that wants the multiplier-free fold must select for it
86+
explicitly.
87+
88+
## What does not transfer
89+
90+
- No asymptotic change: the transform stays O(N log N) with the same
91+
schedule; these are constant-factor, representation-level savings.
92+
- Desktop-CPU scalar code: multipliers are effectively free there.
93+
- All software claims above are cost-model arguments, not measurements;
94+
none of them is validated by this repository's CI. Treat this note as
95+
a map of where measuring would be worthwhile.

generator/prime_scout.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env -S uv run --script
2+
# /// script
3+
# requires-python = ">=3.9"
4+
# dependencies = []
5+
# ///
6+
"""
7+
Prime scout: enumerate Proth primes q = k*2^m + 1 whose k has signed-digit
8+
weight <= 2 (k = 2^a +/- 1), i.e. the primes for which the K-RED fold costs
9+
at most one adder/subtractor per fold — the selection criterion
10+
docs/algorithm-level.md proposes for new protocol parameters.
11+
12+
For each hit we print the fold count F (from kred_gen's planner) and the
13+
smallest shift-friendly psi (signed-digit weight <= 2) that is a primitive
14+
2048-th root of unity, i.e. supports the N = 1024 negacyclic NTT with a
15+
multiplier-free psi-fold ROM ('-' if none of the small candidates works;
16+
the psi-fold then needs a constant multiply instead).
17+
18+
Windows scanned: 14-16 bit (Falcon-class), 28-32 bit (BabyBear-class),
19+
60-64 bit (RNS/FHE-class). Run: uv run generator/prime_scout.py
20+
"""
21+
import sys, os
22+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
23+
from kred_gen import plan_kred
24+
25+
MR_BASES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37) # exact for < 2^64
26+
27+
28+
def is_prime(n):
29+
if n < 2:
30+
return False
31+
for p in MR_BASES:
32+
if n % p == 0:
33+
return n == p
34+
d, s = n - 1, 0
35+
while d % 2 == 0:
36+
d //= 2
37+
s += 1
38+
for a in MR_BASES:
39+
x = pow(a, d, n)
40+
if x in (1, n - 1):
41+
continue
42+
for _ in range(s - 1):
43+
x = x * x % n
44+
if x == n - 1:
45+
break
46+
else:
47+
return False
48+
return True
49+
50+
51+
def shift_friendly_psi(q, n=1024):
52+
"""Smallest psi with signed-digit weight <= 2 that is a primitive
53+
2n-th root mod q (psi^n == -1)."""
54+
if (q - 1) % (2 * n):
55+
return None
56+
cands = sorted({(1 << a) + s for a in range(1, 8) for s in (-1, 1)})
57+
for psi in cands:
58+
if 1 < psi < q and pow(psi, n, q) == q - 1:
59+
return psi
60+
return None
61+
62+
63+
WINDOWS = [(14, 16), (28, 32), (60, 64)]
64+
KNOWN = {12289: "Falcon", 3329: "Kyber", 8380417: "Dilithium",
65+
2013265921: "BabyBear", (2**32 - 1) * 2**32 + 1: "Goldilocks"}
66+
67+
68+
def main():
69+
print(f"{'q':>22} {'k':>12} {'m':>3} {'F':>2} {'psi':>4} note")
70+
for lo, hi in WINDOWS:
71+
print(f"-- {lo}-{hi} bit --")
72+
rows = []
73+
for a in range(1, hi):
74+
for s in (-1, 1):
75+
k = (1 << a) + s
76+
if k < 1 or k % 2 == 0:
77+
continue
78+
for m in range(12, hi + 1):
79+
q = k * (1 << m) + 1
80+
if not (lo <= q.bit_length() <= hi):
81+
continue
82+
if not is_prime(q):
83+
continue
84+
F = len(plan_kred(q)["folds"])
85+
psi = shift_friendly_psi(q)
86+
rows.append((q, k, a, s, m, F, psi))
87+
seen = set()
88+
for q, k, a, s, m, F, psi in sorted(set(rows)):
89+
if q in seen:
90+
continue
91+
seen.add(q)
92+
kform = f"2^{a}{'+' if s > 0 else '-'}1"
93+
note = KNOWN.get(q, "")
94+
print(f"{q:>22} {kform:>12} {m:>3} {F:>2} "
95+
f"{psi if psi else '-':>4} {note}")
96+
97+
98+
if __name__ == "__main__":
99+
main()

0 commit comments

Comments
 (0)