Repository: OpenMined/PySyft
Pull request: Grid clients serialisation correction
## Description
Register `DataCentricFLClient` in serde for serialization.

## Affected Dependencies
`DataCentricFLClient` cannot be serialised currently, as it's not registered in serde.
which causes error while doing SMPC as sharing of tensor raises the error during serialization.
Code : 
`x = torch.tensor([1,2,3,4,5])
x = x.send(bob)
x = x.share(bob,alice)`

Error:
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-66c2dc1e8ea9> in <module>
      3 
      4 x = x.send(bob)
----> 5 x = x.share(bob,alice)

~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/frameworks/torch/tensors/interpreters/native.py in share(self, protocol, field, dtype, crypto_provider, requires_grad, no_wrap, *owners)
    916                 {"requires_grad": requires_grad} if isinstance(chain, syft.PointerTensor) else {}
    917             )
--> 918             shared_tensor = chain.share(
    919                 *owners,
    920                 protocol=protocol,

~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/generic/pointers/pointer_tensor.py in share(self, *args, **kwargs)
    382             raise RuntimeError("Error, share must have > 1 arguments all of type syft.workers")
    383 
--> 384         response = self.owner.send_command(self.location, "share", self, args, kwargs)
    385         return response
    386 

~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/workers/base.py in send_command(self, recipient, cmd_name, target, args_, kwargs_, return_ids, return_value)
    514                 cmd_name, target, args_, kwargs_, return_ids, return_value
    515             )
--> 516             ret_val = self.send_msg(message, location=recipient)
    517         except ResponseSignatureError as e:
    518             ret_val = None

~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/workers/base.py in send_msg(self, message, location)
    312 
    313         # Step 1: serialize the message to a binary
--> 314         bin_message = sy.serde.serialize(message, worker=self)
    315 
    316         # Step 2: send the message and wait for a response

~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/serde/serde.py in serialize(obj, worker, simplified, force_full_simplification, strategy)
     43         strategy = serialize
     44 
---> 45     return strategy(obj, worker, simplified, force_full_simplification)
     46 
     47 

~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/serde/msgpack/serde.py in serialize(obj, worker, simplified, force_full_simplification)
    335 
    336     simple_objects = _serialize_msgpack_simple(obj, worker, simplified, force_full_simplification)
--> 337     return _serialize_msgpack_binary(simple_objects)
    338 
    339 

~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/serde/msgpack/serde.py in _serialize_msgpack_binary(simple_objects, worker, simplified, force_full_simplification)
    289     # 2) Serialize
    290     # serialize into a binary
--> 291     binary = msgpack_lib.dumps(simple_objects)
    292 
    293     # 3) Compress

