flwr-nightly 1.23.0.dev20251008__py3-none-any.whl → 1.23.0.dev20251010__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,10 +14,17 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface `supernode create` command."""
16
16
 
17
+
18
+ import io
19
+ import json
17
20
  from pathlib import Path
18
21
  from typing import Annotated, Optional
19
22
 
20
23
  import typer
24
+ from cryptography.exceptions import UnsupportedAlgorithm
25
+ from cryptography.hazmat.primitives import serialization
26
+ from cryptography.hazmat.primitives.asymmetric import ec
27
+ from rich.console import Console
21
28
 
22
29
  from flwr.cli.config_utils import (
23
30
  exit_if_no_address,
@@ -25,14 +32,24 @@ from flwr.cli.config_utils import (
25
32
  process_loaded_project_config,
26
33
  validate_federation_in_project_config,
27
34
  )
28
- from flwr.common.constant import FAB_CONFIG_FILE
35
+ from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
36
+ from flwr.common.exit import ExitCode, flwr_exit
37
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
38
+ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
39
+ CreateNodeCliRequest,
40
+ CreateNodeCliResponse,
41
+ )
42
+ from flwr.proto.control_pb2_grpc import ControlStub
43
+ from flwr.supercore.primitives.asymmetric import public_key_to_bytes, uses_nist_ec_curve
44
+
45
+ from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
29
46
 
30
47
 
31
48
  def create( # pylint: disable=R0914
32
49
  public_key: Annotated[
33
50
  Path,
34
51
  typer.Argument(
35
- help="Path to the public key file.",
52
+ help="Path to a P-384 (or any other NIST EC curve) public key file.",
36
53
  ),
37
54
  ],
38
55
  app: Annotated[
@@ -43,16 +60,125 @@ def create( # pylint: disable=R0914
43
60
  Optional[str],
44
61
  typer.Argument(help="Name of the federation"),
45
62
  ] = None,
63
+ output_format: Annotated[
64
+ str,
65
+ typer.Option(
66
+ "--format",
67
+ case_sensitive=False,
68
+ help="Format output using 'default' view or 'json'",
69
+ ),
70
+ ] = CliOutputFormat.DEFAULT,
46
71
  ) -> None:
47
72
  """Add a SuperNode to the federation."""
48
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
73
+ suppress_output = output_format == CliOutputFormat.JSON
74
+ captured_output = io.StringIO()
75
+
76
+ # Load public key
77
+ public_key_path = Path(public_key)
78
+ public_key_bytes = try_load_public_key(public_key_path)
79
+
80
+ try:
81
+ if suppress_output:
82
+ redirect_output(captured_output)
83
+
84
+ # Load and validate federation config
85
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
86
+
87
+ pyproject_path = app / FAB_CONFIG_FILE if app else None
88
+ config, errors, warnings = load_and_validate(path=pyproject_path)
89
+ config = process_loaded_project_config(config, errors, warnings)
90
+ federation, federation_config = validate_federation_in_project_config(
91
+ federation, config
92
+ )
93
+ exit_if_no_address(federation_config, "supernode create")
94
+
95
+ channel = None
96
+ try:
97
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
98
+ channel = init_channel(app, federation_config, auth_plugin)
99
+ stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
100
+
101
+ _create_node(
102
+ stub=stub, public_key=public_key_bytes, output_format=output_format
103
+ )
104
+
105
+ except ValueError as err:
106
+ typer.secho(
107
+ f"❌ {err}",
108
+ fg=typer.colors.RED,
109
+ bold=True,
110
+ )
111
+ raise typer.Exit(code=1) from err
112
+ finally:
113
+ if channel:
114
+ channel.close()
115
+
116
+ except (typer.Exit, Exception) as err: # pylint: disable=broad-except
117
+ if suppress_output:
118
+ restore_output()
119
+ e_message = captured_output.getvalue()
120
+ print_json_error(e_message, err)
121
+ else:
122
+ typer.secho(
123
+ f"{err}",
124
+ fg=typer.colors.RED,
125
+ bold=True,
126
+ )
127
+ finally:
128
+ if suppress_output:
129
+ restore_output()
130
+ captured_output.close()
131
+
132
+
133
+ def _create_node(stub: ControlStub, public_key: bytes, output_format: str) -> None:
134
+ """Create a node."""
135
+ with flwr_cli_grpc_exc_handler():
136
+ response: CreateNodeCliResponse = stub.CreateNodeCli(
137
+ request=CreateNodeCliRequest(public_key=public_key)
138
+ )
139
+ if response.node_id:
140
+ typer.secho(
141
+ f"✅ Node {response.node_id} created successfully.", fg=typer.colors.GREEN
142
+ )
143
+ if output_format == CliOutputFormat.JSON:
144
+ run_output = json.dumps(
145
+ {
146
+ "success": True,
147
+ "node-id": response.node_id,
148
+ }
149
+ )
150
+ restore_output()
151
+ Console().print_json(run_output)
152
+ else:
153
+ typer.secho("❌ Node couldn't be created.", fg=typer.colors.RED)
154
+
155
+
156
+ def try_load_public_key(public_key_path: Path) -> bytes:
157
+ """Try to load a public key from a file."""
158
+ if not public_key_path.exists():
159
+ typer.secho(
160
+ f"❌ Public key file '{public_key_path}' does not exist.",
161
+ fg=typer.colors.RED,
162
+ bold=True,
163
+ )
164
+ raise typer.Exit(code=1)
165
+
166
+ with open(public_key_path, "rb") as key_file:
167
+ try:
168
+ public_key = serialization.load_ssh_public_key(key_file.read())
169
+
170
+ if not isinstance(public_key, ec.EllipticCurvePublicKey):
171
+ raise ValueError(f"Not an EC public key, got {type(public_key)}")
49
172
 
50
- pyproject_path = app / FAB_CONFIG_FILE if app else None
51
- config, errors, warnings = load_and_validate(path=pyproject_path)
52
- config = process_loaded_project_config(config, errors, warnings)
53
- federation, federation_config = validate_federation_in_project_config(
54
- federation, config
55
- )
56
- exit_if_no_address(federation_config, "supernode add")
173
+ # Verify it's one of the approved NIST curves
174
+ if not uses_nist_ec_curve(public_key):
175
+ raise ValueError(
176
+ f"EC curve {public_key.curve.name} is not an approved NIST curve"
177
+ )
57
178
 
58
- _ = public_key
179
+ except (ValueError, UnsupportedAlgorithm) as err:
180
+ flwr_exit(
181
+ ExitCode.FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID,
182
+ str(err),
183
+ )
184
+ return public_key_to_bytes(public_key)
@@ -14,10 +14,14 @@
14
14
  # ==============================================================================
15
15
  """Flower command line interface `supernode delete` command."""
16
16
 
17
+
18
+ import io
19
+ import json
17
20
  from pathlib import Path
18
21
  from typing import Annotated, Optional
19
22
 
20
23
  import typer
24
+ from rich.console import Console
21
25
 
22
26
  from flwr.cli.config_utils import (
23
27
  exit_if_no_address,
@@ -25,7 +29,12 @@ from flwr.cli.config_utils import (
25
29
  process_loaded_project_config,
26
30
  validate_federation_in_project_config,
27
31
  )
28
- from flwr.common.constant import FAB_CONFIG_FILE
32
+ from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
33
+ from flwr.common.logger import print_json_error, redirect_output, restore_output
34
+ from flwr.proto.control_pb2 import DeleteNodeCliRequest # pylint: disable=E0611
35
+ from flwr.proto.control_pb2_grpc import ControlStub
36
+
37
+ from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugin
29
38
 
30
39
 
31
40
  def delete( # pylint: disable=R0914
@@ -43,16 +52,85 @@ def delete( # pylint: disable=R0914
43
52
  Optional[str],
44
53
  typer.Argument(help="Name of the federation"),
45
54
  ] = None,
55
+ output_format: Annotated[
56
+ str,
57
+ typer.Option(
58
+ "--format",
59
+ case_sensitive=False,
60
+ help="Format output using 'default' view or 'json'",
61
+ ),
62
+ ] = CliOutputFormat.DEFAULT,
46
63
  ) -> None:
47
64
  """Remove a SuperNode from the federation."""
48
- typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
65
+ suppress_output = output_format == CliOutputFormat.JSON
66
+ captured_output = io.StringIO()
67
+
68
+ try:
69
+ if suppress_output:
70
+ redirect_output(captured_output)
71
+
72
+ # Load and validate federation config
73
+ typer.secho("Loading project configuration... ", fg=typer.colors.BLUE)
74
+
75
+ pyproject_path = app / FAB_CONFIG_FILE if app else None
76
+ config, errors, warnings = load_and_validate(path=pyproject_path)
77
+ config = process_loaded_project_config(config, errors, warnings)
78
+ federation, federation_config = validate_federation_in_project_config(
79
+ federation, config
80
+ )
81
+ exit_if_no_address(federation_config, "supernode remove")
49
82
 
50
- pyproject_path = app / FAB_CONFIG_FILE if app else None
51
- config, errors, warnings = load_and_validate(path=pyproject_path)
52
- config = process_loaded_project_config(config, errors, warnings)
53
- federation, federation_config = validate_federation_in_project_config(
54
- federation, config
55
- )
56
- exit_if_no_address(federation_config, "supernode rm")
83
+ channel = None
84
+ try:
85
+ auth_plugin = load_cli_auth_plugin(app, federation, federation_config)
86
+ channel = init_channel(app, federation_config, auth_plugin)
87
+ stub = ControlStub(channel) # pylint: disable=unused-variable # noqa: F841
57
88
 
58
- _ = node_id
89
+ _delete_node(stub=stub, node_id=node_id, output_format=output_format)
90
+
91
+ except ValueError as err:
92
+ typer.secho(
93
+ f"❌ {err}",
94
+ fg=typer.colors.RED,
95
+ bold=True,
96
+ )
97
+ raise typer.Exit(code=1) from err
98
+ finally:
99
+ if channel:
100
+ channel.close()
101
+
102
+ except (typer.Exit, Exception) as err: # pylint: disable=broad-except
103
+ if suppress_output:
104
+ restore_output()
105
+ e_message = captured_output.getvalue()
106
+ print_json_error(e_message, err)
107
+ else:
108
+ typer.secho(
109
+ f"{err}",
110
+ fg=typer.colors.RED,
111
+ bold=True,
112
+ )
113
+ finally:
114
+ if suppress_output:
115
+ restore_output()
116
+ captured_output.close()
117
+
118
+
119
+ def _delete_node(
120
+ stub: ControlStub,
121
+ node_id: int,
122
+ output_format: str,
123
+ ) -> None:
124
+ """Delete a SuperNode from the federation."""
125
+ with flwr_cli_grpc_exc_handler():
126
+ stub.DeleteNodeCli(request=DeleteNodeCliRequest(node_id=node_id))
127
+ typer.secho(f"✅ SuperNode {node_id} deleted successfully.", fg=typer.colors.GREEN)
128
+ if output_format == CliOutputFormat.JSON:
129
+ run_output = json.dumps(
130
+ {
131
+ "success": True,
132
+ "node-id": node_id,
133
+ }
134
+ )
135
+ restore_output()
136
+ Console().print_json(run_output)
flwr/cli/utils.py CHANGED
@@ -32,6 +32,9 @@ from flwr.common.constant import (
32
32
  FLWR_DIR,
33
33
  NO_ACCOUNT_AUTH_MESSAGE,
34
34
  NO_ARTIFACT_PROVIDER_MESSAGE,
35
+ NODE_NOT_FOUND_MESSAGE,
36
+ PUBLIC_KEY_ALREADY_IN_USE_MESSAGE,
37
+ PUBLIC_KEY_NOT_VALID,
35
38
  PULL_UNFINISHED_RUN_MESSAGE,
36
39
  RUN_ID_NOT_FOUND_MESSAGE,
37
40
  AuthnType,
@@ -293,7 +296,7 @@ def init_channel(
293
296
 
294
297
 
295
298
  @contextmanager
296
- def flwr_cli_grpc_exc_handler() -> Iterator[None]:
299
+ def flwr_cli_grpc_exc_handler() -> Iterator[None]: # pylint: disable=too-many-branches
297
300
  """Context manager to handle specific gRPC errors.
