Secure FL: Zero-Knowledge Federated Learning
A dual-verifiable framework for federated learning using zero-knowledge proofs to ensure training integrity and aggregation correctness.
Core Features
- Dual ZKP Verification: Client-side Groth16 zk-SNARK (Circom, transferable) + PySNARK local audit + Server-side Groth16 zk-SNARK (Circom/SnarkJS/rapidsnark) for aggregation
- FedJSCM Aggregation: Momentum-based federated optimization with auto-reinit on architecture changes
- Dynamic Proof Rigor:
StabilityMonitoradapts proof complexity (low/medium/high/adaptive) based on training stability - Deterministic Hashing: SHA-256 over the full parameter state dict — safe across Python/numpy versions
- Attack Detection: Norm-bound enforcement, hash verification, and ZKP circuit checks block poisoned updates
Architecture
Client: Local Training → Circom Groth16 delta-bound proof (transferable) + PySNARK (local audit)
→ Send full state_dict + proof
Server: Receive updates → verify (hash + norm + Groth16) → FedJSCM aggregation
→ Groth16 SNARK proof → Distribute model
Dual verification:
- Clients generate a transferable Groth16 proof of correct local training (delta norm bound), independently server-verifiable, plus a SHA-256 hash commitment over the full parameter state
- Server generates a Groth16 proof of correct FedJSCM aggregation
Quick Start
Installation
# From source with uv (recommended)
git clone https://github.com/krishantt/secure-fl
cd secure-fl
uv sync --all-extras
ZKP Prerequisites
# Automated setup (installs Rust, Circom, SnarkJS)
make setup-zkp
# Verify
uv run secure-fl setup --action check
Run the attack defense demo
# Synthetic data — fastest, no download needed
uv run python demo/simulate_attack.py --dataset synthetic_small --num-rounds 6
# MNIST
uv run python demo/simulate_attack.py --dataset mnist
# CIFAR-10 with more malicious clients
uv run python demo/simulate_attack.py --dataset cifar10 --malicious 2 --proof-rigor low
# MedMNIST
uv run python demo/simulate_attack.py --dataset medmnist --malicious 2 --proof-rigor low
The TUI shows real-time ZKP verification, attack detection, and accuracy comparison (with ZKP vs projected without). Attackers appear in ~65% of rounds; the rest are all-honest.
Start a federated learning session
# Server
uv run secure-fl-server --config experiments/config.yaml
# Client (in separate terminals)
uv run secure-fl-client --server localhost:8080 --dataset mnist --client-id client_1
uv run secure-fl-client --server localhost:8080 --dataset mnist --client-id client_2
# Or use Docker
docker compose up -d
Python API
from secure_fl.federation.client_runtime import create_client
from secure_fl.models.factory import model_fn
# Create a secure FL client
factory = model_fn("mnist") # returns MNISTCNN factory
client = create_client(
client_id="client_1",
model_fn=factory,
train_data=train_dataset,
enable_zkp=True,
proof_rigor="low", # "low" | "medium" | "high"
local_epochs=1,
learning_rate=0.01,
)
# client.fit(parameters, config) → (updated_params, num_examples, metrics)
# metrics["zkp_proof"] contains the JSON proof string when enable_zkp=True
from secure_fl.federation.aggregation import FedJSCMAggregator
from secure_fl.zkp.proof_manager import ServerProofManager
aggregator = FedJSCMAggregator(momentum=0.9, learning_rate=1.0)
server_pm = ServerProofManager()
# Verify a client proof
ok = server_pm.verify_client_proof(proof_json, updated_params, global_params)
# Aggregate verified updates
global_params = aggregator.aggregate(
client_updates=verified_updates,
client_weights=normalized_weights,
server_round=rnd,
global_params=global_params,
)
Configuration
# experiments/config.yaml
server:
host: "localhost"
port: 8080
num_rounds: 10
strategy:
min_fit_clients: 2
fraction_fit: 1.0
momentum: 0.9
zkp:
enable_zkp: true
proof_rigor: "high" # "low" | "medium" | "high" | "adaptive"
quantize_weights: true
quantization_bits: 8
Models and Datasets
| Dataset | Model | Notes |
|---|---|---|
mnist |
MNISTCNN |
2-layer CNN, fast |
cifar10 |
ResNet18Model |
Full ResNet18 |
medmnist |
ResNet18Model |
Medical imaging |
synthetic_small |
SimpleModel |
MLP, no download needed |
from secure_fl.models.factory import model_fn
factory = model_fn("cifar10") # returns callable → ResNet18Model instance
model = factory()
from secure_fl.data.benchmark_data import load_dataset, partition_dataset, subsample_dataset
train, test = load_dataset("mnist", seed=42)
train = subsample_dataset(train, max_samples=2000, seed=42)
client_subsets = partition_dataset(train, n=6, iid=False, seed=42) # Dirichlet non-IID
Technical Details
FedJSCM Aggregation
m^{t+1} = γ·m^t + (1−γ)·Δ
w^{t+1} = w^t + η·m^{t+1}
where Δ = weighted_avg(client_updates) − global_params. Momentum is reinitialized automatically if the model architecture changes between rounds. Integer-dtype buffers (e.g. BatchNorm num_batches_tracked) are preserved through aggregation.
ZKP Proof Schema
Each client proof is a JSON object containing:
initial_hash,updated_hash,delta_hash— SHA-256 commitments over the full parameter state dict (weights, biases, and BatchNorm running stats) — catches any silent parameter substitution, including in buffers not covered by the norm checkdelta_norm_l2,max_delta_norm_l2— L2 norm bound enforcement over learnable parameters only (BN buffers excluded — their early-round deltas are large and not adversarially controlled)pysnark— PySNARK circuit result (delta_bound_proof), a secondary in-process local auditcircom_proof— transferable Groth16 proof ({"proof": ..., "public": ...}) when Circom/SnarkJS tools are available client-side; independently verifiable by the server via the same mechanism, without re-running any client code
Server verification order: proof present → valid JSON → hash matches (full state dict) → delta hash matches (full state dict) → norm within bound (learnable-only) → PySNARK circuit ok → circom_proof verified (if present).
Parameter Representations
Two lists exist and must not be mixed:
| Source | Contents | Used for | |
|---|---|---|---|
| Full state dict | model.state_dict().values() |
weights + biases + BN buffers | hash commitment, aggregation, eval |
| Learnable only | model.parameters() |
weights + biases only | norm-bound enforcement, ZKP circuits |
fit() returns full state dict. Mixing the two representations with zip causes shape-mismatch errors on BN models (ResNet18, MNISTCNN) — see CLAUDE.md's "Parameter list conventions" section for the full detail.
Dynamic Proof Rigor
StabilityMonitor tracks gradient variance and convergence across rounds and adapts ProofRigor:
- High variance / early rounds →
high(more proof coverage) - Stable training →
low(faster proofs) adaptivemode enables automatic adjustment each round
Experiments & Benchmarks
experiments/full_benchmark.py is the single source of truth for paper-quality
results — k-fold accuracy, overhead, and attack resilience in one run.
# Fast smoke run
uv run python experiments/full_benchmark.py --quick
# Paper-quality run (real ZKP proofs enforced, resumable — skips
# already-completed config JSONs unless --force)
uv run python experiments/full_benchmark.py \
--datasets mnist --k-folds 5 --num-clients 20 --clients-per-round 5 \
--num-rounds 100 --require-real-proofs --force \
--output-dir results/full_benchmark
Outputs per-dataset, per-config k-fold JSONs under --output-dir
(aggregate summary stats + folds[i].rounds[j] per-round detail). See
CLAUDE.md's "Benchmark results status" section for which result set is
currently canonical and why.
Repository Structure
secure-fl/
├── src/secure_fl/
│ ├── core/ # Types, config (thread-safe singleton), exceptions
│ ├── federation/ # SecureFlowerClient, SecureFlowerServer, FedJSCMAggregator,
│ │ # StabilityMonitor, client_utils, helpers
│ ├── zkp/ # ClientProofManager, ServerProofManager, quantization
│ ├── models/ # SimpleModel, MNISTCNN, ResNet18Model, factory
│ ├── data/ # benchmark_data: load/partition/subsample datasets
│ ├── cli/ # Click CLI: secure-fl, secure-fl-server, secure-fl-client, secure-fl-setup
│ ├── proofs/
│ │ ├── client_circuits/ # PySNARK delta_bound_proof circuit
│ │ └── server/ # Circom aggregation SNARK circuits
│ └── utils/ # helpers (compute_hash, compute_parameter_norm), logging
├── demo/
│ └── simulate_attack.py # Textual TUI: real ZKP attack detection demo
├── experiments/
│ ├── full_benchmark.py # Canonical paper benchmark (accuracy + overhead + attacks)
│ ├── train.py # Single training run
│ ├── demo.py # CLI demo entry point
│ └── config.yaml
└── tests/
├── unit/ # No external dependencies (fast)
└── integration/ # Requires Circom + SnarkJS
Development
make dev # Full setup (uv sync + ZKP tools)
make lint # Ruff lint
make format # Ruff format ← run before every commit
make type-check # mypy (strict)
make test # Full suite
make test-quick # Fast, --exitfirst
make test-cov # With coverage (threshold: 30%)
Known Limitations
- Server ZKP (Circom/SnarkJS) requires
make setup-zkp; subprocess calls time out after 300 s - Client-side transferable Groth16 proof falls back gracefully to PySNARK-only when Circom/SnarkJS tools are unavailable client-side
- Coverage threshold is 30% — being raised incrementally
- Norm-bound enforcement does not defend against within-bound poisoning (adversarial updates crafted to satisfy the norm constraint) — see
docs/paper/main.tex§Limitations
License
MIT License — see LICENSE for details.
Citation
@misc{timilsina2026securefl,
title={Secure-FL: Zero-Knowledge Proofs for Dual-Verifiable Federated Learning},
author={Timilsina, Krishant and Paudel, Bindu and Timilsina, Arun Kumar},
year={2026},
url={https://github.com/krishantt/secure-fl},
note={Preprint}
}
Acknowledgments
Metadata
Release files for secure-fl 26.8.85
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| secure_fl-26.8.85.tar.gz | 14.3 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| secure_fl-26.8.85-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.5 MB
Release files / secure_fl-26.8.85.tar.gz
| Download URL | secure_fl-26.8.85.tar.gz |
|---|---|
| Size | 14.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6d318af9be6be4ff98971e98a885a2376f54a3a854e3eeef7b64ec9f04b607a4
|
|
BLAKE2b-256 checksum How to use checksums |
023d3dd631532504086deb776b7fdfe1622af420bdc16b41521e002535229f90
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / secure_fl-26.8.85-py3-none-any.whl
| Download URL | secure_fl-26.8.85-py3-none-any.whl |
|---|---|
| Size | 14.3 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7de88e89ec22006f73f603d72214c730fb8f0521f5088a311c139fe7165bcc2b
|
|
BLAKE2b-256 checksum How to use checksums |
7c91f8a4f324f1533a254efd6517a26498817b91ff4efcbde3bf9692c95ecb1a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|