mirror of
https://github.com/luxfi/lattice-estimator.git
synced 2026-10-30 16:21:33 +00:00
308 lines
10 KiB
Python
308 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
See :ref:`Coded-BKW for LWE` for what is available.
|
||
"""
|
||
from sage.all import ZZ, ceil, log, floor, sqrt, find_root, erf, oo, cached_function, RR
|
||
|
||
from .lwe_parameters import LWEParameters
|
||
from .util import local_minimum
|
||
from .cost import Cost
|
||
from .errors import InsufficientSamplesError
|
||
from .prob import amplify_sigma
|
||
from .nd import sigmaf
|
||
from .io import Logging
|
||
|
||
cfft = 1 # convolutions mod q
|
||
|
||
|
||
class CodedBKW:
|
||
@staticmethod
|
||
def N(i, sigma_set, b, q):
|
||
"""
|
||
Return `N_i` for the `i`-th `[N_i, b]` linear code.
|
||
|
||
:param i: index
|
||
:param sigma_set: target noise level
|
||
"""
|
||
return floor(b / (1 - log(12 * sigma_set**2 / 2**i, q) / 2))
|
||
|
||
@staticmethod
|
||
@cached_function
|
||
def ntest(n, ell, t1, t2, b, q):
|
||
"""
|
||
If the parameter ``ntest`` is not provided, we use this function to estimate it.
|
||
|
||
:param n: LWE dimension > 0.
|
||
:param ell: Table size for hypothesis testing.
|
||
:param t1: Number of normal BKW steps.
|
||
:param t2: Number of coded BKW steps.
|
||
:param b: Table size for BKW steps.
|
||
|
||
"""
|
||
# there is no hypothesis testing because we have enough normal BKW
|
||
# tables to cover all of of n
|
||
if t1 * b >= n:
|
||
return 0
|
||
|
||
# solve for ntest by aiming for ntop == 0
|
||
def ntop(ntest):
|
||
# Patch so that `find_root` (which uses float) doesn't error
|
||
ntest = RR(ntest)
|
||
sigma_set = sqrt(q ** (2 * (1 - ell / ntest)) / 12)
|
||
ncod = sum(CodedBKW.N(i, sigma_set, b, q) for i in range(1, t2 + 1))
|
||
res = n - ncod - ntest - t1 * b
|
||
return res
|
||
|
||
try:
|
||
start = max(int(round(find_root(ntop, 2, n - t1 * b + 1, rtol=0.1))) - 1, 2)
|
||
except RuntimeError:
|
||
start = 2
|
||
ntest_min = 1
|
||
for ntest in range(start, n - t1 * b + 1):
|
||
if abs(ntop(ntest).n()) >= abs(ntop(ntest_min).n()):
|
||
break
|
||
ntest_min = ntest
|
||
return int(ntest_min)
|
||
|
||
def t1(params: LWEParameters, ell, t2, b, ntest=None):
|
||
"""
|
||
We compute t1 from N_i by observing that any N_i ≤ b gives no advantage over vanilla
|
||
BKW, but the estimates for coded BKW always assume quantisation noise, which is too
|
||
pessimistic for N_i ≤ b.
|
||
"""
|
||
|
||
t1 = 0
|
||
if ntest is None:
|
||
ntest = CodedBKW.ntest(params.n, ell, t1, t2, b, params.q)
|
||
sigma_set = sqrt(params.q ** (2 * (1 - ell / ntest)) / 12)
|
||
Ni = [CodedBKW.N(i, sigma_set, b, params.q) for i in range(1, t2 + 1)]
|
||
t1 = sum(e <= b for e in Ni) # comp. how many in Ni are <= to b
|
||
# there is no point in having more tables than needed to cover n
|
||
if b * t1 > params.n:
|
||
t1 = params.n // b
|
||
return t1
|
||
|
||
@staticmethod
|
||
def cost(
|
||
t2: int,
|
||
b: int,
|
||
ntest: int,
|
||
params: LWEParameters,
|
||
success_probability=0.99,
|
||
log_level=1,
|
||
):
|
||
"""
|
||
Coded-BKW cost.
|
||
|
||
:param t2: Number of coded BKW steps (≥ 0).
|
||
:param b: Table size (≥ 1).
|
||
:param success_probability: Targeted success probability < 1.
|
||
:param ntest: Number of coordinates to hypothesis test.
|
||
|
||
"""
|
||
cost = Cost()
|
||
|
||
# Our cost is mainly determined by q^b, on the other hand there are expressions in q^(ℓ+1)
|
||
# below, hence, we set ℓ = b - 1. This allows to achieve the performance reported in
|
||
# [C:GuoJohSta15].
|
||
|
||
cost["b"] = b
|
||
ell = b - 1
|
||
cost["ell"] = ell
|
||
|
||
secret_bounds = params.Xs.bounds
|
||
if params.Xs.is_Gaussian_like and params.Xs.mean == 0:
|
||
secret_bounds = (
|
||
max(secret_bounds[0], -3 * params.Xs.stddev),
|
||
min(secret_bounds[1], 3 * params.Xs.stddev),
|
||
)
|
||
|
||
# base of the table size.
|
||
zeta = secret_bounds[1] - secret_bounds[0] + 1
|
||
|
||
t1 = CodedBKW.t1(params, ell, t2, b, ntest)
|
||
t2 -= t1
|
||
|
||
cost["t1"] = t1
|
||
cost["t2"] = t2
|
||
|
||
cost.register_impermanent(t1=False, t2=False)
|
||
|
||
# compute ntest with the t1 just computed
|
||
if ntest is None:
|
||
ntest = CodedBKW.ntest(params.n, ell, t1, t2, b, params.q)
|
||
|
||
# if there's no ntest then there's no `σ_{set}` and hence no ncod
|
||
if ntest:
|
||
sigma_set = sqrt(params.q ** (2 * (1 - ell / ntest)) / 12)
|
||
ncod = sum(CodedBKW.N(i, sigma_set, b, params.q) for i in range(1, t2 + 1))
|
||
else:
|
||
ncod = 0
|
||
|
||
ntot = ncod + ntest
|
||
ntop = max(params.n - ncod - ntest - t1 * b, 0)
|
||
cost["#cod"] = ncod # coding step
|
||
cost["#top"] = ntop # guessing step, typically zero
|
||
cost["#test"] = ntest # hypothesis testing
|
||
|
||
cost.register_impermanent({"#cod": False, "#top": False, "#test": False})
|
||
|
||
# Theorem 1: quantization noise + addition noise
|
||
coding_variance = params.Xs.stddev**2 * sigma_set**2 * ntot
|
||
sigma_final = float(sqrt(2 ** (t1 + t2) * params.Xe.stddev**2 + coding_variance))
|
||
|
||
M = amplify_sigma(success_probability, sigmaf(sigma_final), params.q)
|
||
if M is oo:
|
||
cost["rop"] = oo
|
||
cost["m"] = oo
|
||
return cost
|
||
m = (t1 + t2) * ZZ(params.q**b - 1) / 2 + M
|
||
cost["m"] = float(m)
|
||
cost.register_impermanent(m=True)
|
||
|
||
if not params.Xs <= params.Xe:
|
||
# Equation (7)
|
||
n = params.n - t1 * b
|
||
C0 = (m - n) * (params.n + 1) * ceil(n / (b - 1))
|
||
assert C0 >= 0
|
||
else:
|
||
C0 = 0
|
||
|
||
# Equation (8)
|
||
C1 = sum(
|
||
(params.n + 1 - i * b) * (m - i * ZZ(params.q**b - 1) / 2) for i in range(1, t1 + 1)
|
||
)
|
||
assert C1 >= 0
|
||
|
||
# Equation (9)
|
||
C2_ = sum(
|
||
4 * (M + i * ZZ(params.q**b - 1) / 2) * CodedBKW.N(i, sigma_set, b, params.q)
|
||
for i in range(1, t2 + 1)
|
||
)
|
||
C2 = float(C2_)
|
||
for i in range(1, t2 + 1):
|
||
C2 += float(
|
||
ntop + ntest + sum(CodedBKW.N(j, sigma_set, b, params.q) for j in range(1, i + 1))
|
||
) * (M + (i - 1) * ZZ(params.q**b - 1) / 2)
|
||
assert C2 >= 0
|
||
|
||
# Equation (10)
|
||
C3 = M * ntop * (2 * zeta + 1) ** ntop
|
||
assert C3 >= 0
|
||
|
||
# Equation (11)
|
||
C4_ = 4 * M * ntest
|
||
C4 = C4_ + (2 * zeta + 1) ** ntop * (
|
||
cfft * ZZ(params.q ** (ell + 1)) * (ell + 1) * log(params.q, 2) + params.q ** (ell + 1)
|
||
)
|
||
assert C4 >= 0
|
||
|
||
C = (C0 + C1 + C2 + C3 + C4) / (
|
||
erf(zeta / sqrt(2 * params.Xe.stddev)) ** ntop
|
||
) # TODO don't ignore success probability
|
||
try:
|
||
cost["rop"] = float(C)
|
||
except TypeError:
|
||
cost["rop"] = oo
|
||
cost["mem"] = (t1 + t2) * params.q**b
|
||
|
||
cost = cost.reorder("rop", "m", "mem", "b", "t1", "t2")
|
||
cost["tag"] = "coded-bkw"
|
||
cost["problem"] = params
|
||
Logging.log("bkw", log_level + 1, f"{cost!r}")
|
||
|
||
return cost
|
||
|
||
@classmethod
|
||
def b(
|
||
cls,
|
||
params: LWEParameters,
|
||
ntest=None,
|
||
log_level=1,
|
||
):
|
||
def sf(x, best):
|
||
return (x["rop"] <= best["rop"]) and not (best["m"] <= params.m < x["m"])
|
||
|
||
# the outer search is over b, which determines the size of the tables: q^b
|
||
b_max = 3 * ceil(log(params.q, 2))
|
||
with local_minimum(2, b_max, smallerf=sf) as it_b:
|
||
for b in it_b:
|
||
# the inner search is over t2, the number of coded steps
|
||
t2_max = max(3, params.n // b)
|
||
with local_minimum(2, t2_max, smallerf=sf) as it_t2:
|
||
for t2 in it_t2:
|
||
y = cls.cost(b=b, t2=t2, ntest=ntest, params=params)
|
||
it_t2.update(y)
|
||
it_b.update(it_t2.y)
|
||
best = it_b.y
|
||
|
||
# the search cannot fail. It just outputs some X with X["oracle"]>m.
|
||
if best["m"] > params.m:
|
||
raise InsufficientSamplesError(
|
||
f"Got m≈2^{float(log(params.m, 2.0)):.1f} samples, but require ≈2^{float(log(best['m'], 2.0)):.1f}.",
|
||
best["m"],
|
||
)
|
||
return best
|
||
|
||
def __call__(
|
||
self,
|
||
params: LWEParameters,
|
||
ntest=None,
|
||
log_level=1,
|
||
):
|
||
"""
|
||
Coded-BKW as described in [C:GuoJohSta15]_.
|
||
|
||
:param params: LWE parameters
|
||
:param ntest: Number of coordinates to hypothesis test.
|
||
:return: A cost dictionary
|
||
|
||
The returned cost dictionary has the following entries:
|
||
|
||
- ``rop``: Total number of word operations (≈ CPU cycles).
|
||
- ``b``: BKW tables have size `q^b`.
|
||
- ``t1``: Number of plain BKW tables.
|
||
- ``t2``: Number of Coded-BKW tables.
|
||
- ``ℓ``: Hypothesis testing has tables of size `q^{ℓ+1}`
|
||
- ``#cod``: Number of coding steps.
|
||
- ``#top``: Number of guessing steps (typically zero)
|
||
- ``#test``: Number of coordinates to do hypothesis testing on.
|
||
|
||
EXAMPLE::
|
||
|
||
>>> from sage.all import oo
|
||
>>> from estimator import *
|
||
>>> LWE.coded_bkw(schemes.LightSaber.updated(m=oo))
|
||
rop: ≈2^171.7, m: ≈2^159.4, mem: ≈2^160.4, b: 12, t1: 3, t2: 18, ℓ: 11, #cod: 423, #top: 1...
|
||
|
||
We may need to amplify the number of samples, which modifies the noise distribution::
|
||
|
||
>>> from sage.all import oo
|
||
>>> from estimator import *
|
||
>>> schemes.Kyber512
|
||
LWEParameters(n=512, q=3329, Xs=D(σ=1.22), Xe=D(σ=1.22), m=512, tag='Kyber 512')
|
||
>>> cost = LWE.coded_bkw(schemes.Kyber512); cost
|
||
rop: ≈2^178.8, m: ≈2^166.8, mem: ≈2^167.8, b: 14, t1: 0, t2: 16, ℓ: 13, #cod: 448, #top: 0, #test: 64, ...
|
||
>>> cost["problem"]
|
||
LWEParameters(n=512, q=3329, Xs=D(σ=1.22), Xe=D(σ=6.24), m=..., tag='Kyber 512')
|
||
|
||
TESTS::
|
||
|
||
>>> LWE.coded_bkw(schemes.TFHE630)
|
||
rop: ≈2^144.7, m: ≈2^131.8, mem: ≈2^132.8, b: 4, t1: 0, t2: 27, ℓ: 3, #cod: 559, #top: 0, #test: 71, ...
|
||
|
||
.. note :: See also [C:KirFou15]_.
|
||
|
||
"""
|
||
params = LWEParameters.normalize(params)
|
||
params_ = params
|
||
while True:
|
||
try:
|
||
return self.b(params_, ntest=ntest, log_level=log_level)
|
||
except InsufficientSamplesError as e:
|
||
m = e.args[1]
|
||
params_ = params.amplify_m(m)
|
||
|
||
|
||
coded_bkw = CodedBKW()
|