298
301
 
299
302
  It catches grpc.RpcError exceptions with UNAUTHENTICATED, UNIMPLEMENTED,
@@ -351,16 +354,21 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]:
351
354
  bold=True,
352
355
  )
353
356
  raise typer.Exit(code=1) from None
354
- if (
355
- e.code() == grpc.StatusCode.NOT_FOUND
356
- and e.details() == RUN_ID_NOT_FOUND_MESSAGE # pylint: disable=E1101
357
- ):
358
- typer.secho(
359
- "❌ Run ID not found.",
360
- fg=typer.colors.RED,
361
- bold=True,
362
- )
363
- raise typer.Exit(code=1) from None
357
+ if e.code() == grpc.StatusCode.NOT_FOUND:
358
+ if e.details() == RUN_ID_NOT_FOUND_MESSAGE: # pylint: disable=E1101
359
+ typer.secho(
360
+ "❌ Run ID not found.",
361
+ fg=typer.colors.RED,
362
+ bold=True,
363
+ )
364
+ raise typer.Exit(code=1) from None
365
+ if e.details() == NODE_NOT_FOUND_MESSAGE: # pylint: disable=E1101
366
+ typer.secho(
367
+ "❌ Node ID not found for this account.",
368
+ fg=typer.colors.RED,
369
+ bold=True,
370
+ )
371
+ raise typer.Exit(code=1) from None
364
372
  if e.code() == grpc.StatusCode.FAILED_PRECONDITION:
365
373
  if e.details() == PULL_UNFINISHED_RUN_MESSAGE: # pylint: disable=E1101
366
374
  typer.secho(
@@ -370,4 +378,22 @@ def flwr_cli_grpc_exc_handler() -> Iterator[None]:
370
378
  bold=True,
371
379
  )
372
380
  raise typer.Exit(code=1) from None
381
+ if (
382
+ e.details() == PUBLIC_KEY_ALREADY_IN_USE_MESSAGE
383
+ ): # pylint: disable=E1101
384
+ typer.secho(
385
+ "❌ The provided public key is already in use by another "
386
+ "SuperNode.",
387
+ fg=typer.colors.RED,
388
+ bold=True,
389
+ )
390
+ raise typer.Exit(code=1) from None
391
+ if e.details() == PUBLIC_KEY_NOT_VALID: # pylint: disable=E1101
392
+ typer.secho(
393
+ "❌ The provided public key is invalid. Please provide a valid "
394
+ "NIST EC public key.",
395
+ fg=typer.colors.RED,
396
+ bold=True,
397
+ )
398
+ raise typer.Exit(code=1) from None
373
399
  raise
flwr/common/constant.py CHANGED
@@ -157,6 +157,9 @@ RUN_ID_NOT_FOUND_MESSAGE = "Run ID not found"
157
157
  NO_ACCOUNT_AUTH_MESSAGE = "ControlServicer initialized without account authentication"
158
158
  NO_ARTIFACT_PROVIDER_MESSAGE = "ControlServicer initialized without artifact provider"
159
159
  PULL_UNFINISHED_RUN_MESSAGE = "Cannot pull artifacts for an unfinished run"
160
+ PUBLIC_KEY_ALREADY_IN_USE_MESSAGE = "Public key already in use"
161
+ PUBLIC_KEY_NOT_VALID = "The provided public key is not valid"
162
+ NODE_NOT_FOUND_MESSAGE = "Node ID not found for account"
160
163
 
161
164
 
162
165
  class MessageType:
@@ -47,6 +47,9 @@ class ExitCode:
47
47
  # SuperExec-specific exit codes (400-499)
48
48
  SUPEREXEC_INVALID_PLUGIN_CONFIG = 400
49
49
 
50
+ # FlowerCLI-specific exit codes (500-599)
51
+ FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID = 500
52
+
50
53
  # Common exit codes (600-699)
51
54
  COMMON_ADDRESS_INVALID = 600
52
55
  COMMON_MISSING_EXTRA_REST = 601
