Repository: OpenMined/PySyft

Issue:
Create temporary `VirtualWorkers` for tracing in each `Role` of a `Protocol`
**Is your feature request related to a problem? Please describe.**
`Protocols` currently require `Workers` to exist before tracing, which hampers the future assignment of multiple `Workers` to a `Role`.

**Describe the solution you'd like**
When creating `Roles` in `@sy.func2protocol`, create a `VirtualWorker` for each `Role` and assign it to an attribute on that `Role`.

**Describe alternatives you've considered**
* ???

**Additional context**
Depends on #3488.



Discussion:

Resolving patch:
diff --git a/syft/execution/protocol.py b/syft/execution/protocol.py
--- a/syft/execution/protocol.py
+++ b/syft/execution/protocol.py
@@ -19,6 +19,7 @@
 from syft.generic.frameworks.types import FrameworkLayerModule
 from syft.generic.object import AbstractObject
 from syft.workers.abstract import AbstractWorker
+from syft.workers.virtual import VirtualWorker
 
 from syft_proto.execution.v1.protocol_pb2 import Protocol as ProtocolPB
 
@@ -32,12 +33,16 @@ class func2protocol(object):
     This class should be used only as a decorator.
     """
 
-    def __init__(self, args_shape=None):
+    def __init__(self, roles: list = [], args_shape: dict = {}):
         self.args_shape = args_shape
+        self.role_names = roles
 
     def __call__(self, protocol_function):
         # create the roles present in decorator
-        roles = {role_id: Role() for role_id in self.args_shape.keys()}
+        roles = {
+            role_id: Role(worker=VirtualWorker(id=role_id, hook=sy.local_worker.hook))
+            for role_id in self.role_names
+        }
 
         protocol = Protocol(
             name=protocol_function.__name__,
@@ -47,19 +52,16 @@ def __call__(self, protocol_function):
             owner=sy.local_worker,
         )
 
-        # Build the protocol automatically
-        # TODO We can always build automatically, can't we? Except if workers doesn't have
-        # tensors yet in store. Do we handle that?
-        if self.args_shape:
-            try:
-                protocol.build()
-            except TypeError as e:
-                raise ValueError(
-                    "Automatic build using @func2protocol failed!\nCheck that:\n"
-                    " - you have provided the correct number of shapes in args_shape\n"
-                    " - you have no simple numbers like int or float as args. If you do "
-                    "so, please consider using a tensor instead."
-                )
+        try:
+            protocol.build()
+        except TypeError as e:
+            raise ValueError(
+                "Automatic build using @func2protocol failed!\nCheck that:\n"
+                " - you have provided the correct number of shapes in args_shape\n"
+                " - you have no simple numbers like int or float as args. If you do "
+                "so, please consider using a tensor instead."
+            )
+
         return protocol
 
 
@@ -139,7 +141,7 @@ def build(self):
         self.toggle_tracing(True)
         self.is_building = True
 
-        results = self.forward(self.roles)
+        results = self.forward(*self.roles.values())
 
         # Disable tracing
         self.toggle_tracing(False)
diff --git a/syft/execution/role.py b/syft/execution/role.py
--- a/syft/execution/role.py
+++ b/syft/execution/role.py
@@ -12,10 +12,10 @@
 from syft.execution.placeholder_id import PlaceholderId
 from syft.execution.state import State
 from syft.generic.frameworks.types import FrameworkTensor
+from syft.serde.syft_serializable import SyftSerializable
 from syft.workers.abstract import AbstractWorker
 
 from syft_proto.execution.v1.role_pb2 import Role as RolePB
-from syft.serde.syft_serializable import SyftSerializable
 
 
 class Role(SyftSerializable):
@@ -25,16 +25,16 @@ class Role(SyftSerializable):
 
     def __init__(
         self,
+        id: Union[str, int] = None,
+        worker: AbstractWorker = None,
         state: State = None,
         actions: List[Action] = None,
         placeholders: Dict[Union[str, int], PlaceHolder] = None,
         input_placeholder_ids: Tuple[int, str] = None,
         output_placeholder_ids: Tuple[int, str] = None,
-        # General kwargs
-        id: Union[str, int] = None,
     ):
         self.id = id or sy.ID_PROVIDER.pop()
-
+        self.worker = worker
         self.actions = actions or []
 
         # All placeholders


Test patch:
diff --git a/test/execution/test_protocol.py b/test/execution/test_protocol.py
--- a/test/execution/test_protocol.py
+++ b/test/execution/test_protocol.py
@@ -2,16 +2,28 @@
 import torch as th
 
 import syft as sy
+from syft.execution.role import Role
 
 
-def test_trace_communication_actions(workers):
-    bob = workers["bob"]
+def test_func2protocol_creates_roles():
+    @sy.func2protocol(roles=["alice", "bob"])
+    def protocol(alice, bob):
+        tensor = alice.fetch(th.tensor([1]))
 
-    @sy.func2protocol(args_shape={"alice": ((1,),)})
-    def protocol(roles):
-        tensor = roles["alice"].fetch(th.tensor([1]))
+        return tensor
+
+    assert protocol.is_built
+    assert len(protocol.roles) == 2
+    assert isinstance(protocol.roles["alice"], Role)
+    assert isinstance(protocol.roles["bob"], Role)
 
-        tensor.send(bob)
+
+def test_trace_communication_actions_send():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),)})
+    def protocol(alice, bob):
+        tensor = alice.fetch(th.tensor([1]))
+
+        tensor.send(bob.worker)
         return tensor
 
     traced_actions = protocol.roles["alice"].actions
@@ -21,14 +33,12 @@ def protocol(roles):
     assert "send" in [action.name for action in traced_actions]
 
 
-def test_trace_communication_actions_get(workers):
-    bob = workers["bob"]
+def test_trace_communication_actions_get():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),)})
+    def protocol(alice, bob):
+        tensor = alice.fetch(th.tensor([1]))
 
-    @sy.func2protocol(args_shape={"alice": ((1,),)})
-    def protocol(roles):
-        tensor = roles["alice"].fetch(th.tensor([1]))
-
-        ptr = tensor.send(bob)
+        ptr = tensor.send(bob.worker)
         res = ptr.get()
         return res
 
@@ -39,15 +49,13 @@ def protocol(roles):
     assert "get" in [action.name for action in traced_actions]
 
 
-def test_trace_communication_actions_send(workers):
-    alice, bob = workers["alice"], workers["bob"]
-
-    @sy.func2protocol(args_shape={"alice": ((1,),)})
-    def protocol(roles):
-        tensor = roles["alice"].fetch(th.tensor([1]))
+def test_trace_communication_actions_ptr_send():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),)})
+    def protocol(alice, bob):
+        tensor = alice.fetch(th.tensor([1]))
 
-        ptr = tensor.send(bob)
-        res = ptr.send(alice)
+        ptr = tensor.send(bob.worker)
+        res = ptr.send(alice.worker)
         return res
 
     traced_actions = protocol.roles["alice"].actions
@@ -57,15 +65,13 @@ def protocol(roles):
     assert "send" in [action.name for action in traced_actions]
 
 
-def test_trace_communication_actions_move(workers):
-    alice, bob = workers["alice"], workers["bob"]
+def test_trace_communication_actions_move():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),)})
+    def protocol(alice, bob):
+        tensor = alice.fetch(th.tensor([1]))
 
-    @sy.func2protocol(args_shape={"alice": ((1,),)})
-    def protocol(roles):
-        tensor = roles["alice"].fetch(th.tensor([1]))
-
-        ptr = tensor.send(bob)
-        res = ptr.move(alice)
+        ptr = tensor.send(bob.worker)
+        res = ptr.move(alice.worker)
         return res
 
     traced_actions = protocol.roles["alice"].actions
@@ -75,16 +81,14 @@ def protocol(roles):
     assert "move" in [action.name for action in traced_actions]
 
 
-def test_trace_communication_actions_share(workers):
-    alice, bob = workers["alice"], workers["bob"]
-
-    @sy.func2protocol(args_shape={"alice": ((1,),)})
-    def protocol(roles):
-        tensor = roles["alice"].fetch(th.tensor([1]))
+def test_trace_communication_actions_share():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),)})
+    def protocol(alice, bob):
+        tensor = alice.fetch(th.tensor([1]))
 
-        ptr = tensor.send(bob)
+        ptr = tensor.send(bob.worker)
         ptr = ptr.fix_prec()
-        res = ptr.share(alice, bob)
+        res = ptr.share(alice.worker, bob.worker)
         return res
 
     traced_actions = protocol.roles["alice"].actions
@@ -94,16 +98,14 @@ def protocol(roles):
     assert "share" in [action.name for action in traced_actions]
 
 
-def test_trace_communication_actions_share_(workers):
-    alice, bob = workers["alice"], workers["bob"]
+def test_trace_communication_actions_share_():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),)})
+    def protocol(alice, bob):
+        tensor = alice.fetch(th.tensor([1]))
 
-    @sy.func2protocol(args_shape={"alice": ((1,),)})
-    def protocol(roles):
-        tensor = roles["alice"].fetch(th.tensor([1]))
-
-        ptr = tensor.send(bob)
+        ptr = tensor.send(bob.worker)
         ptr = ptr.fix_prec()
-        res = ptr.share_(alice, bob)
+        res = ptr.share_(alice.worker, bob.worker)
         return res
 
     traced_actions = protocol.roles["alice"].actions
@@ -113,15 +115,13 @@ def protocol(roles):
     assert "share_" in [action.name for action in traced_actions]
 
 
-def test_trace_communication_actions_remote_send(workers):
-    alice, bob = workers["alice"], workers["bob"]
-
-    @sy.func2protocol(args_shape={"alice": ((1,),)})
-    def protocol(roles):
-        tensor = roles["alice"].fetch(th.tensor([1]))
+def test_trace_communication_actions_remote_send():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),)})
+    def protocol(alice, bob):
+        tensor = alice.fetch(th.tensor([1]))
 
-        ptr = tensor.send(bob)
-        res = ptr.remote_send(alice)
+        ptr = tensor.send(bob.worker)
+        res = ptr.remote_send(alice.worker)
         return res
 
     traced_actions = protocol.roles["alice"].actions
@@ -131,14 +131,12 @@ def protocol(roles):
     assert "remote_send" in [action.name for action in traced_actions]
 
 
-def test_trace_communication_actions_mid_get(workers):
-    bob = workers["bob"]
+def test_trace_communication_actions_mid_get():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),)})
+    def protocol(alice, bob):
+        tensor = alice.fetch(th.tensor([1]))
 
-    @sy.func2protocol(args_shape={"alice": ((1,),)})
-    def protocol(roles):
-        tensor = roles["alice"].fetch(th.tensor([1]))
-
-        ptr = tensor.send(bob)
+        ptr = tensor.send(bob.worker)
         res = ptr.mid_get()
         return res
 
@@ -149,14 +147,12 @@ def protocol(roles):
     assert "mid_get" in [action.name for action in traced_actions]
 
 
-def test_trace_communication_actions_remote_get(workers):
-    alice, bob = workers["alice"], workers["bob"]
-
-    @sy.func2protocol(args_shape={"alice": ((1,),)})
-    def protocol(roles):
-        tensor = roles["alice"].fetch(th.tensor([1]))
+def test_trace_communication_actions_remote_get():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),)})
+    def protocol(alice, bob):
+        tensor = alice.fetch(th.tensor([1]))
 
-        ptr = tensor.send(bob).send(alice)
+        ptr = tensor.send(bob.worker).send(alice.worker)
         res = ptr.remote_get()
         return res
 
@@ -167,15 +163,11 @@ def protocol(roles):
     assert "remote_get" in [action.name for action in traced_actions]
 
 
-def test_create_roles_from_decorator(workers):
-
-    roles_args_shape = {"alice": ((1,),), "bob": ((1,),)}
-
-    @sy.func2protocol(args_shape=roles_args_shape)
-    def protocol(roles):
-        # fetch tensors from stores
-        tensor1 = roles["alice"].fetch(th.tensor([1]))
-        tensor2 = roles["bob"].fetch(th.tensor([1]))
+def test_create_roles_from_decorator():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),), "bob": ((1,),)})
+    def protocol(alice, bob):
+        tensor1 = alice.fetch(th.tensor([1]))
+        tensor2 = bob.fetch(th.tensor([2]))
 
         t1plus = tensor1 + 1
         t2plus = tensor2 + 1
@@ -187,12 +179,11 @@ def protocol(roles):
     assert "bob" in protocol.roles
 
 
-def test_multi_role_tracing(workers):
-    @sy.func2protocol(args_shape={"alice": ((1,),), "bob": ((1,),)})
-    def protocol(roles):
-        # fetch tensors from stores
-        tensor1 = roles["alice"].fetch(th.tensor([1]))
-        tensor2 = roles["bob"].fetch(th.tensor([1]))
+def test_multi_role_tracing():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),), "bob": ((1,),)})
+    def protocol(alice, bob):
+        tensor1 = alice.fetch(th.tensor([1]))
+        tensor2 = bob.fetch(th.tensor([2]))
 
         t1plus = tensor1 + 1
         t2plus = tensor2 + 1
@@ -208,12 +199,12 @@ def protocol(roles):
     assert len(protocol.roles["bob"].actions) == 1
 
 
-def test_multi_role_execution(workers):
-    @sy.func2protocol(args_shape={"alice": ((1,), (1,)), "bob": ((1,),)})
-    def protocol(roles):
-        tensor1 = roles["alice"].fetch(th.tensor([1]))
-        tensor2 = roles["bob"].fetch(th.tensor([2]))
-        tensor3 = roles["alice"].fetch(th.tensor([3]))
+def test_multi_role_execution():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,), (1,)), "bob": ((1,),)})
+    def protocol(alice, bob):
+        tensor1 = alice.fetch(th.tensor([1]))
+        tensor2 = bob.fetch(th.tensor([2]))
+        tensor3 = alice.fetch(th.tensor([3]))
 
         res1 = tensor2
         res2 = tensor1 + tensor3
@@ -231,12 +222,11 @@ def protocol(roles):
     assert (dict_res["alice"][0] == th.tensor([4])).all()
 
 
-def test_copy(workers):
-    @sy.func2protocol(args_shape={"alice": ((1,),), "bob": ((1,),)})
-    def protocol(roles):
-        # fetch tensors from stores
-        tensor1 = roles["alice"].fetch(th.tensor([1]))
-        tensor2 = roles["bob"].fetch(th.tensor([1]))
+def test_copy():
+    @sy.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),), "bob": ((1,),)})
+    def protocol(alice, bob):
+        tensor1 = alice.fetch(th.tensor([1]))
+        tensor2 = bob.fetch(th.tensor([2]))
 
         t1plus = tensor1 + 1
         t2plus = tensor2 + 1
diff --git a/test/serde/serde_helpers.py b/test/serde/serde_helpers.py
--- a/test/serde/serde_helpers.py
+++ b/test/serde/serde_helpers.py
@@ -972,12 +972,10 @@ def make_protocol(**kwargs):
     alice = kwargs["workers"]["alice"]
     bob = kwargs["workers"]["bob"]
 
-    @syft.func2protocol(args_shape={"alice": ((1,),), "bob": ((1,),)})
-    def protocol(roles):
-        # fetch tensors from stores
-        # TODO fix fetch once we have the real implementation of it
-        tensor1 = roles["alice"].fetch(torch.tensor([1]))
-        tensor2 = roles["bob"].fetch(torch.tensor([1]))
+    @syft.func2protocol(roles=["alice", "bob"], args_shape={"alice": ((1,),), "bob": ((1,),)})
+    def protocol(alice, bob):
+        tensor1 = alice.fetch(torch.tensor([1]))
+        tensor2 = bob.fetch(torch.tensor([1]))
 
         t1plus = tensor1 + 1
         t2plus = tensor2 + 1