This release is a pre-release and may not be stable for production use.
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 zk-STARKs (PySNARK) + Server-side zk-SNARKs (Circom/SnarkJS)
- 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 parameter bytes — safe across Python/numpy versions
- Attack Detection: Norm-bound enforcement, hash verification, and ZKP circuit checks block poisoned updates
Architecture
Client: Local Training → zk-STARK proof (learnable params) → Send full state_dict + proof
Server: Receive updates → verify proofs → FedJSCM aggregation → zk-SNARK proof → Distribute model
Dual verification:
- Clients generate zk-STARK proofs of correct local training (delta norm bound, hash commitment)
- Server generates zk-SNARK proofs 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 check-zkp
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 learnable parametersdelta_norm_l2,max_delta_norm_l2— norm bound enforcementpysnark— PySNARK circuit result (delta_bound_proof)
Server verification order: proof present → valid JSON → hash matches → delta hash matches → norm within bound → circuit ok.
Parameter Representations
Two lists exist and must not be mixed:
| Source | Contents | |
|---|---|---|
| Full state dict | model.state_dict().values() |
weights + biases + BN buffers |
| Learnable only | model.parameters() |
weights + biases only |
fit() returns full state dict. ZKP proofs cover learnable-only params. Mixing them with zip causes shape-mismatch errors on BN models (ResNet18, MNISTCNN).
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
# Fast smoke run
uv run python experiments/canonical_benchmark.py --quick
# Full reproducible benchmark with repeats
uv run python experiments/canonical_benchmark.py \
--datasets mnist synthetic_small \
--num-repeats 5 \
--seed 42 \
--require-real-proofs
# Interactive benchmark TUI (supports reattach)
uv run python experiments/benchmark_tui.py
Outputs: benchmark_results.json (per-seed runs) and tradeoff_report.json (aggregated trade-off summary).
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
│ ├── 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/
│ ├── canonical_benchmark.py
│ ├── benchmark_tui.py
│ └── 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: 34%)
Known Limitations
_submit_to_blockchain()raisesNotImplementedError— not yet implemented- Server ZKP (Circom/SnarkJS) requires
make setup-zkp; subprocess calls time out after 300 s - Coverage threshold is 34% — being raised incrementally
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 2026.8.25.dev1
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-2026.8.25.dev1.tar.gz | 14.3 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| secure_fl-2026.8.25.dev1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.5 MB
Release files / secure_fl-2026.8.25.dev1.tar.gz
| Download URL | secure_fl-2026.8.25.dev1.tar.gz |
|---|---|
| Size | 14.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e881ee6fc1e9b7dfa6c1730c457a3e2fa7db6cf85d453a378bfc2a0a2684f305
|
|
BLAKE2b-256 checksum How to use checksums |
a50509939597160280b307cfe58f6607922d1ea44da7be94a792fb7f52015e0b
|
| 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-2026.8.25.dev1-py3-none-any.whl
| Download URL | secure_fl-2026.8.25.dev1-py3-none-any.whl |
|---|---|
| Size | 14.3 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
79b1342f6ae63b966cb75fb2d9f8cd5c377e77af1913e2aa72479f910cc6d30a
|
|
BLAKE2b-256 checksum How to use checksums |
5a1f37bdf1e79c34f373c681204ca90e3055e70974d4b5f918372f38de44fd36
|
| 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}
|