Secure FL: Zero-Knowledge Federated Learning
A dual-verifiable framework for federated learning using zero-knowledge proofs to ensure training integrity and aggregation correctness.
Documentation site: https://krishantt.github.io/secure-fl/ · interactive architecture map
Core Features
- Dual ZKP Verification: Client-side Groth16 zk-SNARK (transferable) + server-side Groth16 zk-SNARK for aggregation, both proved and verified in-process by the Rust core
- 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 → Groth16 delta-bound proof (transferable)
→ Send full state_dict + proof
Server: Receive updates → verify (hash + norm + Groth16) → FedJSCM aggregation
→ Groth16 SNARK proof → Distribute model
Everything touching proofs, aggregation and the stability heuristic runs in one Rust core loaded into both processes; Python keeps the PyTorch training loop and the round strategy.
Interactive pages (generated from this repository): architecture map, data flow, round sequence.
Data flow and round sequence
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 + circuit artifacts)
make setup-zkp
# Build the native proof core (required for any ZKP path)
make build-rust
# 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: the standalone Rust round server (build once, or download the release
# binary and put it on PATH)
make install-server
uv run secure-fl-server --config experiments/config.yaml # REST :8080, gRPC :8081
# Client (in separate terminals)
uv run secure-fl-client --server-address 127.0.0.1:8081 --dataset mnist --client-id client_1
uv run secure-fl-client --server-address 127.0.0.1:8081 --dataset mnist --client-id client_2
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.backend import make_proof_manager
aggregator = FedJSCMAggregator(momentum=0.9, learning_rate=1.0)
server_pm = make_proof_manager()
# 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"
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)circom_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) → circom_proof verified when 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 AGENTS.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
AGENTS.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/ # SecureFlClient, SecureFlServer, FedJSCMAggregator,
│ │ # StabilityMonitor, client_utils, helpers
│ ├── zkp/ # ClientProofManager, CircuitSetup, Rust proof backend
│ ├── 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/
│ │ └── 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)
│ ├── 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%)
Rust proof core (required for ZKP)
All ZKP proving and verification runs in a typed Rust core — client
delta-bound proofs, server aggregation proofs, and both verifiers — with no
snarkjs/rapidsnark subprocess at runtime. Clients and training stay in
PyTorch, so benchmark results remain comparable.
make build-rust # after uv sync; installs src/securefl_rust.so
Running any ZKP path without the extension fails with an import error; there
is no Python fallback. pip install secure-fl pulls the matching prebuilt
securefl-core wheel; make build-rust overrides it for local Rust work, and
make build-rust-wheel builds one locally.
FedJSCM aggregation, the stability heuristic and all proof handling run in the same Rust core; there is no Python backend switch. Aggregation is within ~5% of the old numpy implementation at ResNet18 scale (benchmark).
ZKP requires make setup-zkp once to build the circuit artifacts; circom and
snarkjs are build-time tools only. See rust/README.md for
the port scope and parity tests.
Networking runs on the standalone Rust server; there is no in-process FL
framework. PyTorch clients reach it over gRPC (SecureFlGrpcClient +
SecureFlRoundRunner, or secure-fl-client): the server samples clients, closes
a round as soon as every assigned client submits, and returns the aggregation
proof with the model. Pass --token/SECUREFL_SERVER_TOKEN and, for real
deployments, --tls-cert/--tls-key/--client-ca — with mTLS the certificate CN is
the client's identity, which is what a proof can be bound to. In-process
simulations (demos, tests) call create_server_strategy and
SecureFlStrategy.aggregate_fit directly.
Known Limitations
- Server ZKP (Circom/SnarkJS) requires
make setup-zkp; subprocess calls time out after 300 s. Proving and verification run in-process in the Rust core — seerust/README.md - ZKP requires the
securefl_rustextension (make build-rustor thesecurefl-corewheel); there is no weaker local-audit fallback - 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 - Both circuits are statistical: the client delta-bound proof and the aggregation proof cover 200 sampled learnable parameters, not the full model. The SHA-256 commitment and the norm bound still cover every parameter
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.10.192
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| secure_fl-26.10.192.tar.gz | 14.3 MB | Without provenance | Details for secure_fl-26.10.192.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| secure_fl-26.10.192-py3-none-any.whl | Python 3 | none | any | Details for secure_fl-26.10.192-py3-none-any.whl |
Total release size: 28.6 MB
Release files / secure_fl-26.10.192.tar.gz
| Download URL | secure_fl-26.10.192.tar.gz |
|---|---|
| Size | 14.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
915d2bc9b35a5ad9866f8d0d362856211cabf507cf94d33d915316b4f2c58764
|
|
BLAKE2b-256 checksum How to use checksums |
041e4f9bcc9a4b05850978e385d3198aef8a1defbb6ed864eb138def823292ce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","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.10.192-py3-none-any.whl
| Download URL | secure_fl-26.10.192-py3-none-any.whl |
|---|---|
| Size | 14.3 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
baab3b49b339a0431282437987e76e4c38023016ba09dbfa327f437c42594512
|
|
BLAKE2b-256 checksum How to use checksums |
a84abab947fea0bb1d5da5b507815a984822eab765e40166ddf61e137d63d965
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","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}
|
Security for secure-fl 26.10.192
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.