# Copyright 2025 Flower Labs GmbH. All Rights Reserved.## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.# =============================================================================="""Local DP modifier."""fromcollectionsimportOrderedDictfromloggingimportINFOimportnumpyasnpfromflwr.clientapp.typingimportClientAppCallablefromflwr.commonimportArray,ArrayRecordfromflwr.common.contextimportContextfromflwr.common.differential_privacyimport(add_gaussian_noise_inplace,compute_clip_model_update,)fromflwr.common.loggerimportlogfromflwr.common.messageimportMessagefrom.centraldp_modsimport_handle_array_key_mismatch_err,_handle_multi_record_err
[docs]classLocalDpMod:"""Modifier for local differential privacy. This mod clips the client model updates and adds noise to the params before sending them to the server. It operates on messages of type `MessageType.TRAIN`. Parameters ---------- clipping_norm : float The value of the clipping norm. sensitivity : float The sensitivity of the client model. epsilon : float The privacy budget. Smaller value of epsilon indicates a higher level of privacy protection. delta : float The failure probability. The probability that the privacy mechanism fails to provide the desired level of privacy. A smaller value of delta indicates a stricter privacy guarantee. Examples -------- Create an instance of the local DP mod and add it to the client-side mods:: local_dp_mod = LocalDpMod( ... ) app = fl.client.ClientApp(mods=[local_dp_mod]) """def__init__(self,clipping_norm:float,sensitivity:float,epsilon:float,delta:float)->None:ifclipping_norm<=0:raiseValueError("The clipping norm should be a positive value.")ifsensitivity<0:raiseValueError("The sensitivity should be a non-negative value.")ifepsilon<0:raiseValueError("Epsilon should be a non-negative value.")ifdelta<0:raiseValueError("Delta should be a non-negative value.")self.clipping_norm=clipping_normself.sensitivity=sensitivityself.epsilon=epsilonself.delta=deltadef__call__(self,msg:Message,ctxt:Context,call_next:ClientAppCallable)->Message:"""Perform local DP on the client model parameters. Parameters ---------- msg : Message The message received from the ServerApp. ctxt : Context The context of the ClientApp. call_next : ClientAppCallable The callable to call the next mod (or the ClientApp) in the chain. Returns ------- Message The modified message to be sent back to the server. """iflen(msg.content.array_records)!=1:return_handle_multi_record_err("LocalDpMod",msg,ArrayRecord)# Record array record communicated to client and clipping normoriginal_array_record=next(iter(msg.content.array_records.values()))# Call inner appout_msg=call_next(msg,ctxt)# Check if the msg has errorifout_msg.has_error():returnout_msg# Ensure reply has a single ArrayRecordiflen(out_msg.content.array_records)!=1:return_handle_multi_record_err("LocalDpMod",out_msg,ArrayRecord)new_array_record_key,client_to_server_arrecord=next(iter(out_msg.content.array_records.items()))# Ensure keys in returned ArrayRecord match those in the one sent from serveriflist(original_array_record.keys())!=list(client_to_server_arrecord.keys()):return_handle_array_key_mismatch_err("LocalDpMod",out_msg)client_to_server_ndarrays=client_to_server_arrecord.to_numpy_ndarrays()# Clip the client updatecompute_clip_model_update(client_to_server_ndarrays,original_array_record.to_numpy_ndarrays(),self.clipping_norm,)log(INFO,"LocalDpMod: parameters are clipped by value: %.4f.",self.clipping_norm,)std_dev=(self.sensitivity*np.sqrt(2*np.log(1.25/self.delta))/self.epsilon)add_gaussian_noise_inplace(client_to_server_ndarrays,std_dev,)log(INFO,"LocalDpMod: local DP noise with %.4f stddev added to parameters",std_dev,)# Replace outgoing ArrayRecord's Array while preserving their keysout_msg.content[new_array_record_key]=ArrayRecord(OrderedDict({k:Array(v)fork,vinzip(client_to_server_arrecord.keys(),client_to_server_ndarrays)}))returnout_msg