diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/__init__.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/__init__.py index dabd1e8a..ce418dc1 100644 --- a/cryptographic_estimators/SDEstimator/SDAlgorithms/__init__.py +++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/__init__.py @@ -1,5 +1,8 @@ from .ball_collision import BallCollision +from .bjmm_dw import BJMMdw +from .bjmm_pdw import BJMMpdw from .bjmm import BJMM, BJMMd2, BJMMd3 +from .bjmm_plus import BJMMplus from .both_may import BothMay from .dumer import Dumer from .may_ozerov import MayOzerov, MayOzerovD2, MayOzerovD3 diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/ball_collision.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/ball_collision.py index 62032836..f205e8db 100644 --- a/cryptographic_estimators/SDEstimator/SDAlgorithms/ball_collision.py +++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/ball_collision.py @@ -22,7 +22,7 @@ from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, _list_merge_complexity, binom, log2, inf, min_max from types import SimpleNamespace from ..sd_constants import * -#from ..SDWorkfactorModels.ball_collision import BallCollisionScipyModel +from ..SDWorkfactorModels.ball_collision import BallCollisionScipyModel class BallCollision(SDAlgorithm): @@ -61,7 +61,7 @@ def __init__(self, problem: SDProblem, **kwargs): self.set_parameter_ranges("pl", 0, min_max(10, w, s)) self.set_parameter_ranges("r", 0, n - k) - # self.scipy_model = BallCollisionScipyModel + self.scipy_model = BallCollisionScipyModel @optimal_parameter def l(self): diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm.py index 32a0c1b5..05c55d04 100644 --- a/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm.py +++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm.py @@ -24,7 +24,7 @@ binom, log2, ceil, inf from types import SimpleNamespace from ..sd_constants import * -#from ..SDWorkfactorModels.bjmm import BJMMScipyModel +from ..SDWorkfactorModels.bjmm import BJMMScipyModel from typing import Union @@ -388,7 +388,7 @@ def __init__(self, problem: SDProblem, **kwargs): super(BJMMd3, self).__init__(problem, **kwargs) self._name = "BJMMd3" self.initialize_parameter_ranges() - #self.scipy_model = BJMMScipyModel + self.scipy_model = BJMMScipyModel def initialize_parameter_ranges(self): """ diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_dw.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_dw.py new file mode 100644 index 00000000..52884011 --- /dev/null +++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_dw.py @@ -0,0 +1,306 @@ +# **************************************************************************** +# Copyright 2023 Technology Innovation Institute +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# **************************************************************************** + + +from ...base_algorithm import optimal_parameter +from ...SDEstimator.sd_algorithm import SDAlgorithm +from ...SDEstimator.sd_problem import SDProblem +from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, _mitm_nn_complexity, binom, log2, \ + ceil, inf +from scipy.special import binom as binom_sp +from scipy.optimize import fsolve +from warnings import filterwarnings +from types import SimpleNamespace +from ..sd_constants import * +filterwarnings("ignore", category=RuntimeWarning) + + +class BJMMdw(SDAlgorithm): + def __init__(self, problem: SDProblem, **kwargs): + """ + Construct an instance of BJMM's estimator using *d*isjoint *w*eight distributions combined with + MitM-nearest neighbor search. [EB22]_, [MMT11]_, [BJMM12]_. + + Expected weight distribution:: + + +---------------------------+-------------+------------+----------+----------+----------+----------+ + |<-+ n - k - 2 l1 - 2 l2 +->|<-+ k / 2 +->|<-+ k / 2 ->|<-+ l1 +->|<-+ l1 +->|<-+ l2 +->|<-+ l2 +->| + | w - 2 p - 2 w1 - 2 w2 | p | p | w1 | w1 | w2 | w2 | + +---------------------------+-------------+------------+----------+----------+----------+----------+ + + + INPUT: + + - ``problem`` -- syndrome decoding problem instance + - ``k`` -- dimension of the code + - ``w`` -- Hamming weight of error vector + - ``mem`` -- upper bound on the available memory (as log2), default unlimited + - ``hmap`` -- indicates if hashmap is being used (default: true) + - ``p_range`` -- interval in which the parameter p is searched (default: [0, 25], helps speeding up computation) + - ``memory_access`` -- specifies the memory access cost model (default: 0, choices: 0 - constant, 1 - logarithmic, 2 - square-root, 3 - cube-root or deploy custom function which takes as input the logarithm of the total memory usage) + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: BJMMdw(SDProblem(n=100,k=50,w=10)) + BJMM estimator with disjoint weight distributions in depth 2 for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2 + """ + super(BJMMdw, self).__init__(problem, **kwargs) + self._name = "BJMM-dw" + self.initialize_parameter_ranges() + + def initialize_parameter_ranges(self): + """ + initialize the parameter ranges for p, p1, w1, w11, w2 to start the optimisation + process. + """ + self.set_parameter_ranges("p", 0, 25) + self.set_parameter_ranges("p1", 0, 20) + self.set_parameter_ranges("w1", 0, 10) + self.set_parameter_ranges("w11", 0, 10) + self.set_parameter_ranges("w2", 0, 5) + + @optimal_parameter + def p(self): + """ + Return the optimal parameter $p$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMdw(SDProblem(n=100,k=50,w=10)) + sage: A.p() + 2 + """ + return self._get_optimal_parameter("p") + + @optimal_parameter + def p1(self): + """ + Return the optimal parameter $p1$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMdw(SDProblem(n=100,k=50,w=10)) + sage: A.p1() + 1 + """ + return self._get_optimal_parameter("p1") + + @optimal_parameter + def w1(self): + """ + Return the optimal parameter $w1$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMdw(SDProblem(n=100,k=50,w=10)) + sage: A.w1() + 0 + """ + return self._get_optimal_parameter("w1") + + @optimal_parameter + def w11(self): + """ + Return the optimal parameter $w11$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMdw(SDProblem(n=100,k=50,w=10)) + sage: A.w11() + 0 + """ + return self._get_optimal_parameter("w11") + + @optimal_parameter + def w2(self): + """ + Return the optimal parameter $w2$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMdw(SDProblem(n=100,k=50,w=10)) + sage: A.w2() + 0 + """ + return self._get_optimal_parameter("w2") + + def _are_parameters_invalid(self, parameters): + _, k, w = self.problem.get_parameters() + par = SimpleNamespace(**parameters) + + if par.p % 2 == 1 or par.p > w // 2 or k < par.p or \ + par.p1 < par.p // 2 or par.p1 > w or \ + par.w1 > w // 2 - par.p or par.w1 % 2 == 1 or \ + par.w11 < par.w1 // 2 or par.w11 >= w or \ + par.w2 > w // 2 - par.p - par.w1: + return True + return False + + def _valid_choices(self): + """ + Generator which yields on each call a new set of valid parameters based on the `_parameter_ranges` and already + set parameters in `_optimal_parameters` + + """ + new_ranges = self._fix_ranges_for_already_set_parameters() + n, k, w = self.problem.get_parameters() + for p in range(new_ranges["p"]["min"], min(w // 2, new_ranges["p"]["max"]) + 1, 2): + for p1 in range(max(new_ranges["p1"]["min"], (p + 1) // 2), new_ranges["p1"]["max"] + 1): + s = new_ranges["w1"]["min"] + for w1 in range(s - (s % 2), min(w // 2 - p, new_ranges["w1"]["max"]) + 1, 2): + for w11 in range(max(new_ranges["w11"]["min"], (w1 + 1) // 2), new_ranges["w11"]["max"] + 1, 2): + for w2 in range(new_ranges["w2"]["min"], min(w // 2 - p - w1, new_ranges["w2"]["max"]) + 1): + indices = {"p": p, "p1": p1, "w1": w1, "w11": w11, + "w2": w2, "r": self._optimal_parameters["r"]} + if self._are_parameters_invalid(indices): + continue + yield indices + + def _choose_first_constraint_such_that_representations_cancel_out_exactly(self, parameters: dict): + """ + tries to find a l1 value fulfilling the constraints + """ + _, k, _ = self.problem.get_parameters() + par = SimpleNamespace(**parameters) + + try: + def f(x): return 2 * log2((binom(par.p, par.p // 2) * binom(k // 2 - par.p, par.p1 - par.p // 2)) * ( + binom_sp(x, par.w1 // 2) * binom_sp(x - par.w1, par.w11 - par.w1 // 2)) + 1) - 2 * x + l1_val = int( + fsolve(f, 2 * log2((binom(par.p, par.p // 2) * binom(k // 2 - par.p, par.p1 - par.p // 2))))[0]) + except ValueError: + return -1 + + if f(l1_val) < 0 or f(l1_val) > 10: + return -1 + return l1_val + + def _choose_second_constraint_such_that_list_size_remains_constant(self, parameters: dict, list_size: float): + """ + trues to find a l2 value which does not increase the list size + """ + par = SimpleNamespace(**parameters) + + try: + def f(x): return log2(list_size) + 2 * \ + log2(binom_sp(x, par.w2) + 1) - 2 * x + l2_val = int(fsolve(f, 50)[0]) + except ValueError: + return -1 + + if f(l2_val) < 0 or f(l2_val) > 10: + return -1 + + return l2_val + + def _time_and_memory_complexity(self, parameters: dict, verbose_information=None): + """ + Computes the expected runtime and memory consumption for a given parameter set. + """ + + n, k, w = self.problem.get_parameters() + par = SimpleNamespace(**parameters) + + local_time, local_mem = inf, inf + solutions = self.problem.nsolutions + memory_bound = self.problem.memory_bound + l1_search_radius = self._adjust_radius + l2_search_radius = max(1, self._adjust_radius // 2) + + l1_start_value = self._choose_first_constraint_such_that_representations_cancel_out_exactly( + parameters) + if l1_start_value == -1: + return inf, inf + + for l1 in range(max(l1_start_value - l1_search_radius, par.w1, par.w11), l1_start_value + l1_search_radius): + if 2*l1 >= n-k or n-k-2*l1 < w: + continue + + k1 = k // 2 + reps = (binom(par.p, par.p // 2) * binom(k1 - par.p, par.p1 - par.p // 2)) ** 2 * ( + binom(par.w1, par.w1 // 2) * binom(l1 - par.w1, par.w11 - par.w1 // 2)) ** 2 + reps = max(reps, 1) + + L1 = binom(k1, par.p1) + if self._is_early_abort_possible(log2(L1)): + return inf, inf + L12 = L1 ** 2 * binom(l1, par.w11) ** 2 // 2 ** (2 * l1) + L12 = max(L12, 1) + memory = log2((2 * L1 + L12) + _mem_matrix(n, k, par.r)) + if memory > memory_bound: + continue + + l2_start_value = self._choose_second_constraint_such_that_list_size_remains_constant( + parameters, L12) + if l2_start_value == -1: + continue + + l2_max = (n - k - 2 * l1 - (w - 2 * par.p - + 2 * par.w1 - 2 * par.w2)) // 2 + l2_min = par.w2 + l2_range = [l2_start_value - l2_search_radius, + l2_start_value + l2_search_radius] + for l2 in range(max(l2_min, l2_range[0]), max(1, min(l2_max, l2_range[1]))): + Tp = max( + log2(binom(n, w)) - log2( + binom(n - k - 2 * l1 - 2 * l2, w - 2 * par.p - 2 * par.w1 - 2 * par.w2)) - 2 * log2( + binom(k1, par.p)) - 2 * log2(binom(l1, par.w1)) - 2 * log2( + binom(l2, par.w2)) - solutions, 0) + Tg = _gaussian_elimination_complexity(n, k, par.r) + + T_tree = 2 * _mitm_nn_complexity(L1, 2 * l1, 2 * par.w11, self._hmap) \ + + _mitm_nn_complexity(L12, 2 * l2, 2 * par.w2, self._hmap) + T_rep = int(ceil(2 ** max(2 * l1 - log2(reps), 0))) + + time = Tp + log2(Tg + T_rep * T_tree) + + if time < local_time: + local_time = time + local_mem = memory + if verbose_information is not None: + verbose_information[VerboseInformation.CONSTRAINTS.value] = [ + 2 * l1, 2 * l2] + verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp + verbose_information[VerboseInformation.TREE.value] = log2( + T_rep * T_tree) + verbose_information[VerboseInformation.GAUSS.value] = log2( + Tg) + verbose_information[VerboseInformation.REPRESENTATIONS.value] = reps + verbose_information[VerboseInformation.LISTS.value] = [log2(L1), log2(L12), + 2 * log2(L12) + log2( + binom(2 * l2, 2 * par.w2)) - 2 * l2] + + return local_time, local_mem + + def __repr__(self): + """ + """ + rep = "BJMM estimator with disjoint weight distributions in depth 2 for " + \ + str(self.problem) + return rep diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_pdw.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_pdw.py new file mode 100644 index 00000000..488d8cb5 --- /dev/null +++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_pdw.py @@ -0,0 +1,271 @@ +# **************************************************************************** +# Copyright 2023 Technology Innovation Institute +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# **************************************************************************** + + +from ...base_algorithm import optimal_parameter +from ...SDEstimator.sd_algorithm import SDAlgorithm +from ...SDEstimator.sd_problem import SDProblem +from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, _list_merge_complexity, \ + _mitm_nn_complexity, binom, log2, ceil, inf, min_max +from scipy.special import binom as binom_sp +from scipy.optimize import fsolve +from warnings import filterwarnings +from types import SimpleNamespace +from ..sd_constants import * + +filterwarnings("ignore", category=RuntimeWarning) + + +class BJMMpdw(SDAlgorithm): + def __init__(self, problem: SDProblem, **kwargs): + """ + Construct an instance of BJMM's estimator in depth 2 using partially disjoint + weight, applying explicit MitM-NN search on second level [MMT11]_, + [BJMM12]_, [EB22]_. + + Expected weight distribution:: + + +--------------------------+--------------------+--------------------+--------+--------+ + | <-+ n - k - l1 - 2 l2 +->|<-+ (k + l1) / 2 +->|<-+ (k + l1) / 2 +->| l2 | l2 | + | w - 2 p - 2 w2 | p | p | w2 | w2 | + +--------------------------+--------------------+--------------------+--------+--------+ + + + INPUT: + + - ``problem`` -- SDProblem object including all necessary parameters + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMpdw + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: BJMMpdw(SDProblem(n=100,k=50,w=10)) + BJMM estimator with partially disjoint weight distributions in depth 2 for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2 + + """ + super(BJMMpdw, self).__init__(problem, **kwargs) + self._name = "BJMM-pdw" + self.initialize_parameter_ranges() + + def initialize_parameter_ranges(self): + """ + initialize the parameter ranges for p, p1, w2 to start the optimisation + process. + """ + _, _, w = self.problem.get_parameters() + s = self.full_domain + self.set_parameter_ranges("p", 0, min_max(30, w, s)) + self.set_parameter_ranges("p1", 0, min_max(25, w, s)) + self.set_parameter_ranges("w2", 0, min_max(5, w, s)) + + @optimal_parameter + def p(self): + """ + Return the optimal parameter $p$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMpdw + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMpdw(SDProblem(n=100,k=50,w=10)) + sage: A.p() + 2 + """ + return self._get_optimal_parameter("p") + + @optimal_parameter + def p1(self): + """ + Return the optimal parameter $p1$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMpdw + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMpdw(SDProblem(n=100,k=50,w=10)) + sage: A.p1() + 1 + """ + return self._get_optimal_parameter("p1") + + @optimal_parameter + def w2(self): + """ + Return the optimal parameter $w2$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMdw + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMdw(SDProblem(n=100,k=50,w=10)) + sage: A.w2() + 0 + """ + return self._get_optimal_parameter("w2") + + def _are_parameters_invalid(self, parameters: dict): + """ + return if the parameter set `parameters` is invalid + + """ + _, k, w = self.problem.get_parameters() + par = SimpleNamespace(**parameters) + + if par.p % 2 == 1 or par.p > w // 2 or k < par.p or \ + par.p1 < par.p // 2 or par.p1 > w or \ + par.w2 > w // 2 - par.p: + return True + return False + + def _valid_choices(self): + """ + Generator which yields on each call a new set of valid parameters based on the `_parameter_ranges` and already + set parameters in `_optimal_parameters` + + """ + new_ranges = self._fix_ranges_for_already_set_parameters() + _, _, w = self.problem.get_parameters() + + for p in range(new_ranges["p"]["min"], min(w // 2, new_ranges["p"]["max"]) + 1, 2): + for p1 in range(max(new_ranges["p1"]["min"], (p + 1) // 2), min(w, new_ranges["p1"]["max"]) + 1): + for w2 in range(new_ranges["w2"]["min"], min(w - p1, new_ranges["w2"]["max"]) + 1): + indices = {"p": p, "p1": p1, "w2": w2, + "r": self._optimal_parameters["r"]} + if self._are_parameters_invalid(indices): + continue + yield indices + + def _choose_first_constraint_such_that_representations_cancel_out_exactly(self, parameters: dict): + """ + tries to find an optimal l1 value fulfilling its contraints + """ + _, k, _ = self.problem.get_parameters() + par = SimpleNamespace(**parameters) + + try: + def f(x): return log2((binom(par.p, par.p // 2) * + binom_sp((k + x) / 2 - par.p, par.p1 - par.p // 2))) * 2 - x + l1_val = int(fsolve(f, 0)[0]) + + if f(l1_val) < 0 or f(l1_val) > 1: + return -1 + except ValueError: + return -1 + + return l1_val + + def _choose_second_constraint_such_that_list_size_remains_constant(self, parameters: dict, list_size: float): + """ + tries to find an optimal l2 value fulfilling its contraints + """ + par = SimpleNamespace(**parameters) + + try: + def f(x): return log2(list_size) + 2 * \ + log2(binom_sp(x, par.w2)) - 2 * x + l2_val = int(fsolve(f, 0)[0]) + if f(l2_val) < 0 or f(l2_val) > 1: + return -1 + except ValueError: + return -1 + + return l2_val + + def _time_and_memory_complexity(self, parameters: dict, verbose_information=None): + """ + Computes the expected runtime and memory consumption for a given parameter set. + """ + n, k, w = self.problem.get_parameters() + par = SimpleNamespace(**parameters) + if len(parameters.keys()) == 1: + return inf, inf + + local_time, local_mem = inf, inf + solutions = self.problem.nsolutions + memory_bound = self.problem.memory_bound + l1_search_radius = max(1, self._adjust_radius // 2) + l2_search_radius = max(1, self._adjust_radius // 2) + + l1_start_value = self._choose_first_constraint_such_that_representations_cancel_out_exactly( + parameters) + if l1_start_value == -1: + return inf, inf + + for l1 in range(max(0, l1_start_value - l1_search_radius), l1_start_value + l1_search_radius): + if 2*l1 >= n-k or n-k-2*l1 < w: + continue + + k1 = (k + l1) // 2 + + if k1 - par.p < 0 or k1 - par.p < par.p1 - par.p // 2: + continue + reps = (binom(par.p, par.p // 2) * + binom(k1 - par.p, par.p1 - par.p // 2)) ** 2 + + L1 = binom(k1, par.p1) + if self._is_early_abort_possible(log2(L1)): + return inf, inf + + L12 = max(L1 ** 2 // 2 ** l1, 1) + + memory = log2((2 * L1 + L12) + _mem_matrix(n, k, par.r)) + if memory > memory_bound: + continue + + l2_start_value = self._choose_second_constraint_such_that_list_size_remains_constant( + parameters, L12) + if l2_start_value == -1: + continue + + l2_min, l2_max = par.w2, (n - k - l1 - + (w - 2 * par.p - 2 * par.w2)) // 2 + l2_range = [l2_start_value - l2_search_radius, + l2_start_value + l2_search_radius] + for l2 in range(max(l2_min, l2_range[0]), min(l2_max, l2_range[1])): + Tp = max( + log2(binom(n, w)) - log2(binom(n - k - l1 - 2 * l2, w - 2 * par.p - 2 * par.w2)) - 2 * log2( + binom(k1, par.p)) - 2 * log2(binom(l2, par.w2)) - solutions, 0) + Tg = _gaussian_elimination_complexity(n, k, par.r) + + T_tree = 2 * _list_merge_complexity(L1, l1, self._hmap) + _mitm_nn_complexity(L12, 2 * l2, 2 * par.w2, + self._hmap) + T_rep = int(ceil(2 ** max(l1 - log2(reps), 0))) + + time = Tp + log2(Tg + T_rep * T_tree) + if time < local_time: + local_time = time + local_mem = memory + if verbose_information is not None: + verbose_information[VerboseInformation.CONSTRAINTS.value] = [ + l1, 2 * l2] + verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp + verbose_information[VerboseInformation.TREE.value] = log2( + T_rep * T_tree) + verbose_information[VerboseInformation.GAUSS.value] = log2( + Tg) + verbose_information[VerboseInformation.REPRESENTATIONS.value] = reps + verbose_information[VerboseInformation.LISTS.value] = [ + log2(L1), log2(L12), 2 * log2(L12)] + + return local_time, local_mem + + def __repr__(self): + """ + """ + rep = "BJMM estimator with partially disjoint weight distributions in depth 2 for " + \ + str(self.problem) + return rep diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_plus.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_plus.py new file mode 100644 index 00000000..28c27cb6 --- /dev/null +++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/bjmm_plus.py @@ -0,0 +1,248 @@ +# **************************************************************************** +# Copyright 2023 Technology Innovation Institute +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# **************************************************************************** + + +from ...base_algorithm import optimal_parameter +from ...SDEstimator.sd_algorithm import SDAlgorithm +from ...SDEstimator.sd_problem import SDProblem +from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, _list_merge_complexity, min_max, \ + binom, log2, ceil, inf, _list_merge_async_complexity +from types import SimpleNamespace +from ..sd_constants import * + + +class BJMMplus(SDAlgorithm): + def __init__(self, problem: SDProblem, **kwargs): + """ + Complexity estimate of BJMM+ algorithm in depth 2 + + This class incorporates the improvements by [EZ23]_, regarding a time-memory tradeoff which improves over the + BJMM algorithm in terms of memory usages. + + For further reference see [MMT11]_ and [BJMM12]_. + + expected weight distribution:: + + +--------------------------+-------------------+-------------------+ + | <-----+ n - k - l +----->|<--+ (k + l)/2 +-->|<--+ (k + l)/2 +-->| + | w - 2p | p | p | + +--------------------------+-------------------+-------------------+ + + INPUT: + + - ``problem`` -- SDProblem object including all necessary parameters + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMplus + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: BJMMplus(SDProblem(n=100,k=50,w=10)) + BJMM+ estimator for syndrome decoding problem with (n,k,w) = (100,50,10) over Finite Field of size 2 + + """ + + super(BJMMplus, self).__init__(problem, **kwargs) + self._name = "BJMM" + self.initialize_parameter_ranges() + self.limit_depth = kwargs.get("limit_depth", False) + self.qc = False + + + def initialize_parameter_ranges(self): + """ + initialize the parameter ranges for p, p1, l to start the optimisation + process. + """ + n, k, w = self.problem.get_parameters() + s = self.full_domain + self.set_parameter_ranges("p", 0, min_max(35, w, s)) + self.set_parameter_ranges("p1", 0, min_max(35, w, s)) + self.set_parameter_ranges("l", 0, min_max(500, n - k, s)) + self.set_parameter_ranges("l1", 0, min_max(200, n - k, s)) + + @optimal_parameter + def l(self): + """ + Return the optimal parameter $l$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMplus + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMplus(SDProblem(n=100,k=50,w=10)) + sage: A.l() + 8 + + """ + return self._get_optimal_parameter("l") + + @optimal_parameter + def l1(self): + """ + Return the optimal parameter $l$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMplus + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMplus(SDProblem(n=100,k=50,w=10)) + sage: A.l1() + 2 + + """ + return self._get_optimal_parameter("l1") + + @optimal_parameter + def p(self): + """ + Return the optimal parameter $p$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMplus + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMplus(SDProblem(n=100,k=50,w=10)) + sage: A.p() + 2 + """ + return self._get_optimal_parameter("p") + + @optimal_parameter + def p1(self): + """ + Return the optimal parameter $p1$ used in the algorithm optimization + + EXAMPLES:: + + sage: from cryptographic_estimators.SDEstimator.SDAlgorithms import BJMMplus + sage: from cryptographic_estimators.SDEstimator import SDProblem + sage: A = BJMMplus(SDProblem(n=100,k=50,w=10)) + sage: A.p1() + 1 + """ + return self._get_optimal_parameter("p1") + + def _are_parameters_invalid(self, parameters: dict): + """ + return if the parameter set `parameters` is invalid + + """ + n, k, w = self.problem.get_parameters() + par = SimpleNamespace(**parameters) + k1 = (k + par.l) // 2 + if par.p > w // 2 or \ + k1 < par.p or \ + par.l >= n - k or\ + n - k - par.l < w - 2 * par.p or \ + k1 - par.p < par.p1 - par.p / 2 or \ + par.p1 < par.p / 2: + return True + return False + + def _valid_choices(self): + """ + Generator which yields on each call a new set of valid parameters based on the `_parameter_ranges` and already + set parameters in `_optimal_parameters` + """ + new_ranges = self._fix_ranges_for_already_set_parameters() + + n, k, w = self.problem.get_parameters() + + for p in range(new_ranges["p"]["min"], min(w // 2, new_ranges["p"]["max"]) + 1, 2): + for l in range(new_ranges["l"]["min"], min(n - k - (w - 2 * p), new_ranges["l"]["max"]) + 1): + for p1 in range(max(new_ranges["p1"]["min"], (p + 1) // 2), new_ranges["p1"]["max"] + 1): + L1 = log2(binom((k+l)//2, p1)) + d1 = self._adjust_radius + lower = new_ranges["l1"]["min"] if new_ranges["l1"]["min"] == new_ranges["l1"]["max"] else max(int(L1)-d1, 0) + upper = new_ranges["l1"]["max"] if new_ranges["l1"]["min"] == new_ranges["l1"]["max"] else max(int(L1)+d1, 0) + + for l1 in range(lower, upper): + indices = {"p": p, "p1": p1, "l": l, "l1": l1, + "r": self._optimal_parameters["r"]} + if self._are_parameters_invalid(indices): + continue + yield indices + + def _time_and_memory_complexity(self, parameters: dict, verbose_information=None): + """ + computes the expected runtime and memory consumption for the depth 2 version + + """ + n, k, w = self.problem.get_parameters() + par = SimpleNamespace(**parameters) + k1 = (k + par.l) // 2 + + if self._are_parameters_invalid(parameters): + return inf, inf + + solutions = self.problem.nsolutions + + L1 = binom(k1, par.p1) + if self._is_early_abort_possible(log2(L1)): + return inf, inf + + if self.qc: + L1b = binom(k1, par.p1 - 1) * k + + if not self.qc: + reps = (binom(par.p, par.p // 2) * binom(k1 - par.p, par.p1 - par.p // 2)) ** 2 + else: + reps = binom(par.p, par.p // 2) * binom(k1 - par.p, par.p1 - par.p // 2) * binom(k1 - par.p + 1, par.p1 - par.p // 2) + + L12 = max(1, L1 ** 2 // 2 ** par.l1) + + qc_advantage = 0 + if self.qc: + L12b = max(1, L1 * L1b // 2 ** par.l1) + qc_advantage = log2(k) + + memory = log2((2 * L1 + L12) + _mem_matrix(n, k, par.r)) if not self.qc else\ + log2(L1 + L1b + min(L12, L12b) + _mem_matrix(n, k, par.r)) + if self._is_early_abort_possible(memory): + return inf, inf + + Tp = max(log2(binom(n, w)) + - log2(binom(n - k - par.l, w - 2 * par.p + self.qc)) + - log2(binom(k1, par.p)) + - log2(binom(k1, par.p - self.qc)) + - qc_advantage - solutions, 0) + + Tg = _gaussian_elimination_complexity(n, k, par.r) + if not self.qc: + T_tree = 2 * _list_merge_complexity(L1, par.l1, self._hmap) +\ + _list_merge_complexity(L12, par.l - par.l1, self._hmap) + else: + T_tree = _list_merge_async_complexity(L1, L1b, par.l1, self._hmap) +\ + _list_merge_complexity(L1, par.l1, self._hmap) +\ + _list_merge_async_complexity(L12, L12b, self._hmap) + T_rep = int(ceil(2 ** (par.l1 - log2(reps)))) + time = Tp + log2(Tg + T_rep * T_tree) + + if verbose_information is not None: + verbose_information[VerboseInformation.CONSTRAINTS.value] = [par.l1, par.l - par.l1] + verbose_information[VerboseInformation.PERMUTATIONS.value] = Tp + verbose_information[VerboseInformation.TREE.value] = log2(T_rep * T_tree) + verbose_information[VerboseInformation.GAUSS.value] = log2(Tg) + verbose_information[VerboseInformation.REPRESENTATIONS.value] = reps + verbose_information[VerboseInformation.LISTS.value] = [ + log2(L1), log2(L12), 2 * log2(L12) - (par.l - par.l1)] + + return time, memory + + def __repr__(self): + rep = "BJMM+ estimator for " + str(self.problem) + return rep diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/both_may.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/both_may.py index ce425ce9..f114c689 100644 --- a/cryptographic_estimators/SDEstimator/SDAlgorithms/both_may.py +++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/both_may.py @@ -23,7 +23,7 @@ log2, inf, ceil from types import SimpleNamespace from ..sd_constants import * -#from ..SDWorkfactorModels import BothMayScipyModel +from ..SDWorkfactorModels import BothMayScipyModel class BothMay(SDAlgorithm): @@ -54,7 +54,7 @@ def __init__(self, problem: SDProblem, **kwargs): super(BothMay, self).__init__(problem, **kwargs) self._name = "Both-May" self.initialize_parameter_ranges() - #self.scipy_model = BothMayScipyModel + self.scipy_model = BothMayScipyModel def initialize_parameter_ranges(self): """ diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/dumer.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/dumer.py index ac8ceea1..2b000cbd 100644 --- a/cryptographic_estimators/SDEstimator/SDAlgorithms/dumer.py +++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/dumer.py @@ -23,7 +23,7 @@ binom, log2, inf from types import SimpleNamespace from ..sd_constants import * -#from ..SDWorkfactorModels.dumer import DumerScipyModel +from ..SDWorkfactorModels.dumer import DumerScipyModel class Dumer(SDAlgorithm): @@ -55,7 +55,7 @@ def __init__(self, problem: SDProblem, **kwargs): super(Dumer, self).__init__(problem, **kwargs) self._name = "Dumer" self.initialize_parameter_ranges() - #self.scipy_model = DumerScipyModel + self.scipy_model = DumerScipyModel def initialize_parameter_ranges(self): """ diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/may_ozerov.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/may_ozerov.py index 4c6a9694..42031a3e 100644 --- a/cryptographic_estimators/SDEstimator/SDAlgorithms/may_ozerov.py +++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/may_ozerov.py @@ -24,7 +24,7 @@ _indyk_motwani_complexity, min_max, binom, log2, ceil, inf from types import SimpleNamespace from ..sd_constants import * -#from ..SDWorkfactorModels.may_ozerov import MayOzerovScipyModel +from ..SDWorkfactorModels.may_ozerov import MayOzerovScipyModel from typing import Union @@ -365,7 +365,7 @@ def __init__(self, problem: SDProblem, **kwargs): super(MayOzerovD3, self).__init__(problem, **kwargs) self._name = "May-OzerovD3" self.initialize_parameter_ranges() - #self.scipy_model = MayOzerovScipyModel + self.scipy_model = MayOzerovScipyModel def initialize_parameter_ranges(self): """ diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/prange.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/prange.py index 0afa1f78..735aed7a 100644 --- a/cryptographic_estimators/SDEstimator/SDAlgorithms/prange.py +++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/prange.py @@ -21,7 +21,7 @@ from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, binom, log2 from ...helper import ComplexityType from ..sd_constants import * -#from ..SDWorkfactorModels.prange import PrangeScipyModel +from ..SDWorkfactorModels.prange import PrangeScipyModel class Prange(SDAlgorithm): @@ -49,7 +49,7 @@ def __init__(self, problem: SDProblem, **kwargs): """ self._name = "Prange" super(Prange, self).__init__(problem, **kwargs) - #self.scipy_model = PrangeScipyModel + self.scipy_model = PrangeScipyModel def _time_and_memory_complexity(self, parameters: dict, verbose_information=None): """ diff --git a/cryptographic_estimators/SDEstimator/SDAlgorithms/stern.py b/cryptographic_estimators/SDEstimator/SDAlgorithms/stern.py index 7449a52e..60fad7a0 100644 --- a/cryptographic_estimators/SDEstimator/SDAlgorithms/stern.py +++ b/cryptographic_estimators/SDEstimator/SDAlgorithms/stern.py @@ -22,7 +22,7 @@ from ...SDEstimator.sd_helper import _gaussian_elimination_complexity, _mem_matrix, _list_merge_complexity, binom, log2, min_max, inf from types import SimpleNamespace from ..sd_constants import * -#from ..SDWorkfactorModels.stern import SternScipyModel +from ..SDWorkfactorModels.stern import SternScipyModel class Stern(SDAlgorithm): @@ -51,7 +51,7 @@ def __init__(self, problem: SDProblem, **kwargs): self._name = "Stern" super(Stern, self).__init__(problem, **kwargs) self.initialize_parameter_ranges() -# self.scipy_model = SternScipyModel + self.scipy_model = SternScipyModel def initialize_parameter_ranges(self): """ diff --git a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/__init__.py b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/__init__.py index c55c13ce..32f10ba9 100644 --- a/cryptographic_estimators/SDEstimator/SDWorkfactorModels/__init__.py +++ b/cryptographic_estimators/SDEstimator/SDWorkfactorModels/__init__.py @@ -1,2 +1,2 @@ - #from .scipy_model import ScipyModel -#from .both_may import BothMayScipyModel +from .scipy_model import ScipyModel +from .both_may import BothMayScipyModel \ No newline at end of file diff --git a/cryptographic_estimators/SDEstimator/__init__.py b/cryptographic_estimators/SDEstimator/__init__.py index a78adc69..93d30c76 100644 --- a/cryptographic_estimators/SDEstimator/__init__.py +++ b/cryptographic_estimators/SDEstimator/__init__.py @@ -2,4 +2,4 @@ from .sd_estimator import SDEstimator from .sd_helper import binom, min_max, _gaussian_elimination_complexity, _optimize_m4ri, _mem_matrix, _list_merge_complexity, _indyk_motwani_complexity, _mitm_nn_complexity, _list_merge_async_complexity from .sd_problem import SDProblem -from .SDAlgorithms import BallCollision, BJMM, BothMay, Dumer, MayOzerov, Prange, Stern +from .SDAlgorithms import BallCollision, BJMM, BJMMdw, BJMMpdw, BJMMplus, BothMay, Dumer, MayOzerov, Prange, Stern diff --git a/cryptographic_estimators/__init__.py b/cryptographic_estimators/__init__.py index 6a871fae..f72f5137 100644 --- a/cryptographic_estimators/__init__.py +++ b/cryptographic_estimators/__init__.py @@ -3,16 +3,16 @@ from .base_problem import BaseProblem from .helper import ComplexityType, concat_pretty_tables, _truncate, round_or_truncate from . import SDEstimator -#from . import MQEstimator -#from . import SDFqEstimator -#from . import RegSDEstimator -#from . import PKEstimator -#from . import LEEstimator -#from . import PEEstimator -#from . import DummyEstimator -#from . import MREstimator -#from . import UOVEstimator -#from . import MAYOEstimator +from . import MQEstimator +from . import SDFqEstimator +from . import RegSDEstimator +from . import PKEstimator +from . import LEEstimator +from . import PEEstimator +from . import DummyEstimator +from . import MREstimator +from . import UOVEstimator +from . import MAYOEstimator # WARNING: # This sets the MAXIMUM number of coefficients that can be calculated for any @@ -20,6 +20,6 @@ # Do not remove the power_series import; it's needed to enforce this cap. # The chosen limit should be sufficient for most use cases, and values # below 4000 will raise testing errors. -#MAX_COEFFS = 20000 -#from flint import fmpq_series as power_series, ctx -#ctx.cap = MAX_COEFFS +MAX_COEFFS = 20000 +from flint import fmpq_series as power_series, ctx +ctx.cap = MAX_COEFFS diff --git a/cryptographic_estimators/estimation_renderer.py b/cryptographic_estimators/estimation_renderer.py index 46bbccbc..c31da9f8 100644 --- a/cryptographic_estimators/estimation_renderer.py +++ b/cryptographic_estimators/estimation_renderer.py @@ -20,7 +20,7 @@ from .helper import concat_all_tables, round_or_truncate from copy import deepcopy from prettytable import PrettyTable -#from sage.all import RR +from sage.all import RR class EstimationRenderer(): @@ -128,6 +128,6 @@ def _add_rows(self, sub_table: PrettyTable, estimation: dict) -> PrettyTable: row = [estimation[i][sub_table.title][k] for k in sub_table.field_names] row = [round_or_truncate( - i, self._truncate, self._precision) for i in row] + i, self._truncate, self._precision) if i in RR else i for i in row] sub_table.add_row(row) return sub_table diff --git a/tests.py b/tests.py deleted file mode 100644 index 1bcc585d..00000000 --- a/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from cryptographic_estimators.SDEstimator import SDEstimator -E = SDEstimator(n=500, k=250, w=50) -E.table()