flwr 1.21.0__py3-none-any.whl → 1.23.0__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.
Files changed (175) hide show
  1. flwr/cli/app.py +17 -1
  2. flwr/cli/auth_plugin/__init__.py +15 -6
  3. flwr/cli/auth_plugin/auth_plugin.py +95 -0
  4. flwr/cli/auth_plugin/noop_auth_plugin.py +58 -0
  5. flwr/cli/auth_plugin/oidc_cli_plugin.py +16 -25
  6. flwr/cli/build.py +118 -47
  7. flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +6 -5
  8. flwr/cli/log.py +2 -2
  9. flwr/cli/login/login.py +34 -23
  10. flwr/cli/ls.py +13 -9
  11. flwr/cli/new/new.py +196 -42
  12. flwr/cli/new/templates/app/README.flowertune.md.tpl +1 -1
  13. flwr/cli/new/templates/app/code/client.baseline.py.tpl +64 -47
  14. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +68 -30
  15. flwr/cli/new/templates/app/code/client.jax.py.tpl +63 -42
  16. flwr/cli/new/templates/app/code/client.mlx.py.tpl +80 -51
  17. flwr/cli/new/templates/app/code/client.numpy.py.tpl +36 -13
  18. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +71 -46
  19. flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +55 -0
  20. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +75 -30
  21. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +69 -44
  22. flwr/cli/new/templates/app/code/client.xgboost.py.tpl +110 -0
  23. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +56 -90
  24. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +1 -23
  25. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +37 -58
  26. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +39 -44
  27. flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -14
  28. flwr/cli/new/templates/app/code/server.baseline.py.tpl +27 -29
  29. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -19
  30. flwr/cli/new/templates/app/code/server.jax.py.tpl +27 -14
  31. flwr/cli/new/templates/app/code/server.mlx.py.tpl +29 -19
  32. flwr/cli/new/templates/app/code/server.numpy.py.tpl +30 -17
  33. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +36 -26
  34. flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +31 -0
  35. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +29 -21
  36. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +28 -19
  37. flwr/cli/new/templates/app/code/server.xgboost.py.tpl +56 -0
  38. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +16 -20
  39. flwr/cli/new/templates/app/code/task.jax.py.tpl +1 -1
  40. flwr/cli/new/templates/app/code/task.numpy.py.tpl +1 -1
  41. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +14 -27
  42. flwr/cli/new/templates/app/code/{task.pytorch_msg_api.py.tpl → task.pytorch_legacy_api.py.tpl} +27 -14
  43. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +1 -2
  44. flwr/cli/new/templates/app/code/task.xgboost.py.tpl +67 -0
  45. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
  46. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -2
  47. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
  48. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  49. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -2
  50. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  51. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  52. flwr/cli/new/templates/app/{pyproject.pytorch_msg_api.toml.tpl → pyproject.pytorch_legacy_api.toml.tpl} +3 -3
  53. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  54. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  55. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +61 -0
  56. flwr/cli/pull.py +100 -0
  57. flwr/cli/run/run.py +11 -7
  58. flwr/cli/stop.py +2 -2
  59. flwr/cli/supernode/__init__.py +25 -0
  60. flwr/cli/supernode/ls.py +260 -0
  61. flwr/cli/supernode/register.py +185 -0
  62. flwr/cli/supernode/unregister.py +138 -0
  63. flwr/cli/utils.py +109 -69
  64. flwr/client/__init__.py +2 -1
  65. flwr/client/grpc_adapter_client/connection.py +6 -8
  66. flwr/client/grpc_rere_client/connection.py +59 -31
  67. flwr/client/grpc_rere_client/grpc_adapter.py +28 -12
  68. flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +3 -6
  69. flwr/client/mod/secure_aggregation/secaggplus_mod.py +7 -5
  70. flwr/client/rest_client/connection.py +82 -37
  71. flwr/clientapp/__init__.py +1 -2
  72. flwr/clientapp/mod/__init__.py +4 -1
  73. flwr/clientapp/mod/centraldp_mods.py +156 -40
  74. flwr/clientapp/mod/localdp_mod.py +169 -0
  75. flwr/clientapp/typing.py +22 -0
  76. flwr/{client/clientapp → clientapp}/utils.py +1 -1
  77. flwr/common/constant.py +56 -13
  78. flwr/common/exit/exit_code.py +24 -10
  79. flwr/common/inflatable_utils.py +10 -10
  80. flwr/common/record/array.py +3 -3
  81. flwr/common/record/arrayrecord.py +10 -1
  82. flwr/common/record/typeddict.py +12 -0
  83. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
  84. flwr/common/serde.py +4 -2
  85. flwr/common/typing.py +7 -6
  86. flwr/compat/client/app.py +1 -1
  87. flwr/compat/client/grpc_client/connection.py +2 -2
  88. flwr/proto/control_pb2.py +48 -31
  89. flwr/proto/control_pb2.pyi +95 -5
  90. flwr/proto/control_pb2_grpc.py +136 -0
  91. flwr/proto/control_pb2_grpc.pyi +52 -0
  92. flwr/proto/fab_pb2.py +11 -7
  93. flwr/proto/fab_pb2.pyi +21 -1
  94. flwr/proto/fleet_pb2.py +31 -23
  95. flwr/proto/fleet_pb2.pyi +63 -23
  96. flwr/proto/fleet_pb2_grpc.py +98 -28
  97. flwr/proto/fleet_pb2_grpc.pyi +45 -13
  98. flwr/proto/node_pb2.py +3 -1
  99. flwr/proto/node_pb2.pyi +48 -0
  100. flwr/server/app.py +152 -114
  101. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +17 -7
  102. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +132 -38
  103. flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +27 -51
  104. flwr/server/superlink/fleet/message_handler/message_handler.py +67 -22
  105. flwr/server/superlink/fleet/rest_rere/rest_api.py +52 -31
  106. flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
  107. flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
  108. flwr/server/superlink/fleet/vce/vce_api.py +18 -5
  109. flwr/server/superlink/linkstate/in_memory_linkstate.py +167 -73
  110. flwr/server/superlink/linkstate/linkstate.py +107 -24
  111. flwr/server/superlink/linkstate/linkstate_factory.py +2 -1
  112. flwr/server/superlink/linkstate/sqlite_linkstate.py +306 -255
  113. flwr/server/superlink/linkstate/utils.py +3 -54
  114. flwr/server/superlink/serverappio/serverappio_servicer.py +2 -2
  115. flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
  116. flwr/server/utils/validator.py +2 -3
  117. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +4 -2
  118. flwr/serverapp/strategy/__init__.py +26 -0
  119. flwr/serverapp/strategy/bulyan.py +238 -0
  120. flwr/serverapp/strategy/dp_adaptive_clipping.py +335 -0
  121. flwr/serverapp/strategy/dp_fixed_clipping.py +71 -49
  122. flwr/serverapp/strategy/fedadagrad.py +0 -3
  123. flwr/serverapp/strategy/fedadam.py +0 -3
  124. flwr/serverapp/strategy/fedavg.py +89 -64
  125. flwr/serverapp/strategy/fedavgm.py +198 -0
  126. flwr/serverapp/strategy/fedmedian.py +105 -0
  127. flwr/serverapp/strategy/fedprox.py +174 -0
  128. flwr/serverapp/strategy/fedtrimmedavg.py +176 -0
  129. flwr/serverapp/strategy/fedxgb_bagging.py +117 -0
  130. flwr/serverapp/strategy/fedxgb_cyclic.py +220 -0
  131. flwr/serverapp/strategy/fedyogi.py +0 -3
  132. flwr/serverapp/strategy/krum.py +112 -0
  133. flwr/serverapp/strategy/multikrum.py +247 -0
  134. flwr/serverapp/strategy/qfedavg.py +252 -0
  135. flwr/serverapp/strategy/strategy_utils.py +48 -0
  136. flwr/simulation/app.py +1 -1
  137. flwr/simulation/ray_transport/ray_actor.py +1 -1
  138. flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
  139. flwr/simulation/run_simulation.py +28 -32
  140. flwr/supercore/cli/flower_superexec.py +26 -1
  141. flwr/supercore/constant.py +41 -0
  142. flwr/supercore/object_store/in_memory_object_store.py +0 -4
  143. flwr/supercore/object_store/object_store_factory.py +26 -6
  144. flwr/supercore/object_store/sqlite_object_store.py +252 -0
  145. flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
  146. flwr/supercore/primitives/asymmetric.py +117 -0
  147. flwr/supercore/primitives/asymmetric_ed25519.py +165 -0
  148. flwr/supercore/sqlite_mixin.py +156 -0
  149. flwr/supercore/superexec/plugin/exec_plugin.py +11 -1
  150. flwr/supercore/superexec/run_superexec.py +16 -2
  151. flwr/supercore/utils.py +20 -0
  152. flwr/superlink/artifact_provider/__init__.py +22 -0
  153. flwr/superlink/artifact_provider/artifact_provider.py +37 -0
  154. flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
  155. flwr/superlink/auth_plugin/auth_plugin.py +91 -0
  156. flwr/superlink/auth_plugin/noop_auth_plugin.py +87 -0
  157. flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +19 -19
  158. flwr/superlink/servicer/control/control_event_log_interceptor.py +1 -1
  159. flwr/superlink/servicer/control/control_grpc.py +16 -11
  160. flwr/superlink/servicer/control/control_servicer.py +207 -58
  161. flwr/supernode/cli/flower_supernode.py +19 -26
  162. flwr/supernode/runtime/run_clientapp.py +2 -2
  163. flwr/supernode/servicer/clientappio/clientappio_servicer.py +1 -1
  164. flwr/supernode/start_client_internal.py +17 -9
  165. {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/METADATA +6 -16
  166. {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/RECORD +170 -140
  167. flwr/cli/new/templates/app/code/client.pytorch_msg_api.py.tpl +0 -80
  168. flwr/cli/new/templates/app/code/server.pytorch_msg_api.py.tpl +0 -41
  169. flwr/common/auth_plugin/auth_plugin.py +0 -149
  170. flwr/serverapp/dp_fixed_clipping.py +0 -352
  171. flwr/serverapp/strategy/strategy_utils_tests.py +0 -304
  172. /flwr/cli/new/templates/app/code/{__init__.pytorch_msg_api.py.tpl → __init__.pytorch_legacy_api.py.tpl} +0 -0
  173. /flwr/{client → clientapp}/client_app.py +0 -0
  174. {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/WHEEL +0 -0
  175. {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/entry_points.txt +0 -0
@@ -16,13 +16,26 @@
16
16
 
17
17
 
18
18
  from collections import OrderedDict
19
- from logging import INFO, WARN
19
+ from logging import ERROR, INFO
20
20
  from typing import cast
21
21
 
22
- from flwr.client.typing import ClientAppCallable
23
- from flwr.common import Array, ArrayRecord, Context, Message, MessageType, log
24
- from flwr.common.differential_privacy import compute_clip_model_update
25
- from flwr.common.differential_privacy_constants import KEY_CLIPPING_NORM
22
+ from flwr.app import Error
23
+ from flwr.clientapp.typing import ClientAppCallable
24
+ from flwr.common import (
25
+ Array,
26
+ ArrayRecord,
27
+ ConfigRecord,
28
+ Context,
29
+ Message,
30
+ MetricRecord,
31
+ log,
32
+ )
33
+ from flwr.common.constant import ErrorCode
34
+ from flwr.common.differential_privacy import (
35
+ compute_adaptive_clip_model_update,
36
+ compute_clip_model_update,
37
+ )
38
+ from flwr.common.differential_privacy_constants import KEY_CLIPPING_NORM, KEY_NORM_BIT
26
39
 
27
40
 
28
41
  # pylint: disable=too-many-return-statements
@@ -46,33 +59,15 @@ def fixedclipping_mod(
46
59
 
47
60
  Typically, fixedclipping_mod should be the last to operate on params.
48
61
  """
49
- if msg.metadata.message_type != MessageType.TRAIN:
50
- return call_next(msg, ctxt)
51
-
52
62
  if len(msg.content.array_records) != 1:
53
- log(
54
- WARN,
55
- "fixedclipping_mod is designed to work with a single ArrayRecord. "
56
- "Skipping.",
57
- )
58
- return call_next(msg, ctxt)
59
-
63
+ return _handle_multi_record_err("fixedclipping_mod", msg, ArrayRecord)
60
64
  if len(msg.content.config_records) != 1:
61
- log(
62
- WARN,
63
- "fixedclipping_mod is designed to work with a single ConfigRecord. "
64
- "Skipping.",
65
- )
66
- return call_next(msg, ctxt)
65
+ return _handle_multi_record_err("fixedclipping_mod", msg, ConfigRecord)
67
66
 
68
67
  # Get keys in the single ConfigRecord
69
68
  keys_in_config = set(next(iter(msg.content.config_records.values())).keys())
70
69
  if KEY_CLIPPING_NORM not in keys_in_config:
71
- raise KeyError(
72
- f"The {KEY_CLIPPING_NORM} value is not supplied by the "
73
- f"`DifferentialPrivacyClientSideFixedClipping` wrapper at"
74
- f" the server side."
75
- )
70
+ return _handle_no_key_err("fixedclipping_mod", msg)
76
71
  # Record array record communicated to client and clipping norm
77
72
  original_array_record = next(iter(msg.content.array_records.values()))
78
73
  clipping_norm = cast(
@@ -86,26 +81,16 @@ def fixedclipping_mod(
86
81
  if out_msg.has_error():
87
82
  return out_msg
88
83
 
89
- # Ensure there is a single ArrayRecord
84
+ # Ensure reply has a single ArrayRecord
90
85
  if len(out_msg.content.array_records) != 1:
91
- log(
92
- WARN,
93
- "fixedclipping_mod is designed to work with a single ArrayRecord. "
94
- "Skipping.",
95
- )
96
- return out_msg
86
+ return _handle_multi_record_err("fixedclipping_mod", out_msg, ArrayRecord)
97
87
 
98
88
  new_array_record_key, client_to_server_arrecord = next(
99
89
  iter(out_msg.content.array_records.items())
100
90
  )
101
91
  # Ensure keys in returned ArrayRecord match those in the one sent from server
102
- if set(original_array_record.keys()) != set(client_to_server_arrecord.keys()):
103
- log(
104
- WARN,
105
- "fixedclipping_mod: Keys in ArrayRecord must match those from the model "
106
- "that the ClientApp received. Skipping.",
107
- )
108
- return out_msg
92
+ if list(original_array_record.keys()) != list(client_to_server_arrecord.keys()):
93
+ return _handle_array_key_mismatch_err("fixedclipping_mod", out_msg)
109
94
 
110
95
  client_to_server_ndarrays = client_to_server_arrecord.to_numpy_ndarrays()
111
96
  # Clip the client update
@@ -130,3 +115,134 @@ def fixedclipping_mod(
130
115
  )
131
116
  )
132
117
  return out_msg
118
+
119
+
120
+ def adaptiveclipping_mod(
121
+ msg: Message, ctxt: Context, call_next: ClientAppCallable
122
+ ) -> Message:
123
+ """Client-side adaptive clipping modifier.
124
+
125
+ This mod needs to be used with the DifferentialPrivacyClientSideAdaptiveClipping
126
+ server-side strategy wrapper.
127
+
128
+ The wrapper sends the clipping_norm value to the client.
129
+
130
+ This mod clips the client model updates before sending them to the server.
131
+
132
+ It also sends KEY_NORM_BIT to the server for computing the new clipping value.
133
+
134
+ It operates on messages of type `MessageType.TRAIN`.
135
+
136
+ Notes
137
+ -----
138
+ Consider the order of mods when using multiple.
139
+
140
+ Typically, adaptiveclipping_mod should be the last to operate on params.
141
+ """
142
+ if len(msg.content.array_records) != 1:
143
+ return _handle_multi_record_err("adaptiveclipping_mod", msg, ArrayRecord)
144
+ if len(msg.content.config_records) != 1:
145
+ return _handle_multi_record_err("adaptiveclipping_mod", msg, ConfigRecord)
146
+
147
+ # Get keys in the single ConfigRecord
148
+ keys_in_config = set(next(iter(msg.content.config_records.values())).keys())
149
+ if KEY_CLIPPING_NORM not in keys_in_config:
150
+ return _handle_no_key_err("adaptiveclipping_mod", msg)
151
+
152
+ # Record array record communicated to client and clipping norm
153
+ original_array_record = next(iter(msg.content.array_records.values()))
154
+ clipping_norm = cast(
155
+ float, next(iter(msg.content.config_records.values()))[KEY_CLIPPING_NORM]
156
+ )
157
+
158
+ # Call inner app
159
+ out_msg = call_next(msg, ctxt)
160
+
161
+ # Check if the msg has error
162
+ if out_msg.has_error():
163
+ return out_msg
164
+
165
+ # Ensure reply has a single ArrayRecord
166
+ if len(out_msg.content.array_records) != 1:
167
+ return _handle_multi_record_err("adaptiveclipping_mod", out_msg, ArrayRecord)
168
+
169
+ # Ensure reply has a single MetricRecord
170
+ if len(out_msg.content.metric_records) != 1:
171
+ return _handle_multi_record_err("adaptiveclipping_mod", out_msg, MetricRecord)
172
+
173
+ new_array_record_key, client_to_server_arrecord = next(
174
+ iter(out_msg.content.array_records.items())
175
+ )
176
+
177
+ # Ensure keys in returned ArrayRecord match those in the one sent from server
178
+ if list(original_array_record.keys()) != list(client_to_server_arrecord.keys()):
179
+ return _handle_array_key_mismatch_err("adaptiveclipping_mod", out_msg)
180
+
181
+ client_to_server_ndarrays = client_to_server_arrecord.to_numpy_ndarrays()
182
+ # Clip the client update
183
+ norm_bit = compute_adaptive_clip_model_update(
184
+ client_to_server_ndarrays,
185
+ original_array_record.to_numpy_ndarrays(),
186
+ clipping_norm,
187
+ )
188
+ log(
189
+ INFO,
190
+ "adaptiveclipping_mod: ndarrays are clipped by value: %.4f.",
191
+ clipping_norm,
192
+ )
193
+ # Replace outgoing ArrayRecord's Array while preserving their keys
194
+ out_msg.content.array_records[new_array_record_key] = ArrayRecord(
195
+ OrderedDict(
196
+ {
197
+ k: Array(v)
198
+ for k, v in zip(
199
+ client_to_server_arrecord.keys(), client_to_server_ndarrays
200
+ )
201
+ }
202
+ )
203
+ )
204
+ # Add to the MetricRecords the norm bit (recall reply messages only contain
205
+ # one MetricRecord)
206
+ metric_record_key = list(out_msg.content.metric_records.keys())[0]
207
+ # We cast it to `int` because MetricRecord does not support `bool` values
208
+ out_msg.content.metric_records[metric_record_key][KEY_NORM_BIT] = int(norm_bit)
209
+ return out_msg
210
+
211
+
212
+ def _handle_err(msg: Message, reason: str) -> Message:
213
+ """Log and return error message."""
214
+ log(ERROR, reason)
215
+ return Message(
216
+ Error(code=ErrorCode.MOD_FAILED_PRECONDITION, reason=reason),
217
+ reply_to=msg,
218
+ )
219
+
220
+
221
+ def _handle_multi_record_err(mod_name: str, msg: Message, record_type: type) -> Message:
222
+ """Log and return multi-record error."""
223
+ cnt = sum(isinstance(_, record_type) for _ in msg.content.values())
224
+ return _handle_err(
225
+ msg,
226
+ f"{mod_name} expects exactly one {record_type.__name__}, "
227
+ f"but found {cnt} {record_type.__name__}(s).",
228
+ )
229
+
230
+
231
+ def _handle_no_key_err(mod_name: str, msg: Message) -> Message:
232
+ """Log and return no-key error."""
233
+ return _handle_err(
234
+ msg,
235
+ f"{mod_name} requires the key '{KEY_CLIPPING_NORM}' to be present in the "
236
+ "ConfigRecord, but it was not found. "
237
+ "Please ensure the `DifferentialPrivacyClientSideFixedClipping` wrapper "
238
+ "is used in the ServerApp.",
239
+ )
240
+
241
+
242
+ def _handle_array_key_mismatch_err(mod_name: str, msg: Message) -> Message:
243
+ """Create array-key-mismatch error reasons."""
244
+ return _handle_err(
245
+ msg,
246
+ f"{mod_name} expects the keys in the ArrayRecord of the reply message to match "
247
+ "those from the ArrayRecord that the ClientApp received, but they do not.",
248
+ )
@@ -0,0 +1,169 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Local DP modifier."""
16
+
17
+
18
+ from collections import OrderedDict
19
+ from logging import INFO
20
+
21
+ import numpy as np
22
+
23
+ from flwr.clientapp.typing import ClientAppCallable
24
+ from flwr.common import Array, ArrayRecord
25
+ from flwr.common.context import Context
26
+ from flwr.common.differential_privacy import (
27
+ add_gaussian_noise_inplace,
28
+ compute_clip_model_update,
29
+ )
30
+ from flwr.common.logger import log
31
+ from flwr.common.message import Message
32
+
33
+ from .centraldp_mods import _handle_array_key_mismatch_err, _handle_multi_record_err
34
+
35
+
36
+ class LocalDpMod:
37
+ """Modifier for local differential privacy.
38
+
39
+ This mod clips the client model updates and
40
+ adds noise to the params before sending them to the server.
41
+
42
+ It operates on messages of type `MessageType.TRAIN`.
43
+
44
+ Parameters
45
+ ----------
46
+ clipping_norm : float
47
+ The value of the clipping norm.
48
+ sensitivity : float
49
+ The sensitivity of the client model.
50
+ epsilon : float
51
+ The privacy budget.
52
+ Smaller value of epsilon indicates a higher level of privacy protection.
53
+ delta : float
54
+ The failure probability.
55
+ The probability that the privacy mechanism
56
+ fails to provide the desired level of privacy.
57
+ A smaller value of delta indicates a stricter privacy guarantee.
58
+
59
+ Examples
60
+ --------
61
+ Create an instance of the local DP mod and add it to the client-side mods::
62
+
63
+ local_dp_mod = LocalDpMod( ... )
64
+ app = fl.client.ClientApp(mods=[local_dp_mod])
65
+ """
66
+
67
+ def __init__(
68
+ self, clipping_norm: float, sensitivity: float, epsilon: float, delta: float
69
+ ) -> None:
70
+ if clipping_norm <= 0:
71
+ raise ValueError("The clipping norm should be a positive value.")
72
+
73
+ if sensitivity < 0:
74
+ raise ValueError("The sensitivity should be a non-negative value.")
75
+
76
+ if epsilon < 0:
77
+ raise ValueError("Epsilon should be a non-negative value.")
78
+
79
+ if delta < 0:
80
+ raise ValueError("Delta should be a non-negative value.")
81
+
82
+ self.clipping_norm = clipping_norm
83
+ self.sensitivity = sensitivity
84
+ self.epsilon = epsilon
85
+ self.delta = delta
86
+
87
+ def __call__(
88
+ self, msg: Message, ctxt: Context, call_next: ClientAppCallable
89
+ ) -> Message:
90
+ """Perform local DP on the client model parameters.
91
+
92
+ Parameters
93
+ ----------
94
+ msg : Message
95
+ The message received from the ServerApp.
96
+ ctxt : Context
97
+ The context of the ClientApp.
98
+ call_next : ClientAppCallable
99
+ The callable to call the next mod (or the ClientApp) in the chain.
100
+
101
+ Returns
102
+ -------
103
+ Message
104
+ The modified message to be sent back to the server.
105
+ """
106
+ if len(msg.content.array_records) != 1:
107
+ return _handle_multi_record_err("LocalDpMod", msg, ArrayRecord)
108
+
109
+ # Record array record communicated to client and clipping norm
110
+ original_array_record = next(iter(msg.content.array_records.values()))
111
+
112
+ # Call inner app
113
+ out_msg = call_next(msg, ctxt)
114
+
115
+ # Check if the msg has error
116
+ if out_msg.has_error():
117
+ return out_msg
118
+
119
+ # Ensure reply has a single ArrayRecord
120
+ if len(out_msg.content.array_records) != 1:
121
+ return _handle_multi_record_err("LocalDpMod", out_msg, ArrayRecord)
122
+
123
+ new_array_record_key, client_to_server_arrecord = next(
124
+ iter(out_msg.content.array_records.items())
125
+ )
126
+
127
+ # Ensure keys in returned ArrayRecord match those in the one sent from server
128
+ if list(original_array_record.keys()) != list(client_to_server_arrecord.keys()):
129
+ return _handle_array_key_mismatch_err("LocalDpMod", out_msg)
130
+
131
+ client_to_server_ndarrays = client_to_server_arrecord.to_numpy_ndarrays()
132
+
133
+ # Clip the client update
134
+ compute_clip_model_update(
135
+ client_to_server_ndarrays,
136
+ original_array_record.to_numpy_ndarrays(),
137
+ self.clipping_norm,
138
+ )
139
+ log(
140
+ INFO,
141
+ "LocalDpMod: parameters are clipped by value: %.4f.",
142
+ self.clipping_norm,
143
+ )
144
+
145
+ std_dev = (
146
+ self.sensitivity * np.sqrt(2 * np.log(1.25 / self.delta)) / self.epsilon
147
+ )
148
+ add_gaussian_noise_inplace(
149
+ client_to_server_ndarrays,
150
+ std_dev,
151
+ )
152
+ log(
153
+ INFO,
154
+ "LocalDpMod: local DP noise with %.4f stddev added to parameters",
155
+ std_dev,
156
+ )
157
+
158
+ # Replace outgoing ArrayRecord's Array while preserving their keys
159
+ out_msg.content[new_array_record_key] = ArrayRecord(
160
+ OrderedDict(
161
+ {
162
+ k: Array(v)
163
+ for k, v in zip(
164
+ client_to_server_arrecord.keys(), client_to_server_ndarrays
165
+ )
166
+ }
167
+ )
168
+ )
169
+ return out_msg
@@ -0,0 +1,22 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Custom types for Flower clients."""
16
+
17
+
18
+ from typing import Callable
19
+
20
+ from flwr.common import Context, Message
21
+
22
+ ClientAppCallable = Callable[[Message, Context], Message]
@@ -19,7 +19,7 @@ from logging import DEBUG
19
19
  from pathlib import Path
20
20
  from typing import Callable, Optional
21
21
 
22
- from flwr.client.client_app import ClientApp, LoadClientAppError
22
+ from flwr.clientapp.client_app import ClientApp, LoadClientAppError
23
23
  from flwr.common.config import (
24
24
  get_flwr_dir,
25
25
  get_metadata_from_config,
flwr/common/constant.py CHANGED
@@ -17,6 +17,8 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import os
21
+
20
22
  TRANSPORT_TYPE_GRPC_BIDI = "grpc-bidi"
21
23
  TRANSPORT_TYPE_GRPC_RERE = "grpc-rere"
22
24
  TRANSPORT_TYPE_GRPC_ADAPTER = "grpc-adapter"
@@ -60,7 +62,9 @@ HEARTBEAT_DEFAULT_INTERVAL = 30
60
62
  HEARTBEAT_CALL_TIMEOUT = 5
61
63
  HEARTBEAT_BASE_MULTIPLIER = 0.8
62
64
  HEARTBEAT_RANDOM_RANGE = (-0.1, 0.1)
63
- HEARTBEAT_MAX_INTERVAL = 1e300
65
+ HEARTBEAT_MIN_INTERVAL = 10
66
+ HEARTBEAT_MAX_INTERVAL = 1800 # 30 minutes
67
+ HEARTBEAT_INTERVAL_INF = 1e300 # Large value, disabling heartbeats
64
68
  HEARTBEAT_PATIENCE = 2
65
69
  RUN_FAILURE_DETAILS_NO_HEARTBEAT = "No heartbeat received from the run."
66
70
 
@@ -70,13 +74,23 @@ NODE_ID_NUM_BYTES = 8
70
74
 
71
75
  # Constants for FAB
72
76
  APP_DIR = "apps"
73
- FAB_ALLOWED_EXTENSIONS = {".py", ".toml", ".md"}
74
77
  FAB_CONFIG_FILE = "pyproject.toml"
75
78
  FAB_DATE = (2024, 10, 1, 0, 0, 0)
76
79
  FAB_HASH_TRUNCATION = 8
77
80
  FAB_MAX_SIZE = 10 * 1024 * 1024 # 10 MB
78
81
  FLWR_DIR = ".flwr" # The default Flower directory: ~/.flwr/
79
82
  FLWR_HOME = "FLWR_HOME" # If set, override the default Flower directory
83
+ # FAB file include patterns (gitignore-style patterns)
84
+ FAB_INCLUDE_PATTERNS = (
85
+ "**/*.py",
86
+ "**/*.toml",
87
+ "**/*.md",
88
+ )
89
+ # FAB file exclude patterns (gitignore-style patterns)
90
+ FAB_EXCLUDE_PATTERNS = (
91
+ "**/__pycache__/**",
92
+ FAB_CONFIG_FILE, # Exclude the original pyproject.toml
93
+ )
80
94
 
81
95
  # Constant for SuperLink
82
96
  SUPERLINK_NODE_ID = 1
@@ -109,14 +123,14 @@ LOG_UPLOAD_INTERVAL = 0.2 # Minimum interval between two log uploads
109
123
  # Retry configurations
110
124
  MAX_RETRY_DELAY = 20 # Maximum delay duration between two consecutive retries.
111
125
 
112
- # Constants for user authentication
126
+ # Constants for account authentication
113
127
  CREDENTIALS_DIR = ".credentials"
114
- AUTH_TYPE_JSON_KEY = "auth-type" # For key name in JSON file
115
- AUTH_TYPE_YAML_KEY = "auth_type" # For key name in YAML file
128
+ AUTHN_TYPE_JSON_KEY = "authn-type" # For key name in JSON file
129
+ AUTHN_TYPE_YAML_KEY = "authn_type" # For key name in YAML file
116
130
  ACCESS_TOKEN_KEY = "flwr-oidc-access-token"
117
131
  REFRESH_TOKEN_KEY = "flwr-oidc-refresh-token"
118
132
 
119
- # Constants for user authorization
133
+ # Constants for account authorization
120
134
  AUTHZ_TYPE_YAML_KEY = "authz_type" # For key name in YAML file
121
135
 
122
136
  # Constants for node authentication
@@ -135,7 +149,9 @@ GC_THRESHOLD = 200_000_000 # 200 MB
135
149
  # Constants for Inflatable
136
150
  HEAD_BODY_DIVIDER = b"\x00"
137
151
  HEAD_VALUE_DIVIDER = " "
138
- MAX_ARRAY_CHUNK_SIZE = 20_971_520 # 20 MB
152
+ FLWR_PRIVATE_MAX_ARRAY_CHUNK_SIZE = int(
153
+ os.getenv("FLWR_PRIVATE_MAX_ARRAY_CHUNK_SIZE", "5242880")
154
+ ) # 5 MB
139
155
 
140
156
  # Constants for serialization
141
157
  INT64_MAX_VALUE = 9223372036854775807 # (1 << 63) - 1
@@ -144,8 +160,12 @@ INT64_MAX_VALUE = 9223372036854775807 # (1 << 63) - 1
144
160
  FLWR_APP_TOKEN_LENGTH = 128 # Length of the token used
145
161
 
146
162
  # Constants for object pushing and pulling
147
- MAX_CONCURRENT_PUSHES = 8 # Default maximum number of concurrent pushes
148
- MAX_CONCURRENT_PULLS = 8 # Default maximum number of concurrent pulls
163
+ FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PUSHES = int(
164
+ os.getenv("FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PUSHES", "2")
165
+ ) # Default maximum number of concurrent pushes
166
+ FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PULLS = int(
167
+ os.getenv("FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PULLS", "2")
168
+ ) # Default maximum number of concurrent pulls
149
169
  PULL_MAX_TIME = 7200 # Default maximum time to wait for pulling objects
150
170
  PULL_MAX_TRIES_PER_OBJECT = 500 # Default maximum number of tries to pull an object
151
171
  PULL_INITIAL_BACKOFF = 1 # Initial backoff time for pulling objects
@@ -154,7 +174,13 @@ PULL_BACKOFF_CAP = 10 # Maximum backoff time for pulling objects
154
174
 
155
175
  # ControlServicer constants
156
176
  RUN_ID_NOT_FOUND_MESSAGE = "Run ID not found"
157
- NO_USER_AUTH_MESSAGE = "ControlServicer initialized without user authentication"
177
+ NO_ACCOUNT_AUTH_MESSAGE = "ControlServicer initialized without account authentication"
178
+ NO_ARTIFACT_PROVIDER_MESSAGE = "ControlServicer initialized without artifact provider"
179
+ PULL_UNFINISHED_RUN_MESSAGE = "Cannot pull artifacts for an unfinished run"
180
+ SUPERNODE_NOT_CREATED_FROM_CLI_MESSAGE = "Invalid SuperNode credentials"
181
+ PUBLIC_KEY_ALREADY_IN_USE_MESSAGE = "Public key already in use"
182
+ PUBLIC_KEY_NOT_VALID = "The provided public key is not valid"
183
+ NODE_NOT_FOUND_MESSAGE = "Node ID not found for account"
158
184
 
159
185
 
160
186
  class MessageType:
@@ -200,6 +226,7 @@ class ErrorCode:
200
226
  MESSAGE_UNAVAILABLE = 3
201
227
  REPLY_MESSAGE_UNAVAILABLE = 4
202
228
  NODE_UNAVAILABLE = 5
229
+ MOD_FAILED_PRECONDITION = 6
203
230
 
204
231
  def __new__(cls) -> ErrorCode:
205
232
  """Prevent instantiation."""
@@ -242,12 +269,23 @@ class CliOutputFormat:
242
269
  raise TypeError(f"{cls.__name__} cannot be instantiated.")
243
270
 
244
271
 
245
- class AuthType:
246
- """User authentication types."""
272
+ class AuthnType:
273
+ """Account authentication types."""
247
274
 
275
+ NOOP = "noop"
248
276
  OIDC = "oidc"
249
277
 
250
- def __new__(cls) -> AuthType:
278
+ def __new__(cls) -> AuthnType:
279
+ """Prevent instantiation."""
280
+ raise TypeError(f"{cls.__name__} cannot be instantiated.")
281
+
282
+
283
+ class AuthzType:
284
+ """Account authorization types."""
285
+
286
+ NOOP = "noop"
287
+
288
+ def __new__(cls) -> AuthzType:
251
289
  """Prevent instantiation."""
252
290
  raise TypeError(f"{cls.__name__} cannot be instantiated.")
253
291
 
@@ -278,3 +316,8 @@ class ExecPluginType:
278
316
  """Return all SuperExec plugin types."""
279
317
  # Filter all constants (uppercase) of the class
280
318
  return [v for k, v in vars(ExecPluginType).items() if k.isupper()]
319
+
320
+
321
+ # Constants for No-op auth plugins
322
+ NOOP_FLWR_AID = "<none>"
323
+ NOOP_ACCOUNT_NAME = "sys_noauth"
@@ -41,10 +41,15 @@ class ExitCode:
41
41
 
42
42
  # SuperNode-specific exit codes (300-399)
43
43
  SUPERNODE_REST_ADDRESS_INVALID = 300
44
- SUPERNODE_NODE_AUTH_KEYS_REQUIRED = 301
45
- SUPERNODE_NODE_AUTH_KEYS_INVALID = 302
44
+ # SUPERNODE_NODE_AUTH_KEYS_REQUIRED = 301 --- DELETED ---
45
+ SUPERNODE_NODE_AUTH_KEY_INVALID = 302
46
+ SUPERNODE_STARTED_WITHOUT_TLS_BUT_NODE_AUTH_ENABLED = 303
46
47
 
47
48
  # SuperExec-specific exit codes (400-499)
49
+ SUPEREXEC_INVALID_PLUGIN_CONFIG = 400
50
+
51
+ # FlowerCLI-specific exit codes (500-599)
52
+ FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID = 500
48
53
 
49
54
  # Common exit codes (600-699)
50
55
  COMMON_ADDRESS_INVALID = 600
@@ -101,17 +106,26 @@ EXIT_CODE_HELP = {
101
106
  "When using the REST API, please provide `https://` or "
102
107
  "`http://` before the server address (e.g. `http://127.0.0.1:8080`)"
103
108
  ),
104
- ExitCode.SUPERNODE_NODE_AUTH_KEYS_REQUIRED: (
105
- "Node authentication requires file paths to both "
106
- "'--auth-supernode-private-key' and '--auth-supernode-public-key' "
107
- "to be provided (providing only one of them is not sufficient)."
108
- ),
109
- ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID: (
110
- "Node authentication requires elliptic curve private and public key pair. "
111
- "Please ensure that the file path points to a valid private/public key "
109
+ ExitCode.SUPERNODE_NODE_AUTH_KEY_INVALID: (
110
+ "Node authentication requires elliptic curve private key. "
111
+ "Please ensure that the file path points to a valid private key "
112
112
  "file and try again."
113
113
  ),
114
+ ExitCode.SUPERNODE_STARTED_WITHOUT_TLS_BUT_NODE_AUTH_ENABLED: (
115
+ "The private key for SuperNode authentication was provided, but TLS is not "
116
+ "enabled. Node authentication can only be used when TLS is enabled."
117
+ ),
114
118
  # SuperExec-specific exit codes (400-499)
119
+ ExitCode.SUPEREXEC_INVALID_PLUGIN_CONFIG: (
120
+ "The YAML configuration for the SuperExec plugin is invalid."
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
+ ),
115
129
  # Common exit codes (600-699)
116
130
  ExitCode.COMMON_ADDRESS_INVALID: (
117
131
  "Please provide a valid URL, IPv4 or IPv6 address."