@@ -116,6 +119,13 @@ EXIT_CODE_HELP = {
116
119
  ExitCode.SUPEREXEC_INVALID_PLUGIN_CONFIG: (
117
120
  "The YAML configuration for the SuperExec plugin is invalid."
118
121
  ),
122
+ # FlowerCLI-specific exit codes (500-599)
123
+ ExitCode.FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID: (
124
+ "Node authentication requires a valid elliptic curve public key in the "
125
+ "SSH format and following a NIST standard elliptic curve (e.g. SECP384R1). "
126
+ "Please ensure that the file path points to a valid public key "
127
+ "file and try again."
128
+ ),
119
129
  # Common exit codes (600-699)
120
130
  ExitCode.COMMON_ADDRESS_INVALID: (
121
131
  "Please provide a valid URL, IPv4 or IPv6 address."
flwr/proto/control_pb2.py CHANGED
@@ -19,7 +19,7 @@ from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2
19
19
  from flwr.proto import node_pb2 as flwr_dot_proto_dot_node__pb2
20
20
 
21
21
 
22
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66lwr/proto/control.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x1a\x66lwr/proto/transport.proto\x1a\x1b\x66lwr/proto/recorddict.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x15\x66lwr/proto/node.proto\"\xfa\x01\n\x0fStartRunRequest\x12\x1c\n\x03\x66\x61\x62\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Fab\x12H\n\x0foverride_config\x18\x02 \x03(\x0b\x32/.flwr.proto.StartRunRequest.OverrideConfigEntry\x12\x34\n\x12\x66\x65\x64\x65ration_options\x18\x03 \x01(\x0b\x32\x18.flwr.proto.ConfigRecord\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"2\n\x10StartRunResponse\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"<\n\x11StreamLogsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\x12\x17\n\x0f\x61\x66ter_timestamp\x18\x02 \x01(\x01\"B\n\x12StreamLogsResponse\x12\x12\n\nlog_output\x18\x01 \x01(\t\x12\x18\n\x10latest_timestamp\x18\x02 \x01(\x01\"1\n\x0fListRunsRequest\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"\x9d\x01\n\x10ListRunsResponse\x12;\n\x08run_dict\x18\x01 \x03(\x0b\x32).flwr.proto.ListRunsResponse.RunDictEntry\x12\x0b\n\x03now\x18\x02 \x01(\t\x1a?\n\x0cRunDictEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.flwr.proto.Run:\x02\x38\x01\"\x18\n\x16GetLoginDetailsRequest\"\x8b\x01\n\x17GetLoginDetailsResponse\x12\x12\n\nauthn_type\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65vice_code\x18\x02 \x01(\t\x12!\n\x19verification_uri_complete\x18\x03 \x01(\t\x12\x12\n\nexpires_in\x18\x04 \x01(\x03\x12\x10\n\x08interval\x18\x05 \x01(\x03\"+\n\x14GetAuthTokensRequest\x12\x13\n\x0b\x64\x65vice_code\x18\x01 \x01(\t\"D\n\x15GetAuthTokensResponse\x12\x14\n\x0c\x61\x63\x63\x65ss_token\x18\x01 \x01(\t\x12\x15\n\rrefresh_token\x18\x02 \x01(\t\" \n\x0eStopRunRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"\"\n\x0fStopRunResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"&\n\x14PullArtifactsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"1\n\x15PullArtifactsResponse\x12\x10\n\x03url\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x06\n\x04_url\"*\n\x14\x43reateNodeCliRequest\x12\x12\n\npublic_key\x18\x01 \x01(\t\"9\n\x15\x43reateNodeCliResponse\x12\x14\n\x07node_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_node_id\"\'\n\x14\x44\x65leteNodeCliRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\x17\n\x15\x44\x65leteNodeCliResponse\"\x15\n\x13ListNodesCliRequest\"M\n\x14ListNodesCliResponse\x12(\n\nnodes_info\x18\x01 \x03(\x0b\x32\x14.flwr.proto.NodeInfo\x12\x0b\n\x03now\x18\x02 \x01(\t2\xc5\x06\n\x07\x43ontrol\x12G\n\x08StartRun\x12\x1b.flwr.proto.StartRunRequest\x1a\x1c.flwr.proto.StartRunResponse\"\x00\x12\x44\n\x07StopRun\x12\x1a.flwr.proto.StopRunRequest\x1a\x1b.flwr.proto.StopRunResponse\"\x00\x12O\n\nStreamLogs\x12\x1d.flwr.proto.StreamLogsRequest\x1a\x1e.flwr.proto.StreamLogsResponse\"\x00\x30\x01\x12G\n\x08ListRuns\x12\x1b.flwr.proto.ListRunsRequest\x1a\x1c.flwr.proto.ListRunsResponse\"\x00\x12\\\n\x0fGetLoginDetails\x12\".flwr.proto.GetLoginDetailsRequest\x1a#.flwr.proto.GetLoginDetailsResponse\"\x00\x12V\n\rGetAuthTokens\x12 .flwr.proto.GetAuthTokensRequest\x1a!.flwr.proto.GetAuthTokensResponse\"\x00\x12V\n\rPullArtifacts\x12 .flwr.proto.PullArtifactsRequest\x1a!.flwr.proto.PullArtifactsResponse\"\x00\x12V\n\rCreateNodeCli\x12 .flwr.proto.CreateNodeCliRequest\x1a!.flwr.proto.CreateNodeCliResponse\"\x00\x12V\n\rDeleteNodeCli\x12 .flwr.proto.DeleteNodeCliRequest\x1a!.flwr.proto.DeleteNodeCliResponse\"\x00\x12S\n\x0cListNodesCli\x12\x1f.flwr.proto.ListNodesCliRequest\x1a .flwr.proto.ListNodesCliResponse\"\x00\x62\x06proto3')
22
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66lwr/proto/control.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x1a\x66lwr/proto/transport.proto\x1a\x1b\x66lwr/proto/recorddict.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x15\x66lwr/proto/node.proto\"\xfa\x01\n\x0fStartRunRequest\x12\x1c\n\x03\x66\x61\x62\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Fab\x12H\n\x0foverride_config\x18\x02 \x03(\x0b\x32/.flwr.proto.StartRunRequest.OverrideConfigEntry\x12\x34\n\x12\x66\x65\x64\x65ration_options\x18\x03 \x01(\x0b\x32\x18.flwr.proto.ConfigRecord\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"2\n\x10StartRunResponse\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"<\n\x11StreamLogsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\x12\x17\n\x0f\x61\x66ter_timestamp\x18\x02 \x01(\x01\"B\n\x12StreamLogsResponse\x12\x12\n\nlog_output\x18\x01 \x01(\t\x12\x18\n\x10latest_timestamp\x18\x02 \x01(\x01\"1\n\x0fListRunsRequest\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"\x9d\x01\n\x10ListRunsResponse\x12;\n\x08run_dict\x18\x01 \x03(\x0b\x32).flwr.proto.ListRunsResponse.RunDictEntry\x12\x0b\n\x03now\x18\x02 \x01(\t\x1a?\n\x0cRunDictEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.flwr.proto.Run:\x02\x38\x01\"\x18\n\x16GetLoginDetailsRequest\"\x8b\x01\n\x17GetLoginDetailsResponse\x12\x12\n\nauthn_type\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65vice_code\x18\x02 \x01(\t\x12!\n\x19verification_uri_complete\x18\x03 \x01(\t\x12\x12\n\nexpires_in\x18\x04 \x01(\x03\x12\x10\n\x08interval\x18\x05 \x01(\x03\"+\n\x14GetAuthTokensRequest\x12\x13\n\x0b\x64\x65vice_code\x18\x01 \x01(\t\"D\n\x15GetAuthTokensResponse\x12\x14\n\x0c\x61\x63\x63\x65ss_token\x18\x01 \x01(\t\x12\x15\n\rrefresh_token\x18\x02 \x01(\t\" \n\x0eStopRunRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"\"\n\x0fStopRunResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"&\n\x14PullArtifactsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"1\n\x15PullArtifactsResponse\x12\x10\n\x03url\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x06\n\x04_url\"*\n\x14\x43reateNodeCliRequest\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\"9\n\x15\x43reateNodeCliResponse\x12\x14\n\x07node_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_node_id\"\'\n\x14\x44\x65leteNodeCliRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\x17\n\x15\x44\x65leteNodeCliResponse\"\x15\n\x13ListNodesCliRequest\"M\n\x14ListNodesCliResponse\x12(\n\nnodes_info\x18\x01 \x03(\x0b\x32\x14.flwr.proto.NodeInfo\x12\x0b\n\x03now\x18\x02 \x01(\t2\xc5\x06\n\x07\x43ontrol\x12G\n\x08StartRun\x12\x1b.flwr.proto.StartRunRequest\x1a\x1c.flwr.proto.StartRunResponse\"\x00\x12\x44\n\x07StopRun\x12\x1a.flwr.proto.StopRunRequest\x1a\x1b.flwr.proto.StopRunResponse\"\x00\x12O\n\nStreamLogs\x12\x1d.flwr.proto.StreamLogsRequest\x1a\x1e.flwr.proto.StreamLogsResponse\"\x00\x30\x01\x12G\n\x08ListRuns\x12\x1b.flwr.proto.ListRunsRequest\x1a\x1c.flwr.proto.ListRunsResponse\"\x00\x12\\\n\x0fGetLoginDetails\x12\".flwr.proto.GetLoginDetailsRequest\x1a#.flwr.proto.GetLoginDetailsResponse\"\x00\x12V\n\rGetAuthTokens\x12 .flwr.proto.GetAuthTokensRequest\x1a!.flwr.proto.GetAuthTokensResponse\"\x00\x12V\n\rPullArtifacts\x12 .flwr.proto.PullArtifactsRequest\x1a!.flwr.proto.PullArtifactsResponse\"\x00\x12V\n\rCreateNodeCli\x12 .flwr.proto.CreateNodeCliRequest\x1a!.flwr.proto.CreateNodeCliResponse\"\x00\x12V\n\rDeleteNodeCli\x12 .flwr.proto.DeleteNodeCliRequest\x1a!.flwr.proto.DeleteNodeCliResponse\"\x00\x12S\n\x0cListNodesCli\x12\x1f.flwr.proto.ListNodesCliRequest\x1a .flwr.proto.ListNodesCliResponse\"\x00\x62\x06proto3')
23
23
 
24
24
  _globals = globals()
25
25
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -239,10 +239,10 @@ global___PullArtifactsResponse = PullArtifactsResponse
239
239
  class CreateNodeCliRequest(google.protobuf.message.Message):
240
240
  DESCRIPTOR: google.protobuf.descriptor.Descriptor
241
241
  PUBLIC_KEY_FIELD_NUMBER: builtins.int
242
- public_key: typing.Text
242
+ public_key: builtins.bytes
243
243
  def __init__(self,
244
244
  *,
245
- public_key: typing.Text = ...,
245
+ public_key: builtins.bytes = ...,
246
246
  ) -> None: ...
247
247
  def ClearField(self, field_name: typing_extensions.Literal["public_key",b"public_key"]) -> None: ...
248
248
  global___CreateNodeCliRequest = CreateNodeCliRequest
flwr/proto/node_pb2.py CHANGED
@@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default()
14
14
 
15
15
 
16
16
 
17
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/node.proto\x12\nflwr.proto\"\x17\n\x04Node\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\xd0\x01\n\x08NodeInfo\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\x12\x11\n\towner_aid\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x12\n\ncreated_at\x18\x04 \x01(\t\x12\x19\n\x11last_activated_at\x18\x05 \x01(\t\x12\x1b\n\x13last_deactivated_at\x18\x06 \x01(\t\x12\x12\n\ndeleted_at\x18\x07 \x01(\t\x12\x14\n\x0conline_until\x18\x08 \x01(\x01\x12\x1a\n\x12heartbeat_interval\x18\t \x01(\x01\x62\x06proto3')
17
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/node.proto\x12\nflwr.proto\"\x17\n\x04Node\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\xe4\x01\n\x08NodeInfo\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\x12\x11\n\towner_aid\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x12\n\ncreated_at\x18\x04 \x01(\t\x12\x19\n\x11last_activated_at\x18\x05 \x01(\t\x12\x1b\n\x13last_deactivated_at\x18\x06 \x01(\t\x12\x12\n\ndeleted_at\x18\x07 \x01(\t\x12\x14\n\x0conline_until\x18\x08 \x01(\x01\x12\x1a\n\x12heartbeat_interval\x18\t \x01(\x01\x12\x12\n\npublic_key\x18\n \x01(\x0c\x62\x06proto3')
18
18
 
19
19
  _globals = globals()
20
20
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -24,5 +24,5 @@ if _descriptor._USE_C_DESCRIPTORS == False:
24
24
  _globals['_NODE']._serialized_start=37
25
25
  _globals['_NODE']._serialized_end=60
26
26
  _globals['_NODEINFO']._serialized_start=63
27
- _globals['_NODEINFO']._serialized_end=271
27
+ _globals['_NODEINFO']._serialized_end=291
28
28
  # @@protoc_insertion_point(module_scope)
flwr/proto/node_pb2.pyi CHANGED
@@ -32,6 +32,7 @@ class NodeInfo(google.protobuf.message.Message):
32
32
  DELETED_AT_FIELD_NUMBER: builtins.int
33
33
  ONLINE_UNTIL_FIELD_NUMBER: builtins.int
34
34
  HEARTBEAT_INTERVAL_FIELD_NUMBER: builtins.int
35
+ PUBLIC_KEY_FIELD_NUMBER: builtins.int
35
36
  node_id: builtins.int
36
37
  owner_aid: typing.Text
37
38
  status: typing.Text
@@ -41,6 +42,7 @@ class NodeInfo(google.protobuf.message.Message):
41
42
  deleted_at: typing.Text
42
43
  online_until: builtins.float
43
44
  heartbeat_interval: builtins.float
45
+ public_key: builtins.bytes
44
46
  def __init__(self,
45
47
  *,
46
48
  node_id: builtins.int = ...,
@@ -52,6 +54,7 @@ class NodeInfo(google.protobuf.message.Message):
52
54
  deleted_at: typing.Text = ...,
53
55
  online_until: builtins.float = ...,
54
56
  heartbeat_interval: builtins.float = ...,
57
+ public_key: builtins.bytes = ...,
55
58
  ) -> None: ...
56
- def ClearField(self, field_name: typing_extensions.Literal["created_at",b"created_at","deleted_at",b"deleted_at","heartbeat_interval",b"heartbeat_interval","last_activated_at",b"last_activated_at","last_deactivated_at",b"last_deactivated_at","node_id",b"node_id","online_until",b"online_until","owner_aid",b"owner_aid","status",b"status"]) -> None: ...
59
+ def ClearField(self, field_name: typing_extensions.Literal["created_at",b"created_at","deleted_at",b"deleted_at","heartbeat_interval",b"heartbeat_interval","last_activated_at",b"last_activated_at","last_deactivated_at",b"last_deactivated_at","node_id",b"node_id","online_until",b"online_until","owner_aid",b"owner_aid","public_key",b"public_key","status",b"status"]) -> None: ...
57
60
  global___NodeInfo = NodeInfo
@@ -18,7 +18,7 @@ from logging import ERROR
18
18
  from typing import Optional
19
19
 
20
20
  from flwr.common import Message, log
21
- from flwr.common.constant import Status
21
+ from flwr.common.constant import NOOP_FLWR_AID, Status
22
22
  from flwr.common.inflatable import UnexpectedObjectContentError
23
23
  from flwr.common.serde import (
24
24
  fab_to_proto,
@@ -70,7 +70,9 @@ def create_node(
70
70
  ) -> CreateNodeResponse:
71
71
  """."""
72
72
  # Create node
73
- node_id = state.create_node(request.public_key, request.heartbeat_interval)
73
+ node_id = state.create_node(
74
+ NOOP_FLWR_AID, request.public_key, request.heartbeat_interval
75
+ )
74
76
  return CreateNodeResponse(node=Node(node_id=node_id))
75
77
 
76
78
 
@@ -81,7 +83,7 @@ def delete_node(request: DeleteNodeRequest, state: LinkState) -> DeleteNodeRespo
81
83
  return DeleteNodeResponse()
82
84
 
83
85
  # Update state
84
- state.delete_node(node_id=request.node.node_id)
86
+ state.delete_node(NOOP_FLWR_AID, node_id=request.node.node_id)
85
87
  return DeleteNodeResponse()
86
88
 
87
89
 
@@ -34,6 +34,7 @@ from flwr.clientapp.utils import get_load_client_app_fn
34
34
  from flwr.common import Message
35
35
  from flwr.common.constant import (
36
36
  HEARTBEAT_MAX_INTERVAL,
37
+ NOOP_FLWR_AID,
37
38
  NUM_PARTITIONS_KEY,
38
39
  PARTITION_ID_KEY,
39
40
  ErrorCode,
@@ -56,7 +57,9 @@ def _register_nodes(
56
57
  for i in range(num_nodes):
57
58
  node_id = state.create_node(
58
59
  # No node authentication in simulation;
59
- # use random bytes instead
60
+ # use NOOP_FLWR_AID as owner_aid and
61
+ # use random bytes as public key
62
+ NOOP_FLWR_AID,
60
63
  secrets.token_bytes(32),
61
64
  heartbeat_interval=HEARTBEAT_MAX_INTERVAL,
62
65
  )
@@ -69,10 +69,10 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
69
69
 
70
70
  def __init__(self) -> None:
71
71
 
72
- # Map node_id to (online_until, heartbeat_interval)
72
+ # Map node_id to NodeInfo
73
73
  self.nodes: dict[int, NodeInfo] = {}
74
- self.public_key_to_node_id: dict[bytes, int] = {}
75
- self.node_id_to_public_key: dict[int, bytes] = {}
74
+ self.registered_node_public_keys: set[bytes] = set()
75
+ self.owner_to_node_ids: dict[str, set[int]] = {} # Quick lookup
76
76
 
77
77
  # Map run_id to RunRecord
78
78
  self.run_ids: dict[int, RunRecord] = {}
@@ -330,7 +330,9 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
330
330
  """
331
331
  return len(self.message_res_store)
332
332
 
333
- def create_node(self, public_key: bytes, heartbeat_interval: float) -> int:
333
+ def create_node(
334
+ self, owner_aid: str, public_key: bytes, heartbeat_interval: float
335
+ ) -> int:
334
336
  """Create, store in the link state, and return `node_id`."""
335
337
  # Sample a random int64 as node_id
336
338
  node_id = generate_rand_int_from_bytes(
@@ -341,14 +343,14 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
341
343
  if node_id in self.nodes:
342
344
  log(ERROR, "Unexpected node registration failure.")
343
345
  return 0
344
- if public_key in self.public_key_to_node_id:
346
+ if public_key in self.registered_node_public_keys:
345
347
  raise ValueError("Public key already in use")
346
348
 
347
349
  # Mark the node online until now().timestamp() + heartbeat_interval
348
350
  current = now()
349
351
  self.nodes[node_id] = NodeInfo(
350
352
  node_id=node_id,
351
- owner_aid="", # Unused for now
353
+ owner_aid=owner_aid, # Unused for now
352
354
  status="created", # Unused for now
353
355
  created_at=current.isoformat(), # Unused for now
354
356
  last_activated_at=current.isoformat(), # Unused for now
@@ -356,22 +358,22 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
356
358
  deleted_at="", # Unused for now
357
359
  online_until=current.timestamp() + heartbeat_interval,
358
360
  heartbeat_interval=heartbeat_interval,
361
+ public_key=public_key,
359
362
  )
360
- self.public_key_to_node_id[public_key] = node_id
361
- self.node_id_to_public_key[node_id] = public_key
363
+ self.registered_node_public_keys.add(public_key)
364
+ self.owner_to_node_ids.setdefault(owner_aid, set()).add(node_id)
362
365
  return node_id
363
366
 
364
- def delete_node(self, node_id: int) -> None:
367
+ def delete_node(self, owner_aid: str, node_id: int) -> None:
365
368
  """Delete a node."""
366
369
  with self.lock:
367
- if node_id not in self.nodes:
368
- raise ValueError(f"Node {node_id} not found")
369
-
370
- # Remove node ID <> public key mappings
371
- if pk := self.node_id_to_public_key.pop(node_id, None):
372
- del self.public_key_to_node_id[pk]
370
+ if node_id not in self.nodes or owner_aid != self.nodes[node_id].owner_aid:
371
+ raise ValueError(
372
+ f"Node ID {node_id} not found or unauthorized deletion attempt."
373
+ )
373
374
 
374
- del self.nodes[node_id]
375
+ node = self.nodes.pop(node_id)
376
+ self.registered_node_public_keys.discard(node.public_key)
375
377
 
376
378
  def get_nodes(self, run_id: int) -> set[int]:
377
379
  """Return all available nodes.
@@ -391,29 +393,13 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
391
393
  if info.online_until > current_time
392
394
  }
393
395
 
394
- def set_node_public_key(self, node_id: int, public_key: bytes) -> None:
395
- """Set `public_key` for the specified `node_id`."""
396
- with self.lock:
397
- if node_id not in self.nodes:
398
- raise ValueError(f"Node {node_id} not found")
399
-
400
- if public_key in self.public_key_to_node_id:
401
- raise ValueError("Public key already in use")
402
-
403
- self.public_key_to_node_id[public_key] = node_id
404
- self.node_id_to_public_key[node_id] = public_key
405
-
406
- def get_node_public_key(self, node_id: int) -> Optional[bytes]:
396
+ def get_node_public_key(self, node_id: int) -> bytes:
407
397
  """Get `public_key` for the specified `node_id`."""
408
398
  with self.lock:
409
- if node_id not in self.nodes:
410
- raise ValueError(f"Node {node_id} not found")
411
-
412
- return self.node_id_to_public_key.get(node_id)
399
+ if (node := self.nodes.get(node_id)) is None:
400
+ raise ValueError(f"Node ID {node_id} not found")
413
401
 
414
- def get_node_id(self, node_public_key: bytes) -> Optional[int]:
415
- """Retrieve stored `node_id` filtered by `node_public_keys`."""
416
- return self.public_key_to_node_id.get(node_public_key)
402
+ return node.public_key
417
403
 
418
404
  # pylint: disable=too-many-arguments,too-many-positional-arguments
419
405
  def create_run(
@@ -128,11 +128,13 @@ class LinkState(CoreState): # pylint: disable=R0904
128
128
  """Get all instruction Message IDs for the given run_id."""
129
129
 
130
130
  @abc.abstractmethod
131
- def create_node(self, public_key: bytes, heartbeat_interval: float) -> int:
131
+ def create_node(
132
+ self, owner_aid: str, public_key: bytes, heartbeat_interval: float
133
+ ) -> int:
132
134
  """Create, store in the link state, and return `node_id`."""
133
135
 
134
136
  @abc.abstractmethod
135
- def delete_node(self, node_id: int) -> None:
137
+ def delete_node(self, owner_aid: str, node_id: int) -> None:
136
138
  """Remove `node_id` from the link state."""
137
139
 
138
140
  @abc.abstractmethod
@@ -146,16 +148,24 @@ class LinkState(CoreState): # pylint: disable=R0904
146
148
  """
147
149
 
148
150
  @abc.abstractmethod
149
- def set_node_public_key(self, node_id: int, public_key: bytes) -> None:
150
- """Set `public_key` for the specified `node_id`."""
151
+ def get_node_public_key(self, node_id: int) -> bytes:
152
+ """Get `public_key` for the specified `node_id`.
151
153
 
152
- @abc.abstractmethod
153
- def get_node_public_key(self, node_id: int) -> Optional[bytes]:
154
- """Get `public_key` for the specified `node_id`."""
154
+ Parameters
155
+ ----------
156
+ node_id : int
157
+ The identifier of the node whose public key is to be retrieved.
155
158
 
156
- @abc.abstractmethod
157
- def get_node_id(self, node_public_key: bytes) -> Optional[int]:
158
- """Retrieve stored `node_id` filtered by `node_public_keys`."""
159
+ Returns
160
+ -------
161
+ bytes
162
+ The public key associated with the specified `node_id`.
163
+
164
+ Raises
165
+ ------
166
+ ValueError
167
+ If the specified `node_id` does not exist in the link state.
168
+ """
159
169
 
160
170
  @abc.abstractmethod
161
171
  def create_run( # pylint: disable=too-many-arguments,too-many-positional-arguments
@@ -94,6 +94,10 @@ SQL_CREATE_INDEX_ONLINE_UNTIL = """
94
94
  CREATE INDEX IF NOT EXISTS idx_online_until ON node (online_until);
95
95
  """
96
96
 
97
+ SQL_CREATE_INDEX_OWNER_AID = """
98
+ CREATE INDEX IF NOT EXISTS idx_node_owner_aid ON node(owner_aid);
99
+ """
100
+
97
101
  SQL_CREATE_TABLE_RUN = """
98
102
  CREATE TABLE IF NOT EXISTS run(
99
103
  run_id INTEGER UNIQUE,
@@ -228,6 +232,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
228
232
  cur.execute(SQL_CREATE_TABLE_PUBLIC_KEY)
229
233
  cur.execute(SQL_CREATE_TABLE_TOKEN_STORE)
230
234
  cur.execute(SQL_CREATE_INDEX_ONLINE_UNTIL)
235
+ cur.execute(SQL_CREATE_INDEX_OWNER_AID)
231
236
  res = cur.execute("SELECT name FROM sqlite_schema;")
232
237
  return res.fetchall()
233
238
 
@@ -602,7 +607,9 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
602
607
 
603
608
  return {row["message_id"] for row in rows}
604
609
 
605
- def create_node(self, public_key: bytes, heartbeat_interval: float) -> int:
610
+ def create_node(
611
+ self, owner_aid: str, public_key: bytes, heartbeat_interval: float
612
+ ) -> int:
606
613
  """Create, store in the link state, and return `node_id`."""
607
614
  # Sample a random uint64 as node_id
608
615
  uint64_node_id = generate_rand_int_from_bytes(
@@ -626,7 +633,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
626
633
  query,
627
634
  (
628
635
  sint64_node_id, # node_id
629
- "", # owner_aid, unused for now
636
+ owner_aid, # owner_aid, unused for now
630
637
  "created", # status, unused for now
631
638
  now().isoformat(), # created_at, unused for now
632
639
  now().isoformat(), # last_activated_at, unused for now
@@ -647,13 +654,13 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
647
654
  # Note: we need to return the uint64 value of the node_id
648
655
  return uint64_node_id
649
656
 
650
- def delete_node(self, node_id: int) -> None:
657
+ def delete_node(self, owner_aid: str, node_id: int) -> None:
651
658
  """Delete a node."""
652
659
  # Convert the uint64 value to sint64 for SQLite
653
660
  sint64_node_id = convert_uint64_to_sint64(node_id)
654
661
 
655
- query = "DELETE FROM node WHERE node_id = ?"
656
- params = (sint64_node_id,)
662
+ query = "DELETE FROM node WHERE node_id = ? AND owner_aid = ?"
663
+ params = (sint64_node_id, owner_aid)
657
664
 
658
665
  if self.conn is None:
659
666
  raise AttributeError("LinkState is not initialized.")
@@ -662,7 +669,9 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
662
669
  with self.conn:
663
670
  rows = self.conn.execute(query, params)
664
671
  if rows.rowcount < 1:
665
- raise ValueError(f"Node {node_id} not found")
672
+ raise ValueError(
673
+ f"Node ID {node_id} not found or unauthorized deletion attempt."
674
+ )
666
675
  except KeyError as exc:
667
676
  log(ERROR, {"query": query, "data": params, "exception": exc})
668
677
 
@@ -690,26 +699,7 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
690
699
  result: set[int] = {convert_sint64_to_uint64(row["node_id"]) for row in rows}
691
700
  return result
692
701
 
693
- def set_node_public_key(self, node_id: int, public_key: bytes) -> None:
694
- """Set `public_key` for the specified `node_id`."""
695
- # Convert the uint64 value to sint64 for SQLite
696
- sint64_node_id = convert_uint64_to_sint64(node_id)
697
-
698
- # Check if the node exists in the `node` table
699
- query = "SELECT 1 FROM node WHERE node_id = ?"
700
- if not self.query(query, (sint64_node_id,)):
701
- raise ValueError(f"Node {node_id} not found")
702
-
703
- # Check if the public key is already in use in the `node` table
704
- query = "SELECT 1 FROM node WHERE public_key = ?"
705
- if self.query(query, (public_key,)):
706
- raise ValueError("Public key already in use")
707
-
708
- # Update the `node` table to set the public key for the given node ID
709
- query = "UPDATE node SET public_key = ? WHERE node_id = ?"
710
- self.query(query, (public_key, sint64_node_id))
711
-
712
- def get_node_public_key(self, node_id: int) -> Optional[bytes]:
702
+ def get_node_public_key(self, node_id: int) -> bytes:
713
703
  """Get `public_key` for the specified `node_id`."""
714
704
  # Convert the uint64 value to sint64 for SQLite
715
705
  sint64_node_id = convert_uint64_to_sint64(node_id)
@@ -720,23 +710,10 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
720
710
 
721
711
  # If no result is found, return None
722
712
  if not rows:
723
- raise ValueError(f"Node {node_id} not found")
713
+ raise ValueError(f"Node ID {node_id} not found")
724
714
 
725
- # Return the public key if it is not empty, otherwise return None
726
- return rows[0]["public_key"] or None
727
-
728
- def get_node_id(self, node_public_key: bytes) -> Optional[int]:
729
- """Retrieve stored `node_id` filtered by `node_public_keys`."""
730
- query = "SELECT node_id FROM node WHERE public_key = :public_key;"
731
- row = self.query(query, {"public_key": node_public_key})
732
- if len(row) > 0:
733
- node_id: int = row[0]["node_id"]
734
-
735
- # Convert the sint64 value to uint64 after reading from SQLite
736
- uint64_node_id = convert_sint64_to_uint64(node_id)
737
-
738
- return uint64_node_id
739
- return None
715
+ # Return the public key
716
+ return cast(bytes, rows[0]["public_key"])
740
717
 
741
718
  # pylint: disable=too-many-arguments,too-many-positional-arguments
742
719
  def create_run(
@@ -107,3 +107,11 @@ def verify_signature(
107
107
  return True
108
108
  except InvalidSignature:
109
109
  return False
110
+
111
+
112
+ def uses_nist_ec_curve(public_key: ec.EllipticCurvePublicKey) -> bool:
113
+ """Return True if the provided key uses a NIST EC curve."""
114
+ return isinstance(
115
+ public_key.curve,
116
+ (ec.SECP192R1, ec.SECP224R1, ec.SECP256R1, ec.SECP384R1, ec.SECP521R1),
117
+ )
@@ -28,9 +28,13 @@ from flwr.cli.config_utils import get_fab_metadata
28
28
  from flwr.common import Context, RecordDict, now
29
29
  from flwr.common.constant import (
30
30
  FAB_MAX_SIZE,
31
+ HEARTBEAT_DEFAULT_INTERVAL,
31
32
  LOG_STREAM_INTERVAL,
32
33
  NO_ACCOUNT_AUTH_MESSAGE,
33
34
  NO_ARTIFACT_PROVIDER_MESSAGE,
35
+ NODE_NOT_FOUND_MESSAGE,
36
+ PUBLIC_KEY_ALREADY_IN_USE_MESSAGE,
37
+ PUBLIC_KEY_NOT_VALID,
34
38
  PULL_UNFINISHED_RUN_MESSAGE,
35
39
  RUN_ID_NOT_FOUND_MESSAGE,
36
40
  Status,
@@ -70,6 +74,7 @@ from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
70
74
  from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
71
75
  from flwr.supercore.ffs import FfsFactory
72
76
  from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
77
+ from flwr.supercore.primitives.asymmetric import bytes_to_public_key, uses_nist_ec_curve
73
78
  from flwr.superlink.artifact_provider import ArtifactProvider
74
79
  from flwr.superlink.auth_plugin import ControlAuthnPlugin
75
80
 
@@ -389,13 +394,60 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
389
394
  ) -> CreateNodeCliResponse:
390
395
  """Add a SuperNode."""
391
396
  log(INFO, "ControlServicer.CreateNodeCli")
392
- return CreateNodeCliResponse()
397
+
398
+ # Verify public key
399
+ try:
400
+ # Attempt to deserialize public key
401
+ pub_key = bytes_to_public_key(request.public_key)
402
+ # Check if it's a NIST EC curve public key
403
+ if not uses_nist_ec_curve(pub_key):
404
+ err_msg = "The provided public key is not a NIST EC curve public key."
405
+ log(ERROR, "%s", err_msg)
406
+ raise ValueError(err_msg)
407
+ except (ValueError, AttributeError) as err:
408
+ log(ERROR, "%s", err)
409
+ context.abort(grpc.StatusCode.FAILED_PRECONDITION, PUBLIC_KEY_NOT_VALID)
410
+
411
+ # Init link state
412
+ state = self.linkstate_factory.state()
413
+ node_id = 0
414
+
415
+ flwr_aid = shared_account_info.get().flwr_aid
416
+ flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
417
+ try:
418
+ node_id = state.create_node(
419
+ owner_aid=flwr_aid,
420
+ public_key=request.public_key,
421
+ heartbeat_interval=HEARTBEAT_DEFAULT_INTERVAL,
422
+ )
423
+
424
+ except ValueError:
425
+ # Public key already in use
426
+ log(ERROR, PUBLIC_KEY_ALREADY_IN_USE_MESSAGE)
427
+ context.abort(
428
+ grpc.StatusCode.FAILED_PRECONDITION, PUBLIC_KEY_ALREADY_IN_USE_MESSAGE
429
+ )
430
+ log(INFO, "[ControlServicer.CreateNodeCli] Created node_id=%s", node_id)
431
+
432
+ return CreateNodeCliResponse(node_id=node_id)
393
433
 
394
434
  def DeleteNodeCli(
395
435
  self, request: DeleteNodeCliRequest, context: grpc.ServicerContext
396
436
  ) -> DeleteNodeCliResponse:
397
437
  """Remove a SuperNode."""
398
438
  log(INFO, "ControlServicer.RemoveNode")
439
+
440
+ # Init link state
441
+ state = self.linkstate_factory.state()
442
+
443
+ flwr_aid = shared_account_info.get().flwr_aid
444
+ flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
445
+ try:
446
+ state.delete_node(owner_aid=flwr_aid, node_id=request.node_id)
447
+ except ValueError:
448
+ log(ERROR, NODE_NOT_FOUND_MESSAGE)
449
+ context.abort(grpc.StatusCode.NOT_FOUND, NODE_NOT_FOUND_MESSAGE)
450
+
399
451
  return DeleteNodeCliResponse()
400
452
 
401
453
  def ListNodesCli(
@@ -479,7 +531,7 @@ def _create_list_runs_response(
479
531
 
480
532
  def _check_flwr_aid_exists(
481
533
  flwr_aid: Optional[str], context: grpc.ServicerContext
482
- ) -> None:
534
+ ) -> str:
483
535
  """Guard clause to check if `flwr_aid` exists."""
484
536
  if flwr_aid is None:
485
537
  context.abort(
@@ -487,6 +539,7 @@ def _check_flwr_aid_exists(
487
539
  "️⛔️ Failed to fetch the account information.",
488
540
  )
489
541
  raise RuntimeError # This line is unreachable
542
+ return flwr_aid
490
543
 
491
544
 
492
545
  def _check_flwr_aid_in_run(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flwr-nightly
3
- Version: 1.23.0.dev20251008
3
+ Version: 1.23.0.dev20251010
4
4
  Summary: Flower: A Friendly Federated AI Framework
5
5
  License: Apache-2.0
6
6
  Keywords: Artificial Intelligence,Federated AI,Federated Analytics,Federated Evaluation,Federated Learning,Flower,Machine Learning
@@ -87,10 +87,10 @@ flwr/cli/run/__init__.py,sha256=RPyB7KbYTFl6YRiilCch6oezxrLQrl1kijV7BMGkLbA,790
87
87
  flwr/cli/run/run.py,sha256=ED1mDmO1PnSAgtVOrCeWwzwPm6t3aFYSs3Rh36BJzqk,8161
88
88
  flwr/cli/stop.py,sha256=W7ynTYLm0-_1nC5Il4IaZTji6A3GCCm_0R-HQUudAsI,4988
89
89
  flwr/cli/supernode/__init__.py,sha256=DVrTcyCg9NFll6glPLAAA6WPi7boxu6pFY_PRqIyHMk,893
90
- flwr/cli/supernode/create.py,sha256=MloB7Rr-CE_37jH5lFoM8OsaSPUzWt0TSSB_GMwND5E,1959
91
- flwr/cli/supernode/delete.py,sha256=_oCfBnoeUFPLEq_1EAqQp_CHEdzIEP2N_vHZsFTgWn0,1958
90
+ flwr/cli/supernode/create.py,sha256=9KvRO0IrZa4jw0sypAYxFlzzpjmnzf1KW71b-ySxeuI,6383
91
+ flwr/cli/supernode/delete.py,sha256=SmeKpvVtly8iisNpzJ-MNY-fe0gH2jwwilJs_NMo7do,4528
92
92
  flwr/cli/supernode/ls.py,sha256=exeu-9fpkh27k2oyYNNT7uDhHgf8dlX0TR6WwMV8KIQ,8538
93
- flwr/cli/utils.py,sha256=cdqtT8KrgaH20z2GWZjt2E26UTO6jfkp9qmmH-08hrs,13640
93
+ flwr/cli/utils.py,sha256=66RqZdF0DKsqmR4ZqE9zZ1bX6opdxD2U50GV41WQDoY,14864
94
94
  flwr/client/__init__.py,sha256=Q0MIF442vLIGSkcwHKq_sIfECQynLARJrumAscq2Q6E,1241
95
95
  flwr/client/client.py,sha256=3HAchxvknKG9jYbB7swNyDj-e5vUWDuMKoLvbT7jCVM,7895
96
96
  flwr/client/dpfedavg_numpy_client.py,sha256=3hul067cT2E9jBhzp7bFnFAZ_D2nWcIUEdHYE05FpzU,7404
@@ -126,7 +126,7 @@ flwr/common/__init__.py,sha256=5GCLVk399Az_rTJHNticRlL0Sl_oPw_j5_LuFKfX7-M,4171
126
126
  flwr/common/address.py,sha256=9JucdTwlc-jpeJkRKeUboZoacUtErwSVtnDR9kAtLqE,4119
127
127
  flwr/common/args.py,sha256=Nq2u4yePbkSY0CWFamn0hZY6Rms8G1xYDeDGIcLIITE,5849
128
128
  flwr/common/config.py,sha256=glcZDjco-amw1YfQcYTFJ4S1pt9APoexT-mf1QscuHs,13960
129
- flwr/common/constant.py,sha256=WD-qFz_JiotwVS0VWfepNjpmGEf2QV2bsNCAkeAIJfI,9400
129
+ flwr/common/constant.py,sha256=PKzVHuA9_Hc6ZfKkYxU68R6Kh7lOMpeUJcH9U50yDW8,9583
130
130
  flwr/common/context.py,sha256=Be8obQR_OvEDy1OmshuUKxGRQ7Qx89mf5F4xlhkR10s,2407
131
131
  flwr/common/date.py,sha256=1ZT2cRSpC2DJqprOVTLXYCR_O2_OZR0zXO_brJ3LqWc,1554
132
132
  flwr/common/differential_privacy.py,sha256=FdlpdpPl_H_2HJa8CQM1iCUGBBQ5Dc8CzxmHERM-EoE,6148
@@ -136,7 +136,7 @@ flwr/common/event_log_plugin/__init__.py,sha256=ts3VAL3Fk6Grp1EK_1Qg_V-BfOof9F86
136
136
  flwr/common/event_log_plugin/event_log_plugin.py,sha256=4SkVa1Ic-sPlICJShBuggXmXDcQtWQ1KDby4kthFNF0,2064
137
137
  flwr/common/exit/__init__.py,sha256=8W7xaO1iw0vacgmQW7FTFbSh7csNv6XfsgIlnIbNF6U,978
138
138
  flwr/common/exit/exit.py,sha256=DcXJfbpW1g-pQJqSZmps-1MZydd7T7RaarghIf2e4tU,3636
139
- flwr/common/exit/exit_code.py,sha256=Xa1NFGny2cefZ62kZZOfT8eii__PolMWCHxYmxoSQ2s,5416
139
+ flwr/common/exit/exit_code.py,sha256=g6g10X85mUlN9seFzXZeMNX8e3f2D290DZiF8p2HBAw,5885
140
140
  flwr/common/exit/exit_handler.py,sha256=uzDdWwhKgc1w5csZS52b86kjmEApmDZKwMn_X0zDZZo,2126
141
141
  flwr/common/exit/signal_handler.py,sha256=wqxykrwgmpFzmEMhpnlM7RtO0PnqIvYiSB1qYahZ5Sk,3710
142
142
  flwr/common/grpc.py,sha256=nHnFC7E84pZVTvd6BhcSYWnGd0jf8t5UmGea04qvilM,9806
@@ -191,8 +191,8 @@ flwr/proto/clientappio_pb2.py,sha256=vJjzwWydhg7LruK8cvRAeVQeHPsJztgdIW9nyiPBZF0
191
191
  flwr/proto/clientappio_pb2.pyi,sha256=XbFvpZvvrS7QcH5AFXfpRGl4hQvhd3QdKO6x0oTlCCU,165
192
192
  flwr/proto/clientappio_pb2_grpc.py,sha256=iobNROP0qvn5zddx7k-uIi_dJWP3T_BRp_kbKq086i8,17550
193
193
  flwr/proto/clientappio_pb2_grpc.pyi,sha256=Ytf1O1ktKB0Vsuc3AWLIErGjIJYokzKYzi2uA7mdMeg,4785
194
- flwr/proto/control_pb2.py,sha256=JR38sRRmlrXbr0QgKXyCZ5WsrwOMut1me_gYtBWU3NM,7742
195
- flwr/proto/control_pb2.pyi,sha256=nOvDYkVU0WmsVkkfMs-c0rxS2XvQwO7arq0QjDFRyXs,13380
194
+ flwr/proto/control_pb2.py,sha256=zPejyt6qE3bzHHpSs_RJkTiOJI6YbtRVuNmmNrQgazc,7744
195
+ flwr/proto/control_pb2.pyi,sha256=iBpjs4mVgpa_QeW-01hUk179VyxNa6_vWlDFXApISCU,13386
196
196
  flwr/proto/control_pb2_grpc.py,sha256=Of6tnetwnzvJfJWEXhuTFszz4T_IJbdLJD9uRXBdG04,17182
197
197
  flwr/proto/control_pb2_grpc.pyi,sha256=9z_MRrexz0o3_owST8SvhCjQIIAn-SwMWKIihPHzqdM,4801
198
198
  flwr/proto/error_pb2.py,sha256=PQVWrfjVPo88ql_KgV9nCxyQNCcV9PVfmcw7sOzTMro,1084
@@ -223,8 +223,8 @@ flwr/proto/message_pb2.py,sha256=giymevXYEUdpIO-3A0XKsmRabXW1xSz0sIo5oOlbQ8Y,519
223
223
  flwr/proto/message_pb2.pyi,sha256=EzXZHy2mtabofrd_ZgKSI6M4QH-soIaRZIZBPwBGPv0,11260
224
224
  flwr/proto/message_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
225
225
  flwr/proto/message_pb2_grpc.pyi,sha256=ff2TSiLVnG6IVQcTGzb2DIH3XRSoAvAo_RMcvbMFyc0,76
226
- flwr/proto/node_pb2.py,sha256=QVVaA_zxdQE_g_9iu45p9bUN6rABViJF9Tg-E0EOahg,1534
227
- flwr/proto/node_pb2.pyi,sha256=kmLGmDIqudRtveUop_uVU6w696ahPB_b-O94k3HQIpU,2226
226
+ flwr/proto/node_pb2.py,sha256=C8Pj-6rGP0t44eb4OHkze8wHDf_b_5g8S906tyuqfUI,1572
227
+ flwr/proto/node_pb2.pyi,sha256=hxMkWNE2NjA4j9RVGz8qrfotd4B_1sgdcO4flv-CjHY,2368
228
228
  flwr/proto/node_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
229
229
  flwr/proto/node_pb2_grpc.pyi,sha256=ff2TSiLVnG6IVQcTGzb2DIH3XRSoAvAo_RMcvbMFyc0,76
230
230
  flwr/proto/recorddict_pb2.py,sha256=eVkcnxMTFa3rvknRNiFuJ8z8xxPqgw7bV04aFiTe1j4,5290
@@ -308,19 +308,19 @@ flwr/server/superlink/fleet/grpc_rere/__init__.py,sha256=ahDJJ1e-lDxBpeBMgPk7YZt
308
308
  flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py,sha256=xkmbYjzt7IZCSvtjgyZVTC7Mm18t7pUB6OUxN_Th8nU,8847
309
309
  flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py,sha256=TNK_0-Cpt_s9qTVjm9imke2sF_o_Vb5c-4WDnVg5UGA,5998
310
310
  flwr/server/superlink/fleet/message_handler/__init__.py,sha256=fHsRV0KvJ8HtgSA4_YBsEzuhJLjO8p6xx4aCY2oE1p4,731
311
- flwr/server/superlink/fleet/message_handler/message_handler.py,sha256=-ztViMFASoqlokZp80iQ37svtHUzcp_PagYRD0A7kdc,8692
311
+ flwr/server/superlink/fleet/message_handler/message_handler.py,sha256=ijK-zji1AKkOvJLLIzC2mYQlEh50lI1qPW8QpysNsKk,8751
312
312
  flwr/server/superlink/fleet/rest_rere/__init__.py,sha256=Lzc93nA7tDqoy-zRUaPG316oqFiZX1HUCL5ELaXY_xw,735
313
313
  flwr/server/superlink/fleet/rest_rere/rest_api.py,sha256=mxWKwGpgHPqd7cGFqd2ASnR-KZduIzLfT-d2yiNCqQ0,9257
314
314
  flwr/server/superlink/fleet/vce/__init__.py,sha256=XOKbAWOzlCqEOQ3M2cBYkH7HKA7PxlbCJMunt-ty-DY,784
315
315
  flwr/server/superlink/fleet/vce/backend/__init__.py,sha256=PPH89Yqd1XKm-sRJN6R0WQlKT_b4v54Kzl2yzHAFzM8,1437
316
316
  flwr/server/superlink/fleet/vce/backend/backend.py,sha256=cSrHZ5SjCCvy4vI0pgsyjtx3cDMuMQve8KcKkK-dWWo,2196
317
317
  flwr/server/superlink/fleet/vce/backend/raybackend.py,sha256=cBZYTmfiAsb1HmVUmOQXYLU-UJmJTFWkj1wW4RYRDuc,7218
318
- flwr/server/superlink/fleet/vce/vce_api.py,sha256=DRnRXM8Zr49Qt5F1p39ictquXUxO8wcvV5_Sv5-YzJ4,13108
318
+ flwr/server/superlink/fleet/vce/vce_api.py,sha256=cWlRkgn7G_cS4jw6g1ua6ZcGsyF-1YwP9IgSQ6Dk62Q,13209
319
319
  flwr/server/superlink/linkstate/__init__.py,sha256=OtsgvDTnZLU3k0sUbkHbqoVwW6ql2FDmb6uT6DbNkZo,1064
320
- flwr/server/superlink/linkstate/in_memory_linkstate.py,sha256=2VZLVeJqvxx03ICjmHT_csUqpaMTrQXdBCfjswFGwo8,28677
321
- flwr/server/superlink/linkstate/linkstate.py,sha256=Oo3RLrtQMuS_QMn8IQ-dQg7Mye-HHMMwKFV2AAfxFAw,13306
320
+ flwr/server/superlink/linkstate/in_memory_linkstate.py,sha256=oga9vECdDFSFs6egfOS9iXMQmgc5pWgDSfGwi76XOp8,28082
321
+ flwr/server/superlink/linkstate/linkstate.py,sha256=08C1v7dAMOo5346tKNDJ7Y0OCxhpdPdRU11xPwmOoDw,13400
322
322
  flwr/server/superlink/linkstate/linkstate_factory.py,sha256=8RlosqSpKOoD_vhUUQPY0jtE3A84GeF96Z7sWNkRRcA,2069
323
- flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=3gfHo3x2yW9ZVIjK8Jqo0fJGFdjIotly3NYmOcb97kE,46544
323
+ flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=2a3EP-PFUtg-LUJlct0PQloQ4kuKb0xe18CBsYZHb6U,45361
324
324
  flwr/server/superlink/linkstate/utils.py,sha256=IeLh7iGRCHU5MEWOl7iriaSE4L__8GWOa2OleXadK5M,15444
325
325
  flwr/server/superlink/serverappio/__init__.py,sha256=Fy4zJuoccZe5mZSEIpOmQvU6YeXFBa1M4eZuXXmJcn8,717
326
326
  flwr/server/superlink/serverappio/serverappio_grpc.py,sha256=-I7kBbr4w4ZVYwBZoAIle-xHKthFnZrsVfxa6WR8uxA,2310
@@ -393,7 +393,7 @@ flwr/supercore/object_store/object_store.py,sha256=J-rI3X7ET-F6dqOyM-UfHKCCQtPJ_
393
393
  flwr/supercore/object_store/object_store_factory.py,sha256=QVwE2ywi7vsj2iKfvWWnNw3N_I7Rz91NUt2RpcbJ7iM,1527
394
394
  flwr/supercore/object_store/utils.py,sha256=DcPbrb9PenloAPoQRiKiXX9DrDfpXcSyY0cZpgn4PeQ,1680
395
395
  flwr/supercore/primitives/__init__.py,sha256=Tx8GOjnmMo8Y74RsDGrMpfr-E0Nl8dcUDF784_ge6F8,745
396
- flwr/supercore/primitives/asymmetric.py,sha256=g5NnE_4nnHjqOFCtSi3hVy2CdgQ_G9gcfbOtx-Kmkm0,3502
396
+ flwr/supercore/primitives/asymmetric.py,sha256=wpO0o0G_vStRknFitw2SqyIBSzaBfuXfMc44u-UcxTs,3774
397
397
  flwr/supercore/superexec/__init__.py,sha256=XKX208hZ6a9gZ4KT9kMqfpCtp_8VGxekzKFfHSu2esQ,707
398
398
  flwr/supercore/superexec/plugin/__init__.py,sha256=GNwq8uNdE8RB7ywEFRAvKjLFzgS3YXgz39-HBGsemWw,1035
399
399
  flwr/supercore/superexec/plugin/base_exec_plugin.py,sha256=fL-Ufc9Dp56OhWOzNSJUc7HumbkuSDYqZKwde2opG4g,2074
@@ -415,7 +415,7 @@ flwr/superlink/servicer/control/control_account_auth_interceptor.py,sha256=Tbi4W
415
415
  flwr/superlink/servicer/control/control_event_log_interceptor.py,sha256=5uBl6VcJlUOgCF0d4kmsmJc1Rs1qxyouaZv0-uH2axs,5969
416
416
  flwr/superlink/servicer/control/control_grpc.py,sha256=MRCaX4I2a5ogjKmhtFs6Mj-VdWemxL2h3gU9QbQmvCA,4183
417
417
  flwr/superlink/servicer/control/control_license_interceptor.py,sha256=T3AzmRt-PPwyTq3hrdpmZHQd5_CpPOk7TtnFZrB-JRY,3349
418
- flwr/superlink/servicer/control/control_servicer.py,sha256=mus87XmRsJu_n2Qgm4lltxxvkeX5VkW2xG5HD9THcDY,18515
418
+ flwr/superlink/servicer/control/control_servicer.py,sha256=qQHmhKjn2iIssO63dHSnrK6dZsh_jZrxpSAYH_qV5gA,20550
419
419
  flwr/supernode/__init__.py,sha256=KgeCaVvXWrU3rptNR1y0oBp4YtXbAcrnCcJAiOoWkI4,707
420
420
  flwr/supernode/cli/__init__.py,sha256=JuEMr0-s9zv-PEWKuLB9tj1ocNfroSyNJ-oyv7ati9A,887
421
421
  flwr/supernode/cli/flower_supernode.py,sha256=7aBm0z03OU-npVd1onLCvUotyhSvlZLxAnFkGVMhZcw,8670
@@ -430,7 +430,7 @@ flwr/supernode/servicer/__init__.py,sha256=lucTzre5WPK7G1YLCfaqg3rbFWdNSb7ZTt-ca
430
430
  flwr/supernode/servicer/clientappio/__init__.py,sha256=7Oy62Y_oijqF7Dxi6tpcUQyOpLc_QpIRZ83NvwmB0Yg,813
431
431
  flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=nIHRu38EWK-rpNOkcgBRAAKwYQQWFeCwu0lkO7OPZGQ,10239
432
432
  flwr/supernode/start_client_internal.py,sha256=Y9S1-QlO2WP6eo4JvWzIpfaCoh2aoE7bjEYyxNNnlyg,20777
433
- flwr_nightly-1.23.0.dev20251008.dist-info/METADATA,sha256=v2GJDfsgn1KfcGY4ecjmYoEGrmOn9OHbjbo-lEMPfdQ,14559
434
- flwr_nightly-1.23.0.dev20251008.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
435
- flwr_nightly-1.23.0.dev20251008.dist-info/entry_points.txt,sha256=hxHD2ixb_vJFDOlZV-zB4Ao32_BQlL34ftsDh1GXv14,420
436
- flwr_nightly-1.23.0.dev20251008.dist-info/RECORD,,
433
+ flwr_nightly-1.23.0.dev20251010.dist-info/METADATA,sha256=HtYyoP2UzTQPv3_ixhl6ZPE6VLR2UhzoiWLCspZ9qCM,14559
434
+ flwr_nightly-1.23.0.dev20251010.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
435
+ flwr_nightly-1.23.0.dev20251010.dist-info/entry_points.txt,sha256=hxHD2ixb_vJFDOlZV-zB4Ao32_BQlL34ftsDh1GXv14,420
436
+ flwr_nightly-1.23.0.dev20251010.dist-info/RECORD,,