~/anaconda3/envs/pysyft/lib/python3.8/site-packages/msgpack/__init__.py in packb(o, **kwargs)
     33     See :class:`Packer` for options.
     34     """
---> 35     return Packer(**kwargs).pack(o)
     36 
     37 

msgpack/_packer.pyx in msgpack._cmsgpack.Packer.pack()

msgpack/_packer.pyx in msgpack._cmsgpack.Packer.pack()

msgpack/_packer.pyx in msgpack._cmsgpack.Packer.pack()

msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()

msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()

msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()

msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()

msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()

msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()

msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()

TypeError: can not serialize 'DataCentricFLClient' object
```










Problem statement:
Serde Can not serialize 'DynamicFLClient' object
Cannot share tensor from reference pointer

** This issue is critical for SyferText, since it breaks functionality when working with Grid nodes **
## How to Reproduce

Nodes Creation :
`python -m gridnode --id=bob  --host=localhost --port 3001`
`python -m gridnode --id=alice  --host=localhost --port 3002`
`python -m gridnode --id=bill  --host=localhost --port 3003`

```
bob = DynamicFLClient(hook, "ws://localhost:3001/")
alice = DynamicFLClient(hook, "ws://localhost:3002")
bill = DynamicFLClient(hook, "ws://localhost:3003")
my_grid = sy.PrivateGridNetwork(bob,alice, bill)
t = th.Tensor([4,6]).send(alice)
t_shared = t.fix_prec().share(bob, alice, crypto_provider=bill)
t_shared.get()
```

## Expected Behavior

A clear and concise description of what you expected to happen.

## Error

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-4e56dcdd7ee1> in <module>
      4 my_grid = sy.PrivateGridNetwork(bob,alice, bill)
      5 t = th.Tensor([4,6]).send(alice)
----> 6 t_shared = t.fix_prec().share(bob, alice, crypto_provider=bill )
      7 t_shared.get()
~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/frameworks/torch/tensors/interpreters/native.py in share(self, protocol, field, dtype, crypto_provider, requires_grad, no_wrap, *owners)
    916                 {"requires_grad": requires_grad} if isinstance(chain, syft.PointerTensor) else {}
    917             )
--> 918             shared_tensor = chain.share(
    919                 *owners,
    920                 protocol=protocol,
~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/generic/pointers/pointer_tensor.py in share(self, *args, **kwargs)
    380             raise RuntimeError("Error, share must have > 1 arguments all of type syft.workers")
    381 
--> 382         response = self.owner.send_command(self.location, "share", self, args, kwargs)
    383         return response
    384 
~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/workers/base.py in send_command(self, recipient, cmd_name, target, args_, kwargs_, return_ids, return_value)
    486                 cmd_name, target, args_, kwargs_, return_ids, return_value
    487             )
--> 488             ret_val = self.send_msg(message, location=recipient)
    489         except ResponseSignatureError as e:
    490             ret_val = None
~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/workers/base.py in send_msg(self, message, location)
    304 
    305         # Step 1: serialize the message to a binary
--> 306         bin_message = sy.serde.serialize(message, worker=self)
    307 
    308         # Step 2: send the message and wait for a response
~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/serde/serde.py in serialize(obj, worker, simplified, force_full_simplification, strategy)
     43         strategy = serialize
     44 
---> 45     return strategy(obj, worker, simplified, force_full_simplification)
     46 
     47 
~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/serde/msgpack/serde.py in serialize(obj, worker, simplified, force_full_simplification)
    335 
    336     simple_objects = _serialize_msgpack_simple(obj, worker, simplified, force_full_simplification)
--> 337     return _serialize_msgpack_binary(simple_objects)
    338 
    339 
~/anaconda3/envs/pysyft/lib/python3.8/site-packages/syft/serde/msgpack/serde.py in _serialize_msgpack_binary(simple_objects, worker, simplified, force_full_simplification)
    289     # 2) Serialize
    290     # serialize into a binary
--> 291     binary = msgpack_lib.dumps(simple_objects)
    292 
    293     # 3) Compress
~/anaconda3/envs/pysyft/lib/python3.8/site-packages/msgpack/__init__.py in packb(o, **kwargs)
     33     See :class:`Packer` for options.
     34     """
---> 35     return Packer(**kwargs).pack(o)
     36 
     37 
