Skip to main content

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: StabilityMonitor adapts 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

Dual verification:

  1. 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
  2. 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.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"
  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 check
  • delta_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)
  • adaptive mode 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/      # SecureFlowerClient, SecureFlowerServer, FedJSCMAggregator,
│   │                    #   StabilityMonitor, client_utils, helpers
│   ├── zkp/             # ClientProofManager, CircuitSetup, 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/
│   │   └── 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%)

Optional: Rust verification backend

Proof generation and verification can run in a typed Rust core (native Groth16 for both client and server proofs, no snarkjs/rapidsnark subprocess at runtime). Clients and training stay in PyTorch, so benchmark results remain comparable.

make build-rust
SECURE_FL_BACKEND=rust uv run secure-fl-server --config experiments/config.yaml

SECURE_FL_BACKEND=rust-full also moves FedJSCM aggregation into Rust; it stays in Python by default because numpy is faster for large models. Measured comparison: docs/dev/RUST_BACKEND_BENCHMARK.md.

Or install the prebuilt extension: uv pip install rust/dist/securefl_core-*.whl (after make build-rust-wheel).

See rust/README.md for the port scope and parity tests.

Known Limitations

  • Server ZKP (Circom/SnarkJS) requires make setup-zkp; subprocess calls time out after 300 s. With SECURE_FL_BACKEND=rust, proving and verification run in-process instead — see rust/README.md
  • ZKP requires the securefl_rust extension (make build-rust or the securefl-core wheel); 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

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.140

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for secure-fl 26.10.140
File Size Uploaded Provenance Actions
secure_fl-26.10.140.tar.gz 14.3 MB Without provenance Details for secure_fl-26.10.140.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for secure-fl 26.10.140
File Interpreter ABI Platform Actions
secure_fl-26.10.140-py3-none-any.whl Python 3 none any Details for secure_fl-26.10.140-py3-none-any.whl

Total release size: 28.5 MB

Release files / secure_fl-26.10.140.tar.gz

Download URL secure_fl-26.10.140.tar.gz
Size 14.3 MB
Tags Source
SHA-256 checksum
How to use checksums
1302c88c3a05b5474b974b20694ccf716c68bac7484e4ba99584f843a9110d88
BLAKE2b-256 checksum
How to use checksums
5d14fe2d8f34240103e85f41b4b11d50fad1e2a18440821f3fa31ec58d99e1f0
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.140-py3-none-any.whl

Download URL secure_fl-26.10.140-py3-none-any.whl
Size 14.3 MB
Tags Python 3
SHA-256 checksum
How to use checksums
6cc2450b38a9301fd7c437c777ac5879b364a2244cd76cb5dfadd8b0029ce8d6
BLAKE2b-256 checksum
How to use checksums
c34a49059487984bf1846a0a9b6b798c79bed7c828eff771c422176f77b35e8a
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.140

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.

Release history Release notifications | RSS feed

This release

26.10.140 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page