Repository: OpenMined/PySyft
Pull request: Parameters validation for the FV scheme
## Description
closes #3792 
Checks basic properties that the parameters should have while setting up, and tests the generated prime nos if those are correct.

## Affected Dependencies
None

## How has this been tested?
This code does not need to have dedicated tests and it is verified by already present tests.

## Checklist
- [x] I have followed the [Contribution Guidelines](https://github.com/OpenMined/.github/blob/master/CONTRIBUTING.md) and [Code of Conduct](https://github.com/OpenMined/.github/blob/master/CODE_OF_CONDUCT.md)
- [x] I have commented my code following the [OpenMined Styleguide](https://github.com/OpenMined/.github/blob/master/STYLEGUIDE.md)
- [x] I have labeled this PR with the relevant [Type labels](https://github.com/OpenMined/.github/labels?q=Type%3A)
- [x] My changes are covered by tests


Problem statement:
Implement parameter validation for FV scheme. 
## Feature Description
Need to implement a logic to check the parameter value used in the FV scheme. 
I am not very clear at the moment about its implementation.

## Is your feature request related to a problem?
Parameter selection for the FV scheme is really important, Improper values as parameter lead to wrong results.

## What alternatives have you considered?
Need to think...

Hints from issue discussion:

Commit message: basic param validation before setting

Patch to review:
diff --git a/syft/frameworks/torch/he/fv/context.py b/syft/frameworks/torch/he/fv/context.py
index c4dda8378f8..9c5c9332094 100644
--- a/syft/frameworks/torch/he/fv/context.py
+++ b/syft/frameworks/torch/he/fv/context.py
@@ -1,4 +1,12 @@
+import math
+from functools import reduce
 from syft.frameworks.torch.he.fv.util.rns_tool import RNSTool
+from syft.frameworks.torch.he.fv.util.global_variable import COEFF_MOD_COUNT_MAX
+from syft.frameworks.torch.he.fv.util.global_variable import COEFF_MOD_COUNT_MIN
+from syft.frameworks.torch.he.fv.util.global_variable import COEFF_MOD_BIT_COUNT_MAX
+from syft.frameworks.torch.he.fv.util.global_variable import COEFF_MOD_BIT_COUNT_MIN
+from syft.frameworks.torch.he.fv.util.global_variable import POLY_MOD_DEGREE_MAX
+from syft.frameworks.torch.he.fv.util.global_variable import POLY_MOD_DEGREE_MIN
 
 
 class Context:
@@ -12,14 +20,68 @@ class Context:
         rns_tool: A RNSTool class instance.
     """
 
-    def __init__(self, encryption_param):
+    def __init__(self, params):
 
-        self.param = encryption_param
+        # Validation of params provided for the encryption scheme.
+        self.validate(params)
+
+        self.param = params
+
+        self.rns_tool = RNSTool(params)
+
+    def validate(self, params):
+
+        # The number of coeff moduli is restricted to 62
+        if (
+            len(params.coeff_modulus) > COEFF_MOD_COUNT_MAX
+            or len(params.coeff_modulus) < COEFF_MOD_COUNT_MIN
+        ):
+            raise RuntimeError("Invalid coefficient modulus counts")
+
+        # Check for the range of coefficient modulus primes.
+        for i in range(len(params.coeff_modulus)):
+            if params.coeff_modulus[i] >> COEFF_MOD_BIT_COUNT_MAX or not (
+                params.coeff_modulus[i] >> (COEFF_MOD_BIT_COUNT_MIN - 1)
+            ):
+                raise RuntimeError("Invalid coefficient modulus values")
+
+            # Check for the relative prime of coefficient modulus primes
+            for j in range(i):
+                if math.gcd(params.coeff_modulus[i], params.coeff_modulus[j]) > 1:
+                    raise RuntimeError("Coefficient modulus are not pairwise relatively prime")
+
+        # Compute the product of all coeff moduli
+        total_coeff_modulus = reduce(lambda x, y: x * y, params.coeff_modulus)
+        # total_coeff_modulus_bit_count = get_significant_count(total_coeff_modulus)
+
+        # Check polynomial modulus degree and create poly_modulus
+        poly_modulus_degree = params.poly_modulus
+        coeff_count_power = poly_modulus_degree & (poly_modulus_degree - 1)
+        if (
+            poly_modulus_degree < POLY_MOD_DEGREE_MIN
+            or poly_modulus_degree > POLY_MOD_DEGREE_MAX
+            or coeff_count_power < 0
+        ):
+            raise RuntimeError("Invalid polynomial modulus value")
+
+        # TODO Check if the parameters are secure according to HomomorphicEncryption.org standards
+
+        if params.plain_modulus >> COEFF_MOD_BIT_COUNT_MAX or not (
+            params.plain_modulus >> (COEFF_MOD_COUNT_MIN - 1)
+        ):
+            raise RuntimeError("Invalid plain modulus values")
+
+        if params.plain_modulus > total_coeff_modulus:
+            raise RuntimeError("Plain modulus cannot be greater than total coefficient modulus")
+
+        # Check all coeff moduli are relatively prime to plain_modulus
+        for i in range(len(params.coeff_modulus)):
+            for j in range(i):
+                if math.gcd(params.coeff_modulus[i], params.plain_modulus) > 1:
+                    raise RuntimeError(
+                        "Coefficient modulus are not relatively prime with plain modulus"
+                    )
 
         # A list containing values of coeff_mod[i] / plain_mod for one time computation
         # and useful in encryption process.
-        self.coeff_div_plain_modulus = [
-            x / encryption_param.plain_modulus for x in encryption_param.coeff_modulus
-        ]
-
-        self.rns_tool = RNSTool(encryption_param)
+        self.coeff_div_plain_modulus = [x / params.plain_modulus for x in params.coeff_modulus]
diff --git a/syft/frameworks/torch/he/fv/modulus.py b/syft/frameworks/torch/he/fv/modulus.py
index b0a88894934..b319dd402ae 100644
--- a/syft/frameworks/torch/he/fv/modulus.py
+++ b/syft/frameworks/torch/he/fv/modulus.py
@@ -5,6 +5,9 @@
 from syft.frameworks.torch.he.fv.util.global_variable import DEFAULT_C0EFF_MODULUS_128
 from syft.frameworks.torch.he.fv.util.global_variable import DEFAULT_C0EFF_MODULUS_192
 from syft.frameworks.torch.he.fv.util.global_variable import DEFAULT_C0EFF_MODULUS_256
+from syft.frameworks.torch.he.fv.util.fv_std_param import FV_STD_PARMS_128_TC
+from syft.frameworks.torch.he.fv.util.fv_std_param import FV_STD_PARMS_192_TC
+from syft.frameworks.torch.he.fv.util.fv_std_param import FV_STD_PARMS_256_TC
 
 
 class SeqLevelType(Enum):
@@ -18,6 +21,32 @@ class SeqLevelType(Enum):
 
 
 class CoeffModulus:
+    def max_bit_count(self, poly_modulus_degree, seq_level=SeqLevelType.TC128):
+        """Returns the largest bit-length of the coefficient modulus, i.e., bit-length
+        of the product of the primes in the coefficient modulus, that guarantees
+        a given security level when using a given poly_modulus_degree, according
+        to the HomomorphicEncryption.org security standard.
+        Args:
+            poly_modulus_degree: The value of the poly_modulus_degree
+            encryption parameter
+            seq_level: (optional) The desired standard security level
+        Returns:
+            A integer denoting the largest allowed bit counts for coeff_modulus.
+        Raises:
+            ValueError: seq_level does not match with any standard security level.
+        """
+
+        if seq_level == SeqLevelType.TC128:
+            return FV_STD_PARMS_128_TC(poly_modulus_degree)
+
+        if seq_level == SeqLevelType.TC192:
+            return FV_STD_PARMS_192_TC(poly_modulus_degree)
+
+        if seq_level == SeqLevelType.TC256:
+            return FV_STD_PARMS_256_TC(poly_modulus_degree)
+
+        raise ValueError(f"{seq_level} is not a valid standard security level")
+
     def bfv_default(self, poly_modulus_degree, seq_level=SeqLevelType.TC128):
         """Provide secure coefficient modulus for BFV scheme that guarantees
         a given security level when used with a given polynomial modulus,
diff --git a/syft/frameworks/torch/he/fv/util/fv_std_param.py b/syft/frameworks/torch/he/fv/util/fv_std_param.py
new file mode 100644
index 00000000000..e0dc0adbd00
--- /dev/null
+++ b/syft/frameworks/torch/he/fv/util/fv_std_param.py
@@ -0,0 +1,21 @@
+""" Largest allowed bit counts for coeff_modulus based on the security estimates from
+    HomomorphicEncryption.org security standard. Scheme samples the secret key
+    from a ternary {-1, 0, 1} distribution."""
+
+
+# Ternary secret: 128 bits classical security
+def FV_STD_PARMS_128_TC(poly_modulus_degree):
+    _dict = {1024: 27, 2048: 54, 4096: 109, 8192: 218, 16384: 438, 32768: 881}
+    return _dict.get(poly_modulus_degree, 0)
+
+
+# Ternary secret: 192 bits classical security
+def FV_STD_PARMS_192_TC(poly_modulus_degree):
+    _dict = {1024: 19, 2048: 37, 4096: 75, 8192: 152, 16384: 305, 32768: 611}
+    return _dict.get(poly_modulus_degree, 0)
+
+
+# Ternary secret; 256 bits classical security
+def FV_STD_PARMS_256_TC(poly_modulus_degree):
+    _dict = {1024: 14, 2048: 29, 4096: 58, 8192: 118, 16384: 237, 32768: 476}
+    return _dict.get(poly_modulus_degree, 0)
diff --git a/syft/frameworks/torch/he/fv/util/global_variable.py b/syft/frameworks/torch/he/fv/util/global_variable.py
index 2032525ad94..c0331d41a25 100644
--- a/syft/frameworks/torch/he/fv/util/global_variable.py
+++ b/syft/frameworks/torch/he/fv/util/global_variable.py
@@ -156,6 +156,16 @@
 
 gamma = 0x1FFFFFFFFFC80001
 
+
+COEFF_MOD_COUNT_MIN = 1
+COEFF_MOD_COUNT_MAX = 62
+
+COEFF_MOD_BIT_COUNT_MIN = 2
+COEFF_MOD_BIT_COUNT_MAX = 60
+
+POLY_MOD_DEGREE_MIN = 2
+POLY_MOD_DEGREE_MAX = 32768
+
 NOISE_STANDARD_DEVIATION = 3.20
 NOISE_DISTRIBUTION_WIDTH_MULTIPLIER = 6
 NOISE_MAX_DEVIATION = NOISE_DISTRIBUTION_WIDTH_MULTIPLIER * NOISE_STANDARD_DEVIATION
diff --git a/test/torch/tensors/test_fv.py b/test/torch/tensors/test_fv.py
index deb23f1f796..b92926ef547 100644
--- a/test/torch/tensors/test_fv.py
+++ b/test/torch/tensors/test_fv.py
@@ -252,20 +252,10 @@ def test_mul_ct_sk(poly_mod, coeff_mod, plain_mod, sk, ct, result):
 
 @pytest.mark.parametrize(
     "plain_modulus, value",
-    [
-        (0xFFFFFFFFFFFFFFF, 1),
-        (0xFFFFFFFFFFFFFFF, 2),
-        (0xFFFFFFFFFFFFFFF, -3),
-        (0xFFFFFFFFFFFFFFF, 64),
-        (0xFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF),
-        (0xFFFFFFFFFFFFFFF, 0x80F02),
-        (1024, 64),
-    ],
+    [(128, 1), (64, 2), (32, -3), (256, 64), (128, 0xFFFFFFFFFFFFFFFF), (128, 0x80F02), (128, 64)],
 )
 def test_integer_encoder(plain_modulus, value):
-    enc_param = EncryptionParams(
-        16, CoeffModulus().create(plain_modulus, [100, 100, 100]), plain_modulus
-    )
+    enc_param = EncryptionParams(16, CoeffModulus().create(16, [30]), plain_modulus)
     ctx = Context(enc_param)
     encoder = IntegerEncoder(ctx)
     poly = encoder.encode(value)
@@ -592,7 +582,7 @@ def test_fv_sub_cipher_plain(int1, int2):
     ],
 )
 def test_fv_mul_cipher_cipher(int1, int2):
-    ctx = Context(EncryptionParams(64, CoeffModulus().create(64, [40]), 64))
+    ctx = Context(EncryptionParams(1024, CoeffModulus().create(1024, [40, 40]), 1024))
     keys = KeyGenerator(ctx).keygen()
     encoder = IntegerEncoder(ctx)
     encryptor = Encryptor(ctx, keys[1])  # keys[1] = public_key


Code review comments:
diff_hunk: @@ -12,14 +20,68 @@ class Context:
         rns_tool: A RNSTool class instance.
     """
 
-    def __init__(self, encryption_param):
+    def __init__(self, params):
 
-        self.param = encryption_param
+        # Validation of params provided for the encryption scheme.
+        self.validate(params)
+
+        self.param = params
+
+        self.rns_tool = RNSTool(params)
+
+    def validate(self, params):
+
+        # The number of coeff moduli is restricted to 62
+        if (
+            len(params.coeff_modulus) > COEFF_MOD_COUNT_MAX
+            or len(params.coeff_modulus) < COEFF_MOD_COUNT_MIN
+        ):
+            raise RuntimeError("Invalid coefficient modulus counts")
path: syft/frameworks/torch/he/fv/context.py
text: @user1:
Can you specify in the error message the range of the coeffecients allowed and how many was passed?

diff_hunk: @@ -12,14 +20,68 @@ class Context:
         rns_tool: A RNSTool class instance.
     """
 
-    def __init__(self, encryption_param):
+    def __init__(self, params):
 
-        self.param = encryption_param
+        # Validation of params provided for the encryption scheme.
+        self.validate(params)
+
+        self.param = params
+
+        self.rns_tool = RNSTool(params)
+
+    def validate(self, params):
+
+        # The number of coeff moduli is restricted to 62
+        if (
+            len(params.coeff_modulus) > COEFF_MOD_COUNT_MAX
+            or len(params.coeff_modulus) < COEFF_MOD_COUNT_MIN
+        ):
+            raise RuntimeError("Invalid coefficient modulus counts")
+
+        # Check for the range of coefficient modulus primes.
+        for i in range(len(params.coeff_modulus)):
+            if params.coeff_modulus[i] >> COEFF_MOD_BIT_COUNT_MAX or not (
+                params.coeff_modulus[i] >> (COEFF_MOD_BIT_COUNT_MIN - 1)
+            ):
+                raise RuntimeError("Invalid coefficient modulus values")
+
+            # Check for the relative prime of coefficient modulus primes
+            for j in range(i):
+                if math.gcd(params.coeff_modulus[i], params.coeff_modulus[j]) > 1:
+                    raise RuntimeError("Coefficient modulus are not pairwise relatively prime")
+
+        # Compute the product of all coeff moduli
+        total_coeff_modulus = reduce(lambda x, y: x * y, params.coeff_modulus)
+        # total_coeff_modulus_bit_count = get_significant_count(total_coeff_modulus)
+
+        # Check polynomial modulus degree and create poly_modulus
+        poly_modulus_degree = params.poly_modulus
+        coeff_count_power = poly_modulus_degree & (poly_modulus_degree - 1)
+        if (
+            poly_modulus_degree < POLY_MOD_DEGREE_MIN
+            or poly_modulus_degree > POLY_MOD_DEGREE_MAX
+            or coeff_count_power < 0
+        ):
+            raise RuntimeError("Invalid polynomial modulus value")
+
+        # TODO Check if the parameters are secure according to HomomorphicEncryption.org standards
+
+        if params.plain_modulus >> COEFF_MOD_BIT_COUNT_MAX or not (
+            params.plain_modulus >> (COEFF_MOD_COUNT_MIN - 1)
+        ):
+            raise RuntimeError("Invalid plain modulus values")
+
+        if params.plain_modulus > total_coeff_modulus:
+            raise RuntimeError("Plain modulus cannot be greater than total coefficient modulus")
+
+        # Check all coeff moduli are relatively prime to plain_modulus
+        for i in range(len(params.coeff_modulus)):
+            for j in range(i):
+                if math.gcd(params.coeff_modulus[i], params.plain_modulus) > 1:
path: syft/frameworks/torch/he/fv/context.py
text: @user1:
Why do we need the j loop here?

@author:
By mistake, I copied the above loop and forget to remove the j loop😅

diff_hunk: @@ -12,14 +20,68 @@ class Context:
         rns_tool: A RNSTool class instance.
     """
 
-    def __init__(self, encryption_param):
+    def __init__(self, params):
 
-        self.param = encryption_param
+        # Validation of params provided for the encryption scheme.
+        self.validate(params)
+
+        self.param = params
+
+        self.rns_tool = RNSTool(params)
+
+    def validate(self, params):
+
+        # The number of coeff moduli is restricted to 62
+        if (
+            len(params.coeff_modulus) > COEFF_MOD_COUNT_MAX
+            or len(params.coeff_modulus) < COEFF_MOD_COUNT_MIN
+        ):
+            raise RuntimeError("Invalid coefficient modulus counts")
+
+        # Check for the range of coefficient modulus primes.
+        for i in range(len(params.coeff_modulus)):
+            if params.coeff_modulus[i] >> COEFF_MOD_BIT_COUNT_MAX or not (
path: syft/frameworks/torch/he/fv/context.py
text: @user1:
Maybe 
```
params.coeff_modulus[i] > 2**COEFF_MOD_BIT_COUNT_MAX
```
Is clearer? I am always nervous about operator precedence. :D 

@author:
But, bitwise operators will be much better for performance than exponentiation.

diff_hunk: @@ -12,14 +20,68 @@ class Context:
         rns_tool: A RNSTool class instance.
     """
 
-    def __init__(self, encryption_param):
+    def __init__(self, params):
 
-        self.param = encryption_param
+        # Validation of params provided for the encryption scheme.
+        self.validate(params)
+
+        self.param = params
+
+        self.rns_tool = RNSTool(params)
+
+    def validate(self, params):
+
+        # The number of coeff moduli is restricted to 62
+        if (
+            len(params.coeff_modulus) > COEFF_MOD_COUNT_MAX
+            or len(params.coeff_modulus) < COEFF_MOD_COUNT_MIN
+        ):
+            raise RuntimeError("Invalid coefficient modulus counts")
+
+        # Check for the range of coefficient modulus primes.
+        for i in range(len(params.coeff_modulus)):
+            if params.coeff_modulus[i] >> COEFF_MOD_BIT_COUNT_MAX or not (
+                params.coeff_modulus[i] >> (COEFF_MOD_BIT_COUNT_MIN - 1)
+            ):
+                raise RuntimeError("Invalid coefficient modulus values")
+
+            # Check for the relative prime of coefficient modulus primes
+            for j in range(i):
+                if math.gcd(params.coeff_modulus[i], params.coeff_modulus[j]) > 1:
+                    raise RuntimeError("Coefficient modulus are not pairwise relatively prime")
+
+        # Compute the product of all coeff moduli
+        total_coeff_modulus = reduce(lambda x, y: x * y, params.coeff_modulus)
+        # total_coeff_modulus_bit_count = get_significant_count(total_coeff_modulus)
path: syft/frameworks/torch/he/fv/context.py
text: @user1:
Shouldn't we also verify that the total_coeff_modulus is good for our n-bit security level?

@author:
I have added the code required for the security level checking. but It is currently commented with TODO because I would need to work on how to ask for the security level and if passed then it can be checked otherwise scheme will work without standard security.