From 909ef8d36e10c3a983555ae5b4cc67517fc5c152 Mon Sep 17 00:00:00 2001 From: paulhuynh Date: Mon, 21 Sep 2026 22:11:33 +0400 Subject: [PATCH 01/11] FEATURE/Refactor: unify MILP find_one trail bounds --- .../cipher_modules/models/milp/milp_model.py | 42 +++-- .../milp_xor_differential_model.py | 150 ++++------------- .../milp/milp_models/milp_xor_linear_model.py | 151 ++++-------------- .../milp_xor_differential_model_test.py | 30 +++- .../milp_models/milp_xor_linear_model_test.py | 34 ++-- 5 files changed, 148 insertions(+), 259 deletions(-) diff --git a/claasp/cipher_modules/models/milp/milp_model.py b/claasp/cipher_modules/models/milp/milp_model.py index a1a7cf7f2..b0c90c29d 100644 --- a/claasp/cipher_modules/models/milp/milp_model.py +++ b/claasp/cipher_modules/models/milp/milp_model.py @@ -268,17 +268,18 @@ def weight_constraints(self, weight, weight_precision=MILP_DEFAULT_WEIGHT_PRECIS return self.weight_range_constraints(weight, weight, weight_precision) - def weight_range_constraints(self, min_weight, max_weight, weight_precision=MILP_DEFAULT_WEIGHT_PRECISION): + def weight_range_constraints(self, min_weight=None, max_weight=None, weight_precision=MILP_DEFAULT_WEIGHT_PRECISION): """ - Return a list of variables and a list of constraints that bound the total weight to ``[min_weight, max_weight]``. + Return a list of variables and a list of constraints that bound the total weight. - Unlike :py:meth:`weight_constraints`, which pins the weight to a single value, this bounds it to a - range. Setting ``min_weight == max_weight`` is equivalent to fixing the weight. + If both bounds are provided, this bounds the weight to ``[min_weight, max_weight]``. Setting + ``min_weight == max_weight`` is equivalent to fixing the weight. If only one bound is provided, only that + side of the range is constrained. If neither bound is provided, no weight constraint is returned. INPUT: - - ``min_weight`` -- **integer**; lower bound on the total weight - - ``max_weight`` -- **integer**; upper bound on the total weight + - ``min_weight`` -- **integer** or ``None`` (default: ``None``); lower bound on the total weight + - ``max_weight`` -- **integer** or ``None`` (default: ``None``); upper bound on the total weight - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. EXAMPLES:: @@ -295,16 +296,39 @@ def weight_range_constraints(self, min_weight, max_weight, weight_precision=MILP [300 <= x_0, x_0 <= 1000] sage: milp.weight_range_constraints(5, 5)[1] [x_0 == 500] + + sage: from claasp.ciphers.block_ciphers.simon_block_cipher import SimonBlockCipher + sage: from claasp.cipher_modules.models.milp.milp_model import MilpModel + sage: simon = SimonBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: milp = MilpModel(simon) + sage: milp.init_model_in_sage_milp_class() + sage: milp.weight_range_constraints(3, None)[1] + [300 <= x_0] + + sage: from claasp.ciphers.block_ciphers.simon_block_cipher import SimonBlockCipher + sage: from claasp.cipher_modules.models.milp.milp_model import MilpModel + sage: simon = SimonBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: milp = MilpModel(simon) + sage: milp.init_model_in_sage_milp_class() + sage: milp.weight_range_constraints(None, 5)[1] + [x_0 <= 500] """ + if min_weight is None and max_weight is None: + return [], [] + if min_weight is not None and max_weight is not None and min_weight > max_weight: + raise ValueError("lower_bound must be <= upper_bound") + p = self._integer_variable prec = 10**weight_precision variables = [("p[probability]", p["probability"])] if min_weight == max_weight: constraints = [p["probability"] == prec * min_weight] else: - constraints = [p["probability"] <= prec * max_weight] - if min_weight > 0: - constraints.insert(0, p["probability"] >= prec * min_weight) + constraints = [] + if min_weight is not None: + constraints.append(p["probability"] >= prec * min_weight) + if max_weight is not None: + constraints.append(p["probability"] <= prec * max_weight) return variables, constraints diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py index 0f17be39b..64bce7910 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py @@ -537,18 +537,27 @@ def find_lowest_weight_xor_differential_trail( def find_one_xor_differential_trail( self, fixed_values=[], + lower_bound=None, + upper_bound=None, weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, solver_name=SOLVER_DEFAULT, external_solver_name=None, ): """ Return a XOR differential trail, not necessarily the one with the lowest weight. + + If bounds are provided, the returned trail has weight in ``[lower_bound, upper_bound]``. Passing only + ``lower_bound`` finds one trail with weight at least that value; passing only ``upper_bound`` finds one trail + with weight at most that value, and passing equal bounds fixes the trail weight. + By default, the search is set in the single-key setting. INPUT: - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard format + - ``lower_bound`` -- **integer** (default: `None`); lower bound on the weight of the trail + - ``upper_bound`` -- **integer** (default: `None`); upper bound on the weight of the trail - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - ``solver_name`` -- **string** (default: `GLPK`); the solver to call - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. @@ -566,6 +575,27 @@ def find_one_xor_differential_trail( sage: milp = MilpXorDifferentialModel(speck) sage: trail = milp.find_one_xor_differential_trail() # random # doctest: +SKIP + # single-key setting with upper bound on the weight + sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: milp = MilpXorDifferentialModel(speck) + sage: trail = milp.find_one_xor_differential_trail(upper_bound=5) # random # doctest: +SKIP + sage: 0.0 <= trail['total_weight'] <= 5.0 # doctest: +SKIP + True + + # single-key setting with lower bound on the weight + sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: milp = MilpXorDifferentialModel(speck) + sage: trail = milp.find_one_xor_differential_trail(lower_bound=3) # random # doctest: +SKIP + sage: trail['total_weight'] >= 3.0 # doctest: +SKIP + True + + # single-key setting with bounded weight + sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: milp = MilpXorDifferentialModel(speck) + sage: trail = milp.find_one_xor_differential_trail(lower_bound=3, upper_bound=5) # random # doctest: +SKIP + sage: 3.0 <= trail['total_weight'] <= 5.0 # doctest: +SKIP + True + # related-key setting sage: from claasp.cipher_modules.models.utils import set_fixed_variables sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher @@ -575,135 +605,23 @@ def find_one_xor_differential_trail( sage: key = set_fixed_variables('key', 'not_equal', list(range(64)), [0] * 64) sage: trail = milp.find_one_xor_differential_trail(fixed_values=[key]) # random # doctest: +SKIP """ - start = time.time() - self.init_model_in_sage_milp_class(solver_name) - self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") - mip = self._model - mip.set_objective(None) - self.add_constraints_to_build_in_sage_milp_class(-1, weight_precision, fixed_values) - end = time.time() - building_time = end - start - solution = self.solve(MILP_XOR_DIFFERENTIAL, solver_name, external_solver_name) - solution["building_time"] = building_time - solution["test_name"] = "find_one_xor_differential_trail" - - return solution - - def find_one_xor_differential_trail_with_weight_at_most( - self, - max_weight, - min_weight=0, - fixed_values=[], - weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, - solver_name=SOLVER_DEFAULT, - external_solver_name=None, - ): - """ - Return one XOR differential trail whose weight lies in ``[min_weight, max_weight]``, in standard format. - By default, the search is set in the single-key setting. - - .. NOTE:: - - Feasibility search: returns any trail with weight in ``[min_weight, max_weight]``, not the trail of - lowest weight. With the default ``min_weight=0`` the returned trail may be trivial. - - INPUT: + if lower_bound is not None and upper_bound is not None and lower_bound > upper_bound: + raise ValueError("lower_bound must be <= upper_bound") - - ``max_weight`` -- **integer**; the upper bound on the weight of the trail - - ``min_weight`` -- **integer** (default: `0`); the lower bound on the weight of the trail - - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard - format - - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - - ``solver_name`` -- **string** (default: `GLPK`); the solver to call - - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. - - .. SEEALSO:: - - :py:meth:`~cipher_modules.models.utils.set_fixed_variables` - - EXAMPLES:: - - sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_differential_model import MilpXorDifferentialModel - sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - sage: milp = MilpXorDifferentialModel(speck) - sage: trail = milp.find_one_xor_differential_trail_with_weight_at_most(5) # random - sage: trail['total_weight'] <= 5.0 - True - """ start = time.time() self.init_model_in_sage_milp_class(solver_name) self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, weight_precision, fixed_values) - _, constraints = self.weight_range_constraints(min_weight, max_weight, weight_precision) + _, constraints = self.weight_range_constraints(lower_bound, upper_bound, weight_precision) for constraint in constraints: mip.add_constraint(constraint) end = time.time() building_time = end - start solution = self.solve(MILP_XOR_DIFFERENTIAL, solver_name, external_solver_name) solution["building_time"] = building_time - solution["test_name"] = "find_one_xor_differential_trail_with_weight_at_most" - - return solution - - def find_one_xor_differential_trail_with_fixed_weight( - self, - fixed_weight, - fixed_values=[], - weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, - solver_name=SOLVER_DEFAULT, - external_solver_name=None, - ): - """ - Return one XOR differential trail with weight equal to ``fixed_weight`` as a list in standard format. - By default, the search is set in the single-key setting. - - INPUT: - - - ``fixed_weight`` -- **integer**; the weight found using :py:meth:`~find_lowest_weight_xor_differential_trail` - - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard - format - - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - - ``solver_name`` -- **string** (default: `GLPK`); the solver to call - - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. - - .. SEEALSO:: - - :py:meth:`~cipher_modules.models.utils.set_fixed_variables` - - EXAMPLES:: - - # single-key setting - sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_differential_model import MilpXorDifferentialModel - sage: speck = SpeckBlockCipher(number_of_rounds=3) - sage: milp = MilpXorDifferentialModel(speck) - sage: trail = milp.find_one_xor_differential_trail_with_fixed_weight(3) # random # doctest: +SKIP - sage: trail['total_weight'] # doctest: +SKIP - 3.0 - - # related-key setting - sage: from claasp.cipher_modules.models.utils import set_fixed_variables - sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_differential_model import MilpXorDifferentialModel - sage: speck = SpeckBlockCipher(number_of_rounds=3) - sage: milp = MilpXorDifferentialModel(speck) - sage: key = set_fixed_variables('key', 'not_equal', list(range(64)), [0] * 64) - sage: trail = milp.find_one_xor_differential_trail_with_fixed_weight(3, fixed_values=[key]) # random # doctest: +SKIP - sage: trail['total_weight'] # doctest: +SKIP - 3.0 - """ - solution = self.find_one_xor_differential_trail_with_weight_at_most( - fixed_weight, - fixed_weight, - fixed_values=fixed_values, - weight_precision=weight_precision, - solver_name=solver_name, - external_solver_name=external_solver_name, - ) - solution["test_name"] = "find_one_xor_differential_trail_with_fixed_weight" + solution["test_name"] = "find_one_xor_differential_trail" return solution diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py index 97c219ebf..00c38bfb0 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py @@ -574,12 +574,19 @@ def find_lowest_weight_xor_linear_trail( def find_one_xor_linear_trail( self, fixed_values=[], + lower_bound=None, + upper_bound=None, weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, solver_name=SOLVER_DEFAULT, external_solver_name=None, ): """ Return a XOR linear trail, not necessarily the one with the lowest weight. + + If bounds are provided, the returned trail has weight in ``[lower_bound, upper_bound]``. Passing only + ``lower_bound`` finds one trail with weight at least that value; passing only ``upper_bound`` finds one trail + with weight at most that value, and passing equal bounds fixes the trail weight. + By default, the search removes the key schedule, if any. By default, the weight corresponds to the negative base-2 logarithm of the correlation of the trail. @@ -587,6 +594,8 @@ def find_one_xor_linear_trail( - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard format (see ) + - ``lower_bound`` -- **integer** (default: `None`); lower bound on the weight of the trail + - ``upper_bound`` -- **integer** (default: `None`); upper bound on the weight of the trail - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - ``solver_name`` -- **string** (default: `GLPK`); the name of the solver (if needed) - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. @@ -603,145 +612,53 @@ def find_one_xor_linear_trail( sage: milp = MilpXorLinearModel(speck) sage: trail = milp.find_one_xor_linear_trail() # random - # including the key schedule in the model sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.utils import set_fixed_variables sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - sage: milp = MilpXorLinearModel(speck) - sage: key = set_fixed_variables('key', 'not_equal', list(range(32)), [0] * 32) - sage: trail = milp.find_one_xor_linear_trail(fixed_values=[key]) # random - """ - start = time.time() - self.init_model_in_sage_milp_class(solver_name) - self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") - mip = self._model - mip.set_objective(None) - self.add_constraints_to_build_in_sage_milp_class(-1, weight_precision, fixed_values) - end = time.time() - building_time = end - start - solution = self.solve(MILP_XOR_LINEAR, solver_name, external_solver_name) - solution["building_time"] = building_time - solution["test_name"] = "find_lowest_weight_xor_linear_trail" - - return solution - - def find_one_xor_linear_trail_with_weight_at_most( - self, - max_weight, - min_weight=0, - fixed_values=[], - weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, - solver_name=SOLVER_DEFAULT, - external_solver_name=None, - ): - """ - Return one XOR linear trail whose weight lies in ``[min_weight, max_weight]``, in standard format. - By default, the search removes the key schedule, if any, and the weight corresponds to the negative - base-2 logarithm of the correlation of the trail. - - .. NOTE:: - - Feasibility search: returns any trail with weight in ``[min_weight, max_weight]``, not the trail of - lowest weight. With the default ``min_weight=0`` the returned trail may be trivial. - - INPUT: - - - ``max_weight`` -- **integer**; the upper bound on the weight of the trail - - ``min_weight`` -- **integer** (default: `0`); the lower bound on the weight of the trail - - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard - format - - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - - ``solver_name`` -- **string** (default: `GLPK`); the name of the solver (if needed) - - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. - - .. SEEALSO:: - - :py:meth:`~cipher_modules.models.utils.set_fixed_variables` + sage: trail = milp.find_one_xor_linear_trail(upper_bound=6) # random # doctest: +SKIP + sage: 0.0 <= trail['total_weight'] <= 6.0 # doctest: +SKIP + True - EXAMPLES:: + sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel + sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher + sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: trail = milp.find_one_xor_linear_trail(lower_bound=1) # random # doctest: +SKIP + sage: trail['total_weight'] >= 1.0 # doctest: +SKIP + True + age: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher + sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=6) # random # doctest: +SKIP + sage: 1.0 <= trail['total_weight'] <= 6.0 # doctest: +SKIP + True + + # including the key schedule in the model sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel + sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher + sage: from claasp.cipher_modules.models.utils import set_fixed_variables sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) sage: milp = MilpXorLinearModel(speck) - sage: trail = milp.find_one_xor_linear_trail_with_weight_at_most(6) # random - sage: trail['total_weight'] <= 6.0 - True + sage: key = set_fixed_variables('key', 'not_equal', list(range(32)), [0] * 32) + sage: trail = milp.find_one_xor_linear_trail(fixed_values=[key]) # random """ + if lower_bound is not None and upper_bound is not None and lower_bound > upper_bound: + raise ValueError("lower_bound must be <= upper_bound") + start = time.time() self.init_model_in_sage_milp_class(solver_name) self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, weight_precision, fixed_values) - _, constraints = self.weight_range_constraints(min_weight, max_weight, weight_precision) + _, constraints = self.weight_range_constraints(lower_bound, upper_bound, weight_precision) for constraint in constraints: mip.add_constraint(constraint) end = time.time() building_time = end - start solution = self.solve(MILP_XOR_LINEAR, solver_name, external_solver_name) solution["building_time"] = building_time - solution["test_name"] = "find_one_xor_linear_trail_with_weight_at_most" - - return solution - - def find_one_xor_linear_trail_with_fixed_weight( - self, - fixed_weight, - fixed_values=[], - weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, - solver_name=SOLVER_DEFAULT, - external_solver_name=None, - ): - """ - Return one XOR linear trail with weight equal to ``fixed_weight`` as a list in standard format. - By default, the search removes the key schedule, if any. - By default, the weight corresponds to the negative base-2 logarithm of the correlation of the trail. - - INPUT: - - - ``fixed_weight`` -- **integer**; the weight found using :py:meth:`~find_lowest_weight_xor_linear_trail` - - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard - format - - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - - ``solver_name`` -- **string** (default: `GLPK`); the name of the solver (if needed) - - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. - - .. SEEALSO:: - - :py:meth:`~cipher_modules.models.utils.set_fixed_variables` - - EXAMPLES:: - - sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel - sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - sage: milp = MilpXorLinearModel(speck) - sage: trail = milp.find_one_xor_linear_trail_with_fixed_weight(6) # random - ... - sage: trail['total_weight'] - 6.0 - - sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel - sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.utils import set_fixed_variables - sage: speck = SpeckBlockCipher(block_bit_size=8, key_bit_size=16, number_of_rounds=4) - sage: milp = MilpXorLinearModel(speck) - sage: key = set_fixed_variables('key', 'not_equal', list(range(16)), [0] * 16) - sage: trail = milp.find_one_xor_linear_trail_with_fixed_weight(3, fixed_values=[key]) # random - sage: trail["total_weight"] - 3.0 - """ - solution = self.find_one_xor_linear_trail_with_weight_at_most( - fixed_weight, - fixed_weight, - fixed_values=fixed_values, - weight_precision=weight_precision, - solver_name=solver_name, - external_solver_name=external_solver_name, - ) - solution["test_name"] = "find_one_xor_linear_trail_with_fixed_weight" + solution["test_name"] = "find_one_xor_linear_trail" return solution diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py index b03905935..c026b4abe 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py @@ -1,3 +1,5 @@ +import pytest + from claasp.cipher_modules.models.milp.milp_models.milp_xor_differential_model import MilpXorDifferentialModel from claasp.cipher_modules.models.utils import integer_to_bit_list, set_fixed_variables from claasp.ciphers.block_ciphers.present_block_cipher import PresentBlockCipher @@ -69,7 +71,7 @@ def test_find_one_xor_differential_trail(): def test_find_one_xor_differential_trail_with_fixed_weight(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorDifferentialModel(speck) - trail = milp.find_one_xor_differential_trail_with_fixed_weight(5) + trail = milp.find_one_xor_differential_trail(lower_bound=5, upper_bound=5) assert trail["total_weight"] == 5.0 tea = TeaBlockCipher(block_bit_size=16, key_bit_size=32, number_of_rounds=2) @@ -83,8 +85,8 @@ def test_find_one_xor_differential_trail_with_fixed_weight(): cipher_output = set_fixed_variables( "cipher_output_1_16", "equal", list(range(16)), integer_to_bit_list(0x404A, 16, "big") ) - trail = milp.find_one_xor_differential_trail_with_fixed_weight( - 15, fixed_values=[key, round_0_output, cipher_output] + trail = milp.find_one_xor_differential_trail( + fixed_values=[key, round_0_output, cipher_output], lower_bound=15, upper_bound=15 ) assert trail["total_weight"] == 15.0 # @@ -99,14 +101,28 @@ def test_find_one_xor_differential_trail_with_fixed_weight(): key = set_fixed_variables( component_id=INPUT_KEY, constraint_type="not_equal", bit_positions=range(64), bit_values=(0,) * 64 ) - trail = milp.find_one_xor_differential_trail_with_fixed_weight(5, fixed_values=[key, round_0_output, cipher_output]) + trail = milp.find_one_xor_differential_trail( + fixed_values=[key, round_0_output, cipher_output], lower_bound=5, upper_bound=5 + ) assert trail["total_weight"] == 5.0 -def test_find_one_xor_differential_trail_with_weight_at_most(): +def test_find_one_xor_differential_trail_with_upper_bound(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - trail = MilpXorDifferentialModel(speck).find_one_xor_differential_trail_with_weight_at_most(5) + trail = MilpXorDifferentialModel(speck).find_one_xor_differential_trail(upper_bound=5) assert 0.0 <= trail["total_weight"] <= 5.0 - trail = MilpXorDifferentialModel(speck).find_one_xor_differential_trail_with_weight_at_most(5, 3) + trail = MilpXorDifferentialModel(speck).find_one_xor_differential_trail(lower_bound=3, upper_bound=5) assert 3.0 <= trail["total_weight"] <= 5.0 + + +def test_find_one_xor_differential_trail_with_weight_at_least(): + speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + trail = MilpXorDifferentialModel(speck).find_one_xor_differential_trail(lower_bound=3) + assert trail["total_weight"] >= 3.0 + + +def test_find_one_xor_differential_trail_rejects_invalid_bounds(): + speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + with pytest.raises(ValueError, match="lower_bound must be <= upper_bound"): + MilpXorDifferentialModel(speck).find_one_xor_differential_trail(lower_bound=6, upper_bound=5) diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py index 2885607d7..3d3fa9293 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py @@ -101,36 +101,48 @@ def test_find_one_xor_linear_trail(): def test_find_one_xor_linear_trail_with_fixed_weight(): # speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) # milp = MilpXorLinearModel(speck) - # trail = milp.find_one_xor_linear_trail_with_fixed_weight(6) + # trail = milp.find_one_xor_linear_trail(lower_bound=6, upper_bound=6) # assert len(trail) == 9 # assert trail["total_weight"] == 6.0 speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1) + trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1) assert len(trail) == 10 assert trail["total_weight"] == 1.0 # # speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=4) # milp = MilpXorLinearModel(speck) - # trail = milp.find_one_xor_linear_trail_with_fixed_weight(10) + # trail = milp.find_one_xor_linear_trail(lower_bound=10, upper_bound=10) # assert len(trail) == 9 # assert trail["total_weight"] == 10.0 -def test_find_one_xor_linear_trail_with_weight_at_most(): +def test_find_one_xor_linear_trail_with_upper_bound(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - trail = MilpXorLinearModel(speck).find_one_xor_linear_trail_with_weight_at_most(6) + trail = MilpXorLinearModel(speck).find_one_xor_linear_trail(upper_bound=6) assert 0.0 <= trail["total_weight"] <= 6.0 - trail = MilpXorLinearModel(speck).find_one_xor_linear_trail_with_weight_at_most(6, 1) + trail = MilpXorLinearModel(speck).find_one_xor_linear_trail(lower_bound=1, upper_bound=6) assert 1.0 <= trail["total_weight"] <= 6.0 +def test_find_one_xor_linear_trail_with_weight_at_least(): + speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + trail = MilpXorLinearModel(speck).find_one_xor_linear_trail(lower_bound=1) + assert trail["total_weight"] >= 1.0 + + +def test_find_one_xor_linear_trail_rejects_invalid_bounds(): + speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + with pytest.raises(ValueError, match="lower_bound must be <= upper_bound"): + MilpXorLinearModel(speck).find_one_xor_linear_trail(lower_bound=7, upper_bound=6) + + def test_find_one_xor_linear_trail_with_fixed_weight_with_external_solver(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="glpk_ext") + trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="glpk_ext") assert len(trail) == 10 assert trail["total_weight"] == 1.0 @@ -139,21 +151,23 @@ def test_find_one_xor_linear_trail_with_fixed_weight_with_supported_but_not_inst with pytest.raises(Exception) as e_info: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="cplex_ext") + trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="cplex_ext") def test_find_one_xor_linear_trail_with_fixed_weight_with_installed_external_solver_but_missing_license(): with pytest.raises(Exception) as e_info: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="Gurobi_ext") + trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="Gurobi_ext") def test_find_one_xor_linear_trail_with_fixed_weight_with_unsupported_external_solver(): with pytest.raises(Exception) as e_info: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="unsupported_solver") + trail = milp.find_one_xor_linear_trail( + lower_bound=1, upper_bound=1, external_solver_name="unsupported_solver" + ) def test_fix_variables_value_xor_linear_constraints(): From 1ad56f623ab394ef8ede5da791342d3e561f16db Mon Sep 17 00:00:00 2001 From: paulhuynh Date: Mon, 21 Sep 2026 22:11:33 +0400 Subject: [PATCH 02/11] FEATURE/Refactor: unify MILP find_one trail bounds --- .../cipher_modules/models/milp/milp_model.py | 42 +++-- .../milp_xor_differential_model.py | 150 ++++------------- .../milp/milp_models/milp_xor_linear_model.py | 151 ++++-------------- .../milp_xor_differential_model_test.py | 28 +++- .../milp_models/milp_xor_linear_model_test.py | 32 ++-- 5 files changed, 146 insertions(+), 257 deletions(-) diff --git a/claasp/cipher_modules/models/milp/milp_model.py b/claasp/cipher_modules/models/milp/milp_model.py index a1a7cf7f2..b0c90c29d 100644 --- a/claasp/cipher_modules/models/milp/milp_model.py +++ b/claasp/cipher_modules/models/milp/milp_model.py @@ -268,17 +268,18 @@ def weight_constraints(self, weight, weight_precision=MILP_DEFAULT_WEIGHT_PRECIS return self.weight_range_constraints(weight, weight, weight_precision) - def weight_range_constraints(self, min_weight, max_weight, weight_precision=MILP_DEFAULT_WEIGHT_PRECISION): + def weight_range_constraints(self, min_weight=None, max_weight=None, weight_precision=MILP_DEFAULT_WEIGHT_PRECISION): """ - Return a list of variables and a list of constraints that bound the total weight to ``[min_weight, max_weight]``. + Return a list of variables and a list of constraints that bound the total weight. - Unlike :py:meth:`weight_constraints`, which pins the weight to a single value, this bounds it to a - range. Setting ``min_weight == max_weight`` is equivalent to fixing the weight. + If both bounds are provided, this bounds the weight to ``[min_weight, max_weight]``. Setting + ``min_weight == max_weight`` is equivalent to fixing the weight. If only one bound is provided, only that + side of the range is constrained. If neither bound is provided, no weight constraint is returned. INPUT: - - ``min_weight`` -- **integer**; lower bound on the total weight - - ``max_weight`` -- **integer**; upper bound on the total weight + - ``min_weight`` -- **integer** or ``None`` (default: ``None``); lower bound on the total weight + - ``max_weight`` -- **integer** or ``None`` (default: ``None``); upper bound on the total weight - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. EXAMPLES:: @@ -295,16 +296,39 @@ def weight_range_constraints(self, min_weight, max_weight, weight_precision=MILP [300 <= x_0, x_0 <= 1000] sage: milp.weight_range_constraints(5, 5)[1] [x_0 == 500] + + sage: from claasp.ciphers.block_ciphers.simon_block_cipher import SimonBlockCipher + sage: from claasp.cipher_modules.models.milp.milp_model import MilpModel + sage: simon = SimonBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: milp = MilpModel(simon) + sage: milp.init_model_in_sage_milp_class() + sage: milp.weight_range_constraints(3, None)[1] + [300 <= x_0] + + sage: from claasp.ciphers.block_ciphers.simon_block_cipher import SimonBlockCipher + sage: from claasp.cipher_modules.models.milp.milp_model import MilpModel + sage: simon = SimonBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: milp = MilpModel(simon) + sage: milp.init_model_in_sage_milp_class() + sage: milp.weight_range_constraints(None, 5)[1] + [x_0 <= 500] """ + if min_weight is None and max_weight is None: + return [], [] + if min_weight is not None and max_weight is not None and min_weight > max_weight: + raise ValueError("lower_bound must be <= upper_bound") + p = self._integer_variable prec = 10**weight_precision variables = [("p[probability]", p["probability"])] if min_weight == max_weight: constraints = [p["probability"] == prec * min_weight] else: - constraints = [p["probability"] <= prec * max_weight] - if min_weight > 0: - constraints.insert(0, p["probability"] >= prec * min_weight) + constraints = [] + if min_weight is not None: + constraints.append(p["probability"] >= prec * min_weight) + if max_weight is not None: + constraints.append(p["probability"] <= prec * max_weight) return variables, constraints diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py index 0f17be39b..64bce7910 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py @@ -537,18 +537,27 @@ def find_lowest_weight_xor_differential_trail( def find_one_xor_differential_trail( self, fixed_values=[], + lower_bound=None, + upper_bound=None, weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, solver_name=SOLVER_DEFAULT, external_solver_name=None, ): """ Return a XOR differential trail, not necessarily the one with the lowest weight. + + If bounds are provided, the returned trail has weight in ``[lower_bound, upper_bound]``. Passing only + ``lower_bound`` finds one trail with weight at least that value; passing only ``upper_bound`` finds one trail + with weight at most that value, and passing equal bounds fixes the trail weight. + By default, the search is set in the single-key setting. INPUT: - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard format + - ``lower_bound`` -- **integer** (default: `None`); lower bound on the weight of the trail + - ``upper_bound`` -- **integer** (default: `None`); upper bound on the weight of the trail - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - ``solver_name`` -- **string** (default: `GLPK`); the solver to call - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. @@ -566,6 +575,27 @@ def find_one_xor_differential_trail( sage: milp = MilpXorDifferentialModel(speck) sage: trail = milp.find_one_xor_differential_trail() # random # doctest: +SKIP + # single-key setting with upper bound on the weight + sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: milp = MilpXorDifferentialModel(speck) + sage: trail = milp.find_one_xor_differential_trail(upper_bound=5) # random # doctest: +SKIP + sage: 0.0 <= trail['total_weight'] <= 5.0 # doctest: +SKIP + True + + # single-key setting with lower bound on the weight + sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: milp = MilpXorDifferentialModel(speck) + sage: trail = milp.find_one_xor_differential_trail(lower_bound=3) # random # doctest: +SKIP + sage: trail['total_weight'] >= 3.0 # doctest: +SKIP + True + + # single-key setting with bounded weight + sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: milp = MilpXorDifferentialModel(speck) + sage: trail = milp.find_one_xor_differential_trail(lower_bound=3, upper_bound=5) # random # doctest: +SKIP + sage: 3.0 <= trail['total_weight'] <= 5.0 # doctest: +SKIP + True + # related-key setting sage: from claasp.cipher_modules.models.utils import set_fixed_variables sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher @@ -575,135 +605,23 @@ def find_one_xor_differential_trail( sage: key = set_fixed_variables('key', 'not_equal', list(range(64)), [0] * 64) sage: trail = milp.find_one_xor_differential_trail(fixed_values=[key]) # random # doctest: +SKIP """ - start = time.time() - self.init_model_in_sage_milp_class(solver_name) - self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") - mip = self._model - mip.set_objective(None) - self.add_constraints_to_build_in_sage_milp_class(-1, weight_precision, fixed_values) - end = time.time() - building_time = end - start - solution = self.solve(MILP_XOR_DIFFERENTIAL, solver_name, external_solver_name) - solution["building_time"] = building_time - solution["test_name"] = "find_one_xor_differential_trail" - - return solution - - def find_one_xor_differential_trail_with_weight_at_most( - self, - max_weight, - min_weight=0, - fixed_values=[], - weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, - solver_name=SOLVER_DEFAULT, - external_solver_name=None, - ): - """ - Return one XOR differential trail whose weight lies in ``[min_weight, max_weight]``, in standard format. - By default, the search is set in the single-key setting. - - .. NOTE:: - - Feasibility search: returns any trail with weight in ``[min_weight, max_weight]``, not the trail of - lowest weight. With the default ``min_weight=0`` the returned trail may be trivial. - - INPUT: + if lower_bound is not None and upper_bound is not None and lower_bound > upper_bound: + raise ValueError("lower_bound must be <= upper_bound") - - ``max_weight`` -- **integer**; the upper bound on the weight of the trail - - ``min_weight`` -- **integer** (default: `0`); the lower bound on the weight of the trail - - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard - format - - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - - ``solver_name`` -- **string** (default: `GLPK`); the solver to call - - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. - - .. SEEALSO:: - - :py:meth:`~cipher_modules.models.utils.set_fixed_variables` - - EXAMPLES:: - - sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_differential_model import MilpXorDifferentialModel - sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - sage: milp = MilpXorDifferentialModel(speck) - sage: trail = milp.find_one_xor_differential_trail_with_weight_at_most(5) # random - sage: trail['total_weight'] <= 5.0 - True - """ start = time.time() self.init_model_in_sage_milp_class(solver_name) self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, weight_precision, fixed_values) - _, constraints = self.weight_range_constraints(min_weight, max_weight, weight_precision) + _, constraints = self.weight_range_constraints(lower_bound, upper_bound, weight_precision) for constraint in constraints: mip.add_constraint(constraint) end = time.time() building_time = end - start solution = self.solve(MILP_XOR_DIFFERENTIAL, solver_name, external_solver_name) solution["building_time"] = building_time - solution["test_name"] = "find_one_xor_differential_trail_with_weight_at_most" - - return solution - - def find_one_xor_differential_trail_with_fixed_weight( - self, - fixed_weight, - fixed_values=[], - weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, - solver_name=SOLVER_DEFAULT, - external_solver_name=None, - ): - """ - Return one XOR differential trail with weight equal to ``fixed_weight`` as a list in standard format. - By default, the search is set in the single-key setting. - - INPUT: - - - ``fixed_weight`` -- **integer**; the weight found using :py:meth:`~find_lowest_weight_xor_differential_trail` - - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard - format - - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - - ``solver_name`` -- **string** (default: `GLPK`); the solver to call - - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. - - .. SEEALSO:: - - :py:meth:`~cipher_modules.models.utils.set_fixed_variables` - - EXAMPLES:: - - # single-key setting - sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_differential_model import MilpXorDifferentialModel - sage: speck = SpeckBlockCipher(number_of_rounds=3) - sage: milp = MilpXorDifferentialModel(speck) - sage: trail = milp.find_one_xor_differential_trail_with_fixed_weight(3) # random # doctest: +SKIP - sage: trail['total_weight'] # doctest: +SKIP - 3.0 - - # related-key setting - sage: from claasp.cipher_modules.models.utils import set_fixed_variables - sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_differential_model import MilpXorDifferentialModel - sage: speck = SpeckBlockCipher(number_of_rounds=3) - sage: milp = MilpXorDifferentialModel(speck) - sage: key = set_fixed_variables('key', 'not_equal', list(range(64)), [0] * 64) - sage: trail = milp.find_one_xor_differential_trail_with_fixed_weight(3, fixed_values=[key]) # random # doctest: +SKIP - sage: trail['total_weight'] # doctest: +SKIP - 3.0 - """ - solution = self.find_one_xor_differential_trail_with_weight_at_most( - fixed_weight, - fixed_weight, - fixed_values=fixed_values, - weight_precision=weight_precision, - solver_name=solver_name, - external_solver_name=external_solver_name, - ) - solution["test_name"] = "find_one_xor_differential_trail_with_fixed_weight" + solution["test_name"] = "find_one_xor_differential_trail" return solution diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py index 97c219ebf..00c38bfb0 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py @@ -574,12 +574,19 @@ def find_lowest_weight_xor_linear_trail( def find_one_xor_linear_trail( self, fixed_values=[], + lower_bound=None, + upper_bound=None, weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, solver_name=SOLVER_DEFAULT, external_solver_name=None, ): """ Return a XOR linear trail, not necessarily the one with the lowest weight. + + If bounds are provided, the returned trail has weight in ``[lower_bound, upper_bound]``. Passing only + ``lower_bound`` finds one trail with weight at least that value; passing only ``upper_bound`` finds one trail + with weight at most that value, and passing equal bounds fixes the trail weight. + By default, the search removes the key schedule, if any. By default, the weight corresponds to the negative base-2 logarithm of the correlation of the trail. @@ -587,6 +594,8 @@ def find_one_xor_linear_trail( - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard format (see ) + - ``lower_bound`` -- **integer** (default: `None`); lower bound on the weight of the trail + - ``upper_bound`` -- **integer** (default: `None`); upper bound on the weight of the trail - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - ``solver_name`` -- **string** (default: `GLPK`); the name of the solver (if needed) - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. @@ -603,145 +612,53 @@ def find_one_xor_linear_trail( sage: milp = MilpXorLinearModel(speck) sage: trail = milp.find_one_xor_linear_trail() # random - # including the key schedule in the model sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.utils import set_fixed_variables sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - sage: milp = MilpXorLinearModel(speck) - sage: key = set_fixed_variables('key', 'not_equal', list(range(32)), [0] * 32) - sage: trail = milp.find_one_xor_linear_trail(fixed_values=[key]) # random - """ - start = time.time() - self.init_model_in_sage_milp_class(solver_name) - self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") - mip = self._model - mip.set_objective(None) - self.add_constraints_to_build_in_sage_milp_class(-1, weight_precision, fixed_values) - end = time.time() - building_time = end - start - solution = self.solve(MILP_XOR_LINEAR, solver_name, external_solver_name) - solution["building_time"] = building_time - solution["test_name"] = "find_lowest_weight_xor_linear_trail" - - return solution - - def find_one_xor_linear_trail_with_weight_at_most( - self, - max_weight, - min_weight=0, - fixed_values=[], - weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, - solver_name=SOLVER_DEFAULT, - external_solver_name=None, - ): - """ - Return one XOR linear trail whose weight lies in ``[min_weight, max_weight]``, in standard format. - By default, the search removes the key schedule, if any, and the weight corresponds to the negative - base-2 logarithm of the correlation of the trail. - - .. NOTE:: - - Feasibility search: returns any trail with weight in ``[min_weight, max_weight]``, not the trail of - lowest weight. With the default ``min_weight=0`` the returned trail may be trivial. - - INPUT: - - - ``max_weight`` -- **integer**; the upper bound on the weight of the trail - - ``min_weight`` -- **integer** (default: `0`); the lower bound on the weight of the trail - - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard - format - - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - - ``solver_name`` -- **string** (default: `GLPK`); the name of the solver (if needed) - - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. - - .. SEEALSO:: - - :py:meth:`~cipher_modules.models.utils.set_fixed_variables` + sage: trail = milp.find_one_xor_linear_trail(upper_bound=6) # random # doctest: +SKIP + sage: 0.0 <= trail['total_weight'] <= 6.0 # doctest: +SKIP + True - EXAMPLES:: + sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel + sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher + sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: trail = milp.find_one_xor_linear_trail(lower_bound=1) # random # doctest: +SKIP + sage: trail['total_weight'] >= 1.0 # doctest: +SKIP + True + age: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher + sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + sage: trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=6) # random # doctest: +SKIP + sage: 1.0 <= trail['total_weight'] <= 6.0 # doctest: +SKIP + True + + # including the key schedule in the model sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel + sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher + sage: from claasp.cipher_modules.models.utils import set_fixed_variables sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) sage: milp = MilpXorLinearModel(speck) - sage: trail = milp.find_one_xor_linear_trail_with_weight_at_most(6) # random - sage: trail['total_weight'] <= 6.0 - True + sage: key = set_fixed_variables('key', 'not_equal', list(range(32)), [0] * 32) + sage: trail = milp.find_one_xor_linear_trail(fixed_values=[key]) # random """ + if lower_bound is not None and upper_bound is not None and lower_bound > upper_bound: + raise ValueError("lower_bound must be <= upper_bound") + start = time.time() self.init_model_in_sage_milp_class(solver_name) self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, weight_precision, fixed_values) - _, constraints = self.weight_range_constraints(min_weight, max_weight, weight_precision) + _, constraints = self.weight_range_constraints(lower_bound, upper_bound, weight_precision) for constraint in constraints: mip.add_constraint(constraint) end = time.time() building_time = end - start solution = self.solve(MILP_XOR_LINEAR, solver_name, external_solver_name) solution["building_time"] = building_time - solution["test_name"] = "find_one_xor_linear_trail_with_weight_at_most" - - return solution - - def find_one_xor_linear_trail_with_fixed_weight( - self, - fixed_weight, - fixed_values=[], - weight_precision=MILP_DEFAULT_WEIGHT_PRECISION, - solver_name=SOLVER_DEFAULT, - external_solver_name=None, - ): - """ - Return one XOR linear trail with weight equal to ``fixed_weight`` as a list in standard format. - By default, the search removes the key schedule, if any. - By default, the weight corresponds to the negative base-2 logarithm of the correlation of the trail. - - INPUT: - - - ``fixed_weight`` -- **integer**; the weight found using :py:meth:`~find_lowest_weight_xor_linear_trail` - - ``fixed_values`` -- **list** (default: `[]`); dictionaries containing the variables to be fixed in standard - format - - ``weight_precision`` -- **integer** (default: `2`); the number of decimals to use when rounding the weight of the trail. - - ``solver_name`` -- **string** (default: `GLPK`); the name of the solver (if needed) - - ``external_solver_name`` -- **string** (default: None); if specified, the library will write the internal Sagemath MILP model as a .lp file and solve it outside of Sagemath, using the external solver. - - .. SEEALSO:: - - :py:meth:`~cipher_modules.models.utils.set_fixed_variables` - - EXAMPLES:: - - sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel - sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - sage: milp = MilpXorLinearModel(speck) - sage: trail = milp.find_one_xor_linear_trail_with_fixed_weight(6) # random - ... - sage: trail['total_weight'] - 6.0 - - sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel - sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher - sage: from claasp.cipher_modules.models.utils import set_fixed_variables - sage: speck = SpeckBlockCipher(block_bit_size=8, key_bit_size=16, number_of_rounds=4) - sage: milp = MilpXorLinearModel(speck) - sage: key = set_fixed_variables('key', 'not_equal', list(range(16)), [0] * 16) - sage: trail = milp.find_one_xor_linear_trail_with_fixed_weight(3, fixed_values=[key]) # random - sage: trail["total_weight"] - 3.0 - """ - solution = self.find_one_xor_linear_trail_with_weight_at_most( - fixed_weight, - fixed_weight, - fixed_values=fixed_values, - weight_precision=weight_precision, - solver_name=solver_name, - external_solver_name=external_solver_name, - ) - solution["test_name"] = "find_one_xor_linear_trail_with_fixed_weight" + solution["test_name"] = "find_one_xor_linear_trail" return solution diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py index b03905935..a747b03a7 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py @@ -1,3 +1,5 @@ +import pytest + from claasp.cipher_modules.models.milp.milp_models.milp_xor_differential_model import MilpXorDifferentialModel from claasp.cipher_modules.models.utils import integer_to_bit_list, set_fixed_variables from claasp.ciphers.block_ciphers.present_block_cipher import PresentBlockCipher @@ -69,7 +71,7 @@ def test_find_one_xor_differential_trail(): def test_find_one_xor_differential_trail_with_fixed_weight(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorDifferentialModel(speck) - trail = milp.find_one_xor_differential_trail_with_fixed_weight(5) + trail = milp.find_one_xor_differential_trail(lower_bound=5, upper_bound=5) assert trail["total_weight"] == 5.0 tea = TeaBlockCipher(block_bit_size=16, key_bit_size=32, number_of_rounds=2) @@ -83,8 +85,8 @@ def test_find_one_xor_differential_trail_with_fixed_weight(): cipher_output = set_fixed_variables( "cipher_output_1_16", "equal", list(range(16)), integer_to_bit_list(0x404A, 16, "big") ) - trail = milp.find_one_xor_differential_trail_with_fixed_weight( - 15, fixed_values=[key, round_0_output, cipher_output] + trail = milp.find_one_xor_differential_trail( + fixed_values=[key, round_0_output, cipher_output], lower_bound=15, upper_bound=15 ) assert trail["total_weight"] == 15.0 # @@ -99,14 +101,28 @@ def test_find_one_xor_differential_trail_with_fixed_weight(): key = set_fixed_variables( component_id=INPUT_KEY, constraint_type="not_equal", bit_positions=range(64), bit_values=(0,) * 64 ) - trail = milp.find_one_xor_differential_trail_with_fixed_weight(5, fixed_values=[key, round_0_output, cipher_output]) + trail = milp.find_one_xor_differential_trail( + fixed_values=[key, round_0_output, cipher_output], lower_bound=5, upper_bound=5 + ) assert trail["total_weight"] == 5.0 def test_find_one_xor_differential_trail_with_weight_at_most(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - trail = MilpXorDifferentialModel(speck).find_one_xor_differential_trail_with_weight_at_most(5) + trail = MilpXorDifferentialModel(speck).find_one_xor_differential_trail(upper_bound=5) assert 0.0 <= trail["total_weight"] <= 5.0 - trail = MilpXorDifferentialModel(speck).find_one_xor_differential_trail_with_weight_at_most(5, 3) + trail = MilpXorDifferentialModel(speck).find_one_xor_differential_trail(lower_bound=3, upper_bound=5) assert 3.0 <= trail["total_weight"] <= 5.0 + + +def test_find_one_xor_differential_trail_with_weight_at_least(): + speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + trail = MilpXorDifferentialModel(speck).find_one_xor_differential_trail(lower_bound=3) + assert trail["total_weight"] >= 3.0 + + +def test_find_one_xor_differential_trail_rejects_invalid_bounds(): + speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + with pytest.raises(ValueError, match="lower_bound must be <= upper_bound"): + MilpXorDifferentialModel(speck).find_one_xor_differential_trail(lower_bound=6, upper_bound=5) diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py index 2885607d7..25f3b3b2d 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py @@ -101,36 +101,48 @@ def test_find_one_xor_linear_trail(): def test_find_one_xor_linear_trail_with_fixed_weight(): # speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) # milp = MilpXorLinearModel(speck) - # trail = milp.find_one_xor_linear_trail_with_fixed_weight(6) + # trail = milp.find_one_xor_linear_trail(lower_bound=6, upper_bound=6) # assert len(trail) == 9 # assert trail["total_weight"] == 6.0 speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1) + trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1) assert len(trail) == 10 assert trail["total_weight"] == 1.0 # # speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=4) # milp = MilpXorLinearModel(speck) - # trail = milp.find_one_xor_linear_trail_with_fixed_weight(10) + # trail = milp.find_one_xor_linear_trail(lower_bound=10, upper_bound=10) # assert len(trail) == 9 # assert trail["total_weight"] == 10.0 def test_find_one_xor_linear_trail_with_weight_at_most(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - trail = MilpXorLinearModel(speck).find_one_xor_linear_trail_with_weight_at_most(6) + trail = MilpXorLinearModel(speck).find_one_xor_linear_trail(upper_bound=6) assert 0.0 <= trail["total_weight"] <= 6.0 - trail = MilpXorLinearModel(speck).find_one_xor_linear_trail_with_weight_at_most(6, 1) + trail = MilpXorLinearModel(speck).find_one_xor_linear_trail(lower_bound=1, upper_bound=6) assert 1.0 <= trail["total_weight"] <= 6.0 +def test_find_one_xor_linear_trail_with_weight_at_least(): + speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + trail = MilpXorLinearModel(speck).find_one_xor_linear_trail(lower_bound=1) + assert trail["total_weight"] >= 1.0 + + +def test_find_one_xor_linear_trail_rejects_invalid_bounds(): + speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + with pytest.raises(ValueError, match="lower_bound must be <= upper_bound"): + MilpXorLinearModel(speck).find_one_xor_linear_trail(lower_bound=7, upper_bound=6) + + def test_find_one_xor_linear_trail_with_fixed_weight_with_external_solver(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="glpk_ext") + trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="glpk_ext") assert len(trail) == 10 assert trail["total_weight"] == 1.0 @@ -139,21 +151,23 @@ def test_find_one_xor_linear_trail_with_fixed_weight_with_supported_but_not_inst with pytest.raises(Exception) as e_info: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="cplex_ext") + trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="cplex_ext") def test_find_one_xor_linear_trail_with_fixed_weight_with_installed_external_solver_but_missing_license(): with pytest.raises(Exception) as e_info: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="Gurobi_ext") + trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="Gurobi_ext") def test_find_one_xor_linear_trail_with_fixed_weight_with_unsupported_external_solver(): with pytest.raises(Exception) as e_info: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail_with_fixed_weight(1, external_solver_name="unsupported_solver") + trail = milp.find_one_xor_linear_trail( + lower_bound=1, upper_bound=1, external_solver_name="unsupported_solver" + ) def test_fix_variables_value_xor_linear_constraints(): From 98dc482ff66074dcb8870593f71b14d5dc0aff6b Mon Sep 17 00:00:00 2001 From: paulhuynh Date: Mon, 21 Sep 2026 23:15:32 +0400 Subject: [PATCH 03/11] rename tests --- .../models/milp/milp_models/milp_xor_differential_model_test.py | 2 +- .../models/milp/milp_models/milp_xor_linear_model_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py index c026b4abe..a747b03a7 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py @@ -107,7 +107,7 @@ def test_find_one_xor_differential_trail_with_fixed_weight(): assert trail["total_weight"] == 5.0 -def test_find_one_xor_differential_trail_with_upper_bound(): +def test_find_one_xor_differential_trail_with_weight_at_most(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) trail = MilpXorDifferentialModel(speck).find_one_xor_differential_trail(upper_bound=5) assert 0.0 <= trail["total_weight"] <= 5.0 diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py index 3d3fa9293..25f3b3b2d 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py @@ -118,7 +118,7 @@ def test_find_one_xor_linear_trail_with_fixed_weight(): # assert trail["total_weight"] == 10.0 -def test_find_one_xor_linear_trail_with_upper_bound(): +def test_find_one_xor_linear_trail_with_weight_at_most(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) trail = MilpXorLinearModel(speck).find_one_xor_linear_trail(upper_bound=6) assert 0.0 <= trail["total_weight"] <= 6.0 From 471907795b57ac7f34051bf439bf2690c580c92a Mon Sep 17 00:00:00 2001 From: paulhuynh Date: Mon, 21 Sep 2026 23:19:36 +0400 Subject: [PATCH 04/11] decrease duplication --- .../milp/milp_models/milp_xor_differential_model.py | 6 +----- .../models/milp/milp_models/milp_xor_linear_model.py | 8 ++------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py index 64bce7910..e75cfa735 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py @@ -605,17 +605,13 @@ def find_one_xor_differential_trail( sage: key = set_fixed_variables('key', 'not_equal', list(range(64)), [0] * 64) sage: trail = milp.find_one_xor_differential_trail(fixed_values=[key]) # random # doctest: +SKIP """ - if lower_bound is not None and upper_bound is not None and lower_bound > upper_bound: - raise ValueError("lower_bound must be <= upper_bound") - start = time.time() self.init_model_in_sage_milp_class(solver_name) self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, weight_precision, fixed_values) - _, constraints = self.weight_range_constraints(lower_bound, upper_bound, weight_precision) - for constraint in constraints: + for constraint in self.weight_range_constraints(lower_bound, upper_bound, weight_precision)[1]: mip.add_constraint(constraint) end = time.time() building_time = end - start diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py index 00c38bfb0..5223aa16d 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py @@ -626,7 +626,7 @@ def find_one_xor_linear_trail( sage: trail['total_weight'] >= 1.0 # doctest: +SKIP True - age: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel + sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) sage: trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=6) # random # doctest: +SKIP @@ -642,17 +642,13 @@ def find_one_xor_linear_trail( sage: key = set_fixed_variables('key', 'not_equal', list(range(32)), [0] * 32) sage: trail = milp.find_one_xor_linear_trail(fixed_values=[key]) # random """ - if lower_bound is not None and upper_bound is not None and lower_bound > upper_bound: - raise ValueError("lower_bound must be <= upper_bound") - start = time.time() self.init_model_in_sage_milp_class(solver_name) self._verbose_print(f"Solver used : {solver_name} (Choose Gurobi for Better performance)") mip = self._model mip.set_objective(None) self.add_constraints_to_build_in_sage_milp_class(-1, weight_precision, fixed_values) - _, constraints = self.weight_range_constraints(lower_bound, upper_bound, weight_precision) - for constraint in constraints: + for constraint in self.weight_range_constraints(lower_bound, upper_bound, weight_precision)[1]: mip.add_constraint(constraint) end = time.time() building_time = end - start From 999125ccf45d59054c3bbe011b5d818e3361aa1b Mon Sep 17 00:00:00 2001 From: paulhuynh Date: Tue, 22 Sep 2026 18:49:01 +0400 Subject: [PATCH 05/11] address sonarqube issues --- .../milp_xor_differential_model_test.py | 3 +- .../milp_models/milp_xor_linear_model_test.py | 29 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py index a747b03a7..9fe35f396 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_differential_model_test.py @@ -124,5 +124,6 @@ def test_find_one_xor_differential_trail_with_weight_at_least(): def test_find_one_xor_differential_trail_rejects_invalid_bounds(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + milp = MilpXorDifferentialModel(speck) with pytest.raises(ValueError, match="lower_bound must be <= upper_bound"): - MilpXorDifferentialModel(speck).find_one_xor_differential_trail(lower_bound=6, upper_bound=5) + milp.find_one_xor_differential_trail(lower_bound=6, upper_bound=5) diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py index 25f3b3b2d..67caf9fbf 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py @@ -135,8 +135,9 @@ def test_find_one_xor_linear_trail_with_weight_at_least(): def test_find_one_xor_linear_trail_rejects_invalid_bounds(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + milp = MilpXorLinearModel(speck) with pytest.raises(ValueError, match="lower_bound must be <= upper_bound"): - MilpXorLinearModel(speck).find_one_xor_linear_trail(lower_bound=7, upper_bound=6) + milp.find_one_xor_linear_trail(lower_bound=7, upper_bound=6) def test_find_one_xor_linear_trail_with_fixed_weight_with_external_solver(): @@ -148,26 +149,24 @@ def test_find_one_xor_linear_trail_with_fixed_weight_with_external_solver(): def test_find_one_xor_linear_trail_with_fixed_weight_with_supported_but_not_installed_external_solver(): - with pytest.raises(Exception) as e_info: - speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="cplex_ext") + speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + milp = MilpXorLinearModel(speck) + with pytest.raises(Exception): + milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="cplex_ext") def test_find_one_xor_linear_trail_with_fixed_weight_with_installed_external_solver_but_missing_license(): - with pytest.raises(Exception) as e_info: - speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="Gurobi_ext") + speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + milp = MilpXorLinearModel(speck) + with pytest.raises(Exception): + milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="Gurobi_ext") def test_find_one_xor_linear_trail_with_fixed_weight_with_unsupported_external_solver(): - with pytest.raises(Exception) as e_info: - speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - milp = MilpXorLinearModel(speck) - trail = milp.find_one_xor_linear_trail( - lower_bound=1, upper_bound=1, external_solver_name="unsupported_solver" - ) + speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) + milp = MilpXorLinearModel(speck) + with pytest.raises(Exception): + milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="unsupported_solver") def test_fix_variables_value_xor_linear_constraints(): From 16f0cfca9142c0f34f47a65ac9b9e73ef255dfdb Mon Sep 17 00:00:00 2001 From: paulhuynh Date: Tue, 22 Sep 2026 18:49:52 +0400 Subject: [PATCH 06/11] remove doctests skips --- .../models/milp/milp_models/milp_xor_linear_model.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py index 5223aa16d..1b2b061a3 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py @@ -615,22 +615,21 @@ def find_one_xor_linear_trail( sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - sage: trail = milp.find_one_xor_linear_trail(upper_bound=6) # random # doctest: +SKIP - sage: 0.0 <= trail['total_weight'] <= 6.0 # doctest: +SKIP + sage: trail = milp.find_one_xor_linear_trail(upper_bound=6) + sage: 0.0 <= trail['total_weight'] <= 6.0 True sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - sage: trail = milp.find_one_xor_linear_trail(lower_bound=1) # random # doctest: +SKIP - sage: trail['total_weight'] >= 1.0 # doctest: +SKIP + sage: trail = milp.find_one_xor_linear_trail(lower_bound=1) True sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) - sage: trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=6) # random # doctest: +SKIP - sage: 1.0 <= trail['total_weight'] <= 6.0 # doctest: +SKIP + sage: trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=6) + sage: 1.0 <= trail['total_weight'] <= 6.0 True # including the key schedule in the model From 8f2c5f9b6154de42ec0b548e5d16db154557c437 Mon Sep 17 00:00:00 2001 From: paulhuynh Date: Tue, 22 Sep 2026 19:15:40 +0400 Subject: [PATCH 07/11] fix doctest --- .../models/milp/milp_models/milp_xor_linear_model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py index 1b2b061a3..d34ac8a41 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py @@ -623,6 +623,7 @@ def find_one_xor_linear_trail( sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) sage: trail = milp.find_one_xor_linear_trail(lower_bound=1) + sage: 1.0 <= trail['total_weight'] True sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel From d855009dd0a091b201d46291ec30e44856aeb20f Mon Sep 17 00:00:00 2001 From: paulhuynh Date: Tue, 22 Sep 2026 19:48:22 +0400 Subject: [PATCH 08/11] fix sonarqube warning --- .../models/milp/milp_models/milp_xor_linear_model_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py index 67caf9fbf..227b6e20b 100644 --- a/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py +++ b/tests/unit/cipher_modules/models/milp/milp_models/milp_xor_linear_model_test.py @@ -1,4 +1,5 @@ import pytest +from sage.numerical.mip import MIPSolverException from claasp.cipher_modules.models.milp.milp_models.milp_xor_linear_model import MilpXorLinearModel from claasp.cipher_modules.models.utils import integer_to_bit_list, set_fixed_variables @@ -151,21 +152,21 @@ def test_find_one_xor_linear_trail_with_fixed_weight_with_external_solver(): def test_find_one_xor_linear_trail_with_fixed_weight_with_supported_but_not_installed_external_solver(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - with pytest.raises(Exception): + with pytest.raises(MIPSolverException): milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="cplex_ext") def test_find_one_xor_linear_trail_with_fixed_weight_with_installed_external_solver_but_missing_license(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - with pytest.raises(Exception): + with pytest.raises(MIPSolverException): milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="Gurobi_ext") def test_find_one_xor_linear_trail_with_fixed_weight_with_unsupported_external_solver(): speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) milp = MilpXorLinearModel(speck) - with pytest.raises(Exception): + with pytest.raises(ValueError): milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=1, external_solver_name="unsupported_solver") From 1fcbb70b4d1f190578dce1de356a1e944fb43dd7 Mon Sep 17 00:00:00 2001 From: paulhuynh Date: Fri, 2 Oct 2026 09:27:36 +0400 Subject: [PATCH 09/11] remove doctests skips --- .../milp_models/milp_xor_differential_model.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py index c4e783592..9d804178b 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_differential_model.py @@ -573,27 +573,27 @@ def find_one_xor_differential_trail( sage: from claasp.cipher_modules.models.milp.milp_models.milp_xor_differential_model import MilpXorDifferentialModel sage: speck = SpeckBlockCipher(number_of_rounds=5) sage: milp = MilpXorDifferentialModel(speck) - sage: trail = milp.find_one_xor_differential_trail() # random # doctest: +SKIP + sage: trail = milp.find_one_xor_differential_trail() # random # single-key setting with upper bound on the weight sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) sage: milp = MilpXorDifferentialModel(speck) - sage: trail = milp.find_one_xor_differential_trail(upper_bound=5) # random # doctest: +SKIP - sage: 0.0 <= trail['total_weight'] <= 5.0 # doctest: +SKIP + sage: trail = milp.find_one_xor_differential_trail(upper_bound=5) # random + sage: 0.0 <= trail['total_weight'] <= 5.0 True # single-key setting with lower bound on the weight sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) sage: milp = MilpXorDifferentialModel(speck) - sage: trail = milp.find_one_xor_differential_trail(lower_bound=3) # random # doctest: +SKIP - sage: trail['total_weight'] >= 3.0 # doctest: +SKIP + sage: trail = milp.find_one_xor_differential_trail(lower_bound=3) # random + sage: trail['total_weight'] >= 3.0 True # single-key setting with bounded weight sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) sage: milp = MilpXorDifferentialModel(speck) - sage: trail = milp.find_one_xor_differential_trail(lower_bound=3, upper_bound=5) # random # doctest: +SKIP - sage: 3.0 <= trail['total_weight'] <= 5.0 # doctest: +SKIP + sage: trail = milp.find_one_xor_differential_trail(lower_bound=3, upper_bound=5) # random + sage: 3.0 <= trail['total_weight'] <= 5.0 True # related-key setting @@ -603,7 +603,7 @@ def find_one_xor_differential_trail( sage: speck = SpeckBlockCipher(number_of_rounds=5) sage: milp = MilpXorDifferentialModel(speck) sage: key = set_fixed_variables('key', 'not_equal', list(range(64)), [0] * 64) - sage: trail = milp.find_one_xor_differential_trail(fixed_values=[key]) # random # doctest: +SKIP + sage: trail = milp.find_one_xor_differential_trail(fixed_values=[key]) # random """ start = time.time() self.init_model_in_sage_milp_class(solver_name) From b398555f031bb6c908a7b48fbf276edef1f88523 Mon Sep 17 00:00:00 2001 From: paulhuynh Date: Fri, 2 Oct 2026 10:23:49 +0400 Subject: [PATCH 10/11] address sonarcloud issue --- claasp/cipher_modules/models/milp/milp_model.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/claasp/cipher_modules/models/milp/milp_model.py b/claasp/cipher_modules/models/milp/milp_model.py index b0c90c29d..2b1210b55 100644 --- a/claasp/cipher_modules/models/milp/milp_model.py +++ b/claasp/cipher_modules/models/milp/milp_model.py @@ -221,10 +221,12 @@ def fix_variables_value_constraints(self, fixed_variables=[]): a = x[f"{comp_id}_{pos}"] b = x[f"{var_vals[i][0]}_{var_vals[i][1]}"] - constraints.append(lhs <= a + b) - constraints.append(lhs >= a - b) - constraints.append(lhs >= b - a) - constraints.append(lhs + a + b <= 2) + constraints.extend([ + lhs <= a + b, + lhs >= a - b, + lhs >= b - a, + lhs + a + b <= 2, + ]) constraints.append(sum(x[f"{comp_id}{p}_not_equal_{n_trails}"] for p in bit_pos) >= 1) else: From bea97b06c1c8be9bb1547c31fc858220228acc23 Mon Sep 17 00:00:00 2001 From: paulhuynh Date: Tue, 20 Oct 2026 16:56:07 +0400 Subject: [PATCH 11/11] Add ellipses to doctests to remove Highs logging messages --- .../models/milp/milp_models/milp_xor_linear_model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py index 80d48f681..022da59dd 100644 --- a/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py +++ b/claasp/cipher_modules/models/milp/milp_models/milp_xor_linear_model.py @@ -634,6 +634,7 @@ def find_one_xor_linear_trail( sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) sage: trail = milp.find_one_xor_linear_trail(upper_bound=6) + ... sage: 0.0 <= trail['total_weight'] <= 6.0 True @@ -641,6 +642,7 @@ def find_one_xor_linear_trail( sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) sage: trail = milp.find_one_xor_linear_trail(lower_bound=1) + ... sage: 1.0 <= trail['total_weight'] True @@ -648,6 +650,7 @@ def find_one_xor_linear_trail( sage: from claasp.ciphers.block_ciphers.speck_block_cipher import SpeckBlockCipher sage: speck = SpeckBlockCipher(block_bit_size=32, key_bit_size=64, number_of_rounds=2) sage: trail = milp.find_one_xor_linear_trail(lower_bound=1, upper_bound=6) + ... sage: 1.0 <= trail['total_weight'] <= 6.0 True