From 16a6201a74f1a8f008159c6304c742079d7fe0b9 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 14 May 2021 09:22:41 +0100 Subject: [PATCH] add local copy of lwe-estimator --- Makefile | 20 + README.md | 2 - README.rst | 202 ++ __init__.py | 2 + __pycache__/__init__.cpython-37.pyc | Bin 0 -> 173 bytes __pycache__/estimator.cpython-37.pyc | Bin 0 -> 93391 bytes bitbucket-pipelines.yml | 31 + bkw_legacy.py | 315 +++ doc/Makefile | 225 ++ doc/_templates/autosummary/base.rst | 6 + doc/_templates/autosummary/class.rst | 35 + doc/_templates/autosummary/module.rst | 32 + doc/api_doc.rst | 9 + doc/conf.py | 394 +++ doc/documentationreadme.rst | 18 + doc/genindex.rst | 4 + doc/index.rst | 26 + doc/readme_link.rst | 7 + doctest.sh | 12 + estimator.py | 3467 +++++++++++++++++++++++++ fix-doctest.sh | 13 + pyproject.toml | 2 + readthedocs.yml | 1 + requirements.txt | 2 + 24 files changed, 4823 insertions(+), 2 deletions(-) create mode 100644 Makefile delete mode 100644 README.md create mode 100644 README.rst create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-37.pyc create mode 100644 __pycache__/estimator.cpython-37.pyc create mode 100644 bitbucket-pipelines.yml create mode 100644 bkw_legacy.py create mode 100644 doc/Makefile create mode 100644 doc/_templates/autosummary/base.rst create mode 100644 doc/_templates/autosummary/class.rst create mode 100644 doc/_templates/autosummary/module.rst create mode 100644 doc/api_doc.rst create mode 100644 doc/conf.py create mode 100644 doc/documentationreadme.rst create mode 100644 doc/genindex.rst create mode 100644 doc/index.rst create mode 100644 doc/readme_link.rst create mode 100755 doctest.sh create mode 100644 estimator.py create mode 100755 fix-doctest.sh create mode 100644 pyproject.toml create mode 100644 readthedocs.yml create mode 100644 requirements.txt diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..757cd2dd3 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = LWEEstimator +SOURCEDIR = doc +BUILDDIR = doc/_build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/README.md b/README.md deleted file mode 100644 index e8418a6dc..000000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# estimator-issues -Solving some issues in the LWE Estimator diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..ebe6db180 --- /dev/null +++ b/README.rst @@ -0,0 +1,202 @@ +Security Estimates for the Learning with Errors Problem +======================================================= + +This `Sage `__ module provides functions for estimating the concrete security +of `Learning with Errors `__ instances. + +The main intend of this estimator is to give designers an easy way to choose parameters resisting +known attacks and to enable cryptanalysts to compare their results and ideas with other techniques +known in the literature. + +Usage Examples +-------------- + +:: + + sage: load("estimator.py") + sage: n, alpha, q = Param.Regev(128) + sage: costs = estimate_lwe(n, alpha, q) + usvp: rop: ≈2^57.3, red: ≈2^57.3, δ_0: 1.009214, β: 101, d: 349, m: 220 + dec: rop: ≈2^61.9, m: 229, red: ≈2^61.9, δ_0: 1.009595, β: 93, d: 357, babai: ≈2^46.8, babai_op: ≈2^61.9, repeat: 293, ε: 0.015625 + dual: rop: ≈2^81.1, m: 380, red: ≈2^81.1, δ_0: 1.008631, β: 115, d: 380, |v|: 688.951, repeat: ≈2^17.0, ε: 0.007812 + +Online +------ + +You can `run the estimator +online `__ +using the `Sage Math Cell `__ server. + +Coverage +-------- + +At present the following algorithms are covered by this estimator. + +- meet-in-the-middle exhaustive search +- Coded-BKW [C:GuoJohSta15] +- dual-lattice attack and small/sparse secret variant [EC:Albrecht17] +- lattice-reduction + enumeration [RSA:LinPei11] +- primal attack via uSVP [USENIX:ADPS16,ACISP:BaiGal14] +- Arora-Ge algorithm [ICALP:AroGe11] using Gröbner bases + [EPRINT:ACFP14] + +The following distributions for the secret are supported: + +- ``"normal"`` : normal form instances, i.e. the secret follows the noise distribution (alias: ``True``) +- ``"uniform"`` : uniform mod q (alias: ``False``) +- ``(a,b)`` : uniform in the interval ``[a,…,b]`` +- ``((a,b), h)`` : exactly ``h`` components are ``∈ [a,…,b]\{0}``, all other components are zero + +We note that distributions of the form ``(a,b)`` are assumed to be of fixed Hamming weight, with ``h = round((b-a)/(b-a+1) * n)``. + +Above, we use `cryptobib `__-style bibtex keys as references. + +Documentation +------------- + +Documentation for the ``estimator`` is available `here `__. + +Evolution +--------- + +This code is evolving, new results are added and bugs are fixed. Hence, estimations from earlier +versions might not match current estimations. This is annoying but unavoidable at present. We +recommend to also state the commit that was used when referencing this project. + +We also encourage authors to let us know if their paper uses this code. In particular, we thrive to +tag commits with those cryptobib ePrint references that use it. For example, `this commit +`__ +corresponds to this `ePrint entry `__. + +Contributions +------------- + +Our intent is for this estimator to be maintained by the research community. For example, we +encourage algorithm designers to add their own algorithms to this estimator and we are happy to help +with that process. + +More generally, all contributions such as bugfixes, documentation and tests are welcome. Please go +ahead and submit your pull requests. Also, don’t forget to add yourself to the list of contributors +below in your pull requests. + +At present, this estimator is maintained by Martin Albrecht. Contributors are: + +- Benjamin Curtis +- Cedric Lefebvre +- Fernando Virdia +- Florian Göpfert +- James Owen +- Léo Ducas +- Markus Schmidt +- Martin Albrecht +- Rachel Player +- Sam Scott + +Please follow `PEP8 `__ in your submissions. You can use +`flake8 `__ to check for compliance. We use the following flake8 +configuration (to allow longer line numbers and more complex functions): + +:: + + [flake8] + max-line-length = 120 + max-complexity = 16 + ignore = E22,E241 + +Bugs +---- + +If you run into a bug, please open an `issue on bitbucket +`__. Also, please check +first if the issue has already been reported. + +Citing +------ + +If you use this estimator in your work, please cite + + | Martin R. Albrecht, Rachel Player and Sam Scott. *On the concrete hardness of Learning with Errors*. + | Journal of Mathematical Cryptology. Volume 9, Issue 3, Pages 169–203, ISSN (Online) 1862-2984, + | ISSN (Print) 1862-2976 DOI: 10.1515/jmc-2015-0016, October 2015 + +A pre-print is available as + + Cryptology ePrint Archive, Report 2015/046, 2015. https://eprint.iacr.org/2015/046 + +An updated version of the material covered in the above survey is available in +`Rachel Player's PhD thesis `__. + +License +------- + +The esimator is licensed under the `LGPLv3+ `__ license. + + +Parameters from the Literature +------------------------------ + +The following estimates for various schemes from the literature illustrate the behaviour of the +``estimator``. These estimates do not necessarily correspond to the claimed security levels of the +respective schemes: for several parameter sets below the claimed security level by the designers’ is +lower than the complexity estimated by the ``estimator``. This is usually because the designers +anticipate potential future improvements to lattice-reduction algorithms and strategies. We +recommend to follow the designers’ decision. We intend to extend the estimator to cover these more +optimistic (from an attacker’s point of view) estimates in the future … pull requests welcome, as +always. + +`New Hope `__ :: + + sage: load("estimator.py") + sage: n = 1024; q = 12289; stddev = sqrt(16/2); alpha = alphaf(sigmaf(stddev), q) + sage: _ = estimate_lwe(n, alpha, q, reduction_cost_model=BKZ.sieve) + usvp: rop: ≈2^313.1, red: ≈2^313.1, δ_0: 1.002094, β: 968, d: 2096, m: 1071 + dec: rop: ≈2^410.0, m: 1308, red: ≈2^410.0, δ_0: 1.001763, β: 1213, d: 2332, babai: ≈2^395.5, babai_op: ≈2^410.6, repeat: ≈2^25.2, ε: ≈2^-23.0 + dual: rop: ≈2^355.5, m: 1239, red: ≈2^355.5, δ_0: 1.001884, β: 1113, repeat: ≈2^307.0, d: 2263, c: 1 + +`Frodo `__ :: + + sage: load("estimator.py") + sage: n = 752; q = 2^15; stddev = sqrt(1.75); alpha = alphaf(sigmaf(stddev), q) + sage: _ = estimate_lwe(n, alpha, q, reduction_cost_model=BKZ.sieve) + usvp: rop: ≈2^173.0, red: ≈2^173.0, δ_0: 1.003453, β: 490, d: 1448, m: 695 + dec: rop: ≈2^208.3, m: 829, red: ≈2^208.3, δ_0: 1.003064, β: 579, d: 1581, babai: ≈2^194.5, babai_op: ≈2^209.6, repeat: 588, ε: 0.007812 + dual: rop: ≈2^196.2, m: 836, red: ≈2^196.2, δ_0: 1.003104, β: 569, repeat: ≈2^135.0, d: 1588, c: 1 + +`TESLA `__ :: + + sage: load("estimator.py") + sage: n = 804; q = 2^31 - 19; alpha = sqrt(2*pi)*57/q; m = 4972 + sage: _ = estimate_lwe(n, alpha, q, m=m, reduction_cost_model=BKZ.sieve) + usvp: rop: ≈2^129.3, red: ≈2^129.3, δ_0: 1.004461, β: 339, d: 1937, m: 1132 + dec: rop: ≈2^144.9, m: 1237, red: ≈2^144.9, δ_0: 1.004148, β: 378, d: 2041, babai: ≈2^130.9, babai_op: ≈2^146.0, repeat: 17, ε: 0.250000 + dual: rop: ≈2^139.4, m: 1231, red: ≈2^139.4, δ_0: 1.004180, β: 373, repeat: ≈2^93.0, d: 2035, c: 1 + +`SEAL `__ :: + + sage: load("estimator.py") + sage: n = 2048; q = 2^54 - 2^24 + 1; alpha = 8/q; m = 2*n + sage: _ = estimate_lwe(n, alpha, q, secret_distribution=(-1,1), reduction_cost_model=BKZ.sieve, m=m) + Warning: the LWE secret is assumed to have Hamming weight 1365. + usvp: rop: ≈2^129.7, red: ≈2^129.7, δ_0: 1.004479, β: 337, d: 3914, m: 1865, repeat: 1, k: 0, postprocess: 0 + dec: rop: ≈2^144.4, m: ≈2^11.1, red: ≈2^144.4, δ_0: 1.004154, β: 377, d: 4272, babai: ≈2^131.2, babai_op: ≈2^146.3, repeat: 7, ε: 0.500000 + dual: rop: ≈2^134.2, m: ≈2^11.0, red: ≈2^134.2, δ_0: 1.004353, β: 352, repeat: ≈2^59.6, d: 4091, c: 3.909, k: 32, postprocess: 10 + +`LightSaber `__ :: + + sage: load("estimator.py") + sage: n = 512 + sage: q = 8192 + sage: alpha_0 = alphaf(sqrt(10/4.0), q, sigma_is_stddev=True) # error + sage: alpha_1 = alphaf(sqrt(21/4.0), q, sigma_is_stddev=True) # secret + sage: primal_usvp(n, alpha_0, q, secret_distribution=alpha_1, m=n, reduction_cost_model=BKZ.ADPS16) # not enough samples + Traceback (most recent call last): + ... + NotImplementedError: secret size 0.000701 > error size 0.000484 + + sage: primal_usvp(n, alpha_1, q, secret_distribution=alpha_0, m=n, reduction_cost_model=BKZ.ADPS16) + rop: 2^118.0 + red: 2^118.0 + delta_0: 1.003955 + beta: 404 + d: 1022 + m: 509 diff --git a/__init__.py b/__init__.py new file mode 100644 index 000000000..a8977649b --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from estimator import * # noqa diff --git a/__pycache__/__init__.cpython-37.pyc b/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d11121607bc2e60d83eba762946bb7e1b663121b GIT binary patch literal 173 zcmZ?b<>g`k0+WA}6U>41V-N=hn1BoiATH(s5-AKRj5!P;3@J>(44TX@fg%i=jJFuI z{D359YH>+sZemG((MpCQ7N886_+_IXTAW%`te=#cr|*(loL!P%pzoepl2MwZUyxXo rn5$cynv2KczG$)ehv0&mj8%V^l0` literal 0 HcmV?d00001 diff --git a/__pycache__/estimator.cpython-37.pyc b/__pycache__/estimator.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0229024a5b51c0743aacdebc018d9b6f3ad8c7ee GIT binary patch literal 93391 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 yVq*V=RIcN(@ORe>sdRt7 + +""" +from collections import OrderedDict +from estimator import cost_reorder, stddevf, sigmaf +from estimator import preprocess_params, amplify_sigma, secret_distribution_variance +from sage.functions.log import log +from sage.functions.other import ceil, sqrt +from sage.matrix.all import Matrix +from sage.modules.all import vector +from sage.rings.all import RealField +from sage.rings.all import ZZ +from sage.rings.infinity import PlusInfinity +from sage.structure.element import parent +from sage.symbolic.all import pi + + +oo = PlusInfinity() + + +def bkw_decision(n, alpha, q, success_probability=0.99, prec=None): + """ + Estimate the cost of running BKW to solve Decision-LWE following [DCC:ACFFP15]_. + + :param n: dimension > 0 + :param alpha: fraction of the noise α < 1.0 + :param q: modulus > 0 + :param success_probability: probability of success < 1.0 + :param prec: precision used for floating point computations + :param m: the number of available samples + + .. [DCC:ACFFP15] Albrecht, M. R., Cid, C., Jean-Charles Faugère, Fitzpatrick, R., & + Perret, L. (2015). On the complexity of the BKW algorithm on LWE. + Designs, Codes & Cryptography, Volume 74, Issue 2, pp 325-354 + """ + n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) + sigma = alpha*q + + RR = parent(alpha) + + def _run(t): + a = RR(t*log(n, 2)) # target number of adds: a = t*log_2(n) + b = RR(n/a) # window width + sigma_final = RR(n**t).sqrt() * sigma # after n^t adds we get this σ + + m = amplify_sigma(success_probability, sigma_final, q) + + tmp = a*(a-1)/2 * (n+1) - b*a*(a-1)/4 - b/6 * RR((a-1)**3 + 3/2*(a-1)**2 + (a-1)/2) + stage1a = RR(q**b-1)/2 * tmp + stage1b = m * (a/2 * (n + 2)) + stage1 = stage1a + stage1b + + nrops = RR(stage1) + nbops = RR(log(q, 2) * nrops) + ncalls = RR(a * ceil(RR(q**b)/RR(2)) + m) + nmem = ceil(RR(q**b)/2) * a * (n + 1 - b * (a-1)/2) + + current = OrderedDict([(u"t", t), + (u"bop", nbops), + (u"oracle", ncalls), + (u"m", m), + (u"mem", nmem), + (u"rop", nrops), + (u"a", a), + (u"b", b), + ]) + + current = cost_reorder(current, ("rop", u"oracle", u"t")) + return current + + best_runtime = None + t = RR(2*(log(q, 2) - log(sigma, 2))/log(n, 2)) + while True: + current = _run(t) + if not best_runtime: + best_runtime = current + else: + if best_runtime["rop"] > current["rop"]: + best_runtime = current + else: + break + t += 0.05 + + return best_runtime + + +def bkw_search(n, alpha, q, success_probability=0.99, prec=None): + """ + Estimate the cost of running BKW to solve Search-LWE following [C:DucTraVau15]_. + + :param n: dimension > 0 + :param alpha: fraction of the noise α < 1.0 + :param q: modulus > 0 + :param success_probability: probability of success < 1.0 + :param prec: precision used for floating point computations + + .. [EC:DucTraVau15] Duc, A., Florian Tramèr, & Vaudenay, S. (2015). Better algorithms for + LWE and LWR. + """ + n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability) + sigma = stddevf(alpha*q) + eps = success_probability + + RR = parent(alpha) + + # "To simplify our result, we considered operations over C to have the same + # complexity as operations over Z_q . We also took C_FFT = 1 which is the + # best one can hope to obtain for a FFT." + c_cost = 1 + c_mem = 1 + c_fft = 1 + + def _run(t): + a = RR(t*log(n, 2)) # target number of adds: a = t*log_2(n) + b = RR(n/a) # window width + epp = (1- eps)/a + + m = lambda j, eps: 8 * b * log(q/eps) * (1 - (2 * pi**2 * sigma**2)/(q**2))**(-2**(a-j)) # noqa + + c1 = (q**b-1)/2 * ((a-1)*(a-2)/2 * (n+1) - b/6 * (a*(a-1) * (a-2))) + c2 = sum([m(j, epp) * (a-1-j)/2 * (n+2) for j in range(a)]) + c3 = (2*sum([m(j, epp) for j in range(a)]) + c_fft * n * q**b * log(q, 2)) * c_cost + c4 = (a-1)*(a-2) * b * (q**b - 1)/2 + + nrops = RR(c1 + c2 + c3 + c4) + nbops = RR(log(q, 2) * nrops) + ncalls = (a-1) * (q**b - 1)/2 + m(0, eps) + nmem = ((q**b - 1)/2 * (a-1) * (n + 1 - b*(a-2)/2)) + m(0, eps) + c_mem * q**b + + current = OrderedDict([(u"t", t), + (u"bop", nbops), + (u"oracle", ncalls), + (u"m", m(0, eps)), + (u"mem", nmem), + (u"rop", nrops), + (u"a", a), + (u"b", b), + ]) + + current = cost_reorder(current, ("rop", u"oracle", u"t")) + return current + + best_runtime = None + best = None + t = RR(2*(log(q, 2) - log(sigma, 2))/log(n, 2)) + while True: + current = _run(t) + + if not best_runtime: + best_runtime = current + else: + if best_runtime["rop"] > current["rop"]: + best_runtime = current + else: + break + t += 0.05 + + return best + + +def bkw_small_secret_variances(q, a, b, kappa, o, RR=None): + """ + Helper function for small secret BKW variant. + + :param q: + :param a: + :param b: + :param kappa: + :param o: + :param RR: + :returns: + :rtype: + + """ + if RR is None: + RR = RealField() + q = RR(q) + a = RR(a).round() + b = RR(b) + n = a*b + kappa = RR(kappa) + T = RR(2)**(b*kappa) + n = RR(o)/RR(T*(a+1)) + RR(1) + + U_Var = lambda x: (x**2 - 1)/12 # noqa + red_var = 2*U_Var(q/(2**kappa)) + + if o: + c_ = map(RR, [0.0000000000000000, + 0.4057993538687922, 0.6924478992819291, 0.7898852691349439, + 0.8441959360364506, 0.8549679124679972, 0.8954469872316165, + 0.9157093365103325, 0.9567635780119543, 0.9434245442818547, + 0.9987153221343770]) + + M = Matrix(RR, a, a) # rows are tables, columns are entries those tables + for l in range(M.ncols()): + for c in range(l, M.ncols()): + M[l, c] = U_Var(q) + + for l in range(1, a): + for i in range(l): + M[l, i] = red_var + sum(M[i+1:l].column(i)) + + bl = b*l + if round(bl) < len(c_): + c_tau = c_[round(bl)] + else: + c_tau = RR(1)/RR(5)*RR(sqrt(bl)) + RR(1)/RR(3) + + f = (c_tau*n**(~bl) + 1 - c_tau)**2 + for i in range(l): + M[l, i] = M[l, i]/f + + v = vector(RR, a) + for i in range(a): + v[i] = red_var + sum(M[i+1:].column(i)) + else: + v = vector(RR, a) + for i in range(a)[::-1]: + v[i] = 2**(a-i-1) * red_var + + return v + + +def bkw_small_secret(n, alpha, q, secret_distribution=True, success_probability=0.99, t=None, o=0, samples=None): # noqa + """ + :param n: number of variables in the LWE instance + :param alpha: standard deviation of the LWE instance + :param q: size of the finite field (default: n^2) + """ + + def sigma2f(kappa): + v = bkw_small_secret_variances(q, a, b, kappa, o, RR=RR) + return sigmaf(sum([b * e * secret_variance for e in v], RR(0)).sqrt()) + + def Tf(kappa): + return min(q**b, ZZ(2)**(b*kappa))/2 + + def ops_tf(kappa): + T = Tf(kappa) + return T * (a*(a-1)/2 * (n+1) - b*a*(a-1)/4 - b/6 * ((a-1)**3 + 3/2*(a-1)**2 + 1/RR(2)*(a-1))) + + def bkwssf(kappa): + ret = OrderedDict() + ret[u"κ"] = kappa + m = amplify_sigma(success_probability, [sigma_final, sigma2f(kappa)], q) + ret["m"] = m + ropsm = (m + o) * (a/2 * (n + 2)) + ropst = ops_tf(kappa) + ret["rop"] = ropst + ropsm + T = Tf(kappa) + ret["mem"] = T * a * (n + 1 - b * (a-1)/2) + ret["oracle"] = T * a + ret["m"] + o + return ret + + n, alpha, q, success_probability = preprocess_params(n, alpha, q, success_probability, prec=4*n) + RR = alpha.parent() + sigma = alpha*q + + if o is None: + best = bkw_small_secret(n, alpha, q, secret_distribution, success_probability, t=t, o=0) + o = best["oracle"]/2 + while True: + current = bkw_small_secret(n, alpha, q, secret_distribution, success_probability, t=t, o=o) + if best is None or current["rop"] < best["rop"]: + best = current + if current["rop"] > best["rop"]: + break + + o = o/2 + return best + + if t is None: + t = RR(2*(log(q, 2) - log(sigma, 2))/log(n, 2)) + best = None + while True: + current = bkw_small_secret(n, alpha, q, secret_distribution, success_probability, t=t, o=o) + if best is None or current["rop"] < best["rop"]: + best = current + if current["rop"] > best["rop"]: + break + t += 0.01 + return best + + secret_variance = secret_distribution_variance(secret_distribution) + secret_variance = RR(secret_variance) + + a = RR(t*log(n, 2)) # the target number of additions: a = t*log_2(n) + b = n/a # window width b = n/a + sigma_final = RR(n**t).sqrt() * sigma # after n^t additions we get this stddev + transformation_noise = sqrt(n * 1/RR(12) * secret_variance) + kappa = ceil(log(round(q*transformation_noise/stddevf(sigma)), 2.0)) + 1 + + if kappa > ceil(log(q, 2)): + kappa = ceil(log(q, 2)) + + best = None + while kappa > 0: + current = bkwssf(kappa) + if best is None or current["rop"] < best["rop"]: + best = current + if current["rop"] > best["rop"]: + break + kappa -= 1 + + best["o"] = o + best["t"] = t + best["a"] = a + best["b"] = b + best = cost_reorder(best, ["rop", "oracle", "t", "m", "mem"]) + return best diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 000000000..5e30a8460 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,225 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/LWEEstimator.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/LWEEstimator.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/LWEEstimator" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/LWEEstimator" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/doc/_templates/autosummary/base.rst b/doc/_templates/autosummary/base.rst new file mode 100644 index 000000000..b04cf228d --- /dev/null +++ b/doc/_templates/autosummary/base.rst @@ -0,0 +1,6 @@ +{{ fullname }} +{{ underline }} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} \ No newline at end of file diff --git a/doc/_templates/autosummary/class.rst b/doc/_templates/autosummary/class.rst new file mode 100644 index 000000000..a36207f4f --- /dev/null +++ b/doc/_templates/autosummary/class.rst @@ -0,0 +1,35 @@ + +{{ fullname }} +{{ underline }} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + + {% block methods %} + .. automethod:: __init__ + + {% if methods %} + .. rubric:: Methods + + .. autosummary:: + :toctree: {{ fullname }} + + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: Attributes + + .. autosummary:: + :toctree: {{ fullname }} + + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} \ No newline at end of file diff --git a/doc/_templates/autosummary/module.rst b/doc/_templates/autosummary/module.rst new file mode 100644 index 000000000..1f437906a --- /dev/null +++ b/doc/_templates/autosummary/module.rst @@ -0,0 +1,32 @@ +{{ fullname }} +{{ underline }} + +.. rubric:: Description + +.. automodule:: {{ fullname }} + +.. currentmodule:: {{ fullname }} + +{% if classes %} +.. rubric:: Classes + +.. autosummary:: + :toctree: {{ fullname }} + {% for class in classes %} + {% if class in members %} + {{ class }} + {% endif %} + {% endfor %} + +{% endif %} + +{% if functions %} +.. rubric:: Functions + +.. autosummary:: + :toctree: {{ fullname }} + {% for function in functions %} + {{ function }} + {% endfor %} + +{% endif %} \ No newline at end of file diff --git a/doc/api_doc.rst b/doc/api_doc.rst new file mode 100644 index 000000000..a81875776 --- /dev/null +++ b/doc/api_doc.rst @@ -0,0 +1,9 @@ +API Reference +============= + +.. rubric:: Modules + +.. autosummary:: + :toctree: _apidoc + + estimator \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 000000000..dfa40c0b3 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,394 @@ +# -*- coding: utf-8 -*- +# +# LWE Estimator documentation build configuration file, created by +# sphinx-quickstart on Sat May 19 18:04:37 2018. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +try: + from unittest.mock import MagicMock +except ImportError: + from mock import Mock as MagicMock + +sys.path.insert(0, os.path.abspath('../')) + +class Mock(MagicMock): + @classmethod + def __getattr__(cls, name): + return MagicMock() + +MOCK_MODULES = [ + 'sage', + 'sage.all', + 'sage.arith', + 'sage.arith.srange', + 'sage.calculus', + 'sage.calculus.var', + 'sage.functions', + 'sage.functions.log', + 'sage.functions', + 'sage.functions.other', + 'sage.interfaces', + 'sage.interfaces.magma', + 'sage.misc', + 'sage.misc.all', + 'sage.numerical', + 'sage.numerical.optimize', + 'sage.rings', + 'sage.rings.all', + 'sage.rings', + 'sage.rings.infinity', + 'sage.structure', + 'sage.structure.element', + 'sage.symbolic', + 'sage.symbolic.all', + 'scipy', + 'scipy.optimize', + 'sage.crypto', + 'sage.crypto.lwe', +] +sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) + + # sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' +autoclass_content = "both" +#autodoc_default_flags = [ +# "members", +# "inherited-members", +# "private-members", +# "show-inheritance" +#] +autosummary_generate = True +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'LWE Estimator' +copyright = u'2018, Martin R Albrecht' +author = u'Martin R Albrecht' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'1.0' +# The full version, including alpha/beta/rc tags. +release = u'1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# +# today = '' +# +# Else, today_fmt is used as the format for a strftime call. +# +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +# +# html_title = u'LWE Estimator v1.0' + +# A shorter title for the navigation bar. Default is the same as html_title. +# +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +# html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +# html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# +# html_additional_pages = {} + +# If false, no module index is generated. +# +# html_domain_indices = True + +# If false, no index is generated. +# +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'LWEEstimatordoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'LWEEstimator.tex', u'LWE Estimator Documentation', + u'Martin R Albrecht', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# +# latex_use_parts = False + +# If true, show page references after internal links. +# +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# +# latex_appendices = [] + +# If false, no module index is generated. +# +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'lweestimator', u'LWE Estimator Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'LWEEstimator', u'LWE Estimator Documentation', + author, 'LWEEstimator', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# +# texinfo_appendices = [] + +# If false, no module index is generated. +# +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. diff --git a/doc/documentationreadme.rst b/doc/documentationreadme.rst new file mode 100644 index 000000000..da0afb754 --- /dev/null +++ b/doc/documentationreadme.rst @@ -0,0 +1,18 @@ +Documentation README +====================== + +Documentation for the ``estimator`` is available `online `__. +This documentation can be generated locally by running the following code in the lwe-estimator directory: + + +:: + + pipenv run make html + +If documentation was previously generated locally, to ensure a full regeneration use: + +:: + + pipenv run make clean && rm -fr doc/_apidoc + + diff --git a/doc/genindex.rst b/doc/genindex.rst new file mode 100644 index 000000000..a50680d2d --- /dev/null +++ b/doc/genindex.rst @@ -0,0 +1,4 @@ +.. This file is a placeholder and will be replaced + +Index +##### \ No newline at end of file diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 000000000..feb560454 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,26 @@ +.. LWE Estimator documentation master file, created by + sphinx-quickstart on Sat May 19 18:04:37 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to LWE Estimator's documentation! +========================================= + +.. toctree:: + :caption: Introduction + :maxdepth: 2 + + readme_link + documentationreadme + +.. toctree:: + :caption: API Reference + :glob: + + _apidoc/* + +.. toctree:: + :caption: Appendix + + genindex + diff --git a/doc/readme_link.rst b/doc/readme_link.rst new file mode 100644 index 000000000..c5e3c0fed --- /dev/null +++ b/doc/readme_link.rst @@ -0,0 +1,7 @@ +Module Overview +=============== +.. Ignore the title from the README when importing + as we have written our own one above + +.. include:: ../README.rst + :start-line: 3 \ No newline at end of file diff --git a/doctest.sh b/doctest.sh new file mode 100755 index 000000000..a173383c8 --- /dev/null +++ b/doctest.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +############################################################################### +# Run Sage doctests +############################################################################### +SAGE_ROOT=$(sage -c "import os; print(os.environ['SAGE_ROOT'])") +export SAGE_ROOT="$SAGE_ROOT" + +# shellcheck source=/dev/null +source "$SAGE_ROOT/local/bin/sage-env" +for file in "$@"; do + PYTHONIOENCODING=UTF-8 PYTHONPATH=$(pwd) sage-runtests "$file" +done diff --git a/estimator.py b/estimator.py new file mode 100644 index 000000000..0d7650a80 --- /dev/null +++ b/estimator.py @@ -0,0 +1,3467 @@ +# -*- coding: utf-8 -*- +""" +Cost estimates for solving LWE. + +.. moduleauthor:: Martin R. Albrecht + +# Supported Secret Distributions # + +The following distributions for the secret are supported: + +- ``"normal"`` : normal form instances, i.e. the secret follows the noise distribution (alias: ``True``) +- ``alpha``: where α a positive real number, (discrete) Gaussian distribution with parameter α +- ``"uniform"`` : uniform mod q (alias: ``False``) +- ``(a,b)`` : uniform in the interval ``[a,…,b]`` +- ``((a,b), h)`` : exactly ``h`` components are ``∈ [a,…,b]∖\\{0\\}``, all other components are zero + +""" + + +# Imports +from collections import OrderedDict +from functools import partial +from sage.arith.srange import srange +from sage.calculus.var import var +from sage.functions.log import exp, log +from sage.functions.other import ceil, sqrt, floor, binomial +from sage.all import erf +from sage.interfaces.magma import magma +from sage.misc.all import cached_function, round, prod +from sage.numerical.optimize import find_root +from sage.rings.all import QQ, RR, ZZ, RealField, PowerSeriesRing, RDF +from sage.rings.infinity import PlusInfinity +from sage.structure.element import parent +from sage.symbolic.all import pi, e +from scipy.optimize import newton +import logging +import sage.crypto.lwe + +oo = PlusInfinity() + + +class Logging: + """ + Control level of detail being printed. + """ + + plain_logger = logging.StreamHandler() + plain_logger.setFormatter(logging.Formatter("%(message)s")) + + detail_logger = logging.StreamHandler() + detail_logger.setFormatter(logging.Formatter("%(levelname)s:%(name)s: %(message)s")) + + logging.getLogger("estimator").handlers = [plain_logger] + logging.getLogger("estimator").setLevel(logging.INFO) + + loggers = ("binsearch", "repeat", "guess") + + for logger in loggers: + logging.getLogger(logger).handlers = [detail_logger] + logging.getLogger(logger).setLevel(logging.INFO) + + CRITICAL = logging.CRITICAL + ERROR = logging.ERROR + WARNING = logging.WARNING + INFO = logging.INFO + DEBUG = logging.DEBUG + NOTSET = logging.NOTSET + + @staticmethod + def set_level(lvl, loggers=None): + """Set logging level + + :param lvl: one of `CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`, `NOTSET` + :param loggers: one of `Logging.loggers`, if `None` all loggers are used. + + """ + if loggers is None: + loggers = Logging.loggers + + for logger in loggers: + logging.getLogger(logger).setLevel(lvl) + + +# Utility Classes # + + +class OutOfBoundsError(ValueError): + """ + Used to indicate a wrong value, for example delta_0 < 1. + """ + + pass + + +class InsufficientSamplesError(ValueError): + """ + Used to indicate the number of samples given is too small. + """ + + pass + + +# Binary Search # + + +def binary_search_minimum(f, start, stop, param, extract=lambda x: x, *arg, **kwds): + """ + Return minimum of `f` if `f` is convex. + + :param start: start of range to search + :param stop: stop of range to search (exclusive) + :param param: the parameter to modify when calling `f` + :param extract: comparison is performed on ``extract(f(param=?, *args, **kwds))`` + + """ + return binary_search(f, start, stop, param, predictate=lambda x, best: extract(x) <= extract(best), *arg, **kwds) + + +def binary_search(f, start, stop, param, predicate=lambda x, best: x <= best, *arg, **kwds): + """ + Searches for the best value in the interval [start,stop] depending on the given predicate. + + :param start: start of range to search + :param stop: stop of range to search (exclusive) + :param param: the parameter to modify when calling `f` + :param predicate: comparison is performed by evaluating ``predicate(current, best)`` + """ + kwds[param] = stop + D = {} + D[stop] = f(*arg, **kwds) + best = D[stop] + b = ceil((start + stop) / 2) + direction = 0 + while True: + if b not in D: + kwds[param] = b + D[b] = f(*arg, **kwds) + if b == start: + best = D[start] + break + if not predicate(D[b], best): + if direction == 0: + start = b + b = ceil((stop + b) / 2) + else: + stop = b + b = floor((start + b) / 2) + else: + best = D[b] + logging.getLogger("binsearch").debug(u"%4d, %s" % (b, best)) + if b - 1 not in D: + kwds[param] = b - 1 + D[b - 1] = f(*arg, **kwds) + if predicate(D[b - 1], best): + stop = b + b = floor((b + start) / 2) + direction = 0 + else: + if b + 1 not in D: + kwds[param] = b + 1 + D[b + 1] = f(*arg, **kwds) + if not predicate(D[b + 1], best): + break + else: + start = b + b = ceil((stop + b) / 2) + direction = 1 + return best + + +class Param: + """ + Namespace for processing LWE parameter sets. + """ + + @staticmethod + def Regev(n, m=None, dict=False): + """ + :param n: LWE dimension `n > 0` + :param m: number of LWE samples `m > 0` + :param dict: return a dictionary + + """ + if dict: + return Param.dict(sage.crypto.lwe.Regev(n=n, m=m)) + else: + return Param.tuple(sage.crypto.lwe.Regev(n=n, m=m)) + + @staticmethod + def LindnerPeikert(n, m=None, dict=False): + """ + :param n: LWE dimension `n > 0` + :param m: number of LWE samples `m > 0` + :param dict: return a dictionary + + """ + if dict: + return Param.dict(sage.crypto.lwe.LindnerPeikert(n=n, m=m)) + else: + return Param.tuple(sage.crypto.lwe.LindnerPeikert(n=n, m=m)) + + @staticmethod + def tuple(lwe): + """ + Return (n, α, q) given an LWE instance object. + + :param lwe: LWE object + :returns: (n, α, q) + + """ + n = lwe.n + q = lwe.K.order() + try: + alpha = alphaf(sigmaf(lwe.D.sigma), q) + except AttributeError: + # older versions of Sage use stddev, not sigma + alpha = alphaf(sigmaf(lwe.D.stddev), q) + return n, alpha, q + + @staticmethod + def dict(lwe): + """ + Return dictionary consisting of n, α, q and samples given an LWE instance object. + + :param lwe: LWE object + :returns: "n": n, "alpha": α, "q": q, "samples": samples + :rtype: dictionary + + """ + n, alpha, q = Param.tuple(lwe) + m = lwe.m if lwe.m else oo + return {"n": n, "alpha": alpha, "q": q, "m": m} + + @staticmethod + def preprocess(n, alpha, q, success_probability=None, prec=None, m=oo): + """ + Check if parameters n, α, q are sound and return correct types. + Also, if given, the soundness of the success probability and the + number of samples is ensured. + """ + if n < 1: + raise ValueError("LWE dimension must be greater than 0.") + if alpha <= 0: + raise ValueError("Fraction of noise must be > 0.") + if q < 1: + raise ValueError("LWE modulus must be greater than 0.") + if m is not None and m < 1: + raise InsufficientSamplesError(u"m=%d < 1" % m) + + if prec is None: + try: + prec = alpha.prec() + except AttributeError: + pass + try: + prec = max(success_probability.prec(), prec if prec else 0) + except AttributeError: + pass + + if prec is None or prec < 128: + prec = 128 + RR = RealField(prec) + n, alpha, q = ZZ(n), RR(alpha), ZZ(q) + + if m is not oo: + m = ZZ(m) + + if success_probability is not None: + if success_probability >= 1 or success_probability <= 0: + raise ValueError("success_probability must be between 0 and 1.") + return n, alpha, q, RR(success_probability) + else: + return n, alpha, q + + +# Error Parameter Conversions + + +def stddevf(sigma): + """ + Gaussian width parameter σ → standard deviation + + :param sigma: Gaussian width parameter σ + + EXAMPLE:: + + sage: from estimator import stddevf + sage: stddevf(64.0) + 25.532... + + sage: stddevf(64) + 25.532... + + sage: stddevf(RealField(256)(64)).prec() + 256 + + """ + + try: + prec = parent(sigma).prec() + except AttributeError: + prec = 0 + if prec > 0: + FF = parent(sigma) + else: + FF = RR + return FF(sigma) / FF(sqrt(2 * pi)) + + +def sigmaf(stddev): + """ + standard deviation → Gaussian width parameter σ + + :param sigma: standard deviation + + EXAMPLE:: + + sage: from estimator import stddevf, sigmaf + sage: n = 64.0 + sage: sigmaf(stddevf(n)) + 64.000... + + sage: sigmaf(RealField(128)(1.0)) + 2.5066282746310005024157652848110452530 + sage: sigmaf(1.0) + 2.50662827463100 + sage: sigmaf(1) + 2.50662827463100 + sage: sigmaf(1r) + 2.50662827463100 + + """ + RR = parent(stddev) + # check that we got ourselves a real number type + try: + if (abs(RR(0.5) - 0.5) > 0.001): + RR = RealField(53) # hardcode something + except TypeError: + RR = RealField(53) # hardcode something + return RR(sqrt(2 * pi)) * stddev + + +def alphaf(sigma, q, sigma_is_stddev=False): + """ + Gaussian width σ, modulus q → noise rate α + + :param sigma: Gaussian width parameter (or standard deviation if ``sigma_is_stddev`` is set) + :param q: modulus `0 < q` + :param sigma_is_stddev: if set then `sigma` is interpreted as the standard deviation + + :returns: α = σ/q or σ·sqrt(2π)/q depending on `sigma_is_stddev` + + """ + if sigma_is_stddev is False: + return RR(sigma / q) + else: + return RR(sigmaf(sigma) / q) + + +class Cost: + """ + Algorithms costs. + """ + + def __init__(self, data=None, **kwds): + """ + + :param data: we call ``OrderedDict(data)`` + + """ + if data is None: + self.data = OrderedDict() + else: + self.data = OrderedDict(data) + + for k, v in kwds.items(): + self.data[k] = v + + def str(self, keyword_width=None, newline=None, round_bound=2048, compact=False, unicode=True): + """ + + :param keyword_width: keys are printed with this width + :param newline: insert a newline + :param round_bound: values beyond this bound are represented as powers of two + :param compact: do not add extra whitespace to align entries + :param unicode: use unicode to shorten representation + + EXAMPLE:: + + sage: from estimator import Cost + sage: s = Cost({"delta_0":5, "bar":2}) + sage: print(s) + delta_0: 5, bar: 2 + + sage: s = Cost([(u"delta_0", 5), ("bar",2)]) + sage: print(s) + delta_0: 5, bar: 2 + + """ + if unicode: + unicode_replacements = {"delta_0": u"δ_0", "beta": u"β", "epsilon": u"ε"} + else: + unicode_replacements = {} + + format_strings = { + u"beta": u"%s: %4d", + u"d": u"%s: %4d", + "b": "%s: %3d", + "t1": "%s: %3d", + "t2": "%s: %3d", + "l": "%s: %3d", + "ncod": "%s: %3d", + "ntop": "%s: %3d", + "ntest": "%s: %3d", + } + + d = self.data + s = [] + for k in d: + v = d[k] + kk = unicode_replacements.get(k, k) + if keyword_width: + fmt = u"%%%ds" % keyword_width + kk = fmt % kk + if not newline and k in format_strings: + s.append(format_strings[k] % (kk, v)) + elif ZZ(1) / round_bound < v < round_bound or v == 0 or ZZ(-1) / round_bound > v > -round_bound: + try: + if compact: + s.append(u"%s: %d" % (kk, ZZ(v))) + else: + s.append(u"%s: %8d" % (kk, ZZ(v))) + except TypeError: + if v < 2.0 and v >= 0.0: + if compact: + s.append(u"%s: %.6f" % (kk, v)) + else: + s.append(u"%s: %8.6f" % (kk, v)) + else: + if compact: + s.append(u"%s: %.3f" % (kk, v)) + else: + s.append(u"%s: %8.3f" % (kk, v)) + else: + t = u"%s" % (u"≈" if unicode else "") + u"%s2^%.1f" % ("-" if v < 0 else "", log(abs(v), 2).n()) + if compact: + s.append(u"%s: %s" % (kk, t)) + else: + s.append(u"%s: %8s" % (kk, t)) + if not newline: + if compact: + return u", ".join(s) + else: + return u", ".join(s) + else: + return u"\n".join(s) + + def reorder(self, first): + """ + Return a new ordered dict from the key:value pairs in dictinonary but reordered such that the + ``first`` keys come first. + + :param dictionary: input dictionary + :param first: keys which should come first (in order) + + EXAMPLE:: + + sage: from estimator import Cost + sage: d = Cost([("a",1),("b",2),("c",3)]); d + a: 1 + b: 2 + c: 3 + + sage: d.reorder( ["b","c","a"]) + b: 2 + c: 3 + a: 1 + """ + keys = list(self.data) + for key in first: + keys.pop(keys.index(key)) + keys = list(first) + keys + r = OrderedDict() + for key in keys: + r[key] = self.data[key] + return Cost(r) + + def filter(self, keys): + """ + Return new ordered dictinonary from dictionary restricted to the keys. + + :param dictionary: input dictionary + :param keys: keys which should be copied (ordered) + """ + r = OrderedDict() + for key in keys: + r[key] = self.data[key] + return Cost(r) + + def repeat(self, times, select=None, lll=None): + u""" + Return a report with all costs multiplied by `times`. + + :param d: a cost estimate + :param times: the number of times it should be run + :param select: toggle which fields ought to be repeated and which shouldn't + :param lll: if set amplify lattice reduction times assuming the LLL algorithm suffices and costs ``lll`` + :returns: a new cost estimate + + We maintain a local dictionary which decides if an entry is multiplied by `times` or not. + For example, δ would not be multiplied but "#bop" would be. This check is strict such that + unknown entries raise an error. This is to enforce a decision on whether an entry should be + multiplied by `times` if the function `report` reports on is called `times` often. + + EXAMPLE:: + + sage: from estimator import Param, dual + sage: n, alpha, q = Param.Regev(128) + + sage: dual(n, alpha, q).repeat(2^10) + rop: 2^91.1 + m: 2^18.6 + red: 2^91.1 + delta_0: 1.008631 + beta: 115 + d: 380 + |v|: 688.951 + repeat: 2^27.0 + epsilon: 0.007812 + + sage: dual(n, alpha, q).repeat(1) + rop: 2^81.1 + m: 380 + red: 2^81.1 + delta_0: 1.008631 + beta: 115 + d: 380 + |v|: 688.951 + repeat: 2^17.0 + epsilon: 0.007812 + + """ + # TODO review this list + do_repeat = { + u"rop": True, + u"red": True, + u"babai": True, + u"babai_op": True, + u"epsilon": False, + u"mem": False, + u"delta_0": False, + u"beta": False, + u"k": False, + u"D_reg": False, + u"t": False, + u"m": True, + u"d": False, + u"|v|": False, + u"amplify": False, + u"repeat": False, # we deal with it below + u"c": False, + u"b": False, + u"t1": False, + u"t2": False, + u"l": False, + u"ncod": False, + u"ntop": False, + u"ntest": False, + } + + if lll and self["red"] != self["rop"]: + raise ValueError("Amplification via LLL was requested but 'red' != 'rop'") + + if select is not None: + for key in select: + do_repeat[key] = select[key] + + ret = OrderedDict() + for key in self.data: + try: + if do_repeat[key]: + if lll and key in ("red", "rop"): + ret[key] = self[key] + times * lll + else: + ret[key] = times * self[key] + else: + ret[key] = self.data[key] + except KeyError: + raise NotImplementedError(u"You found a bug, this function does not know about '%s' but should." % key) + ret[u"repeat"] = times * ret.get("repeat", 1) + return Cost(ret) + + def __rmul__(self, times): + return self.repeat(times) + + def combine(self, right, base=None): + """Combine ``left`` and ``right``. + + :param left: cost dictionary + :param right: cost dictionary + :param base: add entries to ``base`` + + """ + if base is None: + cost = Cost() + else: + cost = base + for key in self.data: + cost[key] = self.data[key] + for key in right: + cost[key] = right.data[key] + return Cost(cost) + + def __add__(self, other): + return self.combine(self, other) + + def __getitem__(self, key): + return self.data[key] + + def __setitem__(self, key, value): + self.data[key] = value + + def __iter__(self): + return iter(self.data) + + def values(self): + return self.data.values() + + def __str__(self): + return self.str(unicode=False, compact=True) + + def __repr__(self): + return self.str(unicode=False, newline=True, keyword_width=12) + + def __unicode__(self): + return self.str(unicode=True) + + +class SDis: + """ + Distributions of Secrets. + """ + + @staticmethod + def is_sparse(secret_distribution): + """Return true if the secret distribution is sparse + + :param secret_distribution: distribution of secret, see module level documentation for details + + EXAMPLES:: + + sage: from estimator import SDis + sage: SDis.is_sparse(True) + False + + sage: SDis.is_sparse(0.1) + False + + sage: SDis.is_sparse(0.2) + False + + sage: SDis.is_sparse(((-1, 1), 64)) + True + + sage: SDis.is_sparse(((-3, 3), 64)) + True + + sage: SDis.is_sparse((-3, 3)) + False + + """ + try: + (a, b), h = secret_distribution + # TODO we could check h against n but then this function would have to depend on n + return True + except (TypeError, ValueError): + return False + + @staticmethod + def is_small(secret_distribution, alpha=None): + """Return true if the secret distribution is small + + :param secret_distribution: distribution of secret, see module level documentation for details + :param alpha: optional, used for comparing with `secret_distribution` + + EXAMPLES:: + + sage: from estimator import SDis + sage: SDis.is_small(False) + False + + sage: SDis.is_small(True) + True + + sage: SDis.is_small("normal") + True + + sage: SDis.is_small(1) + True + + sage: SDis.is_small(1.2) + True + + sage: SDis.is_small(((-1, 1), 64)) + True + + sage: SDis.is_small(((-3, 3), 64)) + True + + sage: SDis.is_small((-3, 3)) + True + + sage: SDis.is_small((0, 1)) + True + + """ + + if secret_distribution == "normal" or secret_distribution is True: + return True + try: + if float(secret_distribution) > 0: + if alpha is not None: + if secret_distribution <= alpha: + return True + else: + raise NotImplementedError("secret size %f > error size %f"%(secret_distribution, alpha)) + return True + except TypeError: + pass + try: + (a, b), h = secret_distribution + return True + except (TypeError, ValueError): + try: + (a, b) = secret_distribution + return True + except (TypeError, ValueError): + return False + + @staticmethod + def bounds(secret_distribution): + """Return bounds of secret distribution + + :param secret_distribution: distribution of secret, see module level documentation for details + + EXAMPLES:: + + sage: from estimator import SDis + sage: SDis.bounds(False) + Traceback (most recent call last): + ... + ValueError: Cannot extract bounds for secret. + + sage: SDis.bounds(True) + Traceback (most recent call last): + ... + ValueError: Cannot extract bounds for secret. + + sage: SDis.bounds(0.1) + Traceback (most recent call last): + ... + ValueError: Cannot extract bounds for secret. + + sage: SDis.bounds(0.12) + Traceback (most recent call last): + ... + ValueError: Cannot extract bounds for secret. + + sage: SDis.bounds(((-1, 1), 64)) + (-1, 1) + + sage: SDis.bounds(((-3, 3), 64)) + (-3, 3) + + sage: SDis.bounds((-3, 3)) + (-3, 3) + + """ + try: + (a, b) = secret_distribution + try: + (a, b), _ = (a, b) # noqa + except (TypeError, ValueError): + pass + return a, b + except (TypeError, ValueError): + raise ValueError("Cannot extract bounds for secret.") + + @staticmethod + def is_bounded_uniform(secret_distribution): + """Return true if the secret is bounded uniform (sparse or not). + + :param secret_distribution: distribution of secret, see module level documentation for + details + + EXAMPLES:: + + sage: from estimator import SDis + sage: SDis.is_bounded_uniform(False) + False + + sage: SDis.is_bounded_uniform(True) + False + + sage: SDis.is_bounded_uniform(0.1) + False + + sage: SDis.is_bounded_uniform(0.12) + False + + sage: SDis.is_bounded_uniform(((-1, 1), 64)) + True + + sage: SDis.is_bounded_uniform(((-3, 3), 64)) + True + + sage: SDis.is_bounded_uniform((-3, 3)) + True + + .. note :: This function requires the bounds to be of opposite sign, as scaling code does + not handle the other case. + + """ + + try: + # next will fail if not bounded_uniform + a, b = SDis.bounds(secret_distribution) + + # check bounds are around 0, otherwise not implemented + if a <= 0 and 0 <= b: + return True + except (TypeError, ValueError): + pass + + return False + + @staticmethod + def is_ternary(secret_distribution): + """Return true if the secret is ternary (sparse or not) + + :param secret_distribution: distribution of secret, see module level documentation for details + + EXAMPLES:: + + sage: from estimator import SDis + sage: SDis.is_ternary(False) + False + + sage: SDis.is_ternary(True) + False + + sage: SDis.is_ternary(0.1) + False + + sage: SDis.is_ternary(0.12) + False + + sage: SDis.is_ternary(((-1, 1), 64)) + True + + sage: SDis.is_ternary((-1, 1)) + True + + sage: SDis.is_ternary(((-3, 3), 64)) + False + + sage: SDis.is_ternary((-3, 3)) + False + + """ + + if SDis.is_bounded_uniform(secret_distribution): + a, b = SDis.bounds(secret_distribution) + if a == -1 and b == 1: + return True + + return False + + @staticmethod + def is_binary(secret_distribution): + """Return true if the secret is binary (sparse or not) + + :param secret_distribution: distribution of secret, see module level documentation for details + + EXAMPLES:: + + sage: from estimator import SDis + sage: SDis.is_binary(False) + False + + sage: SDis.is_binary(True) + False + + sage: SDis.is_binary(0.1) + False + + sage: SDis.is_binary(0.12) + False + + sage: SDis.is_binary(((-1, 1), 64)) + False + + sage: SDis.is_binary((-1, 1)) + False + + sage: SDis.is_binary(((0, 1), 64)) + True + + sage: SDis.is_binary((0, 1)) + True + + """ + + if SDis.is_bounded_uniform(secret_distribution): + a, b = SDis.bounds(secret_distribution) + if a == 0 and b == 1: + return True + + return False + + @staticmethod + def nonzero(secret_distribution, n): + """Return number of non-zero elements or ``None`` + + :param secret_distribution: distribution of secret, see module level documentation for details + :param n: LWE dimension `n > 0` + + """ + try: + (a, b) = secret_distribution + try: + (a, b), h = (a, b) # noqa + return h + except (TypeError, ValueError): + if n is None: + raise ValueError("Parameter n is required for sparse secrets.") + B = ZZ(b - a + 1) + h = round((B - 1) / B * n) + return h + except (TypeError, ValueError): + raise ValueError("Cannot extract `h`.") + + @staticmethod + def mean(secret_distribution, q=None, n=None): + """ + Mean of the secret per component. + + :param secret_distribution: distribution of secret, see module level documentation for details + :param n: only used for sparse secrets + + EXAMPLE:: + + sage: from estimator import SDis + sage: SDis.mean(True) + 0 + + sage: SDis.mean(False, q=10) + 0 + + sage: SDis.mean(0.1) + 0 + + sage: SDis.mean(0.12) + 0 + + sage: SDis.mean(((-3,3))) + 0 + + sage: SDis.mean(((-3,3),64), n=256) + 0 + + sage: SDis.mean(((-3,2))) + -1/2 + + sage: SDis.mean(((-3,2),64), n=256) + -3/20 + + """ + if not SDis.is_small(secret_distribution): + # uniform distribution variance + if q is None: + raise ValueError("Parameter q is required for uniform secret.") + a = -floor(q / 2) + b = floor(q / 2) + return (a + b) / ZZ(2) + else: + try: + a, b = SDis.bounds(secret_distribution) + + try: + (a, b), h = secret_distribution + if n is None: + raise ValueError("Parameter n is required for sparse secrets.") + return h / ZZ(n) * (b * (b + 1) - a * (a - 1)) / (2 * (b - a)) + except (TypeError, ValueError): + return (a + b) / ZZ(2) + except ValueError: + # small with no bounds, it's normal + return ZZ(0) + + @staticmethod + def variance(secret_distribution, alpha=None, q=None, n=None): + """ + Variance of the secret per component. + + :param secret_distribution: distribution of secret, see module level documentation for details + :param alpha: only used for normal form LWE + :param q: only used for normal form LWE + :param n: only used for sparse secrets + + EXAMPLE:: + + sage: from estimator import SDis, alphaf + sage: SDis.variance(True, 8./2^15, 2^15).sqrt().n() + 3.19... + + sage: from estimator import SDis + sage: SDis.variance("normal", 8./2^15, 2^15).sqrt().n() + 3.19... + + sage: SDis.variance(alphaf(1.0, 100, True), q=100) + 1.00000000000000 + + sage: SDis.variance(alphaf(1.2, 100, True), q=100) + 1.44000000000000 + + sage: SDis.variance((-3,3), 8./2^15, 2^15) + 4 + + sage: SDis.variance(((-3,3),64), 8./2^15, 2^15, n=256) + 7/6 + + sage: SDis.variance((-3,2)) + 35/12 + + sage: SDis.variance(((-3,2),64), n=256) + 371/400 + + sage: SDis.variance((-1,1), 8./2^15, 2^15) + 2/3 + + sage: SDis.variance(((-1,1),64), 8./2^15, 2^15, n=256) + 1/4 + + .. note :: This function assumes that the bounds are of opposite sign, and that the + distribution is centred around zero. + """ + if not SDis.is_small(secret_distribution): + # uniform distribution variance + a = -floor(q / 2) + b = floor(q / 2) + n = b - a + 1 + return (n ** 2 - 1) / ZZ(12) + else: + try: + a, b = SDis.bounds(secret_distribution) + except ValueError: + # small with no bounds, it's normal + if isinstance(secret_distribution, bool) or secret_distribution == "normal": + return stddevf(alpha * q) ** 2 + else: + return RR(stddevf(secret_distribution * q))**2 + try: + (a, b), h = secret_distribution + except (TypeError, ValueError): + return ((b - a + 1) ** 2 - 1) / ZZ(12) + if n is None: + raise ValueError("Parameter n is required for sparse secrets.") + if not (a <= 0 and 0 <= b): + raise ValueError("a <= 0 and 0 <= b is required for uniform bounded secrets.") + # E(x^2), using https://en.wikipedia.org/wiki/Square_pyramidal_number + tt = (h / ZZ(n)) * (2 * b ** 3 + 3 * b ** 2 + b - 2 * a ** 3 + 3 * a ** 2 - a) / (ZZ(6) * (b - a)) + # Var(x) = E(x^2) - E(x)^2 + return tt - SDis.mean(secret_distribution, n=n) ** 2 + + +def switch_modulus(f, n, alpha, q, secret_distribution, *args, **kwds): + """ + + :param f: run f + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param secret_distribution: distribution of secret, see module level documentation for details + + + """ + length = SDis.nonzero(secret_distribution, n) + s_var = SDis.variance(secret_distribution, alpha, q, n=n) + + p = RR(ceil(sqrt(2 * pi * s_var * length / ZZ(12)) / alpha)) + if p < 32: # some random point + # we can't pretend everything is uniform any more, p is too small + p = RR(ceil(sqrt(2 * pi * s_var * length * 2 / ZZ(12)) / alpha)) + beta = RR(sqrt(2) * alpha) + return f(n, beta, p, secret_distribution, *args, **kwds) + + +# Repetition + + +def amplify(target_success_probability, success_probability, majority=False): + """ + Return the number of trials needed to amplify current `success_probability` to + `target_success_probability` + + :param target_success_probability: targeted success probability < 1 + :param success_probability: targeted success probability < 1 + :param majority: if `True` amplify a deicsional problem, not a computational one + if `False` then we assume that we can check solutions, so one success suffices + + :returns: number of required trials to amplify + """ + if target_success_probability < success_probability: + return ZZ(1) + if success_probability == 0.0: + return oo + + prec = max( + 53, + 2 * ceil(abs(log(success_probability, 2))), + 2 * ceil(abs(log(1 - success_probability, 2))), + 2 * ceil(abs(log(target_success_probability, 2))), + 2 * ceil(abs(log(1 - target_success_probability, 2))), + ) + prec = min(prec, 2048) + RR = RealField(prec) + + success_probability = RR(success_probability) + target_success_probability = RR(target_success_probability) + + try: + if majority: + eps = success_probability / 2 + return ceil(2 * log(2 - 2 * target_success_probability) / log(1 - 4 * eps ** 2)) + else: + # target_success_probability = 1 - (1-success_probability)^trials + return ceil(log(1 - target_success_probability) / log(1 - success_probability)) + except ValueError: + return oo + + +def amplify_sigma(target_advantage, sigma, q): + """ + Amplify distinguishing advantage for a given σ and q + + :param target_advantage: + :param sigma: (lists of) Gaussian width parameters + :param q: modulus `0 < q` + + """ + try: + sigma = sum(sigma_ ** 2 for sigma_ in sigma).sqrt() + except TypeError: + pass + RR = parent(sigma) + advantage = RR(exp(-RR(pi) * (RR(sigma / q) ** 2))) + # return target_advantage/advantage**2 # old + return amplify(target_advantage, advantage, majority=True) + + +def rinse_and_repeat( + f, + n, + alpha, + q, + success_probability=0.99, + m=oo, + optimisation_target=u"red", + decision=True, + repeat_select=None, + *args, + **kwds +): + """Find best trade-off between success probability and running time. + + :param f: a function returning a cost estimate + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param success_probability: targeted success probability < 1 + :param optimisation_target: which value to minimise + :param decision: set if `f` solves a decision problem, unset for search problems + :param repeat_select: passed through to ``cost_repeat`` as parameter ``select`` + :param samples: the number of available samples + + """ + n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability) + RR = parent(alpha) + + best = None + step_size = 32 + i = floor(-log(success_probability, 2)) + has_solution = False + while True: + prob = RR(min(2 ** -i, success_probability)) + try: + current = f(n, alpha, q, success_probability=prob, m=m, *args, **kwds) + repeat = amplify(success_probability, prob, majority=decision) + # TODO does the following hold for all algorithms? + current = current.repeat(repeat, select=repeat_select) + has_solution = True + except (OutOfBoundsError, InsufficientSamplesError): + key = list(best)[0] if best is not None else optimisation_target + current = Cost() + current[key] = PlusInfinity() + current["epsilon"] = ZZ(2) ** -i + + logging.getLogger("repeat").debug(current) + + key = list(current)[0] + if best is None: + if current[key] is not PlusInfinity(): + best = current + i += step_size + + if i > 8192: # somewhat arbitrary constant + raise RuntimeError("Looks like we are stuck in an infinite loop.") + + continue + + if key not in best or current[key] < best[key]: + best = current + i += step_size + else: + # we go back + i = -log(best["epsilon"], 2) - step_size + i += step_size // 2 + if i <= 0: + i = step_size // 2 + # and half the step size + step_size = step_size // 2 + + if step_size == 0: + break + + if not has_solution: + raise RuntimeError("No solution found for chosen parameters.") + + return best + + +class BKZ: + """ + Cost estimates for BKZ. + """ + + @staticmethod + def _delta_0f(k): + """ + Compute `δ_0` from block size `k` without enforcing `k` in ZZ. + + δ_0 for k≤40 were computed as follows: + + ``` + # -*- coding: utf-8 -*- + from fpylll import BKZ, IntegerMatrix + + from multiprocessing import Pool + from sage.all import mean, sqrt, exp, log, cputime + + d, trials = 320, 32 + + def f((A, beta)): + + par = BKZ.Param(block_size=beta, strategies=BKZ.DEFAULT_STRATEGY, flags=BKZ.AUTO_ABORT) + q = A[-1, -1] + d = A.nrows + t = cputime() + A = BKZ.reduction(A, par, float_type="dd") + t = cputime(t) + return t, exp(log(A[0].norm()/sqrt(q).n())/d) + + if __name__ == '__main__': + for beta in (5, 10, 15, 20, 25, 28, 30, 35, 40): + delta_0 = [] + t = [] + i = 0 + !! while i < trials: + threads = int(open("delta_0.nthreads").read()) # make sure this file exists + pool = Pool(threads) + A = [(IntegerMatrix.random(d, "qary", k=d//2, bits=50), beta) for j in range(threads)] + for (t_, delta_0_) in pool.imap_unordered(f, A): + t.append(t_) + delta_0.append(delta_0_) + i += threads + print u"β: %2d, δ_0: %.5f, time: %5.1fs, (%2d,%2d)"%(beta, mean(delta_0), mean(t), i, threads) + print + ``` + + """ + small = ( + (2, 1.02190), # noqa + (5, 1.01862), # noqa + (10, 1.01616), + (15, 1.01485), + (20, 1.01420), + (25, 1.01342), + (28, 1.01331), + (40, 1.01295), + ) + + if k <= 2: + return RR(1.0219) + elif k < 40: + for i in range(1, len(small)): + if small[i][0] > k: + return RR(small[i - 1][1]) + elif k == 40: + return RR(small[-1][1]) + else: + return RR(k / (2 * pi * e) * (pi * k) ** (1 / k)) ** (1 / (2 * (k - 1))) + + @staticmethod + def _beta_secant(delta): + """ + Estimate required blocksize `k` for a given root-hermite factor δ based on [PhD:Chen13]_ + + :param delta: root-hermite factor + + EXAMPLE:: + + sage: from estimator import BKZ + sage: 50 == BKZ._beta_secant(1.0121) + True + sage: 100 == BKZ._beta_secant(1.0093) + True + sage: BKZ._beta_secant(1.0024) # Chen reports 800 + 808 + + .. [PhD:Chen13] Yuanmi Chen. Réduction de réseau et sécurité concrète du chiffrement + complètement homomorphe. PhD thesis, Paris 7, 2013. + """ + # newton() will produce a "warning", if two subsequent function values are + # indistinguishable (i.e. equal in terms of machine precision). In this case + # newton() will return the value k in the middle between the two values + # k1,k2 for which the function values were indistinguishable. + # Since f approaches zero for k->+Infinity, this may be the case for very + # large inputs, like k=1e16. + # For now, these warnings just get printed and the value k is used anyways. + # This seems reasonable, since for such large inputs the exact value of k + # doesn't make such a big difference. + try: + k = newton(lambda k: RR(BKZ._delta_0f(k) - delta), 100, fprime=None, args=(), tol=1.48e-08, maxiter=500) + k = ceil(k) + if k < 40: + # newton may output k < 40. The old beta method wouldn't do this. For + # consistency, call the old beta method, i.e. consider this try as "failed". + raise RuntimeError("k < 40") + return k + except (RuntimeError, TypeError): + # if something fails, use old beta method + k = BKZ._beta_simple(delta) + return k + + @staticmethod + def _beta_find_root(delta): + """ + + TESTS:: + + sage: from estimator import betaf, delta_0f + sage: betaf(delta_0f(500)) + 500 + + """ + # handle k < 40 separately + k = ZZ(40) + if delta_0f(k) < delta: + return k + + try: + k = find_root(lambda k: RR(BKZ._delta_0f(k) - delta), 40, 2 ** 16, maxiter=500) + k = ceil(k - 1e-8) + except RuntimeError: + # finding root failed; reasons: + # 1. maxiter not sufficient + # 2. no root in given interval + k = BKZ._beta_simple(delta) + return k + + @staticmethod + def _beta_simple(delta): + """ + Estimate required blocksize `k` for a given root-hermite factor δ based on [PhD:Chen13]_ + + :param delta: root-hermite factor + + EXAMPLE:: + + sage: from estimator import betaf + sage: 50 == betaf(1.0121) + True + sage: 100 == betaf(1.0093) + True + sage: betaf(1.0024) # Chen reports 800 + 808 + + .. [PhD:Chen13] Yuanmi Chen. Réduction de réseau et sécurité concrète du chiffrement + complètement homomorphe. PhD thesis, Paris 7, 2013. + """ + k = ZZ(40) + + while delta_0f(2 * k) > delta: + k *= 2 + while delta_0f(k + 10) > delta: + k += 10 + while True: + if delta_0f(k) < delta: + break + k += 1 + + return k + + @staticmethod + def svp_repeat(beta, d): + """Return number of SVP calls in BKZ-β + + :param beta: block size + :param d: dimension + + .. note :: loosely based on experiments in [PhD:Chen13] + + """ + return 8 * d + + @staticmethod + @cached_function + def GSA(n, q, delta, m): + """ + Compute the Gram-Schmidt lengths based on the GSA. + + :param n: LWE dimension `n > 0` + :param q: modulus `0 < q` + :param delta: root-Hermite factor + :param m: lattice dimension + + .. [RSA:LinPei11] Richard Lindner and Chris Peikert. Better key sizes (and attacks) for LWE-based encryption. + In Aggelos Kiayias, editor, CT-RSA 2011, volume 6558 of LNCS, pages 319–339. Springer, + February 2011. + """ + log_delta = RDF(log(delta)) + log_q = RDF(log(q)) + qnm = log_q * (n / m) + qnm_p_log_delta_m = qnm + log_delta * m + tmm1 = RDF(2 * m / (m - 1)) + b = [(qnm_p_log_delta_m - log_delta * (tmm1 * i)) for i in range(m)] + b = [log_q - b[-1 - i] for i in range(m)] + b = list(map(lambda x: x.exp(), b)) + return b + + # BKZ Estimates + + @staticmethod + def LLL(d, B=None): + """ + Runtime estimation for LLL algorithm + + :param d: lattice dimension + :param B: bit-size of entries + + .. [CheNgu11] Chen, Y., & Nguyen, P. Q. (2011). BKZ 2.0: better lattice security + estimates. In D. H. Lee, & X. Wang, ASIACRYPT~2011 (pp. 1–20). : Springer, + Heidelberg. + """ + if B: + return d ** 3 * B ** 2 + else: + return d ** 3 # ignoring B for backward compatibility + + @staticmethod + def LinPei11(beta, d, B=None): + """ + Runtime estimation assuming the Lindner-Peikert model in elementary operations. + + .. [LinPei11] Lindner, R., & Peikert, C. (2011). Better key sizes (and attacks) for LWE-based + encryption. In A. Kiayias, CT-RSA~2011 (pp. 319–339). : Springer, Heidelberg. + + :param beta: block size + :param d: lattice dimension + :param B: bit-size of entries + + """ + delta_0 = delta_0f(beta) + return BKZ.LLL(d, B) + ZZ(2) ** RR(1.8 / log(delta_0, 2) - 110 + log(2.3 * 10 ** 9, 2)) + + @staticmethod + def _BDGL16_small(beta, d, B=None): + u""" + Runtime estimation given `β` and assuming sieving is used to realise the SVP oracle for small dimensions. + + :param beta: block size + :param d: lattice dimension + :param B: bit-size of entries + + .. [BDGL16] Becker, A., Ducas, L., Gama, N., & Laarhoven, T. (2016). New directions in + nearest neighbor searching with applications to lattice sieving. In SODA 2016, (pp. + 10–24). + + """ + return BKZ.LLL(d, B) + ZZ(2) ** RR(0.292 * beta + 16.4 + log(BKZ.svp_repeat(beta, d), 2)) + + @staticmethod + def _BDGL16_asymptotic(beta, d, B=None): + u""" + Runtime estimation given `β` and assuming sieving is used to realise the SVP oracle. + + :param beta: block size + :param d: lattice dimension + :param B: bit-size of entries + + .. [BDGL16] Becker, A., Ducas, L., Gama, N., & Laarhoven, T. (2016). New directions in + nearest neighbor searching with applications to lattice sieving. In SODA 2016, (pp. + 10–24). + + """ + # TODO we simply pick the same additive constant 16.4 as for the experimental result in [BDGL16] + return BKZ.LLL(d, B) + ZZ(2) ** RR(0.292 * beta + 16.4 + log(BKZ.svp_repeat(beta, d), 2)) + + @staticmethod + def BDGL16(beta, d, B=None): + u""" + Runtime estimation given `β` and assuming sieving is used to realise the SVP oracle. + + :param beta: block size + :param d: lattice dimension + :param B: bit-size of entries + + .. [BDGL16] Becker, A., Ducas, L., Gama, N., & Laarhoven, T. (2016). New directions in + nearest neighbor searching with applications to lattice sieving. In SODA 2016, (pp. + 10–24). + + """ + # TODO this is somewhat arbitrary + if beta <= 90: + return BKZ._BDGL16_small(beta, d, B) + else: + return BKZ._BDGL16_asymptotic(beta, d, B) + + @staticmethod + def LaaMosPol14(beta, d, B=None): + u""" + Runtime estimation for quantum sieving. + + :param beta: block size + :param d: lattice dimension + :param B: bit-size of entries + + .. [LaaMosPol14] Thijs Laarhoven, Michele Mosca, & Joop van de Pol. Finding shortest + lattice vectors faster using quantum search. Cryptology ePrint Archive, Report + 2014/907, 2014. https://eprint.iacr.org/2014/907. + + .. [Laarhoven15] Laarhoven, T. (2015). Search problems in cryptography: from + fingerprinting to lattice sieving (Doctoral dissertation). Eindhoven University of + Technology. http://repository.tue.nl/837539 + """ + return BKZ.LLL(d, B) + ZZ(2) ** RR((0.265 * beta + 16.4 + log(BKZ.svp_repeat(beta, d), 2))) + + @staticmethod + def CheNgu12(beta, d, B=None): + u""" + Runtime estimation given `β` and assuming [CheNgu12]_ estimates are correct. + + :param beta: block size + :param d: lattice dimension + :param B: bit-size of entries + + The constants in this function were derived as follows based on Table 4 in + [CheNgu12]_:: + + sage: dim = [100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250] + sage: nodes = [39.0, 44.0, 49.0, 54.0, 60.0, 66.0, 72.0, 78.0, 84.0, 96.0, 99.0, 105.0, 111.0, 120.0, 127.0, 134.0] # noqa + sage: times = [c + log(200,2).n() for c in nodes] + sage: T = list(zip(dim, nodes)) + sage: var("a,b,c,k") + (a, b, c, k) + sage: f = a*k*log(k, 2.0) + b*k + c + sage: f = f.function(k) + sage: f.subs(find_fit(T, f, solution_dict=True)) + k |--> 0.270188776350190*k*log(k) - 1.0192050451318417*k + 16.10253135200765 + + The estimation + + 2^(0.270188776350190*beta*log(beta) - 1.0192050451318417*beta + 16.10253135200765) + + is of the number of enumeration nodes, hence we need to multiply by the number of + cycles to process one node. This cost per node is typically estimated as 100 [FPLLL]. + + .. [CheNgu12] Yuanmi Chen and Phong Q. Nguyen. BKZ 2.0: Better lattice security + estimates (Full Version). 2012. http://www.di.ens.fr/~ychen/research/Full_BKZ.pdf + + .. [FPLLL] The FPLLL development team. fplll, a lattice reduction library. 2016. + Available at https://github.com/fplll/fplll + """ + # TODO replace these by fplll timings + repeat = BKZ.svp_repeat(beta, d) + cost = RR(0.270188776350190 * beta * log(beta) - 1.0192050451318417 * beta + 16.10253135200765 + log(100, 2)) + return BKZ.LLL(d, B) + repeat * ZZ(2) ** cost + + @staticmethod + def ABFKSW20(beta, d, B=None): + """ + Enumeration cost according to [ABFKSW20]_. + + :param beta: block size + :param d: lattice dimension + :param B: bit-size of entries + + .. [ABFKSW20] Martin R. Albrecht, Shi Bai, Pierre-Alain Fouque, Paul Kirchner, Damien + Stehlé and Weiqiang Wen. Faster Enumeration-based Lattice Reduction: Root Hermite + Factor $k^{1/(2k)}$ in Time $k^{k/8 + o(k)}$. CRYPTO 2020. + """ + if 1.5 * beta >= d or beta <= 92: # 1.5β is a bit arbitrary, β≤92 is the crossover point + cost = RR(0.1839 * beta * log(beta, 2) - 0.995 * beta + 16.25 + log(100, 2)) + else: + cost = RR(0.125 * beta * log(beta, 2) - 0.547 * beta + 10.4 + log(100, 2)) + repeat = BKZ.svp_repeat(beta, d) + + return BKZ.LLL(d, B) + repeat * ZZ(2) ** cost + + @staticmethod + def ADPS16(beta, d, B=None, mode="classical"): + u""" + Runtime estimation from [ADPS16]_. + + :param beta: block size + :param n: LWE dimension `n > 0` + :param B: bit-size of entries + + EXAMPLE:: + + sage: from estimator import BKZ, Param, dual, partial + sage: cost_model = partial(BKZ.ADPS16, mode="paranoid") + sage: dual(*Param.LindnerPeikert(128), reduction_cost_model=cost_model) + rop: 2^37.3 + m: 346 + red: 2^37.3 + delta_0: 1.008209 + beta: 127 + d: 346 + |v|: 284.363 + repeat: 2000 + epsilon: 0.062500 + + .. [ADPS16] Edem Alkim, Léo Ducas, Thomas Pöppelmann, & Peter Schwabe (2016). + Post-quantum key exchange - A New Hope. In T. Holz, & S. Savage, 25th USENIX + Security Symposium, USENIX Security 16 (pp. 327–343). USENIX Association. + """ + + if mode not in ("classical", "quantum", "paranoid"): + raise ValueError("Mode '%s' not understood" % mode) + + c = { + "classical": 0.2920, + "quantum": 0.2650, # paper writes 0.262 but this isn't right, see above + "paranoid": 0.2075, + } + + c = c[mode] + + return ZZ(2) ** RR(c * beta) + + sieve = BDGL16 + qsieve = LaaMosPol14 + lp = LinPei11 + enum = ABFKSW20 + + +def delta_0f(beta): + """ + Compute root-Hermite factor `δ_0` from block size `β`. + """ + beta = ZZ(round(beta)) + return BKZ._delta_0f(beta) + + +def betaf(delta): + """ + Compute block size `β` from root-Hermite factor `δ_0`. + """ + # TODO: decide for one strategy (secant, find_root, old) and its error handling + k = BKZ._beta_find_root(delta) + return k + + +reduction_default_cost = BKZ.enum + + +def lattice_reduction_cost(cost_model, delta_0, d, B=None): + """ + Return cost dictionary for returning vector of norm` δ_0^d Vol(Λ)^{1/d}` using provided lattice + reduction algorithm. + + :param lattice_reduction_estimate: + :param delta_0: root-Hermite factor `δ_0 > 1` + :param d: lattice dimension + :param B: bit-size of entries + + """ + beta = betaf(delta_0) + cost = cost_model(beta, d, B) + return Cost([("rop", cost), ("red", cost), ("delta_0", delta_0), ("beta", beta)]) + + +def lattice_reduction_opt_m(n, q, delta): + """ + Return the (heuristically) optimal lattice dimension `m` + + :param n: LWE dimension `n > 0` + :param q: modulus `0 < q` + :param delta: root Hermite factor `δ_0` + + """ + # round can go wrong if the input is not a floating point number + return ZZ(round(sqrt(n * log(q, 2) / log(delta, 2)).n())) + + +def sieve_or_enum(func): + """ + Take minimum of sieving or enumeration for lattice-based attacks. + + :param func: a lattice-reduction based estimator + """ + + def wrapper(*args, **kwds): + from copy import copy + + kwds = copy(kwds) + + a = func(*args, reduction_cost_model=BKZ.sieve, **kwds) + b = func(*args, reduction_cost_model=BKZ.enum, **kwds) + if a["red"] <= b["red"]: + return a + else: + return b + + return wrapper + + +# Combinatorial Algorithms for Sparse/Sparse Secrets + + +def guess_and_solve(f, n, alpha, q, secret_distribution, success_probability=0.99, **kwds): + """Guess components of the secret. + + :param f: + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param secret_distribution: distribution of secret, see module level documentation for details + :param success_probability: targeted success probability < 1 + + EXAMPLE:: + + sage: from estimator import guess_and_solve, dual_scale, partial + sage: q = next_prime(2^30) + sage: n, alpha = 512, 8/q + sage: dualg = partial(guess_and_solve, dual_scale) + sage: dualg(n, alpha, q, secret_distribution=((-1,1), 64)) + rop: 2^63.9 + m: 530 + red: 2^63.9 + delta_0: 1.008803 + beta: 111 + repeat: 2^21.6 + d: 1042 + c: 9.027 + k: 0 + + """ + + n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability) + RR = parent(alpha) + + a, b = SDis.bounds(secret_distribution) + h = SDis.nonzero(secret_distribution, n) + + size = b - a + 1 + best = None + step_size = 16 + fail_attempts, max_fail_attempts = 0, 5 + while step_size >= n: + step_size //= 2 + i = 0 + while True: + if i < 0: + break + try: + current = f( + n - i, + alpha, + q, + secret_distribution=secret_distribution, + success_probability=max(1 - 1 / RR(2) ** 80, success_probability), + **kwds + ) + except (OutOfBoundsError, RuntimeError, InsufficientSamplesError): + i += abs(step_size) + fail_attempts += 1 + if fail_attempts > max_fail_attempts: + break + continue + if h is None or i < h: + repeat = size ** i + else: + # TODO: this is too pessimistic + repeat = (size) ** h * binomial(i, h) + current["k"] = i + current = current.repeat(repeat, select={"m": False}) + + logging.getLogger("binsearch").debug(current) + + key = list(current)[0] + if best is None: + best = current + i += step_size + else: + if best[key] > current[key]: + best = current + i += step_size + else: + step_size = -1 * step_size // 2 + i += step_size + + if step_size == 0: + break + if best is None: + raise RuntimeError("No solution could be found.") + return best + + +def success_probability_drop(n, h, k, fail=0, rotations=False): + """ + Probability that `k` randomly sampled components have ``fail`` non-zero components amongst + them. + + :param n: LWE dimension `n > 0` + :param h: number of non-zero components + :param k: number of components to ignore + :param fail: we tolerate ``fail`` number of non-zero components amongst the `k` ignored + components + :param rotations: consider rotations of the basis to exploit ring structure (NTRU only) + """ + + N = n # population size + K = n - h # number of success states in the population + n = k # number of draws + k = n - fail # number of observed successes + prob_drop = binomial(K, k) * binomial(N - K, n - k) / binomial(N, n) + if rotations: + return 1 - (1 - prob_drop) ** N + else: + return prob_drop + + +def drop_and_solve( + f, + n, + alpha, + q, + secret_distribution=True, + success_probability=0.99, + postprocess=True, + decision=True, + rotations=False, + **kwds +): + """ + Solve instances of dimension ``n-k`` with increasing ``k`` using ``f`` and pick parameters such + that cost is minimised. + + :param f: attack estimate function + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param secret_distribution: distribution of secret, see module level documentation for details + :param success_probability: targeted success probability < 1 + :param postprocess: check against shifted distributions + :param decision: the underlying algorithm solves the decision version or not + + EXAMPLE:: + + sage: from estimator import drop_and_solve, dual_scale, primal_usvp, partial + sage: q = next_prime(2^30) + sage: n, alpha = 512, 8/q + sage: primald = partial(drop_and_solve, primal_usvp) + sage: duald = partial(drop_and_solve, dual_scale) + + sage: duald(n, alpha, q, secret_distribution=((-1,1), 64)) + rop: 2^55.9 + m: 478 + red: 2^55.3 + delta_0: 1.009807 + beta: 89 + repeat: 2^14.2 + d: 920 + c: 8.387 + k: 70 + postprocess: 8 + + sage: duald(n, alpha, q, secret_distribution=((-3,3), 64)) + rop: 2^59.8 + m: 517 + red: 2^59.7 + delta_0: 1.009403 + beta: 97 + repeat: 2^18.2 + d: 969 + c: 3.926 + k: 60 + postprocess: 6 + + sage: kwds = {"use_lll":False, "postprocess":False} + sage: duald(n, alpha, q, secret_distribution=((-1,1), 64), **kwds) + rop: 2^69.7 + m: 521 + red: 2^69.7 + delta_0: 1.008953 + beta: 107 + repeat: 257 + d: 1033 + c: 9.027 + k: 0 + postprocess: 0 + + sage: duald(n, alpha, q, secret_distribution=((-3,3), 64), **kwds) + rop: 2^74.2 + m: 560 + red: 2^74.2 + delta_0: 1.008668 + beta: 114 + repeat: 524.610 + d: 1068 + c: 4.162 + k: 4 + postprocess: 0 + + sage: duald(n, alpha, q, secret_distribution=((-3,3), 64), rotations=True, **kwds) + Traceback (most recent call last): + ... + ValueError: Rotations are only support as part of the primal-usvp attack on NTRU. + + sage: primald(n, alpha, q, secret_distribution=((-3,3), 64), rotations=True, **kwds) + rop: 2^51.5 + red: 2^51.5 + delta_0: 1.010046 + beta: 84 + d: 913 + m: 444 + repeat: 1.509286 + k: 44 + postprocess: 0 + + sage: primald(n, alpha, q, secret_distribution=((-3,3), 64), rotations=False, **kwds) + rop: 2^58.2 + red: 2^58.2 + delta_0: 1.009350 + beta: 98 + d: 1001 + m: 492 + repeat: 1.708828 + k: 4 + postprocess: 0 + + This function is based on: + + .. [Albrecht17] Albrecht, M. R. (2017). On dual lattice attacks against small-secret LWE and + parameter choices in helib and SEAL. In J. Coron, & J. B. Nielsen, EUROCRYPT 2017, Part II + (pp. 103–129). + + + """ + + if rotations and f.__name__ != "primal_usvp": + raise ValueError("Rotations are only support as part of the primal-usvp attack on NTRU.") + + n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability) + + best = None + + if not decision and postprocess: + raise ValueError("Postprocessing is only defined for the dual attack which solves the decision version.") + + # too small a step size leads to an early abort, too large a step + # size means stepping over target + step_size = int(n // 32) + + if not SDis.is_bounded_uniform(secret_distribution): + raise NotImplementedError("Only bounded uniform secrets are currently supported.") + + a, b = SDis.bounds(secret_distribution) + assert SDis.is_bounded_uniform(secret_distribution) + h = SDis.nonzero(secret_distribution, n) + + k = ZZ(0) + + while (n - h) > k: + probability = success_probability_drop(n, h, k, rotations=rotations) + + # increase precision until the probability is meaningful + while success_probability ** probability == 1: + success_probability = RealField(64 + success_probability.prec())(success_probability) + + current = f( + n - k, + alpha, + q, + success_probability=success_probability ** probability if decision else success_probability, + secret_distribution=secret_distribution, + **kwds + ) + + cost_lat = list(current.values())[0] + cost_post = 0 + if postprocess: + repeat = current["repeat"] + dim = current["d"] + for i in range(1, k): + # compute inner products with rest of A + cost_post_i = 2 * repeat * dim * k + # there are (k)(i) positions and max(s_i)-min(s_i) options per position + # for each position we need to add/subtract the right elements + cost_post_i += repeat * binomial(k, i) * (b - a) ** i * i + if cost_post + cost_post_i >= cost_lat: + postprocess = i + break + cost_post += cost_post_i + probability += success_probability_drop(n, h, k, i, rotations=rotations) + + current["rop"] = cost_lat + cost_post + current = current.repeat(1 / probability, select={"m": False}) + current["k"] = k + current["postprocess"] = postprocess + current = current.reorder(["rop"]) + + logging.getLogger("guess").debug(current) + + key = list(current)[0] + if best is None: + best = current + k += step_size + continue + + if current[key] < best[key]: + best = current + k += step_size + else: + # we go back + k = best["k"] - step_size + k += step_size // 2 + if k <= 0: + k = step_size // 2 + # and half the step size + step_size = step_size // 2 + + if step_size == 0: + break + + return best + + +# Primal Attack (uSVP) + + +def _primal_scale_factor(secret_distribution, alpha=None, q=None, n=None): + """ + Scale factor for primal attack, in the style of [BaiGal14]. In the case of non-centered secret + distributions, it first appropriately rebalances the lattice bases to maximise the scaling. + + :param secret_distribution: distribution of secret, see module level documentation for details + :param alpha: noise rate `0 ≤ α < 1`, noise has standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param n: only used for sparse secrets + + EXAMPLE:: + + sage: from estimator import _primal_scale_factor, alphaf + sage: _primal_scale_factor(True, 8./2^15, 2^15) + 1.000000000... + + sage: _primal_scale_factor(alphaf(sqrt(2), 2^13, True), \ + alpha=alphaf(sqrt(16/3), 2^13, True), q=2^13) + 1.63299316185545 + + sage: _primal_scale_factor((-1,1), alpha=8./2^15, q=2^15) + 3.908820095... + + sage: _primal_scale_factor(((-1,1), 64), alpha=8./2^15, q=2^15, n=256) + 6.383076486... + + sage: _primal_scale_factor((-3,3), alpha=8./2^15, q=2^15) + 1.595769121... + + sage: _primal_scale_factor(((-3,3), 64), alpha=8./2^15, q=2^15, n=256) + 2.954790254... + + sage: _primal_scale_factor((-3,2), alpha=8./2^15, q=2^15) + 1.868773442... + + sage: _primal_scale_factor(((-3,2), 64), alpha=8./2^15, q=2^15, n=256) + 3.313928192... + + sage: _primal_scale_factor((0, 1), alpha=sqrt(2*pi)/2^15, q=2^15) + 2.000000000... + + sage: _primal_scale_factor((0, 1), alpha=sqrt(2*pi)/2^15/2, q=2^15) + 1.000000000... + + sage: _primal_scale_factor((0, 1), alpha=sqrt(2*pi)/2^15/1.99, q=2^15) > 1 + True + + .. [BaiGal14] Bai, S., & Galbraith, S. D. (2014). Lattice decoding attacks on binary + LWE. In W. Susilo, & Y. Mu, ACISP 14 (pp. 322–337). : Springer, Heidelberg. + """ + + # For small/sparse secret use Bai and Galbraith's scaled embedding + # NOTE: We assume a <= 0 <= b + + scale = RR(1) + if SDis.is_small(secret_distribution, alpha=alpha): + # target same same shortest vector length as in Kannan's embedding of same dimension + stddev = stddevf(alpha * q) + var_s = SDis.variance(secret_distribution, alpha, q, n=n) + if stddev ** 2 > var_s: + # only scale if the error is sampled wider than the secret + scale = stddev / RR(sqrt(var_s)) + + return scale + + +def _primal_usvp( + block_size, + n, + alpha, + q, + scale=1, + m=oo, + success_probability=0.99, + kannan_coeff=None, + d=None, + reduction_cost_model=reduction_default_cost, +): + """ + Estimate cost of solving LWE using primal attack (uSVP version) + + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param scale: The identity part of the lattice basis is scaled by this constant. + :param m: number of available LWE samples `m > 0` + :param d: dimension for the attack d <= m + 1 (`None' for optimized choice) + :parap kannan_coeff: Coeff for Kannan's embedding (`None' to set it kannan_coeff=stddev, + which is optimal at least when Distrib(secret) = Distrib(Error).) + :param success_probability: targeted success probability < 1 + :param reduction_cost_model: cost model for lattice reduction + + .. note:: This is the low-level function, in most cases you will want to call ``primal_usvp`` + + """ + + n, alpha, q = Param.preprocess(n, alpha, q) + RR = alpha.parent() + q = RR(q) + delta_0 = delta_0f(block_size) + stddev = stddevf(alpha * q) + block_size = RR(block_size) + + scale = RR(scale) + + m = min(2 * ceil(sqrt(n * log(q) / log(delta_0))), m) + + if kannan_coeff is None: + kannan_coeff = stddev + + def log_b_star(d): + return delta_0.log() * (2 * block_size - d) + (kannan_coeff.log() + n * scale.log() + (d - n - 1) * q.log()) / d + + C = (stddev ** 2 * (block_size - 1) + kannan_coeff ** 2).log() / 2 + + # find the smallest d \in [n,m] s.t. a*d^2 + b*d + c >= 0 + # if no such d exists, return the upper bound m + def solve_for_d(n, m, a, b, c): + if a*n*n + b*n + c >= 0: # trivial case + return n + + # solve for ad^2 + bd + c == 0 + + disc = b*b - 4*a*c # the discriminant + if disc < 0: # no solution, return m + return m + + # compute the two solutions + d1 = (-b + sqrt(disc))/(2*a) + d2 = (-b - sqrt(disc))/(2*a) + if a > 0: # the only possible solution is ceiling(d2) + return min(m, ceil(d2)) + + # the case a<=0: + + # if n is to the left of d1 then the first solution is ceil(d1) + if n <= d1: + return min(m, ceil(d1)) + + # otherwise, n must be larger than d2 (since an^2+bn+c<0) so no solution + return m + + # we have m samples → the largest permissible dimension d is m+1 + if d is None: + # for d in range(n, m + 2): + # if log_b_star(d) - C >= 0: + # break + aa = -delta_0.log() + bb = delta_0.log()*2*block_size + q.log() - C + cc = kannan_coeff.log() + n * scale.log() - (n + 1) * q.log() + # assert d == solve_for_d(n, m+1, aa, bb, cc) + d = solve_for_d(n, m+1, aa, bb, cc) + + assert d <= m + 1 + + def ineq(d): + lhs = sqrt(stddev ** 2 * (block_size - 1) + kannan_coeff ** 2) + rhs = delta_0 ** (2 * block_size - d) * (kannan_coeff * scale ** n * q ** (d - n - 1)) ** (ZZ(1) / d) + return lhs <= rhs + + ret = lattice_reduction_cost(reduction_cost_model, delta_0, d) + if not ineq(d): + ret["rop"] = oo + ret["red"] = oo + + ret["d"] = d + ret["delta_0"] = delta_0 + + return ret + + +def primal_usvp( + n, + alpha, + q, + secret_distribution=True, + m=oo, + success_probability=0.99, + kannan_coeff=None, + d=None, + reduction_cost_model=reduction_default_cost, + **kwds +): + u""" + Estimate cost of solving LWE using primal attack (uSVP version) + + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param secret_distribution: distribution of secret, see module level documentation for details + :param m: number of LWE samples `m > 0` + :param success_probability: targeted success probability < 1 + :param reduction_cost_model: cost model for lattice reduction + + EXAMPLES:: + + sage: from estimator import primal_usvp, Param, BKZ + sage: n, alpha, q = Param.Regev(256) + + sage: primal_usvp(n, alpha, q) + rop: 2^145.5 + red: 2^145.5 + delta_0: 1.005374 + beta: 256 + d: 694 + m: 437 + + sage: primal_usvp(n, alpha, q, secret_distribution=True, m=n) + rop: 2^186.9 + red: 2^186.9 + delta_0: 1.004638 + beta: 320 + d: 513 + m: 256 + + sage: primal_usvp(n, alpha, q, secret_distribution=False, m=2*n) + rop: 2^186.9 + red: 2^186.9 + delta_0: 1.004638 + beta: 320 + d: 513 + m: 512 + + sage: primal_usvp(n, alpha, q, reduction_cost_model=BKZ.sieve) + rop: 2^103.6 + red: 2^103.6 + delta_0: 1.005374 + beta: 256 + d: 694 + m: 437 + + sage: primal_usvp(n, alpha, q) + rop: 2^145.5 + red: 2^145.5 + delta_0: 1.005374 + beta: 256 + d: 694 + m: 437 + + sage: primal_usvp(n, alpha, q, secret_distribution=(-1,1), m=n) + rop: 2^84.7 + red: 2^84.7 + delta_0: 1.007345 + beta: 154 + d: 513 + m: 256 + + sage: primal_usvp(n, alpha, q, secret_distribution=((-1,1), 64)) + rop: 2^77.1 + red: 2^77.1 + delta_0: 1.007754 + beta: 140 + d: 477 + m: 220 + + .. [USENIX:ADPS16] Alkim, E., Léo Ducas, Thomas Pöppelmann, & Schwabe, P. (2015). + Post-quantum key exchange - a new hope. + + .. [BaiGal14] Bai, S., & Galbraith, S. D. (2014). Lattice decoding attacks on binary + LWE. In W. Susilo, & Y. Mu, ACISP 14 (pp. 322–337). : Springer, Heidelberg. + + """ + + if m < 1: + raise InsufficientSamplesError("m=%d < 1" % m) + + n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability) + + # allow for a larger embedding lattice dimension, using Bai and Galbraith's + if SDis.is_small(secret_distribution, alpha=alpha): + m += n + + scale = _primal_scale_factor(secret_distribution, alpha, q, n) + + kwds = { + "n": n, + "alpha": alpha, + "q": q, + "kannan_coeff": kannan_coeff, + "d": d, + "reduction_cost_model": reduction_cost_model, + "m": m, + "scale": scale, + } + + cost = binary_search( + _primal_usvp, + start=40, + stop=2 * n, + param="block_size", + predicate=lambda x, best: x["red"] <= best["red"], + **kwds + ) + + for block_size in range(32, cost["beta"] + 1)[::-1]: + t = _primal_usvp(block_size=block_size, **kwds) + if t["red"] == oo: + break + cost = t + + if SDis.is_small(secret_distribution, alpha=alpha): + cost["m"] = cost["d"] - n - 1 + else: + cost["m"] = cost["d"] - 1 + + return cost + + +# Primal Attack (Enumeration) + + +def enumeration_cost(n, alpha, q, success_probability, delta_0, m, clocks_per_enum=2 ** 15.1): + """ + Estimates the cost of performing enumeration. + + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param success_probability: target success probability + :param delta_0: root-Hermite factor `δ_0 > 1` + :param m: number of LWE samples `m > 0` + :param clocks_per_enum: the log of the number of clock cycles needed per enumeration + """ + target_success_probability = success_probability + + try: + alpha.is_NaN() + RR = alpha.parent() + + except AttributeError: + RR = RealField(128) + + step = RDF(1) + + B = BKZ.GSA(n, q, delta_0, m) + + d = [RDF(1)] * m + bd = [d[i] * B[i] for i in range(m)] + scaling_factor = RDF(sqrt(pi) / (2 * alpha * q)) + probs_bd = [RDF((bd[i] * scaling_factor)).erf() for i in range(m)] + success_probability = prod(probs_bd) + + if RR(success_probability).is_NaN() or success_probability == 0.0: + # try in higher precision + step = RR(step) + d = [RR(1)] * m + bd = [d[i] * B[i] for i in range(m)] + scaling_factor = RR(sqrt(pi) / (2 * alpha * q)) + probs_bd = [RR((bd[i] * scaling_factor)).erf() for i in range(m)] + success_probability = prod(probs_bd) + + if success_probability.is_NaN(): + return OrderedDict([("babai", oo), ("babai_op", oo)]) + + if success_probability <= 0: + # TODO this seems like the wrong except to raise + raise InsufficientSamplesError("success probability <= 0.") + + bd = map(list, zip(bd, range(len(bd)))) + bd = sorted(bd) + + import bisect + + last_success_probability = success_probability + + while success_probability < RDF(target_success_probability): + v, i = bd.pop(0) + d[i] += step + v += B[i] * step + last_success_probability = success_probability + success_probability /= probs_bd[i] + probs_bd[i] = (v * scaling_factor).erf() + success_probability *= probs_bd[i] + bisect.insort_left(bd, [v, i]) + + if success_probability == 0 or last_success_probability >= success_probability: + return Cost([("babai", oo), ("babai_op", oo)]) + + r = Cost([("babai", RR(prod(d))), ("babai_op", RR(prod(d)) * clocks_per_enum)]) + + return r + + +def _primal_decode( + n, + alpha, + q, + secret_distribution=True, + m=oo, + success_probability=0.99, + reduction_cost_model=reduction_default_cost, + clocks_per_enum=2 ** 15.1, +): + """ + Decoding attack as described in [LinPei11]_. + + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param m: number of LWE samples `m > 0` + :param success_probability: targeted success probability < 1 + :param clocks_per_enum: the number of enumerations computed per clock cycle + + EXAMPLE: + + sage: from estimator import Param, primal_decode + sage: n, alpha, q = Param.Regev(256) + + sage: primal_decode(n, alpha, q) + rop: 2^156.5 + m: 464 + red: 2^156.5 + delta_0: 1.005446 + beta: 251 + d: 720 + babai: 2^141.8 + babai_op: 2^156.9 + repeat: 2^14.2 + epsilon: 2^-12.0 + + sage: primal_decode(n, alpha, q, secret_distribution=(-1,1), m=n) + rop: 2^194.6 + m: 256 + red: 2^194.6 + delta_0: 1.005069 + beta: 280 + d: 512 + babai: 2^179.6 + babai_op: 2^194.7 + repeat: 2^34.2 + epsilon: 2^-32.0 + + sage: primal_decode(n, alpha, q, secret_distribution=((-1,1), 64)) + rop: 2^156.5 + m: 464 + red: 2^156.5 + delta_0: 1.005446 + beta: 251 + d: 720 + babai: 2^141.8 + babai_op: 2^156.9 + repeat: 2^14.2 + epsilon: 2^-12.0 + + .. [LinPei11] Lindner, R., & Peikert, C. (2011). Better key sizes (and attacks) for + LWE-based encryption. In A. Kiayias, CT-RSA~2011 (pp. 319–339). : Springer, Heidelberg. + """ + + n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability) + + if m < 1: + raise InsufficientSamplesError("m=%d < 1" % m) + + if SDis.is_small(secret_distribution, alpha=alpha): + m_ = m + n + else: + m_ = m + + RR = alpha.parent() + + delta_0m1 = ( + _dual(n, alpha, q, success_probability=success_probability, secret_distribution=secret_distribution, m=m)[ + "delta_0" + ] + - 1 + ) + + step = RR(1.05) + direction = -1 + + def combine(enum, bkz): + current = Cost() + current["rop"] = enum["babai_op"] + bkz["red"] + + for key in bkz: + current[key] = bkz[key] + for key in enum: + current[key] = enum[key] + current[u"m"] = m_ - n if SDis.is_small(secret_distribution, alpha=alpha) else m_ + return current + + depth = 6 + current = None + while True: + delta_0 = 1 + delta_0m1 + + if delta_0 >= 1.0219 and current is not None: # LLL is enough + break + + m_optimal = lattice_reduction_opt_m(n, q, delta_0) + m_ = min(m_optimal, m_) + bkz = lattice_reduction_cost(reduction_cost_model, delta_0, m_, B=log(q, 2)) + bkz["d"] = m_ + + enum = enumeration_cost(n, alpha, q, success_probability, delta_0, m_, clocks_per_enum=clocks_per_enum) + current = combine(enum, bkz).reorder(["rop", "m"]) + + # if lattice reduction is cheaper than enumration, make it more expensive + if current["red"] < current["babai_op"]: + prev_direction = direction + direction = -1 + if direction != prev_direction: + step = 1 + RR(step - 1) / 2 + delta_0m1 /= step + elif current["red"] > current["babai_op"]: + prev_direction = direction + direction = 1 + delta_0m1 *= step + else: + break + if direction != prev_direction: + step = 1 + RR(step - 1) / 2 + depth -= 1 + if depth == 0: + break + + return current + + +primal_decode = partial(rinse_and_repeat, _primal_decode, decision=False, repeat_select={"m": False}) + + +# Dual Attack + + +def _dual_scale_factor(secret_distribution, alpha=None, q=None, n=None, c=None): + """ + Scale factor for dual attack. Assumes the secret vector has been "re-balanced", as to + be distributed around 0. + + :param secret_distribution: distribution of secret, see module level documentation for details + :param alpha: noise rate `0 ≤ α < 1`, noise has standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param n: only used for sparse secrets + :param c: explicit constant `c` + + EXAMPLE:: + + sage: from estimator import _dual_scale_factor, alphaf + sage: _dual_scale_factor(True, 8./2^15, 2^15) + (1.00000000000000, 3.19153824321146) + + sage: _dual_scale_factor(alphaf(sqrt(2), 2^13, True), \ + alpha=alphaf(sqrt(16/3), 2^13, True), q=2^13) + (1.63299316185545, 1.41421356237309) + + sage: _dual_scale_factor((-1,1), alpha=8./2^15, q=2^15) + (3.90882009522336, 0.816496580927726) + + sage: _dual_scale_factor(((-1,1), 64), alpha=8./2^15, q=2^15, n=256) + (6.38307648642292, 0.500000000000000) + + sage: _dual_scale_factor((-3,3), alpha=8./2^15, q=2^15) + (1.59576912160573, 2.00000000000000) + + sage: _dual_scale_factor(((-3,3), 64), alpha=8./2^15, q=2^15, n=256) + (2.95479025475795, 1.08012344973464) + + sage: _dual_scale_factor((-3,2), alpha=8./2^15, q=2^15) + (1.86877344262086, 1.70782512765993) + + sage: _dual_scale_factor(((-3,2), 64), alpha=8./2^15, q=2^15, n=256) + (3.31392819210159, 0.963068014212911) + + .. note :: This function assumes that the bounds are of opposite sign, and that the + distribution is centred around zero. + """ + stddev = RR(stddevf(alpha * q)) + # Calculate scaling in the case of bounded_uniform secret distribution + # NOTE: We assume a <= 0 <= b + if SDis.is_small(secret_distribution, alpha=alpha): + stddev_s = SDis.variance(secret_distribution, alpha=alpha, q=q, n=n).sqrt() + if c is None: + # || = || → c * \sqrt{n} * \sqrt{σ_{s_i}^2 + E(s_i)^2} == \sqrt{m} * σ + # TODO: we are assuming n == m here! The coefficient formula for general m + # is the one below * sqrt(m/n) + c = RR(stddev / stddev_s) + else: + stddev_s = stddev + c = RR(1) + + stddev_s = RR(stddev_s) + + return c, stddev_s + + +def _dual( + n, alpha, q, secret_distribution=True, m=oo, success_probability=0.99, reduction_cost_model=reduction_default_cost +): + """ + Estimate cost of solving LWE using dual attack as described in [MicReg09]_ + + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param secret_distribution: distribution of secret, see module level documentation for details + :param m: number of LWE samples `m > 0` + :param success_probability: targeted success probability < 1 + :param reduction_cost_model: cost model for lattice reduction + + .. [MicReg09] Micciancio, D., & Regev, O. (2009). Lattice-based cryptography. In D. J. + Bernstein, J. Buchmann, & E. Dahmen (Eds.), Post-Quantum Cryptography (pp. 147–191). + Berlin, Heidelberg, New York: Springer, Heidelberg. + + EXAMPLE:: + + sage: from estimator import Param, BKZ, dual + sage: n, alpha, q = Param.Regev(256) + + sage: dual(n, alpha, q) + rop: 2^197.2 + m: 751 + red: 2^197.2 + delta_0: 1.005048 + beta: 282 + d: 751 + |v|: 1923.968 + repeat: 2^35.0 + epsilon: 2^-16.0 + + sage: dual(n, alpha, q, secret_distribution=True, m=n) + rop: 2^255.2 + m: 512 + red: 2^255.2 + delta_0: 1.004627 + beta: 322 + d: 512 + |v|: 2^11.4 + repeat: 2^67.0 + epsilon: 2^-32.0 + + sage: dual(n, alpha, q, secret_distribution=False, m=2*n) + rop: 2^255.2 + m: 512 + red: 2^255.2 + delta_0: 1.004627 + beta: 322 + d: 512 + |v|: 2^11.4 + repeat: 2^67.0 + epsilon: 2^-32.0 + + sage: dual(n, alpha, q, reduction_cost_model=BKZ.sieve) + rop: 2^142.9 + m: 787 + red: 2^142.9 + delta_0: 1.004595 + beta: 325 + d: 787 + |v|: 1360.451 + repeat: 2^19.0 + epsilon: 0.003906 + + .. note :: this is the standard dual attack, for the small secret variant see + ``dual_scale`` + + """ + + n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability) + + if m < 1: + raise InsufficientSamplesError("m=%d < 1" % m) + + RR = parent(alpha) + f = lambda eps: RR(sqrt(log(1 / eps) / pi)) # noqa + + if SDis.is_small(secret_distribution, alpha=alpha): + m = m + n + log_delta_0 = log(f(success_probability) / alpha, 2) ** 2 / (4 * n * log(q, 2)) + delta_0 = min(RR(2) ** log_delta_0, RR(1.02190)) # at most LLL + m_optimal = lattice_reduction_opt_m(n, q, delta_0) + if m > m_optimal: + m = m_optimal + else: + log_delta_0 = log(f(success_probability) / alpha, 2) / m - RR(log(q, 2) * n) / (m ** 2) + delta_0 = RR(2 ** log_delta_0) + + # check for valid delta + # TODO delta_0f(m) is HKZ reduction, and thus it is identical 1, i.e. meaningless. + # A better check here would be good. + if delta_0 < delta_0f(m): + raise OutOfBoundsError("delta_0 = %f < %f" % (delta_0, delta_0f(m))) + + ret = lattice_reduction_cost(reduction_cost_model, delta_0, m, B=log(q, 2)) + ret[u"m"] = m + ret[u"d"] = m + ret[u"|v|"] = RR(delta_0 ** m * q ** (n / m)) + return ret.reorder(["rop", "m"]) + + +dual = partial(rinse_and_repeat, _dual, repeat_select={"m": False}) + + +def dual_scale( + n, + alpha, + q, + secret_distribution, + m=oo, + success_probability=0.99, + reduction_cost_model=reduction_default_cost, + c=None, + use_lll=True, +): + """ + Estimate cost of solving LWE by finding small `(y,x/c)` such that `y ⋅ A ≡ c ⋅ x \bmod q` as + described in [EC:Abrecht17]_ + + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param secret_distribution: distribution of secret, see module level documentation for details + :param m: number of LWE samples `m > 0` + :param success_probability: targeted success probability < 1 + :param reduction_cost_model: cost model for lattice reduction + :param c: explicit constant `c` + :param use_lll: use LLL calls to produce more small vectors + + EXAMPLES:: + + sage: from estimator import Param, dual_scale + + sage: dual_scale(*Param.Regev(256), secret_distribution=(-1,1)) + rop: 2^101.8 + m: 288 + red: 2^101.8 + delta_0: 1.006648 + beta: 183 + repeat: 2^65.4 + d: 544 + c: 31.271 + + sage: dual_scale(*Param.Regev(256), secret_distribution=(-1,1), m=200) + rop: 2^106.0 + m: 200 + red: 2^106.0 + delta_0: 1.006443 + beta: 192 + repeat: 2^68.0 + d: 456 + c: 31.271 + + sage: dual_scale(*Param.Regev(256), secret_distribution=((-1,1), 64)) + rop: 2^93.7 + m: 258 + red: 2^93.7 + delta_0: 1.006948 + beta: 170 + repeat: 2^56.0 + d: 514 + c: 51.065 + + .. [Albrecht17] Albrecht, M. R. (2017). On dual lattice attacks against small-secret LWE and + parameter choices in helib and SEAL. In J. Coron, & J. B. Nielsen, EUROCRYPT} 2017, Part {II + (pp. 103–129). + """ + + n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability) + RR = parent(alpha) + + # stddev of the error + stddev = RR(stddevf(alpha * q)) + + m_max = m + + if not SDis.is_small(secret_distribution, alpha=alpha): + m_max -= n # We transform to normal form, spending n samples + + c, stddev_s = _dual_scale_factor(secret_distribution, alpha=alpha, q=q, n=n, c=c) + + best = dual(n=n, alpha=alpha, q=q, m=m, reduction_cost_model=reduction_cost_model) + delta_0 = best["delta_0"] + + if use_lll: + rerand_scale = 2 + else: + rerand_scale = 1 + + while True: + m_optimal = lattice_reduction_opt_m(n, q / c, delta_0) + d = ZZ(min(m_optimal, m_max + n)) + m = d - n + + # the vector found will have norm v + v = rerand_scale * delta_0 ** d * (q / c) ** (n / d) + + # each component has stddev v_ + v_ = v / RR(sqrt(d)) + + # we split our vector in two parts. + v_r = sigmaf(RR(stddev * sqrt(m) * v_)) # 1. v_r is multiplied with the error stddev (dimension m-n) + v_l = sigmaf(RR(c * stddev_s * sqrt(n) * v_)) # 2. v_l is the rounding noise (dimension n) + + ret = lattice_reduction_cost(reduction_cost_model, delta_0, d, B=log(q, 2)) + + repeat = max(amplify_sigma(success_probability, (v_r, v_l), q), RR(1)) + if use_lll: + lll = BKZ.LLL(d, log(q, 2)) + else: + lll = None + ret = ret.repeat(times=repeat, lll=lll) + + ret[u"m"] = m + ret[u"repeat"] = repeat + ret[u"d"] = d + ret[u"c"] = c + + ret = ret.reorder(["rop", "m"]) + logging.getLogger("repeat").debug(ret) + + if best is None: + best = ret + + if ret["rop"] > best["rop"]: + break + + best = ret + delta_0 = delta_0 + RR(0.00005) + + return best + + +# Combinatorial + + +def mitm(n, alpha, q, secret_distribution=True, m=oo, success_probability=0.99): + """ + Meet-in-the-Middle attack as described in [AlbPlaSco15]_ + + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param secret_distribution: distribution of secret, see module level documentation for details + :param m: number of LWE samples `m > 0` + :param success_probability: targeted success probability < 1 + + .. [AlbPlaSco15] Albrecht, M. R., Player, R., & Scott, S. (2015). On the concrete hardness of + Learning with Errors. Journal of Mathematical Cryptology, 9(3), 169–203. + + """ + n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability) + ret = Cost() + RR = alpha.parent() + + if not SDis.is_small(secret_distribution, alpha=alpha): + m = m - n + secret_distribution = True + ret["m"] = n + else: + ret["m"] = 0 + + if m < 1: + raise InsufficientSamplesError("m=%d < 1" % m) + + t = ceil(2 * sqrt(log(n))) + try: + a, b = SDis.bounds(secret_distribution) + size = b - a + 1 + except ValueError: + size = (2 * t * alpha * q) + 1 + + m_required = ceil((log((2 * n) / ((size ** (n / 2)) - 1))) / (log(size / q))) + if m is not None and m < m_required: + raise InsufficientSamplesError("m=%d < %d (required)" % (m, m_required)) + else: + m = m_required + + if (3 * t * alpha) * m > 1 - 1 / (2 * n): + raise ValueError("Cannot find m to satisfy constraints (noise too big).") + + ret["rop"] = RR(size ** (n / 2) * 2 * n * m) + ret["mem"] = RR(size ** (n / 2) * m) + ret["m"] += m + + return ret.reorder(["rop", "m", "mem"]) + + +cfft = 1 # convolutions mod q + + +def _bkw_coded(n, alpha, q, secret_distribution=True, m=oo, success_probability=0.99, t2=0, b=2, ntest=None): # noqa + """ + Coded-BKW. + + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param t2: number of coded BKW steps (≥ 0) + :param b: table size (≥ 1) + :param success_probability: targeted success probability < 1 + :param ntest: optional parameter ntest + + """ + n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability) + sigma = stddevf(alpha * q) # [C:GuoJohSta15] use σ = standard deviation + RR = alpha.parent() + + cost = Cost() + + # Our cost is mainly determined by q**b, on the other hand there are + # expressions in q**(l+1) below, hence, we set l = b - 1. This allows to + # achieve the performance reported in [C:GuoJohSta15]. + + b = ZZ(b) + cost["b"] = b + l = b - 1 # noqa + cost["l"] = l + + try: + secret_bounds = SDis.bounds(secret_distribution) + d = secret_bounds[1] - secret_bounds[0] + 1 + except ValueError: + d = 3 * sigma # TODO make this dependent on success_probability + + # cost["d"] = d + # cost[u"γ"] = gamma + + def N(i, sigma_set): + """ + 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 / ZZ(2) ** i, q) / 2)) + + def find_ntest(n, l, t1, t2, b): # noqa + """ + If the parameter `ntest` is not provided, we use this function to estimate it. + + :param n: LWE dimension `n > 0` + :param l: 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 nest by aiming for ntop == 0 + ntest = var("ntest") + sigma_set = sqrt(q ** (2 * (1 - l / ntest)) / 12) + ncod = sum([N(i, sigma_set) for i in range(1, t2 + 1)]) + ntop = n - ncod - ntest - t1 * b + + try: + start = max(ZZ(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=ntest).n()) < abs(ntop(ntest=ntest_min).n()): + ntest_min = ntest + else: + break + return ZZ(ntest_min) + + # 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_ = find_ntest(n, l, t1, t2, b) + else: + ntest_ = ntest + sigma_set = sqrt(q ** (2 * (1 - l / ntest_)) / 12) + Ni = [N(i, sigma_set) for i in range(1, t2 + 1)] + t1 = len([e for e in Ni if e <= b]) + + # there is no point in having more tables than needed to cover n + if b * t1 > n: + t1 = n // b + t2 -= t1 + + cost["t1"] = t1 + cost["t2"] = t2 + + # compute ntest with the t1 just computed + if ntest is None: + ntest = find_ntest(n, l, t1, t2, b) + + # if there's no ntest then there's no `σ_{set}` and hence no ncod + if ntest: + sigma_set = sqrt(q ** (2 * (1 - l / ntest)) / 12) + # cost[u"σ_set"] = RR(sigma_set) + ncod = sum([N(i, sigma_set) for i in range(1, t2 + 1)]) + else: + ncod = 0 + + ntot = ncod + ntest + ntop = max(n - ncod - ntest - t1 * b, 0) + cost["ncod"] = ncod # coding step + cost["ntop"] = ntop # guessing step, typically zero + cost["ntest"] = ntest # hypothesis testing + + # Theorem 1: quantization noise + addition noise + s_var = SDis.variance(secret_distribution, alpha, q, n=n) + coding_variance = s_var * sigma_set ** 2 * ntot + sigma_final = RR(sqrt(2 ** (t1 + t2) * sigma ** 2 + coding_variance)) + # cost[u"σ_final"] = RR(sigma_final) + + M = amplify_sigma(success_probability, sigmaf(sigma_final), q) + if M is oo: + cost["rop"] = oo + cost["m"] = oo + return cost + m = (t1 + t2) * (q ** b - 1) / 2 + M + cost["m"] = RR(m) + + if not SDis.is_small(secret_distribution, alpha=alpha): + # Equation (7) + n_ = n - t1 * b + C0 = (m - n_) * (n + 1) * ceil(n_ / (b - 1)) + assert C0 >= 0 + # cost["C0(gauss)"] = RR(C0) + else: + C0 = 0 + + # Equation ( 8) + C1 = sum([(n + 1 - i * b) * (m - i * (q ** b - 1) / 2) for i in range(1, t1 + 1)]) + assert C1 >= 0 + # cost["C1(bkw)"] = RR(C1) + + # Equation (9) + C2_ = sum([4 * (M + i * (q ** b - 1) / 2) * N(i, sigma_set) for i in range(1, t2 + 1)]) + C2 = RR(C2_) + for i in range(1, t2 + 1): + C2 += RR(ntop + ntest + sum([N(j, sigma_set) for j in range(1, i + 1)])) * (M + (i - 1) * (q ** b - 1) / 2) + assert C2 >= 0 + # cost["C2(coded)"] = RR(C2) + + # Equation (10) + C3 = M * ntop * (2 * d + 1) ** ntop + assert C3 >= 0 + # cost["C3(guess)"] = RR(C3) + + # Equation (11) + C4_ = 4 * M * ntest + C4 = C4_ + (2 * d + 1) ** ntop * (cfft * q ** (l + 1) * (l + 1) * log(q, 2) + q ** (l + 1)) + assert C4 >= 0 + # cost["C4(test)"] = RR(C4) + + C = (C0 + C1 + C2 + C3 + C4) / (erf(d / sqrt(2 * sigma)) ** ntop) # TODO don't ignore success probability + cost["rop"] = RR(C) + cost["mem"] = (t1 + t2) * q ** b + + cost = cost.reorder(["rop", "m", "mem", "b", "t1", "t2"]) + + return cost + + +def bkw_coded(n, alpha, q, secret_distribution=True, m=oo, success_probability=0.99, ntest=None): + """ + Coded-BKW as described in [C:GuoJohSta15] + + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param success_probability: targeted success probability < 1 + :param samples: the number of available samples + + EXAMPLE:: + + sage: from estimator import Param, bkw_coded + sage: n, alpha, q = Param.Regev(64) + sage: bkw_coded(n, alpha, q) + rop: 2^48.8 + m: 2^39.6 + mem: 2^39.6 + b: 3 + t1: 2 + t2: 10 + l: 2 + ncod: 53 + ntop: 0 + ntest: 6 + + .. [GuoJohSta15] Guo, Q., Johansson, T., & Stankovski, P. (2015). Coded-BKW: solving LWE using lattice + codes. In R. Gennaro, & M. J. B. Robshaw, CRYPTO~2015, Part~I (pp. 23–42). : + Springer, Heidelberg. + + """ + bstart = ceil(log(q, 2)) + + def _run(b=2): + # the noise is 2**(t1+t2) * something so there is no need to go beyond, say, q**2 + r = binary_search( + _bkw_coded, + 2, + min(n // b, ceil(2 * log(q, 2))), + "t2", + predicate=lambda x, best: x["rop"] <= best["rop"] and (best["m"] > m or x["m"] <= m), + b=b, + n=n, + alpha=alpha, + q=q, + secret_distribution=secret_distribution, + success_probability=success_probability, + m=m, + ntest=ntest, + ) + return r + + best = binary_search( + _run, 2, 3 * bstart, "b", predicate=lambda x, best: x["rop"] <= best["rop"] and (best["m"] > m or x["m"] <= m) + ) + # binary search cannot fail. It just outputs some X with X["oracle"]>m. + if best["m"] > m: + raise InsufficientSamplesError("m=%d < %d (required)" % (m, best["m"])) + return best + + +# Algebraic + + +@cached_function +def have_magma(): + try: + magma(1) + return True + except TypeError: + return False + + +def gb_cost(n, D, omega=2, prec=None): + """ + Estimate the complexity of computing a Gröbner basis. + + :param n: number of variables `n > 0` + :param D: tuple of `(d,m)` pairs where `m` is number polynomials and `d` is a degree + :param omega: linear algebra exponent, i.e. matrix-multiplication costs `O(n^ω)` operations. + :param prec: compute power series up to this precision (default: `2n`) + + """ + prec = 2*n if prec is None else prec + + if have_magma(): + R = magma.PowerSeriesRing(QQ, prec) + z = R.gen(1) + coeff = lambda f, d: f.Coefficient(d) # noqa + s = 1 + else: + R = PowerSeriesRing(QQ, "z", prec) + z = R.gen() + z = z.add_bigoh(prec) + coeff = lambda f, d: f[d] # noqa + s = R(1) + s = s.add_bigoh(prec) + + for d, m in D: + s *= (1 - z ** d) ** m + s /= (1 - z) ** n + + retval = Cost([("rop", oo), ("Dreg", oo)]) + + for dreg in range(prec): + if coeff(s, dreg) < 0: + break + else: + return retval + + retval["Dreg"] = dreg + retval["rop"] = binomial(n + dreg, dreg) ** omega + retval["mem"] = binomial(n + dreg, dreg) ** 2 + + return retval + + +def arora_gb(n, alpha, q, secret_distribution=True, m=oo, success_probability=0.99, omega=2): + """ + Arora-GB as described in [AroGe11,ACFP14]_ + + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param secret_distribution: distribution of secret, see module level documentation for details + :param m: number of LWE samples `m > 0` + :param success_probability: targeted success probability < 1 + :param omega: linear algebra constant + + .. [ACFP14] Albrecht, M. R., Cid, C., Jean-Charles Faugère, & Perret, L. (2014). + Algebraic algorithms for LWE. + + .. [AroGe11] Arora, S., & Ge, R. (2011). New algorithms for learning in presence of + errors. In L. Aceto, M. Henzinger, & J. Sgall, ICALP 2011, Part~I (pp. 403–415). : + Springer, Heidelberg. + """ + + n, alpha, q, success_probability = Param.preprocess(n, alpha, q, success_probability, prec=2 * log(n, 2) * n) + + RR = alpha.parent() + stddev = RR(stddevf(RR(alpha) * q)) + + if stddev >= 1.1 * sqrt(n): + return {"rop": oo} + + if SDis.is_small(secret_distribution, alpha=alpha): + try: + a, b = SDis.bounds(secret_distribution) + d2 = b - a + 1 + except ValueError: + d2 = 2 * ceil(3 * stddev) + 1 + d2 = [(d2, n)] + else: + d2 = [] + + ps_single = lambda C: RR(1 - (2 / (C * RR(sqrt(2 * pi))) * exp(-(C ** 2) / 2))) # noqa + + m_req = floor(exp(RR(0.75) * n)) + d = n + t = ZZ(floor((d - 1) / 2)) + C = t / stddev + pred = gb_cost(n, [(d, m_req)] + d2, omega) + pred["t"] = t + pred["m"] = m_req + pred = pred.reorder(["rop", "m", "Dreg", "t"]) + + t = ceil(t / 3) + best = None + stuck = 0 + for t in srange(t, n): + d = 2 * t + 1 + C = RR(t / stddev) + if C < 1: # if C is too small, we ignore it + continue + # Pr[success]^m = Pr[overall success] + m_req = log(success_probability, 2) / log(ps_single(C), 2) + if m_req < n: + continue + m_req = floor(m_req) + + current = gb_cost(n, [(d, m_req)] + d2, omega) + + if current["Dreg"] is None: + continue + + current["t"] = t + current["m"] = m_req + + current = current.reorder(["rop", "m", "Dreg", "t"]) + + logging.getLogger("repeat").debug(current) + + if best is None: + best = current + else: + if best["rop"] > current["rop"] and current["m"] <= m: + best = current + stuck = 0 + else: + stuck += 1 + if stuck >= 5: + break + return best + + +# Toplevel function + + +def estimate_lwe( # noqa + n, + alpha=None, + q=None, + secret_distribution=True, + m=oo, + reduction_cost_model=reduction_default_cost, + skip=("mitm", "arora-gb", "bkw"), +): + """ + Highlevel-function for estimating security of LWE parameter sets + + :param n: LWE dimension `n > 0` + :param alpha: noise rate `0 ≤ α < 1`, noise will have standard deviation `αq/\\sqrt{2π}` + :param q: modulus `0 < q` + :param m: number of LWE samples `m > 0` + :param secret_distribution: distribution of secret, see module level documentation for details + :param reduction_cost_model: use this cost model for lattice reduction + :param skip: skip these algorithms + + EXAMPLE:: + + sage: from estimator import estimate_lwe, Param, BKZ, alphaf + sage: d = estimate_lwe(*Param.Regev(128)) + usvp: rop: ≈2^57.3, red: ≈2^57.3, δ_0: 1.009214, β: 101, d: 349, m: 220 + dec: rop: ≈2^61.9, m: 229, red: ≈2^61.9, δ_0: 1.009595, β: 93, d: 357, ... + dual: rop: ≈2^81.1, m: 380, red: ≈2^81.1, δ_0: 1.008631, β: 115, d: 380, ... + + sage: d = estimate_lwe(**Param.LindnerPeikert(256, dict=True)) + usvp: rop: ≈2^127.8, red: ≈2^127.8, δ_0: 1.005788, β: 228, d: 588, m: 331 + dec: rop: ≈2^137.3, m: 337, red: ≈2^137.3, δ_0: 1.005976, β: 217, d: 593, ... + dual: rop: ≈2^166.0, m: 368, red: ≈2^166.0, δ_0: 1.005479, β: 249, repeat: ... + + sage: d = estimate_lwe(*Param.LindnerPeikert(256), secret_distribution=(-1,1)) + Warning: the LWE secret is assumed to have Hamming weight 171. + usvp: rop: ≈2^98.0, red: ≈2^98.0, δ_0: 1.006744, β: 178, d: 499, m: 242, ... + dec: rop: ≈2^137.3, m: 337, red: ≈2^137.3, δ_0: 1.005976, β: 217, d: 593, ... + dual: rop: ≈2^108.4, m: 270, red: ≈2^108.3, δ_0: 1.006390, β: 195, repeat: ... + + sage: d = estimate_lwe(*Param.LindnerPeikert(256), secret_distribution=(-1,1), reduction_cost_model=BKZ.sieve) + Warning: the LWE secret is assumed to have Hamming weight 171. + usvp: rop: ≈2^80.3, red: ≈2^80.3, δ_0: 1.006744, β: 178, d: 499, m: 242, ... + dec: rop: ≈2^111.8, m: 369, red: ≈2^111.8, δ_0: 1.005423, β: 253, d: 625, ... + dual: rop: ≈2^90.6, m: 284, red: ≈2^90.6, δ_0: 1.006065, β: 212, ... + + sage: d = estimate_lwe(n=100, alpha=8/2^20, q=2^20, skip="arora-gb") + mitm: rop: ≈2^329.2, m: 23, mem: ≈2^321.5 + usvp: rop: ≈2^32.4, red: ≈2^32.4, δ_0: 1.013310, β: 40, d: 141, m: 40 + dec: rop: ≈2^34.0, m: 156, red: ≈2^34.0, δ_0: 1.021398, β: 40, d: 256, ... + dual: rop: ≈2^35.5, m: 311, red: ≈2^35.5, δ_0: 1.014423, β: 40, d: 311, ... + bkw: rop: ≈2^53.6, m: ≈2^43.5, mem: ≈2^44.5, b: 2, t1: 5, t2: 18, l: 1, ... + + sage: d = estimate_lwe(n=100, secret_distribution=alphaf(1, 2^20, True), alpha=8/2^20, q=2^20) + usvp: rop: ≈2^32.2, red: ≈2^32.2, δ_0: 1.013310, β: 40, d: 127, m: 26 + dec: rop: ≈2^34.0, m: 156, red: ≈2^34.0, δ_0: 1.021398, β: 40, d: ... + dual: rop: ≈2^35.5, m: 311, red: ≈2^35.5, δ_0: 1.014423, β: 40, d: ... + + + """ + + algorithms = OrderedDict() + + if skip is None: + skip = () + try: + skip = [s.strip().lower() for s in skip.split(",")] + except AttributeError: + pass + skip = [s.strip().lower() for s in skip] + + if "mitm" not in skip: + algorithms["mitm"] = mitm + + if "usvp" not in skip: + if SDis.is_bounded_uniform(secret_distribution): + algorithms["usvp"] = partial( + drop_and_solve, + primal_usvp, + reduction_cost_model=reduction_cost_model, + postprocess=False, + decision=False, + ) + else: + algorithms["usvp"] = partial(primal_usvp, reduction_cost_model=reduction_cost_model) + + if "dec" not in skip: + if SDis.is_sparse(secret_distribution) and SDis.is_ternary(secret_distribution): + algorithms["dec"] = partial( + drop_and_solve, + primal_decode, + reduction_cost_model=reduction_cost_model, + postprocess=False, + decision=False, + ) + else: + algorithms["dec"] = partial(primal_decode, reduction_cost_model=reduction_cost_model) + + if "dual" not in skip: + if SDis.is_bounded_uniform(secret_distribution): + algorithms["dual"] = partial( + drop_and_solve, dual_scale, reduction_cost_model=reduction_cost_model, postprocess=True + ) + elif SDis.is_small(secret_distribution, alpha=alpha): + algorithms["dual"] = partial(dual_scale, reduction_cost_model=reduction_cost_model) + else: + algorithms["dual"] = partial(dual, reduction_cost_model=reduction_cost_model) + + if "bkw" not in skip: + algorithms["bkw"] = bkw_coded + + if "arora-gb" not in skip: + if SDis.is_sparse(secret_distribution) and SDis.is_small(secret_distribution, alpha=alpha): + algorithms["arora-gb"] = partial(drop_and_solve, arora_gb) + elif SDis.is_small(secret_distribution, alpha=alpha): + algorithms["arora-gb"] = partial(switch_modulus, arora_gb) + else: + algorithms["arora-gb"] = arora_gb + + alg_width = max(len(key) for key in set(algorithms).difference(skip)) + cost_kwds = {"compact": False} + + logger = logging.getLogger("estimator") + + if SDis.is_bounded_uniform(secret_distribution) and not SDis.is_sparse(secret_distribution): + h = SDis.nonzero(secret_distribution, n) + logger.info("Warning: the LWE secret is assumed to have Hamming weight %s." % h) + + results = OrderedDict() + for alg in algorithms: + algf = algorithms[alg] + try: + tmp = algf(n, alpha, q, secret_distribution=secret_distribution, m=m) + results[alg] = tmp + logger.info("%s: %s" % (("%%%ds" % alg_width) % alg, results[alg].str(**cost_kwds))) + except InsufficientSamplesError as e: + logger.info( + "%s: %s" % (("%%%ds" % alg_width) % alg, "insufficient samples to run this algorithm: '%s'" % str(e)) + ) + + return results diff --git a/fix-doctest.sh b/fix-doctest.sh new file mode 100755 index 000000000..7e17f7a32 --- /dev/null +++ b/fix-doctest.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +############################################################################### +# Fix-up Doctests +# +# Please don't just blindly call this to make failures go away, +# but review all changes. +############################################################################### +SAGE_ROOT=$(sage -c "import os; print(os.environ['SAGE_ROOT'])") +export SAGE_ROOT="$SAGE_ROOT" + +# shellcheck source=/dev/null +source "$SAGE_ROOT/local/bin/sage-env" +PYTHONIOENCODING=UTF-8 PYTHONPATH=$(pwd) sage-fixdoctests "$@" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..55ec8d784 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 120 diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 000000000..cad993e27 --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1 @@ +DOCKER_IMAGE: sagemath/sagemath diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..e4ce18027 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flake8 +sphinx==1.4.4