msgpack/_packer.pyx in msgpack._cmsgpack.Packer.pack()
msgpack/_packer.pyx in msgpack._cmsgpack.Packer.pack()
msgpack/_packer.pyx in msgpack._cmsgpack.Packer.pack()
msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()
msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()
msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()
msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()
msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()
msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()
msgpack/_packer.pyx in msgpack._cmsgpack.Packer._pack()
TypeError: can not serialize 'DynamicFLClient' object
```

## System Information
 - OS: Ubuntu
 - Language Version: Python 3.7
- Pysyft Latest master branch

## Additional Context
Add any other context about the problem here.

Hints from issue discussion:
Should we open this issue in PySyft @AlanAboudib @Nilanshrajput ?
`DynamicFLClient` is defined in PySyft, here :point_right: https://github.com/OpenMined/PySyft/blob/f692b992fe14fab7fe5c2006d087a683a856d914/syft/grid/clients/dynamic_fl_client.py#L16
Done @sachin-101 
CC: @IonesioJunior - thoughts on this? Could use your help since it's in the syft.grid package.
It seems like #3857 is for a similar issue. cc @IonesioJunior @AlanAboudib 
Sorry for the delay! It looks like a problem that we [solved ](https://github.com/OpenMined/PyGridNode/pull/7) before. But both PySyft and PyGrid have gone through several refactoring since then, probably something has changed during the process. I will check it when I get some time.

Commit message: lint error resolve

Patch to review:
diff --git a/syft/grid/clients/data_centric_fl_client.py b/syft/grid/clients/data_centric_fl_client.py
index 38e87a7209f..053fae4e012 100644
--- a/syft/grid/clients/data_centric_fl_client.py
+++ b/syft/grid/clients/data_centric_fl_client.py
@@ -2,15 +2,19 @@
 import requests
 from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
 
-from typing import Union
+from typing import Union, Dict
 from urllib.parse import urlparse
+import pickle
+import torch
 
 # Syft imports
+import syft as sy
 from syft.serde import serialize
 from syft.version import __version__
 from syft.execution.plan import Plan
 from syft.codes import REQUEST_MSG, RESPONSE_MSG
 from syft.workers.websocket_client import WebsocketClientWorker
+from syft.workers.abstract import AbstractWorker
 
 
 class DataCentricFLClient(WebsocketClientWorker):
@@ -188,7 +192,6 @@ def serve_model(
         allow_remote_inference: bool = False,
     ):
         """ Hosts the model and optionally serve it using a Socket / Rest API.
-
         Args:
             model : A jit model or Syft Plan.
             model_id (str): An integer/string representing the model id.
@@ -251,7 +254,6 @@ def progress_callback(monitor):
 
     def run_remote_inference(self, model_id, data):
         """ Run a dataset inference using a remote model.
-
         Args:
             model_id (str) : Model ID.
             data (Tensor) : dataset to be inferred.
@@ -272,7 +274,6 @@ def run_remote_inference(self, model_id, data):
 
     def delete_model(self, model_id: str) -> bool:
         """ Delete a model previously registered.
-
         Args:
             model_id (String) : ID of the model that will be deleted.
         Returns:
@@ -284,3 +285,77 @@ def delete_model(self, model_id: str) -> bool:
 
     def __str__(self) -> str:
         return f"<Federated Worker id:{self.id}>"
+
+    @staticmethod
+    def simplify(worker: AbstractWorker, data_centric_fl_client: "DataCentricFLClient") -> tuple:
+
+        # Simplify the attributes for DataCentricFLClient
+        address = pickle.dumps(data_centric_fl_client.address)
+        id = pickle.dumps(data_centric_fl_client.id)
+        is_client_worker = pickle.dumps(data_centric_fl_client.is_client_worker)
+        log_msgs = pickle.dumps(data_centric_fl_client.log_msgs)
+        verbose = pickle.dumps(data_centric_fl_client.verbose)
+        encoding = pickle.dumps(data_centric_fl_client.encoding)
+        timeout = pickle.dumps(data_centric_fl_client.timeout)
+
+        return (address, id, is_client_worker, log_msgs, verbose, encoding, timeout)
+
+    @staticmethod
+    def detail(
+        worker: AbstractWorker, client_tuple: tuple
+    ) -> Union["DataCentricFLClient", int, str]:
+
+        address, id, is_client_worker, log_msgs, verbose, encoding, timeout = client_tuple
+
+        # detail client attributes
+        address = pickle.loads(address)
+        id = pickle.loads(id)
+        is_client_worker = pickle.loads(is_client_worker)
+        log_msgs = pickle.loads(log_msgs)
+        verbose = pickle.loads(verbose)
+        encoding = pickle.loads(encoding)
+        timeout = pickle.loads(timeout)
+
+        # create torch hook
+        hook = sy.TorchHook(torch)
+        me = hook.local_worker
+
+        # if worker with same id exist return that worker 2 worker with same id raises error
+        if id in me._known_workers.keys():
+            return me._known_workers[id]
+
+        Client = DataCentricFLClient(
+            hook, address, id, is_client_worker, log_msgs, verbose, encoding, timeout
+        )
+
+        return Client
+
+    @staticmethod
+    def get_msgpack_code() -> Dict[str, int]:
+        """This is the implementation of the `get_msgpack_code()`
+        method required by PySyft's SyftSerializable class.
+        It provides a code for msgpack if the type is not present in proto.json.
+        The returned object should be similar to:
+        {
+            "code": int value,
+            "forced_code": int value
+        }
+        Both keys are optional, the common and right way would be to add only the "code" key.
+        Returns:
+            dict: A dict with the "code" and/or "forced_code" keys.
+        """
+
+        # If a msgpack code is not already generated, then generate one
+        # the code is hash of class name
+        if not hasattr(DataCentricFLClient, "proto_id"):
+            DataCentricFLClient.proto_id = sy.serde.msgpack.serde.msgpack_code_generator(
+                DataCentricFLClient.__qualname__
+            )
+
+        code_dict = {}
+        code_dict["code"] = DataCentricFLClient.proto_id
+        code_dict["forced_code"] = sy.serde.msgpack.serde.msgpack_code_generator(
+            DataCentricFLClient.__qualname__ + "forced"
+        )
+
+        return code_dict
diff --git a/syft/serde/msgpack/serde.py b/syft/serde/msgpack/serde.py
index 624c28f2109..b26a1b1d957 100644
--- a/syft/serde/msgpack/serde.py
+++ b/syft/serde/msgpack/serde.py
@@ -33,6 +33,7 @@
 from collections import OrderedDict
 import inspect
 from dataclasses import dataclass
+import hashlib
 
 import syft
 import msgpack as msgpack_lib
@@ -519,4 +520,16 @@ def _detail(worker: AbstractWorker, obj: object, **kwargs) -> object:
         return obj
 
 
+def msgpack_code_generator(class_name: str) -> int:
+    """Generates and returns a unique msgpack code.
+    Args:
+    class_name: The name of class for which the msgpack code is required.
+    Returns:
+        An integer to serve as a msgpack serialization code.
+    """
+
+    # the code Msgpack is generated as the hash of class name(first 8 digit of hash being used)
+    return int(hashlib.sha1(class_name.encode("utf-8")).hexdigest(), 16) % (10 ** 8)
+
+
 msgpack_global_state = MsgpackGlobalState()


Code review comments:
diff_hunk: @@ -284,3 +285,77 @@ def delete_model(self, model_id: str) -> bool:
 
     def __str__(self) -> str:
         return f"<Federated Worker id:{self.id}>"
+
+    @staticmethod
+    def simplify(worker: AbstractWorker, data_centric_fl_client: "DataCentricFLClient") -> tuple:
+
+        # Simplify the attributes for DataCentricFLClient
+        address = pickle.dumps(data_centric_fl_client.address)
+        id = pickle.dumps(data_centric_fl_client.id)
+        is_client_worker = pickle.dumps(data_centric_fl_client.is_client_worker)
+        log_msgs = pickle.dumps(data_centric_fl_client.log_msgs)
+        verbose = pickle.dumps(data_centric_fl_client.verbose)
+        encoding = pickle.dumps(data_centric_fl_client.encoding)
+        timeout = pickle.dumps(data_centric_fl_client.timeout)
+
+        return (address, id, is_client_worker, log_msgs, verbose, encoding, timeout)
+
+    @staticmethod
+    def detail(
+        worker: AbstractWorker, client_tuple: tuple
+    ) -> Union["DataCentricFLClient", int, str]:
+
+        address, id, is_client_worker, log_msgs, verbose, encoding, timeout = client_tuple
+
+        # detail client attributes
+        address = pickle.loads(address)
+        id = pickle.loads(id)
+        is_client_worker = pickle.loads(is_client_worker)
+        log_msgs = pickle.loads(log_msgs)
+        verbose = pickle.loads(verbose)
+        encoding = pickle.loads(encoding)
+        timeout = pickle.loads(timeout)
+
+        # create torch hook
path: syft/grid/clients/data_centric_fl_client.py
text: @user1:
Q: if we reach here doesn't mean we already are hooked?

@author:
yup we have created but I am not sure how to access that hook from here, also creating a new one should not raise any issue. 

@user1:
I think you can access it from the worker.

@author:
The problem is that `detail` is called by a gridnode on the simplified objects it receives over the network from client(so we don't have control from client side over the behaviour of the function.). 
and you cant pass the hook to it, (unless I serialise the hook instance in `simplify ` function which we will mean we are essentially sending hook over the network which is not required. 

@author:
and it's a class method which takes simplified input as arguments.

@user1:
It can't be accessed using ```syft.local_worker.hook```?

@user2:
@author I think that @user1 is right. Please make this modification before merging.

@author:
got it, updated