# Source code for estimator.simulator

```# -*- coding: utf-8 -*-
"""
Simulate lattice reduction on the rows of::

⌜ ξI  A  0 ⌝
ǀ  0 qI  0 |
⌞ 0   c  τ ⌟

where

- ξI ∈ ZZ^{n × n},
- A ∈ ZZ_q^{n × m},
- qI ∈ ZZ^{m × m},
- τ ∈ ZZ and
- d = m + n + 1.

The last row is optional.
"""

from sage.all import RR, log, line, cached_function, pi, exp
from functools import partial

[docs]
def qary_simulator(f, d, n, q, beta, xi=1, tau=1, dual=False, ignore_qary=False):
"""
Reduced lattice shape calling ``f``.

:param d: Lattice dimension.
:param n: The number of `q` vectors is `d-n-1`.
:param q: Modulus `q`
:param beta: Block size β.
:param xi: Scaling factor ξ for identity part.
:param tau: Kannan factor τ.
:param dual: perform reduction on the dual.
:param ignore_qary: Ignore the special q-ary structure (forget q vectors)

"""

if not tau:
r = [q**2] * (d - n) + [xi**2] * n
else:
r = [q**2] * (d - n - 1) + [xi**2] * n + [tau**2]

if ignore_qary:
r = GSA(d, n, q, 2, xi=xi, tau=tau)

if dual:
# 1. reverse and reflect the basis (go to dual)
r = [1 / r_ for r_ in reversed(r)]
# 2. simulate reduction on the dual basis
r = f(r, beta)
# 3. reflect and reverse the basis (go back to primal)
r = [1 / r_ for r_ in reversed(r)]
return r
else:
return f(r, beta)

[docs]
def CN11(d, n, q, beta, xi=1, tau=1, dual=False, ignore_qary=False):
"""
Reduced lattice shape using simulator from [AC:CheNgu11]_

:param d: Lattice dimension.
:param n: The number of `q` vectors is `d-n-1`.
:param q: Modulus `q`
:param beta: Block size β.
:param xi: Scaling factor ξ for identity part.
:param tau: Kannan factor τ.
:param dual: perform reduction on the dual.
:param ignore_qary: Ignore the special q-ary structure (forget q vectors)
:returns: squared Gram-Schmidt norms

"""

from fpylll import BKZ
from fpylll.tools.bkz_simulator import simulate

def f(r, beta):
return simulate(r, BKZ.EasyParam(beta))[0]

return qary_simulator(f=f, d=d, n=n, q=q, beta=beta, xi=xi, tau=tau, dual=dual, ignore_qary=ignore_qary)

CN11_NQ = partial(CN11, ignore_qary=True)

[docs]
def GSA(d, n, q, beta, xi=1, tau=1, dual=False):
"""
Reduced lattice shape following the Geometric Series Assumption [Schnorr03]_

:param d: Lattice dimension.
:param n: The number of `q` vectors is `d-n-1`.
:param q: Modulus `q`
:param beta: Block size β.
:param xi: Scaling factor ξ for identity part.
:param tau: Kannan factor τ.
:param dual: ignored, since GSA is self-dual: applying the GSA to the dual is equivalent to
applying it to the primal.
:returns: squared Gram-Schmidt norms

"""
from .reduction import delta as deltaf

if not tau:
log_vol = RR(log(q, 2) * (d - n) + log(xi, 2) * n)
else:
log_vol = RR(log(q, 2) * (d - n - 1) + log(xi, 2) * n + log(tau, 2))

delta = deltaf(beta)
r_log = [(d - 1 - 2 * i) * RR(log(delta, 2)) + log_vol / d for i in range(d)]
r = [2 ** (2 * r_) for r_ in r_log]
return r

[docs]
def ZGSA(d, n, q, beta, xi=1, tau=1, dual=False):
from math import lgamma
from .util import gh_constant, small_slope_t8
"""
Reduced lattice Z-shape following the Geometric Series Assumption as specified in
NTRU fatrigue [DucWoe21]_
:param d: Lattice dimension.
:param n: The number of `q` vectors is `d-n`.
:param q: Modulus `q`
:param beta: Block size β.
:param xi: Scaling factor ξ for identity part.
:param dual: ignored, since GSA is self-dual: applying the GSA to the dual is equivalent to
applying it to the primal.
:returns: Squared Gram-Schmidt norms

EXAMPLES:

>>> from estimator.simulator import GSA, ZGSA, CN11
>>> n = 128
>>> d = 213
>>> q = 2048
>>> beta = 40
>>> xi = 1
>>> tau = 1
>>> zgsa_profile = ZGSA(d, n, q, beta, xi, tau)
>>> len(zgsa_profile)
214

Setting tau to False indicates a homogeneous instance.

>>> tau = False
>>> zgsa_profile = ZGSA(d, n, q, beta, xi, tau)
>>> len(zgsa_profile)
213

All three profiles should have the same product (represent the same lattice volume)

>>> gsa_profile = GSA(d, n, q, beta, xi, tau)
>>> cn11_profile = CN11(d, n, q, beta, xi, tau)
>>> sum([log(x) for x in cn11_profile]
1296.1852276471009
>>> sum([log(x) for x in zgsa_profile])
1296.18522764710
>>> sum([log(x) for x in gsa_profile])
1296.18522764710

Changing xi will change the volume of the lattice

>>> xi = 2
>>> gsa_profile = GSA(d, n, q, beta, xi, tau)
>>> zgsa_profile = ZGSA(d, n, q, beta, xi, tau)
>>> cn11_profile = CN11(d, n, q, beta, xi, tau)
>>> sum([log(x) for x in gsa_profile])
1473.63090587044
>>> sum([log(x) for x in zgsa_profile])
1473.63090587044
>>> sum([log(x) for x in cn11_profile])
1473.630905870442
"""

@cached_function
def ball_log_vol(n):
return RR((n/2.) * log(pi) - lgamma(n/2. + 1))

def log_gh(d, logvol=0):
if d < 49:
return RR(gh_constant[d] + logvol/d)

return RR(1./d * (logvol - ball_log_vol(d)))

def delta(k):
assert k >= 60
delta = exp(log_gh(k)/(k-1))
return RR(delta)

@cached_function
def slope(beta):
if beta<=60:
return small_slope_t8[beta]
if beta<=70:
# interpolate between experimental and asymptotics
ratio = (70-beta)/10.
return ratio*small_slope_t8[60]+(1.-ratio)*2*log(delta(70))
else:
return 2 * log(delta(beta))

if not tau:
L_log = (d - n)*[RR(log(q))] + n * [RR(log(xi))]
num_q_vec = (d - n)

else:
L_log = (d - n - 1)*[RR(log(q))] + n * [RR(log(xi))] + [RR(log(tau))]
num_q_vec = (d - n - 1)

slope_ = slope(beta)
diff = slope(beta)/2.

for i in range(num_q_vec):
if diff > (RR(log(q)) - RR(log(xi)))/2.:
break

low = (num_q_vec)-i-1
high = (num_q_vec) + i
if low >= 0:
L_log[low] = (RR(log(q)) + RR(log(xi)))/2. + diff

if high < len(L_log):
L_log[high] = (RR(log(q)) + RR(log(xi)))/2. - diff

diff += slope_

# Output basis profile as squared lengths, not ln(length)
L = [exp(2 * l_) for l_ in sorted(L_log, reverse=True)]
return L

[docs]
def LGSA(d, n, q, beta, xi=1, tau=1, dual=False):
"""
Reduced lattice shape following the Z-shape Geometric Series Assumption with basis
rerandomization. Results in BKZ 'forgetting' the q-vectors [Dilithium21]_

:param d: Lattice dimension.
:param n: The number of `q` vectors is `d-n-1`.
:param q: Modulus `q`
:param beta: Block size β.
:param xi: Scaling factor ξ for identity part.
:param tau: Kannan factor τ.
:param dual: ignored, since LGSA is self-dual: applying the GSA to the dual is equivalent to
applying it to the primal.
:returns: squared Gram-Schmidt norms

EXAMPLES:

>>> from estimator.simulator import GSA, CN11, CN11_NQ, ZGSA, LGSA
>>> n = 6
>>> d = 12
>>> q = 31
>>> beta = 3
>>> xi = 1
>>> tau = 1

Let's check out some toy basis shapes for clarity.
First the GSA. Assumes that the (log) basis profile follows a line

>>> print(["{0:0.2f}".format(RR(log(r_ , 2))) for r_ in GSA(d, n, q, beta, xi, tau)])
['4.82', '4.69', '4.57', '4.44', '4.32', '4.19', '4.07', '3.94', '3.82', '3.69', '3.57', '3.44']

Next, the ZGSA. Assumes the (log) basis profile follows a Z-shape. Here, only Zone III (length 1 vectors)
is present. The dimension is too small to exhibit the q-vectors at the beginning of the profile.

>>> print(["{0:0.2f}".format(RR(log(r_ , 2))) for r_ in ZGSA(d, n, q, beta, xi, tau)])
['5.53', '5.41', '5.28', '5.15', '5.02', '4.89', '4.76', '4.63', '4.50', '4.37', '0.00', '0.00']

The LGSA. Assumes the (log) basis profile follows an L-shape. The dimension is too small and thus it
follows the regular GSA.

>>> print(["{0:0.2f}".format(RR(log(r_ , 2))) for r_ in LGSA(d, n, q, beta, xi, tau)])
['4.82', '4.69', '4.57', '4.44', '4.32', '4.19', '4.07', '3.94', '3.82', '3.69', '3.57', '3.44']

The CN11 simulator is supposed to be the most accurate shape estimator, comming from [CheNgu12]_.
>>> print(["{0:0.2f}".format(RR(log(r_ , 2))) for r_ in CN11(d, n, q, beta, xi, tau)])
['4.94', '4.79', '4.62', '4.45', '4.27', '4.10', '3.95', '3.83', '3.73', '3.66', '3.61', '3.60']

If we want to ignore the q-ary structure of the lattice, but still use the CN11 simulator, use CN11_NQ. It first
processes the basis with LLL (using the GSA, beta=2), then running CN11 on the preprocessed basis.

>>> print(["{0:0.2f}".format(RR(log(r_ , 2))) for r_ in CN11_NQ(d, n, q, beta, xi, tau)])
['4.37', '4.32', '4.28', '4.23', '4.19', '4.14', '4.10', '4.06', '4.01', '3.98', '3.94', '3.93']
>>> zgsa_profile = ZGSA(d, n, q, beta, xi, tau)
"""
from .reduction import delta as deltaf

if not tau:
log_vol = RR((d - n)*log(q, 2) + n*log(xi, 2))
r_log = d*[RR(log(xi, 2))]
profile_log_vol = d*RR(log(xi, 2))

else:
log_vol = RR((d - n - 1)*log(q, 2) + n*log(xi, 2) + log(tau, 2))
r_log = (d - 1)*[RR(log(xi, 2))] + [RR(log(tau, 2))]
profile_log_vol = RR((d - 1)*log(xi, 2) + log(tau, 2))

slope = -2 * RR(log(deltaf(beta), 2))
log_vec_len = 0
for i in range(d - 1, -1, -1):
log_vec_len -= slope
profile_log_vol += log_vec_len
r_log[i] += log_vec_len

if profile_log_vol > log_vol:
break

# Rearrange r to have proper profile shape
num_gsa_vec = d - i
r_log = sorted(r_log, reverse=True)

profile_log_vol = sum(r_log)
diff = profile_log_vol - log_vol

for i in range(num_gsa_vec):  # Small shift of the GSA sequence to fix volume
r_log[i] -= diff / num_gsa_vec

profile_log_vol = sum(r_log)
assert abs(profile_log_vol/log_vol - 1) < 1e-6  # Sanity check the volume

r = [2**(2 * r_) for r_ in r_log]
return r

[docs]
def normalize(name):
if str(name).upper() == "CN11":
return CN11

if str(name).upper() == "CN11_NQ":
return CN11_NQ

if str(name).upper() == "GSA":
return GSA

if str(name).upper() == "ZGSA":
return ZGSA

if str(name).upper() == "LGSA":
return LGSA

return name

[docs]
def plot_gso(r, *args, **kwds):
return line([(i, log(r_, 2) / 2.0) for i, r_ in enumerate(r)], *args, **kwds)

```